无界微前端源码解析:插件系统
深入分析 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 | 覆盖属性 | 初始化时 |
核心设计:
- 组合模式:多个 loader 通过 compose 组合
- 钩子机制:关键操作提供钩子扩展
- 默认插件:内置 CSS 路径处理
- 灵活配置:支持字符串和正则匹配
📦 源码版本:wujie v1.0.22
上一篇:通信机制
系列完结