使用webpack5来取代vue-element-admin中的vue-cil(webpack5实践)

515 阅读6分钟

使用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)方法来计算值,同时将直接使用除法取值作为错误的语法在编译阶段报出

相关文档: sass语法更新, math支持更新

由此引发的问题是在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.jswebpack.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环境

最后附上完整的配置文件

all.png

部分包版本相关信息

nameversionnameversion
sass1.54.5sass-loader13.0.2
babel-plugin-transform-vue-jsx3.7.0clean-webpack-plugin4.0.0
vue-loader15.10.0vue-template-compiler2.6.10
webpack-dev-server4.10.0webpack-merge5.8.0

结尾

其实文章尚遗漏了babel处理的一节,对于语法降级和浏览器兼容还是挺重要的一环,再写下去文章就显得有些累赘了,所以关于babel会另写一篇文章来叙述

整个改造过程收获颇丰,毕竟以前作为一个切图仔,就是在vue.config.js的基础上修修改改,整个改造过程也能够视为一种对vue-cli打包的解构过程