# 记一次webpack使用loader与plugin

# 背景

遇到这样一个页面需求:主工程A中用iframe引入工程B,但B又需要使用A中注册的插件,都是用的vueB中代码大概如下:

let c = decodeURIComponent(location.search.split('=')[1]);
if(window.top.allInnerComponent){
    let components = Object.keys(window.top.allInnerComponent);
    components.forEach(componentName=>{
        Vue.component(componentName, window.top.allInnerComponent[componentName]);
    })
}
if(option){
    option.el = '#app';
    vm=new Vue(option)
}

其中,allInnerComponentA的全局变量,生成过程如下:

const requireComponent = require.context(
    './panel',    // 其组件目录的相对路径
    true,    // 是否查询其子目录 
    /Inner-[\w-]+\.vue$/,    // 匹配基础组件文件名的正则表达式
);
window.allInnerComponent = {};
requireComponent.keys().forEach((fileName) => {
    // 获取组件配置
    const componentConfig = requireComponent(fileName);
    // 全局注册组件
    window.allInnerComponent[fileName] = componentConfig.default || componentConfig;
    Vue.component(fileName, componentConfig.default || componentConfig);
});

这个方案的合理性姑且不论,它遇到一个显著的问题,A组件中的css样式,并没有包含在allInnerComponent之中。为什么呢?

仔细看vue的打包后代码,可以看出,vue是将组件的template代码以字符串的形式,写入到js里的,而组件的css部分,并不是与组件代码放一起,而是单独提取出来,最终与其它组件的css一起,要么生成单独的css文件,要么以css in js的形式,再用js显示到页面(调用的是vue-style-loader)。

也就是说,vuecss部分与templatescript是解耦的,唯一关联是如果使用scope标签,则会记录一个hash值,对应的DOM中会加入data-[hash]的属性,css也会相应添加,这样保障了样式的隔离。

如果能找到组件中css所存储的地方,那么问题就解决了。遗憾的是,我没看出来。只能选择一个笨办法,这时就用到了webpackpluginloader

vue解析后的css数据用loader拦截,用Object存储起来,再写个plugin,将得到的css对象赋给window全局变量,整个以字符串的形势,插入到html里。

# loader

存储代码css-parse

let map = {}

module.exports = {
    store(filename, css) {
        map[filename] = css;
    },
    getSource() {
        return map;
    },
    clear(){
        map = {};
    }
}

loader代码:

const cssParse = require('./css-parse');

module.exports = function (source) {
  const path = this.resource; //生成css文件的源路径拼接的字符串
  const reg = /src\\components\\panel\\(.*).vue\?vue&type=style/;
  const arr = path.match(reg);
  if (arr && source) {
    const fileName = arr[1];
    cssParse.store(fileName, source.replace(/[\r\n]/g, ''));
  }
  return source;
};

# 插件

const cssParse = require('./css-parse');
class VueCssPlugin {
    apply(compiler) {
        compiler.plugin('compilation', (compilation) => {
            // console.log('The compiler is starting a new compilation...');
            compilation.plugin(
                'html-webpack-plugin-before-html-processing',
                (data, cb) => {
                    const html = data.html;
                    const index = html.lastIndexOf(`</html>`);
                    let str = html.substr(0, index);
                    str += `<script>
                    window.allInnerComponentCss = ${JSON.stringify(cssParse.getSource())}
                    </script>`
                    str += `</html>`
                    data.html = str
                    // cb(null, data)
                }
            )
        })

    }
}
// 导出插件 
module.exports = VueCssPlugin;

有个插曲,我用webpack测试时,用的html-webpack-plugin的版本是4.5,而vue-cli项目中集成的是3.2,API不一样,报错,在github上找到对应版本,看了说明文档才搞定。

# vue配置

我写的loader,是需要使用到css-loader之前的。在webpack中配置大概如下:

module.exports = {
  resolveLoader: {
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules', './loader/'], 
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      title: 'My App',
      filename: 'public/index.html'
    }),
    new MyPlugin() //我的插件
  ],
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: [ 'vue-loader', ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader'
        ]
      },
      {
        test: /\.(js)$/,
        use: [
          'js-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'style-loader',
          'css-loader',
          'vue-css-loader' //我的loader,需要放到css-loader之后,其实加载顺序是从后往前的,这里也就是它先执行。如果有less-loader之类,就需要放中间了
        ]
      },
    ]
  }
};

对应到vue-config中,就是这样了:

const VueCssPlugin = require('./build/vue-css-plugin');

module.exports = {
    configureWebpack: {
        plugins: [
            new VueCssPlugin(), //我的plugin
        ],
    },
    chainWebpack: (config) => {
        config.resolveLoader.modules.store.add('./build/'); //本地loader目录
        config.module
            .rule('css').oneOf('vue').use('vue-css-loader').loader('vue-css-loader') //我的loader
            .end();
    }
};

# loader的顺序问题

上面的配置,只解决了vue文件中css的配置,而其它scsslessstylus之类,都没处理。

于是,需要在chainWebpack中增加如下内容:

config.module
            .rule('scss').oneOf('vue').use('vue-css-loader').loader('vue-css-loader') //我的loader
            .end();

但这样我们只能得到scss的字符串,并不是编译为css之后的。

怎么办呢?

使用vue ui找开项目,再在任务中找到inspect,点击运行,得到当前工程的webpack配置,会发现scss部分大概是这样的:

/* config.module.rule('scss') */
{
  test: /\.scss$/,
  oneOf: [
    /* config.module.rule('scss').rule('vue-modules') */
    {
      resourceQuery: /module/,
      use: [
       ... //忽略
      ]
    },
    /* config.module.rule('scss').rule('vue') */
    {
      resourceQuery: /\?vue/,
      use: [
        {
          loader: '/test/node_modules/mini-css-extract-plugin/dist/loader.js',
          options: {
            hmr: false,
            publicPath: '../../'
          }
        },
        {
          loader: '/test/node_modules/css-loader/dist/cjs.js',
          options: {
            sourceMap: false,
            importLoaders: 2
          }
        },
        {
          loader: '/test/node_modules/postcss-loader/src/index.js',
          options: {
            sourceMap: false,
            plugins: [
              function () { /* omitted long function */ }
            ]
          }
        },
        {
          loader: '/test/node_modules/sass-loader/dist/cjs.js',
          options: {
            sourceMap: false,
            prependData: '@import "@/assets/css/global_variable.scss";'
          }
        },
        {
          loader: 'vue-css-loader'
        },
      ]
    },
    /* config.module.rule('scss').rule('normal-modules') */
    { 
      test: /\.module\.\w+$/,
      use: [ //忽略
        ...
      ]
    },
    /* config.module.rule('scss').rule('normal') */
    {
      use: [ //忽略
        ...
      ]
    }
  ]
},

从上面可以看出,我们的vue-css-loader是在最下面,webpackloader加载顺序是从后往前,所以顺序需要调整。

怎么调整呢? 从官网上没找到例子,于是找断点,发现可以使用before

config.module.rule('scss').oneOf('vue')
  .use('vue-css-loader')
  .before('mini-css-extract-plugin')
  .loader('vue-css-loader').end();

这个花了较长时间,原因是我是复制页面上的webpack配置,再放到ide里查看,一直没有清控制台,没有发现其实自己已经调对了。 还是不够细心。

# 总结

这个方案还有个问题, components目录下的css的重复加载,浪费。

但总的讲,通过这个功能,学习了webpack的插件和loader,为下来另一个功能打下了基础。