NPM 的那些基本知识

62 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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 时,基本步骤如下:

  1. 从 package.json 文件中读取 scripts 对象里面的全部配置;
  2. 以传给 npm run 的第一个参数作为键,本例中为 xxx,在 scripts 对象里面获取对应的值作为接下来要执行的命令,如果没找到直接报错;
  3. 在系统默认的 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 文件最佳实践检查;

  • chai,测试断言库,必要的时候可以结合 sinon 使用;

  • 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 的设计者为命令的执行增加了类似生命周期的机制,具体来说就是 prepost 钩子脚本。

举例来说,运行 npm run test 的时候,分 3 个阶段:

  1. 检查 scripts 对象中是否存在 pretest 命令,如果有,先执行该命令;
  2. 检查是否有 test 命令,有的话运行 test 命令,没有的话报错;
  3. 检查是否存在 posttest 命令,如果有,执行 posttest 命令;
{
  "scripts": {
    "lint": "npm-run-all --parallel lint:*",
    "pretest": "npm run lint",
    "test": "mocha tests/",
  }
}

当我们运行 npm test 的时候,会先自动执行 pretest 里面的 lint,实际输出如下:

增加覆盖率收集

增加覆盖率收集的命令,并且覆盖率收集完毕之后自动打开 html 版本的覆盖率报告。

  • 覆盖率收集工具 nyc
  • 打开 html 文件的工具 open-cli
npm i nyc open-cli -D

在 package.json 增加 nyc 的配置,告诉 nyc 该忽略哪些文件。

{
  "nyc": {
    "exclude": [
      "**/*.spec.js",
      ".*.js"
    ]
  }
}

新增 3 条命令:

  1. precover,收集覆盖率之前把之前的覆盖率报告目录清理掉;
  2. cover,直接调用 nyc,让其生成 html 格式的覆盖率报告;
  3. 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 件事情:

  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,直接预览覆盖率报告;

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 中涉及到的文件系统操作包括文件和目录的创建、删除、移动、复制等操作,而社区为这些基本操作也提供了跨平台兼容的包,列举如下:

  • rimrafdel-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:cleanuppostcover 挪到 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 命令和上面各文件的对应关系如下:

命令文件备注
coverscripts/cover.sh内含 precover、postcover 的逻辑
cover:servescripts/cover/serve.sh启动服务
cover:openscripts/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。

代码仓库:gitee.com/suiboyu/npm…

参考文章:掘金小册