preface

React16.8+ next.js +Koa2 Github full stack project is a reference to jokcy’s moOCs project.

The source address

Github.com/sl1673495/n…

introduce

Next. Js is a lightweight React server rendering application framework.

Chinese official website: nextjs.org

When developing the React system, you often need to configure a lot of complicated parameters, such as Webpack configuration, Router configuration, and server configuration. If you need to do SEO, there are more things to consider, how to keep the server side rendering and client side rendering consistent is a cumbersome thing, need to introduce a lot of third-party libraries. Next. Js provides a good solution to these problems, allowing developers to focus on business and free themselves from tedious configuration. Let’s build a good next project from scratch.

Initialization of the project

First install create-next-app scaffolding

npm i -g create-next-app
Copy the code

Then use scaffolding to build the next project

create-next-app next-github
cd next-github
npm run dev
Copy the code

You can see index.js in the Pages folder

The generated directory structure is simple, so let’s add a few things

├ ─ ─ the README. Md ├ ─ ─ the components / / not page-level share components │ └ ─ ─ nav. Js ├ ─ ─ package - lock. Json ├ ─ ─ package. The json ├ ─ ─ pages / / page level component can be parsed into routing │ ├─ ├─ ├─ ├─ ├─ faviconCopy the code

After the project is started, the default port is started on port 3000. When localhost:3000 is opened, the default access is the contents of index.js

Use next as middleware for Koa. (optional)

If you want to integrate KOA, refer to this section. Create a new server.js file in the root directory

// server.js

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')

constdev = process.env.NODE_ENV ! = ='production'
const app = next({ dev })
const handle = app.getRequestHandler()

const PORT = 3001
// Start the service to respond to the request after the Pages directory is compiled
app.prepare().then((a)= > {
  const server = new Koa()
  const router = new Router()

  server.use(async (ctx, next) => {
    await handle(ctx.req, ctx.res)
    ctx.respond = false
  })

  server.listen(PORT, () => {
    console.log(`koa server listening on ${PORT}`)})})Copy the code

Then change the dev command in package.json

scripts": {"dev":"node server.js","build":"next build","start":"next start"}Copy the code

Ctx. req and ctx.res are provided natively by Node

Ctx. req and ctx.res are passed because next isn’t just koA compliant, so you need to pass the REq and RES that Node natively provides

The integration of CSS

By default, next does not support direct import of CSS files. It provides us with a CSS in JS solution by default, so we need to add next’s plug-in package to support CSS

yarn add @zeit/next-css
Copy the code

If you don’t have one in the project root directory, create a new next.config.js file and add the following code

const withCss = require('@zeit/next-css')

if (typeof require! = ='undefined') {
  require.extensions['.css'] = file= >{}}// withCss yields a next config configuration
module.exports = withCss({})
Copy the code

Integrated ant – design

Yarn add antd yarn add babel-plugin-import // Load the plug-in as requiredCopy the code

Create a new. Babelrc file in the root directory

{
  "presets": ["next/babel"]."plugins": [["import",
      {
        "libraryName": "antd"}}]]Copy the code

The purpose of this Babel plugin is to bring

import { Button } from 'antd'
Copy the code

Parsed into

import Button from 'antd/lib/button'
Copy the code

This completes importing components on demand

Create _app.js in the Pages folder. This is next’s way of rewriting App components, and here we can introduce the ANTD style

pages/_app.js

import App from 'next/app'

import 'antd/dist/antd.css'

export default App
Copy the code

Routing in Next

usingLinkComponent jump

import Link from 'next/link'
import { Button } from 'antd'

const LinkTest = (a)= > (
  <div>
    <Link href="/a">
      <Button>The page a is displayed</Button>
    </Link>
  </div>
)

export default LinkTest
Copy the code

usingRouterModule jump

import Link from 'next/link'
import Router from 'next/router'
import { Button } from 'antd'

export default() = > {const goB = (a)= > {
    Router.push('/b')}return (
    <>
      <Link href="/a">
        <Button>The page a is displayed</Button>
      </Link>
      <Button onClick={goB}>The PAGE B is displayed</Button>
    </>)}Copy the code

Dynamic routing

In next, only query is used to implement dynamic routing. /b/:id is not supported

Home page

import Link from 'next/link'
import Router from 'next/router'
import { Button } from 'antd'

export default() = > {const goB = (a)= > {
    Router.push('/b? id=2')
    / / or
    Router.push({
      pathname: '/b'.query: {
        id: 2,}})}return <Button onClick={goB}>The PAGE B is displayed</Button>
}
Copy the code

