Let’s start with a picture:

This is an e-commerce website, for this kind of e-commerce website, we generally want the effect is:

1, users from Baidu or some Search engines directly Search keywords, can be directed to my website, this is SEO(Search Engine Optimization). About SEO Baidu encyclopedia explanation is:

SEO (Search Engine Optimization) : Chinese translation for Search Engine Optimization. Is a way to use search engine rules to improve the site’s natural ranking within the relevant search engines. The goal is to take a leading position in the industry and reap brand benefits. It is largely a business practice by web site operators to move themselves or their companies up the rankings. 2, the speed of opening the home page is fast enough, here fast enough, which means that the data on the page has been obtained from the server when opening the home page, rather than asynchronously requesting the interface.

The above two requirements are the two main problems solved by SSR(Server Slider Rendering). And about SSR more content, there are already too many resources on the network, this article will not repeat. This article mainly introduces the React technology stack implementation of SSR architecture solution – NextJS, how to build our website from 0. For more documentation on NextJS, see www.nextjs.cn/

Project background

This project mainly realizes an e-commerce project, mainly including modules as shown in the figure below:General users can browse commodities and recommended merchants on the home page (see the picture at the beginning), or click merchants or commodities to enter the details. However, when placing an order, it is necessary to check whether the user has logged in. If not, it is necessary to log in before placing an order.

The code structure

1. Directory structure

Go straight to the screenshot:

2. Brief explanation

A brief description of a few important folders and files:

API -- -- -- -- -- -- -- -- -- -- -- -- -- the backend interface related folder build -- -- -- -- -- -- -- -- -- -- - compiled resources folder components -- -- -- -- -- - out of the common components of clip pages -- -- -- -- -- -- -- -- -- -- - front page folder, According to the Pages file structure, Static ---------- Front-end image resource directory. Babelrc -------- ANTD Load configuration files on demand next. Config. js -- Nextjs project configuration files (important configuration files in the project)Copy the code

3, Pages file description

For the Next project, there are two implementation routes:

  • 1. Page routing uses file system routing. This is the default and I think the easiest way to do it. That is to treat the pages folder structure as a routing configuration, and that is what my project is based on. What does that mean? Index.js is the default address of the project, corresponding to “/”. If there is an abou.js, the corresponding address is: ‘/about’, if you want to group some pages under the pages file, such as a user, login.js, then login address: ‘user/login’, and so on. Anyway, to add a page, just add a JS file under Pages or a folder under Pages, simple and crude.
  • 2, in addition to the page routing, but also provides the so-called API routing, this road is equivalent to write interface maock data test, according to their own situation to choose (I think may not be generally used) specific can see the official website.

Main configuration

1. Start with the package.json file

As the most important and basic configuration file of the project, I first release the configuration. The comment after the configuration item is to explain the configuration. There is no comment in the actual operation:

