导航菜单

  • 0.api
  • 0.Async
  • 0.module
  • 1.ES2015
  • 2.Promise
  • 3.Node
  • 4.NodeInstall
  • 5.REPL
  • 6.NodeCore
  • 7.module&NPM
  • 8.Encoding
  • 9.Buffer
  • 10.fs
  • 11.Stream-1
  • 11.Stream-2
  • 11.Stream-3
  • 11.Stream-4
  • 12-Network-2
  • 12.NetWork-3
  • 12.Network-1
  • 13.tcp
  • 14.http-1
  • 14.http-2
  • 15.compress
  • 16.crypto
  • 17.process
  • 18.yargs
  • 19.cache
  • 20.action
  • 21.https
  • 22.cookie
  • 23.session
  • 24.express-1
  • 24.express-2
  • 24.express-3
  • 24.express-4
  • 25.koa-1
  • 26.webpack-1-basic
  • 26.webpack-2-optimize
  • 26.webpack-3-file
  • 26.webpack-4.tapable
  • 26.webpack-5-AST
  • 26.webpack-6-sources
  • 26.webpack-7-loader
  • 26.webpack-8-plugin
  • 26.webpack-9-hand
  • 26.webpack-10-prepare
  • 28.redux
  • 28.redux-jwt-back
  • 28.redux-jwt-front
  • 29.mongodb-1
  • 29.mongodb-2
  • 29.mongodb-3
  • 29.mongodb-4
  • 29.mongodb-5
  • 29.mongodb-6
  • 30.cms-1-mysql
  • 30.cms-2-mysql
  • 30.cms-3-mysql
  • 30.cms-4-nunjucks
  • 30.cms-5-mock
  • 30.cms-6-egg
  • 30.cms-7-api
  • 30.cms-8-roadhog
  • 30.cms-9-yaml
  • 30.cms-10-umi
  • 30.cms-12-dva
  • 30.cms-13-dva-ant
  • 30.cms-14-front
  • 30.cms-15-deploy
  • 31.dva
  • 31.cms-13-dva-antdesign
  • 33.redis
  • 34.unittest
  • 35.jwt
  • 36.websocket-1
  • 36.websocket-2
  • 38.chat-api-1
  • 38.chat-api-2
  • 38.chat-3
  • 38.chat-api-3
  • 38.chat
  • 38.chat2
  • 38.chat2
  • 39.crawl-0
  • 39.crawl-1
  • 39.crawl-2
  • 40.deploy
  • 41.safe
  • 42.test
  • 43.nginx
  • 44.enzyme
  • 45.docker
  • 46.elastic
  • 47.oauth
  • 48.wxpay
  • index
  • 52.UML
  • 53.design
  • index
  • 54.linux
  • 57.ts
  • 56.react-ssr
  • 58.ts_react
  • 59.ketang
  • 59.ketang2
  • 61.1.devops-linux
  • 61.2.devops-vi
  • 61.3.devops-user
  • 61.4.devops-auth
  • 61.5.devops-shell
  • 61.6.devops-install
  • 61.7.devops-system
  • 61.8.devops-service
  • 61.9.devops-network
  • 61.10.devops-nginx
  • 61.11.devops-docker
  • 61.12.devops-jekins
  • 61.13.devops-groovy
  • 61.14.devops-php
  • 61.15.devops-java
  • 61.16.devops-node
  • 61.17.devops-k8s
  • 62.1.react-basic
  • 62.2.react-state
  • 62.3.react-high
  • 62.4.react-optimize
  • 62.5.react-hooks
  • 62.6.react-immutable
  • 62.7.react-mobx
  • 62.8.react-source
  • 63.1.redux
  • 63.2.redux-middleware
  • 63.3.redux-hooks
  • 63.4.redux-saga
  • 63.5.redux-saga-hand
  • 64.1.router
  • 64.2.router-connected
  • 65.1.typescript
  • 65.2.typescript
  • 65.3.typescript
  • 65.4.antd
  • 65.4.definition
  • 66-1.vue-base
  • 66-2.vue-component
  • 66-3.vue-cli3.0
  • 66-4.$message组件
  • 66-5.Form组件
  • 66-6.tree
  • 66-7.vue-router-apply
  • 66-8.axios-apply
  • 66-9.vuex-apply
  • 66-10.jwt-vue
  • 66-11.vue-ssr
  • 66-12.nuxt-apply
  • 66-13.pwa
  • 66-14.vue单元测试
  • 66-15.权限校验
  • 67-1-network
  • 68-2-wireshark
  • 7.npm2
  • 69-hooks
  • 70-deploy
  • 71-hmr
  • 72.deploy
  • 73.import
  • 74.mobile
  • 75.webpack-1.文件分析
  • 75.webpack-2.loader
  • 75.webpack-3.源码流程
  • 75.webpack-4.tapable
  • 75.webpack-5.prepare
  • 75.webpack-6.resolve
  • 75.webpack-7.loader
  • 75.webpack-8.module
  • 75.webpack-9.chunk
  • 75.webpack-10.asset
  • 75.webpack-11.实现
  • 76.react_optimize
  • 77.ts_ketang_back
  • 77.ts_ketang_front
  • 78.vue-domdiff
  • 79.grammar
  • 80.tree
  • 81.axios
  • 82.1.react
  • 82.2.react-high
  • 82.3.react-router
  • 82.4.redux
  • 82.5.redux_middleware
  • 82.6.connected
  • 82.7.saga
  • 82.8.dva
  • 82.8.dva-source
  • 82.9.roadhog
  • 82.10.umi
  • 82.11.antdesign
  • 82.12.ketang-front
  • 82.12.ketang-back
  • 83.upload
  • 84.graphql
  • 85.antpro
  • 86.1.uml
  • 86.2.design
  • 87.postcss
  • 88.react16-1
  • 89.nextjs
  • 90.react-test
  • 91.react-ts
  • 92.rbac
  • 93.tsnode
  • 94.1.JavaScript
  • 94.2.JavaScript
  • 94.3.MODULE
  • 94.4.EventLoop
  • 94.5.文件上传
  • 94.6.https
  • 94.7. nginx
  • 95.1. react
  • 95.2.react
  • 96.1.react16
  • 96.2.fiber
  • 96.3.fiber
  • 97.serverless
  • 98.websocket
  • 100.1.react-basic
  • 101.1.monitor
  • 101.2.monitor
  • 102.java
  • 103.1.webpack-usage
  • 103.2.webpack-bundle
  • 103.3.webpack-ast
  • 103.4.webpack-flow
  • 103.5.webpack-loader
  • 103.6.webpack-tapable
  • 103.7.webpack-plugin
  • 103.8.webpack-optimize1
  • 103.9.webpack-optimize2
  • 103.10.webpack-hand
  • 103.11.webpack-hmr
  • 103.11.webpack5
  • 103.13.splitChunks
  • 103.14.webpack-sourcemap
  • 103.15.webpack-compiler1
  • 103.15.webpack-compiler2
  • 103.16.rollup.1
  • 103.16.rollup.2
  • 103.16.rollup.3
  • 103.16.vite.basic
  • 103.16.vite.source
  • 103.16.vite.plugin
  • 103.16.vite.1
  • 103.16.vite.2
  • 103.17.polyfill
  • 104.1.binary
  • 104.2.binary
  • 105.skeleton
  • 106.1.react
  • 106.2.react_hooks
  • 106.3.react_router
  • 106.4.redux
  • 106.5.redux_middleware
  • 106.6.connected-react-router
  • 106.6.redux-first-history
  • 106.7.redux-saga
  • 106.8.dva
  • 106.9.umi
  • 106.10.ketang
  • 106.11.antdesign
  • 106.12.antpro
  • 106.13.router-6
  • 106.14.ssr
  • 106.15.nextjs
  • 106.16.1.cms
  • 106.16.2.cms
  • 106.16.3.cms
  • 106.16.4.cms
  • 106.16.mobx
  • 106.17.fomily
  • 107.fiber
  • 108.http
  • 109.1.webpack_usage
  • 109.2.webpack_source
  • 109.3.dll
  • 110.nest.js
  • 111.xstate
  • 112.Form
  • 113.redux-saga
  • 114.react+typescript
  • 115.immer
  • 116.pro5
  • 117.css-loader
  • 118.1.umi-core
  • 119.2.module-federation
  • 119.1.module-federation
  • 120.create-react-app
  • 121.react-scripts
  • 122.react-optimize
  • 123.jsx-runtime
  • 124.next.js
  • 125.1.linux
  • 125.2.linux-vi
  • 125.3.linux-user
  • 125.4.linux-auth
  • 125.5.linux-shell
  • 125.6.linux-install
  • 125.7.linux-system
  • 125.8.linux-service
  • 125.9.linux-network
  • 125.10.nginx
  • 125.11.docker
  • 125.12.ci
  • 125.13.k8s
  • 125.14.k8s
  • 125.15.k8s
  • 125.16.k8s
  • 126.11.react-1
  • 126.12.react-2
  • 126.12.react-3
  • 126.12.react-4
  • 126.12.react-5
  • 126.12.react-6
  • 126.12.react-7
  • 126.12.react-8
  • 127.frontend
  • 128.rollup
  • 129.px2rem-loader
  • 130.health
  • 131.hooks
  • 132.keepalive
  • 133.vue-cli
  • 134.react18
  • 134.2.react18
  • 134.3.react18
  • 135.function
  • 136.toolkit
  • 137.lerna
  • 138.create-vite
  • 139.cli
  • 140.antd
  • 141.react-dnd
  • 142.1.link
  • 143.1.gulp
  • 143.2.stream
  • 143.3.gulp
  • 144.1.closure
  • 144.2.v8
  • 144.3.gc
  • 145.react-router-v6
  • 146.browser
  • 147.lighthouse
  • 148.1.basic
  • 148.2.basic
  • 148.3.basic
  • 148.4.basic
  • 148.5.basic
  • 149.1.vite
  • 149.2.vite
  • 149.3.vite
  • 149.4.vite
  • 150.react-window
  • 151.react-query
  • 152.useRequest
  • 153.transition
  • 154.emotion
  • 155.1.formily
  • 155.2.formily
  • 155.3.formily
  • 155.3.1.mobx.usage
  • 155.3.2.mobx.source
  • 156.vue-loader
  • 103.11.mf
  • 157.1.react18
  • 158.umi4
  • 159.rxjs
  • 159.rxjs2
  • 160.bff
  • 161.zustand
  • 162.vscode
  • 163.emp
  • 164.cors
  • 1. 什么是同构
  • 2.Next.js
  • 3.项目初始化
    • 3.1 创建项目
    • 3.2 添加脚本
    • 3.3 访问服务
      • 3.3.1 index.js
      • 3.3.2 pages\user.js
      • 3.3.3 访问
  • 4.跑通路由和样式
    • 4.1 知识点
    • 4.2 pages_app.js
    • 4.3 _app.module.css
    • 4.4 global.css
    • 4.5 pages\index.js
    • 4.6 pages\user.js
    • 4.7 pages\profile.js
  • 5.二级路由
    • 5.1 知识点
    • 5.2 执行顺序
      • 5.2.1 后台顺序
      • 5.2.2 前台顺序
      • 5.2.3 _app.js
      • 5.2.4 user\index.js
      • 5.2.5 user\list.js
      • 5.2.6 user\add.js
      • 5.2.7 user\detail[id].js
  • 6.调用接口
    • 6.1 方法执行顺序
      • 6.1.1 首次访问
      • 6.1.2 切换路由
    • 6.2 pages_app.js
    • 6.3 pages\user\list.js
    • 6.4 pages\user\add.js
    • 6.5 [id].js
    • 6.6 utils\request.js
  • 7.懒加载
    • 7.1 [id].js
    • 7.2 components\UserInfo.js
    • 7.3 jsconfig.json
  • 8.集成 redux
    • 8.1 知识点
    • 8.2 pages_app.js
    • 8.3 action-types.js
    • 8.4 reducer.js
    • 8.5 store\index.js
    • 8.6 login.js
  • 9.loading
    • 9.1 _app.js
  • 10.受保护路由
    • 10.1 profile.js
  • 11.自定义Document
    • 11.1 pages_document.js
    • 11.2 pages\index.js
  • 12. getServerSideProps
    • 12.1 list.js
  • 13. getStaticProps
    • 13.1 pages\user\list.js
    • 13.2 pages\user\detail[id].js
  • 14.布署
    • 14.1 直接布署
    • 14.2 集成express布署
      • 14.2.1 编译
      • 14.2.2 start.js
  • 15.api.js

