上一篇: 前端动画解决方案下一篇: 写一个模板引擎

前端 Makefile 自动化脚本配置


一切项目, 要形成规模都必须有规范来约束, 项目参与者都要遵守才能让项目持续发展。shell命令 的传参有它约定俗成的规范 —— 以 -- 开头表属性, - 开头表属性简称, 无 - 表属性值。在规范里, 任何未知模块的行为是我们可预测, 而我们也必须严格去遵守。

随着前端工程化的发展, 开发目录 src 和打包、预编译之后的 dist 目录是完全独立的两个目录, 为了防止发布导致代码冲突, 关闭 git 对 dist 跟踪, 发布时通常直接将生成的 dist 并用 scp 传到服务器上。

事实上, 不同公司流程会有些差异。 第一种流程大概是: 受保护的 master 分支的推送、合并权限仅掌握在 QA开发leader 手中, 一个迭代中, 开发人员先从 master 分支 checkout 出一个分支 dev 来开发, 开发完成之后合并到 test 分支并发布测试环境, 待 QA 验证, 然后修复bug, 再次验证, 通过后会将 dev 分支合并到 release 分支并发布到预发布环境并让 QA 验证, 通过后请求将 dev 分支合并到 master 分支, QA 允许合并之后就发布到正式环境并再次验证。如果发布线上之后遇到紧急问题就立马从 master 切出一个分支修复, 然后重复预发布和线上的流程

第二种情况通常是在规模较小, 流程不太完整的公司, 通常没有专业 QA 介入发布流程, 测试人员只负责功能测试, 通过后, 开发人员直接发布正式环境, 但这样不规范的流程难免导致发错分支、未拉取更新、本地未提交、未推送到远程以及冲突未解决问题。发布时如果检查不严格, 问题代码一旦发到线上将会导致事故, 承受巨大的损失! 其实这些检查也可以在 gulpfilegit hooksshell脚本 中做, 但用 Makefile 来做这些事会更加方便, 因为功能强大, 学习成本低, 方便拆分任务, 且内部可插入 shell 等特点 被我们经常使用, 从而大大提升了项目的检查、构建、发布、回滚效率, 简化了整个流程。

在这里, 分享我自己总结针对于第二种流程的 Makefile 发布最佳实践, 只是起一个抛砖引玉的作用, 读者完全可以是用 Makefile 对根据自己项目情况对流程做一个深度定制。

什么是 make?

类unix(linux, osx等)中, make 是一个常用于大型程序编译、链接、构建流程的自动化的工具, 可以通过 Makefile 来决定不同编译目标环境的依赖关系, 这样可以根据情况实现高效的自动化编译、局部重新编译。

命令行基础

所有命令都是 二进制文件 或 用 #! 开头的 纯文本文件纯文本文件 首行的 #! 后追加解析此文件的二进制程序路径。比如基于 python 的命令 pip 在文件的第一行是 #!/usr/bin/python, 就是告诉系统此脚本通过 /usr/bin/python 解析并执行。

假设现在有 /data/pip 文件, 用 chmod a+x /data/pip 对其添加 x 执行权限, 此时可直接输入 /data/pip 将其当命令执行, 此方式等价于 /usr/bin/python pip, 在执行命令时, 可用空格隔开传入多个参数, 例如 /data/pip jack tom 或者 /usr/bin/python pip jack tom, 其中, jack tom 是在执行命令的时候动态传入的参数, 内部可通过 argv[2], argv[3] 获取到动态参数, 并根据逻辑做进一步处理, 由此带来的命令高度可扩展性不言而喻!

由于用脚本的方式制作命令开发效率极高, 此方法常用来制作对性能要求不是很高的命令, 例如 ss, npm, webpack, gulp 以及各类脚手架。

环境变量

当我们在 shell 中执行 make 命令时, 系统会在环境变量 PATH 中去查找名叫 make 的二进制文件。当我们通过 echo $PATH 来打印 PATH 的值, 可能会得到如下结果:

echo $PATH

/usr/local/bin:/usr/bin:/bin

环境变量 PATH 其实是以 : 为分隔符由多个路径组合而成, 系统此时会分别在 /usr/local/bin, /usr/bin, /bin 中查找名为 make 的文件, 一旦找到就立即执行, 找不到则提示 -bash: make: 未找到命令类unix 系统中 make 内置于 /usr/bin 中, 因此可以直接使用 make 执行

如果要执行的二进制文件不在系统默认 PATH 指定目录中, 则需要用执行相对路径或绝对路径(如 /etc/nginx/sbin/nginx), 或者是将 /etc/nginx/sbin 添加到 PATH 中. 反正必须保证执行的二进制文件能找得到。

Makefile

由上可知, 在执行 make 的时候, 实际上是在执行 /usr/bin/make, make 二进制文件会读取当前工作目录(pwd 查看) 中的 Makefile 文件的 规则(rule) 并执行器包含的命令, 如果此规则依赖于其他文件, 则会先依次引入并执行依赖的文件。

规则的格式

target: dep [ dep2 ...]
    命令...

