By yugasun from yugasun.com/post/server… This article can be reproduced in full, but the original author and source need to be retained.

As a front-end developer, when I choose the Nodejs back-end service framework, I will immediately think of egg.js. I have to say that egg.js is an excellent enterprise-level framework with high scalability and rich plug-ins, which greatly improves the development efficiency. Developers just need to focus on the business, such as using Redis, importing the egg-Redis plugin, and simply configuring it. Because of that, I fell in love with it when I first encountered it, and have since developed a number of apps with it.

Given such an excellent framework, how do you migrate an egg.js service to a Serverless architecture?

The online preview

background

In the article, I described how to rapidly deploy a vue.js-based front-end application and express-based back-end service to Tencent Cloud in the full-stack solution based on Serverless Component. Although it was loved by many developers, many developers asked me in private letters that it was just a Demo project and there was no more practical solution. In addition, many of their actual development uses the egg.js framework, can you provide a solution to egg.js?

This article will teach you how to combine egg.js and Serverless to achieve a back-end management system.

After reading this article, you will learn:

  1. Egg.js is basically used
  2. How do I use the Sequelize ORM module to perform Mysql operations
  3. How do I use Redis
  4. How do I use JWT for user login authentication
  5. Basic use of the Serverless Framework
  6. How to deploy locally developed egg. js application to Tencent Yunyun function
  7. How to quickly deploy static Websites based on cloud object storage

An Egg. Introduction to js

Initialize the egg.js project:

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

Launch project:

$ npm run dev
Copy the code

Then your browser goes to http://localhost:7001 and you’ll see the friendly Hi, egg.

For more information about the framework of egg.js, we recommend reading the official documentation

To prepare

Now that we have a brief understanding of egg.js, let’s initialize our background management system and create a new project directory admin-system:

$ mkdir admin-system
Copy the code

Copy the egg.js project created above to the admin-system directory and rename it backend. Then copy the frontend template project into the frontend folder:

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

Description: Vue-admin-template is a management system template based on Vue2.0, which is a very excellent project. It is recommended that developers who are interested in vue.js can learn it. Of course, if you are not familiar with vue.js, Here is a basic introductory Vuejs tutorial from beginner to master series of articles

Then your project directory structure looks like this:

├── ├─ frontend // Clone Vue. Js front-end project templateCopy the code

Start the front-end project and get familiar with the interface:

$ cd frontend
$ npm install
$ npm run dev
Copy the code

Then visit http://localhost:9528 to see the login screen.

Develop back-end services

For a background management system services, here we only achieve login authentication and article management functions, the rest of the other functions are similar, readers can be free to supplement the expansion.

1. Add the Sequelize plug-in

Egg-sequelize ORM: Sequelize ORM: Sequelize ORM: Sequelize ORM: Sequelize ORM: Sequelize ORM: Sequelize ORM

$ cd backend
This also installs the mysql2 module because you need to connect to mysql via sequelize
$ npm install egg-sequelize mysql2 --save
Copy the code

This plug-in is then imported in backend/config/plugin.js:

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

In the backend/config/config. The default. The configuration database connection parameters in js:

// ...
const userConfig = {
  // ...
  sequelize: {
    dialect: "mysql".// It is also possible to inject environment variables through the.env file and fetch them through process.env
    host: "xxx".port: "xxx".database: "xxx".username: "xxx".password: "xxx"
  }
  // ...
};
// ...
Copy the code

2. Add the JWT plug-in

The system will use the JWT token for login authentication. For installation and configuration, refer to the official document, Egg-jWT

3. Add the Redis plug-in

The system will use Redis to store and manage user tokens. The installation and configuration refer to the official document, egg-Redis

4. Role of API

Define the user model, creating backend/app/model/role. Js file is as follows:

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

  // Define the relationship with the Users table, a role can contain multiple users, foreign key related
  Role.associate = () = > {
    app.model.Role.hasMany(app.model.User, { as: "users" });
  };

  return Role;
};
Copy the code

Implement Role related services, create a backend/app/service/Role. Js file is as follows:

const { Service } = require("egg");

