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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • webpack 要解决的两个问题 现有代码(接上文) 很遗憾,这三个文件不能运行因为浏览器不支持直接运行带有 im...
    littleyu阅读 478评论 0 0
  • 1. 怎样才能运行 import / export 不同浏览器功能不同现代浏览器可以通过 来支持 import...
    sweetBoy_9126阅读 648评论 0 1
  • 背景 说到构建工具,我往往会在前面加「自动化」三个字,因为构建工具就是用来让我们不再做机械重复的事情,解放我们的双...
    天明丶胡金斌阅读 412评论 0 0
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,674评论 7 110
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,033评论 0 4