如何將你的 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 的依賴引用並不是顯式的,致使了在打包上的一些困難,其它的都仍是很方便的。若是有什麼其它的問題,也歡迎你們多多交流。

參考資料:

相關文章
相關標籤/搜索