target 通常是要编译生成的目标文件名, 或是要构建的目标、任务等。 在下文, target 都将被当成一个任务。 dep 则是依赖的文件或任务。 命令 则是开发者设置当前任务要执行的命令(必须用 tab 缩进)。

简单 Makefile 例子:

edit: build
    tar -czf dist.zip dist

build:
    webpack

上面例子包含 editbuild 两个 target, 可以通过 make [target名] 执行对应的 target, 不传 target 默认执行第一个, make 默认会认为所有 target 都是要编译生成的文件, 所以 当我们执行 make edit 时, 系统回去查找当前目录是否已经存在名为 edit 的文件, 可以通过 .PHONY 显式声明执行的 target 为 纯任务, 也就是说此 target 不会生成同名的文件。

注意: make 在执行时默认认为 target 是目标生成文件, 所以会查找当前目录的同名文件是否已生成, 若存在, 则停止执行。 但如果我们显式声明了某个 target 为 .PHONY, 则执行 make 时不会去查找和 target 同名的文件

.PHONY: edit

edit 依赖于 build, 执行 make 时, 会先执行 build 这个子进程, 当 build 执行完成且返回码为 exit 0, 才会开始执行 edit 的任务内容

在任务(target)中, 每次只提取一句 shell 命令去执行, 在任务进程中, 还包含多个子进程, 它们的 环境变量工作目录 并不一样, 如果必须换行的话, 需要用到 \ 转义换行符

和 npm scripts 比较

对于前端开发, 通常将较长的命令放在 npm scripts 中, 每次执行诸如 npm run dev, npm run build 这类短命令很方便, 而且, 在执行 npm scripts 的某个任务时, 所开启的子进程中会注入当前项目的 node_modules/.bin 的路径到环境变量的 PATH 中, 因此即使全局没有安装 webpack, 只有项目中安装了也能直接找到。

可通过如下方法查看子进程中的环境变量 PATH

// package.json
"scripts": {
    "echo": "echo $PATH"
}
npm run echo

/Users/tom/Documents/test/node_modules/.bin:/usr/local/bin:/usr/bin:/bin

相比之下, make 不会自动对子进程环境变量的 PATH 添加当前项目下的 node_modules/.bin, 所以打包时要用 ./node_modules/.bin/webpack 指定依赖 webpack 的具体位置。 但 make 的强项在于它可以利用 Makefile 随意拆分任务, 结合自定义变量、自定义函数、自带函数以及复杂 shell 命令实现复杂操作, 极大地增强了构建和发布功能, 简化了发布流程, 并且可以结合一些打包工具, 注入当前版本号到代码中, 方便回滚。

结合 Makefile

其实我想做的事情是下面几个:

  1. master分支检查
  2. 检查是否有未提交代码, 以免代码中注入的版本号和当前开发版本号对不上
  3. 检查是否拉取远程更新, 以免他人提交到远程的代码没有拉取就被发到线上
  4. 打标记, 每次发布前我会添加一条注释类似于 正式环境发布 2018-5-10 20:00:00 的提交记录(即使代码无修改), 方便 git log 浏览最近发布记录, 同时方便将此版本号注入到代码中
  5. 自动打包、构建, 并将上一步 打标记 中的版本号获取到, 并使用 webpack.DefinePlugin 将此版本号注入到代码
  6. 发布到线上环境
  7. 自动push本地改动到远程仓库,

可以看出, 主要目的是 发布到线上环境, 因为它依赖的 1~5 步是串行执行的, 所以可以拆分为独立的子任务依次排列。

1. master分支检查

_branch:=$(shell git branch | grep -e '^*' | cut -c3-)

_checkMasterBranch:
    @if [ "$(_branch)" != 'master' ]; then echo "\n$(RED)请先切换到 master 分支再发布正式环境!$(NO)\n"; exit 1;fi

上面代码块中, 用到了 Makefile 中内置函数 $(shell 命令), 在其内部传入命令的 标准输出 会直接保存在 Makefile 的变量 _branch, 主要是先用 git branch 输出本地分支并结合 unix 管道符用 grep 的正则模式匹配, 将以 * 开头的分支名匹配出来, 然后裁剪出分支名, _branch 供后面使用(为什么是 :=, 而不是 =?= 可查文档), _checkMasterBranch 内部的命令会新起一个 shell 的子进程用, 一旦子进程抛出非0的退出码, 整个任务就抛出异常。

Makefile 的每个 target 默认会把即将执行的命令先打印在控制台, 可以在前面加一个 @ 来防止打印命令, 注意: 命令本身的 标准输出 不会影响

2. 检查是否有未提交代码

_checkFileTree:
    @if [ -n "`git status --porcelain`" ]; then echo "\n$(RED)请先提交本地代码再发布!$(NO)\n"; exit 1;fi

3. 检查是否拉取远程更新

必须先联网抓取远程更新, 但不会合并改变到当前分支, 然后可以通过 git branch -vv 可以查看到当前分支和远程对应分支领先落后情况, 通过 "behind" 关键字判断当前分支是否未处于最新状态

