如何將你的 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 這個公司的知識也可以看這篇知乎回答瞭解更多。
如何使用 ZEIT
註冊非常方便,開啟 zeit.co 點選右上角的 "Join Free",使用 Github 或者 Gitlab 賬號登入後會自動註冊。當然你也可以使用郵箱註冊,會傳送一封確認郵件到你的郵箱。登入後會讓你填寫暱稱、頭像和唯一 ID等配置。
選擇 Continue 之後如果是通過郵箱登入進來的會問你是否需要繫結 Github 賬號,可以讓 Github 與 ZEIT 之間的持續整合更加方便,當然你也可以選擇 SKIP 跳過。最後一步則會指導你如何建立專案,它提供了很多快速建立的模板,例如
按照示例使用 npm install -g now
安裝 CLI 工具,初始化專案後直接使用 now
命令即可釋出到 ZEIT 上,整體流程非常簡單。
部署 Koa.js 服務
通過剛才的示例我們可以瞭解到其實它的本質就是將 HTTP 請求的 request
和response
傳入方法中,處理後再返回給 HTTP,所以它除了 Serverless 函式之外也是完全支援 Koa.js 以及基於 Koa.js 的 ThinkJS 服務部署的。我們先來看看如果要部署一個 Koa.js 服務應該怎麼做。
Fork 快速部署
由於 ZEIT 官方主推 Serverless 服務,所以把 Node.js 的腳手架模板去除掉了,所以我們只能自己建立專案了,為了方便我提供了一個 DEMO 倉庫 github.com/lizheming/n…。如果在剛才的註冊流程中你綁定了 Github 的賬號的話你可以選擇直接 Fork 該倉庫,等一小會兒之後就會收到 ZEIT 的 Github 通知告訴你網站已經部署成功,並在 commit 中提供部署後的地址。
命令列部署
如果沒有繫結 Github 賬戶也沒關係,我們可以通過命令列部署服務。將 DEMO 倉庫克隆下來後直接使用 now
命令就可以了。部署成功後 ZEIT 會給我們返回一個當前提交版本的唯一地址,比如說 now-koa-demo-pac7dbxrf.now.sh/ 開啟之後就會見到 Hello from koa.js!
的返回資訊。
注意事項
index.js
檔案內容與正常的 Koa.js 專案程式碼無異,唯一的區別是最終專案沒有直接調 app.listen()
方法進行監聽,而是使用 module.exports = app.callback()
將最終的 callback 方法進行了返回。我們知道 app.callback() 方法返回的是接受 request
和 response
物件作為引數的函式,這就回到了文章最開始的示例了。
我們再來看看 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 倉庫 github.com/lizheming/n…,Fork 該倉庫可快速體驗 Now 部署 ThinkJS 服務。Fork 成功後過一會就會收到部署成功後的提示,同時告知你部署後的唯一地址,例如 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 專案打包成單檔案的打包工具開源出來了 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 的依賴引用並非顯式的,導致了在打包上的一些困難,其它的都還是很方便的。如果有什麼其它的問題,也歡迎大家多多交流。
參考資料: