Server side rendering Next project summary

preface

Github address Welcome to check and learn together, progress together, another star refill~
In this paper, I refer to Mr. jokcy's React16.8+Next.js+Koa2 development of Github full stack project , record for later learning. 🙂

introduce

Next.js It is a lightweight rendering application framework of React server.

Official website: https://nextjs.org
Chinese official website: https://nextjs.frontendx.cn

When using React to develop a system, it is often necessary to configure many tedious 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 consistent with the client-side rendering is a very troublesome thing, and many third-party libraries need to be introduced. In response to these problems, Next.js Provides a good solution, so that developers can focus on the business, from the tedious configuration of the liberation. Let's build a perfect next project from scratch.

Project initialization

First install the create next app scaffold

npm i -g create-next-app

Then use scaffolding to build the next project

npx create-next-app next-github
cd next-github
npm run dev

You can see the index.js

The generated directory structure is very simple. Let's add a few things

├── README.md
├── components // Non page level common components
│   └── nav.js
├── package-lock.json
├── package.json
├── pages // Page level components are resolved to routes
│   └── index.js
├── lib // Some general js
├── static // Static resources
│   └── favicon.ico

After starting the project, the default port starts at port 3000, open localhost:3000 After that, the default access is index.js Contents of

Use next as the middleware of Koa. (optional)

If you want to integrate koa, you can refer to this paragraph.
New at root server.js file

// server.js

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

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

const PORT = 3001
// Wait until the pages directory is compiled and start the service to respond to the request
app.prepare().then(() => {
  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}`)
  })
})

And then put package.json The dev command in the

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

ctx.req and ctx.res It is provided by node natively

The reason is that ctx.req and ctx.res , because next is not only compatible with the koa framework, so you need to pass the req and res provided by node natively

Integrated css

Direct import of css file is not supported by default in next. It provides us with a scheme of css in js by default, so we need to add the next plug-in package for css support

yarn add @zeit/next-css

If not in the root directory of the project
Let's build a new one next.config.js
Then add the following code

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

if (typeof require !== 'undefined') {
  require.extensions['.css'] = file => {}
}

// With CSS, we get a next config configuration
module.exports = withCss({})

Integrated Ant Design

yarn add antd
yarn add babel-plugin-import // Load plug-ins on demand

Create a new. babelrc file in the root directory

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd"
      }
    ]
  ]
}

The function of this babel plug-in is to

import { Button } from 'antd'

Resolved into

import Button from 'antd/lib/button'

This completes the introduction of components on demand

New under pages folder_ app.js , which is the way next provides for you to rewrite App components. Here we can introduce the antd style

pages/_app.js

import App from 'next/app'

import 'antd/dist/antd.css'

export default App

Route in next

Jump using Link components

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

const LinkTest = () => (
  <div>
    <Link href="/a">
      <Button>Jump to a page</Button>
    </Link>
  </div>
)

export default LinkTest

Using Router module to jump

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

export default () => {
  const goB = () => {
    Router.push('/b')
  }

  return (
    <>
      <Link href="/a">
        <Button>Jump to a page</Button>
      </Link>
      <Button onClick={goB}>Jump to b page</Button>
    </>
  )
}

Dynamic routing

In next, dynamic routing can only be realized through query, and the definition method of / 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 = () => {
    Router.push('/b?id=2')
    // or
    Router.push({
      pathname: '/b',
      query: {
        id: 2
      }
    })
  }

  return <Button onClick={goB}>Jump to b page</Button>
}

Page B

import { withRouter } from 'next/router'

const B = ({ router }) => <span>This isBpage, Parameter is{router.query.id}</span>
export default withRouter(B)

The path to jump to bpage is / b?id=2

If you really want to display it in the form of / b/2, you can also use the as attribute on the Link

<Link href="/a?id=1" as="/a/1">
  <Button>Jump to a page</Button>
</Link>

Or when using Router

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

But with this method, the page will refresh 404
Because this alias method is only added when the front-end route jumps
When the request is refreshed, the server will not recognize the route

Using koa can solve this problem

// server.js

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

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

const PORT = 3001
// Wait until the pages directory is compiled and start the service to respond to the request
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  // start
  // Using koa router to route / a/1
  // Proxy to / a?id=1, so there will be no 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}`)
  })
})