_checkRemote:
    @if [ -n "`git remote update &> /dev/null && git branch -vv | grep -e '^*' | grep behind`" ]; then echo "\n$(RED)请先拉去远程的更新$(NO)\n"; exit 1;fi

4. 打标记

在本地代码提交之后, 即使是代码一行不改动, 每次发布都必须单独创建一个空的提交来打个标记, 以便之后跟踪, 回滚。

_commitProduction:
    @git add .;\
    git commit --allow-empty -m '正式环境发布 $(_deploy_time)'

5. 自动打包、构建

这属于应用层面的命令, 需要根据项目实际情况修改, 比如 webpack 打包, 那么:

production:
    @rm -rf dist && NODE_ENV=production ./node_modules/.bin/webpack\
     --config build/webpack.config.js\
     --verbose -p\
     --progress\
     --define "process.env.info='name: $(_name), branch: $(_branch), commit: $(shell git rev-parse --verify HEAD | cut -c-10), time: $(_deploy_time)'"

注意: 最后一行通过 --define$(shell) 注入了当前发布人、发布分支、版本号、以及发布时间, 在线上出现紧急问题时, 这些信息对版本回滚的非常有用的

6. 发布到服务器上

发布过程通常都是使用 scp 命令, 先把本地发布目录 dist 压缩为 deploy.zip, 然后复制到服务器, 再用 ssh 连接并解压到目标目录。

考虑到流程稍微严格一点的公司会用到跳板机, 因此这里使用了代理相关的参数。 因为依赖关系, 上述 1~5 步骤需要放到此任务的依赖中

deploy-production: _checkMasterBranch _checkFileTree _checkRemote _commitProduction production
    @tar -czf $(zip_name) $(from) &&\
    echo "\n$(YELLOW)正在发送前端代码...$(NO)\n" &&\
    scp -o ProxyJump=$(js_host) -P $(pro_port) $(zip_name) $(pro_user)@$(pro_ip):$(pro_to) &&\
    ssh -J $(js_host) -t -p $(pro_port) $(pro_user)@$(pro_ip) "cd $(pro_to); tar -xzf $(zip_name) -C .; rm -rf $(zip_name);" &&\
    rm -rf $(zip_name) &&\
    git push &&\
    echo "\n\n$(GREEN)正式环境前端代码发布成功!$(NO)\n"

把上述步骤汇总到一个 Makefile 里应该是:

_deploy_time:=$(shell date +"%Y/%m/%d %H:%M:%S")
_name:=$(shell git config user.name)
_branch:=$(shell git branch | grep -e '^*' | cut -c3-)

NO=\x1b[0m
GREEN=\x1b[32;01m
RED=\x1b[31;01m
YELLOW=\x1b[33;01m

from=dist
zip_name=deploy.zip

# 跳板机 jump host
jh_port=22
jh_ip=100.50.100.200
jh_user=admin
js_host=$(jh_user)@$(jh_ip):$(jh_port)

# 线上服务器
pro_port=22
pro_ip=100.50.100.150
pro_user=admin
pro_to=/data/app/project

# 打包构建
production:
    @rm -rf $(from) && NODE_ENV=production ./node_modules/.bin/webpack\
     --config build/webpack.base.js\
     --verbose -p\
     --progress\
     --define "process.env.info='name: $(_name), branch: $(_branch), commit: $(shell git rev-parse --verify HEAD | cut -c-10), time: $(_deploy_time)'"

# 发布
deploy-production: _checkMasterBranch _checkFileTree _checkRemote _commitProduction production _compress
    @echo "\n$(YELLOW)正在发送前端代码...$(NO)\n" &&\
    scp -o ProxyJump=$(js_host) -P $(pro_port) $(zip_name) $(pro_user)@$(pro_ip):$(pro_to) &&\
    ssh -J $(js_host) -t -p $(pro_port) $(pro_user)@$(pro_ip) "cd $(pro_to); tar -xzf $(zip_name) -C .; rm -rf $(zip_name);" &&\
    rm -rf $(zip_name) &&\
    git push &&\
    echo "\n\n$(GREEN)正式环境前端代码发布成功!$(NO)\n"

# 压缩 dist
_compress:
    @tar -czf $(zip_name) $(from)

# 检查 master 分支
_checkMasterBranch:
    @if [ "$(_branch)" != 'master' ]; then echo "\n$(RED)请先切换到 master 分支再发布正式环境!$(NO)\n"; exit 1;fi

# 检查本地是否提交
_checkFileTree:
    @if [ -n "`git status --porcelain`" ]; then echo "\n$(RED)请先提交本地代码再发布!$(NO)\n"; exit 1;fi

# 检查远程是否未拉取
_checkRemote:
    @if [ -n "`git remote update &> /dev/null && git branch -vv | grep -e '^*' | grep behind`" ]; then echo "\n$(RED)请先拉去远程的更新$(NO)\n"; exit 1;fi

# 打点提交
_commitProduction:
    @git add .;\
    git commit --allow-empty -m '正式环境发布 $(_deploy_time)'


.PHONY: production deploy-production _checkMasterBranch _checkFileTree _checkRemote _commitProduction

上一篇: 前端动画解决方案下一篇: 写一个模板引擎