铁子们好我是跑不快的猪,新的一年,新的开始,先预祝各位都有华丽丽的变身。
本篇文章主要进行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
module.exports = {
// 关于webpack的配置写在这里
}
至此,准备工作已经完成了,这里要注意:运行webpack5的node版本最低要是10.13.0(LTS)
在mac上使用
n
模块,在windows使用nvm
模块可以更快更方便的切换node版本
关于工程目录没有什么可说的,构建一个清晰的,模块化的并且操作起来方便的工程目录即可,本篇中使用的工程目录如下
开始正式进入正题,当我们需要使用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]
多用于将多个文件打包成一个文件,进行一下融合{}
对象语法的配置是扩展性最高的,可以对入口进行更细颗粒度的配置,推荐使用这个配置由于入口对象的配置比较少所以这里列一下然后简单说明下
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’。
出口定义了当我们通过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,我们为您默认指定了 production
, 请您为不同的环境设置进行mode的设定。
在一个项目从创建到上线,最少会有两个阶段。一个是“开发阶段”,我们在这里进行开发、调试、接口对接。还有一个就是“上线阶段”,也就是可以熬夜发版,然后坐等公司赚钱的阶段。这两个阶段对应两个环境就是“开发环境”和“生产环境”。
这里的 mode
就是我们去对应上述不同阶段(环境)的开关
在开发阶段,我们往往需要更明确的“source-map”文件,来定位错误,需要一个明确的本地环境模拟上线的环境。这些通过它都可以实现。
在“生产环境”我们则需要的是体积更小的文件,能去除无用代码的功能(tree shaking)
// webpack.config.js
module.exports = {
...
mode: 'development', // 开发环境(production:生产环境)
...
}
这个东西基本上是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
和 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样式文件都放入其中。免去了我们手动添加和修改的麻烦过程。
外部扩展的目的是为了从输出的 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
配置可以让我们针对模块的解析进行颗粒度的配置,最常用的有两个选项。
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
。
其实从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也提供给我们了预加载和预获取的功能。
两者的不同点就在于一个是当前的导航,一个是未来的导航。它们也是可以加在魔法注释中。
// main.js --> prefetch
const addFun = () => {
return import(/*webpackChunkName: 'test',webpackPrefetch: true*/'@/entry/test.js').then(res => {
return res.add(1, 4)
});
}
如此一来,我们就可以预获取资源 test
了, 浏览器会在网络的空闲期去主动下载,而不会一股脑的在开头讲文件全部下载下来,这样可以减少页面的空白期。
如果想改成预加载,只需要添加注释 webpackReload: true
就可以了。效果和懒加载类似。
从名字分析这东西肯定和我们的开发阶段有关系,是的没错。在我们开发的过程中,有时候需要看页面源码,和我们开发一样的代码,而不是注入了webpack的代码,这时候我们需要使用它。 它有如下几种常用类型
sourcemap文件就是一个和我们源码的对照表,在我们开发过程中,项目出现什么问题需要定位的时候,会根据sourcemap文件中映射到我们真实的文件。通常我们在开发环境使用 cheap-module-source-map
比较好。
注意,这个配置只需要在开发环境进行配置,如果是生产环境要关掉
值改为false
。其原因有两点。
- 线上的产物通过sourcemap文件可能会被反编译出源码,有暴露的危险
- sourcemap文件的体积较大,和我们生产环境所追求的代码轻量不符合。
在使用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
来访问我们设定的值了。
https://gitee.com/monkey__wh/webpack_five