技术体系解析

更新时间: 2023-03-12 20:45:49

开源库需要支持不同技术体系,以及在不同技术体系下库开发技术的变迁。

在开始之前先来看一个场景:深拷贝库中有一个type函数,用来获取数据的类型,现在假设还有一个库也要用到这个函数,所以我们决定将其单独抽象为一个库,现在就有了两个库,其中clone库会依赖type库。

一般JavaScript库都会依赖另外一些库,真实的JavaScript库的依赖关系会更复杂。

# 传统体系

在传统体系中,一般通过在HTML文件中使用script标签来引入JavaScript文件,这种体系下的每个库都需要提供一个.js格式的文件。下面是传统体系下的项目目录结构示例:

├── index.html
└── lib
    ├── clone.js
    └── type.js
1
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>
1
2
3
4
5
6

随着库规模的扩大,将依赖关系交给库的使用者手动维护对库的使用者非常不友好,因为要提供包含全部代码的入口文件,所以在这种体系下,大部分库都不会依赖很多其他的库。

兼容传统体系的库,需要将所有代码及其依赖库的代码合并成一个文件。但也存在例外情况,例如jQuery插件必须依赖jQuery才能运行,React插件必须依赖React才能运行。

# Node.js体系

Node.js的模块系统遵守前面提到的CommonJS规范,Node.js有内置的依赖解析系统,如果要依赖一个模块,则可以像下面代码这样使用require系统函数直接引用文件:

const clone = require('./clone.js')
1

在使用reqiure函数引用文件时,被引用文件的路径需遵循一套复杂的规则,引用支持相对路径绝对路径第三方包,如果忽略后缀,则会被当做Node.js的模块去解析。

Node.js模块目录下需要有一个package.json文件,用于定义模块的一些属性。如果想要新建模块,则可以使用Node.js提供的npm工具快速初始化。通过下面的命令可以在lib目录下新建并初始化clone模块

mkdir clone
cd clone
npm init
1
2
3

npm会提示填写模块的信息,这里不做修改,一直保持默认设置即可,执行后会生成一个package.json文件,该文件包含的字段如下:

{
    "name":"clone",
    "verson":"1.0.0",
    "description":"",
    "main":"index.js"
}
1
2
3
4
5
6

这里主要关注main字段,其定义的是当前模块对应的逻辑入口文件,当该模块被其他模块引用时,Node.js会找到main字段对应的文件。

通过同样的操作完成对type模块的初始化。此时项目的目录结构如下:

├── index.js
└── lib
    ├── clone
    │    ├──index.js
    │    └──package.json
    └── type
         ├──index.js
         └──package.json
1
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)
1
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
1
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')
    }
}
1
2
3
4
5
6
7
8
9

然后执行下的命令,完成打包工作。

npx webpack
1

接下来,添加一个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
1
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.由于历史原因,这个字段有两个命名,分别是modulejsnext,目前比较主流的是module字段,也可以两个都设置,只需要在库的package.json文件中增加字段名modulejsnext,并设置为ES Module文件的路径即可:

{
    "main":"index.js",
    "module":"index.esm.js",
    "jsnext":"index.esm.js"
}
1
2
3
4
5

webpack中,可以通过配置mainFields来支持优先使用module字段,只需要在webpack.config.js文件中添加如下的配置代码即可:

module.exports = {
    //...
    resolve:{
        mainFields:['module','main']
    }
}
1
2
3
4
5
6

index.js文件提供对CommonJS模块的支持:

function clone(source) {
    //...
}
module.exports = clone
1
2
3
4

index.esm.js文件提供对ES Module的支持,可以看到,支持ES Module的写法更加简洁。

export function clone(source) {
    //...
}
1
2
3

对于库的使用者来说,不用关心ES Module规范和CommonJS规范之间的区别,只需要像下面代码这样引用即可:

const clone = require('clone')
1

打包工具会优先查看依赖的库是否支持ES Module,如果不支持,则会遵循CommonJS规范

综上所述,在这种体系下,开源库需要同时提供对ES Module和CommonJS模块的支持,对其依赖的库不需要进行特殊处理。

技术体系 模块规范 依赖库的处理逻辑
传统体系 原始模块 依赖打包
Node.js体系 CommonJS 无须处理
工具化体系 ES Module + CommonJS 无须处理