1. 什么是同构 #

  • 同构的项目支持客户端渲染和服务器端渲染
  • 客户端渲染缺点
    • 首屏速度加载慢
    • 不支持 SEO 和搜索引擎优化
    • 首页需要通过请求初始化数据

renderflow

2.Next.js #

  • Next.js 英文文档,Next.js 中文文档 是一个轻量级的 React 服务端渲染应用框架
  • 默认支持服务端渲染
  • 自动根据页面进行代码分割
  • 基于页面的客户端路由方案
  • 基于 Webpack 的开发环境,支持热模块替换
  • 可以跟Koa或者其它Node.js服务器进行集成
  • 支持 Babel 和 Webpack 的配置项定制
  • 静态文件服务 public

3.项目初始化 #

3.1 创建项目 #

mkdir zhufengnextjs
cd zhufengnextjs
npm init -y
npm install react  react-dom  next  redux react-redux  --save
npm install axios express  body-parser  cors express-session connect-mongo mongoose koa koa-router --save

3.2 添加脚本 #

package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

.gitignore

3.3 访问服务 #

  • 以 ./pages 作为服务端的渲染和索引的根目录
    • pages 是 next.js 中非常重要的一个目录,其中每一个 js 文件就代表一个页面,但是有两个例外,_app.js 和 _document.js
  • next.js 会将 pages 下的 js 文件根据其路径名和文件名自动生成对应的路由
  • pages组件代码自动分割
  • next.js 项目运行之后会自动生成.next目录

