历史问题
- ES Modules 存在环境兼容问题
- 模块文件过多,网络请求频繁
- 所有的前端资源都需要模块化
毋庸置疑,模块化是必要的
提出设想
目标
- 新特性代码编译
- 模块化
javascript
打包 - 支持不同类型的资源模块
打包工具
打包工具解决的是前端 整体的模块化,并不单指 javascript
模块化
Webpack
模块打包器 (Module bundler)-
根据模块加载器(Loader)-- 有环境兼容性问题的代码就可以在打包过程中通过 Loader 进行编译转换
-
代码拆分(Code Splitting)-- 它能够将应用当中所有的代码都按照我们的需要去打包,就能避免打包到一起,产生的文件就会很大的问题
-
资源模块(Asset Module)-- 以模块化的方式去载入什么问题类型的资源文件
-
Webpack
1. Webpack 快速上手
注意 使用 webpack ^4.40.2, webpack-cli ^3.3.9
yarn add webpack webpack-cli
package.json
中增加"build": "webpack"
2. Webpack 配置文件
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
}
}
3. 工作模式
yarn webpack --mode none
- development
- production
- none - 原始状态的打包
- webpack.js.org/configurati…
4. Webpack 打包结果运行原理
5. 资源模块加载
// webpack.config.js
module: {
// 针对其它资源模块的加载规则的配置
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
] // 当是数组时,优先执行后面的
}
]
}
6. 导入资源模块
// 在main.js 当中
import './main.css'
Webpack
建议我们根据代码的需要动态导入资源
7. 文件资源加载器
file-loader
注意配置网站的根目录
8. URL 加载器
Data URLs
是一种当前url
就可以直接去表示文件内容的方式,这种url
当中的文本就已经包含了文件的内容,那我们在使用这种url
的时候就不会发送任何的http
请求
- 加载器
url-loader
- 小文件使用
Data URLs
,减少请求次数 - 大文件单独提取存放,提高加载速度
- 小文件使用
{
test:/\.png$/,
use: {
loader: 'url-laoder',
options: {
limit: 10 * 1024 // 10KB 只会对10KB 以下的文件进行转换
}
}
}
9. 常用加载器
- 编译转换类
- eg:
css-loader
- eg:
- 文件操作类
- eg:
file-loader
- eg:
- 代码检查类
- eg:
eslint-loader
- eg:
10. Webpack 与 ES 2015
- 安装好babel相关
yarn add babel-=loader @babel/core @babel/preset-env -D
// rules
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: [
presets: ["@babel/preset-env"]
]
}
}
Webpack 只是打包工具 加载器可以用来编译转换代码
11. Webpack 加载资源的方式
- 遵循
ES Modules
标准的import
声明 - 遵循
CommonJS
标准的require
函数 - 遵循
AMD
标准的define
函数和require
函数 - 样式代码中的
@import
指令和url
函数 HTML
代码中图片标签的src
属性
注意
- html中的 src 属性会触发打包,若其它属性引用文件也想触发,则需要如下配置
// rules
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
12. Webpack 核心工作原理
Loader
机制是Webpack
的核心
13. 开发一个 Loader
// my-markdown-loader.js
const marked = require('marked')
module.exports = source => {
const html = marked(source)
// 以下两种都可以
// A
return `module.exports = ${JSON.stringify(html)}`
return `module.exports = ${JSON.stringify(html)}`
// B 若使用以下情况,则还需要一个 html-loader 来继续处理结果
return html
}
// B的情况
// rules
{
test: /\.md$/,
use: [
'html-loader',
'./my-markdown-loader'
]
}
14. 插件机制
增强
Webpack
自动化能力
Loader
专注实现资源模块加载
Plugin
解决其他自动化工作
- eg: 清除
dist
目录 - eg: 拷贝静态文件至输出目录
- eg: 压缩输出代码
14.2 自动清除输出目录插件
- 使用
clean-webpack-plugin
插件
yarn add clean-webpack-plugin -D
使用:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// webpack.config.js
plugins: [
new CleanWebpackPlugin()
]
14.3 自动生成HTML插件
html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
// HtmlWebpackPlugin可以配置参数
// https://www.npmjs.com/package/html-webpack-plugin
new HtmlWebpackPlugin({
title: 'webpacl plugin sample',
meta: {
viewport: 'width-device-width'
},
template: './src/index.html'
})
]
// 会自动生成html到dist
- 最好的方式是在源代码当中配置模板
<!-- HtmlWebpackPlugin要配置template的路径 -->
<h1><%= htmlWebpackPlugin.options.title %></h1>
- 同时输出多个页面文件
plugins: [
new HtmlWebpackPlugin({
title: 'webpacl plugin sample',
meta: {
viewport: 'width-device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
14.4 静态文件使用复制
copy-webpack-plugin
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins: [
new CopyWebpackPlugin([
'public'
])
]
15. 开发一个插件
Plugin
通过钩子机制实现 www.webpackjs.com/api/compile…- 一个函数或者是一个包含
apply
方法的对象
class MyPlugin {
apply(compiler) {
// 找到 emit 钩子(生成资源到 output 目录之前
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (let name in compilation.assets) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
})
}
}
Webpack
处理开发场景
设想:理想的开发环境
- idea 1. 以
HTTP Server
运行 - idea 2. 自动编译 + 自动刷新
- idea 3. 提供
Source Map
支持
1. 自动编译
watch
工作模式,监听文件变化 ,自动重新打包yarn webpack --watch
2. 自动刷新浏览器
- 希望编译过后自动刷新浏览器
BrowserSync
操作上麻烦了,而且效率也变低了
3. Webpack Dev Server
- 提供用于开发的
HTTP Server
- 集成
自动编译
和自动刷新浏览器
等功能
- 集成
yarn add webpack-dev-server -D
# 打包结果并不会写入到磁盘当中, 存在内存当中
yarn webpack-dev-server
3.2 Dev Server 默认只会 serve
打包输出文件
- 只要是
Webpack
输出的文件,都可以直接被访问,如果其它静态资源就你说的也需要serve
// webpack.config.js
{
devServer: {
// 额外为开发服务器指定查找资源目录
contentBase: './public'
}
}
3.3 Dev Server 代理 API
同源部署没必要开启 CORS
,则不允许跨域
问题:开发阶段接口跨域问题
module.exports = {
devServer: {
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
}
}
}
}
4. Source Map (源代码地图)
运行代码与源代码之间完全不同,如果需要调试应用,错误信息无法定位,调试和报错都是基于运行代码
- 举例
jquery-3.4.1.min.js
- 如果想使用
Source Map
需要在文件末尾添加
//# sourceMappingURL-jquery-3.4.1.min.map
4.2 Webpac 配置 Source Map
Webpack
对Source Map
支持很多种,每种方式的效率和效果各不相同
// webpack.config.js
module.exports = {
devtool: 'source-map',
}
支持表 (也可以参考官网 webpack.js.org/configurati… )
eval 模式
eval('console.log(123) //# sourceURL=./foo/bar.js')
// 意味着我们可以通过 sourceURL改变我们通过 eval 执行的这段代码
devtool 模式对比
- eval - 是否使用
eval
执行模块代码 - cheap - Source Map 是否包含行信息
- module - 是否能够得到
Loader
处理之前的源代码 - 如何选择(建议)
- 开发模式 - cheap-module-eval-source-map
- 代码每行不会超过80个字符
- 代码经过
Loader
转换过后的差异较大 - 首次打包速度慢无所谓,重新打包相对较快
- 生产模式 - none
Source Map
会暴露源代码- 调试是开发阶段的事情
- 如果对代码没信心 - 建议
nosources-source-map
理解不同模式的差异,适配不同的环境和场景
- 开发模式 - cheap-module-eval-source-map
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
5. 自动刷新的问题
页面不刷新的前提下,模块也可以及时更新
5.2 HMR - 模块热替换 (Hot Module Replacement)
- 热拔插 - 在一个正在运行的机器上随时插拔设备
Webpack
中的热替换是在应用运行过程中实时替换某个模块,应用运行状态不受影响HMR
是Webpack
中最强大的功能之一 - 极大程度的提高了开发者的工作效率
5.3 开启 HMR
HMR
集成在webpack-dev-server
中- 运行
webpack-dev-server --hot
- 也可以通过配置文件开启
const webpack = require('webpack') plugins: [ new webpack.HotModuleReplacementPlugin() ] // 直接运行 // yarn webpack-dev-server
- 运行
Webpack
中的HMR
并不可以开箱即用- 需要手动处理模块热替换逻辑
为什么样式文件的热更新可以开箱即用?
因为样式文件是有 Loader
处理的 在 style-loader
当中就已经处理样式文件的热更新
样式文件简单,修改之后只需要替换到浏览器当中就行
通过脚手架创建的项目内部都集成了 HMR
方案
总结:我们需要手动处理 JS
模块更新后的热替换
5.4 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)
if (module.hot) {
let lastEditor = editor
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)
})
}
5.5 HMR 注意事项
- 处理
HMR
的代码报错会导致自动刷新 - 没启用
HMR
的情况下,HMR API
报错-
在devServer: { hot: true, hotOnly: true } 如果这两个没有开启,则
module.hot
不存在
-
Webpack
生产环境优化
- 开发环境注重开发效率
- 生产环境注重运行效率
为不同的工作环境创建不同的配置
1. 不同环境下的配置
- 配置文件根据环境不同导出不同的配置
- 一个环境对应一个配置文件
1.2 Webpack 的配置文件支持导出一个函数
module.exports = (env, argv) => {
if (env == 'production') {
// ...
}
}
2. 不同环境使用配置文件
- webpack.common.js
- webpack.dev.js
- webpack.prod.js
配置的时候需要与 common
里面的信息做merge,社区提供了更好的merge工具 webpack-merge
yarn webpack --config webpack.prod.js
3. 优化配置
3.1 DefinePlugin
为代码注入全局成员
- 在 production 环境下,默认会启动起来并注入一个
process.env.NODE_ENV
常量
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
// 如果是 'https://api.example.com'
// 在使用的地方 会这样 console.log(API_BASE_URL) ==> console.log(https://api.example.com)
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
3.2 Tree Snaking (摇树)
"摇掉"代码中未引用部分(未引用代码 - dead-code)
-
会在生产模式下自动开启
-
Tree Shaking
不是指某个配置选项, 是一组功能搭配使用后的优化效果 -
optimization
集中配置Webpack
内部的优化功能的{ optimization: { // 模块只导出被使用的成员 usedExports: true, // 压缩输出结果 // minimize: true } } // 使用 usedExports: true 和 minimize: true 就类似于 Tree Shaking的功能 // usedExports 负责标记 "枯树叶 " // minimize 负责 "摇掉" 它们
- 合并模块函数
Scope Hoisting
- 尽可能的将所有模块合并输出到一个函数中 - 既提升了运行效率,又减少了代码的体积 (这个特性又被称之为 `Scope Hoisting - 作用域提升)
optimization: { // 模块只导出被使用的成员 usedExports: true, // 尽可能合并每一个模块到一个函数中 concatenateModules: true, }
- 合并模块函数
3.3 Tree Shaking 与 Babel
前景:很多资料查阅出来显示当我们使用了 babel
Tree Shaking
就会失效,这里我们统一来说明一下。
Tree Shaking
使用前提必须ES Modules
- 由
Webpack
打包的代码必须使用ESM
Webpack
是打包模块之前根据配置将模块交给不同的Loader
处理,最后再将处理结果打包到一起- 那么为了转换代码中的
ESMAScript
特性,我们使用babel-loader
来处理 - 而
babel-loader
在处理代码时就有可能将ES Modules
转换成CommonJS
- 那么为了转换代码中的
- 由
- 探索过程
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
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: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
4. sideEffects 副作用
其实 sideEffects 和 Tree Shaking 没啥关系
- 副作用:模块执行时除了导出成员之外所作的事情
sideEffects
一般用于npm
包标记是否有副作用
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
sideEffects: true
// 开启后 webpack 在打包时会检查当前代码所属的这个package.json当中有没有
// sideEffects 标识,以此来判断这个是否有副作用,
// 那如果说没有 副作用,那这些没有用到的模块就不会再打包
}
}
// package.json
{
"sideEffects": false // 我们当前这个package.json所影响的这个项目它当中所有的代码都没有副作用 - 没有副作用,就会被移除掉
}
4.2 sideEffects 注意
确保你的代码真的没有副作用
- 举例
// 为 Number 的原型添加一个扩展方法
// extend.js
Number.prototype.pad = function(size) {
// 将数字转为字符串 => '8'
let result = this + ''
// 在数字前补指定个数的0 => '008'
while(result.length < size) {
result = '0' + result
}
return result
}
// 以上就是一段副作用代码
// 在 main.js当中
import 'extend.js'
console.log((8).pad(3))
如果使用方式如上:在将副作用配置当中设置为没有副作用,则在打包后 extend.js 当中的代码不会被打包进去
比如:全局的css模块,就属于副作用,若设置为没有副作用,则打包也会忽略它
所以,在配置时可以告诉webpack哪些是有副作用的
// package.json
{
// "sideEffects": false
"sideEffects": [
"./src/extend.js",
"./src/global.css"
]
}
5. Code Splitting (代码分包 / 代码分割)
背景:因为所有代码最终都被打包到一起, bundle体积过大
- 并不是每个模块在启动时都是必要的
- 分包,按需加载
现在主流的HTTP1.1的版本本身就有很多的缺陷 - eg: 同域并行请求限制
不打包问题,资源文件太多:
- 每次请求都会有一定的延迟
- 请求的
Header
浪费带宽流量
所以模块打包肯定是有必要的
webpack 实现分包的方式主要有2种
- 多入口打包
- 动态导入
5.2 多入口打包 (Multi Entry)
- 多页应用程序 - 一个页面对应一个打包入口
// webpack.config.js
entry: {
index: "./src/index.js",
album: "./src/album.js"
},
output: {
filename: "[name].bundle.js"
},
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
// 配置输出的html引用的bundle, 可以使用 chunks
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
5.3提取公共模块
{
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
5.4 动态导入
- 按需加载 - 指需要用到某个模块时,再加载这个模块
- 动态导入的模块会被自动分包
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
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)
5.5 魔法注释 (Magic Comments)
/* webpackChunkName: '(名称)' */
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
6. MiniCssExtractPlugin
提取 css 到单个文件,通过这个插件可以实现CSS模块的按需加载
- 如果样式体积不是很大,提成单个文件可能会适得其反
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
7. OptimizeCssAssetsWebpackPlugin
压缩输出的 CSS 文件
Webpack
内置的压缩是针对js
文件来说- 对于
CSS
使用optimize-css-assets-webpack-plugin
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
plugins: [
new OptimizeCssAssetsWebpackPlugin()
]
注意: Webpack
建议像这种压缩类的插件应该配置 在 数组当中,以便于miner 这个选项去统一控制
optimization: {
minimizer: [ // webpack 认为,只要配置了这个数组,就认为我们要自定义配置压缩插件
new OptimizeCssAssetsWebpackPlugin() // 只配置这个 原本正常压缩的 js 文件又不压缩了
]
}
// 需要将内置的js插件设置回来
// yarn add terser-webpack-plugin -D
const TerserWebpackPlugin = require('terser-webpack-plugin')
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
}
8. 输出文件名 Hash (substitutions)
生产模式下,文件名使用
Hash
- 使用方式:
filename: '[name].[hash].bundle.js'
hash
: 每次构建会生成一个hash
。和整个项目有关,只要有项目文件更改,就会改变hash
。chunkhash
:和webpack
打包生成的chunk
相关。每一个entry
,都会有不同的hash
。contenthash
:和单个文件的内容相关。指定文件的内容发生改变,就会改变hash
。
hash可以指定长度 '[contenthash:8]'
如果是控制缓存的话,个人认为 contenthash:8 是最好的选择了