npm(Node Package Manager)

npm历史发展与解决的问题

npm,这个使用javascript开发的全球最大的软件包管理系统,聊到它的时候就不得不的提到javaScript代码管理的发展历史。

javascript,这门被在10天内开发出来的脚本语言,甚至连名字的由来,也是蹭了java的热度,可能连Brendan Eich自己都没有想到能发展到如今这么庞大。

web1.0时代,javascript的作用普遍用于处理表单和页面布局。直至web2.0流行开来,对于前端的优化需求逐渐变得棘手,项目逐渐变得庞大,很多复杂逻辑也有了被抽离共用的必要。例如jQuery,他封装了dom操作,使得dom操作变得更加方便,于此同时也兼容了不同版本的浏览器。想要在项目中引用这段共用逻辑,这也有了早期引用js文件的方式

<script src='./jquery.js'></script>

然而这样引入js文件的问题很多,当我们依赖的依赖的文件变多,并且存在一个文件依赖于另一个文件时,操作便会变得更加复杂,例如我们的引用关系如下:

使用script标签需要严格按照他们的排列顺序进行引用

<script src='./e.js'></script>
<script src='./d.js'></script>
<script src='./b.js'></script>
<script src='./c.js'></script>
<script src='./a.js'></script>

这样,我们在开发时就会需要花费更多的时间来对js文件的引入顺序进行管理。想要让javascript去开发大型前端项目,就不得不去解决各个js文件相互引用的问题。

与此同时,随着js的不断壮大,另有一部分开发者们更希望将js的开发目标放到服务端上,然而当时的服务端的逻辑结构远比前端复杂的多,在代码的组织上,再一次道出了javascript天生的缺陷问题——众多依赖文件的组织上。在其他高级语言中,java有类文件,Pythonimport机制,Rubyrequire,而javascriptscript标签加载方式则显得杂乱无章。

渐渐的,javascript社区中也提出了js模块化概念。模块即是一个独立的javascript文件,他可以是一个类的定义,一个使用函数库或者是一些待执行的代码......他可以被独立的进行引用和被其他模块引用。

终于,在2009年,Ryan Dahl的nodejs亮相于世。nodejsjavascript带到了服务端的同时,也很优雅的解决了js模块化的问题。他正是参考commonjs规范。在服务端,模块化的出现使得js如鱼得水,发展迅速。

Eg1:规范只是理论,并不代表实践结果。例如ES规定了javascript的规范,他规范了通过var可以定义一个变量,而浏览器需要去实现它,至于如何去实现,那便是浏览器厂商需要思考的事情了。

Eg2:commonjs是一个项目,它意在为javascript在浏览器之外的开发创造规范。它的提案涵盖了模块、包、系统、编码、二进制、文件系统、输入输出等方面。这无疑是想将js推向与其他服务端高级语言并肩。

但对浏览器上的模块化发展并没有如此容易。因为浏览器单线程同步加载的问题,倘若像服务端中commonjs规范的那样,像下面这样执行

const math = require('math')
math.add(1,2)

那么浏览器会等到完全加载完math模块在向下执行,浏览器就会停在此处不在继续加载。所以,前端的模块化需求实则是和服务端不同的。但尽管如此,AMD(Asynchronous Module Definition)规范终究解决了这个问题。requirejs与curljs便实现了这个规范,很多流行的库例如Dojo也将AMD规范内置了进去。

模块概念的实现使得原来的类库重写为模块,并更加易于引用。但尽管如此,此时的javascript代码的管理模式仍有很大的问题。例如代码共享,对于一个需要引用众多模块的项目,我需要去官网或者在之后出现的github上去进行模块的下载然后引用,这无异又是一件繁琐的事情。此外,很多模块共同完成一件事,他们也属于某种意义上的整体,然而每次项目中,我们除了要关注要引用的模块外,还需要关注这些模块本身的依赖模块。

而为了解决这些问题NPM应运而生。

针对众多模块分散难以分享的问题,NPM平台设置了远程服务器来存储需要被共享的包,包的作者使用npm的工具将包发布到云端,使用者只需要一条命令即可将包下载到本地

