Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

我的 ThinkJS 之旅 - 从前端向后端迈进 #47

Open
ufologist opened this issue Dec 15, 2016 · 0 comments
Open

我的 ThinkJS 之旅 - 从前端向后端迈进 #47

ufologist opened this issue Dec 15, 2016 · 0 comments
Labels

Comments

@ufologist
Copy link
Member

ufologist commented Dec 15, 2016

简单介绍下 ThinkJS 是用来做什么的

ThinkJS 是奇舞团开源的一款 Node.js 框架

借助 Babel 编译, 可以大胆使用 ES6/7 特性开发 Web 项目, 是一款后端 MVC 全功能框架. 使用 async & await 让异步执行以同步的方式来书写, 这个 feel 倍爽...

与其他框架的对比

  • 与 express/koa 对比

    express/koa 是 2 个比较简单的框架,框架本身提供的功能比较简单,项目中需要借助大量的第三方插件才能完成项目的开发,所以灵活度比较高

  • 与 sails 对比

    sails 也是一个提供整套解决方案的 Node.js 框架,对数据库、REST API、安全方面也很多封装,使用起来比较方便。但 sails 对异步回调的问题还没有优化,还是使用 callback 的方式,给开发带来很大的不便,导致项目中无法较好的使用 ES6/7 特性。

如果你还是有点蒙, 那么提下与 ThinkJS 同类型(Node.js web application framework)的框架 Express, Koa, Sails, 你是不是就大概了解 ThinkJS 的作用了呢.

与 Express 和 Koa 相比, ThinkJS 更符合一个全功能的 Web 开发框架, 你可以使用 Express/Koa + xx 插件 + xx 模块 + ... 来达到和 ThinkJS 类似的功能, 但其中充满了诸多选择, 需要灵活搭配才能满足所有的需求, 也就是说由于经验的不足, 保不准你就掉坑里了. 而 ThinkJS 自带了 Web 开发的常用模块, 让你开箱即用, 省力省心.

ThinkJS 与 Sails 相比, 就差不多是同一个重量级别的框架了, 类似 Ruby on Rails.

我们列一下 ThinkJS 更多的特性

以提升我们学习使用 TA 的兴趣

  • 支持 ES6/7 特性

    可以直接在项目里使用 ES6/7(Generator Function, Class, Async & Await)等特性,借助 Babel 编译,可稳定运行在 Node.js 环境上

  • 支持丰富的数据库

    支持 Mysql、SQLite、MongoDB 等常见的数据库,提供了很多简单易用、高度封装的方法,自动防止 SQL 注入

  • REST API

    自动生成 REST API,而无需写任何的代码。也可以根据接口定制,隐藏部分数据和进行权限控制

  • 丰富的 Adapter

    快速切换 Cache、Store、Session、Template 等功能,而无需关心具体使用哪种方式

  • 支持 WebSocket

    支持 socket.io、SockJS 等常见的 WebSocket 客户端,而服务端代码始终保持一致

  • 命令行调用

    支持命令行方式调用 Action,方便执行定时任务

  • 自动更新

    开发模式下,文件修改后立即生效,无需重启 Node.js 服务

  • Hook & Middleware

    系统提供了大量的钩子和中间件,可以方便地对请求进行控制和修改, 还有更多插件包含 Middleware 和 Adapter

  • 支持多种项目环境

  • 丰富的路由机制

  • 详细的日志,如:请求日志、错误日志、性能日志

  • 支持国际化多主题

  • 支持自定义多种错误页面,如:400,404,500,503

  • 单元测试

看看 ThinkJS 的版本历程

  • 2.2.15 2016.12.01
  • 2.0.0 2015.10.30
  • 1.0.0 2014.09.22

让我们试用一把尝尝鲜

大概了解下使用 ThinkJS 开发一个 Web 项目, TA 能提供什么功能, 能给我们带来什么好处, 是不是提高了我们的工作效率.

安装和创建新项目的细节大家请参考官网创建项目的文档, 我这里只是列下重要的命令和注意事项.

npm install thinkjs@2 -g

thinkjs new thinkjs-demo
cd thinkjs-demo
npm install

npm start

