关于thinkjs:如何使用-ThinkJS-优雅的编写-RESTful-API

RESTful 是目前比拟支流的一种用来设计和编排服务端 API 的一种标准。在 RESTful API 中,所有的接口操作都被认为是对资源的 CRUD,应用 URI 来示意操作的资源,申请办法示意具体的操作,响应状态码示意操作后果。之前应用 RESTful 的标准写过不少 API 接口,我集体认为它最大的益处就是帮忙咱们更好的去布局整顿接口,如果还是依照以前依据需要来写接口的话接口的复用率不高不说,整个我的项目也会变得十分的芜杂。 文件即路由是 ThinkJS 的一大特色,比方 /user 这个路由等价于 /user/index,会对应到 src/controller/user.js 中的 indexAction 办法。那么就以 /user 这个 API 为例,在 ThinkJS 中要创立 RESTful 格调的 API 须要以下两个步骤: <!--more--> 运行命令 thinkjs controller user -r 会创立路由文件 src/controller/user.js在 src/config/router.js 中应用自定义路由标记该路由为 RESTful 路由 //src/config/router.jsmodule.exports = [ ['/user/:id?', 'rest']];这样咱们就实现了一个 RESTful 路由的初始化,这个资源的所有操作都会被映射成路由文件中对应申请办法的 Action 函数中,例如: GET /user 获取用户列表,对应 getAction 办法GET /user/:id 获取某个用户的详细信息,也对应 getAction` 办法POST /user 增加一位用户,对应 postAction 办法PUT /user/:id 更新一位用户材料,对应 putAction 办法DELETE /user/:id 删除一位用户,对应 deleteAction 办法然而每个 RESTful 路由都须要去 router.js 中写一遍自定义路由未免过于麻烦。所以我写了一个中间件 think-router-rest,只须要在 Controller 文件中应用 _REST 动态属性标记一下就能够将其转换成 RESTful 路由了。 ...

September 28, 2020 · 4 min · jiezi

关于thinkjs:谈谈-MySQL-的-JSON-数据类型

MySQL 5.7 减少了 JSON 数据类型的反对,在之前如果要存储 JSON 类型的数据的话咱们只能本人做 JSON.stringify() 和 JSON.parse() 的操作,而且没方法针对 JSON 内的数据进行查问操作,所有的操作必须读取进去 parse 之后进行,十分的麻烦。原生的 JSON 数据类型反对之后,咱们就能够间接对 JSON 进行数据查问和批改等操作了,较之前会不便十分多。 为了不便演示我先创立一个 user 表,其中 info 字段用来存储用户的根底信息。要将字段定义成 JSON 类型数据非常简单,间接字段名后接 JSON 即可。 CREATE TABLE user ( id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, info JSON);表创立胜利之后咱们就依照经典的 CRUD 数据操作来讲讲怎么进行 JSON 数据类型的操作。 <!--more--> 增加数据增加数据这块是比较简单,不过须要了解 MySQL 对 JSON 的存储实质上还是字符串的存储操作。只是当定义为 JSON 类型之后外部会对数据再进行一些索引的创立不便后续的操作而已。所以增加 JSON 数据的时候须要应用字符串包装。 mysql> INSERT INTO user (`name`, `info`) VALUES('lilei', '{"sex": "male", "age": 18, "hobby": ["basketball", "football"], "score": [85, 90, 100]}');Query OK, 1 row affected (0.00 sec)除了本人拼 JSON 之外,你还能够调用 MySQL 的 JSON 创立函数进行创立。 ...

September 14, 2020 · 5 min · jiezi

WEB-安全漏洞之目录遍历

什么是目录遍历第一次接触到目录遍历漏洞还是在 ThinkJS 2 的时候。代码如下图,目的是当用户访问的 URL 是静态资源的时候返回静态资源的地址。其中 pathname 就是用户访问的 URL 中的路径,我们发现代码中只是简单的解码之后就在22行将其与资源目录做了拼接,这就是非常明显的目录遍历漏洞了。 为什么这么说呢?假设用户访问的 URL 是 http://xxx.com/../../../xxx.jpg 的话最终返回的文件地址就会变成 think.RESOURCE_PATH 的上三层目录中的文件了。而这种利用网站的安全缺陷来列出服务器目录或者文件的方式就成为目录遍历漏洞(Directory traversal),也称之为路径遍历漏洞(英文:Path traversal)。 目录遍历在英文世界里又名../ 攻击(Dot dot slash attack)、目录攀登(Directory climbing)及回溯(Backtracking)。其部分攻击手段也可划分为规范化攻击(Canonicalization attack)。via: wikipedia目录遍历的危害目录遍历最大的危害是能够让任意用户访问系统的敏感文件,继而攻陷整个服务器。例如获取linux下的/etc/passwd文件后可能会破解出root用户的密码等。 防御方法可以看到大部分情况下问题的关键就是 ../ 目录跳转符,所以防御的第一要务就是它进行过滤。除了过滤之外,还可以针对最终的文件路径进行判断,确保请求文件完整目录后的头N个字符与文档根目录完全相同,如果相同则返回内容,否则则可能是攻击地址不予返回。 回到文章开头说的那个代码问题,最终就是通过上述方法修复的,对最终的文件地址进行规范化后判断开头是否包含 RESOURCE_PATH 目录,如果不包含则返回空。

