编者注:ThinkJS 作为一款 Node.js 高性能企业级 Web 框架,收到了越来越多的用户的喜爱。今天我们请来了 ThinkJS 用户 @lscho 同学为我们分享他基于 ThinkJS 开发一款类 CMS 的博客系统的心得。下面就赶紧让我们来看看 ThinkJS 和 Vue.js 能擦除怎样的火花吧!

前言

前段时间利用闲暇时间把博客重写了一遍,除了实现博客基本的文章系统、评论系统外还完成了一个简单的插件系统。博客采用 ThinkJS 完成了服务端功能,Vue.js 完成了前后端分离的后台管理功能,而博客前台部分考虑到搜索引擎的问题,还是放在了服务端做渲染。在这里记录一下主要实现的功能与遇到的问题。

功能分析

一个完整的博客系统大概需要用户登录、文章管理、标签、分类、评论、自定义配置等,根据这些功能,初步预计需要这些表:

  1. 文章表
  2. 评论表
  3. 文章分类表
  4. 标签表
  5. 文章与分类映射表(一对多)
  6. 文章与标签映射表(多对多)
  7. 配置表
  8. 用户表

共8张表,然后参考 Typecho 的设计,再结合 ThinkJS 的模型关联功能,做了一下精简,分类表与标签表合并,两个映射表合并,最终得到以下6张表设计方案。

内容表 - content
关系表 - relationship
项目表 - meta
评论表 - comment
配置表 - config
用户表 - user
复制代码

ThinkJS 的模型关联功能可以很方便的处理这种表结构的分类和标签关系,比如我们在内容模型即 src/model/content.js 写如下关联关系,即可在使用模型查询文章时将分类和标签数据查到,而不用手工执行多次查询。

get relation() {return {category: {type: think.Model.BELONG_TO,model: 'meta',key: 'category_id',fKey: 'id',field: 'id,name,slug,description,count'},tag: {type: think.Model.MANY_TO_MANY,model: 'meta',rModel: 'relationship',rfKey: 'meta_id',key: 'id',fKey: 'content_id',field: 'id,name,slug,description,count'}};
}
复制代码

接口鉴权

表结构设计好了之后剩下就要开始开发接口了。接口方面因为使用了 RESTful 接口规范,所以基本上就是 CURD 功能,具体的就不多表了,这里我们主要说一下如何对所有接口进行权限验证。

因为后台部分是前后端分离的,所以鉴权部分使用了 JWT 鉴权。JWT 之前大概了解过,之前自己也实现过类似的功能,搜索了一下,找到了 node-jsonwebtoken 这个包,使用起来很简单,主要就是加密和解密两个功能一番折腾之后成功运行。

偶然去 ThinkJS 仓库看了一下,竟然有发现了 think-session-jwt 这个插件,也是基于 node-jsonwebtoken 的。这个就更好用了,配置完之后直接用 ThinkJS 的 ctx.session 方法就可以生成和验证。配置的时候需要注意一下 tokenType 这个参数,他决定了如何获取 token ,我这里用的是 header ,也就是说后面会从每个请求的 header 中找 token,key 值为配置的 tokenName。

后端权限认证

因为 API 接口遵循 RESTful 风格,而且也没有复杂的角色权限概念,所以简单的对非 GET 类型的请求,都验证 token 是否有效,ThinkJS 的控制器提供了前置操作 __before。在src/controller/rest.js中做一下逻辑判断,通过的才会继续执行。

async __before() {this.userInfo = await this.session('userInfo').catch(_ => ({}));const isAllowedMethod = this.isMethod('GET');const isAllowedResource = this.resource === 'token';const isLogin = !think.isEmpty(this.userInfo);if(!isAllowedMethod && !isAllowedResource && !isLogin) {return this.ctx.throw(401, '请登录后操作');}
}
复制代码

这里遇到一个问题,就是当 token 错误时,node-jsonwebtoken 会抛出一个异常,所以这里用了 try catch 捕获处理一下。

前端身份失效检测

为了安全起见,我们的 token 一般设置的都有效期,所以有三种情况需要我们进行处理.

  1. token 不存在,这种很好处理,直接在路由的前置操作中判断是否存在,存在则放行,不存在则转向登录界面
beforeEnter:(to, from, next)=>{if(!localStorage.getItem('token')){next({ path: '/login' });}else{next();}
}
复制代码

2.token 错误。这种需要后端检测之后才能知道该 token 是否有效。这里服务端检测失效之后会返回 401 状态码以便前端识别。我们在 axios 的请求响应拦截器中进行判断即可,因为 4XX 的状态码会抛出异常,所以代码如下