注意事项

  • 需要 Node.js 的版本 >=0.12.0, 建议将 Node.js 版本升级到 4.2.1 或更高版本
  • 是要安装 [email protected] 的版本, 以下实践都是基于 2.2.15 的版本
  • 2.2.12 版本开始, 创建的项目默认为 ES6 模式
  • 使用 ES6 模式创建的项目, 项目启动时会自动将 src 目录下的文件编译到 app 目录下

最后打开浏览器, 访问 http://127.0.0.1:8360/ 即可

深入了解 ThinkJS 的各个功能模块

项目算是跑起来了, 但要真正的使用, 还需理解下 ThinkJS 的项目结构, 看下其中各个目录和文件的作用, 以便知道如何新增或者修改某些功能.

首先打开 src 目录, 看见项目目录是按照模块来划分的, 新创建的项目提供了两个模块

  • common 通用模块, 用来放通用逻辑和配置信息的
  • home 一个业务模块(默认模块), 要添加一个新的模块: thinkjs module xxx
    • controller 控制器, 一个 URL 对应一个 controller 下的一个 action
    • config 模块下的配置信息
    • model 模型, 数据库相关操作, 创建模型: thinkjs model home/user
    • logic 逻辑处理, 每个 controller action 执行前可以先进行逻辑校验, 可以包含: 参数是否合法、提交的数据是否正常、当前用户是否已经登录、当前用户是否有权限等, 这样可以降低 controller 里的 action 的复杂度. logic 里的 action 和控制器里的 action 一一对应, 系统在调用控制器里的 action 之前会自动调用 logic 里的 action

另外, view 为视图目录, 放置对应的模版文件, 支持国际化和多主题. www/static 用于放置静态资源文件. www/development.js, www/testing.js, www/production.js 分别为三套项目环境(开发/测试/生产)对应的入口启动文件.

路由

这些目录大概都搞清楚后, 接下来就是重点内容了: 一个 HTTP 请求过来最终是由哪段代码负责处理的呢?

例如: http://127.0.0.1:8360/test/index.html

这就是路由的工作了, 当用户访问一个 URL 时, 最终执行哪个模块下哪个控制器的哪个操作, 这是由路由来解析后决定的.

  • 首先路由会将 URL 解析为 pathname

    例如: http://127.0.0.1:8360/test/index.html, 将 URL 进行解析(去除 host 信息)得到的 pathname 为 /test/index.html

  • 然后会对 pathname 过滤

    因为有时候为了搜索引擎优化或者一些其他的原因, URL 上会多加一些东西. 比如: 当前页面是一个动态页面, 但 URL 最后加了 .html, 这样对搜索引擎更加友好. 但这些在后续的路由解析中是无用的, 需要去除.

    默认会去除配置的 pathname 前缀和后缀内容, 以及自动去除左右的 /

    // src/common/config/config.js
    pathname_prefix: "",
    pathname_suffix: ".html"

因此经过路由处理后, 会拿到干净的 pathname, 我们就可以根据这个 pathname 来判断执行哪个模块下哪个控制器的哪个操作了.

例如

  • http://127.0.0.1:8360/test/index.html URL

  • /test/index.html 解析

  • test/index 过滤

  • 路由识别默认根据 模块/控制器/操作/参数1/参数1值/参数2/参数2值 来识别过滤后的 pathname

  • 当解析 pathname 没有对应的值时, 便使用对应的默认值

    其中模块默认值为 home, controller 默认值为 index, action 默认值为 index, 这些值可以在 src/common/config/config.js 中进行修改

    默认模块为 home 模块, 当解析用户的请求找不到模块时会自动对应到 home 下, 可以通过配 default_module 来修改默认模块

    关于大小写转化

    路由识别后, module、controller 和 action 值都会自动转为小写, 如果 action 值里有 _, 会作一些转化, 如: 假设识别后的 controller 值为 index, action 值为 user_add, 那么对应的 action 方法名为 userAddAction, 但模版名还是 index_user_add.html

  • 因此我们得知上面的 URL 会调用 test 模块(module)的 index 控制器(controller)的 index 操作(action)

  • 即: src/test/controller/index.js#indexAction

  • 默认的视图文件路径为 view/[module]/[controller]_[action].html模块/控制器_操作.html

  • 因此 display() 时对应的视图为: view/test/index_index.html

