前言

本期我们将从0开始搭建一个小型的具备crud功能的web服务,并且不只是在本地开发,而是部署到真实线上服务器。语言使用node.js,无语言切换成本,适合前端入门。

这期我们期望实现的效果如下:

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

其中web服务能够支持以下功能:

  1. 增、删、改、查;
  2. 查询支持模糊匹配、按更新时间倒序排序;

准备工具

  • 一台centsOS的腾讯云服务器
  • MySQLWorkbench

服务器安装MySQL

首先我们通过ssh进入自己的服务器

ssh root@110.42.172.19 # 购买服务器后会自动创建一个root用户,把这个ip换成你自己的服务器公网ip即可

然后查看本机系统版本,以确定下载哪个版本的mySQL

cat /etc/redhat-release

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)
比如像我本机是Linux 8的,进入dev.mysql.com/downloads/r… 页面,找到Linux8对应的rpm包地址

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

开始下载并安装rpm包:

wget https://dev.mysql.com/get/mysql80-community-release-el8-4.noarch.rpm # 这里要拼接一下url
rpm -ivh mysql80-community-release-el8-4.noarch.rpm

然后下载mysql服务器

yum install mysql-server # 这一步会比较漫长,耐心等待就好

服务器MySQL启动与初始化设置

下载完成后,我们开始启动mysql:

systemctl start mysqld.service

启动完成后,我们可以通过systemctl status mysqld.service命令来查看mysql是否已经成功启动,如果看到如下的running小绿点,说明mysql已经成功启动了。

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

密码重置

接下来我们要进入mysql,进行root用户的密码重置:

mysql -u root -p # 初次进入是没有密码的,直接回车就可以进去
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '12345'; # 把12345换成你自己期望修改的密码即可

这里之所以使用mysql_native_password来修改是因为我们安装的是mysql8,但是我们的node.js项目大部分不支持mysql8的最新默认加密方式,所以需要显式地指定加密方式。

允许外部连接

mysql本身只支持本地连接,如果要让外部也能访问,需要在默认的mysql数据进行如下配置:

show databases; # 这里会展示默认的几个数据库,mysql就是其中之一
use mysql; # 进入mysql数据库
select host from user where user='root'; # 查看host,默认值为localhost
update user set host = '%' where user ='root'; # 修改host的值
flush privileges; # 刷新配置

完成设置后,通过以下命令退出mysql命令行:

exit;

注意,mysql命令行里面的指令最好显示在末尾添加分号,告诉mysql指令已经输完

本机连接MySQL,建库建表

由于笔者用的是macOS系统,所以暂时没发现怎么像windows一样通过打补丁的形式使用盗版Navicat,无奈选择了MySQLWorkbench这个软件,虽然界面不好看,但是能用。

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

进入MySQLWorkbench,新建对数据库的连接。

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

但是当我们输入密码后,做连接测试的却发现连接失败,聪明的我们很快想到是防火墙的问题,于是我们登录腾讯云官网,为3306端口放行。

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

连接成功后,我们开始可以愉快滴建库建表了:

比如这里,我建立的库名为easy-server-main,里面包含一张名为device_controllers的表。

  1. 建库(database),输个库名就行;

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

  1. 建表(Table),除了输入个名字以外,还需要对字段类型进行定义。

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

这里包括定义主键、为字段选择整型/字符串/日期类型,需要对mysql有一丢丢基础了解即可。简单来说,一张表就类似于我们WPS里面的excel表格。

初始化egg.js项目

建好了库表之后,我们开始使用egg.js在电脑本地初始化一个项目:

cd ~/tech # 换成你自己电脑的任意文件地址即可
mkdir easy-server && cd easy-server
npm init egg --type=simple # 模版选择simple即可
yarn

这里使用egg.js的原因有两个:

  1. egg.js是阿里开发,有完善的中文文档,查文档方便;
  2. egg.js使用起来非常简单。

通过yarn安装完依赖后,在项目终端输入yarn dev,打开http://127.0.0.1:7001/即可看到一个默认的接口返回。

但是我们的目的不只于此,我们需要连接到远程的数据库,所以我们在本地安装egg-sequelize

yarn add egg-sequelize

这里解释一下为什么没有用egg官方提供的egg-mysql作为连接工具,因为egg-mysql本身功能支持得比较少,遇到一些稍微复杂的查询是hold不住的,所以选用基于sequelize的egg-sequelize是一个非常稳健的选择。

config/config.default.js中加入以下配置:

  config.sequelize = {
    dialect: 'mysql',
    host: '110.42.172.19',
    port: 3306,
    database: 'easy-server-main',
    username: 'root',
    password: '12345',
    timezone: '+8:00', // 时区改成东8区
  };

config/plugin.js中加入如下配置:

sequelize: {
    enable: true,
    package: 'egg-sequelize',
  },

这样就完成了服务端项目对远程数据库的连接。

controller、model、service的关系

接下来我们会来写业务接口,传统的接口业务开发,一般由三部分组成:

  • model,包含对mysql具体表结构的定义;
  • service,直接使用model对表进行数据查询,具体的查询逻辑可以写在这里;
  • controller,接收前端的入参,并调用相关的service方法,返回结果给前端;

使用ORM (egg-sequelize) 开发接口与测试

