# npm 学习 **Repository Path**: suiboyu/npm-learning ## Basic Information - **Project Name**: npm 学习 - **Description**: npm 学习npm 学习 - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-01-18 - **Last Updated**: 2024-01-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # NPM 学习 ### 用 npm init 快速创建项目 ```bash makdir npm-learning ``` ```bash cd npm-learning ``` ```bash npm init -y ``` package.json ```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 时,基本步骤如下: 1. 从 package.json 文件中读取 scripts 对象里面的全部配置; 2. 以传给 npm run 的第一个参数作为键,本例中为 xxx,在 scripts 对象里面获取对应的值作为接下来要执行的命令,如果没找到直接报错; 3. 在系统默认的 shell 中执行上述命令,系统默认 shell 通常是 bash,windows 环境下可能略有不同。 ### Eslint ###### 添加 eslint 依赖 ```bash npm install eslint -D ``` ###### 初始化 eslint 配置 ```bash ./node_modules/.bin/eslint --init ``` 选择需要的信息,不断回车,生成 .eslintrc.js。 ```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 命令 ```json { "scripts": { "eslint": "eslint *.js", }, } ``` ###### 执行 npm run eslint 看到终端会出现不符合规则的提示。 通常来说,前端项目会包含 js、css、less、scss、json、markdown 等格式的文件,为保障代码质量,给不同的代码添加检查是很有必要的。 - [eslint](https://link.juejin.cn/?target=https%3A%2F%2Feslint.org),可定制的 js 代码检查; - [stylelint](https://link.juejin.cn/?target=https%3A%2F%2Fstylelint.io),可定制的样式文件检查,支持 css、less、scss; - [jsonlint](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fzaach%2Fjsonlint),json 文件语法检查; - [markdownlint-cli](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Figorshubovych%2Fmarkdownlint-cli),Markdown 文件最佳实践检查; - [chai](https://link.juejin.cn/?target=http%3A%2F%2Fchaijs.com),测试断言库,必要的时候可以结合 [sinon](https://link.juejin.cn/?target=http%3A%2F%2Fsinonjs.org) 使用; - [mocha](https://link.juejin.cn/?target=https%3A%2F%2Fmochajs.org),测试用例组织,测试用例运行和结果收集的框架; ###### 安装依赖 ```bash npm install chai jsonlint markdownlint-cli mocha stylelint stylelint-config-standard postcss-less ``` package.json ```json { "scripts": { "lint:js": "eslint *.js", "lint:css": "stylelint *.less", "lint:json": "jsonlint --quiet *.json", "lint:markdown": "markdownlint --config .markdownlint.json *.md", } } ``` .stylelintrc.js ```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 ```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 按先后顺序串起来即可 ```json { "scripts": { "test:serial": "npm run lint:js && npm run lint:css && npm run lint:json && npm run lint:markdown && mocha tests/", } } ``` 执行 `npm test`,执行顺序是严格按照我们在 scripts 中声明的先后顺序来。 ![](https://s1.ax1x.com/2023/01/18/pS3WP4U.png) 需要注意:串行执行的时候如果前序命令失败(通常进程退出码非0),后续全部命令都会终止。 ### npm script 并行 把连接多条命令的 `&&` 符号替换成 `&` 即可。 ```json { "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 ```bash npm i npm-run-all -D ``` 上面这样用原生方式来运行多条命令很臃肿,可以使用 `npm-run-all` 实现更轻量和简洁的多命令运行。 ```json { "scripts": { "test:all": "npm-run-all lint:js lint:css lint:json lint:markdown mocha", } } ``` npm-run-all 还支持通配符匹配分组的 npm script,上面的脚本可以进一步简化成: ```json { "scripts": { "test:allReg": "npm-run-all lint:* mocha", } } ``` npm script 并行执行 ```json { "scripts": { "test:allParallel": "npm-run-all --parallel lint:* mocha", } } ``` ### npm script 传递参数 eslint 内置了代码风格自动修复模式,只需给它传入 `--fix` 参数即可 ```json { "scripts": { "lint:js:fix": "npm run lint:js -- --fix", } } ``` 要格外注意 `--fix` 参数前面的 `--` 分隔符,意指要给 `npm run lint:js` 实际指向的命令传递额外的参数。 ![](https://s1.ax1x.com/2023/01/18/pS3W9EV.png) ### npm script 运行时日志输出 日志级别控制参数: - 显示尽可能少的有用信息:npm test -s - 尽可能多的运行时状态:npm test -d ### 使用 npm script 的钩子 为了方便开发者自定义,npm script 的设计者为命令的执行增加了类似生命周期的机制,具体来说就是 `pre` 和 `post` 钩子脚本。 举例来说,运行 npm run test 的时候,分 3 个阶段: 1. 检查 scripts 对象中是否存在 pretest 命令,如果有,先执行该命令; 2. 检查是否有 test 命令,有的话运行 test 命令,没有的话报错; 3. 检查是否存在 posttest 命令,如果有,执行 posttest 命令; ```json { "scripts": { "lint": "npm-run-all --parallel lint:*", "pretest": "npm run lint", "test": "mocha tests/", } } ``` 当我们运行 npm test 的时候,会先自动执行 pretest 里面的 lint,实际输出如下: ![](https://s1.ax1x.com/2023/01/18/pS3Wk34.png) ### 增加覆盖率收集 增加覆盖率收集的命令,并且覆盖率收集完毕之后自动打开 html 版本的覆盖率报告。 - 覆盖率收集工具 [nyc](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fistanbuljs%2Fnyc) - 打开 html 文件的工具 [open-cli](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fsindresorhus%2Fopn-cli) ```bash npm i nyc open-cli -D ``` 在 package.json 增加 nyc 的配置,告诉 nyc 该忽略哪些文件。 ```json { "nyc": { "exclude": [ "**/*.spec.js", ".*.js" ] } } ``` 新增 3 条命令: 1. precover,收集覆盖率之前把之前的覆盖率报告目录清理掉; 2. cover,直接调用 nyc,让其生成 html 格式的覆盖率报告; 3. postcover,清理掉临时文件,并且在浏览器中预览覆盖率报告; ```json { "scripts": { "precover": "rm -rf coverage", "cover": "nyc --reporter=html npm test", "postcover": "rm -rf .nyc_output && opn coverage/index.html" }, } ``` 执行 npm run cover,自动打开测试报告 ![](https://s1.ax1x.com/2023/01/18/pS3WCNT.png) ### npm script 中使用变量 通过运行 `npm run env` 就能拿到完整的变量列表。 测试覆盖率归档是比较常见的需求,因为它方便我们追踪覆盖率的变化趋势,最彻底的做法是归档到 CI 系统里面,对于简单项目,则可以直接归档到文件系统中,即把收集到的覆盖率报告按版本号去存放。利用变量机制把归档和版本号关联起来。 ```json { "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 件事情: 1. `mkdir -p coverage_archive/$npm_package_version` 准备当前版本号的归档目录; 2. `cp -r coverage/* coverage_archive/$npm_package_version`,直接复制文件来归档; postcover 做了 3 件事情: 1. `npm run cover:archive`,归档本次覆盖率报告; 2. `npm run cover:cleanup`,清理本次覆盖率报告; 3. `opn coverage_archive/$npm_package_version/index.html`,直接预览覆盖率报告; ![](https://s1.ax1x.com/2023/01/18/pS3WFCF.png) ### http-server 生成线上测试报告 ```bash npm i http-server -D ``` - 新增的命令 `cover:serve` 中同时使用了预定义变量 `$npm_package_version` 和自定义变量 `$npm_package_config_port`; - 预览覆盖率报告的方式从直接打开文件修改为打开网址: `http://localhost:$npm_package_config_port`; - postcover 命令要做的事情比较多,我们直接使用 npm-run-all 来编排子命令。 ```json { "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](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisaacs%2Frimraf) 或 [del-cli](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fdel-cli),用来删除文件和目录,实现类似于 `rm -rf` 的功能; - [cpr](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fcpr),用于拷贝、复制文件和目录,实现类似于 `cp -r` 的功能; - [make-dir-cli](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fmake-dir-cli),用于创建目录,实现类似于 `mkdir -p` 的功能; ```bash npm i rimraf cpr make-dir-cli cross-var -D ``` 改造涉及文件系统操作的 npm script: ```json { "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 [options]` 的格式,即配置项放在最后面; - 把 `cover:cleanup` 从 `postcover` 挪到 `precover` 里面去执行,规避 `cpr` 没归档完毕覆盖率报告就被清空的问题; ### npm script 拆到单独文件中 借助 [scripty](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Ftestdouble%2Fscripty) 我们可以将 npm script 剥离到单独的文件中,从而把复杂性隔到单独的模块里面,让代码整体看起来更加清晰。 ###### 安装依赖 ```bash npm i scripty -D ``` ###### 准备目录和文件 ```bash 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 执行时会报错**,我们可以给所有的脚本增加可执行权限: ```shell chmod -R a+x scripts/**/*.sh ``` `scripts/cover.sh` ```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` ```sh #!/usr/bin/env bash http-server coverage_archive/$npm_package_version -p $npm_package_config_port ``` `scripts/cover/open.sh` ```sh #!/usr/bin/env bash sleep 1 open http://localhost:$npm_package_config_port ``` package.json ```json { "scripts": { "cover": "scripty", "cover:serve": "scripty", "cover:open": "scripty" }, } ``` 重新运行 npm run cover,不出意外的话,我们能得到和原来完全相同的结果。 ### 文件变化时自动运行 npm script ##### 单元测试自动化 mocha 本身支持 `--watch` 参数,即在代码变化时自动重跑所有的测试,我们只需要在 scripts 对象中新增一条命令即可: ```json { "scripts": { "watch:test": "npm test -- --watch", }, } ``` ##### 代码检查自动化 onchange 可以方便的让我们在文件被修改、添加、删除时运行需要的命令。 ```bash npm i onchange -D ``` package.json ```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 实现自动刷新 ```bash npm i livereload -D ``` package.json ```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 ```html Document ``` main.css ```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: ```json { "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](https://link.juejin.cn/?target=https%3A%2F%2Fsemver.org) 的版本号规范来方便你管理版本,patch 是更新补丁版本,minor 是更新小版本,major 是更新大版本。在必要的时候,可以通过运行 npm run version:patch 来升补丁版本,运行输出如下: ![](https://s1.ax1x.com/2023/01/18/pS3WAgJ.png) ### npm script 进行服务进程和日志管理 在生产环境的服务进程和日志管理领域,[pm2](https://link.juejin.cn/?target=http%3A%2F%2Fpm2.keymetrics.io) 是当之无愧的首选。项目中使用 npm script 进行服务进程和日志管理的基本步骤如下: #### 准备 http 服务 ```bash npm i express morgan -D ``` 根目录下创建文件 server.js ```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 ```bash mkdir logs touch logs/.gitkeep ``` .gitignore ```bash dist ``` #### 安装和配置 pm2 ```bash npm i pm2 -D ``` 添加服务启动配置到项目根目录下 pm2.json ```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,以及两个环境变量。 #### 配置服务部署命令 ```json { "scripts": { "predeploy": "npm run build", "deploy": "pm2 restart pm2.json", "build": "webpack" } } ``` 安装 webpack ```bash npm install webpack webpack-cli -D ``` 根目录下新建 `webpack.config.js` ```js const path = require('path') module.exports = { // 入口文件 entry: './index.js', // 出口文件 output: { // 打包之后的文件名 filename: 'bundle.js', // 打包之后文件的存放路径 path: path.resolve(__dirname, 'dist') } } ``` ### 配置日志查看命令 ```json { "scripts": { "logs": "tail -f logs/*", } } ``` 需要查看日志时,直接运行 npm run logs。 ![](https://s1.ax1x.com/2023/01/18/pS3WEv9.png)