Router's hook

In a route jump, it will be triggered successively
routeChangeStart
beforeHistoryChange
routeChangeComplete

If there is an error, it will trigger
routeChangeError

The way to monitor is

Router.events.on(eventName, callback)

Custom document

  • It will be called only when the server renders
  • Used to modify the content of documents rendered by the server
  • Generally used with the third-party css in js scheme

New under pages_ document.js , we can rewrite it according to our needs.

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

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

Custom app

next, pages/_app.js The components exposed in this file will be used as a global package component, which will be wrapped in the outer layer of each page component. We can use it to

  • Fixed Layout
  • Keep some common state
  • Pass in some custom data to the page
    pages/_app.js

Give me a simple example. Don't change it_ app.js Otherwise, getInitialProps will not get the data. We will deal with this 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 to package
    const { Component } = this.props
    return (
      <Container>
        <Component />
      </Container>
    )
  }
}

Package getInitialProps

getInitialProps is very powerful. It can help us synchronize data between the server and the client. We should try to put the logic of data acquisition in getInitialProps. It can:

  • Get data on page
  • Get global data in App

Basic use

The value returned by the static method getInitialProps will be passed in as props

const A = ({ name }) => (
  <span>This isApage, adopt getInitialProps Acquired name yes{name}</span>
)

A.getInitialProps = () => {
  return {
    name: 'ssh'
  }
}
export default A

However, it should be noted that only the components under the pages folder (page level components) can call this method. next will call this method for you before routing switching. This method will be executed in both server-side rendering and client-side rendering. (refresh or front jump)
And if the server-side rendering has already been executed, it will not help you to execute when you render on the client side.

Asynchronous scenario

Asynchronous scenes can be solved by async await. next will wait until the asynchronous processing is completed and the result is returned before rendering the page

const A = ({ name }) => (
  <span>This isApage, adopt getInitialProps Acquired name yes{name}</span>
)

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

At_ app.js Get data from

Let's rewrite some_ app.js The logic of getting data in

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

export default class MyApp extends App {
  // getInitialProps of App components are special
  // Can get some extra parameters
  // Component: wrapped component
  static async getInitialProps(ctx) {
    const { Component } = ctx
    let pageProps = {}

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

    // Return to component
    return {
      pageProps
    }
  }

  render() {
    const { Component, pageProps } = this.props
    return (
      <Container>
        {/* Deconstruct pageProps and pass them to components */}
        <Component {...pageProps} />
      </Container>
    )
  }
}

Package general Layout

We hope that each page can have a common head navigation bar after jump, which can be used to_ app.js Here we go.

New under components folder Layout.jsx :

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

export default ({ children }) => (
  <header>
    <Link href="/a">
      <Button>Jump to a page</Button>
    </Link>
    <Link href="/b">
      <Button>Jump to b page</Button>
    </Link>
    <section className="container">{children}</section>
  </header>
)

At_ app.js in

// ellipsis
import Layout from '../components/Layout'

export default class MyApp extends App {
  // ellipsis

  render() {
    const { Component, pageProps } = this.props
    return (
      <Container>
        {/* Layout It's outside */}
        <Layout>
          {/* Deconstruct pageProps and pass them to components */}
          <Component {...pageProps} />
        </Layout>
      </Container>
    )
  }
}

Solution of document title

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. next also provides us with a scheme

pages/a.js

import Head from 'next/head'

const A = ({ name }) => (
  <>
    <Head>
      <title>A</title>
    </Head>
    <span>This isApage, adopt getInitialProps Acquired name yes{name}</span>
  </>
)

export default A

Style solution (css in js)

