背景
前面我们已经从源码深入研究过「qiankun 微前端框架」了,感兴趣的小伙伴可以去看看《微前端 qiankun@2.10.5 源码分析(二)》,今天我们来研究另外一个比较牛的微前端框架「无界」。
简介
无界微前端(Wujie)是一款由腾讯团队开源的微前端框架,专注于解决复杂前端应用的模块化、子应用独立开发和部署等问题。它通过 Web Components 和 Shadow DOM 技术实现子应用的样式隔离和脚本沙箱,同时支持跨技术栈的无缝集成(如 React、Vue、Angular 等)。
具体的特性跟使用就不详细说明了,小伙伴们可以自己查阅「无界」开发文档,今天我们主要是从源码角度去分析下无界微前端框架。
开始
我们直接去 github 官网拖一份目前最新的 Wujie 源码@1.0.28,然后进入项目根目录按装依赖:
cd wujie && pnpm install
注意:Node 版本要指定 <=16。
先来一张 wujie 框架的流程图,这样就不会在庞大的源码中感到迷茫了:
我们先在项目根目录安装依赖并执行 pnpm start 命令来启动一下项目:
pnpm install && pnpm start
启动完成后:
setupApp 函数
找到主应用 examples/main-vue/src/main.js:
import "whatwg-fetch"; // fetch polyfill
import "custom-event-polyfill";
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import WujieVue from "wujie-vue2";
import hostMap from "./hostMap";
import credentialsFetch from "./fetch";
import Switch from "ant-design-vue/es/switch";
import Tooltip from "ant-design-vue/es/tooltip";
import button from "ant-design-vue/es/button/index";
import Icon from "ant-design-vue/es/icon/index";
import "ant-design-vue/es/button/style/index.css";
import "ant-design-vue/es/style/index.css";
import "ant-design-vue/es/switch/style/index.css";
import "ant-design-vue/es/tooltip/style/index.css";
import "ant-design-vue/es/icon/style/index.css";
import lifecycles from "./lifecycle";
// import plugins from "./plugin";
const isProduction = process.env.NODE_ENV === "production";
const {
setupApp, bus } = WujieVue;
Vue.use(WujieVue).use(Switch).use(Tooltip).use(button).use(Icon);
Vue.config.productionTip = false;
bus.$on("click", (msg) => window.alert(msg));
// 在 xxx-sub 路由下子应用将激活路由同步给主应用,主应用跳转对应路由高亮菜单栏
bus.$on("sub-route-change", (name, path) => {
const mainName = `${
name}-sub`;
const mainPath = `/${
name}-sub${
path}`;
const currentName = router.currentRoute.name;
const currentPath = router.currentRoute.path;
if (mainName === currentName && mainPath !== currentPath) {
router.push({
path: mainPath });
}
});
const degrade = window.localStorage.getItem("degrade") === "true" || !window.Proxy || !window.CustomElementRegistry;
const props = {
jump: (name) => {
router.push({
name });
},
};
/**
* 大部分业务无需设置 attrs
* 此处修正 iframe 的 src,是防止github pages csp报错
* 因为默认是只有 host+port,没有携带路径
*/
const attrs = isProduction ? {
src: hostMap("//localhost:8000/") } : {
};
/**
* 配置应用,主要是设置默认配置
* preloadApp、startApp的配置会基于这个配置做覆盖
*/
//...
setupApp({
name: "vue2",
url: hostMap("//localhost:7200/"),
attrs,
exec: true,
props,
fetch: credentialsFetch,
degrade,
alive: false,
...lifecycles,
});
//...
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
可以看到,用 setupApp
方法注册了一个子应用“vue2”,并提供了入口文件“//localhost:7200/”,我们找到 setupApp
方法,在 packages/wujie-core/src/index.ts 文件的第 179 行:
/**
* setupApp 参数
*/
type baseOptions = {
/** 唯一性用户必须保证 */
name: string;
/** 需要渲染的url */
url: string;
/** 需要渲染的html, 如果已有则无需从url请求 */
html?: string;
/** 代码替换钩子 */
replace?: (code: string) => string;
/** 自定义fetch */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/** 注入给子应用的属性 */
props?: { [key: string]: any };
/** 自定义运行iframe的属性 */
attrs?: { [key: string]: any };
/** 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any };
/** 子应用采用fiber模式执行 */
fiber?: boolean;
/** 子应用保活,state不会丢失 */
alive?: boolean;
/** 子应用采用降级iframe方案 */
degrade?: boolean;
/** 子应用插件 */
plugins?: Array<plugin>;
/** 子应用生命周期 */
beforeLoad?: lifecycle;
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
activated?: lifecycle;
deactivated?: lifecycle;
loadError?: loadErrorHandler;
};
export function setupApp(options: cacheOptions): void {
if (options.name) addSandboxCacheWithOptions(options.name, options);
}
//...
export function addSandboxCacheWithOptions(id: string, options: cacheOptions): void {
const wujieCache = idToSandboxCacheMap.get(id);
if (wujieCache) idToSandboxCacheMap.set(id, { ...wujieCache, options });
else idToSandboxCacheMap.set(id, { options });
}
可以看到,其实 setupApp
方法就是把当前子应用的一些参数信息先缓存起来,后面创建、渲染的时候再去取。
当我们浏览器输入“http://localhost:8000/vue2-sub/home” 地址的时候,首先我们的主项目“main-vue” 中的 vue-router 会匹配对应的渲染页面:
import Vue from "vue";
import VueRouter from "vue-router";
import Vue2Sub from "../views/Vue2-sub.vue";
//...
const basename = process.env.NODE_ENV === "production" ? "/demo-main-vue/" : "";
Vue.use(VueRouter);
const routes = [
//..
{
path: "/vue2-sub/:path",
name: "vue2-sub",
component: Vue2Sub,
},
//...
];
const router = new VueRouter(