无界微前端源码解析:插件系统

32 阅读2分钟

无界微前端源码解析:插件系统

深入分析 loader 和 hook 机制,理解如何扩展无界的能力。

插件类型

// packages/wujie-core/src/index.ts
export interface plugin {
  // HTML 处理
  htmlLoader?: (code: string) => string;
  
  // JS 处理
  jsExcludes?: Array<string | RegExp>;      // 排除列表
  jsIgnores?: Array<string | RegExp>;       // 忽略列表
  jsBeforeLoaders?: Array<ScriptObjectLoader>;  // 前置脚本
  jsLoader?: (code: string, url: string, base: string) => string;  // 代码处理
  jsAfterLoaders?: Array<ScriptObjectLoader>;   // 后置脚本
  
  // CSS 处理
  cssExcludes?: Array<string | RegExp>;     // 排除列表
  cssIgnores?: Array<string | RegExp>;      // 忽略列表
  cssBeforeLoaders?: Array<StyleObject>;    // 前置样式
  cssLoader?: (code: string, url: string, base: string) => string;  // 代码处理
  cssAfterLoaders?: Array<StyleObject>;     // 后置样式
  
  // 事件钩子
  windowAddEventListenerHook?: eventListenerHook;
  windowRemoveEventListenerHook?: eventListenerHook;
  documentAddEventListenerHook?: eventListenerHook;
  documentRemoveEventListenerHook?: eventListenerHook;
  
  // DOM 钩子
  appendOrInsertElementHook?: <T extends Node>(element: T, iframeWindow: Window) => void;
  patchElementHook?: <T extends Node>(element: T, iframeWindow: Window) => void;
  
  // 属性覆盖
  windowPropertyOverride?: (iframeWindow: Window) => void;
  documentPropertyOverride?: (iframeWindow: Window) => void;
}

默认插件

// packages/wujie-core/src/plugin.ts
function cssRelativePathResolve(code: string, src: string, base: string) {
  const baseUrl = src ? getAbsolutePath(src, base) : base;
  const urlReg = /url\((['"]?)((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*)(\1)\)/g;

  return code.replace(urlReg, (_m, pre, url, post) => {
    // 跳过 base64
    if (/^data:/.test(url)) return _m;
    // 转换为绝对路径
    return `url(${pre}${getAbsolutePath(url, baseUrl)}${post})`;
  });
}

const defaultPlugin = {
  cssLoader: cssRelativePathResolve,
  // 修复 view-transition 问题
  cssBeforeLoaders: [{ content: "html {view-transition-name: none;}" }],
};

export function getPlugins(plugins: Array<plugin>): Array<plugin> {
  return Array.isArray(plugins) ? [defaultPlugin, ...plugins] : [defaultPlugin];
}

CSS Loader

// packages/wujie-core/src/plugin.ts
export function getCssLoader({ plugins, replace }: loaderOption) {
  return (code: string, src: string = "", base: string): string =>
    compose(plugins.map((plugin) => plugin.cssLoader))(
      replace ? replace(code) : code, 
      src, 
      base
    );
}

使用 compose 组合多个 loader:

// packages/wujie-core/src/utils.ts
export function compose(fnList: Array<Function>) {
  return function (...args) {
    return fnList.filter(Boolean).reduce((result, fn) => {
      return fn(result, ...args.slice(1));
    }, args[0]);
  };
}

JS Loader

// packages/wujie-core/src/plugin.ts
export function getJsLoader({ plugins, replace }: loaderOption) {
  return (code: string, src: string = "", base: string): string =>
    compose(plugins.map((plugin) => plugin.jsLoader))(
      replace ? replace(code) : code, 
      src, 
      base
    );
}

预置 Loader

// packages/wujie-core/src/plugin.ts
type presetLoadersType = "cssBeforeLoaders" | "cssAfterLoaders" | "jsBeforeLoaders" | "jsAfterLoaders";

export function getPresetLoaders(loaderType: presetLoadersType, plugins: Array<plugin>): plugin[presetLoadersType] {
  const loaders = plugins
    .map((plugin) => plugin[loaderType])
    .filter((loaders) => loaders?.length);
  
  const res = loaders.reduce((preLoaders, curLoaders) => preLoaders.concat(curLoaders), []);
  
  // cssBeforeLoaders 需要反转顺序
  return loaderType === "cssBeforeLoaders" ? res.reverse() : res;
}

排除/忽略列表

// packages/wujie-core/src/plugin.ts
type effectLoadersType = "jsExcludes" | "cssExcludes" | "jsIgnores" | "cssIgnores";

export function getEffectLoaders(loaderType: effectLoadersType, plugins: Array<plugin>): plugin[effectLoadersType] {
  return plugins
    .map((plugin) => plugin[loaderType])
    .filter((loaders) => loaders?.length)
    .reduce((preLoaders, curLoaders) => preLoaders.concat(curLoaders), []);
}

// 判断 URL 是否匹配
export function isMatchUrl(url: string, effectLoaders: plugin[effectLoadersType]): boolean {
  return effectLoaders.some((loader) => 
    typeof loader === "string" ? url === loader : loader.test(url)
  );
}

钩子执行

// packages/wujie-core/src/utils.ts
export function execHooks(plugins: Array<plugin>, hookName: string, ...args: any[]): void {
  plugins.forEach((plugin) => {
    const hook = plugin[hookName];
    if (typeof hook === "function") {
      hook(...args);
    }
  });
}

使用示例:

// packages/wujie-core/src/iframe.ts
// 事件监听钩子
execHooks(iframeWindow.__WUJIE.plugins, "windowAddEventListenerHook", iframeWindow, type, listener, options);

// 元素 patch 钩子
execHooks(iframeWindow.__WUJIE.plugins, "patchElementHook", element, iframeWindow);

// 属性覆盖钩子
execHooks(iframeWindow.__WUJIE.plugins, "windowPropertyOverride", iframeWindow);

插件使用示例

1. 修改 JS 代码

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    jsLoader: (code, url, base) => {
      // 替换 API 地址
      return code.replace(
        /https:\/\/api\.example\.com/g,
        'https://api.proxy.com'
      );
    },
  }],
});

