Next.js与TypeScript从零起步学习笔记-5(终).项目实战NextJs+PostgreSQL+AntDesign

此文章,会从零开始结合Ant Design UI和PostgreSQL做一个简单的增删改
这里只是一个简单的demo,真实的开发中我们能可能还需要权限,日志,连接池等等。

参考官网:https://nextjs.org/learn/basics/getting-started

开发环境:
window10 x64
node v10.16.3
npm v6.13.4

1.项目初始化

参考‘Next.js与TypeScript从零起步学习笔记-1’,我们先创建一个空项目并添加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"
}

然后我们在'pages'目录下,建一个'index.tsx',随便输入点代码,测试一下是否能跑去起来:

//pages/index.tsx

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

export default Home;

运行项目:

npm run dev

应该可以看到效果:

图1.项目运行

图2.目录结构

这里我并打算用到严格模式(类型约束),因为后面文章引用到的类库,会出现诸多问题,为项目简单起见,我不建议用严格模式。

2.创建RESTful API

2.1 创建表

我们需要在数据库创建一张用户表,用来存放用户的数据,并对其进行简单的增删改查,我用的数据库是PostgreSQL。(PS:你需要有一点数据库相关的知识)

--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 创建RESTful API

我们在pages文件夹下创建一个api文件夹,主要用来存放我们api

//以项目根目录为准

cd pages
mkdir api
cd api
mkdir user

我们在'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 "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");
}

以上,我们已经创建了我们最简单的RESTful API了,把项目跑起来,我们能看到效果


图3.RESTful API效果图

PS:以上是动态路由,即根据路径获取资源Id,如图3URL:http://localhost:3000/api/user/1,后面的'/1'表示的是访问这个id的资源,现实中只有编辑时候才会用到,新增或获取列表时候,并不适用,所以:
我们需要在'pages/api'下,新添一个路由文件'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");
}

我们再来修改'pages/api/user/[id].ts',把post去掉,因为post是新增,所以逻辑上在这个文件永远用不上

//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.文件结构

这样建立文件,看上去有点繁琐,但目前我所知道,Next的路由就是这样。
能不能像.NET这样,用标签属性来确定呢?我目前没有找到好方法,知道的小伙伴请不吝赐教,私信告诉我

2.3 对数据库进行读写

我们连接数据库,第一步是先安装上驱动,我开始使用的是'ts-postgres',为什么用它?没啥的,就是因为这个东西是TS写的,但后面发现坑有点多,例如我要count多少行,总是返回null,我以为是函数问题,但换了now又正常,弄了很久,后面放弃了,只好换另一个驱动'node-postgres',这个东西问题就是用js写的,ts里用,你会发现一堆都是any。
安装node-postgres:

npm install pg

我们先来创建一个配置文件,这个配置文件用来保存一些系统的配置,例如数据库连接等等,在根目录下创建'config.json':

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

然后我们创建一个文件,命名为'repositories',用来存放sql操作等逻辑:

//以项目根目录为准

cd pages
mkdir repositories

在'repositories'文件夹下,我们创建一个文件'user-repository.ts',这里主要编写用户表(见2.1)的读写逻辑。

//repositories/user-repository.ts

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

//这个接口应该单独弄出去,弄个文件夹夹叫utility之类放着,因为后续肯定不止这个地方用到。
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 {
    //分页获取所有用户
    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的话,会同步等待结果返回
            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();
        }
    }

    //根据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'文件,让他们访问数据库
PS:正常来说,为了逻辑复用,api不应该直接访问数据仓储层(repository),中间应该多一个service层什么的,这边只是一个演示demo,以简优先,所以省略很多

//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"});
}

启动一下服务器,测试一下结果:

npm run dev

我们先post加一条数据:


图5.测试添加数据

图6.测试添加数据

然后看看接口查询返回:


图7.测试获取数据

其他诸如删除修改等,我就不一一截图了。

3.引入Ant Design UI

3.1配置Ant Design UI

我UI的功底十分差劲,所以引入第三方UI框架,实际开发上,有很多开源且优秀的框架以供你使用,你并不需要重复造轮子。阿里体系貌似全部推荐用yarn,可能大概是跟他们作者之间有什么关联吧?开始yarn有一个lock的优势,但npm后面也跟着更新了,所以我本人觉得yarn相对npm,优势不大。
引入Ant Design UI,在命令行输入:

npm install antd --save

这里有些按需加载的知识需要了解,实际上你应该按需加载。本篇为了简单,所以用全加载。
Any Design UI的使用,请参阅:https://ant.design/index-cn;
除了上述UI的使用文档,我们可能还需要参阅Pro Ant Design:https://pro.ant.design/docs/uset-typescript-cn,这里收集了一些TypeScript的问题。
Next在git上有一个demo,请参阅:https://github.com/zeit/next.js/tree/canary/examples/with-ant-design

Next加载全局CSS时候,需要配置,我这边习惯用less,所以sass不安装了。
安装less:

npm install less less-loader

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

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

然后我们在跟目录下,添加一个配置文件'next.config.js'。没错,这就是我们的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
  }
}))

还有最后一步,就是要找一个地方,配置ant design的全局样式,我们建立一个在跟目录下,建一个文件夹'css',在css下建立一个'antd.less',敲入:

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

