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
有类文件,Python
有import
机制,Ruby
有require
,而javascript
的script
标签加载方式则显得杂乱无章。
渐渐的,javascript
社区中也提出了js模块化
概念。模块即是一个独立的javascript文件
,他可以是一个类的定义,一个使用函数库或者是一些待执行的代码......他可以被独立的进行引用和被其他模块引用。
终于,在2009年,Ryan Dahl的nodejs
亮相于世。nodejs
将javascript
带到了服务端的同时,也很优雅的解决了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包,例如react
、axios
、redux
等,他是我们需要在代码中引用使用到的。第三种,我们可以将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/core
和lerna create @lerna-test/utils
对于包命名来说,通常的规范是,例如我们想要暴露出来的名字为
lerna-test
,那么他的入口包就叫lerna-test
,其他子包皆以@lerna-test/
开头例如@lerna-test/utils
这是会在packages
目录下创建三个包,每个包都有独立的npm结构。
现在,我们需要在lerna-test
中引用其他两个包,分别使用命令lerna add @lerna-test/core --scope=lerna-test
和lerna 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
字段