编写webpack插件,解决几个小问题

1,128 阅读3分钟

背景

系统是基于qiankun的微前端框架,有父应用和多个子应用,我负责父应用,其他业务线负责各自的子应用。

问题A - box-sizing 样式污染

大部分子应用都引入了antdProLayout会有样式重置文件;但是有一个子应用(Vue2)引入的是ElementUI,默认没有样式重置文件,也没有另外引入样式重置文件。

我们知道box-sizing的值可以是border-boxcontent-box,且默认为content-box。 父应用antd的layout会把所有的 box-sizing 变为 border-box,源码如下:

.ant-layout, .ant-layout *{
    box-sizing: border-box;
}

子应用没有设置全局的box-sizing,使用的是默认的content-box,只有在需要时,才显式设置 box-sizing: border-box;

但是在我们父应用的全局影响下,全部样式的box-sizing都变成了border-box,导致子应用的盒子模型全部乱了,宽高全部不对,可以脑补一下画面。

我出了几次方案修改子应用都没成功,我意识到只能把源码的那句话删了才行,也不会影响其他子应用,又可以完美解决这个特殊子应用的问题。于是就写了一个插件来处理。

因为我们作为父应用是改过antd前缀的,所以加了一个变量来处理。

config/config.ts

// config/config.ts
import webpackPlugin from './plugin';
export const ANT_PREFIX_CLS = 'bapAnt';
export default defineConfig({
  antd: {
    config: {
      prefixCls: ANT_PREFIX_CLS,
    },
  },
  lessLoader: { modifyVars: { '@ant-prefix': ANT_PREFIX_CLS } },
  chainWebpack: webpackPlugin,
  // ... 其他配置
})

config/plugin.ts

// config/plugin.ts
const DeleteCssPlugin = require('./DeleteCssPlugin');
import * as IWebpackChainConfig from 'webpack-chain';

const webpackPlugin = (config: IWebpackChainConfig) => {
  config.plugin('DeleteCssPlugin').use(DeleteCssPlugin);
  // ... 其他配置
};
export default webpackPlugin;

DeleteCssPlugin.ts

// config/DeleteCssPlugin.ts
const { ConcatSource } = require('webpack-sources');
import { ANT_PREFIX_CLS } from './config';
const pluginName = 'DeleteCssPlugin';

// 删除特定的CSS
class DeleteCssPlugin {
  apply(compiler: any) {
    // 代表开始读取 records 之前执行
    compiler.hooks.afterCompile.tapAsync(pluginName, (compilation: any, callback: () => void) => {
      Object.keys(compilation.assets).forEach((filename) => {
        // 找到这个文件:layouts__BasicLayout.f800a130.chunk.css
        // 也可以用正则:/^layouts__BasicLayout\..+\.chunk\.css$/
        if (/^layouts__BasicLayout\..+\.chunk\.css$/.test(filename)) {
          console.log('1:', filename);
          compilation.updateAsset(filename, (source: typeof ConcatSource) => {
            // source.source() 获取源码
            const deleteCss = `.${ANT_PREFIX_CLS}-layout,.${ANT_PREFIX_CLS}-layout *{box-sizing:border-box}`;
            const input = source.source().replace(deleteCss, '');
            return new ConcatSource(input);
          });
        }
      });
      callback();
    });
  }
}

module.exports = DeleteCssPlugin;

一顿操作过后,一切都搞定了!

如果你为我为什么知道替换哪个文件,因为浏览器F12的时候,知道是哪个/哪些文件影响了样式,这里只找到一个文件,直接把这个文件的样式替换掉就行。这里是直接去掉。

问题B - 子应用播放器报错

微应用独立运行时没有问题,接入到父应用下报错,报错来源是微应用里面引入得到第三方的播放器源码。

Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': 
The node before which the new node is to be inserted is not a child of this node.

video.js 里面有一句源码 var head = $('head');(见下面代码块),这个在子应用独立运行时是没问题的,但是在qiankun的模式下,子应用没有自己的head,通过这句话获取的是父应用的head,父子应用在同一个上下文,全局只有一个head

this.styleEl_ = createStyleElement('vjs-styles-dimensions');
var defaultsStyleEl = $('.vjs-styles-defaults');
var head = $('head');
head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);

只需要把转换一下思维,把var head = $('head');替换为 var head = defaultsStyleEl.parentNode;即可。

编写插件 videojsPlugin.js

// filename: ./webpack-plugins/videojsPlugin.js
// 专门处理videojs,在乾坤下报错的BUG:
// Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.

const {
  ConcatSource
} = require('webpack-sources')
const pluginName = 'VideoJsPlugin'
// console.log('pluginName:', pluginName)

class VideoJsPlugin {
  apply(compiler) {
    compiler.hooks.afterCompile.tapAsync(pluginName, (compilation, callback) => {
      // console.log(Object.keys(compilation.assets))
      Object.keys(compilation.assets).forEach((filename) => {
        // 找到这个文件:static/js/4.4f3f2cc46c91e7e88078.js
        // 也可以用正则:/^static\/js\/.+\.js$/
        if (/^static\/js\/.+\.js$/.test(filename)) {
          // console.log('找到的filename:', filename)
          compilation.updateAsset(filename, (source) => {
            // console.log(source.source()); // 获取ast
            const input = source.source().replace(`var head = $('head');`, `var head = defaultsStyleEl.parentNode;`)
            return new ConcatSource(input)
          })
        }
      })
      callback()
    })
  }
}
module.exports = VideoJsPlugin

这个子应用是vue写的,在vue.config.js里面配置:

const VideoJsPlugin = require('./webpack-plugins/videojsPlugin')
module.exports = {
  configureWebpack: (config) => {
    config.plugins.push(
      // ... 其他插件
      new VideoJsPlugin(),
    )
  }
}

完美解决!

最后

如果有帮到您,记得点赞哈~