Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NPM 相关知识 #42

Open
w4096 opened this issue Dec 11, 2016 · 4 comments
Open

NPM 相关知识 #42

w4096 opened this issue Dec 11, 2016 · 4 comments

Comments

@w4096
Copy link
Owner

w4096 commented Dec 11, 2016

系列文章

@w4096
Copy link
Owner Author

w4096 commented Dec 11, 2016

npm 基本用法

每天都在使用 npm,但自己明白很多地方还是不清楚,为此决定仔细阅读 npm 的官方文档,这篇文章讲解了 npm 的基本使用方法,后续会讲 npm 的一些高级用法。

虽然前不久出现的 yarn 确实比 npm 快,但是我想 npm 至少还会使用很久,因此有必要详细了解它。

package.json 中的常见字段

package.json 相当于是一个项目的整体描述,其中记录了该项目的 git 地址,项目简介,依赖模块,bug 汇报地址等等信息,如今很多工具都利用 package.json 来存放配置文件,比如 eslint 可以读取 package.json 中的 eslintConfig 字段来获取配置,babel 可以读取 babel 字段来获取配置。

关于 package.json 中每个字段的定义,请参照官方文档 package.json,这里只提到一些非常常用的字段。

name

包名,这个名字决定了包在 node_modules 中的文件夹名,可以加上 scope,比如 @att/wxx,scope 为 att,用户安装的时候会被安装在 node_modules/@att/ 这个文件夹下。

main

该模块的入口文件,比如 "main": "build/index.js",别人在 require 这个包的时候,实际是 require 了项目中的 build/index.js 这个文件。

bin

用来指定可执行文件的名称和路径,比如 { "bin" : { "wxx-cli" : "src/cli.js" } },这样用户在安装该模块的时候,npm 会创建一个文件叫做 wxx-cli 放置在 node_modules/.bin 文件下,并将其链接至 src/cli.js 这个文件上。关于此,后面还会详细讲解。

scripts

用来指定一些脚本,也就是人们通常所说的 npm script,这个字段是一个对象,在这里可以自定义多个命令,这些命令可以使用 npm run 来执行,比如:

{
    "scripts":{
        "build": "webpack",
        "start": "webpack-dev-server"
    }
}

有了上面这样的配置,在项目中执行 npm run build 就会执行命令 webpack,npm script 涉及的东西较多,后面有专门的一章来讲它。

dependencies 和 devDependencies

package.json 中的 dependenciesdevDependencies 分别记录了该项目在生产环境下依赖的模块,和在开发环境下依赖的模块。

比如一个项目使用了 typescript 来开发,在发布的时候通常要将代码编译为 JavaScript,因此在开发的时候使用的 typescript 就应该放在 devDependencies,所以在安装的时候,可以使用 npm i typescript --save-dev 来安装。

另外项目代码中依赖了一个模块 qs 来解析 url,为了让你的代码跑起来,这个模块必须存在,因此,必须将该模块记录在 dependencies 中,使用 npm i qs --save 来进行安装。这样用户在运行你的代码的时候,你代码中的 require('qs') 才不至于找不到模块报错。

peerDependencies

peerDependencies 常常出现在一些插件的 package.json 中,比如一个 react 的第三方组件,它只能用在 react 版本高于 15.0.0 的时候,此时它就应该这样写:

{
    "peerDependencies": {
        "react": "15.x"
    }
}

这表示,要想使用我,你必须使用 react 15.0.0 以后的版本,请确保你安装了 react 15.0.0 以后的版本。

为什么要存在 peerDependencies 呢?

使用 peerDependencies 表明的是,我与另外一个模块协调工作。这有别于依赖于某个模块,仔细体会,他们之间的差异很微妙。一个插件,可能不会去引用摸个模块,比如一个 jQuery 插件,它只是在 jQuery.propotype 上添加了一些方法,因此在安装插件的时候,插件应该是默认你已经安装了它的宿主模块。npm3 以后,在执行 npm install 的时候不会安装 peerDependencies 中的模块,这是符合 peerDependencies 的设计理念的。

如何使用 peerDependencies?

如果你的模块与另外一些模块协同工作,该模块要想工作,需要用户已经安装另外一个模块,比如一个 webpack 的 loader,一个 gulp 的插件,一个 express 的中间件等等。这个时候你需要考虑使用 peerDependencies

