部署 Node.js 项目
Zeabur 支持多种类型的 Node.js 项目:
- 原生项目(支持 NPM/Yarn/PNPM/Bun 等包管理器)
- 使用 Vite 打包的项目
- Qwik
- Next.js
- Remix
- Nuxt.js
- Umi
- Svelte (Kit)
- Nest.js
- Nue.js
- Express
- Astro
- Waku
- Nue
- Create React App
- vue-cli
- Hexo
- Vitepress
- Astro(静态、SSR)
- Slidev
- Docusaurus
- Solid Start(Node、静态)
Monorepo 支持
Zeabur 会自动识别 pnpm workspace、Yarn Workspace 以及绝大多数基于这两个 Monorepo 方法的工具链,如 Turborepo 和 Lerna 等。
以 Turborepo 的 basic
模板为例,它的 pnpm-workspace.yaml
内容为:
packages:
- "apps/*"
- "packages/*"
而 apps
下有 docs
及 web
两个目录。Zeabur 默认会挑选列在 workspace 包清单中的第一个 Node.js 应用程序来部署,就此例而言,Zeabur 会部署 apps/docs
这个应用程序。如果想要部署 apps/web
,可以在项目的根目录新增 zbpack.json
文件,并加入以下内容:
{
"app_dir": "apps/web"
}
或者是使用 环境变量 进行设置:
ZBPACK_APP_DIR=apps/web
这样就会部署 apps/web
这个应用程序。
如果你的应用程序确实在根目录,但使用 pnpm-workspace.yaml
放置 React 组件、设计系统等元素,你可以使用环境变量 ZBPACK_APP_DIR=/
或在 zbpack.json
加入
{
"app_dir": "/"
}
让 Zeabur 部署你放在根目录的应用程序。
停用缓存功能
默认 Zeabur 会调换安装流程,通过记录安装依赖的步骤,加速你往后 CI/CD 的速度。一般的 Node.js 项目应该不受影响,但假如你的项目在安装依赖时需要用到项目中的其他文件而导致项目编译失败,你可能得停用缓存功能才能正常使用。
在项目的根目录新增 zbpack.json
文件,并加入以下内容,即可停用这个功能。
{
"cache_dependencies": false
}
或者是设置 环境变量:
ZBPACK_CACHE_DEPENDENCIES=false
更改编译和启动命令
假如你的项目类型较为特殊(如使用自定义的 Monorepo 工具链),你可能会需要指定服务的编译 (build) 和启动 (start) 命令,比如将 frontend
服务的启动命令改成 pnpm run start:frontend
;把 api
服务的编译命令改成 pnpm run start:api
。
这里介绍两种更改这个命令的方式。
使用文件修改
在 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
。
使用环境变量修改
你也可以使用 环境变量 来设置编译和启动命令:
ZBPACK_BUILD_COMMAND=pnpm run build:api
ZBPACK_START_COMMAND=pnpm run start:api
指定 Node.js 和包管理器版本
Node.js 版本
默认情况下,Zeabur 会使用最新的 LTS Node.js 版本来构建你的项目。
如果你想使用不同的版本,你可以在 package.json
中指定:
{
"engines": {
"node": "18.1.0"
}
}
包管理器版本
默认情况下,Zeabur 会使用 yarn 来安装你的项目依赖。
如果你想使用其他的包管理器以及特定的版本,你可以在 package.json
中指定:
{
"packageManager": "pnpm@8.0.0"
}
网页爬取
Playwright 支持
如果你的 package.json
有声明 playwright-chromium
,Zeabur 会自动帮您准备好运行 Playwright 必要的环境。
注意 Playwright 应以 Headless 模式运行,通常默认就是如此。
Puppeteer 支持
如果你的 package.json
有声明 puppeteer
,Zeabur 会自动帮您准备好运行 Puppeteer 必要的环境。
注意 Puppeteer 应以 Headless 模式运行,通常默认就是如此。
以 Serverless 方式部署
如果你的 Node.js 符合以下条件,那么你可以在 Zeabur 使用 Serverless 方式部署:
- 项目本身使用了如 Next.js、Nuxt.js、Remix 等专为 Serverless 部署所设计的框架。
- 项目本身不使用上述框架,但服务本身符合 Serverless 的理念:每个请求的状态彼此独立,且可以在没有请求的情况下自动休眠。
启用 Serverless
如果您的项目是使用 Next.js、Nuxt.js、Waku、Angular 和 Remix 编写的(完整名单可以参考 zbpack 代码库的 getServerless
函数),
Zeabur 会自动将项目部署成 serverless 形式。如果需要停用,可参考〈启用 Serverless〉一章。
如果是其他框架编写而成的项目(或者是如下文,自行制作的 Serverless 格式),目前需要 opt-in。请参考 启用 Serverless 页面启用 serverless 支持。如果测试上没有问题,也欢迎送 Pull Request 到 zbpack 代码库。
把项目构建成 Serverless 格式
如果你的项目使用了 Next.js、Nuxt.js、Remix 等框架,那么你可以直接跳过这个步骤,因为 zbpack 会自动将他转换成 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 构建你的项目,然后把所有 .js
文件按照原有的相对路径放入 .zeabur/output/functions/index.func
目录下,并且把 node_modules
和 package.json
复制到 .zeabur/output/functions/index.func
目录下,这样就完成了项目的构建。
特别注意到,我们在这个脚本中保留了两个特殊的可设定字段:
// dynamic-required files
const dynamicRequiredDirs = ['views']
// static files
const staticFileDirs = ['public']
这两个字段让我们把除了 .js
以外的东西也一起放入产物内,其中 dynamicRequiredDirs
是一些在运行时期才会被 require
的文件,比如 views
目录下的模板文件;
则是一些静态文件,比如 public
目录下的静态资源,这些资源被放入 .zeabur/output/static
目录下,让他们可以直接被 Zeabur Edge Network 用更快的速度分发给全世界的使用者。
加入这个脚本以后别忘了在项目中安装 esbuild
,然后在 package.json
的 scripts
中加入 build
指令:
{
"scripts": {
"build": "node scripts/build.js"
}
}
你可以在 zeabur/expressjs-template 找到完整的范例代码。也可以根据自己的需求修改 scripts/build.js
脚本。
额外注意事项
-
监听的 port 使用
process.env.PORT
例如:
const port = process.env.PORT || 3000 // 而非 const port = 3000
-
避免使用 nodemon 作为 runtime,开发完后,换成一般的 node 指令
例如 package.json 內:
{ "scripts": { "start": "node server.js" // 而非 "start": "nodemon server.js" } }