1. 同构 #

renderflow

2.Next.js #

3.初始化 #

3.1 创建项目 #

mkdir zhufengnextjs
cd zhufengnextjs
npm init -y
yarn add --dev typescript react @types/react react-dom @types/node next axios
mkdir pages

3.2 创建脚本 #

package.json

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

https://github.com/zeit/next.js/blob/canary/.gitignore

3.3 访问 #

index.tsx

export default function () {
    return (
        <div>Home</div>
    )
}
curl http://localhost:3000/

4.跑通路由 #

4.1 知识点 #

4.2 安装依赖包 #

yarn add @zeit/next-css antd babel-plugin-import

4.3 .babelrc #

.babelrc

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

4.4 next.config.js #

next.config.js

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

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

module.exports = withCSS({})

4.5 pages_app.tsx #

pages_app.tsx

import App, { Container } from 'next/app';
import Link from 'next/link';
import { Layout, Menu, Icon } from 'antd';
import 'antd/dist/antd.css';
import { withRouter } from 'next/router';
const { Header, Footer } = Layout;
class LayoutApp extends App<any> {
    render() {
        let { Component } = this.props as any;
        let pathname = this.props.router.pathname;
        pathname = '/' + (pathname.split('/')[1]);
        return (
            <Container>
                <style jsx>
                     {`a{display:inline-block!important;}`}
                </style>
                <Layout>
                    <Header className="header">
                       <img src="/images/jglogo.png" style={{ width: 120, height: 31, margin: '16px 24px 16px 0px', float: 'left' }} />
                        <Menu theme="dark" mode="horizontal" style={{ lineHeight: '64px', display: 'inline-block' }}
                            selectedKeys={[pathname]} 
                            defaultSelectedKeys={[pathname]}>
                            <Menu.Item key="/">
                                <Icon type="home" /><Link href="/"><a>首页</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/user">
                                <Icon type="/user" /> <Link href="/user" ><a>用户管理</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/profile">
                                <Icon type="profile" /><Link href="/profile"><a>个人中心</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/login">
                                <Icon type="login" /><Link href="/login"><a>登录</a></Link>
                            </Menu.Item>
                        </Menu>
                    </Header>
                    <Component />
                    <Footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</Footer>
                </Layout>
            </Container>
        )
    }
}
export default withRouter(LayoutApp);

4.6 pages\index.tsx #

pages\index.tsx

import router from 'next/router'
import { Button, Layout } from 'antd';
const { Content } = Layout;
export default function () {
    return (
        <Content  style={{ margin: '20px auto' }}>
            <div>Home</div>
            <Button onClick={() => router.push('/user')}>/user</Button>
        </Content>

    )
}

4.7 pages\user.tsx #

pages\user.tsx

import router from 'next/router';
import { Button, Layout } from 'antd';
const { Content } = Layout;
export default function () {
    return (
        <Content  style={{ margin: '20px auto' }}>
            <div>User</div>
            <Button onClick={() => router.push('/profile')}>/profile</Button>
        </Content>
    )
}

4.8 pages\profile.tsx #

pages\profile.tsx

import router from 'next/router';
import { Button, Layout } from 'antd';
const { Content } = Layout;
export default function () {
    return (
        <Content  style={{ margin: '20px auto' }}>
            <div>Profile</div>
            <Button onClick={() => router.back()}>返回</Button>
        </Content>
    )
}

5.二级路由 #

5.1 知识点 #

5.2 执行顺序 #

5.2.1 后台顺序 #

severflow.png

5.2.2 前台顺序 #

clientflow.png

5.3 pages_app.tsx #

pages_app.tsx