另外,设置 peerDependencies 的时候 版本范围要大,试想一下,你的项目中使用了 10 个 react 的第三方组件,这些第三方组件的 peerDependencies 分别是这样写的 "react": "15.0.0""react": "15.1.0""react": "15.2.0" ...,这时候会出现什么情况?情况是你没办法同时使用这些第三方组件,因为它们限定了要使用某个特定版本的 react,你没有办法兼顾所有组件的要求。

当相反,如果版本的限制要求的比较宽泛,比如 "react": ">15.0.0" 表示大于 15.0.0 版本就好了,这样以来所有的组件都可以融洽相处了。

Ok,上面这些 package.json 中的字段算是最为常见的了,其他有些看到单词就知道是什么意思了,还有一些不常见,如需了解,请参见官方文档。

创建 package.json

使用 npm init 来创建一个 package.json,在创建的过程中要求输入一些基本信息,如果希望使用默认内容,可以使用 npm init --yes 这样就不需要一步步按回车了。

可以通过 npm set 来设置默认项,比如 npm set init.author.email "[email protected]" 设置默认邮件。

安装模块时候的选项

在安装模块的时候通常使用 npm install <package name> 比如 npm install q,也可以使用缩写 npm i q,但更多时候当我们安装了一个模块后,通常希望将它记录下来,将其写入到 package.json 中,在安装模块的时候,可以通过 --save--save-dev 选项来将一个模块添加至 dependenciesdevDependencies 中。

关于模块的版本

一个模块的版本号应该形如 1.0.0,在版本升级的时候,每一位代表着不同的变化:

  • 修改 bug,打补丁,应该修改最后一位,比如 1.0.1
  • 新的特性,且不会影响现存的特性,比较小的修改,增加中间一位,比如 1.1.0
  • 大的修改,对现有功能有影响,增加第一位,比如 2.0.0

设定使用的版本的模块

安装第三方模块的时候需要指定其版本号,如果你打开一个既有项目的 package.json,会看到 ^, ~ 这样的符号,因为依赖的模块是在不断升级的,这些符号表示接受怎样的升级:

用户在安装某个模块的时候,也会一并安装该模块依赖的模块(存在于 dependencies 中的模块),这个时候 npm 会安装最新的满足你版本要求的模块。看下面详细说明:

  • 接受微小的升级,bug 修复:1.0 或者 1.0.x 或者 ~1.0.4,这表示在安装的时候接受最后一位的变化。比如在初次安装的时候,模块 A 是 1.0.0 版本,用户下次安装的时候 A 模块已经升级到了 1.0.9,这个时候就会安装 1.0.9 版本,如果 A 项目的维护者,下次发布了 1.1.0 那么,再次安装的时候依然会安装 1.0.9。
  • 接受较小的升级,新增特性:1 或者 1.x 或者 ^1.0.4,这表示接受中间位置的变化
  • 接受重大升级:* 或者 x

懂得这个很重要,很多时候这个符号写的不对,导致依赖模块升级后,整个项目就跑不起来了。当然了有时候会看到更加复杂的写法,请参照这里 semver 了解更高级的用法。

升级模块

依赖的模块才你开发的过程中也可能在升级,为了获得符合你的版本描述的最新版本,可以在 package.json 所在目录下执行 npm update 来升级模块。也可以通过执行 npm outdated 来获取当前项目中可升级的模块的信息。

卸载模块

因为项目变动,不需要使用某个模块了,这个时候不应该让他继续存在于 package.json 中的,没有使用到,但还是要被安装,这显然是浪费资源,污染环境的做法。使用 npm uninstall loadsh 来卸载一个模块,不会从 package.json 中删除该模块,需要添加 --save--save-dev 来卸载他们。

安装全局模块

有两种模式来安装模块 global 和 local,安装在全局的模块通常是一些命令行工具,比如 eslint,可以将其安装在全局,在任何目录下都能使用。但是全局的模块只能存在一个版本,这也是为什么很多时候一些模块建议安装在本地,这样不同的项目可以使用不同的版本,避免不同项目之间的冲突。将一个模块安装在全局,只需要加上 -g 选项,比如 npm i eslint -g

