传统的前端部署往往需要经历几个阶段:
对于这一过程我们往往可以通过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()
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",
},
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"
},
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
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
}
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;
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
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()
}
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",
},
2
3
4
# 集成
可以通过一个命令,直接自动更新版本号,然后自动打包,再自动上传 FTP:
"scripts": {
"release": "pnpm run changever && pnpm run build && pnpm run docs:build && pnpm run upload:test",
},
2
3