preface

There will be no further introductions about Next. Js, isoglyph, SEO, SPA, etc. This paper mainly focuses on the core code analysis and main pit explanation of the front-end engineering of Next. Js to build a complete production deployment with TypeScript and MobX. Here are some of the skills and knowledge I need to get started with this tutorial:

  • Nodejs server programming basics
  • You have read the official documentation of next.js at least once
  • Use React skillfully
  • Proficient with Webpack
  • Understand the concept of isomorphism and what pain points it solves
  • Experience in front-end engineering and automatic deployment

At the beginning of the text, there is a tacit understanding that the students who read this article have the above abilities

Echo Lynn’s Blog

The author will continue to share the advanced development experience about Next. Js in the original article. Interested friends can also leave comments on the blog about your problems or communicate with the author

Create typescript-based projects

The two most interesting features in Next. Js 9 are built-in zero-config TypeScript Support and File System-based Dynamic Routing stands for zero-configuration built-in TypeScript support and dynamic Routing based on file systems. This is a brief look at TypeScript support. In versions prior to 9.0, Next. Js provided support for the base version of typescript starting in 6.0 with a @zeit/ Next-typescript feature, but did not incorporate type checking. The Next. Js core code itself does not provide types, so the TypeScript support provided in this release is not friendly. Zeit’s Next. Js 9 core code is refactored in TypeScript, so the development experience is vastly improved. The following will use the official Demo with-typescript seed project on which the rest of the content will be integrated

The installation

npx create-next-app --example with-typescript with-typescript-app
# or
yarn create next-app --example with-typescript with-typescript-app
Copy the code

Start the

cd with-typescript-app
yarn dev
Copy the code

The following directory structure is obtained:

├─ ├─ class exercises, class exercises, class exercises, class exercises, class Exercises, class Exercises, class Exercises, class Exercises ├─ ─ ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ├─ ├─ ├─ trib.org.txt TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXTCopy the code

Use MobX as the app state management solution

For MobX, please visit [mobx.js.org/]

Install dependencies

Install mobx and Mobx-React modules:

yarn add mobx mobx-react
// or
npm install --save mobx mobx-react
Copy the code

Install Babel Plugin to provide compilation support for decorators:

yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
// or
npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
Copy the code

configuration

Create a. Babelrc file in the project root directory

touch .babelrc
vi .babelrc
Copy the code

write

