微前端(无界)

慈云数据 1年前 (2024-03-25) 技术支持 81 0

前言:微前端已经是一个非常成熟的领域了,但开发者不管采用哪个现有方案,在适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite 框架支持、应用共享等用户核心诉求都或存在问题,或无法提供支持。本文提供一种基于 iframe 的全新微前端方案,完善的解决了这些核心诉求。

微前端概念

微前端是借鉴了微服务的理念,将一个庞大的应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发,独立运行,独立部署,还可以随意组合,这样就降低了耦合度,从而更加灵活。

微前端特性

  • 技术栈无关 主框架不限制接入应用的技术栈,子应用可自主选择技术栈(vue,react,jq,ng等)
  • 独立开发/部署 各个团队之间仓库独立,单独部署,互不依赖
  • 增量升级 当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性
  • 独立运行时 微应用之间运行时互不依赖,有独立的状态管理

    场景演示

    1. 后台管理系统

    最外面一层可以当主应用,里面可以放不同的子应用子应用不受技术的限制。

    1. web商店(未来趋势)

    例如一些导航网站,可以提供微前端的接入,我们的网站也可以入驻该网站,并且还可以提供一些API增加交互,有点类似于小程序。小程序可以调用微信的一些能力例如支付,扫码等,导航类型的网站也可以提供一些API,我们的网站接入之后提供API调用,可以实现更多有趣的玩法。

    微前端方案

    iframe 方案

    特点

    1. 接入比较简单
    2. 隔离非常稳完美

    不足

    1. dom割裂感严重,弹框只能在iframe,而且有滚动条
    2. 通讯非常麻烦,而且刷新iframe url状态丢失
    3. 前进后退按钮无效

    qiankun 方案

    qiankun 方案是基于 single-spa 的微前端方案。

    特点

    1. html entry 的方式引入子应用,相比 js entry 极大的降低了应用改造的成本;
    2. 完备的沙箱方案,js 沙箱做了 SnapshotSandbox、LegacySandbox、ProxySandbox 三套渐进增强方案,css 沙箱做了 strictStyleIsolation、experimentalStyleIsolation 两套适用不同场景的方案;
    3. 做了静态资源预加载能力;

    不足

    1. 适配成本比较高,工程化、生命周期、静态资源路径、路由等都要做一系列的适配工作;
    2. css 沙箱采用严格隔离会有各种问题,js 沙箱在某些场景下执行性能下降严重;
    3. 无法同时激活多个子应用,也不支持子应用保活;
    4. 无法支持 vite 等 esmodule 脚本运行;

    底层原理 js沙箱使用的是proxy进行快照然后用用 with(window){} 包裹起来 with内的window其实就是proxy.window 我们声明变量 var name = '小满' 实际这个变量挂到了proxy.window 并不是真正的window css沙箱原理 第一个就是shadowDom隔离 第二个类似于Vue的scoped [data-qiankun-426732]

    micro-app 方案

    micro-app 是基于 webcomponent + qiankun sandbox 的微前端方案。

    特点

    1. 使用 webcomponet 加载子应用相比 single-spa 这种注册监听方案更加优雅;
    2. 复用经过大量项目验证过 qiankun 的沙箱机制也使得框架更加可靠;
    3. 组件式的 api 更加符合使用习惯,支持子应用保活;
    4. 降低子应用改造的成本,提供静态资源预加载能力;

    不足

    1. 接入成本较 qiankun 有所降低,但是路由依然存在依赖; (虚拟路由已解决)
    2. 多应用激活后无法保持各子应用的路由状态,刷新后全部丢失; (虚拟路由已解决)
    3. css 沙箱依然无法绝对的隔离,js 沙箱做全局变量查找缓存,性能有所优化;
    4. 支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离;
    5. 对于不支持 webcompnent 的浏览器没有做降级处理;

    底层原理 js隔离跟qiankun类似也是使用proxy + with,css隔离自定义前缀类似于scoped

    const prefix = `micro-app[name=${appName}]`
    复制代码

    EMP 方案

    EMP 方案是基于 webpack 5 module federation 的微前端方案。

    特点

    1. webpack 联邦编译可以保证所有子应用依赖解耦;
    2. 应用间去中心化的调用、共享模块;
    3. 模块远程 ts 支持;

    不足

    1. 对 webpack 强依赖,老旧项目不友好;
    2. 没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉;
    3. 子应用保活、多应用激活无法实现;
    4. 主、子应用的路由可能发生冲突;

    底层原理 这个东西有点类似于拆包,也可以叫模块共享,例如React有个模块可以共享给Vue项目用Vue2的组件可以共享给Vue3用。

    无界微前端 方案

    预览demo wujie-micro.github.io/demo-main-v…**

    特点

    1. 接入简单只需要四五行代码
    2. 不需要针对vite额外处理
    3. 预加载
    4. 应用保活机制

    不足

    1. 隔离js使用一个空的iframe进行隔离
    2. 子应用axios需要自行适配
    3. iframe沙箱的src设置了主应用的host,初始化iframe的时候需要等待iframe的location.orign从'about:blank'初始化为主应用的host,这个采用的计时器去等待的不是很悠亚。

    底层原理 使用shadowDom 隔离css,js使用空的iframe隔离,通讯使用的是proxy

    前置知识了解webComponents

    演示webComponents的 传参 样式隔离 以及写法

    window.onload = () => {
        class WuJie extends HTMLElement {
            constructor() {
                super()
                this.init()
                this.getAttr('url')
            }
            init() {
              const shadow =  this.attachShadow({ mode: "open" }) //开启影子dom 也就是样式隔离
              const template = document.querySelector('#wu-jie') as HTMLTemplateElement
              console.log(template);
              
              shadow.appendChild(template.content.cloneNode(true))
            }
            getAttr (str:string) {
               console.log('获取参数',this.getAttribute(str));
               
            }
            //生命周期自动触发有东西插入
            connectedCallback () {
               console.log('类似于vue 的mounted');
            }
            //生命周期卸载
            disconnectedCallback () {
                  console.log('类似于vue 的destory');
            }
            //跟watch类似
            attributeChangedCallback (name:any, oldVal:any, newVal:any) {
                console.log('跟vue 的watch 类似 有属性发生变化自动触发');
            }
        }
        
        window.customElements.define('wu-jie', WuJie)
    }
    复制代码
    
    
        
        
        
        Document
        
    
    
        
        我是div
        
        
           
            
                div {
                    background: red;
                }
            
             
                小满zs(测试样式隔离)
             
        
    
    
    复制代码

    可以完美隔离CSS样式

    前置知识2 monorepo架构

    我们采用的是微前端一个主应用,和多个子应用,我们肯定不会一个一个去install安装依赖,太傻了,我们采用monorepo 架构 一次install 即可安装完成。

    第一步需要安装pnpm

    pnpm内置了对单个代码仓库包含多个软件包的支持,是monorepo架构模式的不二速选

    npm i pnpm -g
    复制代码

    最外层建一个main充当主应用然后新建一个web文件夹里面放两个子应用分别是vue和react

    配置monorepo

    在根目录新建一个 pnpm-workspace.yaml 配置依赖项

    packages:
      # all packages in direct subdirs of packages/
      - 'main'
      # all packages in subdirs of components/
      - 'web/**'
    复制代码

    配置完成后install一次就行

    他会把所有的公共依赖项抽到外层,而里层的依赖项都是一些最核心的

    无界入门

    我们使用Vue3来充当主应用 首先需要安装依赖

    vue2 npm i wujie-vue2 -S
    vue3 npm i wujie-vue3 -S
    react npm i wujie-react -S
    

    主应用的main.ts

    import { createApp } from 'vue'
    import App from './App.vue'
    import Wujie from 'wujie-vue3'
    createApp(App).use(Wujie).mount('#app')
    

    主应用hellowWord url填写子应用的url 子应用通过npm run dev启动

      
        
      
    
    
    import { ref, reactive } from 'vue'
    const url = 'http://127.0.0.1:5174/'
    
    
    

    只需要简单的几行代码就可以实现微前端应用,接入成本很低

    wujie-vue3 原理

    这个包其实是作者根据wujie 自行封装的我们也可以自己去封装一下

    文档地址

    设置子应用​

    非必须,由于preloadApp和startApp参数重复,为了避免重复输入,可以通过setupApp来设置默认参数。

    javascript

    setupApp({ name: "唯一id", url: "子应用地址", exec: true, el: "容器", sync: true })
    

    预加载​

    javascript

    preloadApp({ name: "唯一id"});
    

    启动子应用​

    javascript

    startApp({ name: "唯一id" });
    

    知道以上几个API的用法就可以简单封装一个无界的组件我们使用vue3 + webpack + swc 封装

    依赖

        "@swc/core": "^1.3.42",
        "swc-loader": "^0.2.3",
        "ts-loader": "^9.4.2",
        "TypeScript": "^5.0.2",
        "vue": "^3.2.47",
        "webpack": "^5.77.0",
        "webpack-cli": "^5.0.1",
        "wujie": "^1.0.13"
    

    1.webpack 配置

    const { Configuration } = require('webpack')
    const path = require('path')
    /**
     * @type {Configuration} //配置智能提示
     */
    const config = {
        entry: "./src/index.ts",
        output: {
            filename: "wujie.js",
            path:path.resolve(__dirname, './lib') ,
            library:"Wujie",
            libraryTarget:"umd",
            umdNamedDefine:true
        },
        externals:{
          vue:'vue',
          wujie:"wujie"
        },
        mode:"none",
        cache:true,
        module: {
            rules: [
                {
                    test: /\.ts$/,  //解析ts
                    loader: "swc-loader", //使用新技术swc-loader
                }
            ]
        },
    }
    module.exports = config
    

    这个就是差距 为什么使用新技术swc swc是rust写的性能是原生的好几倍,他官网也说了他是babel 的20倍

    2.编写组件

    import type { plugin } from 'wujie'
    type lifecycle = (appWindow: Window) => any;
    interface Props {
        /** 唯一性用户必须保证 */
        name: string;
        /** 需要渲染的url */
        url: string;
        /** 需要渲染的html, 如果用户已有则无需从url请求 */
        html?: string;
        /** 渲染的容器 */
        loading?: HTMLElement;
        /** 路由同步开关, false刷新无效,但是前进后退依然有效 */
        sync?: boolean;
        /** 子应用短路径替换,路由同步时生效 */
        prefix?: { [key: string]: string };
        /** 子应用保活模式,state不会丢失 */
        alive?: boolean;
        /** 注入给子应用的数据 */
        props?: { [key: string]: any };
        /** js采用fiber模式执行 */
        fiber?: boolean;
        /** 子应用采用降级iframe方案 */
        degrade?: boolean;
        /** 自定义运行iframe的属性 */
        attrs?: { [key: string]: any };
        /** 自定义降级渲染iframe的属性 */
        degradeAttrs?: { [key: string]: any };
        /** 代码替换钩子 */
        replace?: (codeText: string) => string;
        /** 自定义fetch,资源和接口 */
        fetch?: (input: RequestInfo, init?: RequestInit) => Promise;
        /** 子应插件 */
        plugins: Array;
        /** 子应用生命周期 */
        beforeLoad?: lifecycle;
        /** 没有做生命周期改造的子应用不会调用 */
        beforeMount?: lifecycle;
        afterMount?: lifecycle;
        beforeUnmount?: lifecycle;
        afterUnmount?: lifecycle;
        /** 非保活应用不会调用 */
        activated?: lifecycle;
        deactivated?: lifecycle;
    };
    export { Props } 
    
    import { startApp, bus } from 'wujie'
    import { h, defineComponent, onMounted, getCurrentInstance, onBeforeUnmount } from 'vue'
    import type { App, PropType } from 'vue'
    import { Props } from './type'
    const WuJie = defineComponent({
        props: {
            width: { type: String, default: "" },
            height: { type: String, default: "" },
            name: { type: String, default: "", required: true },
            loading: { type: HTMLElement, default: undefined },
            url: { type: String, default: "", required: true },
            sync: { type: Boolean, default: undefined },
            prefix: { type: Object, default: undefined },
            alive: { type: Boolean, default: undefined },
            props: { type: Object, default: undefined },
            attrs: { type: Object, default: undefined },
            replace: { type: Function as PropType, default: undefined },
            fetch: { type: Function as PropType, default: undefined },
            fiber: { type: Boolean, default: undefined },
            degrade: { type: Boolean, default: undefined },
            plugins: { type: Array as PropType, default: null },
            beforeLoad: { type: Function as PropType, default: null },
            beforeMount: { type: Function as PropType, default: null },
            afterMount: { type: Function as PropType, default: null },
            beforeUnmount: { type: Function as PropType, default: null },
            afterUnmount: { type: Function as PropType, default: null },
            activated: { type: Function as PropType, default: null },
            deactivated: { type: Function as PropType, default: null },
        },
        setup(props: Props, { emit }) {
            const instance = getCurrentInstance()
            const handlerEmit = (event: string, ...args: any[]) => {
                emit(event, ...args)
            }
            onMounted(() => {
                bus.$onAll(handlerEmit) //添加事件订阅
                //初始化无界
                startApp({
                    name: props.name,
                    url: props.url,
                    el: instance?.refs.wujie as HTMLElement,
                    loading: props.loading,
                    alive: props.alive,
                    fetch: props.fetch,
                    props: props.props,
                    attrs: props.attrs,
                    replace: props.replace,
                    sync: props.sync,
                    prefix: props.prefix,
                    fiber: props.fiber,
                    degrade: props.degrade,
                    plugins: props.plugins,
                    beforeLoad: props.beforeLoad,
                    beforeMount: props.beforeMount,
                    afterMount: props.afterMount,
                    beforeUnmount: props.beforeUnmount,
                    afterUnmount: props.afterUnmount,
                    activated: props.activated,
                    deactivated: props.deactivated,
                })
            })
            onBeforeUnmount(() => {
                bus.$offAll(handlerEmit) //取消事件订阅
            })
            return () => h('div', {
                style: {
                    width: 200,
                    height: 200
                },
                ref: "wujie"
            }, '')
        }
    })
    WuJie.install = (app: App) => {
        app.component('wujie', WuJie)
    }
    export default WuJie
    

    编写声明文件

    import { bus, preloadApp, destroyApp, setupApp } from "wujie";
    import type { App } from 'vue';
    declare const WujieVue: {
        bus: typeof bus;
        setupApp: typeof setupApp;
        preloadApp: typeof preloadApp;
        destroyApp: typeof destroyApp;
        install: (app: App) => void
    };
    export default WujieVue;
    

    编写package json

    {
      "name": "wujie-vue-setup",
      "version": "0.0.4",
      "description": "",
      "main": "lib/index.js",
      "module": "esm/index.js",
      "files": [
         "esm",
         "lib",
         "index.d.ts"
      ],
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "wujie": "^1.0.13"
      },
      "devDependencies": {
        "@swc/core": "^1.3.42",
        "swc-loader": "^0.2.3",
        "ts-loader": "^9.4.2",
        "typescript": "^5.0.2",
        "vue": "^3.2.47",
        "webpack": "^5.77.0",
        "webpack-cli": "^5.0.1"
      }
    }
    

    编写完成之后就是npm adduser 创建账号 npm login 登录 npm publish 发布到npm 发布的时候记住看看源是否是npm 的

微信扫一扫加客服

微信扫一扫加客服