然后针对针对模块依赖分散的问题,npm参照commonjs包的规范将它进行了实现。在包中,描述了这个模块所依赖的其他模块(包),并且npm提供命令让我们一键下载他的所有依赖包。包的出现使得我们可以在模块的基础上进一步进行代码组织。

为了让npm更好的服务开发者,npm的作者Isaac而后与Ryan Dahl进行商定,在nodev0.6.3时将npm集成进入node。在不久后Ryan Dahl离职后,Isaac也成为node新一代掌门人。至此之后,js的文件管理也渐渐得到了稳固的管理。

对于npm的管理方式其实也存在异议,就连Ryan Dahl也再后来指出了它的众多缺陷,可了解此处,但不管怎样,它已经能够很好的解决我们再业务中遇到的大部分问题。

包的作用

通常来说,npm包有三种常见的作用

  • 命令行工具
  • 应用中共有的逻辑功能
  • 用于存储

第一种,命令行工具,最常见的例如create-react-app,我们将它下载到全局,然后使用create-react-app my-app来初始化一个react项目。第二种,也是我们使用最多的npm包,例如reactaxiosredux等,他是我们需要在代码中引用使用到的。第三种,我们可以将npm看作一个仓库,来将一些常常需要用到的文件存于此处,常见的例如cra-template,这是一个react的基础项目的模板,在我们使用create-react-app去安装一个项目的时候,实际上就是将cra-template包中的代码下载到本地你指定的目录中,此时,该npm包便作为存储介质而存在。除此之外,我们的应用本身也可以理解为是一个npm包,他的作用就是更好的去存储和管理我们的应用。

包的规范

npm中的包是参照了commonjs中包的规范。在commonjs中,它规范了包的结构与描述文件。

对于包结构,完全符合commonjs的包结构如下

- package.json # 包描述文件
- bin # 用于存放可执行的二进制文件的目录
- lib # 用于存放javascript代码的目录
- doc # 文档说明
- test # 单元测试

而对于包的描述文件(即package.json中的字段),commonjs中的规范如下

{
  "name": "npm-test", // 包名
  "version": "1.0.0", // 版本
  "description": "用于测试", // 简介
  "keywords": [ // 用于检索的关键词
    "node",
    "test"
  ],
  "maintainers": [ // 包的维护者列表
    {
      "name": "xiaweixuan",
      "email": "xia_weixuan@163.com",
      "web": "https://xiawx.top"
    }
  ],
  "contributors": [ // 贡献者列表
    {
      "name": "xiaweixuan",
      "email": "xia_weixuan@163.com",
      "web": "https://xiawx.top"
    }
  ],
  "bugs": "xia_weixuan@163.com", //反馈bug的网址
  "license": "ISC", // 许可证
  "repository": { // 托管代码的列表
    "type": "git",
    "url": "https://github.com/xiaweixuan/npm-test.git"
  },
  "dependencies": { // 包的依赖
    "seahorse-cli": "^0.0.1"
  },
  "homepage": "https://github.com/xiaweixuan/npm-test", //当前包的主页
  "os": [ //操作系统支持列表
    "macos"
  ],
  "cpu": [ // cpu支持列表
    "arm"
  ],
  "engine": [ //js引擎支持列表
    "node",
    "v8"
  ],
  "builtin": "", // 标志当前包是否为内建在底层系统的标准组件
  "directiries": {}, //包目录说明
  "implements": [], //实现了那些commonjs规范
  "scripts": {
    "start": "node script.js",
  }
}

而在此基础上,为了更方便的管理node包,npm做了在此基础上做了补充,添加了

{
  "author": "xiaweixuan", // 包的作者
  "bin": { // 安装后将被注册的命令
    "npm-test": "npm-test.js"
  },
  "main": "index.js", // 被require引入时的默认入口
  "devDependencies": { // 开发依赖
    "seahorse-cli": "^0.0.1"
  }
}

对于一个项目而言,name、description、version、author、contributors、keywords、repository这些属于信息类,是需要你填写的。此外,根据我们包的不同作用还需要添加一些其他字段。

