preface

In the last section, we dealt with resources a little bit. Next, we will configure the production environment and load JS dynamically. In many cases, we need to subcontract based on routing or some less used libraries.

The production environment is configured on the front end

Previously, webpack.base.js separated the same parts of the server and client. Now that you have a production environment configuration, modify webpack.base.js to separate the same parts of the client development and production environments. On the server side, process.env.node_env determines the development environment and production environment.

Directory structure:

├─ WebPack // Setup folder │ ├─ Loader // Custom Loader folder │ ├─ scripts // Node Script folder │ ├─ Webpack.base.js ├─ Webpack.client.dev.js // Front-end Environment Configuration file │ ├─ Webpack.client.pro.js // Front-end Production Environment Configuration file │ ├─ webpack.server.js // Server Configuration fileCopy the code

Install dependencies

$ npm i optimize-css-assets-webpack-plugin webpack-manifest-plugin -D
Copy the code

Optimize – CSS-assets-webpack-plugin is used to compress CSS files, and webpack-manifest-plugin is used to generate json files. The server needs to get the correct file name, which can be obtained from json generated by the plugin. Use webpack.base.js to separate the same configuration from the front-end development environment and production environment.

module.exports = {
  entry: resolvePath('.. /src/client/index.js'),
  resolve: {
    extensions: ['.js'.'.jsx']},module: {
    rules: [{
      test: /.jsx? $/,
      use: 'babel-loader'.exclude: /node_modules/
    }, {
      test: /\.css$/,
      use: [MiniCssExtractPlugin.loader, 'css-loader'] {},test: /\.(jpg|png|jpeg)$/,
      use: [{
        loader: 'url-loader'.options: {
          limit: 10240.name: 'img/[name].[hash:7].[ext]'.esModule: false}}}}]],optimization: {
    splitChunks: {
      cacheGroups: {
        libs: {
          test: /node_modules/,
          chunks: 'initial'.name: 'libs'}}}},plugins: [
    new webpack.DefinePlugin({
      '__isServer': false
    }),
    new CleanWebpackPlugin()
  ]
}
Copy the code

So the front-end development environment configuration is relatively simple, except the base part, only mode output plugins and Devtool. Devtool is used for debugging errors in the development environment. I used cheap-module-eval-source-map.

module.exports = merge(base, {
  mode: 'development'.output: {
    filename: 'index.js'.path: resolvePath('.. /dist/client')},devtool: 'cheap-module-eval-source-map'.plugins: [
    new MiniCssExtractPlugin({
      filename: 'index.css'})]});Copy the code

As for the front-end production environment, it is mainly to add hash, produce resource mapping table, and perform CSS code compression. By the way, webpack4 has done a lot of processing when mode = production, such as JS compression, so we can omit the steps of JS compression.

module.exports = merge(base, {
  mode: 'production'.output: {
    filename: 'index-[contentHash:10].js'.path: resolvePath('.. /dist/client')},plugins: [
    new MiniCssExtractPlugin({
      filename: 'index-[contentHash:10].css'
    }),
    new webpackManifestPlugin({
      fileName: '.. /mainfest.json'.// This is fileName, the top is fileName, confused
      filter: ({ name }) = > {
        const ext = name.slice(name.lastIndexOf('. '));
        return ext === '.js' || ext === '.css'; }}),new OptimizeCssAssetsWebpackPlugin()
  ]
})
Copy the code

The purpose of the webpackManifestPlugin filter is to produce the resource mapping table only for CSS and JS files; for other types of files, such as IMG, it has been correctly referenced.

app.use(express.static(‘./dist/client’)); This is equivalent to the relative path reference in the dist/client directory, so the image SSR output is not a problem.

So now the front end can package the generator & resource mapping table:

CSS files are also compressed:

Configure the production environment on the server

So now there is a new problem for the server. When the server joins HTML, which file name should be introduced, that is, index.js or index-xxx.js?

This can be done by setting global constants directly via webpack.definePlugin and introducing different file names for different NODE_ENV.

On package.json, set NODE_ENV=production.

In addition, set alias for the path to import files.

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  mode: process.env.NODE_ENV,
  target: 'node'.entry: resolvePath('.. /src/server/index.js'),
  output: {
    filename: 'index.js'.path: resolvePath('.. /dist/server')},externals: [nodeExternals()],
  module: {
    rules: [{
      test: /.css$/,
      loader: './webpack/loader/css-remove-loader'
    }, {
      test: /.jsx? $/,
      use: 'babel-loader'.exclude: /node_modules/
    }, {
      test: /.(jpg|jpeg|png)$/,
      use: {
        loader: 'url-loader'.options: {
          limit: 10240.name: 'img/[name].[hash:7].[ext]'.esModule: false}}}},resolve: {
    alias: {
      '@dist': resolvePath('.. /dist'),}},plugins: [
    new webpack.DefinePlugin({
      '__isServer': true.'__isDev': isDev
    }),
    new CleanWebpackPlugin()
  ]
};
Copy the code