axios.interceptors.response.use(data => {//这里可以对成功的请求进行各种处理return data;
},error=>{if (error.response) {switch (error.response.status) {case 401:store.commit("clearToken");router.replace("/login");break;}}return Promise.reject(error.response.data)
})
复制代码

3.token 过期。这种情况也可以不用处理,因为我们在 axios 的响应拦截器中已经判断过,如果返回状态码为401的话也会跳转到登录页面。但是在实际使用中却发现体验不好的地方,因为客户端中 token 是保存在 localStorage 中,不会自动清理,所以我们在 token 过期之后直接打开后台的话,界面会先显示后台,然后请求返回401,页面才跳转到登录界面。包括阿里云控制台、七牛云控制台等用了类似鉴权方式其实都存在这种现象,对于强迫症来说可能有点不爽。这种情况也是可以解决掉的。

我们先来看一下 JWT 的相关知识,JWT 包含了使用.分隔的三部分: Header 头部,Payload 负载,Signature 签名,其结构看起来是这样的 Header.Payload.Signature。抛开Header、Signature不去介绍,Payload 其实是一段明文数据经过 base64 转码之后得到的。而其中就包含了我们设置的信息,一般都会有过期时间。在路由前置操作中进行判断即可得知token是否过期,这样就可以避免页面两次跳转的问题。我们对 Payload 解码之后会得到:

{"userInfo":{"id":1},"iat":1534065923,"exp":1534109123}
复制代码

可以看到 exp 就是过期时间,对这个时间进行判断,即可得知是否过期.

let tokenArray = token.split('.')
if (tokenArray.length !== 3) {next('/login')
}
let payload = Base64.decode(tokenArray[1])
if (Date.now() > payload.exp * 1000) {next('/login')
}
复制代码

另外这里顺便提一下,因为 Payload 是明文数据,所以千万不要在 jwt 中保存敏感数据

插件机制

除了正常的增删改查功能之外,在我的博客系统中我还实现了一个简单的插件机制,方便我对代码进行解耦,提高代码灵活性。举个例子,有时候我们会针对某个点扩展出很多功能,比如在用户评论之后,我们可能需要更新缓存、邮件通知、文章评论数量更新等等,我们可能会写下如下代码。

let insertId = await model.add(data);
if(insertId){await this.updateCache();await this.push();...
}
复制代码

后面一旦这些方法发生改变,修改起来就太麻烦了。用过 php 博客系统的同学应该都知道,插件机制强大又方便,所以我决定实现一个插件功能。

期望功能是在程序某个点留下标识(一般都称为钩子),即可对这个点进行扩展,如下。

let insertId = await model.add(data);
if(insertId){await this.hook('commentCreate',data);
}
复制代码

因为程序是自用的,只是方便自己以后扩展功能,只需要实现核心功能即可。所以并没有增加某个目录作为插件目录,而是放在 src/service/ 下面,符合 ThinkJS 的文件结构,然后做了一个约定。只要在 src/service/ 下面的 js 文件,并且有 registerHook 方法,那么就可以作为插件被调用。如 src/service/email.js 这个文件用来处理邮件通知,那么给他增加一个方法:

static registerHook() {return {'comment': ['commentCreate']};
}
复制代码

就表示在 commentCreate 这个功能点下,会调用 src/service/email.jscomment方法。

然后我们扩展一下 controller ,增加一个 hook 方法,用来根据不同的标识调用对应的插件。我们可以遍历一下 src/service/ 找到对应的文件,然后调用其方法即可。但是考虑到文件遍历可能出现的异常和性能的损耗,我把这部分功能转移到了服务启动时即检测插件并保存到配置中。看一下 ThinkJS 的运行流程,可以放到 src/bootstrap/worker.js 这个文件中。大致代码如下。

const hooks = [];for (const Service of Object.values(think.app.services)) {const isHookService = think.isFunction(Service.registerHook);if (!isHookService) {continue;}const service = new Service();const serviceHooks = Service.registerHook();for (const hookFuncName in serviceHooks) {if (!think.isFunction(service[hookFuncName])) {continue;}let funcForHooks = serviceHooks[hookFuncName];if (think.isString(funcForHooks)) {funcForHooks = [funcForHooks];}if (!think.isArray(funcForHooks)) {continue;}for (const hookName of funcForHooks) {if (!hooks[hookName]) {hooks[hookName] = [];}hooks[hookName].push({ service, method: hookFuncName });}}
}
think.config('hooks', hooks);
复制代码

然后在 src/extend/controller.js 中的 hook 中对插件列表遍历并依次执行即可。

//src/extend/controller.js
module.exports = {async hook(...args) {const { hooks } = think.config();const hookFuncs = hooks[name];if (!think.isArray(hookFuncs)) {return;}for(const {service, method} of hookFuncs) {await service[method](...args);};}
}
复制代码