如果项目里并没有这个模块或者这个模块被禁用了, 就会识别为默认模块. 例如: http://127.0.0.1:8360/foo/bar.html, 会识别为 src/home/controller/foo.js#barAction

另外我们还可以自定义路由

默认的路由虽然看起来清晰明了, 解析起来也很简单, 但看起来不够简洁. 有时候需要更加简洁的路由, 这时候就需要使用自定义路由解析了, 路由配置文件为: src/common/config/route.js

  • 正则路由
  • 规则路由
  • 静态路由
  • 按模块来配置路由, 使用这种方式后, 通用模块里的路由配置不再配置具体的路由规则, 而是配置哪些规则命中到哪个模块, 再在对应模块下配置具体的路由规则

控制器

错误处理

扩展错误类型: 添加完错误后, 需要在对应地方调用显示错误才能让用户看到, 可以通过 think.statusAction 方法实现 return think.statusAction(600, this.http)

REST API Controller

可以用很便捷的方式来创建 REST API, 创建后无需额外的代码即可响应 REST API 的处理, 同时也可以通过定制响应额外的需求

thinkjs controller home/ticket --rest

上面的命令表示在 home 模块下创建了一个 ticket 的 Rest Controller,该 Controller 用来处理资源 ticket 的请求, 资源名称和数据表名称是一一对应的. 注意继承的类是: think.controller.rest

多级控制器

对于很复杂的项目, 一层控制器有时候不能满足需求, 这个时候可以创建多级控制器, 如:src/test/controller/group/article.js, 这时解析到的控制器为二级, 具体为 group/article, Logic 和 View 的目录与此相同. 例如: http://127.0.0.1:8360/test/group/article/index.html

下面是一个完整的 controller 示例, 列举了一般开发中所需要的功能

// src/foo/controller/bar.js
// 控制器是一类操作的集合, 用来响应用户同一类的请求
export default class extends think.controller.base {
    // 前置操作: 在 action 调用之前自动调用
    // 推荐放在一个 base controller 类中, 其他 controller 继承 base controller
    __before() {
        // 如果想在前置操作里阻止后续 action 代码继续执行
        // 可以用 return this.end('__before') 之类的操作提前结束请求
        console.log('controller __before');
    }

    // 后置操作: 在 action 调用之后自动调用
    __after() {
        // 如果 action 里阻止了后续的代码继续执行(用了 return), 则后置操作不会调用
        console.log('controller __after');
    }

    // 空操作: 当解析后的 URL 对应的控制器存在, 但 action 不存在时调用
    // 一般不需要使用
    __call() {
        console.log('controller __call');
        return this.end('404');
    }

    // 不指定 action 时默认用 indexAction 来处理这个请求
    // http://127.0.0.1:8360/foo/bar/a/%E4%B8%AD%E6%96%87/b/2?c=123
    indexAction() {
        // 获取 URL
        console.log('url', this.http.method, this.http.host, this.http.url);
        console.log('module/controller/action', this.http.module + '/' + this.http.controller + '.js#' + this.http.action + 'Action');

        // 获取请求参数
        console.log('pathparam-a', this.get('a'));
        console.log('pathparam-b', this.get('b'));
        console.log('urlparam-c', this.get('c'));
        console.log('GET param', this.get());
        // 当上传文件时, 包含 form 表单中除开 file 类型的其他字段的值
        console.log('POST param', this.post());
        console.log('param', this.param());
        // 上传的文件保存在临时目录(runtime/upload)中, 可以通过 path 属性看到
        // 使用时需要将其移动到项目里的目录, 否则请求结束时会被删除
        console.log('file', this.file());

        // 获取模型数据
        // 项目开发中, 经常要操作数据库, 如: 增删改查等操作.
        // 模型就是为了方便操作数据库进行的封装, 一个模型对应数据库中的一个数据表.
        // 模型文件不是必须存在, 如果没有自定义方法可以不创建模型文件, 实例化时会取模型基类的实例
        let model = this.model('city');
        // 操作模型
        model.where({name: 'Kabul'}).select().then(function(rs) {
            console.log('model', model.name, model.schema, rs);
        });
        // 指定 SQL 语句执行查询
        this.model().query("SELECT * FROM city WHERE name = '%s'", 'Kabul').then(function(rs) {
            console.log(rs);
        });

        // 变量赋值和模版渲染
        this.assign({
            title: '我们一起来学习 ThinkJS',
            author: 'Sun'
        });
        // 默认模版变量: 框架自动向模版里注册了 http, controller, config
        // 例如 <%- http.url %> <%- controller.ip() %> <%- config.port %> <%- config.db.type %>
        return this.display();
        // return this.end('随便输出点什么内容');

        // 返回 JSON/JSONP
        // return this.json({a: 1});
        // return this.jsonp({a: 1});

        // 返回格式化的正常数据, 一般是操作成功后输出
        // return this.success({a: 1});
        // 返回格式化的异常数据, 一般是操作失败后输出
        // return this.fail(1000, 'error...', {e: 1});

        // 跳转页面
        // return this.redirect('https://thinkjs.org');
    }