例如我做的是一个命令行工具,则需要填写bin属性,再填写"npm-test": "npm-test.js"之后,将该包安装到全局,会自动把npm-test注册为全局命令。在命令行使用npm-test时,则会去调用npm-test.js中的内容。

例如我们要做一个公用逻辑的包,通常需要添加main字段,填写"main": "index.js"后,我们在一个前端项目中下载了此包作为依赖,在代码中使用require('npm-test')将该包的内容引出时,实则是引出了index.js文件中的内容。

例如我们要做一个用于存储的包,例如一个用react编写的前端项目,我们通常需要添加script字段,例如"dev": react-scripts start,当我们在命令行输入npm run dev时,他默认执行react-scripts start命令。

npm包的制作、发布流程

npm的常见命令如下,更多命令可查看官方文档

npm init # 初始化一个项目
npm link # 将本模块链接到全局
npm list --depth=0 -global # 罗列全局安装的模块
npm install # 下载依赖
npm install --production # 下载生产依赖
npm install [packageName] # 下载指定依赖
npm uninstall [packageName] # 卸载指定依赖
npm login # 登录
npm publish # 发布
npm unpublish npm-test-111xwx --force # 删除包,一个包发布后72小时内可以删除
npm run [customCommand] # 执行自定义命令
npm config set registry https://registry.npmjs.org # 切换源
npm config get registry # 查看当前源
npm deprecate [packageName]@1.1.0 'tipe content' # 废弃包的指定版本

我们以制作一个命令工具为例进行演示。

首先我们进入叫npmtest的目录执行npm init将该目录初始化为一个npm包。初始化完成后,他帮我们自动生成了一个package.json文件。此处我们创建一个index.js作为我们编写代码的文件。

Eg:此处示例将不按照标准目录结构进行创建。

package.json中加入bin字段

{
  "name": "npmtest",
  "version": "1.0.0",
  "description": "a script for test",
  "bin": {
    "npmtest": "index.js"
  },
  "author": "xiaweixuan",
  "license": "ISC"
}

这样,当我们在全局使用npmtest这个命令的时候,便会去执行index.js文件。但是系统是不识别js语言的,这个时候,我们就要在系统执行index.js的时候告诉系统,使用node环境去执行它,所以我们在index.js文件的首行加入

#! /usr/bin/env node

然后,我们随便写点什么逻辑

#! /usr/bin/env node

console.log('你执行了npmtest命令')

然后,开始测试我们的逻辑。当然,你可以在现在的目录下的命令行输入node index.js来检测输出,但在实际开发中并不可取,例如我们开发的是一个脚手架工具用来初始化项目,我们一定是希望他能在任何目录下执行测试,此时,我们在此目录下输入npm link

macos或linux下,需要添加sudo

完成后,该命令就被挂到了全局,相当于执行了npm -g install npmtest,但是远程并没有我们的仓库,所以存储在全局的只是只想此处文件的一个软连接。现在,我们可以在任意地方使用该命令。

在测试完成后,我们使用npm -g uninstall npmtest将全局上模块卸载掉。

在发布阶段,首先使用npm login进行登录,然后即可使用npm publish进行发布。

Eg:使用publish命令是将该包发布到当前npm所绑定的源,在企业中,每个企业通常有自己的仓库,我们需要先使用npm set切换到相对应的源,然后再进行登录发布

现在,我们要开发一个公用逻辑的模块,初始化的阶段和之前相同,我们仍然将index.js作为我们编写逻辑的入口。然后我们填写package.json

{
  "name": "npmtestfn",
  "version": "1.0.0",
  "description": "a script for test",
  "main": "index.js",
  "author": "xiaweixuan",
  "license": "ISC"
}

index.js我们应该暴露出去我们公用的逻辑,例如如下

function npmtestfn() {
  console.log('你调用了 npmtestfn 方法')
}

export default npmtestfn;

Eg:此处使用了es6的模块化规范进行导出,在实际开发中可根据实际项目需求选择不同的模块划规范。

现在,我们想在我们的前端项目中去使用这个模块,首先还是需要先进行npm link将它链接到全局。然后进入我们要使用的前端项目(my-app)的目录下,调用npm link npmtestfn即可将模块连接到此处,然后在项目中,我们即可引入来使用它

import npmtestfn from 'npmtestfn';

npmtestfn()

在测试完成后仍然使用之前的方法去进行发布。

lerna

解决的问题

包的出现,更好的管理了更多的模块。然而在面对一个复杂功能或复杂功能集合的javascript项目时,我们通常会将逻辑拆成多个包进行开发与维护。例如react脚手架这个项目,而由于这个项目逻辑过于庞大,我们需要将他的逻辑进行拆分,将核心逻辑放入create-react-app这个包中,将它注册的所有命令的逻辑放到react-script这个包中,将初始化项目时需要下载的模版放入cra-template......而这么多的包实则是一个整体,在开发时应该统一管理,这时便会遇到问题。

因为我需要管理这些相互依赖的包的版本引用,在react-script包进行升级后,我需要在所有依赖它的包内的package.json中将它的依赖版本进行修改。而当所有包的依赖关系过于复杂时,管理他们的版本则是不小的人力。lerna的出现就是为了解决这个问题。

使用lerna可以帮助你更好的管理多个包的开发与发布。

使用方法与基本操作

lerna的常用操作如下,更多操作可在官网产看

lerna init # 初始化一个lerna项目
lerna create [packageName] # 创建子包
lerna add [packageName] # 为所有子包添加依赖包
lerna add [packageName] --scope=[myPackageName] # 为制定包添加依赖包
lerna bootstrap # 为所有包下载依赖
lerna bootstrap --hoist # 抽离公共的包到最外层node_module,但并不写入全局的package.json中
lerna publish # 全部发布

下面将展示一个项目创建、开发及发布流程

使用lerna init初始化后会初始化当前的目录,出现以下结构

- packages
- lerna.json
- package.json

packages目录是存放一个个npm包的目录,lerna.json是对此lerna项目的一些配置,新项目包括以下字段

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

其中packages字段指定了存放npm包的目录,当前所指的是packages下所有文件会被当作npm包。

在完成项目初始化后,开始创建多个npm包,lerna create lerna-test来创建第一个npm包,随后在创建两个包分别使用lerna create @lerna-test/corelerna create @lerna-test/utils

对于包命名来说,通常的规范是,例如我们想要暴露出来的名字为lerna-test,那么他的入口包就叫lerna-test,其他子包皆以@lerna-test/开头例如@lerna-test/utils

这是会在packages目录下创建三个包,每个包都有独立的npm结构。

现在,我们需要在lerna-test中引用其他两个包,分别使用命令lerna add @lerna-test/core --scope=lerna-testlerna add @lerna-test/utils --scope=lerna-test,然后即可在lerna-test的目录下进行引用了。

'use strict';
const core = require('@lerna-test/core');
const utils = require('@lerna-test/utils')

module.exports = lernaTest;

function lernaTest() {
    // TODO
}

如果我们需要为utils安装axios这个包,即可使用lerna add axios --scope=@lerna-test/utils,如果lerna-test、utils、core这三个包都需要用到axios这个包,那么直接使用lerna add axios

如果你开发的是一个命令行工具,那么在开发完毕前,需要对他去进行检验。这时候,我们进入lerna-test包中去执行npm link

Eg:mac或linux系统下在执行全局命令时需要在命令前添加sudo,例如sudo npm link

这时你会发现控制台报了错误

npm ERR! 404  '@lerna-test/core@^0.0.0' is not in the npm registry.

这是因为,在第一次发布前,该包是不存在的。此时无法找到该包的引用,我们需要手动去lerna-test这个包的package.json中将依赖更改路径(发布前需要修改回去)

"dependencies": {
  "@lerna-test/core": "file:../core/",
  "@lerna-test/utils": "file:../utils/"
}

再次使用sudo npm link即会成功。可以在全局使用此命令。

Eg:修改包中的内容后无需重新npm link,但是如果修改package.json中的字段,例如修改bin字段来更改命令名字时,则需要重新npm link。

在调试完成后,即可进行发布,使用lerna publish即可同时发布所有包。

发布前需要注意以下几点:

1、在发布前需要提前使用npm login进行登录,否则会有没有权限的报错

2、需要把最外层lerna项目的package.json文件中添加"private": true字段