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.
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:
- React Best Practices (I) Technology selection
- React Best Practices (part 3)