# webpack-module **Repository Path**: laddish/webpack-module ## Basic Information - **Project Name**: webpack-module - **Description**: webpack原理与实战 https://www.yuque.com/laddish/webpack/cfvw8a - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-14 - **Last Updated**: 2021-04-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # JS中的模块化 要明白我们的打包工具究竟做了什么,首先必须明白的一点就是JS中的模块化, 在ES6规范之前,我们有commonjs, amd等主流的模块化规范。 ## CommonJS JS在早期时原生时没有模块化规范的。Node.js就是一个基于V8引擎,事件驱动I/O的服务端运行环境,在09年推出时它就实现了一套名为CommonJS的模块化规范。 ### 对于死循环截断操作 ### 基于文件同步加载 require函数加载的时候去访问文件里的模块代码进行解析。 ### 单例加载只执行一次 多次require相同的module,最后加载的结果都是相同的,并不是每加载一次就去执行一次代码。每个模块都是单例的。 ### 关键词require和module.exports 在CommonJS规范里,每个JS文件就是一个模块(module),每个模块内部可以使用require函数和module.exports对象来对模块进行导入和导出。 ```javascript //moduleB.js module.exports = new Date().toLocaleTimeString(); //moduleA.js const time = require("./moduleB") setTimeout(()=>{ console.log('moduleA',time) },3000) //index.js require("./moduleA") const str = require("./moduleB") console.log(str) ``` - **index.js** 代表的模块通过执行 **require **函数,分别加载了相对路径为。**./moduleA** 和** ./moduleB **两个模块, 同时输出 **moduleB **模块的结果。 - **moduleA.js** 文件内也通过** require** 函数加载了 **moduleB.js** 模块,在3s后页输出了加载进来的结果。 - **moduleB.js** 文件内部相对来说就简单得多,仅仅定义了一个时间戳,然后通过 **module.exports** 导出。 ## AMD 另一个为WEB开发者所熟知的JS运行环境就是浏览器了。浏览器并没有提供像 Node.js 里一样的require 方法。不过,收到CommonJS模块化规范的启发,WEB端还是逐渐发展起来了AMD,SystermJS规范等适合浏览器端运行的JS模块化规范。 ### 异步获取 AMD 全称是 Asynchronous module definition,意为 异步的模块定义, 不同于CommonJS规范的同步加载, AMD正如其名所有模块默认都是异步加载,这也是早期为了满足web开发的需要,因为如果在web端也使用同步加载,那么页面在解析脚本文件的过程中可能会造成页面暂停响应。 ### ### 基于参数和回调形式来确定模块被完全加载 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618301182923-6290916f-ee64-45cb-ad47-f07032a5195d.png#align=left&display=inline&height=300&margin=%5Bobject%20Object%5D&name=image.png&originHeight=300&originWidth=747&size=32145&status=done&style=none&width=747) ### 关键词通过define和require定义和加载模块 ```javascript //moduleB.js define(function(){ return new Date().toLocaleTimeString() }); //moduleA.js define(function(require){ var time = require("moduleB") setTimeout(()=>{ console.log(time) },3000) }) //index.js require(["moduleA", "moduleB"], function (moduleA, moduleB) { console.log(moduleB); }); ``` ### 单例-同一个文件不管被加载多少次,结果也是只执行一次,后面加载的都是缓存 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618301182923-6290916f-ee64-45cb-ad47-f07032a5195d.png#align=left&display=inline&height=300&margin=%5Bobject%20Object%5D&name=image.png&originHeight=300&originWidth=747&size=32145&status=done&style=none&width=747) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618301204320-c1f8ba20-ceb8-406d-861b-2b71f60458c0.png#align=left&display=inline&height=59&margin=%5Bobject%20Object%5D&name=image.png&originHeight=59&originWidth=927&size=3976&status=done&style=none&width=927) ### RequireJS #### RequireJS文档 [https://requirejs.org/](https://requirejs.org/) #### 下载RequireJS [https://requirejs.org/docs/release/2.3.6/comments/require.js](https://requirejs.org/docs/release/2.3.6/comments/require.js) 如果想要使用 AMD 规范, 我们还需要在页面中添加一个符合AMD规范的加载器脚本,符合AMD规范实现的库有很多,比较有名的就是 **require.js** ## ESModule 前面我们说到的 CommonJS 规范和 AMD 规范有这么几个特点? 1. 语言上层的运行**环境中实现的模块化规范**,模块化规范是由环境自己定义。 1. **相互之间不能共用模块**。例如不能在Node.js运行 AMD 模块, 不能直接在浏览器运行 CommonJS 模块 在ES6之后,JS有了**语言层面的模块化**导入导出关键词与语法以及与之匹配的ESModule规范。使用ESModule规范,我们可以通过 import 和 export 两个关键词来对模块进行导入和导出。 ### 代码 ```javascript //moduleB.js let time = new Date().toLocaleTimeString; export default time; //moduleA.js import m from "./moduleB"; setTimeout(() => { console.log(m); }, 3000); //index.js import "./moduleA"; import m from "./moduleB"; console.log(m); ``` 每个JS的运行环境都有一个解析器,否则这个环境也不会认识JS语法。 大部分环境都不直接支持ESModule,所以需要进行编译。 使用webpack打包,babel转换 ```javascript yarn add webpack webpack-cli @babel/core babel-loader @babel/preset-env ``` webpack.config.js webpack基本使用系列-js兼容性处理 babel-loader @babel/core @babel/preset-env - SegmentFault 思否 [https://segmentfault.com/a/1190000022413068](https://segmentfault.com/a/1190000022413068) ```javascript const path = require("path"); module.exports = { mode: "none", entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), //绝对路径 filename: "index.bundle.js", // publicPath: "dist/", }, module: { rules: [ { test: /\/js$/, exclude: /node_modules/, loader: "babel-loader", options: { // 预设:指示babel做怎么样的兼容性处理 presets: [["@babel/preset-env"]], }, }, ], }, }; ``` ```javascript npx webpack ``` `@babel/preset-env` 只能对基本js兼容处理,也就是只能转换基本语法,遇到promise高级语法不能转换 解决: 可以全部js兼容处理 安装 @babel/polyfill ,支持全部高级语法兼容转换,但是问题是`我只要解决部分兼容性问题,但是将所有兼容性代码全部引入,体积太大了~` 最终解决方案:兼容处理只需:按需加载 ,需要安装core-js 解析器的作用就是用ECMAScript的规范去解释JS语法,也就是处理和执行语言本身的内容,例如按照逻辑正确执行 ` var a = "123";function func(){} `  之类的内容. 在解析器的上层,每个运行环境都会在解释器的基础上封装一些环境相关的 API。例如Node.js 中的global对象、process对象,浏览器中的window对象,document对象等等。 这些运行环境的API收到各自规范的影响。例如浏览器端的W3C规范,他们规定了window对象和document对象上的API内容,以使得我们能够让这些API正常运行。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618304884771-f71d6c04-6a4f-40cc-a9e2-f5c244ce6998.png#align=left&display=inline&height=271&margin=%5Bobject%20Object%5D&name=image.png&originHeight=271&originWidth=474&size=50373&status=done&style=none&width=474) ESModule就属于JS Core层面的规范,而AMD,commonjs试运行环境的规范,所以想要是的运行环境支持超ESModule其实是比较简单的,只需要升级自己环境中的JS Core解释引擎导入够的版本,引擎层面就能认识这种语法,从而不认为这是一个 语法错误(syntax error),运行环境只需要做一些兼容工作即可。 Node.js在V12版本之后才可以使用ESModule,需要升级Node到高版本。 ### Node.js 如何处理 ES6 模块 - 阮一峰的网络日志 [http://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html](http://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html) S6 模块和 CommonJS 模块有很大的差异。 语法上面,CommonJS 模块使用`require()`加载和`module.exports`输出,ES6 模块使用`import`和`export`。 用法上面,`require()`是同步加载,后面的代码必须等待这个命令执行完,才会执行。`import`命令则是异步加载,或者更准确地说,ES6 模块有一个独立的静态解析阶段,依赖关系的分析是在那个阶段完成的,最底层的模块第一个执行。 Node.js 要求 ES6 模块采用`.mjs`后缀文件名。也就是说,只要脚本文件里面使用`import`或者`export`命令,那么就必须采用`.mjs`后缀名。Node.js 遇到`.mjs`文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定`"use strict"`。 如果不希望将后缀名改成`.mjs`,可以在项目的`package.json`文件中,指定`type`字段为`module`。 ``` { "type": "module" } ``` 一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。 ``` # 解释成 ES6 模块 $ node my-app.js ``` 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成`.cjs`。如果没有`type`字段,或者`type`字段为`commonjs`,则`.js`脚本会被解释成 CommonJS 模块。 总结为一句话:**`.mjs`文件总是以 ES6 模块加载,`.cjs`文件总是以 CommonJS 模块加载,`.js`文件的加载取决于`package.json`里面`type`字段的设置**。 注意,ES6 模块与 CommonJS 模块尽量不要混用。`require`命令不能加载`.mjs`文件,会报错,只有`import`命令才可以加载`.mjs`文件。反过来,`.mjs`文件里面也不能使用`require`命令,必须使用`import`。 ## 后模块化的编译时代 通过前面分析,使用ESModule的模块明显更符合JS开发的历史进程,因为任何一个支持JS的环境,随着应用解释器的升级,最终一定会支持ESM的标准。 但是web端受制于用户使用的浏览器版本,我们并不能随心所欲随时使用JS最新特性,为了是我们的代码能够运行在老旧的浏览器中,需要能够静态把高版本规范的代码编译为低版本规范的代码的工具,例如熟知的babel。 然而,不幸的是,**对于模块化相关的import和export关键字,babel最终会把它编译为包含require和exports的CommonJS规范**。 ### babel编译无法解决模块化通用的问题 Babel 中文网 · Babel - 下一代 JavaScript 语法的编译器 [https://www.babeljs.cn/repl](https://www.babeljs.cn/repl) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618306830026-c77d3901-b267-45a6-9fd3-caad49aff05f.png#align=left&display=inline&height=371&margin=%5Bobject%20Object%5D&name=image.png&originHeight=371&originWidth=908&size=31047&status=done&style=none&width=908) 这就产生了问题,这样带有模块化关键词的模块,编译之后还是无法直接运行在浏览器中,因为浏览器并不能直接运行CommonJs规范的模块,除了编译之外,我们还需要一个步骤叫做** 打包(Module)。** ** ### 打包可以将模块化内部实现的细节抹平 打包工具的作用,就是**将模块化内部实现的细节抹平**。无论是AMD还是CommonJs模块化规范的模块,经过打包处理后能够变成直接运行在浏览器、Node中的代码。 # 如何处理打包 ## NodeJS如何进行commonjs模块化 ```javascript const str = `require("./moduleA"); const str = require("./moduleB"); console.log(str);`; const functionWrapper = ["function a(require,module,exports){", "}"]; //1.将文件进行包裹 成为一个字符串函数 const result = functionWrapper[0] + str + functionWrapper[1]; console.log(result) const vm = require('vm'); //eval new Function //把字符串变成可执行函数 注入require,module,exports就可以进行导入导出操作 vm.runInNewContext(result) ``` ### [vm_runinnewcontext](http://nodejs.cn/api/vm.html#vm_vm_runinnewcontext_code_contextobject_options) | Node.js API 文档 ```javascript const vm = require('vm'); const contextObject = { animal: 'cat', count: 2 }; vm.runInNewContext('count += 1; name = "kitty"', contextObject); console.log(contextObject); // 打印: { animal: 'cat', count: 3, name: 'kitty' } ``` [http://nodejs.cn/api/vm.html#vm_vm_runinnewcontext_code_contextobject_options](http://nodejs.cn/api/vm.html#vm_vm_runinnewcontext_code_contextobject_options) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618308002261-b07e74cb-ab65-45d0-8d48-c4e33abdb96d.png#align=left&display=inline&height=400&margin=%5Bobject%20Object%5D&name=image.png&originHeight=400&originWidth=671&size=130750&status=done&style=none&width=671) ## 参考Node.js源码来熟悉CommonJS的处理方式 ```javascript (function () { var moduleA = function (require, module, exports) { console.log("hello bundler"); module.exports = "hello world"; }; var module = { exports: {} }; moduleA(null,module); })(); ``` ## 浏览器中对CommonJS处理 ### 只有导出的CommonJS的简单脚本 **index.js** ```javascript console.log("hello bundler"); module.exports = "hello world"; ``` **index.module.js** ```javascript (function () { //为了不用起名字 放到数组里面 数组里面的每一项都是匿名的模块 var moduleList = [ //index.js function (require, module, exports) { console.log("hello bundler"); module.exports = "hello world"; }, //上面这部分都是通用的 ]; var module = { exports: {} }; moduleList[0](null, module,module.exports); })(); ``` **bundle.js** **读取样本然后replace替换然后写入新文件** ```javascript const path = require("path"); const fs = require("fs"); const boiler = fs.readFileSync( path.resolve(__dirname, "index.bundle.boilerplate"), "utf-8" ); const target = fs.readFileSync( path.resolve(__dirname, "..", "index.js"), "utf-8" ); const content = boiler.replace("/* template */", target); fs.writeFileSync(path.resolve(__dirname, "dist/index.bundle.js"), content, "utf-8"); ``` **index.bundle.boilerplate** ```javascript (function () { var moduleList = [ function (require, module, exports) { /* template */ }, ]; var module = { exports: {} }; moduleList[0](null, module,module.exports); })(); ``` #### 打包结果 ```javascript (function () { var moduleList = [ function (require, module, exports) { console.log("hello bundler"); module.exports = "hello world"; }, ]; var module = { exports: {} }; moduleList[0](null, module); })(); ``` ### 针对导入的脚本 需要找依赖关系然后同步处理导入导出依赖 **index.bundle-require.js** ```javascript (function () { //为了不用起名字 放到数组里面 数组里面的每一项都是匿名的模块 var moduleList = [ //index.js function (require, module, exports) { const moduleA = require("./moduleA"); console.log("moduleA", moduleA); console.log("hello bundler"); module.exports = "hello world"; }, // moduleA.js function (require, module, exports) { module.exports = new Date().toLocaleTimeString(); }, ]; // 模块依赖数组 var moduleDepIdList = [{ "./moduleA": 1 }, {}]; function require(id, parentId) { var currentModuleId = parentId !== undefined ? moduleDepIdList[parentId][id] : id; var module = { exports: {} }; var moduleFunc = moduleList[currentModuleId]; moduleFunc((id) => require(id, currentModuleId), module, module.exports); return module.exports; } require(0); // var module = { exports: {} }; // moduleList[0](null, module); })(); ``` **替换模板 index.bundle.boilerplate** ```javascript (function () { var moduleList = [ /* template-module-list */ ]; var moduleDepIdList = [ /* template-module-dep-id-list */ ]; function require(id, parentId) { var currentModuleId = parentId !== undefined ? moduleDepIdList[parentId][id] : id; var module = { exports: {} }; var moduleFunc = moduleList[currentModuleId]; moduleFunc((id) => require(id, currentModuleId), module, module.exports); return module.exports; } require(0); })(); ``` ## 异步组件打包 ### 动态import导入在webpack中是如何实现的? **index.js** ```javascript setTimeout(() => { import("./moduleA").then((content) => { console.log(content); }); }, 5000); ``` **moduleA.js** ```javascript import m from "./moduleB"; setTimeout(() => { console.log(m); }, 3000); ``` **moduleB.js** ```javascript let time = new Date().toLocaleTimeString(); export default time; ``` 针对异步加载的,webpack把他们打成多个包,然后异步加载,通过JSONP实现 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618385651214-f6b00efb-7ac3-4bb1-9124-bcb7860e8ae3.png#align=left&display=inline&height=87&margin=%5Bobject%20Object%5D&name=image.png&originHeight=87&originWidth=253&size=3198&status=done&style=none&width=253) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/349593/1618385471983-bb29f15b-3525-4466-a01a-8673f107600c.png#align=left&display=inline&height=454&margin=%5Bobject%20Object%5D&name=image.png&originHeight=454&originWidth=507&size=27742&status=done&style=none&width=507) ### require.ensure ```javascript (function () { var moduleList = [ function (require, exports, module) { require.ensure("1").then((res) => { console.log(res); }); }, ]; var moduleDepIdList = []; var cache = {}; //异步 function require(id, parentId) { var currentModuleId = parentId !== undefined ? moduleDepIdList[parentId][id] : id; var module = { exports: {} }; var moduleFunc = moduleList[currentModuleId]; moduleFunc((id) => require(id, currentModuleId), module, module.exports); return module.exports; } //JSONP作用就是从全局cache里面读取到刚才存储的resolve方法 //在resolve里面 通过函数执行把模块导出的对象resolve出去 //就可以在then里面拿到模块 window.__JSONP = function (chunkId, moduleFunc) { var currentChunkStatus = cache[chunkId]; var resolve = currentChunkStatus[0]; var module = { exports: {} }; moduleFunc(require, module, module.exports); resolve(module.exports); }; //对于import() 遇到require.ensure 通过jsonp进行异步加载 //通过全局的对象对当前模块的状态进行缓存 //没有就创建一个script标签通过jsonp形式异步通过trunkId拿到打包结果 //最终返回一个promise //把两个状态添加到全局的cache //第一个是resolve状态 记录当前status 为true就是还在加载 require.ensure = function (chunkId, parentId) { var currentModuleId = parentId !== undefined ? moduleDepIdList[parentId][chunkId] : chunkId; var currentChunkStatus = cache[currentModuleId]; if (currentChunkStatus === undefined) { //没有 var $script = document.createElement("script"); // $script.src = chunkId + currentChunkStatus + ".js"; $script.src = "chunk_" + chunkId + ".js"; document.body.appendChild($script); var promise = new Promise(function (resolve) { var chunkCache = [resolve]; chunkCache.status = true; cache[currentModuleId] = chunkCache; }); cache[currentModuleId].push(promise); return promise; } if (currentChunkStatus.status) { return currentChunkStatus[1]; } return chunkCache; }; // require(0); moduleList[0](require, null, null); })(); ```