模块打包工具
存在的问题
- ESM存在环境兼容问题
- 模块文件过多,网络请求频繁
- 所有的前端资源都需要模块化
处理的痛点
- 新特性代码编译
- 模块化JavaScript打包
- 支持不同类型的资源模块
打包工具解决的是前端整体的模块化,并不单指JavaScript模块化
资源模块加载
webpack除了打包JS外,它还是整个前端工程的打包工具
webpack默认处理JS文件
处理其他类型文件的模块打包工作需要加载对应类型文件的加载器
内部的loader只能加载处理JS文件,对于其他文件添加对应loader
安装加载对应模块后还需在配置文件中对加载规则进行配置
$ yarn add css-loader --dev
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
// 加载规则配置
rules: [
{
test: /.css$/,
// 正则表达式匹配路径
use: [
'style-loader',
'css-loader'
]
}
]
}
}
到这一步仅仅是对CSS进行打包而已,还需要将打包的CSS引入页面中
$ yarn add style-loader --dev
Loader是Webpack的核心特性
借助于Loader就可以加载任何类型的资源
导入资源模块
打包入口相当于运行入口
JavaScript驱动整个前端应用的业务
根据代码的需要动态导入资源
需要资源的不是应用,而是代码
逻辑合理,JS确实需要这些资源文件
确保上线资源不缺失,都是必要的
文件资源加载器
$ yarn add file-loader --dev
webpack会把项目的根目录默认为网站的根目录
webpack在打包时遇到了图片文件,然后根据配置文件的配置,匹配到对应文件的文件加载器,文件加载器先将找到的文件拷贝到输出的目录,再将改输出目录的路径作为当前模块的返回值返回,如此所需要的资源就能发布出来,同时也可以通过模块的导出成员拿到这个资源的返回路径。
Data URLs 与 url-loader
Data URLs是一种特殊的url协议,他可以用来直接表示一个文件
传统的URL会要求服务器上有一个对应的文件,我们通过请求地址得到对应的文件
Data URLs则是直接表示文件内容的一种方式,意思是url的文本已经包含了文件内容,我们不会对该url发起请求
我们要用到如下的加载器
$ 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 进行具体特性转换的集合
$ 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文件
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
// 标题设置
meta: {
viewport: 'width=device-width'
// 元数据标题
},
template: './src/index.html'
// 模板指定
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
}
静态文件拷贝
copy-webpack-plugin
插件机制的工作原理
开发一个插件
实现移除注释的功能
相比于 Loader,Plugin 拥有更宽的能力范围
Plugin 插件是常见的软件开发中的钩子机制实现的
钩子机制有些类似于事件
插件必须是一个函数或者是一个包含apply方法的对象
我们一般会将其定义为一个类型,在内部定义apply方法,然后将其生成实例来使用。
插件是通过在生命周期的钩子中挂载函数实现拓展的
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// console.log(name)
// 打印文件名称
// console.log(compilation.assets[name].source())
// 打印文件内容
if (name.endsWith('.js')) {
// 判断js结尾的文件
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
// 注释替换
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
开发体验问题
设想:理想的开发环境
以 HTTP Server 运行
自动编译和自动刷新
提供 Source Map 支持
如何增强 Webpack 开发体验
增强开发体验 实现自动编译
watch 工作模式
监听文件变化,自动重新打包
$ yarn webpack --watch
执行后webpack会以监视模式去运行
自动刷新浏览器
希望编译过后自动刷新浏览器
BrowserSync
Webpack Dev Server
提供用于开发的 HTTP Server
集成「自动编译」和「自动刷新浏览器」等功能
$ yarn add webpack--dev-server --dev
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 专门配置代理服务
devServer: {
contentBase: './public',
proxy: {
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
changeOrigin: true
}
}
},
主机名是 HTTP 协议中的相关概念
Source Map 解决了源代码与运行代码不一致所产生的问题
建议使用 cheap-module-eval-source-map
不同 devtool 之间的差异
Source Map 会暴露源代码
逼不得已用 nosources-spurce-map
理解不同模式的差异,适配不同的环境
问题核心:自动刷新导致的页面状态丢失
最好是页面不刷新的前提下,模块也可以即使更新
Webpack HMR 体验
Hot Module Replacement 模块热替换,模块热更新
热拔插:在一个正在运行的记起上随时插拔设备
模块热替换:应用程序过程中实施替换某个模块,应用运行状态不受影响
自动刷新到这页面状态丢失,热替换直降修改的模块实时替换纸应用中
HMR 是 Webpack 最强杀手锏,已经集成在了Webpack Server中
$ yarn webpack-dev-server --hot
也可通过配置文件开启
Webpack 中的 HMR 需要手动处理模块热替换逻辑 通过 HMR 的 API
生产环境优化
开发体验提升的同时,开发环境变得臃肿。
生产环境和开发环境有很大的差异,生产环境注重运行效率,开发环境注重开发效率
webpack4提供了模块(node)
为不同的环境创建不同的配置
不同环境下的配置
- 配置文件根据环境不同导出不同配置
- 一个环境对应一个配置文件
多配置文件
利用webpack-merge合并多个配置
DefinePlugin
为代码注入全局成员
Tree-shaking
「摇掉」代码中未引用部分
未引用代码(dead-code)
自动监测未引用代码自动移除
Tree-Shaking 不是指某个配置选项,是一组功能搭配使用后的优化效果,生产模式(production)下自动开启
Tree-shaking的usedExports负责标记「枯树叶」只是为死代码做标记,真正移除(minimize)是通过压缩工具来进行的,使用terser-webpack-plugin
在Webpack4版本中,将mode设置为production也可以达到相同的效果
optimization集中配置webpack内部优化功能的
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
合并模块函数(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