Extras

Here we go through things you might not need, but might want to include in your project.

Favicon

A recommended feature of all websites, especially now with so many mobile devices browsing the internet, is to have a favicon in your website. A favicon is usually the logo of your company or website. In our case you can either use the ones you can find here or make your own. If you want to make your own, I suggest creating a 260 x 260 pixel image, which you then generate into all the useful formats using RealFaviconGenerator.

If you used RealFaviconGenerator you should now have the following files:

  • android-chrome-192x192.png
  • android-chrome-512x512.png
  • apple-touch-icon.png
  • favicon-16x16.png
  • favicon-32x32.png
  • favicon.ico
  • mstile-150x150.png
  • safari-pinned-tab.svg
  • manifest.json
  • browserconfig.xml Now move manifest.json and browserconfig.xml to your root folder and the rest to src/icons.

Now that we have everything in place, let's first update our index.html by adding the following parts to inside the head tag:

        <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
        <link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
        <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
        <link rel="manifest" href="/assets/manifest.json" />
        <link rel="mask-icon" href="/assets/icons/safari-pinned-tab.svg" color="#5bbad5" />
        <link rel="shortcut icon" href="/assets/icons/favicon.ico" />
        <meta name="msapplication-config" content="/assets/browserconfig.xml" />
        <meta name="theme-color" content="#FF8041" />

where the first line is to have an icon present if an iPhone user saves your website to their Home screen. The second, third and sixth line are for different sizes of the traditional browser favicon. The fourth line is for a manifest.json which does the same thing as the first line for Android users. The fifth line is an icon for Safari users when they pin your website. The seventh line is to define a tile for Microsoft users when they pin your website.aspx). The final line is a theme color for your website which is used for example by mobile browsers.

The contents of you manifest.json should look something like this:

{
    "name": "",
    "icons": [
        {
            "src": "/assets/icons/android-chrome-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/assets/icons/android-chrome-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "theme_color": "#FF8041", // Change this and the following line to match your website
    "background_color": "#FFFFFF",
    "display": "standalone"
}

And the contents of your browserconfig.xml should look something like this:

<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
    <msapplication>
        <tile>
            <square150x150logo src="/assets/icons/mstile-150x150.png"/>
            <TileColor>#FF8041</TileColor>
        </tile>
    </msapplication>
</browserconfig>

where you should change the TileColor to match your icon's background.


Now we also need to update our webpack configurations to include our new icons and manifests into the project, so add the following to your webpack.dev.js:

    plugins: [
        new CopyWebpackPlugin([
            { from: path.resolve(__dirname, 'index.html') },
            { from: path.resolve(__dirname, 'manifest.json'), to: 'assets' },
            { from: path.resolve(__dirname, 'browserconfig.xml'), to: 'assets' },
            { from: path.resolve(__dirname, 'src/icons'), to: 'assets/icons' }
        ]),
        // ...
    ]

And then add the following to your webpack.prod.js:

var CopyWebpackPlugin = require('copy-webpack-plugin');
// ...
    plugins: [
        new CopyWebpackPlugin([
            { from: path.resolve(__dirname, 'manifest.json') },
            { from: path.resolve(__dirname, 'browserconfig.xml') },
            { from: path.resolve(__dirname, 'src/icons'), to: 'icons' }
        ]),
        // ...
    ]

Server-side rendering

Server-side rendering is the act of having a server render your React-application and sending it as an html file to the client, which can considerably reduce initial loading times and enables a lot of SEO. This is usually achieved by adding a node-server to your application and then hosting your code on a server.

For our needs, we'll use Express, starting with installing the new, required dependencies (http-status-enum is just a simple enumeration of HTTP Status Codes for TypeScript)

    yarn add express http-status-enum
    yarn add -D @types/express

Now the actual server we run will live in a file called server.tsx inside the src-folder

import * as path from 'path';
import * as express from 'express';
import * as React from 'react';
import HttpStatus from 'http-status-enum';
import { Provider } from 'react-redux';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { routerMiddleware } from 'react-router-redux';
import createHistory from 'history/createMemoryHistory';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import reducer, { epics, State } from './redux/reducer';
import AppContainer from './modules/AppContainer';

const normalizePort = (val: number | string): number | string | boolean => {
    const base = 10;
    const port: number = typeof val === 'string' ? parseInt(val, base) : val;
    return isNaN(port) ? val : port >= 0 ? port : false;
};

const renderHtml = (html: string, preloadedState: State) =>
    `
    <!doctype html>
    <html>
        <head>
            <meta charset="utf-8" />
            <title>Todo app</title>
            <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" />
            <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
            <link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
            <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
            <link rel="manifest" href="/assets/manifest.json" />
            <link rel="mask-icon" href="/assets/icons/safari-pinned-tab.svg" color="#5bbad5" />
            <link rel="shortcut icon" href="/assets/icons/favicon.ico" />
            <meta name="msapplication-config" content="/assets/browserconfig.xml" />
            <meta name="theme-color" content="#FF8041" />
            <link rel="stylesheet" href="/assets/styles.css">
        </head>
        <body>
            <div id="app">${html}</div>
            <script>
                window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
            </script>
            <script src="/assets/bundle.js"></script>
        </body>
    </html>
    `;

const defaultPort = 8080;
const port = normalizePort(process.env.PORT || defaultPort);
const app = express();

app.use('/assets', express.static(path.join('assets'), { redirect: false }));

app.use((req: express.Request, res: express.Response) => {
    const store = createStore<State>(
        reducer,
        applyMiddleware(routerMiddleware(createHistory()), createEpicMiddleware(epics)),
    );
    const context: { url?: string } = {};
    const html = renderToString(
        <Provider store={store}>
            <StaticRouter location={req.url} context={context}>
                <Route component={AppContainer} />
            </StaticRouter>
        </Provider>,
    );
    if (context.url) {
        res.redirect(HttpStatus.MOVED_PERMANENTLY, context.url);
    } else {
        res.send(renderHtml(html, store.getState()));
    }
});

app.listen(port, () => console.log(`App is listening on ${port}`));

which will serve the React-application on all other routes except ROOT/assets, where our assets are served from.


First we made a simple function to normalize an incoming PORT-parameter

const normalizePort = (val: number | string): number | string | boolean => {
    const base = 10;
    const port: number = typeof val === 'string' ? parseInt(val, base) : val;
    return isNaN(port) ? val : port >= 0 ? port : false;
};

which tries to parse the incoming val-parameter.


Next we create a function to render our html based on the rendered React application

const renderHtml = (html: string, preloadedState: State) => (
    `
    <!doctype html>
    <html>
        <head>
            <meta charset="utf-8" />
            <title>Todo app</title>
            <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" />
            <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
            <link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
            <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
            <link rel="manifest" href="/assets/manifest.json" />
            <link rel="mask-icon" href="/assets/icons/safari-pinned-tab.svg" color="#5bbad5" />
            <link rel="shortcut icon" href="/assets/icons/favicon.ico" />
            <meta name="msapplication-config" content="/assets/browserconfig.xml" />
            <meta name="theme-color" content="#FF8041" />
            <link rel="stylesheet" href="/styles/styles.css">
        </head>
        <body>
            <div id="app">${html}</div>
            <script>
                window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
            </script>
            <script src="/js/bundle.js"></script>
        </body>
    </html>
    `
);

where the biggest point is window.__PRELOADED_STATE__ which lets us set the state of the application in the server, although it requires the following modification to our `store.ts``

// Below is a necessary hack to access __PRELOADED_STATE__ on the global window object
const preloadedState: State = (<any>window).__PRELOADED_STATE__;
delete (<any>window).__PRELOADED_STATE__;
const configureStore = (history: History) => createStore<State>(
    reducer,
    preloadedState,
    applyMiddleware(epicMiddleware),
);

Next we set some required variables and create our Express-application

const defaultPort = 8080;
const port = normalizePort(process.env.PORT || defaultPort);
const app = express();

where we use our previously created normalizePort to normalize the (possibly) given PORT environment variable.


Next up we set up Express to serve our static assets

app.use('/js', express.static(path.join('js'), { redirect: false }));
app.use('/styles', express.static(path.join('styles'), { redirect: false }));

with use you can set a middleware-function to a specific path, in this case express.static, which will serve static files from a relative path.

path.join will create a path using platform-specific separators.


Next is the beef of our server application, the actual server-side rendering

app.use((req: express.Request, res: express.Response) => {
    const store = createStore<State>(reducer, applyMiddleware(
        routerMiddleware(createHistory()),
        createEpicMiddleware(epics),
    ));
    const context: { url?: string } = {};
    const html = renderToString(
        <Provider store={store}>
            <StaticRouter location={req.url} context={context}>
                <Route component={AppContainer} />
            </StaticRouter>
        </Provider>,
    );
    if (context.url) {
        res.redirect(HttpStatus.MOVED_PERMANENTLY, context.url);
    } else {
        res.send(renderHtml(html, store.getState()));
    }
});

where we use react-router to match the current path to our client code, where the match-function matches the current route without rendering. Afterwards we create a store and render the application as html and finally send it to the client.


Finally we start the application itself

app.listen(port, () => console.log(`App is listening on ${port}`));

Of course we need to again make some changes to our webpack configurations, but this time only to the production configuration and then we need to create a configuration for the server code itself.

For the production configuration we want to remove our index.html creation plugin as the server itself serves the index file, so remove the following lines:

var HtmlWebpackPlugin = require('html-webpack-plugin');
// ...
        new HtmlWebpackPlugin(),

after which, we can remove the plugin itself, by running

yarn remove html-webpack-plugin

For the building of the server side code, we need to create another webpack configuration file, this time called webpack.server.js, which will have the following content:

var path = require('path');
var webpack = require('webpack');

module.exports = {
    target: 'node',
    entry: path.resolve(__dirname, 'src', 'server.tsx'),
    output: {
        filename: 'server.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: ['babel-loader', 'awesome-typescript-loader'],
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    }
};

where we simply define the entry to be server.tsx, the output folder to be dist (with the output file being server.js) and make it process TypeScript.

Then we also need to update our build scripts to include our server, so we change the old build-script into the following three scripts:

"scripts": {
    "build:server": "webpack -d --env=server -p --colors",
    "build:client": "webpack -d --env=prod --colors",
    "build": "yarn clean && concurrently --kill-others-on-fail -p \"{name}\" -n \"SERVER,CLIENT\" -c \"bgBlue,bgMagenta\" \"yarn build:server\" \"yarn build:client\"",
}

where the build:client-script is the same as before, build:server calls webpack with our new server configuration and build runs both of these at the same time using concurrecntly.


Finally we create the actual start-script which will run our application

    "scripts": {
        "start": "cd dist && NODE_ENV=production node server.js",
    }

which is very simple, just setting the production-flag for our NODE_ENV and starting the server with node.

That's it, you should now have fully working server-side rendered application!

PWA

PWA stands for Progressive Web Apps, which are websites that can behave like apps, e.g. work when offline, can be installed on the home screen etc. In order to begin transforming our service into a PWA, we first need to add some values to the manifest.json, replace applicable lines with information appropriate to your application:

{
    "name": "HN PWA",
    "short_name": "HN PWA",
    "description": "An example HN PWA with TypeScript and React",
    "lang": "en-US",
    "orientation": "portrait-primary",
    "start_url": "/",
    // ...
}

where name, short_name and description should be self-evident, whereas lang defines the language of the application, orientation sets the wished orientation for your app. start_url is one of the most important ones, as it sets the url your app opens into when it is opened from the home screen.

Docker

If you want to dockerize your application you need to add a Dockerfile to your application's root folder (which is just a file named Dockerfile)

FROM node:4-onbuild

LABEL maintainer "your.email.here@domain.com"

COPY dist/ /

ENV NODE_ENV=production

EXPOSE 8080

CMD node /server.js

which tells Docker how to build your container (installation instructions for Docker can be found here).

To start your new Docker-container you can just run

docker build .

then take the image id given by the build command and use it here

docker run -d -p 8080:8080 IMAGEID

results matching ""

    No results matching ""