next uses the styled JSX library by default
https://github.com/zeit/styled-jsx

It should be noted that the style label inside the component will not be added to the head until the component is rendered, and the style will be invalid after the component is destroyed.

Component internal style

next provides a style solution by default. When writing inside a component, the default scope is the component. The writing method is as follows:

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

export default A
)

We can see that the generated span tag becomes

<span class="jsx-3081729934 link">This is A page</span>

The css style in effect becomes

.link.jsx-3081729934 {
  color: red;
}

In this way, component level style isolation is achieved, and if the link class has defined styles globally, it can also get styles.

Global style

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

Styled component

Install dependencies first

yarn add styled-components babel-plugin-styled-components

Then we add plugin to. babelrc

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

In pages/_document.js Add the support of jsx. Here we use a method of overriding app provided by next. In fact, we use high-level 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 original renderPage function and override
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          // Root App components
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
        })
      // If you rewrite getInitialProps, you need to re implement this logic
      const props = await Document.getInitialProps(ctx)
      return {
        ...props,
        styles: (
          <>
            {props.styles}
            {sheet.getStyleElement()}
          </>
        )
      }
    } finally {
      sheet.seal()
    }
  }

  // If you want to rewrite render, you have to write according to this structure
  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

Then in pages/a.js

import styled from 'styled-components'

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

export default A

LazyLoading in next

next, LazyLoading is enabled by default. Only when we switch to the corresponding route can we load the corresponding js module.

LazyLoading is generally divided into two categories

  • Asynchronous load module
  • Load components asynchronously

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

Asynchronous load module

We introduce moment module into page a
// pages/a.js

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 isApage, The time difference is{time}</Title>
    </>
  )
}

export default A

This will cause a problem. If we introduce moment in multiple pages, the module will be extracted to the public after packaging by default vendor.js Inside.

We can use the dynamic import syntax of Web pack

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

In this way, only after entering the A page can we download the code of moment.

Load components asynchronously

next officially provides us with a dynamic method, using the following example:

import dynamic from 'next/dynamic'

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

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

export default A

Using this method to introduce A common react component, the code of this component will only be downloaded after entering the A page.

next.config.js Full configuration

Next go back to read the next.config.js Documents, each of which is indicated by notes, can be used according to their own 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 during local development
  onDemandEntries: {
    // Length of time content is cached in memory (ms)
    maxInactiveAge: 25 * 1000,
    // Number of pages cached at the same time
    pagesBufferLength: 2
  },
  // It will be used as the suffix of page resolution in pages directory
  pageExtensions: ['jsx', 'js'],
  // Configure buildiid
  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 webbackdevmiddleware configuration
  webpackDevMiddleware(config) {
    return config
  },
  // You can use the process.env.customkey  Get value
  env: {
    customkey: 'value'
  },
  // The next two are to be read through 'next/config'
  // It can be read on the page by introducing import getConfig from 'next/config'

  // Configuration acquired only when rendering on the server
  serverRuntimeConfig: {
    mySecret: 'secret',
    secondSecret: process.env.SECOND_SECRET
  },
  // Configuration available for both server-side rendering and client-side rendering
  publicRuntimeConfig: {
    staticFolder: '/static'
  }
}

if (typeof require !== 'undefined') {
  require.extensions['.css'] = file => {}
}

// With CSS, we get a config configuration of nextjs
module.exports = withCss(configs)

ssr process

next helps us solve the problem of getInitialProps synchronization between the client and the server,

Next will pass the data from the server rendering to next_ The data key is injected into the html page.

Next.js When rendering on the server side, the getInitialProps function of the corresponding React component of the page is called, and the asynchronous result is an important part of the "dehydrated" data. In addition to passing it to the React component of the page to complete rendering, it is also placed in the next of the embedded script_ In data, in this way, when rendering on the browser side, getInitialProps will not be called, directly through NEXT_DATA to start rendering of the page React component.
In this way, if there is an asynchronous operation calling API in getInitialProps, it can only be done once on the server side and not on the browser side.
So when will getInitialProps be called on the browser side?
When switching pages in a single page application, for example, from Home page to Product page, it has nothing to do with the server side but the browser side. The getInitialProps function of the Product page will be called on the browser side, and the data obtained will be used to start the React native life cycle process of the page.
Reference source: https://blog.csdn.net/gwdgwd123/article/details/85030708

