前段时间陆续面试了一波候选人,其中提到最多的就是微前端方案,微前端不像前端框架的面试题那样,它更偏重于项目实战,更加考察候选人的技术水平,不像React,Vue随便一问,就是各种响应式原理,Fiber架构等等烂大街的。
如果你的简历平平无奇,面试官实在在你的简历上问不出什么,那么只能给你上点“手写题”强度了
作为面试官,我经常听到很多候选人说在公司做的项目很简单,平常就是堆页面,写管理端,写H5,没有任何亮点,我以我一次面试候选人的经历分享给大家
从这里你会觉得候选人的想法有点奇葩,但是换个角度来想,一定要等到项目庞大拆服务了才用微前端么,我管理端项目一开始就上微前端不行么。其实从这里可以看出来,管理系统使用微前端的成本并不会太大,而且从后面的技术问答中,候选人的微前端还是挺优秀的,各个细节基本都涉略到了。
如果你在公司内部很闲,又是刚好负责无关紧要的运营管理端,那么新的管理端可以一开始接入微前端方案,为未来的技术升级提供一个接口,风险也可控,还能够倒腾技术,简历还能新增亮点,何乐而不为
另外提到H5了,就提多一嘴,H5面向C端用户比较多,这方面更应该关心一些性能指标数据,比如FP,FCP等等,围绕这些指标进行优化,亮点不就来了么,这类例子比比皆是,要学会多挖掘
接下来是我作为面试官,经常考察候选人的问题,因为大部分候选人都是用qiankun框架,所以本文以qiankun框架为模板,重点剖析项目实战中微前端中遇到的问题和原理
微前端是一种将不同的前端应用组合到一起的架构模式。这些应用可以独立开发、独立部署、独立运行,然后在一个主应用中进行集成。这种模式的主要目标是解决大型、长期演进的前端项目的复杂性问题。
主要优点:
主要挑战:
qiankun 是一个基于 single-spa 的微前端实现框架。它的工作原理主要涉及到以下几个方面:
在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,静态资源的加载问题可能会成为一个挑战。这是因为在微前端环境中,子应用的静态资源路径可能需要进行特殊处理才能正确加载。这里有几种可能的解决方案:
在子应用的静态资源路径前添加公共路径前缀。例如,如果子应用的静态资源存放在
http://localhost:8080/static/,那么可以在所有的静态资源路径前添加这个前缀。
这个方案分为两步:
例如,我们可以传递一个 getTemplate 函数,将图片的相对路径转为绝对路径,它会在处理模板时使用:
start({ getTemplate(tpl,...rest) { // 为了直接看到效果,所以写死了,实际中需要用正则匹配 return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">'); }});
对于动态插入的标签,劫持其插入 DOM 的函数,注入前缀。
beforeMount: app => { if(app.name === 'purehtml'){ // jQuery 的 html 方法是一个挺复杂的函数,这里只是为了看效果,简写了 $.prototype.html = function(value){ const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">') this[0].innerHTML = str; } }}
这个方案的可行性不高,都是陈年老项目了,没必要这样折腾。
在使用 qiankun 时,如果子应用动态插入了一些标签,我们可以通过劫持 DOM 的一些方法来处理。例如,我们可以劫持 appendChild、innerHTML 和 insertBefore 等方法,将资源的相对路径替换为绝对路径。
以下是一个例子,假设我们有一个子应用,它使用 jQuery 动态插入了一张图片:
const render = $ => { $('#app-container').html('<p>Hello, render with jQuery</p><img src="./img/my-image.png">'); return Promise.resolve();};
我们可以在主应用中劫持 jQuery 的 html 方法,将图片的相对路径替换为绝对路径:
beforeMount: app => { if(app.name === 'my-app'){ // jQuery 的 html 方法是一个复杂的函数,这里为了简化,我们只处理 img 标签 $.prototype.html = function(value){ const str = value.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">') this[0].innerHTML = str; } }}
在这个例子中,我们劫持了 jQuery 的 html 方法,将图片的相对路径 ./img/my-image.png 替换为了绝对路径
http://localhost:8080/img/my-image.png。这样,无论子应用在哪里运行,图片都可以正确地加载。
在使用 qiankun 时,处理老项目的资源加载问题可以有多种方案,具体的选择取决于项目的具体情况。以下是一些可能的解决方案:
start({ getTemplate(tpl,...rest) { // 为了直接看到效果,所以写死了,实际中需要用正则匹配 return tpl.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">'); }});
beforeMount: app => { if(app.name === 'my-app'){ $.prototype.html = function(value){ const str = value.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">') this[0].innerHTML = str; } }}
qiankun 的 start 函数是用来启动微前端应用的。在注册完所有的子应用之后,我们需要调用 start 函数来启动微前端应用。
start 函数接收一个可选的配置对象作为参数,这个对象可以包含以下属性:
如果只有一个子项目,要想启用预加载,可以这样使用 start 函数:
start({ prefetch: 'all' });
这样,主应用 start 之后会预加载子应用的所有静态资源,无论子应用是否激活。
qiankun 的 js 沙箱机制主要是通过代理 window 对象来实现的,它可以有效地隔离子应用的全局变量,防止子应用之间的全局变量污染。然而,这种机制并不能解决所有的 js 污染问题。例如,如果我们使用 onclick 或 addEventListener 给 <body> 添加了一个点击事件,js 沙箱并不能消除它的影响。
对于这种情况,我们需要依赖于良好的代码规范和开发者的自觉。在开发子应用时,我们需要避免直接操作全局对象,如 window 和 document。如果必须要操作,我们应该在子应用卸载时,清理掉这些全局事件和全局变量,以防止对其他子应用或主应用造成影响。
例如,如果我们在子应用中添加了一个全局的点击事件,我们可以在子应用的 unmount 生命周期函数中移除这个事件:
export async function mount(props) { // 添加全局点击事件 window.addEventListener('click', handleClick);}export async function unmount() { // 移除全局点击事件 window.removeEventListener('click', handleClick);}function handleClick() { // 处理点击事件}
这样,当子应用卸载时,全局的点击事件也会被移除,不会影响到其他的子应用。
在 qiankun 中,实现 keep-alive 的需求有一定的挑战性。这是因为 qiankun 的设计理念是在子应用卸载时,将环境还原到子应用加载前的状态,以防止子应用对全局环境造成污染。这种设计理念与 keep-alive 的需求是相悖的,因为 keep-alive 需要保留子应用的状态,而不是在子应用卸载时将其状态清除。
然而,我们可以通过一些技巧来实现 keep-alive 的效果。一种可能的方法是在子应用的生命周期函数中保存和恢复子应用的状态。例如,我们可以在子应用的 unmount 函数中保存子应用的状态,然后在 mount 函数中恢复这个状态:
// 伪代码let savedState;export async function mount(props) { // 恢复子应用的状态 if (savedState) { restoreState(savedState); }}export async function unmount() { // 保存子应用的状态 savedState = saveState();}function saveState() { // 保存子应用的状态 // 这个函数的实现取决于你的应用}function restoreState(state) { // 恢复子应用的状态 // 这个函数的实现取决于你的应用}
这种方法的缺点是需要手动保存和恢复子应用的状态,这可能会增加开发的复杂性。此外,这种方法也不能保留子应用的 DOM 状态,只能保留 JavaScript 的状态。
还有一种就是手动*loadMicroApp*+display:none,直接隐藏Dom
另一种可能的方法是使用 single-spa 的 Parcel 功能。Parcel 是 single-spa 的一个功能,它允许你在一个应用中挂载另一个应用,并且可以控制这个应用的生命周期。通过 Parcel,我们可以将子应用挂载到一个隐藏的 DOM 元素上,从而实现 keep-alive 的效果。然而,这种方法需要对 qiankun 的源码进行修改,因为 qiankun 目前并不支持 Parcel。
qiankun 和 iframe 都是微前端的实现方式,但它们在实现原理和使用场景上有一些区别。
qiankun 是基于 single-spa 的微前端解决方案,它通过 JavaScript 的 import 功能动态加载子应用,然后在主应用的 DOM 中挂载子应用的 DOM。qiankun 提供了一种 JavaScript 沙箱机制,可以隔离子应用的全局变量,防止子应用之间的全局变量污染。此外,qiankun 还提供了一种样式隔离机制,可以防止子应用的 CSS 影响其他应用。这些特性使得 qiankun 在处理复杂的微前端场景时具有很高的灵活性。
iframe 是一种较为传统的前端技术,它可以在一个独立的窗口中加载一个 HTML 页面。iframe 本身就是一种天然的沙箱,它可以完全隔离子应用的 JavaScript 和 CSS,防止子应用之间的相互影响。然而,iframe 的这种隔离性也是它的缺点,因为它使得主应用和子应用之间的通信变得困难。此外,iframe 还有一些其他的问题,比如性能问题、SEO 问题等。
在选择 qiankun 和 iframe 时,需要根据具体的使用场景来决定。如果你的子应用是基于现代前端框架(如 React、Vue、Angular 等)开发的单页应用,那么 qiankun 可能是一个更好的选择,因为它可以提供更好的用户体验和更高的开发效率。如果你的子应用是基于 jQuery 或者其他传统技术开发的多页应用,或者你需要在子应用中加载一些第三方的页面,那么 iframe 可能是一个更好的选择,因为它可以提供更强的隔离性。
在使用qiankun处理多个子项目的调试问题时,通常的方式是将每个子项目作为一个独立的应用进行开发和调试。每个子项目都可以在本地启动,并通过修改主应用的配置,让主应用去加载本地正在运行的子应用,这样就可以对子应用进行调试了。这种方式的好处是,子应用与主应用解耦,可以独立进行开发和调试,不会相互影响。
对于如何同时启动多个子应用,你可以使用npm-run-all这个工具。npm-run-all是一个CLI工具,可以并行或者串行执行多个npm脚本。这个工具对于同时启动多个子应用非常有用。使用方式如下:
npm install --save-dev npm-run-all
"scripts": { "start:app1": "npm start --prefix ./app1", "start:app2": "npm start --prefix ./app2", "start:all": "npm-run-all start:app1 start:app2"}
在这个例子中,start:app1和start:app2脚本分别用于启动app1和app2应用,start:all脚本则用于同时启动这两个应用。
npm-run-all不仅可以并行运行多个脚本,还可以串行运行多个脚本。在某些情况下,你可能需要按照一定的顺序启动你的应用,这时你可以使用npm-run-all的-s选项来串行执行脚本,例如:npm-run-all -s script1 script2,这将会先执行script1,然后再执行script2。
qiankun主要通过使用Shadow DOM来实现CSS隔离。
// qiankun使用Shadow DOM挂载子应用const container = document.getElementById('container');const shadowRoot = container.attachShadow({mode: 'open'});shadowRoot.innerHTML = '<div id="subapp-container"></div>';
对于qiankun的隔离方案,一个潜在的缺点是它需要浏览器支持Shadow DOM,这在一些旧的浏览器或者不兼容Shadow DOM的浏览器中可能会出现问题。
另一种可能的方案是使用CSS模块(CSS Modules)。CSS模块是一种将CSS类名局部化的方式,可以避免全局样式冲突。在使用CSS模块时,每个模块的类名都会被转换成一个唯一的名字,从而实现样式的隔离。
例如,假设你有一个名为Button的CSS模块:
/* Button.module.css */.button { background-color: blue;}
在你的JavaScript文件中,你可以这样引入并使用这个模块:
import styles from './Button.module.css';function Button() { return <button className={styles.button}>Click me</button>;}
在这个例子中,button类名会被转换成一个唯一的名字,如Button_button__xxx,这样就可以避免全局样式冲突了。
3.BEM命名规范隔离
window.globalEvent = { events: {}, emit(event, data) { if (!this.events[event]) { return; } this.events[event].forEach(callback => callback(data)); }, on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); },};
在项目间共享组件时,可以考虑以下几种方式:
需要注意的是,在使用异步组件或手动加载子项目时,可能会遇到样式加载的问题,可以尝试解决该问题。另外,如果共享的组件依赖全局插件(如store和i18n),需要进行特殊处理以确保插件的正确初始化。
Webpack 5 的联邦模块(Federation Module)是一个功能强大的特性,可以在微前端应用中实现模块共享和动态加载,从而提供更好的代码复用和可扩展性
Webpack 5 的联邦模块允许不同的微前端应用之间共享模块,避免重复加载和代码冗余。通过联邦模块,我们可以将一些公共的模块抽离成一个独立的模块,并在各个微前端应用中进行引用。这样可以节省资源,并提高应用的加载速度。
// main-app webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin');const { ModuleFederationPlugin } = require('webpack').container;module.exports = { // ...其他配置 plugins: [ new HtmlWebpackPlugin(), new ModuleFederationPlugin({ name: 'main_app', remotes: { shared_module: 'shared_module@http://localhost:8081/remoteEntry.js', }, }), ],};// shared-module webpack.config.jsconst { ModuleFederationPlugin } = require('webpack').container;module.exports = { // ...其他配置 plugins: [ new ModuleFederationPlugin({ name: 'shared_module', filename: 'remoteEntry.js', exposes: { './Button': './src/components/Button', }, }), ],};
在上述示例中,main-app 和 shared-module 分别是两个微前端应用的 webpack 配置文件。通过 ModuleFederationPlugin 插件,shared-module 将 Button 组件暴露给其他应用使用,而 main-app 则通过 remotes 配置引入了 shared-module。
Webpack 5 联邦模块还支持动态加载模块,这对于微前端应用的按需加载和性能优化非常有用。通过动态加载,可以在需要时动态地加载远程模块,而不是在应用初始化时一次性加载所有模块。
// main-appconst remoteModule = () => import('shared_module/Button');// ...其他代码// 在需要的时候动态加载模块remoteModule().then((module) => { // 使用加载的模块 const Button = module.default; // ...});
在上述示例中,main-app 使用 import() 函数动态加载 shared_module 中的 Button 组件。通过动态加载,可以在需要时异步地加载远程模块,并在加载完成后使用模块。
在微前端应用中可以实现模块共享和动态加载,提供了更好的代码复用和可扩展性。通过模块共享,可以避免重复加载和代码冗余,而动态加载则可以按需加载模块,提高应用的性能和用户体验。
qiankun import-html-entry 是qiankun 框架中用于加载子应用的 HTML 入口文件的工具函数。它提供了一种方便的方式来动态加载和解析子应用的 HTML 入口文件,并返回一个可以加载子应用的 JavaScript 模块。
具体而言,import-html-entry 实现了以下功能:
通过使用 qiankun import-html-entry,开发者可以方便地将子应用的 HTML 入口文件作为模块加载,并获得一个可以加载和启动子应用的函数,简化了子应用的加载和集成过程。
以下是对各个微前端框架优缺点的总结:
作者:linwu
链接:
https://juejin.cn/post/7252342216843296828