至此,简单的插件功能完成。

当然如果想实现像 Wordpress 、Typecho 那种完整的插件功能也很简单。后台增加一个插件管理,可以进行上传,然后给插件增加一个激活函数和一个禁用函数。点击插件管理中的激活与禁用就分别调用这两个方法,可以保存默认配置等等。如果插件需要创建数据表,可以在激活函数中执行相关 sql 语句。激活完成后重启进程让代码生效即可。重启功能可以参考子进程如何通知主进程重启服务?

其他

项目的开发过程中或多或少也存在一些问题,这里我也分享一下我碰到的一些问题,希望能帮助到大家。

编辑器及文件上传

markdown 编辑器用了 mavonEditor 配置很方便,不多说,主要说一下文件上传遇到的一个问题。

前端代码

<mavon-editor ref=md @imgAdd="imgAdd" class="editor" v-model="formItem.content"></mavon-editor>
复制代码
imgAdd(pos, $file){var formdata = new FormData();formdata.append('image', $file); image.upload(formdata).then(res=>{if(res.errno==0&&res.data.url){this.$refs.md.$img2Url(pos, res.data.url);}});
}
复制代码

后端处理

const file = this.file('image');
const extname=path.extname(file.name);
const filename = path.basename(file.path);
const basename=think.md5(filename)+extname;
const savepath = '/upload/'+basename;
const filepath = path.join(think.ROOT_PATH, "www"+savepath);
think.mkdir(path.dirname(filepath));
await rename(file.path, filepath);
复制代码

最初使用了 ThinkJS 官网的上传示例代码,使用 rename 进行文件转移,而在 windows 下临时目录可能和项目目录不在同一盘符下,进行移动的话就会抛出一个异常:Error: EXDEV, cross-device link not permitted,没有权限移动,这时候就只能先读文件,再写文件。所以这里也用了一个 try catch 来捕获异常,主要是因为 ThinkJS 会将上传的文件先放到临时目录中。关于跨盘 rename 的问题,在 github.com/nodejs/node… 找到了原因,大意是操作系统限制 rename 仅仅是重命名路径引用地址,并没有将数据移动过去,重命名不能跨文件系统操作,所以如果跨文件系统操作需要先复制、然后删除旧数据。

后来在群里聊天,@阿特 大佬提到,上传是 payload 这个中间件处理的, 可以对 payload 这个中间件设置指定临时目录为项目下的某个目录,这样就保证临时目录和项目目录在同一盘符下。

{handle: 'payload',options: {uploadDir: path.join(think.ROOT_PATH, 'runtime/data')}
}
复制代码

这样就可以直接使用 rename 来操作了。

iView 按需加载

因为 iView 默认是作为插件全部加载进来,所以打包出来的文件很大。需要调整为按需加载。按照www.iViewui.com/docs/guide/…搞定之后出现了一个问题,就是执行 npm run build 时会报一个错。ERROR in js/index.c26f6242.js? from UglifyJs 大概是这个样子,看了一下错误原因,大概是因为按需加载之后,是直接加载的 iView 模块下 src 的 js文件,里面采用的都是 ES6 语法,造成压缩失败。去 Issue 搜了一下,找到了解决方案 github.com/iView/iView…

部署

如果前后端不分离的话,用 webpack 将前端的入口页面 index.html 编译到 ThinkJS 后端项目的首页模版位置,然后把资源编译到后端项目资源文件夹下,对应路径设置好。这样就把前端项目整合进了后端项目,然后再按照 ThinkJS 部署方式来部署,也是可以的。

如果是前后端分离,作为两个项目部署的话,前端路由使用普通模式的话也很好处理,如果使用 history 模式,就要要将请求转发至 index.html 入口页面处理,跟有些 mvc 框架单入口是一个概念。这时候其实就是前端项目接管了路由。

location / {try_files $uri $uri/ /index.html;
}
复制代码

然后还要处理一下后端请求部分,如果不是同一域名,就要解决跨域问题。这里前后端使用同一个域名,针对 api 请求做一下反向代理即可。注意这部分要写在请求转发的上面。

