如何将你的 ThinkJS 项目部署到 ZEIT 上

什么是 ZEIT

ZEIT 是免费的云平台,支持部署静态网站以及 Serverless 函数。Serverless 是近几年比较火的概念,简单去理解就是你只须要去实现具体的业务逻辑,而与最终服务相关的服务器、HTTP 服务等则由第三方管理。Serverless 又被称为 FaaS(函数即服务),因为业务粒度很是细,因此很是方便作动态扩容等自动化运维任务。javascript

//一个最简单的基于 Node.js 的 Serverless 函数
module.exports = function(req, res) {
  const { name = 'World' } = req.query
  res.send(`Hello ${name}!`)
}

经过 ZEIT 提供的 CLI 工具 now,咱们能够一条命令将 Node.js, Golang, Python, Ruby, PHP, Rust 等语言的应用部署到 ZEIT 上。若是你想了解更多关于 ZEIT 这个公司的知识也能够看这篇知乎回答了解更多。php

如何使用 ZEIT

注册很是方便,打开 https://zeit.co 点击右上角的 "Join Free",使用 Github 或者 Gitlab 帐号登陆后会自动注册。固然你也可使用邮箱注册,会发送一封确认邮件到你的邮箱。登陆后会让你填写昵称、头像和惟一 ID等配置。vue

选择 Continue 以后若是是经过邮箱登陆进来的会问你是否须要绑定 Github 帐号,可让 Github 与 ZEIT 之间的持续集成更加方便,固然你也能够选择 SKIP 跳过。最后一步则会指导你如何建立项目,它提供了不少快速建立的模板,例如 Next.jsReactVuepressGatsbyDoczNuxt.jsSvelteAngularjava

按照示例使用 npm install -g now 安装 CLI 工具,初始化项目后直接使用 now 命令便可发布到 ZEIT 上,总体流程很是简单。node

部署 Koa.js 服务

经过刚才的示例咱们能够了解到其实它的本质就是将 HTTP 请求的 requestresponse传入方法中,处理后再返回给 HTTP,因此它除了 Serverless 函数以外也是彻底支持 Koa.js 以及基于 Koa.js 的 ThinkJS 服务部署的。咱们先来看看若是要部署一个 Koa.js 服务应该怎么作。python

Fork 快速部署

因为 ZEIT 官方主推 Serverless 服务,因此把 Node.js 的脚手架模板去除掉了,因此咱们只能本身建立项目了,为了方便我提供了一个 DEMO 仓库 https://github.com/lizheming/now-koa-demo。若是在刚才的注册流程中你绑定了 Github 的帐号的话你能够选择直接 Fork 该仓库,等一小会儿以后就会收到 ZEIT 的 Github 通知告诉你网站已经部署成功,并在 commit 中提供部署后的地址。react

命令行部署

若是没有绑定 Github 帐户也不要紧,咱们能够经过命令行部署服务。将 DEMO 仓库克隆下来后直接使用 now 命令就能够了。部署成功后 ZEIT 会给咱们返回一个当前提交版本的惟一地址,好比说 https://now-koa-demo-pac7dbxrf.now.sh/ 打开以后就会见到 Hello from koa.js! 的返回信息。webpack

注意事项

index.js 文件内容与正常的 Koa.js 项目代码无异,惟一的区别是最终项目没有直接调 app.listen() 方法进行监听,而是使用 module.exports = app.callback() 将最终的 callback 方法进行了返回。咱们知道 app.callback() 方法返回的是接受 request 和 response 对象做为参数的函数,这就回到了文章最开始的示例了。git

咱们再来看看 now.json 的内容。该 JSON 文件用于告诉 now 服务 index.js 文件须要使用 @now/node 运行时执行,而全部的请求须要转发到 index.js 文件上。听起来是否是很是像 Nginx 上的内容?github

{
  "version": 2,
  "builds": [
    { "src": "index.js", "use": "@now/node" }
  ],
  "routes": [
    { "src": "/(.*)", "dest": "/index.js" }
  ]
}

部署 ThinkJS 服务

成功部署 Koa.js 服务以后,下面咱们就来看看怎么给你的 ThinkJS 服务找一个免费空间部署上去吧!为了方便我也提供了一个 DEMO 仓库 https://github.com/lizheming/now-thinkjs-demo,Fork 该仓库可快速体验 Now 部署 ThinkJS 服务。Fork 成功后过一会就会收到部署成功后的提示,同时告知你部署后的惟一地址,例如 https://now-thinkjs-demo-hrmqxxv2p.now.sh/

然而这只是我折腾成功后的结果,基于 ThinkJS 的服务直接部署并无部署 Koa.js 服务那么简单,这主要是由 ThinkJS 框架自己的特性决定的。下面我将其中须要注意的点一一道来,方便其它已有服务的迁移。咱们先来看看针对 ZEIT 平台的 ThinkJS 启动文件有那些内容。而后咱们基于该文件主要讲述下碰到的问题以及为何须要这么作。

const path = require('path');
const Application = require('thinkjs');

const Loader = require('thinkjs/lib/loader');
class NowLoader extends Loader {
  writeConfig() {}
}

const app = new Application({
  ROOT_PATH: __dirname,
  APP_PATH: path.join(__dirname, 'src'),
  VIEW_PATH: path.join(__dirname, 'view'),
  proxy: true, // use proxy
  env: 'now',
  external: {
    log4js: {
      stdout: path.join(__dirname, 'node_modules/log4js/lib/appenders/stdout.js'),
      console: path.join(__dirname, 'node_modules/log4js/lib/appenders/console.js')
    },
    static: {
      www: path.join(__dirname, 'www')
    }
  }
});

