Serverless + Egg.js 后臺管理系統(tǒng)實戰(zhàn)

Serverelsss + Egg.js

by yugasun from https://yugasun.com/post/serverless-admin-system.html
本文可全文轉(zhuǎn)載初厚,但需要保留原作者和出處唆香。

作為一名前端開發(fā)者,在選擇 Nodejs 后端服務(wù)框架時,第一時間會想到 Egg.js昧互,不得不說 Egg.js 是一個非常優(yōu)秀的企業(yè)級框架筋蓖,它的高擴展性和豐富的插件,極大的提高了開發(fā)效率澜搅。開發(fā)者只需要關(guān)注業(yè)務(wù)就好伍俘,比如要使用 redis,引入 egg-redis 插件勉躺,然后簡單配置就可以了癌瘾。正因為如此,第一次接觸它饵溅,我便喜歡上了它妨退,之后也用它開發(fā)過不少應(yīng)用。

有了如此優(yōu)秀的框架蜕企,那么如何將一個 Egg.js 的服務(wù)遷移到 Serverless 架構(gòu)上呢咬荷?

在線預(yù)覽

背景

我在文章 基于 Serverless Component 的全棧解決方案 中講述了,如何將一個基于 Vue.js 的前端應(yīng)用和基于 Express 的后端服務(wù)糖赔,快速部署到騰訊云上萍丐。雖然受到不少開發(fā)者的喜愛,但是很多開發(fā)者私信問我放典,這還是一個 Demo 性質(zhì)的項目而已逝变,有沒有更加實用性的解決方案。而且他們實際開發(fā)中奋构,很多使用的正是 Egg.js 框架壳影,能不能提供一個 Egg.js 的解決方案?

本文將手把手教你結(jié)合 Egg.jsServerless 實現(xiàn)一個后臺管理系統(tǒng)弥臼。

讀完此文你將學(xué)到:

  1. Egg.js 基本使用
  2. 如何使用 Sequelize ORM 模塊進行 Mysql 操作
  3. 如何使用 Redis
  4. 如何使用 JWT 進行用戶登錄驗證
  5. Serverless Framework 的基本使用
  6. 如何將本地開發(fā)好的 Egg.js 應(yīng)用部署到騰訊云云函數(shù)上
  7. 如何基于云端對象存儲快速部署靜態(tài)網(wǎng)站

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 了。

關(guān)于 Egg.js 的框架更多知識径缅,建議閱讀 官方文檔

準(zhǔn)備

對 Egg.js 有了簡單了解掺栅,接下來我們來初始化我們的后臺管理系統(tǒng)烙肺,新建一個項目目錄 admin-system:

$ mkdir admin-system

將上面創(chuàng)建的 Egg.js 項目復(fù)制到 admin-system 目錄下,重命名為 backend氧卧。然后將前端模板項目復(fù)制到 frontend 文件夾中:

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

說明: vue-admin-template 是基于 Vue2.0 的管理系統(tǒng)模板桃笙,是一個非常優(yōu)秀的項目,建議對 Vue.js 感興趣的開發(fā)者可以去學(xué)習(xí)下沙绝,當(dāng)然如果你對 Vue.js 還不是太了解搏明,這里有個基礎(chǔ)入門學(xué)習(xí)教程 Vuejs 從入門到精通系列文章

之后你的項目目錄結(jié)構(gòu)如下:

.
├── README.md
├── backend     // 創(chuàng)建的 Egg.js 項目
└── frontend    // 克隆的 Vue.js 前端項目模板

啟動前端項目熟悉下界面:

$ cd frontend
$ npm install
$ npm run dev

然后訪問 http://localhost:9528 就可以看到登錄界面了。

開發(fā)后端服務(wù)

對于一個后臺管理系統(tǒng)服務(wù)闪檬,我們這里只實現(xiàn)登錄鑒權(quán)和文章管理功能星著,剩下的其他功能大同小異,讀者可以之后自由補充擴展粗悯。

