Next. Js server rendering framework based on react. js stack

Published an article on nuggets for the first time. With a learning attitude, I summarized the restoration of my project using Next. Js to develop server-side rendering to consolidate knowledge points and discuss technologies with peers. (The article is being improved…)

1. Project background

The original project of the company was developed based on PHP and jQuery, and proposed reconstruction requirements. However, the backend technology stack has changed from PHP to Java microservices, and the front-end technology stack has changed from jQuery to react.js. Since the company is in an industry that needs to do online promotion, the project reconstruction must consider SEO optimization friendly. I usually use the React. Js technology stack to do front-end development, so I found the next.js (based on React) server rendering framework.

2. Basic needs

  • Search engine included: SEO friendly, conducive to search caused by crawling page information
  • Route beautification: Routes should be displayed according to rules
  • Display different data by city: Display different city data by city (IP location)
  • PC /M: Render component templates on different clients based on devices
  • SEO information configurable: Each first screen sub-page (such as home page, About us, company profile page) supports SEO information configurable
  • Support for development/formal environment switching: Determine the current environment based on the command line and configure the API prefix
  • Wechat authorization file deployment: The wechat payment authorization file *. TXT is deployed in the root directory of the project
  • Partial Http request proxy processing: Partial interface cross-domain processing (HTTP-proxy-Middleware)
  • Similar to redirection: Access the current domain name, pull the page data under different domain names, and display the page data under the current route
  • Mock. Js/json-server/faker.js [API document management tool can use yAPI]
  • Deployment mode of the project: there are two deployment modes of the project, Docker deployment and Node deployment (Docker debugging will also be used locally).

3. Next. Js principle

Next. Js is a lightweight React server rendering framework. Server-side rendering is one of the most traditional forms of PHP nested static HTML. PHP uses the template engine to render data from the database into HTML. When the front end accesses the specified route, PHP sends the page specified by the front end. What this page recognizes on the browser side is a content-type :text/ HTML file, which is a static HTML file format that the browser parses and renders the page. When the browser looks at the source code, it is a rich HTML tag and the text information in the tag, such as SEO information, Article title/content etc. Such pages can be easily captured by search engines. In the React component, the server dynamic data obtained by the getInitialProps method is embedded. On the server side, the React component is rendered into AN HTML page and sent to the foreground.

4. Next. Js key points

File system:

The Next file system specifies that each *.js file in the Pages folder will become a route, automatically processed and rendered

Create./pages/index.js in your project. After the project runs, you can access the page through localhost:3000/index. Similarly./pages/second.js can be accessed via localhost:3000/second

Static file service:

Such as images, fonts, JS tool classes

Creating a new folder in the root directory is called static. Do not define the name of the static folder. Just call it static, because only that name will count as a static resource

export default () => <img src="/static/my-image.png" alt="my image" />
Copy the code

Data acquisition:

This is where the key to next-js server-side rendering comes in. The getInitialProps function provides lifecycle hooks to get data

Create a React component with stateful, life-cycle, or initial data

import React from 'react'

export default class extends React.Component {
  static async getInitialProps({ req }) {
    const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
    return{userAgent} // bind userAgent data to the Props, so that the component can access this.props. UserAgent}render() {const {userAgent} = this.props // ES6return(< div > Hello World {userAgent} < / div >)}} = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / / define getInitialProps stateless components * Const Page = ({stars}) => <div> Next stars: {stars} </div> Page.getInitialProps = async ({ req }) => { const res = await fetch('https://api.github.com/repos/zeit/next.js')
  const json = await res.json()
  return { stars: json.stargazers_count }
}

export default Page
Copy the code

The code above gets the data via the asynchronous method getInitialProps, bound to the props. When the service renders, getInitialProps will serialize the data, just like json.stringify. When the page is initially loaded, getInitialProps is only loaded on the server. The client only executes getInitialProps when a route jump (Link component jump or API method jump) occurs

Important: getInitialProps will not be used in child components. Only subcomponents in the Pages page can obtain data from pages in the Pages folder, and then pass values to the subcomponents

The properties of the getInitialProps input object are as follows:

