模块打包工具

存在的问题

  • ESM存在环境兼容问题
  • 模块文件过多,网络请求频繁
  • 所有的前端资源都需要模块化

处理的痛点

  • 新特性代码编译
  • 模块化JavaScript打包
  • 支持不同类型的资源模块

打包工具解决的是前端整体的模块化,并不单指JavaScript模块化

资源模块加载

webpack除了打包JS外,它还是整个前端工程的打包工具
webpack默认处理JS文件
处理其他类型文件的模块打包工作需要加载对应类型文件的加载器
内部的loader只能加载处理JS文件,对于其他文件添加对应loader
安装加载对应模块后还需在配置文件中对加载规则进行配置

  1. $ yarn add css-loader --dev
  1. const path = require('path')
  2. module.exports = {
  3. mode: 'none',
  4. entry: './src/main.css',
  5. output: {
  6. filename: 'bundle.js',
  7. path: path.join(__dirname, 'dist')
  8. },
  9. module: {
  10. // 加载规则配置
  11. rules: [
  12. {
  13. test: /.css$/,
  14. // 正则表达式匹配路径
  15. use: [
  16. 'style-loader',
  17. 'css-loader'
  18. ]
  19. }
  20. ]
  21. }
  22. }

到这一步仅仅是对CSS进行打包而已,还需要将打包的CSS引入页面中

  1. $ yarn add style-loader --dev

Loader是Webpack的核心特性
借助于Loader就可以加载任何类型的资源

导入资源模块

打包入口相当于运行入口
JavaScript驱动整个前端应用的业务
根据代码的需要动态导入资源
需要资源的不是应用,而是代码
逻辑合理,JS确实需要这些资源文件
确保上线资源不缺失,都是必要的

文件资源加载器

  1. $ yarn add file-loader --dev

webpack会把项目的根目录默认为网站的根目录

webpack在打包时遇到了图片文件,然后根据配置文件的配置,匹配到对应文件的文件加载器,文件加载器先将找到的文件拷贝到输出的目录,再将改输出目录的路径作为当前模块的返回值返回,如此所需要的资源就能发布出来,同时也可以通过模块的导出成员拿到这个资源的返回路径。

Data URLs 与 url-loader

Data URLs是一种特殊的url协议,他可以用来直接表示一个文件
传统的URL会要求服务器上有一个对应的文件,我们通过请求地址得到对应的文件
Data URLs则是直接表示文件内容的一种方式,意思是url的文本已经包含了文件内容,我们不会对该url发起请求
我们要用到如下的加载器

  1. $ yarn add url-loader --dev

小文件使用Data URLs,减少请求次数
大文件单独提取存放,提高加载速度

超出10KB文件单独提取存放
小于10KB文件转换为Data URLs嵌入代码中

常用加载器分类

编译转化类
文件操作类
代码检查类

与ES 2015的关系

因为模块打包需要,所以处理import和export,webpack本身对ES新特性没有支持并不能对其转换
如果我们需要webpack在打包过程中对js进行转换的话,我们需要为其配置相应的编译型loader,如babel-loader
我们还需安装babel的核心模块@babel/core ,以及@babel/preset-env 进行具体特性转换的集合

  1. $ yarn add babel-loader @babel/core @babel/preset-env --dev

webpack只是打包工具
加载器可以用来编译转换代码

模块加载方式

除了important可以触发模块的加载外,webpack还提供了其他几种方式
遵循 ES MOdules 标准的 import 声明
遵循 CommonJs 标准的 require 函数
遵循 AMD 标准的 define 函数和 require 函数
Loader 加载的非 JavaScript 也会触发资源加载
样式代码中的 @important 指令和 url 函数
HTML 代码中图片标签的 src 属性

核心工作原理

Loader 的工作原理

Loader负责资源文件从输入到输出的转换
对于同一个资源可以依次使用多个Loader

插件机制

增强Webpack自动化能力
Loader 专注实现资源模块加载
Plugin 解决其他自动化工作
e.g.
清除dist目录
拷贝静态文件至输出目录
压缩输出代码
webpack + Plugin 实现了大多前端工程化工作,以至于大多数菜鸡开发存在webpack = 前端工程化的错误认知

Webpack 常用插件

clean-webpack-plugin
自动清除输出目录插件

