做为一名前端开发者,在选择 Nodejs 后端服务框架时,第一时间会想到 Egg.js,不得不说 Egg.js
是一个很是优秀的企业级框架,它的高扩展性和丰富的插件,极大的提升了开发效率。开发者只须要关注业务就好,好比要使用 redis
,引入 egg-redis 插件,而后简单配置就能够了。正由于如此,第一次接触它,我便喜欢上了它,以后也用它开发过很多应用。css
有了如此优秀的框架,那么如何将一个 Egg.js
的服务迁移到 Serverless
架构上呢?html
我在文章 基于 Serverless Component 的全栈解决方案 中讲述了,如何将一个基于 Vue.js
的前端应用和基于 Express
的后端服务,快速部署到腾讯云上。虽然受到很多开发者的喜好,可是不少开发者私信问我,这仍是一个 Demo
性质的项目而已,有没有更加实用性的解决方案。并且他们实际开发中,不少使用的正是 Egg.js
框架,能不能提供一个 Egg.js
的解决方案?前端
本文将手把手教你结合 Egg.js
和 Serverless
实现一个后台管理系统。vue
读完此文你将学到:mysql
初始化 Egg.js 项目:ios
$ mkdir egg-example && cd egg-example $ npm init egg --type=simple $ npm i
启动项目:git
$ npm run dev
而后浏览器访问 http://localhost:7001
,就能够看到亲切的 hi, egg
了。github
关于 Egg.js 的框架更多知识,建议阅读 官方文档web
对 Egg.js 有了简单了解,接下来咱们来初始化咱们的后台管理系统,新建一个项目目录 admin-system
:redis
$ 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
就能够看到登陆界面了。
对于一个后台管理系统服务,咱们这里只实现登陆鉴权和文章管理功能,剩下的其余功能大同小异,读者能够以后自由补充扩展。
在正式开发以前,咱们须要引入数据库插件,这里本人偏向于使用 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" } // ... }; // ...
系统将使用 JWT token 方式进行登陆鉴权,安装配置参考官方文档,egg-jwt
系统将使用 redis 来存储和管理用户 token,安装配置参考官方文档,egg-redis
定义用户模型,建立 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
文件。详细说明参考 官方文档
同 Role 同样定义咱们的用户 API,这里就不复制粘贴了,能够参考项目实例源码 admin-system。
上面只是定义好了 Role
和 User
两个 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
能够获取全部用户列表了。
这里登陆逻辑比较简单,客户端发送 用户名
和 密码
到 /login
路由,后端经过 login
函数接受,而后从数据库中查询该用户名,同时比对密码是否正确。若是正确则调用 app.jwt.sign()
函数生成 token
,并将 token
存入到 redis
中,同时返回该 token
,以后客户端须要鉴权的请求都会携带 token
,进行鉴权验证。思路很简单,咱们就开始实现了。
流程图以下:
<center>
<img src="https://static.yugasun.com/serverless/login-process.jpg" width="300" alt="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', }; }
userInfo
和 logout
函数很是简单,重点是路由中间件如何处理。
接下来,咱们来定义登陆相关路由,修改 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
路由,由于咱们在注销登陆时,须要将用户的 token
从 redis
中移除,因此这里借助了 koa-jwt2 的 isRevokded
参数,来进行 token
删除。
到这里,后端服务的登陆和注销逻辑基本完成了。那么如何部署到云函数呢?能够直接使用 tencent-egg 组件,它是专门为 Egg.js 框架打造的 Serverless Component,使用它能够快速将咱们的 Egg.js 项目部署到腾讯云云函数上。
咱们先建立一个 backend/sls.js
入口文件:
const { Application } = require("egg"); const app = new Application(); module.exports = app;
而后修改 backend/config/config.default.js
文件:
const config = (exports = { env: "prod", // 推荐云函数的 egg 运行环境变量修改成 prod rundir: "/tmp", logger: { dir: "/tmp" } });
注释:这里之全部须要修改运行和日志目录,是由于云函数运行时,只有
/tmp
才有写权限。
全局安装 serverless
命令:
$ npm install serverless -g
在项目根目录下建立 serverless.yml
文件,同时新增 backend
配置:
backend: component: "@serverless/tencent-egg" inputs: code: ./backend functionName: admin-system # 这里必须指定一个具备操做 mysql 和 redis 的角色,具体角色建立,可访问 https://console.cloud.tencent.com/cam/role role: QCS_SCFFull functionConf: timeout: 120 # 这里的私有网络必须和 mysql、redis 实例一致 vpcConfig: vpcId: vpc-xxx subnetId: subnet-xxx apigatewayConf: protocols: - https
此时你的项目目录结构以下:
. ├── README.md // 项目说明文件 ├── serverless.yml // serverless yml 配合文件 ├── backend // 建立的 Egg.js 项目 └── frontend // 克隆的 Vue.js 前端项目模板
执行部署命令:
$ serverless --debug
以后控制台须要进行扫码登陆验证腾讯云帐号,扫码登陆就好。等部署成功会发挥以下信息:
backend: region: ap-guangzhou functionName: admin-system apiGatewayServiceId: service-f1bhmhk4 url: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/
这里输出的 url 就是部署成功的 API 网关接口,能够直接访问测试。
注释:云函数部署时,会自动在腾讯云的 API 网关建立一个服务,同时建立一个 API,经过该 API 就能够触发云函数执行了。
当前默认支持 Serverless cli 扫描二维码登陆,若是但愿配置持久的环境变量/秘钥信息,也能够在项目根目录建立 .env
文件
在 .env
文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存,密钥能够在 API 密钥管理 中获取或者建立.
# .env TENCENT_SECRET_ID=123 TENCENT_SECRET_KEY=123
跟用户 API 相似,只须要复制粘贴上面用户相关模块,修更名称为 posts
, 并修改数据模型就行,这里就不粘贴代码了。
本实例直接使用的 vue-admin-template 的前端模板。
咱们须要作以下几部分修改:
frontend/src/api/user.js
和文章相关接口 frontend/src/api/post.js
。frontend/src/utils/request.js
文件,包括 axios
请求的 baseURL
和请求的 header。首先删除 frontend/mock
文件夹。而后修改前端入口文件 frontend/src/main.js
:
// 1. 引入接口变量文件,这个会依赖 @serverless/tencent-website 组件自动生成 import "./env.js"; import Vue from "vue"; import "normalize.css/normalize.css"; import ElementUI from "element-ui"; import "element-ui/lib/theme-chalk/index.css"; import locale from "element-ui/lib/locale/lang/en"; import "@/styles/index.scss"; import App from "./App"; import store from "./store"; import router from "./router"; import "@/icons"; import "@/permission"; // 2. 下面这段就是 mock server 引入,删除就好 // if (process.env.NODE_ENV === 'production') { // const { mockXHR } = require('../mock') // mockXHR() // } Vue.use(ElementUI, { locale }); Vue.config.productionTip = false; new Vue({ el: "#app", router, store, render: h => h(App) });
修改 frontend/src/api/user.js
文件,包括登陆、注销、获取用户信息和获取用户列表函数以下:
import request from "@/utils/request"; // 登陆 export function login(data) { return request({ url: "/login", method: "post", data }); } // 获取用户信息 export function getInfo(token) { return request({ url: "/user-info", method: "get" }); } // 注销登陆 export function logout() { return request({ url: "/logout", method: "post" }); } // 获取用户列表 export function getList() { return request({ url: "/users", method: "get" }); }
新增 frontend/src/api/post.js
文件以下:
import request from "@/utils/request"; // 获取文章列表 export function getList(params) { return request({ url: "/posts", method: "get", params }); } // 建立文章 export function create(data) { return request({ url: "/posts", method: "post", data }); } // 删除文章 export function destroy(id) { return request({ url: `/posts/${id}`, method: "delete" }); }
由于 @serverless/tencent-website
组件能够定义 env
参数,执行成功后它会在指定 root
目录自动生成 env.js
,而后在 frontend/src/main.js
中引入使用。
它会挂载 env
中定义的接口变量到 window
对象上。好比这生成的 env.js
文件以下:
window.env = {}; window.env.apiUrl = "https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/";
根据此文件咱们来修改 frontend/src/utils/request.js
文件:
import axios from "axios"; import { MessageBox, Message } from "element-ui"; import store from "@/store"; import { getToken } from "@/utils/auth"; // 建立 axios 实例 const service = axios.create({ // 1. 这里设置为 `env.js` 中的变量 `window.env.apiUrl` baseURL: window.env.apiUrl || "/", // url = base url + request url timeout: 5000 // request timeout }); // request 注入 service.interceptors.request.use( config => { // 2. 添加鉴权token if (store.getters.token) { config.headers["Authorization"] = `Bearer ${getToken()}`; } return config; }, error => { console.log(error); // for debug return Promise.reject(error); } ); // 请求 response 注入 service.interceptors.response.use( response => { const res = response.data; // 只有请求code为0,才是正常返回,不然须要提示接口错误 if (res.code !== 0) { Message({ message: res.message || "Error", type: "error", duration: 5 * 1000 }); if (res.code === 50008 || res.code === 50012 || res.code === 50014) { // to re-login MessageBox.confirm( "You have been logged out, you can cancel to stay on this page, or log in again", "Confirm logout", { confirmButtonText: "Re-Login", cancelButtonText: "Cancel", type: "warning" } ).then(() => { store.dispatch("user/resetToken").then(() => { location.reload(); }); }); } return Promise.reject(new Error(res.message || "Error")); } else { return res; } }, error => { console.log("err" + error); Message({ message: error.message, type: "error", duration: 5 * 1000 }); return Promise.reject(error); } ); export default service;
关于 UI 界面修改,这里就不作说明了,由于涉及到 Vue.js 的基础使用,若是还不会使用 Vue.js,建议先复制示例代码就好。若是对 Vue.js 感兴趣,能够到 Vue.js 官网 学习。也能够阅读本人的 Vuejs 从入门到精通系列文章,喜欢的话,能够送上您宝贵的 Star (*^▽^*)
这里只须要复制 Demo 源码 的 frontend/router
和 frontend/views
两个文件夹就好。
由于前端编译后都是静态文件,咱们须要将静态文件上传到腾讯云的 COS(对象存储) 服务,而后开启 COS 的静态网站功能就能够了,这些都不须要你手动操做,使用 @serverless/tencent-website 组件就能够轻松搞定。
修改项目根目录下 serverless.yml
文件,新增前端相关配置:
name: admin-system # 前端配置 frontend: component: "@serverless/tencent-website" inputs: code: src: dist root: frontend envPath: src # 相对于 root 指定目录,这里实际就是 frontend/src hook: npm run build env: # 依赖后端部署成功后生成的 url apiUrl: ${backend.url} protocol: https # TODO: CDN 配置,请修改!!! hosts: - host: sls-admin.yugasun.com # CDN 加速域名 https: certId: abcdedg # 为加速域名在腾讯云平台申请的免费证书 ID http2: off httpsType: 4 forceSwitch: -2 # 后端配置 backend: component: "@serverless/tencent-egg" inputs: code: ./backend functionName: admin-system role: QCS_SCFFull functionConf: timeout: 120 vpcConfig: vpcId: vpc-6n5x55kb subnetId: subnet-4cvr91js apigatewayConf: protocols: - https
执行部署命令:
$ serverless --debug
输出以下成功结果:
frontend: url: https://dtnu69vl-470dpfh-1251556596.cos-website.ap-guangzhou.myqcloud.com env: apiUrl: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/ host: - https://sls-admin.yugasun.com (CNAME: sls-admin.yugasun.com.cdn.dnsv1.com) backend: region: ap-guangzhou functionName: admin-system apiGatewayServiceId: service-f1bhmhk4 url: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/
注释:这里frontend
中多输出了host
,是咱们的 CDN 加速域名,能够经过配置@serverless/tencent-website
组件的inputs.hosts
来实现。有关 CDN 相关配置说明能够阅读 基于 Serverless Component 的全栈解决方案 - 续集。固然,若是你不想配置 CDN,直接删除,而后访问 COS 生成的静态网站 url。
部署成功后,咱们就能够访问 https://sls-admin.yugasun.com
登陆体验了。
本篇涉及到全部源码都维护在开源项目 tencent-serverless-demo 中 admin-system
本文基于腾讯云的无服务器框架 Serverless Framework 实现,涉及到内容较多,推荐在阅读时,边看边开发,跟着文章节奏一步一步实现。
若是遇到问题,能够参考本文源码。若是你成功实现了,能够到官网进一步熟悉 Egg.js 框架,以便从此能够实现更加复杂的应用。虽然本文使用的是 Vue.js 前端框架,可是你也能够将 frontend
更换为任何你喜欢的前端框架项目,开发时只须要将接口请求前缀使用 @serverless/tencent-website
组件生成的 env.js
文件就行。
传送门:
- GitHub: github.com/serverless
- 官网:serverless.com
欢迎访问:Serverless 中文网,您能够在 最佳实践 里体验更多关于 Serverless 应用的开发!
推荐阅读: 《Serverless 架构:从原理、设计到项目实战》