聊聊低代码海报平台的服务端技术选型

4,158 阅读12分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本系列的前面几篇文章呢,主要是以偏前端为主,今天这篇文章,我们来梳理下低代码海报平台的服务端该技术选型。

服务端的技术选型囊括的的东西会多一些,主要是以下几个方面:

  • NodeJS框架选型
  • 数据库选型
  • 数据结构设计
  • 登录校验相关
  • 单元测试和接口测试

框架选型

关于框架的选型,社区有这么一个共识,也可以说是决定因素:

  • 场景,是做api还是管理后台,还是h5,不同的应用场景会有不一样的选择
  • 团队能力,如果团队Node.js经验非常丰富就无所谓,如果不是特别熟,那就至少要有一个人能cover住,如果都没有,那就挑选最成熟的最保守的做吧
  • 趋势,如果leader大局观不错,综合上面2点,再辅以趋势的话,就非常好,毕竟现在技术革新太快,别你刚学会,别人都不用了,也是比较痛苦的。

首先我们这里的场景是rest api,其次在团队能力上,大家对koa2express都比较熟悉。

koa2 仍然是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。借助 cogenerator,很好地解决了异步流程控制和异常捕获问题。其次,koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量。

综合上述因素,最终NodeJS框架选择的是koa2

数据库选型

摆开数据结构不谈去谈数据库,就是耍流氓

我先说结论:像基础的作品信息(不包含作品内容)比较适合用表格形式存储,对应的也就是mysql。而作品内容一般都是JSON,这种更适合使用mongodb存储。真正的线上环境肯定会存在高并发的场景,这个时候缓存就很有必要了,这里使用的是redis作为缓存方案。

至于为什么这么选择,下面我会分别简单介绍一下mysqlmongodbredis

数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。每个数据库都有一个或多个不同的 API 用于创建,访问,管理,搜索和复制所保存的数据。我们也可以将数据存储在文件中,但是在文件中读写数据速度相对较慢。

Mysql

俗话说:不懂MySQL的前端不是一个好前端。作为Web应用方面最好的关系数据库管理系统应用软件之一,MySQL体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,一般中小型网站的开发都选择MySQL作为网站数据库。

MySQL 是一个关系型数据库管理系统。关系型数据库的特点有:

  • 数据以表格的形式出现
  • 每行为各种记录名称
  • 每列为记录名称所对应的数据域
  • 许多的行和列组成一张表单
  • 若干的表单组成database

可以看出,它是一种结构化的数据存储形式,再结合我们的数据结构,显然用MySQL去存储作品信息是很合适的,因为我们要清晰的知道每个作品的具体信息,也方便查询和展示。

聊完了MySQL,就不得不聊一下在node中怎么对接MySQL,对应的ORM框架这里我们选择的是Sequelize

简单说,ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。

在node开发中,通常使用ORM框架来简化直接操作数据库,Sequelize是当前很流行的ORM框架。

这里贴几个使用Sequelize进行增、改、查的例子。

查询单个用户

 const result = await UserModel.findOne({
    where: {
        id: id
    },
 });

查询所有用户

const result = await UserModel.findAndCountAll({
    order: [
      ["id", "desc"], // 倒序
    ],
    where: whereOpt,
});

创建新用户

const result = await UserModel.create(data);

更新用户信息

const result = await UserModel.update(data, {
    where: {
      username,
    },
});

Mongodb

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

看到这里,不自觉地就会想到作品数据是很适合用MongoDB来存储的。我们其实不太关心作品数据那一大串JSON的具体信息,也不存在什么数据字段关联。

同样MongoDB对应的ORM框架一般都是用mongoose

mongoosenodeJS提供连接 mongodb的一个库,类似于jqueryjs的关系,对mongodb一些原生方法进行了封装以及优化。简单的说,Mongoose就是对node环境中MongoDB数据库操作的封装,一个对象模型(ODM)工具,将数据库中的数据转换为JavaScript对象以供我们在应用中使用。

和上面类似,这里也贴一下几本数据操作的实例:

创建作品

const newContent = await WorkContentModel.create({
    components,
    props,
    setting,
});

根据作品ID查询作品内容

const content = await WorkContentModel.findById(contentId);

更新指定ID作品内容

await WorkContentModel.findByIdAndUpdate(contentId, {
  components: content.components || [],
  props: content.props || {},
  setting: content.setting || {},
});

Redis

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。

Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。

Redis的优势有很多,比如:

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

结合它的特性和优势,在一些高并发场景,我们都会选作它为缓存第一优先级方案。在我们的作品上线时,都会同时写一份作品数据到Redis,后面只有每次更新作品数据的时候,才会去更新缓存。用户访问作品时,都会优先访问缓存,如果缓存读不到,访问请求才会打到数据库。

登录校验

目前常用的用于用户信息认证与授权的有两种方式-JWTSession。下面我们分别对比一下两种鉴权方式的优劣点。

