{"id":16751658,"url":"https://github.com/lq782655835/webpack-module-example","last_synced_at":"2025-04-10T15:50:49.667Z","repository":{"id":96217549,"uuid":"176918249","full_name":"lq782655835/webpack-module-example","owner":"lq782655835","description":"example to explain  webpack module","archived":false,"fork":false,"pushed_at":"2019-03-28T06:54:49.000Z","size":47,"stargazers_count":7,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-24T13:37:05.209Z","etag":null,"topics":["article","webpack"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lq782655835.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-03-21T10:02:10.000Z","updated_at":"2024-03-12T14:00:17.000Z","dependencies_parsed_at":"2023-04-13T05:42:45.525Z","dependency_job_id":null,"html_url":"https://github.com/lq782655835/webpack-module-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lq782655835%2Fwebpack-module-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lq782655835%2Fwebpack-module-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lq782655835%2Fwebpack-module-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lq782655835%2Fwebpack-module-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lq782655835","download_url":"https://codeload.github.com/lq782655835/webpack-module-example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248244899,"owners_count":21071363,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["article","webpack"],"created_at":"2024-10-13T02:44:32.507Z","updated_at":"2025-04-10T15:50:49.660Z","avatar_url":"https://github.com/lq782655835.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Webpack 模块打包原理\n\n在使用webpack的过程中，你是否好奇webpack打包的代码为什么可以直接在浏览器中跑？为什么webpack可以支持各种ES6最新语法？为什么在webpack中可以书写import ES6模块，也支持require CommonJS模块？\n\n## 模块规范\n关于模块，我们先来认识下目前主流的模块规范（自从有了ES6 Module及Webpack等工具，AMD/CMD规范生存空间已经很小了）：\n* CommonJS\n* UMD\n* ES6 Module\n\n### CommonJS\nES6前，js没有属于自己的模块规范，所以社区制定了 CommonJS规范。而NodeJS所使用的模块系统就是基于CommonJS规范实现的。\n``` js\n// CommonJS 导出\nmodule.exports = { age: 1, a: 'hello', foo:function(){} }\n\n// CommonJS 导入\nconst foo = require('./foo.js')\n```\n\n### UMD\n根据当前运行环境的判断，如果是 Node 环境 就是使用 CommonJS 规范， 如果不是就判断是否为 AMD 环境， 最后导出全局变量。这样代码可以同时运行在Node和浏览器环境中。目前大部分库都是打包成UMD规范，Webpack也支持UMD打包，配置API是[output.libraryTarget](https://webpack.docschina.org/configuration/output/#output-librarytarget)。详细案例可以看笔者封装的npm工具包：[cache-manage-js](https://github.com/lq782655835/cache-manage-js)\n``` js\n(function (global, factory) {\n    typeof exports === 'object' \u0026\u0026 typeof module !== 'undefined' ? module.exports = factory() :\n    typeof define === 'function' \u0026\u0026 define.amd ? define(factory) :\n    (global.libName = factory());\n}(this, (function () { 'use strict';})));\n```\n\n### ES6 Module\nES6 模块的设计思想是尽量的静态化，使得编译时就能确定模块的依赖关系，以及输入和输出的变量。具体思想和语法可以看笔者的另外一篇文章：[ES6-模块详解]((https://lq782655835.github.io/blogs/es6/es6-4.module.html))\n\n``` js\n// es6模块 导出\nexport default { age: 1, a: 'hello', foo:function(){} }\n\n// es6模块 导入\nimport foo from './foo'\n```\n\n## Webpack模块打包\n既然模块规范有这么多，那webpack是如何去解析不同的模块呢？\n\nwebpack根据webpack.config.js中的入口文件，在入口文件里识别模块依赖，不管这里的模块依赖是用CommonJS写的，还是ES6 Module规范写的，webpack会自动进行分析，并通过转换、编译代码，打包成最终的文件。`最终文件中的模块实现是基于webpack自己实现的webpack_require（es5代码）`，所以打包后的文件可以跑在浏览器上。\n\n同时以上意味着在webapck环境下，你可以只使用ES6 模块语法书写代码（通常我们都是这么做的），也可以使用CommonJS模块语法，甚至可以两者混合使用。因为从webpack2开始，内置了对ES6、CommonJS、AMD 模块化语句的支持，`webpack会对各种模块进行语法分析，并做转换编译`。\n\n我们举个例子来分析下打包后的源码文件，例子源代码在 [webpack-module-example](https://github.com/lq782655835/webpack-module-example)\n``` js\n// webpack.config.js\nconst path = require('path');\n\nmodule.exports = {\n    mode: 'development',\n  // JavaScript 执行入口文件\n  entry: './src/main.js',\n  output: {\n    // 把所有依赖的模块合并输出到一个 bundle.js 文件\n    filename: 'bundle.js',\n    // 输出文件都放到 dist 目录下\n    path: path.resolve(__dirname, './dist'),\n  }\n};\n```\n\n``` js\n// src/add\nexport default function(a, b) {\n    let { name } = { name: 'hello world,'} // 这里特意使用了ES6语法\n    return name + a + b\n}\n\n// src/main.js\nimport Add from './add'\nconsole.log(Add, Add(1, 2))\n```\n\n打包后精简的bundle.js文件如下:\n\n``` js\n// modules是存放所有模块的数组，数组中每个元素存储{ 模块路径: 模块导出代码函数 }\n(function(modules) {\n// 模块缓存作用，已加载的模块可以不用再重新读取，提升性能\nvar installedModules = {};\n\n// 关键函数，加载模块代码\n// 形式有点像Node的CommonJS模块，但这里是可跑在浏览器上的es5代码\nfunction __webpack_require__(moduleId) {\n  // 缓存检查，有则直接从缓存中取得\n  if(installedModules[moduleId]) {\n    return installedModules[moduleId].exports;\n  }\n  // 先创建一个空模块，塞入缓存中\n  var module = installedModules[moduleId] = {\n    i: moduleId,\n    l: false, // 标记是否已经加载\n    exports: {} // 初始模块为空\n  };\n\n  // 把要加载的模块内容，挂载到module.exports上\n  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n  module.l = true; // 标记为已加载\n\n  // 返回加载的模块，调用方直接调用即可\n  return module.exports;\n}\n\n// __webpack_require__对象下的r函数\n// 在module.exports上定义__esModule为true，表明是一个模块对象\n__webpack_require__.r = function(exports) {\n  Object.defineProperty(exports, '__esModule', { value: true });\n};\n\n// 启动入口模块main.js\nreturn __webpack_require__(__webpack_require__.s = \"./src/main.js\");\n})\n({\n  // add模块\n  \"./src/add.js\": (function(module, __webpack_exports__, __webpack_require__) {\n    // 在module.exports上定义__esModule为true\n    __webpack_require__.r(__webpack_exports__);\n    // 直接把add模块内容，赋给module.exports.default对象上\n    __webpack_exports__[\"default\"] = (function(a, b) {\n      let { name } = { name: 'hello world,'}\n      return name + a + b\n    });\n  }),\n\n  // 入口模块\n  \"./src/main.js\": (function(module, __webpack_exports__, __webpack_require__) {\n    __webpack_require__.r(__webpack_exports__)\n    // 拿到add模块的定义\n    // _add__WEBPACK_IMPORTED_MODULE_0__ = module.exports，有点类似require\n    var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./src/add.js\");\n    // add模块内容: _add__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n    console.log(_add__WEBPACK_IMPORTED_MODULE_0__[\"default\"], Object(_add__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(1, 2))\n  })\n});\n```\n以上核心代码中，能让打包后的代码直接跑在浏览器中，是因为webpack通过__webpack_require__ 函数模拟了模块的加载（类似于node中的require语法），把定义的模块内容挂载到module.exports上。同时__webpack_require__函数中也对模块缓存做了优化，防止模块二次重新加载，优化性能。\n\n再让我们看下webpack的源码：\n``` js\n// webpack/lib/MainTemplate.js\n\n// 主文件模板\n// webpack生成的最终文件叫chunk，chunk包含若干的逻辑模块，即为module\nthis.hooks.render.tap( \"MainTemplate\",\n(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) =\u003e {\n  const source = new ConcatSource();\n  source.add(\"/******/ (function(modules) { // webpackBootstrap\\n\");\n  // 入口内容，__webpack_require__就在bootstrapSource中\n  source.add(new PrefixSource(\"/******/\", bootstrapSource));\n  source.add(\"/******/ })\\n\");\n  source.add(\n    \"/************************************************************************/\\n\"\n  );\n  source.add(\"/******/ (\");\n  source.add(\n    // 依赖的module都会写入对应数组\n    this.hooks.modules.call(\n      new RawSource(\"\"),\n      chunk,\n      hash,\n      moduleTemplate,\n      dependencyTemplates\n    )\n  );\n  source.add(\")\");\n  return source;\n}\n```\n\n## Webpack ES6语法支持\n可能细心的读者看到，以上打包后的add模块代码中依然还是ES6语法，在低端的浏览器中不支持。这是因为没有对应的loader去解析js代码，webpack把所有的资源都视作模块，不同的资源使用不同的loader进行转换。\n\n这里需要使用babel-loader及其插件@babel/preset-env进行处理，把ES6代码转换成可在浏览器中跑的es5代码。\n\n``` js\n// webpack.config.js\nmodule.exports = {\n  ...,\n  module: {\n    rules: [\n      {\n        // 对以js后缀的文件资源，用babel进行处理\n        test: /\\.m?js$/,\n        exclude: /(node_modules|bower_components)/,\n        use: {\n          loader: 'babel-loader',\n          options: {\n            presets: ['@babel/preset-env']\n          }\n        }\n      }\n    ]\n  }\n};\n```\n\n``` js\n// 经过babel处理es6语法后的代码\n__webpack_exports__[\"default\"] = (function (a, b) {\n  var _name = {    name: 'hello world,'  }, name = _name.name;\n  return name + a + b;\n});\n```\n\n## Webpack 模块异步加载\n以上webpack把所有模块打包到主文件中，所以模块加载方式都是同步方式。但在开发应用过程中，按需加载（也叫懒加载）也是经常使用的优化技巧之一。按需加载，通俗讲就是代码执行到异步模块（模块内容在另外一个js文件中），通过网络请求即时加载对应的异步模块代码，再继续接下去的流程。那webpack是如何执行代码时，判断哪些代码是异步模块呢？webpack又是如何加载异步模块呢？\n\nwebpack有个[require.ensure](https://webpack.js.org/api/module-methods/#requireensure) api语法来标记为异步加载模块，最新的webpack4推荐使用新的[import()](https://webpack.js.org/api/module-methods/#import-1) api(需要配合@babel/plugin-syntax-dynamic-import插件)。因为require.ensure是通过回调函数执行接下来的流程，而import()返回promise，这意味着可以使用到最新的ES8 async/await语法，使得可以像书写同步代码一样，执行异步流程。\n\n现在我们从webpack打包后的源码来看下，webpack是如何实现异步模块加载的。修改入口文件main.js，引入异步模块async.js：\n``` js\n// main.js\nimport Add from './add'\nconsole.log(Add, Add(1, 2), 123)\n\n// 按需加载\n// 方式1: require.ensure\n// require.ensure([], function(require){\n//     var asyncModule = require('./async')\n//     console.log(asyncModule.default, 234)\n// })\n\n// 方式2: webpack4新的import语法\n// 需要加@babel/plugin-syntax-dynamic-import插件\nlet asyncModuleWarp = async () =\u003e await import('./async')\nconsole.log(asyncModuleWarp().default, 234)\n```\n\n``` js\n// async.js\nexport default function() {\n    return 'hello, aysnc module'\n}\n```\n\n以上代码打包会生成两个chunk文件，分别是主文件`main.bundle.js`以及异步模块文件`0.bundle.js`。同样，为方便读者快速理解，精简保留主流程代码。\n\n``` js\n// 0.bundle.js\n\n// 异步模块\n// window[\"webpackJsonp\"]是连接多个chunk文件的桥梁\n// window[\"webpackJsonp\"].push = 主chunk文件.webpackJsonpCallback\n(window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || []).push([\n  [0], // 异步模块标识chunkId,可判断异步代码是否加载成功\n  // 跟同步模块一样，存放了{模块路径：模块内容}\n  {\n  \"./src/async.js\": (function(module, __webpack_exports__, __webpack_require__) {\n      __webpack_require__.r(__webpack_exports__);\n      __webpack_exports__[\"default\"] = (function () {\n        return 'hello, aysnc module';\n      });\n    })\n  }\n]);\n```\n\n以上知道，异步模块打包后的文件中**保存着异步模块源代码**，同时为了区分不同的异步模块，**还保存着该异步模块对应的标识：chunkId**。以上代码主动调用window[\"webpackJsonp\"].push函数，该函数是连接异步模块与主模块的关键函数，该函数定义在主文件中，实际上`window[\"webpackJsonp\"].push = webpackJsonpCallback`，详细源码咱们看看主文件打包后的代码：\n\n``` js\n// main.bundle.js\n\n(function(modules) {\n// 获取到异步chunk代码后的回调函数\n// 连接两个模块文件的关键函数\nfunction webpackJsonpCallback(data) {\n  var chunkIds = data[0]; //data[0]存放了异步模块对应的chunkId\n  var moreModules = data[1]; // data[1]存放了异步模块代码\n\n  // 标记异步模块已加载成功\n  var moduleId, chunkId, i = 0, resolves = [];\n  for(;i \u003c chunkIds.length; i++) {\n    chunkId = chunkIds[i];\n    if(installedChunks[chunkId]) {\n      resolves.push(installedChunks[chunkId][0]);\n    }\n    installedChunks[chunkId] = 0;\n  }\n\n  // 把异步模块代码都存放到modules中\n  // 此时万事俱备，异步代码都已经同步加载到主模块中\n  for(moduleId in moreModules) {\n    modules[moduleId] = moreModules[moduleId];\n  }\n\n  // 重点：执行resolve() = installedChunks[chunkId][0]()返回promise\n  while(resolves.length) {\n    resolves.shift()();\n  }\n};\n\n// 记录哪些chunk已加载完成\nvar installedChunks = {\n  \"main\": 0\n};\n\n// __webpack_require__依然是同步读取模块代码作用\nfunction __webpack_require__(moduleId) {\n  ...\n}\n\n// 加载异步模块\n__webpack_require__.e = function requireEnsure(chunkId) {\n  // 创建promise\n  // 把resolve保存到installedChunks[chunkId]中，等待代码加载好再执行resolve()以返回promise\n  var promise = new Promise(function(resolve, reject) {\n    installedChunks[chunkId] = [resolve, reject];\n  });\n\n  // 通过往head头部插入script标签异步加载到chunk代码\n  var script = document.createElement('script');\n  script.charset = 'utf-8';\n  script.timeout = 120;\n  script.src = __webpack_require__.p + \"\" + ({}[chunkId]||chunkId) + \".bundle.js\"\n  var onScriptComplete = function (event) {\n    var chunk = installedChunks[chunkId];\n  };\n  script.onerror = script.onload = onScriptComplete;\n  document.head.appendChild(script);\n\n  return promise;\n};\n\nvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n// 关键代码： window[\"webpackJsonp\"].push = webpackJsonpCallback\njsonpArray.push = webpackJsonpCallback;\n\n// 入口执行\nreturn __webpack_require__(__webpack_require__.s = \"./src/main.js\");\n})\n({\n\"./src/add.js\": (function(module, __webpack_exports__, __webpack_require__) {...}),\n\n\"./src/main.js\": (function(module, exports, __webpack_require__) {\n  // 同步方式\n  var Add = __webpack_require__(\"./src/add.js\").default;\n  console.log(Add, Add(1, 2), 123);\n\n  // 异步方式\n  var asyncModuleWarp =function () {\n    var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {\n      return regeneratorRuntime.wrap(function _callee$(_context) {\n        // 执行到异步代码时，会去执行__webpack_require__.e方法\n        // __webpack_require__.e其返回promise，表示异步代码都已经加载到主模块了\n        // 接下来像同步一样，直接加载模块\n        return __webpack_require__.e(0)\n              .then(__webpack_require__.bind(null, \"./src/async.js\"))\n      }, _callee);\n    }));\n\n    return function asyncModuleWarp() {\n      return _ref.apply(this, arguments);\n    };\n  }();\n  console.log(asyncModuleWarp().default, 234)\n})\n});\n```\n\n从上面源码可以知道，**webpack实现模块的异步加载有点像jsonp的流程**。在主js文件中通过在head中构建script标签方式，异步加载模块信息；再使用回调函数webpackJsonpCallback，把异步的模块源码同步到主文件中，所以后续操作异步模块可以像同步模块一样。\n源码具体实现流程：\n1. 遇到异步模块时，使用`__webpack_require__.e`函数去把异步代码加载进来。该函数会在html的head中动态增加script标签，src指向指定的异步模块存放的文件。\n2. 加载的异步模块文件会执行`webpackJsonpCallback`函数，把异步模块加载到主文件中。\n3. 所以后续可以像同步模块一样,直接使用`__webpack_require__(\"./src/async.js\")`加载异步模块。\n\u003e 注意源码中的primose使用非常精妙，主模块加载完成异步模块才resolve()\n\n## 总结\n1. webpack对于ES模块/CommonJS模块的实现，是基于自己实现的webpack_require，所以代码能跑在浏览器中。\n2. 从 webpack2 开始，已经内置了对 ES6、CommonJS、AMD 模块化语句的支持。但不包括新的ES6语法转为ES5代码，这部分工作还是留给了babel及其插件。\n3. 在webpack中可以同时使用ES6模块和CommonJS模块。因为 module.exports很像export default，所以ES6模块可以很方便兼容 CommonJS：import XXX from 'commonjs-module'。反过来CommonJS兼容ES6模块，需要额外加上default：require('es-module').default。\n4. webpack异步加载模块实现流程跟jsonp基本一致。\n\n## 参考文章\n\n* [前端模块化：CommonJS,AMD,CMD,ES6](https://juejin.im/post/5aaa37c8f265da23945f365c)\n* [深入 CommonJs 与 ES6 Module](https://segmentfault.com/a/1190000017878394)\n* [Webpack将代码打包成什么样子](https://github.com/Pines-Cheng/blog/issues/45)\n* [Webpack源码分析](https://zhuanlan.zhihu.com/p/29551683)\n* [Webpack Code Splitting](https://webpack.js.org/guides/code-splitting)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flq782655835%2Fwebpack-module-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flq782655835%2Fwebpack-module-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flq782655835%2Fwebpack-module-example/lists"}