For example, in the page a of our previous example, it's probably in this format

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>

Introduction of redux (client common writing method)

yarn add redux

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

// 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's exposed here is the factory method to create the store
// You need to recreate a store instance every time you render
// Prevent the server from reusing the old instance all the time, and cannot synchronize with the client state
export default function initializeStore() {
  const store = createStore(reducer, initialState, applyMiddleware(ReduxThunk))
  return store
}

Introduce react Redux

yarn add react-redux
And then_ app.js The Provider provided by this library is wrapped in the outer layer of the component and passed into the store defined by you

import { Provider } from 'react-redux'
import initializeStore from '../store/store'

...
render() {
    const { Component, pageProps } = this.props
    return (
      <Container>
        <Layout>
          <Provider store={initializeStore()}>
            {/* Deconstruct pageProps and pass them to components */}
            <Component {...pageProps} />
          </Provider>
        </Layout>
      </Container>
    )
  }

Inside the component

import { connect } from 'react-redux'

const Index = ({ count, add }) => {
  return (
    <>
      <span>home page state Of count yes{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)

Integrating redux and next with hoc

In the introduction of redux (client common writing method) above, we simply introduced store as usual, but there is a serious problem when we use next for server rendering. If we write this in getInitialProps of Index component

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

After entering the index page, an error will be reported

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

And every time you refresh the value behind the Server, 1 will be added, which means that if multiple browsers access at the same time, the count in the store will increase all the time, which is a very serious bug.

This error message means that the status of the server is inconsistent with that of the client. The server gets a count of 1, but the client's count is 0. In fact, the root cause is the server's resolution store.js The status of the store obtained after the file is different from that of the store obtained by the client. In fact, in the homogeneous project, the server and the client will have different status Store, and store keeps the same reference in the life cycle started by the server, so we must find a way to unify the two states, and keep the same behavior with store reinitialization after each refresh in a single page application. After the server obtains the store after parsing, the client directly initializes the store with the value parsed by the server.

To summarize, our goals are:

  • Each time the server is requested (the page enters for the first time, the page refreshes), the store is recreated.
  • When the front-end route jumps, the store is created before reuse.
  • This kind of judgment cannot be written in getInitialProps of each component, and it can be abstracted.

So we decided to use hoc to realize this logic reuse.

First, let's transform the store/store.js , instead of directly exposing the store object, it exposes a method to create the store and allows the initial state to be passed in 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
}

Create a new with Redux in the lib directory- app.js , we decided to use this hoc to package_ app.js For the components exported in, each time the app is loaded, it must pass our hoc.

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 creates a new store every time it executes
    return initializeStore(initialState)
  }
  // When the client executes this method, the existing store on the window will be returned first
  // You can't recreate a store every time you execute it, or the state will reset infinitely
  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. Why do you recreate it here?
      // Because the server returns the serialized string to the client after executing getInitialProps
      // There are many methods in redux that are not suitable for serialized storage
      // So choose getInitialProps to return the initial state of initialReduxState
      // Here, create a complete store through initialReduxState
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }

    render() {
      const { Component, pageProps, ...rest } = this.props
      return (
        <Comp
          {...rest}
          Component={Component}
          pageProps={pageProps}
          reduxStore={this.reduxStore}
        />
      )
    }
  }

  // This is actually_ app.js getInitialProps for
  // It will be executed during server rendering and client route skipping
  // So it is very suitable for Redux 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
}

