本文主要从五个维度进行解读webpack,包括webpack的基础配置、Loader、Plugin、开发体验、生产优化
本文是用的webpack版本为 4.44.2
webpack主要解决的问题
- 对于新特性代码的编译
- 模块化JavaScript打包
- 不同资源类型的打包(js,css,html等)
基础配置
快速上手
- 安装webpack模块
yarn add webpack webpack-cli --dev - 创建webpack.config.js 作为webpack 的配置文件
- 文件配置完成执行
yarn webpack会根据配置将src/index.js转成dist/build.js配置文件(webpack.config.js 是node环境中运行的js代码)
const path = require('path')
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js' // 输出文件位置
path: path.join(__dirname, 'dist') // 得到完整的绝对路径
}
}
导入资源模块
打包入口从某种成都上来说是我们应用的入口,就目前而言,JavaScript驱动整个前端业务,通常我们还是会使用js作为打包入口,然后在js代码中通过import的方式引入css模块
在js中加载资源文件的优势
- 逻辑合理,js确实需要这些资源文件
- 确保上线资源不确实,而且每一个上线的文件都是必要的资源
打包结果的运行原理
为了更好的看到效果,在webpack.config.js文件上设置mode为none,之后执行
yarn webpack去观察bundle.js
- 打包后文件里面是一个立即执行的函数,此函数为webpack的工作入口,接收一个modules参数
- 函数调用时传入的是一个数组,数组中每个函数对应源代码中的模块,也就是说我们每个模块最终都会包含到这个函数中,从而实现模块的私有作用域
- 模块的工作入口函数,先定义了一个
installedModules对象,用来缓存加载过的模块,__webpack_require__函数用来加载模块。__webpack_require__函数上面挂载了一些数据和工具函数,函数执行return __webpack_require__(_webpack_require__.s = 0),加载输入模块并导出 - 解读
__webpack_require__函数- 进入函数内部发现,首先判断传入的模块是否被加载,如果有,就从缓存里面读(
return installedModules[moduleId].exports), 如果没有,创建一个新的对象
var module = installedModules[moduleId] = { i: moduleId, l: false, // 表示模块是否被加载 exports: {} }- 创建完成后调用模块所对应的函数,把我们刚创建的模块对象,导出成员还有
__webpack_require__函数传进去,这样在模块内部就可以用module.export导出成员,__webpack_require__去载入模块modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - module.l = true; 标记这个模块已经被加载
- return module.exports 返回模块的导出
- 进入函数内部发现,首先判断传入的模块是否被加载,如果有,就从缓存里面读(
- 解读模块内部函数
- 模块内部先用调用
__webpack_require__.r(__webpack_exports__)用来在导出对象上加标记,在导出对象上定义了__esModule的标记,用来对外界表明该模块是个ES Module - 再往下调用模块内部方法,如果模块有对其他模块的引用,会执行__webpack_require__(其他模块的index),加载其他模块后继续向下执行,执行完毕后回到
__webpack_require__函数内部,标记模块被加载,返回模块导出 - module.exports应该是一个对象,ES Module 里面默认导出放在default上面
- 模块内部先用调用
- webpack打包后的结果只是帮我们把所有模块放在同一文件中,并提供一些基础代码,让模块之间的依赖关系维持原有状态
资源模块加载——Loader
webpack内部loader只能去处理js文件,想处理其他文件,要去载入对应的资源loader
Loader是实现前端模块化的核心,通过不同的Loader就可以实现加载任何类型的资源
loader的配置具体就是在webpack.config.js的module.exports对象中配置module的rules:[]
css-loader & style-loader
- 载入css-loader
yarn add css-loader --dev - css-loader的作用就是将css文件转换成js模块
- style-loader 把css-loader 转换的代码,通过style的方式追加到页面上
- 载入style-loader
yarn add style-loader --dev
// webpack.config.js 对应的配置
module.export = {
module:{
// 用来配置模块的加载规则
rules: [
{
test: /.css$/, // 参数接收一个正则,匹配对应模块的加载文件
use: ['style-loader','css-loader'] // 所使用的的loader,配置成数组的时候执行顺序是从后往前执行
}
]
}
}
文件资源加载器 file-loader & url-loader
- file-loader 生成资源文件,输出文件到指定目录,并返回public url
- url-loader 类似于file-loader,但是在文件大小(单位byte)低于指定的限制时,可以返回一个DataUrl
- 小文件使用url-loader,减少请求的次数
- 大文件单独提取存放,提高加载速度
rules:[
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 代表10kb
}
}
}
]
以上的配置,超过10kb的文件会单独提取存放,小于10kb文件转换为Data URLs 嵌入代码中
这种方式使用url-loader 还是要去载入file-loader 因为超出10kb的时候,url-loader 还是会调用file-loader
babel-loader
因为模块打包的需要,所以webpack处理了import和export,除此之外不能转换代码中其他的ES6特性,如需转换,需要为js代码配置额外的编译型loader,如babel-loader
yarn add babel-loader @babel/core @babel/preset-env --dev
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
html-loader
webpack加载资源的几种方式
- 遵循ES Modules 标准的import 声明
- 遵循 CommonJS 标准的require函数
- 遵循 AMD 标准的 define 函数 和 require 函数
- 样式代码中的@import 和 url
- html代码中的图片标签src
ruels:[
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src','a:href'] // 默认html-loader只能处理img中的src,对于a中的herf需要额外的配置
}
}
}
]
常用加载器分类
加载器:主要是处理打包中的资源文件
- 编译转换类:
- 把加载的资源模块,转换成JavaScript代码,例如css-loader
- 文件操作类:
- 把加载的资源模块,拷贝到输出目录,同事将文件的访问路径向外导出,例如file-loader
- 代码检查类:
- 对加载到的资源文件代码进行校验的加载器,目的是为了统一代码风格,从而提高代码质量
webpack核心工作原理
- 找到入口文件,例如main.js,根据里面的模块引入,找到对应的依赖模块,形成对应关系树
- 之后去递归依赖树,找到每个节点对应的资源文件
- 根据每个资源文件对应的rules属性,找到对应模块的加载器,交给加载器加载
- 加载后的结果会放到bundle.js中,从而实现整个项目的打包
Loader资源加载机制是webpack的核心
Loader工作原理
- Loader负责资源文件从输入到输出的转换,Loader 是一种管道的概念,对于同一个资源可以依次使用多个Loader进行处理
- loader 中代码实现内部是一个函数,可以通过参数获得传入的source,并且函数需要返回一段JavaScript代码
module.exports = source =>{
console.log(source)
return 'console.log("hello~")'
}
Plugin
Plugin 解决除了资源加载以外的其他自动化工作,例如清除dist目录,拷贝静态文件到输出目录,压缩输出代码等
CleanWebpackPlugin 自动清除输出目录的插件
- 安装插件
yarn add clean-webpack-plugin --dev - 回到webpack中导入插件并结构成员
const { CleanWebpackPlugin } = require('clean-webpack-plugin') - 在webpack中通过plugins属性进行配置
// webpack.config.js
module.export = {
plugins:[
new CleanWebpackPlugin()
]
}
HtmlWebpackPlugin 自动生成使用bundle.js的HTML
- 安装模块
yarn add html-webpack-plugin --dev - 回到webpack导出插件
const HtmlWebpackPlugin = require('html-webpack-plugin') - 配置plugins 进行配置
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample', // 配置html的title
meta: { // 以对象的方式设置页面中的源数据标签
viewport: 'width=device-width'
},
template: './src/index.html'
})
]
- 自定义生成的html
- 增加简单的参数,可以给HtmlWebpackPlugin传入对象参数,可对生成的html进行配置
- 有大量自定义,最好在源代码中添加一个生成html的模板,让插件根据模板生成页面,通过template属性去指定输出模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Webpack</title> </head> <body> <div class="container"> <h1><%= htmlWebpackPlugin.options.title %></h1> </div> </body> </html> - 同时输出多个页面文件
plugins: [
// 用于生成index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample', // 配置html的title
meta: { // 以对象的方式设置页面中的源数据标签
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成about.html
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
CopyWebpackPlugin 复制静态文件
- 安装模块
yarn add copy-webpack-plugin - 回到webpack导出插件
const CopyWebpackPlugin = require('copy-webpack-plugin') - 配置plugins 进行配置
plugins: [
new CopyWebpackPlugin([
// 'public/**'
'public'
])
]
Plugin 工作原理
- Plugin通过钩子机制实现
- Webpack要求Plugin 必须是一个函数或者是一个包含apply方法的对象
- apply方法会在任务启动时自动调用,接收一个compiler参数,通过这个对象去注册钩子函数
开发体验
- 使用http服务区运行
- 自动编译 + 即时显示
- 提供Source Map 支持
Webpack Dev Server
提供用于开发的HTTP Server,集成「自动编译」和「自动刷新浏览器」等功能
- 安装
yarn add webpack-dev-server - 安装包内部提供了一个叫 webpack-dev-server 的cli,直接运行
yarn webpack-dev-server就能启动项目并监听,一旦文件变化,项目会重新打包后浏览器刷新(webpack为了提高效率,并没有将打包结果写入到磁盘中,而是暂存在内存中,减少了不必要的读写操作) - 在devServer的时候访问静态资源路径
devServer: {
contentBase: './public' // 为devServer指定静态资源目录,也可以接收一个数组
}
- 代理API服务 运行在通过浏览器直连后端服务器的接口,会产生跨域问题,可以通过浏览器访问开发服务器,再由开发服务器做代理去请求后端服务器接口
devServer: {
proxy: {
'/api':{
target: 'http:xx.xx.xx',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
}
}
Source Map
主要解决前端引入构建编译后,前端编写的源代码与运行代码不一致所产生的调试问题
配置source map
module.exports = {
// 配置开发过程中的辅助工具
devtool : 'source-map'
}
webpack 支持12种不同的配置方式,每种方式的效果和效率各不相同
source map 不同模式的实现
- eval模式
- 将模块代码放到eval函数里面去执行,在代码后面通过注释
//# sourceURL=webpack:///./src/main.js的方式,去指定源代码的路径,这种情况不会生成source map文件 - 所以构建速度是最快的,但是只能知道源代码的路径,不知道行列信息。
- 将模块代码放到eval函数里面去执行,在代码后面通过注释
- eval-source-map
- 相比eval 生成了source map
- cheap-eval-source-map
- 生成了简易版的source map,只能定位到行,不能定位到列的信息
- 显示的是经过es6转换后的结果
- cheap-module-eval-source-map
- 解析出来的代码不会经过loader加工,与源代码相同
总结
- eval- 是否使用eval执行模块代码
- cheap - Source Map 是否包含行信息
- module - 是否能够得到Loader 处理之前的源代码
如何选择
- 开发环境中: cheap-module-eval-source-map
- 一般每行代码不会太多
- 代码经过Loader转换后的差异比较大
- 首次打包速度慢无所谓,重写打包相对较快
- 生产环境打包: none
- Source Map 会暴露源代码
- 生产环境也可以使用: nosources-source-map 出现问题会定位到行数,但是不会显示源代码。方便在生产上定位问题
Webpack -- Hot Module Replacement 热更新
页面不刷新的前提下,模块也可以及时更新, HMR已经集成在devServer中,只需要在运行
webpack-dev-server --hot加上 --hot参数开启,也可以通过配置文件开启
// 配置完成后还要载入一个webpack内置的插件
devServer:{
hot: true
},
module:{
plugins:[
new webpack.HotModuleReplacementPlugin()
]
}
HMR 的疑问
webpack中的HMR并不可以开箱即用,还需要手动处理模块热更新逻辑
Q1: 样式文件为什么可以直接的去热更新
- 样式文件是经过loader处理的,在style-loader中自动处理了文件热更新,就不需要我们在去手动处理了
Q2:为什么样式可以自动处理?
- 样式文件编写后,loader 通过及时的替换就能实现热更新,而js模块更新是无规律的,可以没办法实现通用所有模块的热更新
Q3: 为什么项目中没有手动处理js,还是可以热更新js
- 项目框架中的js是有规律的,框架实现了通用的替换办法,通过脚手架创建的项目内部都集成了HMR方案
总结:我们需要手动处理js模块更新后的热替换
HMR APIs
HotModuleReplacementPlugin提供了一些API,可以让我们在引入项目模块的js文件通过API去处理需要热更新的代码
//main.js
module.hot.accept('./editor',()=>{
console.log('editor模块更新了,这里需要手动处理热更新')
})
生产优化
不同环境下的配置
- 配置文件根据环境不同导出不同配置
- webpack文件中还支持导出一个函数,在函数中返回我们所需要的配置对象。函数接收两个参数
- env: 在cli中传递的环境名参数
- argv: 运行cli传递的所有参数
- 配置文件
webpack.config.js
module.exports = (env,argv) =>{
const config = {
// 开发环境的配置
}
if(env === 'production'){
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
- 不同环境对应不同配置文件
- 新建一个webpack.common.js 去抽象相同的配置
- 分别创建webpack.dev.js 和 webpack.prod.js 去对不同环境做配置
- 当合并webpack.common.js 和环境配置文件的时候需要注意
- Object.assign() 方法会覆盖plugins
- 社区中提供了
webpack-merge的模块,来帮助我们合并文件 yarn add webpack-merge --dev安装,载入const merge = require('webpack-merge')- 配置文件webpack.prod.js
const merge = require('webpack-merge') const common = require('./webpack.common') module.exports = merge(common,{ mode: 'production', ... // 其他配置 }) - 运行的时候通过配置参数去指定所使用的配置文件
yarn webpack --config webpack.prod.js
DefinePlugin (为代码注入全局成员)
webpack4中,新增的production模式下面,内部开启了很多通用的优化功能
- 在production 模式开启DefinePlugin,并注入常量
process.env.NODE_ENV,很多第三方通过这个成员去判断运行环境 - 想自己去注入成员,可以导入此插件并进行配置
- DefinePlugin 其实就是把我们注入成员的值,直接替换在代码中
const webpack = require('webpack')
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: "'https://api.example.com'" // 可以传入一段代码片段
})
]
Tree-shaking(去除未引用代码)
Tree Shaking 不是指某个配置选项,而是一组功能搭配使用后的优化效果,这组功能会在生产模式下自动启用
- 下面演示在none模式下如何开启Tree Shaking优化
// webpack.config.js
module.exports = {
// 集中配置webpack内部优化功能
optimization: {
usedExports: true // 表示在输出结果中只导出外部使用了的成员
minimize: true // 开启压缩,此时未引用的代码都被移除掉了
// 使用下面的属性继续优化我们的输出,下面配置的作用就是尽可能将所有模块合并输出到一个函数中,既提升了运行效率又提升了输出代码的体积,这个优化又称为Scope Hoisting(作用域提升)
concatenateModules: true // 开启后的压缩就不是一个模块对应一个函数了,而是把所有的模块放在同一个函数中
}
}
- usedExports 负责标记「枯树叶」
- minimize 负责「摇掉」它们
Tree-shaking & Babel
对于很多资料显示 使用babel-loader,tree-shaking 会失效的说明
-
tree-shaking前提是必须使用ES Module组织我们的代码,交给webpack 打包的代码必须使用ESM,
-
开发中为了转换代码中ESMAScript新特性,通常会用babel-loader去处理js,babel转换代码时很有可能 将ES Modules转化成 CommonJS,
-
但是在最新版本的babel-loader中,自动关闭了ES Module 转化的插件,可以在babel-loader的源码 injectCaller.js中查看到的 supportsStaticESM: true, 表示的意思是我们当前环境支持ESM,在preset-env转换的源码中,根据配置的参数,自动禁用了ES Modules 的转换
通过配置的方式配置转换
options:[
preset: [
['@babel/preset-env', { modules: 'commonjs' }] // 转化为commonjs
]
]
sideEffects(副作用)
webpack4中新增的新特性,允许我们通过配置的方式标识代码是否有副作用,从而为tree-shaking提供更大的压缩空间
- 副作用:除了模块导出成员外所做是事情,一般用于npm包标记是否有副作用
Code Splitting 代码分包/代码分割
webpack 的弊端:
- 项目中所有代码最终都会被打包到一起,应用复杂时体积过大。
- 并不是每个模块在启动时都要被加载进来 解决方式:分包,按需加载
多入口打包
适用于传统的多页面应用,一个页面对应一个打包入口,公共部分单独提取
- 把entry 定义为一个对象
- output动态输出文件名
module.exports = {
entry: {
index: './src/index.js'
album: './src/album.js'
},
output: {
filename: '[name].build.js'
},
plugins: [
new HTMLWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HTMLWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album'] //为html指定他所使用的bundler
})
]
}
提取公共模块 不同的入口文件中肯定会有公共模块,
// 只需要配置优化属性即可
optimization:{
splitChunks: {
chunks: 'all' //把所有的chunk中的公共模块都提取出来
}
}
动态导入
在应用过程中用到某个模块时,再加载这个模块,动态导入的模块会被自动分包
import(/*这里面是魔法注释,给生成的bundle加上名字 webpackChunkName: 'posts'*/'./posts/posts').then(({default: post()})=>{
mainElement.appendChild(posts())
})
魔法注释中,相同的chunkName 就会被打包到一起
MiniCssExtractPlugin(css模块的按需加载)
提取CSS到单个文件,通过这个插件可以实现css模块的按需加载
yarn add mini-css-extract-plugin --dev
OptimizeCssAssetsWebpackPlugin(压缩css)
用来压缩css文件