webpack打包是如何运行的

  • 也可以称为,webpack是如何实现模块化的
  • CommonJS是同步加载模块,一般用于node。因为node应用程序运行在服务器上,程序通过文件系统可以直接读取到各个模块的文件,特点是响应快速,不会因为同步而阻塞了程序的运行;
  • AMD是异步加载模块,所以普遍用于前端。而前端项目运行在浏览器中,每个模块都要通过http请求加载js模块文件,受到网络等因素的影响如果同步的话就会使浏览器出现“假死”(卡死)的情况,影响到了用户体验。
  • ESModule 旨在实现前后端模块化的统一。而webpack就是把ES6的模块化代码转码成CommonJS的形式,从而兼容浏览器的。
  • 为什么webpack打包后的文件,可以用在浏览器:此时webpack会将所有的js模块打包到bundle.js中(异步加载的模块除外,异步模块后面会讲),读取到了内存里,就不会再分模块加载了。

webpack对CommonJS的模块化处理

  • 举例:

    • index.js文件,引入foo.js文件
    const foo = require('./foo');console.log(foo);
    console.log('我是高级前端工程师~');
    
    • foo.js文件
    module.exports = {name: 'quanquan',job: 'fe',
    };
    
  • 当我们执行webpack之后,打包完成,可以看到bundle.js内的代码
