微前端入门篇 | 详解微前端演变(附源码)

什么是微前端

  • 各个开发团队都可以自行选择技术栈不受同一项目中其它团队影响;
  • 各个交付产物都可以被独立使用,避免和其它交付产物耦合;
  • 各个交付产物中的样式不会污染到其它组件;
  • 各个交付产物都可以自由使用浏览器原生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");

源码

https://github.com/MYQ1996/singleSpa.git

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值