- 前端项目交付流程中需要考量的点能让我们更有大局观
- npm script视频链接: https://pan.baidu.com/s/1gfeZ619 密码: xx8j
前端工作流
- 代码风格检查
- 单元测试
- 测试运行
- 覆盖率收集
- 覆盖率查
软件工程师做的事情基本都是在实现自动化,比如:
- 各种业务系统是为了业务运转的自动化
- 部署系统是为了运维的自动化
- 对于开发者本身,自动化也是提升效率的关键环节,实际开发过程中也有不少事情是可以自动化的
- npm script 依赖 package.json
- Google Trends https://trends.google.com/trends/explore
- npm init 初始化 package.json
- 包名称、版本号、作者信息、入口文件、仓库地址、许可协议等,多数问题已经提供了默认值
- npm init -y 跳过参数,直接创建 package.json
mkdir npm-demo & cd npm-demo
npm init
npm init -f # --force的意思
npm init -y # --yes
将默认配置和 -f 参数结合使用,能让你用最短的时间创建 package.json
npm init
package name: (npm-script)
version: (0.1.0)
description: hello npm script
entry point: (index.js)
test command:
git repository:
keywords: npm, script
license: (MIT)
package.json
{
"name": "npm-script",
"version": "0.1.0",
"description": "npm script",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"npm",
"script"
],
"author": "",
"license": "MIT"
}
npm config set init 修改 package.json的配置
npm config set init.author.email "lulongwen@live.com"
npm config set init.author.name "lulongwen"
npm config set init.author.url "http://github.com/lulongwen"
npm config set init.license "MIT"
npm config set init.version "0.1.0"
npm run test
运行项目测试
npm run test // 简写为 npm test,或 npm t
npm test
npm t
npm cache 清除缓存
npm cache clean
npm cache clean --force // 强制清除
npm内置命令
npm start
npm test
npm 是如何管理和执行各种 scripts?
- npm run 是 npm run-script的简写
- 当我们运行 npm run xxx 时,步骤如下
- 从 package.json 文件中读取 scripts 对象里面的全部配置
- 以传给 npm run 的第一个参数作为键,本例中为 xxx,在 scripts 对象里面获取对应的值作为接下来要执行的命令,如果没找到直接报错
- 在系统默认的 shell 中执行上述命令,系统默认 shell 通常是 bash
- npm run 原理:npm 在执行指定 script 之前会把 node_modules/.bin 加到环境变量 $PATH 的前面
- 任何内含可执行文件的 npm 依赖都可以在 npm script 中直接调用
- 你不需要在 npm script 中加上可执行文件的完整路径,比如
./node_modules/.bin/eslint **.js
自定义 npm script
package.json如下:
{
"name": "npm-script",
"devDependencies": {
"eslint": "latest"
},
"scripts": {
"eslint": "eslint **.js"
}
}
不带任何参数执行 npm run,会列出可执行的所有命令,比如下面这样
Available scripts in the myproject package:
eslint
eslint **.js
运行 npm run eslint,npm 会在 shell 中运行 eslint **.js
eslint
- vue的 eslint:eslint-plugin-vue https://github.com/vuejs/eslint-plugin-vue
- react eslint
- eslint-plugin-react https://github.com/yannickcr/eslint-plugin-react
- eslint-plugin-react-native https://github.com/Intellicode/eslint-plugin-react-native
- eslint-config-airbnb 内置了 eslint-plugin-react https://www.npmjs.com/package/eslint-config-airbnb
- 存放规则集的文件就是配置文件,
./node_modules/.bin/eslint --init
- 把 eslint 安装为项目依赖而非全局命令,项目可移植性更高
- eslint 内置了代码风格自动修复模式
--fix
.eslintrc*
module.exports = {
env: {
es6: true,
node: true,
},
extends: 'eslint:recommended',
rules: {
indent: ['error', 2],
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
semi: ['error', 'always'],
},
}
peerDependencies 安装失败问题可参照 npmjs 主页上的如下方法解决
(
export PKG=eslint-config-airbnb;
npm info "$PKG@latest" peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev "$PKG@latest"
)
给 npm script 传递参数以减少重复的 npm script
—fix,—是分隔符;要给 npm run lint:js
实际指向的命令传递额外的参数
{
"lint:js": "eslint *.js",
"lint:js:fix": "eslint *.js --fix",
}
npm run lint:js -- --fix
不想单独声明 lint:js:fix
命令,在需要的时候直接运行: npm run lint:js -- --fix
来实现同样的效果
eslint代码检查
- eslint 定制的 js 代码检查 https://eslint.org/
- stylelint 样式文件检查,支持 css、less、scss https://stylelint.io/
- jsonlint json文件语法检查 https://github.com/zaach/jsonlint
- markdownlint-cli https://github.com/igorshubovych/markdownlint-cli
{
"name": "npm-script",
"version": "0.1.0",
"main": "index.js",
"scripts": {
"lint:js": "eslint *.js",
"lint:css": "stylelint *.less",
"lint:json": "jsonlint --quiet *.json",
"lint:markdown": "markdownlint --config .markdownlint.json *.md",
"test": "mocha tests/"
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^4.11.0",
"jsonlint": "^1.6.2",
"markdownlint-cli": "^0.5.0",
"mocha": "^4.0.1",
"stylelint": "^8.2.0",
"stylelint-config-standard": "^17.0.0"
}
}
运行多个 npm命令
- npm-run-all
- 用
&&
符号把多条 npm script 按先后顺序串起来,&& 串行- 把子命令的运行从串行改成并行,把连接多条命令的
&&
符号替换成&
即可,& 并行
- 把子命令的运行从串行改成并行,把连接多条命令的
- 串行执行的时候如果前序命令失败(通常进程退出码非0),后续全部命令都会终止执行
- & wait: 加上 wait 的额外好处是:子命令中启动了长时间运行的进程,可以
ctrl + c
结束进程 - npm run all 代替 & await
- & wait: 加上 wait 的额外好处是:子命令中启动了长时间运行的进程,可以
"scripts": {
// 串行
"test": "npm run lint:js && npm run lint:css && npm run lint:json && npm run lint:markdown && mocha tests/",
// 并行
"test": "npm run lint:js & npm run lint:css & npm run lint:json & npm run lint:markdown & mocha tests/"
}
// 执行顺序
eslint ==> stylelint ==> jsonlint ==> markdownlint ==> mocha
npm run all
https://github.com/mysticatea/npm-run-all/blob/HEAD/docs/npm-run-all.md
npm i npm-run-all -D
package.json
{
"test": "npm-run-all lint:js lint:css lint:json lint:markdown mocha",
// npm-run-all 还支持通配符匹配分组的 npm script,以上可以修改为 *
"test": "npm-run-all lint:* mocha",
// 让多个 npm script 并行执行
"test": "npm-run-all --parallel lint:* mocha"
}
让多个 npm script 并行执行, —parallel
并行执行的时候,并不需要在后面增加 & wait
,因为 npm-run-all 已经帮我们做
{
"test": "npm-run-all lint:* mocha",
"test": "npm-run-all --parallel lint:* mocha"
}
在根目录的,最顶部引入 ployfill
import '@babel/ployfill'
npm命令
# 查看npm全局安装过的包
npm list -g --depth 0
npm fund # 捐赠
npm script 传递参数, —
npm run lint:js -- --fix
不想单独声明 lint:js:fix
命令,在需要的时候直接运行: npm run lint:js -- --fix
来实现同样的效果
npm script 添加注释
- json 天然是不支持添加注释的,trick写法
- 增加
//
为键的值,注释就可以写在对应的值里面,npm 会忽略这种键,缺点:- npm run 列出来的命令列表不能把注释和实际命令对应上,在命令前面加上注释
{
"//": "运行所有代码检查和单元测试",
"test": "npm-run-all --parallel lint:* mocha"
}
- CLI的本质是 shell 命令
- 注意:注释后面的换行符
\n
和多余的空格,- 换行符是用于将注释和命令分隔开,这样命令就相当于微型的 shell 脚本
- 多余的空格是为了控制缩进,也可以用制表符
\t
替代
- 能让 npm run 列出来的命令更美观,但是 scripts 声明阅读起来不那么整齐
- 注意:注释后面的换行符
{
"test": "# 运行所有代码检查和单元测试 \n npm-run-all --parallel lint:* mocha"
}
- 更好的做法: 把复杂的命令抽离到单独的文件中管理
npm script日志输出
运行 npm script 出现问题时,要要有能力去调试
- 默认的日志输出:不加任何日志控制参数得到的输出
- 显示尽可能少的有用信息
- 结合其他工具调用 npm script 的时候比较有用,—loglevel silent,或 —silent 控制
- 显示尽可能多的运行时状态
- 排查脚本问题的时候比较有用,需要使用
--loglevel verbose
,或者--verbose
- 排查脚本问题的时候比较有用,需要使用
npm script钩子
npm命令的执行增加了类似生命周期的机制
- pre
- post
- 在某些操作前需要做检查、某些操作后需要做清理的情况下非常有用
- 举例来说,运行 npm run test 的时候,分 3 个阶段:
- 检查 scripts 对象中是否存在 pretest 命令,如果有,先执行该命令;
- 检查是否有 test 命令,有的话运行 test 命令,没有的话报错;
- 检查是否存在 posttest 命令,如果有,执行 posttest 命令
覆盖率收集
改造 test
- 基于钩子机制对现有的 scripts 做以下 3 点重构,把代码检查和测试运行串起来
- 增加简单的 lint 命令,并行运行所有的 lint 子命令;
- 增加 pretest 钩子,在其中运行 lint 命令;
- 把 test 替换为更简单的
mocha tests/
"lint": "npm-run-all --parallel lint:*",
"lint:js": "eslint *.js",
"lint:js:fix": "npm run lint:js -- --fix",
"lint:css": "stylelint *.less",
"lint:json": "jsonlint --quiet *.json",
"lint:markdown": "markdownlint --config .markdownlint.json *.md",
- "mocha": "mocha tests/",
- "test": "# 运行所有代码检查和单元测试 \n npm-run-all --parallel lint:* mocha"
+ "pretest": "npm run lint",
+ "test": "mocha tests/",
运行 npm test 的时候,会先自动执行 pretest 里面的 lint
增加覆盖率收集
- 把运行测试和覆盖率收集串起来
- 做法:增加覆盖率收集的命令,并且覆盖率收集完毕之后自动打开 html 版本的覆盖率报告
- 覆盖率收集工具 nyc,是覆盖率收集工具 istanbul 的命令行版本,istanbul 支持生成各种格式的覆盖率报告
- 衡量测试效果的重要指标是:测试覆盖率
- 打开 html 文件的工具 opn-cli,是能够打开任意程序的工具 opn 的命令行版本
npm i nyc opn-cli -D
- 然后在 package.json 增加 nyc 的配置,告诉 nyc 该忽略哪些文件。最后是在 scripts 中新增 3 条命令:
- precover,收集覆盖率之前把之前的覆盖率报告目录清理掉;
- cover,直接调用 nyc,让其生成 html 格式的覆盖率报告;
- postcover,清理掉临时文件,并且在浏览器中预览覆盖率报告
- 运行 npm run cover
{
" scripts":{
"precover":"rm -rf coverage",
"cover":"nyc --reporter=html npm test",
"postcover":"rm -rf .nyc_output && opn coverage/index.html"
},
"devDependencies":{
"npm-run-all":"^4.1.2",
"nyc":"^11.3.0",
"opn-cli":"^3.1.0",
"stylelint":"^8.2.0",
"stylelint-config-standard":"^17.0.0"
},
"nyc":{
"exclude":[
"**/*.spec.js",
".*.js"
]
}
}
环境变量
$PATH
正在执行的命令、包的名称和版本号、日志输出的级别
预定义变量
自定义变量
变量的使用方法遵循 shell 里面的语法,直接在 npm script 给想要引用的变量前面加上 $
符号即可
{
"dummy": "echo $npm_package_name"
}
postcover 做了 3 件事情:
npm run cover:archive
,归档本次覆盖率报告;npm run cover:cleanup
,清理本次覆盖率报告;opn coverage_archive/$npm_package_version/index.html
,直接预览覆盖率报告
npm 跨平台的兼容性
- linux, max兼容,window不兼容;不是所有的 shell 命令都是跨平台兼容的
- window建议使用 git bash 来运行 npm script,自带的 cmd 可能会遇到比较多的问题**
- npm script 的跨平台兼容注意点
- 所有使用引号的地方,建议使用双引号,并且加上转义
- 没做特殊处理的命令比如 eslint、stylelint、mocha、opn 等工具本身都是跨平台兼容的
- 使用 Linux 做开发
- NODE_ENV=test,NODE_ENV是 node的环境变量
cross-env 设置环境变量
cross-env 来实现 npm script 的跨平台兼容
npm install cross-env --save-dev
package.json
"scripts": {
- "test": "NODE_ENV=test mocha tests/",
+ "test": "cross-env NODE_ENV=test mocha tests/",
},
文件系统操作的跨平台兼容
- 跨平台兼容的包,npmjs.com上搜索 cross platform
- npm script涉及 目录的创建、删除、移动、复制等操作
- rimraf 或 del-cli,用来删除文件和目录,实现类似于
rm -rf
的功能; - cpr,用于拷贝、复制文件和目录,实现类似于
cp -r
的功能; - make-dir-cli,用于创建目录,实现类似于
mkdir -p
的功能
- rimraf 或 del-cli,用来删除文件和目录,实现类似于
pm install rimraf cpr make-dir-cli --save-dev
修改 package.json兼容
"scripts": {
- "cover:cleanup": "rm -rf coverage && rm -rf .nyc_output",
- "cover:archive": "cross-var \"mkdir -p coverage_archive/$npm_package_version && cp -r coverage/* coverage_archive/$npm_package_version\"",
+ "cover:cleanup": "rimraf coverage && rimraf .nyc_output",
+ "cover:archive": "cross-var \"make-dir coverage_archive/$npm_package_version && cpr coverage/* coverage_archive/$npm_package_version -o\"",
"cover:serve": "cross-var http-server coverage_archive/$npm_package_version -p $npm_package_config_port",
"cover:open": "cross-var opn http://localhost:$npm_package_config_port",
- "postcover": "npm-run-all cover:archive cover:cleanup --parallel cover:serve cover:open"
+ "precover": "npm run cover:cleanup",
+ "postcover": "npm-run-all cover:archive --parallel cover:serve cover:open"
},
rm -rf
直接替换成rimraf
;mkdir -p
直接替换成make-dir
;cp -r
的替换需特别说明下,cpr
默认是不覆盖的,需要显示传入-o
配置项,并且参数必须严格是cpr <source> <destination> [options]
的格式,即配置项放在最后面;- 把
cover:cleanup
从postcover
挪到precover
里面去执行,规避cpr
没归档完毕覆盖率报告就被清空的问题 - 任何改动之后记得重新运行 npm run cover,确保所有的 npm script 还是按预期工作的
cross-var 引用变量
- 使用内置和预定义变量减少代码重复代码
- 用 cross-var 实现跨平台的变量引用
- Linux 用
$npm_package_name
- Windows 用
%npm_package_name%
- Linux 用
- 安装 cross-var
npm install cross-var --save-dev
修改 package.json
"scripts": {
"cover:cleanup": "rm -rf coverage && rm -rf .nyc_output",
- "cover:archive": "mkdir -p coverage_archive/$npm_package_version && cp -r coverage/* coverage_archive/$npm_package_version",
- "cover:serve": "http-server coverage_archive/$npm_package_version -p $npm_package_config_port",
- "cover:open": "opn http://localhost:$npm_package_config_port",
+ "cover:archive": "cross-var \"mkdir -p coverage_archive/$npm_package_version && cp -r coverage/* coverage_archive/$npm_package_version\"",
+ "cover:serve": "cross-var http-server coverage_archive/$npm_package_version -p $npm_package_config_port",
+ "cover:open": "cross-var opn http://localhost:$npm_package_config_port",
"postcover": "npm-run-all cover:archive cover:cleanup --parallel cover:serve cover:open"
},
直接在原始命令前增加 cross-var 命令即可
cover:archive 内含了两条子命令,我们需要用引号把整个命令包起来(注意这里是用的双引号,且必须转义),然后在前面加上 cross-var
引入 cross-var 之后,竟然还安装了 babel,如果想保持依赖更轻量的话,用 cross-var-no-babel。
npm script实战
监听文件变化并自动运行 npm script
- 代码检查自动化,要借助 onchange 工具包来实现,
- 因为: stylelint、eslint、jsonlint 不全支持 —watch 模式
- onchange 可以方便的让我们在文件被修改、添加、删除时运行需要的命令
- onchange在运行指定命令之前,会输出哪个文件发生了哪些变化
安装 onchange
npm install onchange --save-dev
yarn add onchange -D
package.json添加 watch:lint 和 watch 两个子命令
+ "watch": "npm-run-all --parallel watch:*",
+ "watch:lint": "onchange -i \"**/*.js\" \"**/*.less\" -- npm run lint",
"watch:test": "npm t -- --watch",
watch:lint
里面的文件匹配模式可以使用通配符,但是模式两边使用了转义的双引号,做跨平台兼容的;watch:lint
里面的-i
参数是让 onchange 在启动时就运行一次--
之后的命令,即代码没变化的时候,变化前后的对比大多数时候还是有价值的- watch 命令实际上是使用了 npm-run-all 来运行所有的 watch 子命令
- watch使用了跨平台的文件系统监听包 chokidar
- 你能基于 chokidar 做点什么有意思的事情呢?
单元测试自动化
- 运行 npm run watch:test
- 进程并没有退出,接下来尝试去修改测试代码,测试是不是自动重跑了!
"test": "cross-env NODE_ENV=test mocha tests/",
+ "watch:test": "npm t -- --watch",
"cover": "node scripts/cover.js",
live-reload自动刷新
- 前端开发,实际上最浪费时间的操作是什么?就是刷新页面
- 要让变更生效,需要重新加载,刷新页面的操作就变成了重复低效的操作
- create-react-app刷新用的是
- LiveReload的缺点:刷新页面意味着客户端状态的全部丢失,HMR、HR 都是基于 liveReload的优化
- Hot Module Replacement HMR,vue-cli用的就是 HMR提高效率
npm install livereload http-server --save-dev
yarn add livereload http-server -D
添加 npm script,client 命令能同时启动 livereload 服务、静态文件服务
运行 npm run client
- "cover:open": "scripty"
+ "cover:open": "scripty",
+ "client": "npm-run-all --parallel client:*",
+ "client:reload-server": "livereload client/",
+ "client:static-server": "http-server client/"
为什么启动2个服务?
- http-server 启动的是静态文件服务器,启动后可以通过 http 的方式访问文件系统上的文件
- livereload 启动了自动刷新服务,负责监听文件系统变化,并在文件系统变化时通知所有连接的客户端
index.html
中嵌入的那段 js 实际上是和 livereload-server 连接的一个 livereload-client
- index.html
<body>
<h2>LiveReload Demo</h2>
<script>
document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] +
':3579/livereload.js?snipver=1"></' + 'script>')
</script>
</body>
嵌入的 script,通过 判断 location.hostname 去检查当前页面运行环境,如果是线上环境就不嵌入了
或者使用打包工具处理 html 文件,上线前直接去掉即可
node配置自动检测文件改变不重启
npm install -g nodemon
nodemon ./bin/www
或者在npm start命令里把node改为nodemon
git hooks中运行 npm script
pre
、post
的钩子机制,叫做 Git Hooks- 钩子机制能让我们在代码 commit、push 之前(后)做自己想做的事情
- 通过 npm script 为本地仓库配置了 pre-commit、pre-push 钩子检查
- 本地检查:为了尽早给提交代码的同学反馈,哪些地方不符合规范,哪些地方需要注意
- pre-commit、pre-push
- 远程检查 Remotes:为了确保远程仓库收到的代码是符合团队约定的规范的
- pre-receive
--no-verify
(简写为-n
) 参数可以跳过本地检查
- 本地检查:为了尽早给提交代码的同学反馈,哪些地方不符合规范,哪些地方需要注意
- npm script 和 git-hooks 的方案
- husky 支持更多的 Git Hooks 种类,再结合 lint-staged 试用就更溜
- pre-commit
husky
npm install husky lint-staged --save-dev
yarn add husky lint-staged -D
package.json
"scripts": {
"precommit": "npm run lint",
"prepush": "npm run test",
"lint": "npm-run-all --parallel lint:*",
"lint:js": "eslint *.js",
"test": "jest",
"format": "prettier --single-quote --no-semi --write **/*.js",
"install": "node ./bin/install.js",
"uninstall": "node ./bin/uninstall.js"
},
install 就是你在项目中安装 husky 时执行的脚本(所有的魔法都藏在在这里了
检查仓库的 .git/hooks
目录
ls .git/hooks
cat .git/hooks/pre-commit
lint-staged 改进 pre-commit
- lint 每次提交代码会检查所有的代码,比较慢就不说了,初期 lint 工具可能会报告几百上千个错误,让人崩溃
- lint-staged 来缓解这个问题,每个团队成员提交的时候,只检查当次改动的文件
"scripts": {
- "precommit": "npm run lint",
+ "precommit": "lint-staged",
"prepush": "npm run test",
"lint": "npm-run-all --parallel lint:*",
},
+ "lint-staged": {
+ "*.js": "eslint",
+ "*.less": "stylelint",
+ "*.css": "stylelint",
+ "*.json": "jsonlint --quiet",
+ "*.md": "markdownlint --config .markdownlint.json"
+ },
尝试提交这个文件:git commit -m 'add eslint error' index.js
如果 husky 的 pre-commit 钩子执行失败,提交也就没有成功
husky 和 lint-staged 构建超溜的代码检查工作流 https://juejin.im/post/6844903479283040269
npm script构建流水线
- 部署前最关键的环节就是构建,构建环节要完成的事情:
- 源代码预编译:比如 less、sass、typescript;
- 图片优化、雪碧图生成;
- JS、CSS 合并、压缩;
- 静态资源加版本号和引用替换;
- 静态资源传 CDN 等
- 大多数项目构建都是脚手架配置好的,但要知道构建过程的原理
- 构建过程必须遵循下面的步骤
- 压缩图片;
- 编译 less、压缩 css;
- 编译、压缩 js;
- 给图片加版本号并替换 js、css 中的引用;
- 给 js、css 加版本号并替换 html 中的引用
构建过程
- 构建产生的结果代码,放在 dist 目录
- 每次构建前,清空之前的构建目录
- 构建过程分为:images、styles、scripts、hash 四个步骤
- 利用 npm 的钩子机制添加 prebuild 命令
{
"client:static-server": "http-server client/",
"prebuild": "rm -rf dist && mkdir -p dist/{images,styles,scripts}"
}
图片构建的经典工具是 imagemin,提供了命令行版本 imagemin-cli
npm install imagemin-cli --save-dev
yarn add imagemin-cli -D
scripts/build/images.sh 中添加 imagemin client/images/* --out-dir=dist/images
package.json 中添加 build:images 命令
运行 npm run prebuild && npm run build:images,然后观察 dist 目录的变化
"build:images": "scripty"
样式构建
cssmin 来完成代码预压缩
npm install cssmin --save-dev
运行 npm run prebuild && npm run build:styles
js构建
uglify-es 来进行 es6代码压缩, uglify-es配置 https://github.com/mishoo/UglifyJS/tree/harmony#command-line-options
uglify-js 来压缩 js代码
npm install uglify-es --save-dev
静态资源版本号
- 静态资源加版本号的原因:是线上环境的静态资源通常都放在 CDN 上,或者设置了很长时间的缓存
- 如果资源更新了但没有更新版本号,浏览器端是拿不到最新内容的
- 手动加版本号的过程很繁琐并且容易出错
- 通常的做法是利用文件内容做哈希,比如 md5
- hashmark,自动添加版本号;
- replaceinfiles,自动完成引用替换,它需要将版本号过程的输出作为输入
npm install hashmark replaceinfiles --save-dev