At_ app.js Introducing hoc in

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 {
  // getInitialProps of App components are special
  // Can get some extra parameters
  // Component: wrapped component
  static async getInitialProps(ctx) {
    const { Component } = ctx
    let pageProps = {}

    // Get getInitialProps defined on Component
    if (Component.getInitialProps) {
      // Execute get return result`
      pageProps = await Component.getInitialProps(ctx)
    }

    // Return to component
    return {
      pageProps
    }
  }

  render() {
    const { Component, pageProps, reduxStore } = this.props
    return (
      <Container>
        <Layout>
          <Provider store={reduxStore}>
            {/* Deconstruct pageProps and pass them to components */}
            <Component {...pageProps} />
          </Provider>
        </Layout>
      </Container>
    )
  }
}

export default withRedux(MyApp)

In this way, we realize the integration of redux in next.

github Oauth certification

Next, access to github for third-party login

New github applications

First, enter the new app page of github
https://github.com/settings/applications/new

The Homepage URL here can temporarily fill in the locally developed url
Then the Authorization callback URL is filled in with the locally developed url + /auth (for example localhost:3001/auth)

After the creation is successful, you can see the Client ID and
Client Secret

New under project root config.js

module.exports = {
  github: {
    client_id: 'Your client_id',
    client_secret: 'Your client_secret'
  }
}

You can
https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/
See the notes for the Oauth certification.

Oauth request

Jump field

client_id: application id
redirect_uri: Authorization callback URL filled in during registration
scope: permissions allowed
allow_signup: allow users to register

Can be accessed by
https://github.com/login/oauth/authorize?client_id = your client_id plus the above fields to try.

Request token

client_id: same as above
client_secret: Client Secret obtained after registration
Code: after the user agrees to the authentication and jumps to the Authorization callback URL filled in, the code can be obtained in the parameter. This code can only be used once.
Interface address (post): https://github.com/login/oauth/access_token

Get user information

https://api.github.com/user The Authorization field needs to be added in the header, and the value is the token requested by the token.

Maintain page access address before OAuth

Record the current url before jump login, put it in the link parameter and send prepare auth? url=${ router.asPath }, save to session. When login succeeds, jump back to visit the current url

tips: get router with router

The strategy to ensure the security of Oauth Code

  • One time code. After a token is requested by code, the code will be invalid.
  • id + secret authentication
  • redirect_ If the URI is different from the one filled in github configuration, an error will be reported directly.

As the access party, as long as we guarantee that the secret will not be disclosed, redirect_ If the URI is filled in correctly, the security of the user account can be guaranteed.

Cookies and session s

Use the koa session library to handle sessions,

yarn add koa-seassion

Basic use

server.js

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

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

const PORT = 3001

// Wait until the pages directory is compiled and start the service to respond to the request
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

    // Used to encrypt session
  server.keys = ['ssh develop github app']
  const sessionConfig = {
    // Set the key in the browser's cookie
    key: 'sid',
  }
  server.use(session(sessionConfig, server))

  server.use(async (ctx, next) => {
      console.log(`session is ${JSON.stringify(ctx.session)}`)
      next()
  })

  router.get('/set/user', async (ctx) => {
    ctx.session.user = {
      name: 'ssh',
      age: 18
    }
    ctx.body = 'set session successd'
  })

  server.use(router.routes())

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

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

At this time, you can access / set/user to print out the session in the node console.

Koa session does something similar to this, using cookie s to store session id, so that the corresponding session information can be found in the next access interface.

server.use((ctx, next) => {
  if (ctx.cookies.get('sid')) {
    ctx.session = {}
  }

  // Wait for later middleware processing
  await next()

  ctx.cookies.set(xxxx)
})

Using redis to store information

Because the koa session library allows us to customize a store to access sessions, we need to create a new session storage class and provide get, set, and destroy methods.
New server folder, new server/session-store.js file

// Prefix
function getRedisSessionId(sessionId) {
  return `ssid:${sessionId}`
}

export default class RedisSessionStore {
  constructor(client) {
    // node.js Redis client of
    this.client = client
  }

