教學
Node.js

部署 Node.js 專案

Zeabur 支援眾多類型的 Node.js 專案:

停用快取功能

預設 Zeabur 會調換安裝流程,透過儲存(快取)安裝依賴的步驟,加速你往後 CI/CD 的速度。 一般的 Node.js 專案應該不受影響,但假如你的專案是以 pnpm-workspace 等利用工具管理的 monorepo,並且預設的設定會導致專案編譯失敗,你可能得停用快取功能才能正常使用。

在專案的根目錄新增 zbpack.json 檔案,並加入以下內容,即可停用這個功能。

{
    "cache_dependencies": false
}

更改編譯和啟動命令

假如你的專案類型較為特殊(如使用工具達到的 Monorepo),你可能會需要指定服務的編譯 (build) 和啟動 (start) 命令,比如將 frontend 服務的啟動命令改成 pnpm run start:frontend;把 api 服務的編譯命令改成 pnpm run start:api

這裡介紹兩個更改這個命令的方式。

在網頁端修改

部署專案到 Zeabur 服務後展開 “Settings”,即可修改「自訂編譯命令」和「自訂啟動命令」。 修改完成後點選 “Redeploy” 重新部署即可。

Node.js: Custom Command

使用檔案修改

zbpack.json 加入以下兩個設定項目:

{
    "build_command": "<自訂編譯命令>",
    "start_command": "<自訂啟動命令>"
}

預設 zbpack.json 的設定會套用到所有部署的服務。如果你想限定 「服務名稱為 api 的使用某個命令;服務名稱為 frontend 的使用另一個命令」, 則需要建立 zbpack.[服務名稱].json 的檔案:

// zbpack.api.json
{
    "build_command": "pnpm run build:api",
    "start_command": "pnpm run start:api"
}
// zbpack.frontend.json
{
    "build_command": "pnpm run build:frontend",
    "start_command": "pnpm run start:frontend"
}

zbpack.json 的套用優先級是 zbpack.[服務名稱].json > zbpack.json

指定 Node.js 和包管理器版本

Node.js 版本

預設情況下,Zeabur 會使用最新的 LTS Node.js 版本來建置你的項目。

如果你想使用不同的版本,你可以在 package.json 中指定:

{
    "engines": {
        "node": "18.1.0"
    }
}

套件管理器版本

預設情況下,Zeabur 會使用 yarn 來安裝你的專案依賴。

如果你想使用其他的套件管理器以及特定的版本,你可以在 package.json 中指定:

{
    "packageManager": "pnpm@8.0.0"
}

以 Serverless 方式部署

如果你的 Node.js 符合以下條件,那麼你可以在 Zeabur 使用 Serverless 方式部署:

  1. 專案本身使用了如 Next.js (opens in a new tab)Nuxt.js (opens in a new tab)Remix (opens in a new tab) 等專為 Serverless 部署所設計的框架。
  2. 專案本身不使用上述框架,但服務本身符合 Serverless 的理念:每個請求的狀態彼此獨立,且可以在沒有請求的情況下自動休眠。

啟用 serverless

如果你的專案是使用 Next.js、Nuxt.js、Waku、Angular 和 Remix 撰寫的(完整名單可以參考 zbpack 程式碼庫的 getServerless 函式 (opens in a new tab)), Zeabur 會自動將專案部署成 serverless 形式。如果需要停用,可參考〈啟用 Serverless〉一章。

如果是其他框架撰寫而成的專案(或者是如下文,自行製作的 Serverless 格式),目前需要 opt-in。請參考 啟用 Serverless 頁面啟用 serverless 支援。如果測試上沒有問題,也歡迎送個 Pull Request 到 zbpack 程式碼庫 (opens in a new tab)

把專案構建成 Serverless 格式

如果你的專案使用了 Next.js、Nuxt.js、Remix 等框架,那麼你可以直接跳過這個步驟,因為 zbpack (opens in a new tab) 會自動將他轉換成 Serverless 產物格式。

如果你的專案不使用上述框架,那麼你需要自行把專案構建成 Serverless 格式,這裡以一個最基本的 Express.js 應用程式為例:

// app.js
 
const express = require('express')
const app = express()
 
app.get('/', (req, res) => {
    res.send('Hello World!')
})
 
module.exports = app

從上述示範中,可以看到 app.js 這個模組已經輸出了 app 這個物件,這是一個符合 Zeabur 的 Serverless 處理函數格式的物件,因此我們唯一要做的就是將他在構建階段輸出到 .zeabur/output/functions 中的 index.func 目錄下。

為了實現這件事,我們可以在程式碼中加入以下腳本:

// scripts/build.js
 
