koa2初尝试
原文首发于我的博客,欢迎点击查看获得更好的阅读体验~
前言
一直都想接触下服务端的内容,以前看过python基础,但是因为在工作中基本上使用不到,所以基础看了几遍没有实际项目操作就忘记了,忘~记~了~
朝三暮四后最终选择了基于Nodejs的中间件Koa2完成了一个简单的RESTful风格的API项目。
项目中主要完成了以下API接口:
- 登录
- 用户的增删改查
- 图片上传、视频上传以及获取缩略图
- 图片、视频列表查询
- 节目新建(三表关联)
- 节目查询(三表关联)
项目根据molunerfinn
的全栈开发实战:用Vue2+Koa1开发完整的前后端项目(更新Koa2)教程搭建,在此感谢作者分享。
项目结构
. ├── package.json // npm的依赖、项目信息文件 ├── README.md // 说明文件 ├── upload // 上传文件存储位置 ├── index.js // Koa入口文件 ├── server // vue-cli 生成,用于webpack监听、构建 │ ├── config // 配置文件夹 │ ├── controllers // controller-控制器 │ ├── models // model-模型 │ ├── routes // route-路由 │ ├── schema // schema-数据库表结构 └── └── utils // 实用工具
项目依赖包
以下依赖的版本都是本文所写的时候的版本:
@koa/cors
v2.2.3 (跨域)koa
v2.7.0koa-body
v4.1.0 (解析post以及文件上传)koa-json
v2.0.2 (Koa中间件)koa-jwt
v3.6.0 (Koa token的中间件)koa-logger
v3.2.1 (Koa日志中间件)koa-router
v7.4.0 (Koa路由中间件)mysql2
v1.6.5 (nodejs的mysql驱动)sequelize
v5.12.1 (操作数据库的ORM)
为什么要使用mysql2
呢?因为使用mysql
时启动项目sequelize
会报Error: Please install mysql2 package manually
,提示安装mysql2
。
项目搭建
初始化项目
首先我们得新建一个项目文件夹koa-demo
,然后用命令行进入该文件夹,执行npm init
创建项目描述文件package.json
E:\Project\WebStorm\Node\koa-demo> npm init
命令行里会以交互的形式让你填一些项目的介绍信息,依次介绍如下:(不知道怎么填的直接回车、回车...)
- name 项目名称
- version 项目的版本号
- description 项目的描述信息
- entry point 项目的入口文件
- test command 项目启动时脚本命令
- git repository 如果你有 Git 地址,可以将这个项目放到你的 Git 仓库里
- keywords 关键词
- author 作者叫啥
- license 项目要发行的时候需要的证书,平时玩玩忽略它
然后就可以打开项目文件夹,可以看到自动生成的package.json
文件
入口文件
接下来我们先加入入口文件index.js
,写入基本内容:
// index.js import Koa from 'koa' import koaRouter from 'koa-router' import json from 'koa-json' import logger from 'koa-logger' import koaBody from 'koa-body' const app = new Koa() const router = koaRouter() app.use(koaBody()) app.use(json()) app.use(logger()) app.use(async (ctx,next) => { await next() }) app.on('error',(err,ctx) => { console.log('server error', err) }) app.listen(3000,()=>{ console.log('服务启动成功,端口:3000,地址:http://localhost:3000') }) export default app
然后在控制台输入node index.js
,发现报错,因为我们使用了es6的import/export
Babel引入
为了支持import/export
我们需要引入babel
转码器
npm install @babel/core @babel/node @babel/preset-env @babel/register --save-dev
然后在根目录添加.babelrc
文件,写入
{ "presets": [ ["@babel/preset-env", { "targets": { "node": "current" } }] ], "plugins": [] }
然后在package.json
中的scripts
写入
"scripts": { "server": "npx babel-node index.js" //使用 npx 省去了输入 babel-node 完整路径的麻烦。 // or "server": "node_modules/.bin/babel-node index.js" }
然后运行npm run server
就可以了,能看到输出服务启动成功,端口:3000,地址:http://localhost:3000
,则说明我们的koa
已经启动成功了,并在3000端口监听。
热启动
用过vue-cli的都知道前端代码修改过后,会进行热启动,就免去了手动重启项目的麻烦,那么koa2
中如何进行热启动呢?nodemon
可以帮助我们~
npm install nodemon
然后在package.json
中的scripts
加入
"scripts": { "server": "npx babel-node index.js", "start": "nodemon index.js --exec babel-node" }
最后运行npm run start
就可以了
断点调试
在IDE中断点调试需要配置一下
- 首先在打开菜单栏的
Run
->Run Configurations
- 然后点击绿色
+
号,选择Node.js
在右侧的
Configuration
下面填入对应的参数Node interpreter
:选择node的执行程序Node parameters
: 填写参数,参数如下E:\Project\WebStorm\Node\koa-demo\node_modules\nodemon\bin\nodemon --exec E:\Project\WebStorm\Node\koa-demo\node_modules\.bin\babel-node
可以理解为将
package.json
中的scripts
下的start
命令加入了完整路径Working directory
:填写项目目录JavaScript file
:入口文件
环境搭建
项目流程图
为了方便理解项目结构,我做一张图,我们就按照这个顺序进行环境搭建。
创建数据库
去mysql
官网下载一个对应系统的安装程序,安装过程还是比较简单的,这里就不详细描述了
对于初次接触mysql
的我,使用命令操作实在有此难为我了,还好之前装了个可视化的工具Navicat
(破解版),当然也有一些免费版的,例如:Windows上HediSQL,macOS上Sequel Pro。
好了,现在我们先创建一个连接
koa
- 连接名:
koa
- 主机名或IP地址:
localhost
- 端口:
3306
- 用户名:
root
- 密码:
123456
- 连接名:
然后新建一个数据库
demo
- 数据库名:
demo
- 字符集:
`utf8 -- UTF-8 Unicode
- 排序规则:
utf8_general_ci
- 数据库名:
- 最后我们创建两个表
user
与resource
user表:
字段 | 类型 | 长度 | 主键 | 说明 |
---|---|---|---|---|
id | int(自增) | 255 | 1 | 用户ID |
nickname | varchar | 50 | 昵称 | |
username | varchar | 50 | 用户名 | |
password | varchar | 128 | 密码 | |
creationTime | datetime | 0 | 创建时间 | |
updateTime | datetime | 0 | 更新时间 |
resource表:
字段 | 类型 | 长度 | 主键 | 说明 |
---|---|---|---|---|
id | int(自增) | 125 | 1 | 资源ID |
name | varchar | 255 | 资源名称 | |
size | double | 0 | 资源大小 | |
measure | varchar | 255 | 分辨率 | |
thumbnail | varchar | 255 | 资源地址 | |
operator | varchar | 255 | 操作者 | |
time | datetime | 0 | 创建时间 |
生成数据结构
我们需要把数据库的表结构用sequelize-auto
导出来。
更多关于sequelize-auto
的使用可以参考官方介绍或者这篇文章
由此,我们首先全局安装sequelize-auto
npm install -g sequelize-auto
进入server
的目录,执行如下语句
sequelize-auto -o "./schema" -d demo -h localhost -u root -p 3306 -x 123456 -e mysql
-o
参数后面的是输出的文件夹目录-d
参数后面的是数据库名-h
参数后面是数据库地址-u
参数后面是数据库用户名-p
参数后面是端口号-x
参数后面是数据库密码,这个要根据自己的数据库密码来!-e
参数后面指定数据库为mysql
然后就会在schema
文件夹下自动生成两个文件:
// user.js /* jshint indent: 2 */ module.exports = function(sequelize, DataTypes) { return sequelize.define('user', { id: { type: DataTypes.INTEGER(255), allowNull: false, primaryKey: true, autoIncrement: true }, nickname: { type: DataTypes.STRING(50), allowNull: true }, username: { type: DataTypes.STRING(50), allowNull: true }, password: { type: DataTypes.STRING(128), allowNull: true }, creationTime: { type: DataTypes.DATE, allowNull: true }, updateTime: { type: DataTypes.DATE, allowNull: true } }, { tableName: 'user' }); };
// resource.js /* jshint indent: 2 */ module.exports = function(sequelize, DataTypes) { return sequelize.define('resource', { id: { type: DataTypes.INTEGER(125), allowNull: false, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING(255), allowNull: true }, size: { type: "DOUBLE", allowNull: true }, measure: { type: DataTypes.STRING(255), allowNull: true }, thumbnailProxy: { type: DataTypes.STRING(255), allowNull: true }, operator: { type: DataTypes.STRING(255), allowNull: true }, time: { type: DataTypes.DATE, allowNull: true } }, { tableName: 'resource' }); };
连接数据库
跟数据库打交道的时候我们都需要一个好的操作数据库的工具,能够让我们用比较简单的方法来对数据库进行增删改查。我选用的是
sequelize
,它支持多种关系型数据库(Sqlite
、mysql
、Postgres
等),它的操作基本都能返回一个Promise
对象,这样在Koa里面我们能够很方便地进行”同步”操作。更多关于
sequelize
的用法,可以参考官方文档,以及这几篇文章——Sequelize中文API文档、Sequelize和MySQL对照、Sequelize快速入门
安装sequelize
依赖
npm install --save sequelize
然后在server
目录下的config
目录下我们新建一个db.js
,用于初始化sequelize
和数据库的连接。
// config/db.js import Sequelize from 'sequelize' // 引入sequelize // 使用url连接的形式进行连接,注意将root: 后面的XXXX改成自己数据库的密码 const demo = new Sequelize('mysql://root:[email protected]/demo',{ define:{ // 取消Sequelzie自动给数据表加入时间戳(createdAt以及updatedAt) timestamps: false }, timezone: '+08:00' // 时差区,国内需要加入不然存储的时间会有时差 }) export default demo // 将demo暴露出接口方便Model调用
数据库操作
接着我们去models
文件夹里将数据库和表结构文件连接起来。在这个文件夹下新建一个user.js
的文件。
所谓增、删、改、查,那么我们就先来写一个新增用户的操作。
// models/user.js import demoDB from '../config/db' // 引入user的表结构 const userModel = '../schema/user.js' // 用sequelize的import方法引入表结构,实例化了User。 const User = demoDB.import(userModel) // async 异步操作 const addUser = async (userInfo) => { await User.create(userInfo) } module.exports = { addUser }
业务逻辑操作
现在我们就需要写一写接收参数后的一些操作以及返回信息。
// controllers/user.js import user from '../models/login' const postUserInfo = async ctx => { const data = ctx.request.body const userAuth = await login.getUserByName(data.username) if(userAuth === null){ let userInfo = { username: data.username, password: data.password, nickname: data.nickname, creationTime: dataTime(), updateTime: dataTime() } user.addUser(userInfo) ctx.body = { code: '0000', info: '新建成功!' } }else { ctx.body = { code: '9999', info: '用户已存在!' } } } export default { postUserInfo }
写完这个还不能直接请求,因为我们还没有定义路由,请求经过koa
找不到这个路径是没有反应的。
创建路由
在routes
文件夹下写一个api.js
的文件。
// routes/api.js import user from '../controllers/user' import koaRouter from 'koa-router' const router = koaRouter() router.post('/user',user.postUserInfo) export default router
至此我们已经接近完成我们的第一个API了,还缺最后一步,将这个路由规则“挂载”到Koa上去。
挂载路由
为了节约篇幅,下面省略了一些代码,只写了上下文作为位置标记
// index.js // ... import logger from 'koa-logger' import koaRouter from 'koa-router' const router = koaRouter() import api from './server/routes/api.js' // ... app.on('error',(err,ctx) => { console.log('server error', err) }) // 挂载到koa-router上,同时会让所有的auth的请求路径前面加上'/auth'的请求路径 router.use('/api', api.routes()) // 将路由规则挂载到Koa上。 app.use(router.routes()) app.listen(3000,()=>{ console.log('服务启动成功,端口:3000,地址:http://localhost:3000') }) // ...
打开你的控制台,输入node app.js
,一切运行正常没有报错的话,大功告成,我们的第一个API已经构建完成!
API 测试
接口在跟前端对接之前,我们应该先进行一遍测试,防止出现问题。
在测试接口的工具上我想postman
的大名应该众所周知了,官网下载安装好后便可使用。
其它接口的完善
刚才实现的不过是一个简单的用户新增接口,但是我们要实现一个完整的系统demo,还需要做一些工作。
剩下的API添加,基本上只需要在model
和controllers
写好方法,定好接口即可~
下面主要列举一下上传接口
以及分页查询接口
的一些知识点。
图片上传
在项目根目录下我们创建的有一个upload
文件夹,上传成功后的图片就存储到这里,那么这里的图片怎么通过链接访问呢?这就需要我们搭建一个简易的静态资源服务器了。
静态资源
这里提供两种方法:
koa-static中间件
npm install koa-static
// index.js //... import statics from 'koa-static' import path from 'path' app.use(logger()) // 静态服务 app.use(statics(path.join(__dirname, './upload/')))
我们在upload目录下新建一个
image
文件夹,用来放图片文件,然后挂载好后启动服务就可以使用http://localhost:3000/image/test.jpg
查看图片了。注意:将
upload
目录配置为静态资源,那么访问的时候不需要输入upload
,而是直接访问下级目录Nginx搭建静态资源
这里使用
Nginx
进行搭建,在官网下载稳定版本,解压后打开conf
文件夹下的nginx.conf
进行修改配置server { listen 3001; server_name localhost; location /upload/ { root E:/Project/WebStorm/Node/koa-demo/; autoindex on; } }
然后运行
nginx.exe
即可,然后我们在upload
中添加一张图片,打开浏览器输入http://localhost:3001/upload/test.jpg
便可看见图片
接口编写
在models
下创建一个resource.js
// models/resource.js import demoDB from '../config/db' const resModel = '../schema/resource' const Res = demoDB.import(resModel) const postResImage = async data => { await Res.create(data) } export default { postResImage }
主要是用来存储图片的一些数据到数据库
下面我们在controllers
下创建一个resource.js
多文件需要遍历ctx.request.files
,与文件一起传过来的参数在ctx.request.body
中获取
// controllers/resource.js import fs from 'fs' import path from 'path' import res from '../models/resource' import formatTime from '../utils/formatTime' import _res from '../utils/response' import probe from 'probe-image-size' const imageUrl = 'http://localhost:3000/image/' const imagePath = path.join(__dirname,'../../upload/image') const videoUrl = 'http://localhost:3000/video/' const videoPath = path.join(__dirname,'../../upload/video') const uploadImage = async ctx => { const file = ctx.request.files.file; const reader = fs.createReadStream(file.path); // 创建可读流 // 获取图片流的尺寸,注意,这里不能直接使用reader,不然会导致图片损坏。 let measure = await probe(fs.createReadStream(file.path)) const upStream = fs.createWriteStream(`${imagePath}\\${file.name}`); // 创建可写流 const data = { name : file.name, size : (file.size / 1024 / 1024).toFixed(2), measure : `${measure.width}*${measure.height}`, thumbnailProxy : `${imageUrl}${file.name}`, operator : 'admin', time : formatTime() } await res.postRes(data) if(!fs.existsSync(imagePath)){ fs.mkdir(imagePath,err => { if(err) throw err reader.pipe(upStream) // 可读流通过管道写入可写流 return ctx.body = _res.success('上传成功') }) }else { reader.pipe(upStream) // 可读流通过管道写入可写流 return ctx.body = _res.success('上传成功') } } export default { uploadImage }
接收到图片后,再想获取图片的尺寸需要npm install probe-image-size
,刚开始的时候一直在image-size
上折腾,真是搞了好久,一直以为是自己哪里用法不对。后来才发现是对流形式的不支持,就换到了probe-image-size
.
最后在routes
添加接口
// router/api.js import koaRouter from 'koa-router' const router = koaRouter() import resource from '../controllers/resource' router.post('/upload/image',resource.uploadImage) export default router
获取视频缩略图
上传视频后在我们一般都需要获取视频的缩略图,用来在前端列表中展示。
我们使用FFmpeg
,一个领先的多媒体框架。
首先在官网下载对应平台的包。我这里使用的是windows,下载完成后将FFmpeg
解压到D:\ffmpeg
下。
并配置好系统的环境变量,添加D:\ffmpeg\bin
到系统变量。详细点击查看
如果设置环境变量无效的话,还可以手动设置ffpemg的位置。
FFMpeg.setFfmpegPath('D:/ffmpeg/bin/ffmpeg.exe')
然后在项目目录安装node的中间件fluent-ffmpeg
npm install fluent-ffmpeg
然后就可以使用了。
import FFMpeg from 'fluent-ffmpeg' FFMpeg.setFfmpegPath('D:/ffmpeg/bin/ffmpeg.exe') const screenshots = function(fileName){ FFMpeg('upload/video/'+ fileName) .screenshots({ timemarks: ['0.5'], filename: 'thumbnail-%b.png', count: 1, folder: 'upload/video' }) } export default { screenshots }
其它操作请参考官方文档
分页查询
这里的查询主要是查询上传的图片的信息,返回给前端进行列表展示。
所以接口与上传接口同在resource.js
文件中。
// models/resource.js // ...省略 const getResImage = async (data) => { const { pageNo, pageSize } = data return await Res.findAndCountAll({ limit: parseInt(pageSize), offset: (parseInt(pageNo)-1) * parseInt(pageSize), }).then(result=>{ return result }) } export default { getResImage, postResImage }
// controllers/resource.js // ...省略 const getResImageList = async ctx => { const data = ctx.query const list = await res.getResImage(data) const _resData = { pages:{ total: list.count }, sources: list.rows } ctx.body = _res.success('成功',_resData) } export default { uploadFile, getResImageList }
// router/api.js router.get('/resource/image',resource.getResImageList)
至此,KOA2中实现RESTFul 风格的API就算完成了。
一对多的多表分页查询时会在子查询里中分页,可使用subQuery:true
。详见项目中节目查询的代码。
总结
最后将本文的项目代码放至了Github,如果这个项目对你有帮助,希望大家可以fork,给我提建议,如果再有时间,可以点个Star那就更好啦~
参考文献
- 全栈开发实战:用Vue2+Koa1开发完整的前后端项目(更新Koa2)
- koa2 使用 koa-body 代替 koa-bodyparser 和 koa-multer
- Koa2 之文件上传下载
- koa2基于stream(流)进行文件上传和下载
- Babel v7 指南
- [[Node.js] 使用 Babel 做 ES6 開發](https://medium.com/10coding/node-js-%E4%BD%BF%E7%94%A8-babel-%E5%81%9A-es6-%E9%96%8B%E7%99%BC-44b5b9e5f508)
- nodemon webstorm 配置应用
- sequelize联表查询
- Sequelize 多对多的创建、查询、更新