1. 添加 Sequelize 插件

在正式開發(fā)之前虚循,我們需要引入數(shù)據(jù)庫插件,這里本人偏向于使用 Sequelize ORM 工具進行數(shù)據(jù)庫操作为黎,正好 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 中配置數(shù)據(jù)庫連接參數(shù):

// ...
const userConfig = {
  // ...
  sequelize: {
    dialect: "mysql",

    // 這里也可以通過 .env 文件注入環(huán)境變量铭乾,然后通過 process.env 獲取
    host: "xxx",
    port: "xxx",
    database: "xxx",
    username: "xxx",
    password: "xxx"
  }
  // ...
};
// ...

2. 添加 JWT 插件

系統(tǒng)將使用 JWT token 方式進行登錄鑒權(quán)剪廉,安裝配置參考官方文檔,egg-jwt

3. 添加 Redis 插件

系統(tǒng)將使用 redis 來存儲和管理用戶 token炕檩,安裝配置參考官方文檔斗蒋,egg-redis

4. 角色 API

定義用戶模型,創(chuàng)建 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 表的關(guān)系笛质,一個角色可以含有多個用戶泉沾,外鍵相關(guān)
  Role.associate = () => {
    app.model.Role.hasMany(app.model.User, { as: "users" });
  };

  return Role;
};

實現(xiàn) Role 相關(guān)服務(wù),創(chuàng)建 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;
  }

  // 創(chuàng)建角色
  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 就該包括以上五個方法妇押,然后實現(xiàn) RoleController, 創(chuàng)建 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敲霍,這里就不復(fù)制粘貼了俊马,可以參考項目實例源碼 admin-system

6. 同步數(shù)據(jù)庫表格

上面只是定義好了 RoleUser 兩個 Schema肩杈,那么如何同步到數(shù)據(jù)庫呢柴我?這里先借助 Egg.js 啟動的 hooks 來實現(xiàn),Egg.js 框架提供了統(tǒng)一的入口文件(app.js)進行啟動過程自定義扩然,這個文件返回一個 Boot 類艘儒,我們可以通過定義 Boot 類中的生命周期方法來執(zhí)行啟動應(yīng)用過程中的初始化工作。

我們在 backend 目錄中創(chuàng)建 app.js 文件,如下:

"use strict";

class AppBootHook {
  constructor(app) {
    this.app = app;
  }

