本文原载于SegmentFault专栏“前端进阶”
作者:Jerry谭金杰
整理编辑:SegmentFault
webpack马上要出5了,完全手写一个优化后的脚手架是不可或缺的技能。
本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本,所有代码均出自手写,亲自试验过可以运行达到优化效果。
杜绝5分钟的技术,先深入原理再写配置,大家一起学习参考。
实现需求:
-
识别
JSX
文件
-
tree shaking
摇树优化 删除掉无用代码
-
PWA
功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用
-
CSS
模块化,不怕命名冲突
-
小图片的
base64
处理
-
文件后缀省掉
jsx js json
等
-
实现React懒加载,按需加载 , 代码分割 并且支持服务端渲染
-
支持
less sass stylus
等预处理
-
code spliting
优化首屏加载时间 不让一个文件体积过大
-
提取公共代码,打包成一个
chunk
-
每个
chunk
有对应的
chunkhash
,每个文件有对应的
contenthash
,方便浏览器区别缓存
-
图片压缩
-
CSS
压缩
-
增加
CSS
前缀 兼容各种浏览器
-
对于各种不同文件打包输出指定文件夹下
-
缓存
babel
的编译结果,加快编译速度
-
每个入口文件,对应一个
chunk
,打包出来后对应一个文件 也是
code spliting
-
删除
HTML
文件的注释等无用内容
-
每次编译删除旧的打包代码
-
将
CSS
文件单独抽取出来
-
让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
-
等等....
-
webpack
中文官网的标语是 :让一切都变得简单
概念:
本质上,
webpack
是一个现代
JavaScript
应用程序的静态模块打包器(
module bundler
)。当
webpack
处理应用程序时,它会递归地构建一个依赖关系图(
dependency graph
),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个
bundle
。
webpack v4.0.0
开始,可以不用引入一个配置文件。然而,
webpack
仍然还是高度可配置的。在开始前你需要先理解四个核心概念:
-
入口(
entry
)
-
输出(
output
)
-
loader
-
插件(
plugins
)
本文旨在给出这些概念的高度概述,同时提供具体概念的详尽相关用例。
让我们一起来复习一下最基础的Webpack知识,如果你是高手,那么请直接忽略这些往下看吧!
-
入口起点`(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
-
每个依赖项随即被处理,最后输出到称之为
bundles
的文件中,我们将在下一章节详细讨论这个过程。
-
可以通过在
webpack
配置中配置
entry
属性,来指定一个入口起点(或多个入口起点)。默认值为
./src
。
-
接下来我们看一个
entry
配置的最简单例子:
webpack.config.js
module.exports = {
entry: './path/to/my/entry/file.js'
};
entry: {
app: ['./src/index.js', './src/index.html'],
vendor: ['react']
},
entry: ['./src/index.js', './src/index.html'],
webpack.config.js
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
在上面的示例中,我们通过
output.filename
和
output.path
属性,来告诉
webpack bundle
的名称,以及我们想要
bundle
生成(
emit
)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个
Node.js
核心
模块,用于操作文件路径。
-
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
-
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
-
注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能,其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是有很必要的,因为这可以使开发人员创建出更准确的依赖关系图。
-
在更高层面,在 webpack 的配置中 loader 有两个目标:
-
test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
-
use 属性,表示进行转换时,应该使用哪个 loader。
webpack.config.js
const path = require('path');
const config = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
}
};
module.exports = config;
-
以上配置中,对一个单独的
module
对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(
compiler
) 如下信息:
-
“嘿,
webpack
编译器,当你碰到「在
require()/import
语句中被解析为
'.txt'
的路径」时,在你对它打包之前,先使用
raw-loader
转换一下。”
-
重要的是要记得,在
webpack
配置中定义
loader
时,要定义在
module.rules
中,而不是 rules。然而,在定义错误时
webpack
会给出严重的警告。为了使你受益于此,如果没有按照正确方式去做,
webpack
会“给出严重的警告”
-
loader
还有更多我们尚未提到的具体配置属性。
-
这里引用这位作者的优质文章内容,手写一个
loader
和
plugin
高潮来了 ,
webpack
的编译原理 ,为什么要先学学习原理? 因为你起码得知道你写的是干什么的!
-
识别入口文件
-
通过逐层识别模块依赖。(
Commonjs、amd
或者es6的
import,webpack
都会对其进行分析。来获取代码的依赖)
-
webpack
做的就是分析代码。转换代码,编译代码,输出代码,最终形成打包后的代码。
-
这些都是
webpack
的一些基础知识,对于理解
webpack
的工作机制很有帮助。
-
loader
是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
-
处理一个文件可以使用多个
loader
,
loader
的执行顺序是和本身的顺序是相反的,即最后一个
loader
最先执行,第一个
loader
最后执行。
-
第一个执行的
loader
接收源文件内容作为参数,其他
loader
接收前一个执行的
loader
的返回值作为参数。最后执行的
loader
会返回此模块的
JavaScript
源码
-
在使用多个
loader
处理文件时,如果要修改
outputPath
输出目录,那么请在最上面的
loader中options设置
-
在
Webpack
运行的生命周期中会广播出许多事件,
Plugin
可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
-
plugin和loader
的区别是什么?
-
对于
loader
,它就是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
-
plugin
是一个扩展器,它丰富了
wepack
本身,针对是
loader
结束后,
webpack
打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听
webpack
打包过程中的某些节点,执行广泛的任务。
-
webpack
启动后,在读取配置的过程中会先执行
new MyPlugin(options)
初始化一个 MyPlugin 获得其实例。在初始化compiler 对象后,再调用
myPlugin.apply(compiler)
给插件实例传入
compiler
对象。插件实例在获取到
compiler
对象后,就可以通过
compiler.plugin
(事件名称, 回调函数) 监听到
Webpack
广播出来的事件。并且可以通过
compiler
对象去操作
webpack
-
看到这里可能会问
compiler
是啥,
compilation
又是啥?
-
Compiler
对象包含了 Webpack 环境所有的的配置信息,包含
options,loaders,plugins
这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为
Webpack
实例;
-
Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当
Webpack
以开发模式运行时,每当检测到一个文件变化,一次新的
Compilation
将被创建。
Compilation
对象也提供了很多事件回调供插件做扩展。通过
Compilation
也能读取到
Compiler
对象。
-
Compiler
和
Compilation
的区别在于:
-
Compiler
代表了整个
Webpack
从启动到关闭的生命周期,而
Compilation
只是代表了一次新的编译。
-
webpack
通过
Tapable
来组织这条复杂的生产线。
-
webpack
的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
-
webpack
的事件流机制应用了观察者模式,和
Node.js 中的 EventEmitter
非常相似。
下面正式开始开发环境的配置:
-
设置APP,几个入口文件,即会最终分割成几个
chunk
-
在入口中配置
vendor
,可以
code spliting
,将这些公共的复用代码最终抽取成一个
chunk
,单独打包出来
-
要想在开发模式中
HMTL
文件也热更新,需要加入·
index.html
为入口文件。
entry: {
app: ['./src/index.js', './src/index.html'],
vendor: ['react'] //这里还可以加入redux react-redux better-scroll等公共代码
},
output: {
filename: '[name].[contenthash:8].js',
path: resolve(__dirname, '../dist')
},
resolve: {
extensions: [".js", ".json", ".jsx"]
}
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
-
@babel/preset-react
解析
jsx语法
-
@babel/preset-env
解析
es6
语法
-
@babel/plugin-syntax-dynamic-import
解析
react-loadable
的
import
按需加载,附带
code spliting
功能
{
test: /\.(js|jsx)$/,
use:
{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false }]],
plugins: ["@babel/plugin-syntax-dynamic-import"]
},
cacheDirectory: true//开启babel编译缓存
}
},
const os = require('os')
{
loader: 'thread-loader',
options: {
workers: os.cpus().length
}
}
import React from 'react'
import loadable from 'react-loadable'
import Loading from '../loading'
const LoadableComponent = loadable({
loader: () => import('../Test/index.jsx'),
loading: Loading,
});
class Assets extends React.Component {
render() {
return (
<div>
<div>这即将按需加载div>
<LoadableComponent />
div>
)
}
}
export default Assets
{
test: /\.(html)$/,
loader: 'html-loader'
}
{
enforce:'pre',
test:/\.js$/,
exclude:/node_modules/,
include:resolve(__dirname,'/src/js'),
loader:'eslint-loader'
}
必须了解的
webpack
热更新原理 :
-
第一步,在
webpack 的 watch
模式下,文件系统中某一个文件发生修改,
webpack
监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的
JavaScript
对象保存在内存中。
-
第二步是
webpack-dev-server
和
webpack
之间的接口交互,而在这一步,主要是
dev-server
的中间件
webpack-dev-middleware 和 webpack
之间的交互,
webpack-dev-middleware
调用
webpack
暴露的 API对代码变化进行监控,并且告诉
webpack
,将代码打包到内存中。
-
第三步是
webpack-dev-server
对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了
devServer.watchContentBase
为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
-
第四步也是
webpack-dev-server
代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
-
webpack-dev-server/client
端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了
webpack,webpack/hot/dev-server
的工作就是根据
webpack-dev-server/client
传给它的信息以及
dev-server
的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
-
HotModuleReplacement.runtime
是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的
hash
值,它通过
JsonpMainTemplate.runtime
向 server 端发送 Ajax 请求,服务端返回一个
json
,该
json
包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
-
而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,
HotModulePlugin
将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
-
最后一步,当
HMR
失败后,回退到
live reload
操作,也就是进行浏览器刷新来获取最新打包代码。
正式开始生产环节:
-
加入
WorkboxPlugin
,
PWA
的插件
pwa
这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的
lavas
框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true,
importWorkboxFrom: 'local',
include: [/\.js$/