使用webpack5来取代vue-element-admin中的vue-cil(webpack5实践)
还在使用传统webpack?一起来实战电子webpack5吧!
前言
从头开始总是最简单的,难的是接手一份已经成型的项目,就像让开发人员新建一个ssr的项目总是比将旧有项目改造为ssr来的更加轻松,使用webpack5来开发一个新项目可能只是几个命令行的事,而将一个成型的项目升级为webpack5则要踩无数的坑才能成型。
行文逻辑:先逐步踩坑production模式下正常运行,保证打包文件的准确性与可用性, 而后根据环境分离webpack配置文件,以及打包优化,中间夹杂叙述webpack5较过往不同的点,以及部分loader在新版与旧版的区别,最后将给出部分loader包版本数据。总是要先做,再看
文章更适合有基础的开发人员阅读,许多细小的点不会列出,例如使用loader前并不会赘叙如何
npm i loader, 不会解释path.resolve或者__dirname的具体释义,若有错误之处,还望留言斧正
版本相关信息
过程中使用的loader以及其他包均以当下能安装到的最新版为基准来使用
- nodejs: v16.16; npm: v8.15.1
- webpack: v5.74;webpack-cli: v4.10.0
命令配置
在这里使用最简单的webpack打包版本来实现初步的打包流程, 我们在package.json中加入"build": "webpack",来作为我们的打包启动命令
Production模式
对于一个简单的SPA,我们的设想是应该是只存在单纯的vue、sass、html、js、css,故而使用vue-loader来解析vue文件,使用sass-loader来解析scss的样式,初步写下如下配置文件配置好初始版的webpack.config.js来试运行
webpack打包默认会读取根目录下的
webpack.config.js文件作为配置文件,这是一个约定
module.exports = {
mode: 'production',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './bundle'),
filename: 'bundle-[name]-[contenthash:8].js',
},
module: {
rules: [
{
test: /\.(scss|sass|css)$/,
use: ['css-loader', 'style-loader', 'sass-loader']
},
{
test: /\.vue$/,
use: ['vue-loader']
},
]
},
plugins: [
// 通过模板创建html,并将打包出来的js注入html(最重要的)
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html'
}),
new VueLoaderPlugin()
]
}
除了上述配置文件,我们需要在根目录下新建index.html, 并写入<div id="app"></div>做为vue组件的挂载点
需要注意的是webpack的预处理器调用顺序是在匹配到对应文件后,按照use从后往前的顺序调用,css-loader负责的打包css文件, 而style-loader则负责将css作为style标签插入html文件中
紧接着我们直接运行打包命令来尝试打包,却出现了大量报错,接着一个一个处理
resove 文件别名以及后缀识别
Module not found: Error: Can't resolve '@/xxx' in 'xxx\vue-element-admin\xxx'
简单浏览报错信息过后,我们发现是admin项目中使用了'@'别名来作为src引入,导致webpack无法正确识别, 我们加入以下处理别名错误, 这是webpack4即存在的配置
...
resolve: {
extensions: ['.js', '.vue'], // 文件导入时省略的文件后缀从此处匹配
alias: {
'@': path.resolve(__dirname, 'src') // 别名, 将@符号作为src来解析路径
}
},
Asset Modules 静态资源处理
ERROR in ./src/icons/svg/xxx.svg Module parse failed: Unexpected token
对于第二类的静态资源文件处理,在webpack4的版本中,通常会使用file-loader作为预处理器来处理静态资源文件,亦或者使用url-loader将图片编译为base64来作为url直接插入目标文件中, 而在webpack5中, 内置了Asset Modules来作为图片、字体等资源的处理模块
- asset/resource: 类似file-loader,处理文件导入地址并将其替换为文件访问地址
- asset/inline: 类似url-loader, 将文件处理为Base64作为dataUrl直接插入目标导入处
- asset/source: 类似raw-loader, 以字符串的形式导出文件
- asset: 在resource和inline中自动选择, 小于8kb则使用asset/inline打包为dataUrl,反之则使用asset/resouce打包为访问地址
output: {
...
assetModuleFilename: 'img/[hash:6][ext][query]'
}
module: {
rules: [{
test: /\.(png|gif|svg)$/,
type: 'asset/resource' // webpack5新特性内置文件处理模块
}]
}
上述配置虽然能够达到我们的打包要求,但是为了资源优化考虑,我们依旧希望对于大小不同的资源采取不同的策略,所以有了如下修改
rules: [{
test: /\.(png|gif|svg)$/,
type: 'asset/resource', // webpack5新特性内置文件处理模块
parser: {
dataUrlCondition: {
maxSize: 6 * 1024 // 小于6kb的图片转为base64的dataurl
}
}
}]
nodejs polyfill in vue-element-admin
webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.
根据报错提示,vue-element-admin中居然使用了nodejs api,虽然不知道是出于什么考虑,我们需要为他加上一个polyfill来兼容此类api, 解决方法为引入一个NodePolyfillPlugin的插件
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
module.exports = {
...
plugins: [
new NodePolyfillPlugin()
]
}
jsx in vue with webpack
紧接着我们尝试再次build, 却发现vue-element-admin中的src/layout/components/Sidebar/Item.vue中居然使用了jsx的render语法来与渲染组件, 为了处理此类文件编译,我们使用babel来处理
module.exports = {
...
module: {
rules: [
...
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 缓存
presets: ['@babel/preset-env']
}
},
exclude: /node_module/
},
]
}
}
// .babelrc
{
"plugins": ["transform-vue-jsx"] // babel-plugin-transform-vue-jsx@3.7.0
}
此处暂时先不考虑语法降级以及浏览器兼容问题
sass-loader升级处理
本着用新不用旧的原则,在将sass-loader升级至13.0.2后,发现原有的一些旧语法是无法正常编译的, 为了尽量保持不降级的原则,我们只能对源文件进行修改,
自sass 1.33.0开始,其支持并推荐在scss文件中使用math.div(a /b)方法来计算值,同时将直接使用除法取值作为错误的语法在编译阶段报出
由此引发的问题是在vue-element-admin中src/components/MDinput中的scss样式使用除法导致编译失败,由此作出如下修改
<!-- src/components/MDinput -->
<style lang="scss" scope>
@use "sass:math";
/* padding: $spacer $spacer $spacer - $apixel * 10 $spacer/2; */
padding: $spacer $spacer $spacer - $apixel * 10 math.div($spacer, 2);
第二个错误则是sass文件的变量导出带来的,在style/variables.scss中使用了:export{ theme: $--color-primary; }来与js文件共享变量,如此即可以在vue中导入sass变量作为来注入js中。
// style/element-variables.scss
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
theme: $--color-primary;
}
而在原生的css module的规范中,是需要以.module.css后缀结尾的文件才能作为module来与js共享变量, vue-cli帮我们处理了这一步,而webpack没有,所以我们将目标文件修改为.module.scss后缀,同步修改导入此文件的文件
这样引出了另一个问题,当css文件后缀为.module.css结尾时,解析器将此文件视为模块化的文件,而不会去打包内部的样式代码,仅仅只会导出变量,所以在原有的基础上,我们需要新建一个element-ui.scss文件,将生效的css样式转移至此文件,然后在main.js中引入,
相关文档: css module
打包优化
splitChunk
这里直接采用源vue.config.js中的打包策略配置
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // 只打包初始时依赖的第三方
},
elementUI: {
name: 'chunk-elementUI', // 单独将 elementUI 拆包
priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
test: /[\\/]node_modules[\\/]element-ui[\\/]/
},
commons: {
name: 'chunk-commons',
test: path.resolve(__dirname, './src/components'), // 可自定义拓展你的规则
minChunks: 2, // 最小共用次数
priority: 5,
reuseExistingChunk: true
}}}}}
其他插件
webpack-bundle-analyzer
引入webpack-bundle-analyzer插件来为我们生成打包大小视图, 在打包目录生成report.html文件, vue-cli的打包参数--report也是基于此插件实现
const BundleAnaly = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
plugins: [
new BundleAnaly({
analyzerMode: 'static' // 使用生成静态文件的方式来
}),
}
clean-webpack-plugin
引入clean-webpack-plugin插件,在每次打包开始前, 先清空打包文件目录,避免多次打包文件堆叠
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin(),
}
mini-css-extract-plugin
将样式提取为单独的css文件,然后插入index.html中
const MiniCssPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
...
{
test: /\.(scss|sass|css)$/i,
use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
}]
},
plugins: [
...
new CleanWebpackPlugin(),
}
}
dev与production配置分离
Development模式
出去生产环境的打包,我们还需要实现开发环境下的预览、热加载等,为此我们基于webpack.merge, 将公共配置放在webpack.config.js,将生产、开发环境的配置分别置于webpack.prop.config.js、webpack.dev.conf.js中
webpack.merge是webpack社区提供的用于webpack配置合并工具
env
为了区分不同的环境, 我们使用cross-env插件包,为打包命令中注入环境变量
devServer
不同于webpack4,webpack5内置了hmr(模块热加载)插件,所以我们只需要引入webpack-dev-server并开启hot: true参数即可实现基本的功能, 鉴于原项目没有使用history模式的路由,这一点我们也无需配置
module.exports = {
mode: 'development',
devtool: 'eval-cheap-module-source-map', // 开启source-map
devServer: { // 使用了webpack-dev-server
open: true, // 编译完成后自动打开
hot: true, // webpack5会自动引入hot module replacement
// historyApiFallback: true, // html5history模式
port: 8088,
compress: true, // 是否开启静态资源gzip
static: {
directory: path.resolve(__dirname)
},
// publicPath: '/dist/', // web服务器资源读取 目录, /为默认值读取内存中
setupMiddlewares: require('./mock/mock-server-new.js')
},
...
}
mockServer
可以看到源文件是引入了mock-serve来模拟请求数据,但是自webpack v4.7开始,就抛弃了before字段,转而使用setupMiddlewares来作为中间件注入,所以我们新建了一个mock-server-new.js文件,并需要做出如下修改
// mock/mock-server-new.js
const { mocks } = require('./index.js')
const bodyParser = require('body-parser')
module.exports = (middlewares, devServer) => {
if (!devServer) {
throw new Error('dev-server setupMiddlewares is not defined')
}
// parse application/x-www-form-urlencoded
devServer.app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
devServer.app.use(bodyParser.json())
mocks.forEach(item => {
const { url, type = 'get', response } = item
devServer.app[type](new RegExp(`${url}`), (_, res) => {
const data = response instanceof Function ? response(_) : response
res.json(data)
})
})
return middlewares
}
// webpack.dev.config.js
module.exports = {
...
devServer: {
// before: require('./mock/mock-server.js')
setupMiddlewares: require('./mock/mock-server-new.js')
}
}
其他
不同的环境有不同的侧重点,主要区分有如下几点
- css: 开发环境中为了提升预览速度,我们不会提取css文件,而是使用
style-loader将样式作为标签直接插入html来提升我们的预览速度 - sourcemap: 开发阶段为了便于调试与查看报错栈,我们将启用devtool配置
- dev: 开发环境下的文件内容存在于内存中,所以我们无需CleanWebpackPlugin、BundleAnaly插件,他将仅仅存在于prop环境
最后附上完整的配置文件
部分包版本相关信息
| name | version | name | version |
|---|---|---|---|
| sass | 1.54.5 | sass-loader | 13.0.2 |
| babel-plugin-transform-vue-jsx | 3.7.0 | clean-webpack-plugin | 4.0.0 |
| vue-loader | 15.10.0 | vue-template-compiler | 2.6.10 |
| webpack-dev-server | 4.10.0 | webpack-merge | 5.8.0 |
结尾
其实文章尚遗漏了babel处理的一节,对于语法降级和浏览器兼容还是挺重要的一环,再写下去文章就显得有些累赘了,所以关于babel会另写一篇文章来叙述
整个改造过程收获颇丰,毕竟以前作为一个切图仔,就是在vue.config.js的基础上修修改改,整个改造过程也能够视为一种对vue-cli打包的解构过程