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-router 和 koa-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.