B page

Import {withRouter} from 'next/router' const B = ({router}) => <span> {router.query.id}</span> export default withRouter(B)Copy the code

The path to page B is /b? id=2

If you really want to display /b/2, you can do this via the AS attribute on Link

<Link href="/a? Id =1" as="/a/1"> <Button>Copy the code

Or when using the Router

Router.push(
  {
    pathname: '/b'.query: {
      id: 2,}},'/b/2'
)
Copy the code

But with this method, it’s 404 when the page is refreshed because the alias method is only added when the front end route jumps and the server doesn’t recognize the route when the refresh request goes away

Using KOA solves this problem

// server.js

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')

constdev = process.env.NODE_ENV ! = ='production'
const app = next({ dev })
const handle = app.getRequestHandler()

const PORT = 3001
// Start the service to respond to the request after the Pages directory is compiled
app.prepare().then((a)= > {
  const server = new Koa()
  const router = new Router()

  // start
  // Use koa-router to route /a/1
  // proxy to /a? Id =1, so you don't have 404
  router.get('/a/:id'.async ctx => {
    const id = ctx.params.id
    await handle(ctx.req, ctx.res, {
      pathname: '/a'.query: {
        id,
      },
    })
    ctx.respond = false
  })
  server.use(router.routes())
  // end

  server.use(async (ctx, next) => {
    await handle(ctx.req, ctx.res)
    ctx.respond = false
  })

  server.listen(PORT, () => {
    console.log(`koa server listening on ${PORT}`)})})Copy the code

The Router of hook

In a route jump, routeChangeStart beforeHistoryChange routeChangeComplete is triggered successively

If there is an error, a routeChangeError is raised

The way you listen in is

Router.events.on(eventName, callback)
Copy the code

The custom document

  • This is only called when the server is rendering
  • Used to modify the content of the document rendered by the server
  • It is generally used with third-party CSS in JS solutions

Create _document.js under Pages and we can rewrite it as needed.

import Document, { Html, Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  // If you want to rewrite render, you must use this structure
  render() {
    return (
      <Html>
        <Head>
          <title>ssh-next-github</title>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>)}}Copy the code

Custom app

Next, the components exposed in the pages/_app.js file will be a global wrapper component that will be wrapped around each page component and we can use it

  • A fixed Layout
  • Keep something in common
  • Pass some custom data to the page pages/_app.js

To give a simple example, don’t change the code in _app.js, otherwise the getInitialProps will not get the data, and we’ll deal with that later.

import App, { Container } from 'next/app'
import 'antd/dist/antd.css'
import React from 'react'

export default class MyApp extends App {
  render() {
    // Component is the page Component we want wrapped
    const { Component } = this.props
    return (
      <Container>
        <Component />
      </Container>)}}Copy the code

Encapsulation getInitialProps

GetInitialProps is a very powerful tool for synchronizing server and client data. We should try to put the logic for retrieving data in getInitialProps, which can:

  • Get the data in the page
  • Get global data in App

The basic use

Any value returned by the static method getInitialProps is passed to the component as props

const A = ({ name }) = > (
  <span>This is the A page, and the name we get from getInitialProps is {name}</span>
)

A.getInitialProps = (a)= > {
  return {
    name: 'ssh',}}export default A
Copy the code

Note, however, that only components in the Pages folder (page-level components) call this method. Next will call this method for you before the route switch, and this method will be executed on both the server and client side. (refresh or front-end jump) and if the server rendering has already been done, it will not be done for you during the client rendering.

Asynchronous scenario

Asynchronous scenarios can be solved with async await, and Next will wait until asynchronous processing is complete and results are returned before rendering the page

const A = ({ name }) = > (
  <span>This is the A page, and the name we get from getInitialProps is {name}</span>
)

A.getInitialProps = async() = > {const result = Promise.resolve({ name: 'ssh' })
  await new Promise(resolve= > setTimeout(resolve, 1000))
  return result
}
export default A
Copy the code

Get the data in _app.js

Let’s rewrite some of the logic in _app.js to get data

import App, { Container } from 'next/app'
import 'antd/dist/antd.css'
import React from 'react'

export default class MyApp extends App {
  // The App component's getInitialProps is special
  // Get some extra parameters
  // Component: wrapped Component
  static async getInitialProps(ctx) {
    const { Component } = ctx
    let pageProps = {}

    // Get the getInitialProps defined on Component
    if (Component.getInitialProps) {
      // execute to get the return result
      pageProps = await Component.getInitialProps(ctx)
    }

    // Return to the component
    return {
      pageProps,
    }
  }

