深入webpack之bundle
现有以下三个文件
- index.js
import a from './a.js'
import b from './b.js'
console.log(a.getB())
console.log(b.getA())
- a.js
import b from './b.js'
const a = {
value: 'a',
getB: () => b.value + 'from a.js'
}
export default a
- b.js
import a from './a.js'
const b = {
value: 'b',
getB: () => a.value + 'from b.js'
}
export default b
很遗憾,以上三个文件不能运行
因为浏览器不支持直接运行带有import
/ export
关键字的代码
怎么在浏览器运行 import / export
- 不同浏览器功能不同
现代浏览器可以通过<script type=“module”>
来支持import
export
IE 8~15不支持import
export
,所以不可能运行 - 兼容策略
激进的兼容策略:把代码全放在<script type="module">
里面
缺点:不被IE 8~15支持;文件之间的引用,建立了过多的HTTP请求,这在普通用户的电脑上是无法容忍的
平稳的兼容策略:把关键字转译为普通代码,并把所有文件打包成一个文件
平稳的兼容策略:把关键字转译为普通代码,并把所有文件打包成一个文件
解决第一个问题,怎么把关键字转译为普通代码,也就是怎么把import
和export
转成函数
@babel/core已经帮我们做了
较之前的collectCodeAndDeps
方法,只做了一处小的改动
const code = readFileSync(filepath).toString()
const { code: es5Code } = babel.transform(code, {
presets: ['@babel/preset-env']
})
depRelation[key] = { deps: [], code: es5Code }
上面代码将原始code
使用babel.transform
变成新的code
并重命名为es5Code
运行代码
{ 'index.js':
{ deps: [ 'a.js', 'b.js' ],
code: '"use strict";\n\nvar _a = _interopRequireDefault(require("./a.js"));\n\nvar _b = _interopRequireDefault(require("./b.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_a["default"].getB());\nconsole.log(_b["default"].getA());' },
'a.js':
{ deps: [ 'b.js' ],
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _b = _interopRequireDefault(require("./b.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar a = {\n value: \'a\',\n getB: function getB() {\n return _b["default"].value + \' from a.js\';\n }\n};\nvar _default = a;\nexports["default"] = _default;' },
'b.js':
{ deps: [ 'a.js' ],
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _a = _interopRequireDefault(require("./a.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar b = {\n value: \'b\',\n getA: function getA() {\n return _a["default"].value + \' from b.js\';\n }\n};\nvar _default = b;\nexports["default"] = _default;' }
}
发现import
关键字变成了require()
,而export
变成了exports["default"]
还有,Object.defineProperty(exports, "__esModule", {\n value: true\n})
这个怎么理解?
其实是,给当前模块添加__esModule: true
属性,用来跟CommonJS模块做区分
那么给对象添加属性,为什么不直接写成exports['__esModule'] = true
/ exports.__esModule = true
?
exports["default"] = void 0;
这句话是用来强制清空exports["default"]
的值
import b from './b.js
变成了var _b = _interopRequireDefault(require("./b.js"));
b.value
变成了b['default'].value
_interopRequireDefault(module)
下划线前缀是为了与其他变量重名,而这个函数的意义是为了给模块加default
为什么要加default
?CommonJS模块没有默认导出,加上也是为了方便兼容,代码:
function _interopRequireDefault(obj) {
// 是否是esModule
// 如果是的话返回本身
// 如果不是的话就给对象添加default属性
return obj && obj.__esModule ? obj : { "default": obj };
}
其他_interop
开头的函数,大多都是为了兼容旧代码
到这里就很明朗啦,我们使用@babel/core
,将import
关键字变成require
函数,把export
关键字变成exports
对象,其本质就是将 ESModule 语法变成了 CommonJS 规则
第二个问题,怎么把多个文件打包成一个
设想一下,打包成一个什么样的文件?
包含所有模块,且能执行所有模块
回顾一下《深入webpack之JS文件的依赖关系》,我们已经知道了怎么收集整个项目的依赖(此时还是个对象),那么如果通过一个依赖的数组var depRelation = []
,将入口文件放在第一位execute(depRelation[0])
去执行每个依赖的code,看似可行
- depRelation需要变成一个数组
- code是字符串,需要变成一个函数
- excute函数怎么写
将depRelation变成数组,首先对collectCodeAndDeps
方法进行改动
- if (Object.keys(depRelation).includes(key)) {
+ if (depRelation.find(i => i.key === key)) {
console.warn(`duplicated dependency: ${key}`)
return
}
- depRelation[key] = { deps: [], code: es5Code }
+ const item = { key, deps: [], code: es5Code }
+ depRelation.push(item)
...
- depRelation[key].deps.push(depProjectPath)
+ item.deps.push(depProjectPath)
再来把code
字符串变成函数
// 原code
code = `
var b = require(''./b.js)
export.default = 'a'
`
code2 = `function(require, module, exports) {${code}}`
注意此时的code2
还是字符串,但是当我们将code: ${code2}
写入最终文件,最终文件里面的code
就是函数了
require, module, export这三个参数是CommonJS规定的
最后再完善一下execute
函数
const modules = {} // modules 用于缓存所有模块
function execute(key) { // index.js a.js b.js
if (modules[key]) { rturn modules[key] }
var item = depRelation.find(i => i.key === key)
var require = (path) => {
return execute(pathToKey(path)) // 导入的依赖,继续去执行
modules[key] = { __esModule: true } // modules['a.js'],放到缓存里
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules.exports
}
我们已经得到了最终内容,那最终文件长什么样
其实就是拼凑字符串,思路分下面几步
var dist = "";
dist += content;
writeFileSync('dist.js', dist)
dist += content;
这一步的代码如下
// 打包文件 bundle.js
...
function generateCode() {
let code = ''
code += 'var depRelation = [' + depRelation.map(item => {
// 遍历依赖,拼凑对象
const { key, deps, code } = item
return `{
key: ${JSON.stringify(key)},
deps: ${JSON.stringify(deps)},
code: function(require, module, exports){
// code外卖包裹function
${code}
}
}`
}).join(',') + '];\n'
// 用于缓存所有模块
code += 'var modules = {};\n'
// 执行入口文件
code += `execute(depRelation[0].key)\n`
code += `
function execute(key) {
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
}
var require = (path) => {
return execute(pathToKey(path))
}
modules[key] = { __esModule: true }
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules[key]
}`
return code
}
最后的最后,再将最终code写入文件即可
writeFileSync('dist.js', generateCode())
可以正常运行
node dist.js
上面代码bundle
就是一个简易打包器,也就是webpack的核心内容