写给前端的自动化部署攻略

​ 对于前端开发而言,静态页面的部署是容易的。只需要将打包好的页面,放入已经配置好的 nginx 或者 apache 的静态资源目录下即可。然而,重复的、机械化的打包、上传、部署工作,是无意义且令人厌烦的。

​ 自动化部署就是为了解决这一问题而存在的。

​ 很多重量级的软件 DockerJenkins 等,都提供了自动化部署的相关功能。但也为 自动化部署 流程,蒙上了一层神秘的面纱。其实,自动化部署并不是什么高深的技术,只要你掌握了一些基本的工具和原理,就可以轻松地实现自动化部署。

​ 因此,本文仅以前端开发最熟悉的 NodeJS 为基础,攻略 自动化部署 ,并使之能够运用到实际的项目中。希望能对大家有所帮助。

部署流程

​ 现在来回顾一下,前端页面的手动部署流程。

git 拉取最新代码 => npm 更新依赖 => 打包生成 dist => 登录服务器 => 上传文件到指定目录

​ 打包生成 dist , 通常是缓慢耗时、需要等待的。如果,打包出的文件数很多,我们还可能需要将文件 zip 压缩, 上传到服务器后再解压。

​ 手动部署通常是涉及本机文件和服务器交互的。自动化部署 中,我们可以将上述步骤全权交由服务器。

服务器部署流程:

git 拉取最新代码 => npm 更新依赖 => 打包生成 dist => 移动 dist 内容到 nginx 静态资源

服务器相关依赖:

  • git: 拉取源码
  • node/npm: 更新依赖与打包
  • nginx: 静态资源目录

Webhooks

​ 你可能已经发现了,如果服务器实现了上述流程。开发者只需要告诉服务器,什么时候跑这个流程。这就是全部。

​ 幸运的是 git 对于每个仓库,都提供了 webhooks 功能。简而言之,就是如果你对仓库做了某些操作,git 将通过 http 向你提供的地址发送操作信息(json).

要确保 git 平台 与 部署服务器 之间是可连通的,webhooks 才具有可行性

gitlab 为例:

  • trigger: 这里选中了 push events , pattern 填入了 master. 当 master 分支 提交事件发生时,触发该 webhook

  • url: 发送信息到 http://192.168.110.130:3000/deploy/zz-platform-config

  • secret token: 可以输入一个和部署服务器约定的 hash 值, 用作校验。

这里演示使用的 gitlab、部署服务器 都使用的是内网。当然,你也可以使用公网的配置,只要确保 git 平台 与 部署服务器 之间可连通。

如果你使用的也是内网环境,需要打开 webhooks 对本机地址的允许

服务端应用程序

​ 接下来创建服务端应用程序,监听 3000 端口,接受 webhooks 将会发送的信息。

​ 这里以 NestJS 为例

Controller

​ 这个应用程序将部署在 192.168.110.130 服务器上

webhooks 操作信息将发送到 http://192.168.110.130:3000/deploy/zz-platform-config

整理一下思路之后,一个 DeployController 的实现如下:

ts 复制代码
@Controller('deploy')
export class DeployController {
  constructor (private readonly deployService: DeployService) {}

  @Post('zz-platform-config')
  async deployZzPlatformConfig (
    @Body() body: GitlabPushEvent,
    @Headers('X-Gitlab-Token') token: string,
  ) {

    // 1. 校验 Secret token 是否正确
    if (token !== '123456') return {
      code: 401,
      message: 'Unauthorized',
    }

    // 2. 通过 git 下载代码到本地 git clone
 	const { dir: codeDir } = await this.deployService.cuCode(body)


    // 3. 更新代码的依赖 pnpm i 
    await this.deployService.uDependencies(codeDir)
        
    // 4. 获取部署信息,如 打包命令名称、产物部署目录 等,
    const deployConfigs = await this.deployService.rDeployConfigs(codeDir, body)


    // 5. 根据部署信息,运行打包脚本,将打包产物拷贝至静态资源目录
    for (const deployConfig of deployConfigs) {
      await this.deployService.deploy(codeDir, deployConfig)
    }

    return {
      code: 200,
      message: 'ok',
    }
  }

}

