微前端入门篇 | 详解微前端演变(附源码)什么是微前端 各个开发团队都可以自行选择技术栈不受同一项目中其它团队影响; 各
什么是微前端
- 各个开发团队都可以自行选择技术栈不受同一项目中其它团队影响;
- 各个交付产物都可以被独立使用,避免和其它交付产物耦合;
- 各个交付产物中的样式不会污染到其它组件;
- 各个交付产物都可以自由使用浏览器原生API,而非要求使用封装后的API;
背景
前端应用越来越复杂
导致
- 人力成本压力
- 维护成本高
- 迭代成本高
- 需求变更影响范围大
- 持续化投入产出比不足
期望
- 单体应用,独立升级
- 单体应用,挂了,不影响整体项目
场景分析
如何实施微前端拆分和聚合?
大仓库拆分独立的模块,统一构建
名词解释:
微前端就是后端微服务思维在前端的映射
Monorepo 仓库管理
大仓库通过menorepa methodeogy做成npm包,集成主项目
大仓库拆分子仓库,构建应用出独立的服务/应用
大仓库拆分多仓库,构建后集成到主应用
优点: 虽然提高了复用性
缺点: 首先版本与版本之间就有问题,牵一发动全身,不够独立性,技术债限制。
微前端如何在浏览器中落地?
场景/模型 + 模块机制 + 加载机制
微内核应用-前端系统
iframe方案
web自带方法
优点:
- 实现简单、子系统加载时依然保持单页应用体验。
缺点
- 不可控制iframe嵌入的显示区大小不容易控制,存在一定局限性。
- 页面刷新之后,无法保持子系统当前的路由状态
- Iframe的适配存在一定问题。
- 性能开销iframe阻塞onload,占用连接池、多层嵌套页面崩溃
MPA+nginx路由分发
这种方式就是在多个独立的SPA应用之间跳转。
优点:
- 框架无关
- 独立开发、部署、运行
- 应用之间100%隔离
缺点:
- 应用之间的彻底割裂导致复用困难。
- 每个独立的SPA应用加载时间较长,容易出现白屏,影响用户体验;
- 后续如果要做同屏多应用,不便于扩展。
server {
listen 80;
server_name xxx.xxx.com;
location / {
index index.html
try_files $uri $uri/ /index.html;
}
location /client_studycenter {
alias /code/studycenter;
try_files $uri $uri/ @rewrites;
}
location @rewrites {
rewrite ^/(client_studycenter)/(.*)$ /$1/index.html last;
}
}
singleSpa
将子模块打包成类库 -> 在父应用中直接调用
优点:
- 自由度高,可以通过js做到预加载,有基座应用做把控,体验更完善,并且同一页面可以存在多个子应用
缺点:
-
不够灵活 不能动态加载js文件
-
样式不隔离 没有js沙箱的机制
构建子应用
vue create child-vue
npm install single-spa-vue
// child-vue main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import singleSpaVue from "single-spa-vue";
Vue.config.productionTip = false;
const appOptions = {
el: "#vue", // 挂载到父应用的标签中
router,
render: h => h(App)
};
// 1. 需要加载父项目加载子应用
// bootstrap mount unmount
const vueLifeCycle = singleSpaVue({
Vue,
appOptions
});
if (window.singleSpaNavigate) {
__webpack_public_path__ = "http://localhost:8081/";
}
if (!window.singleSpaNavigate) {
delete appOptions.el;
new Vue(appOptions).$mount("#app");
}
// 2.协议接入 我定义好了协议 父应用会调用这些函数
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
export default vueLifeCycle;
// 3.将子应用打包成lib去给父应用使用
const router = new VueRouter({
mode: 'history',
base: '/vue',
routes
})
配置库打包
module.exports = {
configureWebpack: {
output: {
library: "singleVue",
libraryTarget: "umd"
},
devServer: {
port: 8081
}
}
};
主应用搭建
vue create parent-vue
<div id="nav">
<router-link to="/vue">vue项目</router-link>
<div id="vue"></div>
</div>
main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { registerApplication, start } from "single-spa";
Vue.config.productionTip = false;
async function loadScript(url) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// singleSpa 缺陷 不够灵活 不能动态加载js文件
// 样式不隔离 没有js沙箱的机制
registerApplication(
"myVueapp",
async () => {
console.log("加载模块");
await loadScript(`http://localhost:8081/js/chunk-vendors.js`);
await loadScript(`http://localhost:8081/js/app.js`);
return window.singleVue;
},
location => location.pathname.startsWith("/vue")
); //用户切换到/vue的路径下,我需要加载刚才定义的子应用
start();
new Vue({
router,
render: h => h(App)
}).$mount("#app");
源码
css隔离方案
子应用之间样式隔离
Dynamic Stylesheet动态样式表,当应用切换时移除老应用样式
添加新应用样式主应用和子应用之间的样式隔离
BEM(Block Element Modifier) 约定项目前缀
CSS-Modules 打包时生成不冲突的选择器名
Shadow DOM 真正意义上的隔离
css-in-js
let shadowDom = shadow.attachShadow({ mode: 'open' });
let pElement = document.createElement('p');
pElement.innerHTML = 'hello world';
let styleElement = document.createElement('style');
styleElement.textContent = `p{color:red}`
shadowDom.appendChild(pElement);
shadowDom.appendChild(styleElement)
缺点: 当弹窗挂载到到弹窗上
qiankun 的 css 沙箱的原理是重写 HTMLHeadElement.prototype.appendChild 事件,记录子项目运行时新增的 style/link 标签,卸载子项目时移除这些标签。
JS沙箱机制
当运行子应用时应该跑在内部沙箱环境中
- 快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实例)Proxy 代理沙箱,不影响全局环境
快照沙箱
-
激活时将当前window属性进行快照处理失活时用快照中的内容和当前window属性比对
-
如果属性发生变化保存到modifyPropsMap中,并用快照还原window属性
-
在次激活时,再次进行快照,并用上次修改的结果还原window
8 qiankun
vue create qiankun-base
qiankun-base
1 基座路由
<div id="app">
<div>
<router-link to="/">首页</router-link> | <router-link to="/vue">vue应用</router-link> |
<router-link to="/react">react应用</router-link>
</div>
<router-view v-show="$route.name"></router-view>
<div v-show="!$route.name" id="vue"></div>
<div v-show="!$route.name" id="react"></div>
</div>
2 注册子应用
qiankun-base
import {registerMicroApps,start} from 'qiankun'
const apps = [ { name:'vueApp', entry:'//localhost:10000', container:'#vue', activeRule:'/vue' }, { name:'reactApp', entry:'//localhost:20000', container:'#react', activeRule:'/react' }]
registerMicroApps(apps);
start();
3 vue子应用
vue create qiankun-vue
qiankun-vue main.js
let instance = null;
function render(props) {
const { container } = props;
instance = new Vue({
router,
store,
render: h => h(App)
// 这里挂载自己的html 基座会拿到这个挂载后的html 将插入进去
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 判断是够使用了乾坤
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 子组件的协议
export async function bootstrap() {}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.$destroy();
}
打包配置 vue.config.js
module.exports = {
devServer: {
port: 10000,
headers: {
"Access-Control-Allow-Origin": "*"
}
},
configureWebpack: {
output: {
library: "vueApp",
libraryTarget: "umd"
}
}
};
4 react子应用
create-react-app qiankun-react
qiankun-react src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
function render() {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {}
export async function mount() {
render();
}
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
打包配置
yarn add react-app-rewired --save-dev
新建 config-overrides.js 重写 webpack
"scripts": {
"start": "BROWSER=none react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
}
配置 .env文件
PORT=20000
WDS_SOCKET_PORT=20000
源码
git@gitee.com:bjgzs/qiankun-demo.git
转载自:https://juejin.cn/post/6997607114893950989