    // http://127.0.0.1:8360/foo/bar/baz?callback=abc
    // http://127.0.0.1:8360/模块/控制器/操作
    bazAction() {
        return this.jsonp({baz: 'hello thinkjs'});
    }
}

Logic

支持的数据类型有: booleanstringintfloatarrayobject, 默认为 string. 内置了很多校验类型

扩展校验类型

如果默认支持的校验类型不能满足需求, 可以在 src/common/bootstrap/validate.js 中通过 think.validate 方法对校验类型进行扩展

下面是一个完整的 logic 示例, 列举了一般开发中所需要的功能

// src/foo/logic/bar.js
// Logic 用于校验类(例如请求类型, 请求参数数据校验)的逻辑处理
// 与控制器里的 action 一一对应
// 系统在调用控制器里的 action 之前会自动调用 Logic 里的 action
export default class extends think.logic.base {
    indexAction() {
        // 验证请求类型
        this.allowMethods = 'get,post';
        // 自动校验, 如果有错误则直接输出 JSON 格式的错误信息
        // this.rules = {};

        let rules = {
            field1: {
                required: true, // 要去掉这个属性才能让参数变成不是必要的
                int: [10, 20], // 多个参数以数组形式传入
                // default: 20,
                post: true // 指定获取数据的方式
            }
        };

        // 验证请求参数
        let flag = this.validate(rules);
        // 验证通过返回 true
        if(!flag) {
            return this.fail('validate error', this.errors());
            // 在模版中显示错误
            // return this.display();
        }
    }
}

视图

视图配置可以在 src/common/config/view.js 中修改, 如果想每个模块有独立的视图目录,将配置 root_path 修改为空即可.

那么 http://127.0.0.1:8360/foo/bar.html 会去找 src/foo/view/bar_index.html

修改连接符

默认控制器和操作之间的连接符是 _, 文件名类似为 index_index.html, 如果想将控制器作为一层目录的话, 如: index/index.html, 可以将连接符修改为 file_depr: "/".

断点调试

在 VS Code(v1.7+)下断点调试, 设置调试配置后, 在源码中直接添加断点即可调试

配置

可以在不同的模块和不同的项目环境下使用不同的配置, 且这些配置在服务启动时就已经生效

可以在每个模块下定义不同的配置. 其中 common 模块下定义一些通用的配置, 其他模块下配置会继承 common 下的配置. 如: home 模块下的最终配置是将 commonhome 模块下配置合并的结果.

支持多种级别的配置文件, 会按如下顺序进行读取: 框架默认的配置 -> 项目模式下框架配置 -> 项目公共配置 -> 项目模式下的公共配置 -> 模块下的配置

默认支持 3 种项目环境, 可以根据不同的环境进行配置, 以满足不同情况下的配置需要. 项目里也可以扩展其他的环境, 当前使用哪种环境可以在 入口文件 中设置, 设置 env 值即可.

不同项目环境差异化配置一般不是很多, 所以放在一个文件中定义. 这时候如果要修改一个独立功能的配置, 就需要将独立功能对应的 key 带上. 如修改数据库配置需要将数据库对应的名称 db 带上.

扩展配置

