简介
webpack
是一个用于现代JavaScript
应用程序的静态模块打包工具。用于前端代码的工程化。webpack
处理应用程序时,它会在内部构建一个 依赖图(dependency graph
),此依赖图对应映射到项目所需的每个模块,并生成一个或多个bundle
。webpack + loader
可以支持多种语言和预处理器语法编写的模块。loader
向webpack
描述了如何处理非原生模块,并将相关依赖引入到你的 bundles中。webpack
还支持可自由扩展的插件(plugin
)体系。这是webpack
的 支柱 功能。插件目的在于解决loader
无法实现的其他功能。webpack
天生支持如下模块类型:ECMAScript
CommonJS
AMD
Assets``WebAssembly
.
接下来,我们以一个 React + SASS
的简单web
项目,来学习webpack
的基础配置。
以下配置都是基于
webpack 4.30.0
。
基础配置
首先,我们要安装webpack
系列npm
包 (这些 loader
和 plugins
,后面会逐步介绍):
// package.json
{
"devDependencies": {
/** webpack **/
"webpack": "4.30.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.7.2", // 本地调试用
/** loader **/
"css-loader": "2.1.1",
"style-loader": "^2.0.0",
"file-loader": "^6.2.0",
"node-sass": "^4.0.0",
"sass-loader": "^7.1.0",
"@babel/preset-env": "7.4.3",
"@babel/preset-react": "7.0.0",
/** plugins **/
"html-webpack-plugin": "4.0.0",
"mini-css-extract-plugin": "^0.6.0",
"webpack-bundle-analyzer": "^4.4.0"
}
}
webpack
打包,需要做一系列的配置,而且 本地调试 和 生产打包 的配置是有一些差异的。一般情况下,我们在项目的根目录下设置两个配置文件:
|
|- webpack.config.js # 生产打包
|- webpack.config.dev.js # 本地调试
// package.json
{
//...
"scripts": {
"start": "webpack-dev-server --config webpack.config.dev.js",
"build": "webpack-cli"
}
//...
}
webpack
配置文件可以直接返回一个json
对象,或者一个返回json
对象的函数。
// webpack.config.js
export default {
//...
}
//or
export default () => {
return {
//...
}
}
接下来,我们开始具体配置:
entry
web
项目中,我们一直有主JS
,或者入口JS
的概念。webpack
要打包,构建依赖图谱。肯定也需要一个入口文件。webpack
通过 entry
来配置一个(或多个)入口。默认值是 ./src/index.js
。
const path = require('path')
module.exports = {
entry: './src/pages/app.js'
}
或者对象,可以标示入口文件name
(一般用于多个)
module.exports = {
entry: {
app: './src/pages/app.js'
}
}
output
有打包输入(entry
),就有打包输出。webpack
通过 output
来配置输出文件信息。默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
module.exports = {
output: {
path: path.resolve(__dirname, 'build'), // 指定输出目录
filename: '[name].js', // 指定输出文件名称; [name] 动态使用entry指定的名称,对应上面是:app
}
}
高阶配置
实际项目中,我们静态资源会使用CDN
,或者通过其他特有目录引入。
这个时候,我们需要通过 publicPath
来配置.
编译时指定:
module.exports = {
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
publicPath: '/my-project/static/resource/',
}
}
运行时指定:
module.exports = {
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
publicPath: './', // 可随意填写,运行时会覆盖为 __webpack_public_path__
}
}
// 重点在这里:
__webpack_public_path__ = window.global_info.staticCDN;
// 如果项目在一个Java/Node容器,staticCDN 的值一般是服务端路由中读取环境变量赋予。
// 如果项目是纯前端,staticCDN的值一般是通过 域名+规则 判断确定。
Loaders
- 面向现代
JS
,我们大都不是通过直接编写原生JS
来开发项目,而是运用各种框架(React
,Vue
……),各种JS+
语言(TS
,JSX
……),ES5+
语法糖等。 - 但是我们的浏览器只识别原生
JS
,虽然主流浏览器也开始兼容绝大多数ES5+
语法,但是我们也得考虑兼容性问题。 - 所以,
webpack
提供了loaders
机制,我们可以通过使用loader
用于对模块的源代码进行转换。 - 不同的模块类型,应用不同的
loaders
。 loaders
是通过module
对象下的rules
数组来配置。
简单示例:
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' }, // 对每个 .css 使用 css-loader
{ test: /\.ts$/, use: 'ts-loader' } // 对所有 .ts 文件使用 ts-loader
]
}
};
test
:RegExp
,用来匹配需要处理的文件类型。use
:String | Object | Array
,指定具体的loader
。上述示例,使用 单个默认配置的loader
,可以使用String
类型指定即可。
** loader
可以通过 options
对象配置更多参数。带参数配置的单个loader
示例:**
module.exports = {
module: {
rules: [
// 图片资源处理
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: require.resolve('file-loader'),
options: { // loader参数配置
esModule: false,
name: './image/[name].[hash:8].[ext]'
}
}
]
}
]
}
}
负责图片处理的 loader
有两种, 一般一起使用:
file-loader
可以把js
和css
中导入的图片替换成正确的地址,并把图片文件输出到对应的位置。文件名是根据文件内容计算出的hash
值。url-loader
可以把文件通过base64
编码后注入到JS
或者css
中去。图片的数据量太大,会导致JS
和CSS
文件变大,一般利用url-loader
把页面需要的小图片注入到代码中去,以减少加载次数。url-loader
通过limit
参数来控制,小于limit
的图片才会处理。
多个 loader
配置,将按照相反的顺序执行。以支持 scss
为例:
module.exports = {
module: {
rules: [
// 处理sass样式文件
{
test: /\.(css|scss)$/,
// 使用多个 loaders;都使用默认配置;
use: ["style-loader", "css-loader","sass-loader"]
}
]
}
}
sass-loader
负责把Scss
源码转换为CSS
代码,再交给css-loader
处理。css-loader
会找出@import
和URL()
这样的导入语句,告诉webpack
依赖这些资源。同时还支持css modules
,压缩css
等功能。style-loader
会把css
代码转换成字符串,然后注入到JS
代码中。通过JS
给DOM
增加样式。也可以通过plugin
(MiniCssExtractPlugin
)把css
提取到单独的文件中。
插件 (plugin
) 可以为 loader
带来更多特性:
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/, // 正则表达式,设置该loader需要忽略/排除的目录/文件
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env','@babel/preset-react'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
}
}
}
]
}
}
@babel/runtime和@babel/plugin-transform-runtime
:babel
编译时只转换语法,几乎可以编译所有时新的JavaScript
语法,但并不会转化BOM(浏览器)
里面不兼容的API
。比如Promise
,Set
,Symbol
,Array.from
,async
等等的一些API。这2个包就是来搞定这些api
的。@babel/plugin-proposal-class-properties
:用来解析类的属性的。在
babel
执行编译的过程中,会从项目的根目录下的.balelrc
文件中读取配置。.balelrc
是一个json
格式的文件,当babel loader
配置项很多的时候可以使用。
由此可见, plugins
可以在 loader
配置中配合使用。当然,也可以单独配置使用。下面,我们来详解了解一下plugins
。
Plugins
功能: 用于解决
loader
无法实现的其他事,进行功能扩展。原理:
webpack plugins
是一个具有apply
方法的JavaScript
对象。apply
方法会被webpack compiler
调用,并且在整个编译生命周期都可以访问compiler
对象。你可以基于次编写自定义插件。用法: 可以通过在
webpack.config.js
增加Plugins
配置使用。也可以通过NodeAPI
的形式使用。
下面,我们通过几个常用的 plugin
来了解如何配置使用:
// 该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 一般,plugins都是通过 new 一个自身的实例来使用
// 支持option对象参数配置
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './temp.html', // 可以自定义用于生成Html的模版文件,非必填
filename: 'home.html' // 指定html文件名称;默认为 index.html
})
]
}
** 使用默认配置生成的 html
文件如下:**
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpack App</title>
</head>
<body>
<script src="index_bundle.js"></script>
</body>
</html>
多个
entry
,对应多个script
标签。
前面有提到,有的 plugins
可作为有些 loader
的 option
配置项使用。
还有一些插件需要在Plugins
和Loader
中都需要增加配置:
// 本插件会将 CSS 提取到单独的文件中
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(css|scss)$/,
use: [MiniCssExtractPlugin.loader, "css-loader","sass-loader"]
},
]
},
plugins: [
new MiniCssExtractPlugin({ filename: '[name].css'})
]
}
使用
MiniCssExtractPlugin
生成的css
文件会在生成的Html
文件中通过<link>
引入。所以不再需要使用style-loader
。style-loader
: 将模块导出的内容作为样式并添加到DOM
中。
mode
production
ordevelopment
.用来指定是生产模式还是开发模式。- 在代码中可以通过
process.env.NODE_ENV
获取,用来做两种模式的兼容处理。
到此,我们这个简单项目的完整webpack.config.js
配置如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = () => {
return {
mode: 'production',
entry: {
app: path.resolve(__dirname, 'src/pages/app.js'),
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/my-project/static/resource/',
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.jsx', '.ts'],
},
module: {
rules: [
{
test: /\.(css|scss)$/,
use: [MiniCssExtractPlugin.loader, "css-loader","sass-loader"]
},
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env','@babel/preset-react'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
}
}
},
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: require.resolve('file-loader'),
options: {
esModule: false,
name: './image/[name].[hash:8].[ext]'
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({template: './temp.html', filename: 'home.html'}),
new MiniCssExtractPlugin({ filename: '[name].css'})
],
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial'
}
}
}
}
}
}
resolve.extensions
: 如果文件引入的时候没有后缀名,将自动按配置的文件名匹配查找。optimization
: 用于提取公共js
配置。
开发模式
我们在本地调试的时候,webpack
配置和生产会有一些差异。比如:
- 生产可能配置特殊的
publicPath
,本地则不能配置。 - 本地调试专用的
devServer
配置。 - 开发模式专用的一些
plugins
。
我们可以设置单独的配置文件(如 webpack.config.dev.js
):
// 下面只展示差异点配置
// new plugin 用来做打包后文件的成分分析,以做某些优化。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = () => {
return {
mode: 'development', // 指定为开发环境
// 下无 publicPath配置
output: {
path: path.resolve(__dirname, 'pcDist'),
filename: '[name].js',
},
// 专用plugin
plugins: [
new BundleAnalyzerPlugin()
],
// 本地调试专用配置
devServer: {
contentBase: path.join(__dirname, 'pcDist'),
port: 8009,
hot:true,
open: true
}
}
}
DevServer
其实是一个方便开发的小型http
服务器,是基于webpack-dev-middleware
和 Express
实现的。webpack-dev-middleware
会导出一个函数。该函数接收一个 webpack
的 Compiler
实例作为参数,导出一个 Express
中间件。该中间件具有以下功能:
- 接收
Compiler
实例输出的文件,但不会存在硬盘,而是放入内存。 - 往
Express
app
上注册路由,拦截http
请求,根据请求路径响应对应的文件。
明显,开发和生产配置大部分还是通用。所以在实际项目中我们使用一个通用配置文件,然后在自有的cli工具库中去抽象,处理,复用。
module,chunk,bundle理解
moule
是模块,webpack
中一切皆模块,所以module
就是我们编写的一个个文件。chunk
是指webpack
根据文件引用关系生成的chunk
文件。一般来说,一个entry
对应一个chunk
文件。bundle
是指webpack
最终生成的浏览器可以直接运行的bundle
文件。一般来说,一个chunk
对应一个bundle
文件。但是也有例外,比如我们使用MiniCssExtractPlugin
插件会从一个chunk
中抽取出单独的css bundle
文件。
webpack 异步加载
在使用 webpack
打包的应用中,我们可以使用 require.ensure
进行异步加载,也有人称为代码切割。他其实就是将指定的 js
模块独立导出一个.js
文件,然后使用这个模块的时候,再创建一个 script
对象,加入到 document.head
对象中,浏览器会自动帮我们发起请求,去请求这个 js
文件,然后写个回调函数,让请求到的 js
文件做一些业务操作。
require.ensure
这个函数是一个代码分离的分割线,表示回调里面的 require
是我们想要进行分割出去的,webpack
会打包成单独 js
文件。它的语法如下:
// 语法如下:
`require.ensure(dependencies: String[], callback: function(require), chunkName: String)`
按需加载
webpack4
官方文档提供了模块按需切割加载,配合 es6
的按需加载 import()
方法,可以做到减少首页包体积,加快首页的请求速度,只有其他模块,只有当需要的时候才会加载对应 js
。
import()
的语法十分简单。该函数只接受一个参数,就是引用包的地址,并且使用了 promise
式的回调,获取加载的包。在代码中所有被 import()
的模块,都将打成一个单独的包,放在 chunk
存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。
常用的优化手段
优化
loader
配置: 通过include
等配置,不需要处理的文件尽量不处理。异步按需加载:(上面有专门提到)。
分包:提取公共的一些模块 + 缓存提高性能。
SplitChunksPlugin
: 提取公共模块,问题是业务模块依赖改变也会影响公共包,哈希会改变,缓存会失效。Dllplugin & DllReferencePlugin
: 将指定的公共模块打包成动态链接库形式的js文件。其他模块引用到这些指定的公共模块会直接在公共js文件中加载,不会打包在业务模块中。问题是这些动态链接库文件要独立先行打包,并提前引入。
Tree Shaking
: webpack依赖静态的 ES6 模块化语法,分析出都要哪些功能被用到了,然后剔除没有的代码。可以在启动Webpack
时带上--optimize-minimize
参数,快速接入Tree Shaking
;也可以使用UglifyJSPlugin
来处理。Scope Hoisting
(作用域提升): 分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。- 使用内置的
ModuleConcatenationPlugin()
即可开启。
- 使用内置的