{
  "presets": [
    "next/babel"]."plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true}}]]Copy the code

Json and add a line to tsconfig.json to make TS support decorator syntax:

{
  "compilerOptions": {
    "experimentalDecorators": true}}Copy the code

Store sub-module code implementation

Create stores folder and create user.ts:

mkdir stores
touch stores/user.ts
Copy the code

Writing:

// user.ts
import {action, observable} from 'mobx'

export default class UserStore {

  @observable name: string = 'Clint'
  
  constructor (initialState: any = {}) {
    this.name = initialState.name;
  }

  @action setName(name: string) {
    this.name = name
  }
}
Copy the code

The constructor in the UserStore class has the following meanings: Receive initialization data to initialize the state under the store or synchronize the state that has been generated when rendering the first screen on the server to the client. Since we need to implement the same constructor each time we create one of these store submodules to initialize or synchronize state in the module, we can optimize our code by writing a base class that all store submodules inherit from: Create stores/base.ts and say:

// base.ts
export default class Base {
  [key: string] :any

  constructor(initState: { [key: string] :any } = {}) {
    for (const k in initState) {
      if (initState.hasOwnProperty(k)) {
        this[k] = initState[k]
      }
    }
  }
}
Copy the code

Modify user. Ts:

// user.ts
import {action, observable} from 'mobx'
import Base from './base'

export default class UserStore extends Base {

  @observable name: string = 'Clint'

  @action setName(name: string) {
    this.name = name
  }
}
Copy the code

Create stores/config.ts. When a new store submodule needs to be created, it can be automatically integrated into the root store by importing the submodule through this configuration file:

touch stores/config.ts
Copy the code

Writing:

import userStore from './user'
import Base from './base'

const config: { [key: string] :typeof Base } = {
  userStore
}

export default config
Copy the code

MobX principal logic

After optimizing the code for the Store submodule, implement the store body logic and create stores/index.ts:

touch stores/index.ts
Copy the code

Writing:

import {useStaticRendering} from 'mobx-react'
import config from './config'

const isServer = typeof window= = ='undefined'
// Comment 1
useStaticRendering(isServer)

export class Store {
  [key: string] :any
  // Comment 2
  constructor(initialState: any = {}) {
    for (const k in config) {
      if (config.hasOwnProperty(k)) {
        this[k] = new config[k](initialState[k])
      }
    }
  }
}

let store: any = null
// Comment 3
export function initializeStore(initialState = {}) {
  if (isServer) {
    return new Store(initialState)
  }
  if (store === null) {
    store = new Store(initialState)
  }

  return store
}

Copy the code

Code comments:

  1. As the first screen rendering of Next. Js is performed on the server, the state created by MobX is an observable. The observable created by MobX will use an in-memory listener to listen for changes in the object. The client takes over the rest of the work, so if the object is observable on the server side, there is a risk of a memory leak, so we use the useStaticRendering method, and let MobX create a static ordinary JS object while the file is executed on the server side
  2. The constructor will mount the submodule created above in the root store of MobX and assign the received initial state/server passthrough state to the submodule one by one. When the assignment is server state synchronization, since the execution environment is the client, the state in the submodule will regain the observable properties. Allows the React component that uses the state value to respond to changes
  3. initializeStoreMethod, the service side rendering, each independent request will create a new store, in order to isolate the request between the state of confusion, when the client to render, you just need to quote has been created before the store, because the same application (SPA) should share a tree above the MobX state management of the main logic implementation, How MobX works with Next-js and React to implement state management

mobx-react

MobX react implements state management with React. Mobx-react implements state management with React. Mobx-react implements state management with React.

When designing the MobX code structure, we implemented the concept of a store submodule, so the first question is, can we use injection to load the page on demand store submodule we need?

In addition, we already know that Next. Js implements a static method called getInitialProps on the server to get the data needed to render the page when the page is requested on the front screen. How do I get a Store object in getInitialProps?

Thirdly, as mentioned above, when we render the first screen of the server, some initial states will be generated and stored in one or some sub-modules of the Store. How does Next. Js bring these states to the client and ** how can we synchronize these states to the client store object to keep the server client state consistent? ** Here are the third and fourth questions. Summarize the issues that need to be resolved:

  1. Inject the Store submodule into the React component
  2. ingetInitialPropsMethod using a Store object to populate the data
  3. Analyze the mechanism of Next. Js data synchronization from server to client
  4. Synchronize the store status of the server and client

To solve the first problem we need to rewrite the *_app.tsx* file of next.js:

touch pages/_app.tsx
Copy the code

Writing:

// pages/_app.tsx import App, {AppContext} from 'next/app' import React from 'react' import {initializeStore, Store} from '.. /stores' import {Provider} from 'mobx-react' class MyMobxApp extends App { mobxStore: Store // Fetching serialized(JSON) store state static async getInitialProps(appContext: AppContext): Promise<any> { const ctx: any = appContext.ctx // Comment 1 ctx.mobxStore = initializeStore() const appProps = await App.getInitialProps(appContext) return { ... appProps, initialMobxState: ctx.mobxStore } } constructor(props: any) { super(props) // Comment 2 const isServer = typeof window === 'undefined' this.mobxStore = isServer ? props.initialMobxState : initializeStore(props.initialMobxState) } render() { const {Component, pageProps}: any = this.props return ( // Comment 3 <Provider {... this.mobxStore}> <Component {... pageProps} /> </Provider> ) } } export default MyMobxAppCopy the code

Code comments:

  1. The second problem is solved by creating (server) or getting (client) a Store object named mobxStore and mounting the mobxStore to the AppContext.ctx object, which is passed as an input parameter to the getInitialProps method of the page

  2. In fact, we need to first explain the principle of Next-js isomorphism: When the first screen is requested, Next. Js renders the HTML file on the server using the React rendering page mechanism (the server rendering life cycle only runs until render) to meet the needs of SEO and display the first screen page, and then returns it to the client (usually the browser). Next. Js will run through the entire React lifecycle render, so as long as the render is consistent, the React built-in diff algorithm doesn’t make any difference, and you won’t see any noticeable changes to the page. So that’s the third thing we need to do. When the Next. Js server renders the HTML file, the relevant data generated by this request is inserted into the HTML file by writing script tags and returned. Starting with local services, let’s use the Chrome console to take a look at the actual data

yarn dev
Copy the code
<script id="__NEXT_DATA__" type="application/json"> {"dataManager":"[]","props":{"pageProps":{},"initialMobxState":{"userStore":{}}},"page":"/","query":{},"buildId":"develo pment"} </script>Copy the code

In this way, when running oN the client, Next. Js builds React SPA based on NEXT_DATA brought back by the server, which is the core principle of isomorphism.

From the above data, we can easily see that initialMobxState is brought back. At this point, we can go back to the code in pages/_app.tsx:

constructor(props: any) {
    super(props)
    const isServer = typeof window= = ='undefined'
    this.mobxStore = isServer ? props.initialMobxState : initializeStore(props.initialMobxState)
  }
Copy the code

When the constructor is executed on the client, the store object is created according to props. InitialMobxState * in *NEXT_DATA. This completes the synchronization of the server store state to the client, which resolves transaction 4

  1. Inject the sub module into the provider component through the extended operator store. In combination with the Inject method provided by Mobx-React, the function of obtaining the store module on demand is achieved. The following is an example of usage code. Mobx-react [github.com/mobxjs/mobx…] To learn more

    // pages/detail.tsx import * as React from 'react' import Layout from '.. /components/Layout' import {User} from '.. /interfaces' import {findData} from '.. /utils/sample-api' import ListDetail from '.. /components/ListDetail' import {inject, observer} from 'mobx-react' import UserStore from '.. /stores/user' type Props = { item? : User userStore: UserStore errors? : string } @inject('userStore') @observer class InitialPropsDetail extends React.Component<Props> { static getInitialProps  = async ({query, mobxStore}: any) => { mobxStore.userStore.setName('set by server') try { const {id} = query const item = await findData(Array.isArray(id) ? id[0] : id) return {item} } catch (err) { return {errors: err.message} } } render() { const {item, errors} = this.props if (errors) { return ( <Layout title={`Error | Next.js + TypeScript Example`}> <p> <span style={{color: 'red'}}>Error:</span> {errors} </p> </Layout> ) } return ( <Layout title={`${item ? item.name : 'Detail'} | Next.js + TypeScript Example`} > {item && <ListDetail item={item}/>} <p> Name: {this.props.userStore.name} </p> <button onClick={() => { this.props.userStore.setName('set by client') }}>click to set name </button> </Layout> ) } } export default InitialPropsDetailCopy the code

    Access: [http://localhost:3000/detail?id=101] to check the effect

These are the core ideas and the use of libraries based on the development of next.js. Let’s start with the construction and deployment

Build to compile

Next. Js uses WebPack to build packaged projects. When a project does not require a special custom build, execute the following command to build the project package

next build
Copy the code

Also mentioned in introduction, this paper focuses on the deployment of Next. Complete instance of js, the only way in the default build project is clearly do not meet our demands of actual production, I will speak some usual here we needed to build the project for more general demand point, cover, of course, not all, but can also provide some ideas.

Here, by the way, when we use a framework to build an application, please try to use the API function provided by the framework itself, what are the benefits of doing so:

  1. Avoid reinventing the wheel
  2. Naturally form a set of norms and standards, team development to reduce learning costs
  3. Documentation is readily available and easy to use
  4. The less code with subjective preferences in the project, the better

Division of environmental

A production project cannot avoid the problem of environment. The more common project environment is divided into dev test production, that is, development, test and production. Here we take such environment as an example

Usually, we pull out the environment variables referenced in the project, save the variables in the configuration file, index the corresponding configuration file according to the program running environment, and take out the variables to use

Js, test.js, prod.js*, dev.js, test.js, and prod.js*.

mkdir config
touch config/dev.js config/test.js config/prod.js config/index.js
Copy the code

Write separately:

// config/dev.js
module.exports = {
  env: 'dev'
}
Copy the code
// config/test.js
module.exports = {
  env: 'test'
}
Copy the code
// config/prod.js
module.exports = {
  env: 'prod'
}
Copy the code
// config/index.js
const dev = require('./dev')
const test = require('./test')
const prod = require('./prod')

module.exports = {
  dev,
  test,
  prod
}
Copy the code

Next. Js Builds (Next, Next Start) and starts applications (Next, Next Start) by reading custom configuration options in the root directory of Next

Create next. Config. Js

touch next.config.js
Copy the code
// next.config.js
const config = require('./config')
// Get process DEPLOY_ENV value
const DEPLOY_ENV = process.env.DEPLOY_ENV || 'dev'

module.exports = {
  serverRuntimeConfig: {
    // Will only be available on the server side
    secret: 'secret',},// Use which config file according to DEPLOY_ENV
  publicRuntimeConfig: config[DEPLOY_ENV]
}

Copy the code

Alter pages/index.tsx file:

// pages/index.tsx import * as React from 'react' import Link from 'next/link' import Layout from '.. /components/Layout' import { NextPage } from 'next' import getConfig from 'next/config' const {publicRuntimeConfig, serverRuntimeConfig} = getConfig() const IndexPage: NextPage = () = > {return (< Layout title = "Home | Next. Js + TypeScript Example" > < h1 > Hello Next. Js 👋 < / h1 > < p > the Public config JSON string: {JSON.stringify(publicRuntimeConfig)}</p> <p>Server side config JSON string: {JSON.stringify(serverRuntimeConfig)}</p> <p> <Link href="/about"> <a>About</a> </Link> </p> </Layout> ) } export default IndexPageCopy the code

ServerRuntimeConfig, publicRuntimeConfig and serverRuntimeConfig are only allowed to be used when the program is running on the server. The publicRuntimeConfig option allows both the server and the client to get it. I use publicRuntimeConfig to illustrate the idea

After completing the above code, execute the command

next
Copy the code

Use a browser to open [http://localhost:3000] to see the effect

Notice that the browser displays publicRuntimeConfig as the contents of config/dev.js and serverRuntimeConfig as an empty object. Careful friends will notice that when you refresh the page quickly and repeatedly, ServerRuntimeConfig is changed from {“secret”: “secret”} to {}.

So, the problem we need to solve is to build the application and run the test/prod environment, the page display config/test.js or config/prod.js content

For example, the build command of Next. Js is Next build and the start command is Next start. The custom configuration of the build and start of the application is determined according to Next. We index the configuration file based on an environment variable called DEPLOY_ENV, so we just need to assign to DEPLOY_ENV when we run next Build and next Start

DEPLOY_ENV=test next build
DEPLOY_ENV=test next start
Copy the code

After executing the above command, open the [http://localhost:3000] page to check whether the config/test.js content is displayed

CSS precompiled

This is more simple, the official plug-in, I don’t need to speak again, directly on the link

It’s worth mentioning that, yes, Next. Js has one CSS weakness: all the style files end up packaged as a style.chunk. CSS file that is returned with the first screen load. One minor drawback of this is that when your app project is large, the size of the file will affect the loading of the first screen a little bit. This effect is minimal after gzip compression, but it will need to be optimized. Another problem is that the class name conflicts. You might need to wrap unrelated page styles in a namespace using nested style scripts such as Less and Sass, or use {cssModules: true} to hash your class names.

The author has put forward an issue about CSS file cutting to the author of Next. Js, looking forward to the solution of the later version.

Strong hands-on ability webpack principle hard enough students can also try to achieve this function. If I am lucky enough to realize it, I will share it again.

Service deployment

Next. Js is different from a normal static Web project. Of course, you can build a normal static web project with Next

So if you want to deploy homogeneity straight out, you need to have a Web server, front-end field is currently more popular or Node.js, Next. Js server is also running on Node.js, Next to introduce a simple deployment scheme, and then continue for some I think frequent some scenarios to explain the deployment ideas.

Projects can be deployed in two ways:

One is to upload the entire project directory to the server with all the source files except node_modules (you can also bring this directory to the server if you connect to the server fast enough) and install the project dependencies

yarn
// or
npm install
Copy the code

build

DEPLOY_ENV=$YOUR_SERVER_ENV_TYPE next build
Copy the code

Start the service

DEPLOY_ENV=$YOUR_SERVER_ENV_TYPE next start
Copy the code

Second, you can use Docs Gitlab Com Runner (recommended).

After the build, upload the required resources to the server and make a list of the required directories

App ├─.next // ├─ pages // Just EmptyforSafe ├ ─ next. Config. Js / /ifHave ├ ─ server. Js / /ifHave ├ ─ static / /if├─ Config // Mentioned above,if├─ ├─ download. json // Optional ├─ download. TXT // optional ├─ download. TXT // optional ├─ download. TXT // optionalCopy the code
  • .next:next buildDirectory of files compiled after execution
  • pages: You are advised to upload an empty directory. No, because the source files are already packed into the *.next* directory, but recently I encountered an error message saying pages could not be found during deployment, so I made an empty directory and it worked. emm… Make an issue later
  • next.config.js: If you have a custom configuration
  • server.js: If you have a custom Node service
  • staticThis directory is ignored by Next. Js compilation. If your app has static resources that reference this directory, you need to bring it
  • config: As mentioned earlier, if you do the environment segmentation in this article
  • package.jsonNPM modules such as next.js are required on the server to start the service, so this file is needed to install dependencies
  • package-lock.json: No explanation
  • yarn.lock: No explanation

When the transfer is complete, run

yarn
// or
npm install
// no build command needed
DEPLOY_ENV=$YOUR_SERVER_ENV_TYPE next start
Copy the code

Deployment path

As you know, Next. Js is file-system routing by default. Suppose you project deployment of domain name is www.myapp.com, you want to visit * / pages directory home. The TSX *, visit the url as http://www.myapp.com/home, usually it is able to satisfy most of the business scenario, this chapter I want to say, Is another business scenario that is more likely to occur, where multiple projects are deployed under a single domain, not only Next. Js projects, but also Vue, React, Angular, JQuery, and other types of Web projects

.

The original link continues to be updated: Echo Lynn’s Blog

The author

Echo-Lynn

Ken