做爲一名前端開發者,在選擇 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 架構:從原理、設計到項目實戰》