class RoleService extends Service {
  // Get the list of roles
  async list(options) {
    const {
      ctx: { model }
    } = this;
    returnmodel.Role.findAndCountAll({ ... options,order: [["created_at"."desc"],
        ["id"."desc"]]}); }// Get the role by 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;
  }

  // Create a role
  async create(role) {
    const {
      ctx: { model }
    } = this;
    return model.Role.create(role);
  }

  // Update the 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);
  }

  // Delete the role
  async destroy(id) {
    const role = await this.ctx.model.Role.findByPk(id);
    if(! role) {this.ctx.throw(404."role not found");
    }
    returnrole.destroy(); }}module.exports = RoleService;
Copy the code

A complete RESTful API should include the above five methods, then implement RoleController, creating 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;
Copy the code

In backend/app/route.js routing configuration file, define the RESTful API of role:

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

Using the router.resources method, we mapped the interface for adding, deleting, modifying and querying roles to the app/ Controller/Roles.js file. For details, see the official documentation

5. User API

Define our user API in the same way as Role. Instead of copying and pasting, refer to the project instance source code admin-system.

6. Synchronize database tables

The Role and User schemas are defined, so how to synchronize to the database? The egg.js framework provides a unified entry file (app.js) to customize the startup process. This file returns a Boot class, which we can initialize by defining life cycle methods in the Boot class.

We create the app.js file in the Backend directory as follows:

"use strict";

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

  async willReady() {
    // Database tables can only be synchronized in development mode
    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', 'the 2020-02-04 09:55:23', 'the 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.', 'the 2020-02-04 10:00:23', 'the 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;
Copy the code

With the willReady lifecycle function, we can execute this.app.model.sync() to synchronize the tables, initializing both the role and user data records for demonstration purposes.

Note: Sequelize DB: Migrate If you want to use the Mysql database on Tencent Cloud, you are advised to sequelize db: Migrate on a remote connection, rather than synchronize data each time you start Egg. In order to save trouble, I directly open Tencent Cloud Mysql public network connection, modify the sequelize configuration in config.default.js, and run NPM run dev to synchronize the development mode.

Here, our users and roles of the API have been defined, start the service NPM run dev, visit https://127.0.0.1:7001/users to get all the user list.

7. User login/logout API

The login logic is simple. The client sends the user name and password to the /login route, and the back end receives the route through the login function. Then, the back end queries the user name in the database and checks whether the password is correct. If correct, the function app.jwt.sign() will be called to generate the token, and the token will be stored in redis. At the same time, the token will be returned. After that, all requests requiring authentication by the client will carry the token for authentication verification. The idea was simple, and we started implementing it.

The flow chart is as follows:

First of all, in the backend/app/controller/home. The new login to handle the login method in js:

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"}; }}}}Copy the code

EncryptPwd () : encryptPwd() : encryptPwd() : encryptPwd() : encryptPwd() : encryptPwd() : encryptPwd() : encryptPwd() How to add a helper.js function to the framework, just add a helper.js file in backend/app/extend and an object containing the function in modole.exports. Refer to the framework extension documentation

Then, in the backend/app/controller/home. Add the userInfo in js method, get the user information:

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

The egg-jwt plugin will add the user information encrypted by app.jwt.sign(user, secrete) to ctx.state.user in the controller function corresponding to the route authenticated. So the userInfo function just returns it.

After the backend/app/controller/home. New logout method in js:

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

The userInfo and logout functions are very simple and focus on how the routing middleware handles them.

Modify backend/app/router.js and add /login, /user-info, and /logout routes.

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);
};
Copy the code

When the egg.js framework defines a route, the router.post() function can accept middleware functions that handle some route-specific logic.

For example, the /user-info route adds app.jwt as a JWT authentication middleware function. The egg-jwt plugin explains why this is used.

The /logout route is slightly more complicated because the user’s token is removed from redis when logged out, so koA-JwT2 isRevokded is used to delete the token.

Back-end service deployment

At this point, the login and logout logic for the back-end service is almost complete. So how do you deploy to cloud functions? You can directly use the Oh-Egg Component, which is the Serverless Component specially built for the egg.js framework. It can be used to quickly deploy our egg.js project to Tencent Yunyun function.

1. Prepare

We will create a backend/sls.js entry file:

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

And then modify the backend/config/config. Default. Js file:

const config = (exports = {
  env: "prod".// It is recommended that the egg run environment variable of cloud functions be changed to prod
  rundir: "/tmp".logger: {
    dir: "/tmp"}});Copy the code

Note: All changes to run and log directories are required here because only/TMP has write permission when the cloud function is running.

Global install serverless command:

$ npm install serverless -g
Copy the code

2. Configuration Serverless

Create the serverless. Yml file in the root directory of the project and add the backend configuration:

backend:
  component: "@serverless/tencent-egg"
  inputs:
    code: ./backend
    functionName: admin-system
    # here must specify a mysql and the role of redis operation, specific character creation, can go to https://console.cloud.tencent.com/cam/role
    role: QCS_SCFFull
    functionConf:
      timeout: 120
      The private network must be consistent with the mysql/Redis instance
      vpcConfig:
        vpcId: vpc-xxx
        subnetId: subnet-xxx
    apigatewayConf:
      protocols:
        - https
Copy the code

Your project directory structure is as follows:

. ├ ─ ─ the README. Md / / project documents ├ ─ ─ serverless. Yml / / serverless yml file ├ ─ ─ the backend / / create an Egg. The js project └ ─ ─ frontend / / clone Vue.js front-end project templateCopy the code

3. Perform the deployment

Execute the deployment command:

$ serverless --debug
Copy the code

After that, the console needs to scan for login to verify Tencent cloud account. It is good to scan for login. After the deployment is successful, the following information will be displayed:

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

The output URL is the successfully deployed API gateway interface, and you can access the test directly.

Note: When the cloud function is deployed, a service will be automatically created in Tencent cloud API gateway, and an API will be created at the same time, through which the cloud function can be triggered to execute.

4. (Optional) Account Configuration

The default login mode is Serverless CLI scanning TWO-DIMENSIONAL code. To configure persistent environment variables and key information, you can also create an. Env file in the root directory of the project

Env. Configure and save the SecretId and SecretKey information of Tencent cloud. The key can be obtained or created in API key management.

# .env
TENCENT_SECRET_ID=123
TENCENT_SECRET_KEY=123
Copy the code

5. The article API

Similar to the user API, just copy and paste the user related module above, change the name to Posts, and modify the data model. There is no code to paste here.

The front-end development

The vue-admin-template front-end template is directly used in this example.

We need to make the following modifications:

  1. Delete interface simulation: Replace it with a real back-end service interface
  2. Modify interface functions: including user-specific onesfrontend/src/api/user.jsAnd article related interfacefrontend/src/api/post.js.
  3. Modify interface tool functions: Mainly modifyfrontend/src/utils/request.jsDocuments, includingaxiosThe request ofbaseURLAnd the requested header.
  4. UI interface modification: mainly new article management page, including list page and new page.

1. Delete interface emulation

First delete the frontend/mock folder. Then modify the front-end entry file frontend/ SRC /main.js:

// 1. Introduce the interface variable file, which will be generated automatically by the @serverless/ Tencent -website component
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";

// select * from 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)
});
Copy the code

2. Modify interface functions

Modify the frontend/ SRC/API /user.js file, including login, logout, obtaining user information, and obtaining user list functions as follows:

import request from "@/utils/request";

/ / login
export function login(data) {
  return request({
    url: "/login".method: "post",
    data
  });
}

// Get user information
export function getInfo(token) {
  return request({
    url: "/user-info".method: "get"
  });
}

// Log out
export function logout() {
  return request({
    url: "/logout".method: "post"
  });
}

// Get the user list
export function getList() {
  return request({
    url: "/users".method: "get"
  });
}
Copy the code

The new frontend/ SRC/API /post.js file is as follows:

import request from "@/utils/request";

// Get the list of articles
export function getList(params) {
  return request({
    url: "/posts".method: "get",
    params
  });
}

// Create the article
export function create(data) {
  return request({
    url: "/posts".method: "post",
    data
  });
}