项目里可以根据需要扩展配置, 扩展配置只需在 src/common/config/ 建立对应的文件即可, 例如: src/common/config/foo.js, 这样就可以通过 think.config('foo') 来获取对应的配置了

模型

数据库配置和常用的CRUD 操作以及关联模型查询缓存

代码规范

  • 文件路径必须小写, 因为有些操作系统对文件路径不区分大小写, 有些又区分大小写, 因此统一必须小写
  • 不要使用 constrcutor 方法, 而是统一使用 init
  • 使用 async/await
  • 通过 Babel 编译来使用 ES6 语法开发

线上部署

上线前执行 npm run compile 命令,将 src/ 目录编译到 app/ 目录,然后将 app/ 目录下的文件上线

  • 使用 PM2 管理服务

    注意 pm2.json 中的 "cwd": "F:/tmp/thinkjs-demo" 路径必须使用 / 来分隔, 否则 PM2 启动会报错: Error: ENOENT: no such file or directory, uv_chdir

  • 使用 nginx 做反向代理

    同时最好设置 ThinkJS 禁止端口访问和关闭 ThinkJS 本身的静态资源处理

默认使用的 www/development.js 作为启动脚本, 因此你会发现通过 PM2 启动项目后, 修改 src 中的源码自动编译到 app 目录了, 就相对于代码已经重新部署生效了.

因此当你运行了 pm2 startOrReload pm2.json 后, 想修改为 www/production.js 作为启动脚本, 需要先 pm2 stop pm2.jsonpm2 delete pm2.json, 再修改 pm2.json 中的 script, 最后 pm2 startOrReload pm2.json 来重新启动, 否则你会发现修改了 script 没有效果, 还是用的以前的那份配置.

这样当你再修改 src 中的源码时, 需要手动 npm run compile, 然后还需要 pm2 startOrReload pm2.json 来重新部署.

Adapter

用来解决一类功能的多种实现, 如: 支持多种数据库,支持多种模版引擎等. 系统默认支持的 Adapter 有: Cache, Session, WebSocket, DB, Template.

Middleware

在请求处理中埋很多 hook, 每个 hook 串行执行一系列的 middleware, 最终完成一个请求的逻辑处理

建议使用追加的方式配置 middleware, 系统的 middleware 名称可能在后续的版本中有所修改

路径常量

系统提供了很多常量供项目里使用, 利用这些常量可以方便的访问对应的文件, 还可以通过入口文件启动文件在项目里定义额外的路径常量

常见问题

  • 如何修改服务监听的端口

    默认监听的端口为 8360, 可以通过配置文件 src/common/config/config.js 来修改: port: 1234

  • 并行处理

    使用 async/await 来处理异步时, 是串行执行的, 但很多场景下我们需要并行处理, 此时可以结合 Promise.all 来处理

  • 如何在不同的环境下使用不同的配置/如何跨模块调用/如何请求其他接口数据/如何输出图片/修改请求超时时间/如何捕获异常/如何忽略异常/如何开启 cluster/如何让 Action 只允许命令行调用/设置跨域头信息(CORS)/如何让用户登录后才能访问

更多资源

总结

ThinkJS 这么玩了一圈下来, 我们发现其实对于前端来说, 后端也没有想象中的那么难, 还能将 ES6/7 这样的新技术运用到项目中, 而不像在浏览器上随便用个什么新特性都畏畏缩缩, 担心兼容性问题, 何乐而不为呢?

如果你以前感觉前后端跨界似鸿沟般难以逾越, 现在借助 ThinkJS 这么智能的框架, 前端也可以很块上手做后端的开发, 什么接口, 什么访问数据库都是小 case. 况且基于 Node.js 平台从开发到上线到运维都已经很成熟了, 工具和生态圈都很完善而且很活跃, 大型互联网公司早就用于生产环境了, 这方面没有什么后顾之忧.

前端向后端迈进地主要的难点还是在于模型这一块, 对数据库比较陌生的前端多补补课, 把各种关联模型搞清楚, 不出几日已然就是全栈工程师了.

俗话说万事开头难, 最后我只想告诉大家: Node.js 已经彻底改变了前端, 扩大了前端的圈子, 每一个前端工程师都应该拿起这个有力的武器, 在这个前端最好的时代, 实现自己的理想.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant