微前端(乾坤)项目下,如何使用 dllplugin 打包抽取公共依赖及其中遇到的问题

本文探讨了如何利用Webpack的externals和dll插件来优化大型项目中的依赖管理,包括DLL配置、自动引入dll、子应用适配及DLL打包注意事项。遇到的问题如沙箱干扰、版本冲突和体积控制也一并解析。
摘要由CSDN通过智能技术生成

前言

对于大型的项目,往往都需要抽取公共部分的依赖,避免依赖重复加载。而在抽取公共部分插件的时候,我们可以考虑使用 externals【Webpack的externals的使用.note 】
可是 externals 前提是依赖都要有 cdn 或 找到它对应的 js 文件,例如:jQuery.min.js 之类的,也就是说这些依赖插件得要是支持 umd 格式的才行。
而 dll 插件可以帮助我们直接将已安装好的依赖在 node_module 中打包出来,结合 add-asset-html-webpack-plugin 插件帮助我们将生成打包好的 js 文件插入到 html 中(也不需要我们像 externals 那样自己手动写 script 引入)

1、安装需要的插件

三个插件

  • webpack-cli // 一般项目已经安装有了
  • add-asset-html-webpack-plugin // 用于通过script 标签,动态插入 依赖 到 html 中
  • clean-webpack-plugin // 清空文件夹
npm install webpack-cli@^3.2.3 add-asset-html-webpack-plugin@^3.1.3 clean-webpack-plugin@^1.0.1 --dev

2、编写 dll 配置相关的文件

在根目录下(路径可以自己定),新建 webpack.dll.conf.js 文件

// webpack.dll.conf.js

// 引入依赖
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // 清空文件用的
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 压缩代码用的

// 具体配置
module.exports = {
  mode: 'production', // 告诉 webpack 当前环境为生产环境
  // 入口 dll 自定义模块名字: [依赖1,依赖2,...] 
  entry: {
    vue: ['vue', 'vue-router', 'vuex'], // 打包 vue、vue-router、vuex依赖打包到一个叫 vue 的dll模块中去
    elementui: ['element-ui'],
    vendor: ['axios']
  },
  // 出口
  output: {
    filename: 'dll.[name].js', // 其中[name]就是entry中的dll模块名字,因此filename就是dll.vue.js
    path: path.resolve(__dirname, './dll/js'), // 输出打包的依赖文件到dll/js文件夹中
    library: '[name]_library'// 暴露出的全局变量名,用于给 manifest 映射 
  },
  plugins: [
    // 重新打包时,清除之前打包的dll文件
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, './dll/**/*')] // ** 代表文件夹, * 代表文件
    }),
    // 生成 manifest.json 描述动态链接库包含了哪些内容
    new webpack.DllPlugin({
      // 暴露出的dll的函数名;此处需要和 output.library 的值一致
      // 输出的manifest.json中的name值
      name: '[name]_library',
      context: __dirname, // 在项目主要的配置中需要和这保持一致
      // path 指定manifest.json文件的输出路径
      path: path.resolve(__dirname, './dll/[name]-manifest.json'),  // DllReferencePlugin使用该json文件来做映射依赖。(这个文件会告诉我们的哪些文件已经提取打包好了)
    }),
    new BundleAnalyzerPlugin(),// 压缩
  ]
}

3、打包生成 dll

在 package.json 中加入如下命令

"scripts": {
    "build:dll": "webpack --config webpack.dll.config.js"
}

运行该指令后就会执行第二步中 dll 的配置生成 dll
在这里插入图片描述

4、在项目主要配置中忽略已编译的文件

为了节约编译的时间,这时间我们需要告诉 webpack 公共库依赖已经编译好了,减少 webpack 对公共库的编译时间。在项目根目录下找到 vue.config.js ( 没有则新建 ),配置如下:

//  vue.config.js
const webpack = require("webpack");

module.exports = {
      ...
      configureWebpack: {
         ['vue', 'elementui', 'vendor'].map(item => 
            config.plugins.push(
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require(path.resolve(__dirname, `./public/dll/${item}-manifest.json`))
              }),
          )
      );
     }
 }

5、index.html 中引用生成的 dll.js 文件

经过上面的配置,公共库提取出来了,编译速度快了,但如果不引用生成的 dll 文件,那项目的依赖其实是没有被引入的,会报错。
可以打开 public/index.html,手动插入 script 标签。

<script src="./dll/js/vendor.dll.js"></script>

到此公用库提取完成,但总觉得最后一部手工插入 script 不太优雅,下面介绍下如何自动引入生成的 dll 文件。
打开 vue.config.js 在 configureWebpack plugins 配置下,基于第 4 步的 configureWebpack 继续配置 add-asset-html-webpack-plugin

// vue.config.js 
// 已经引入所需依赖
const path = require('path')
const webpack = require('webpack')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

module.exports = {
      ...
      configureWebpack: {
         ['vue', 'elementui', 'vendor'].map(item => 
            config.plugins.push(
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require(path.resolve(__dirname, `./public/dll/${item}-manifest.json`))
              }),
          )
      );
      config.plugins.push(
        new AddAssetHtmlPlugin({
             // 引用的dll.js文件位置
             filepath: path.resolve(__dirname, './public/dll/js/*.js'),
             // dll 引用路径 对dll静态资源引用的位置
             publicPath: './dll/js/*.js'),
             // dll最终输出的目录 打包后具体在dist下的文件位置
             outputPath: './dll/js/*.js'),
             includeSourcemap: false
           })
      )
     }
 }

配置完第五步后,重启项目,刷新浏览器页面就会看到出现我们刚刚设置的这三个js引用。
在这里插入图片描述

6、子应用使用这些公共依赖

以 微前端 为项目背景,上诉的五步都是在 主应用 中配置的,而在 子应用(需要使用公共依赖的应用) 中,我们的配置就相对简单很多。
只需在对应的 子应用 的 webpack 配置中加上第四步的配置,告诉 webpack 忽略这些依赖,并根据 manifest 去映射这些依赖。

//  vue.config.js

module.exports = {
      ...
      configureWebpack: {
         ['vue', 'elementui', 'vendor'].map(item => 
            config.plugins.push(
            new webpack.DllReferencePlugin({
                context: __dirname,
                // 注意路径别写错
                manifest: require(path.resolve(__dirname, `../../main/public/dll/${item}-manifest.json`))
              }),
          )
      );
     }
 }

7、按需依赖的引入(要不要得看情况)

以 ant-design-vue 为例,一般组件库按需依赖如下:

import Button from 'ant-design-vue/lib/button';
import 'ant-design-vue/dist/antd.css'; //css 样式就直接引入吧

原本就要求按需引入,这样我们就不适合直接将整个 ant-design-vue 打包进 dll 模块了,所以 webpack.dll.conf.js 中的入口就要修改为:

// 入口 dll模块名字: [依赖1,,依赖2,,...] 
entry: {
    vendor: ['ant-design-vue/lib/button']
},

dllplugin 的缺点:

因为使用公共依赖,意味着所有使用公共依赖的应用,必须使用同版本的依赖,其实我觉得这不算缺点,算是强制规范了依赖的版本,毕竟项目多的时候更要统一依赖的版本,所以独立开发子应用的时候,安装公共的依赖时,版本号一定要一致。例如:echarts 依赖,主应用是 5.1.0,而子应用是 4.1.0,就会因为版本不同的 echarts 不同的引用方式,导致依赖子应用无法引用主应用的公共依赖而报错,使得子应用无法启动。

过程中遇到的问题:

1、子应用在使用公共依赖的时候,有时候会莫名的报错些错误,虽然我不是很清楚到底是怎么回事。
例如使用了 highcharts (对 highcharts 的引用都是一样的),应用 a 使用是正常,应用 b 却会报奇怪的错误,而应用 c 更离谱,直接无法启动子应用了。。。
这三个引用的 dll 依赖一致都是:
在这里插入图片描述
应用b的报错:
在这里插入图片描述
应用c的报错:
在这里插入图片描述
经过很多次的重复尝试,通过查找和排除等手段,发现应用 b 只是修改了 dll 模块的顺序,简单的把highcharts 移到第一位,应用 b 就一切正常了!(什么鬼!!!)
在这里插入图片描述
然后应用 c 也尝试更换顺序,把highcharts 移到第一位,也正常了。。。

