第1章
何为webpack:
Webpack是一个开元的JS模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个JS文件,这个过程就叫做模块打包。
为什么需要webpack:
应用规模大了以后,必须借助一定的工具,否则人工维护代码的成本将逐渐变得难以承受,学会使用工具可以让开发效率成倍的提升。
何为模块:
在设计程序结构时,把所有代码都堆到一起是非常糟糕的做法。更好的组织方式按照特定的功能将其拆分为多个代码段,每个代码段实现一个特定的目的。你可以对其进行独立的设计、开发和测试,最终通过接口来将它们组合到一起,这就是基本的模块化思想。
引入多个js文件到页面中的缺点:
①需要手动维护js的加载顺序。页面的多个script之间通常会有依赖关系,但由于这种依赖关系是隐式的,除了添加注释以外很难清晰地指明谁依赖了谁,这样当加载文件过多的时候就会出现问题。
②每一个script标签,都意味着需要向服务器请求一次静态资源,在HTTP2还没出现的时期,建立连接的成本是很高的,过多的请求会严重拖慢网页的渲染速度。
③每个script标签中,全局作用域,如果没有进行任何处理而直接在代码中进行变量或者函数声明,就会造成全局作用域污染。
模块化则解决了上述的所有问题:
①通过导入和导出与我们可以清晰模块之间的依赖关系。
②模块可以借助工具来打包,在页面上只需要加载整合后的资源文件,减少了网络开销。
③多个模块之间的作用域是隔离的彼此不会有命名冲突。
09年开始js社区开始进行模块化尝试,并依次出现了AMD、CommonJS、CMD等解决方案。但这些都是社区提出的,并不能算语言本身的特性。而在2015年。ES6正式定义了模块标准,这门语言在诞生20年之后终于有人模块这一概念。
ES6模块标目前已经得到了大多数现代浏览器的支持,但在实际应用方面还需要一段时间,有以下原因:
①无法使用 code splitting 和 tree shaking(webpack的两个重要特性)
②大多数npm模块还是CommonJS的形式,而浏览器并不支持其语法,因此这些包没有办法直接使用。
③仍需考虑个别浏览器及平台的兼容性问题。
模块打包工具的两种工作方式:
①将存在依赖关系的模块按照特定规则合并为单个JS文件,一次全部加载进页面中。
②在页面初始时加载一个入口模块,其他模块异步地进行加载。
目前社区中比较流行的打包模块有Webpack,Parcel,Rollup等。
为什么选择webpack?对比同类模块打包工具,webpack具备哪些优势?
①Webpack默认支持多种模块标准,包括AMD、CommonJS,以及最新的ES6模块,而其他工具大多数只能支持一到两种。这对于使用多种模块标准的工程非常有用,Webpack会帮助我们处理不同类型模块之间的依赖关系。
②Webpack有完备的代码分割解决方案。它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态地加载。这对于资源体积较大的应用来说尤为重要,可以有效地减小资源体积,提升首页渲染速度。
③Webpack可以处理各种类型的资源。除了js外,webpack还可以处理样式,模板,图片等,而开发者需要做的仅仅是导入它们。比如你可以从js文件导入一个CSS或者PNG,而这一切最终都可以由loader来处理。
④Webpack拥有庞大的社区支持。除了webpack核心库以外,还有无数开发者为它编写周边的插件和工具,绝大多数需求都可以找到已有解决方案。
安装:
webpack,对操作系统没有要求,唯一的依赖就是Node.js
webpack对node版本是有一定要求的,推荐使用LTS版本。LTS版本是node在当前阶段较为稳定的版本。该版本中不会包含太多激进的特性。
安装好node,使用 Node.js 的包管理器 npm 来安装 Webpack,安装模块方式有两种:全局安装,本地安装。
两种安装方式利弊及其特点:
全局安装的好处是,npm会帮我们绑定一个命令行环境变量,一次安装处处运行;本地安装则会添加其成为项目中的依赖,只能在项目内部使用。
建议本地安装,有以下原因:
①如果采用全局安装,那么在与他人进行项目协作的时候,由于每个人系统中webpack版本不同,可能会导致输出结果不一样。
②部分依赖于webpack的插件会调用项目中webpack的内部模块,这种情况下仍需要进行本地安装,而全局本地都有,则容易造成混淆。
npx webpack --entry=./index.js --output-filename=bundle.js -mode=development
第一个参数:entry 是资源打包的入口。webpack从这里开始进行模块依赖的查找,的到项目中的两个js模块,并通过它们来生成最终产物。
第二个参数:output-filenam 是输出资源名。打包后生成的dist目录下,包含一个bundle.js就是webpack的打包结果。
最后的参数:mode 指的是打包模式。Webpack为开发者提供了 development、production、none三种模式,除了none模式都会自动添加适合当前模式的一系列配置,为了减少工作量,开发中选择development模式即可。
scripts 是 npm 提供的脚本命令功能,在这里可以直接使用由模块添加的命令。(比如 webpack 取代之前的 npx webpack)
使用默认的目录配置:
工程源代码放在 /src 中,
输出资源放在 /dist 中。
对于资源输出目录来说 webpack ,默认是 /dist ,我们不需要做任何改动。
同时webpack默认源代码入口就是 src/index.js , 因此按照此目录顺序,可以省略掉 entry 的配置了。
虽然目录命名并不是强制的,但还是建议遵循统一命名规范,这样会使得大体结构比较清晰,也利于多人协作。
1.4.4 使用配置文件
通过 module.exports 导出一个对象,也就是打包时被webpack接收的配置对象。先前在命令行中输入的一大串参数就都要改为 key-value 的形式放在这个对象下。
目前该对象包含两个关于资源输入资源输出的属性——entry 和 output 。
entry就是我们的资源入口,output则是一个包含更多详细配置的对象。
之前的参数 --output-filename 和 --output-path 现在都成为了 output 下面的属性。filename,和先前一样都是bundle.js,不需要改动,而path和之前是有所区别的,webpack 对于 output.path 的要求是使用绝对路径(从系统根目录开始的完整路径),之前命令行中为了简洁都是相对路径。
而在webpack.config.js 中,我们通过调用node.js的路径拼装函数——path.join,将_dirname (Node.js 内置的全局变量,值为当前文件所在的绝对路径)与dist(输出目录)连接起来,得到最终的输出目录。
1.4.5 webpack-dev-server
安装指令中的--save-dev参数是将webpack-dev-server作为工程的devDependencies(开发环境依赖)记录在package.json中。
这样做是因为webpack-dev-server仅仅在本地开发环境中才用到。
假如工程上线时要进行依赖安装,就可以通过 npm install --production 过滤掉 devDependencies 中的冗余模块,从而加快安装和发布的速度。
为了便捷地启动 webpack-dev-server,我们再package.json中添加一个dev指令:
然后还需要对 " webpack-dev-server " 进行配置。编辑webpack.config.js 如下:
我们在配置中添加了一个 devServer 对象,它是专门用来放 webpack,dev-server 配置的。webpack-dev-server 可以看做是一个服务者,它的主要工作就是接收浏览器请求,然后将资源返回。当服务启动时,会先让 Webpack 进行模块打包并将资源准备好(在示例中就是bundle.js)。
当 webpack-dev-server 接收到浏览器的资源请求时。它会首先进行 URL 地址校验。如果该地址是资源服务地址(上面配置的publicPath),就会从 Webpack 的打包结果中寻找该资源并返回浏览器。反之,如果请求不属于资源服务地址,则直接读取硬盘中的源文件并将其返回。
综上,总结出 webpack-dev-server 的两大职能:
①令Webpack进行模块打包,并处理打包结果的资源请求。
②作为普通的 Web Server ,处理静态资源文件请求。
webpack-dev-server,不是像直接用webpack开发那样每次都会生成bundle.js ,而 webpack-dev-server 只是将打包结果放在内存中,并不会写入实际的bundle.js ,在每次 webpack-dev-server 接收到请求时都只是将内存中的打包结果返回给浏览器。
webpack-dev-server 还有一项很便捷的特性就是 live-reloading(自动刷新)。
当webpack-dev-server发现工程源文件进行了更新操作就会自动刷新浏览器,显示更新后的内容。
之后会讲到,hot-module-replacement(模块热替换),我们始终不需要刷新浏览器就能获取到更新之后的内容。
1.5 小结
webpack的功能,它可以处理模块之间的依赖,将它们串联起来合并为单一的JS文件。
安装webpack一般选择本地安装,这样可以使团队开发时共用一个版本,并且可以让其他插件直接获取webpack的内部模块。
配置本地开发环境可以借助 npm scripts 来维护命令行脚本,当打包脚本参数过多时,我们需要将其转换为 webpack.config.js ,用文件的方式维护复杂的 webpack 配置。
webpack-dev-server 的作用启动一个本地服务,可以处理打包资源与静态资源的请求。它的 live-reloading 功能可以监听文件变化,自动刷新界面提高开发效率。
第2章
CommonJS
导出是一个模块向外暴露自身的唯一方式。CommonJS中,通过 module.exports 进行导出
导入,CommonJS中使用 require 进行模块导入
module 对象用来存放其信息,其 loaded 属性用于记录该模块是否被加载过。第一次被加载和执行后会被置为 true ,后面再次加载时检查到 module.loaded 为 true ,则不会再执行模块代码了。
ES6 Module
ES6 Module 会自动采取严格模式 (所以要将未开启严格模式的代码转换为ESM要注意此点)
使用 export 命令导出模块,命名导出 和 默认导出
命名导出可以用as关键字改变名字,导入导出的时候都可以。
默认导出就是 export default ,可以理解为 对外输出了一个名为 default 的变量。
使用 import 语法导入模块。
导入多个变量可以用 import * as <myMoudle> 把所有导入的变量作为属性值添加到 <myModule>
默认导出 import 后面的名字可以*指定,它指代了导出文件默认导出的值。
CommonJS 和 ES6 Module 的区别?
commonJS 对模块依赖的解决是动态的,依赖建立在代码发生阶段。require 指定路径可以动态指定。
ES6Module 对模块依赖关系的建立是在代码的编译阶段。声明式的导入、导出语句,不支持导入路径是表达式。
ESM相对于CommonJS的优点:
①死代码检测和排除。可以用静态分析工具检测出那些模块没有被调用过,打包的时候可以去除,减小资源体积。
②模块变量类型检查。js属于动态类型语言,不会在代码执行前检查类型错误。ESM的静态模块结构有助于确保模块之间传递的值和接口类型是正确的。
③编译器优化。在CommonJS等动态模块系统中,无论怎么导入都是一个对象,但是esm可以支持直接导入变量,减少了引用层级,程序效率更高。
值拷贝与动态映射
在导入模块时,
CommonJS获取的是一份导出值的拷贝;
ES6Module中则是动态映射。
在产生循环依赖的时候CommonJS会输出{}空对象,ESModule会输出undefined。
但是因为ESModule为动态映射,如果我们保证当导入的值被使用时已经设置好正确的导出值,就可以解决循环依赖产生的问题。
AMD标准:
define函数来定义,同步加载模块标准语法更加冗长,异步加载方式比较混乱,容易造成回调地狱,已经很少使用了。
UMD:
是一组模块形式的集合,UMD一般先判断AMD环境,也就是检查全局环境下是否有define函数。而通过AMD定义的模式是不支持CommonJS和ESM的,使用webpack的时候可以更改下UMD的判断顺序。
加载npm模块:
与其它语言相比,js缺乏标准库。
当开发者需要解析URL,日期解析等常见问题的时候,只能自己封装,
npm包管理器为开发者带来便捷,npm 可以让开发者在其它平台上找到由他人开发和发布的库。
很多语言都有包管理器,比如 JAVA 的 maven , Ruby 的 gem。
目前JavaScript的有两个主流包管理器,npm 和 yarn 。
每一个npm模块都有一个入口。当我们加载一个模块时,实际上就是加载该模块的入口文件。这个入口被维护在模块内部 package.json 文件的 main 字段中。
当加载模块时,实际上加载的是 node_module/lodash/lodash.js
除了直接加载外,也可以通过 <package_name>/<path> 的形式单独加载模块内部的某个JS文件。
import all from "lodash/fp/all.js"
这样webpack在打包的时候,也只是打包这一个引入文件,不会打包全部的lodash库,可以减小打包资源的体积。
模块打包原理:
bundle在浏览器上运行:
①最外层匿名函数会初始化浏览器执行环境,包括定义 installedModules对象、_wepack_require_ 函数等,为模块加载和执行做准备工作。
②加载入口模块,每个bundle.js 都只有一个入口模块
③执行模块代码,如果执行到了 module.exports 则记录下模块导出值;如果遇到 require 函数(_webpack_require_),则会暂时交出执行权。进入 _webpack_require_ 内加载其它模块的逻辑。
④_webpack_require_ 内会判断即将加载的模块是或否存在于 installedModule 中。存在直接取值,否则回到第3步,执行该模块的代码获取导出值。
⑤所有依赖加载完毕,执行权回到入口模块。
第三步和第四部是一个递归的过程。webpack为每个模块创造了一个可以导出和导入的环境,本质上没有修改代码的执行逻辑,因此代码执行顺序和模块加载顺序是完全一致的,这就是webpack打包的奥秘。
小结:
CommonJS 和 ESModule 主要区别在于:
前者建立模块依赖关系是在运行时,后者是编译时。
导入方面,CommonJS 是值拷贝,ESModule导入的是只读的变量映射。
esm通过其静态特性可以进行编译过程中的优化,并且具备处理循环依赖的能力。