各位听众朋友们,大家好!我是今天的主讲人,很高兴能和大家一起聊聊JS微前端架构,这个听起来高大上,但其实也没那么神秘的技术。今天咱们不搞虚的,直接上干货,用最接地气的方式,把微前端这事儿掰开了揉碎了讲明白。
开场白:微前端,你到底是个啥?
首先,咱们得搞明白,啥是微前端?简单来说,就是把一个大型前端应用拆分成多个小型、自治的应用,这些小应用可以由不同的团队开发、部署和维护,最终组合成一个完整的用户界面。
你可以把它想象成乐高积木,每个积木(微应用)都是独立制作的,可以单独更换,但最终拼起来还是一个完整的城堡(整体应用)。
为什么要搞微前端?
你可能会问,为啥要这么折腾?好好的一个应用,拆它干啥?原因有很多:
- 团队自治: 各个团队可以独立开发、测试和部署自己的微应用,互不干扰,提高开发效率。想象一下,一个团队在搞React,另一个在搞Vue,互不耽误,岂不美哉?
- 技术栈无关: 每个微应用可以选择最适合自己的技术栈,不再受限于整个应用的统一技术选型。比如,你想用Svelte写个小模块,完全没问题!
- 增量升级: 可以逐步升级应用,而不是一次性重构整个应用。这对于大型、遗留应用来说,简直是救命稻草。
- 独立部署: 每个微应用可以独立部署,降低了部署风险,提高了发布频率。出问题了,只回滚一个小应用,影响范围可控。
- 更好的可维护性: 小型应用更容易维护和调试,代码量少,逻辑清晰。
微前端架构的核心要素
微前端架构的核心在于:框架无关、应用隔离和通信机制。
-
框架无关性:
这意味着每个微应用可以使用不同的前端框架(React、Vue、Angular等),甚至可以混用。为了实现这一点,我们需要一些技巧,比如使用Web Components或者自定义元素。
-
Web Components: Web Components 是一套浏览器原生支持的技术,允许我们创建可重用的自定义 HTML 元素。它们与框架无关,可以在任何支持 Web Components 的浏览器中使用。
<!-- my-element.js --> <script> class MyElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> :host { display: block; border: 1px solid black; padding: 10px; } </style> <h1>Hello from My Element!</h1> <p>This is a Web Component.</p> `; } } customElements.define('my-element', MyElement); </script>
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>Web Component Example</title> </head> <body> <my-element></my-element> </body> </html>
-
自定义元素: 你也可以不使用 shadow DOM,直接用 JavaScript 创建自定义元素。但这通常不如 Web Components 规范强大和安全。
-
-
应用隔离:
每个微应用应该运行在自己的沙箱环境中,避免样式和JavaScript冲突。
-
Shadow DOM: Web Components 的 Shadow DOM 提供了一种封装样式和行为的方式,可以有效地隔离微应用。
-
iframe: iframe 是最简单的隔离方式,但它也有一些缺点,比如通信比较麻烦,性能也相对较差。
-
JavaScript沙箱: 可以通过一些技术手段(比如Proxy)创建一个JavaScript沙箱,限制微应用的访问权限。
-
-
通信机制:
微应用之间需要进行通信,比如共享数据、触发事件等。
-
自定义事件 (Custom Events): 微应用可以通过派发自定义事件来通知其他微应用。
// 微应用A const event = new CustomEvent('my-event', { detail: { message: 'Hello from A!' } }); window.dispatchEvent(event); // 微应用B window.addEventListener('my-event', (event) => { console.log(event.detail.message); // 输出: Hello from A! });
-
Shared Storage (localStorage/sessionStorage): 可以使用 localStorage 或 sessionStorage 共享一些简单的数据。但是要注意,这可能会导致命名冲突。
-
消息队列 (Message Queue): 可以使用消息队列服务(比如RabbitMQ、Redis)来实现更复杂的微应用通信。
-
状态管理工具 (Redux/Vuex): 可以使用全局状态管理工具来共享状态。但是要注意,这可能会导致微应用之间的耦合。
-
微前端架构的几种常见模式
现在,我们来看看几种常见的微前端架构模式:
-
构建时集成 (Build-time Integration):
这种模式是在构建时将所有微应用打包成一个完整的应用。
- 优点: 简单,易于实现。
- 缺点: 缺乏灵活性,每次修改都需要重新构建整个应用。
- 适用场景: 比较小的项目,或者对性能要求不高的项目。
-
运行时集成 (Runtime Integration):
这种模式是在运行时动态加载和渲染微应用。
- 优点: 灵活性高,可以独立部署和更新微应用。
- 缺点: 实现起来比较复杂,需要考虑应用隔离和通信。
- 适用场景: 大型项目,或者需要频繁更新和部署的项目。
运行时集成又可以细分为几种子模式:
-
基于路由的集成 (Route-based Integration): 根据 URL 路由将请求转发到不同的微应用。
// 伪代码 const routes = { '/app1': 'http://localhost:3001/app1.js', '/app2': 'http://localhost:3002/app2.js', }; function loadApp(route) { const script = document.createElement('script'); script.src = routes[route]; document.body.appendChild(script); } window.addEventListener('hashchange', () => { const route = window.location.hash.slice(1); // 获取 #/app1 之后的部分 loadApp(route); });
-
基于 Web Components 的集成 (Web Components-based Integration): 将微应用封装成 Web Components,然后在主应用中动态加载和渲染这些 Web Components。
// 主应用 import('./app1.js').then(() => { const app1 = document.createElement('app-1'); document.getElementById('container').appendChild(app1); });
-
基于 iframe 的集成 (Iframe-based Integration): 将每个微应用放到一个 iframe 中,然后在主应用中嵌入这些 iframe。
<!-- 主应用 --> <iframe src="http://localhost:3001/app1.html"></iframe> <iframe src="http://localhost:3002/app2.html"></iframe>
-
边缘集成 (Edge Integration):
这种模式是在边缘服务器(比如CDN)上进行微应用的组合。
- 优点: 性能好,可以利用CDN的缓存能力。
- 缺点: 实现起来比较复杂,需要对边缘服务器进行配置。
- 适用场景: 对性能要求非常高的项目。
微前端框架和工具
市面上有很多微前端框架和工具,可以帮助我们更方便地实现微前端架构。这里列举几个比较流行的:
框架/工具 | 特点 |
---|---|
Qiankun | 基于 single-spa,支持多种框架,提供完善的应用隔离和通信机制。阿里出品,中文文档完善。 |
single-spa | 一个微前端路由协调器,可以集成各种框架的应用。比较底层,需要自己实现应用隔离和通信。 |
Module Federation | Webpack 5 的一个特性,允许不同的 Webpack 构建之间共享代码。可以实现非常灵活的微前端架构。 |
FrintJS | 基于 RxJS 的微前端框架,强调响应式编程。 |
Piral | 一个基于 React 的微前端框架,提供插件机制。 |
实战案例:用 Qiankun 实现一个简单的微前端应用
接下来,我们用 Qiankun 来实现一个简单的微前端应用,包含两个微应用:一个 React 应用和一个 Vue 应用。
-
创建主应用 (Main App):
npx create-react-app main-app cd main-app npm install qiankun --save npm start
修改
src/App.js
:import React, { useEffect } from 'react'; import { registerMicroApps, start } from 'qiankun'; function App() { useEffect(() => { registerMicroApps([ { name: 'react-app', entry: '//localhost:3001', // React 应用的地址 container: '#container', activeRule: '/react', }, { name: 'vue-app', entry: '//localhost:3002', // Vue 应用的地址 container: '#container', activeRule: '/vue', }, ]); start(); }, []); return ( <div> <h1>Main App</h1> <div id="container"></div> </div> ); } export default App;
-
创建 React 微应用 (React App):
npx create-react-app react-app cd react-app npm start
修改
src/index.js
:import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; function render(props) { const { container } = props; ReactDOM.render(<App {...props} />, container ? container.querySelector('#root') : document.getElementById('root')); } if (!window.__POWERED_BY_QIANKUN__) { render({}); } export async function bootstrap(props) { console.log('[react-app] bootstrap', props); } export async function mount(props) { console.log('[react-app] mount', props); render(props); } export async function unmount(props) { console.log('[react-app] unmount', props); const { container } = props; ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.getElementById('root')); }
修改
public/index.html
:<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <div id="container"></div> </body> </html>
修改
package.json
:{ "name": "react-app", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devServer": { "port": 3001, "headers": { "Access-Control-Allow-Origin": "*" } } }
在
package.json
中添加devServer
配置,允许跨域请求。 -
创建 Vue 微应用 (Vue App):
vue create vue-app cd vue-app npm install npm start
修改
src/main.js
:import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false let instance = null; function render(props = {}) { const { container } = props; instance = new Vue({ render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue-app] bootstraped'); } export async function mount(props) { console.log('[vue-app] mount', props); render(props); } export async function unmount() { console.log('[vue-app] unmount'); instance.$destroy(); instance.$el.innerHTML = ''; instance = null; }
修改
vue.config.js
:module.exports = { devServer: { port: 3002, headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: 'vue-app', libraryTarget: 'umd', }, }, };
在
vue.config.js
中配置devServer
和configureWebpack
。devServer
用于允许跨域请求,configureWebpack
用于配置输出格式为 UMD。 -
启动所有应用:
分别启动主应用、React 微应用和 Vue 微应用。
在浏览器中访问主应用 (http://localhost:3000),然后访问 http://localhost:3000/#/react 和 http://localhost:3000/#/vue,就可以看到 React 和 Vue 微应用被加载到主应用中了。
微前端的挑战与注意事项
微前端架构虽然有很多优点,但也带来了一些挑战:
- 复杂性增加: 需要管理多个应用,增加了整体的复杂性。
- 运维成本增加: 需要部署和维护多个应用,增加了运维成本。
- 共享依赖: 如果多个微应用依赖同一个库的不同版本,可能会导致冲突。
- 性能问题: 如果微应用加载和渲染速度慢,可能会影响用户体验。
- SEO问题: 微前端架构可能会对搜索引擎优化产生影响,需要特别注意。
因此,在选择微前端架构之前,需要仔细评估项目的需求和团队的能力。
总结
微前端是一种非常有潜力的前端架构,可以帮助我们解决大型前端应用开发和维护的难题。但是,它也带来了一些挑战,需要我们认真对待。希望今天的讲座能帮助大家更好地理解微前端,并在实际项目中应用它。
今天的分享就到这里,谢谢大家!有问题欢迎提问。