2. 注入前置脚本

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    jsBeforeLoaders: [
      {
        content: `
          window.ENV = 'production';
          window.API_BASE = 'https://api.example.com';
        `,
      },
    ],
  }],
});

3. 排除特定脚本

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    // 排除(不加载)
    jsExcludes: [
      'https://cdn.example.com/analytics.js',
      /google-analytics/,
    ],
    // 忽略(加载但不执行)
    jsIgnores: [
      'https://cdn.example.com/ad.js',
    ],
  }],
});

4. 注入样式

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    cssBeforeLoaders: [
      {
        content: `
          :root {
            --primary-color: #1890ff;
          }
        `,
      },
    ],
    cssAfterLoaders: [
      {
        content: `
          .custom-override {
            display: none !important;
          }
        `,
      },
    ],
  }],
});

5. 覆盖 window 属性

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    windowPropertyOverride: (iframeWindow) => {
      // 覆盖 localStorage
      iframeWindow.localStorage = {
        getItem: (key) => window.localStorage.getItem(`vue3_${key}`),
        setItem: (key, value) => window.localStorage.setItem(`vue3_${key}`, value),
        removeItem: (key) => window.localStorage.removeItem(`vue3_${key}`),
        clear: () => { /* 只清理当前应用的数据 */ },
      };
    },
  }],
});

6. 监听事件

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    windowAddEventListenerHook: (iframeWindow, type, handler, options) => {
      console.log(`[${iframeWindow.__WUJIE.id}] addEventListener: ${type}`);
    },
    documentAddEventListenerHook: (iframeWindow, type, handler, options) => {
      // 阻止某些事件
      if (type === 'contextmenu') {
        return false;
      }
    },
  }],
});

7. 处理 DOM 插入

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    appendOrInsertElementHook: (element, iframeWindow) => {
      // 处理动态插入的 script
      if (element.tagName === 'SCRIPT') {
        console.log('Script inserted:', element.src);
      }
      // 处理动态插入的 style
      if (element.tagName === 'STYLE') {
        console.log('Style inserted');
      }
    },
  }],
});

8. 元素 patch

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [{
    patchElementHook: (element, iframeWindow) => {
      // 修改图片路径
      if (element.tagName === 'IMG') {
        const src = element.getAttribute('src');
        if (src && src.startsWith('/')) {
          element.setAttribute('src', 'https://cdn.example.com' + src);
        }
      }
    },
  }],
});

多插件组合

const analyticsPlugin = {
  jsExcludes: [/google-analytics/, /baidu-analytics/],
};

const stylePlugin = {
  cssBeforeLoaders: [{ content: ':root { --theme: dark; }' }],
};

const securityPlugin = {
  windowPropertyOverride: (iframeWindow) => {
    // 禁用 eval
    iframeWindow.eval = () => {
      throw new Error('eval is disabled');
    };
  },
};

startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  plugins: [analyticsPlugin, stylePlugin, securityPlugin],
});

小结

无界的插件系统:

类型作用时机
Loader处理代码加载时
Excludes排除资源加载前
Ignores忽略执行执行前
Hook拦截操作运行时
Override覆盖属性初始化时

核心设计:

  1. 组合模式:多个 loader 通过 compose 组合
  2. 钩子机制:关键操作提供钩子扩展
  3. 默认插件:内置 CSS 路径处理
  4. 灵活配置:支持字符串和正则匹配

📦 源码版本:wujie v1.0.22

上一篇:通信机制

系列完结