半自动化部署

2024/8/20 Devops

传统的前端部署往往需要经历几个阶段:

image-20240820090208460

​ 对于这一过程我们往往可以通过CI/CD方法进行优化。在没有用 Gitlab 或者 Jenkins 等工具之前,我们也可以通过在项目中增加一些脚本来实现自动打包部署的过程。虽然需要手动执行一些指令,但还是大大减少了工作量,也减少了因手动上传而出现一些错误。

# 自动更新版本号

​ 对于一些需要上传到私有 npm 镜像上的项目,每次发布时,我们都需要去更改 package.json 中的版本号,确保上传的版本号无重复。而一些 monorepo 项目里有很多个 package.json 文件,例如组件库项目,手动更改极其容易遗漏,无法保证版本号一致性,所以我们需要一个自动更新版本号的脚本。

  • changeVersion.ts
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const fsPromises = fs.promises;
const pathResolve = (target: string) => (...args: any) => path.resolve(__dirname, `../packages/${target}`, ...args)

/**
 * 修改所有packages的版本号
 */
async function changeVersion(): Promise<boolean | undefined> {
  const version = getVersion()
  // 输出提示信息
  console.log(chalk.blue('正在更改版本号:version ...'))
  // 获取项目根路径
  const rootPackages = path.resolve(__dirname, '../package.json')
  // 向项目根路径 package.json 文件写入版本号
  setPackagesVersion(rootPackages, version)
  // 获取项目子项目路径
  const projectPath = path.resolve(__dirname, '../packages')

  // 读取项目路径下的所有文件/文件夹
  const targets = await fsPromises.readdir(projectPath)

  // 遍历每个文件/文件夹
  for (let target of targets) {
    // 创建解析路径的函数
    const resolve = pathResolve(target)
    // 获取 package.json 的路径
    const packagePath = resolve('package.json')
    // 向 package.json 文件写入版本号
    setPackagesVersion(packagePath, version)
  }
  console.log(chalk.blue(`完成版本号更改,当前把版本号为: ${version}`))
  return true
}

/**
 * 获取当前版本号并加1
 * @returns 
 */
function getVersion() {
  const packagePath = path.resolve(__dirname, '../package.json');
  const packageJSON = require(packagePath);
  let versions = packageJSON.version.split('.');
  let lastVer = parseInt(versions.pop(), 10);
  lastVer = ++lastVer; // 使用前缀形式加1
  versions.push(lastVer.toString());
  return versions.join('.');
}

/**
 * 向 package.json 文件写入版本号
 * @param packagePath 
 * @param version 
 */
async function setPackagesVersion(packagePath: string, version: string) {
  // 读取 package.json 文件内容
  const packageJSON = require(packagePath)

  // 更新版本号
  packageJSON.version = version
  try {
    // 将更新后的 package.json 文件内容写回文件
    await fsPromises.writeFile(packagePath, JSON.stringify(packageJSON, null, 2));
  } catch (err) {
    console.log(chalk.red('版本号文件写入失败,请检查文件权限'))
  }

}


changeVersion()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  • 在项目最外层的 package.json 中加入 script:
  "scripts": {
          "changever": "ts-node scripts/changeVersion.ts",
  },
1
2
3

# 自动打包

​ 想要自动打包哪些项目,我们可以在项目最外层的 package.json 中去配置:

  "scripts": {
    "build": "pnpm run build:elp && pnpm run build:elu && pnpm run build:permission && pnpm run build:micro-app",
    "build:elp": "pnpm run -C packages/element-plus build",
    "build:elu": "pnpm run -C packages/element-ui build",
    "build:permission": "pnpm run -C packages/permission build",
    "build:micro-app": "pnpm run -C packages/micro-app build"
  },
1
2
3
4
5
6
7

​ 例如以上配置,执行pnpm run build命令时,就可以直接把 packages 中的 4 项目同时打包。对于组件库项目,打包完我们的项目后,还需要打包文档项目,配置步骤也是同样的,只不过 build 变成了 docs:build 而已。

# 自动上传FTP

​ 打包完成后,我们需要将生成的 dist 文件夹上传到 FTP 上,这个过程我们也可以借助脚本来实现。

# 检查分支

​ 为了确保测试服仅能上传测试分支代码,正式服仅能上传主分支代码,我们需要在上传前先对当前分支进行检查。(在这里,我们假设测试分支是 test,主分支是 main)

/**
 * 上传规范:
 * 正式服只能上传 prod 分支代码,测试服只能上传 test 分支代码。
 */
import fs from 'fs';  
import path from 'path';  
import utils from './utils.ts'

// 获取当前 git 分支名称
function getCurrentBranchName(p = process.cwd()) {
    const gitHeadPath = `${p}/.git/HEAD`

    return fs.existsSync(p)
        ? fs.existsSync(gitHeadPath)
            ? fs.readFileSync(gitHeadPath, 'utf-8').trim().split('/')[2]
            : getCurrentBranchName(path.resolve(p, '..'))
        : false
}

/**
 * git 分支检查
 * @returns 
 */
