技术体系解析
开源库需要支持不同技术体系,以及在不同技术体系下库开发技术的变迁。
在开始之前先来看一个场景:深拷贝库中有一个type函数,用来获取数据的类型,现在假设还有一个库也要用到这个函数,所以我们决定将其单独抽象为一个库,现在就有了两个库,其中clone库会依赖type库。
一般JavaScript库都会依赖另外一些库,真实的JavaScript库的依赖关系会更复杂。
# 传统体系
在传统体系中,一般通过在HTML
文件中使用script
标签来引入JavaScript文件
,这种体系下的每个库都需要提供一个.js
格式的文件。下面是传统体系下的项目目录结构示例:
├── index.html
└── lib
├── clone.js
└── type.js
2
3
4
在传统体系下,如果想使用一个库,就必须在使用之前手动引入要用到的库及其依赖的库。例如,如果想要使用clone库,就必须在引入clone库之前引入type库,否则就会报错。
<script src="lib/type.js"></script>
<script src="lib/clone.js"></script>
<script>
let a = {c:1}
let b = clone(a)
</script>
2
3
4
5
6
随着库规模的扩大,将依赖关系交给库的使用者手动维护对库的使用者非常不友好,因为要提供包含全部代码的入口文件,所以在这种体系下,大部分库都不会依赖很多其他的库。
兼容传统体系的库,需要将所有代码及其依赖库的代码合并成一个文件。但也存在例外情况,例如jQuery插件必须依赖jQuery才能运行,React插件必须依赖React才能运行。
# Node.js体系
Node.js
的模块系统遵守前面提到的CommonJS
规范,Node.js
有内置的依赖解析系统,如果要依赖一个模块,则可以像下面代码这样使用require
系统函数直接引用文件:
const clone = require('./clone.js')
在使用reqiure
函数引用文件时,被引用文件的路径需遵循一套复杂的规则,引用支持相对路径、绝对路径和第三方包,如果忽略后缀,则会被当做Node.js
的模块去解析。
Node.js
模块目录下需要有一个package.json
文件,用于定义模块的一些属性。如果想要新建模块,则可以使用Node.js
提供的npm
工具快速初始化。通过下面的命令可以在lib
目录下新建并初始化clone
模块
mkdir clone
cd clone
npm init
2
3
npm
会提示填写模块的信息,这里不做修改,一直保持默认设置即可,执行后会生成一个package.json
文件,该文件包含的字段如下:
{
"name":"clone",
"verson":"1.0.0",
"description":"",
"main":"index.js"
}
2
3
4
5
6
这里主要关注main
字段,其定义的是当前模块对应的逻辑入口文件,当该模块被其他模块引用时,Node.js
会找到main
字段对应的文件。
通过同样的操作完成对type模块的初始化。此时项目的目录结构如下:
├── index.js
└── lib
├── clone
│ ├──index.js
│ └──package.json
└── type
├──index.js
└──package.json
2
3
4
5
6
7
8
通过一下代码可以在index.js文件中直接引入clone模块,Node.js会自动完成模块解析,并加载好依赖项。
const clone = require('./lib/clone')
let a = {c:1}
let b = clone(a)
2
3
4
在Node.js
体系下,库只需要提供对CommonJS
模块或UMD
模块的支持即可,对依赖的库不需要进行特殊处理。
# 工具化体系
随着前端工程化的发展,前端构建工具目前已经成为中大型项目的标配。构建工具的典型代表是webpack
,webpack
支持CommonJS
规范。
如果想要使用webpack,则需要先安装webpack,命令如下:
npm init -y # 在当前目录下初始化package.json文件
npm install webpack webpack-cli --save-dev #安装webpack
2
在项目的根目录下添加webpack.config.js
文件,并在该文件中添加如下配置代码,其含义是将当前目录下的index.js
文件打包输出为dist/index.js
文件。
const path = require('path')
module.exportx = {
entry: './index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist')
}
}
2
3
4
5
6
7
8
9
然后执行下的命令,完成打包工作。
npx webpack
接下来,添加一个index.html
文件,引用打包输出的dist/index.js
文件即可
至此,项目的完整目录结构如下:
├── dist
│ └──index.js
├── index.html
├── index.js
├── lib
│ ├── clone
│ │ ├──index.js
│ │ └──package.json
│ └── type
│ ├──index.js
│ └──package.json
├──package.json
└──webpack.config.js
2
3
4
5
6
7
8
9
10
11
12
13
最开始,构建工具仅支持CommonJS
规范,随着ECMAScript 2015
的发布,rollup.js
最先支持ES Module
,现在主流的构建工具均已支持ES Module
。
打包工具在加载一个库时,需要知道这个库是支持CommonJS
模块的还是支持ES Module
的,构建工具给的方案是扩展一个新的入口字段,开源库可以通过设置这个字段来标识自己是否支持ES Module.由于历史原因,这个字段有两个命名,分别是module
和jsnext
,目前比较主流的是module字段,也可以两个都设置,只需要在库的package.json
文件中增加字段名module
和jsnext
,并设置为ES Module
文件的路径即可:
{
"main":"index.js",
"module":"index.esm.js",
"jsnext":"index.esm.js"
}
2
3
4
5
在webpack
中,可以通过配置mainFields
来支持优先使用module
字段,只需要在webpack.config.js
文件中添加如下的配置代码即可:
module.exports = {
//...
resolve:{
mainFields:['module','main']
}
}
2
3
4
5
6
index.js
文件提供对CommonJS
模块的支持:
function clone(source) {
//...
}
module.exports = clone
2
3
4
index.esm.js
文件提供对ES Module
的支持,可以看到,支持ES Module
的写法更加简洁。
export function clone(source) {
//...
}
2
3
对于库的使用者来说,不用关心ES Module规范和CommonJS规范之间的区别,只需要像下面代码这样引用即可:
const clone = require('clone')
打包工具会优先查看依赖的库是否支持ES Module,如果不支持,则会遵循CommonJS规范
综上所述,在这种体系下,开源库需要同时提供对ES Module和CommonJS模块的支持,对其依赖的库不需要进行特殊处理。
技术体系 | 模块规范 | 依赖库的处理逻辑 |
---|---|---|
传统体系 | 原始模块 | 依赖打包 |
Node.js体系 | CommonJS | 无须处理 |
工具化体系 | ES Module + CommonJS | 无须处理 |