4
头图

1. webpack loader

loader官方解释是文件预处理器,通俗点说就是webpack在处理静态文件的时候,需要使用 loader 来加载各种文件,比如: js文件需要使用babel-loader,html文件需要使用html-loader,css文件需要使用css-loader、style-loader等等。

loader编写原则

  • 单一原则: 每个loader只做一件事,使每个loader易维护,也可以在更多场景链式调用;
  • 链式调用: Webpack会从右到左(或从下到上)按顺序链式调用每个loader;

    • 最后的 loader 最早调用,将会传入原始资源内容,期望值是传出 code、 sourceMap(可选) 和 data(可选,例如AST)
    • 当 loader 需要返回多个结果时,必须返回undefined,使用

      this.callback(error: Error | null, source: string | Buffer, sourceMap?: SourceMap, data?: any);
    • 第一个 loader 最后调用,会传入前一个 loader 输出的结果

    顺序会受enforce: pre -> normal(默认) -> inline -> post影响

  • 统一原则: 遵循Webpack制定的设计规则和结构,输入与输出均为字符串,各个loader完全独立,即插即用;

loader实例

代码地址
在编写类似vant的移动端组件库时,作为展示用的vue页面其实是用md文档转的,这样做的好处是让markdown文档复用,同时兼顾页面展示和描述文件,自己参照@vant/markdown-loader写了个md-parser-loader,效果如图所示

image.png

中间那部分页面其实只是个README.md文档

image.png

router.js
动态加载markdown文件而不是vue文件

const routes = [
  {
    name: 'home',
    path: '/',
    component: () => import('../../README.md')
  },
  ...__config__.nav.map(i => i.items).flat().map(i => ({
    name: i.path,
    path: `/${i.path}`,
    component: () => import(`@/${i.path}/README.md`)
  }))
];

webpack.config.js

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.md$/,
        use: [
          'vue-loader',
          require.resolve('./md-parser-loader')
        ]
      }
    ]
  }
};

md-parser-loader/index.js
更多关于markdown的规则可以查看这篇文章编写markdown-it的插件和规则

const { getOptions } = require('loader-utils');
const MarkdownIt = require('markdown-it');
const CardWrapper = require('./card-wrapper');
const highlight = require('./highlight');

function wrapper(content) {
  content = CardWrapper(content);
  content = escape(content);
  return `
    <template>
      <section v-html="content" v-once />
    </template>
    
    <script>
    export default {
      created() {
        this.content = unescape(\`${content}\`);
      },
    };
    </script>
  `;
}

module.exports = function (source) {
  // loader-utils包提供了许多有用的工具,但最常用的是获取传递给loader的选项
  const options = getOptions(this) || {};
  const parser = new MarkdownIt({
    html: true,
    highlight,
    ...options
  });
  return wrapper(parser.render(source));
};

md-parser-loader/highlight.js

// 标签code代码块高亮
const hljs = require('highlight.js');

module.exports = function highlight(str, lang) {
  if (lang && hljs.getLanguage(lang)) {
    return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
  }

  return '';
};

md-parser-loader/card-wrapper.js

// 美化h3标签 转为卡片式
module.exports = function cardWrapper(html) {
  const group = html.replace(/<h3/g, ':::<h3').replace(/<h2/g, ':::<h2').split(':::');

  return group.map(fragment => {
    if (fragment.indexOf('<h3') !== -1) {
      return `<div class="card">${fragment}</div>`;
    }

    return fragment;
  }).join('');
};

2. webpack plugin

plugin监听Webpack运行生命周期的所有事件,在合适的时机通过Webpack提供的API改变输出结果。大家熟知的BundleAnalyzerPlugin即是监听done事件后分析各个打包文件的大小。

plugin由以下组成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

Compiler 和 Compilation

  • compiler对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

plugin实例

在实践Hybrid App离线包的过程中,需要监听emit钩子(after生成资源&before输出到目录),在每次compilation版本构建时遍历对象的assets属性读取资源并记录在一个资源映射的json文件里。App端通过拦截该资源对应的服务端请求(remoteUrl),并根据相对路径(path)从本地命中相关资源然后返回,以避免webview请求远程静态资源达到优化白屏时间的目的。

资源JSON图示
image.png

打包结果图示
image.png

offline-package-plugin/index.js

const { lookup } = require('mime-types');

class OfflinePackagePlugin {
  constructor(options) {
    this.options = Object.assign({
      packageName: 'packageName',  // 包名,区分不同项目
      version: +new Date(), // 版本号,若不同,进行bsdiff算法计算出差分文件列表
      baseUrl: 'http://localhost:8080/', // 静态资源域名
      fileTypes: ['html', 'js', 'css', 'png'], // 需要配置的静态资源后缀
    }, options);
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('OfflinePackagePlugin', (compilation, callback) => {
      const content = {
        packageName: this.options.packageName,
        version: this.options.version,
        items: []
      };

      for (const filename in compilation.assets) {
        if (
          !this.options.fileTypes.some(item => {
            return new RegExp(`\\.${item}$`).test(filename)
          })
        ) {
          continue;
        }

        content.items.push({
          packageName: this.options.packageName,
          version: this.options.version,
          remoteUrl: this.options.baseUrl + filename,
          path: filename,
          mimeType: lookup(filename)
        });
      }

      // stringify tab: 2
      const outputFile = (manifest => {
        return JSON.stringify(manifest, null, 2);
      })(content);
      // create offline-package.json
      compilation.assets['offline-package.json'] = {
        source: () => {
          return outputFile;
        },
        size: () => {
          return outputFile.length;
        }
      };

      // todo: add option isNeedGzip to compress
      callback();
    });
  }
};

module.exports = OfflinePackagePlugin;

webpack.config.js

const OfflinePackagePlugin = require('./offline-package-plugin');

module.exports = {
  //...
  plugins: [
    new OfflinePackagePlugin({ packageName: 'demo' })
  ]
};

3. 官网

如何编写一个loader
如何编写一个plugin


小皇帝James
600 声望7 粉丝

IT吴彦祖