  async willReady() {
    // 這里只能在開發(fā)模式下同步數(shù)據(jù)庫表格
    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 生命周期函數(shù)界睁,我們可以執(zhí)行 this.app.model.sync() 函數(shù)來同步數(shù)據(jù)表觉增,當(dāng)然這里同時初始化了角色和用戶數(shù)據(jù)記錄,用來做為演示用翻斟。

注意:這的數(shù)據(jù)庫同步只是本地調(diào)試用抑片,如果想要騰訊云的 Mysql 數(shù)據(jù)庫,建議開啟遠程連接杨赤,通過 sequelize db:migrate 實現(xiàn),而不是每次啟動 Egg 應(yīng)用時同步截汪,示例代碼已經(jīng)完成此功能疾牲,參考 Egg Sequelize 文檔
這里本人為了省事衙解,直接開啟騰訊云 Mysql 公網(wǎng)連接阳柔,然后修改 config.default.js 中的 sequelize 配置,運行 npm run dev 進行開發(fā)模式同步蚓峦。

到這里舌剂,我們的用戶和角色的 API 都已經(jīng)定義好了,啟動服務(wù) npm run dev暑椰,訪問 https://127.0.0.1:7001/users 可以獲取所有用戶列表了霍转。

7. 用戶登錄/注銷 API

這里登錄邏輯比較簡單,客戶端發(fā)送 用戶名密碼/login 路由一汽,后端通過 login 函數(shù)接受避消,然后從數(shù)據(jù)庫中查詢該用戶名,同時比對密碼是否正確召夹。如果正確則調(diào)用 app.jwt.sign() 函數(shù)生成 token岩喷,并將 token 存入到 redis 中,同時返回該 token监憎,之后客戶端需要鑒權(quán)的請求都會攜帶 token纱意,進行鑒權(quán)驗證。思路很簡單鲸阔,我們就開始實現(xiàn)了偷霉。

流程圖如下:

<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 函數(shù) encryptPwd() 進行加密的(這里用到最簡單的 md5 加密方式,實際開發(fā)中建議使用更加高級加密方式)死讹,所以在校驗密碼正確性時瞒滴,也需要先加密一次。至于如何在 Egg.js 框架中新增 helper 函數(shù),只需要在 backend/app/extend 文件夾中新增 helper.js 文件妓忍,然后 modole.exports 一個包含該函數(shù)的對象就行虏两,參考 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 插件定罢,在鑒權(quán)通過的路由對應(yīng) controller 函數(shù)中,會將 app.jwt.sign(user, secrete) 加密的用戶信息旁瘫,添加到 ctx.state.user 中祖凫,所以 userInfo 函數(shù)只需要將它返回就行。

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

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

userInfologout 函數(shù)非常簡單惠况,重點是路由中間件如何處理。

接下來宁仔,我們來定義登錄相關(guān)路由稠屠,修改 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() 函數(shù)可以接受中間件函數(shù)权埠,用來處理一些路由相關(guān)的特殊邏輯。

比如 /user-info煎谍,路由添加了 app.jwt 作為 JWT 鑒權(quán)中間件函數(shù)攘蔽,至于為什么這么用,egg-jwt 插件有明確說明粱快。

這里稍微復(fù)雜的是 /logout 路由秩彤,因為我們在注銷登錄時,需要將用戶的 tokenredis 中移除事哭,所以這里借助了 koa-jwt2isRevokded 參數(shù)漫雷,來進行 token 刪除。

后端服務(wù)部署

到這里鳍咱,后端服務(wù)的登錄和注銷邏輯基本完成了降盹。那么如何部署到云函數(shù)呢?可以直接使用 tencent-egg 組件谤辜,它是專門為 Egg.js 框架打造的 Serverless Component蓄坏,使用它可以快速將我們的 Egg.js 項目部署到騰訊云云函數(shù)上。

1. 準(zhǔn)備

我們先創(chuàng)建一個 backend/sls.js 入口文件:

const { Application } = require("egg");
const app = new Application();
module.exports = app;

然后修改 backend/config/config.default.js 文件:

const config = (exports = {
  env: "prod", // 推薦云函數(shù)的 egg 運行環(huán)境變量修改為 prod
  rundir: "/tmp",
  logger: {
    dir: "/tmp"
  }
});

注釋:這里之所有需要修改運行和日志目錄丑念,是因為云函數(shù)運行時涡戳,只有 /tmp 才有寫權(quán)限。

全局安裝 serverless 命令:

$ npm install serverless -g

2. 配置 Serverless

在項目根目錄下創(chuàng)建 serverless.yml 文件脯倚,同時新增 backend 配置:

backend:
  component: "@serverless/tencent-egg"
  inputs:
    code: ./backend
    functionName: admin-system
    # 這里必須指定一個具有操作 mysql 和 redis 的角色渔彰,具體角色創(chuàng)建嵌屎,可訪問 https://console.cloud.tencent.com/cam/role
    role: QCS_SCFFull
    functionConf:
      timeout: 120
      # 這里的私有網(wǎng)絡(luò)必須和 mysql、redis 實例一致
      vpcConfig:
        vpcId: vpc-xxx
        subnetId: subnet-xxx
    apigatewayConf:
      protocols:
        - https

此時你的項目目錄結(jié)構(gòu)如下:

.
├── README.md         // 項目說明文件
├── serverless.yml    // serverless yml 配合文件
├── backend           // 創(chuàng)建的 Egg.js 項目
└── frontend          // 克隆的 Vue.js 前端項目模板

3. 執(zhí)行部署

執(zhí)行部署命令:

$ serverless --debug

之后控制臺需要進行掃碼登錄驗證騰訊云賬號恍涂,掃碼登錄就好宝惰。等部署成功會發(fā)揮如下信息:

  backend:
    region:              ap-guangzhou
    functionName:        admin-system
    apiGatewayServiceId: service-f1bhmhk4
    url:                 https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/

這里輸出的 url 就是部署成功的 API 網(wǎng)關(guān)接口,可以直接訪問測試再沧。

注釋:云函數(shù)部署時尼夺,會自動在騰訊云的 API 網(wǎng)關(guān)創(chuàng)建一個服務(wù),同時創(chuàng)建一個 API炒瘸,通過該 API 就可以觸發(fā)云函數(shù)執(zhí)行了淤堵。

4. 賬號配置(可選)

當(dāng)前默認(rèn)支持 Serverless cli 掃描二維碼登錄,如果希望配置持久的環(huán)境變量/秘鑰信息顷扩,也可以在項目根目錄創(chuàng)建 .env 文件

.env 文件中配置騰訊云的 SecretId 和 SecretKey 信息并保存粘勒,密鑰可以在 API 密鑰管理 中獲取或者創(chuàng)建.

# .env
TENCENT_SECRET_ID=123
TENCENT_SECRET_KEY=123

5. 文章 API

跟用戶 API 類似,只需要復(fù)制粘貼上面用戶相關(guān)模塊屎即,修改名稱為 posts, 并修改數(shù)據(jù)模型就行事富,這里就不粘貼代碼了技俐。

前端開發(fā)

本實例直接使用的 vue-admin-template 的前端模板。

我們需要做如下幾部分修改:

  1. 刪除接口模擬:更換為真實的后端服務(wù)接口
  2. 修改接口函數(shù):包括用戶相關(guān)的 frontend/src/api/user.js 和文章相關(guān)接口 frontend/src/api/post.js统台。
  3. 修改接口工具函數(shù):主要是修改 frontend/src/utils/request.js 文件雕擂,包括 axios請求的 baseURL 和請求的 header。
  4. UI 界面修改:主要是新增文章管理頁面贱勃,包括列表頁和新增頁井赌。

1. 刪除接口模擬

首先刪除 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)
});

