Skip to content

前端部署方案

一些约定

  1. 兼容目标部署平台:Railway,服务器 docker 容器,手动启动
  2. 兼容开发和生产环境
  3. 前端独立部署,且后端不开放跨域,处理好请求后端 API 代理问题
  4. 处理好前端 SPA 的路由问题
  5. 部署环境开启 gzip 压缩

公共设置

为了优化在本地测试构建 Dockerfile 的体验,以及构建出镜像的体积,我们默认使用分阶段构建。 同时将代理的路径和目标写入环境变量。

Dockerfile
FROM node:18-alpine as build-stage

WORKDIR /app
RUN corepack enable

COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build
txt
node_modules
dist
.nitro
.output
txt
VITE_PROXY_TARGET="https://example.com/api/v1"
VITE_PROXY_PATH="/api"

Nginx

Nginx 是一个高性能的开源 Web 服务器,性能出色,配置文件标准丰富,是我们首选的前端部署方案。

配置示例

Dockerfile
FROM nginx:stable-alpine as production-stage

COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY --from=build-stage /app/nginx.conf /etc/nginx/conf.d/default.conf
COPY ./entrypoint.sh /app/entrypoint.sh

EXPOSE 80

RUN chmod +x /app/entrypoint.sh
CMD /app/entrypoint.sh
bash
# 为了避免在一些平台比如 Railway 需要手动设置 PORT 环境变量的问题,这里动态的修改 Nginx 配置的端口
p="${PORT:-80}"
sed -E -i.bak 's/listen[[:space:]]+[[:digit:]]+/listen '${p}'/' /etc/nginx/conf.d/default.conf

nginx -g "daemon off;"
ts
// 关于 `nginx.conf` 配置文件我们可以写一个 vite 插件来读取项目环境变量动态生成
import fs from "node:fs"

import type { Plugin } from "vite"

type ViteProxyPluginOptions = {
  proxyPath?: string
  proxyTarget?: string
}

function ViteProxyPlugin(options?: ViteProxyPluginOptions): Plugin {
  const { proxyPath = "/api", proxyTarget = "http://localhost:3000" } =
    options ?? {}

  return {
    name: "vite-proxy-plugin",
    apply: "build",
    closeBundle: () => {
      fs.writeFileSync(
        "nginx.conf",
        `
server {
  listen 80;

  root /usr/share/nginx/html;
  index index.html;

  # 开启 gzip 压缩
  gzip on;
  gzip_types text/plain text/css application/javascript application/json;

  location ^~${proxyPath}/ {
    rewrite ^/(.*)$ /$1 break;
    proxy_pass ${proxyTarget};
    proxy_http_version 1.1;
  }

  location / {
    try_files $uri $uri/ /index.html =404;
  }
}`,
        "utf-8",
      )
    },
  }
}
ts
import { defineConfig, loadEnv } from "vite"

const env = loadEnv(
  process.env["RAILWAY_ENVIRONMENT_NAME"] ??
    process.env["NODE_ENV"] ??
    "development",
  process.cwd(),
)

const proxyPath = env["VITE_PROXY_PATH"]
const proxyTarget = env["VITE_PROXY_TARGET"]

if (!proxyPath || !proxyTarget) {
  throw new Error("VITE_PROXY_PATH or VITE_PROXY_TARGET is not set")
}

export default defineConfig({
  plugins: [
    // ...
    ViteProxyPlugin({
      proxyPath,
      proxyTarget,
    }),
  ],
  server: {
    // 开发环境中我们借助 vite 来做 API 代理
    proxy: {
      [proxyPath]: {
        target: proxyTarget,
        secure: false,
        changeOrigin: true,
      },
    },
  },
})

Nitro

基于 Nitro 我们可以手动部署项目,并且直接支持部署到各种平台。

TIP

可以通过 vite-plugin-nitro-deploy 快速整合。

准备流程

安装 nitropack

sh
ni nitropack h3 defu

相关配置

json
{
  "prepare": "nitro prepare",
  "build": "vite build && nitropack build",
  "start": "node .output/server/index.mjs"
}
ts
import type { NitroConfig } from "nitropack"

export default defineNitroConfig({
  runtimeConfig: {
    // 后端 API 代理
    proxyTarget: "https://xxx.com/api/v1",
  },
  serverAssets: [
    {
      baseName: "dist",
      dir: "./dist",
    },
  ],
  publicAssets: [
    {
      dir: "./dist",
    },
  ],
})
json
{
  "include": ["routes", "api", "./.nitro/types/nitro.d.ts"]
}
ts
export default defineEventHandler(async (event) => {
  const index = await useStorage("assets/dist/").getItem("/index.html")
  appendHeader(event, "Content-Type", "text/html")

  return index as string
})
ts
export default defineEventHandler((event) => {
  const proxyTarget =
    (useRuntimeConfig(event)["proxyTarget"] as string) +
    getRequestURL(event).pathname

  return proxyRequest(event, proxyTarget)
})
Dockerfile
FROM node:18-alpine as production-stage

WORKDIR /app
COPY --from=build-stage /app/dist ./dist
COPY --from=build-stage /app/.output ./.output

CMD node .output/server/index.mjs

Next.js

Next.js 项目的部署主要分为 Vercel 托管以及自部署,参考官方文档给出的部署指南,其中需要注意自部署时一些额外的配置。