安装全局模块,常常出现一些权限问题,通常是因为当前用户没有 npm 全局安装目录的写权限,为此可以使用 chown 来修权限,也可以直接使用 sudo 来执行 npm install 操作,或者将 npm 默认安装目录修改为一个自己具有权限的目录。这些操作请参考 Fixing npm permissions

升级全局模块

升级一个全局模块,只需要执行 npm update -g <package> 即可,比如 npm update -g gulp

卸载全局模块

很简单 npm uninstall -g <package> 比如 npm uninstall -g eslint

发布一个 npm 模块

创建了一个项目,填写了 package.json,为了让自己的代码让更多人使用,需要将其发布到 npm 上面,需要仔细填写 package.json 中的 descriptionkeywords 这些字段的信息将有助于其他人在 npm 上搜索到该模块。

为了将模块发布到 npm 上,首先需要创建一个 npm 的账号,这可以在 https://npmjs.com/ 上创建账号,也可以使用 npm adduser 在命令行中创建账号。如果是在网站上注册的账号,需要使用 npm login 来登录。在登录的时候需要输入用户名和密码,以及你的 E-mail。

登录完成后执行 npm publish 就可以发布模块了,在发布模块之前要确保你的模块名在当前的 npm 仓库中是不存在的。另外本地的一些测试文件,通常不需要发布在 npm 上,这样用户在安装的时候只会下载那些必要的文件,npm 在发布的时候会忽略掉所有 .gitignore.npmignore 中忽略的文件,通常你应该在 .npmignore 中忽略掉测试文件、文档等内容,另外要提供一个 README.md

升级模块

对自己的模块进行了升级以后,需要将更新发布到 npm 上去,这个时候要同样是使用 npm publish,但是在此之前要注意更新版本号,你可以手动更改,也可以使用 npm version <update_type> 来更改,这儿的 update_type 可以是 patch, minor, major 使用这三个值,npm 会对应地将版本号的第 3 位、第 2 位、第 1 位加 1,分别表示此次升级有微小改动、有较小改动以及有大幅改动。

带有命名空间的模块

有些时候需要创建一个公司内部使用的模块,或者某个模块已经有了同名的模块存在,这个时候可以用到带有命名空间的模块,其名称形如 @scope/project,在 npm 上面带有命名空间的模块默认是私有的,而且每个用户有有一个属于自己的命名空间也就是 @username,在 npm 上私有的模块是要付费的,因此为了免费地发布一个带有命名空间的模块,需要将该模块设置为公开的,只需要在执行 publish 的时候加上 --access=public 选项即可。

在安装这些带有命名空间的模块的时候需要这样安装 npm install @scope/project --save,在项目中引用的时候也要带上 scope,require('@scope/project')

使用 tag

npm 也允许开发着给某个版本打 tag,比如当版本进行到 1.0.9 的时候可以给他打个 tag 叫做 beta,这个时候用户可以使用 npm i project-name@beta 来安装这个版本,这等价于 npm i [email protected]

使用 npm dist-tag add <package>@<version> tag 来给某个版本打 tag,默认情况下载 npm publish 的时候 npm 会给当前版本打一个 tag 叫做 latest,表示这是最新的,可以使用 npm publish --tag <tag-name> 来改变默认的 tag。

@w4096
Copy link
Owner Author

w4096 commented Dec 11, 2016

常用的 npm 命令

npm 有很多命令,有很多并不常用,而有些则非常常用,还有些很有用,但很多人却不知道,下面列出了一些常见和一些非常有用的命令,并详细说明了它的用法。


ls (alias list, la, ll)

使用该命令可以列出当前项目依赖的模块,比较常见的参数有 depth

 只列出最上层的依赖
npm ls --depth=0

使用 npm la 可以一并列出模块的简要说明(也就是存在于 description 中的内容)。

link

这是一个相当有用的命名,对于开发这很重要。设想一下场景:

你在开发一个模块 A,同时需要在另外一个项目 B 中测试它,你当然可以将该模块的代码移动到需要使用它的项目中,但这也不是理想的方法。

此时可以使用 link 来解决。npm link 的使用分为两步。

第一步

在模块 A 的项目根目录下执行 npm link,这个时候 npm 会将模块 A 通过链接安装在全局。

