Github address: github.com/chikara-cha…

directory

  • preface
  • Server-side rendering benefits
  • thinking
  • The principle of
  • The homogeneous solution
  • Status Management scheme
  • Routing scheme
  • Static resource processing scheme
  • Dynamic loading scheme
  • Optimization scheme
  • Deployment plan
  • other
  • At the end

preface

Recently, the company has a product demand requires the use of the Node. Js intermediary to do server side rendering, then went through the whole community, not found a special suitable scaffolding, as a pursuit of the front end of the siege of the lion, decided to build a rendering development environment, the most perfect service end during tread countless pit, It took about three weeks.

Server-side rendering benefits

  1. SEO makes it easier for search engines to read page content
  2. The first screen rendering is faster (important) and there is no need to wait for the JS file to download and execute
  3. Easier to maintain, server and client can share some code

thinking

  1. How to implement component isomorphism?
  2. How do I keep the application status of the front and back ends consistent?
  3. How to solve the problem of route matching between the front and back ends?
  4. How to handle server dependency on static resources?
  5. How do YOU configure two different environments (development and production)?
  6. How to partition a more reasonable project directory structure?

The goal of this article is to teach you how to build an elegant server rendering development environment, from packaging, deployment and optimization to launch.

The principle of





Thanks to the growth and popularity of Node.js, Javascript has become a homogeneous language, which means we only need to write a single set of code that can be executed on both the client and server side.

The homogeneous solution

Here, we use React technology system for isomorphism. Due to its design characteristics, React is stored in memory in the form of Virtual DOM, which is the premise of server rendering.

For the client, the reactdom. render method is used to convert the Virtual DOM into the real DOM and render it to the interface.

import { render } from 'react-dom'
import App from './App'

render(<App />, document.getElementById('root'))Copy the code

For the server, by calling the ReactDOMServer. RenderToString method to convert the Virtual DOM HTML string returned to the client, so as to achieve the purpose of the service side rendering.

import { renderToString } from 'react-dom/server'
import App from './App'

async function(ctx) {
    await ctx.render('index', {
        root: renderToString(<App />)
    })
}Copy the code

Status Management scheme

We chose Redux to manage the non-private component state of the React component, and to augment the application with the community’s powerful middleware Devtools, Thunk, Promise, and so on. When server rendering is carried out, the initial state must be returned to the client after the store instance is created. The client takes the initial state and uses it as the pre-loading state to create the store instance. Otherwise, the markup generated on the client does not match the markup generated on the server, and the client has to load data again. Unnecessary performance drain.

The service side
import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './App'
import rootReducer from './reducers'

const store = createStore(rootReducer)

async function(ctx) {
    await ctx.render('index', {
        root: renderToString(
            <Provider store={store}>
                <App />
            </Provider>
        ),
        state: store.getState()
    })
}Copy the code
HTML
<body>
    <div id="root"><% - root% ></div>
    <script>
        window.REDUX_STATE = <% - JSON.stringify(state) % >
    </script>
</body>Copy the code
The client
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './App'
import rootReducer from './reducers'

const store = createStore(rootReducer, window.REDUX_STATE)

render(
    <Provider store={store}>
        <App />
    </Provider>, 
    document.getElementById('root'))Copy the code

Routing scheme

The benefits of client routing are needless to say. The client can render different views according to the hash method or call the History API without relying on the server, realizing seamless page switching and excellent user experience. The difference with server-side rendering, however, is that before rendering, matching components must be correctly found and returned to the client based on the URL. The React Router provides two apis for server-side rendering:

  • matchThe routing component is matched against the URL prior to rendering
  • RoutingContextRenders routing components synchronously
The service side
import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { match, RouterContext } from 'react-router'
import rootReducer from './reducers'
import routes from './routes'

const store = createStore(rootReducer)

async function clientRoute(ctx, next) {
    let _renderProps

    match({routes, location: ctx.url}, (error, redirectLocation, renderProps) => {
        _renderProps = renderProps
    })

    if (_renderProps) {
        await ctx.render('index', { root: renderToString( <Provider store={store}> <RouterContext {... _renderProps} /> </Provider> ), state: store.getState() }) }else {
        await next()
    }
}Copy the code
The client
import { Route, IndexRoute } from 'react-router'
import Common from './Common'
import Home from './Home'
import Explore from './Explore'
import About from './About'

