微前端(无界)

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

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


微前端概念


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


微前端特性


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


场景演示


1.后台管理系统


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


c195279863d46152c1dc17567a481801.png


1.web商店(未来趋势)


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


4d2fa8c28bb8858f91ebde095b42d075.png


微前端方案


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}]`
复制代码

6654978af686e11956d0d0998e6baeee.png


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)
}
复制代码


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./index.js"></script>
</head>
<body>
    <!--外层写一个div测试隔离-->
    <div>我是div</div>
    <wu-jie url="xxxxxx"></wu-jie>
    <template id="wu-jie">
       <!--div的样式是作用于全局的-->
        <style>
            div {
                background: red;
            }
        </style>
         <div>
            小满zs(测试样式隔离)
         </div>
    </template>
</body>
</html>
复制代码


可以完美隔离css样式


a2af2bd214536993e2a4c46de7bbac95.png


前置知识2 monorepo架构


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


第一步需要安装pnpm


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


npm i pnpm -g
复制代码


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


77ae96ae88846e195ef1139037f695cf.png


配置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启动


<template>
  <div>
    <WujieVue width="100%" height="100%" name="xxx" :url="url" ></WujieVue>
  </div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
const url = 'http://127.0.0.1:5174/'
</script>
<style scoped lang='less'></style>


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


4e8466bce0b9e4ef243411fe0f0f9ed5.png


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


6a2f78fe6afb00a6e8ee7c7820fd9405.png


5e707b0cf1539aef1db581763c655726.png


这个就是差距 为什么使用新技术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<Response>;
    /** 子应插件 */
    plugins: Array<plugin>;
    /** 子应用生命周期 */
    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<Props['replace']>, default: undefined },
        fetch: { type: Function as PropType<Props['fetch']>, default: undefined },
        fiber: { type: Boolean, default: undefined },
        degrade: { type: Boolean, default: undefined },
        plugins: { type: Array as PropType<Props['plugins']>, default: null },
        beforeLoad: { type: Function as PropType<Props['beforeLoad']>, default: null },
        beforeMount: { type: Function as PropType<Props['beforeMount']>, default: null },
        afterMount: { type: Function as PropType<Props['afterMount']>, default: null },
        beforeUnmount: { type: Function as PropType<Props['beforeUnmount']>, default: null },
        afterUnmount: { type: Function as PropType<Props['afterUnmount']>, default: null },
        activated: { type: Function as PropType<Props['activated']>, default: null },
        deactivated: { type: Function as PropType<Props['deactivated']>, 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 的

目录
相关文章
|
8月前
|
前端开发 JavaScript
微前端——无界wujie
微前端——无界wujie
457 1
|
JavaScript 前端开发 API
无界微前端是如何渲染子应用的?(下)
无界微前端是如何渲染子应用的?(下)
638 0
|
29天前
|
编解码 前端开发 开发者
探索无界:前端开发中的响应式设计深度实践与思考###
本文将带你领略响应式设计的精髓,一种超越传统页面布局限制的设计策略,它要求开发者以灵活多变的思维,打造能够无缝适应各种设备与屏幕尺寸的Web体验。通过深入浅出的讲解、实际案例分析以及技术实现细节的探讨,本文目的是激发读者对于响应式设计深层次的理解与兴趣,鼓励在实际应用中不断创新与优化。 ###
73 10
|
1月前
|
前端开发 JavaScript 人机交互
探索无界:前端开发的响应式设计哲学####
在数字化浪潮汹涌的当下,响应式设计已不仅仅是一种技术实践,它是连接创意与功能、艺术与科学的桥梁。本文旨在探讨响应式设计的深层价值,揭示其如何赋予Web开发无限可能,让用户体验跨越设备界限,实现真正的“一网打尽”。不同于传统摘要的概括性介绍,本文将以一次思维旅行的形式,引领读者穿梭于代码与美学之间,感受响应式设计的魅力所在。 ####
29 3
|
2月前
|
编解码 前端开发 物联网
探索无界:前端开发中的响应式设计哲学####
在这篇文章中,我们不深入代码的细节,而是提升一个层次,探讨响应式设计的核心理念——它如何作为一种开发哲学,指导着前端开发者构建出能够适应各种设备与屏幕尺寸的网页。我们将通过一系列启发性的观点与思考,理解响应式设计不仅仅是技术实现,更是一种对用户体验的深刻洞察和尊重。 ####
38 5
|
2月前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计哲学####
在数字化浪潮汹涌的今天,用户体验成为了产品设计的核心。本文深入探讨了响应式设计在前端开发中的重要性及其背后的技术哲学,通过实际案例分析,阐述了如何运用流体布局、弹性图片与媒体查询等技术手段,实现跨设备的完美呈现。文章强调,响应式设计不仅仅是技术的堆砌,更是一种以用户为中心,追求无缝体验的设计思维。本文旨在为开发者提供新的视角和灵感,促进更加人性化、智能化的界面设计发展。 ####
|
2月前
|
机器学习/深度学习 编解码 前端开发
探索无界:前端开发中的响应式设计深度解析####
【10月更文挑战第29天】 在当今数字化时代,用户体验的优化已成为网站与应用成功的关键。本文旨在深入探讨响应式设计的核心理念、技术实现及最佳实践,揭示其如何颠覆传统布局限制,实现跨设备无缝对接,从而提升用户满意度和访问量。通过剖析响应式设计的精髓,我们将一同见证其在现代Web开发中的重要地位与未来趋势。 ####
48 7
|
2月前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计深度解析与实践####
【10月更文挑战第29天】 本文深入探讨了响应式设计的核心理念,即通过灵活的布局、媒体查询及弹性图片等技术手段,使网站能够在不同设备上提供一致且优质的用户体验。不同于传统摘要概述,本文将以一次具体项目实践为引,逐步剖析响应式设计的关键技术点,分享实战经验与避坑指南,旨在为前端开发者提供一套实用的响应式设计方法论。 ####
70 4
|
2月前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计哲学####
在数字时代的浪潮里,技术的迭代如同潮汐,每一次涌动都深刻地重塑着我们构建与感知虚拟世界的方式。本文旨在探讨前端开发中的核心议题——响应式设计,它如同一座桥梁,连接着技术实现与用户体验的两端,确保信息在不同设备间流畅、优雅地呈现。通过剖析响应式设计的精髓,本文将揭示其背后的哲学思考,以及如何在不断变化的技术景观中保持设计的前瞻性和灵活性。 ####
62 0
|
2月前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计哲学与实践####
本文不拘泥于传统摘要的框架,而是以一种对话的方式,引领读者踏入响应式设计的奇妙世界。想象一下,互联网如同一片浩瀚的海洋,而网页则是航行其中的船只。在这片不断变化的海域中,如何让我们的“船只”既稳固又灵活地适应各种屏幕尺寸和设备?这正是响应式设计的魅力所在。通过深入浅出的探讨,我们将一同揭开它背后的哲学思想与实战技巧,让你的网页在任何设备上都能展现出最佳姿态。 ####
28 0