Next.js與TypeScript從零起步學(xué)習(xí)筆記-5(終).項(xiàng)目實(shí)戰(zhàn)NextJs+PostgreSQL+AntDesign

此文章,會(huì)從零開始結(jié)合Ant Design UI和PostgreSQL做一個(gè)簡(jiǎn)單的增刪改
這里只是一個(gè)簡(jiǎn)單的demo蒜田,真實(shí)的開發(fā)中我們能可能還需要權(quán)限鸦做,日志,連接池等等负敏。

參考官網(wǎng):https://nextjs.org/learn/basics/getting-started

開發(fā)環(huán)境:
window10 x64
node v10.16.3
npm v6.13.4

1.項(xiàng)目初始化

參考‘Next.js與TypeScript從零起步學(xué)習(xí)筆記-1’壤躲,我們先創(chuàng)建一個(gè)空項(xiàng)目并添加TS引用:

npm init -y
npm install --save react react-dom next
mkdir pages

npm install --save-dev typescript @types/react @types/node

改一下生成命令,修改'package.json'文件代碼:

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

然后我們?cè)?pages'目錄下城菊,建一個(gè)'index.tsx',隨便輸入點(diǎn)代碼碉克,測(cè)試一下是否能跑去起來:

//pages/index.tsx

const Home = () => <h1>Hello world!</h1>;

export default Home;

運(yùn)行項(xiàng)目:

npm run dev

應(yīng)該可以看到效果:

圖1.項(xiàng)目運(yùn)行

圖2.目錄結(jié)構(gòu)

這里我并打算用到嚴(yán)格模式(類型約束)凌唬,因?yàn)楹竺嫖恼乱玫降念悗?kù),會(huì)出現(xiàn)諸多問題漏麦,為項(xiàng)目簡(jiǎn)單起見客税,我不建議用嚴(yán)格模式。

2.創(chuàng)建RESTful API

2.1 創(chuàng)建表

我們需要在數(shù)據(jù)庫(kù)創(chuàng)建一張用戶表撕贞,用來存放用戶的數(shù)據(jù)更耻,并對(duì)其進(jìn)行簡(jiǎn)單的增刪改查,我用的數(shù)據(jù)庫(kù)是PostgreSQL捏膨。(PS:你需要有一點(diǎn)數(shù)據(jù)庫(kù)相關(guān)的知識(shí))

--table next_user 

CREATE TABLE public.next_user (
    id int4 NOT NULL GENERATED ALWAYS AS IDENTITY,
    "name" varchar(40) NOT NULL,
    age int2 NOT NULL,
    created_date_time timestamp NOT NULL,
    CONSTRAINT next_user_pk PRIMARY KEY (id)
);

2.2 創(chuàng)建RESTful API

我們?cè)趐ages文件夾下創(chuàng)建一個(gè)api文件夾秧均,主要用來存放我們api

//以項(xiàng)目根目錄為準(zhǔn)

cd pages
mkdir api
cd api
mkdir user

我們?cè)?user'文件夾下,創(chuàng)建一個(gè)'[id].ts'的文件号涯,這是我們的接口文件目胡,并在文件敲入如下代碼:

import { NextApiRequest, NextApiResponse } from 'next';


export default (req: NextApiRequest, res: NextApiResponse) => {
    try{
        switch (req.method.toUpperCase()) {
            case "GET":
                _get(req,res);
                break;
            case "POST":
                _post(req,res);
                break;
            case "PUT":
                _put(req,res);
                break;
            case "DELETE":
                _delete(req,res);
                break;
            default:
                res.status(404).send("");
                break;
        }
    } catch (e){
        //make some logs
        console.debug("error");
        console.debug(e);
        res.status(500).send("");
    }
};

function _get(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("GET");
}

function _post(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("POST");
}

function _put(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("PUT");
}

function _delete(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("DELETE");
}

以上,我們已經(jīng)創(chuàng)建了我們最簡(jiǎn)單的RESTful API了链快,把項(xiàng)目跑起來誉己,我們能看到效果


圖3.RESTful API效果圖

PS:以上是動(dòng)態(tài)路由,即根據(jù)路徑獲取資源Id久又,如圖3URL:http://localhost:3000/api/user/1,后面的'/1'表示的是訪問這個(gè)id的資源,現(xiàn)實(shí)中只有編輯時(shí)候才會(huì)用到效五,新增或獲取列表時(shí)候地消,并不適用,所以:
我們需要在'pages/api'下畏妖,新添一個(gè)路由文件'user.ts'脉执,主要作用是處理非指定資源(沒有id):

//pages/api/user.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default (req: NextApiRequest, res: NextApiResponse) => {
    try{
        switch (req.method.toUpperCase()) {
            case "GET":
                _get(req,res);
                break;
            case "POST":
                _post(req,res);
                break;
            default:
                res.status(404).send("");
                break;
        }
    } catch (e){
        //make some logs
        console.debug("error");
        console.debug(e);
        res.status(500).send("");
    }
};

function _get(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("GET");
}

function _post(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("POST");
}

我們?cè)賮硇薷?pages/api/user/[id].ts',把post去掉戒劫,因?yàn)閜ost是新增半夷,所以邏輯上在這個(gè)文件永遠(yuǎn)用不上

//pages/api/user/[id].ts
import { NextApiRequest, NextApiResponse } from 'next';

export default (req: NextApiRequest, res: NextApiResponse) => {
    try{
        switch (req.method.toUpperCase()) {
            case "GET":
                _get(req,res);
                break;
            case "PUT":
                _put(req,res);
                break;
            case "DELETE":
                _delete(req,res);
                break;
            default:
                res.status(404).send("");
                break;
        }
    } catch (e){
        //make some logs
        console.debug("error");
        console.debug(e);
        res.status(500).send("");
    }
};

function _get(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("GET");
}

function _put(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("PUT");
}

function _delete(req: NextApiRequest, res: NextApiResponse){
    res.status(200).send("DELETE");
}

圖4.文件結(jié)構(gòu)

這樣建立文件婆廊,看上去有點(diǎn)繁瑣,但目前我所知道巫橄,Next的路由就是這樣淘邻。
能不能像.NET這樣,用標(biāo)簽屬性來確定呢湘换?我目前沒有找到好方法,知道的小伙伴請(qǐng)不吝賜教,私信告訴我

2.3 對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫

我們連接數(shù)據(jù)庫(kù)绩社,第一步是先安裝上驅(qū)動(dòng)卷要,我開始使用的是'ts-postgres',為什么用它帆离?沒啥的蔬蕊,就是因?yàn)檫@個(gè)東西是TS寫的,但后面發(fā)現(xiàn)坑有點(diǎn)多哥谷,例如我要count多少行岸夯,總是返回null,我以為是函數(shù)問題呼巷,但換了now又正常囱修,弄了很久,后面放棄了王悍,只好換另一個(gè)驅(qū)動(dòng)'node-postgres'破镰,這個(gè)東西問題就是用js寫的,ts里用压储,你會(huì)發(fā)現(xiàn)一堆都是any鲜漩。
安裝node-postgres:

npm install pg

我們先來創(chuàng)建一個(gè)配置文件,這個(gè)配置文件用來保存一些系統(tǒng)的配置集惋,例如數(shù)據(jù)庫(kù)連接等等孕似,在根目錄下創(chuàng)建'config.json':

//config.json
{
    "dbCofing":{
        "host": "localhost",
        "port":5432,
        "user":"postgres",
        "database":"next_learn",
        "password":"123456"
    }
}

然后我們創(chuàng)建一個(gè)文件,命名為'repositories'刮刑,用來存放sql操作等邏輯:

//以項(xiàng)目根目錄為準(zhǔn)

cd pages
mkdir repositories

在'repositories'文件夾下喉祭,我們創(chuàng)建一個(gè)文件'user-repository.ts',這里主要編寫用戶表(見2.1)的讀寫邏輯雷绢。

//repositories/user-repository.ts

import { Client } from 'pg';
import config from "../config.json";

//這個(gè)接口應(yīng)該單獨(dú)弄出去泛烙,弄個(gè)文件夾夾叫utility之類放著,因?yàn)楹罄m(xù)肯定不止這個(gè)地方用到翘紊。
export interface PageData<T> {
    index?: number;
    pageSize?: number;
    totalCount?: number;
    list?: Array<T>;
}


export interface NextUser {
    id?: number;
    name?: string;
    age?: number;
    createdDateTime?: Date;
}

export class UserRepository {
    //分頁(yè)獲取所有用戶
    async getAllUser(index: number, pageSize: number): Promise<PageData<NextUser>> {
        const client = new Client(config.dbCofing);
        await client.connect();

        try {
            let pageData: PageData<NextUser> = {}
            pageData.index = index;
            pageData.pageSize = pageSize;

            //以下加await的話蔽氨,會(huì)同步等待結(jié)果返回
            const totalCount = await client.query(
                `SELECT count(*) as total_count from next_user`
            );
            pageData.totalCount = parseInt(totalCount.rows[0].total_count);

            const result = await client.query(
                `select id,name,age,created_date_time from next_user order by id desc limit ${pageSize} offset ${(pageSize * (index - 1))}; `
            );

            let nextUsers: NextUser[] = [];
            for (const row of result.rows) {
                let nextUser: NextUser = {
                    id: row.id as number,
                    name: row.name as string,
                    age: row.age as number,
                    createdDateTime: row.created_date_time as Date
                };

                nextUsers.push(nextUser);
            }
            pageData.list = nextUsers;

            return pageData;

        }  catch(e){
            console.log(e);
        }
        finally {
            await client.end();
        }
    }

    //根據(jù)id獲取用戶
    async getUser(id: number): Promise<NextUser> {
        const client = new Client(config.dbCofing);
        await client.connect();

        try {

            const result = await client.query(
                `select id,name,age,created_date_time from next_user where id = $1`, [id]
            );

            let nextUser: NextUser = null;
            if(result.rows.length > 0){
                nextUser = {
                    id: result.rows[0].id as number,
                    name: result.rows[0].name as string,
                    age: result.rows[0].age as number,
                    createdDateTime: result.rows[0].created_date_time as Date
                };
            }
            
            return nextUser;
        } finally {
            await client.end();
        }
    }

    //添加用戶
    async addUser(user: NextUser) {
        const client = new Client(config.dbCofing);
        await client.connect();

        try {
            await client.query(
                `INSERT INTO public.next_user ("name", age, created_date_time) VALUES($1, $2, $3);`, [user.name, user.age, new Date()]
            );
        } finally {
            await client.end();
        }
    }

    //更新用戶
    async updateUser(user: NextUser) {
        const client = new Client(config.dbCofing);
        await client.connect();

        try {
            await client.query(
                `UPDATE public.next_user SET "name"=$1, age=$2 WHERE id=$3;
            `, [user.name, user.age,user.id]
            );
        } finally {
            await client.end();
        }
    }

    //刪除用戶
    async deleteUser(id: number) {
        const client = new Client(config.dbCofing);
        await client.connect();

        try {
            await client.query(
                `delete from next_user where id = $1`, [id]
            );
        } finally {
            await client.end();
        }
    }
}

然后我們分別來修改一下'pages/api/user/[id].ts'和'pages/api/user.ts'文件,讓他們?cè)L問數(shù)據(jù)庫(kù)
PS:正常來說,為了邏輯復(fù)用鹉究,api不應(yīng)該直接訪問數(shù)據(jù)倉(cāng)儲(chǔ)層(repository)宇立,中間應(yīng)該多一個(gè)service層什么的,這邊只是一個(gè)演示demo自赔,以簡(jiǎn)優(yōu)先妈嘹,所以省略很多

//pages/api/user/[id].ts

import { NextApiRequest, NextApiResponse } from 'next';
import {UserRepository,NextUser} from '../../../repositories/user-repository';

export default (req: NextApiRequest, res: NextApiResponse) => {
    try{
        switch (req.method.toUpperCase()) {
            case "GET":
                _get(req,res);
                break;
            case "PUT":
                _put(req,res);
                break;
            case "DELETE":
                _delete(req,res);
                break;
            default:
                res.status(404).send("");
                break;
        }
    } catch (e){
        //make some logs
        console.debug("error");
        console.debug(e);
        res.status(500).send("");
    }
};

async function _get(req: NextApiRequest, res: NextApiResponse){
    let userRepository = new UserRepository();
    let id = parseInt(req.query.id.toString());
    let user = await userRepository.getUser(id);

    res.status(200).json({status:"ok",data:user});
}

async function _put(req: NextApiRequest, res: NextApiResponse){
    let userRepository = new UserRepository();
    let user:NextUser= req.body as NextUser;
    user.id = parseInt(req.query.id.toString());
    await userRepository.updateUser(user);

    res.status(200).send({status:"ok"});
}1

async function _delete(req: NextApiRequest, res: NextApiResponse){
    let userRepository = new UserRepository();
    await userRepository.deleteUser(parseInt(req.query.id.toString()));

    res.status(200).json({status:"ok"})
}

//pages/api/user.ts

import { NextApiRequest, NextApiResponse } from 'next';
import {UserRepository,NextUser} from '../../repositories/user-repository';

export default (req: NextApiRequest, res: NextApiResponse) => {
    try{
        switch (req.method.toUpperCase()) {
            case "GET":
                _get(req,res);
                break;
            case "POST":
                _post(req,res);
                break;
            default:
                res.status(404).send("");
                break;
        }
    } catch (e){
        //make some logs
        console.debug("error");
        console.debug(e);
        res.status(500).send("");
    }
};

async function _get(req: NextApiRequest, res: NextApiResponse){
    let userRepository = new UserRepository();
    let index = parseInt(req.query.index.toString());
    let pageSize =  parseInt(req.query.pageSize.toString());

    let pageData = await userRepository.getAllUser(index,pageSize)

    res.status(200).json({status:"ok",data:pageData});
}

async function _post(req: NextApiRequest, res: NextApiResponse){
    let userRepository = new UserRepository();
    let user:NextUser= req.body as NextUser;
    await userRepository.addUser(user);

    res.status(200).send({status:"ok"});
}

啟動(dòng)一下服務(wù)器,測(cè)試一下結(jié)果:

npm run dev

我們先post加一條數(shù)據(jù):


圖5.測(cè)試添加數(shù)據(jù)

圖6.測(cè)試添加數(shù)據(jù)

然后看看接口查詢返回:


圖7.測(cè)試獲取數(shù)據(jù)

其他諸如刪除修改等匿级,我就不一一截圖了蟋滴。

3.引入Ant Design UI

3.1配置Ant Design UI

我UI的功底十分差勁,所以引入第三方UI框架痘绎,實(shí)際開發(fā)上津函,有很多開源且優(yōu)秀的框架以供你使用,你并不需要重復(fù)造輪子孤页。阿里體系貌似全部推薦用yarn,可能大概是跟他們作者之間有什么關(guān)聯(lián)吧尔苦?開始yarn有一個(gè)lock的優(yōu)勢(shì),但npm后面也跟著更新了行施,所以我本人覺得yarn相對(duì)npm,優(yōu)勢(shì)不大允坚。
引入Ant Design UI,在命令行輸入:

