AdonisJS
基本
安装
参考官网,执行指令初始化项目
1 | npm init adonisjs@latest <项目名> |
执行后出现的四个选项,即选择内置版本框架:
- Slim Starter Kit: 精简版本,包含http服务、路由、控制器等基本功能
- Web Starter Kit: Web套件,包含模板引擎,即古早后端渲染的方式,适用于前端不想做复杂功能,仅展示的项目
- API Starter Kit: API套件,可以理解为前者的简化版,即前者去掉模板引擎的版本,适用于前后端分离的项目
- Innertia Starter Kit: 简化 vue 等前端项目开发流程,router、fetch 等全在后端自动处理,和 Nuxt 的开发体验比较类似,缺点是前后端耦合较重。
选择基础架构后,继续弹出的选项为选择验证手段:
- Session: session、cookie 等传统方式,适用于普通网站项目,优势是可自动携带。在选择 Web 套件 的情况下多端项目无法使用这种方式,选择 API 套件 则无所谓。
- Access Token: JWT 方式,更灵活但较前者复杂,且在选择 Web 套件 的情况下可以支持多端项目
- Basic Auth: base64 加密,多适用于内网项目,使用较少
- Skip: 不使用内置验证,自己配置
然后会选择数据库,这个因人而异。
命令
使用框架时会高频使用框架给出的命令行工具,通用命令如下:
1 | # 查看框架命令 |
数据库
项目中使用的是 AdonisJS 官方维护的数据库 ORM(Object-Relational Mapping)工具 Lucid。
先修改 .env 文件中的数据库连接配置,如果配置错误会导致后续的迁移文件无法执行。
1 | # 数据库地址 |
同时,.env 的时区字段也不要忘记设置,否则自动创建的时间字段(例如 created_at)会有时区问题,导致时间不对。
1 | # PRC 为北京时间时区 |
每一个业务都有几个概念:
- 数据表、版本控制(migration)
- 批量生成测试数据(factory)
- 填充文件填充定义的测试数据(seeder)
- 数据模型(model)
- 控制器,响应用户请求(controller)
迁移文件(migration)
随着项目版本更新,可能需要频繁改动数据库,增删字段等。这就需要给数据库进行版本控制。
数据迁移文件(migration)就是用来管理数据库版本的工具,记录每次对数据库的改动,并且可以随时回滚到之前的版本。
可以通过命令 make:migration 来创建一个迁移文件:
1 | node ace make:migration <迁移文件名> |
执行完毕后会自动在 database/migrations 目录下生成一个新的迁移文件,文件名格式为 <时间戳>_create_<指定name的复数形态>_table.ts,其中时间戳为当前时间的毫秒数。
时间戳指定了表创建的顺序,后面做一些操作的时候会有影响(例如关联表的时候只能关联在前面创建的表)。
但通常情况下,我们并不建议使用这种方式创建迁移文件。在实际开发中,我们需要将业务牵扯到的三个概念一起创建,而非在这里一个一个创建。这部分继续参考后续的模型章节。
执行迁移文件
有了迁移文件后,我们就可以执行迁移文件来创建数据库表了。执行迁移文件的命令如下:
1 | node ace migration:run |
只要数据库链接设置没有问题,执行这个命令就会自动创建数据库+建表。
然后我们使用数据库可视化工具查看创建的数据库,可以看到一个 adonis_migrations 的表,这个表就是用来记录迁移文件执行情况的,每次执行迁移文件都会在这个表中插入一条记录,记录迁移文件的名称和执行时间。
回滚
既然有了版本,那就必然就可以回滚到之前的版本。回滚的命令如下:
1 | # 删除所有表,然后重新执行所有迁移。适用于开发阶段重置数据库结构,生产环境慎用(开发最常用了x |
执行回滚时会根据迁移文件的 down 方法来回滚数据库结构。
只要修改了已有的迁移文件内容(哪怕是新增字段,没有动旧字段),就必须要执行回滚命令重跑。因此生产环境一定不要修改已执行的迁移文件内容,而是应该新建一个迁移文件来修改数据库结构。
测试数据(factory)
类似英文起名,这个概念是像工厂一样批量生产测试数据的工具。每个模型都可以有一个对应的工厂文件,工厂文件中定义了如何生成测试数据。
可以通过命令 make:factory 来创建一个 artist 工厂文件:
1 | # node ace make:factory <工厂文件名> |
但通常依旧不会像这样单独创建工厂文件,而是会在创建模型的时候同时创建工厂文件,后续继续参考模型章节。
执行命令后,会在 database/factories 目录下生成一个新的工厂文件,文件名格式为 <指定name>.factory.ts,文件初始内容为:
1 | import factory from "@adonisjs/lucid/factories"; |
此处关于 faker 对象,牵扯到另外一个库 faker.js,这个库提供了大量的假数据生成方法,可以用来生成各种类型的测试数据。
我们可以自行添加需要的字段和数据生成方法,例如:
1 | export const ArtistFactory = factory |
如果此时我们已经定义了模型,就有可能会出现类型报错。这是因为工厂中设定的字段必须要在模型中定义,否则就会报错。解决这个问题的方法就是在模型中定义对应的字段。
交互式命令行测试工厂文件
之后我们可以通过交互式命令来尝试创建一下数据,交互式命令为 repl:
1 | node ace repl |
在交互式命令行中,可以执行 .ls 来查看当前可以使用的功能/函数,这里使用 loadFactories 函数:
1 | # 记得加小括号调用函数 |
之后就可以进行工厂相关操作了,此时也会开始有即时执行结果提示。此时我们根据代码提示,来进行操作:
1 | # 生成数据,但不写入到数据库 |
填充文件(seeder)
有了工厂文件后,就可以通过工厂文件来填充数据了。此时我们还缺一个填充文件(seeder),填充文件用来执行工厂文件生成测试数据的命令。
可以通过命令 make:seeder 来创建一个 artist 填充文件:
1 | # node ace make:seeder <填充文件名> |
执行命令后,会在 database/seeders 目录下生成一个新的填充文件,文件名格式为 <指定name>.seeder.ts,文件初始内容为:
1 | import { BaseSeeder } from "@adonisjs/lucid/seeders"; |
在填充文件里,我们可以像上面交互式命令一样,引入我们定义好的工厂文件进行操作:
1 | import { BaseSeeder } from "@adonisjs/lucid/seeders"; |
编写好填充文件后,就可以通过命令 db:seed 来执行填充文件了:
1 | node ace db:seed |
可以看到数据库里多了三条数据出来。
自行修改内容
seeder 文件的内容是可以任意编写的,我们也可以在里面修改数据库数据。
例如我们查找 artist 表里的第一条数据,并修改它的 name 字段:
1 | import { BaseSeeder } from "@adonisjs/lucid/seeders"; |
再次执行 db:seed 后,就会看到第一条数据的 name 字段被修改了。
回滚后快速填充数据
在回滚命令后面添加 --seed 参数,就会在回滚后自动执行填充文件来快速填充数据了:
1 | node ace migration:fresh --seed |
命令创建后,会在 app/Controllers/Http 目录下生成一个新的控制器文件,文件名格式为 <指定name的复数形态>Controller.ts,文件初始内容为:
1 | // import type { HttpContext } from '@adonisjs/core/http' |
控制器有一个参数 --resource,简写为 -r 可以一次性创建一个包含增删改查方法的控制器:
1 | node ace make:controller artist -r |
生成后的控制器内容为:
1 | import type { HttpContext } from "@adonisjs/core/http"; |
这里解释下控制器这几个方法对应的路由:
index: 列表 - GET /artistcreate: 创建页面 - GET /artist/createstore: 创建 - POST /artistshow: 详情 - GET /artist/:idedit: 编辑页面 - GET /artist/:id/editupdate: 更新 - PUT /artist/:iddestroy: 删除 - DELETE /artist/:id
其中 create 和 edit 方法是用来返回页面的,这是模板引擎相关的方法,常规开发 api 并不需要他们。
因此这就引出了控制器的另一个参数 --api,简写为 -a,其创建的控制器内容相对 -r,会去掉 create 和 edit 方法。
这里你可能还会对这个路由与控制器方法的映射比较疑惑。接下来我们会通过路由注册来解释这层映射关系
路由注册
路由在 start/routes.ts 文件中定义。我们可以通过命令 list:routes 来查看当前定义的所有路由:
1 | node ace list:routes |
通常我们可以对路由进行逐条编写,例如:
1 | router.get( "/artist", () => {} ); |
但这样太麻烦了,我们就可以借助 router.resource 来批量创建接口
1 | // ArtistsController 即控制器类 |
此时使用 list:routes 来查看路由,会发现所有相关的接口全都被注册了:
1 | GET /artist (artist.index) |
不难发现,生成的路由里存在控制器里并不存在的 create 和 edit。
对此,router.resource 还可以通过 apiOnly 方法来过滤掉模板引擎特有的 create 和 edit 路由:
1 | router.resource( "artist", ArtistsController ).apiOnly(); |
然后再次查看路由,会发现结果中已经不存在 create 和 edit 了。
这里就需要确定一下映射关系了:即,**router.resource 负责批量创建路由,在收到api访问时,根据映射,去调用控制器内的相关方法。它创建的路由和控制器的内容毫无关系,控制器里没有的方法只会导致在对应 api 被调用时,会返回 500 而已**
手动指定路由和控制器触发方法
对此,当我们并不需要创建全套路由时,可以手动声明路由,并手动导向控制器的执行方法:
1 | router.get( "/images", [ ImagesController, "index" ] ); |
控制器内必须要有对应方法,否则会出现类型错误。
同样,控制器内的方法也可以自己任意取名,不用强制按照 resource 的命名要求:
1 | // start/routes.ts |
模型(model)
模型是最关键的一个概念,它有很多关键作用,例如可以对数据进行操作(查找、替换、排序等等)、会被工厂函数所绑定等等。
可以通过命令 make:model 来创建一个模型:
1 | node ace make:model <模型名> |
但这里我们更多的是添加参数 -mcf(可以通过 --help 来查看参数详情)来同时创建迁移文件、工厂文件和控制器文件:
1 | node ace make:model <模型名> -mcf |
接口
参数接收
控制器方法接收一个 HttpContext 类型的对象,里面包含一个 request 用来从前端接收数据,详情参考官网Request,这里仅备注一些注意事项。
莫名其妙的 null
request.input 方法拥有第二个参数,可以设置默认值
1 | request.input( "description", "" ); |
但该行为仅在前端没有传入该字段时才会生效,但凡前端传入了这个字段,Adonis 均不会处理默认值。
这就牵扯到了一个 input 逻辑的坑点,我们在前端尝试通过 formdata 传入一个空字符串:
1 | format.append( "description", "" ); |
在接口中尝试打印:
1 | console.log( request.input( "description" ), request.input( "description", "" ) ); |
我们会得到如下结果
1 | null null |
当前端在 multipart/form-data 请求里传入空字符串时,后端框架的实现并不统一。
显然在这里,Adonis 会自动将其视为前端传入了一个 null 过来,因为字段存在,因此也不会使用相关的默认值。