2. 修改接口函數(shù)

修改 frontend/src/api/user.js 文件,包括登錄戚绕、注銷纹坐、獲取用戶信息和獲取用戶列表函數(shù)如下:

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
  });
}

// 創(chuàng)建文章
export function create(data) {
  return request({
    url: "/posts",
    method: "post",
    data
  });
}

// 刪除文章
export function destroy(id) {
  return request({
    url: `/posts/${id}`,
    method: "delete"
  });
}

3. 修改接口工具函數(shù)

因為 @serverless/tencent-website 組件可以定義 env 參數(shù),執(zhí)行成功后它會在指定 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/";

根據(jù)此文件我們來修改 frontend/src/utils/request.js 文件:

import axios from "axios";
import { MessageBox, Message } from "element-ui";
import store from "@/store";
import { getToken } from "@/utils/auth";

// 創(chuàng)建 axios 實例
const service = axios.create({
  // 1. 這里設(shè)置為 `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. 添加鑒權(quán)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;

4. UI 界面修改

關(guān)于 UI 界面修改,這里就不做說明了吨凑,因為涉及到 Vue.js 的基礎(chǔ)使用捍歪,如果還不會使用 Vue.js,建議先復(fù)制示例代碼就好。如果對 Vue.js 感興趣费封,可以到 Vue.js 官網(wǎng) 學(xué)習(xí)焕妙。也可以閱讀本人的 Vuejs 從入門到精通系列文章,喜歡的話弓摘,可以送上您寶貴的 Star (*^▽^*)

這里只需要復(fù)制 Demo 源碼frontend/routerfrontend/views 兩個文件夾就好焚鹊。

前端部署

因為前端編譯后都是靜態(tài)文件,我們需要將靜態(tài)文件上傳到騰訊云的 COS(對象存儲) 服務(wù)韧献,然后開啟 COS 的靜態(tài)網(wǎng)站功能就可以了末患,這些都不需要你手動操作,使用 @serverless/tencent-website 組件就可以輕松搞定锤窑。

1. 修改 Serverless 配置文件

修改項目根目錄下 serverless.yml 文件璧针,新增前端相關(guān)配置:

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 配置渊啰,請修改L匠鳌!绘证!
    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

2. 執(zhí)行部署

執(zhí)行部署命令:

$ serverless --debug

輸出如下成功結(jié)果:

  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 來實現(xiàn)嚷那。有關(guān) CDN 相關(guān)配置說明可以閱讀 基于 Serverless Component 的全棧解決方案 - 續(xù)集胞枕。當(dāng)然,如果你不想配置 CDN魏宽,直接刪除腐泻,然后訪問 COS 生成的靜態(tài)網(wǎng)站 url。

部署成功后队询,我們就可以訪問 https://sls-admin.yugasun.com 登錄體驗了派桩。

源碼

本篇涉及到所有源碼都維護在開源項目 tencent-serverless-demoadmin-system

總結(jié)

本篇文章涉及到內(nèi)容較多,推薦在閱讀時蚌斩,邊看邊開發(fā)窄坦,跟著文章節(jié)奏一步一步實現(xiàn)。如果遇到問題凳寺,可以參考本文源碼鸭津。如果你成功實現(xiàn)了,可以到官網(wǎng)進一步熟悉 Egg.js 框架肠缨,以便今后可以實現(xiàn)更加復(fù)雜的應(yīng)用逆趋。雖然本文使用的是 Vue.js 前端框架,但是你也可以將 frontend 更換為任何你喜歡的前端框架項目晒奕,開發(fā)時只需要將接口請求前綴使用 @serverless/tencent-website 組件生成的 env.js 文件就行闻书。

如果你在開發(fā)中名斟,想找一些優(yōu)秀的 Serverless Component,卻不知道哪里去找魄眉,可以收藏本人長期維護更新的開源項目 awesome-serverless-framework砰盐。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坑律,隨后出現(xiàn)的幾起案子岩梳,更是在濱河造成了極大的恐慌,老刑警劉巖晃择,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冀值,死亡現(xiàn)場離奇詭異,居然都是意外死亡宫屠,警方通過查閱死者的電腦和手機列疗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浪蹂,“玉大人抵栈,你說我怎么就攤上這事±ご危” “怎么了竭讳?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長浙踢。 經(jīng)常有香客問我,道長灿渴,這世上最難降的妖魔是什么洛波? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮骚露,結(jié)果婚禮上蹬挤,老公的妹妹穿的比我還像新娘。我一直安慰自己棘幸,他們只是感情好焰扳,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著误续,像睡著了一般吨悍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蹋嵌,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天育瓜,我揣著相機與錄音,去河邊找鬼栽烂。 笑死躏仇,一個胖子當(dāng)著我的面吹牛恋脚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播焰手,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼糟描,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了书妻?” 一聲冷哼從身側(cè)響起船响,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驻子,沒想到半個月后灿意,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡崇呵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年缤剧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片域慷。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡荒辕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出犹褒,到底是詐尸還是另有隱情抵窒,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布叠骑,位于F島的核電站李皇,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏宙枷。R本人自食惡果不足惜掉房,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慰丛。 院中可真熱鬧卓囚,春花似錦、人聲如沸诅病。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贤笆。三九已至蝇棉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間芥永,已是汗流浹背银萍。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恤左,地道東北人贴唇。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓搀绣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親戳气。 傳聞我的和親對象是個殘疾皇子链患,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容