npm install antd --save

這里有些按需加載的知識(shí)需要了解,實(shí)際上你應(yīng)該按需加載蛾号。本篇為了簡(jiǎn)單稠项,所以用全加載。
Any Design UI的使用鲜结,請(qǐng)參閱:https://ant.design/index-cn;
除了上述UI的使用文檔展运,我們可能還需要參閱Pro Ant Design:https://pro.ant.design/docs/uset-typescript-cn,這里收集了一些TypeScript的問題。
Next在git上有一個(gè)demo精刷,請(qǐng)參閱:https://github.com/zeit/next.js/tree/canary/examples/with-ant-design

Next加載全局CSS時(shí)候拗胜,需要配置,我這邊習(xí)慣用less怒允,所以sass不安裝了埂软。
安裝less:

npm install less less-loader

安裝完less之后,我們需要安裝Next的全局css引入

npm install @zeit/next-css @zeit/next-less

然后我們?cè)诟夸浵氯沂拢砑右粋€(gè)配置文件'next.config.js'勘畔。沒錯(cuò),這就是我們的webpack配置文件丽惶,在文件敲入:

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

const isProd = process.env.NODE_ENV === 'production'

// fix: prevents error when .less files are required by node
if (typeof require !== 'undefined') {
  require.extensions['.less'] = file => { }
}

module.exports = withLess(withCSS({
  lessLoaderOptions: {
    javascriptEnabled: true
  }
}))

還有最后一步炫七,就是要找一個(gè)地方,配置ant design的全局樣式蚊夫,我們建立一個(gè)在跟目錄下诉字,建一個(gè)文件夾'css',在css下建立一個(gè)'antd.less'懦尝,敲入:

//css/antd.less
@import "~antd/dist/antd.less";

然后我們?cè)谑醉?yè)知纷,加點(diǎn)ant design的東西壤圃,看看成功沒有。
修改我們'pages/index.tsx'琅轧,代碼如下:

//pages/index.tsx

import {Button} from 'antd'
import '../css/antd.less'

const Home = () => 
    <div>
        <Button type="primary">Primary</Button>
        <Button>Default</Button>
        <Button type="dashed">Dashed</Button>
        <Button type="danger">Danger</Button>
        <Button type="link">Link</Button>
    </div>
    
export default Home;

啟動(dòng)項(xiàng)目伍绳,我們可以看到,ant design的東西出來:


圖8.ant design配置測(cè)試

目前整體目錄結(jié)構(gòu)如下(next-env.d.ts乍桂,tsconfig.json這個(gè)是系統(tǒng)運(yùn)動(dòng)時(shí)候生成的):


圖9.配置后的目錄結(jié)構(gòu)
3.2 做一個(gè)簡(jiǎn)單的增刪改頁(yè)面

現(xiàn)在我們可以用它來做一個(gè)帶分頁(yè)的增刪改頁(yè)面了冲杀,頁(yè)面我本地已經(jīng)做好了,這里直接把它粘貼出來睹酌,也沒什么好說的权谁。。憋沿。旺芽。不過是無腦的套用UI框架而已,時(shí)間格式可能需要美化,自己寫一個(gè)方法或者借助第三庫(kù)如'@angular/common'辐啄,這里用toString帶過采章,修改我們'pages/index.tsx',代碼如下:

//pages/index.tsx

import React from 'react'
import { FormComponentProps } from "antd/lib/form/Form";
import { NextPage } from 'next';
import {
    Form,
    Input,
    Tooltip,
    Icon,
    Cascader,
    Select,
    Row,
    Col,
    Checkbox,
    AutoComplete,
    Button,
    Modal,
    InputNumber,
    Table,
    Divider
} from 'antd';
import '../css/antd.less';
import '../css/style.less';


interface IProps extends FormComponentProps {

}

interface IState {
    modalVisible?: boolean;
}

class Home extends React.Component<IProps, IState> {
    constructor(props: IProps) {
        super(props);

        this.state = {
            modalVisible: false
        };
    };

    
    //注意這里要寫成'handleAdd = () => {}',假如普通寫'handleAdd() {}'會(huì)引起this為undefined
    handleAdd = () => {
        this.setState({ modalVisible: true });
    };



    closeModal = () => {
        this.setState({ modalVisible: false });
    }

    doEdit = () => {



    };