2、引用 antd 组件库,表格的全选功能有问题。一般情况下页面表格的使用是正常的,但是表格是在一个弹窗里的,点击全选,打印数据是可以的,也就是说的的确确有全选,但是 checkd 的类名没加上去,看着好像没有全选。而且我去掉子应用的 antd 对 dll 的引用,改成使用子应用 node_module 中的 antd 就没问题,所以这问题的确和 dll 模块的打包有关。(主应用的 antd 是1.7.5,子应用的是1.7.2)
在这里插入图片描述
为了确认是否和 antd 的版本有无关系,我把子应用的 antd 升级到 1.7.5 ,然后使用这个1.7.5的依赖,结果是正常的(那说明和1.7.5版本无关系咯?)。但后面我将子应用改回使用 dll 依赖,并且把主应用的 antd 组件库的版本改成 1.7.2 版本,再重新打包 dll 后(每次重新打包 dll 我都会重新启动子应用的服务),结果就解决问题了。所以说问题是和 1.7.5 版本有关还是无关,这很难说。

3、使用dll打包后项目体积没变化
其实在子应用使用 dll 忽略提取的依赖时,重点还是会去看 dll 生成的 xxx-manifest.json 映射文件,如果映射包含了子应用开发时所使用依赖的范围(依赖的版本一致),那子应用才会忽略 dll 提取的依赖,从而减少打包后的体积。如果打包后体积没变化,大概率是子应用或需要忽略dll打包依赖的应用,它的依赖和打包依赖的映射不一致,所以我项目的公共依赖设计如下:

-根目录
-主应用
    -node_modules // 主应用非公共的依赖
-子应用文件夹
    -子应用a
        -node_modules // 只安装子应用a非公共的依赖
    -子应用b
        -node_modules // 只安装子应用b非公共的依赖
-根目录下的node_modules // 公共依赖都npm安装在这    

根据依赖查找机制(先查找当前文件 node_modules,之后再一层一层往外找,直到全局的npm依赖)和把非公共依赖变为 devDependencies,公共依赖变为 dependence,如此设计,不仅方便管理项目的依赖(保证公共依赖开发使用是一样的),而且开发时也只需 npm install --only=dev 安装 devDependencies 的依赖即可,只有子应用独立部署的时候才需要 npm install 安装全部依赖。

问题总结:

  1. 遇到资源加载相关的问题,可以试试调整一下,dll 模块在子应用的插入顺序,即上面第六步的数组里的顺序。
  2. 对于一些应用的依赖,出现功能上的问题,那就试试保持和子应用开发时的版本一致再重新打包 dll 依赖。

总结:

externals 和 dll 对于多应用的大项目是很有必要的,因为重复的依赖被提取出来可以极大的缩减打包后的体积,优化项目的加载,至少在我所开发的微前端项目中,经过测试,体积比原来减少了一半。但是对于单个应用的项目来说可能作用不大,最多减少打包时间,但是体积可能变化不大,毕竟该要的依赖还是会有,大小很难会变,而有依赖重复使用的情况就可以考虑使用这两个方法去优化,减少包的体积。

dll 打包的时候记住,不用每次特意去执行 dll 的打包,因为 dll 打包的模块大多数都是第三方不变的模块,一般只要打包一次,存放在固定位置(例如:主应用的public下)一起提交代码就行,还可以减少 run build 时对依赖的打包时间,而在每次修改 dll 的依赖时才需要重新打包 dll,而且重新打包 dll 后,记得给子应用重启服务,要是 dll 依赖有增删,也记得增删子应用的 dll 模块。

问题补充

提取公共依赖后,qiankun框架的沙箱被破坏,导致不同子应用中的全局 filter、component、mixin 等相互干扰问题
问题背景
在子应用 A 中有个过滤器函数 byteToSize,但是应用 A 没有用到。子应用 B 中也有这个函数,在某个表格中用到。两个应用都是将其 filter函数 注册为全局的 filter 挂载在 Vue 实例下。打包上线后发现,只要先打开子应用 B ,然后再打开子应用 A,再切回子应用 B 就会发现 B 用的却是 A 的 byteToSize 过滤器。

感觉好像因为两个 byteToSize 函数都是全局函数,后写的子应用 A 的 byteToSize 覆盖了前面的子应用 B 的 byteToSize ,但两个应用之间不是沙箱隔离吗?为什么后面的应用代码会影响到前面的应用代码?莫非无论主应用还是子应用,它们vue.use、filter、component等注册过的插件是共享的?

1、验证和复现问题
如果所有应用的vue是共享(认为是同一个),那应用中的 vue.use 注册插件,事实是已经注册好多边了。为了证实这个观点,我去掉了某个子应用中,例如 antd 这些插件的应用,结果重新打包后,打开这个子应用的页面发现一切是正常的,也就是说子应用中的ui组件库 antd 插件其实都不需要再注册了。当然这种样式类的插件被重复注册没啥影响,但是其它问题中的 filter 或是全局的 components 很容易因为重复 use 注册而导致覆盖冲突污染。
按上面分析得出的结果来说,后面注册的插件应该会覆盖前面,那每次点开一个子应用,不都应该加载自己当前的 filters、components 之类的吗?于是我在每个子应用的 main.js 中添加了打印的代码,发现只有首次加载子应用的时候,它才会打印。也就是说我之前理解的微前端中子应用卸载,其实并不是完完全全的将子应用干掉,然后毫无保留,而是卸载掉 vue 的实例,之后的子应用非首次加载,甚至都不会再执行一遍 main.js 中的代码,同域的微前端的子应用真的就和一个vue项目没两样(不禁在想,如果不是要跨域部署前端项目的话,好像还真的没必要使用微前端的技术,毕竟 vue 框架这些本来就已经是模块化开发了。可同域的微前端起码比不用微前端的模块化开发更好的拆卸分离模块,这也无可否认)。
2、问题原因
而当我去除子应用的 dllplugin 配置后,重新打包查看系统,控制台就会报

Uncaught TypeError: Cannot redefine property: $router

错误,因为主应用用 script 标签引入了 vue-router ,那子应用的 vue.use 就不能再用自己 node_modules 依赖里面的 vue-router ,只能用主应用的 script 标签的 vue-router,否则就会因为同个 vue 下重复定义 $router 而报错。
无奈,我只能取消主应用公共依赖的配置,还原成不使用公共依赖前的配置,然后打包后发现这个问题没有了。若依赖以主应用的script标签引入的方式的,那乾坤的 import-html-entry 下,是无法隔离子应用和主应用这些公共依赖的js,因为它们毕竟共处一个 window 下,公共依赖中被注册成全局的变量都是共享的,不像iframe那种会有很强的JS沙箱隔离。
3、解决问题
其实无非是 vue 这个作为公共依赖,会导致应用互相干扰。如果我把 vue 这个依赖从公共依赖中完全剔除掉,会不会就可以解决了?可当我去除后,发现控制台还是会报

Uncaught TypeError: Cannot redefine property: $router

错误。这就很奇怪了。于是我尝试把与 vue 相关的公共依赖都去掉,甚至试过只留下一个 echarts 公共依赖,都无法去掉这个报错。明明 vue 和 vue-router 这些都没有再作为 index.html 的 script 标签引入,可还是会报这个错!只有子应用不用 DllReferencePlugin 的时候,才不会报这个错。感觉 DllReferencePlugin 好像做了什么特殊的操作,让不同子应用的 js 上下文作用域扩大到整个系统。现在看起来,这个 DllReferencePlugin 似乎打破了qiankun 框架的 js 沙箱。

最后还是在qiankun的GitHub提问区中得到解决的思路:
在这里插入图片描述
总而言之就是,要么放弃公共依赖的提取,要么把 vue2.x 升级到 vue3.x 。vue3.x 用的是 createApp 通过实例注册,这样就不会有全局污染的情况,这是最好的解决办法。至于修改 filter 函数名为唯一,我觉得不是解决根本的办法,方法名能唯一,但是 minxin 这些全局的是没有名字的,它只会在所有应用所有页面被混入,这很糟糕。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值