webpack
马上要出 5 了,完全手写一个优化后的脚手架是不可或缺的技能。
- 本文书写时间
2019 年 5 月 9 日
,webpack
版本4.30.0
最新版本 - 要转载必须联系本人经过同意才可转载 谢谢!
- 杜绝
5 分钟
的技术,我们先深入原理再写配置,那会简单很多。 -
实现需求:
- 识别
JSX
文件 -
tree shaking
摇树优化 删除掉无用代码 -
PWA
功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用 -
CSS
模块化,不怕命名冲突 - 小图片的
base64
处理 - 文件后缀省掉
jsx js json
等 - 实现懒加载,按需加载,代码分割
- 支持
less sass stylus
等预处理 -
code spliting
优化首屏加载时间 不让一个文件体积过大 - 提取公共代码,打包成一个
chunk
- 每个
chunk
有对应的chunkhash
, 每个文件有对应的contenthash
, 方便浏览器缓存 - 图片压缩
-
CSS
压缩 - 增加
CSS
前缀 兼容各种浏览器 - 对于各种不同文件打包输出指定文件夹下
- 缓存
babel
的编译结果,加快编译速度 - 每个入口文件,对应一个
chunk
,打包出来后对应一个文件 也是code spliting
- 删除
HTML
文件的注释等无用内容
- 识别
-
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'],
- 有人可能会说,入口怎么放
HTML
文件,因为开发模式下热更新如果不设置入口为HTML
,那么更改了HTML
文件内容,是不会刷新页面的,需要手动刷新,所以这里给了入口HTML
文件,一个细节。
-
出口(output)
- output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段,来配置这些处理过程:
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
-
- 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
手写一个 loader 和 plugin
高潮来了,
webpack
的编译原理,为什么要先学学习原理?因为你起码得知道你写的是干什么的!
-
webpack
打包原理- 识别入口文件
- 通过逐层识别模块依赖。(
Commonjs、amd
或者 es6 的import,webpack
都会对其进行分析。来获取代码的依赖) -
webpack
做的就是分析代码。转换代码,编译代码,输出代码 - 最终形成打包后的代码
- 这些都是
webpack
的一些基础知识,对于理解webpack
的工作机制很有帮助。
-
什么是
loader
?-
loader
是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中 - 处理一个文件可以使用多个
loader
,loader
的执行顺序是和本身的顺序是相反的,即最后一个loader
最先执行,第一个loader
最后执行。 - 第一个执行的
loader
接收源文件内容作为参数,其他loader
接收前一个执行的loader
的返回值作为参数。最后执行的loader
会返回此模块的JavaScript
源码 - 在使用多个
loader
处理文件时,如果要修改outputPath
输出目录,那么请在最上面的loader 中 options 设置
-
-
什么是
plugin?
-
- 在
Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
- 在
-
plugin 和 loader
的区别是什么? - 对于
loader
,它就是一个转换器,将 A 文件进行编译形成 B 文件,这里操作的是文件,比如将 A.scss 或 A.less 转变为 B.css,单纯的文件转换过程 -
plugin
是一个扩展器,它丰富了wepack
本身,针对是loader
结束后,webpack
打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听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
为入口文件
- 设置 APP,几个入口文件,即会最终分割成几个
entry: {app: ['./src/index.js', './src/index.html'],
vendor: ['react'] // 这里还可以加入 redux react-redux better-scroll 等公共代码
},
-
output
出口-
webpack
基于Node.js
环境运行,可以使用Node.js
的API
,path
模块的resolve
方法 - 对输出的
JS
文件,加入contenthash
标示,让浏览器缓存文件,区别版本。
-
output: {filename: '[name].[contenthash:8].js',
path: resolve(__dirname, '../dist')
},
-
mode: 'development'
模式选择,这里直接设置成开发模式,先从开发模式开始。 -
resolve
解析配置,为了为了给所有文件后缀省掉js jsx json
,加入配置resolve: {extensions: [".js", ".json", ".jsx"] }
-
加入插件 热更新
plugin
和html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin') const webpack = require('webpack') new HtmlWebpackPlugin({template: './src/index.html'}), new webpack.HotModuleReplacementPlugin(),
-
加入
babel-loader
还有 解析JSX ES6
语法的babel preset
-
@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"]
},
}
},
- 加入
html-loader
识别html
文件
{test: /\.(html)$/,
loader: 'html-loader'
}
- 加入
eslint-loader
{
enforce:'pre',
test:/\.js$/,
exclude:/node_modules/,
include:resolve(__dirname,'/src/js'),
loader:'eslint-loader'
}
- 开发模式结束 代码在下面
const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = {
entry: {app: ['./src/index.js', './src/index.html'],
vendor: ['react',]
},
output: {filename: '[name].[hash:8].js',
path: resolve(__dirname, '../build')
},
module: {
rules: [
{
enforce:'pre',
test:/\.js$/,
exclude:/node_modules/,
include:resolve(__dirname,'/src/js'),
loader:'eslint-loader'
},
{
oneOf: [{test: /\.(html)$/,
loader: 'html-loader'
},
{test: /\.(js|jsx)$/,
use:
{
loader: 'babel-loader',
options: {presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false}]],
plugins: ["@babel/plugin-syntax-dynamic-import"]
},
}
},
{test: /\.(less)$/,
use: [{ loader: 'style-loader'},
{
loader: 'css-loader', options: {
modules: true,
localIdentName: '[local]--[hash:base64:5]'
}
},
{loader: 'less-loader'}
]
}, {test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[name].[hash:8].[ext]',
}
}, {exclude: /\.(js|json|less|css|jsx)$/,
loader: 'file-loader',
options: {
outputPath: 'media/',
name: '[name].[hash].[ext]'
}
}
]
}]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),],
mode: 'development',
devServer: {
contentBase: '../build',
open: true,
port: 3000,
hot: true
},
resolve: {extensions: [".js", ".json", ".jsx"]
}
}
必须了解的
webpack
热更新原理:
-
webpack
的热更新又称热替换(Hot Module Replacement
),缩写为HMR
。这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 -
首先要知道 server 端和 client 端都做了处理工作
- 第一步,在
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
操作,也就是进行浏览器刷新来获取最新打包代码。 - 参考文章 [][4]
- 第一步,在