3.3.1 index.js #

function Home() {
  return <div>Home</div>;
}
export default Home;

3.3.2 pages\user.js #

pages\user.js

function User() {
  return <div>User</div>;
}
export default User;

3.3.3 访问 #

npm run dev
curl http://localhost:3000/

4.跑通路由和样式 #

4.1 知识点 #

  • 绑定 styled-jsx 来生成独立作用域的 CSS
  • 如何支持本地和全局css
  • 路由的使用和两种跳转路径的方法

4.2 pages_app.js #

  • App组件是每个页面的根组件,页面切换时 App 不会销毁,但是里面的页面组件会销毁,因此可以利用这个来设置全局属性和样式
  • 全局 css 只能写在这里,否则会报错
    • 当页面变化时保持页面布局
    • 当路由变化时保持页面状态
    • 注入额外数据到页面里

pages_app.js

import App from "next/app";
import Link from "next/link";
import _appStyle from "./_app.module.css";
import "../styles/global.css";
class LayoutApp extends App {
  render() {
    let { Component } = this.props;
    return (
      <div>
        <style jsx>
          {`
            li {
              display: inline-block;
              margin-left: 10px;
              line-height: 31px;
            }
          `}
        </style>
        <header>
          <img src="/images/logo.png" className={_appStyle.logo} />
          <ul>
            <li>
              <Link href="/">首页</Link>
            </li>
            <li>
              <Link href="/user">用户管理</Link>
            </li>
            <li>
              <Link href="/profile">个人中心</Link>
            </li>
          </ul>
        </header>
        <Component />
        <footer style={{ textAlign: "center" }}>@copyright 前端架构</footer>
      </div>
    );
  }
}
export default LayoutApp;