August 28, 2019 · 1 min · jiezi

基于-ThinkJS-的-WebSocket-通信详解

基于 ThinkJS 的 WebSocket 通信详解 前言我们的项目是基于 ThinkJS + Vue 开发的,最近实现了一个多端实时同步数据的功能,所以想写一篇文章来介绍下如何在 ThinkJS 的项目中利用 WebSocket 实现多端的实时通信。ThinkJS 是基于 Koa 2 开发的企业级 Node.js 服务端框架,文章中会从零开始实现一个简单的聊天室,希望读者们能有所收获。 WebSocketWebSocket 是 HTML5 中提出的一种协议。它的出现是为了解决客户端和服务端的实时通信问题。在 WebSocket 出现之前,如果想实现实时消息传递一般有两种方式: 客户端通过轮询不停的向服务端发送请求,如果有新消息客户端进行更新。这种方式的缺点很明显,客户端需要不停向服务器发送请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多带宽资源HTTP 长连接,客户端通过 HTTP 请求连接到服务端后, 底层的 TCP 连接不会马上断开,后续的信息还是可以通过同一个连接来传输。这种方式有一个问题是每个连接会占用服务端资源,在收到消息后连接断开,就需要重新发送请求。如此循环往复。可以看到,这两种实现方式的本质还是客户端向服务端“Pull”的过程,并没有一个服务端主动“Push”到客户端的方式,所有的方式都是依赖客户端先发起请求。为了满足两方的实时通信, WebSocket 应运而生。 WebSocket 协议首先,WebSocket 是基于 HTTP 协议的,或者说借用了 HTTP 协议来完成连接的握手部分。其次,WebSocket 是一个持久化协议,相对于 HTTP 这种非持久的协议来说,一个 HTTP 请求在收到服务端回复后会直接断开连接,下次获取消息需要重新发送 HTTP 请求,而 WebSocket 在连接成功后可以保持连接状态。下图应该能体现两者的关系: 在发起 WebSocket 请求时需要先通过 HTTP 请求告诉服务端需求将协议升级为 WebSocket。 浏览器先发送请求: GET / HTTP/1.1Host: localhost:8080Origin: [url=http://127.0.0.1:3000]http://127.0.0.1:3000[/url]Connection: UpgradeUpgrade: WebSocketSec-WebSocket-Version: 13Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==服务端回应请求: ...

June 12, 2019 · 4 min · jiezi

Whats-New-in-JavaScript

前几天 Google IO 上 V8 团队为我们分享了《What's New in JavaScript》主题,分享的语速很慢推荐大家可以都去听听就当锻炼下听力了。看完之后我整理了一个文字版帮助大家快速了解分享内容,嘉宾主要是分享了以下几点: JS 解析快了 2 倍async 执行快了 11 倍平均减少了 20% 的内存使用class fileds 可以直接在 class 中初始化变量不用写在 constructor 里私有变量前缀string.matchAll 用来做正则多次匹配numeric seperator 允许我们在写数字的时候使用 _ 作为分隔符提高可读性bigint 新的大数字类型支持Intl.NumberFormat 本地化格式化数字显示Array.prototype.flat(), Array.prototype.flatMap() 多层数组打平方法Object.entries() 和 Object.fromEntries() 快速对对象进行数组操作globalThis 无环境依赖的全局 this 支持Array.prototype.sort() 的排序结果稳定输出Intl.RelativeTimeFormat(), Intl.DateTimeFormat() 本地化显示时间Intl.ListFormat() 本地化显示多个名词列表Intl.locale() 提供某一本地化语言的各种常量查询顶级 await 无需写 async 的支持Promise.allSettled() 和 Promise.any() 的增加丰富 Promise 场景WeakRef 类型用来做部分变量弱引用减少内存泄露Async 执行比之前快了11倍开场就用 11x faster 数字把大家惊到了,也有很多同学好奇到底是怎么做到的。其实这个优化并不是最近做的,去年11月的时候 V8 团队就发了一篇文章 《Faster async functions and promises》,这里面就非常详尽的讲述了如何让 async/await 优化到这个速度的,其主要归功于以下三点: ...

May 11, 2019 · 3 min · jiezi

ThinkJS关联模型实践

在数据库设计特别是关系型数据库设计中,我们的各个表之间都会存在各种关联关系。在传统行业中,使用人数有限且可控的情况下,我们可以使用外键来进行关联,降低开发成本,借助数据库产品自身的触发器可以实现表与关联表之间的数据一致性和更新。但是在 web 开发中,却不太适合使用外键。因为在并发量比较大的情况下,数据库很容易成为性能瓶颈,受IO能力限制,且不能轻易地水平扩展,并且程序中会有诸多限制。所以在 web 开发中,对于各个数据表之间的关联关系一般都在应用中实现。在 ThinkJS 中,关联模型就可以很好的解决这个问题。下面我们来学习一下在 ThinkJS 中关联模型的应用。<!–more–>场景模拟我们以最常见的学生、班级、社团之间的关系来模拟一下场景。创建班级表CREATE TABLE thinkjs_class ( id int(10) NOT NULL, name varchar(50) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;创建学生表CREATE TABLE thinkjs_student ( id int(10) NOT NULL, class_id int(10) NOT NULL, name varchar(20) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;创建社团表CREATE TABLE thinkjs_club ( id int(10) NOT NULL, name varchar(50) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;然后我们按照官网文档关联模型一一讲起,如果不熟悉官网文档建议先看一遍文档。一对一这个很好理解,很多时候一个表内容太多我们都会将其拆分为两个表,一个主表用来存放使用频率较高的数据,一个附表用来存放使用频率较低的数据。我们可以对学生表创建一个附表,用来存放学生个人信息以便我们进行测试。CREATE TABLE thinkjs_student_info ( id int(10) NOT NULL, student_id int(10) NOT NULL, sex varchar(10) NOT NULL, age int(2) UNSIGNED NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;相对于主表来说,外键即是 student_id ,这样按照规范的命名我们直接在 student 模型文件中定义一下关联关系即可。// src/model/student.jsmodule.exports = class extends think.Model { get relation() { return { student_info: think.Model.HAS_ONE }; }}然后我们执行一次查询// src/controller/student.jsmodule.exports = class extends think.Controller { async indexAction() { const student=await this.model(‘student’).where({id:1}).find(); return this.success(student); }}即可得到主表与关联附表的数据{ “student”: { “id”: 1, “class_id”: 1, “name”: “王小明”, “student_info”: { “id”: 1, “student_id”: 1, “sex”: “男”, “age”: 13 } }}查看控制台,我们会发现执行了两次查询[2018-08-27T23:06:33.760] [41493] [INFO] - SQL: SELECT * FROM thinkjs_student WHERE ( id = 1 ) LIMIT 1, Time: 12ms[2018-08-27T23:06:33.764] [41493] [INFO] - SQL: SELECT * FROM thinkjs_student_info WHERE ( student_id = 1 ), Time: 2ms第二次查询就是 ThinkJS 中的模型功能自动帮我们完成的。如果我们希望修改一下查询结果关联数据的 key,或者我们的表名、外键名没有按照规范创建。那么我们稍微修改一下关联关系,即可自定义这些数据。// src/model/student.jsmodule.exports = class extends think.Model { get relation() { return { info:{ type:think.Model.HAS_ONE, model:‘student_info’, fKey:‘student_id’ } } }}再次执行查询,会发现返回数据中关联表的数据的 key,已经变成了 info。当然除了配置外键、模型名这里还可以配置查询条件、排序规则,甚至分页等。具体可以参考[model.relation](https://thinkjs.org/zh-cn/doc…。一对一(属于)说完第一种一对一关系,我们来说第二种一对一关系。上面的一对一关系是我们期望查询主表后得到关联表的数据。也就是主表的主键thinkjs_student.id,是附表的外键thinkjs_student_info.student_id。那么我们如何通过外键查找到另外一张表的数据呢?这就是另外一种一对一关系了。比如学生与班级的关系,从上面我们创建的表可以看到,学生表中我们通过thinkjs_student.class_id来关联thinkjs_class.id,我们在student模型中设置一下关联关系// src/model/student.jsmodule.exports = class extends think.Model { get relation() { return { class: think.Model.BELONG_TO } }}查询后即可得到相关关联数据{ “student”: { “id”: 1, “class_id”: 1, “name”: “王小明”, “class”: { “id”: 1, “name”: “三年二班” } }}同样,我们也可以自定义数据的 key,以及关联表的表名、查询条件等等。一对多一对多的关系也很好理解,一个班级下面有多个学生,如果我们查询班级的时候,想把关联的学生信息也查出来,这时候班级与学生的关系就是一对多关系。这时候设置模型关系就要在 class 模型中设置了// src/model/class.jsmodule.exports = class extends think.Model { get relation() { return { student:think.Model.HAS_MANY } }}即可得到关联学生数据{ “id”: 1, “name”: “三年二班”, “student”: [ { “id”: 1, “class_id”: 1, “name”: “王小明” }, { “id”: 2, “class_id”: 1, “name”: “陈二狗” } ]}当然我们也可以通过配置参数来达到自定义查询// src/model/class.jsmodule.exports = class extends think.Model { get relation() { return { list:{ type:think.Model.HAS_MANY, model:‘student’, fKey: ‘class_id’, where:‘id>0’, field:‘id,name’, limit:10 } } }}设置完之后我们测试一下,会发现页面一直正在加载,打开控制台会发现一直在循环执行几条sql语句,这是为什么呢?因为上面的一对一例子,我们是用 student 和 class 做了 BELONG_TO 的关联,而这里我们又拿 class 和 student 做了 HAS_MANY 的关联,这样就陷入了死循环。我们通过官网文档可以看到,有个 relation 可以解决这个问题。所以我们把上面的 student 模型中的 BELONG_TO 关联修改一下// src/model/student.jsmodule.exports = class extends think.Model { get relation() { return { class: { type:think.Model.BELONG_TO, relation:false } } }}这样,即可在正常处理 class 模型的一对多关系了。如果我们想要在 student 模型中继续使用 BELONG_TO 来得到关联表数据,只需要在代码中重新启用一下即可// src/controller/student.jsmodule.exports = class extends think.Controller { async relationAction(){ let student=await this.model(‘student’).setRelation(‘class’).where({id:2}).find(); return this.success(student); }}官网文档 model.setRelation(name, value) 有更多关于临时开启或关闭关联关系的使用方法。多对多前面的一对一、一对多还算很容易理解,多对多就有点绕了。想象一下,每个学生可以加入很多社团,而社团同样由很多学生组成。社团与学生的关系,就是一个多对多的关系。这种情况下,两张表已经无法完成这个关联关系了,需要增加一个中间表来处理关联关系CREATE TABLE thinkjs_student_club ( id int(10) NOT NULL, student_id int(10) NOT NULL, club_id int(10) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;根据文档中多对多关系的介绍,当我们在 student 模型中关联 club 时,rModel 为中间表,rfKey 就是 club_id 了// src/model/student.jsmodule.exports = class extends think.Model { get relation() { return { club:{ type: think.Model.MANY_TO_MANY, rModel: ‘student_club’, rfKey: ‘club_id’ } } }}如果我们想在 club 模型中关联 student 的数据,只需要把 rfKey 改为 student_id 即可。当然,多对多也会遇到循环关联问题。我们只需要把其中一个模型设置 relation:false 即可。关联循环在上面我们多次提到关联循环问题,我们来试着从代码执行流程来理解这个 feature。在 think-model 的第30行 看到,在构造方法中,会有一个 Relation 实例放到 this[RELATION]。RELATION 是由 Symbol 函数生成的一个Symbol类型的独一无二的值,在这里应该是用来实现私有属性的作用。然后略过 new Relation() 做了什么,来看一下模型中 select 这个最终查询的方法来看一下,在第576行发现在执行了const data = await this.db().select(options);查询之后,又调用了一个 this.afterFind 方法。而this.afterFind方法又调用了上面提到的 Relation 实例的 afterFind 方法 return this[RELATION].afterFind(data); 。看到这里我们通过命名几乎已经知道了大概流程:就是在模型正常的查询之后,又来处理关联模型的查询。我们继续追踪代码,来看一下 Relation 的 afterFind 方法又调用了 this.getRelationData 。this.getRelationData则开始解析我们在模型中设置的 relation 属性,通过循环来调用 parseItemRelation 得到一个 Promise 对象,最终通过 await Promise.all(promises);来全部执行。而parseItemRelation方法则通过调用 this.getRelationInstance 来获得一个实例,并且执行实例的 getRelationData 方法,并返回。所以上面 this.getRelationData 方法中 Promise.all 执行的其实都是 this.getRelationInstance 生成实例的 getRelationData 方法。getRelationInstance的作用就是,解析我们设置的模型关联关系,来生成对应的实例。然后我们可以看一下对应的 getRelationData 方法,最终又执行了模型的select方法,形成递归闭环。从描述看起来似乎很复杂,其实实现的很简单且精巧。在模型的查询方法之后,分析模型关联以后再次调用查询方法。这样无论有多少个模型互相关联都可以查询出来。唯一要注意的就是上面提到的互相关联问题,如果我们的模型存在互相关联问题,可以通过 relation:false 来关闭。后记通过上面的实践可以发现,ThinkJS 的关联模型实现的精巧且强大,通过简单的配置,即可实现复杂的关联。而且通过 setRelation 方法动态的开启和关闭模型关联查询,保证了灵活性。只要我们在数据库设计时理解关联关系,并且设计合理,即可节省我们大量的数据库查询工作。PS:以上代码放在https://github.com/lscho/thinkjs_model_demo。本文首发于知乎 ThinkJS 专栏 [ThinkJS关联模型实践](https://zhuanlan.zhihu.com/p/…。 ...

February 19, 2019 · 3 min · jiezi

如何解析你,Excel的Date呀

简单的背景介绍不久前,我们接了一个自己做前端后端产品的活,从此过上了可怜巴巴敲代码开开心心收获知识的日子呢。那是一个平平无奇的周一下午用户小姐姐在群里说,系统筛选工卡有效期不好使。(系统:不不不,不是我的锅我看了一下数据库,发现,我们原定的有效期格式是这样的整整齐齐。数据库中当时的数据是这样的甚至是这样的看到这种情况,我觉得肯定是输入的时候输的不太对(年轻…于是我决定从Excel下手小姐姐们的操作流程是先用我们的系统导出一份Excel,编辑之后再导入系统的,那只要我把这工卡一列的格式限制为日期,就一定可以统一格式的,嗯。我们项目使用了js-xlsx处理表格的导入导出,下面是导出Excel的伪代码:import * as XLSX from ‘xlsx’;const xlsxMineType = ‘application/vnd.openxmlformats-officedocument.spreadsheetml.sheet’const data = 数据.map((s: any) => ({ID: s.id,工卡有效期: s.card_expired,……}));const sheet = XLSX.utils.json_to_sheet(data);const wb = XLSX.utils.book_new();XLSX.utils.book_append_sheet(wb, sheet, ‘员工信息表’);const wbbuf = XLSX.write(wb, { type: ‘base64’});this.success({ name: “员工信息表.xlsx”, data: wbbuf, type:xlsxMineType });通过 json_to_sheet 可以拿到包含单元格信息的对象{ A2: { t: ’n’, v: 3776 }, B2: { t: ’s’, v: ‘2019-04-01’ }, A3: { t: ’n’, v: 3831 }, B3: { t: ’s’, v: ‘2019-04-01’ }, A1: { t: ’s’, v: ‘ID’ }, B1: { t: ’s’, v: ‘工卡有效期’ }, ‘!ref’: ‘A1:B3’ }对象中以单元格位置作为key,每个单元格的值(v)、类型(t)等等属性作为value。其中单元格的类型支持:b Boolean, n Number, e error, s String, d Date看起来Date类型十分符合上面的要求,就尝试了一下:const sheet = XLSX.utils.json_to_sheet(data);// 筛选出除表头的工卡列Object.keys(sheet).filter(item => /^B/.test(item) && item !== “B1”).forEach(key => { sheet[key].t = “d”;})然鹅,如果工卡有效期本来就为空,这时候导出,打开Excel会报错,并且空的位置会变成NaN翻阅了各种中英文文档、Issue,导出一百多个员工信息表之后,我发现Excel真的很奇妙,或许应该在js上来格式化导入的数据,而不是限制单元格的类型。如果不控制单元格类型的话,那么当管理员输入日期的时候,这个单元格可能是:文本、常规、日期、自定义类型,所以只要保证不管单元格是什么格式,程序都能拿到正确的数据就好了。当管理员使的工卡有效期的单元格类型是文本或者常规的时候,则比较简单,程序可以按预期解析出来一个相应的字符串,用moment解析一下,就可以获得想要的格式的数据了。那么当有效期单元格的类型是日期和自定义的时候,我们拿到的数据是像下图一样这也就是之前数据库中奇怪的数字的由来,这个数字的意义,其实是当前日期距离1900年1月0日的天数。还需要注意的是,Excel中有个bug:它以为1900年是闰年,所以我们拿到的天数都会多了一天,因为转换之前还需要先进行减一操作…item.工卡有效期 = new Date(1900, 0, expried - 1)这样之后就可以拿到正确的日期啦。咕叽。 ...

December 6, 2018 · 1 min · jiezi