Serverless + Egg.js 后台管理系统实战(上) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
scf10cent
V2EX    Serverless

Serverless + Egg.js 后台管理系统实战(上)

  •  
  •   scf10cent 2020-02-27 11:48:05 +08:00 6504 次点击
    这是一个创建于 2120 天前的主题,其中的信息可能已经有所发展或是发改变。

    本文将介绍如何基于 Egg.js 和 Serverless 实现一个后台管理系统

    作为一名前端开发者,在选择 Nodejs 后端服务框架时,第一时间会想到 Egg.js,不得不说 Egg.js 是一个非常优秀的企业级框架,它的高扩展性和丰富的插件,极大的提高了开发效率。开发者只需要关注业务就好,比如要使用 redis,引入 egg-redis 插件,然后简单配置就可以了。正因为如此,第一次接触它,我便喜欢上了它,之后也用它开发过不少应用。

    有了如此优秀的框架,那么如何将一个 Egg.js 的服务迁移到 Serverless 架构上呢?

    背景

    我在文章 基于 Serverless Component 的全栈解决方案 中讲述了,如何将一个基于 Vue.js 的前端应用和基于 Express 的后端服务,快速部署到腾讯云上。虽然受到不少开发者的喜爱,但是很多开发者私信问我,这还是一个 Demo 性质的项目而已,有没有更加实用性的解决方案。而且他们实际开发中,很多使用的正是 Egg.js 框架,能不能提供一个 Egg.js 的解决方案?

    本文将手把手教你结合 Egg.jsServerless 实现一个后台管理系统。

    读完此文你将学到:

    1. Egg.js 基本使用
    2. 如何使用 Sequelize ORM 模块进行 Mysql 操作
    3. 如何使用 Redis
    4. 如何使用 JWT 进行用户登录验证
    5. Serverless Framework 的基本使用
    6. 如何将本地开发好的 Egg.js 应用部署到腾讯云云函数上
    7. 如何基于云端对象存储快速部署静态网站

    Egg.js 入门

    初始化 Egg.js 项目:

    $ mkdir egg-example && cd egg-example $ npm init egg --type=simple $ npm i 

    启动项目:

    $ npm run dev 

    然后浏览器访问 http://localhost:7001,就可以看到亲切的 hi, egg 了。

    关于 Egg.js 的框架更多知识,建议阅读 官方文档

    准备

    对 Egg.js 有了简单了解,接下来我们来初始化我们的后台管理系统,新建一个项目目录 admin-system:

    $ mkdir admin-system 

    将上面创建的 Egg.js 项目复制到 admin-system 目录下,重命名为 backend。然后将前端模板项目复制到 frontend 文件夹中:

    $ git clone https://github.com/PanJiaChen/vue-admin-template.git frontend 

    说明: vue-admin-template 是基于 Vue2.0 的管理系统模板,是一个非常优秀的项目,建议对 Vue.js 感兴趣的开发者可以去学习下,当然如果你对 Vue.js 还不是太了解,这里有个基础入门学习教程 Vuejs 从入门到精通系列文章

    之后你的项目目录结构如下:

    . ├── README.md ├── backend // 创建的 Egg.js 项目 └── frontend // 克隆的 Vue.js 前端项目模板 

    启动前端项目熟悉下界面:

    $ cd frontend $ npm install $ npm run dev 

    然后访问 http://localhost:9528 就可以看到登录界面了。

    开发后端服务

    对于一个后台管理系统服务,我们这里只实现登录鉴权和文章管理功能,剩下的其他功能大同小异,读者可以之后自由补充扩展。

    1. 添加 Sequelize 插件

    在正式开发之前,我们需要引入数据库插件,这里本人偏向于使用 Sequelize ORM 工具进行数据库操作,正好 Egg.js 提供了 egg-sequelize 插件,于是直接拿来用,需要先安装:

    $ cd frontend # 因为需要通过 sequelize 链接 mysql 所以这也同时安装 mysql2 模块 $ npm install egg-sequelize mysql2 --save 

    然后在 backend/config/plugin.js 中引入该插件:

    module.exports = { // .... sequelize: { enable: true, package: "egg-sequelize" } // .... }; 

    backend/config/config.default.js 中配置数据库连接参数:

    // ... const userCOnfig= { // ... sequelize: { dialect: "mysql", // 这里也可以通过 .env 文件注入环境变量,然后通过 process.env 获取 host: "xxx", port: "xxx", database: "xxx", username: "xxx", password: "xxx" } // ... }; // ... 

    2. 添加 JWT 插件

    系统将使用 JWT token 方式进行登录鉴权,安装配置参考官方文档,egg-jwt

    3. 添加 Redis 插件

    系统将使用 redis 来存储和管理用户 token,安装配置参考官方文档,egg-redis

    4. 角色 API

    定义用户模型,创建 backend/app/model/role.js 文件如下:

    module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; const Role = app.model.define("role", { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, name: STRING(30), created_at: DATE, updated_at: DATE }); // 这里定义与 users 表的关系,一个角色可以含有多个用户,外键相关 Role.associate = () => { app.model.Role.hasMany(app.model.User, { as: "users" }); }; return Role; }; 

    实现 Role 相关服务,创建 backend/app/service/role.js 文件如下:

    const { Service } = require("egg"); class RoleService extends Service { // 获取角色列表 async list(options) { const { ctx: { model } } = this; return model.Role.findAndCountAll({ ...options, order: [ ["created_at", "desc"], ["id", "desc"] ] }); } // 通过 id 获取角色 async find(id) { const { ctx: { model } } = this; const role = await model.Role.findByPk(id); if (!role) { this.ctx.throw(404, "role not found"); } return role; } // 创建角色 async create(role) { const { ctx: { model } } = this; return model.Role.create(role); } // 更新角色 async update({ id, updates }) { const role = await this.ctx.model.Role.findByPk(id); if (!role) { this.ctx.throw(404, "role not found"); } return role.update(updates); } // 删除角色 async destroy(id) { const role = await this.ctx.model.Role.findByPk(id); if (!role) { this.ctx.throw(404, "role not found"); } return role.destroy(); } } module.exports = RoleService; 

    一个完整的 RESTful API 就该包括以上五个方法,然后实现 RoleController, 创建 backend/app/controller/role.js:

    const { Controller } = require("egg"); class RoleController extends Controller { async index() { const { ctx } = this; const { query, service, helper } = ctx; const optiOns= { limit: helper.parseInt(query.limit), offset: helper.parseInt(query.offset) }; const data = await service.role.list(options); ctx.body = { code: 0, data: { count: data.count, items: data.rows } }; } async show() { const { ctx } = this; const { params, service, helper } = ctx; const id = helper.parseInt(params.id); ctx.body = await service.role.find(id); } async create() { const { ctx } = this; const { service } = ctx; const body = ctx.request.body; const role = await service.role.create(body); ctx.status = 201; ctx.body = role; } async update() { const { ctx } = this; const { params, service, helper } = ctx; const body = ctx.request.body; const id = helper.parseInt(params.id); ctx.body = await service.role.update({ id, updates: body }); } async destroy() { const { ctx } = this; const { params, service, helper } = ctx; const id = helper.parseInt(params.id); await service.role.destroy(id); ctx.status = 200; } } module.exports = RoleController; 

    之后在 backend/app/route.js 路由配置文件中定义 role 的 RESTful API:

    router.resources("roles", "/roles", controller.role); 

    通过 router.resources 方法,我们将 roles 这个资源的增删改查接口映射到了 app/controller/roles.js 文件。详细说明参考 官方文档

    5. 用户 API

    同 Role 一样定义我们的用户 API,这里就不复制粘贴了,可以参考项目实例源码 admin-system

    6. 同步数据库表格

    上面只是定义好了 RoleUser 两个 Schema,那么如何同步到数据库呢?这里先借助 Egg.js 动的 hooks 来实现,Egg.js 框架提供了统一的入口文件( app.js )进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。

    我们在 backend 目录中创建 app.js 文件,如下:

    "use strict"; class AppBootHook { constructor(app) { this.app = app; } async willReady() { // 这里只能在开发模式下同步数据库表格 const isDev = process.env.NODE_ENV === "development"; if (isDev) { try { console.log("Start syncing database models..."); await this.app.model.sync({ logging: console.log, force: isDev }); console.log("Start init database data..."); await this.app.model.query( "INSERT INTO roles (id, name, created_at, updated_at) VALUES (1, 'admin', '2020-02-04 09:54:25', '2020-02-04 09:54:25'),(2, 'editor', '2020-02-04 09:54:30', '2020-02-04 09:54:30');" ); await this.app.model.query( "INSERT INTO users (id, name, password, age, avatar, introduction, created_at, updated_at, role_id) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 20, 'https://yugasun.com/static/avatar.jpg', 'Fullstack Engineer', '2020-02-04 09:55:23', '2020-02-04 09:55:23', 1);" ); await this.app.model.query( "INSERT INTO posts (id, title, content, created_at, updated_at, user_id) VALUES (2, 'Awesome Egg.js', 'Egg.js is a awesome framework', '2020-02-04 09:57:24', '2020-02-04 09:57:24', 1),(3, 'Awesome Serverless', 'Build web, mobile and IoT applications using Tencent Cloud and API Gateway, Tencent Cloud Functions, and more.', '2020-02-04 10:00:23', '2020-02-04 10:00:23', 1);" ); console.log("Successfully init database data."); console.log("Successfully sync database models."); } catch (e) { console.log(e); throw new Error("Database migration failed."); } } } } module.exports = AppBootHook; 

    通过 willReady 生命周期函数,我们可以执行 this.app.model.sync() 函数来同步数据表,当然这里同时初始化了角色和用户数据记录,用来做为演示用。

    注意:这的数据库同步只是本地调试用,如果想要腾讯云的 Mysql 数据库,建议开启远程连接,通过 sequelize db:migrate 实现,而不是每次启动 Egg 应用时同步,示例代码已经完成此功能,参考 Egg Sequelize 文档。 这里本人为了省事,直接开启腾讯云 Mysql 公网连接,然后修改 config.default.js 中的 sequelize 配置,运行 npm run dev 进行开发模式同步。

    到这里,我们的用户和角色的 API 都已经定义好了,启动服务 npm run dev,访问 https://127.0.0.1:7001/users 可以获取所有用户列表了。

    7. 用户登录 /注销 API

    这里登录逻辑比较简单,客户端发送 用户名密码/login 路由,后端通过 login 函数接受,然后从数据库中查询该用户名,同时比对密码是否正确。如果正确则调用 app.jwt.sign() 函数生成 token,并将 token 存入到 redis 中,同时返回该 token,之后客户端需要鉴权的请求都会携带 token,进行鉴权验证。思路很简单,我们就开始实现了。

    流程图如下:

    <center> Login Process </center>

    首先,在 backend/app/controller/home.js 中新增登录处理 login 方法:

    class HomeController extends Controller { // ... async login() { const { ctx, app, config } = this; const { service, helper } = ctx; const { username, password } = ctx.request.body; const user = await service.user.findByName(username); if (!user) { ctx.status = 403; ctx.body = { code: 403, message: "Username or password wrong" }; } else { if (user.password === helper.encryptPwd(password)) { ctx.status = 200; const token = app.jwt.sign( { id: user.id, name: user.name, role: user.role.name, avatar: user.avatar }, config.jwt.secret, { expiresIn: "1h" } ); try { await app.redis.set(`token_${user.id}`, token); ctx.body = { code: 0, message: "Get token success", token }; } catch (e) { console.error(e); ctx.body = { code: 500, message: "Server busy, please try again" }; } } else { ctx.status = 403; ctx.body = { code: 403, message: "Username or password wrong" }; } } } } 

    注释:这里有个密码存储逻辑,用户在注册时,密码都是通过 helper 函数 encryptPwd() 进行加密的(这里用到最简单的 md5 加密方式,实际开发中建议使用更加高级加密方式),所以在校验密码正确性时,也需要先加密一次。至于如何在 Egg.js 框架中新增 helper 函数,只需要在 backend/app/extend 文件夹中新增 helper.js 文件,然后 modole.exports 一个包含该函数的对象就行,参考 Egg 框架扩展文档

    然后,在 backend/app/controller/home.js 中新增 userInfo 方法,获取用户信息:

    async userInfo() { const { ctx } = this; const { user } = ctx.state; ctx.status = 200; ctx.body = { code: 0, data: user, }; } 

    egg-jwt 插件,在鉴权通过的路由对应 controller 函数中,会将 app.jwt.sign(user, secrete) 加密的用户信息,添加到 ctx.state.user 中,所以 userInfo 函数只需要将它返回就行。

    之后,在 backend/app/controller/home.js 中新增 logout 方法:

    async logout() { const { ctx } = this; ctx.status = 200; ctx.body = { code: 0, message: 'Logout success', }; } 

    userInfologout 函数非常简单,重点是路由中间件如何处理。

    接下来,我们来定义登录相关路由,修改 backend/app/router.js 文件,新增 /login, /user-info, /logout 三个路由:

    const koajwt = require("koa-jwt2"); module.exports = app => { const { router, controller, jwt } = app; router.get("/", controller.home.index); router.post("/login", controller.home.login); router.get("/user-info", jwt, controller.home.userInfo); const isRevokedAsync = function(req, payload) { return new Promise(resolve => { try { const userId = payload.id; const tokenKey = `token_${userId}`; const token = app.redis.get(tokenKey); if (token) { app.redis.del(tokenKey); } resolve(false); } catch (e) { resolve(true); } }); }; router.post( "/logout", koajwt({ secret: app.config.jwt.secret, credentialsRequired: false, isRevoked: isRevokedAsync }), controller.home.logout ); router.resources("roles", "/roles", controller.role); router.resources("users", "/users", controller.user); router.resources("posts", "/posts", controller.post); }; 

    Egg.js 框架定义路由时,router.post() 函数可以接受中间件函数,用来处理一些路由相关的特殊逻辑。

    比如 /user-info,路由添加了 app.jwt 作为 JWT 鉴权中间件函数,至于为什么这么用,egg-jwt 插件有明确说明。

    这里稍微复杂的是 /logout 路由,因为我们在注销登录时,需要将用户的 tokenredis 中移除,所以这里借助了 koa-jwt2isRevokded 参数,来进行 token 删除。


    限于篇幅,后端服务部署和前端开发将在下集中介绍,感兴趣的也可以自行了解 Serverless 相关内容:

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1110 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 23:25 PVG 07:25 LAX 15:25 JFK 18:25
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86