开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
NPM 学习
用 npm init 快速创建项目
makdir npm-learning
cd npm-learning
npm init -y
package.json
{
"name": "npm-learning",
"version": "1.0.0",
"description": "#### Description {**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
},
"repository": {
"type": "git",
"url": "git@gitee.com:suiboyu/npm-learning.git"
},
"keywords": [],
"author": "",
"license": "ISC",
}
运行 npm run test,能看到 Error: no test specified 的输出。
npm run 实际上是 npm run-script 命令的简写。当我们运行 npm run xxx 时,基本步骤如下:
- 从 package.json 文件中读取 scripts 对象里面的全部配置;
- 以传给 npm run 的第一个参数作为键,本例中为 xxx,在 scripts 对象里面获取对应的值作为接下来要执行的命令,如果没找到直接报错;
- 在系统默认的 shell 中执行上述命令,系统默认 shell 通常是 bash,windows 环境下可能略有不同。
Eslint
添加 eslint 依赖
npm install eslint -D
初始化 eslint 配置
./node_modules/.bin/eslint --init
选择需要的信息,不断回车,生成 .eslintrc.js。
module.exports = {
env: {
browser: true,
es2021: true
},
extends: 'standard',
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
"space-before-function-paren": 0,
"eol-last": 0
}
}
添加 eslint 命令
{
"scripts": {
"eslint": "eslint *.js",
},
}
执行 npm run eslint
看到终端会出现不符合规则的提示。
通常来说,前端项目会包含 js、css、less、scss、json、markdown 等格式的文件,为保障代码质量,给不同的代码添加检查是很有必要的。
-
eslint,可定制的 js 代码检查;
-
stylelint,可定制的样式文件检查,支持 css、less、scss;
-
jsonlint,json 文件语法检查;
-
markdownlint-cli,Markdown 文件最佳实践检查;
-
mocha,测试用例组织,测试用例运行和结果收集的框架;
安装依赖
npm install chai jsonlint markdownlint-cli mocha stylelint stylelint-config-standard postcss-less
package.json
{
"scripts": {
"lint:js": "eslint *.js",
"lint:css": "stylelint *.less",
"lint:json": "jsonlint --quiet *.json",
"lint:markdown": "markdownlint --config .markdownlint.json *.md",
}
}
.stylelintrc.js
module.exports = {
extends: 'stylelint-config-standard',
customSyntax: "postcss-less",
rules: {
// 颜色值小写
'color-hex-case': 'lower',
// 注释前无须空行
'comment-empty-line-before': 'never',
// 使用数字或命名的 (可能的情况下) font-weight 值
'font-weight-notation': null,
// 在函数的逗号之后要求有一个换行符或禁止有空白
'function-comma-newline-after': null,
// 在函数的括号内要求有一个换行符或禁止有空白
'function-parentheses-newline-inside': null,
// url使用引号
'function-url-quotes': 'always',
// 字符串使用单引号
'string-quotes': 'single',
// 缩进
'indentation': 4,
// 禁止低优先级的选择器出现在高优先级的选择器之后
'no-descending-specificity': null,
// 禁止空源
'no-empty-source': null,
// 禁止缺少文件末尾的换行符
'no-missing-end-of-source-newline': null
}
};
npm-learning/tests/index.spec.js
const { expect } = require('chai')
const { add } = require('../index')
describe('npm-learning', () => {
describe('#add', () => {
it('should return sum when param are numbers', () => {
expect(add(0, 1)).to.equal(1)
expect(add(0, 2)).to.equal(2)
})
it('should return NaN when param invalid', () => {
expect(isNaN(add(0, undefined))).to.equal(true);
expect(isNaN(add(null, undefined))).to.equal(true);
expect(isNaN(add({}, undefined))).to.equal(true);
})
})
})
npm script 串行
用 &&
符号把多条 npm script 按先后顺序串起来即可
{
"scripts": {
"test:serial": "npm run lint:js && npm run lint:css && npm run lint:json && npm run lint:markdown && mocha tests/",
}
}
执行 npm test
,执行顺序是严格按照我们在 scripts 中声明的先后顺序来。
需要注意:串行执行的时候如果前序命令失败(通常进程退出码非0),后续全部命令都会终止。
npm script 并行
把连接多条命令的 &&
符号替换成 &
即可。
{
"scripts": {
"test:parallel": "npm run lint:js & npm run lint:css & npm run lint:json & npm run lint:markdown & mocha tests/ & wait"
}
}
npm-run-all
npm i npm-run-all -D
上面这样用原生方式来运行多条命令很臃肿,可以使用 npm-run-all
实现更轻量和简洁的多命令运行。
{
"scripts": {
"test:all": "npm-run-all lint:js lint:css lint:json lint:markdown mocha",
}
}
npm-run-all 还支持通配符匹配分组的 npm script,上面的脚本可以进一步简化成:
{
"scripts": {
"test:allReg": "npm-run-all lint:* mocha",
}
}
npm script 并行执行
{
"scripts": {
"test:allParallel": "npm-run-all --parallel lint:* mocha",
}
}
npm script 传递参数
eslint 内置了代码风格自动修复模式,只需给它传入 --fix
参数即可
{
"scripts": {
"lint:js:fix": "npm run lint:js -- --fix",
}
}
要格外注意 --fix
参数前面的 --
分隔符,意指要给 npm run lint:js
实际指向的命令传递额外的参数。
npm script 运行时日志输出
日志级别控制参数:
- 显示尽可能少的有用信息:npm test -s
- 尽可能多的运行时状态:npm test -d
使用 npm script 的钩子
为了方便开发者自定义,npm script 的设计者为命令的执行增加了类似生命周期的机制,具体来说就是 pre
和 post
钩子脚本。
举例来说,运行 npm run test 的时候,分 3 个阶段:
- 检查 scripts 对象中是否存在 pretest 命令,如果有,先执行该命令;
- 检查是否有 test 命令,有的话运行 test 命令,没有的话报错;
- 检查是否存在 posttest 命令,如果有,执行 posttest 命令;
{
"scripts": {
"lint": "npm-run-all --parallel lint:*",
"pretest": "npm run lint",
"test": "mocha tests/",
}
}
当我们运行 npm test 的时候,会先自动执行 pretest 里面的 lint,实际输出如下:
增加覆盖率收集
增加覆盖率收集的命令,并且覆盖率收集完毕之后自动打开 html 版本的覆盖率报告。
npm i nyc open-cli -D
在 package.json 增加 nyc 的配置,告诉 nyc 该忽略哪些文件。
{
"nyc": {
"exclude": [
"**/*.spec.js",
".*.js"
]
}
}
新增 3 条命令:
- precover,收集覆盖率之前把之前的覆盖率报告目录清理掉;
- cover,直接调用 nyc,让其生成 html 格式的覆盖率报告;
- postcover,清理掉临时文件,并且在浏览器中预览覆盖率报告;
{
"scripts": {
"precover": "rm -rf coverage",
"cover": "nyc --reporter=html npm test",
"postcover": "rm -rf .nyc_output && opn coverage/index.html"
},
}
执行 npm run cover,自动打开测试报告
npm script 中使用变量
通过运行 npm run env
就能拿到完整的变量列表。
测试覆盖率归档是比较常见的需求,因为它方便我们追踪覆盖率的变化趋势,最彻底的做法是归档到 CI 系统里面,对于简单项目,则可以直接归档到文件系统中,即把收集到的覆盖率报告按版本号去存放。利用变量机制把归档和版本号关联起来。
{
"scripts": {
"postcover": "npm run cover:archive && npm run cover:cleanup && opn coverage_archive/$npm_package_version/index.html",
"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:archive 做了 2 件事情:
mkdir -p coverage_archive/$npm_package_version
准备当前版本号的归档目录;cp -r coverage/* coverage_archive/$npm_package_version
,直接复制文件来归档;
postcover 做了 3 件事情:
npm run cover:archive
,归档本次覆盖率报告;npm run cover:cleanup
,清理本次覆盖率报告;opn coverage_archive/$npm_package_version/index.html
,直接预览覆盖率报告;
http-server 生成线上测试报告
npm i http-server -D
- 新增的命令
cover:serve
中同时使用了预定义变量$npm_package_version
和自定义变量$npm_package_config_port
; - 预览覆盖率报告的方式从直接打开文件修改为打开网址:
http://localhost:$npm_package_config_port
; - postcover 命令要做的事情比较多,我们直接使用 npm-run-all 来编排子命令。
{
"config": {
"port": 3000
},
"scripts": {
"postcover": "npm-run-all cover:archive cover:cleanup --parallel cover:serve cover:open",
"cover:serve": "http-server coverage_archive/$npm_package_version -p $npm_package_config_port",
"cover:open": "open http://localhost:$npm_package_config_port"
},
}
npm script 跨平台兼容
npm script 中涉及到的文件系统操作包括文件和目录的创建、删除、移动、复制等操作,而社区为这些基本操作也提供了跨平台兼容的包,列举如下:
- rimraf 或 del-cli,用来删除文件和目录,实现类似于
rm -rf
的功能; - cpr,用于拷贝、复制文件和目录,实现类似于
cp -r
的功能; - make-dir-cli,用于创建目录,实现类似于
mkdir -p
的功能;
npm i rimraf cpr make-dir-cli cross-var -D
改造涉及文件系统操作的 npm script:
{
"scripts": {
"cover:cleanup": "rimraf coverage && rimraf .nyc_output",
"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 open http://localhost:$npm_package_config_port",
"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 script 拆到单独文件中
借助 scripty 我们可以将 npm script 剥离到单独的文件中,从而把复杂性隔到单独的模块里面,让代码整体看起来更加清晰。
安装依赖
npm i scripty -D
准备目录和文件
mkdir -p scripts/cover
touch scripts/cover.sh
touch scripts/cover/serve.sh
touch scripts/cover/open.sh
按照 scripty 的默认约定,npm script 命令和上面各文件的对应关系如下:
命令 | 文件 | 备注 |
---|---|---|
cover | scripts/cover.sh | 内含 precover、postcover 的逻辑 |
cover:serve | scripts/cover/serve.sh | 启动服务 |
cover:open | scripts/cover/open.sh | 打开预览 |
特别注意的是,给所有脚本增加可执行权限是必须的,否则 scripty 执行时会报错,我们可以给所有的脚本增加可执行权限:
chmod -R a+x scripts/**/*.sh
scripts/cover.sh
#!/usr/bin/env bash
# remove old coverage reports
rimraf coverage && rimraf .nyc_output
# run test and collect new coverage
nyc --reporter=html npm run test
# achive coverage report by version
mkdir -p coverage_archive/$npm_package_version
cp -r coverage/* coverage_archive/$npm_package_version
# open coverage report for preview
npm-run-all --parallel cover:serve cover:open
scripts/cover/serve.sh
#!/usr/bin/env bash
http-server coverage_archive/$npm_package_version -p $npm_package_config_port
scripts/cover/open.sh
#!/usr/bin/env bash
sleep 1
open http://localhost:$npm_package_config_port
package.json
{
"scripts": {
"cover": "scripty",
"cover:serve": "scripty",
"cover:open": "scripty"
},
}
重新运行 npm run cover,不出意外的话,我们能得到和原来完全相同的结果。
文件变化时自动运行 npm script
单元测试自动化
mocha 本身支持 --watch
参数,即在代码变化时自动重跑所有的测试,我们只需要在 scripts 对象中新增一条命令即可:
{
"scripts": {
"watch:test": "npm test -- --watch",
},
}
代码检查自动化
onchange 可以方便的让我们在文件被修改、添加、删除时运行需要的命令。
npm i onchange -D
package.json
{
"scripts": {
"watch": "npm-run-all --parallel watch:*",
"watch:lint": "onchange -i \"**/*.js\" \"**/*.less\" -- npm run lint",
},
}
watch:lint
里面的文件匹配模式可以使用通配符,但是模式两边使用了转义的双引号,这样是跨平台兼容的;watch:lint
里面的-i
参数是让 onchange 在启动时就运行一次--
之后的命令,即代码没变化的时候,变化前后的对比大多数时候还是有价值的;- watch 命令实际上是使用了 npm-run-all 来运行所有的 watch 子命令;
livereload 实现自动刷新
npm i livereload -D
package.json
{
"scripts": {
"client": "npm-run-all --parallel client:*",
"client:reload-server": "livereload client/",
"client:static-server": "http-server client/"
},
}
为什么需要启动两个服务,其中 http-server 启动的是静态文件服务器,该服务启动后可以通过 http 的方式访问文件系统上的文件,而 livereload 是启动了自动刷新服务,该服务负责监听文件系统变化,并在文件系统变化时通知所有连接的客户端,在 client/index.html
中嵌入的那段 js 实际上是和 livereload-server 连接的一个 livereload-client。
页面中嵌入 livereload 脚本
修改 client/index.html 嵌入 livereload 脚本(能够连接我们的 livereload 服务)
client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<script>
document.title = 'bbbb'
</script>
<script>
document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] +
':35729/livereload.js?snipver=1"></' + 'script>')
</script>
</body>
</html>
main.css
body {
background-color: aqua;
}
运行 npm run client 之后,打开浏览器访问:http://localhost:8080,接着修改client/main.css 并保存,浏览器自动刷新了。
实测会出现以下问题,请问如何解决?
1.当修改完css样式,(如把白色背景色改为红色)前台页面会自动刷新,背景变为红色,没问题。 2.但当接下来我修改了index.html这个页面标题,保存后页面也会自动刷新,但是样式又变成之前的了(背景色又变为白色)。
npm script 进行版本管理
每次构建完的代码都应该有新的版本号,修改版本号直接使用 npm 内置的 version 自命令即可,如果是简单粗暴的版本管理,可以在 package.json 中添加如下 scripts:
{
"scripts": {
"release:patch": "npm version patch && git push && git push --tags",
"release:minor": "npm version minor && git push && git push --tags",
"release:major": "npm version major && git push && git push --tags",
}
}
这 3 条命令遵循 semver 的版本号规范来方便你管理版本,patch 是更新补丁版本,minor 是更新小版本,major 是更新大版本。在必要的时候,可以通过运行 npm run version:patch 来升补丁版本,运行输出如下:
npm script 进行服务进程和日志管理
在生产环境的服务进程和日志管理领域,pm2 是当之无愧的首选。项目中使用 npm script 进行服务进程和日志管理的基本步骤如下:
准备 http 服务
npm i express morgan -D
根目录下创建文件 server.js
const express = require('express');
const morgan = require('morgan');
const app = express();
const port = process.env.PORT || 8080;
app.use(express.static('./dist'));
app.use(morgan('combined'));
app.listen(port, err => {
if (err) {
console.error('server start error', err); // eslint-disable-line
process.exit(1);
}
console.log(`server started at port ${port}`); // eslint-disable-line
});
准备日志目录
项目中创建日志存储目录 logs,设置该目录为 git 忽略的,需要改动 .gitignore
mkdir logs
touch logs/.gitkeep
.gitignore
dist
安装和配置 pm2
npm i pm2 -D
添加服务启动配置到项目根目录下 pm2.json
{
"apps": [
{
"name": "npm-script-workflow",
"script": "./server.js",
"out_file": "./logs/stdout.log",
"error_file": "./logs/stderr.log",
"log_date_format": "YYYY-MM-DD HH:mm:ss",
"instances": 0,
"exec_mode": "cluster",
"max_memory_restart": "800M",
"merge_logs": true,
"env": {
"NODE_ENV": "production",
"PORT": 8080,
}
}
]
}
上面的配置指定了服务脚本为 server.js,日志输出文件路径,日志时间格式,进程数量 = CPU 核数,启动方式为 cluster,以及两个环境变量。
配置服务部署命令
{
"scripts": {
"predeploy": "npm run build",
"deploy": "pm2 restart pm2.json",
"build": "webpack"
}
}
安装 webpack
npm install webpack webpack-cli -D
根目录下新建 webpack.config.js
const path = require('path')
module.exports = {
// 入口文件
entry: './index.js',
// 出口文件
output: {
// 打包之后的文件名
filename: 'bundle.js',
// 打包之后文件的存放路径
path: path.resolve(__dirname, 'dist')
}
}
配置日志查看命令
{
"scripts": {
"logs": "tail -f logs/*",
}
}
需要查看日志时,直接运行 npm run logs。
参考文章:掘金小册