function getCheck() {
    // 获取当前 git 分支名称
    const currentBranchName = getCurrentBranchName()
    // 上传的服务器环境与 git 分支对应表: key 为上传的服务器环境, value 对应的 git 分支代码
    const envtoGitbranchMap = {
        'test': 'test', 
        'production': 'prod'
    }
    if (envtoGitbranchMap[utils.getNodeEnv()] !== currentBranchName) {
        console.error('💥请检查分支代码!!! 测试服仅能上传 test 分支代码,正式服仅能上传 prod 分支代码!💥')
        return false
    }
    return true
}

export default getCheck
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 配置 FTP 相关信息

# 获取执行脚本的 NODE_ENV 值

​ 首先,我们需要一个工具函数,用于获取执行脚本的 NODE_ENV 值,可以将其放在 utils.ts 文件中:

// 获取执行脚本的 NODE_ENV值
function getNodeEnv() {
    const script = process.env.npm_lifecycle_script;
    const reg = new RegExp("NODE_ENV=([a-z_\\d]+)");
    const result = reg.exec(script);
    return result[1];
};

export default {
    getNodeEnv
}
1
2
3
4
5
6
7
8
9
10
11

# 配置用户信息

​ 我们还需要配置上传 FTP 的账号密码、地址、文件夹等信息,可以将其放在 users.ts 文件中(为了避免信息泄露,记得要将它加入 .gitignore 中 ):

import path from 'path';  
import { fileURLToPath } from 'url';  
  
const __filename = fileURLToPath(import.meta.url);  
const __dirname = path.dirname(__filename);  
  
const userConfig = {  
    test:{
        user: '测试服用户名',  
        password: '测试服用户密码',  
        host:'测试服服务器地址',
        port:2021, // 测试服端口
        localRoot: path.resolve(__dirname, '../dist/test'),  
        remoteRoot: '/test' // 测试服文件夹 
    },
    production:{
        user: '正式服用户名',  
        password: '正式服用户密码',  
        host:'正式服服务器地址',
        port:2021, // 正式服端口
        localRoot: path.resolve(__dirname, '../dist/prod'),  
        remoteRoot: '/prod' // 正式服文件夹   
    },
};  
  
export default userConfig;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 整合

​ 有了前面的这些文件后,我们可以整合一下,写在一个 serverConfig.ts 文件中:

import path from 'path';  
import inquirer from "inquirer"
import userConfig from "./users.ts";
import { fileURLToPath } from 'url';  
import utils from "./utils.ts";
 
const __filename = fileURLToPath(import.meta.url);  
const __dirname = path.dirname(__filename);  

async function getInput() {
  const info = {
    user: null,
    password: null
  }
  await inquirer
    .prompt([
      {
        type: 'input',
        name: 'user',
        message: '请输入用户名,按回车确认:',
        validate: function (val) {
          return val ? true : "请输入非空字符串";
        }
      },
      {
        type: 'input',
        name: 'password',
        message: '请输入密码,按回车确认:',
        validate: function (val) {
          return val ? true : "请输入非空字符串";
        }
      }
    ])
    .then((answers) => {
      info.user = answers.user
      info.password = answers.password
    })
  return info
}

//获取上传配置信息
const getConfig = async function () {

  let users = {
    user: '',
    password: '',
    test: '',
    production: '',
    host: '',
    port: 2021,
    localRoot: path.resolve(__dirname, '../dist/test'),
    remoteRoot: '/test'
  }
  try {
    users = userConfig[utils.getNodeEnv()];
  } catch (err) {
    console.log('用户信息文件不存在 ', err)
  } finally {
    if (!users.user || !users.password) {
      users = await getInput()
    }
  }

  return users;
}

export default getConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 入口文件

​ 配置完 FTP 相关信息后,我们就可以写入口文件了,在一个 index.ts 文件中,写入以下内容:

import getFtpDeployConfig from "./config.ts";
import gitCheck from "./gitCheck.ts";
import FtpDeploy from "ftp-deploy";

const ftpDeploy = new FtpDeploy();
/**
 * 上传文件函数
 */
async function upload() {
  const config = await getFtpDeployConfig();
  ftpDeploy
    .deploy(config)
    .then((res) => console.log('finished:', res))
    .catch((err) => console.log(err))

  ftpDeploy.on('uploading', function (data) {
    console.log('total file count being transferred: ', data.totalFilesCount) // total file count being transferred
    console.log(data.transferredFileCount, ' of files transferred') // number of files transferred
    console.log('partial path with filename being uploaded:', data.filename) // partial path with filename being uploaded
  })

  ftpDeploy.on('uploaded', function (data) {
    console.log('uploaded: ', data) // same data as uploading event
  })

  ftpDeploy.on('upload-error', function (data) {
    console.log('upload error: ', data.err) // data will also include filename, relativePath, and other goodies
  })

}


if (gitCheck()) {
  upload()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 修改 package.json

​ 完成以上一系列操作后,我们就可以在 package.json 中添加 script 了:

  "scripts": {
    "upload:test": "cross-env NODE_ENV=test node --loader ts-node/esm ./deploy/index.ts",
    "upload:prod": "cross-env NODE_ENV=production node --loader ts-node/esm ./deploy/index.ts",
  },
1
2
3
4

# 集成

​ 可以通过一个命令,直接自动更新版本号,然后自动打包,再自动上传 FTP:

  "scripts": {
    "release": "pnpm run changever && pnpm run build && pnpm run docs:build && pnpm run upload:test",
  },
1
2
3
上次更新: 2024/8/20 02:18:47