然后我们在首页,加点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;

启动项目,我们可以看到,ant design的东西出来:


图8.ant design配置测试

目前整体目录结构如下(next-env.d.ts,tsconfig.json这个是系统运动时候生成的):


图9.配置后的目录结构
3.2 做一个简单的增删改页面

现在我们可以用它来做一个带分页的增删改页面了,页面我本地已经做好了,这里直接把它粘贴出来,也没什么好说的。。。。不过是无脑的套用UI框架而已,时间格式可能需要美化,自己写一个方法或者借助第三库如'@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() {}'会引起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);

启动可以看到效果:


图10.效果图

图11.效果图

很简单的就能搭建一个页面,感谢这些开源企业的无私奉献,让我们的工作变得简单。

4.与API对接

现在到了开发的最后一步,我们需要利用之前的RESTful API(详见2)对数据库读写,并渲染页面。这里我使用第三库'axios'请求http接口,安装axios:

npm install axios

然后在页面'pages/index.tsx'的顶部引用进来:

//pages/index.tsx

import axios from 'axios';
4.1 获取所有数据

我们需要把现在的数据库的用户都显示在界面,在'pages/index.tsx'中,我们可以写一个方法这样请求接口:

//pages/index.tsx

//引用用户类型接口,主要为了API返回数据作类型转换
import { NextUser, PageData } from "../repositories/user-repository";
//引用Ant Design的table props,用于table的数据约束
import { ColumnProps } from 'antd/es/table';

//新建一个用户接口,这个接口用于table的数据显示
interface TableUser{
    key?:number,
    id?: number;
    name?: string;
    age?: number;
}

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

//这个是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>
        ),
    }
];

//用我们新数据类型,从新封装table
class NextUserTable extends Table<TableUser> {}

//这个方法方法是获取数据
 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
        });
    };

//分页获取数据
    handleTableChange = (pagination, filters, sorter) => {
        this.setState({
            index: pagination.current
        });

        this.getData();
      };

然后我们的可以把ant design的table控件写成这样:

//pages/index.tsx

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

因为我目前数据库只有4条数据,我想弄个分页看看,所以设置pageSize:为了2,即1页2条,这样我们就能看到有2页面。
完整页面代码:

//pages/index.tsx

import React from 'react'
import { FormComponentProps } from "antd/lib/form/Form";
import { ColumnProps } from 'antd/es/table';
//引用用户类型接口,主要为了API返回数据作类型转换
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;
}

//新建一个用户接口,这个接口为table的item格式约束
interface TableUser {
    key?: number,
    id?: number;
    name?: string;
    age?: number;
}

//这个是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, //一页2条,是为了测试看到分页,我数据库不想做太多数据
            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
        });
    };


    //分页获取数据
    handleTableChange = (pagination, filters, sorter) => {
        this.setState({
            index: pagination.current
        });

        this.getData();
    };


    //注意这里要写成'handleAdd = () => {}',假如普通写'handleAdd() {}'会引起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);

启动我们浏览器,可以看到我们数据库的数据显示在页面上了:

图12.获取所有数据显示

图13.获取所有数据显示(数据库)

至此,创建API,引入Ant Design,做数据请求以及TypeScript的数据约束,我们涉及到要用知识点,我们都已经基本用上了,这里用上,余下部门(修改,删除,新增),我这边只会直接把代码贴出来,原理与上述原理并无差异

4.2 余下部分:单个用户查询,新增,修改及删除用户

这里并无新的知识点,仅仅只是调用API,然后渲染页面(把页面弄得好看点),仅此,我这里把所有代码都塞一个文件里,其实这样并不方便维护,在实际开发时候,应该注意分离逻辑,增加复用,另外,上面获取全部数据时候分页有BUG,下面的代码也一并修复了,'pages/index.tsx'完整代码:

//pages/index.tsx

import React from 'react'
import { FormComponentProps } from "antd/lib/form/Form";
import { ColumnProps } from 'antd/es/table';
//引用用户类型接口,主要为了API返回数据作类型转换
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;
    };
}

//新建一个用户接口,这个接口为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, //一页2条,是为了测试看到分页,我数据库不想做太多数据
            tableData: [],
            formData: {}
        };
    };

    //这个是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
        });
    };


    //分页获取数据
    handleTableChange = async (pagination, filters, sorter) => {
        await this.getData(pagination.current);
    };

    //注意这里要写成'handleAdd = () => {}',假如普通写'handleAdd() {}'会引起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.总结

总体来说,Next.js可以满足某些项目的全栈开发需求,当然,我这个demo用在生产环境上还远远不足,例如缺少权限验证(api接口不是谁都能调用,要授权),日志,连接池等等。

然后,Next是可以用服务端渲染,这个就有点像.NET的MVC和Java的JSP,但我估计现在很少人会这样用了吧?

另外,本人React方面新手一枚,有误导的地方,望留言指出。

我个人感觉这个东西在一些小项目上面尝试,其实还是不错(大项目不用是因为我目前这方面的知识欠缺,不敢乱来),anyway,每一样东西总有个起步吧?
最后祝大家新年快乐!
demo的git地址:https://github.com/JaqenHo/next_js_learn.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350