    render() {
        const { getFieldDecorator } = this.props.form;
        const formItemLayout = {
            labelCol: {
                xs: { span: 18 },
                sm: { span: 6 },
            },
            wrapperCol: {
                xs: { span: 24 },
                sm: { span: 16 },
            },
        };


        const columns = [
            {
              title: '姓名',
              dataIndex: 'name',
              key: 'name',
            },
            {
              title: '年齡',
              dataIndex: 'age',
              key: 'age',
            },
            {
                title: '操作',
                key: 'action',
                render: (text, record) => (
                  <span>
                    <a>編輯</a>
                    <Divider type="vertical" />
                    <a>刪除</a>
                  </span>
                ),
              }
          ];

          const dataSource = [
            {
              key: '1',
              name: '吳彥祖',
              age: 32
            },
            {
              key: '2',
              name: '彭于晏',
              age: 42
            },
          ];


        return (
            <div className="container">
                <div style={{ paddingLeft: "50px" }}>
                    <Button type="primary" onClick={this.handleAdd}>新增</Button>
                </div>
                <div style={{ padding: "20px 50px" }}>
                    <Table dataSource={dataSource} columns={columns} />;
                </div>


                <Modal
                    title="Basic Modal"
                    visible={this.state.modalVisible}
                    onOk={this.closeModal}
                    onCancel={this.closeModal}
                >
                    <Form {...formItemLayout} onSubmit={this.doEdit}>
                        <Form.Item label="姓名">
                            {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 label="年齡">
                            <InputNumber min={1} max={200} defaultValue={17} style={{ width: '100%', marginRight: '3%' }} />
                        </Form.Item>
                    </Form>
                </Modal>
            </div>
        );
    }
}


export default Form.create()(Home);

啟動(dòng)可以看到效果:


圖10.效果圖

圖11.效果圖

很簡(jiǎn)單的就能搭建一個(gè)頁(yè)面壶辜,感謝這些開源企業(yè)的無私奉獻(xiàn)悯舟,讓我們的工作變得簡(jiǎn)單。

4.與API對(duì)接

現(xiàn)在到了開發(fā)的最后一步砸民,我們需要利用之前的RESTful API(詳見2)對(duì)數(shù)據(jù)庫(kù)讀寫抵怎,并渲染頁(yè)面。這里我使用第三庫(kù)'axios'請(qǐng)求http接口,安裝axios:

npm install axios

然后在頁(yè)面'pages/index.tsx'的頂部引用進(jìn)來:

//pages/index.tsx

import axios from 'axios';
4.1 獲取所有數(shù)據(jù)

我們需要把現(xiàn)在的數(shù)據(jù)庫(kù)的用戶都顯示在界面阱洪,在'pages/index.tsx'中便贵,我們可以寫一個(gè)方法這樣請(qǐng)求接口:

//pages/index.tsx

//引用用戶類型接口,主要為了API返回?cái)?shù)據(jù)作類型轉(zhuǎn)換
import { NextUser, PageData } from "../repositories/user-repository";
//引用Ant Design的table props冗荸,用于table的數(shù)據(jù)約束
import { ColumnProps } from 'antd/es/table';

//新建一個(gè)用戶接口承璃,這個(gè)接口用于table的數(shù)據(jù)顯示
interface TableUser{
    key?:number,
    id?: number;
    name?: string;
    age?: number;
}

//修改state接口,加上用戶屬性特性
interface IState {
    modalVisible?: boolean;
    index?: number;
    pageSize?: number;
    tableData?: Array<TableUser>;
    pagination?: any;
}

//這個(gè)是ant design的列
const columns: ColumnProps<TableUser>[] = [
    {
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
    },
    {
        title: '年齡',
        dataIndex: 'age',
        key: 'age',
    },
    {
        title: '操作',
        key: 'action',
        render: (text, record) => (
            <span>
                <a>編輯</a>
                <Divider type="vertical" />
                <a>刪除</a>
            </span>
        ),
    }
];

//用我們新數(shù)據(jù)類型蚌本,從新封裝table
class NextUserTable extends Table<TableUser> {}

//這個(gè)方法方法是獲取數(shù)據(jù)
 getData = async () => {
        let { data } = await axios.get(`/api/user`, {
            params: {
                index: this.state.index,
                pageSize: this.state.pageSize
            }
        });


        if (data.status !== 'ok') return; //or error message

        let pageData: PageData<NextUser> = data.data;

        const pagination = { ...this.state.pagination };
        pagination.total = pageData.totalCount;
        pagination.pageSize = this.state.pageSize;

        const users: Array<NextUser> = pageData.list;
        const tableData: Array<TableUser> = [];

        for (let user of users) {
            tableData.push({
                key: user.id,
                id: user.id,
                name: user.name,
                age: user.age
            });
        }

        this.setState({
            index: this.state.index,
            tableData: tableData,
            pagination
        });
    };

//分頁(yè)獲取數(shù)據(jù)
    handleTableChange = (pagination, filters, sorter) => {
        this.setState({
            index: pagination.current
        });

        this.getData();
      };

然后我們的可以把a(bǔ)nt design的table控件寫成這樣:

//pages/index.tsx

 <NextUserTable
      dataSource={this.state.tableData}
      columns={columns}
      pagination={this.state.pagination} 
      onChange = {this.handleTableChange}
/>

因?yàn)槲夷壳皵?shù)據(jù)庫(kù)只有4條數(shù)據(jù)盔粹,我想弄個(gè)分頁(yè)看看,所以設(shè)置pageSize:為了2程癌,即1頁(yè)2條舷嗡,這樣我們就能看到有2頁(yè)面。
完整頁(yè)面代碼:

//pages/index.tsx

