What is isomorphism

Isomorphism refers to developing a program that can run on different platforms. For example, developing a piece of JS code can be used by both a Node.js-based Web server and a browser. In this article we will talk about why and how to develop an isomorphic Web application in this scenario.

Second, the benefits of isomorphism

We don’t make any decisions for nothing, and we use isomorphism because there are certain advantages to isomorphism:

  • Reduce the amount of code development and increase the amount of code duplication. Because one piece of code can run in both the browser and the server, not only is the amount of code reduced, but much of the business logic does not need to be maintained on both sides of the browser and the server, thus reducing the potential for errors.
  • SSR (Server-side Render) function can be completed at a small cost. SSR offers at least two benefits.
    • First screen performance, allowing users to see page content earlier.
    • SEO (Search Engine Optimization), friendly to crawlers.

Third, isomorphism brings problems

  • Performance loss. Both client and server have to render pages, resulting in a certain amount of performance waste (it can be optimized by means of client DOM reverse collection and virtual-DOM, but it is unavoidable).
  • A module that can be isomorphic must be compatible with both the client and Node.js environment, thus incurs some additional development costs. Especially if you’re used to client development, remember that Windows, documents, DOM, and so on are objects that only exist on the client side.
  • The risk of running out of memory on the server side. The client code environment is rebuilt with the browser refresh, so you don’t need to pay much attention to running out of memory, unlike the server side.
  • Pay special attention to asynchronous operations. Those of you who are used to client development may be used to making asynchronous data requests and operations on the front end at will, because all operations cause page redrawing. On the server side, the component can only call Render once or a limited number of times, so all asynchronous requests for server rendering must be completed before render returns HTML.
  • All prefetch states on the server should have a way for the client to obtain, so that the client and the server do not render different results resulting in flash screen. Because the client renders the page once anyway, if the server uses different data for rendering than the client, the DOM rendered will be different, resulting in a splash screen.

Four, which parts of the application can be isomorphic

  1. Single-page application routes can be isomorphic. In this way, any sub-page of a single-page application can enjoy the benefits brought by SSR.
  2. Templates, with one rendering engine for both the front and back ends, can be used to create templates for both the front and back ends, so the days of developing two sets of templates for the same data for both the front and back ends are over.
  3. Data request, development support isomorphism httpClient, so the back end of the request data code can also be isomorphism. Note that there are no cookies on the server, so session-related request code needs to be extremely careful.
  4. Other platform-independent code, such as react and Vue’s global state management module, data processing, and some platform-independent pure functions.

5. What cannot be isomorphic

  • Platform-related codes, for example, can only perform DOM and BOM operations on the browser side and file read and write and database operations on the server side.

Six, we need isomorphism or not

6.1 Can the benefits of isomorphism be obtained in other ways?

  • SSR

    SSR certainly doesn’t have to be isomorphism, but using isomorphism to implement SSR can reduce a lot of repetitive code development.

  • Reduce the likelihood of errors because the front and back ends use two pieces of code to maintain one piece of logic simultaneously

    I can’t think of a better solution to this problem than isomorphism.

When SSR is necessary, I feel isomorphism is necessary.

6.2 Isomorphism is supported, but SSR is not abused

Therefore, I think a good solution is to develop an application that supports isomorphism, but does not force the use of SSR, because SSR brings a certain performance waste.

  • Isomorphism is supported. A piece of code can run either on the client side or on the server side, but whether to run the code on the server side depends on the specific business.
  • Using SSR only for high first screen performance and SEO needs, and purely client-side rendering for other situations seems like a good compromise.

Write a multi-page application that supports isomorphism from scratch

Example code repository address. Read the following paragraphs for a better experience while launching the example. Express is used as the Web Server framework in the examples, so it will be easier for readers to understand the examples if they have some foundation in Express.

7.1 Responsibilities of the front-end and back-end code

  • Front end: [(single page application) handles routing ->] request data -> Render -> bind events
  • Back end: [processing routing ->] Request data -> Render

7.2 Differences between the functions of front-end and back-end codes

  • Front end: With no output, the code works directly on page elements
  • Back end: Output HTML string