NODE_ENV===’development’ in the server webpack configuration file, the global constant __isDev is set. In the HTML splicing file, you can use this global variable to determine whether to introduce index.js or index-xxx.js.

export const handleHtml = ({ reactStr, initialData, styles }) = > {
  const jsKeys = ['libs.js'.'main.js'];
  const cssKeys = ['main.css'];

  let jsValues = [];
  let cssValues = [];

  if (__isDev) {
    jsValues = ['libs.index.js'.'index.js'];
    cssValues = ['index.css'];

  } else {
    const mainfest = require('@dist/mainfest.json');

    jsValues = jsKeys.map(v= > mainfest[v]);
    cssValues = cssKeys.map(v= > mainfest[v]);
  };
  return `..... `;
};
Copy the code

The code looks pretty clear and easy to understand, so I won’t explain it.

Finally, in HTML, we can introduce our jsValues and cssValues.

` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title>${cssValues.map(v => `<link rel="stylesheet" href="${v}"></link>`).join(' ')}
  </head>
  <body>
      <div id="root">${reactStr}</div>
      <textarea id="textareaSsrData" style="display: none">${initialData}</textarea>
  </body>
  ${jsValues.map(v => `<script type="text/javascript" src="${v}"></script>`).join(' ')}
  </html>`
Copy the code

Configure run scripts & run

Finally, we configure our front-end packaging and server-side packaging commands in package.json.

{
    "client:build": "webpack --config ./webpack/webpack.client.pro.js"."server:build": "NODE_ENV=production webpack --config ./webpack/webpack.server.js"."build": "npm run client:build && npm run server:build"."start": "node ./dist/server/index.js"
}
Copy the code

Also change server:dev, after all, to pass in NODE_ENV.

{
    "server:dev": "NODE_ENV=development webpack --watch --config ./webpack/webpack.server.js"
}
Copy the code

By the way, if writing && is a hassle, you can use node task management modules such as npm-run-all, script-runner, etc. So at this point, you’re ready to compile and run.

Complete Code (SSR-PRO)

JS dynamic loading

Dynamic loading of JS is mainly through the import() method, which uses the Promise callback to get the module we want to load asynchronously. Components loaded by import() are processed into separate JS files, but in some cases they are not processed into separate files.

For example, under the CommonJS specification, if one component of the file is introduced, but the rest of the components are dynamically loaded, it will fail, and the rest of the components have already been pushed into main.js. This is because CommonJS modules are dynamically defined and it is difficult to analyze them, which is why using CommonJS can cause packages to become large. The general solution is to use Tree Sharking.

Install dependencies

$ npm i @babel/{plugin-proposal-class-properties,plugin-syntax-dynamic-import} -D
Copy the code

And add it to.babelrc

{
  "plugins": [
    "@babel/plugin-syntax-dynamic-import"."@babel/plugin-proposal-class-properties"]}Copy the code

React.lazy

React.lazy is used to load components asynchronously for components that are not used for page components but take up a large portion of the JS file. Lazy is simply an optimization for dynamic import.

const Comp = React.lazy(() = > import('./components/child'));
const Index = () = > {
  const [show, setShow] = useState(false);
  const click = () = > setShow(true);

  return (
    <div>
      <div onClick={click}>page: Orange</div>
      {
        show ?
          <React.Suspense fallback={<div>loading</div>} ><Comp />
          </React.Suspense> : null
      }
    </div>)};Copy the code

Dynamic load based on routing

For routed components, we should pass in the function ()=>import() and call this function inside the component. In the callback, the asynchronously loaded component is rendered.

Here I use the function, pass in the load function to return the component, and then use the load function as a static method of the component, convenient for the later server SSR, directly load the component.

const asyncComp = (load) = > {
  return class AsyncComp extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        Comp: null
      };
    };

    static _load = load;

    asyncLoad = async() = > {const { default: Comp } = await AsyncComp._load();
      this.setState({ Comp });
    };

    componentDidMount() {
      if (!this.state.Comp) {
        console.log('Load asynchronous component');
        this.asyncLoad(); }};render() {
      const { Comp } = this.state;
      return (
        Comp ? <Comp {. this.props} / > : <div>loading</div>)}; }; };Copy the code

The routing configuration file needs to be modified. For components that need to be loaded asynchronously, the following changes are used