html-webpack-plugin
自动生成使用 bundle.js 的 HTML
通过Webpack输出HTML文件

  1. const path = require('path')
  2. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  3. const HtmlWebpackPlugin = require('html-webpack-plugin')
  4. module.exports = {
  5. mode: 'none',
  6. entry: './src/main.js',
  7. output: {
  8. filename: 'bundle.js',
  9. path: path.join(__dirname, 'dist'),
  10. // publicPath: 'dist/'
  11. },
  12. module: {
  13. rules: [
  14. {
  15. test: /.css$/,
  16. use: [
  17. 'style-loader',
  18. 'css-loader'
  19. ]
  20. },
  21. {
  22. test: /.png$/,
  23. use: {
  24. loader: 'url-loader',
  25. options: {
  26. limit: 10 * 1024 // 10 KB
  27. }
  28. }
  29. }
  30. ]
  31. },
  32. plugins: [
  33. new CleanWebpackPlugin(),
  34. // 用于生成 index.html
  35. new HtmlWebpackPlugin({
  36. title: 'Webpack Plugin Sample',
  37. // 标题设置
  38. meta: {
  39. viewport: 'width=device-width'
  40. // 元数据标题
  41. },
  42. template: './src/index.html'
  43. // 模板指定
  44. }),
  45. // 用于生成 about.html
  46. new HtmlWebpackPlugin({
  47. filename: 'about.html'
  48. })
  49. ]
  50. }

静态文件拷贝
copy-webpack-plugin

插件机制的工作原理

开发一个插件

实现移除注释的功能
相比于 Loader,Plugin 拥有更宽的能力范围
Plugin 插件是常见的软件开发中的钩子机制实现的
钩子机制有些类似于事件
插件必须是一个函数或者是一个包含apply方法的对象
我们一般会将其定义为一个类型,在内部定义apply方法,然后将其生成实例来使用。
插件是通过在生命周期的钩子中挂载函数实现拓展的

  1. class MyPlugin {
  2. apply (compiler) {
  3. console.log('MyPlugin 启动')
  4. compiler.hooks.emit.tap('MyPlugin', compilation => {
  5. // compilation => 可以理解为此次打包的上下文
  6. for (const name in compilation.assets) {
  7. // console.log(name)
  8. // 打印文件名称
  9. // console.log(compilation.assets[name].source())
  10. // 打印文件内容
  11. if (name.endsWith('.js')) {
  12. // 判断js结尾的文件
  13. const contents = compilation.assets[name].source()
  14. const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
  15. // 注释替换
  16. compilation.assets[name] = {
  17. source: () => withoutComments,
  18. size: () => withoutComments.length
  19. }
  20. }
  21. }
  22. })
  23. }
  24. }

开发体验问题

设想:理想的开发环境
以 HTTP Server 运行
自动编译和自动刷新
提供 Source Map 支持
如何增强 Webpack 开发体验

增强开发体验 实现自动编译

watch 工作模式

监听文件变化,自动重新打包

  1. $ yarn webpack --watch

执行后webpack会以监视模式去运行

自动刷新浏览器

希望编译过后自动刷新浏览器
BrowserSync

Webpack Dev Server

提供用于开发的 HTTP Server
集成「自动编译」和「自动刷新浏览器」等功能

  1. $ yarn add webpack--dev-server --dev
  1. yarn webpack-dev-server --open

打包结果暂时存放在内存当中,http sever从内存当中读取发送给浏览器

静态资源访问

Dev Server 默认只会 serve 打包输出文件
只要是 Webpack 输出的文件 都可以直接被访问
其他静态资源文件也需要 serve ,具体操作位在配置文件中添加对应的配置属性 devServer ,指定额外的静态资源路径

代理 API 服务

由于开发服务器的缘故,我们这里会将应用运行在localhost的一个端口号上面
而最终上线过后,我们的应用和我们的API会部署到同源地址下面
在实际生产环境当中,我们可以直接访问API,但是回到开发环境当中,就会遇到跨域请求问题

我们可以使用跨域资源共享(CORS)的方式解决
前提是API支持CORS
如果前后端都进行同源部署,域名、端口、协议一致,则没必要使用CORS

问题:开发阶段接口快鱼问题
解决方法:开发服务器当中配置代理服务,也就是把接口服务代理到本地服务地址
Webpack Dev Server 支持配置代理

目标:将 GitHub API 代理到开发服务器

Endpoint 可以理解为 接口端点/入口
devServer中的proxy 专门配置代理服务

  1. devServer: {
  2. contentBase: './public',
  3. proxy: {
  4. '/api': {
  5. // http://localhost:8080/api/users -> https://api.github.com/api/users
  6. target: 'https://api.github.com',
  7. // http://localhost:8080/api/users -> https://api.github.com/users
  8. pathRewrite: {
  9. '^/api': ''
  10. },
  11. // 不能使用 localhost:8080 作为请求 GitHub 的主机名
  12. changeOrigin: true
  13. }
  14. }
  15. },

