Github address: github.com/bbwlfx/ts-b…

Once the configuration is complete, it’s time to consider the architectural aspects of package startup and front and back end isomorphism.

Webpack packaging

First of all, my overall idea is: according to the webpack.ssr. Config. js configuration file, pack the front-end code into the node layer for node to use SSR, and then start the Webpack-dev-server server normally.

package.json
"startfe": "run-p client ssr",
"client": "BABEL_ENV=client NODE_ENV=development webpack-dev-server --config public/webpack.dev.config.js",
"ssr": "BABEL_ENV=ssr NODE_ENV=development webpack --watch --config public/webpack.ssr.config.js",
Copy the code

After wrapping the front-end code into Node, start the Node server as normal:

package.json
"start": "BABEL_ENV=server NODE_ENV=development nodemon src/app.ts --exec babel-node --extensions '.ts,.tsx'",
Copy the code

So basically webpack the whole packaging idea is clear.

In the final production mode, we just need to pack the whole front-end code into the SRC directory through webpack, and then output the whole SRC directory to the output directory after Babel escape. In the final production mode, we just need to start output/app.js.

package.json
"buildfe": "run-p client:prod ssr:prod",
"build": "BABEL_ENV=server NODE_ENV=production babel src -D -d output/src --extensions '.ts,.tsx'",
"ssr:prod": "BABEL_ENV=ssr NODE_ENV=production webpack --config public/webpack.ssr.config.js",
"client:prod": "BABEL_ENV=client NODE_ENV=production webpack --progess --config public/webpack.prod.config.js",
Copy the code
$node output/app.js // Start production modeCopy the code

Webpack configuration

For client packaging, we need to use the webpack-manifest-plugin. This plugin writes the path of all webpack files into a manifest.json file, which we can read to find the correct path of all resources.

