Lapanti/ts-react-boilerplate

View on GitHub
docs/EXTRAS.md

Summary

Maintainability
Test Coverage
# 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](https://en.wikipedia.org/wiki/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](https://github.com/Lapanti/ts-react-boilerplate/tree/master/src/icons) 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](https://realfavicongenerator.net/).

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:
```html
        <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](https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html) 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](https://developer.mozilla.org/en-US/docs/Web/Manifest) 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](https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/pinnedTabs/pinnedTabs.html). The seventh line is to define a tile for Microsoft users when they [pin your website](https://msdn.microsoft.com/en-us/library/dn320426(v=vs.85).aspx). The final line is a theme color for your website which is used for example by [mobile browsers](https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/).

The contents of you `manifest.json` should look something like this:
```json
{
    "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
<?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`:
```javascript
    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`:
```javascript
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](https://nodejs.org/en/)-server to your application and then hosting your code on a server.

For our needs, we'll use [Express](https://expressjs.com/), starting with installing the new, required dependencies (*[http-status-enum](https://github.com/KyleNeedham/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
```typescript
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
```typescript
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
```typescript
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``
```typescript
// 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
```typescript
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
```typescript
app.use('/js', express.static(path.join('js'), { redirect: false }));
app.use('/styles', express.static(path.join('styles'), { redirect: false }));
```
with [`use`](https://expressjs.com/en/4x/api.html#app.use) you can set a middleware-function to a specific path, in this case [`express.static`](https://expressjs.com/en/4x/api.html#express.static), which will serve static files from a relative path.
> [`path.join`](https://nodejs.org/api/path.html#path_path_join_paths) will create a path using platform-specific separators.

---

Next is the beef of our server application, the actual server-side rendering
```typescript
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`](http://knowbody.github.io/react-router-docs/api/match.html)-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
```typescript
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:
```javascript
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:
```javascript
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:
```json
"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
```json
    "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](https://developers.google.com/web/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:
```json
{
    "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](https://developer.mozilla.org/en-US/docs/Web/Manifest) 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](https://www.docker.com/) 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](https://docs.docker.com/engine/installation/)*).

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
```