美文网首页
深入webpack之bundle

深入webpack之bundle

作者: 左冬的博客 | 来源:发表于2021-08-06 16:33 被阅读0次

    现有以下三个文件

    • 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请求,这在普通用户的电脑上是无法容忍的
      平稳的兼容策略:把关键字转译为普通代码,并把所有文件打包成一个文件

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

    解决第一个问题,怎么把关键字转译为普通代码,也就是怎么把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需要变成一个数组
    • 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的核心内容

    相关文章

      网友评论

          本文标题:深入webpack之bundle

          本文链接:https://www.haomeiwen.com/subject/kyjmvltx.html