  render() {
    const { Component, pageProps } = this.props
    return (
      <Container>{/* Pass pageProps to component */}<Component {. pageProps} / >
      </Container>)}}Copy the code

Encapsulate generic Layout

We want each page to have a common header navigation bar, which we can do with _app.js.

In the Components folder, create a new layout. JSX:

import Link from 'next/link'
import { Button } from 'antd'

export default ({ children }) => (
  <header>
    <Link href="/a">
      <Button>The page a is displayed</Button>
    </Link>
    <Link href="/b">
      <Button>The PAGE B is displayed</Button>
    </Link>
    <section className="container">{children}</section>
  </header>
)
Copy the code

In _app. Js

/ / to omit
import Layout from '.. /components/Layout'

export default class MyApp extends App {
  / / to omit

  render() {
    const { Component, pageProps } = this.props
    return (
      <Container>{/* Layout package outside */}<Layout>{/* Pass pageProps to component */}<Component {. pageProps} / >
        </Layout>
      </Container>)}}Copy the code

Document Title solution

For example, in pages/ A.js, I want the title of the page to be A, and in B, I want the title to be B. This function also provides a solution for us

pages/a.js

import Head from 'next/head'

const A = ({ name }) = > (
  <>
    <Head>
      <title>A</title>
    </Head>
    <span>This is the A page, and the name we get from getInitialProps is {name}</span>
  </>
)

export default A
Copy the code

Style solutions (CSS in JS)

The default for next is styled- JSX the library github.com/zeit/styled…

Note that the style tag inside the component is only applied to the head after the component has been rendered, and will not be applied after the component has been destroyed.

Component internal style

Next provides a solution for styling by default. The default scope for writing inside a component is that component, as follows:

const A = ({ name }) = > (
  <>
    <span className="link">This is page A</span>
    <style jsx>{` .link { color: red; } `}</style>
  </>
)

export default A
)
Copy the code

We can see that the generated SPAN tag becomes

<span class=" jsX-3081729934 link">Copy the code

The CSS style in effect becomes

.link.jsx-3081729934 {
  color: red;
}
Copy the code

In this way, component-level style isolation is achieved, and the Link class can be styled as well if it is globally styled.

Global style

<style jsx global>
  {`
    .link {
      color: red;
    }
  `}
</style>
Copy the code

Styled Solution (Styled component)

First install dependencies

yarn add styled-components babel-plugin-styled-components
Copy the code

Then we add plugin to.babelrc

{
  "presets": ["next/babel"]."plugins": [["import",
      {
        "libraryName": "antd"}], ["styled-components", { "ssr": true}}]]Copy the code

Add JSX support to Pages /_document.js, which uses a method provided by Next to override app, actually using higher-order components.