主机名是 HTTP 协议中的相关概念

Source Map 解决了源代码与运行代码不一致所产生的问题

建议使用 cheap-module-eval-source-map

不同 devtool 之间的差异

Source Map 会暴露源代码

逼不得已用 nosources-spurce-map

理解不同模式的差异,适配不同的环境

问题核心:自动刷新导致的页面状态丢失
最好是页面不刷新的前提下,模块也可以即使更新

Webpack HMR 体验

Hot Module Replacement 模块热替换,模块热更新
热拔插:在一个正在运行的记起上随时插拔设备
模块热替换:应用程序过程中实施替换某个模块,应用运行状态不受影响
自动刷新到这页面状态丢失,热替换直降修改的模块实时替换纸应用中
HMR 是 Webpack 最强杀手锏,已经集成在了Webpack Server中

  1. $ yarn webpack-dev-server --hot

也可通过配置文件开启
Webpack 中的 HMR 需要手动处理模块热替换逻辑 通过 HMR 的 API

生产环境优化

开发体验提升的同时,开发环境变得臃肿。
生产环境和开发环境有很大的差异,生产环境注重运行效率,开发环境注重开发效率
webpack4提供了模块(node)
为不同的环境创建不同的配置

不同环境下的配置

  1. 配置文件根据环境不同导出不同配置
  2. 一个环境对应一个配置文件

多配置文件

利用webpack-merge合并多个配置

DefinePlugin

为代码注入全局成员

Tree-shaking

「摇掉」代码中未引用部分
未引用代码(dead-code)
自动监测未引用代码自动移除
Tree-Shaking 不是指某个配置选项,是一组功能搭配使用后的优化效果,生产模式(production)下自动开启
Tree-shaking的usedExports负责标记「枯树叶」只是为死代码做标记,真正移除(minimize)是通过压缩工具来进行的,使用terser-webpack-plugin
在Webpack4版本中,将mode设置为production也可以达到相同的效果
optimization集中配置webpack内部优化功能的

  1. module.exports = {
  2. mode: 'none',
  3. entry: './src/index.js',
  4. output: {
  5. filename: 'bundle.js'
  6. },
  7. optimization: {
  8. // 模块只导出被使用的成员
  9. usedExports: true,
  10. // 尽可能合并每一个模块到一个函数中
  11. concatenateModules: true,
  12. // 压缩输出结果
  13. // minimize: true
  14. }
  15. }

合并模块函数(Scope Hoisiting)

concatenateModules
尽可能把所有模块合并输出到一个函数当中,既提升了运行效率,又减少了代码的体积
该特效被称为 Scope Hoisiting 既作用域提升,是 webpack3 的特性

Tree-shaking & Babel

很多情况下,使用了Babel会导致Tree-shaking失效
Tree-shaking的前提是ES Modules,所以由 Webpack 打包的代码必须使用 ESM
为了转换代码中的 ECMAScript 新特性,我们通常会使用 babel-loader 来处理,这会导致 ESM 被转化为 CommonJS
如今最新版本的 babel-loader 帮我们自动关闭了 ESM 转换的插件

sideEffects副作用

副作用:模块执行时除了导出成员之外所做的事情
sideEffects 一般用于 npm 包标记是否有副作用
使用的前提是确保代码真的没有副作用,否则会误删除有副作用的代码
开启副作用监测是在webpack.js
标识副作用文件是在package.json

代码分包/代码分割 COde Splitting

所有代码最终都被打爆到一起
应用负责模块很多,最终会导致打包结果极大,应用启动工作时并不是每个模块在启动时都是必要的
这时就需要分包,按需加载

目前主流的协议HTTP1.1版本存在很多缺陷,它存在同域并行请求限制的问题,而且每次请求都会有一定的延迟,请求的 Header 浪费带宽流量

模块打包是必要的

  • 多入口打包
  • 动态导入

多入口打包

主要应用于多页应用程序
最常见的就是一个页面对应一个打包入口
对不同页面公共的部分再单独提取

提取公共模块

不同入口肯定会有公共模块

动态导入/按需加载

需要用到某个模块时,在加载这个模块
动态导入的模块会被自动分包

魔法注释

给分包bundle起名字

MiniCssExtractPlugin

从打包结果中提取 CSS 到单个文件,以此实现 CSS 模块的按需加载

OptimizeCssAssetsWebpackPlugin

压缩输出的 CSS 文件

输出文件名 Hash

生产模式下,文件名使用 Hash