最近的项目开始了优化的工作,趁此机会重新学习了一波 webpack 4,希望把部分学习过程分享给大家。
webpack 是什么?
工欲善其事,必先利其器。了解工具是很重要的,但在开始之前,我们要问自己一件很重要的问题:“该工具解决了什么问题?” webpack 是一个 模块打包器,它可以合并一组模块和他们的依赖关系,输出一个或者多个文件。除了模块打包以外,webpack 还可以分析你的项目结构,找到 JavaScript 模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypewScript等),并将其打包为合适的格式以供浏览器使用。
开发前的准备
既然是从零开始,那我们就新建项目来学习 webpack 的使用。
mkdir webpack4-demo && cd webpack4-demo
npm init -y
npm install webpack webpack-cli --save-dev
复制代码
- webpack 4 分离了 webpack-cli 与 webpack 所以需要单独安装 cli
初始化之后我们对 package.json
做如下修改:
"scripts": {
"build": "webpack"
}
复制代码
这样,在运行 npm run build
时,会使用 node_modules
中的 webpack
命令。
基本概念
从 webpack 4 开始可以不需要任何配置(其实就是自带了一些默认配置)。但随着项目的发展我们还是需要对配置进行修改和优化的。webpack 会读取项目根目录下 webpack.config.js
文件。
- entry 入口:webpack 架构第一步会从入口文件开始,可以是一个或者多个。
module.exports = {
entry: {
first: './src/first.js',
second: './src/second.js',
}
};
复制代码
- output 输出:默认情况下只输出一个文件,通过配置可以解决这个问题。
const path = require('path');
module.exports = {
entry: {
first: './src/one.js',
second: './src/two.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
复制代码
Loader
Loader 非常强大,可以将 scss、less、typescript 等浏览器无法直接理解的转换为浏览器可以理解的语言,甚至连图片字体等资源也可以交由 loader 来处理。而使用它们非常简单,只需三步:
- 安装 loader
- 在
webpage.config.js
中指定规则 - 生效!
解析 css
首先安装 css 相关的 loader
npm install css-loader style-loader
复制代码
修改 webpage.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
}
复制代码
在 index.js
中引入 css
import './style.css';
复制代码
webpack 将会做如下工作:
- Webpack 将尝试解析style.css文件
- 文件名匹配到
/\.css$/
- 该文件将由 css-loader 解释
- css-loader 的结果将传递给 style-loader
- 最后,style-loader 将返回一个 JavaScript 代码
解析 scss
同样,按安装 scss 的 loader
npm install node-sass sass-loader
复制代码
- tips: sass-loader需要安装node-sass才能工作。
修改 webpage.config.js
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
}
复制代码
解析图片
安装相关 loader
npm install url-loader file-loader
复制代码
tips: url-loader 是对 file-loader 的上层封装
修改 webpage.config.js
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader','sass-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 5000
}
}
]
}
]
}
};
复制代码
url-loader 可以将小图片转换为 base64 URI,这样可以减少网络请求,而大小由 limit 字段决定。
转换前
body {
background-image: url('./big-background.png');
}
.icon {
background-image: url('./icon.png');
}
复制代码
转换后
<style type="text/css">
body {
background-image: url(ca3ebe0891c7823ff1e137d8eb5b4609.png);
}
.icon {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAALElEQVR4AWMYIWAU1FPLoP9AXEFI0QEi8H+YYdQyqIEaXuumRhh1DZdUMwoATlYWfwh9eYkAAAAASUVORK5CYII=);
}
</style>
复制代码
解析 js
使用 babel-loader 后,就可以愉快的在 javascript 中写 es6 es7.... babel 可以通通帮我们翻译成浏览器可以认识的旧版本。
按照国际惯例,先安装 loader
npm install babel-loader babel-core babel-preset-env
复制代码
为什么安装这么多?别着急,一个一个来解释。
babel-core
babel 编译库的核心包。babel-loader
配合 webpack 的 loader。babel-preset-env
在 babel 翻译之前,我们需要先告诉 babel 我们要以什么样的规范去编译。比如按照 es6 标准编译,那么我们就安装一个 babel-preset-es2015, 同样,如果我们要按照 es7 来编译,那么我们就安装 babel-preset-es2016,而 es 新规范层出不穷,如果要按照最新的规范做编译,直接安装 babel-preset-env 就可以了,它包含了 babel-preset-es2015, babel-preset-es2016, babel-preset-es2017,等价于 babel-preset-latest,可以编译所有最新规范中的代码。
之后就是配置规则啦:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
复制代码
tips: exclude 表示不会转换那些文件。
插件
webpack 有着丰富的插件接口,这个插件接口使 webpack 变得极其灵活。同样,使用插件也分三步。
- 安装插件
- 通过 new 实例化插件,这样可以多次使用同一个插件。
- 生效!
HtmlWebpackPlugin
应该是最常用的插件之一,它可以帮我们在打包后自动生成引入打包文件的 HTML,对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。可以使用已有的模板来生成。
安装插件
npm install --save-dev html-webpack-plugin
复制代码
添加配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
复制代码
如果输出的是多个文件呢?也很简单
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
one: './src/one.js',
two: './src/two.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
filename: 'one.html',
template: './src/one.html',
chunks: ['one']
}),
new HtmlWebpackPlugin({
filename: 'two.html',
template: './src/two.html',
chunks: ['two']
})
]
};
复制代码
MiniCssExtractPlugin
MiniCssExtractPlugin 可以帮我们提取 css 到单独的文件中,在 webapck 4 之前的版本是用 ExtractTextWebpackPlugin 来实现的,webapck 4 之后才可以使用 MiniCssExtractPlugin。而且与 ExtractTextWebpackPlugin 比起来有如下特点
- 异步加载
- 没有重复的编译
- 更容易使用
- 特定于CSS 但是目前还不支持开发时的 HRM (Hot Module Replacement),所以需要针对开发版单独配置。
安装插件
npm install --save-dev mini-css-extract-plugin
复制代码
配置规则
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== 'production'
module.exports = {
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css',
})
],
}
复制代码
代码拆分
在解释这个概念之前,我们先看如下代码: user.js
export default [
{ name: "xiaoming", age: 28 },
{ name: "xiaohong", age: 24 },
{ name: "zhangsan", age: 31 },
{ name: "lisi", age: 40 }
]
复制代码
a.js
import _ from 'lodash';
import users from './users';
const zhangsan = _.find(users, { name: 'zhangsan' });
复制代码
b.js
import _ from 'lodash';
import users from './users';
const lisi = _.find(users, { name: 'lisi' });
复制代码
webpack.config.js
module.exports = {
entry: {
a: "./src/a.js",
b: "./src/b.js"
},
output: {
filename: "[name].[chunkhash].bundle.js",
path: __dirname + "/dist"
}
};
复制代码
运行 webpack 后,我们会看到两个文件 a.[chunkhash].bundle.js
和 b.[chunkhash].bundle.js
,打开这两个文件后我们发现:
居然每一个文件都有 lodash ..... 所以我们希望可以共享的代码抽离出来,其他文件引入这个文件即可。帮我们实现这个功能的就是 SplitChunksPlugin。
SplitChunksPlugin
从 webpack 4 开始,就删除了 CommonsChunkPlugin,而是引入了 SplitChunksPlugin ,而且开箱即用,不需要另外安装。下面我们修改一下 webpack 的默认值。
module.exports = {
entry: {
a: "./src/a.js",
b: "./src/b.js"
},
output: {
filename: "[name].[chunkhash].bundle.js",
path: __dirname + "/dist"
},
optimization: {
splitChunks: {
chunks: "all"
}
},
};
复制代码
修改后重新运行,我们再次打开文件查看结果:
可以发现,新打包了一个 vendors~a~b~.[hashchunk].js
,这个文件是 a.js 和 b.js 中共用部分的代码, 同时,文件 a 和 b 中都不再有 lodash 了。 但是这里还有个小问题,就是文件 user.js
中输出的数据其实也是可以共用的,而这一部分没有被单独被打包到一个新文件的原因是 默认情况下只打包大于 30k 的文件。同样我们通过修改配置可以实现拆分共用文件的部分。
module.exports = {
entry: {
a: "./src/a.js",
b: "./src/b.js"
},
output: {
filename: "[name].[chunkhash].bundle.js",
path: __dirname + "/dist"
},
optimization: {
splitChunks: {
chunks: "all",
minSize: 0
}
}
};
复制代码
运行结果:
可以发现新打包了一个 a~b.[chunkhash].js
的文件,这就是共用的数据。然而事实上,默认不打包 30k 以下的文件是很合适的配置。因为多一个文件意味着多一个请求,在实际开发中,我们要在文件大小和请求中间做一个平衡。