微前端 无界wujie
开发环境配置:
Node.js 版本 < 18.0.0
pnpm 脚手架示例模版基于 pnpm + turborepo 管理项目
如果您的当前环境中需要切换 node.js 版本, 可以使用 nvm or fnm 进行安装.
以下是通过 nvm 或者nvs 安装 Node.js 16 LTS 版本
nvs安装教程 https://blog.csdn.net/glorydx/article/details/134056903
C:\>node -v
v20.18.1
C:\>nvs use 16
Specified version not found.
To add this version now: nvs add node/16
C:\>nvs add node/16
Extracting [###########################################################################################] 100%
Added at: %LOCALAPPDATA%\nvs\node\16.20.2\x64\node.exe
To use this version now: nvs use node/16.20.2/x64
C:\>npx create-wujie@latest
Need to install the following packages:
create-wujie@0.4.0
Ok to proceed? (y) y
📦 Welcome To Create Template for WuJie! V0.3.2
√ Project name: ... wujie-main
√ What framework do you choose as your main application ? » Webpack + Vue2
√ Select the main application route pattern » history
√ What framework do you choose as your sub application ? » Vite, Vue2, Vue3, React16, React17
√ Select the sub application route pattern » history
安装完成以后,分别单独启动wujie的主应用,和子应用,记得将node的版本都统一设置为 16 这样就可以正常体验wujie官方提供的demo。
wujie代码分析 vue2主应用
import "whatwg-fetch"; // fetch polyfill
import "custom-event-polyfill"; // CustomEvent polyfill
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import WujieVue from "wujie-vue2";
import hostMap from "../wujie-config/hostMap";
import credentialsFetch from "../wujie-config/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 "../wujie-config/lifecycle";
import plugins from "../wujie-config/plugin";
const isProduction = process.env.NODE_ENV === "production";
const { setupApp, preloadApp, 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 });
}
});
// 根据浏览器的版本,如果不支持 Proxy,则降级 Object.defineProperty 如果不支持webcomponent 则降级iframe 理论上可以兼容到 IE 9
const degrade =
window.localStorage.getItem("degrade") === "true" ||
!window.Proxy ||
!window.CustomElementRegistry;
const props = {
// 将主应用的router.push方法传递给子应用,这样子应用就能通过获取jump方法,来控制主应用的跳转
jump: (name) => {
router.push({ name });
},
};
/**
* 大部分业务无需设置 attrs
* 此处修正 iframe 的 src,是防止github pages csp报错
* 因为默认是只有 host+port,没有携带路径
*/
const attrs = isProduction ? { src: hostMap("//localhost:8000/") } : {};
/**
* 配置应用,主要是设置默认配置
* preloadApp、startApp的配置会基于这个配置做覆盖
*/
setupApp({
name: "react16",
url: hostMap("//localhost:7600/"),
attrs,
exec: true,
props, // 给子应用传递的参数
fetch: credentialsFetch,
plugins,
// prefix 用于改变子路径过长,在主应用中进行替换
prefix: { "prefix-dialog": "/dialog", "prefix-location": "/location" },
degrade,
...lifecycles,
});
setupApp({
name: "react17",
url: hostMap("//localhost:7100/"),
attrs,
exec: true, //是否预先执行子应用
alive: true, // 是否保存子应用的状态
props,
fetch: credentialsFetch,
degrade,
...lifecycles,
});
setupApp({
name: "vue2",
url: hostMap("//localhost:6100/"),
attrs,
exec: true,
props,
fetch: credentialsFetch,
degrade,
...lifecycles,
});
setupApp({
name: "vue3",
url: hostMap("//localhost:8082/"),
attrs,
exec: true,
alive: true,
plugins: [
{
cssExcludes: [
"https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css",
],
},
],
props,
// 引入了的第三方样式不需要添加credentials
fetch: (url, options) =>
url.includes(hostMap("//localhost:8082/"))
? credentialsFetch(url, options)
: window.fetch(url, options),
degrade,
...lifecycles,
});
setupApp({
name: "vite",
url: hostMap("//localhost:8083/"),
attrs,
exec: true,
props,
fetch: credentialsFetch,
degrade,
...lifecycles,
});
// 因为已经setupApp注册了,这里可以简写,预加载只写name就可以了
if (window.localStorage.getItem("preload") !== "false") {
preloadApp({
name: "react16",
});
preloadApp({
name: "react17",
});
preloadApp({
name: "vue2",
});
if (window.Proxy) {
preloadApp({
name: "vue3",
});
preloadApp({
name: "vite",
});
}
}
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
主应用中,用来显示子应用的配置
wujieVue这个组件只要url一变化,在非保活模式下,子应用就会重新加载
<template>
<!--保活模式,name相同则复用一个子应用实例,改变url无效,必须采用通信的方式告知路由变化 -->
<WujieVue width="100%" height="100%" name="react17" :url="react17Url"></WujieVue>
</template>
<script>
import hostMap from "../../wujie-config/hostMap";
import wujieVue from "wujie-vue2"; // 引入wujie-vue2,如果是vue3请引入wujie-vue3 用来显示子应用
export default {
data() {
return {
react17Url: hostMap("//localhost:7100/") + this.$route.params.path, //hostMap区分开发环境和生产环境
};
},
watch: {
// 保活模式,name相同则复用一个子应用实例,改变url无效,必须采用通信的方式告知路由变化
"$route.params.path": {
handler: function () {
wujieVue.bus.$emit("react17-router-change", `/${this.$route.params.path}`);
},
immediate: true,
},
},
};
</script>
<style lang="scss" scoped></style>
非保活模式的子应用在主应用中的配置
<template>
<!--单例模式,name相同则复用一个无界实例,改变url则子应用重新渲染实例到对应路由 -->
<WujieVue width="100%" height="100%" name="vite" :url="viteUrl"></WujieVue>
</template>
<script>
import hostMap from "../../wujie-config/hostMap";
export default {
// 如果是非保活模式,不需要watch这个$route.params.path变化,并去调用wujieVue.bus.$emit
computed: {
viteUrl() {
return hostMap("//localhost:8083/") + this.$route.params.path;
},
},
};
</script>
<style lang="scss" scoped></style>
vue2主应用,路由的配置
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import Multiple from "../views/Multiple.vue";
import Vue2 from "../views/Vue2.vue";
import Vue2Sub from "../views/Vue2-sub.vue";
import Vue3 from "../views/Vue3.vue";
import Vue3Sub from "../views/Vue3-sub.vue";
import Vite from "../views/Vite.vue";
import ViteSub from "../views/Vite-sub.vue";
import React16 from "../views/React16.vue";
import React16Sub from "../views/React16-sub.vue";
import React17 from "../views/React17.vue";
import React17Sub from "../views/React17-sub.vue";
const basename = process.env.NODE_ENV === "production" ? "/demo-main-vue/" : ""; // 区分不同环境下,资源所在的不同文件夹
Vue.use(VueRouter);
const routes = [
{
path: "/all",
name: "all",
component: Multiple,
},
{
path: "/",
redirect: "/home",
},
{
path: "/home",
name: "home",
component: Home,
},
{
path: "/vue2",
name: "vue2",
component: Vue2,
},
{
path: "/vue2-sub/:path",
name: "vue2-sub",
component: Vue2Sub,
},
{
path: "/vue3",
name: "vue3",
component: Vue3,
},
{
path: "/vue3-sub/:path",
name: "vue3-sub",
component: Vue3Sub,
},
{
path: "/vite",
name: "vite",
component: Vite,
},
{
path: "/vite-sub/:path",
name: "vite-sub",
component: ViteSub,
},
{
path: "/react16",
name: "react16",
component: React16,
},
{
path: "/react16-sub/:path",
name: "react16-sub",
component: React16Sub,
},
{
path: "/react17",
name: "react17",
component: React17,
},
{
path: "/react17-sub/:path",
name: "react17-sub",
component: React17Sub,
},
];
// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置
const router = new VueRouter({
mode: "history",
base: basename,
routes,
});
export default router;
vue2主应用vue.config 配置
// vue.config.js
/**
* @type {import('@vue/cli-service').ProjectOptions}
*/
module.exports = {
publicPath: process.env.NODE_ENV === "production" ? "/demo-main-vue/" : "/", // 区分开发和生产服务器的路径
devServer: {
headers: {
"Access-Control-Allow-Origin": "*", // 如果需要跨域,请打开此配置
},
open: process.env.NODE_ENV === "development", // 只有开发环境需要使用devServer
port: "8000",
},
lintOnSave: false // 是否关闭eslint检查,只在保存时才检查
};
react子应用代码分析
子应用可以用到的wujie的数据 https://wujie-micro.github.io/doc/api/wujie.html#VPSidebarNav
import "react-app-polyfill/stable";
import "react-app-polyfill/ie11";
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
import "./styles.css";
const basename = process.env.NODE_ENV === "production" ? "/demo-react16/" : "";
// 如果作为无界的子应用打开,就需要使用 window.__POWERED_BY_WUJIE__ 判断,然后挂载函数__WUJIE_MOUNT和卸载函数__WUJIE_UNMOUNT
if (window.__POWERED_BY_WUJIE__) {
// eslint-disable-next-line no-undef
window.__WUJIE_MOUNT = () => {
ReactDOM.render(
<Router basename={basename}>
<App />
</Router>,
document.getElementById("root")
);
};
window.__WUJIE_UNMOUNT = () => {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
};
} else {
ReactDOM.render(
<Router basename={basename}>
<App />
</Router>,
document.getElementById("root")
);
}
react 子应用,嵌套其他子应用
import React from "react";
import WujieReact from "wujie-react"; // 需要引入的wujie react 组件
import lifecycles from "./lifecycle"; // 对应的生命周期
import hostMap from "./hostMap"; // 对应的一些开发环境和生产环境的host映射
function selfFetch(url, options) {
const includeFlag = process.env.NODE_ENV === "production";
return window.fetch(url, { ...options, credentials: includeFlag ? "include" : "omit" });
}
export default function React17() {
const react17Url = hostMap("//localhost:7100/");
const degrade = window.localStorage.getItem("degrade") === "true";
const props = {
jump: (name) => {
window?.$wujie.props.jump(name); // 从主应用vue2中得到的改变主应用router的函数jump,再传递给嵌套的子应用
},
};
return (
<div>
<h2>子应用嵌套</h2>
<div className="content" style={{ border: "1px dashed #ccc", overflow: "auto" }}>
<WujieReact
width="100%"
height="500px"
name="react17"
url={react17Url}
alive={true}
sync={true}
fetch={selfFetch}
props={props}
degrade={degrade}
beforeLoad={lifecycles.beforeLoad}
beforeMount={lifecycles.beforeMount}
afterMount={lifecycles.afterMount}
beforeUnmount={lifecycles.beforeUnmount}
afterUnmount={lifecycles.afterUnmount}
></WujieReact>
</div>
</div>
);
}
子应用可能还会遇到的问题
无界快速上手 https://wujie-micro.github.io/doc/guide/start.html