  // Get session data stored in redis
  async get(sessionId) {
    console.log('get sessionId: ', sessionId);
    const id = getRedisSessionId(sessionId)
    // Get instruction corresponding to the command line operation redis to get value
    const data = await this.client.get(id)
    if (!data) {
      return null
    }
    try {
      const result = JSON.parse(data)
      return result
    } catch (err) {
      console.error(err)
    }
  }

  // Storing session data in redis
  async set(sessionId, session, ttl /** Expiration time */) {
    console.log('set sessionId: ', sessionId);
    const id = getRedisSessionId(sessionId)
    let ttlSecond
    if (typeof ttl === 'number') {
      // Millisecond to second
      ttlSecond = Math.ceil(ttl / 1000)
    }

    try {
      const sessionStr = JSON.stringify(session)
      // Call different APIs based on expiration time
      if (ttl) {
        // set with expire
        await this.client.setex(id, ttlSecond, sessionStr)
      } else {
        await this.client.set(id, sessionStr)
      }
    } catch (error) {
      console.error('error: ', error);
    }
  }

  // Delete a session from resid
  // Set in koa ctx.session  =When null, this method is called
  async destroy(sessionId) {
    console.log('destroy sessionId: ', sessionId);
    const id = getRedisSessionId(sessionId)
    await this.client.del(id)
  }
}

In this way, a simplified version of session store is implemented, and then server.js The user-defined store is introduced in, and a node available redis client is instantiated by using the ioredis library and passed to the user-defined store

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')
const session = require('koa-session')
const Redis = require('ioredis')
const RedisSessionStore = require('./server/session-store')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
// Instantiate a redisClient
const redisClient = new Redis()
const PORT = 3001
// Wait until the pages directory is compiled and start the service to respond to the request
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  // Used to encrypt session
  server.keys = ['ssh develop github app']
  const sessionConfig = {
    // Set the key in the browser's cookie
    key: 'sid',
    // Pass custom storage logic to koa session
    store: new RedisSessionStore(redisClient),
  }
  server.use(session(sessionConfig, server))

  router.get('/a/:id', async (ctx) => {
    const { id } = ctx.params
    await handle(ctx.req, ctx.res, {
      pathname: '/a',
      query: {
        id,
      },
    })
    ctx.respond = false
  })

  router.get('/set/user', async (ctx) => {
    // If there is no user information in the session
    ctx.session.user = {
      name: 'ssh',
      age: 18,
    }
    ctx.body = 'set session successd'
  })

  server.use(router.routes())

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

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

After restarting the service, access the / set/user interface, and then enter redis client in the command line. You can see the session id at the beginning of our customized SSID through keys *. Through get ssid:xxx You can see the user information we put in the code.

Github Oauth access

In the page, a tag is introduced into an entry, and the href points to https://github.com/login/oauth/authorize?client_id = your client_id, after logging in successfully, GitHub will help you jump back to the callback address you filled in, and bring the code field in the query parameter. Now, write the process after getting the code.

Firstly, axios is introduced as the request library
yarn add axios

Put the url of the request token in the config.js in

module.exports = {
  github: {
    request_token_url: 'https://github.com/login/oauth/access_token',
    // ... omitted
  },
}

New server/auth.js

// Handle the auth code returned by github
const axios = require('axios')
const config = require('../config')

const { client_id, client_secret, request_token_url } = config.github

module.exports = (server) => {
  server.use(async (ctx, next) => {
    if (ctx.path === '/auth') {
      const { code } = ctx.query
      if (code) {
        // Access to authentication information of Oauth
        const result = await axios({
          method: 'POST',
          url: request_token_url,
          data: {
            client_id,
            client_secret,
            code,
          },
          headers: {
            Accept: 'application/json',
          },
        })

        // github may return error information when status is 200
        if (result.status === 200 && (result.data && !result.data.error)) {
          ctx.session.githubAuth = result.data
        
          const { access_token, token_type } = result.data
          // Get user information
          const { data: userInfo } = await axios({
            method: 'GET',
            url: 'https://api.github.com/user',
            headers: {
              Authorization: `${token_type} ${access_token}`,
            },
          })

          ctx.session.userInfo = userInfo
          // Redirect to home page
          ctx.redirect('/')
        } else {
          ctx.body = `request token failed ${result.data && result.data.error}`
        }
      } else {
        ctx.body = 'code not exist'
      }
    } else {
      await next()
    }
  })
}

