2020/5/16 update:
This post is invalidated with a better upgrade alternative: juejin.cn/post/684490…
Case project address: React-coat-SSR-demo
You may feel that this Demo is too heavy on routing encapsulation, and that you don’t like the idea of using inheritance to organize your models, but that’s ok, this is just a primer, and you can remove that logic at your discretion.
Meaning of this Demo
There are already plenty of articles and tutorials on React SSR online, but they…
- Or just teach you principles and knowledge, no real product engineering.
- Or it just introduces some core elements and lacks completeness.
- Or it’s just on paper, not even a decent Demo.
- Or some outdated version.
So what you lack is not an SSR tutorial, but a complete example that can be applied to a production environment.
Single page isomorphic SSR
For server-side Rendering on React, you might say: we already have next.js, and prerender too. But honey, have you actually used them for a more complicated project? Our goal is to go further, not only SSR, but also the user experience of Single Page and the engineering scheme of isomorphic. Therefore, we set 8 requirements for ourselves:
- Browsers and servers reuse the same set of code and routing.
- Compiled code should be easy to deploy and not too dependent.
- The first screen that the browser loads is rendered by the server to improve load speed and SEO.
- The browser does not repeat the rendering work done by the server (including request data that is not repeated).
- The first screen is no longer refreshed as a whole, but partially updated through Ajax, bringing a single-page user experience.
- During the interaction, the page can be refreshed at any time, and the current content can be reproduced through the URL (including opening pop-ups and other actions).
- All route hops link back to original < span style = “box-sizing: border-box! Important;
- browser jump behavior, switch to a single page mode open.
The last two requirements above can be verified in this way:
Click on a link with the left mouse button to see if it is a single-page user experience. Right-click and choose open in a new window to see if it can jump to multiple pages.
This engineering highlights
Scaffolding is complete and ready out of the box
You may have tried to build SSR engineering scaffolding and encountered a similar problem:
- SSR needs to generate browser running code and server running code, so two sets of Webpack compilation and deployment are required.
- In addition to webpackDevSever, you also need to start ssrServer and mockServer
- WebpackDevSever can use hot update, but can ssrServer use hot update?
Ok, this project scaffolding has solved the above problem, you just need one command to run:
npm start
Browser rendering? Server rendering? A key switch
Open the project root./package.json, in the “devServer” TAB, set SSR to true to enable server rendering, and set it to false to use browser only rendering
"devServer": {
"url": "http://localhost:7445"."ssr": true, // Whether server rendering is enabled"mock": true."proxy": {
"/ajax/**": {
"target": "http://localhost:7446/"."secure": false."changeOrigin": true}}}Copy the code
Take advantage of Typescript’s powerful type checking
This Demo not only uses THE TS type to define various data structures, but more importantly, the module, model, view, action, and Router are linked together to constrain and check each other, fully transforming Typescript into productivity.
The installation
git clone https://github.com/wooline/react-coat-ssr-demo.git
npm install
Copy the code
run
npm start
Run in development modenpm run build
Build files in production modenpm run prod-express-demo
Compile the build file in production mode and enable express for demonpm run gen-icon
Automatically generateiconfontFile and TS type
Viewing online Demo
Click to view the online Demo and be aware of the following behaviors:
- Click on any link to open a new page and refresh your browser to see if you can keep the current page content.
- Click on a link with the left mouse button to see if it is a single-page user experience, right click and choose in
A new window opened
To see if you can jump to multiple pages. - Look at the source code of the web page to see if the server outputs static Html.
start
First, you need a homogeneous framework
When developing React single-page SPA apps, you may have used some of the top frameworks like DvaJS and Rematch and found them much more exciting than the original React+Redux.
- No, although server rendering and browser rendering are running JS, but the principle is still very different, the above framework can only be used in the browser.
Isn’t there a homogeneous framework that runs in both the browser and the server?
- Yes, react-coat: Click on it first
Forget for a moment that you’re doing SSR
React-coat supports server and browser isomorphism, so you can forget you’re doing SSR for a while and start with the same logic you would use for a one-page SPA application, including how to design Store, Model, plan routes, partition modules, load on demand, etc.
So you can watch the first two demos:
SPA one-page app to start with: Helloworld
SPA single page application advances: Optimization and reuse
Modified for SSR
One set of code, two entries, two sets of output
The browser and server code are 99% common, with the exception of slightly different entry files. We create different entry files for each of them under/SRC /.
- Client.tsx original browser-side entry file that uses the buildApp() method to create the application
- Server.tsx added server-side entry files to create applications using renderApp()
Browser rendering can use modular schemes like AMD, ES, and asynchronous import, while server rendering usually uses commonJS, asynchronous loading on demand doesn’t make sense, and there’s no need to compile to ES5. So we use two sets of Webpack configuration to build these two entries into client and server output respectively:
npm run build
- /build/client outputs the code that the browser runs. JS splits the code by module, generating multiple bundles to load on demand.
- /build/server output into the server to run the code, the server run does not need to split the code, so just generate a main.js file, simple and convenient.
Browser-side subordinate run
We created a directory called /build/client, which contains Html, Js, Css, Imgs, etc. for the browser to run. It is a purely static resource, so you just need to upload the entire directory to the nginx publishing directory.
The server is running subordinate
We generated /build/server/main.js, which contains the server rendering logic for the application. You just need to copy it to your Web server framework for execution, such as Express:
const mainModule = require("./server/main.js");// Build generated main.js
const app = express();
app.use((req, res, next) = >{
const errorHandler = (err) = > {
if (err.code === "301" || err.code === "302") {
// Server routing also depends on Express
res.redirect(parseInt(err.code, 10), err.detail);
} else {
res.send(err.message || "Server error!"); }};try {
mainModule.default(req.url).then(result= > {
const { ssrInitStoreKey, data, html } = result;
// HTML render HTML string
// data Specifies the state of the redux store
// ssrInitStoreKey Dehydrated data key. }).catch(errorHandler); }catch(err) { errorHandler(err); }}); app.listen(3000);
Copy the code
Simple, right? Run main.js and get ssrInitStoreKey, data, and HTML. Once you get them, you can play with them as much as you want.
Routing short-circuit design
We originally used the React-Router in a single page because of the idea of routing as a component, and we would write something like this:
<Redirect exact path="/" to="/list" />
Copy the code
React renders to this route if the path matches. But rendering doesn’t jump until this point, so isn’t all the running cost wasted? The Server side has high requirements for execution efficiency. Therefore, for some static Redirect in SSR, it is best to implement it in advance, even before Node.js, such as directly in Nginx. In order to reduce the dependence on third parties, the Demo still uses Node.js to handle itself. However, this is all before initializing the application, which we can understand as the short-circuit design of the route.
const rootRouter = advanceRouter(path);
if (typeof rootRouter === "string") {
throw new RedirectError("301", rootRouter);
} else {
return renderApp(moduleGetter, ModuleNames.app, [path], {initData: {router: rootRouter}});
}
Copy the code
Unidirectional data flow
Redux Store->React->Store->React -> Redux Store->React-> Redux Store->React Strictly enforce UI(State) pure functions instead of relying on React lifecycle hooks to fetch data.
React-coat already encapsulates all the data logic in the Model. It also emphasizes the independence of the Model from the beginning to the end, not depending on the View, even without the View, the Model can run.
So… The server rendering process is relatively pure:
- First of all, Build the model
- And then Render the view
Two render stages
After SSR rendering is enabled, the application rendering process is similar to the birth of a baby, which is divided into two stages:
- Conceived in October, in the niang belly first developed into human. (Render part of the server first)
- Once delivered, they continue to develop on their own after birth. (The browser then renders further on the server basis)
Specific in niang belly development to what stage was born? This varies from person to person, some babies are born with 10 kg, some babies are born with less than 4 kg, @ ○ @, so you want to do more things on the server side, the browser will do less things.
As we know, in the Model of the React-Coat framework, the moduleName/INIT action is issued for the initialization of each module. We can handle this action to request data and initialize it.
Therefore, we stipulate that in SSR, the Model should be born only after executing the main INITActionHandler module. In other words, the main module INITActionHandler is the mother of all actionHandler logic that you want to run on the server.
- The INITActionHandler of the Model is written:
@effect()
protected async [ModuleNames.app + "/INIT"]() {
...
}
Copy the code
Differences in module initialization
SSR only executes INITActionHandler for the main module. What about initialization for other modules? After all, apps can’t just have a main module, right?
When we render a View on a SPA page, the framework automatically imports and initializes its Model, saving time and effort. However, in SSR, we emphasized the one-way data flow above, all models must be ready before View render, so you can’t rely on view to import automatically.
- Therefore, in SSR, if the initialization of a Model requires the participation of another Model, manual loadModel is required. Such as:
@effect()
protected async [ModuleNames.app + "/INIT"] () {const { views } = this.rootState.router; // What Views are currently displayed
// If photos are displayed, manually load and initialize the photosModel
if (views.photos) {
await loadModel(moduleGetter.photos).then(subModel= > subModel(this.store)); }}Copy the code
Extract routing logic
As can be seen from the above initialization differences, since SSR requires one-way data flow, all models must be ready before View Render. The initialization logic of some models depends on the routing logic. In single-page spAs, we tend to split the routing logic into components, because routes are components, so…
- With SSR, we had to recycle some of the necessary routing logic from the View into the Model.
- Essentially, the routing logic should also be part of the Model data logic.
Of course, if you know in advance that you are going to make SSR, you can put it directly into the model from the beginning.
Route extraction does not mean centralized configuration
We just said that reclaiming some of the routing logic from the View into the Model is not the same as centrally configuring the routing. The routing logic is still scattered in each model and is still externally encapsulated. The parent module only deals with the sub-module, but does not participate in the internal routing logic of the sub-module. This is great for decoupling and modularity.
At present, most SSR schemes are centralized configuration of routes, and then bind the logic of data acquisition (Ajax) with routes, resulting in greatly reduced readability, maintainability and reusability. In contrast, React-Coat’s routing scheme is superior.
Generate a static Link Url
In a single page SPA application, we click on a link to jump to a route, usually saying:
onItemClick = (id:string) => { const url = generateUrl(id); this.props.dispatch(routerActions.push(url)) } render(){ ... < a href = "#" onClick = {() = > enclosing onItemClick (item. Id)} > check list < / a >... }Copy the code
- When you click link, the URL is calculated and the route is switched. The URL doesn’t compute if you don’t click on it.
- But in SSR, in order for the search engine to crawl the link, we have to calculate the URL in advance and put it in the href attribute.
onItemClick = (e: React.MouseEvent<HTMLAnchorElement>) => { e.preventDefault(); const href = e.currentTarget.getAttribute("href") as string; this.props.dispatch(routerActions.push(href)); } render(){ ... <a href={generateUrl(id)} onClick={this.onItemClick}> View the list </a>... }Copy the code
Error handling
In the browser runtime environment, the react-coat listens for window.onError. Whenever there is an uncatched error, it dispatches an ErrorAction. You can listen to this action in the model and handle it, for example:
@effect(null)
protected async ["@@framework/ERROR"](error: CustomError) {
if (error.code === "401") {
this.dispatch(this.actions.putShowLoginPop(true));
} else if (error.code === "404") {
this.dispatch(this.actions.putShowNotFoundPop(true));
} else if (error.code === "301" || error.code === "302") {
this.dispatch(this.routerActions.replace(error.detail));
} else {
Toast.fail(error.message);
awaitsettingsService.api.reportError(error); }}Copy the code
In server rendering, the ErrorActionHandler is still valid, but because of one-way data flow, the model must be completed before the View, so it can only handle the error in the model running. The error in render View cannot be handled. If you need handle, please try catch on the upper layer of the application, such as express.
Use the Transfer – Encoding: chunked
SSR means that what you see on the first screen needs to be calculated by the server and then returned. In order to reduce the waiting time for the white screen, you can use transfer-Encoding: chunked of Http to send the server back to a static Loading page first, and then start the server rendering.
If the Http header is already output, you can’t Redirect the Redirect, so you have to continue to output JS to get the browser to Redirect, for example:
if (err.code === "301" || err.code === "302") {
if (res.headersSent) {
res.write('<span> </span></body> <script>window.location.href="${err.detail}"</script>
</html>`);
res.end();
} else {
res.redirect(parseInt(err.code, 10), err.detail); }}Copy the code
Afterword.
The above list of personal feel more important points, there are many other practical skills can directly see the Demo, there are notes, there are questions welcome to discuss together.