1 创建Next.js项目

1
yarn create next-app --typescript

按照提示填写项目名称后, 项目就搭建完成了,以下是项目文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│   ├── _app.tsx
│   ├── api
│   │   └── hello.ts
│   └── index.tsx
├── public
│   ├── favicon.ico
│   └── vercel.svg
├── styles
│   ├── Home.module.css
│   └── globals.css
├── tsconfig.json
└── yarn.lock

2 docker启动postgresql

  1. 创建数据库目录/blog-data并添加至.gitignore
  2. Docker启动PostgreSQL
1
docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
  • 释义:

    • -v 本地目录:容器目录 或 -v 容器目录——表示将本机某目录,挂载到镜像中某目录下
    • -d——后台运行容器
    • -p——端口映射,指定主机的5432端口映射到容器的5432端口
    • -it——以交互模式运行启动容器 
    • -e——指定容器环境变量
  • windows系统需要将$PWD去掉

  • 如果下载慢,先搜阿里云镜像(需要登录阿里云),再搜Docker加速

  • 容器搭建结束后会返回容器的id

  • docker ps -a查看全部容器运行状态

  • docker logs id查看容器启动日志

  1. 验证是否成功启动
  • docker exec -it 容器id bash使用bash命令行进入容器
  • psql -U blog -W进入postgres数据库(没有设置密码)
  • 执行pg命令
    • \l: list databases
    • \c:connect to a database
    • \dt:display tables
  1. 创建数据库

由于TypeORM没有提供单纯创建数据库的API,只能使用SQL语句来创建数据库

1
CREATE DATABASE xxx ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';

分别创建blog_devblog_productblog_test三个数据库

3 Next.js安装TypeORM

安装相关依赖

1
yarn add typeorm reflect-metadata pg  

修改tsconfig.json

1
2
"emitDecoratorMetadata": true,
"experimentalDecorators": true

在初始化TypeORM前,应提交一次代码,用来恢复被初始化后的文件

使用npx执行TypeORM初始化命令

1
npx typeorm init --database postgres

利用checkout命令将.gitignorepackage.jsontsconfig.json恢复至上次最新提交的状态

1
git checkout HEAD -- .gitignore

最终初始化的结果为新增了src文件夹与ormconfig.json,修改ormconfig

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "type": "postgres",
  "host": "localhost",
  "port": 5432,
  "username": "blog",
  "password": "",
  "database": "blog_dev",
  "synchronize": false,
  "logging": false,
  "entities": ["src/entity/**/*.ts"],
  "migrations": ["src/migration/**/*.ts"],
  "subscribers": ["src/subscriber/**/*.ts"],
  "cli": {
    "entitiesDir": "src/entity",
    "migrationsDir": "src/migration",
    "subscribersDir": "src/subscriber"
  }
}

初始化完成

4 统一TypeScript编译

TypeORM建议使用ts-node运行ts,而Next.js默认使用内置的babel转义ts为js后再用node来运行。因此我们统一用babel和node来编译和运行ts。

安装@babel/cli

1
yarn add @babel/cli @babel/core

利用npx执行babel命令,将src文件编译为js文件,存入dist文件夹下

1
npx babel ./src --out-dir dist --extensions ".ts,.tsx"

创建.babelrc,在next内置babel的基础上添加装饰器插件

1
2
3
4
{
  "presets": ["next/babel"],
  "plugins": []
}

修改ormconfig.json中的entities、migrations和subscribers,否则迁移数据库等操作会报错

1
2
3
4
5
...
"entities": ["dist/entity/**/*.js"],
"migrations": ["dist/migration/**/*.js"],
"subscribers": ["dist/subscriber/**/*.js"],
...

修改tsconfig.json,将module改成commonjs

运行node ./dist/index.js后,控制台打印connection的信息,说明已成功连接数据库服务

5 使用migration创建表

利用npx执行typeorm migration创建

1
npx typeorm migration:create -n CreatePost

得到src/migrations/{TIMESTAMP}-CreatePost.ts,编写up和down函数(分别用于升级与降级数据库)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { MigrationInterface, QueryRunner, Table } from "typeorm";

export class CreatePost1641976068405 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    // 升级数据库
    // return可选,但await要记得添加上
    return await queryRunner.createTable(
      new Table({
        name: "posts",
        columns: [
          {
            name: "id",
            type: "int",
            isPrimary: true,
            isGenerated: true,
            generationStrategy: "increment",
          },
          {
            name: "title",
            type: "varchar",
          },
          {
            name: "content",
            type: "text",
          },
        ],
      })
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    // 降级数据库
    // revert报错无法继续回退的情况,可以利用try catch来解决
    try{
      return await queryRunner.dropTable("posts");
    } catch(error) {}
  }
}

将编译指令与执行数据库迁移指令封装到package.json的scripts属性

1
2
“migration:run": "typeorm migration:run",
"migration:revert": "typeorm migration:revert"