Some webpack. Client. Config. Js
const ManifestPlugin = require("webpack-manifest-plugin");
module.exports = merge(baseConfig, {
  // ...
  plugins: [
    new ManifestPlugin(),
    // ...]});Copy the code
Mapping loaded modules to bundles

In order to make sure that the client loads all the modules that were rendered server-side, we’ll need to map them to the bundles that Webpack created.

Our client rendering uses react-loadable, we need to know whether the module has been rendered by the server in advance, otherwise there will be a problem of reloading. Therefore, it is necessary to generate a map file of bundles packaged by Webpack and pass react-loadable in SSR. Here we use the React-loadable /webpack plugin.

Some webpack. Client. Config. Js
import { ReactLoadablePlugin } from 'react-loadable/webpack';
const outputDir = path.resolve(__dirname, ".. /src/public/buildPublic");
plugins: [
    // ...
    new ReactLoadablePlugin({
      filename: path.resolve(outputDir, "react-loadable.json")})// ...].Copy the code

Next comes the issue of resource paths for webPack packaging artifacts.

Production mode usually uploads the output file to the CDN, so we only need to use the CDN address in pubicPath.

Some webpack. Prod. Config. Js
mode: "production".output: {
    filename: "[name].[chunkhash].js".publicPath: "//cdn.address.com".chunkFilename: "chunk.[name].[chunkhash].js"
  },
Copy the code

In the development environment, we just need to read the address of the corresponding module in the manifest.json file.

manifest.json
{
  "home.js": "http://127.0.0.1:4999/static/home.js"."home.css": "http://127.0.0.1:4999/static/home.css"."home.js.map": "http://127.0.0.1:4999/static/home.js.map"."home.css.map": "http://127.0.0.1:4999/static/home.css.map"
}
Copy the code

SSR code

With packaging out of the way, we need to consider SSR.

In fact, the overall idea is relatively simple: Json file to store the static resource path, and react-loadable. Json file to store the information of each module output. Just read the JS and CSS paths in the SSR place.
then fill the puG template with renderToString, the component wrapped in

src/utils/bundle.ts
function getScript(src) {
  return `<script type="text/javascript" src="${src}"></script>`;
}
function getStyle(src) {
  return `<link rel="stylesheet" href="${src}"/ > `;
}

export { getScript, getStyle };
Copy the code
src/utils/getPage.ts
import { getBundles } from "react-loadable/webpack";
import React from "react";
import { getScript, getStyle } from "./bundle";
import { renderToString } from "react-dom/server";
import Loadable from "react-loadable";

export default async function getPage({ store, url, Component, page }) {
  const manifest = require(".. /public/buildPublic/manifest.json");
  const mainjs = getScript(manifest[`${page}.js`]);
  const maincss = getStyle(manifest[`${page}.css`]);

  let modules: string[] = [];

  constdom = ( <Loadable.Capture report={moduleName => { modules.push(moduleName); }} > <Component url={url} store={store} /> </Loadable.Capture> ); const html = renderToString(dom); const stats = require(".. /public/buildPublic/react-loadable.json"); let bundles: any[] = getBundles(stats, modules); const _styles = bundles .filter(bundle => bundle && bundle.file.endsWith(".css")) .map(bundle => getStyle(bundle.publicPath)) .concat(maincss); const styles = [...new Set(_styles)].join("\n"); const _scripts = bundles .filter(bundle => bundle && bundle.file.endsWith(".js")) .map(bundle => getScript(bundle.publicPath)) .concat(mainjs); const scripts = [...new Set(_scripts)].join("\n"); return { html, __INIT_STATES__: JSON.stringify(store.getState()), scripts, styles }; }Copy the code

Path description: SRC /public/buildPublic stores all front-end files. SRC /public/buildPublic stores webpack.client.config.js. SRC /public/buildServer stores server-side rendered code packaged with Webpack.ssr. Config. js.

The server-side rendering is now almost complete.

Other node layer startup code can be viewed directly in the SRC /server.ts file.

The front and back ends are isomorphic

The next step is to write the front-end business code to test whether the server-side rendering works.

Here we want to make sure that we use the least amount of code to accomplish the function of front and back end isomorphism.

First we need to define a variable IS_NODE in the Webpack, according to this variable in the code can be separated from the SSR part of the code and the client part of the code.

webpack.client.config.js
plugins: [
    // ...
    new webpack.DefinePlugin({
        IS_NODE: false
    })
    // ...
]
Copy the code

Next, write the entry file of the front-end page, and the entry file is to make a difference between SSR and client rendering:

public/js/decorators/entry.tsx
import React, { Component } from "react";
import { Provider } from "react-redux";
import ReactDOM from "react-dom";
import Loadable from "react-loadable";
import { BrowserRouter, StaticRouter } from "react-router-dom";

// server side render
const SSR = App= >
  class SSR extends Component<{
    store: any;
    url: string;
  }> {
    render() {
      const context = {};
      return (
        <Provider store={this.props.store} context={context}>
          <StaticRouter location={this.props.url}>
            <App />
          </StaticRouter>
        </Provider>); }};// client side render
const CLIENT = configureState= > Component => {
  const initStates = window.__INIT_STATES__;
  const store = configureState(initStates);
  Loadable.preloadReady().then((a)= > {
    ReactDOM.hydrate(
      <Provider store={store}>
        <BrowserRouter>
          <Component />
        </BrowserRouter>
      </Provider>.document.getElementById("root")); }); };export default function entry(configureState) {
  return IS_NODE ? SSR : CLIENT(configureState);
}
Copy the code

Here the configureState in the Entry parameter is our store declaration file.

public/js/models/configure.ts
import { init } from "@rematch/core";
import immerPlugin from "@rematch/immer";
import * as models from "./index";

const immer = immerPlugin();

export default function configure(initStates) {
  const store = init({
    models,
    plugins: [immer]
  });
  for (const model of Object.keys(models)) {
    store.dispatch({
      type: `${model}/@init`.payload: initStates[model]
    });
  }
  return store;
}
Copy the code

Then we’re all set. All we need to do is agree on our single page entry.

Here I put the entry of a single page under the public/js/entry directory. Each single page is a directory. For example, there is only one single page in my project, so I only create a home directory.

Each directory has an index. TSX file and a routes. TSX file, divided into a single page of the overall entry code, the route definition code.

Such as:

/entry/home/routes.tsx
import Loadable from "react-loadable";
import * as Path from "constants/path";
import Loading from "components/loading";

export default[{name: "demo".path: Path.Demo,
    component: Loadable({
      loader: (a)= > import("containers/demo"),
      loading: Loading
    }),
    exact: true
  },
  {
    name: "todolist".path: Path.Todolist,
    component: Loadable({
      loader: (a)= > import("containers/todolist"),
      loading: Loading
    }),
    exact: true}];Copy the code
/entry/home.index.tsx
import React, { Component } from "react";
import configureStore from "models/configure";
import entry from "decorators/entry";
import { Route } from "react-router-dom";
import Layout from "components/layout";
import routes from "./routes";

class Home extends Component {
  render() {
    return (
      <Layout>
        {routes.map(({ path, component: Component, exact = true }) => {
          return (
            <Route path={path} component={Component} key={path} exact={exact} />
          );
        })}
      </Layout>
    );
  }
}

const Entry = entry(configureStore)(Home);
export { Entry as default, Entry, configureStore };
Copy the code

The Layout component is the common part that holds all pages, such as the Nav bar, Footer, and so on.

Now that all the preparatory work is done, all that remains is to write the component code and load the first screen data.

Series of articles:

  1. React Best Practices (I) Technology selection
  2. React Best Practices (part 3)