// Delete the article
export function destroy(id) {
  return request({
    url: `/posts/${id}`.method: "delete"
  });
}
Copy the code

3. Modify interface tool functions

Since the @serverless/ Tceh-website component can define env parameters, it automatically generates env.js in the specified root directory after successful execution, and then uses it in frontend/ SRC /main.js. It will mount the interface variables defined in env to the window object. For example, the resulting env.js file looks like this:

window.env = {};
window.env.apiUrl =
  "https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/";
Copy the code

According to this file we to modify the frontend/SRC/utils/request. Js file:

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

// Create an axios instance
const service = axios.create({
  // 1. Set this to variable 'window.env.apiurl' in 'env.js'
  baseURL: window.env.apiUrl || "/".// url = base url + request url
  timeout: 5000 // request timeout
});

/ / request injection
service.interceptors.request.use(
  config= > {
    // 2. Add the authentication token
    if (store.getters.token) {
      config.headers["Authorization"] = `Bearer ${getToken()}`;
    }
    return config;
  },
  error= > {
    console.log(error); // for debug
    return Promise.reject(error); });// Request response injection
service.interceptors.response.use(
  response= > {
    const res = response.data;

    If the request code is 0, an interface error message is displayed
    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 {
      returnres; }},error= > {
    console.log("err" + error);
    Message({
      message: error.message,
      type: "error".duration: 5 * 1000
    });
    return Promise.reject(error); });export default service;
Copy the code

4. Modify the UI

As for the UI interface modification, I will not explain it here, because it involves the basic use of vue.js. If you do not know how to use vue.js, it is recommended to copy the sample code first. If you are interested in vue.js, you can go to the official website of vue.js to learn. You can also read my Vuejs series from beginner to master, if you like, you can send your precious Star (*^▽^*)

Just copy the Demo source’s frontend/ Router and frontend/views folders.

The front-end deployment

Because the front-end is static files after compilation, we need to upload the static files to COS (object storage) service of Tencent Cloud, and then open the static website function of COS. All these do not need manual operation, you can easily do it by using @serverless/ T700-website component.

1. Modify the Serverless configuration file

Modified the serverless. Yml file in the root directory of the project, and added front-end configurations:

name: admin-system

# Front-end configuration
frontend:
  component: "@serverless/tencent-website"
  inputs:
    code:
      src: dist
      root: frontend
      envPath: src Frontend/SRC is the directory specified by root
      hook: npm run build
    env:
      Depend on the URL generated after successful back-end deployment
      apiUrl: ${backend.url}
    protocol: https
    # TODO:CDN configuration, please modify!!
    hosts:
      - host: sls-admin.yugasun.com # CDN Accelerated domain name
        https:
          certId: abcdedg # Free certificate ID for accelerating domain name application in Tencent cloud platform
          http2: off
          httpsType: 4
          forceSwitch: 2 -

# Backend configuration
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
Copy the code

2. Perform the deployment

Execute the deployment command:

$ serverless --debug
Copy the code

The following success results are displayed:

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 - through functionName: admin - system apiGatewayServiceId: service-f1bhmhk4 url: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/Copy the code

Note: The frontend output host is our CDN accelerated domain name, which can be achieved by configuring the inputs. Hosts of the @serverless/ Tencent -website component. For CDN configuration instructions, read Serverless Component-based Full-stack solution – Sequel. Of course, if you don’t want to configure the CDN, just delete it and visit the static website URL generated by COS.

After successful deployment, we can access https://sls-admin.yugasun.com login experience.

The source code

All the source code involved in this article is maintained in the open source project 0700-Serverless-demo admin-system

conclusion

This article involves a lot of content, it is recommended to read while developing, follow the pace of the article step by step. If you run into problems, refer to this source code. If you succeed, go to the official website to familiarize yourself with the egg.js framework so that you can implement more complex applications in the future. While this article uses vue.js frontend, you can replace Frontend with any front-end framework project you like by using the env.js file generated by the @serverless/ T700-website component as the interface request prefix.

If you’re looking for some great Serverless components in your development and don’t know where to look, you can bookend my open-source project, awesome-Serverless Framework, which HAS been updated for a long time.