了解完这几个概念后,我们现在model层定义要连接的表:

module.exports = app => {
  const { STRING, INTEGER } = app.Sequelize;
  const Main = app.model.define(
    'device_controller',
    {
      id: { type: INTEGER, primaryKey: true, autoIncrement: true },
      active: INTEGER,
      ip: STRING(20),
      name: STRING(50),
    },
    { timestamps: true }
  );
  return Main;
};

在controller部分针对增删改查各写一个接口:

const Controller = require('egg').Controller;
class HomeController extends Controller {
  // 获取控制器列表
  async getDeviceControllerList() {
    const { ctx } = this;
    const res = await ctx.service.home.getDeviceControllerList(ctx.query);
    ctx.body = res;
  }
  // 新增控制器
  async addNewDeviceController() {
    const { ctx } = this;
    const res = await ctx.service.home.addNewDeviceController(ctx.request.body);
    ctx.body = res;
  }
  // 编辑控制器
  async updateDeviceController() {
    const { ctx } = this;
    const res = await ctx.service.home.updateDeviceController(ctx.request.body);
    ctx.body = res;
  }
  // 删除控制器
  async removeDeviceController() {
    const { ctx } = this;
    const res = await ctx.service.home.removeDeviceController(
      ctx.request.body.id
    );
    ctx.body = res;
  }
}

以及在service层书写具体查询逻辑:

const Service = require('egg').Service;
const Op = require('sequelize').Op;
const { isNil } = require('lodash');
const getResponseBody = result => {
  return {
    code: 200,
    data: result,
    msg: 'success',
  };
};
class HomeService extends Service {
  async getDeviceControllerList(query) {
    const { ctx } = this;
    const { active, ip, name } = query;
    const params = {};
    if (!isNil(active)) {
      params.active = active;
    }
    if (ip) {
      params.ip = {
        [Op.like]: `%${ip}%`,
      };
    }
    if (name) {
      params.name = {
        [Op.like]: `%${name}%`, // 模糊查询的写法
      };
    }
    const res = await ctx.model.Home.findAll({
      where: params,
      order: [[ 'updated_at', 'DESC' ]], // 按更新时间倒序排
    });
    return getResponseBody(res || []);
  }
  async addNewDeviceController(body) {
    const { ctx } = this;
    const { active, ip, name } = body;
    await ctx.model.Home.create({ active, ip, name });
    return getResponseBody();
  }
  async updateDeviceController(record) {
    const { ctx } = this;
    const { id, ip, name, active } = record || {};
    await ctx.model.Home.update(
      { ip, name, active },
      {
        where: { id },
      }
    );
    return getResponseBody();
  }
  async removeDeviceController(id) {
    const { ctx } = this;
    await ctx.model.Home.destroy({
      where: { id },
    });
    return getResponseBody();
  }
}
module.exports = HomeService;

最后,在路由文件router.js中将我们写好的接口注册上去就大功告成了:

module.exports = app => {
  const { router, controller } = app;
  router.get('/device_controller/list', controller.home.getDeviceControllerList);
  router.post('/device_controller/add', controller.home.addNewDeviceController);
  router.post(
    '/device_controller/update',
    controller.home.updateDeviceController
  );
  router.post(
    '/device_controller/remove',
    controller.home.removeDeviceController
  );
};

配置跨域

要让前端项目访问到,还需要配置跨域,我们在项目中安装egg-cors这个插件即可:

yarn add egg-cors

因为我们这次只是做个CRUD的服务示例,没有涉及到权限、认证等环节,在config/config.default.js加入如下配置:

config.security = {
    csrf: {
      enable: false,
    },
  };
 config.cors = {
    origin: '*',
    allowMethods: 'GET,HEAD,PUT,POST',
  };

然后在config/plugin.js中使用:

cors: {
    enable: true,
    package: 'egg-cors',
  },

部署与启动

在本地,我们可以使用postman进行接口测试,测试完毕后手动部署到远程服务器:

因为我们打算使用PM2来做进程管理,所以先在根目录增加serve.js文件,书写内容如下:

const egg = require("egg");
const workers = Number(process.argv[2] || require("os").cpus().length);
egg.startCluster({
  workers,
  baseDir: __dirname,
});

然后进入终端,将项目压缩后传到远程服务器:

tar -zcvf ./release.tgz . # 压缩项目文件
scp ./release.tgz root@110.42.172.19:/usr/backend/easy-server # 通过scp传到远程服务器,具体存放地址看个人喜欢

重新通过ssh登录远程服务器,如果没有npm的话,通过如下命令安装npm:

yum -y install npm

然后再通过npm全局安装pm2

npm install pm2 -g

进入我们刚刚传上来压缩文件的目录/usr/backend/easy-server,通过如下tar命令进行解压到当前目录:

tar -zxf release.tgz ./

然后通过pm2启动项目:

pm2 start server.js --name easy-server

可以通过pm2 list来查看当前应用的运行状况。

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

至此,一切已经大功告成,效果如下:

写给前端的从0到1搭建一个CRUD的线上web服务(基于egg.js+MySQL+centOS)

源码地址

前端(前端是用umi3搭的,基于react,用vue的同学自行用vue项目测试即可):github.com/zhangnan24/…

服务端(数据库mysql密码已改):github.com/zhangnan24/…