// modules 即为存放所有模块的数组,数组中的每一个元素都是一个函数
(function(modules) {// 安装过的模块都存放在这里面// 作用是把已经加载过的模块缓存在内存中,提升性能var installedModules = {};// 去数组中加载一个模块,moduleId 为要加载模块在数组中的 index// __webpack_require__作用和 Node.js 中 require 语句相似function __webpack_require__(moduleId) {// require 模块时先判断是否已经缓存, 已经缓存的模块直接返回if(installedModules[moduleId]) {return installedModules[moduleId].exports;}// 如果缓存中不存在需要加载的模块,就新建一个模块,并把它存在缓存中var module = installedModules[moduleId] = {// 模块在数组中的indexi: moduleId,// 该模块是否已加载完毕l: false,// 该模块的导出值,也叫模块主体内容, 会被重写exports: {}};// 从 modules 中获取 index 为 moduleId 的模块对应的函数// 再调用这个函数,同时把函数需要的参数传入,this指向模块的主体内容modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// 将模块标记为已加载module.l = true;// 返回模块的导出值,即模块主体内容return module.exports;}// 向外暴露所有的模块__webpack_require__.m = modules;// 向外暴露已缓存的模块__webpack_require__.c = installedModules;......// Webpack 配置中的 publicPath,用于加载被分割出去的异步代码,这个暂时还没有用到__webpack_require__.p = "";// Load entry module and return exports// 准备工作做完了, require 一下入口模块, 让项目跑起来// 使用 __webpack_require__ 去加载 index 为 0 的模块,并且返回该模块导出的内容// index 为 0 的模块就是 index.js文件,也就是执行入口模块// __webpack_require__.s 的含义是启动模块对应的 indexreturn __webpack_require__(__webpack_require__.s = 0);
})
/***** 华丽的分割线 上边时 webpack 初始化代码, 下边是我们写的模块代码 *******/
// 所有的模块都存放在了一个数组里,根据每个模块在数组的 index 来区分和定位模块
([/* 模块 0 对应 index.js */(function(module, exports, __webpack_require__) {// 通过 __webpack_require__ 规范导入 foo 函数,foo.js 对应的模块 index 为 1const foo = __webpack_require__(1);console.log(foo);console.log('我是高级前端工程师~');}),/* 模块 1 对应 foo.js */(function(module, exports) {// 通过 CommonJS 规范导出对象module.exports = {name: 'quanquan',job: 'fe',};})
]);
  • 上面是一个立即执行函数,简单点写:
(function(modules) {// 模拟 require 语句function __webpack_require__(index) {return [/*存放所有模块的数组中,第index个模块暴露的东西*/]}// 执行存放所有模块数组中的第0个模块,并且返回该模块导出的内容return __webpack_require__(0);})([/*存放所有模块的数组*/])
  • bundle.js 能直接运行在浏览器中的原因在于:

    • webpack通过 _webpack_require_ 函数(该函数定义了一个可以在浏览器中执行的加载函数)模拟了模块的加载(类似于Node.js 中的 require 语句),把定义的模块内容挂载到module.exports上;
    • 同时__webpack_require__函数中也对模块缓存做了优化,执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。
  • 原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于,浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

webpack对es6 Module模块化的处理

  • 举例

    • index.js文件,引入foo.js文件
    const foo = require('./foo');❎
    import foo from './foo';✅console.log(foo);
    console.log('我是高级前端工程师~');
    
    • foo.js文件
    module.exports = {❎
    export default {✅name: 'quanquan',job: 'fe',
    };
    
  • 打包完后bundle.js代码如下
(function(modules) {var installedModules = {};function __webpack_require__(moduleId) {if(installedModules[moduleId]) {return installedModules[moduleId].exports;}var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);module.l = true;return module.exports;}__webpack_require__.m = modules;__webpack_require__.c = installedModules;__webpack_require__.d = function(exports, name, getter) {if(!__webpack_require__.o(exports, name)) {Object.defineProperty(exports, name, {configurable: false,enumerable: true,get: getter});}};__webpack_require__.n = function(module) {var getter = module && module.__esModule ?function getDefault() { return module['default']; } :function getModuleExports() { return module; };__webpack_require__.d(getter, 'a', getter);return getter;};__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };__webpack_require__.p = "";return __webpack_require__(__webpack_require__.s = 0);
})([相关模块]);
  • 打包好的内容和commonjs模块化方法差不多
function(module, __webpack_exports__, __webpack_require__) {"use strict";// 在__webpack_exports__上定义__esModule为true,表明是一个模块对象Object.defineProperty(__webpack_exports__, "__esModule", { value: true });var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);console.log(__WEBPACK_IMPORTED_MODULE_0__foo__["a"]);console.log('我是高级前端工程师~');
},
function(module, __webpack_exports__, __webpack_require__) {"use strict";__webpack_exports__["a"] = ({name: 'quanquan',job: 'fe',});
}
  • 和 commonjs 不同的地方

    • 首先, 包装函数的参数之前的 module.exports 变成了_webpack_exports_
    • 其次, 在使用了 es6 模块导入语法(import)的地方, 给__webpack_exports__添加了属性__esModule
    • 其余的部分和 commonjs 类似

webpack文件的按需加载

  • 以上webpack把所有模块打包到主文件中,所以模块加载方式都是同步方式。但在开发应用过程中,按需加载(也叫懒加载)也是经常使用的优化技巧之一。
  • 按需加载,通俗讲就是代码执行到异步模块(模块内容在另外一个js文件中),通过网络请求即时加载对应的异步模块代码,再继续接下去的流程。
  • 在给单页应用做按需加载优化时,一般采用以下原则:
    • 把整个网站划分成一个个小功能,再按照每个功能的相关程度把它们分成几类。
    • 把每一类合并为一个 Chunk,按需加载对应的 Chunk。
    • 对于用户首次打开你的网站时需要看到的画面所对应的功能,不要对它们做按需加载,而是放到执行入口所在的 Chunk 中,以降低用户能感知的网页加载时间。
    • 对于个别依赖大量代码的功能点,例如依赖 Chart.js 去画图表、依赖 flv.js 去播放视频的功能点,可再对其进行按需加载。
  • 被分割出去的代码的加载需要一定的时机去触发,也就是当用户操作到了或者即将操作到对应的功能时再去加载对应的代码。 被分割出去的代码的加载时机需要开发者自己去根据网页的需求去衡量和确定。
  • 由于被分割出去进行按需加载的代码在加载的过程中也需要耗时,你可以预言用户接下来可能会进行的操作,并提前加载好对应的代码,从而让用户感知不到网络加载时间。
  • 举个例子
    • 网页首次加载时只加载 main.js 文件,网页会展示一个按钮,main.js 文件中只包含监听按钮事件和加载按需加载的代码。当按钮被点击时才去加载被分割出去的 show.js 文件,加载成功后再执行 show.js 里的函数。
    • main.js 文件
    window.document.getElementById('btn').addEventListener('click', function () {// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数import(/* webpackChunkName: "show" */ './show').then((show) => {show('Webpack');})
    });
    
    • show.js 文件
    module.exports = function (content) {window.alert('Hello ' + content);
    };
    
    • 代码中最关键的一句是 import(/* webpackChunkName: “show” / ‘./show’),Webpack 内置了对 import() 语句的支持,当 Webpack 遇到了类似的语句时会这样处理:

      • 以 ./show.js 为入口新生成一个 Chunk;
      • 当代码执行到 import 所在语句时才会去加载由 Chunk 对应生成的文件。
      • import 返回一个 Promise,当文件加载成功时可以在 Promise 的 then 方法中获取到 show.js 导出的内容。
  • webpack有个require.ensure api语法来标记为异步加载模块,最新的webpack4推荐使用新的import() api(需要配合@babel/plugin-syntax-dynamic-import插件)。

  • 因为require.ensure是通过回调函数执行接下来的流程,而import()返回promise,这意味着可以使用最新的ES8 async/await语法,使得可以像书写同步代码一样,执行异步流程。

按需加载输出代码分析

  • 举例

    • main.js
    // main.js
    import Add from './add'
    console.log(Add, Add(1, 2), 123)// 按需加载
    // 方式1: require.ensure
    // require.ensure([], function(require){
    //     var asyncModule = require('./async')
    //     console.log(asyncModule.default, 234)
    // })// 方式2: webpack4新的import语法
    // 需要加@babel/plugin-syntax-dynamic-import插件
    let asyncModuleWarp = async () => await import('./async')
    console.log(asyncModuleWarp().default, 234)
    
    • async.js
    // async.js
    export default function() {return 'hello, aysnc module'
    }
    
  • 打包后会生成两个chunk文件,分别是主文件执行入口文件 bundle.js 和 异步加载文件 0.bundle.js。
// 0.bundle.js
// 异步模块
// window["webpackJsonp"]是连接多个chunk文件的桥梁
// window["webpackJsonp"].push = 主chunk文件.webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], // 异步模块标识chunkId,可判断异步代码是否加载成功// 跟同步模块一样,存放了{模块路径:模块内容}{"./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);__webpack_exports__["default"] = (function () {return 'hello, aysnc module';});})}
]);
  • 异步模块打包后的文件中保存着异步模块源代码,同时为了区分不同的异步模块,还保存着该异步模块对应的标识:chunkId。以上代码主动调用window[“webpackJsonp”].push函数,该函数是连接异步模块与主模块的关键函数,该函数定义在主文件中,实际上window[“webpackJsonp”].push = webpackJsonpCallback,详细源码咱们看看主文件打包后的代码bundle.js:
(function(modules) {// 获取到异步chunk代码后的回调函数// 连接两个模块文件的关键函数function webpackJsonpCallback(data) {var chunkIds = data[0]; //data[0]存放了异步模块对应的chunkIdvar moreModules = data[1]; // data[1]存放了异步模块代码// 标记异步模块已加载成功var moduleId, chunkId, i = 0, resolves = [];for(;i < chunkIds.length; i++) {chunkId = chunkIds[i];if(installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);}installedChunks[chunkId] = 0;}// 把异步模块代码都存放到modules中// 此时万事俱备,异步代码都已经同步加载到主模块中for(moduleId in moreModules) {modules[moduleId] = moreModules[moduleId];}// 重点:执行resolve() = installedChunks[chunkId][0]()返回promisewhile(resolves.length) {resolves.shift()();}};// 记录哪些chunk已加载完成var installedChunks = {"main": 0};// __webpack_require__依然是同步读取模块代码作用function __webpack_require__(moduleId) {...}// 加载异步模块__webpack_require__.e = function requireEnsure(chunkId) {// 创建promise// 把resolve保存到installedChunks[chunkId]中,等待代码加载好再执行resolve()以返回promisevar promise = new Promise(function(resolve, reject) {installedChunks[chunkId] = [resolve, reject];});// 通过往head头部插入script标签异步加载到chunk代码var script = document.createElement('script');script.charset = 'utf-8';script.timeout = 120;script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js"var onScriptComplete = function (event) {var chunk = installedChunks[chunkId];};script.onerror = script.onload = onScriptComplete;document.head.appendChild(script);return promise;};var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];// 关键代码: window["webpackJsonp"].push = webpackJsonpCallbackjsonpArray.push = webpackJsonpCallback;// 入口执行return __webpack_require__(__webpack_require__.s = "./src/main.js");})({"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {...}),"./src/main.js": (function(module, exports, __webpack_require__) {// 同步方式var Add = __webpack_require__("./src/add.js").default;console.log(Add, Add(1, 2), 123);// 异步方式var asyncModuleWarp =function () {var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {return regeneratorRuntime.wrap(function _callee$(_context) {// 执行到异步代码时,会去执行__webpack_require__.e方法// __webpack_require__.e其返回promise,表示异步代码都已经加载到主模块了// 接下来像同步一样,直接加载模块return __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/async.js"))}, _callee);}));return function asyncModuleWarp() {return _ref.apply(this, arguments);};}();console.log(asyncModuleWarp().default, 234)})
});
  • webpack实现模块的异步加载有点像jsonp的流程。

    • 在主js文件中通过在head中构建script标签方式,异步加载模块信息;
    • 再使用回调函数webpackJsonpCallback,把异步的模块源码同步到主文件中,所以后续操作异步模块可以像同步模块一样。
  • 源码具体实现流程:

    • 遇到异步模块时,使用_webpack_require_.e函数去把异步代码加载进来。该函数会在html的head中动态增加script标签,src指向指定的异步模块存放的文件。
    • 加载的异步模块文件会执行webpackJsonpCallback函数,把异步模块加载到主文件中。
    • 所以后续可以像同步模块一样,直接使用_webpack_require_("./src/async.js")加载异步模块。
  • 这里的 bundle.js 和上面所讲的 bundle.js 非常相似,区别在于:

    • 多了一个 webpack_require.e 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件;
    • 多了一个 webpackJsonp 函数用于从异步加载的文件中安装模块。
    • 在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是一样的,都会有 webpack_require.e 和 webpackJsonp。 原因在于提取公共代码和异步加载本质上都是代码分割。

总结

本面试题为前端常考面试题,后续有机会继续完善。我是歌谣,一个沉迷于故事的讲述者。

欢迎一起私信交流。

“睡服“面试官系列之各系列目录汇总(建议学习收藏)

“约见”面试官系列之常见面试题第四十四篇之webpack打包原理解析?(建议收藏)相关推荐

  1. “约见”面试官系列之常见面试题第三十五篇之轮循机制(建议收藏)

    目录 前言 任务队列 事件的概念 回调函数 事件轮询机制Event Loop: 结语 前言 有人称Event Loop为事件循环机制,而我更愿意将其解释为事件轮询机制,在之后的内容中你会感受到这一点的 ...

  2. “约见”面试官系列之常见面试题第三十二篇之async和await(建议收藏)

    一.async和await async和await的概念 1)async 函数是 Generator 函数的语法糖,使用 关键字 async 来表示,在函数内部使用 await 来表示异步 2)ES7 ...

  3. “约见”面试官系列之常见面试题之第九十九篇之router和route(建议收藏)

    1.router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了 ...

  4. “约见”面试官系列之常见面试题之第八十三篇之node.js理解(建议收藏)

    1.模块的引用示例 var math = require('math'): 在common.js规范中,存在require()方法,这个方法接受模块标识,此引引入一个模块的api 到当前的上下文中. ...

  5. “约见”面试官系列之常见面试题之第九十五篇之vue-router的组件组成(建议收藏)

    <router-link :to='' class='active-class'> //路由声明式跳转 ,active-class是标签被点击时的样式<router-view> ...

  6. “约见”面试官系列之常见面试题之第九十四篇之MVVM框架(建议收藏)

    目录 一句话总结:vm层(视图模型层)通过接口从后台m层(model层)请求数据,vm层继而和v(view层)实现数据的双向绑定. 1.我大前端应该不应该做复杂的数据处理的工作? 2.mvc和mvvm ...

  7. “约见”面试官系列之常见面试题之第九十二篇之created和mounted区别(建议收藏)

    beforeCreate 创建之前:已经完成了 初始化事件和生命周期 created 创建完成:已经完成了 初始化注册和响应 beforeMount 挂载之前:已经完成了模板渲染 mounted :挂 ...

  8. “约见”面试官系列之常见面试题之第八十一篇之webpack(建议收藏)

    从我进公司那天起,公司就一直在用webpack,这是一个前端自动打包工具,但我以前从来没接触过,不过幸好我聪明机智,天赋异禀,倒是能上手用,只不过有些配置还是看不懂,于是,我就趁着项目空闲时间好好研究 ...

  9. “约见”面试官系列之常见面试题第三十九篇之异步更新队列-$nextTick(建议收藏)

    目录 一,前言 二,什么是异步更新队列 三,使用异步更新队列 四,结尾 一,前言 这一篇介绍有关异步更新队列的知识,通过异步更新队列的学习和研究能够更好的理解Vue的更新机制 二,什么是异步更新队列 ...

  10. “约见”面试官系列之常见面试题第三十八篇之js常见的继承方式(建议收藏)

    1.原型链继承 核心: 将父类的实例作为子类的原型 将构造函数的原型设置为另一个构造函数的实例对象,这样就可以继承另一个原型对象的所有属性和方法,可以继续往上,最终形成原型链 父类 // 定义一个动物 ...

最新文章

  1. mysql 释放错误连接_JSP连接MySQL后数据库链接释放的错误
  2. 解決 Tomcat 5.0.x % include file ... % 的中文亂碼問題
  3. idea mybatis generator插件_Mybatis使用自定义插件去掉POJO的Getter和Setter方法
  4. Tomcat6.0启动startup.bat一闪而过
  5. 四种python 单继承的实现方式
  6. linkedlist java 实现_Java LinkedList 实现原理
  7. 三圆相交阴影部分面积_小学六年级图形面积的题很多家长都不会,一些初中生也未必会做...
  8. Adobe Acrobat DC
  9. 奇妙的安全旅行之DES算法(一)
  10. ffmpeg添加到环境变量_如何在Windows 10上下载和安装FFmpeg
  11. 实现putty基于密钥的安全登录
  12. VC知识库1-54期合订本索引文件
  13. 使用Spring Security实现权限管理
  14. Python:体脂计算
  15. 弘辽科技:农夫山泉溜到了下坡路
  16. chromecast 协议_Chromecast和Android TV有什么区别?
  17. 计算机未设置无线网络,没有电脑怎么设置无线路由器
  18. Laszlo 和 LZX 的 一些概念
  19. Linux处理cds文件,Linux 使用CDS磁盘+LVM
  20. 海外出货量占比超七成,海外市场决定小米手机的未来

热门文章

  1. Django之静态文件配置
  2. Selenium入门11 滚动条控制(通过js)
  3. HDMI转MIPI DSI芯片方案TC358779XBG
  4. HandlerThread用法
  5. bzoj 3924 幻想乡战略游戏
  6. CentOS 7 yum 安装php5.6
  7. Unity AssetBundles and Resources指引 (三) AssetBundle基础
  8. BZOJ2087 : [Poi2010]Sheep
  9. 近期H5项目开发小结
  10. Java学习----方法的重载