Session

  • 相关的概念介绍

    • session::主要存放在服务器,相对安全
    • cookie:主要存放在客户端,并且不是很安全
    • sessionStorage:仅在当前会话下有效,关闭页面或浏览器后被清除
    • localstorage:除非被清除,否则永久保存
  • 工作原理

    • 客户端带着用户名和密码去访问/login 接口,服务器端收到后校验用户名和密码,校验正确就会在服务器端存储一个 sessionId 和 session 的映射关系。
    • 服务器端返回 response,并且将 sessionId 以 set-cookie 的方式种在客户端,这样,sessionId 就存在了客户端。
    • 客户端发起非登录请求时,假如服务器给了 set-cookie,浏览器会自动在请求头中添加 cookie。
    • 服务器接收请求,分解 cookie,验证信息,核对成功后返回 response 给客户端。
  • 优势

    • 相比 JWT,最大的优势就在于可以主动清除ession 了
    • session 保存在服务器端,相对较为安全
    • 结合 cookie 使用,较为灵活,兼容性较好(客户端服务端都可以清除,也可以加密)
  • 劣势

    • cookie+session 在跨域场景表现并不好(不可跨域,domain 变量,需要复杂处理跨域)
    • 如果是分布式部署,需要做多机共享 Session 机制(成本增加)
    • 基于 cookie 的机制很容易被 CSRF
    • 查询 Session 信息可能会有数据库查询操作

JWT

  • 相关的概念介绍

    由于详细的介绍 JWT 会占用大量文章篇幅,也不是本文的重点。所以这里只是简单介绍一下。主要是和 Session 方式做一个对比。关于 JWT 详细的介绍可以参考https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样:

{
  "姓名": "森林",
  "角色": "搬砖工",
  "到期时间": "2022年11月09日19点09分"
}

以后,用户与服务端通信的时候,都要返回这个 JSON 对象。服务器完全只靠这个对象认证用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的格式大致如下:

它是一个很长的字符串,中间用点(.)分隔成三个部分。

JWT 的三个部分依次如下:

Header(头部)
Payload(负载)
Signature(签名)
  • JWT相比Session
    • 安全性(两者均有缺陷)
    • RESTful API,JWT 优胜,因为 RESTful API 提倡无状态,JWT 符合要求
    • 性能(各有利弊,因为 JWT 信息较强,所以体积也较大。不过 Session 每次都需要服务器查找,JWT 信息都保存好了,不需要再去查询数据库)
    • 时效性,Session 能直接从服务端销毁,JWT 只能等到时效性到了才会销毁(修改密码也无法阻止篡夺者的使用)

jsonwebtoken

由于 RESTful API 提倡无状态,而 JWT 又恰巧符合这一要求,因此我们采用JWT来实现用户信息的授权与认证。

项目中采用的是比较流行的jsonwebtoken。具体使用方式可以参考https://www.npmjs.com/package/jsonwebtoken

单元测试和接口测试

代码部署之前,进行一定的单元测试是十分必要的,这样能够有效并且持续保证代码质量。而实践表明,高质量的单元测试还可以帮助我们完善自己的代码。

Nodejs最常用的测试框架一般都少不了Mocha

Mocha 是一个功能丰富的Javascript测试框架,它能运行在Node.js和浏览器中。

这里以一个最常见的加法来介绍下Mocha

function add(a,b) {
    return a+b;
}

如果你之前没有接触过单元测试,可能写出的测试用例是这样的:

var sum = add(1,2);
if (sum == 3) {
    console.log('加法测试成功了');
} else {
    console.error('加法测试出错了');
}

接着又需要写减法函数、乘法函数、除法函数,但是随着模块的增加,就会遇到一个问题,如果按照上面的代码模式的话,一则输出格式比较乱,二则缺失测试结果统计。这时候大名鼎鼎的 mocha 就要闪亮登场了。

有了 mocha ,测试结果输出格式化和测试结果统计的需求就可以迎刃而解了。比如说对于上面那个加法测试用例,就可以这么写了:

var assert = require('assert');
describe('Calculator', function() {
  describe('#add()', function() {
    it('should get 3 when 1 add 2', function() {
      assert.equal(3, add(1,2));
    });
  });
});

这就是mocha最基础的用法了。

但仅仅用mocha还不够,Node.js是用于后端开发的语言,而后端开发其实很大程度上等价于编写HTTP接口,为前端提供服务。那么,Node.js单元测试则少不了对HTTP接口进行测试。

如果按照mocha的逻辑对http接口测试:

require("../server.js");
var http = require("http");
var assert = require("assert");
it("should return hi cosen", function(done) {
    http.get("http://localhost:8000", function(res)     {
        res.setEncoding("utf8");
        res.on("data", function(text)         {
            assert.equal(res.statusCode, 200);
            assert.equal(text, "hi cosen");
            done();
        });
    });
});

如上代码所示:访问接口、获取返回数据、验证返回结果。使用Node.js原生的http与assert模块就可以了。

值得稍微注意的一点是,http.get访问HTTP接口是一个异步操作。Mocha在测试异步代码是需要为it函数添加回调函数done,在断言结束的地方调用done,这样Mocha才能知道什么时候结束这个测试。

既然Node.js自带的模块就能够测试HTTP接口了,为什么还需要SuperTest呢?不妨先看一下下面的代码:

var request = require("supertest");
var server = require("../server.js");
var assert = require("assert");
it("should return hi cosen", function(done) {
    request(server)
        .get("/")
        .expect(200)
        .expect(function(res)         {
            assert.equal(res.text, "hi cosen");
        })
        .end(done);
});

对比两个测试代码,会发现后者简洁很多。

这背后的原因是因为SuperTest封装了发送HTTP请求的接口,并且提供了简单的expect断言来判定接口返回结果。对于POST接口,使用SuperTest的优势将更加明显,因为使用Node.js的http模块发送POST请求是很麻烦的。

到这里,我分别从NodeJS框架选型、数据库选型、登录校验和单元测试、接口测试几个方面阐述了我们服务端选型的依据。在下一篇文章中,我会详细的为大家介绍服务端整体架构设计以及不同模块之间的关联关系。