set $node_port 8360;location ~ ^/api/ {proxy_pass http://127.0.0.1:$node_port$request_uri;}
复制代码

后端使用 pm2 守护进程即可。

后记

以上就是我整个项目的开发过程以及遇到的一些问题的总结,如果有什么疑问欢迎大家留言讨论。最后欢迎大家 Star 基于 ThinkJS + Vue 开发的博客系统。

使用 ThinkJS + Vue.js 开发博客系统相关推荐

  1. WEB安全之PHP安全开发 博客系统(二):前台js登陆验证、套用模板主体修改登陆和后台样式

    WEB安全之PHP安全开发 博客系统(二):前台js登陆验证.套用模板主体修改登陆和后台样式 前台验证 模板的套用 后台模板的套用 前台验证 做自动提交 点击登陆,自动提交 如果等于false,不会提 ...

  2. 基于SpringBoot和Vue的个人博客系统

    基于SpringBoot和Vue的个人博客系统 前言 ​ 本期项目分享一个漫威主题的炫酷博客系统,基于SpringBoot和Vue开发的前端分离项目.博客系统分为博客前台和博客后台两部分,游客可以访问 ...

  3. Node项目实战开发-博客系统

    Nodejs项目实战开发-博客系统(已完结) 个人博客系统 欢迎访问我的博客~ MaXiaoYu's Bolg 前言: 开发技术 技术 版本 Node ^14.3.0 ejs ^3.1.3 expre ...

  4. 微信小程序云开发博客系统源代码,让写博客像发朋友圈一样简单,含使用部署教程

    博客就两种:一是随笔,记录自己的成长历程,二是有目的的发文,例如搬运各种网赚文,我想大部分朋友做博客的初衷都是有一块自己的心灵净土,于是催生了wxapp-blog这款小程序. 完整代码下载地址:微信小 ...

  5. 一个 Vue + Node + MongoDB 博客系统

    源码 耗时半载(半个月)的大项目终于完成了.这是一个博客系统,使用 Vue 做前端框架,Node + express 做后端,数据库使用的是 MongoDB.实现了用户注册.用户登录.博客管理(文章的 ...

  6. 基于SpringBoot + Vue的个人博客系统12——使用vue-admin-template展示文章列表(后台管理)

    简介 前面我们实现了博客系统的前台展示页面,还有留言功能没有实现,实现留言功能无非就是在后端增加留言表,对留言进行增删改查.和文章表类似,这里就不在赘述. 既然作为一款动态博客,那么后台管理是必不可少 ...

  7. 推荐一个基于Springboot+Vue的开源博客系统

    简介 这是一个基于Springboot2.x,vue2.x的前后端分离的开源博客系统,提供 前端界面+管理界面+后台服务 的整套系统源码.响应式设计,手机.平板.PC,都有良好的视觉效果! 你可以拿它 ...

  8. php开发博客系统源码,php简单博客系统

    [实例简介] php简单博客系统,是基于php+mysql组合的简单系统,下来看看吧 [实例截图] [核心代码] webstar ├── web_star │   ├── code.php │   ├ ...

  9. Node.js +个人博客系统搭建设计方案

    目录 1.Node开发基础 1.1基础(服务器端开发) 1.2模块加载及第三方包 1.2.1 Node.js模块化开发 1.2.2 系统模块 1.2.3第三方模块(包) 1.2.4模块加载机制 2.请 ...

最新文章

  1. Linux用户环境变量
  2. ECshop在文章列表页调用文章简介
  3. 论文排版之公式居中、编号右对齐
  4. 安利一下这个群投票的小程序,比较好用
  5. 你应该要掌握的7种回归分析方法
  6. nginx 网络模型,cpu亲和等优点
  7. 以太坊2.0质押地址余额超过170万枚
  8. 回归的正则化模型:岭回归和Lasso回归(套索回归)
  9. Linux CFS调度系统----周期性调度器
  10. java简单入门教程_史上最快速最简单的java入门教程
  11. OpenJWeb2.61 Java Web应用快速开发平台技术白皮书
  12. 实数,有理数,无理数,自然数,整数的概念分别是什么?
  13. 拳皇97c语言编码,拳皇97金手指代码
  14. linux dd从磁盘读取文件命令
  15. coap python_Python coap
  16. 强化学习训练营-学习笔记
  17. 【USB笔记】USB 2.0 帧开始包Start-of-Frame Packets
  18. 示波器观察IIC通讯协议-STM32F4读写24C08EEPROM时序图-新人首更
  19. 天文基础浏览-盖亚计划
  20. python卡尔曼滤波_[转]python起步之卡尔曼滤波

热门文章

  1. Scala与Java交互
  2. CentOS下添加新硬盘并分区格式化
  3. python经典问题在stack overflow上的回答
  4. 恐怖的shell命令
  5. PX Deq: Execution Msg等待事件
  6. Zend Studio添加ThinkPHP代码提示方法
  7. WF4.0实战(四):博客申请流程
  8. jQuery 特殊选择器this
  9. 重新定位svn地址的方法(windows和linux),svn switch(sw)的帮助信息
  10. [转]Python 命令行参数和getopt模块详解