import React from 'react'
import { FormComponentProps } from "antd/lib/form/Form";
import { ColumnProps } from 'antd/es/table';
//引用用戶類型接口嵌莉,主要為了API返回?cái)?shù)據(jù)作類型轉(zhuǎn)換
import { NextUser, PageData } from "../repositories/user-repository";
import axios from 'axios';
import {
    Form,
    Input,
    Tooltip,
    Icon,
    Cascader,
    Select,
    Row,
    Col,
    Checkbox,
    AutoComplete,
    Button,
    Modal,
    InputNumber,
    Table,
    Divider
} from 'antd';
import '../css/antd.less';
import '../css/style.less';


interface IProps extends FormComponentProps {

}

interface IState {
    modalVisible?: boolean;
    index?: number;
    pageSize?: number;
    tableData?: Array<TableUser>;
    pagination?: any;
}

//新建一個(gè)用戶接口进萄,這個(gè)接口為table的item格式約束
interface TableUser {
    key?: number,
    id?: number;
    name?: string;
    age?: number;
}

//這個(gè)是ant design的列
const columns: ColumnProps<TableUser>[] = [
    {
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
    },
    {
        title: '年齡',
        dataIndex: 'age',
        key: 'age',
    },
    {
        title: '操作',
        key: 'action',
        render: (text, record) => (
            <span>
                <a>編輯</a>
                <Divider type="vertical" />
                <a>刪除</a>
            </span>
        ),
    }
];

const formItemLayout = {
    labelCol: {
        xs: { span: 18 },
        sm: { span: 6 },
    },
    wrapperCol: {
        xs: { span: 24 },
        sm: { span: 16 },
    },
};

//封裝table
class NextUserTable extends Table<TableUser> { }

class Home extends React.Component<IProps, IState> {
    constructor(props: IProps) {
        super(props);

        this.state = {
            modalVisible: false,
            index: 1,
            pageSize: 2, //一頁(yè)2條,是為了測(cè)試看到分頁(yè),我數(shù)據(jù)庫(kù)不想做太多數(shù)據(jù)
            tableData: []
        };
    };

    componentDidMount?(): void {
        this.getData();
    }

    getData = async () => {
        let { data } = await axios.get(`/api/user`, {
            params: {
                index: this.state.index,
                pageSize: this.state.pageSize
            }
        });


        if (data.status !== 'ok') return; //or error message

        let pageData: PageData<NextUser> = data.data;

        const pagination = { ...this.state.pagination };
        pagination.total = pageData.totalCount;
        pagination.pageSize = this.state.pageSize;

        const users: Array<NextUser> = pageData.list;
        const tableData: Array<TableUser> = [];

        for (let user of users) {
            tableData.push({
                key: user.id,
                id: user.id,
                name: user.name,
                age: user.age
            });
        }

        this.setState({
            index: this.state.index,
            tableData: tableData,
            pagination
        });
    };


    //分頁(yè)獲取數(shù)據(jù)
    handleTableChange = (pagination, filters, sorter) => {
        this.setState({
            index: pagination.current
        });

        this.getData();
    };


    //注意這里要寫成'handleAdd = () => {}',假如普通寫'handleAdd() {}'會(huì)引起this為undefined
    handleAdd = () => {
        this.setState({ modalVisible: true });
    };



    closeModal = () => {
        this.setState({ modalVisible: false });
    }

    doEdit = () => {



    };

    render() {
        const { getFieldDecorator } = this.props.form;

        return (
            <div className="container">
                <div style={{ paddingLeft: "50px" }}>
                    <Button type="primary" onClick={this.handleAdd}>新增</Button>
                </div>
                <div style={{ padding: "20px 50px" }}>
                    <NextUserTable
                        dataSource={this.state.tableData}
                        columns={columns}
                        pagination={this.state.pagination}
                        onChange={this.handleTableChange}
                    />
                </div>


                <Modal
                    title="Basic Modal"
                    visible={this.state.modalVisible}
                    onOk={this.closeModal}
                    onCancel={this.closeModal}
                >
                    <Form {...formItemLayout} onSubmit={this.doEdit}>
                        <Form.Item label="姓名">
                            {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 label="年齡">
                            <InputNumber min={1} max={200} defaultValue={17} style={{ width: '100%', marginRight: '3%' }} />
                        </Form.Item>
                    </Form>
                </Modal>
            </div>
        );
    }
}


export default Form.create()(Home);

啟動(dòng)我們?yōu)g覽器中鼠,可以看到我們數(shù)據(jù)庫(kù)的數(shù)據(jù)顯示在頁(yè)面上了:

圖12.獲取所有數(shù)據(jù)顯示

圖13.獲取所有數(shù)據(jù)顯示(數(shù)據(jù)庫(kù))

至此可婶,創(chuàng)建API,引入Ant Design援雇,做數(shù)據(jù)請(qǐng)求以及TypeScript的數(shù)據(jù)約束矛渴,我們涉及到要用知識(shí)點(diǎn),我們都已經(jīng)基本用上了惫搏,這里用上具温,余下部門(修改,刪除筐赔,新增)铣猩,我這邊只會(huì)直接把代碼貼出來,原理與上述原理并無差異