After the whole process is completed, we can get token and userInfo, and save them in session

And then server.js Introgression

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')
const session = require('koa-session')
const Redis = require('ioredis')
const koaBody = require('koa-body')
const auth = require('./server/auth')
const api = require('./server/api')
const RedisSessionStore = require('./server/session-store')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
// Instantiate a redisClient
const redisClient = new Redis()
const PORT = 3001
// Wait until the pages directory is compiled and start the service to respond to the request
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  // Used to encrypt session
  server.keys = ['ssh develop github app']
  // Parsing the content of a post request
  server.use(koaBody())

  const sessionConfig = {
    // Set the key in the browser's cookie
    key: 'sid',
    // Pass custom storage logic to koa session
    store: new RedisSessionStore(redisClient),
  }
  server.use(session(sessionConfig, server))

  // Handle github Oauth login
  auth(server)
  // Handle github request broker
  api(server)

  router.get('/a/:id', async (ctx) => {
    const { id } = ctx.params
    await handle(ctx.req, ctx.res, {
      pathname: '/a',
      query: {
        id,
      },
    })
    ctx.respond = false
  })

  router.get('/api/user/info', async (ctx) => {
    const { userInfo } = ctx.session
    if (userInfo) {
      ctx.body = userInfo
      // Set header to return to json
      ctx.set('Content-Type', 'application/json')
    } else {
      ctx.status = 401
      ctx.body = 'Need Login'
    }
  })

  server.use(router.routes())

  server.use(async (ctx) => {
    // Get session in req
    ctx.req.session = ctx.session
    await handle(ctx.req, ctx.res)
    ctx.respond = false
  })

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

The LRU cache strategy that teacher jokcy used in the project, I encapsulated it into a more common tool, which can package getInitialProps of all pages.

lib/utils/client-cache.js

import { useEffect } from 'react'
import LRU from 'lru-cache'

const isServer = typeof window === 'undefined'
const DEFAULT_CACHE_KEY = 'cache'
export default function initClientCache({ lruConfig = {}, genCacheKeyStrate } = {}) {
  // Default 10 minute cache
  const {
    maxAge = 1000 * 60 * 10,
    ...restConfig
  } = lruConfig || {}

  const lruCache = new LRU({
    maxAge,
    ...restConfig,
  })

  function getCacheKey(context) {
    return genCacheKeyStrate ? genCacheKeyStrate(context) : DEFAULT_CACHE_KEY
  }

  function cache(fn) {
    // The server cannot keep the cache, which will be shared among multiple users
    if (isServer) {
      return fn
    }

    return async (...args) => {
      const key = getCacheKey(...args)
      const cached = lruCache.get(key)
      if (cached) {
        return cached
      }
      const result = await fn(...args)
      lruCache.set(key, result)
      return result
    }
  }

  function setCache(key, cachedData) {
    lruCache.set(key, cachedData)
  }

  // Allow external clients to manually set cache data
  function useCache(key, cachedData) {
    useEffect(() => {
      if (!isServer) {
        setCache(key, cachedData)
      }
    }, [])
  }

  return {
    cache,
    useCache,
    setCache,
  }
}

Use example:

/**
*  Use example
*  The key point is the use of cache and useCache
**/

import { Button, Icon, Tabs } from 'antd'
import getConfig from 'next/config'
import Router, { withRouter } from 'next/router'
import { useSelector } from 'react-redux'
import { request } from '../lib/api'
import initCache from '../lib/client-cache'
import Repo from '../components/Repo'