4.3 _app.module.css #

pages_app.module.css

.logo {
  width: 120px;
  height: 31px;
  float: left;
}

4.4 global.css #

styles\global.css

html,
body {
  padding: 0;
  margin: 0;
}

4.5 pages\index.js #

pages\index.js

function Home() {
  return <div>Home</div>;
}
export default Home;

4.6 pages\user.js #

pages\user.js

import Link from "next/link";
function User() {
  return (
    <div>
      <p>User</p>
      <Link href="/">首页</Link>
    </div>
  );
}
export default User;

4.7 pages\profile.js #

pages\profile.js

import router from "next/router";
function Profile() {
  return (
    <div>
      <p>Profile</p>
      <button onClick={() => router.back()}>返回</button>
    </div>
  );
}
export default Profile;

5.二级路由 #

5.1 知识点 #

  • 支持二级路由
  • 实现二级布局组件
  • 路由跳转传递参数
  • 页面组件通过getInitialProps获取数据

5.2 执行顺序 #

5.2.1 后台顺序 #

  • LayoutApp getInitialProps
  • UseList getInitialProps
  • LayoutApp constructor
  • UseList constructor

5.2.2 前台顺序 #

  • 初次渲染
    • LayoutApp constructor
    • UseList constructor
  • 路由切换
    • LayoutApp getInitialProps
    • UseList getInitialProps
    • UseList constructor

5.2.3 _app.js #

pages_app.js

import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
class LayoutApp extends App {
+ static async getInitialProps({ Component,ctx }) {
+   let pageProps = {};
+   if (Component.getInitialProps)
+     pageProps = await Component.getInitialProps(ctx);
+   return { pageProps };
+ }
  render() {
+   let { Component, pageProps } = this.props;
    return (
      <div>
        <style jsx>
          {
            `li{
                  display:inline-block;
                  margin-left:10px;
                  line-height:31px;
             }`
          }
        </style>
        <header>
          <img src="/images/logo.png" className={_appStyle.logo} />
          <ul>
            <li><Link href="/">首页</Link></li>
+           <li><Link href="/user/list" >用户管理</Link></li>
            <li><Link href="/profile">个人中心</Link></li>
          </ul>
        </header>
+       <Component {...pageProps} />
        <footer style={{ textAlign: 'center' }} >@copyright 前端架构</footer>
      </div>
    )
  }
}
export default LayoutApp;

5.2.4 user\index.js #

pages\user\index.js

import Link from "next/link";
function UserLayout(props) {
  return (
    <div>
      <div>
        <ul>
          <li>
            <Link href="/user/list">
              <a>用户列表</a>
            </Link>
          </li>
          <li>
            <Link href="/user/add">
              <a>添加用户</a>
            </Link>
          </li>
        </ul>
        <div>{props.children}</div>
      </div>
    </div>
  );
}
export default UserLayout;

5.2.5 user\list.js #

pages\user\list.js

import Link from "next/link";
import UserLayout from "./";
function UseList(props) {
  return (
    <UserLayout>
      <ul>
        {props.list.map((user) => (
          <li key={user.id}>
            <Link href={`/user/detail/${user.id}`}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </UserLayout>
  );
}
UseList.getInitialProps = async () => {
  let list = [
    { id: 1, name: "张三" },
    { id: 2, name: "李四" },
  ];
  return { list };
};
export default UseList;

5.2.6 user\add.js #

pages\user\add.js

import UserLayout from "./";
import React from "react";
function UserAdd() {
  let nameRef = React.useRef();
  let passwordRef = React.useRef();
  let handleSubmit = (event) => {
    event.preventDefault();
    let user = {
      name: nameRef.current.value,
      password: passwordRef.current.value,
    };
  };
  return (
    <UserLayout>
      <form onSubmit={handleSubmit}>
        用户名:
        <input ref={nameRef} />
        密码:
        <input ref={passwordRef} />
        <button type="submit">添加</button>
      </form>
    </UserLayout>
  );
}

export default UserAdd;

5.2.7 user\detail[id].js #

pages\user\detail[id].js

import React from "react";
import UserLayout from "../";
function UserDetail(props) {
  return (
    <UserLayout>
      <p>ID:{props.user.id}</p>
    </UserLayout>
  );
}
UserDetail.getInitialProps = async (ctx) => {
  return { user: { id: ctx.query.id } };
};
export default UserDetail;

6.调用接口 #

  • 当服务渲染时,getInitialProps将会把数据序列化,就像JSON.stringify
  • 所以确保getInitialProps返回的是一个普通 JS 对象,而不是 Date, Map 或 Set 类型
  • 当页面初始化加载时,getInitialProps只会加载在服务端。只有当路由跳转(Link 组件跳转或 API 方法跳转)时,客户端才会执行getInitialProps
  • getInitialProps将不能使用在子组件中。只能使用在pages页面中

6.1 方法执行顺序 #

6.1.1 首次访问 #

服务器端

LayoutApp getInitialProps 获取LayoutApp初始属性
UseList getInitialProps 调用页面组件的getInitialProps方法
LayoutApp constructor 根据属性创建LayoutAPp的实例
LayoutApp render  调用此实例的render方法,返回react元素
UseList constructor 创建子组件
UseList render 渲染子组件
  • 然后会把HTML结构和LayoutApp属性对象序列化后发给客户端
  • 客户端再把LayoutApp属性反序列化后变成对象

客户端

LayoutApp constructor
LayoutApp render
UseList constructor
UseList render

6.1.2 切换路由 #

客户端

LayoutApp getInitialProps
UseList getInitialProps
LayoutApp render
UseList constructor
UseList render

6.2 pages_app.js #

pages_app.js

import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
class LayoutApp extends App {
+ constructor(props) {
+   super(props)
+   console.log('LayoutApp constructor');
+ }
  static async getInitialProps({ Component, ctx }) {
+   console.log('LayoutApp getInitialProps');
    let pageProps = {};
    if (Component.getInitialProps)
      pageProps = await Component.getInitialProps(ctx);
    return { pageProps };
  }
  render() {
    console.log('LayoutApp render');
    let { Component, pageProps } = this.props;
    return (
      <div>
        <style jsx>
          {
            `li{
                  display:inline-block;
                  margin-left:10px;
                  line-height:31px;
             }`
          }
        </style>
        <header>
          <img src="/images/logo.png" className={_appStyle.logo} />
          <ul>
            <li><Link href="/">首页</Link></li>
            <li><Link href="/user/list" >用户管理</Link></li>
            <li><Link href="/profile">个人中心</Link></li>
          </ul>
        </header>
        <Component {...pageProps} />
        <footer style={{ textAlign: 'center' }} >@copyright 前端架构</footer>
      </div>
    )
  }
}
export default LayoutApp;

6.3 pages\user\list.js #

pages\user\list.js

import React from 'react';
import Link from 'next/link';
import UserLayout from './';
+import request from '../../utils/request';
class UseList extends React.Component {
+ constructor(props) {
+   super(props);
+   console.log('UseList constructor');
+ }
  render() {
    console.log('UseList render');
    return (
      <UserLayout>
        <ul>
          {
            this.props.list.map((user) => (
              <li key={user.id}>
                <Link href={`/user/detail/${user.id}`}>{user.name}</Link>
              </li>
            ))
          }
        </ul>
      </UserLayout>
    )
  }
}

UseList.getInitialProps = async () => {
+ console.log('UseList getInitialProps');
  let response = await request({ url: '/api/users', method: 'GET' }).then(res => res.data);
  return { list: response.data };
}
export default UseList;

6.4 pages\user\add.js #

pages\user\add.js

import UserLayout from './';
import React from 'react';
+import request from '../../utils/request';
import router from 'next/router'
function UserAdd() {
  let nameRef = React.useRef();
  let passwordRef = React.useRef();
  let handleSubmit = async (event) => {
    event.preventDefault();
    const user = { name: nameRef.current.value, password: passwordRef.current.value };
+   let response = await request.post('/api/register', user).then(res => res.data);
+   if (response.success) {
+     router.push('/user/list');
+   } else {
+     alert('添加用户失败');
+   }
  }
  return (
    <UserLayout>
      <form onSubmit={handleSubmit}>
        用户名:<input ref={nameRef} />
        密码:<input ref={passwordRef} />
        <button type="submit">添加</button>
      </form>
    </UserLayout>
  )
}

export default UserAdd;

6.5 [id].js #

pages\user\detail[id].js

import React from 'react';
import UserLayout from '../';
+import request from '../../../utils/request';
function UserDetail(props) {
  return (
    <UserLayout>
      <p>ID:{props.user.id}</p>
      <p>NAME:{props.user.name}</p>
    </UserLayout>
  )
}
UserDetail.getInitialProps = async (ctx) => {
+ let response = await request({ url: `/api/users/${ctx.query.id}`, method: 'GET' }).then(res => res.data);
+ return { user: response.data };
}
export default UserDetail;

6.6 utils\request.js #

utils\request.js

import axios from "axios";
axios.defaults.withCredentials = true;
const instance = axios.create({
  baseURL: "http://localhost:5000",
});
export default instance;

7.懒加载 #

7.1 [id].js #

pages\user\detail[id].js

import React from 'react';
import UserLayout from '../';
import request from '../../../utils/request';
+import dynamic from 'next/dynamic';
//import UserInfo from '@/components/UserInfo';
+const DynamicUserInfo = dynamic(() => import('@/components/UserInfo'));
function UserDetail(props) {
  const [show, setShow] = React.useState(false);
  return (
    <UserLayout>
      <p>ID:{props.user && props.user.id}</p>
+     <button onClick={() => setShow(!show)}>显示/隐藏</button>
+     {
+       show && props.user && <DynamicUserInfo user={props.user} />
+     }
+   </UserLayout>
  )
}
UserDetail.getInitialProps = async (ctx) => {
  let response = await request({ url: `/api/users/${ctx.query.id}`, method: 'GET' }).then(res => res.data);
  return { user: response.data };
}
export default UserDetail;

7.2 components\UserInfo.js #

UserInfo.js

import React, { useState } from "react";
function UserInfo(props) {
  const [createdAt, setCreatedAt] = useState(props.user.createdAt);
  async function changeFormat() {
    const moment = await import("moment");
    setCreatedAt(moment.default(createdAt).fromNow());
  }
  return (
    <>
      <p>NAME:{props.user.name}</p>
      <p>创建时间:{createdAt}</p>
      <button onClick={changeFormat}>切换为相对时间</button>
    </>
  );
}
export default UserInfo;

7.3 jsconfig.json #

jsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["/*"]
    }
  }
}

8.集成 redux #

8.1 知识点 #

  • 集成redux
  • cookie保存和存递

8.2 pages_app.js #

pages_app.js

import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
+import { Provider } from 'react-redux';
+import request from '../utils/request';
+import createStore from '../store';
+import * as types from '../store/action-types';
+function getStore(initialState) {
+  if (typeof window === 'undefined') {
+    return createStore(initialState);//如果是服务器端,每次都返回新的仓库
+  } else {
+    if (!window._REDUX_STORE_) {
+      window._REDUX_STORE_ = createStore(initialState);
+    }
+    return window._REDUX_STORE_;
+  }
+}
class LayoutApp extends App {
  constructor(props) {
    super(props)
+   this.store = getStore(props.initialState);
    console.log('LayoutApp constructor');
  }
  static async getInitialProps({ Component, ctx }) {
+   console.log('LayoutApp getInitialProps');
+   let store = getStore();//1.后台创建新仓库  5.每次切换路由都会执行此方法获取老仓库
+   if (typeof window == 'undefined') {//2.后台获取用户信息
+     let options = { url: '/api/validate' };
+     if (ctx.req && ctx.req.headers.cookie) { 
+       options.headers = options.headers || {};
+       options.headers.cookie = ctx.req.headers.cookie;
+     }
+     let response = await request(options).then(res => res.data);
+     if (response.success) {
+       store.dispatch({ type: types.SET_USER_INFO, payload: response.data });
+     }
+   }
+   let pageProps = {};
+   if (Component.getInitialProps)
+     pageProps = await Component.getInitialProps(ctx);
+   const props = { pageProps };
+   if (typeof window == 'undefined') {//后台获取用赋值状态
+     props.initialState = store.getState();
+   }
+   return props;
  }
  render() {
    console.log('LayoutApp render');
    let state = this.store.getState();
    let { Component, pageProps } = this.props;
    return (
+     <Provider store={this.store}>
        <style jsx>
          {
            `li{
                  display:inline-block;
                  margin-left:10px;
                  line-height:31px;
            }`
          }
        </style>
        <header>
          <img src="/images/logo.png" className={_appStyle.logo} />
          <ul>
            <li><Link href="/">首页</Link></li>
            <li><Link href="/user/list" >用户管理</Link></li>
            <li><Link href="/profile">个人中心</Link></li>
            <li>
              {
+               state.currentUser ? <span>{state.currentUser.name}</span> : <Link href="/login">登录</Link>
              }
            </li>
          </ul>
        </header>
        <Component {...pageProps} />
        <footer style={{ textAlign: 'center' }} >@copyright 前端架构</footer>
+     </Provider>
    )
  }
}
export default LayoutApp;

8.3 action-types.js #

store\action-types.js

export const SET_USER_INFO = 'SET_USER_INFO';//设置用户信息

8.4 reducer.js #

store\reducer.js

import * as types from './action-types';
let initState = {
    currentUser: null
}
const reducer = (state = initState, action) => {
    switch (action.type) {
        case types.SET_USER_INFO:
            return { currentUser: action.payload }
        default:
            return state;
    }
}
export default reducer;

8.5 store\index.js #

store\index.js

import { createStore } from 'redux';
import reducer from './reducer';

export default function (initialState) {
    return createStore(reducer, initialState);
}

8.6 login.js #

pages\login.js

import React from 'react';
import request from '@/utils/request';
import router from 'next/router';
import { connect } from 'react-redux';
import * as types from '@/store/action-types';
function Login(props) {
    let nameRef = React.useRef();
    let passwordRef = React.useRef();
    let handleSubmit = async (event) => {
        event.preventDefault();
        let user = { name: nameRef.current.value, password: passwordRef.current.value };
        let response = await request.post('/api/login', user).then(res => res.data);
        if (response.success) {
            props.dispatch({ type: types.SET_USER_INFO, payload: response.data });
            router.push('/');
        } else {
            alert('登录失败');
        }
    }
    return (
        <form onSubmit={handleSubmit}>
            用户名:<input ref={nameRef} />
            密码:<input ref={passwordRef} />
            <button type="submit">登录</button>
        </form>
    )
}

export default connect(state => state)(Login);

9.loading #

  • 路由事件
事件 触发时机
routeChangeStart(url) 路由开始切换时触发
routeChangeComplete(url) 完成路由切换时触发
routeChangeError(err, url) 路由切换报错时触发
beforeHistoryChange(url) 浏览器 history 模式开始切换时触发
hashChangeStart(url) 开始切换 hash 值但是没有切换页面路由时触发
hashChangeComplete(url) 完成切换 hash 值但是没有切换页面路由时触发

9.1 _app.js #

pages_app.js

import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
import { Provider } from 'react-redux';
import request from '../utils/request';
import createStore from '../store';
import * as types from '../store/action-types';
+import router from 'next/router';
function getStore(initialState) {
  if (typeof window === 'undefined') {
    return createStore(initialState);//如果是服务器端,每次都返回新的仓库
  } else {
    if (!window._REDUX_STORE_) {
      window._REDUX_STORE_ = createStore(initialState);
    }
    return window._REDUX_STORE_;
  }
}
class LayoutApp extends App {
  constructor(props) {
    super(props)
+   this.state = { loading: false }
    this.store = getStore(props.initialState);
    console.log('LayoutApp constructor');
  }
  static async getInitialProps({ Component, ctx }) {
    console.log('LayoutApp getInitialProps');
    let store = getStore();//1.后台创建新仓库  5.每次切换路由都会执行此方法获取老仓库
    if (typeof window == 'undefined') {//2.后台获取用户信息
      let options = { url: '/api/validate' };
      if (ctx.req && ctx.req.headers.cookie) {
        options.headers = options.headers || {};
        options.headers.cookie = ctx.req.headers.cookie;
      }
      let response = await request(options).then(res => res.data);
      if (response.success) {
        store.dispatch({ type: types.SET_USER_INFO, payload: response.data });
      }
    }
    let pageProps = {};
    if (Component.getInitialProps)
      pageProps = await Component.getInitialProps(ctx);
    const props = { pageProps };
    if (typeof window == 'undefined') {//后台获取用赋值状态
      props.initialState = store.getState();
    }
    return props;
  }
+  componentDidMount() {
+    this.routeChangeStart = (url) => {
+      this.setState({ loading: true });
+    };
+    router.events.on('routeChangeStart', this.routeChangeStart);
+    this.routeChangeComplete = (url) => {
+      this.setState({ loading: false });
+    };
+    router.events.on('routeChangeComplete', this.routeChangeComplete);
+  }
+  componentWillUnmount() {
+    router.events.off('routeChangeStart', this.routeChangeStart)
+    router.events.off('routeChangeStart', this.routeChangeComplete)
+  }
  render() {
    console.log('LayoutApp render');
    let state = this.store.getState();
    let { Component, pageProps } = this.props;
    return (
      <Provider store={this.store}>
        <style jsx>
          {
            `li{
                  display:inline-block;
                  margin-left:10px;
                  line-height:31px;
            }`
          }
        </style>
        <header>
          <img src="/images/logo.png" className={_appStyle.logo} />
          <ul>
            <li><Link href="/">首页</Link></li>
            <li><Link href="/user/list" >用户管理</Link></li>
            <li><Link href="/profile">个人中心</Link></li>
            <li>
              {
                state.currentUser ? <span>{state.currentUser.name}</span> : <Link href="/login">登录</Link>
              }
            </li>
          </ul>
        </header>
        {
+         this.state.loading ? <div>切换中......</div> : <Component {...pageProps} />
        }
        <footer style={{ textAlign: 'center' }} >@copyright 前端架构</footer>
      </Provider>
    )
  }
}
export default LayoutApp;

10.受保护路由 #

10.1 profile.js #

pages\profile.js

import router from 'next/router';
import { connect } from 'react-redux';
import request from '../utils/request';
function Profile(props) {
    let { currentUser } = props;
    return (
        <div>
            <p>当前登录用户:{currentUser.name}</p>
            <button onClick={() => router.back()}>返回</button>
        </div>
    )
}
Profile.getInitialProps = async function (ctx) {
    let options = { url: '/api/validate' };
    if (ctx.req && ctx.req.headers.cookie) {
        options.headers = options.headers || {};
        options.headers.cookie = ctx.req.headers.cookie;
    }
    let response = await request(options).then(res=>res.data);
    if (response.success) {
        return {currentUser:response.data};
    } else {
        if (ctx.req) {
            ctx.res.writeHead(303, { Location: '/login' })
            ctx.res.end()
        } else {
            router.push('/login');
        }
        return {};
    }
}

const WrappedProfile = connect(
    state => state
)(Profile);
export default WrappedProfile;

11.自定义Document #

11.1 pages_document.js #

pages_document.js

import Document, { Html, Head, Main, NextScript } from 'next/document';
class CustomDocument extends Document {
    static async getInitialProps(ctx) {
        const props = await Document.getInitialProps(ctx);
        return { ...props };
    }
    render() {
        return (
            <Html>
                <Head>
                    <style>
                        {
                            `
                            *{
                                padding:0;
                                margin:0;
                            }
                            `
                        }
                    </style>
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        )
    }
}
export default CustomDocument;

11.2 pages\index.js #

pages\index.js

import Head from 'next/head'
export default function (props) {
    return (
        <div>
              <Head>
                <title>首页</title>
                <meta name="description" content="这是首页" />
             </Head>
            <p>Home</p>
        </div>
    )
}

12. getServerSideProps #

  • data-fetching
    • getServerSideProps (Server-side Rendering): Fetch data on each request

12.1 list.js #

pages\user\list.js

import React from 'react';
import Link from 'next/link';
import UserLayout from './';
import request from '@/utils/request';
class UseList extends React.Component {
  constructor(props) {
    super(props);
    console.log('UseList constructor');
  }
  render() {
    console.log('UseList render');
    return (
      <UserLayout>
        <ul>
          {
            this.props.list.map((user) => (
              <li key={user.id}>
                <Link href={`/user/detail/${user.id}`}>{user.name}</Link>
              </li>
            ))
          }
        </ul>
      </UserLayout>
    )
  }
}

-UseList.getInitialProps = async () => {
-  console.log('UseList getInitialProps');
-  let response = await request({ url: '/api/users', method: 'GET' }).then(res => res.data);
-  return { list: response.data };
-} 
//每个请求都会调用
+export async function getServerSideProps() {
+  const res = await request.get('http://localhost:5000/api/users')
+  return {
+    props: {
+      list: res.data.data
+    },
+  }
+}
export default UseList;

13. getStaticProps #

  • data-fetching
    • getStaticProps (Static Generation): Fetch data at build time
    • getStaticPaths (Static Generation): Specify dynamic routes to pre-render based on data

13.1 pages\user\list.js #

pages\user\list.js

import React from 'react';
import Link from 'next/link';
import UserLayout from './';
import request from '@/utils/request';

class UseList extends React.Component {
  constructor(props) {
    super(props);
    console.log('UseList constructor');
  }
  render() {
    console.log('UseList render');
    return (
      <UserLayout>
        <ul>
          {
            this.props.list.map((user) => (
              <li key={user.id}>
                <Link href={`/user/detail/${user.id}`}>{user.name}</Link>
              </li>
            ))
          }
        </ul>
      </UserLayout>
    )
  }
}

/* UseList.getInitialProps = async () => {
  console.log('UseList getInitialProps');
  let response = await request({ url: '/api/users', method: 'GET' }).then(res => res.data);
  return { list: response.data };
} */
//每个请求都会调用
export async function getServerSideProps() {
  const res = await request.get('http://localhost:5000/api/users')
  return {
    props: {
      list: res.data.data
    },
  }
}

// 这个函数在编译阶段被调用
export async function getStaticProps() {
  const res = await request.get('http://localhost:5000/api/users');
  return {
    props: {
      list: res.data.data
    },
  }
}
export default UseList;

13.2 pages\user\detail[id].js #

pages\user\detail[id].js

import React from 'react';
import UserLayout from '../';
import request from '@/utils/request';
import dynamic from 'next/dynamic';
//import UserInfo from '@/components/UserInfo';
const DynamicUserInfo = dynamic(() => import('@/components/UserInfo'));
function UserDetail(props) {
  const [show, setShow] = React.useState(false);
  return (
    <UserLayout>
      <p>ID:{props.user && props.user.id}</p>
      <button onClick={() => setShow(!show)}>显示/隐藏</button>
      {
        show && props.user && <DynamicUserInfo user={props.user} />
      }
    </UserLayout>
  )
}
UserDetail.getInitialProps = async (ctx) => {
  let response = await request.get(`/api/users/${ctx.query.id}`)
  return { user: response.data.data };
}
export async function getStaticPaths() {
  const res = await request.get('http://localhost:5000/api/users');
  const users = res.data;
  const paths = users.map(user => `/user/detail/${user.id}`);
  return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
  console.log('params', params, new Date().toTimeString());
  const res = await request.get(`http://localhost:5000/api/users/${params.id}`);
  return {
    props: {
      user: res.data
    }
  }
}
export default UserDetail;

14.布署 #

14.1 直接布署 #

npm run build
npm run start

14.2 集成express布署 #

14.2.1 编译 #

npm run build

14.2.2 start.js #

start.js

const next = require('next');
const app = next({ dev:false });
const handler = app.getRequestHandler();
app.prepare().then(() => {
    let express = require("express");
    let bodyParser = require("body-parser");
    let {UserModel} = require('./model');
    let session = require("express-session");
    let config = require('./config');
    let MongoStore = require('connect-mongo')(session);
    let app = express();
    //.......
    app.get('*', async (req, res) => {
        await handler(req, res);
    })
    app.listen(5000, () => {
        console.log('服务器在5000端口启动!');
    });
});

15.api.js #

const express = require('express');
const cors = require('cors');
const session = require('express-session');
const app = express();
app.use(
  cors({
    origin: ['http://localhost:3000'],
    credentials: true,
    allowedHeaders: "Content-Type,Authorization",
    methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS"
  })
);
app.use(session({
  saveUninitialized: true,
  resave: true,
  secret: 'zhufeng'
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const users = [];
app.get('/api/users', (req, res) => {
  res.json({
    success: true,
    data: users
  });
});
app.post('/api/register', (req, res) => {
  const user = req.body;
  user.id = Date.now() + "";
  user.createdAt = new Date().toISOString();
  users.push(user);
  res.json({
    success: true,
    data: user
  })
});
app.get('/api/users/:id', (req, res) => {
  const id = req.params.id;
  const user = users.find(user => user.id === id);
  res.json({
    success: true,
    data: user
  })
});
app.post('/api/login', (req, res) => {
  const user = req.body;
  req.session.user = user;
  res.json({
    success: true,
    data: user
  })
});

app.get('/api/logout', (req, res) => {
  req.session.user = null;
  res.json({
    success: true,
    data: null
  })
});
app.get('/api/validate', (req, res) => {
  const user = req.session.user;
  if (user) {
    res.json({
      success: true,
      data: user
    })
  } else {
    res.json({
      success: false,
      error: `用户未登录`
    })
  }
});
app.listen(5000, () => console.log('api server started on port 5000'));

访问验证

请输入访问令牌

Token不正确,请重新输入