{" name ":" web ", "version" : "0.1.0 from", "private" : true, "scripts" : {" dev ": "Build ": "next build",// project build package "start": "Next start", // start the service after deploying the project. APP_ENV='dev' next dev -p 8090 ', // start:test: "APP_ENV='test' next start -p 8091",// test environment start service command (if any), port 8091" start:prod": "APP_ENV='prod' next start -p 8092"}, "dependencies": {"@ant-design/ ICONS ": "^ 4.1.0", "@ zeit/next - CSS" : "^" 1.0.1, "@ zeit/next - less" : "^" 1.0.1, "add" : "^ 2.0.6", "antd" : "^ holdings", "axios" : "^ 0.19.2 Babel - plugin -", "import" : "^ 1.13.0", "less" : "^ 3.11.2", "less - vars - to - js" : "^ 1.3.0", "lodash" : "^ 4.17.15", "next" : "9.4.0", / / the latest version seems to be the next version 9.5 has officially released "next - antd - aza - less" : "^ 1.0.2", "next - cookies" : "^ 2.0.3 next --", "size" : "^ 2.1.0", "path" : "^ 0.12.7", "react" : "16.13.1," "the react - dom" : "16.13.1," "webpack - bundle - analyzer" : "^ 3.7.0", "webpack - filter - warnings - plugin" : "^ 1.2.1", "yarn" : "^ 1.22.4"}, "devDependencies" : {" next - compose - plugins ":" ^ 2.2.0 "}}Copy the code

The rest need not be said, but for the above configuration file scripts configuration, to explain: Because the SSR project is actually equivalent to the project has a node server, it is said that after the compiled files of our project are thrown to the official server, the project needs to start the node server to run, and the command to start the node server here is the start command, if the port is specified, The default port is used. In our local development, the server is actually started with the dev command, which is different from the typical front-end project where the front and back ends are separated. To put it simply:

Yarn start:dev 2, after the project is completed: 1, compile the resource file: yarn build 2, start the node service: To deploy to a test environment, run the following command: yarn start:test Deploy to a build environment yarn start:prodCopy the code

In addition to the default operation of throwing resource files directly into the service container and starting the Node service, you can also do some hardening or other operations (such as pM2 daemon, etc.). Here I did not go into in-depth research, so it is not easy to expand (mainly lazy……). . Of course, for projects that are generally not very large, the default operation will be sufficient.

2. Next. Config. js

const withLess = require("@zeit/next-less"); const withCss = require("@zeit/next-css"); const withPlugins = require("next-compose-plugins"); const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js"); const path = require('path'); const fs = require('fs'); const lessToJS = require('less-vars-to-js'); const FilterWarningsPlugin = require('webpack-filter-warnings-plugin'); module.exports = withPlugins([withLess,withCss], { distDir: 'build', publicRuntimeConfig: {APP_ENV: Process.env.app_env}, // if antD is used, antD-mobile does not need javascriptEnabled. true, modifyVars: ReadFileSync (path.resolve(__dirname, './util/antd.less'), 'utf8'))}, //3, enable CSS modularized cssModules: true, cssLoaderOptions: {camelCase: true, localIdentName: "[local]___[hash:base64:5]", getLocalIdent: (context, localIdentName, localName, options) => { let hz = context.resourcePath.replace(context.rootContext, ""); if (/node_modules/.test(hz)) { return localName; } else { return cssLoaderGetLocalIdent( context, localIdentName, localName, options ); } } }, webpack(config,{isServer}){ // Fixes npm packages that depend on `fs` module if (! isServer) { config.node = { fs: If (config.externals){const includes = [/antd/]; if(config.externals){const includes = [/antd/]; config.externals = config.externals.map(external => { if (typeof external ! == 'function') return external; return (ctx, req, cb) => { return includes.find(include => req.startsWith('.') ? include.test(path.resolve(ctx, req)) : include.test(req) ) ? cb() : external(ctx, req, cb); }; }); } config.plugins.push( new FilterWarningsPlugin({ exclude: /mini-css-extract-plugin[^]*Conflicting order between:/ }) ); iCopy the code

For this configuration file, it can be said that it is the most important configuration file of the Next project. Because my project integrates ANTD, the key points have been annotated and will not be described again. For details about the configuration of next.config.js, you can check it on the official website: www.nextjs.cn/docs/api-re…

Project code implementation problem analysis

1. How can SSR be realized?

For that, let’s start with a picture:In fact, for the next project page, if you look at the source code of the web page, then you will see with the general no SSR front-end page is different, the page structure code and data are assembled together (the circled part of the figure is the data), such a page, can SEO when there is a better weight to give. That said, the react project relies on a very important lifecycle hook function in the Next framework to do this emotionally, but intellectually:getInitialProps

There’s a lot of documentation on this lifecycle function, but it really boils down to this:

You implement SSR by making a data request from componentDidMount and making it from getInitialProps.

If you want to add another sentence,

This getInitialProps can only exist in the parent page under Pages (files in the file system route), not in the child component.

Here’s an example:

import React from 'react'; import { getStoreBannerApi, getStoreGoodsListApi, getStoreInfoApi } from '.. /api/Api'; import HtmlHead from '.. /components/HtmlHead'; import MyCarousel from '.. /components/MyCarousel'; import SearchArea from '.. /components/SearchArea'; import StoreGoods from '.. /components/StoreDetailPage/StoreGoods'; import StoreLogo from '.. /components/StoreDetailPage/StoreLogo'; /** * merchant details page (this must be a file under the Pages folder) */ const StoreDetail = ({goodsList, // These properties of the component are storeInfo extracted from the object returned by getInitialProps, Clearly}) => {return (<> <HtmlHead title={' merchant details '}/> <SearchArea/> {/*<div className={' bw '} > <BreadcrumbNav showTotal={false}/> </div>*/} <StoreLogo data={storeInfo}/> { banners.length ? <MyCarousel imgHeight={500} swiperList={banners} /> : Null} <div className={' bw '}> {/*StoreGoods is a subcomponent that has no getInitialProps lifecycle */} <StoreGoods list={goodsList}/> </div> </>);  }; // 1. Call getInitialProps way is such StoreDetail getInitialProps = async (props) = > {try {/ / 2, coming from routing parameters acquisition const = {storeId} props.query; Const res = await getStoreGoodsListApi({page: 0, size: 9999, storeId: storeId}). Catch (e => ({})); const res1 = await getStoreInfoApi(storeId).catch(e => ({})); const banners = await getStoreBannerApi(storeId).catch(e => ({})); // each getInitialProps must return a key-value object, which is injected directly into the component props. Return {goodsList: res.code === 20000? res.data : [], storeInfo: res1.code === 20000 ? res1.data : [], storeId: storeId, banners: banners.code === 20000 ? banners.data : [], }; } catch (e) { } }; export default StoreDetail;Copy the code

The function component calls the getInitialProps hook function differently than the class component. The class component calls the getInitialProps hook function.

import React from 'react'

class Page extends React.Component {
 static async getInitialProps(ctx) {
   const res = await fetch('https://api.github.com/repos/vercel/next.js')
   const json = await res.json()
   return { stars: json.stargazers_count }
 }

 render() {
   return <div>Next stars: {this.props.stars}</div>
 }
}

export default Page
Copy the code

2. Questions about interface requests?

In THE SSR project, it is important to make it clear that a request for data can occur on the browser side as well as the server side (node is actually a middle tier, so we still need data from a real back-end server, such as Java). On the browser side we don’t need to say much, just ajax, but on the Node side there is a problem. There are no Ajax objects, so we need to use a specification that can be used on both the browser and the Node side to send requests. The one I recommend is Axios. The axiOS instance wrapper code used in this project is as follows:

import { message } from 'antd'; import axios from 'axios'; import Router from 'next/router'; import { clearLoginStorage, getToken } from './saveLogin'; //1. Export let serverAuthorization = object.create (null); Const request = axios. Create ({baseURL: 'http://139.9.113.127:8080', the timeout: 30000,}); / / the interceptor request. Interceptors. Response. Use ((response) = > {. / / the console log (response) let resObj = {}; if (response.data && response.data.code === 20000) { /*if (process.browser) { resObj = { code: Response.data. code, data: response.data.data}} else {// server, return data directly resObj = {... response.data.data} }*/ resObj = { code: response.data.code, data: response.data.data }; } else { resObj = { code: response.data.code, message: response.data.message, data: null }; /*if (process.browser) { resObj = { code: response.data.code, message: response.data.message, data: null } }*/ } return resObj; }, (error) => { const res = error.response || {}; console.log(res.status); If (process.browser && res.status === 401) {// Only required for client rendering, login expired, jump to home // console.log('---->'); clearLoginStorage(); Message. error(' Login expired, please log in again '); Router.replace('/user/login'); } return Promise.reject(error); }); request.interceptors.request.use((config) => { //const token = process.browser //(getToken() || {}) : serverAuthorization; // console.log(config.url); let auth = ''; / / 2, distinguish the request from the browser node requests from the client if (process. Browser) {auth = getToken () [' access_token '] | | '. } else {// add token from request auth = ''; //request['access_token'] || ''; } return { ... config, headers: { Authorization: `${auth ? 'Bearer ' + auth : ''}`, Accept: 'application/json', 'Content-Type': 'application/json; charset=utf-8', }, //data: config.param ? JSON.stringify(config.param) : '' }; }, (error) => { return Promise.reject(error); }); export default request;Copy the code

The above code is relatively simple, so I won’t go into details.

3. Token synchronization problem?

This question is somewhat similar to point 2. The getInitialProps lifecycle function is used to execute both server-side and non-server-side renderings, so we don’t have to focus on when SSR or non-SSR is used. However, this causes a problem, because our token is cached in the browser (the project is stored in the session), so when the request is made by the Node side, I cannot get the token cached by the browser. If the request is made by the Node side, and the token is needed, What to do? In this project, a relatively simple and crude way is adopted. In the _app.js file in the Pages folder, this is equivalent to the entry configuration file for the global project:

import { ConfigProvider } from "antd"; // import zhCN from 'antd/es/locale/zh_CN'; import zhCN from 'antd/lib/locale/zh_CN'; import App from 'next/app'; import getCofnig from 'next/config'; import Router from 'next/router'; import React from 'react'; import LayoutBasic from '.. /components/Layout/BasicLayout'; import UserLayout from ".. /components/Layout/UserLayout"; import '.. /static/styles/base.less'; import request from ".. /util/request"; import { getToken } from ".. /util/saveLogin"; Router.events.on('routeChangeComplete', () => { if (process.env.NODE_ENV ! == 'production' && document) { const els = document.querySelectorAll('link[href*="/_next/static/css/styles.chunk.css"]'); const timestamp = new Date().valueOf(); els[0].href = '/_next/static/css/styles.chunk.css? v=' + timestamp; } }) const {serverRuntimeConfig, publicRuntimeConfig} = getCofnig() // console.log(serverRuntimeConfig, PublicRuntimeConfig class NextApp extends App {// static async getInitialProps({Component = {}, ctx}) { const token = getToken(ctx) || {}; Request ['access_token'] = token.access_token; let pageProps; if (Component.getInitialProps) { pageProps = await Component.getInitialProps(ctx); } return { pageProps, }; } render() { const {Component, pageProps, store, router = {}} = this.props; const {pathname} = router; let LayOut = ( <LayoutBasic> <Component {... PageProps} router={router}/> </LayoutBasic>) if (pathname.indexof ('/user/') === 0) {// UserLayout = (<UserLayout>) <Component {... pageProps} router={router}/> </UserLayout> ) } return ( <ConfigProvider locale={zhCN}> {LayOut} </ConfigProvider> ) } } export default NextApp;Copy the code

The key is to look at this code:

Request ['access_token'] = token.access_token;Copy the code

Since this file is a global entry file, as mentioned earlier, the getInitialProps lifecycle function can be executed on both the server side and the browser side, so WHEN I execute on the browser side, I take the token cached by the browser and put it in the Access_token of the instance object of the AXIos request. Since node requests also use this instance, it is possible to retrieve the token from it and put it into the parameters of the node request. In this way, the token is synchronized between the browser and node.

conclusion

To sum up:

  • SSR is good for page loading (especially home page loading) and SEO optimization
  • Nextjs is a well-known framework for implementing SSR in react technology stack, and it is relatively easy to get started. Basically, just follow the documentation
  • The key to implementing SSR in NextJS is the use of the getInitialProps lifecycle function, and nothing else is different from writing general front-end applications

Of course, the above is actually just to sort out some key points of nextJS project practice, so that the project can run, can start. In fact, as for SSR, it may seem that there are not many things, but in practice, there are many details to be considered, such as adding express or KOA packaging intermediate layer, such as token synchronization. I feel that what is described in the article is not a good way. Maybe it will be better to do some token caching or verification on the node side, etc. Maybe we will do more research in the future, and also hope to have more advice from the leaders. The document is a little tired, if you have inspiration, ask to encourage three even 🙏🙏🙏

Project source: github.com/phonet/next…