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