  • Pathname – The path part of the URL
  • Query-the Query part of the URL, which is parsed into objects
  • AsPath – The actual path (including the query part) displayed in the browser is of type String
  • Req-http request object (server-side only)
  • Res-http return object (server side only)
  • JsonPageRes – Get the data response object (only available on the client)
  • Err – Any errors in the rendering process

Use components to switch client routes

If you need to inject PathName, Query, or asPath into your component, you can use the withRouter higher-order component

// pages/index.js
import Link from 'next/link'

export default () =>
  <div>
    Click{' '}
    <Link href="/about">
      <a>here</a>
    </Link>{' '}
    to readMore </div> // import {withRouter} from'next/router'

const ActiveLink = ({ children, router, href }) => {
  const style = {
    marginRight: 10,
    color: router.pathname === href? 'red' : 'black'
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default withRouter(ActiveLink)
Copy the code

5. Project Catalog

From this step, it is the process of actually creating the project and writing the code. Since it is a company project, all the simulated data are used here, but the project requirements mentioned above will be implemented from scratch.

The installation

1. Create the SSR directory. Run CNPM install --save next react react-dom in the SSR directory. After the command is executed, the node_module folder and package.json file are displayed in the directory."dependencies": {
    "next": "^ 8.1.0"."react": "^ 16.8.6"."react-dom": "^ 16.8.6"}} 3. Add the script to package.json file. Here we can customize the NPM script command {"scripts": {
    "dev": "next"."build": "next build"."start": "next start"
  },
  "dependencies": {
    "next": "^ 8.1.0"."react": "^ 16.8.6"."react-dom": "^ 16.8.6"}} 4. A new folder in the SSR directory pages | static | components... , and then create a file named index.js under Pages. The file content is as followsexportdefault () => <div>Welcome to next.js! </div> The final new directory structure is as follows: ssr-node_modules-package. json -components - static-imgs-logo.png -fonts -example.ttf -utils -index.js -pages -index.js -about.js -.gitignore -README.md -next.config.js -server.js 5. Run the NPM run dev command and open http://localhost:3000. Before running the NPM run start command, run the NPM run build command. Otherwise, an error message will be displayedCopy the code

This is easier to understand than a SPA single-page application, which has a single root container for mounting components. You won’t see any other rich HTML code inside the container

6. Project requirements realization

The project is to facilitate SEO server-side rendering. Speaking of SEO, you need to set the head information in the HTML document. There are three critical information, kywords | description | title respectively the current web page keywords, descriptions, page title. The search engine will crawl the key information of the web page according to the content in these tags, and then the user will make the search results page display according to the matching degree of these keywords when searching. (Of course, the presentation algorithm is much more than referring to this information, the semantics of the page tag, keyword density, external links, internal links, visits, user stay time…)

<! DOCTYPE html> <html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="keywords" content="Winyh | Next.js | React.js | Node.js | ...">
    <meta name="description" content="This is a page associated with the next. Js server."</title> </head> <body> </body> </ HTML >Copy the code

Requirement 1: SEO information can be configured

This implementation, the search engine search record is also a simple implementation. To achieve search engine friendly in fact, there are many aspects of the above can be optimized.

  • Set up a built-in component to load into the page and name the file headseo.js
/ / components/Common/HeadSeo. Js code in the file below import Head the from'next/head'

export default () =>
    <Head>
        <meta charSet="UTF-8"React JSX <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
        <meta name="keywords" content="Winyh | Next.js | React.js | Node.js | ...">
        <meta name="description" content="This is a page associated with the next. Js server."</title> </Head> // pages/index.js import Layout from </title> </Head".. /components/Layouts/PcLayout"

exportdefault () => <Layout> <div>Welcome to next.js! </div> </Layout> SSR - node_modules-package. json -components -Common // public component - headseo.js -Layouts // Pclayout.js - mlayout.js -static // Static resources - imgs-logo.png - fonts-example.ttf-utils-index.js -pages -index.js -about.js -static // Static resources - imgs-logo.png - fonts-example.ttf-utils-index.js -pages -index.js -about.js -.gitignore -readme.md -next-config. js // Configuration file -server.js // Server side scriptCopy the code

Open localhost:3000 and see that the relevant head seo information has been rendered. If you need to dynamically render data on the server side, you can request background data in the files under the Pages directory and render it to the HeadSeo file by means of Props pass values. Here, the method is described temporarily and the actual code is written later.

Requirement 2: Route beautification

Customize server routes to beautify routes. For example, in Wuhan (Wuhan) site, the need to access the home page route is like this

city Home page About us
wuhan /wuhan/index /wuhan/about
Shanghai /shanghai/index /shanghai/about
nanjing /nanjing/index /nanjing/about

Create server script file server.js, server using Express as server

CNPM I Express HTTP-proxy-middleware --saveCopy the code
const express = require('express')
const next = require('next') const server = express() const port = parseInt(process.env.PORT, 10) | | 3000 / / set the listener port const dev = process. The env. NODE_ENV! = ='production'// Const app = next({dev}) const handle = app.getrequesthandler () app.prepare().then(() => {server.get())'/:city', (req, res) => {
        const actualPage = '/index'; const queryParams = { city: req.params.city}; Console. log(req.params) app.render(req, res, actualPage, queryParams); }); server.get('/:city/index', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/about', (req, res) => {
        const actualPage = '/about';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/posts/:id', (req, res) => {
      return app.render(req, res, '/posts', { id: req.params.id })
    })

    server.get(The '*', (req, res) => {
      return handle(req, res)
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)})})Copy the code