7.3 Determining the Code Execution Environment

The simplest way to determine the current code execution environment is by the presence or absence of the Window object, which exists only in the browser execution environment

const isBrowser = typeof window ! == 'undefined';Copy the code

7.4 Basic Design of Isomorphic Applications

7.5 Isomorphic component base classes

7.5.1 Life Cycle Planning

A homogeneous component whose lifecycle is executed differently on the server and client sides. The life cycle before the mount operation can run on the server side.

  • BeforeMount -> render -> Mounted
  • Server: preFetch -> beforeMount -> render

BeforeMount and Render lifecyms are executed on both the server and client side, so platform-specific code should not be written during these lifecyms.

Here are the isomorphic component base classes used in this demo:

// ./lib/Component.js const {isBrowser} = require('.. /utils'); module.exports = class Component { constructor (props = {}, options) { this.props = props; this.options = options; this.beforeMount(); If (isBrowser) {/ / the browser to perform the life cycle of this. The options. The mount. The innerHTML = this. Render (); this.mounted(); this.bind(); // Life cycle async preFetch() {} // Life cycle beforeMount() {} // Life cycle mounted() {} // Use bind() {} // Call setState() when rerendering  { this.options.mount.innerHTML = this.render(); this.bind(); } render() { return ''; }};Copy the code

All business components inherit from this base class, such as an actual business component as follows:

// ./pages/index.js const Component = require('.. /lib/Component'); Module. Exports = class Index extends Component {render() {return '</h1> <a href="/list"> }}Copy the code

After launching the example, you can visit http://localhost:3000/ to access this page and see what SSR looks like.

7.6 Server Processing

7.6.1 Use ServerRenderer to render components

A simple ServerRenderer implementation is as follows:

// ./lib/ServerRenderer.js const path = require('path'); const fs = require('fs'); Module.exports = async (mod) => {// Get Component const Component = require(path.resolve(__dirname, '.. /', mod)); Const template = fs.readfilesync (path.resolve(__dirname, '.. /index.html'), 'utf8'); // Initialize business Component const com = new Component() // data preFetch await com.prefetch (); Return template.replace('<! -- ssr -->', Com.render () + // put the data retrieved from the back end into a global variable for the front-end to initialize '<script> windot.__initial_props__ = '+ json.stringify (com.props) + '< / script >') / / replace insert static resource tag. The replace (' ${modName} 'mod); }Copy the code
7.6.2 Page Templates

All the pages in this demo use the same HTML template:

<! -- ./index.html --> <html> <head> <title>test</title> </head> <body> <div id="app"> <! Below are placeholders for filling content after SSR rendering --> <! -- ssr --> </div> <! - insert the front end of the actual business js code - > < script SRC = "http://localhost:9000/build/${modName}" > < / script > < / body > < / HTML >Copy the code
7.6.3 Server Routing and Controller

This demo was developed based on the Express framework. The following code uses the ServerRenderer to render homogeneous components and then outputs the page HTML to the browser.

// ./routes/index.js var express = require('express'); var router = express.Router(); var ServerRenderer = require('.. /lib/ServerRenderer'); router.get('/', function(req, res, next) { ServerRenderer('pages/index.js').then((html) => { res.send(html); }); });Copy the code

7.7 Client Processor ClientLoader

The ClientLoader in demo is a Webpack loader with the following code:

//./lib/ clientloader.js module.exports = function(source) {return '${source}' // module.exports; __initial_props__ = window.__initial_props__ = window.__initial_props__ = window.__initial_props__ new Com(window.__initial_props__, { mount: document.querySelector('#app') }); `; };Copy the code

Use this plug-in in webpack.config.js (only for page entry components)

// webpack.config.js module.exports = { ... , module: { rules: [ ..., { test: /pages\/.+\.js$/, use: [ {loader: Path. The resolve (__dirname, '. / lib/ClientLoader. Js')}}]],}};Copy the code

7.8 A Service Component

With this foundation, we can easily write a component that supports isomorphism. Below is a list page.

