前言
铁子们好我是跑不快的猪,新的一年,新的开始,先预祝各位都有华丽丽的变身。
本篇文章主要进行webpack5.0+版本的配置,在这个脚手架横行的时代,最终还是需要掌握一些基础的配置,对工作、面试、以及各脚手架中webpack的调试都有不小的作用。接下来会一步步的分享我学习配置中的一些坑和心得。
最终是配置了常用vue+vuex+vue-router的单页面应用工程
文末可以直接下载demo,修修改改可以简单开发,会逐步更新!
请注意,在每个配置讲解中路径与项目架构中的路径一致
概念预热
在学习webpack的过程中,有如下几个概念贯穿始终,在开始之前先列出来。掌握了它们虽然不至于事半功倍,但是对一些配置的理解可能会更简单一点。
- module(模块):预备被webpack处理的,相对整个程序的离散功能块,它们遵循commonJs规范、umd规范、ES6 Module,提供可靠的抽象和封装界限
- chunk(块):在被webpack打包中,将模块按照引用关系合并的模块集合称之为chunk,webpack可以对这些chunk进行操作。
- bundle:bundle是在chunk经过处理后,最终输出的也就是可以上线的文件。
安装
npm init
初始化package.json文件yarn add webpack webpack-cli -D
使用喜欢的包模块管理工具安装webpack和webpack脚手架到工程目录下- 创建webpack.config.js文件
// webpack.config.js
module.exports = {
// 关于webpack的配置写在这里
}
至此,准备工作已经完成了,这里要注意:运行webpack5的node版本最低要是10.13.0(LTS)
在mac上使用
n
模块,在windows使用nvm
模块可以更快更方便的切换node版本
工程目录
关于工程目录没有什么可说的,构建一个清晰的,模块化的并且操作起来方便的工程目录即可,本篇中使用的工程目录如下
- build:放入webpack的一些配置文件
- src-assets:放入静态资源比如图片、字体、音频、视频等
- src-store:vuex配置文件夹
- src-routes:vue路由配置文件夹
- src-entry: 工程的一些入口文件,如:App.vue等
- src-views:路由对应的每个vue插件
入口(entry)
开始正式进入正题,当我们需要使用webpack打包的开始,首先要告诉webpack从哪里开始进行,也就是入口。
此处还有个不属于入口配置,但是与入口息息相关的配置context
,上下文配置。这个配置能干嘛呢,其实就是将配置中的相对路径改变了一下,默认我们的相对路径是针对当前的工程目录
// 会找src/index.js
module.exports = {
entry: './src/index.js'
}
// 会找app1/src/index.js
module.exports = {
context: 'app1',
entry: './src/index.js'
}
context
我能想到的用法,就是当项目非常庞大,由多个单独项目组成,但是打包功能相似,可以使用一套配置打包,官方给出的解释是这个功能让你的配置独立与CWD(当前工作目录)也就是你的配置可以放在任意地方。
说回entry
,它支持多种类型string | [string] | {}
,不同类型配置作用于不同的场景之下。
string
配置一个入口文件,比如配置一个SPA项目[string]
多用于将多个文件打包成一个文件,进行一下融合{}
对象语法的配置是扩展性最高的,可以对入口进行更细颗粒度的配置,推荐使用这个配置
由于入口对象的配置比较少所以这里列一下然后简单说明下
- dependOn: 当前入口所依赖的入口
- filename:输出的文件名称
- import:启动时加载的模块
- library:制定library选项,为当前entry创建一个library
- runtime:运行时chunk的名字,设置了之后,会创建一个新的运行时
- publickPath:当输出文件被引用时,制定一个公共的URL地址
library
:这个东西要关注一下,我们不光可以使用webpack来进行应用工程的打包,也可以用webpack
来进行库的创作,而这个libraray
以及文档中很多关于它的相关配置,在创建一个库的时候便是极其有用的。
runtime
:将运行时文件单独打包出来为一个chunk,将运行时文件和我们的项目代码进行拆分,运行时文件大意其实就是一个在辅助浏览器通过mainfest
来进行解析、查找的功能块,具体这里不做分析,后期会专门做出一期来讲讲这个。
dependOn
:这个属性主要是为了将入口文件中共用的模块进行一个提取,算是一个优化操作,提取出来的模块不常改动,可以利用浏览器的缓存机制进行缓存,来提高我们的页面打开速度。
// a.js
export const add = (a, b) => a + b;
// main.js
import {add} from './a.js';
console.log(add(3,4));
// main2.js
import { add } from './a.js';
console.log(add(1, 2));
如上面的例子,在main.js
与 main2.js
中全部引用了a.js
, 这个时候我们就可以进行一个提取打包
entry: {
index: {
import: path.resolve(__dirname, '../src/index.js'),
dependOn: 'vender'
},
main: {
import: path.resolve(__dirname, '../src/entry/c.js'),
dependOn: 'vender'
},
vender: path.resolve(__dirname, '../src/entry/vender.js')
},
如此一来,我们会把两个文件中共用的文件进行额外打包。如下:
最终的bundle有三个。并且这样做会减少“index.js”和“main.js”的体积大小。
webpack入口的默认值是’./src/index.js’。
出口(output)
出口定义了当我们通过webpack
对于 chunk
一系列的打包工作完成后,根据自己的需求定义一下该如何输出,关于“出口”的配置项有非常之多,从功能上分为:位置、名称、方式等。从目标文件上分为:静态资源、入口模块、单独提取出的公共chunk等。这里我们仅仅做一个简单配置。
// webpack.config.js
module.exports = {
...
output: {
filename: '[name].[contenthash].js', // 输出文件名
path: resolvePath(__dirname, '../dist', // 输出文件的目标文件夹
clean: true, // 每次打包后是否清除dist
hashDigestLength: 5, // hash值的长度
assetModuleFilename: '[name][contenthash][ext] // 静态资源的文件名
}
...
}
webpack5.0版本新增了一个clean
是非常好用的,在之前的版本,我们每次打包工程的时候要通过 clean-webpack-plugin
来对目标文件夹的内容进行清除,现在通过这一个属性就可以做到,而且能够对其进行具体配置,通常情况下配置为 true
就可以。更详细的可以参考官方文档
hashDigestLength
则是对输出文件的那串hash值的长度进行了一个控制。
assetModuleFilename
是对一些静态资源文件名称配置了。
这里有个坑,如果你使用
webpack-dev-server
启动了一个服务的时候,不要在出口中配置clean
,否则会导致热更新生成的json
文件被清除,无法达到热更新效果
mode
当你配置好入口和出口之后,进行一个简单的打包,会发现如下提示
它的意思就是,您好,您当前的配置没有指定mode,我们为您默认指定了 production
, 请您为不同的环境设置进行mode的设定。
在一个项目从创建到上线,最少会有两个阶段。一个是“开发阶段”,我们在这里进行开发、调试、接口对接。还有一个就是“上线阶段”,也就是可以熬夜发版,然后坐等公司赚钱的阶段。这两个阶段对应两个环境就是“开发环境”和“生产环境”。
这里的 mode
就是我们去对应上述不同阶段(环境)的开关
-
在开发阶段,我们往往需要更明确的“source-map”文件,来定位错误,需要一个明确的本地环境模拟上线的环境。这些通过它都可以实现。
-
在“生产环境”我们则需要的是体积更小的文件,能去除无用代码的功能(tree shaking)
// webpack.config.js
module.exports = {
...
mode: 'development', // 开发环境(production:生产环境)
...
}
module
这个东西基本上是webpack配置的核心了,通过它我们能对不同类型的文件进行处理,并且最终来进行打包。大体的模版如下:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: '',
use: {
loader: ''
}
}
]
}
虽然看起来大致模版如此简单,但是实际上在 module.rules
中能配置的东西如此让人眼花撩乱。
在webpack5中,它为我们提供了一个 Asset Modules
,这是一个静态资源模块用来处理我们的静态资源。能让我们处理的更加简单。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.(jpg|png|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 *1024
}
},
generator: {
outputPath: 'images'
}
}
]
}
}
如上的代码其实就是配置了一个简单的处理图片资源的规则,它实现的是如果是10kb以下的图片资源采用base64的方式进行打包。否则则发送一个单独的文件导出。对外输出的文件夹是 images
相比webpack4我们不需要再去安装 url-loader
file-loader
来进行对静态资源的处理。在webpack5中,通过 type
的配置可以将静态资源打包成我们所需要的大多数模式。所有的 type
类型有如下几种:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。asset/inline
导出一个资源的 data URI。之前通过使用 url-loader 实现。asset/source
导出资源的源代码。之前通过使用 raw-loader 实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
另外啰嗦一下,对于 rules
中的 loader
处理方式是从后往前比如
// webpack.config.js
// 要先下载miniCssExtractPlugin插件
module.exports = {
modules:{
rules: [
{
test: /.scss$/,
// 提取css为单独的文件,可以不使用style-loader
use: [
isEnv() ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
require('autoprefixer')
]
]
}
}
},
'sass-loader'
]
},
]
},
plugins: [
new miniCssExtractPlugin()
]
}
上面是对于scss的一个配置,我们在项目中的scss样式文件,会先经过 sass-loader
解析成css文件,然后通过了 postcss-loader
的解析,增加不同内核前缀,然后才是 css-loader
解析css文件,我这里最后是通过配置了环境变量来判断,如果是开发环境,使用style-loader
, 正式环境进行了提取。
这样做的目的,是开发环境我们需要使用
style-loader
提供的开箱即用的hmr功能
我们会发现其实所有类型的文件配置的模板都差不多,可以举一反三,只不过不同类型的文件需要的 loader
有所区别,也有可能需要额外的一些配置,比如用 babel-loader
去配置js。
我们为什么要使用babel,其实很简单,由于浏览器的不统一,会造成我们的代码在不同浏览器支持效果不同,使用babel能实现向下兼容,并且,可以使用很多JavaScript新出的特性,而不用为浏览器是否支持而烦恼
在配置过程中发现,如果使用了vue-loader15,需要额外安装一个插件
vue-template-compiler
,而且要注意这个插件的版本要和vue
的版本保持一致
plugin
plugin
和 loader
有什么具体的不同呢?为什么 loader
可以做文件转换后,我们还需要使用到 plugin
而且,它也是作为了webpack支持之一。
根据我的理解,首先他们针对的目标不同,loader
主要针对的是文件,可以理解为一个转换器,我们需要通过它来进行不同文件的编译转换。而 plugin
其实更偏向针对的是webpack,大家应该对机器人很感兴趣,如果我给机器人安装枪,那么机器人可以开火射击,安装光剑那可以劈砍,类似的 plugin
就相当于于不同的接口安装在webpack上面,来帮助我们更好的达成我们的目标。
webpack的插件生态很完整也很全,出场率最高的就是 html-webpack-plugin
,这里就用简单用一下它,其余的铁子们上 npmjs 查找吧。
// webpack.config.js
// 记得要先下载 html-webpack-plugin
module.exports = {
...
plugins: [
new htmlWebpackPlugin({
template: resolvePath(__dirname, '../public/index.html'),
title: 'webpack5'
})
]
...
}
这个插件的作用就是自动帮我们生成 index.html
文件,并且将我们打包完成后的javascript文件以及css样式文件都放入其中。免去了我们手动添加和修改的麻烦过程。
externals
外部扩展的目的是为了从输出的 bundle
中排除依赖项,通常我们使用的包模块会一起打包到输出文件中,这会导致我们的输出文件非常的巨大,从而导致加长了加载的时间,最终会造成白屏时间过长的问题。通过使用externals
我们能将项目中引入的不变的包模块 vue
, react
等进行一下抽离。在运行时再去获取这些扩展依赖。
在 index.html
中我们可以引用外部的文件,例如 CDN
中。通过 script
标签将它们引入,然后在配置文件中进行配置
// index.html
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.js"></script>
// webpack.config.js
module.exports = {
...
externals: {
'vue': 'Vue',
}
}
这里的第一个 ‘vue’, 表示我们要引入 vue
的时候,所定义的代称。我们在文件中可能会这样使用, import vue from 'vue'
,后面的 ‘Vue’是表示 vue
模块所暴露出来的变量,可以去文件中查找。如此一来,我们就将 vue
模块从本应该在打包文件中的地方挪移出去了,减少了我们输出包的目标。
还有一个配置是 externalsType
, 指定了我们引用的类型,如上面的代码,我们发现还需要在 index.html
中配置一个 script
标签,如果我们想通过打包自动的去引入标签,这个时候就需要配置一下 externalsType: 'script'
然后修改一下 externals
配置。
// webpack.config.js
module.exports = {
...
externals: {
'vue': [
'https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.js',
'Vue'
],
}
}
如此一来我们就不需要在 index.html
中去配置外部链接了。通过 externalsType
我们可以定义很多种类型,具体请看:externalsType更多类型
不过这里遇到一个问题,还没有解决,如果通过
externalsType: 'script'
这种方式配置之后,在开启本地服务会报错。希望有明白的大佬能提供思路。
还需要注意一点,我们最好要把我们需要的模块放在项目所在服务器上,因为cdn也会挂掉,影响项目体验。
Resolve
resolve
配置可以让我们针对模块的解析进行颗粒度的配置,最常用的有两个选项。
- alias
- extensions
alias
解决一个路径问题,当我们的工作路径很深,在项目中引入文件写冗长的目录,既不美观,也不利于代码阅读。
// webpack.config.js
module.exports = {
resolve: {
alias: {
// 根据开头的项目目录定制
'@': path.resolve(__dirname, '../src')
}
}
}
这样我们在其他文件中就可以使用 import test from '@/views/test
的方式引入,实际上它引入的路径是 src/views/test
。
extensions
则是尝试按照定义的顺序解析后缀名。
// webpack.config.js
module.exports = {
resolve: {
extensions: ['.vue', '.js', '.json']
}
}
比如我们引入一个模块,但是不写后缀名的时候。 import test from 'test'
,它就会按照我们定义的后缀名顺序解析,先查找是否有 test.vue
, 然后是 test.js
, 最后是 test.json
。
Optimization
其实从webpack4开始,就会根据项目所设定的mode来执行不同的优化策略,但是webpack更是支持自定义的优化策略。
可以使用 splitchunks
来进行一个公共模块提取和缓存。
// webpack.config.js
module.exports = {
...
optimization: {
minimizer: [
'...',
new cssMinimizerWebpackPlugin()
],
splitChunks: {
cacheGroups: {
vender: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
}
...
}
minimizer
可以覆盖webpack默认的打包插件,使用自己喜欢的插件来进行替换,如果只替换其中一个,剩余的访问默认值的话, 就使用 ···
来占位替换。
splitChunks
中的配置,就是提取出在项目中引用 node_modules
的公共资源进行一个提取并放入缓存组,这样就可以将不改动的代码都缓存起来。
动态加载 | 预获取 | 预加载
这里还想提一下这个东西, 动态加载模块。通过esm的 import
来实现。动态加载会单独生成一个chunk进行输出。
// test.js
export let add = (a, b) => {
return a + b;
}
export let bdd = (a, b) => {
return a - b;
}
// main.js
const addFun = () => {
return import('@/entry/test.js').then(res => {
return res.add(1, 4)
});
}
addFun().then((res) => {
console.log(res)
})
我们在 entry
文件夹下创建了一个"test.js"文件,然后暴露出两个方法, 在入口文件进行引入。在打包的时候我们的打包目录就会多生成一个src_entry_test.js
文件这个名字是根据 output.filename
的规则定义的,如果我们想修改它的名字,可以添加注释。
// main.js
const addFun = () => {
return import(/*webpackChunkName: 'test'*/'@/entry/test.js').then(res => {
return res.add(1, 4)
});
}
如上述代码,就是将名称修改成了 test
。
动态加载对于我们的项目来说有什么好处呢,一个是可以实现懒加载,再者webpack也提供给我们了预加载和预获取的功能。
- prefetch(预获取):将来某些导航下可能需要用到的资源
- preload(预加载):当前导航下可能需要的资源
两者的不同点就在于一个是当前的导航,一个是未来的导航。它们也是可以加在魔法注释中。
// main.js --> prefetch
const addFun = () => {
return import(/*webpackChunkName: 'test',webpackPrefetch: true*/'@/entry/test.js').then(res => {
return res.add(1, 4)
});
}
如此一来,我们就可以预获取资源 test
了, 浏览器会在网络的空闲期去主动下载,而不会一股脑的在开头讲文件全部下载下来,这样可以减少页面的空白期。
如果想改成预加载,只需要添加注释 webpackReload: true
就可以了。效果和懒加载类似。
devtool
从名字分析这东西肯定和我们的开发阶段有关系,是的没错。在我们开发的过程中,有时候需要看页面源码,和我们开发一样的代码,而不是注入了webpack的代码,这时候我们需要使用它。 它有如下几种常用类型
- eval:每个module会封装到eval中执行,在末尾追加注释
- source-map:生成一个sourceMap文件
- hidden-source-map:和source-map一样,但是不会再末尾追加注释
- inline-source-map:生成一个DataUrl形式的sourceMap文件
- eval-source-map:每个module会封装到eval中执行,并且声称一个DataUrl形式的sourceMap
- cheap-source-map:生成一个没有列信息的sourceMap,不包含loader的sourcemap
- cheap-module-source-map:生成一个没有列信息的sourceMap,同事loader的sourcemap被简化为对应行。
sourcemap文件就是一个和我们源码的对照表,在我们开发过程中,项目出现什么问题需要定位的时候,会根据sourcemap文件中映射到我们真实的文件。通常我们在开发环境使用 cheap-module-source-map
比较好。
注意,这个配置只需要在开发环境进行配置,如果是生产环境要关掉
值改为false
。其原因有两点。
- 线上的产物通过sourcemap文件可能会被反编译出源码,有暴露的危险
- sourcemap文件的体积较大,和我们生产环境所追求的代码轻量不符合。
webpack-dev-server
在使用webpack开发的时候,必不可少的就是 webpack-dev-server
, 它为我们提供了一个本地服务的环境,模拟了一下用户访问时看到的项目最终运行效果。
// 下载webpack-dev-server
yarn add webpack-dev-server -D
// webpack.config.js
module.exports = {
...
devServer: {
// 提供的静态资源文件夹
static: _resolve(__dirname, '../dist'),
// 服务的端口
port: 9528,
// 是否开启热模块替换
hot: true,
// 自动打开浏览器
open: true,
client: {
// 当有错误的时候在客户端进行覆盖显示
overlay: true,
},
// 是否压缩代码
compress: true,
// 启用代理
proxy: {
'base/api': {
target: 'http://www.baidu.com',
// 是否跨域
secure: true
}
}
},
}
配置之后我们可以在package.json中进行一个命令行的简化配置
// package.json
{
"scripts": {
"start": "cross-env NODE_ENV=dev webpack server --config ./build/webpack.config.js",
"build": "cross-env NODE_ENV=pro webpack --config ./build/webpack.config.js"
},
}
配置之后,我们就可以通过 npm run build
来进行打包。通过 npm run s
来开启本地服务器。
这里设置了一个环境变量 NODE_ENV
,通过使用模块 cross-env
,这个模块的主要功能是兼容不同系统上设定环境变量的写法。设定之后,我们在 webpack.config.js
中就可以通过 process.env.NODE_ENV
来访问我们设定的值了。