Modify package. Json file script is as follows: then run the command NPM run ssrdev open port 3000, so far can beautify the routing access to the page after the localhost: 3000 / wuhan/index localhost:3000/wuhan/about

{
  "scripts": {
    "dev": "next"."build": "next build"."start": "next start"."ssrdev": "node server.js"// You can use Nodemon to replace Node, so that you do not need to re-run the script after modifying the server.js file"ssrstart": "npm run build && NODE_ENV=production node server.js"// Run NPM run build first"export": "npm run build && next export"
  },
  "dependencies": {
    "express": "^ 4.17.0"."http-proxy-middleware": "^ 0.19.1." "."next": "^ 8.1.0"."react": "^ 16.8.6"."react-dom": "^ 16.8.6"}}Copy the code

Requirement 3: Display different data according to different cities

Display the home page of the corresponding city site according to the geographical location of the user to obtain the data of different cities. This is where data emulation and server-side data acquisition begin. This project will try two methods of data simulation

  • json-server

  • Mock.js (this is easier, add later)

First, install the open source JSON-server. For details, see Github

cnpm install -g json-server
Copy the code

Create a mock file in the SSR directory, and then create data.json under mock

{
    "index": {"city":"wuhan"."id": 1,"theme":"Default site"
    },
    "posts": [{"id": 1, "title": "json-server"."author": "typicode"}]."comments": [{"id": 1, "body": "some comment"."postId": 1}]."profile": { "name": "typicode" },
    "seo": {"title":"Next. Js server Rendering Framework based on React.js Stack"."keywords":"Winyh, Next.js, React.js, Node.js"."description":"Next. Js server render data request simulation page test"}}Copy the code

Create a new routing rule file routes.json in the current directory to add a/API/prefix to the simulation API. The file types are as follows

{
    "/api/*": "/The $1"
}
Copy the code

Modify package.json file to add data simulation command line script

{
  "scripts": {
    "dev": "next"."build": "next build"."start": "next start"."ssrdev": "nodemon server.js"."ssrstart": "npm run build && NODE_ENV=production nodemon server.js"."export": "npm run build && next export",
  + "mock": "cd ./mock && json-server --watch data.json --routes routes.json --port 4000"
  },
  "dependencies": {
    "express": "^ 4.17.0"."http-proxy-middleware": "^ 0.19.1." "."next": "^ 8.1.0"."react": "^ 16.8.6"."react-dom": "^ 16.8.6"}}Copy the code

To access the data, run the command NPM run mock to start the mock data server

localhost:4000/api/seo

Start by installing the Ajax request facility

cnpm install isomorphic-unfetch --save
Copy the code

Update the contents of the pages/index.js file to

import React, { Component } from 'react';
import Layout from ".. /components/Layouts/PcLayout"
import 'isomorphic-unfetch'

class index extends Component {
    constructor(props) {
		super(props);
		this.state = {
			city:"Wuhan"
		};
    }

    static async getInitialProps({ req }) {
        const res = await fetch('http://localhost:4000/api/seo')
        const seo = await res.json()
        return { seo }
    }

    componentDidMount(){
        console.log(this.props)
    }
    
    render(){
        const { seo } = this.props;
        return( <Layout seo={seo}> <div>Welcome to next.js! </div> <div>{seo.title}</div> </Layout> ) } }export default index
Copy the code

/Layouts/ pclayout. js file content is changed to

import HeadSeo from '.. /Common/HeadSeo'

export default ({ children, seo }) => (
  <div id="pc-container">
    <HeadSeo seo={ seo }></HeadSeo>
    { children }
  </div>
)
Copy the code

/ components/Common/HeadSeo. The content is modified to js file

import Head from 'next/head'

export default ({seo}) =>
    <Head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
        <meta name="keywords" content={seo.keywords} />
        <meta name="description" content={seo.description} />
        <title>{seo.title}</title>
    </Head>
Copy the code

At this point you can see the printed data and the displayed data on the page

The next step is to determine the page city to be displayed according to the user’s geographical location. The solution steps are as follows.

  • First request Baidu open API according to IP location to obtain the city name and unique code
  • Request city data corresponding to unique code by unique code as parameter
  • After beautifying the route, the corresponding city page data will be returned to the foreground for display

Requirement 4: PC /M side render different pages

Basic principle: according to the request header user-agnet judge terminal, then render different components in static folder to create js folder, in js folder to create util.js tool class module, code is as follows

Const util = {isMobile: (req) => {const deviceAgent = req.headers["user-agent"];
        return /Android|webOS|iPhone|iPod|BlackBerry/i.test(deviceAgent)
    },

};

module.exports  = util
Copy the code

Create mindex.js file in pages folder as the home page of mobile terminal rendering

import React, { Component } from 'react';
import Layout from ".. /components/Layouts/MLayout"
import 'isomorphic-unfetch'

class index extends Component {
    constructor(props) {
		super(props);
		this.state = {
			city:"Wuhan"
		};
    }

    static async getInitialProps({ req }) {
        const res = await fetch('http://localhost:4000/api/seo')
        const seo = await res.json()
        return { seo }
    }

    componentDidMount(){
        console.log(this.props)
    }
    
    render(){
        const { seo } = this.props;
        return( <Layout seo={seo}> <div>Welcome to next.js! </div> </Layout>)}}export default index
    
Copy the code

Modify the contents of the server.js file as follows

const express = require('express')
const next = require('next')
const server = express()

const util = require("./static/js/util"); const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV ! = ='production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
  .then(() => {

    server.get('/:city', (req, res) => {
        const actualPage = util.isMobile(req) ? '/mindex' : '/index'; Const queryParams = {city: req.params.city}; console.log(req.params.city, actualPage) app.render(req, res, actualPage, queryParams); }); server.get('/:city/index', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/about', (req, res) => {
        const actualPage = '/about';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/posts/:id', (req, res) => {
      return app.render(req, res, '/posts', { id: req.params.id })
    })

    server.get(The '*', (req, res) => {
      return handle(req, res)
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)})})Copy the code

Open the mobile mode in the debug panel with a browser to automatically render the mobile page

Requirement five: SEO information can be configured

In fact, it has basically implemented, the front end through different page routing page parameters request terminal page first screen initialization data interface, request in the server rendering getInitialProps method to complete. For example: / Wuhan /index can obtain seo information from background configuration to the index page according to index as a parameter. / Wuhan /posts can obtain SEO information from background configuration to posts page according to posts as a parameter

Requirement 6: Support development/formal environment switching

The server.js server can be implemented in the following ways

const dev = process.env.NODE_ENV ! = ='production';
Copy the code

The client can be implemented through the configuration file next-config.js

/*
* @Author: winyh
* @Date:   2018-11-01 17:17:10
 * @Last Modified by: winyh
 * @Last Modified time: 2018-12-14 11:01:35
*/
const withPlugins = require('next-compose-plugins')
const path = require("path");
const sass = require('@zeit/next-sass') const isDev = process.env.NODE_ENV ! = ='production'Console. log({isDev}) // API host const host = isDev?'http://localhost:4000':'http://localhost:4001'

const {
    PHASE_PRODUCTION_BUILD,
    PHASE_PRODUCTION_SERVER,
    PHASE_DEVELOPMENT_SERVER,
    PHASE_EXPORT,
} = require('next/constants');

const nextConfiguration = {
    //useFileSystemPublicRoutes: false, 
    //distDir: 'build'.testConfig:"www",
    webpack: (config, options) => {

        config.module.rules.push({
            test: /\.(jpe? g|png|svg|gif|ico|webp)$/, use: [ { loader:"url-loader",
                options: {
                  limit: 20000,
                  publicPath: `https://www.winyh.com/`,
                  outputPath: `/winyh/static/images/`,
                  name: "[name].[ext]"}}}])return config;
    },

    serverRuntimeConfig: { // Will only be available on the server side
        mySecret: 'secret'
    },
    publicRuntimeConfig: { // Will be available on both server and client
        mySecret: 'client',
        host: host,
        akSecert:'GYxVZ027Mo0yFUahvF3XvZHZzAYog9Zo'}} module.exports = withPlugins([[sass, {cssModules:false,
        cssLoaderOptions: {
          localIdentName: '[path]___[local]___[hash:base64:5]',
        },
        [PHASE_PRODUCTION_BUILD]: {
          cssLoaderOptions: {
            localIdentName: '[hash:base64:8]',
          },
        },
    }]

], nextConfiguration)
Copy the code

Pages /index.js modifies the API host address through the configuration file as follows (the fetch request will be wrapped as a public method later)

import React, { Component } from 'react';
import Layout from ".. /components/Layouts/PcLayout"
import 'isomorphic-unfetch'
import getConfig from 'next/config'Const {publicRuntimeConfig} = getConfig() class index extends Component {constructor(props) { super(props); this.state = { city:"Wuhan"
		};
    }

    static async getInitialProps({ req }) {
        const res = await fetch(publicRuntimeConfig.host + '/api/seo'Const seo = await res.json()return { seo }
    }

    componentDidMount(){
        console.log(this.props)
    }
    
    render(){
        const { seo } = this.props;
        return( <Layout seo={seo}> <div>Welcome to next.js! </div> <div>{seo.title}</div> </Layout> ) } }export default index
Copy the code

Requirement 7: wechat authorization file deployment

When making wechat payment or authorization on the webpage, you need to pass the security check of the wechat server. The wechat server delivers a key file *. TXT, which is generally stored in the root directory of the project and must be accessible, for example, localhost:3000/ mp_verify_hjspu6DavebgwSvauh.txt

  • Use (express.static(__dirname)). This is too insecure. All files in the root directory are exposed

  • Add methods for handling.txt files to server.js

server.get(The '*', (req, res) => {
const express = require('express')
const next = require('next')
const server = express()

const util = require("./static/js/util"); const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV ! = ='production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
  .then(() => {

    server.get('/:city', (req, res) => { const txt = req.url; // Get the request path // do a request interception judgment hereif(txt.indexOf(".txt") > 0){
          res.sendFile(__dirname + `/${txt}`);
        }
        const actualPage = util.isMobile(req) ? '/mindex' : '/index';
        const queryParams = { city: req.params.city};
        console.log(req.params.city, actualPage)
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/index', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/about', (req, res) => {
        const actualPage = '/about';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/posts/:id', (req, res) => {
      return app.render(req, res, '/posts', { id: req.params.id })
    })

    // server.get(The '*', (req, res) => {
    //   return handle(req, res)
    // })

    server.get(The '*', (req, res) => { const txt = req.url; // Get the request pathif(txt.indexOf(".txt") > 0){
        res.sendFile(__dirname + `/${txt}`);
      } else {
        return handle(req, res)
      }
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)})})Copy the code

Create a file mp_verify_hjspu6davebgwSvauh. TXT in the root directory to test the browser access result

Requirement eight: Partial Http request proxy processing

Add the following code to the server.js file to automatically match the proxy to http://api.test-proxy.com when accessing the /proxy/* route

const proxyApi = "http://api.test-proxy.com"
server.use('/proxy/*', proxy({ 
  target: proxyApi, 
  changeOrigin: true 
}));
Copy the code

Requirement 9: Class redirection processing

When accessing the localhost:3000/winyh route, ww.redirect.com/about?type_… The content on the page. CNPM I urllib –save

// Change the server.js file to server.get('/winyh', async (req, res) => {
  const agent  = req.header("User-Agent");
  const result = await urllib.request(
    'http://ww.redirect.com/about?type_id=3', 
    {   
      method: 'GET',
      headers: {
        'User-Agent': agent
      },
    })
    res.header("Content-Type"."text/html; charset=utf-8"); res.send(result.data); // we need to get result.data otherwise we will display the data in binary 45 59 55})Copy the code

Requirement 10: Local data simulation

As mentioned in the above article, it has been implemented

Requirement 11: Project deployment mode

Mainly write Dockerfile file, native VsCode can start container debugging, later demo

FROM mhart/alpine-node

WORKDIR /app
COPY . .

RUN yarn install
RUN yarn build

EXPOSE 80

CMD ["node"."server.js"]
Copy the code

Conclusion:

  • There are still a lot of details to perfect, I hope to help you, but also hope to comment and exchange, learn from each other.
  • We’ll change the data request to graphqL.js later. In this article I wrote a GraphQL primer on the new way graphQL.js interacts with the server