import App from 'next/app';
import Link from 'next/link';
import { Layout, Menu, Icon } from 'antd';
import 'antd/dist/antd.css';
import { withRouter } from 'next/router';
const { Header, Footer } = Layout;
class LayoutApp extends App<any> {
+    static async getInitialProps(params) {
+        if (params.ctx.req) params.ctx.req = 'req';
+        if (params.ctx.res) params.ctx.res = 'res';
+        console.log('1.LayoutApp.getInitialProps', params);
+        let { Component, ctx } = params;
+        let pageProps = {};
+        if (Component.getInitialProps)
+            pageProps = await Component.getInitialProps(ctx);
+        return { pageProps };
+    }
    render() {
+        console.log('3.LayoutApp.render', this.props);
        let { Component } = this.props as any;
        let pathname = this.props.router.pathname;
        pathname = '/' + (pathname.split('/')[1]);
        return (
            <>
                <style jsx>
+                    {`a{display:inline-block!important;}`}
                </style>
                <Layout>
                    <Header className="header">
+                        <img src="/images/jglogo.png" style={{ width: 120, height: 31, margin: '16px 24px 16px 0px', float: 'left' }} />
+                        <Menu theme="dark" mode="horizontal" style={{ lineHeight: '64px', display: 'inline-block' }}
                            selectedKeys={[pathname]}
                            defaultSelectedKeys={[pathname]}>
                            <Menu.Item key="/">
                                <Icon type="home" /><Link href="/"><a>首页</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/user">
                                <Icon type="/user" /> <Link href="/user" ><a>用户管理</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/profile">
                                <Icon type="profile" /><Link href="/profile"><a>个人中心</a></Link>
                            </Menu.Item>
                        </Menu>
                    </Header>
+                    <Component {...this.props.pageProps} />
                    <Footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</Footer>
                </Layout>
            </>
        )
    }
}
export default withRouter(LayoutApp);

5.4 user\index.tsx #

pages\user\index.tsx

import { withRouter } from 'next/router';
import Link from 'next/link';
import { Layout, Menu, Icon } from 'antd';
const { Sider, Content } = Layout;
function UserLayout(props) {
    return (
        <>
            <style jsx>
                 {`a{display:inline-block!important;}`}
            </style>
            <Layout>
                <Sider>
                    <Menu
                        theme="dark"
                        mode="inline"
                        defaultSelectedKeys={[props.router.pathname]}
                    >
                        <Menu.Item key="/user/list">
                            <Icon type="user" /><Link href="/user/list"><a>用户列表</a></Link>
                        </Menu.Item>
                        <Menu.Item key="/user/add">
                            <Icon type="plus" /><Link href="/user/add"><a>添加用户</a></Link>
                        </Menu.Item>
                    </Menu>
                </Sider>
                <Content style={{ padding: 8 }}>
                    {props.children}
                </Content>
            </Layout>
        </>
    )
}
export default withRouter(UserLayout);

5.5 user\list.tsx #

pages\user\list.tsx

import UserLayout from './index';
import { List } from 'antd';
import Link from 'next/link';
function UseList(props) {
    console.log('4.UseList.render', props);
    return (
        <UserLayout>
            <List
                header={<div>用户列表</div>}
                footer={<div>共计多少{props.list.length}个用户</div>}
                bordered
                dataSource={props.list}
                renderItem={(item: any) => (
                    <List.Item key={item._id}>
                        <Link as={`/user/detail/${item._id}`} href={{ pathname: `/user/detail`, query: { id: item._id } }}><a>{item.username}</a></Link>
                    </List.Item>
                )}
            />
        </UserLayout>
    )
}

UseList.getInitialProps = async (ctx) => {
    if (ctx.req) { ctx.req = 'req'; }
    if (ctx.res) { ctx.res = 'res'; }
    console.log('2.UseList.getInitialProps ctx', ctx);
    let list = [{ _id: 1, username: 'zhangsan', password: '1' }, { _id: 2, username: 'lisi', password: '2' }];
    return { list };
}

export default UseList;

5.6 user\add.tsx #

pages\user\add.tsx