const loader = new NowLoader(app.options);
loader.loadAll('worker');
module.exports = function (req, res) {
  return think.beforeStartServer().catch(err => {
    think.logger.error(err);
  }).then(() => {
    const callback = think.app.callback();
    return callback(req, res);
  }).then(() => {
    think.app.emit('appReady');
  });
};

服务启动问题

刚才部署 Koa.js 的时候咱们知道了,ZEIT 运行时接受的文件须要返回一个函数。在 Koa.js 中是 app.callback(),而在 ThinkJS 中则是 think.app.callback() 。不过咱们却不能直接这么返回,由于从源码中咱们能够了解到 ThinkJS 服务启动作了如下几件事情:

  • 初始化 Loader 实例,在对应的进程上加载须要的文件,包括 config, middleware, controller, logic, model, service 等。
  • 执行 beforeStartServer() 启动前钩子
  • 启动服务
  • 启动后向全局发送 appReady 事件

目前 ThinkJS 服务中并无纯粹的非启动方法包含这些内容,因此我选择了在启动脚本中模拟正常的启动流程自定义启动过程的方式。因为多进程逻辑稍微复杂点,因此我直接按照单进程模式模拟。

  • 实例化 Loader,使用 loader.loadAll('worker') 加载全部的依赖文件
  • 在回调中执行 beforeStartServer() 启动前钩子
  • 执行 callback() 启动服务
  • 启动后向全局发送 appReady 事件

文件引用问题

项目文件相对引用

咱们知道 ThinkJS 的本质是文件夹即路由的模式,Controller, Model, View 等文件按照必定的文件夹规则放置,经过动态读取文件的形式找到对应的文件并加载执行。这在正常的项目中原本不存在什么问题,可是 ZEIT Now 平台为了节省空间,会对在入口文件中没有显示依赖的文件进行忽略。

咱们正常的启动文件中只会定义 APP_PATH ,而 VIEW_PATH 甚至是静态资源目录是在 src/config/adapter.js 以及 src/config/middleware.js 中定义的。而这两个文件又是动态读取文件引入的,致使在上传的时候因为没有显式依赖该文件而不上传该文件。因此为了解决这个问题,我选择了在启动文件中再次显示声明一下须要加载的文件。固然这些配置对 ThinkJS 来讲是没有用的。

依赖文件相对引用

能够看到,除了正常的项目文件的引用以外,我还写了两个 log4js 文件的引用,这又是为何呢?

主要仍是由于 ZEIT 为了节省体积,除了会限制只上传须要的文件以外,还会针对入口文件使用 webpack 进行打包。使用 webpack 打包后全部的依赖都在入口文件中了,这样就不用上传硕大的 node_modules 文件夹,能够极大的减少体积。ZEIT 将该针对 Node.js 项目打包成单文件的打包工具开源出来了 https://github.com/zeit/ncc 若是项目中有须要打包成单文件减少体积的需求也可使用。

log4js 很是早期的版本中是经过 require(./${type}) 的形式将对应的日志输出器加载进来的。因为打包后目录结构发生变化,打包后当前文件夹并无对应的文件,因此会致使执行的时候报文件找不到的错误。因此为了解决这个问题则一样须要在入口文件中显式的声明这些文件的依赖。

去年2月份就有用户针对这个问题提了 Commit 将全部的加载器显式依赖后再进行选择解决了这个问题。因此在新版 log4js 的中已经不存在这个问题了,不过我仍是在这里说明一下,是由于可能项目中引用的其它依赖会有这个问题,仍是须要注意一下的。

写入权限问题

除了上面的问题以外,部署的时候我还碰到了文件写入无权限的问题。因为 ZEIT Now 提供无状态服务,因此写入文件等反作用操做在 ZEIT 中被禁止了。若是你有文件写入操做的话会在控制台中提示写入失败并抛错。

而在 ThinkJS 中因为各类配置文件比较多,为了方便问题排查,会在配置文件加载完成后调用 writeConfig() 方法写一份最终合并后的配置在 runtime 目录中,例如 runtime/config/production.json 文件。这样的话在 ZEIT 平台就会报错致使服务没法正常启动了。

不过目前 ThinkJS 并无提供一个配置可以取消这个配置文件写入的操做。因此我提供的解决方法则是经过继承将 writeConfig() 方法复写掉来组织文件写入的操做。

固然这是 ThinkJS 自己的文件写入操做,若是说你的项目中还有其它文件写入操做的话,也须要作对应的操做。例如 logger 日志的配置能够输出到控制台,文件上传等必须写入文件的则能够写到系统临时目录 /tmp 中。不一样的系统临时目录可能不太同样,Node.js 中建议经过 require('os').tmpdir() 来获取。

后记

经过 ZEIT 平台,极大的下降了部署 Node.js 服务的成本,不只是机器成本,维护成本也极大的下降了。其实正常的 Node.js 项目部署起来仍是很是方便的,主要仍是 ThinkJS 的依赖引用并不是显式的,致使了在打包上的一些困难,其它的都仍是很方便的。若是有什么其它的问题,也欢迎你们多多交流。

参考资料:

相关文章
相关标签/搜索