Webpack快速上手:
- yarn init --yes
- yarn add webpack webpack-cli --dev
//Webpack.config.js 配置文件
const path = require('path')
module.exports = {
// 这个属性有三种取值,分别是 production、development 和 none。
// 1. 生产模式下,Webpack 会自动优化打包结果;
// 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
// 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
mode: 'none',
// 入口路径(相对路径时./不能省)
entry: './src/main.js',
// 输出文件位置
output: {
// 输出文件名称
filename: 'bundle.js',
// 文件目录 => 必须是绝对路径
path: path.join(__dirname, 'output')
}
}
webpack打包运行原理:
以上目录结构,打包生成bundle.js,打包结果解析:
将打包结果折叠,打包结果是一个立即执行函数,接受一个modules的参数,立即执行函数传入的实参就是我们对应的两个模块,每个模块最终被包裹到一个函数中实现私有作用域。
结果内部代码分析:见之后文章,后续更新
Webpack核心工作原理:
- loader机制是webpack核心:webpack通过打包入口的js => 解析代码依赖模块 => 解析模块依赖 => 形成依赖树 => 递归依赖树 => 找到资源文件 => 根据webpack.config.js配置文件rules属性找到对应加载器 => 最后将加载结果输出到输出文件中
Webpack模块加载方式:兼容多种模块化标准
- import:遵循ES Module标准的import声明
- require:遵循CommonJS标准的require函数 => require也可以载入ES Module模块,载入默认导出,需要default属性。例:require('./heading.js').default
- define函数和require函数:遵循AMD标准
- loader加载的非JS也会触发资源加载:例:css中的@import指令和URl函数;html代码中的src属性
webpack资源模块加载:
webpack内部loader默认处理只处理js文件,loader是webpack的核心特性,借助于loader就可以加载任何类型的资源
- CSS模块加载器:css-loader 与 style-loader
- yarn add css-loader --dev => 转换css
- yarn add style-loader --dev => 把css-loader转换结果 通过Style标签追加到页面上
注:webpack.config.js配置文件如下:use的配置从右向左执行,先执行css-loader后style-loader
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
附:导入资源模块:webpack建议根据代码得需要动态导入资源,因为需要资源的不是应用,而是你所编写的代码,在js文件中,加载所有哦需要的资源文件,例如使用import加载css资源文件,文件入口为js文件。这样做的好处是逻辑合理,js需要资源文件,确保上线资源不缺失,都是必要的。
- webpack文件(图片)资源加载器:file-loader
- yarn add file-loader --dev
//Webpack.config.js 配置文件
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/' => 输出路径时/不能省略
},
module: {
rules: [{
test: /.png$/,
use: 'file-loader'
=> use配置:1.可写模块名称/路径(./markdown-loader.js)
2.也可为数组,从右向左执行
}
]
}
}
- webpack文件(图片)资源加载器:url-loader
- webpack Data Urls:特殊的url协议,直接表示一个文件,url中的文本已经包含文本内容,使用并不会发送http请求,内容转换为base64编码,Data Urls适合体积较小的资源文件,小文件使用Data Urls,减少请求次数;大文件单独提取存放,提高加载速度。
- 安装
- yarn add url-loader --dev
- yarn add file-loader --dev => 同时安装file-loader处理超过大文件,当文件大于10KB则会使用file-loader
//Webpack.config.js 配置文件
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
=> 10为字节,超出10KB单独存放,小于10KB文件转换为Data Urls嵌入代码
}
}
}
]
}
}
- Webpack处理ES2015:babel-loader
- webpack内部因为模块打包需要,所以可以处理import与export,但并不能处理ES6代码,需要babel-loader处理,babel也只是一个平台,转换ES6代码需要插件
- yarn add babel-loader @babel/core @babel/preset-env
- => babel/core:babel依赖的核心模块;preset-env:是转换具体特性的插件集合
//Webpack.config.js 配置文件
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
- Webpack识别html文件:html-loader
- yarn add html-loader --dev
//Webpack.config.js 配置文件
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
Webpack常用加载器分类:
- 加载器的作用:处理打包过程中所遇到的资源文件
- 编译转换类:将资源转换为浏览器可识别的js代码 => css-loader
- 文件操作类:拷贝到输出目录,导出文件访问路径即可 => file-loader
- 代码检查类:统一代码风格(不修改代码) => eslint-loader
Webpack中loader的工作原理:
输入到输出的转换:loader相当于一个管道,转换时可以使用多个loader来得到最终的转换结果,最终结果要求是js代码。
loader的导出:每个 Webpack 的 loader 都需要导出一个函数,函数是对加载的资源处理过程,函数的输入是加载到的资源,输出加工处理过后的结果,结果最终要求是js代码
实现自己开发的loader:
- 需求将md文件作为html文件输出
- 创建markdown-loader.js,以下为代码内容
- 第一种方式:安装mardown文件解析模块:yarn add marked --dev
- 第二种方式:返回html字符串交给下一个loader处理
- 安装 html - loader :yarn add html - loader --dev
//mark-loader.js
const marked = require('marked')
module.exports = source => {
// console.log(source)
const html = marked(source)
//第一种方式
// 直接返回html并不是js代码,需要对这个html进行处理,以下方式将html转换为js代码,但是直接拼接并不能对字符串中的换行符及引号进行处理
// return `module.exports = "${html}"`
// 对换行符及引号等进行处理,转换为json格式字符串;同样支持ES Module方式导出
// return `export default ${JSON.stringify(html)}`
// 返回 html 字符串交给下一个 loader 处理
return html
}
//Webpack.config.js 配置文件
test: /.md$/,
use: [
'html-loader',
'./markdown-loader'
]
Webpack 插件机制:
-
plugin增强webpack自动化能力,loader专注实现资源模块加载,plugin解决除资源加载外其他自动化工作,绝大多数插件模块导出的都是类。插件的工作例如:自动在打包前清除dist目录(上一次的打包结果);拷贝静态文件至输出目录(不需要参与打包的文件);压缩输出目录的代码文件;
-
自动清除输出目录插件安装:yarn add clean-webpack-plugin --dev
-
自动生成HTML插件安装:yarn add html-webpack-plugin --dev
-
对无需打包的文件进行拷贝:yarn add copy-webpack-plugin --dev
//Webpack.config.js 配置文件
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {...},
module: {
rules: [...]
},
plugins: [
new webpack.ProgressPlugin(),
// 清除打包目录
new CleanWebpackPlugin(),
// 用于生成 index.html => 对html插件进行配置
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
// 对无需打包的文件进行拷贝,开发阶段最好不要使用这个插件
new CopyWebpackPlugin([
// 'public/**'
'public'
])
]
}
Webpack 开发一个插件:
- 相比于loader,plugin拥有更宽的能力范围;plugin通过钩子机制实现,webpack要求钩子必须是一个函数或者是一个包含apply方法的对象,一般会把插件定义为一个类型,在类型中定义apply方法。开发一个MyPlugin插件,删除js文件生成的注释:
//Webpack.config.js 配置文件
class MyPlugin {
// webpack启动时自动调用apply方法
apply (compiler) {
// compiler 对象参数,此次对象的所有信息
console.log('MyPlugin 启动')
// emit 钩子:即将往输出目录输出文件
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文(所有打包过程产生的结果都会放到这个对象中)
for (const name in compilation.assets) {
// 获取资源文件信息
// console.log(name)
// console.log(compilation.assets[name].source())
// 判断是否为js文件
if (name.endsWith('.js')) {
// 获取js资源文件内容
const contents = compilation.assets[name].source()
// 进行注释替换
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
// 暴露两个方法:替换后的内容
source: () => withoutComments,
// 内容长度
size: () => withoutComments.length
}
}
}
})
}
}
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {...},
module: {
rules: [...]
},
plugins: [
new MyPlugin()
]
}
Webpack 自动化开发:
- Webpack Dev Server :提供用于开发的HTTP Server ,集成自动编译和自动刷新浏览器等功能
- 安装:yarn add webpack-dev-server --dev
- 运行:yarn webpack-dev-server
- 直接打开浏览器:yarn webpack-dev-server --open
//Webpack.config.js 配置文件
module.exports = {
...
devServer: {
// 指定静态文件资源目录
contentBase: './public',
// 代理 API 服务:将 GitHub API 代理到开发服务器
// proxy添加代理服务配置
proxy: {
// /api 请求路径前缀
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
// target 代理目标
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
// 代理路径重写
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名,以代理主机名请求
changeOrigin: true
}
}
},
...
}
Source Map : 映射源代码与转换后代码的关系,通过Source Map 逆向解析,定位报错代码位置
//Webpack.config.js 配置文件
module.exports = {
...
devtool: 'eval',
...
}
- webpack的devtool支持12种不同的方式对 Source Map ,每种方式效率和效果各不相同,以下是部分方式比较,详细比较链接:webpack.docschina.org/configurati…
| devtool | 备注 |
|---|---|
| none) | 生产模式建议使用 |
| eval | 将模块代码放eval中执行,通过 Source URl标注路径,只能定位错误文件;开发模式建议使用。 |
| eval-cheap-source-map | 只能定位行,无列,速度稍快 => 编译后,有loader加工 |
| eval-cheap-module-source-map | 只能定位行,无列,源代码 => 无loader加工 |
| eval-source-map | 同样使用eval函数执行模块代码,还可具体到行和列,生成source-map文件 |
| cheap-source-map | 无eval,loader 处理过(无 module) |
| inline-source-map | data Url方式的物理文件,将源代码放入代码中 |
| nosources-source-map | 看不到源代码,但可以看到行列信息 |
| hidden-source-map | 代码中无注释引入,但生成了Source Map |
Webpack Hot Module Replacement 模块热更新:无需刷新整个页面,可以刷新模块内容
- 开启HMR:
- 方式一:yarn webpack-dev-server --hot
- 方式二:
//Webpack.config.js 配置文件
const webpack = require('webpack')
module.exports = {
...
devServer: {
// hot: true => 正常配置
hotOnly: true
// 只使用 HMR,不会 fallback 到 live reloading;当页面报错不会自动刷新
},
...
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
- 运行 yarn webpack-dev-server --open
处理js模块热更新需要手动处理,css模块的css-loader已经处理了关于热更新的部分。
- HMR APIS => 处理js模块热替换,代码如下:
// main.js
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ 以下用于处理 HMR,与业务代码无关 ============
// console.log(createEditor)
// 判断是否开启HMR
if (module.hot) {
let lastEditor = editor
// js模块热更新处理
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
// 图片模块热替换
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
})
}
生产环境配置优化:模式mode,为不同环境创建不同配置
- 方式一:根据环境不同导出不同配置文件
//Webpack.config.js 配置文件
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// env:通过cli传递的环境名参数
// argv:运行cli过程中传递的所有参数
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false => 生产环境禁用source map
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
- 方式二:多配置文件:不同环境对应不同配置文件;2个不同环境配置文件,一个公共配置文件
- yarn add webpack-merge --dev => 合并配置所需插件
// Webpack.common.js 公共配置文件
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
// Webpack.dev.js 开发环境配置文件
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
// Webpack.prod.js 生产环境配置文件
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
// package.json 指定配置文件
"scripts": {
"build": "webpack --config webpack.prod.js"
},
为代码注入全局成员:
//Webpack.config.js 配置文件
const webpack = require('webpack')
module.exports = {
...
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段 API_BASE_URL变量在js中全局可使用
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
Tree - Shaking:
- [揺掉]代码中未引用的部分,Tree Shaking并不是指某个配置选项,是一组功能下搭配使用后的优化效果,生产模式下自动开启;其他模式下可以通过配置选项开启:
//Webpack.config.js 配置文件
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
},
optimization: {
// 标记npm包是否有副作用,生产模式自动开启
sideEffects: true,
// 模块只导出被使用的成员
usedExports: true, => userExports 负责标记未使用代码
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
minimize: true => minimize 负责清除标记后的代码
}
}
// package.json "sideEffects":false => 标识项目代码无副作用
// 但是函数的原型方法,css模块均有副作用,以下方式标识有副作用的文件
"sideEffects": [
"./src/extend.js",
"*.css"
]
Code Splitting 代码分包/代码分割:
- 应用复杂时,打包代码体积过大,影响js执行速度,但并不是每个模块启动时都是必要的,我们可以进行分包,按需加载。
- 方式一:多入口打包:适用于多页应用程序,一个页面对应一个打包入口,不同页面公共部分单独提取
//Webpack.config.js 配置文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
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']
})
]
}
- 方式二:动态导入:按需加载,需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包
// index.js文件
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
// /* webpackChunkName: 'components' */ 为分包的bundle起名字
import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
//Webpack.config.js 配置文件
output: {
filename: '[name].bundle.js'
},
提取css到单个文件:
- yarn add mini-css-extract-plugin --dev => css文件使用link标签引入而非style标签
- yarn add optimize-css-assets-webpack-plugin --dev => 压缩输出的css文件
- yarn add terser-webpack-plugin --dev => 压缩输出的js文件
//Webpack.config.js 配置文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
// 输出文件名配置
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
// 若配置在plugin中则任何时候都会工作,配置在optimization中,当开启minimizer时,才会工作(生产模式minimizer会自动开启),使用自定义压缩器,js不能自动压缩,使用TerserWebpackPlugin压缩js代码
optimization: {
minimizer: [
new TerserWebpackPlugin(), => 压缩js模块
new OptimizeCssAssetsWebpackPlugin() => 压缩输出的css文件
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
...
new MiniCssExtractPlugin({
// 输出文件名配置
filename: '[name]-[contenthash:8].bundle.css'
})
]
}