什么是package
package 指拥有 package.json 的一个文件夹(或压缩包),而 package 的属性就是 package.json 文件的内容,比如:
name:这个包叫什么名字,唯一version:这个包的版本号是多少main:这个包默认引入的是哪个文件homepage:这个包的官网或者文档
semver
semver,语义化版本,它由[major,minor,patch]三部分组成,其中
major:包中发生Api级别的变化时,递增major版本号minor:新增一个向后兼容的功能时,递增minor版本号patch:修复一个向后兼容的bug时,递增patch版本号 除此之外,一些大型包的管理中,如果没有正式发布,会选择使用prerelease版本号,例如1.0.0-alpha。在比较小的包就没必要使用。
semver 与语言无关,不仅在 JavaScript 中使用 semver,在其它一些语言中也可以使用 semver 该语义化版本命名版本号。
我们接下来拿vue举一个例子
-
Vue 2 到 Vue 3 的迁移是一个 major 版本更新的例子,涉及了不兼容的重大变化,比如:
- Composition API 替代 Options API: 从 Vue 2 的 Options API 到 Vue 3 的 Composition API,组件逻辑写法有较大改变。
- 基于 Proxy 的响应式系统: Vue 3 使用 Proxy 取代了 Vue 2 的 Object.defineProperty,影响了响应式数据的更新和追踪。
- Fragment 语法: Vue 3 引入了 Fragment 语法,不需要像 Vue 2 一样在模板中使用额外的包装元素。
-
如果 Vue Router 新增了一个向后兼容的功能,会递增 minor 版本号。假设在
3.0.0版本中没有的导航守卫的选项被添加到3.1.0版本,这个变化不会影响到已有的导航守卫。 -
当 Vue Router 修复一个向后兼容的 bug 时,会递增 patch 版本号。例如,假设
3.0.0版本中的一个已知 bug 被修复,就会发布3.0.1版本。
版本号范围·
当我们手动安装一个包,它写在 package.json 中的是一个版本号范围。
{
dependencies: {
lodash: '~1.8.1'
}
}
版本号一般有~ ^两种
对于~1.2.3来说,它的版本号范围是>=1.2.3 <1.3.0
对于^1.2.3来说,它的版本号范围是>=1.2.3 <2.0.0
我们在写项目时可以使用 yarn.lock/package-lock.json 锁定版本号。
依赖
依赖分为dependency和devdependency两种
它们两者之间的区别就是一个是项目运行时所需要的依赖,一个是开发时的依赖是不会打包进最终产物的。
除此之外,它们在包开发和项目开发时也有差别。
- 在开发包时,
devdependency和dependency需要有严格的区分,因为在npm i xxx时,只会下载xxx包package.json目录下的dependency,如是略有差错,项目就会出错, - 在开发项目时,这两者就不必要有很严格区分。不管是使用
webapck还是vite打包项目时,这些打包工具都会对项目依赖进行分析,用到的打包,不用的剔除。不过作为一种规范,能遵守还是得遵守。
URI as dependency
{
"dependencies": {
"npm": "git+ssh://git@github.com:npm/cli.git",
"foo": "http://q.shanyue.tech/foo.tar.gz",
"bar": "file:../bar"
}
}
这种写法常用于一个项目依赖另一个项目,但又不想发包,我们就可以直接用它仓库url作为依赖。
依赖别名
一个项目需要用到一个包的两个版本,此时我们就需要使用依赖别名来区分
$ npm install <alias>@npm:<name>
$ npm install vue2@npm:vue@2
$ npm install vue3@npm:vue@3
package.json:
{
"dependencies": {
"vue2": "npm:vue@2",
"vue3": "npm:vue@3"
}
}
engines
一个项目所需的node最小版本,确保你的 Node.js 包在正确版本的环境中稳定运行。
示例:
{
"engines": {
"node": ">=14.0.0"
}
}
当我们的node版本<14时,此时npm会发出警告,提醒node版本不符合 而yarn会直接报错
error next-app@1.0.0: The engine "node" is incompatible with this module. Expected version ">=14.0.0". Got "10.24.1"
sideeffects
sideEffects 用于指示npm包是否具有副作用。
副作用指的是模块在导入时会产生除了导出值之外的其他影响,比如修改全局变量、执行一些代码等。在现代的 JavaScript 模块系统中,为了优化打包和代码分割,打包工具(比如Webpack)会尝试去除那些没有副作用的模块,以减小打包后的代码体积。
在package.json中,我们可以通过以下方式配置sideEffects
{
"name": "redux",
"version": "5.0.0-beta.0",
"sideEffects": false
}
false: 表示模块没有副作用,可以被安全地删除。这通常用于纯粹的导出模块,例如只包含函数、类、对象等,没有执行任何其他操作的模块。true: 表示模块具有副作用,不会被删除。这是默认值,如果你不在package.json中显式设置sideEffects,那么模块会被认为具有副作用。- 字符串数组:可以列出模块中具有副作用的文件的路径。这些路径会被用于指定哪些模块具有副作用,从而防止它们被删除。
使用场景举例:
{
"name": "my-react-app",
"dependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0",
"lodash": "^4.0.0"
},
"sideEffects": [
"node_modules/lodash/*.js"
]
}
在这个例子中,我们使用了React和React DOM作为依赖项,并且还使用了Lodash库。但是,我们可能只使用了Lodash的部分功能,其他部分具有副作用。通过设置 sideEffects 字段,我们告诉打包工具只保留Lodash库中我们使用的部分,并且删除没有副作用的部分。这可以显著减小打包后的代码体积,提高应用程序加载速度。
一句话概括它的作用:打包器webpack、vite等通过这个字段进行Tree Shaking优化,它会安全地删除未使用的模块,减小最终打包体积。
npm scripts
npm scripts为js项目提供了执行脚本快捷方法,npm run <command>就可以执行对应的脚本。
一些常见的自动化场景都可以使用它来完成,如编译代码、运行测试、启动服务器等。
在 package.json 默认的 scripts 有:
install:依赖安装start:启动服务test:测试项目
他们可以通过 npm <command> 直接运行,如 npm install、npm start。
除了默认的 scripts 外,还有一些约定俗成的脚本,比如:
build:构建打包dev:开发环境lint:格式化
对于此类自定义的 scripts 需要 npm run <command> 方可执行。
一次执行多条命令可以使用&符连接
pre/post script
npm scripts可以完成一系列自动任务,提高我们的开发效率。除此之外,还有一些好用的钩子,可以提高npm scripts效率
当我们npm publish发包之前忘记npm run build,导致发包无效,这时就需要用到 pre/post script,它允许我们在npm xxx前后自动执行脚本。
当我们在手动执行 npm run xxx 时,如果 prexxx 及 postxxx 在 scripts 存在时,它将会自动执行 npm run prexxx 以及 npm run postxxx 。
{
"scripts": {
"prelint": "echo 'Preparing to lint...'",
"lint": "eslint",
"postlint": "echo 'liting complete.'"
}
}
lockfile
当我们npm i时,默认的版本号为^,可以最大限度的向后兼容和新特性之前取舍,但是有些库有可能不遵循该规则,我们在项目时应当使用 yarn.lock/package-lock.json 锁定版本号。
不管是yarn.lock还是package-lock,都是锁住当前版本号范围,如果当前依赖版本不符合lock文件所记录的范围就会覆盖掉lock文件版本。
比如:当前项目webapck的版本为^6.0.0,而lock文件为^5.10.0 => >=5.10.0 <6.0.0很明显不符合。就会重写lock文件中webpack的版本为^6.0.0
main/module/exports
-
main: 这个字段指定了模块的主入口文件。npm package 的入口文件,当我们对某个 package 进行导入时,实际上导入的是
main字段所指向的文件。// package.json 内容 { name: 'midash', main: './dist/index.js' } const midash = require('midash') // 实际上是通过 main 字段来找到入口文件,等同于该引用 const midash = require('midash/dist/index.js') -
module: 这个字段定义了 ES6 模块的入口文件。ES6 模块是一种在现代 JavaScript 中使用的模块系统,它具有更强大的功能,比如静态导入。与
main字段类似,这个字段的值也是一个相对于package.json的路径,指向一个 JavaScript 文件。一些工具和环境,比如现代的前端构建工具,可能会利用这个字段来提供更优化的代码分发。 如果使用import对该库进行导入,则首次寻找module字段引入,否则引入main字段。{ name: 'midash', main: './dist/index.js', module: './dist/index.mjs' } // 以下两者等同 import midash from 'midash' import midash from 'midash/dist/index.mjs' -
exports: 这个字段用于定义模块的导出方式。它允许你指定哪些部分(变量、函数、类等)会被暴露给其他模块。
exports字段的值可以是一个对象,其中键表示导出的名称,而值表示对应的实际导出内容。这样做可以帮助你控制模块的公共接口,以及哪些功能可以被其他模块访问。 不在exports字段中的模块,即使直接访问路径,也无法引用!
bin
bin 用以指定最终的命令行工具的名字,用作该 npm 包可执行文件的入口。
当我们使用 npm 或者 yarn 命令安装包时,如果该包的 package.json 文件有 bin 字段,就会在 node_modules 文件夹下面的 .bin 目录中复制了 bin 字段链接的执行文件。我们在调用执行文件时,可以不带路径,直接使用命令名来执行相对应的执行文件。
npm i -g原理
- 全局包下载:
npm i -g <package-name>时,npm把指定包下载到全局包目录中。 - bin字段创建可执行文件链接:根据该库的
package.json中bin字段的指示,把对应的命令行路径通过符号链接挂载到 PATH 路径 - 对应的二进制脚本添加 x 权限 (可执行文件权限)
小结
这篇文档主要介绍了 前端工程化中package 的几个重要概念。这些知识平时开发时遇到较少,需要注意平时运用时一些细节。