第二步

在项目 B 的根目录下执行 npm link A,此后就可以在项目 B 中使用 require('A') 来使用 A 模块了。因为只是链接,所以 A 模块的任何修改,都会事实地同步过来。

install (alias i)

这个命令想必都很熟悉了,用来安装模块。只是安装模块的来源有很多种,有来自 git 地址的,有来自 npm 的,有来自 github 仓库的,如何来安装这些来自不同途径的模块呢?

npm install

不带任何参数,这会安装 package.json 中记录的模块。

npm install

从 npm 源上安装一个模块

npm install @<version|tag>

从 npm 源上安装指定版本或者 tag 的模块

npm install

从一个 git 地址上安装模块,比如:

npm install git+ssh://[email protected]:npm/npm.git#v1.0.27
npm install git+https://[email protected]/npm/npm.git
npm install git://github.com/npm/npm.git#v1.0.27

npm install /[#]

从 github 上的每个开发这的仓库里安装,比如安装最新的 express:

 默认会安装 master 分支
npm i expressjs/express


 安装 5.0 分支
npm i expressjs/express#5.0

npm install file:

从本地文件系统安装,比如:

npm i file:../moduleA

安装上层目录的 moduleA

安装来源真非常多,更多方式请参考 npm install

uninstall

移除一个模块,这会移除某个模块自身和其依赖,为了将其从 package.json 中也删除,需要添加 --save 或者 --save-dev 选项。

shrinkwrap

npm 推荐开发者使用 semver 来设置模块的版本号,而且 npm 默认使用 ^1.0.0 这种形式来记录版本(关于这 ^ 代表什么,请参照前面的文章),初衷是让开发者能够时刻使用确保功能正常且又是最新的代码。但不能保证开发者遵守了这个规定。且即使在 dependencies 中明确写定某个模块的版本,也不能保证该模块依赖的模块的版本在下次升级的时候不会改变。

模块的不确定性导致开发阶段表现良好的代码,在部署以后出现了问题。为此需要做的是限定在下次安装的时候也只使用特定的版本。有的团队采用将 node_modules 也纳入版本管理的方案,只是这种方案存在太多缺点,代码库过大,某些在安装时候需要根据不同平台进行编译的模块无法正常工作,等等。

npm shrinkwrap 可以记录下当前项目使用的所有的模块的版本,并将他们记录在一个 npm-shrinkwrap.json 的文件中,在下次安装的时候 npm 会使用这里面记录的模板的版本来安装,这保证了下次安装的时候会使用和上次完全一致的版本。

如何使用 npm shrinkwrap

  1. 运行 npm install 安装所有模块
  2. 测试你的代码,保证没有问题
  3. 运行 npm shrinkwrap,并将产物 npm-shrinkwrap.json 纳入版本管理。

当你增加或者移除了依赖,这个时候需要重新生成一下 npm-shrinkwrap.json。npm 安装模块的顺序不同,会导致 node_modules 的结构不同,因此在增加或者移除了依赖后,可以删掉 node_modules 重新执行一次 npm i

使用了 shrinkwrap 之后,用户在安装你的模块的时候也会安装符合你的模块要求的依赖,这样可以有效地避免因为依赖升级导致自己的项目挂掉。当然了,及时这样也不能保证,完全不会出问题,如果一个模块的维护者,强制更新了代码但是没有更改版本号,这还是可能会出现问题,所以在使用第三方模块的时候要小心选择。

run

我想这个命令的使用频率仅次于 install 了。在 npm 的基本用法 一文中提到了 npm script,npm run 就是用来运行这些指令的。

比如在 package.json 中有这样的内容:

{
    "scripts":{
        "build": "webpack"
    }
}

为了运行 build 命令,需要执行 npm run build,在使用 npm run 的时候会将 node_modules/.bin 加入环境变量 PATH 中,在命令执行完了再移除,因此你不需要写成:

"build": "./node_modules/.bin/webpack"

如果需要在命令行中给 webpack 传入一些参数,比如想要执行 webpack -p,这时候执行 npm run build -p 不会得到想要的结果,因为这里的 -p 应该传给 npm 还是传给你的脚本,很难说,你需要执行 npm run build -- -p, npm 会将 -- 后面的内容全部传给你的脚本。

运行 npm run 不带任何参数,会列出当前所有可用的 npm script。

关于 npm script 的内容后面还会细说。

adduser (alias login)

在发布模块的时候常常要先进行登录,这个时候就需要用到该命令,键入 npm adduser 后会要求输入用户名,密码,邮箱,默认是登录到 npm 上,如要登录到其他源上,需要使用 registry 选项,要想登录到某个 scope 中需要使用 scope 选项:

npm adduser --registry=http://myregistry.example.com --scope=@myco

whoami

就像 Linux 系统里面的 whoami 一样,npm whoami 会打印出当前登录的用户名,你可能登录过多个仓库,比如 npm 的,公司的 npm 等,此时需要指定 --registry 选项来看自己在某个 npm 源上登录的用户。

npm whoami --registry https://registry.npm.taobao.org

outdated

使用 npm outdated 可以获取到当前项目依赖的模块的更新信息,键入该命令,会得到如下信息:

Package                  Current  Wanted  Latest  Location
babel-eslint               6.1.2   6.1.2   7.1.1  react-xxx
base64-js                  0.0.8   0.0.8   1.2.0  react-xxx

第一列是可以升级的模块,第二列是当前版本号,第三列是在 package.json 中指定的版本号,第四列是指该模块最新的版本,最后一列是当前项目名称。


npm 有很多有用的命令,知道了这些常用的命令的使用方法,以及适用场景能够极大地加快效率,毕竟是每天都要使用的工具,有必要了解它、熟悉它。下一篇文章,准备谈谈 npm script 的相关知识。

@w4096
Copy link
Owner Author

w4096 commented Dec 11, 2016

npm script 用法详解

什么是 npm script

npm script 是记录在 package.json 中的 scripts 字段中的一些自定义脚本,使用自定义脚本,用户可以将一些项目中常用的命令行记录在 package.json 不需要每次都要敲一遍。

必须开发者常常需要使用以下命令来统计项目中的代码行数:

find src -name "*.js" | xargs cat | wc -l

开发者可以将其写入 package.json 中:

"scripts":{
    "lines": "find src -name \"*.js\" | xargs cat | wc -l",
}

以后开发者只需要执行 npm run lines 就可以了,而不需要再去写那么长的命令行,这可以大幅提高效率。需要注意的是,因为命令是写在 json 文件中的,有些特字符需要进行转译,比如上面的双引号。

环境变量 PATH

npm scripts 不是简简单单地执行 shell 语句而已,在执行之前它会将 node_modules/.bin/ 加入到环境变量 PATH 中,所以在 npm scripts 中可以直接使用那些存在于 node_modules/.bin/ 中的可执行文件。

很多使用 mocha 作为测试框架的项目中都有这么一个 npm script

"scripts":{
    "test": "mocha"
}

mocha 并没有全局安装,它的命令行工具存在于 node_modules/.bin/ 中,之所以能够访问到它,正是因为 npm 背后的这一操作才使得这样的命令能够正常执行,在 npm script 执行完成后,会从 PATH 中移除。

在执行 npm run test (后面会看到这可以简写为 npm test) 的时候就等同于:

./node_modules/.bin/mocha

传入参数

对于上面的脚本 "test": "mocha" 如果希望给 mocha 传入一些选项,比如希望执行:

mocha --reporter spec

需要这样执行 npm test:

npm test -- --reporter spec

需要使用两个短线将选项隔开,或者将选项直接写在 package.json 中:

"scripts":{
    "test": "mocha --reporter spec"
}

在 shell 中传入的参数都要使用 -- 隔开,这个 -- 被视作 npm run 命令参数的结束,-- 后面的内容都会原封不动地传给运行的命令。

钩子脚本

在 npm script 中存在两个钩子,prepost,就拿上面的 lines 脚本来说,它的钩子脚本就是 prelinespostlines

"scripts":{
    "prelines": "node prelines.js"
    "lines": "find src -name \"*.js\" | xargs cat | wc -l",
    "postlines": "node postlines"
}

执行 npm run lines,会先执行 prelines 再执行 lines 之后再执行 postlines

生命周期事件

还有一些 script 会在模块安装,发布,卸载等不同的生命周期被执行。

  • prepublish, publish, postpublish:发布模块
  • preinstall, install, postinstall:安装模块
  • preuninstall, uninstall, postuninstall:卸载模块
  • preversion, version, postversion:在使用 npm version 修改版本号的时候执行
  • pretest, test, posttest:执行 npm test 的时候
  • prestop, stop, poststop:执行 npm stop 的时候
  • prestart, start, poststart:执行 npm start 的时候
  • prerestart, restart, postrestart:执行 npm restart 的时候

这些 script 会在不同的时刻自动被执行,这也是为什么 npm run test 可以简写为 npm test 的原因了,在执行 npm test 的时候会以次执行 pretesttestposttest,当然了,如果没有指定 pretestposttest,会默默地跳过。

还有 npm restart 这个命令比较叼,它不单单执行 prerestart, restart, postrestart 这三个,而是按下面的顺序来执行:

  1. prerestart
  2. prestop
  3. stop
  4. poststop
  5. restart
  6. prestart
  7. start
  8. poststart
  9. postrestart

环境变量

在执行 npm script 的时候还可以访问到一些特殊的环境变量,通过 process.env.npm_package_xxx 可以获得到 package.json 中的内容。比如 process.env.npm_package_name 可以获得到 package.jsonname 的值 "sv"

{
  "name": "sv",
  "version": "1.3.0",
  "description": "",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "git+ssh://[email protected]/wy-ei/sv.git"
  }
}

