preface

I originally wanted to write this article last week, but in the process of learning, I felt more and more that many previous ideas need to be modified, so I made up my mind to write the second tutorial after I finished reconstructing the project.

Go to github, the repository

If you haven’t read my first article, you should already know the basics of react SSR. If you haven’t read my first article, you should read tutorial 1 first, but it’s still not enough.

Let’s start by sorting out some of the issues that arose from the last tutorial

  • The route is configured twice and held manuallyreact-routerkoa-routerThe path is consistent.
  • The same request needs to be written twice.
  • Even after the client resources are packaged, the server still relies on the client source code.
  • Can’t writecss module.
  • The development environment is not friendly, two services need to be started, and hot update support is poor.

Fortunately, all of these problems are resolved in V2. Following me to solve the above problems in turn, due to the length of the article, THIS time I will not post too much source code, only describe my ideas and part of the core code, I strongly suggest digging friends do their own code.

Reconstruct routes and requests

In the last article, I used the React-Router and koa-router respectively to build the route of the project, and manually maintained the consistency of the routes at both ends. This has the advantage of being more flexible and decoupled, but has the disadvantage of writing a lot of repetitive code. Considering our actual development, As the front and back ends of the HTML routing output are basically the same, and the data processing difference is not large, we can completely adopt the react-Router configuration in the HTML routing part of the KOA-Router.

NPM I react-router-config-s; this package will play a crucial role later.

The configuration of the route reconstruction is as follows


import React from 'react';
import Home from './pages/home'
import Detail from './pages/detail'

export default[{path: '/'.component: Home,
        exact: true}, {path: '/detail/:id'.component: Detail,
        exact: true,},]Copy the code

The KOA-router is modified as follows

router.get('/api/flash', HomeControl.flash);
router.get('/api/column', HomeControl.column);
router.get('/api/detail', DetailControl.detail);
router.get(The '*'.async (ctx, next) => {
    await render(ctx, template);
    next();
})
Copy the code

So all the routing parts that go straight out of HTML go to the same controller. What did Render do?

Just like before, we output the HTML for the route using renderToString, then fill in the data and return the final HTML. Let’s take a look

import { renderRoutes } from 'react-router-config';
function templating(template) {
    return props= > template.replace(/ <! --([\s\S]*?) -->/g, (_, key) => props[key.trim()]);
}

function(ctx, template) {
    try {
        const render = templating(template);
        const html = renderToString(
            <Provider store={store}>
                <StaticRouter location={ctx.url} context={ctx}>{renderRoutes(routerConfig)</StaticRouter>
            </Provider>
        );
        const body = render({
            html,
            store: `<script>window.__STORE__ = The ${JSON.stringify(ctx.store.getState())}</script>`}); ctx.body = body; ctx.type ='text/html';
    }
    catch (err) {
        console.error(err.message);
        ctx.body = err.message;
        ctx.type = 'text/html'; }}Copy the code

Use comments as placeholders in templates and discard curly braces so that the front and back ends share the same template.

But how do we get the store part above? We used to request data before each route was rendered and then pass the data to the render function. Now we route through the same controller, how do we handle store?

So let’s refactor store

Start by writing a static method asyncData on each routing component

function mapDispatchToProps(dispatch) {
    return {
        fetchHome: (id) = > dispatch(homeActions.fetchHome(id)),
        fetchColumn: (page) = > dispatch(homeActions.fetchColumn(page)),
    }
}

class Home extends React.Component {
    state = {
        tabs: [{title: 'Technology News'.index: 0 },
            { title: '24 h express'.index: 1}].columnPage: this.props.column.length > 0 ? 1 : 0,}static asyncData(store) {
        const { fetchHome, fetchColumn } = mapDispatchToProps(store.dispatch);
        // There must be a return Promise and the API path must be absolute.
        return Promise.all([
            fetchHome(),
            fetchColumn(),
        ])
    }
}
Copy the code

We then call the asyncData of the corresponding component in our Render function to initialize the store

import { renderRoutes, matchRoutes } from 'react-router-config';
import createStore from '.. /createStore.js'
function templating(template) {
    return props= > template.replace(/ <! --([\s\S]*?) -->/g, (_, key) => props[key.trim()]);
}

function(ctx, template) {
    try {
        // Initialize the store
        const store = createStore();
        // Obtain all matched routes
        const routes = matchRoutes(routerConfig, ctx.url);
        // If there is no match, 404 is returned
        if (routes.length <= 0) {
            return reject({ code: 404.message: 'Not Page' });
        }
        // After all data requests come back in render, note that the routing information on CTX is not used here, use the front-end routing information
        const promises = routes
        .filter(item= > item.route.component.asyncData) // Filter out components without asyncData
        .map(item= > item.route.component.asyncData(store, item.match)); // Call asyncData inside the component, which modifies store
        Promise.all(promises).then((a)= > {
            ....同上
        })
    }
    catch(err) { .... Ditto}}Copy the code

Now the store initialization is completely controlled by the action, and we do not need to manually initialize the store with its initialization values. If you don’t understand, look at the picture below

Okay, so at this point we’re done with routing and data processing and refactoring.

Refactor the KOA code

In the previous tutorial, we had to compile the source code using Babel before running because our server code was full of JSX code. However, it was a bad decision to compile the entire server code for a small portion of JSX code. So now let’s refactor koA’s code

What if we don’t want to compile koA code and want Node to recognize JSX? It’s very simple, as long as we extract the part that contains the JSX code into a separate file, and then we just compile that file, that’s it?

In fact, the above idea is to write a server entry file. Now we have both client and server entrances, and they both depend on the React React-router Redux, so we’ll write a public file and export this part of the code.

// createApp.js
import routerConfig from './router';
import createStore from './redux/store/createStore';
import { renderRoutes } from 'react-router-config';


export default function(store = {}) {
    return {
        router: renderRoutes(routerConfig),
        store: createStore(store),
        routerConfig,
    }
}
Copy the code

Then write server-entry.js to return a Controller

import ReactDom from 'react-dom';
import { StaticRouter } from 'react-router-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { matchRoutes } from 'react-router-config';
import createApp from './createApp';

export default ctx => {
    return new Promise((resolve, reject) = > {
        const { router, store, routerConfig } = createApp();

        const routes = matchRoutes(routerConfig, ctx.url);

        // If there is no match, 404 is returned
        if (routes.length <= 0) {
            return reject({ code: 404.message: 'Not Page' });
        }

        // After all data requests come back in render, note that the routing information on CTX is not used here, use the front-end routing information
        const promises = routes
        .filter(item= > item.route.component.asyncData)
        .map(item= > item.route.component.asyncData(store, item.match));

        Promise.all(promises).then((a)= > {
            ctx.store = store; // Mount to CTX for easy rendering to pages
            resolve(
                <Provider store={store}>
                    <StaticRouter location={ctx.url} context={ctx}>
                        { router }
                    </StaticRouter>
                </Provider>) }).catch(reject); })}Copy the code

Now we just need to write a server-side packaged WebPack configuration file, package the server entry into a file that node can recognize, and then import the compiled Controller on the Node side.

const merge = require('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.base.config');
const config = require('./config')[process.env.NODE_ENV];
const nodeExternals = require('webpack-node-externals');
const { resolve } = require('./utils');

module.exports = merge(baseConfig(config), {
    target: 'node'.devtool: config.devtool,
    entry: resolve('app/server-entry.js'),
    output: {
        filename: 'js/server-bundle.js'.libraryTarget: 'commonjs2' // Use commonJS modularity
    },
    // The server ignores the external NPM package
    externals: nodeExternals({
        // Of course, external CSS can be typed in
        whitelist: /\.css$/
    }),
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(config.env),
            'process.env.VUE_ENV': '"server"'})]})Copy the code

Don’t put CSS into this package. Node does not recognize CSS, so you need to remove the CSS code.

Now we can write code comfortably on the server side, running without compiling, and we can happily use CSS Modules without relying on front-end source code

It is easy to start the CSS Module. Css-loader comes with this function.

{
    loader: 'css-loader'.options: {
        modules: true.// Enable the CSS Module
        localIdentName: '[path][local]-[hash:base64:5]' // Naming rules of CSS Modules}},Copy the code

Finally, we just need NPM build to package the client and server resources, and we can start the service directly with NPM start.

Since the services we started depended on the packaged files, the production environment was fine, but the development environment couldn’t be repackaged every time I changed the code, which would seriously affect efficiency. How does the development environment handle this problem?

Development environment construction

Initially, I was going to start the two services as I did last time, and the client would use the Webpack-dev-server server to do a layer forwarding of static resources to the dev-server service. However, this would not be possible in the development environment, so I decided to merge the two services. The dev-server function is implemented by KOA.

Write the dev – server. Js

const fs = require('fs')
const path = require('path')
const MFS = require('memory-fs')
const webpack = require('webpack')
const chokidar = require('chokidar')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')

const readFile = (fs, file) = > {
    return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')}module.exports = function(app, templatePath) {
    let bundle
    let template
    let clientHtml

    / / here is actually resolve out alone, in fact you can also directly inside the following code is written in the promise, the benefits of this is to reduce the code nested.
    let ready
    const readyPromise = new Promise(r= > {
        ready = r
    })

    // Update the triggered function
    const update = (a)= > {
        if(bundle && clientHtml) { ready({ bundle, clientHtml }); }}// Listen for template files
    template = fs.readFileSync(templatePath, 'utf-8')
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf-8')
        console.log('index.html template updated.')
        update()
    })

    // Add a hot update entry
    clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
    clientConfig.output.filename = '[name].js'
    clientConfig.plugins.push(
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
    )

    // Create the dev service
    const clientCompiler = webpack(clientConfig)
    const devMiddleware = require('koa-webpack-dev-middleware')(clientCompiler, {
        publicPath: clientConfig.output.publicPath,
        noInfo: true
    });
    app.use(devMiddleware)
    clientCompiler.hooks.done.tap('DevPlugin', stats => {
        stats = stats.toJson()
        stats.errors.forEach(err= > console.error(err))
        stats.warnings.forEach(err= > console.warn(err))
        if (stats.errors.length) return
        // Get dev memory entry HTML
        clientHtml = readFile(
            devMiddleware.fileSystem,
            'server.tpl.html',
        )
        update()
    })

    // Enable hot update
    app.use(require('koa-webpack-hot-middleware')(clientCompiler))

    // Listen for and update the server entry file
    const serverCompiler = webpack(serverConfig)

    // Create a memory file system
    const mfs = new MFS()
    serverCompiler.outputFileSystem = mfs
    serverCompiler.watch({}, (err, stats) => {
        if (err) throw err
        stats = stats.toJson()
        if (stats.errors.length) return

        // Get the server bundle in memory and use eval to return the controller
        bundle = eval(readFile(mfs, 'js/server-bundle.js')).default;
        update()
    })

    return readyPromise
}
Copy the code

Finally, the two environments are distinguished in KOA

if (isPro) {
    // The build environment uses packaged resources directly
    serverBundle = require('.. /dist/js/server-bundle').default;
    template = fs.readFileSync(resolve('.. /dist/server.tpl.html'), 'utf-8');
} else {
    // The development environment creates a service
    readyPromise = require('.. /build/dev-server')(app, resolve('.. /app/index.html'));
}

router.get(The '*'.async (ctx, next) => {
    if (isPro) {
        await render(ctx, serverBundle, template);
    } else {
        // Wait for the file to be retrieved before rendering.
        const { bundle, clientHtml } = await readyPromise;
        await render(ctx, bundle, clientHtml);
    }
    next();
})
Copy the code

Well, that’s the end of this tutorial. If it helped, please feel free to leave your likes and start questions in the comments below or on Github. Finally, everyone to give my github a start, xiaobian grateful.