npm和package.json
npm原来只是node.js的包管理工具,从npm仓库下载包,向npm仓库发布包,npm仓库位于https://registry.npmmirror.com/,UI页面在https://www.npmjs.com/,现在也能执行命令了,如npx,npm run等。下载包和执行命令需要记录,发布包需要对包进行描述,这都用到Node.js的一个核心文件,package.json,所以创建node项目,首先会创建一个pacakge.json文件。执行npm init,需要回答问题,npm init -y 使用默认配置
{ "name": "npml", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
name是项目名称,version是版本号,通过它们找到下载包,也是对包的基本描述。版本号有严格的规范,由三个部分组成,每一部分都是数字,格式为Major.Minor.Patch。Major表示大版本,通常是大的功能升级,比如删除过时的功能,和以前的版本不兼容,是破坏性升级。Minor表示小版本,通常是增加了一些新功能,不会破坏以前的功能。Patch表示补丁版本,通常是一些bug的修复,功能不变。main是Node.js早期定义的使用包时要执行的文件。scripts是要执行的命令
应用开发
项目开发通常会用到第三方包,比如React,Express,首先要安装它们,npm install会先从npm仓库下载,安装到npm的缓存中,然后再解压到项目node_modules中,同时在package.json的dependencies进行记录。npm install express,项目多了node_modules目录,同时package.json文件多了一个dependencies字段,包括了express,还指定了相应的版本号。安装包时,没有指定版本号,就安装最新版本,但版本号前面多了一个^("express": "^4.15.2"),^表示可以安装4.x.x 的版本,只要中间的x大于15就可以,这主要影响多人协作开发。比如你安装express,并把package.json上传到git仓库,其他人拉取代码,会执行npm install。npm install会按照包在package.json中的顺序,从上到下依次安装。如果安装时,express升级到了4.18.2,他安装也是4.18.2版本,通常没有问题,但如果他在项目中使用了4.18.2 中的内容(res.cookie设置优先级 )。当你再拉取代码,本地报错,因为本地node_module中的expess版本是4.15.2,没有这个功能。npm install按从上到下顺序安装包,也会带来问题,因为它扁平化安装包,包和它依赖的包安装到同级目录,包也称为项目的依赖。如果App项目依赖A,A依赖B,A和B在node_modules同一级目录
项目再依赖C,C依赖B(v2.0),npm先安装C,然后尝试把B安装到C同级目录,也就node_modules顶级目录,但node_modules顶级目录下面已经有了B 1.0依赖,不能再安装B 2.0依赖,否则就冲突了,所以B(v2.0)只能安装到C目录下
假设项目再依赖D和E,D依赖B(v2.0),E依赖B(v1.0),先安装D顶级node_modules中,再尝试安装B,还是由于node_modules顶级目录中安装了B1.0, B2.0要安装到D下面。而E就不一样了,顶级node_modules安装E,再安装B,发现顶级node_modules已经有了B1.0,所从E的依赖B1.0 就不用安装了。
现在升级A到2.0,正好它也依赖B2.0,首先是package.json中A依赖的版本号从1.0 到2.0, 再在node_module中,删除掉A1.0,安装2.0,不能删除B1.0 因为E在用,最后安装B2.0
现在package.json中的依赖如下
"dependencies": { "mod-a": "^2.0.0", "mod-c": "^1.0.0", "mod-d": "^1.0.0", "mod-e": "^1.0.0" }
此时,项目初始化完成了,提交git仓库。其他人拿到新代码,就npm install,由于npm从上到下依次安装依赖,他的node_modules目录如下
安装顺序起到了重要作用,npm install 先安装A2.0, 它依赖B2.0, 所以在node_modules顶级目录中安装了A2.0和B2.0,由于C和D都是依赖B2.0, 只在node_modules中安装C和D就可以了,B2.0已经存在,安装E时,它依赖B1.0,所以在它的node_modules下面安装了B1.0。npm install一个依赖时,它会从当前目录向上找,如果在祖先node_modules中找到符合的依赖,它就不会在本目录下安装,这也解决了循环依赖问题。但这会引起问题,比如 import {} from B, 其他人引用是B2.0, 而你引用的是B1.0,代码也会报错。
为了解决以上问题,package-lock.json出现了,它不仅记录准确的版本号,还记录了安装顺序。npm install虽然在package.json中记录的是“^”,但package-lock.json中记录的却是精确的版本号,package-lock.json会准确的记录安装的是哪一个版本。A从1.0升级到2.0,package.json中只会把A升级到2.0,但package-lock.json 则会记录
{ "dependencies": { "A": { "version": "2.0.0", "requires": { "B": "2.0.0" }, "dependencies": { "B": { "version": "2.0.0" } } }, "B": { "version": "1.0.0", }, "E": { "version": "1.0.0", "requires": { "B": "1.0.0", } } } }
node_modules中B是1.0,A依赖B2.0。npm install时,只要按照package-lock.json中进行安装,就没有问题。
那这又带来了一个问题,每个人的电脑上都安装的固定版本,怎么升级?重新安装包(直接npm i express 或更改package.json,再npm install),会从npm仓库拉取最新的包,并同步更新package.json 和package-lock.json。package.json和package-lock.json中的版本不一致,npm会依据package.json安装依赖,并同步更新package-lock.json。把E升级了,它依赖B2.0,npm会删除掉E1.0,安装E2.0,同时删除掉B1.0,因为没有模块依赖它了,安装B2.0到node_module顶级目录。
这时,又有一个问题,模块B2.0 在每一个目录中,为了移除冗余,可以使用 npm deque, 这个命令找到依赖B2.0的模块,然后,重定向到顶级目录中的依赖,然后删掉嵌套的依赖b2.0
node_modules目录结构没有办法显示依赖关系, 可以使用npm ls 命令,列出依赖及其关系,列出主依赖,则是 npm ls --depth=0。npm list 包名,可以列出某个包在哪个包下用到
当然并不是所有的依赖都是程序运行所需要的依赖,比如jest,仅在开发时使用,npm install时要加 --save-dev 或 -D,表示开发依赖。npm install jest --save–dev,package.json中多了devDependencies,包含jest。Node.js刚出现时,主要用于web服务器开发,此时dependencies和devDependencies容易区分,web服务运行时所需要的依赖就是dependencies 中,不需要但在开发中使用的依赖就是devDependencies。node index.js 启动服务,index.js及其引用的文件所引用的模块就是dependencies,其他全是devDependecies。但随着Node.js的发展,它用到了前端打包工具,比如webpack, 运行node.js 只是为了运行webpack进行打包,把多个文件打包成一个文件,完全没有服务器开发的概念,也不存在服务运行时的所需要的依赖。其实webpack(打包工具)打包时,它也不在乎dependencies还是devDependecies,它只会从入口文件开始遍历,只要文件中引入了一个依赖,它就会把这个依赖打包到最终的build文件中,它只关心依赖在node_modules中有没有,而不关系依赖在package.json中是devDependencies 还是 devDependecies,依赖记录在哪里无所谓。但通常会把打包到最终输出文件中的依赖放到dependencies ,其他依赖放到devDependencies 。如果不想把一个包打包到最终的输出文件中,不是把这个包放到devDependecies 中,而是使用webpack的external配置。
有一次npm insall,报了一个错误Unexpected end of JSON input while parsing near ‘--- “https:// github.com/w”’,打开node安装依赖报错的日志,它有一个fetch 304(from cache),竟然是缓存有问题,需要清缓存,然后再重新npm install。清缓存用npm cache verify或npm cache clean --force, 如果不起作用,就找到npm cache存放的位置,Windows下在C:\Users\{用户名}\AppData\Roaming\npm-cache,把整个文件夹全删掉。
npm config: 设置或获取npm 的配置信息,用的最多的是配置源。npm config list 可以获取整个npm 配置信息。npm config get <key> 可以获取某个key的配置信息。比如npm config get registry, 获取npm 配置的源。npm config set <key> 则是设置某个key的值,比如npm config set registry="taobao.register.org". npm config delete <key> ,则是删除某条配置信息。
开发node.js程序,有两种规范(CommonJS和ESM),文件的后缀名 .js默认是CommonJS规范,如果要使用ESM,可以把文件后缀名改为.mjs,也可以在package.json中设置"type": "module"。运行程序,就要执行命令,"script"定义项目中需要执行的命令,然后用 npm run 去执行,
"scripts": {
"build": "babel index.js -o server.js"
},
npm run build, 就会执行 babel index.js -o server.js,babel是从node_module中读取bin目录下定义的命令。如果安装包的是命令包,比如babel-cli,它里面的bin目录会链接到node_modules中的./bin目录下,当在package.json中的script写命令,然后使用npm run 执行时,npm 会找到相应的命令执行。scripts命令中有pre和post, pre<name>, 会在某个name命令之前,自动执行,post<name>则是在某个name命令执行之后执行。
"scripts": { "prebuild": "rm -rf build", "build": "rollup --config", "postbuild": "echo build complete" },
npm run build, prebuild、build、postbuild这三个命令依次执行。
npm create 根据模版创建项目,npm create react-app my-app 相等于npm install -g create-react-app, 然后 create-react-app my-app, 它借鉴了yarn create
包开发
包开发和应用开发过程一样,但它的测试,它的发布却比应用开发复杂。它是一个单独的存在,需要把它放到应用程序中进行测试。可以使用npm install,npm install能安装本地依赖。本地有一个文件夹sum,它里面有一个package.json.
{ "name": "sum", "version": "1.0.0", "main": "index.js" }
假设index.js文件
module.exports = function sum(a, b) { return a + b; }
sum文件夹已经变成一个package。在sum同级目录,新建node.js项目app, npm i ../sum,app的package.json中的dependencies 中有了sum
"dependencies": { "sum": "file:../sum" }
但当sum包更新时,app中也要及时更新,可以重新install,也可以npm update sum 更新项目依赖。当sum包频繁更新时,不得不在app项目中不停的npm update, 此时可以使用npm link。npm link 模块使用是软链接或快捷方式,使用软链接或快捷方式链接到本地模块。链接一个模块有两步
1,创建一个全局链接到本地模块。在sum目录下 npm link, windows 下C:\Users\{用户名}\AppData\Roaming\npm\node_modules\sum -> D:\sum,Linux下 /usr/local/lib/node_modules/sum -> /home/sam/sum,就是在全局node_modules下,创建一个软链接或快捷方式sum,指向本地模块sum,也相当于在全局node_modules下安装了sum。
2,创建一个本地链接。在app项目中,npm link sum, windows下D:\app\node_modules\sum -> C:\Users\Admin\AppData\Roaming\npm\node_modules\sum -> D:\sum, linux下, /home/sam/app/node_modules/sum -> /usr/local/lib/node_modules/sum -> /home/sam/sum。app中node_modules中的sum链接到全局,全局指向了真实的sum模块。由于都是链接,sum的更新能够及时反应到app项目中。链接完成,app项目node_modules下面有了sum。
npm unlink 则取消链接模块. 在 app项目下,npm unlink sum, 则取消链接sum模块,node_module变成了空目录。
发布,定义包暴露的内容,package.json中的exports字段,定义哪些内容可以从包中导入,哪些内容不能导入,以及导入内容的名称。如果未在exports中列出,则使用者无法import/require它。换句话说,它就像包用户的公共 API,有助于定义哪些内容是公共的,哪些内容是内部的
"exports": { ".": { "module": "./dist/index.mjs", "import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" }, "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }, "default": "./dist/index.mjs" }, "./package.json": "./package.json" }
"."定义了包的默认入口,当import或require时,先找exports定义的入口,从上到下依次匹配,碰到匹配的就停止,顺序很重要。module是rollup和webpack提出的,它指向esm build目录。当package.json中定义module,webpack和rollup就会读取它,不管代码中使用import还是require,所以放到了最前面。代码中使用import时,读取"import" 定义(指向esm目录)。代码中使用require时,读取"require" 定义(指向cjs目录)。default只是提供fallback,暂时没有用。"exports"代替了以前top-level层的"main"、"module"和types(ts类型目录),如果package.json中定义了exports,那就会忽略main和module的设置,只有没有exports时,才使用它们。 看一下@compiled/react 的package.json
除了能import 整个包,还能import('@compiled/react/runtime')和import('@compiled/react/babel-plugin')。当有了exports之后,Node.js 解析查找module的规则都发生了变化,首先解析路径 (@compiled/react/runtime),从中提取包范围(@compiled)、包名称(react)和子路径(路径中的所有其他内容,即 /runtime),然后从范围+名称组成的路径(@compiled/react)下, 查找有没有 package.json,如果有,读取exports字段,尝试定义的入口与所需路径(/runtime)进行匹配,匹配成功,找到'./dist/esm/rutime.js',这时不管代码中使用import还是require都会读取'./dist/esm/runtime.js'。更兼容的方式是import 读取esm,require读取cjs,
如果匹配失败,那就找不到,报错了。如果范围+名称组成的路径(@compiled/react)下,没有package.json或package.json中没有exports,会查找全路径@compiled/react/runtime,是文件,就直接使用,是目录,就按照目录的方式解析。
"files"包含发布的内容
"files": ["dist"]
如果不确定是否配置正确,npm pack打一个tar包,包含发布的所有内容,然后检查它。设置sideEffects,包有没有副作用,利用打包软件的tree shaking。"sideEffects": false 表示没有副作用,"sideEffects": ["module.js"] 表示只有module.js有副作用,其他没有副作用。
{ // all modules are "pure" "sideEffects": false }
peerDependecies告诉使用者,包依赖另外的包,需要进行提供,要不然,该包无法使用。比如开发一个react的UI库,它依赖react,把react加到peerDependecies。既然peerDepencies需要外部提供,那就没有必要打包到bundle.js中,那么就需要告诉webpack, 它有一个external 属性,打包排除。
output: { path: path.resolve(__dirname, 'build'), filename: 'index.js', libraryTarget: 'commonjs2' }, externals: { 'react': 'commonjs react' }
到底什么依赖是peerDependecies?开发包的时候,包的功能返回了依赖的api,比如React UI库,肯定返回了React.createElement, 依赖react。再比如
import { Jpeg } from 'jpeg' export const createSquareJpeg = (size) => new Jpeg(Buffer.alloc(size * size, 0), size, size)
包暴露的API(createSquareJpeg)也暴露了jpeg依赖的API(Jpeg),所以jpeg依赖是peerDependecies
css文件通常是打包一个文件,有没有必要一个组件,一个css文件
维护升级时,一定要按照规范进行版本号的升级,比如修复bug,就只增加patch 部分,版本号变成"1.0.1", 还要注意,要一个一个版本号加,不能跳跃,不能从"1.0.1" 升级到"1.0.3"。增加功能就升级Minor部分,patch部分要重置为0,从"1.0.1" 升级到"1.1.0"。升级大版本时,小版本和补丁版本部分都要重置为0,从"1.1.0" 升级到"2.0.0"。