import { Fruit } from '.. /pages/index';
export default [{
  component: Fruit,
  path: '/'.exact: true.name: 'Fruits'
}, {
  component: asyncComp(() = > import('.. /pages/apple/index')),
  path: '/apple'.exact: true.name: 'Apple'
},/ /...
]
Copy the code

But with the above configuration, there are not a number of packets

There was a lot of talk on the web about, when the Webpack was packed, about.jsx? Babel modules defaults to CommonJS. That is, our code now uses ES Module but Babel will convert to CommonJS.

However, the babel-loader8.x version is used and is no longer converted to CommonJS by default.

At the same time, if the asynchronous loading behind is deleted and only Fruits are kept, other page components will be removed after Tree Sharking is opened, indicating that Tree Sharking is not invalid.

However, it is still packaged into mian.js, indicating that Tree Sharking cannot handle this situation and another method is needed.

The solution is also more straightforward:

  • In the appropriate directory, import the component
  • Use sideEffects to remove code

Directly introducing

import Fruit from '.. /pages/fruit/index';
Copy the code

sideEffects

In webpack4 you can add sideEffects to mark a file with no sideEffects via package.json, and webpack will remove any unused code from that file.

SideEffects is different from Tree Sharking, which can only remove unused code members, while sideEffects can remove entire modules.

To use sideEffects, you need to turn sideEffects: true on in Optimization, which is enabled by default in Production mode.

{
  "sideEffects": [
    "./src/client/pages/index.js"]}Copy the code

Both methods allow WebPack to separate asynchronous components into separate packages.

As you can see, js files are loaded asynchronously during route switching.

However, if you access the route directly, the server does not return the corresponding component but is in loading state, indicating that no component is obtained on the server.

Because the server uses the same routing configuration as the client, the server does not need to load the component asynchronously, but instead needs to return the component directly. Routing configuration needs to be handled.

The server handles dynamic loading issues

We need to handle the routing. The current route configuration component is loaded asynchronously, so we need to get the static route configuration. Before, we put the dynamic loading function into the static method of the component, on the server side we can call the static method of the component, to get the component loaded asynchronously.

export const getStaticRoute = async (asyncRoute) => {
  const staitcRoute = [];
  const len = asyncRoute.length;

  for (let i = 0; i < len; i++) {
    constitem = asyncRoute[i]; staitcRoute.push({ ... item });if (item.component._load) {
      const component = (await item.component._load()).default;
      staitcRoute[staitcRoute.length - 1].component = component; }};return staitcRoute;
};
Copy the code

In SSR middleware, we need to cache the results of obtaining static routes. At run time, the static route results are saved to variables through self-executing functions.

let staticRoute = [];
(async () => {
  staticRoute = awaitgetStaticRoute(routeConfig); }) ();Copy the code

And modify the previous App component to get the route array as one of its arguments. On the client side you get a route configuration with asynchronous components, and on the server side you get a static route configuration.

const App = ({ pathname, initialData, routeConfig }) = > {
  return (
    <Fragment>
      <Header />
      <Switch>{ routeConfig.map(v => { const { name, ... rest } = v; if (pathname === v.path) { const { component: Component, ... _rest } = rest; return<Route key={v.path} {. _rest} render={(props)= > {
                props.initialData = initialData;
                return <Component {. props} / >
              }} />
            } else {
              return <Route key={v.path} {. rest} / >}})}<Route component={()= > <Redirect to="/" />} / ></Switch>
    </Fragment>)};Copy the code

So now access the route directly, SSR will return the component result directly.

But after the browser takes over, the client rendering overwrites the server rendering.

This is because, although the server SSR returns the Banana component rendering result. However, the browser does not request xx.js that the component splits. Loading is the process of loading XX.js.

The solution is to make the JS file request before the client renders, and then render in the JS file request callback. Also modify the route configuration array to set Component to the corresponding component, and the asynchronous loading will no longer be done.

const clientRender = () = > {
  ReactDOM.hydrate(
    <BrowserRouter>
      <App pathname={pathname} initialData={initialData} routeConfig={routeConfig} />
    </BrowserRouter>.document.getElementById('root')); };const judgeAsync = () = > {
  const route = getAimComp(routeConfig, pathname);
  const asyncLoad = route.component._load;
  if (asyncLoad) {
    asyncLoad().then(res= > {
      route.component = res.default; // Modify the route configuration array
      clientRender();
    });
  } else{ clientRender(); }}; judgeAsync();Copy the code

Then dynamic loading based on routing will complete liao, the complete code is as follows.

The complete code

Other chapters:

React SSR Practice (1)

React SSR Practice (part 2)