执行同步数据库命令后,进入docker检查对应数据库是否同步成功

  • 优化package.jsonscripts,启动next项目的同时自动开启babel编译,并监听代码变动
1
2
3
"dev": "next dev & babel -w /src --out-dir dist --extensions .ts,.tsx"
// windows系统下 yarn add -D concurrently
"dev": "concurrently \"next dev\" \"babel -w /src --out-dir dist --extensions .ts,.tsx\""

6 数据映射到实体

将新建entity的命令添加至scripts

1
"entity:create": "typeorm entity:create"

执行创建命令

1
yarn entity:create -n Post

执行后,src文件夹下新增了entity文件夹,以及其中的Post.ts,新建代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

// posts表的实体
@Entity("posts")
export class Post {
  // 自增 主键
  @PrimaryGeneratedColumn("increment")
  id: string;
  // 类型为varchar的列
  @Column("varchar")
  title: string;
  // 类型为text的列
  @Column("text")
  content: string;
}

7 操作实体

  • TypeORM为我们提供了两套操作实体的API,分别是ManagerRepository,这里选择ManagerAPI

  • Manager API的封装思路就是将数据的具体操作交给manager,把User实体类,user1对象和其它参数传给manager即可,Manager常用API如下:

    • await manager.find(User, { name: 'frank' })
    • await manager.create(User, { name: '...' })
    • await manager.save(user1)
    • await manager.save([user1, user2, user3])
    • await manager.remove(user1)(涉及到事务操作)
    • await manager.update(User, 1, { name: 'frank' })
    • await manager.delete(User, 1)
    • await manager.findOne(User, 1)
  • Repository API的封装思路为先通过User构造一个repo对象,这个repo对象就只操作User表了,具体示例如下:

    • 1
      2
      3
      
      const userRepository = getRepository(User)
      await userRepository.findOne(1)
      await userRepository.save(user)
      
  • 测试manager API,回到index.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "reflect-metadata";
import { createConnection } from "typeorm";
import { Post } from "./entity/Post";

createConnection()
  .then(async (connection) => {
    // 在connection中取出manager
    const { manager } = connection;
    const posts = await manager.find(Post);
    // 第一次查找全部Post数据应该为空
    console.log(posts);
    const p = new Post();
    p.title = "Post 1";
    p.content = "我的第一篇博客";
    // 保存p对象
    await manager.save(p);
    const posts2 = await manager.find(Post);
    // 应该有新增的一条数据
    console.log(posts2);
    // 最后要关闭连接
    connection.close();
  })
  .catch((error) => console.log(error));
  • 可能会出现不支持装饰器语法的bug,添加babel插件即可
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
yarn add --dev @babel/plugin-proposal-decorators
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
      [
          "@babel/plugin-proposal-decorators",
          {
              "legacy": true
          }
      ]
  ]
}
  • 测试结果:
1
2
3
$ node ./dist/index
[]
[ Post { id: 1, title: 'Post 1', content: '我的第一篇博客' } ]

8 数据填充

  • 目前数据库与数据表都完成了,但是没有数据,可以通过seed脚本来构造数据

  • 将src下的index.ts改为seed.ts

  • 方便新增数据,改写entity下的Post.ts,添加构造器函数

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
    
    // posts表的实体
    @Entity("posts")
    export class Post {
      // 自增 主键
      @PrimaryGeneratedColumn("increment")
      id: string;
      // 类型为varchar的列
      @Column("varchar")
      title: string;
      // 类型为text的列
      @Column("text")
      content: string;
      // Partial的作用在于 不需要将Post的全部属性都传递过来
      constructor(attributes: Partial<Post>) {
        // 将attributes的全部属性放到this上即可
        Object.assign(this, attributes);
      }
    }
    
  • Seed.ts

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    import "reflect-metadata";
    import { createConnection } from "typeorm";
    import { Post } from "./entity/Post";
    
    createConnection()
      .then(async (connection) => {
        const { manager } = connection;
        const posts = await manager.find(Post);
        if (posts.length === 0) {
          await manager.save(
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => {
              return new Post({
                title: `Post ${v}`,
                content: `这是我的第${v}篇博客`,
              });
            })
          );
          console.log(`posts数据填充完成!`);
        }
        connection.close();
      })
      .catch((error) => console.log(error));
    
  • 执行结果

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    $ node ./dist/seed   
    posts数据填充完成!
    
    blog_dev=# select * from posts;
     id |  title  |      content       
    ----+---------+--------------------
      1 | Post 1  | 这是我的第1篇博客
      2 | Post 2  | 这是我的第2篇博客
      3 | Post 3  | 这是我的第3篇博客
      4 | Post 4  | 这是我的第4篇博客
      5 | Post 5  | 这是我的第5篇博客
      6 | Post 6  | 这是我的第6篇博客
      7 | Post 7  | 这是我的第7篇博客
      8 | Post 8  | 这是我的第8篇博客
      9 | Post 9  | 这是我的第9篇博客
     10 | Post 10 | 这是我的第10篇博客
    (10 rows)