const { publicRuntimeConfig } = getConfig()

const { cache, useCache } = initCache()

const Index = ({ userRepos, starred, router }) => {
  useCache('cache', {
    userRepos,
    starred,
  })
  
  return (
    <div className="root">
      <div className="user-repos">
            {userRepos.map((repo) => (
              <Repo key={repo.id} repo={repo} />
            ))}
      </div>
    </div>
  )
}

Index.getInitialProps = cache(async ({ ctx, reduxStore }) => {
  const { user } = reduxStore.getState()
  if (!user || !user.id) {
    return {}
  }

  const { data: userRepos } = await request(
    {
      url: '/user/repos',
    },
    ctx.req,
    ctx.res,
  )

  const { data: starred } = await request(
    {
      url: '/user/starred',
    },
    ctx.req,
    ctx.res,
  )
  return {
    userRepos,
    starred,
  }
})

export default withRouter(Index)

The principle is to find the cache value of the key in the LRU cache. The key generation strategy can be customized. For example, in the search page, the key can be spliced into a key similar to ssh nextgithub according to the values of various parameters in query. The next time you search for nextgithub under ssh, the results in the cache will be returned directly.

The reason to use useCache is that first, the cache will not be executed on the server, but the first time you open the page getInitialProps, the result will be executed on the server.
When the client renders the component, it can get the return value through the value injected by the server in the window, but there is no such value in the cache at this time.
So we need to manually use useCache to set the cache value, so that the next time we perform client jump, we can use the return value of getInitialProps executed by the server when rendering.

https://blog.csdn.net/weixin_33737134/article/details/91438412
https://juejin.im/post/5d5a54f0e51d4561af16dd19#heading-5

Hooks gives function components the ability to class components

One way data flow state management tool
redux-thunk

Math.round() according to the literal meaning of "around", we can guess that the function is to find a nearby integer, that is, to round
Math.ceil() according to the literal meaning "ceiling" of "ceil", round it up
Math.floor(): according to the literal meaning of "floor", round down

nextjs integration react Redux

WithReduxApp.getInitialProps = async (ctx) => {
  ...
  // Assign reduxStore to ctx
  ctx.reduxStore = reduxStore

  ...
}

Hooks gives function components the ability to class components

Using koa body to process post requests

  const KoaBody = require('koa-body')
  server.use(KoaBody())

Then we can use ctx.request.body Get request parameters

Cache data

let cacheUserRepo;
Index.getInitialProps = () => {
  ...
  cacheUserRepo = userRepo
}

Notes for HOC high level components

export default (Comp) => {
  const Child = ({router, ...rest}) => {
    return (
      <>
        <div>Header</div>
        <div><Comp { ...rest } /></div>
        <div>Footer</div>
      </>
    )
  }
  Child.getInitialProps = aysnc (context) => {
    const { ctx, router } = context
    // Call public data method
    ...
    // matters needing attention
    let compProps = {}
    if (typeof Comp.getInitialProps === 'function') {
      compProps = await Comp.getInitialProps(context)
    }
    return {
      commonData,
      ...compProps
    }
  }
  return withRouter(Child)
}
  • Judge whether the sub component has getInitialProps, and if so, assign the data obtained by the complete context parameter to the container component props through deconstruction
  • Pass the container component props data to < comp { rest}>

Process detail page readme

1,base64_to_utf8
const content = decodeURLComponent(escape(atob(str)))
2. Processing markdown data

// style
import 'github-markdown-css'
import 'MarkdownIt' from 'markdown-it'
const md = new MarkdownIt({html: true, linkify: true})
const html md.render(content)

<div className="markdown-body">
  <div dangerouslySetInnerHtml={{_html: html}} />
</div>

Extract the markdown component and use memo. If the props are unchanged, do not re render. Use memo (() = > {}, [content])

Export static page

Use npm run export after using npm run build

Tags: Session github React Redis

Posted on Mon, 22 Jun 2020 23:32:00 -0400 by p2003morris