import UserLayout from './index';
import router from 'next/router';
import { Form, Input, Button, Icon } from 'antd';
function UserAdd(props) {
    const { getFieldDecorator } = props.form;
    return (
        <UserLayout>
            <Form className="login-form" style={{ maxWidth: '300px' }}>
                <Form.Item>
                    {getFieldDecorator('username', {
                        rules: [{ required: true, message: 'Please input your username!' }],
                    })(
                        <Input
                            prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                            placeholder="Username"
                        />,
                    )}
                </Form.Item>
                <Form.Item>
                    {getFieldDecorator('password', {
                        rules: [{ required: true, message: 'Please input your Password!' }],
                    })(
                        <Input
                            prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                            type="password"
                            placeholder="Password"
                        />,
                    )}
                </Form.Item>
                <Form.Item>
                    <Button type="primary" htmlType="submit" className="login-form-button">
                        添加用户
                </Button>
                </Form.Item>
            </Form>
        </UserLayout>
    )
}
export default Form.create({ name: 'UserAdd' })(UserAdd);

6.集成koa #

6.1 知识点 #

6.2 安装依赖 #

yarn add koa koa-router

6.3 client\index.js #

client\index.js

let Koa = require('koa');
let Router = require('koa-router');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev: true });
const handler = app.getRequestHandler();
app.prepare().then(() => {
    const server = new Koa();
    let router = new Router();
    server.use(router.routes());
    server.use(async (ctx, next) => {
        await handler(ctx.req, ctx.res);
        ctx.response = false;
    });
    server.listen(3000, () => console.log('server started at port 3000'));
});

6.4 package.json #

package.json

  "scripts": {
+   "client": "nodemon client",
    "build": "next build",
    "start": "next start"
  },

7.调用接口 #

7.1 utils\axios.tsx #

utils\axios.tsx

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

7.2 pages\user\add.tsx #

pages\user\add.tsx

import UserLayout from './index';
import router from 'next/router';
+import { Form, Input, Button, Icon, message } from 'antd';
+import axios from '../../utils/axios';
function UserAdd(props) {
+    async function handleSubmit(event) {
+        event.preventDefault();
+        let values = props.form.getFieldsValue();
+        let response = await axios.post('/api/register', values);
+        if (response.data.code === 0) {
+            router.push('/user/list');
+        } else {
+            message.error('添加用户失败');
+        }
+    }
    const { getFieldDecorator } = props.form;
    return (
        <UserLayout>
+            <Form onSubmit={handleSubmit} className="login-form" style={{ maxWidth: '300px' }}>
                {props.currentUser && <span>创建者:{props.currentUser.username}</span>}
                <Form.Item>
                    {getFieldDecorator('username', {
                        rules: [{ required: true, message: 'Please input your username!' }],
                    })(
                        <Input
                            prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                            placeholder="Username"
                        />,
                    )}
                </Form.Item>
                <Form.Item>
                    {getFieldDecorator('password', {
                        rules: [{ required: true, message: 'Please input your Password!' }],
                    })(
                        <Input
                            prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                            type="password"
                            placeholder="Password"
                        />,
                    )}
                </Form.Item>
                <Form.Item>
                    <Button type="primary" htmlType="submit" className="login-form-button">
                        添加用户
                </Button>
                </Form.Item>
            </Form>
        </UserLayout>
    )
}
export default Form.create({ name: 'UserAdd' })(UserAdd);

7.3 user\list.tsx #

pages\user\list.tsx

import UserLayout from './index';
import { List } from 'antd';
import Link from 'next/link';
+import axios from '../../utils/axios';
function UseList(props) {
    console.log('4.UseList.render', props);
    return (
        <UserLayout>
            <List
                header={<div>用户列表</div>}
                footer={<div>共计多少{props.list.length}个用户</div>}
                bordered
                dataSource={props.list}
                renderItem={(item: any) => (
                    <List.Item key={item._id}>
                        <Link as={`/user/detail/${item._id}`} href={{ pathname: `/user/detail`, query: { id: item._id } }}><a>{item.username}</a></Link>
                    </List.Item>
                )}
            />
        </UserLayout>
    )
}

