【前端工程化】万字拆解package.json

197 阅读5分钟

什么是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 版本更新的例子,涉及了不兼容的重大变化,比如:

    1. Composition API 替代 Options API: 从 Vue 2 的 Options API 到 Vue 3 的 Composition API,组件逻辑写法有较大改变。
    2. 基于 Proxy 的响应式系统: Vue 3 使用 Proxy 取代了 Vue 2 的 Object.defineProperty,影响了响应式数据的更新和追踪。
    3. 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 锁定版本号。

依赖

依赖分为dependencydevdependency两种

它们两者之间的区别就是一个是项目运行时所需要的依赖,一个是开发时的依赖是不会打包进最终产物的。

除此之外,它们在包开发和项目开发时也有差别。

  • 在开发包时,devdependencydependency需要有严格的区分,因为在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
}
  1. false: 表示模块没有副作用,可以被安全地删除。这通常用于纯粹的导出模块,例如只包含函数、类、对象等,没有执行任何其他操作的模块。
  2. true: 表示模块具有副作用,不会被删除。这是默认值,如果你不在 package.json 中显式设置 sideEffects,那么模块会被认为具有副作用。
  3. 字符串数组:可以列出模块中具有副作用的文件的路径。这些路径会被用于指定哪些模块具有副作用,从而防止它们被删除。

使用场景举例:

{
  "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 installnpm 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

  1. 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')
    
  2. 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'
    
  3. 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.jsonbin 字段的指示,把对应的命令行路径通过符号链接挂载到 PATH 路径
  • 对应的二进制脚本添加 x 权限 (可执行文件权限)

小结

这篇文档主要介绍了 前端工程化中package 的几个重要概念。这些知识平时开发时遇到较少,需要注意平时运用时一些细节。