前言
通过一些脚手架,搭建一个前端项目慢慢地变得越来越容易。自己如何从前端到后端完整地做一个项目,是大家越来越关心的问题。
今天,我们就一步一步地来实现一个后端项目的搭建。
毕竟是以一个前端的角度来实现的,对后端研究得还不够深入。不到之处,还忘大佬们补充指正,先在此谢过。
技术选型
习惯写 javascript
的我们,一说到写后端的代码。当然首选 nodejs
。这里我也不拐弯抹角。我们使用基于 koa
框架进行二次封装的 eggjs
来进行后端项目开发。eggjs
有以下优点:
-
统一的开发规范。规范大于配置的理念使每个功能都有特定的规范,开发和修改都更加简单。 -
系统稳定性。天然具备进行管理和监控功能。 -
生态好。需要用到的插件及功能都可以搜索到。开箱即用。 -
多环境配置。可以自定义不同的环境所需要用到的不同配置。 -
插件开发方便。根据插件开发规范,可以很容易开发一个新的插件。
功能清单
一个后端项目需要具备哪些功能?我们平时在使用时,似乎只用到了向它发送一个 api
请求,服务器返回给我们对应的数据。
就这???
我们都知道。后端,肯定没这么简单!!!
整理一下我所理解的后端基础功能,有以下几点:
-
操作数据库 -
权限验证 -
缓存 -
请求限流 -
安全防护 -
日志记录 -
……
梳理了需要具备的基本功能,接下来,我们就一步一步地来实现这些功能吧~~~
初始化项目
进入开发目录,创建一个项目文件夹。执行:
mkdir website-manage-api
进入新建的文件夹。执行:
cd website-manage-api
使用命令初始化一个简单的项目,执行:
yarn create egg –type=simple
安装依赖。执行:
yarn
![从0到1搭建完整的后台管理系统(后端篇) 从0到1搭建完整的后台管理系统(后端篇)](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
启动项目,执行:
yarn dev
初始化 git
在远端仓库下新增一个仓库,取名为:website-manage-api
,创建好后,复制仓库地址
![从0到1搭建完整的后台管理系统(后端篇) 从0到1搭建完整的后台管理系统(后端篇)](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
PS: 这里的截图是用的另外一个项目的,方法是一样的,容许在这里偷个懒~~~
进入本地项目,初始化 git
。执行:
git init
git remote add origin https://github.com/****/website-manage-api.git
这里的 https://github.com/****/website-manage-api.git
是自己新建的仓库地址。
git add .
git commit -m ‘feat: init’
git push –set-upstream origin master
再进入远端的仓库中,就有初始化的项目代码了。后续的操作可以随便修改,有修改错的地方,执行 git checkout $filepath/$filename
回退回来就行。
添加 egg-jwt
权限验证采用 jwt
技术。所以这里先安装一下 egg-jwt
。执行:
yarn add egg-jwt
在 config/plugin.js
下面添加
exports.jwt = {
enable: true,
package: 'egg-jwt',
};
在 config/config/default.js
中添加配置
config.jwt = {
secret: 'website-manage-secret', // jwt 需要用到的密钥
};
添加 md5
用于对登录,注册时给密码进行加密处理。执行:
yarn add md5
添加 context 的扩展
常见的方法是从 context
的 request
中获取参数。从 token
中获取用户名和用户id。从 request
中获取参数需要区分请求方法是 GET
还是其它。因为从 GET
方法中请求数据是从 query
中获取。其它方式是从 body
中获取。每处在获取参数时都要写很长的链式调用,所以将它提取为一个 context
的扩展。在用的时候直接使用 ctx.method()
就行。
// extend/context.js
'use strict';
module.exports = {
params(key) {
const method = this.request.method;
if (method === 'GET') {
return key ? this.query[key] : this.query;
}
return key ? this.request.body[key] : this.request;
},
get username() {
const token = this.request.header['auth-token'];
const tokenCache = token ? this.app.jwt.verify(token, this.app.config.jwt.secret) : undefined;
return tokenCache ? tokenCache.username : undefined;
},
get userId() {
const token = this.request.header['auth-token'];
const tokenCache = token ? this.app.jwt.verify(token, this.app.config.jwt.secret) : undefined;
return tokenCache ? tokenCache.userId : undefined;
},
};
如何自己写扩展,可以参考:https://eggjs.org/zh-cn/basics/extend.html
扩展 helper
常见的方法可以扩展在 helper
中,需要的时候直接 ctx.helper.method()
就可以调用。
// extend/helper.js
'use strict';
const dayjs = require('dayjs');
module.exports = {
base64Encode(str = '') {
return new Buffer(str).toString('base64');
},
time() {
return dayjs().format('YYYY-MM-DD HH:mm:ss');
},
timestamp(data) {
return new Date(data).getTime();
},
// 从一个对象中排除一些值
unPick(source, arr) {
if (Array.isArray(arr)) {
const obj = {};
for (const i in source) {
if (!arr.includes(i)) {
obj[i] = source[i];
}
}
return obj;
}
},
};
这里面在处理时间时,有使用到
dayjs
,所以这里需要执行yarn add dayjs
扩展 request
每次请求时,一些请求参数都会带在 request
中。比如需要获取请求头是否有带 auth-token
字段。对 request
进行扩展,获取 token
等信息时,就不用再使用很长的链式调用的方式来获取请求中所携带的内容了。
'use strict';
module.exports = {
get token() {
return this.get('auth-token');
},
};
扩展 response
'use strict';
module.exports = {
set token(token) {
this.set('auth-token', token);
},
};
添加 mysql 插件
数据库我们选择大家熟悉的 mysql
数据库。
执行:
yarn add egg-mysql
在 config/config.default.js
中配置
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
在 config/config.default.js
中配置连接数据库的参数
config.mysql = {
app: true, // 挂载到 app 下面
agent: false,
client: {
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'root123456',
database: 'website-manage',
},
};
注意:这里需要先下载并安装 mysql
数据库。创建一个名为 website-manage
的数据库。并且在这个数据库中,创建一个 user
表,其中包含的字段如下所示:
![从0到1搭建完整的后台管理系统(后端篇) 从0到1搭建完整的后台管理系统(后端篇)](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
![从0到1搭建完整的后台管理系统(后端篇) 从0到1搭建完整的后台管理系统(后端篇)](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
创建用户表命令如下所示:
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`password` varchar(64) DEFAULT NULL COMMENT '密码',
`avatar` longtext COMMENT '头像',
`phone` varchar(20) DEFAULT NULL COMMENT '电话',
`sign` varchar(300) DEFAULT NULL COMMENT '签名',
`createTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
添加 egg-sequelize
数据库的基本操作是 CRUD
,即为增删改查。这些操作在写 sql
语句时,都很简单。但如果想要进行多表查询,比如查询一个用户(user表
)有哪些角色(role表
),每个角色又具备哪些权限(authority表
)时。这种 sql
语句写起来就更复杂了。并且不容易修改。而这些方法的操作,都有人帮我们先写好了库,它就是 sequelize
。
添加 egg-sequelize
,执行:
yarn add egg-sequelize mysql2
在 config/plugin.js
中添加配置项
exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};
在 config/config.default.js
中添加配置
config.sequelize = {
dialect: 'mysql', // 指定方言
host: '127.0.0.1',
port: 3306,
username: 'root',
password: 'root123456',
database: 'website-manage',
define: {
timestamps: false,
freezeTableName: true, // 冻结表名称,使用原始的表名称,不会因为使用了 sequelize 就发生变化
},
};
添加 user 表的 model
对数据库的操作,我们不直接使用 egg-mysql
提供的方法,而是使用 egg-sequelize
提供的方法。egg-sequelize
要求我们写与数据库中的表一致的 model
结构。这里我们的 user
表的 model
如下所示:
// model/user.js
'use strict';
module.exports = app => {
const { STRING, INTEGER, TEXT, DATE } = app.Sequelize;
const User = app.model.define('user', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
username: STRING,
password: STRING,
avatar: TEXT('long'),
phone: STRING,
sign: STRING,
createTime: DATE,
updateTime: DATE,
});
return User;
};
更多关于 sequelize
的方法,比如表与表之间一对多、多对一、多对多关系的建立。可以参考:https://www.sequelize.com.cn/core-concepts/assocs
添加 BaseController 和 BaseService
controller
中对数据进行成功或者失败的数据返回时,都需要统一设置 code
和 message
。在 service
中操作数据库时,也有可能执行失败,失败后需要打印错误信息,并返回 null
。由于每处的操作都一样,所以我们将它们提取为两个基础类。
// controller/base.js
'use strict';
const Controller = require('egg').Controller;
class BaseController extends Controller {
success(data) {
this.ctx.body = {
code: 200,
data,
success: true,
message: '',
};
}
error(message, code = 500) {
this.ctx.body = {
code,
message,
success: false,
};
}
}
module.exports = BaseController;
// service/base.js
'use strict';
const Service = require('egg').Service;
class BaseService extends Service {
async run(callback) {
const { ctx, app } = this;
try {
if (callback) {
return callback(ctx, app);
}
} catch (error) {
console.log('log:error-------------: ', error);
return null;
}
}
}
module.exports = BaseService;
项目中的 controller
和 service
之前是继承的 egg
的 Controller
和 egg
的 Service
。这里就直接继承 BaseController
和 BaseService
,这样我们就能使用 BaseController
和 BaseSerice
中提供的方法了。
添加用户注册
在前面中,我们有创建了一张用户表,但是用户表中什么数据都没有,为方便后续开发登录以及权限验证等功能。我们需要创建一些真实的用户数据,所以,这一步,先开发注册接口。
在 controller
目录下新建 user.js
文件
async register() {
const { ctx, app } = this;
const params = ctx.params();
const user = await ctx.service.user.getUser(params.username);
if (user) {
this.error('用户已经存在');
return;
}
const result = await ctx.service.user.add({
...params,
password: md5(params.password, app.config.salt),
createTime: ctx.helper.time(),
});
if (result) {
const token = await this.jwtSign({
id: result.id,
username: result.username,
});
this.success({ // BaseController 中提供的方法
...this.parseResult(ctx, result),
token,
});
} else {
this.error('用户注册失败!');
}
}
service
目录下新建 user.js
,并添加方法 add
async add(params) {
return this.run(async ctx => await ctx.model.User.create(params));
}
在 router.js
中添加路由配置
module.exports = app => {
const { router, controller } = app;
router.post('/api/user/register', controller.user.register);
};
添加 egg-redis
上一步将注册接口写好了,下一步当然应该是要开发登录接口。但在登录后,我们需要将一些用户的信息进行缓存起来,这里我们先来添加缓存需要用到的数据库 redis
。
yarn add egg-redis
配置 config/plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
在 config/config.default.js
配置 redis
连接
config.redis = {
client: {
port: 6379,
host: '127.0.0.1',
password: 'redis123456',
db: 0, // 选择默认的数据库 0
},
};
在 config/config.default.js
中配置 redis
缓存时间
const userConfig = {
salt: 'website-manage-salt',
redisExpire: 60 * 60 * 24,
};
更多 redis
信息。可以参考:https://www.redis.net.cn/order/
添加登录
-
查询用户是否存在 -
如果存在,将 user.id
和user.username
保存到redis
中 -
将 token
返回给前端
async login() {
const { ctx } = this;
const { username, password } = ctx.params();
const user = await ctx.service.user.getUser(username, password);
if (user) {
const token = await this.jwtSign({
id: user.id,
username: user.username,
});
this.success({
...this.parseResult(ctx, user),
token,
});
} else {
this.error('该用户不存在');
}
}`
router.js
中添加登录路由
module.exports = app => {
const { router, controller } = app;
...
router.post('/api/user/login', controller.user.login);
...
};
具体的代码可以查看文章末尾给出的 github
地址。
添加 egg-auth
插件
用户在请求接口时,需要先判断用户是否登录。如果没有登录,就不再继续执行后面的查询操作,直接返回用户未登录的错误信息给前端。我们将权限验证相关的代码写成一个中间件 egg-auth
。
egg 的中间件建议使用
egg-middlewarnName
的命名方式来开发。
在 lib/plugin
下面新建一个以 egg
开头的 egg-auth/app
文件夹。文件夹下面包含 package.json
和 middleware
文件夹。
package.json
中包含当前插件的命名和对外的命名。
// package.json
{
"name": "egg-auth",
"eggPlugin": {
"name": "auth"
}
}
middleware
下面就是具体的实现代码。
// lib/plugin/egg-auth/middleware/auth.js
'use strict';
module.exports = options => {
return async (ctx, next) => {
const url = ctx.request.url;
const token = ctx.request.token;
const tokenCache = await ctx.app.redis.get(ctx.username);
if (tokenCache !== token && !options.exclude.includes(url.split('?')[0])) {
ctx.body = {
code: 1001,
message: '用户未登录',
};
} else {
await next();
}
};
};
注意这个文件的文件名与
package.json
中的eggPlugin.name
下面的名字保持一致。
在 config/config.default.js
中添加 auth
的排除项。
config.auth = {
exclude: [ '/api/user/login', '/api/user/register' ],
};
这里添加的配置内容,可以通过插件里的
options
获取到。
开发好后,在 config/plugin.js
中添加配置。注意这个是本地的插件,所以使用的是 path
的方式。path
和 package
两者互斥,只能使用其中的一种。path
主要用于自己开发的插件,package
主要用于别人开发的并且可以直接通过 yarn add egg-pluginName
安装的插件。
exports.auth = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-auth'),
};
在 app.js
中添加插件的引入。项目根目录下面如果没有 app.js
,就需要新建一个 app.js
文件。代码如下:
'use strict';
module.exports = app => {
const mids = app.config.coreMiddleware;
app.config.coreMiddleware = [
...mids,
'auth',
];
};
注意
package.json
中,eggPlugin
下面的name
需要与插件中middleware
下面的文件名、config/plugin.js
中的配置、app.js
中间键的名字一致。
想详细了解 app.js
。可以参考:https://eggjs.org/zh-cn/basics/app-start.html
其它插件
介绍了 egg-auth
插件的开发,其它插件的开发也是大同小异,所以不再详细讲解。其它的基础插件还包括:
-
requestLimit
接口限流。防止短时间内,接口被大量暴力请求。 -
allowHosts
允许的主机。防止其它主机直接调用我们的接口。 -
notFound
接口是否存在。判断接口是否存在。 -
interfaceCache
接口缓存。将一些不变的接口进行缓存进来,第二次请求后,就不用再请求数据库,减少查询时间。 -
httpLog
中间件。将每次的请求记录保存起来,方面后面定位问题所在。
这些插件都有实现,在项目的源码中可以查看。
github 地址
最后,送上 github
地址。欢迎大家 fork
or star
。
https://github.com/xiangming25/website-manage-api
总结
项目从最初的搭建到后面基础功能的完善。特别是在文章中间的 md5
、context
、request
、response
进行扩展、jwt
、sequelize
等处。看似毫无关系,甚至还有一头雾水的感觉。但为什么我要写在其它功能的前面?是因为我在一步一步实现功能的同时。也在一步一步地去修改和完善最初做得不好的点。最后总结时,发现那些功能都是需要提前准备好的,所以写在了文章靠前部分。
这一次的总结,只浅显地实现和总结了后端的基础功能。后端所需要掌握的知识和技能绝不是这么简单。这,只是一个开始!!!
文中写得不好的地方,请大家不吝指正!!!
原文始发于微信公众号(前端学习总结):从0到1搭建完整的后台管理系统(后端篇)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/83149.html