通过 process.env.npm_package_repository_type 可以拿到值 "git"

另外可以通过 process.env.npm_config_xxx 来拿到 npm config 中的值。比如通过 process.env.npm_config_user_email 可以拿到 user.email 的值。

还有一个比较特殊的环境变量 process.env.npm_lifecycle_event 在执行不同的 npm script 的时候这个值是不同的,比如执行 npm run build 的时候,这个值为 build,通过判断这个变量,将一个脚本使用在不同的 npm script 中。

使用任何脚本语言编写的 npm script 都可以拿到环境变量,比如在 shell 中要想拿到只需要使用 $npm_config_user_email 就好了。不同的脚本需要使用其自身获取环境变量的方法来读取环境变。

另外,这些环境变量只能在执行 npm script 的时候拿到,正常执行的 node 脚本是获取不到的。

编写 node 命令行工具

在 npm script 常常用到一些模块中的可执行程序,比如 eslint,webpack 等,那么要如何来自己编写一个命令行工具能,让它可以在 npm script 中被调用。

1. 编写命令行脚本

新建文件 cli.js,写入需要的逻辑。

console.log("This article is Awesome, isn't it?");

2. 在 package.json 的 bin 字段中指定命令行文件名称和路径

{
    "bin": {
        "cli": "./cli.js"
    }
}