Service

逐一分析实现之前,介绍一些重要的变量 \ 方法 \ 接口

ts 复制代码
export const workRoot = '[当前部署应用程序的根目录]'
// 代码下载到 codes 目录下
export const codesRoot = path.resolve(workRoot, 'codes')

// webhook 发送的信息 
export interface GitlabPushEvent {
  project: {
    // @example rd/platform/zz-platform-config
    path_with_namespace: string
  }
  repository: {
    // @example http://192.168.110.138:30080/rd/platform/zz-platform-config.git
    git_http_url: string
  }
}
ts 复制代码
import { spawn } from 'node:child_process'

export const run = async (
  script: string, 
  cwd?: string,
) => { // 使用这个方法, 调用脚本命令
  return new Promise((resolve) => {
    const [cmd, ...args] = script.split(' ')

    // 在node中使用子进程运行脚本
    const app = spawn(cmd, args, {
      cwd,
      stdio: 'inherit',
      shell: true,
      
    })

    app.on('close', resolve)
  })
}

run 是实现程序中运行 cmd 命令的重要方法。

更新代码

ts 复制代码
/**
   * 创建/更新代码 
   * @param body 
   */
  async cuCode (body: GitlabPushEvent) { 
    // 如果 codesRoot 不存在, 创建
    if (!fs.existsSync(codesRoot)) {
      fs.mkdirSync(codesRoot, { recursive: true })
    }

    // 检查仓库是否已经拉取过
    const codeDir = path.resolve(
      codesRoot,
      
      body.project.path_with_namespace,
    )

    if (
      // 文件夹存在
      fs.existsSync(codeDir)
      // 并且文件夹为空
      && !fs.readdirSync(codeDir).length
    ) {
      // 清除该文件夹
      fs.rmdirSync(codeDir)
    }


    if (
      // 文件夹不存在
      !fs.existsSync(codeDir)
    ) {
      /**
       * git clone 
       * @example
       * git clone http://192.168.110.138:30080/rd/platform/zz-platform-config.git rd/platform/zz-platform-config
       */
      await run(`git clone ${body.repository.git_http_url} ${body.project.path_with_namespace}`, codesRoot)
    } else {
      // 文件夹存在, 且不为空
      // 放弃当前修改
      await run('git reset --hard', codeDir)
      // 拉取最新代码
      await run('git pull', codeDir)
    }

    return {
      dir: codeDir,
    }

    
  }

上述函数将会把目标仓库的源码下载到本地指定的目录中。函数返回了 dir , 即源码的根目录,方便我们后续使用。

这是一个执行 cuCode 之后的工作目录,在这个示例中 dir = codes/rd/platform/zz-platform-config .

更新依赖

使用 pnpm 下载/更新依赖,需要部署环境中里提前安装 pnpm

ts 复制代码
  /**
   * 更新依赖
   */
  async uDependencies (codeDir: string) {
    // 安装依赖
    await run('pnpm i', codeDir)
  }

部署配置

为了使,部署服务的通用性更强,考虑在目标仓库(源码)中,放入一个json ,来描述如何部署这个项目

json 复制代码
{
    "scripts": [
      // 依次执行命令,最终生成打包产物
      "build:skzz" // npm run build:skzz
    ],
    "path": {
       // 打包产物所在目录
      "dist": "./app/dist/zz/zz-platform-config",
        
       // 静态资源目录
      "html": "/home/nginx/html/zz/zz-platform-config"
    }
}
ts 复制代码
  /**
   * 获取部署配置
   */
  async rDeployConfigs (codeDir: string, body: GitlabPushEvent) {

    let deployList = [{}] as DeployConfig[]
    if (fs.existsSync(
      path.resolve(codeDir, 'deploy.config.json'),
    )) {
      const jsonObj = JSON.parse(
        fs.readFileSync(
          path.resolve(codeDir, 'deploy.config.json'),
          'utf-8',
        ),
      )
      if (Array.isArray(jsonObj)) {
        deployList = jsonObj
      } else {
        deployList = [jsonObj]
      }
    }
    
    return deployList.map(deployConfig => {
      const scripts = deployConfig.scripts || []
      const pathDist = deployConfig.path?.dist ?? './dist'

      // prod: deployConfig.path?.html ??
      const pathHtml = deployConfig.path?.html ?? path.resolve(workRoot, 'nginx/html', body.project.path_with_namespace)

      return {
        scripts,

        path: {
          // 从 dist 中取出所有文件
          dist: path.resolve(codeDir, pathDist),

          // 将取出的文件放入 nginx 的指定目录下
          html: pathHtml,
        },
      }
    })
 
  }

