前言
关于 webpack
,在我眼里一直都是蒙了一层“面纱”,一些配置也只能做到 “眼熟”。至于如何从 0 到 1 地去搭建一套“能用”的 webpack
配置更是“痴人说梦”。
“痛定思痛”之后, 我决定揭开那层“面纱”,重新梳理一遍它的基础配置,并进行一次从 0 到 1 的搭建。而这篇文章就是我揭开它的 “面纱”的整个过程,希望能对你有所帮助~
一、准备
webpack
是一个现代JavaScript
应用程序的静态模块打包器。当webpack
处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle
。
核心概念
-
入口(entry)
入口起点指示
webpack
应该使用哪个模块,来作为构建其内部依赖图的开始。 -
输出(output)
output 属性告诉
webpack
在哪里输出它所创建的bundles
,以及如何命名这些文件,默认值为./dist
。 -
loader
loader 让
webpack
能够去处理那些非JavaScript
文件。 -
插件(plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
初始化项目
mkdir webpack-demo && cd webpack-demo
npm init -y
# webpack@4.43.0
# webpack-cli@3.3.11
npm install --save-dev webpack webpack-cli
设置目录结构:
├── src #
| ├── assets # 资源文件
| ├── css # css
| ├── html # html
| |── js # js
| |──index.js # 入口文件
├── static # 静态资源文件
├── webpack.config.js # webpack配置文件
├── package.json #
在 package.json
中添加命令:
"scripts": {
"dev": "webpack --mode=development",
"build": "webpack --mdoe=production"
}
二、JS
在 src/js
目录下新建 index.js
文件,内容如下。并在入口文件 index.js
中引入该 js import './js/index'
:
const arrowFn = () => {
console.log('arrowFn')
}
arrowFn()
由于 webpack 4
是开箱即用的,所以我们可以直接执行命令 npm run dev
(前面已经将 webpack
的命令配置在了 package.json
中)。
执行完成后,在 dist/main.js
中我们可以看到刚刚的箭头函数,而 webpack
并没有主动帮助我们将它转义为低版本的代码。想要实现这个功能,我们需要通过 loader
对代码进行转换。
如何使用 loader
- 将我们需要使用的
loader
编写在webpack
配置中的module.rules
数组中。 loader
的基本格式为:
其中,{ test: xxx, use: xxx, options: {} }
test
属性用于标识出应该被对应的 loader 进行转换的某个或某些文件;use
属性表示进行转换时,应该使用哪个loader
;options
属性用于设置单独的配置。use
属性有三种写法:- 字符串
use: 'babel-loader'
- 对象
use: { loader: 'babel-loader', options: {} }
- 数组
use: ['babel-loader']
处理 js
将 js 转换为低版本的代码,我们需要使用 babel-loader
,另外还需要添加 babel
相关的配置。
依次执行下面的命令(关于babel 7的详细介绍):
# babel-loader@8.1.0
npm install --save-dev babel-loader
# @babel/core:babel的核心,包含所有的核心 API
# @babel/preset-env:将 js 引入的新语法转换为 ES5 的语法(不包括新增的全局变量、方法等)
# @babel/plugin-transform-runtime:用于构建过程中的代码转换
npm install --save-dev @babel/core @babel/preset-env @babel/plugin-transform-runtime
# @babel/runtime:实际导入项目代码的功能模块
# @babel/runtime-corejs3:配合 @babel/plugin-transform-runtime 避免全局污染
npm install --save @babel/runtime @babel/runtime-corejs3
修改 webpack.config.js
,内容如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
}
]
}
}
添加 babel
配置文件 .babelrc
,内容如下:
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
添加 .browserslist
(控制目标浏览器的范围),内容如下:
> 0.25%
not dead
执行 npm run dev
后,我们会发现 dist/main.js
中输出的代码已经被转义成低版本的代码了。
三、HTML
在 src/html
目录下新建 index.html
文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
这是 Index 页面
</body>
</html>
打包 html
文件,我们需要使用 html-webpack-plugin 插件。
# html-webpack-plugin@4.3.0
npm install --save-dev html-webpack-plugin
修改 webpack.config.js
:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
# ...
plugins: [
new HtmlWebpackPlugin({
template: './src/html/index.html',
filename: 'index.html', # 打包后的文件名
minify: { # 设置静态资源压缩情况
collapseWhitespace: false, # 是否折叠空白
}
})
]
# ...
}
执行 npm run dev
命令后,我们可以看到 dist
文件夹下新增了 index.html
文件,并且其中自动引入的是打包之后的 js 文件。此时直接通过浏览器访问 dist/index.html
,一切都那么自然~
html-webpack-plugin
插件的可扩展性还是很强的,例如设置 favicon
、meta
等,我们也可以自己添加自定义属性,根据不同的环境,在 html 中进行不同的设置等,例如可以手动设置页面的标题:<title><%= htmlWebpackPlugin.options.title %></title>
,而在 webpack
的配置文件中,title
属性的值可以设置为某个变量,以此达到灵活配置的目的。更多的使用方式还需要我们自己结合实际情况进行配置。
四、CSS
webpack
需要借助 style-loader
、css-loader
才能处理 css
文件,由于考虑到兼容性的问题,我们通常还会添加 postcss-loader
进行处理。而当我们用到 css
预处理器: sass
和 less
时,还需要分别使用 less-loader
和 less-loader
。这里以 less
为例。
安装依赖:
npm install --save-dev style-loader css-loader postcss-loader autoprefixer less-loader less
新建 css/index.less
文件,内容如下:
@bgColor: skyblue;
body {
background: @bgColor;
}
.bold {
font-weight: 500;
}
在 webpack.config.js
文件的 module.rules
下添加:
{
test: /\.(le|c)ss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
},
'less-loader'
]
}
在入口文件 src/index.js
中引入样式文件:
import './css/index.less'
执行 npm run dev
,在浏览器中打开 dist/index.html
,打开控制台,检查 html
的 head
标签,我们可以看到新增了 style
标签以及刚刚添加的样式。
首先我们要知道的是,loader
的执行顺序是从后往前的,即上面的执行顺序是:less-loader
-> postcss-loader
-> css-loader
-> style-loader
。
下面我们就按顺序分析一下,刚刚的几个 loader
分别做了哪些事情吧~
less-loader
:处理.less
文件,将其转为.css
文件;postcss-loader
和autoprefixer
:生成浏览器前缀;css-loader
:处理import
等语句,分析多个css
文件并合成一段css
;style-loader
:动态创建style
标签,将css
插入到head
中;
到了这里我们应该对 webpack
如何处理样式文件有了一个比较清晰的认识了。下面我们来看一下,如何抽离 css
。即:通过 link
标签的形式从外部引入。
抽离 css
# mini-css-extract-plugin@0.9.0
npm install --save-dev mini-css-extract-plugin
mini-css-extract-plugin
和 extract-text-webpack-plugin
比较:
- 异步加载
- 无重复编译
- 使用方式简单
- 只适用于
css
修改 webpack.config.js
文件如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
# 引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.(le|c)ss$/,
use: [
MiniCssExtractPlugin.loader, # 替换之前的 style-loader
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
},
'less-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/html/index.html',
filename: 'index.html',
minify: {
collapseWhitespace: false,
}
}),
# 输出 css 文件
new MiniCssExtractPlugin({
filename: '[name].[hash:6].css'
})
]
}
执行编译命令后,在 dist
目录下我们可以看到已经多出了 main.xxxxxx.css
样式文件,并在 html
中通过 link
标签的形式进行了引入,“大功告成”~
简单对比一下,这两种的方式其本质的不同就是在最后一步的写入。前者通过 style-loader
在 html
中添加内联样式表,后者首先通过 MiniCssExtractPlugin
输出 css
,再通过 MiniCssExtractPlugin.loader
在 html
中添加外部样式表。
优化压缩 css
安装 optimize-css-assets-webpack-plugin
:
npm install --save-dev optimize-css-assets-webpack-plugin
在 webpack.config.js
中添加配置:
plugins: [
new OptimizeCssPlugin(),
]
五、图片资源处理
css 使用本地图片
在样式文件中使用了图片资源时,需要使用 url-loader
和 file-loader
进行处理,url-loader
依赖于 file-loader
,可以理解为 url-loader
是对 file-loader
的一种封装。他们的区别在于,url-loader
在 options
配置添加了一个 limit
属性。当资源大小小于设置的 limit
值时,webpack
会将资源转换为 base64,超过 限制则会将图片拷贝到 dist
目录下。
安装 loader
。
# url-loader@4.1.0
# file-loader@6.0.0
npm install --save-dev url-loader file-loader
在 webpack.config.js
的 module.rules
中添加配置:
# ...
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240, # 10K
name: '[name]_[hash:6].[ext]',
outputPath: 'assets'
}
}
]
}
# ...
在 src/assets
目录下添加两张图片:avatar.jpeg
(超过 10k)和 logo.png
(小于 10k)。
在 html/index.html
中添加:
<div class="logo"></div>
<div class="avatar"></div>
在 css/index.less
中添加样式:
.avatar {
width: 200px;
height: 200px;
background: url('../assets/avatar.jpeg');
}
.logo {
width: 200px;
height: 200px;
background: url('../assets/logo.png');
}
执行编译命令,在浏览器中打开 dist/index.html
,两个背景图片都可以正常显示出来。我们也可以很清楚的看到 avatar.jpeg
是直接被拷贝到 dist/assets
目录下的,而 logo.png
是被转换成 base64 进行使用的。
html 使用本地图片
在 html/index.html
中添加:
<img src="../assets/avatar.jpeg" alt="">
此时执行编译命令,打开 dist/index.html
我们会发现找不到 avatar.jpeg
图片。这是因为经过 webpack
的构建后,通过相对路径已经无法找到图片了。
我们可以通过添加 html-loader
来解决:
# html-loader@1.1.0
npm install --save-dev html-loader
修改 webpack.config.js
:
{
test: /\.html$/,
use: 'html-loader'
}
重新打包后,所有资源都可以正常加载了~
至此,我们常见的 html
、js
、css
以及图片资源的使用都可以通过 webpack
打包了。但是这个时候都是使用的 webpack
默认的入口出口配置,下面我们就来看一下如何设置入口出口的配置吧。
六、入口出口配置
入口配置
入口配置的字段为:entry
,值可以是字符串、数组、对象。单个页面一般这三种都可以选择,但用的最多的还是字符串和对象。数组是在当有“多个主入口”,多个依赖文件一起注入时使用的。
修改 webpack.config.js
:
module.exports = {
# webpack的默认配置
entry: {
index: './src/index.js'
}
}
这里通过对象的形式进行了设置,执行编译命令后我们会发现,之前的 main.[hash].[ext]
现在都变成了 index.[hash].[ext]
,这也就说明我们设置的入口配置成功了。如果直接是以字符串的形式设置,那么在配置中使用 [name]
,打包后的输出结果的默认名就是 main
。(注意:我们在配置中所使用的 [name]
不是所有地方都是指文件的名称,部分是指设置的入口的属性名称)
出口配置
出口配置的字段为:output
。
修改 webpack.config.js
:
const path = require('path')
# ...
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash:6].js',
publicPath: '/'
}
# ...
由于我们在出口配置中设置了 publicPath: '/'
,则所有的资源默认从根目录下获取。所以我们就不能再使用之前的方式查看编译后的效果了。
解决方案:
- 在
dist
目录下,通过http-server
开启本地服务器。 - 通过
webpack-dev-server
开启本地服务。
七、配置补充
mode
提供 mode
配置选项,告知 webpack
使用相应模式的内置优化。
webpack
给我们提供了两种选项:development
和 production
。(关于 mode)
一开始我们就将 mode
配置进了命令行,通过 --mode=xxx
的形式指定。下面我们将通过环境变量来判断到底启用哪种模式。
安装 cross-env
(跨平台设置 NODE_ENV
):
npm install --save-dev cross-env
修改 package.json
:
"scripts": {
"dev": "cross-env NODE_ENV=development webpack",
"build": "cross-env NODE_ENV=production webpack"
},
修改 webpack.config.js
:
const isDev = process.env.NODE_ENV === 'development'
module.exports = {
mode: isDev ? 'development' : 'production'
}
配置 resolve
在 webpack.dev.conf.js
中添加 resolve
配置:
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.js', '.less', '.json', '/index.js'],
alias: {
'@': path.join(__dirname, 'src'),
}
}
module
通过 resolve.module
告诉 webpack
查找模块时应该搜索哪些目录,默认值为 ['node_modules']
。按照数组顺序从左往右依次查找。
我们通过一个例子可以更加直观的理解下面的配置:
import { Button } from 'antd'
,此时 webpack
会先查找 src
目录下有没有对应的模块,如果没有再去 node_modules
目录下查找。
extensions
如果多个文件有着相同的名称,但具有不同的扩展名,则 webpack
将解析其扩展名列在数组中首位的文件,并跳过其余文件。
举个例子:
common
文件夹下有 index.js
和 index.less
文件。当我们通过 import './common/index'
的形式引入文件时,webpack
会根据 resolve.extensions
的配置进行判断,从左往右依次匹配。当找到可以匹配的目标后,跳过其余文件。此处 webpack
的解析结果就等于是 import './common/index.js'
。
alias
当我们的项目越来越庞大,目录结构越来越复杂时,有些时候通过相对路径引入文件,代码会显得异常不清晰(很多层 “../”)。
所幸 webpack
为我们提供了 resolve.alias
配置项来设置别名,帮助我们优雅地通过绝对路径引入文件。
例子:引入 css/index.css
。
import '../../../css/index.css'
import '@/css/index.css'
这里通过别名引入文件,可以很清晰的知道文件的位置:src/css/index.css
。
webpack-dev-server
前面我们只是让 webpack
正常的运行起来,但在实际开发中我们会需要:提供 HTTP
服务而不是使用本地文件预览;监听文件的变化并自动刷新网页。
官方已经为我们准备好了开发工具 DevServer
,它会启动一个 HTTP
服务用于网页请求,同时会帮助我们启动 webpack
,并接收 webpack
发出的变更信号,自动刷新网页。
安装 DevServer
:
# webpack-dev-server@3.11.0
npm install --save-dev webpack-dev-server
修改 webpack.config.js
:
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server",
"build": "cross-env NODE_ENV=production webpack"
},
在 webpack.config.js
中添加配置:
module.exports = {
devServer: {
host: 'localhost', # 服务器地址
port: '8899', # 默认是8080
compress: true # 是否启用 gzip 压缩
}
}
执行 npm run dev
后,访问 http://localhost:8899/
,这样我们就可以在浏览器中看到实际的效果啦。
这个时候我们会发现并没有文件输出到 dist
目录下,原因是因为 DevServer
会把 webpack
构建出的文件保存在内存中,在要访问输出的文件时,必须通过 HTTP
服务访问。
通过 DevServer
启动的 webpack
会开启监听模式(默认是关闭的,可以通过 webpack --watch
手动开启监听),当发生变化时重新执行构建,并通知 DevServer
。DevServer
会让 webpack
在构建出的 js 代码里注入一个代理客户端用于控制网页,以方便 DevServer
主动向客户端发送命令。 DevServer
在收到来自 webpack
的文件变化通知时通过注入的客户端控制网页刷新。
另外我们还可以通过 devServer
解决开发环境跨域的问题。
例子:假设我们本地代码运行在 localhost:8899
,而服务端接口在 http://dev.api.com
。此时有 http://dev.api.com/user/login
在 devServer
中添加 proxy
配置:
proxy: {
"/api": { # 起标识作用,表示此处配置用于接口。(名字可以随意定)
target: "http://dev.api.com", # 目标服务器
pathRewrite: {
"/api": "" # 重写 “/api” 为空
}
}
}
清空 dist
之前我们每次执行 npm run build
,dist
目录下都会保留上一次的打包内容,每次手动去删除岂不是太麻烦了,所以我们需要通过 clean-webpack-plugin
插件帮助我们在每次打包前,先将 dist
目录清空。
安装 clean-webpack-plugin
:
npm install --save-dev clean-webpack-plugin
修改 webpack.config.js
:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
静态资源处理
src/assets
和 static
,它们俩都是用来存放静态资源文件的,那么它们有什么区别呢?
在我们的项目中,html
和 css
通过 html-loader
和 css-loader
分析静态资源 URL 的,例如 <img src="../assets/avatar.jpeg" alt="">
、background: url('../assets/avatar.jpeg');
。这些文件都是通过相对路径引用的,webpack
会将它们作为依赖模块处理。相比之下,我们存放在 static
目录下的资源文件不会 webpack
处理(这是我们的初衷),而是直接拷贝到打包后的 dist
目录下,所以我们在使用 static
目录下的文件时,需要使用绝对路径的方式进行引用。
安装 copy-webpack-plugin
:
# copy-webpack-plugin@6.0.2
npm install --save-dev copy-webpack-plugin
在 static
目录下新建 index.js
,并在 html/index.html
中引入:
const fn = () => {
console.log('我是static目录下的index.js')
}
fn()
修改 webpack.config.js
:
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'static/*',
to: 'static/[name].[ext]'
}
]
}),
]
}
在 html/index.html
中引用 static/index.js
:
<script src="/static/index.js"></script>
重启本地服务器(当我们修改了 webpack
配置后,都需要重新启动才能生效)。访问 http://localhost:8899/
,一切都很正常~
exclude 和 include
我们在配置 loader
的时候,可以通过 exclude
和 include
来减少 webpack
转义的文件。这两个配置只需要设置其中一个即可,exclude
优先级大于 include
。
exclude
:排除某些文件。
include
:指定某些文件。
八、webapck 多环境配置
通常我们在开发环境和生产环境会采取不同的编译配置,下面我们就来看看如何对不同的环境添加不同的配置。
新建 build
目录,在该目录下添加以下文件:
util.js
:提供通用方法。webpack.base.conf.js
:提供公共配置。webpack.dev.conf.js
:提供开发环境配置。webpack.prod.conf.js
:提供生产环境配置。
新建 config
目录,在该目录下添加以下文件:
index.js
:提供webpack编译参数。dev.env.js
:提供开发环境参数。prod.env.js
:提供生产环境参数。
两个不同文件的 webpack
我们可以通过 wepack-merge
进行合并。
ps:目前项目的目录是参考的 vue-cli
创建的项目,这里可以根据自己的想法进行设置。
九、多页应用
通过前面的配置,我们单页应用的打包配置和多环境的配置基本上已经 ok 了,下面我们来看一下如何进行多页应用打包。
设置多页应用
-
在
html
目录下新建home.html
。 -
直接看目录结构吧:
新建
entry
目录,用于存放入口文件,修改文件内资源引用的路径。
-
修改
webpack.base.conf.js
:module.exports = { entry: { index: './src/entry/index.js', home: './src/entry/home.js' }, plugins: [ new HtmlWebpackPlugin({ template: './src/html/index.html', filename: 'index.html', minify: { collapseWhitespace: !isDev, }, chunks: ['index'] }), new HtmlWebpackPlugin({ template: './src/html/home.html', filename: 'home.html', minify: { collapseWhitespace: !isDev, }, chunks: ['home'] }), ] }
historyApiFallback
现在项目有多个入口JS和HTML,对于这种多页应用(实际上就是由多个单页应用组成的),我们期望的是在开发时能通过路由切换到对应的页面下,所以我们需要通过 historyApiFallback
在 devServer
中设置路由规则。
修改 webpack.dev.conf.js
:
devServer: {
host: config.dev.host,
port: config.dev.port,
compress: true,
historyApiFallback: {
rewrites: [
{ from: /^\/index/, to: '/index.html' },
{ from: /^\/home/, to: '/home.html' }
]
}
},
这样我们就可以在开发的时候随便查看对应的页面啦~
提取公共代码
在多页应用中,经常会有一些公共的 css
和 js
,我们需要通过 optimization.splitChunks
将它们进行分割,打包成一个新的模块后在对应的页面分别进行引入。
在 webpack.base.conf.js
中添加相关配置:
optimization: {
splitChunks: {
chunks: 'all', # 代码块类型,`all`(默认值)、`initial`(初始化)、`async`(动态加载)
minSize: 0, # 生成块的最小大小
minChunks: 1, # 生成块前共享模块的最小块数
# 缓存组可以继承或覆盖splitChunks.*;中的任何选项配置。
# 但是test,priority和reuseExistingChunk只能用于缓存组级别上的配置。
# 要禁用任何默认缓存组,需要在缓存组配置中添加 default: false。
cacheGroups: {
vendors: { # 处理第三方依赖
priority: 1,
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
minSize: 0,
minChunks: 1
},
commons: { # 处理公共模块
chunks: 'initial',
name: 'common',
minSize: 0,
minChunks: 2
}
}
}
}
最后
没有最好的,只有最适合的,采用什么样的配置还是需要根据项目的实际情况进行分析。
有一段时间没更新了,一方面是有点忙,另一方面是自己也松懈了。嗐,越学习就会发现自己不会的越多,而“会”的部分有很多也是一知半解,真是令人惆怅。不过还是要加油呢~文中若有不足之处和错误的地方还请大佬们帮忙指正,谢谢~