4.2 余下部分:?jiǎn)蝹€(gè)用戶查詢茴丰,新增剂习,修改及刪除用戶

這里并無新的知識(shí)點(diǎn),僅僅只是調(diào)用API较沪,然后渲染頁(yè)面(把頁(yè)面弄得好看點(diǎn))鳞绕,僅此,我這里把所有代碼都塞一個(gè)文件里尸曼,其實(shí)這樣并不方便維護(hù)们何,在實(shí)際開發(fā)時(shí)候,應(yīng)該注意分離邏輯控轿,增加復(fù)用冤竹,另外,上面獲取全部數(shù)據(jù)時(shí)候分頁(yè)有BUG茬射,下面的代碼也一并修復(fù)了鹦蠕,'pages/index.tsx'完整代碼:

//pages/index.tsx

import React from 'react'
import { FormComponentProps } from "antd/lib/form/Form";
import { ColumnProps } from 'antd/es/table';
//引用用戶類型接口,主要為了API返回?cái)?shù)據(jù)作類型轉(zhuǎn)換
import { NextUser, PageData } from "../repositories/user-repository";
import axios from 'axios';
import {
    Form,
    Input,
    Tooltip,
    Icon,
    Cascader,
    Select,
    Row,
    Col,
    Checkbox,
    AutoComplete,
    Button,
    Modal,
    InputNumber,
    Table,
    Divider,
    message
} from 'antd';
import '../css/antd.less';
import '../css/style.less';


interface IProps extends FormComponentProps {

}

interface IState {
    modalVisible?: boolean;
    pageSize?: number;
    tableData?: Array<TableUser>;
    pagination?: any;
    formData?: {
        id?: number;
        name?: string;
        age?: number;
    };
}

//新建一個(gè)用戶接口在抛,這個(gè)接口為table的item格式約束
interface TableUser {
    key?: number,
    id?: number;
    name?: string;
    age?: number;
}

const formItemLayout = {
    labelCol: {
        xs: { span: 18 },
        sm: { span: 6 },
    },
    wrapperCol: {
        xs: { span: 24 },
        sm: { span: 16 },
    },
};

//封裝table
class NextUserTable extends Table<TableUser> { }

class Home extends React.Component<IProps, IState> {
    constructor(props: IProps) {
        super(props);

        this.state = {
            modalVisible: false,
            pageSize: 2, //一頁(yè)2條钟病,是為了測(cè)試看到分頁(yè),我數(shù)據(jù)庫(kù)不想做太多數(shù)據(jù)
            tableData: [],
            formData: {}
        };
    };

    //這個(gè)是ant design的列
    columns: ColumnProps<TableUser>[] = [
        {
            title: '姓名',
            dataIndex: 'name',
            key: 'name',
        },
        {
            title: '年齡',
            dataIndex: 'age',
            key: 'age',
        },
        {
            title: '操作',
            key: 'action',
            render: (text, record) => (
                <span>
                    <a onClick={() =>this.handleEdit(record.id)}>編輯</a>
                    <Divider type="vertical" />
                    <a onClick={() =>this.doDelete(record.id)}>刪除</a>
                </span>
            ),
        }
    ];

    componentDidMount?(): void {
        this.getData(1);
    }

    getData = async (index) => {
        let { data } = await axios.get(`/api/user`, {
            params: {
                index: index,
                pageSize: this.state.pageSize
            }
        });


        if (data.status !== 'ok') return; //or error message

        let pageData: PageData<NextUser> = data.data;

        const pagination = { ...this.state.pagination };
        pagination.total = pageData.totalCount;
        pagination.pageSize = this.state.pageSize;

        const users: Array<NextUser> = pageData.list;
        const tableData: Array<TableUser> = [];

        for (let user of users) {
            tableData.push({
                key: user.id,
                id: user.id,
                name: user.name,
                age: user.age
            });
        }

        this.setState({
            tableData: tableData,
            pagination
        });
    };


    //分頁(yè)獲取數(shù)據(jù)
    handleTableChange = async (pagination, filters, sorter) => {
        await this.getData(pagination.current);
    };

    //注意這里要寫成'handleAdd = () => {}',假如普通寫'handleAdd() {}'會(huì)引起this為undefined
    handleAdd = () => {
        this.setState({
            modalVisible: true,
            formData: {}
        });
    };

    handleEdit = async id => {
        let { data } = await axios.get(`/api/user/${id}`);
        if(data.status !== "ok") return; //show some error

        let user:NextUser = data.data;
        this.setState({
            modalVisible: true,
            formData: {
                id:id,
                name:user.name,
                age:user.age
            }
        });
    };

    closeModal = () => {
        this.setState({ modalVisible: false });
    }

    doDelete = async id =>{
        let { data } = await axios.delete(`/api/user/${id}`);

        if (data.status === "ok") {
            message.success('刪除成功');
            this.getData(1);
        } else {
            message.error('刪除失敗');
        }
    };