7.8.1 code
// ./routes/index.js /* GET list page. */ router.get('/list', function(req, res, next) { ServerRenderer('pages/list.js').then((html) => { res.send(html); }); }); // ./pages/list.js const Component = require('.. /lib/Component'); const { getList, addToList } = require('.. /api/list.api'); module.exports = class Index extends Component { constructor (props, options) { super(props, options); } async preFetch() {await this.getList(); } async getList() { const list = (await getList()).data; this.props.list = list; }... . Render () {return '<h1> <button class="add-btn">add</button> <button class="save-btn">save</button> <ul> ${render() {return' <h1> <button class="add-btn">add</button> </button> this.props.list.length ? This. Props. List. The map ((val, index) = > ` < li > ${val. The name} < button class = "del - BTN" > delete < / button > < / li > `). Join (") : 'List is empty'} </ul> '; }}Copy the code
7.8.2 Rendering Results on the Server

If already to start the demo server, visit http://localhost:3000/list can see server-side rendering results are as follows. The presence of windod. __initial_props__ ensures that the results of the front and back renderings are consistent.

< HTML > <head> <title>test</title> </head> <body> <div id="app"> <h1> <button class=" add-bTN ">add</button> <button class="save-btn">save</button> <ul> <! <li> 1 <button class="del-btn"> Delete </button> </li> 2 <button class="del-btn"> delete </button> </li> < li > 3 < button class = "del - BTN" > delete < / button > < / li > < li > 4 < button class = "del - BTN" > delete < / button > < / li > < / ul > < script > / / // The first client render will have the same result as the server render, so the user will not see the client render. window.__initial_props__ = { "list": [{ "name": 1 }, { "name": 2 }, { "name": 3 }, { "name": 4 }] } </script> </div> <script src="http://localhost:9000/build/pages/list.js"></script> </body> </html>Copy the code

A single page isomorphic application.

Compared to multi-page applications, single-page applications require multiple isomorphic portions of front-end routing.

8.1 Server Processing

Initialize components with routing information:

// ./lib/ServerRenderer.js module.exports = async (mod, url) => { ... // Initialize the business Component const com = new Component({url}); . }Copy the code

When writing controller, enter route to ServerRenderer:

/* GET single page. */
router.get('/single/:type', function(req, res, next) {
  ServerRender('pages/single.js', req.url).then((html) => {
    res.send(html); 
  });
});
Copy the code

8.2 Client code

Here is a single page application component. Click the Toggle button to switch routes and change the view:

// ./pages/single.js const Component = require('.. /lib/Component'); module.exports = class Index extends Component { switchUrl() { const isYou = this.props.url === '/single/you'; const newUrl = `/single/${isYou ? 'me' : 'you'}`; this.props.url = newUrl; window.history.pushState({}, 'hahha', newUrl); this.setState(); } bind() { this.options.mount.getElementsByClassName('switch-btn')[0].onclick = this.switchUrl.bind(this); } render() { ; Return '<h1>${this.props. Url}</h1> <button class=" switched-btn "> toggle </button>'; }}Copy the code

Accessing the /single/you server returns:

<html> <head> <title>test</title> </head> <body> <div id="app"> <h1>/single/you</h1> <button Class ="switch-btn"> switch </button> <script> window.__initial_props__ = {"url": "/single/you" } </script> </div> <script src="http://localhost:9000/build/pages/single.js"></script> </body> </html>Copy the code

Isomorphism of public state management

The isomorphism of the common state management is very similar to the isomorphism of the component props. Both of them need to render the entire state tree after the back-end prefetch data to the page, and then use this tree as the initial state when the front-end initialises the state manager Store, so as to ensure that the front-end rendering results are consistent with the back-end.

Special HttpClient

The httpClient used in the demo above is AXIos, a library that already supports isomorphism. But there is still a problem, and this problem has been mentioned before. When it comes to session-related requests, browsers generally send requests with cookie information, but server initiated requests do not. Therefore, when a server initiates a request, it needs to manually add cookies to the request header.





Creative Commons Attribution – Non-commercial Use – Same way Share 4.0 International License