3. 指定解释器

当用户安装以后,通过 ./node_modules/.bin/cli 执行,会报错,原因是目前 shell 不知道使用什么解释器来执行这些代码,为此需要在脚本上方指定解释器。

!usr/bin/env node
console.log("This article is Awesome, isn't it?");

上面这一行在所有脚本文件中都可以看到,它叫做 SheBang 或者 HashBang,详见 Shebang_(Unix),这行代码是告诉 shell 使用何种解释器来执行代码。usr/bin/env 是一个程序,usr/bin/env node 会找到当前 PATH 中的 node 来解释后面的代码。

有了这三步,就开发出了一个 node 的命令行工具。当用户安装这个模块的时候,npm 会在 node_modules/.bin/ 中创建文件,名为 package.json 中的 bin 指定的命令名,并链接至对应的脚本。此后就可以在 npm script 中使用它了。

多说两句,将上面的 #!usr/bin/env node 写入 JavaScript 文件第一行,不会报错。因为这是一个 UNIX 世界都认识的东西。通过 chmod +x cli.js,你可以使用 ./cli.js 直接执行它,因为这一行已经告诉 shell 使用 node 来执行它了。

总结

早在今年 3 月,在网上看到很多帖子,涌现出一个观点,要使用 npm script 来代替 gulp 这样的构建工具,如今看到 npm script 的功能确实强大,利用 node 和 shell 我们能够写出一些很实用的脚本,来解决手头的问题,并不一定需要利用 gulp,grunt 这样的东西。据统计,在 2015 年 gulp 以绝对的优势占据着 task runner 使用率第一的位置,而在 2016 年 npm script 的使用率提升的非常快。

无论如何,npm script 总是会出现在你每天的工作中,愿本文能助你搞懂 npm script,让 npm script 帮助你加快开发效率。

@w4096
Copy link
Owner Author

w4096 commented Dec 14, 2016

npm 是如何影响 node_modules 的目录结构的 ?

一个大型项目常常要依赖很多第三方的模块,而第三方的模块又有自己的依赖,假如其中有两个模块依赖了同一个模块的不同版本,这个时候该模块就要存在两个不同版本,那么它们在 node_modules 中是如何存在的呢? npm 的大量工作都是在处理这样的版本依赖问题。

比如你的项目 yxxx,有如下依赖:

"dependencies": {
    A: "1.0.0",
    C: "1.0.0"
}

而 A 和 C 两个模块有如下的依赖关系。

npm v2 时代

在 npm v2 时代,执行 npm install 后 node_modules 会是这样的:

node_modules
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]

这个时候如果在安装一个模块,[email protected],D 有如下依赖:

安装完成之后,node_modules 会是这样的:

node_modules
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]

[email protected] 存在了两份,这显然是浪费的,这也是被吐槽最多的点,一个项目中存在太多相同版本的模块的副本。

想想 require 在寻找模块时候的机制,它会向上级目录去寻找,因此 npm 3 做了改变。

npm v3 时代

安装 [email protected] 模块,现在的目录结构变为:

node_modules
├── [email protected]
└── [email protected]

可以看到他们存在于同一级目录,这个时候 A 中的 js 脚本在 A 中找不到 node_modules 后会在父级目录中找到 B 模块。

继续安装 [email protected] 模块,因为 [email protected] 依赖的是 [email protected] 模块,而此时在 node_modules 中已经存在了 [email protected],因此安装后的目录结构是这样的:

node_modules
├── [email protected]
├── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]

现在继续安装一个模块 [email protected],它有如下依赖:

其实 [email protected] 已经存在了,只是它位于 [email protected] 模块下,

安装完成后目录结构变为了:

node_modules
├── [email protected]
├── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]

这个时候 [email protected] 又存在了两份。Ok,继续安装一个模块 [email protected],它的依赖关系如下:

这个时候因为 [email protected] 已经存在于项目根目录下的 node_modules 中了,因此目录结构是这样的:

├── [email protected]
├── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]

npm v3 去重

好了,这个时候突然 [email protected] 需要升级到 2.0.0 版本,依赖关系也变为了:

安装后目录结构变为了:

node_modules
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]

随后 F 模块也升级至 2.0.0 版本了,依赖关系也变为了:

执行安装,在这个过程中首先会移除掉,[email protected] 然后发现,[email protected] 已经没有模块依赖它了,因此也移除了 [email protected],然后安装 [email protected],并安装其依赖 [email protected],发现项目根目录的 node_modules 中并没有 B 模块的任何版本,于是就安装在了根目录的 node_modules 中。

得到目录结构为:

node_modules
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]

坑爹呢,[email protected] 存在了很多个副本了。但也不要紧张,通常 npm 会利用链接来将多个副本指向同一个模块。这样的目录结构虽然觉得有些浪费,但是对代码运行没有丝毫影响。也许你想让他好看一点,没有问题,执行命令:

npm dedupe

该命令会遍历模块依赖树,根据模块之间的依赖关系,移动模块的位置,去除重复,让整个 node_modules 的目录结构更加扁平一些。

node_modules
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]

目录结构的不确定性

模块的安装次序决定了 node_modules 中的目录结构,这也是为什么明明 dependencies 中依赖的模块但得到的目录结构不同,假如有如下两个模块需要安装:

安装 A 和 C 的次序不同得到的 node_modules 也就不同,因为 npm 会优先将模块放置在根目录下的 node_modules 中,所以先安装 A 和 C 中的哪一个决定了在 根目录下的 node_modules 中存在的是 B 的 2.0.0 版本还是 1.0.0 版本。

只有在手动使用 npm i <package> --save 的时候才会出现这种情况,使用 npm i,npm 会去读取 package.json 中的 dependencies,而 dependencies 是安装字母顺序排列的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant