【欧冠体育体育竞猜 官网入口】

Vite 微前端实际,实现一个组件化的规划

发布日期:2022-08-06 18:36    点击次数:111

本文转载自微信群众号「前端星辰」,作者旋律 。转载本文请联络前端星辰群众号。

什么是微前端

微前端是一种多个团队经由过程独立宣布功用的编制来怪异构建今世化 web 应用的技能伎俩及编制计策。

微前端自创了微服务的架构理念,将一个零乱的前端应用拆分为多个独立灵巧的小型应用,每个应用均可以或许独立开发、独立运行、独立陈列,再将这些小型应用联合为一个完备的应用。微前端既可以或许将多个名目领悟为一,又可以或许削减名目之间的耦合,提升名目扩张性,相比一整块的前端货仓旅馆,微前端架构下的前端货仓旅馆标的目标于更小更灵巧。

特点 技能栈有关 主框架不限定接入应用的技能栈,子应用可自主抉择技能栈 独立开发/陈列 各个团队之间货仓旅馆独立,零丁陈列,互不寄托 增量降级 当一个应用零乱今后,技能降级或重构相当麻烦,而微应用具有渐进式降级的特点 独立运行时 微应用之间运行时互不寄托,有独立的形态打点 提升效劳 应用越零乱,越难以回护,协作效劳越低下。微应用可以或许很好拆分,提升效劳 而今可用的微前端规划

微前端的规划而今有下列几种范例:

基于 iframe 齐全断绝的规划

作为前端开发,我们对 iframe 已经极度意识了,在一个应用中可以或许独立运行另外一个应用。它具有较着的所长:

极度俭朴,无需任何鼎新 完善断绝,JS、CSS 都是独立的运行情形 不限定应用,页面上可以或许放多个 iframe 来组合业务

固然,弱点也极度突出:

没法对立路由形态,刷新后路由形态就遗失 齐全的断绝导致与子应用的交互变得极为费力 iframe 中的弹窗没法冲破其本身

全副应用全量资源加载,加载太慢

这些较着的弱点也催生了别的规划的孕育发生。

基于 single-spa 路由劫持规划

single-spa 经由过程劫持路由的编制来做子应用之间的切换,但接入编制须法子悟本身的路由,有必定的范围性。

qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品同一接入平台。它对 single-spa 做了一层封装。首要经管了 single-spa 的一些痛点和无余。经由过程 import-html-entry 包剖析 HTML 取得资源门路,尔后对资源举行剖析、加载。

经由过程对执行情形的编削,它实现了 JS 沙箱、款式断绝 等特点。

京东 micro-app 规划

京东 micro-app 并无沿袭 single-spa 的思路,而是自创了 WebComponent 的思惟,经由过程 CustomElement 联合自定义的 ShadowDom,将微前端封装成一个类 webComponents 组件,从而实现微前端的组件化衬着。

在 Vite 上应用微前端

我们从 我们从 UmiJS 迁移到了 Vite 今后,微前端一样成了势在必行,事先也调研了良多规划。

为何没用 qiankun

qiankun 是而今是社区主流微前端规划。它诚然很完善、流行,但最大的成就就是不支持 Vite。它基于 import-html-entry 剖析 HTML 来取得资源,因为 qiankun 是经由过程 eval 来执行这些 js 的内容,而 Vite 中的 script 标签范例是 type="module",内里包孕 import/export 等模块代码, 所以会报错:不许许在非 type="module" 的 script 内里应用 import。

退一步实现,我们给与了 single-spa 的编制,并应用 systemjs 的编制举行了微前端加载规划,也踩了良多的坑。single-spa 没有一个敌对的教程来接入,文档诚然多,但大多都在讲见解,事先让人感应有一种深奥的感到。

其后看了它的源码缔造,这都是些什么……内里大部份代码都是萦绕路由劫持而开展的,基本没有文档上那种高峻上的感到。而我们又用不到它路由劫持的功用,那我们为何要用它?

从组件化的层面来说 single-spa 这类编制实现得一点都不优雅。

它劫持了路由,与 react-router 和组件化的思惟同床异梦 接入编制一大堆繁冗的设置 单实例的规划,即同一时分,只有一个子应用被展现

其后料到着 single-spa 的弱点,我们可以或许本身实现一个组件化的微前端规划。

怎么样实现一个俭朴、通明、组件化的规划

经由过程组件化思惟实现一个微应用极度俭朴:子应用导出一个编制,主应用加载子应用并调用该编制,并传入一个 Element 节点参数,子应用失去该 Element 节点,将本身的组件 appendChild 到 Element 节点上。

范例约定

在此从前我们必要约定一个主应用与子应用之间的一个交互编制。首要经由过程三个钩子来担保应用的准确执行、更新、和卸载。

范例定义:

export interface AppConfig {   // 挂载   mount?: (props: unknown) => void;   // 更新   render?: (props: unknown) => ReactNode | void;   // 卸载   unmount?: () => void; } 

子应用导出

经由过程范例的约定,我们可以或许将子应用导出:mount、render、unmount 为首要钩子。

React 子应用实现:

export default (container: HTMLElement) => {   let handleRender: (props: AppProps) => void;    // 包裹一个新的组件,用作更新处理惩罚   function Main(props: AppProps) {     const [state, setState] = React.useState(props);     // 将 setState 编制提取给 render 函数调用,对立父子应用触发更新     handleRender = setState;     return <App {...state} />;   }    return {     mount(props: AppProps) {       ReactDOM.render(<Main {...props} />, container);     },     render(props: AppProps) {       handleRender?.(props);     },     unmount() {       ReactDOM.unmountComponentAtNode(container);     },   }; }; 

 

Vue 子应用实现:

import { createApp } from 'vue'; import App from './App.vue';  export default (container: HTMLElement) => {   // 创立   const app = createApp(App);   return {     mount() {       // 装载       app.mount(container);     },     unmount() {       // 卸载       app.unmount();     },   }; }; 
主应用实现

React 实现

其焦点代码仅十余行,首要处理惩罚与子应用交互 (为了易读性,企业文化潜匿了舛误处理惩罚代码):

export function MicroApp({ entry, ...props }: MicroAppProps) {   // 通报给子应用的节点   const containerRef = useRef<HTMLDivElement>(null);   // 子应用设置   const configRef = useRef<AppConfig>();    useLayoutEffect(() => {     import(/* @vite-ignore */ entry).then((res) => {       // 将 div 传给子应用衬着       const config = res.default(containerRef.current);       // 调用子应用的装载编制       config.mount?.(props);       configRef.current = config;     });     return () => {       // 调用子应用的卸载编制       configRef.current?.unmount?.();       configRef.current = undefined;     };   }, [entry]);    return <div ref={containerRef}>{configRef.current?.render?.(props)}</div>; } 

实现,而今已经实现了主应用与子应用的装载、更新、卸载的操作。而今,它是一个组件,可以或许同时衬着出多个差别的子应用,这点就比 single-spa 优雅良多。

entry 子应用地点,固然其实情形会痛处 dev 和 prod 情势给出差别的地点:

<MicroApp className="micro-app" entry="//localhost:3002/src/main.tsx" /> 

Vue 实现

<script setup lang="ts"> import { onMounted, onUnmounted, ref } from 'vue';  const { entry, ...props } = defineProps<{ entry: string }>(); const container = ref<HTMLDivElement | null>(null); const config = ref();  onMounted(() => {   const element = container.value;   import(/* @vite-ignore */ entry).then((res) => {     // 将 div 传给子应用衬着     const config = res.default(element);     // 调用子应用的装载编制     config.mount?.(props);     config.value = config;   }); });  onUnmounted(() => {   // 调用子应用的卸载编制   config.value?.unmount?.(); }); </script>  <template>   <div ref="container"></div> </template>

怎么样让子应用也能独立运行

single-spa 等众多规划,都是将一个变量挂载到 window 上,经由过程鉴定该变量是否处于微前端情形,这样很不优雅。在 ESM 中,我们可以或许经由过程 import.meta.url 传入参数来鉴定:

if (!import.meta.url.includes('microAppEnv')) {   ReactDOM.render(     <React.StrictMode>       <App />     </React.StrictMode>,     document.getElementById('root'),   ); } 

入口导入编削:

// 增加情形参数和今后时光防止被缓存 import(/* @vite-ignore */ `${entry}?microAppEnv&t=${Date.now()}`); 

阅读器兼容性

IE 阅读器已经缓缓退出我们的视野,基于 Vite,我们只有要支持 import 的特点阅读器就够了。固然,假定推敲 IE 阅读器的话也不是不成以,很俭朴:将上面代码的 import 替代为 System.import 即 systemjs,也是 single-spa 的所推崇的用法。

阅读器 Chrome Edge Firefox Internet Explorer Safari import 61 16 60 No 10.1 Dynamic import 63 79 67 No 11.1 import.meta 64 79 62 No 11.1

模块专用

我们的子组件必必要应用 mount 、unount 情势吗?答案是不必定,假定我们的技能栈都是 React 的话。我们的子应用只导出一个 render 就够了。这样用的就是同一个 React 来衬着,益处是子应用可以或许破费父应用的 Provider。但有个前提是两个应用之间的 React 必须为同一个实例,否则就会报错。

我们可以或许将 react、react-dom 、styled-componets 等经常使用模块提早打包成 ESM 模块,尔后放到文件服务中应用。

改观 Vite 设置增加 alias:

defineConfig({   resolve: {     alias: {       react: '//localhost:8000/react@17.js',       'react-dom': '//localhost:8000/react-dom@17.js',     },   }, }); 

这样就能舒畅地应用同一份 React 代码了。还能抽离出主应用和子应用之间的专用模块,让应用整体积更小。固然假定没上 http2 的话,就必要推敲颗粒度的成就了。

在线 CDN 规划:https://esm.sh

另有个 importmap 规划,兼容性不太好,但未来是趋势:

<script type="importmap">   {     "imports": {       "react": "//localhost:8000/react@17.js"     }   } </script

 

父子通信

组件式微应用,可以或许通报参数而通信,齐全就是 React 组件通信的模型。

资源门路

import logo from './images/logo.svg';  <img src={logo} />; 

在 Vite 的 dev 情势中,子应用内里静态资源普通会这样引入:

import logo from './images/logo.svg';  <img src={logo} />; 

图片的门路:/basename/src/logo.svg,在主应用体现就会 404。因为该门路只是存在于子应用。我们必要共同 URL 模块应用,这样门路前面会带上 origin 前缀:

const logoURL = new URL(logo, import.meta.url);  <img src={logoURL.href} />; 

固然这样应用相比繁缛,我们可以或许将其封装为一个 Vite 插件自动处理惩罚该场景。

路由同步

名目应用 react-router,那末它可以或许会存在路由差别步的成就,因为不是同一个 react-router 实例。即路由之间出现不联动的景象。

在 react-router 支持自定义 history 库,我们可以或许创立:

import { createBrowserHistory } from 'history';  export const history = createBrowserHistory();  // 主应用:路由入口 <HistoryRouter history={history}>{children}</HistoryRouter>;  // 主应用:通报给子应用 <Route   path="/child-app/*"   element={<MicroApp entry="//localhost:3002/src/main.tsx" history={history} />} />;  // 子应用:路由入口 <HistoryRouter basename="/child-app" history={history}>   {children} </HistoryRouter>; 

终究子应用应用同一份 history 模块。固然这不是仅有的实现,也不是优雅的编制,我们可以或许将路由实例 navigate 通报给子应用,这样也能实现路由的交互。

留心:子应用的 basename 必须与主应用的 path 名称对立分歧。这里还必要编削 Vite 的设置 base 字段:

export default defineConfig({   base: '/child-app/',   server: {     port: 3002,   },   plugins: [react()], }); 

JS 沙箱

因为沙箱在 ESM 下不支持,因为没法静态改变执行情形中模块 window 工具,也没法注入新的全局工具。

普通 React、Vue 名目也很少编削全局变量,做好代码尺度查抄才是最首要的。

CSS 款式断绝

自动 CSS 款式断绝是有价值的,普通我们倡导子应用应用差别的 CSS 前缀,再共同 CSS Modules 根抵上能实现需要。

打包陈列

陈列可以或许痛处子应用的 base 搁置在差别的目录,并将名称对应。设置好 nginx 转发划定端方就能了。我们可以或许将子应用同一同由前缀,便于 nginx 将主应用判别隔断绝分散并设置通用划定端方。

比喻将主应用搁置在 system 目录,子应用搁置在 app- 结尾的目录:

location ~ ^/app-.*(\..+)$ {     root /usr/share/nginx/html; }  location / {     try_files $uri $uri/ /index.html;     root /usr/share/nginx/html/system;     index  index.html index.htm; } 

所长

1. 俭朴 焦点无余 100 行代码,无需多余的文档

2. 灵巧 经由过程约定的编制接入,也可以渐进增强

3. 通明 无任何劫持规划,更多逻辑通明性

4. 组件化 组件化的衬着及参数通信

5. 基于 ESM 支持 Vite,面向未来

6. 向下兼容 可选 SystemJS 规划,兼容低版本阅读器

有示例吗

示例代码在 Github,感兴致的同伙可以或许 clone 上去深造。因为我们的技能栈是 React,所以这里示例的主应用的实现用的是 React 。

微前端组件(React):https://github.com/MinJieLiu/micro-app

微前端示例:https://github.com/MinJieLiu/micro-app-demo

结语

微前端的规划得当团队场景的最佳,打造一个团队能把握的规划尤其首要。

参考材料:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import