简介
这个个人博客网站最初制作的目的就是练习使用thinkJs,这一篇就主要讲一下thinkJs的一些特性和注意事项。涉及到了文件上传,thinkJs的插件机制,model层建立以及CURD的编写方式等。本项目github地址。目thinkJs端主要参考了知乎上大神Ischo的文章,链接。
thinkJs model层写法
这里主要讲两个部分,一是表对应的js文件,二是CRUD写法。项目表结构比较简单,一共八个表,包含多对一,一对多,多对多关系。主要的几个表,都对应着model文件夹下的js文件,表关系也在这个js里维护。这里我们以model/content.js为例讲一哈:
module.exports = class extends think.Model { // 模型关联 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" }, comment: { type: think.Model.HAS_MANY, key: "id", fKey: "content_id", where: "status=99", order: "create_time desc" }, user: { type: think.Model.BELONG_TO, model: "user", key: "user_id", fKey: "id", field: "id,username,email,qq,github,weibo,zhihu" } }; } // 添加文章 async insert(data) { const tags = data.tag; data = this.parseContent(data); delete data.tag; const id = await this.add(data); const relation = []; tags.forEach(val => { relation.push({ content_id: id, meta_id: val }); }); think.model("relationship").addMany(relation); // 更新文章数量 this.updateCount(data.category_id, tags); return id; }}复制代码
这里代码没有截全,完整代码看github。
我们看到这个对象分为两部分,一个是get relation写的表映射关系。可以看到content表与meta表存在一对一关系(type: think.Model.BELONG_TO),这里key:category_id是content表里的字段,即外键,fkey:id是对应的meta表里的字段。查询时,会封装层user.category对象,对象属性就是field 定义的id,name,slug,description,count。content 与user也存在多对多关系(type: think.Model.MANY_TO_MANY),rfModel是多对多关系下,对应的关联关系模型名,默认值为二个模型名的组合,rfKey是多对多关系下,关系表对应的 key。
另一个是Model里的方法,相当于自定义的model方法,比如这里定义的insert,就可以在controller里通过this.model('content').insert()调用。
thinkJS的CRUD操作,不是直接写sql,而是在sql基础上封装一层,通过调用model的方法来操作。think.Model 基类提供了丰富的方法进行 CRUD 操作,具体如下:
查询数据模型提供了多种方法来查询数据,如:find 查询单条数据select 查询多条数据count 查询总条数countSelect 分页查询数据max 查询字段的最大值avg 查询字段的平均值min 查询字段的最小值sum 对字段值进行求和getField 查询指定字段的值同时模型支持通过下面的方法指定 SQL 语句中的特定条件,如:where 指定 SQL 语句中的 where 条件limit / page 指定 SQL 语句中的 limitfield / fieldReverse 指定 SQL 语句中的 fieldorder 指定 SQL 语句中的 ordergroup 指定 SQL 语句中的 groupjoin 指定 SQL 语句中的 joinunion 指定 SQL 语句中的 unionhaving 指定 SQL 语句中的 havingcache 设置查询缓存添加数据模型提供了下列的方法来添加数据:add 添加单条数据thenAdd where 条件不存在时添加addMany 添加多条数据selectAdd 添加子查询的结果数据更新数据模型提供了下列的方法来更新数据:update 更新单条数据updateMany 更新多条数据thenUpdate 条件式更新increment 字段增加值decrement 字段减少值删除数据模型提供了下列的方法来删除数据:delete 删除数据手动执行 SQL 语句有时候模型包装的方法不能满足所有的情况,这时候需要手工指定 SQL 语句,可以通过下面的方法进行:query 手写 SQL 语句查询execute 手写 SQL 语句执行复制代码
比如我们要查询content表数据,在Controller里通过thin.model('content').where(param).select()来查询。
thinkJs的Model层与之前用过的java的数据层框架hibernate比较相似,都是基于面向对象的思想对sql进行封装,表与Model(实体类),通过model方法进行CRUD操作,特别省sql。
插件机制的实现
参考的博主实现的插件机制还是很好用的,这里我就拿了过来。插件机制可以说是自定义的钩子函数。首先在src新建service文件夹,新建js文件(以cache.js为例)
module.exports = class extends think.Service { static registerHook() { return { content: ["contentCreate", "contentUpdate", "contentDelete"] }; } /** * 更新内容缓存 * @param {[type]} data [description] * @return {[type]} [description] */ content(data) { think.cache("recent_content", null); }};复制代码
registerHook里content对应的数组表示钩子函数的调用名,具体调用的是下面的content方法。在controller里这么调用
await this.hook("contentUpdate", data);复制代码
钩子函数的注册这里放到了worker进程里,thinkJs运行流程具体的可以看看官网 。work.js代码如下:
think.beforeStartServer(async () => { 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);});复制代码
这里将service里定义的method遍历取出,按一定格式保存并存放到数组,最后放到think.config里面,项目启动后这些过程就已经执行了。
think.Controller本身没有hook方法,这里需要在extend里面加上controller.js,代码如下:
module.exports = { /** * 执行hook * @param {[type]} name [description] * @param {...[type]} args [description] * @return {[type]} [description] */ async hook(name, ...args) { const { hooks } = think.config(); const hookFuncs = hooks[name]; if (!think.isArray(hookFuncs)) { return; } for (const { service, method } of hookFuncs) { await service[method](...args); } }};复制代码
这样自定义钩子函数就实现了,一些通用的后置方法就可以直接共用一个了。
路由
thinkJs路由写在config/router.js里,具体代码如下:
module.exports = [ // RESTFUL [/\/api\/(\w+)(?:\/(.*))?/, 'api/:1?id=:2', 'rest'], [/\/font\/(\w+)\/(\w+)/, 'fontend/:1/:2'], ['/:category/:slug', 'content/detail'], ['/:category/:slug/comment', 'content/comment']];复制代码
里面的数组的第一个元素是匹配url的表达式,第二个元素是分配的资源,如果是采用RESTFUL规范定义的接口,第三个元素要写作'rest'。本项目的后台接口基本都是采用RESTFUL规范,具体路由的详细讲解可以。
部署
项目线上部署采用PM2管理node进程,部署时把src,view,www,pm2.json,production.js放到服务器上。安装好pm2后运行
pm2 start pm2.json复制代码
注意pm2.json里需要修改cwd为服务器上你项目的目录。本项目前后端是一个服务,不存在所以没有用nginx代理。thinkJs部署相关可以。