从零创建第一个node后端接口项目
本node项目作者是用于开源项目的纯接口后端,所以没有考虑静态页面相关内容
1、搭建egg脚手架
官方使用npm来搭建脚手架
mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i
我一般比较喜欢用yarn
mkdir egg-example && cd egg-example
yarn create egg --registry https://registry.npm.taobao.org/
// --registry 用于切换淘宝镜像源
yarn
2、安装插件
egg-mysql
数据库使用mysql时需要安装插件egg-mysql来连接数据库
yarn add egg-mysql
egg-validate
对前端请求参数数据进行校验,如果满足则会执行后续代码,反之则报错并给出错误信息
yarn add egg-mysql
3、连接数据库
前提条件是已经安装egg-mysql插件
(1) 添加配置
在conifg目录下新增不同环境下不同配置文件,例如config.local或者config.prod,如果存在则新增(相关问题查看官方网站)
也有人建议写在config.default.js中,具体放置在何处根据个人习惯存放
文件中添加如下内容:
如果项目中存在访问多个不同数据库则使用下面的方法
exports.mysql = {
clients: {
// clientId, 获取client实例,需要通过 app.mysql.get('clientId') 获取
db1: {
// host
host: 'mysql1.com',
// 端口号
port: '3307',
// 用户名
user: 'test_user',
// 密码
password: 'test_password',
// 数据库名
database: 'test',
},
db2: {
// host
host: 'mysql2.com',
// 端口号
port: '3307',
// 用户名
user: 'test_user',
// 密码
password: 'test_password',
// 数据库名
database: 'test',
},
// ...
},
// 所有数据库配置的默认值
default: {
timezone: '+8:00',
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
如果项目中仅使用了一个数据库则可以简单写为
exports.mysql = {
// 单数据库信息配置
client: {
// host
host: 'mysql.com',
// 端口号
port: '3306',
// 用户名
user: 'test_user',
// 密码
password: 'test_password',
// 数据库名
database: 'test',
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
(2) 操作数据库
如果是多数据源的设置,那么需要使用下面的代码进行访问数据库以及操作数据库
// 获取指定数据库
let client1 = this.app.mysql.get('db1');
// 查找数据库
let user = await client1.select('test', {
where: { user_age: age }
});
如果是单数据源则可以使用下面代码访问数据库以及操作数据库 未尝试过但大体应该没有问题
// 获取指定数据库
let client1 = this.app.mysql.select('test', {
where: { user_age: age }
});
4、接口校验
前提条件是已经安装egg-validate插件
在control目录下的相关control文件中,对某个接口入参进行校验
// 表示age参数需要使用number类型
const apiRule = {
age: {
type: 'number',
}
};
可以配置的属性
- required: 是否为必须
- type: 字段属性的类型
- converType: 使参数转换输入参数的具体类型,支持
int、number、string、boolean以及自定义函数 - default: 字段的默认值,如果该属性为非必须的且入参时不存在,则使用默认值
- widelyUndefined: 覆盖options.widelyUndefined,用于将NaN和NULL转换成undefined
type类型包括
- int
- nuber
- date
- dateTime
- id
- boolean
- bool
- string
- password
- url
- enum
- object
- array
- abbr
在接收到接口传入的参数后进行校验,如果校验通过则会执行后续操作,否则将会报错弹出
5、中间件
中间件的文件命名必须与内部函数名称一致,否会无法挂载中间件
中间件文件放在app目录下的middleware文件夹中,如果没有则新增一个
需要在config.default.js的文件中挂载中间件
// 统一错误处理
config.middleware = ['errorHandler'];
(1)统一错误处理中间件
对所有的错误进行统一处理
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (error) {
// 控制台输出
console.error(`发生错误: ${error}`);
// 所有异常在app上触发一个error事件,框架会记录一条错误日志
ctx.app.emit('error', error, ctx);
// status 500 來表示服务器内部错误
let status = error.status || 500;
// 如果是500错误,且是生产环境,则统一显示服务器内部错误
let errorInfo = status === 500 && ctx.app.config.env === 'prod'? '服务器内部错误': error;
// 改变上下文状态
ctx.status = status;
// 从 error 对象独处各个属性,设置到响应中,下面两种中需要根据是否做框架扩展选择其中一个使用
// 未做框架扩展
ctx.body = {
errorInfo,
};
// 做框架扩展后
ctx.response.error(ctx, {
code: status,
msg: errorInfo.message,
data: errorInfo.errors,
})
};
};
}
6、框架扩展
Response
由于需要对返回数据做统一格式所以对框架中Response对象做扩展
在app目录下创建extend文件夹并创建response.js文件来对Response文件做扩展
module.exports = {
/**
* @desc: 接口调用正常情况的返回数据封装
* @param {Object} ctx - context上下文
* @param {string} msg - message 相关信息
* @param {*} data - 返回的数据
* @return {*}
*/
success(ctx, msg, data) {
ctx.body = {
code: 0,
message: msg,
data,
};
ctx.status = 200;
},
/**
* @desc: 处理失败,处理传入的失败原因
* @param {Object} ctx - contentx上下文
* @param {Object} res - 返回的状态数据
* @return {*}
*/
error(ctx, res) {
ctx.body = {
code: res.code,
message: res.msg,
data: res.data
};
ctx.status = 200
}
}
然后修改control文件夹下文件中对返回值的处理
// 原先
ctx.body = userInfo;
ctx.status = 200;
// 扩展后
ctx.response.success(ctx, "ok", userInfo);
7、安全
CSRF
CSRF(Cross-site request forgery跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。 CSRF 攻击会对网站发起恶意伪造的请求,严重影响网站的安全。
鉴于安全性,未关闭框架内置的防范方案,由于安全性需要前后端均设置相关内容
后台
由于防止在query或者body中需要每次调用是都去写语句获取csrfToken比较麻烦且前端使用的是统一封装的请求接口,所以防止在header中更方便
// config.default.js
// 配置接口通过header传递csrfToken
const userConfig = {
security: {
csrf: {
headerName: 'x-csrf-token',
// useSession: true, // 默认为 false,当设置为 true 时,将会把 csrf token 保存到 Session 中
// cookieName: 'csrfToken', // Cookie 中的字段名,默认为 csrfToken
// sessionName: 'csrfToken',
},
},
};
前端
前端使用的是axios的封装接口,且仅需要在post接口需要进行csrf鉴权操作,所以针对性的对post类型的接口做处理
instance.post(realUrl, params, { headers: { 'x-csrf-token': cookie }}).then(response => resolve(response)).catch(error => reject(error));
8、其他
(1) VSCode调试
在vscode调试文件中配置launch.json
{
"name": "Launch Egg",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"windows": { "runtimeExecutable": "npm.cmd" },
"runtimeArgs": [ "run", "debug" ],
"console": "integratedTerminal",
"protocol": "auto",
"restart": true,
"port": 7001,
"autoAttachChildProcesses": true
}
(2)配置独立入口地址与端口
在config.default.js文件中添加
listen: {
// 端口
port: '',
// IP地址
hostname: '',
},
(3) ESlint校验
使用脚手架生成的代码框架中的ESlint使用的是2字符空格缩进,我不是很喜欢所以修改项目中eslint对缩进的校验方式,修改为4字符的tab缩进
9、遇到的问题
(1)前后端分离项目第一个接口为post请求时报message: 'invalid csrf token'
这是因为Egg.js默认开启了CSRF的防护,由于前后端分离导致node项目仅用于接口,会导致第一个接口使用post时无法从请求中获取存放在cookie中的csrf的值。
解决办法
- 关闭csrf的防护
//config.default.js
security: {
csrf: {
enable: false,
}
}
- 在前端加载的首页中加入一个测试服务器通讯的
get接口用于获取csrf的值