深入webpack之bundle

2021-08-06  本文已影响0人  左冬的博客

现有以下三个文件

import a from './a.js'
import b from './b.js'
console.log(a.getB())
console.log(b.getA())
import b from './b.js'
const a = {
  value: 'a',
  getB: () => b.value + 'from a.js'
}
export default a
import a from './a.js'
const b = {
  value: 'b',
  getB: () => a.value + 'from b.js'
}
export default b

很遗憾,以上三个文件不能运行

因为浏览器不支持直接运行带有import / export关键字的代码

怎么在浏览器运行 import / export

平稳的兼容策略:把关键字转译为普通代码,并把所有文件打包成一个文件

解决第一个问题,怎么把关键字转译为普通代码,也就是怎么把importexport转成函数

@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变成数组,首先对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
}

我们已经得到了最终内容,那最终文件长什么样
其实就是拼凑字符串,思路分下面几步

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的核心内容

上一篇下一篇

猜你喜欢

热点阅读