const esbuild = require('esbuild');
const fs = require('fs');
 
// dynamic-required files
const dynamicRequiredDirs = ['views']
 
// static files
const staticFileDirs = ['public']
 
// Remove old output
if (fs.existsSync('.zeabur/output')) {
    console.info('Removing old .zeabur/output')
    fs.rmSync('.zeabur/output', {recursive: true})
}
 
function getModuleEntries() {
    function getModuleEntriesRecursive(dir) {
        let entries = []
        fs.readdirSync(dir).forEach(file => {
            const path = `${dir}/${file}`
            if (fs.statSync(path).isDirectory()) {
                if(file === 'node_modules') return
                entries = entries.concat(getModuleEntriesRecursive(path))
            } else if (file.endsWith('.js')) {
                entries.push(path)
            }
        })
        return entries
    }
    return getModuleEntriesRecursive('.')
}
 
// build with esbuild
try {
    esbuild.build({
        entryPoints: getModuleEntries(),
        bundle: false,
        minify: false,
        outdir: '.zeabur/output/functions/index.func',
        platform: 'node',
        target: 'node20',
        plugins: [{
            name: 'make-all-packages-external',
            setup(build) {
                let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/ // Must not start with "/" or "./" or "../"
                build.onResolve({filter}, args => ({path: args.path, external: true}))
            },
        }],
    }).then(res => {
        if (res.errors.length > 0) {
            console.error(res.errors)
            process.exit(1)
        }
        console.info('Successfully built app.js into .zeabur/output/functions/index.func')
        fs.copyFileSync('.zeabur/output/functions/index.func/app.js', '.zeabur/output/functions/index.func/index.js')
        fs.rmSync('.zeabur/output/functions/index.func/app.js')
    })
} catch (error) {
    console.error(error)
}
 
// copy node_modules into function output directory
console.info('Copying node_modules into .zeabur/output/functions/index.func/node_modules')
fs.cpSync('node_modules', '.zeabur/output/functions/index.func/node_modules', {recursive: true, verbatimSymlinks: true})
 
// copy package.json into function output directory
console.info('Copying package.json into .zeabur/output/functions/index.func')
fs.cpSync('package.json', '.zeabur/output/functions/index.func/package.json')
 
// copy dynamic-required files into function output directory, so they can be required during runtime
dynamicRequiredDirs.forEach(dir => {
    copyIfDirExists(dir, `.zeabur/output/functions/index.func/${dir}`)
})
 
// copy static files into function output directory, so they can be served by the web server directly
staticFileDirs.forEach(dir => {
    copyIfDirExists(dir, `.zeabur/output/static`)
})
 
function copyIfDirExists(src, dest) {
    if (fs.statSync(src).isDirectory()) {
        console.info(`Copying ${src} to ${dest}`)
        fs.cp(src, dest, {recursive: true}, (err) => {
            if (err) throw err;
        });
        return
    }
    console.warn(`${src} is not a directory`)
}

這個腳本做的事情非常單純,他會使用 esbuild (opens in a new tab) 構建你的專案,然後把所有 .js 檔案按照原有的相對路徑放入 .zeabur/output/functions/index.func 目錄下,並且把 node_modulespackage.json 複製到 .zeabur/output/functions/index.func 目錄下,這樣就完成了專案的構建。

特別注意到,我們在這個腳本中保留了兩個特殊的可設定欄位:

// dynamic-required files
const dynamicRequiredDirs = ['views']
 
// static files
const staticFileDirs = ['public']

這兩個欄位讓我們把除了 .js 以外的東西也一起放入產物內,其中 dynamicRequiredDirs 是一些在執行時期才會被 require 的檔案,比如 views 目錄下的模板檔案;staticFileDirs 則是一些靜態檔案,比如 public 目錄下的靜態資源,這些資源被放入 .zeabur/output/static 目錄下,讓他們可以直接被 Zeabur Edge Network 用更快的速度分發給全世界的使用者。

加入這個腳本以後別忘了在專案中安裝 esbuild,然後在 package.jsonscripts 中加入 build 指令:

{
    "scripts": {
        "build": "node scripts/build.js"
    }
}

你可以在 zeabur/expressjs-template (opens in a new tab) 找到完整的範例程式碼。也可以根據自己的需求修改 scripts/build.js 腳本。

額外注意事項

  1. 監聽的 port 使用 process.env.PORT

    例如

    const port = process.env.PORT || 3000
    // 而非 const port = 3000
  2. 避免使用 nodemon 作為 runtime,開發完後,換成一般的 node 指令

    例如 package.json 內

    {
    "scripts": {
        "start": "node server.js"
        // 而非 "start": "nodemon server.js"
    },
    }