const routes = (
    <Route path="/" component={Common}>
        <IndexRoute component={Home} />
        <Route path="explore" component={Explore} />
        <Route path="about" component={About} />
    </Route>
)

export default routesCopy the code

Static resource processing scheme

In the client, we used a lot of ES6/7 syntax, JSX syntax, CSS resources, image resources, and finally packaged into a file with webpack and various loaders to run in the browser environment. On the server side, however, the syntax import and JSX are not supported, and module references to CSS and image resource suffixes are not recognized. So how to deal with these static resources? We need tools and plug-ins to enable the Node.js parser to load and execute this type of code. Here are two different solutions for development and production environments.

The development environment
  1. The library babel-Polyfill was first introduced to provide the Regenerator runtime and core-JS to simulate a fully functional ES6 environment.
  2. Babel-register is a require hook that automatically transcodes js files loaded by the require command in real time. Note that this library is only suitable for development environments.
  3. We introduced csS-modules-require -hook, which is also a hook, only for style files. Since we use CSS modules and use SASS to write code, we need the node-sass precompiler to recognize files with the extension.scss. You can also use LESS to automatically extract the className hash into the React component of the server.
  4. Asset-require-hook is introduced to identify image resources. Images smaller than 8K are converted to base64 strings, and images larger than 8K are converted to path references.
// Provide custom regenerator runtime and core-js
require('babel-polyfill')

// Javascript required hook
require('babel-register')({presets: ['es2015'.'react'.'stage-0']})

// Css required hook
require('css-modules-require-hook')({
    extensions: ['.scss'],
    preprocessCss: (data, filename) =>
        require('node-sass').renderSync({
            data,
            file: filename
        }).css,
    camelCase: true,
    generateScopedName: '[name]__[local]__[hash:base64:8]'
})

// Image required hook
require('asset-require-hook')({
    extensions: ['jpg'.'png'.'gif'.'webp'],
    limit: 8000
})Copy the code
Production environment

For a production environment, we use WebPack to package the client and server code separately. For the server code, you need to specify the runtime environment as Node, provide polyfill, and set __filename and __dirname to true. Because CSS Modules are used, the server only needs to fetch className, without loading the style code. Use CSS-loader /locals instead of csS-loader to load the style file

// webpack.config.js
{
    target: 'node',
    node: {
        __filename: true,
        __dirname: true
    },
    module: {
        loaders: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: {presets: ['es2015'.'react'.'stage-0']}
        }, {
            test: /\.scss$/,
            loaders: [
                'css/locals? modules&camelCase&importLoaders=1&localIdentName=[hash:base64:8]'.'sass'
            ]
        }, {
            test: /\.(jpg|png|gif|webp)$/,
            loader: 'url? limit=8000'}}}]Copy the code

Dynamic loading scheme

For large Web applications, packing all the code into a single file is not an elegant approach, especially for single-page applications where users sometimes don’t want the rest of the routing module content, and loading the entire module content not only increases user wait time, but also increases server load. Webpack provides a feature that allows you to split modules. Each module is called a chunk. This feature is called Code Splitting. Ensure does not exist for server-side rendering, so you need to determine the runtime environment and provide hook functions.

The reconstructed routing module is

// Hook for server
if (typeof require.ensure ! = ='function') {
    require.ensure = function(dependencies, callback) {
        callback(require)}}const routes = {
    childRoutes: [{
        path: '/',
        component: require('./common/containers/Root').default,
        indexRoute: {
            getComponent(nextState, callback) {
                require.ensure([], require => {
                    callback(null.require('./home/containers/App').default)},'home')
            }
        },
        childRoutes: [{
            path: 'explore',
            getComponent(nextState, callback) {
                require.ensure([], require => {
                    callback(null.require('./explore/containers/App').default)},'explore')
            }
        }, {
            path: 'about',
            getComponent(nextState, callback) {
                require.ensure([], require => {
                    callback(null.require('./about/containers/App').default)},'about')
            }
        }]
    }]
}

export default routesCopy the code

Optimization scheme

Extract the third-party library and name it Vendor

vendor: ['react'.'react-dom'.'redux'.'react-redux']Copy the code

All JS modules are named as chunkhash

output: {
    filename: '[name].[chunkhash:8].js',
    chunkFilename: 'chunk.[name].[chunkhash:8].js',}Copy the code

Extract the common module, the manifest file plays a transitional role

new webpack.optimize.CommonsChunkPlugin({
    names: ['vendor'.'manifest'],
    filename: '[name].[chunkhash:8].js'
})Copy the code

Extract the CSS file and name it contenthash

new ExtractTextPlugin('[name].[contenthash:8].css')Copy the code

Module sorting, de-duplication, compression

new webpack.optimize.OccurrenceOrderPlugin(), // Webpack2 is removed
new webpack.optimize.DedupePlugin(), // Webpack2 is removed
new webpack.optimize.UglifyJsPlugin({
    compress: {warnings: false},
    comments: false
})Copy the code

It is important to note that you cannot use the latest built-in instance methods, such as includes for arrays

{
    presets: ['es2015'.'react'.'stage-0'],
    plugins: ['transform-runtime']}Copy the code

Final package result





Paste_Image.png

Deployment plan

For client code, upload all static resources to the CDN server. For server code, pM2 deployment is used, which is a Node application process manager with load balancing function. It supports monitoring, logging, 0-second overloading, and can start the maximum number of processes in a cluster based on the number of available cpus

pm2 start. /server.js -i 0Copy the code




Paste_Image.png

other

Improve the development experience

For client code, you can use Hot Module Replacement with koa-webpack-dev-middleware and koa-webpack-hot-middleware. Unlike BrowserSync, you can use Hot Module Replacement with koa-Webpack-dev-middleware and koa-webpack-hot-middleware. It allows javascript and CSS changes to be fed back to the browser interface in real time without having to refresh the browser.

app.use(convert(devMiddleware(compiler.{
    noInfo: true,
    publicPath: config.output.publicPath
})))
app.use(convert(hotMiddleware(compiler)))Copy the code

For server code, Nodemon is used to monitor code changes and automatically restart node server. Compared with Supervisor, Nodemon is more flexible, lightweight, occupies less memory, and has higher configurability.

nodemon ./server.js --watch serverCopy the code

For React component state management, the middleware Redux DevTools is used to track every state and action, monitor data flow, and have the capability of state backtracking due to the pure functional programming philosophy. Note that the React component only executes to componentWillMount during the server lifecycle, so mount this middleware to the componentDidMount method to avoid errors when rendering on the server.

class Root extends Component {
    constructor() {
        super(a)this.state = {isMounted: false}
    }
    componentDidMount() {
        this.setState({isMounted: true})
    }
    render() {
        const {isMounted} = this.state
        return (
            <div>
                {isMounted && <DevTools/>}
            </div>
        )
    }
}Copy the code

Code style constraints

Is recommended to use in the most popular ESLint, compared to other QA tools, more and more flexible, more scalable, configuration, both for the individual and team cooperation, introduce code style inspection tools, exams, and suggest that you spend a day time try again ESLint every configuration, and then make a decision which need configuration, abandon what configuration, Instead of going straight to the Airbnb spec, the Google spec, etc.

Tips: Use the fix parameter to quickly fix some common errors and, to some extent, replace the editor formatting tool

eslint test.js --fixCopy the code

Development environment Demo

Youtubee video, bring your own ladder www.youtube.com/watch?v=h3n…

At the end

Today, in the open source community is not a perfect service side rendering solution, and at the beginning to build the purpose of the scaffold is based on ease of use, with the most clear configuration, use one of the most popular stack, the directory structure of a group is the most reasonable, give developers the most perfect development experience, from the development of packaging deployment optimization to online and entity. Even if you have no experience, you can easily get started with server-side rendering development.

Source code: github.com/chikara-cha…