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

什么是 ZEIT

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

//一个最简单的基于 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

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

image

选择 Continue 之后如果是通过邮箱登录进来的会问你是否需要绑定 Github 账号,可以让 Github 与 ZEIT 之间的持续集成更加方便,当然你也可以选择 SKIP 跳过。最后一步则会指导你如何创建项目,它提供了很多快速创建的模板,例如 Next.js, React, Vuepress, Gatsby, Docz, Nuxt.js, Svelte, Angular

image

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

部署 Koa.js 服务

通过刚才的示例我们可以了解到其实它的本质就是将 HTTP 请求的 requestresponse传入方法中,处理后再返回给 HTTP,所以它除了 Serverless 函数之外也是完全支持 Koa.js 以及基于 Koa.js 的 ThinkJS 服务部署的。我们先来看看如果要部署一个 Koa.js 服务应该怎么做。

Fork 快速部署

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

image
命令行部署

如果没有绑定 Github 账户也没关系,我们可以通过命令行部署服务。将 DEMO 仓库克隆下来后直接使用 now 命令就可以了。部署成功后 ZEIT 会给我们返回一个当前提交版本的唯一地址,比如说 https://now-koa-demo-pac7dbxrf.now.sh/ 打开之后就会见到 Hello from koa.js! 的返回信息。

image
注意事项

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

我们再来看看 now.json 的内容。该 JSON 文件用于告诉 now 服务 index.js 文件需要使用 @now/node 运行时执行,而所有的请求需要转发到 index.js 文件上。听起来是不是非常像 Nginx 上的内容?

{
  "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 服务启动做了以下几件事情:

1. 初始化 Loader 实例,在对应的进程上加载需要的文件,包括 config, middleware, controller, logic, model, service 等。

2. 执行 beforeStartServer() 启动前钩子

3. 启动服务

4. 启动后向全局发送 appReady 事件

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

1. 实例化 Loader,使用 loader.loadAll('worker') 加载所有的依赖文件

2. 在回调中执行 beforeStartServer() 启动前钩子

3. 执行 callback() 启动服务

4. 启动后向全局发送 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 的依赖引用并非显式的,导致了在打包上的一些困难,其它的都还是很方便的。如果有什么其它的问题,也欢迎大家多多交流。

原文作者:怡红公子

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容