UseList.getInitialProps = async (ctx) => {
    if (ctx.req) { ctx.req = 'req'; }
    if (ctx.res) { ctx.res = 'res'; }
    console.log('2.UseList.getInitialProps ctx', ctx);
+    let response = await axios({ url: '/api/users', method: 'GET' });
+    return { list: response.data.data };
}

export default UseList;

8.用户详情 #

8.1 知识点 #

8.2 user\detail.tsx #

pages\user\detail.tsx

import React, { useState } from 'react';
import UserLayout from './';
import { withRouter } from 'next/router';
import { Button } from 'antd';
import axios from '../../utils/axios';
import dynamic from 'next/dynamic';
const UserInfo = dynamic(import('../../components/UserInfo'))

function UserDetail(props) {
  let [show, setShow] = useState(false);
  return (
    <UserLayout>
      <p>ID: {props.router.query.id}</p>
      <Button onClick={() => setShow(!show)}>显示/隐藏</Button>
      {
        show && <UserInfo user={props.user} />
      }
    </UserLayout>
  )
}
UserDetail.getInitialProps = async (ctx) => {
  let response = await axios({ url: `/api/users/${ctx.query.id}`, method: 'GET' });
  return { user: response.data.data };
}
export default withRouter(UserDetail);

8.2 components\UserInfo.tsx #

components\UserInfo.tsx

import { useState } from 'react';
import { Button } from 'antd';
function UserInfo(props) {
    let [created, setCreated] = useState(props.user.created);
    async function changeFormat() {
        let moment = await import('moment');
        setCreated(moment.default(props.user.created).fromNow());
    }
    return (
        <>
            <p>用户名:{props.user.username}</p>
            <p>密码:{props.user.password}</p>
            <p>创建时间:{created}<Button onClick={changeFormat}>切换为相对时间</Button></p>
        </>
    )
}
export default UserInfo;

8.3 client\index.js #

client\index.js

let Koa = require('koa');
let Router = require('koa-router');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev: true });
const handler = app.getRequestHandler();
app.prepare().then(() => {
    const server = new Koa();
    let router = new Router();
+    router.get('/user/detail/:id', async (ctx, next) => {
+        const id = ctx.params.id;
+        await handler(ctx.req, ctx.res, {
+            pathname: '/user/detail',
+            query: { id }
+        });
+        ctx.response = false;
+    });
    server.use(router.routes());
    server.use(async (ctx, next) => {
        await handler(ctx.req, ctx.res);
        ctx.response = false;
    });
    server.listen(3000, () => console.log('server started at port 3000'));
});

9.登录 #

9.1 pages\login.tsx #

import router from 'next/router';
import { Form, Input, Button, Icon, message } from 'antd';
import axios from '../utils/axios';
function Login(props) {
    const { getFieldDecorator } = props.form;
    async function handleSubmit(event) {
        event.preventDefault();
        let values = props.form.getFieldsValue();
        let response = await axios.post('/api/login', values);
        if (response.data.code === 0) {
            router.push('/');
        } else {
            message.error('登录失败');
        }
    }
    return (
        <Form onSubmit={handleSubmit} className="login-form" style={{ maxWidth: '300px',margin:'20px auto' }}>
            <Form.Item>
                {getFieldDecorator('username', {
                    rules: [{ required: true, message: 'Please input your username!' }],
                })(
                    <Input
                        prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        placeholder="Username"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('password', {
                    rules: [{ required: true, message: 'Please input your Password!' }],
                })(
                    <Input
                        prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        type="password"
                        placeholder="Password"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                <Button type="primary" htmlType="submit" className="login-form-button">
                    登录
                </Button>
            </Form.Item>
        </Form>
    )
}
const WrappedLogin = Form.create({ name: 'Login' })(Login);
export default WrappedLogin;

10.集成redux #

10.1 安装依赖 #

yarn add redux react-redux

10.2 pages_app.tsx #

pages_app.tsx

import App from 'next/app';
import Link from 'next/link';
+import { Layout, Menu, Icon, Avatar } from 'antd';
import 'antd/dist/antd.css';
import { withRouter } from 'next/router';
+import axios from '../utils/axios';
+import initStore from '../store';
+import { Provider } from 'react-redux';
+import * as TYEPS from '../store/action-types';
+const { Header, Footer, Content } = Layout;
+declare global {
+    interface Window {
+        _redux_store_: any
+    }
+}
+function getStore(initialState) {
+    if (typeof window == 'undefined') {
+        return initStore(initialState);
+    } else {
+        if (!window._redux_store_) {
+            window._redux_store_ = initStore(initialState);
+        }
+        return window._redux_store_;
+    }
+}
class LayoutApp extends App<any> {
+    store: any
+    constructor(props) {
+        super(props);
+        this.store = getStore(props.initialState);
+    }
+    static async getInitialProps({ Component, ctx }) {
+        let store = getStore({});
+        let currentUser;
+        let options: any = { url: '/api/currentUser' };
+        if (ctx.req&&ctx.req.headers.cookie) {
+            (options.headers = options.headers || {}).cookie = ctx.req.headers.cookie;
+        }
+        let response = await axios(options);
+        if (response.data.code == 0) {
+            currentUser = response.data.data;
+            store.dispatch({ type: TYEPS.SET_USER_INFO, payload: currentUser });
+        }
        let pageProps = {};
        if (Component.getInitialProps)
            pageProps = await Component.getInitialProps(ctx);

+        return { pageProps, currentUser, initialState: store.getState() };
    }
    render() {
        let { Component } = this.props as any;
        let pathname = this.props.router.pathname;
        pathname = '/' + (pathname.split('/')[1]);
        return (
+            <Provider store={this.store}>
                <style jsx>
                    {`a{display:inline-block!important;}`}
                </style>
                <Layout>
                    <Header className="header">
                        <img src="/images/jglogo.png" style={{ width: 120, height: 31, margin: '16px 24px 16px 0px', float: 'left' }} />
                        <Menu theme="dark" mode="horizontal" style={{ lineHeight: '64px', display: 'inline-block' }}
                            selectedKeys={[pathname]}
                            defaultSelectedKeys={[pathname]}>
                            <Menu.Item key="/">
                                <Icon type="home" /><Link href="/"><a>首页</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/user">
                                <Icon type="/user" /> <Link href="/user" ><a>用户管理</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/profile">
                                <Icon type="profile" /><Link href="/profile"><a>个人中心</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/login">
                                <Icon type="login" /><Link href="/login"><a>登录</a></Link>
                            </Menu.Item>
                        </Menu>
+                        {
+                            this.props.currentUser && <div style={{ display: 'inline-block', float: 'right', color: 'red' }}>
+                                <Avatar style={{ color: '#F00', backgroundColor: '#CCC' }}>{this.props.currentUser.username}</Avatar>
+                            </div>
+                        }
                    </Header>
                    <Component {...this.props.pageProps} />
                    <Footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</Footer>
                </Layout>
+            </Provider>
        )
    }
}
export default withRouter(LayoutApp);

10.3 user\add.tsx #

pages\user\add.tsx

import UserLayout from './index';
import router from 'next/router';
import { Form, Input, Button, Icon, message } from 'antd';
import axios from '../../utils/axios';
+import { connect } from 'react-redux';
function UserAdd(props) {
    async function handleSubmit(event) {
        event.preventDefault();
        let values = props.form.getFieldsValue();
        let response = await axios.post('/api/register', values);
        if (response.data.code === 0) {
            router.push('/user/list');
        } else {
            message.error('添加用户失败');
        }
    }
    const { getFieldDecorator } = props.form;
    return (
        <UserLayout>
            <Form onSubmit={handleSubmit} className="login-form" style={{ maxWidth: '300px' }}>
                <Form.Item>
                    {getFieldDecorator('username', {
                        rules: [{ required: true, message: 'Please input your username!' }],
                    })(
                        <Input
                            prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                            placeholder="Username"
                        />,
                    )}
                </Form.Item>
                <Form.Item>
                    {getFieldDecorator('password', {
                        rules: [{ required: true, message: 'Please input your Password!' }],
                    })(
                        <Input
                            prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                            type="password"
                            placeholder="Password"
                        />,
                    )}
                </Form.Item>
                <Form.Item>
                    <Button type="primary" htmlType="submit" className="login-form-button">
                        添加用户
                </Button>
                </Form.Item>
            </Form>
        </UserLayout>
    )
}
+const WrappedUserAdd = Form.create({ name: 'UserAdd' })(UserAdd);
+export default WrappedUserAdd;

10.4 action-types.tsx #

store\action-types.tsx

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

10.5 store\index.tsx #

store\index.tsx

import { createStore } from 'redux';
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 function initStore(initialState) {
    const store = createStore(reducer, initialState);
    return store;
}

11.loading效果 #

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

11.1 pages_app.tsx #

import App from 'next/app';
import Link from 'next/link';
import { Layout, Menu, Icon, Avatar } from 'antd';
import 'antd/dist/antd.css';
import { withRouter } from 'next/router';
import axios from '../utils/axios';
import initStore from '../store';
import { Provider } from 'react-redux';
import * as TYEPS from '../store/action-types';
+import router from 'next/router';
+import { Spin } from 'antd';
const { Header, Footer, Content } = Layout;
declare global {
    interface Window {
        _redux_store_: any
    }
}
function getStore(initialState) {
    if (typeof window == 'undefined') {
        return initStore(initialState);
    } else {
        if (!window._redux_store_) {
            window._redux_store_ = initStore(initialState);
        }
        return window._redux_store_;
    }
}
class LayoutApp extends App<any> {
    store: any
+    state = { loading: false }
+    routeChangeStart: any
+    routeChangeComplete: any
    constructor(props) {
        super(props);
        this.store = getStore(props.initialState);
    }
    static async getInitialProps({ Component, ctx }) {
        let store = getStore({});
        let currentUser;
        let options: any = { url: '/api/currentUser' };
        if (ctx.req) {
            (options.headers = options.headers || {}).cookie = ctx.req.headers.cookie;
        }
        let response = await axios(options);
        if (response.data.code == 0) {
            currentUser = response.data.data;
            store.dispatch({ type: TYEPS.SET_USER_INFO, payload: currentUser });
        }
        let pageProps = {};
        if (Component.getInitialProps)
            pageProps = await Component.getInitialProps(ctx);

        return { pageProps, currentUser, initialState: store.getState() };
    }
+    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() {
        let { Component, pageProps, currentUser } = this.props as any;
        let pathname = this.props.router.pathname;
        pathname = '/' + (pathname.split('/')[1]);
        return (
            <Provider store={this.store}>
                <style jsx>
                    {`a{display:inline-block!important;}`}
                </style>
                <Layout>
                    <Header className="header">
                        <img src="/images/jglogo.png" style={{ width: 120, height: 31, margin: '16px 24px 16px 0px', float: 'left' }} />
                        <Menu theme="dark" mode="horizontal" style={{ lineHeight: '64px', display: 'inline-block' }}
                            selectedKeys={[pathname]}
                            defaultSelectedKeys={[pathname]}>
                            <Menu.Item key="/">
                                <Icon type="home" /><Link href="/"><a>首页</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/user">
                                <Icon type="/user" /> <Link href="/user" ><a>用户管理</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/profile">
                                <Icon type="profile" /><Link href="/profile"><a>个人中心</a></Link>
                            </Menu.Item>
                            <Menu.Item key="/login">
                                <Icon type="login" /><Link href="/login"><a>登录</a></Link>
                            </Menu.Item>
                        </Menu>
+                        {
+                            this.props.currentUser && <div style={{ display: 'inline-block', float: 'right', color: 'red' }}>
+                                <Avatar style={{ color: '#F00', backgroundColor: '#CCC' }}>{this.props.currentUser.username}</Avatar>
+                            </div>
                        }
                    </Header>
                    {
                        this.state.loading ? <Spin style={{ margin: '50px auto' }} /> : <Component {...pageProps} currentUser={currentUser} />
                    }
                    <Footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</Footer>
                </Layout>
            </Provider>
        )
    }
}
export default withRouter(LayoutApp);

12.受保护路由 #

12.1 pages\profile.tsx #

pages\profile.tsx

import router from 'next/router';
import { Button, Layout } from 'antd';
+import axios from '../utils/axios';
const { Content } = Layout;
+function Profile(props) {
    return (
        <Content style={{ margin: '20px auto' }}>
+           <div>当前登录用户:{props.currentUser.username}</div>
            <Button onClick={() => router.back()}>返回</Button>
        </Content>
    )
}
+Profile.getInitialProps = async function (ctx) {
+    let options: any = { url: '/api/currentUser' };
+    if (ctx.req&&ctx.req.headers.cookie) {
+        (options.headers = options.headers || {}).cookie = ctx.req.headers.cookie;
+    }
+    let response = await axios(options);
+    if (response.data.code == 0) {
+        return {};
+    } 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;

13.自定义Document #

13.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;

13.2 pages\index.tsx #

pages\index.tsx

import router from 'next/router'
import { Button, Layout } from 'antd';
import Head from 'next/head'
const { Content } = Layout;
export default function () {
    return (
        <>
            <Head>
                <title>首页</title>
                <meta name="description" content="这是首页" />
            </Head>
            <Content style={{ margin: '20px auto' }}>
                <div>Home</div>
                <Button onClick={() => router.push('/user')}>/user</Button>
            </Content>
        </>
    )
}

14.服务端 #

yarn add express  body-parser  cors express-session connect-mongo mongoose

14.1 api\index.js #

let express = require("express");
let bodyParser = require("body-parser");
let cors = require("cors");
let Models = require('./db');
let session = require("express-session");
let config = require('./config');
let MongoStore = require('connect-mongo')(session);
let 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(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(
    session({
        secret: config.secret,
        resave: false,
        saveUninitialized: true,
        store: new MongoStore({
            url: config.dbUrl,
            mongoOptions: {
                useNewUrlParser: true,
                useUnifiedTopology: true
            }
        })
    })
);
app.get('/api/users', async (req, res) => {
    let users = await Models.UserModel.find();
    res.send({ code: 0, data: users });
});
app.get('/api/users/:id', async (req, res) => {
    let user = await Models.UserModel.findById(req.params.id);
    res.send({ code: 0, data: user });
});
app.post('/api/register', async (req, res) => {
    let user = req.body;
    user = await Models.UserModel.create(user);
    res.send({ code: 0, data: user });
});
app.post('/api/login', async (req, res) => {
    let user = req.body;
    let dbUser = await Models.UserModel.findOne(user);
    if (dbUser) {
        req.session.currentUser = dbUser;
        res.send({ code: 0, data: dbUser });
    } else {
        res.send({ code: 1, error: '登录失败' });
    }
});

app.get('/api/currentUser', async (req, res) => {
    let currentUser = req.session.currentUser;
    if (currentUser) {
        res.send({ code: 0, data: currentUser });
    } else {
        res.send({ code: 1, error: '当前用户未登录' });
    }
});
app.listen(4000, () => {
    console.log('服务器在4000端口启动!');
});

14.2 api\config.js #

api\config.js

module.exports = {
    secret: 'zhufeng',
    dbUrl: "mongodb://127.0.0.1/zhufengnext2"
}

14.3 api\db.js #

api\db.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ObjectId = Schema.Types.ObjectId;
const config = require('./config');
const conn = mongoose.createConnection(config.dbUrl, { useNewUrlParser: true, useUnifiedTopology: true });
const UserModel = conn.model('User', new Schema({
    username: { type: String },
    password: { type: String }
}, { timestamps: { createdAt: 'created', updatedAt: 'updated' } }));

module.exports = {
    UserModel
}