    doEdit = async e => {
        e.preventDefault();
        this.props.form.validateFields(async (err, values) => {
            if (err) return;

            //Received values of form: {name: "1231212312", age: 123}

            if (this.state.formData.id) {
                let { data } = await axios.put(`/api/user/${this.state.formData.id}`, values);

                if (data.status === "ok") {
                    message.success('修改成功');
                    this.setState({ 
                        modalVisible: false
                     });
                    this.getData(1);
                } else {
                    message.error('修改失敗');
                }
            } else {
                let { data } = await axios.post(`/api/user`, values);

                if (data.status === "ok") {
                    message.success('添加成功');
                    this.setState({ modalVisible: false });
                } else {
                    message.error('添加失敗');
                }
            }
        });
    };

    render() {
        const { getFieldDecorator } = this.props.form;

        return (
            <div className="container">
                <div style={{ paddingLeft: "50px" }}>
                    <Button type="primary" onClick={this.handleAdd}>新增</Button>
                </div>
                <div style={{ padding: "20px 50px" }}>
                    <NextUserTable
                        dataSource={this.state.tableData}
                        columns={this.columns}
                        pagination={this.state.pagination}
                        onChange={this.handleTableChange}
                    />
                </div>


                <Modal
                    title="Basic Modal"
                    visible={this.state.modalVisible}
                    onCancel={this.closeModal}
                    footer={[
                        <Button form="form" key="submit" htmlType="submit">
                            Submit
                        </Button>
                    ]}
                >
                    <Form id="form" {...formItemLayout} onSubmit={this.doEdit}>
                        <Form.Item label="姓名">
                            {getFieldDecorator('name', {
                                initialValue: this.state.formData.name,
                                rules: [{ required: true, message: 'Please input your name!' }],
                            })(
                                <Input
                                    prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                                    placeholder="name"
                                />,
                            )}

                        </Form.Item>
                        <Form.Item label="年齡">
                            {getFieldDecorator('age', {
                                initialValue: this.state.formData.age,
                                rules: [{ required: true, message: 'Please input your age!' }],
                            })(
                                <InputNumber
                                    min={1}
                                    max={200}
                                    style={{ width: '100%', marginRight: '3%' }}
                                    placeholder="age"
                                />
                            )}
                        </Form.Item>
                    </Form>
                </Modal>
            </div>
        );
    }
}


export default Form.create()(Home);

5.總結(jié)

總體來說刚梭,Next.js可以滿足某些項(xiàng)目的全棧開發(fā)需求肠阱,當(dāng)然,我這個(gè)demo用在生產(chǎn)環(huán)境上還遠(yuǎn)遠(yuǎn)不足朴读,例如缺少權(quán)限驗(yàn)證(api接口不是誰(shuí)都能調(diào)用屹徘,要授權(quán)),日志衅金,連接池等等噪伊。

然后簿煌,Next是可以用服務(wù)端渲染,這個(gè)就有點(diǎn)像.NET的MVC和Java的JSP鉴吹,但我估計(jì)現(xiàn)在很少人會(huì)這樣用了吧啦吧?

另外,本人React方面新手一枚拙寡,有誤導(dǎo)的地方,望留言指出琳水。

我個(gè)人感覺這個(gè)東西在一些小項(xiàng)目上面嘗試肆糕,其實(shí)還是不錯(cuò)(大項(xiàng)目不用是因?yàn)槲夷壳斑@方面的知識(shí)欠缺,不敢亂來)在孝,anyway,每一樣?xùn)|西總有個(gè)起步吧诚啃?
最后祝大家新年快樂!
demo的git地址:https://github.com/JaqenHo/next_js_learn.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末私沮,一起剝皮案震驚了整個(gè)濱河市始赎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仔燕,老刑警劉巖造垛,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異晰搀,居然都是意外死亡五辽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門外恕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杆逗,“玉大人,你說我怎么就攤上這事鳞疲∽锝迹” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵尚洽,是天一觀的道長(zhǎng)悔橄。 經(jīng)常有香客問我,道長(zhǎng)腺毫,這世上最難降的妖魔是什么橄维? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮拴曲,結(jié)果婚禮上争舞,老公的妹妹穿的比我還像新娘。我一直安慰自己澈灼,他們只是感情好竞川,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布店溢。 她就那樣靜靜地躺著,像睡著了一般委乌。 火紅的嫁衣襯著肌膚如雪床牧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天遭贸,我揣著相機(jī)與錄音戈咳,去河邊找鬼。 笑死壕吹,一個(gè)胖子當(dāng)著我的面吹牛著蛙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播耳贬,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼踏堡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了咒劲?” 一聲冷哼從身側(cè)響起顷蟆,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腐魂,沒想到半個(gè)月后帐偎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛔屹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年肮街,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片判导。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嫉父,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出眼刃,到底是詐尸還是另有隱情绕辖,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布擂红,位于F島的核電站仪际,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏昵骤。R本人自食惡果不足惜树碱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望变秦。 院中可真熱鬧成榜,春花似錦、人聲如沸蹦玫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至挣输,卻和暖如春纬凤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背撩嚼。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工停士, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人完丽。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓恋技,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親舰涌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353