可能一个目标仓库中,存在多个应用需要部署,例如文档和应用本身。这里处理成数组,增强健壮性。

部署

ts 复制代码
  async deploy (codeDir: string, deployInfo: DeployConfig) {
    // 如果 dist 文件夹存在,清空文件夹
    if (fs.existsSync(deployInfo.path.dist)) {
      await fs.promises.rm(
        deployInfo.path.dist, 
        { recursive: true },
      )
    }
  
  
    // 执行构建命令
    for (const script of deployInfo.scripts) {
      await run(`npm run ${script}`, codeDir)
    }
  
    // 如果 html 文件夹存在,重名文件夹【自动备份】
    if (fs.existsSync(deployInfo.path.html)) {
      await fs.promises.rename(
        deployInfo.path.html,
        deployInfo.path.html + '.' + new Date().toISOString(),
      )
    }
  
  
    // 将 dist 下所有文件放入 html 下
    consola.log('开始拷贝文件')
    await fs.promises.cp(
      deployInfo.path.dist,
      deployInfo.path.html,
      {
        recursive: true,
      },
    )
    consola.success('部署成功')
  }

根据部署配置,执行命令,转移产物,即部署成功。

:::warning

这里展示了一个最基础的部署实现。当然,可以根据实际的需求,去调整代码。这一过程是自由的。

举个例子,如果认为 deployInfo.path.html ,完全由目标仓库决定是有安全风险的,则使用 静态 或者 静态+动态拼接 的方式决定。

:::

服务器中启动

安装必要依赖

  • git

  • node >= 18.0.x

  • pnpm - npm i pnpm -g

配置git免登

推荐使用 SSH 密钥或访问令牌(Personal Access Token)来进行身份验证。

也有一种简单的设置,运行以下命令,设置全局凭证存储:

shell 复制代码
git config --global credential.helper store

这会在用户主目录下的 .git-credentials 文件中存储明文的用户名和密码。请注意,这并不是最安全的方式,因为凭证以明文形式存储在文件中。

执行一次 Git 操作(如克隆或拉取),并在提示输入用户名和密码时输入你的 GitLab 用户名和密码。

接下来的 Git 操作将不再要求输入用户名和密码,因为它们已经存储在 .git-credentials 文件中。

但由于密码以明文形式存储在文件中,它并不是最安全的方式。

后台启动 nest 服务

shell 复制代码
# 服务器中拉取 部署应用 安装依赖

# 后台启动 nest 服务
# 在命令末尾添加 & 可以将命令放入后台运行 日志默认放入当前文件夹
$ nohup npm run start &

# 查看日志 
$ tail -f nohup.out

# 查看进程
$ ps aux | grep "npm"

服务启动后,接受到提交事件,开始打包部署。

​ 本文涉及的代码@simple-deploy

​ 通过本文的实践,我们可以看到,自动化部署并不是一个遥不可及的技术,而是通过合理利用现有工具和技术,结合适当的编程实践就能实现的过程。通过配置 webhooks 触发部署事件,服务器端应用程序能够自动处理从代码更新到部署的整个流程,包括代码拉取、依赖安装、打包、以及将打包好的文件放入资源目录。

​ 这些理解和技能的转移,可以帮助我们更深入地理解自动化部署的本质,以及如何在更复杂的系统中实施自动化部署策略。对将来接触和使用 DockerJenkins 也有着重要的启示和补充意义。

相关推荐
Marst Code几秒前
(Django)初步使用
后端·python·django
代码之光_19807 分钟前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长20 分钟前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记41 分钟前
DataX+Crontab实现多任务顺序定时同步
后端
安冬的码畜日常44 分钟前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
姜学迁2 小时前
Rust-枚举
开发语言·后端·rust
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范