这篇主要就讲一下【打包】(bundle是打包,bundler是打包器)
现有问题
上面三个文件的代码都不能直接运行在浏览器中,因为浏览器不支持直接运行带有import和export关键字的代码,所以就引出了下面的问题
问题一:很多浏览器不认识import和export,只有现代浏览器(chrome、firefox、edge等),通过来支持import和export;
问题二:虽然通过可以支持import和export,但是不兼容IE8~15,而且可能会导致文件请求过多;
平稳的兼容策略:把关键字转译成普通代码;且把所有文件打包成一个文件;
下面就来讲一下如何来实现上面这个策略
编译import和export关键字
项目中新增了bundler_1.ts,可以拿它和deps_4.ts做一下比较,看哪里做了改动;
主要添加了下面几行代码,通过babel把code转译一下:
const { code: es5Code } = babel.transform(code, {
presets: ['@babel/preset-env']
})
运行一下,取其中的a.js的结果看一下:
可以看出区别:
①import关键字没有了,变成了require;
②export关键字也没有了,变成了exports['default']
注意:这时这里的code是字符串
把所有代码打包成一个文件
那么这个文件应该是什么样的呢,首先它应该包含所有的模块,其次它还要可以执行所有的模块;
这时就有三个问题我们要来解决一下:
1、depRelation目前是对象,我们要把它变成数组(为什么变成数组呢,因为数组的第一项就是入口呀,而对象没有第一项这么一个概念);
2、code目前是字符串,我们要把它变成函数;
3、完善execude函数(execude函数就是用来执行入口文件的)
把depRelation从对象变成数组
引入bundler_2.ts,把它与bundler_1.ts做一下对比;node -r ts-node/register ./bundler_2.ts
运行bundler_2.ts;
把code从字符串变成函数
步骤
1、把code字符串外面包一个function(require, modules, exports){....};
2、再把这个code写到文件里,注意不要让引号出现在文件中;
完善execude函数
function execute(key) {
// 如果已经 require 过,就直接返回上次的结果
if (modules[key]) { return modules[key] }
// 找到要执行的项目
var item = depRelation.find(i => i.key === key)
// 找不到就报错,中断执行
if (!item) { throw new Error(`${item} is not found`) }
// 把相对路径变成项目路径
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(/\.\//g, '').replace(/\/\//, '/')
return projectPath
}
// 创建 require 函数
var require = (path) => {
return execute(pathToKey(path))
}
// 初始化当前模块
modules[key] = { __esModule: true }
// 初始化 module 方便 code 往 module.exports 上添加属性
var module = { exports: modules[key] }
// 调用 code 函数,往 module.exports 上添加导出属性
// 第二个参数 module 大部分时候是无用的,主要用于兼容旧代码
item.code(require, module, module.exports)
// 返回当前模块
return modules[key]
}
手动构造dist.js文件
最后dist.js文件的主体结构应该如下
var depRelation = [
{key: 'index.js', deps: ['a.js', 'b.js'], code: function...},
{key: 'a.js', deps: ['b.js'], code: function...},
{key: 'b.js', deps: ['a.js'], code: function...},
]
var modules = {}
execute(depRelation[0].key)
function execute(key){
var require = ...
var module = ...
item.code(require, module, module.exports )
....
}
运行dist.js,可以得到和index.js一样的结果
如何得到最终文件dist.js
我们已经知道了dist.js内容是什么了,但是该怎么去得到它呢,也就是说怎么自动生成dist.js文件呢?
答:拼凑出字符串,再把这些字符串写入到文件中即可,如下:
var code = ''
code += content
writeFileSync('dist.js', code)
编写bundler_3.ts,把它与bundler_2.ts做一下对比看哪里做了改动;bundler_3.ts就是打包器,可以自动生成最终文件,运行bundler_3.ts,得到dist_2.js(只是把名字变了下),node ./dist_2.js
,得到结果与index.js一样;
并把我们通过打包器自动生成的dist_2.js与我们手动写出来的dist.js作对比,发现内容是一样的。
所以bundler_3.ts里的内容就是一个简易打包器,也就是webpack的核心内容!!
具体步骤
1、需要得到一个depRelation来收集依赖,它是一个数组,具体内容如下:
depRelation = [
{key: 'index.js', deps: ['a.js', 'b.js'], code: `index.js代码内容的字符串`},
{key: 'a.js', deps: ['b.js'], code: `a.js代码内容的字符串`},
{key: 'b.js', deps: ['a.js'], code: `b.js代码内容的字符串`},
]
2、得到execude函数,函数具体内容可见上面,它接受一个参数,参数为depRelation[0].key,相当于depRelation[0].key(其实就是index.js)就是一个入口文件;
3、编写generateCode函数,它是打包器的核心,主要通过字符串拼接把我们手写出的dist.js里的内容,通过writeFileSync('dist2.js', generateCode())
输出到指定文件里面去;这个过程中需要把depRelation中每一项中的code由字符串变成函数;
4、最后生成的dist2.js就是打包出来的文件啦;
不过目前这个简易打包器还有不少问题:
1、生成的代码中有不少重复的函数;
2、目前只能引入和运行JS文件;
3、只能理解import,无法理解require;
4、不支持插件;
5、不支持配置入口文件和dist的文件名;
但本篇文章主旨在于理解打包器是怎么打包的,所以这些问题后面一一再来梳理;