import Document, { Html, Head, Main, NextScript } from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    // Hijack the renderPage function and rewrite it
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = (a)= >
        originalRenderPage({
          // Root App component
          enhanceApp: App= > props => sheet.collectStyles(<App {. props} / >Const props = await document.getInitialprops (CTX) return {... props, styles: (<>
            {props.styles}
            {sheet.getStyleElement()}
          </>),}} finally {sheet.seal()}} // Render () {return ();<Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>)}}Copy the code

Then in Pages/A.js

import styled from 'styled-components'

const Title = styled.h1` color: yellow; font-size: 40px; `
const A = ({ name }) = > (
  <>
    <Title>This is page A</Title>
  </>
)

export default A
Copy the code

In the next LazyLoading

LazyLoading is enabled in next by default, and the js module will be loaded only when we switch to the corresponding route.

LazyLoading generally falls into two categories

  • Asynchronous loading module
  • Asynchronously loading components

First we use the moment library to demonstrate the asynchronous loading module.

Asynchronous loading module

We introduce moment module // pages/a.js in page A

import styled from 'styled-components'
import moment from 'moment'

const Title = styled.h1` color: yellow; font-size: 40px; `
const A = ({ name }) = > {
  const time = moment(Date.now() - 60 * 1000).fromNow()
  return (
    <>
      <Title>This is page A, the time difference is {time}</Title>
    </>
  )
}

export default A
Copy the code

One problem with this is that if we introduce moment across multiple pages, the module will be extracted into the packaged public vendor.js by default.

We can take advantage of Webpack’s dynamic import syntax

A.getInitialProps = async ctx => {
  const moment = await import('moment')
  const timeDiff = moment.default(Date.now() - 60 * 1000).fromNow()
  return { timeDiff }
}
Copy the code

The moment code will be downloaded only after you get to page A.

Asynchronously loading components

Next provides an example of the dynamic method:

import dynamic from 'next/dynamic'

const Comp = dynamic(import('.. /components/Comp'))

const A = ({ name, timeDiff }) => {
  return (
    <>
      <Comp />
    </>
  )
}

export default A

Copy the code

With the regular React component introduced this way, the code for the component will only be downloaded after the A page is displayed.

Next.config.js is fully configured

Next goes back and reads the next.config.js file in the root directory. Each item is commented out and can be used according to your needs.

const withCss = require('@zeit/next-css')

const configs = {
  // Output directory
  distDir: 'dest'.// Whether Etag is generated for each route
  generateEtags: true.// Caching of page content for local development
  onDemandEntries: {
    // How long the contents are cached in memory (ms)
    maxInactiveAge: 25 * 1000.// The number of pages cached simultaneously
    pagesBufferLength: 2,},// is used as the suffix for page parsing in the pages directory
  pageExtensions: ['jsx'.'js']./ / configuration buildId
  generateBuildId: async() = > {if (process.env.YOUR_BUILD_ID) {
      return process.env.YOUR_BUILD_ID
    }

    // Return null default unique ID
    return null
  },
  // Manually modify the WebPack configuration
  webpack(config, options) {
    return config
  },
  // Manually modify the webpackDevMiddleware configuration
  webpackDevMiddleware(config) {
    return config
  },
  // Value can be obtained on the page through process.env.customKey
  env: {
    customkey: 'value',},// The next two are read by 'next/config'
  // This can be read on the page by introducing import getConfig from 'next/config'

  // A configuration that is retrieved only when rendered on the server
  serverRuntimeConfig: {
    mySecret: 'secret'.secondSecret: process.env.SECOND_SECRET,
  },
  // Configuration available on both the server side and the client side
  publicRuntimeConfig: {
    staticFolder: '/static',}}if (typeof require! = ='undefined') {
  require.extensions['.css'] = file= >{}}// withCss yields a nextjs config configuration
module.exports = withCss(configs)
Copy the code

SSR process

Next helped us with the synchronization of getInitialProps on the client and server side,

Next injects server rendered data into the HTML page using the NEXT_DATA key.

For example, in the example of page A, it might look like this

script id="__NEXT_DATA__" type="application/json">
      {
        "dataManager":"[]",
        "props":
          {
            "pageProps":{"timeDiff":"a minute ago"}
          },
        "page":"/a",
        "query":{},
        "buildId":"development",
        "dynamicBuildId":false,
        "dynamicIds":["./components/Comp.jsx"]
      }
      </script>
Copy the code

Introducing Redux (client-side common)

yarn add redux

Create a store/store.js file in the root directory

// store.js

import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'

const initialState = {
  count: 0,}function reducer(state = initialState, action) {
  switch (action.type) {
    case 'add':
      return {
        count: state.count + 1,}break

    default:
      return state
  }
}

// What is exposed here is the factory method for creating a store
// A store instance needs to be recreated for each rendering
// Prevent the server from overusing the old instance and failing to synchronize with the client state
export default function initializeStore() {
  const store = createStore(reducer, initialState, applyMiddleware(ReduxThunk))
  return store
}
Copy the code

The introduction of the react – story

Yarn Add React-redux then wrap the Provider provided by this library around the component in _app.js and pass in the store you define

import { Provider } from 'react-redux'
import initializeStore from '.. /store/store'. render() {const { Component, pageProps } = this.props
    return (
      <Container>
        <Layout>
          <Provider store={initializeStore()}>{/* Pass pageProps to component */}<Component {. pageProps} / >
          </Provider>
        </Layout>
      </Container>)}Copy the code

Inside the component

import { connect } from 'react-redux'

const Index = ({ count, add }) = > {
  return (
    <>
      <span>Home state's count is {count}</span>
      <button onClick={add}>increase</button>
    </>
  )
}

function mapStateToProps(state) {
  const { count } = state
  return {
    count,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    add() {
      dispatch({ type: 'add' })
    },
  }
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Index)
Copy the code

Use Hoc to integrate Redux and Next

In the introduction to Redux above, we simply introduced store as usual, but this approach has a serious problem when we use next for server rendering, if we write it in the getInitialProps of the Index component

Index.getInitialProps = async ({ reduxStore }) => {
  store.dispatch({ type: 'add' })
  return{}}Copy the code

When you enter the index page, an error is reported

Text content did not match. Server: "1" Client: "0"
Copy the code

And every time you refresh the Server, the value increases by 1, which means that the count in the store keeps increasing if multiple browsers are accessing it at the same time, which is a serious bug.

The server gets a count of 1, but the client gets a count of 0. The root cause of this error is that the server gets a count of 1, but the client gets a count of 0. In a homogeneous project, the server and client will hold different stores, and stores will keep the same reference for the lifetime of the server, so we have to find a way to keep the state of the two states consistent with the behavior of store reinitialization after each refresh in a single page application. After the server parses the store, the client initializes the store with the value parsed by the server.

To sum up, our goals are:

  • Each time the server is requested (the page is refreshed for the first time), the store is recreated.
  • When the front-end route jumps, the store reuse the previously created route.
  • This judgment cannot be written in the getInitialProps for each component.

So we decided to use Hoc to achieve this logical reuse.

First, we’ll modify store/store.js so that instead of exposing the store object directly, we’ll expose a method that creates the store and allow passing in initial state for initialization.

import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'

const initialState = {
  count: 0,}function reducer(state = initialState, action) {
  switch (action.type) {
    case 'add':
      return {
        count: state.count + 1,}break

    default:
      return state
  }
}

export default function initializeStore(state) {
  const store = createStore(
    reducer,
    Object.assign({}, initialState, state),
    applyMiddleware(ReduxThunk)
  )
  return store
}
Copy the code

By creating a new with-redux-app.js directory in the lib directory, we decided to use this hoc to wrap the exported components in _app.js and load the app through our hoc every time.

import React from 'react'
import initializeStore from '.. /store/store'

const isServer = typeof window= = ='undefined'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'

function getOrCreateStore(initialState) {
  if (isServer) {
    // The server recreates a store each time it executes
    return initializeStore(initialState)
  }
  // When the client executes this method, it returns the existing store on the window first
  // Instead of recreating a store every time, the state would reset indefinitely
  if (!window[__NEXT_REDUX_STORE__]) {
    window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
  }
  return window[__NEXT_REDUX_STORE__]
}

export default Comp => {
  class withReduxApp extends React.Component {
    constructor(props) {
      super(props)
      // getInitialProps created the store.
      // The serialized string is returned to the client after the server executes getInitialProps
      // There are many methods in redux that are not suitable for serialized storage
      // So choose getInitialProps to return initialReduxState the initial state
      // Create a full store here with initialReduxState
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }

    render() {
      const{ Component, pageProps, ... rest } =this.props
      return (
        <Comp
          {. rest}
          Component={Component}
          pageProps={pageProps}
          reduxStore={this.reduxStore}
        />}} / / that is actually _app. Js getInitialProps / / in the rendering of a service and the client routing jump will be executed / / so it is suitable for story - store initialization withReduxApp. GetInitialProps = async ctx => { const reduxStore = getOrCreateStore() ctx.reduxStore = reduxStore let appProps = {} if (typeof Comp.getInitialProps === 'function') { appProps = await Comp.getInitialProps(ctx) } return { ... appProps, initialReduxState: reduxStore.getState(), } } return withReduxApp }Copy the code

Introduce hoc in _app.js

import App, { Container } from 'next/app'
import 'antd/dist/antd.css'
import React from 'react'
import { Provider } from 'react-redux'
import Layout from '.. /components/Layout'
import initializeStore from '.. /store/store'
import withRedux from '.. /lib/with-redux-app'
class MyApp extends App {
  // The App component's getInitialProps is special
  // Get some extra parameters
  // Component: wrapped Component
  static async getInitialProps(ctx) {
    const { Component } = ctx
    let pageProps = {}

    // Get the getInitialProps defined on Component
    if (Component.getInitialProps) {
      // execute to get the return result
      pageProps = await Component.getInitialProps(ctx)
    }

    // Return to the component
    return {
      pageProps,
    }
  }

  render() {
    const { Component, pageProps, reduxStore } = this.props
    return (
      <Container>
        <Layout>
          <Provider store={reduxStore}>{/* Pass pageProps to component */}<Component {. pageProps} / >
          </Provider>
        </Layout>
      </Container>
    )
  }
}

export default withRedux(MyApp)
Copy the code

Thus, we are able to integrate Redux with next.