前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >金九银十,带你复盘大厂常问的项目难点

金九银十,带你复盘大厂常问的项目难点

作者头像
linwu
发布2023-09-07 08:11:13
6690
发布2023-09-07 08:11:13
举报
文章被收录于专栏:编程时光编程时光

引言

最近整理了一套面试小册,有在线版和离线版本

离线版本效果如下,可添加微信linwu-hi获取,阅读效果非常不错

image.png
image.png

微前端

为什么选择微前端作为项目亮点

如果你的简历平平无奇,面试官实在在你的简历上问不出什么,那么只能给你上点“手写题”强度了

作为面试官,我经常听到很多候选人说在公司做的项目很简单,平常就是堆页面,写管理端,写H5,没有任何亮点,我以我一次面试候选人的经历分享给大家

面试官:你为什么选择用微前端做管理端升级,你的项目很庞大么?

候选人: 不是的,其实是我们把两个管理端合并,让用户方便使用。

面试官:咦,竟然这样你们还不如用a标签链接或者nginx转发一下就行了,更加方便,杀鸡焉用牛刀啊

候选人:为了让客户体验到单页面的感觉,体验感更好

面试官:enen....

从这里你会觉得候选人的想法有点奇葩,但是换个角度来想,一定要等到项目庞大拆服务了才用微前端么,我管理端项目一开始就上微前端不行么。其实从这里可以看出来,管理系统使用微前端的成本并不会太大,而且从后面的技术问答中,候选人的微前端还是挺优秀的,各个细节基本都涉略到了。

如果你在公司内部很闲,又是刚好负责无关紧要的运营管理端,那么新的管理端可以一开始接入微前端方案,为未来的技术升级提供一个接口,风险也可控,还能够倒腾技术,简历还能新增亮点,何乐而不为

另外提到H5了,就提多一嘴,H5面向C端用户比较多,这方面更应该关心一些性能指标数据,比如FPFCP等等,围绕这些指标进行优化,亮点不就来了么,这类例子比比皆是,要学会多挖掘

接下来是我作为面试官,经常考察候选人的问题,因为大部分候选人都是用qiankun框架,所以本文以qiankun框架为模板,重点剖析项目实战中微前端中遇到的问题和原理

请解释一下微前端的概念以及它的主要优点和挑战?

f44b35de-1d21-45dd-9998-e65cc52c0266.png
f44b35de-1d21-45dd-9998-e65cc52c0266.png

微前端是一种将不同的前端应用组合到一起的架构模式。这些应用可以独立开发、独立部署、独立运行,然后在一个主应用中进行集成。这种模式的主要目标是解决大型、长期演进的前端项目的复杂性问题。

主要优点:

  1. 解耦: 微前端架构可以将大型项目分解为多个可以独立开发、测试和部署的小型应用。这种解耦可以提高开发效率,减少团队间的协调成本。
  2. 技术栈无关: 不同的微前端应用可以使用不同的技术栈,这为使用新技术、升级旧技术提供了可能。
  3. 并行开发: 因为微前端应用是独立的,所以多个团队可以并行开发不同的应用,无需担心相互影响。
  4. 独立部署: 每个微前端应用可以独立部署,这意味着可以更快地推出新功能,同时降低了部署失败的风险。

主要挑战:

  1. 性能问题: 如果不同的微前端应用使用了不同的库或框架,可能会导致加载和运行的性能问题。
  2. 一致性: 保持不同的微前端应用在用户体验、设计和行为上的一致性可能会比较困难。
  3. 状态共享: 在微前端应用之间共享状态可能会比较复杂,需要使用特殊的工具或模式。
  4. 复杂性: 尽管微前端可以解决大型项目的复杂性问题,但是它自身也带来了一些复杂性,比如需要管理和协调多个独立的应用。
  5. 安全性: 微前端架构可能会增加跨域等安全问题。

你能详细描述一下 qiankun 微前端框架的工作原理吗?

qiankun 是一个基于 single-spa 的微前端实现框架。它的工作原理主要涉及到以下几个方面:

  1. 应用加载:qiankun 通过动态创建 script 标签的方式加载子应用的入口文件。加载完成后,会执行子应用暴露出的生命周期函数。
  2. 生命周期管理:qiankun 要求每个子应用都需要暴露出 bootstrap、mount 和 unmount 三个生命周期函数。bootstrap 函数在应用加载时被调用,mount 函数在应用启动时被调用,unmount 函数在应用卸载时被调用。
  3. 沙箱隔离:qiankun 通过 Proxy 对象创建了一个 JavaScript 沙箱,用于隔离子应用的全局变量,防止子应用之间的全局变量污染。
  4. 样式隔离:qiankun 通过动态添加和移除样式标签的方式实现了样式隔离。当子应用启动时,会动态添加子应用的样式标签,当子应用卸载时,会移除子应用的样式标签。
  5. 通信机制:qiankun 提供了一个全局的通信机制,允许子应用之间进行通信。

在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,你会如何处理静态资源的加载问题?

在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,静态资源的加载问题可能会成为一个挑战。这是因为在微前端环境中,子应用的静态资源路径可能需要进行特殊处理才能正确加载。这里有几种可能的解决方案:

方案一:使用公共路径

在子应用的静态资源路径前添加公共路径前缀。例如,如果子应用的静态资源存放在 http://localhost:8080/static/,那么可以在所有的静态资源路径前添加这个前缀。

方案二:劫持标签插入函数

这个方案分为两步:

    1. 对于 HTML 中已有的 img/audio/video 等标签,qiankun 支持重写 getTemplate 函数,可以将入口文件 index.html 中的静态资源路径替换掉。
    1. 对于动态插入的 img/audio/video 等标签,劫持 appendChild、innerHTML、insertBefore 等事件,将资源的相对路径替换成绝对路径。

例如,我们可以传递一个 getTemplate 函数,将图片的相对路径转为绝对路径,它会在处理模板时使用:

代码语言:javascript
复制
start({
  getTemplate(tpl,...rest) {
    // 为了直接看到效果,所以写死了,实际中需要用正则匹配
    return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">');
  }
});

对于动态插入的标签,劫持其插入 DOM 的函数,注入前缀。

代码语言:javascript
复制
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;
       }
   }
}
方案三:给 jQuery 项目加上 webpack 打包

这个方案的可行性不高,都是陈年老项目了,没必要这样折腾。

在使用 qiankun 时,如果子应用动态插入了一些标签,你会如何处理?

在使用 qiankun 时,如果子应用动态插入了一些标签,我们可以通过劫持 DOM 的一些方法来处理。例如,我们可以劫持 appendChildinnerHTMLinsertBefore 等方法,将资源的相对路径替换为绝对路径。

以下是一个例子,假设我们有一个子应用,它使用 jQuery 动态插入了一张图片:

代码语言:javascript
复制
const render = $ => {
  $('#app-container').html('<p>Hello, render with jQuery</p><img src="./img/my-image.png">');
  return Promise.resolve();
};

我们可以在主应用中劫持 jQuery 的 html 方法,将图片的相对路径替换为绝对路径:

代码语言:javascript
复制
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 时,你如何处理老项目的资源加载问题?你能给出一些具体的解决方案吗?

在使用 qiankun 时,处理老项目的资源加载问题可以有多种方案,具体的选择取决于项目的具体情况。以下是一些可能的解决方案:

  1. 使用 qiankungetTemplate 函数重写静态资源路径:对于 HTML 中已有的 img/audio/video 等标签,qiankun 支持重写 getTemplate 函数,可以将入口文件 index.html 中的静态资源路径替换掉。例如:
代码语言:javascript
复制
start({
  getTemplate(tpl,...rest) {
    // 为了直接看到效果,所以写死了,实际中需要用正则匹配
    return tpl.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">');
  }
});
  1. 劫持标签插入函数:对于动态插入的 img/audio/video 等标签,我们可以劫持 appendChildinnerHTMLinsertBefore 等事件,将资源的相对路径替换成绝对路径。例如,我们可以劫持 jQuery 的 html 方法,将图片的相对路径替换为绝对路径:
代码语言:javascript
复制
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;
       }
   }
}
  1. 给老项目加上 webpack 打包:这个方案的可行性不高,都是陈年老项目了,没必要这样折腾。
  2. 使用 iframe 嵌入老项目:虽然 qiankun 支持 jQuery 老项目,但是似乎对多页应用没有很好的解决办法。每个页面都去修改,成本很大也很麻烦,但是使用 iframe 嵌入这些老项目就比较方便。

你能解释一下 qiankunstart 函数的作用和参数吗?如果只有一个子项目,你会如何启用预加载?

qiankunstart 函数是用来启动微前端应用的。在注册完所有的子应用之后,我们需要调用 start 函数来启动微前端应用。

start 函数接收一个可选的配置对象作为参数,这个对象可以包含以下属性:

  • prefetch:预加载模式,可选值有 truefalse'all''popstate'。默认值为 true,即在主应用 start 之后即刻开始预加载所有子应用的静态资源。如果设置为 'all',则主应用 start 之后会预加载所有子应用静态资源,无论子应用是否激活。如果设置为 'popstate',则只有在路由切换的时候才会去预加载对应子应用的静态资源。
  • sandbox:沙箱模式,可选值有 truefalse{ strictStyleIsolation: true }。默认值为 true,即为每个子应用创建一个新的沙箱环境。如果设置为 false,则子应用运行在当前环境下,没有任何的隔离。如果设置为 { strictStyleIsolation: true },则会启用严格的样式隔离模式,即子应用的样式会被完全隔离,不会影响到其他子应用和主应用。
  • singular:是否为单例模式,可选值有 truefalse。默认值为 true,即一次只能有一个子应用处于激活状态。如果设置为 false,则可以同时激活多个子应用。
  • fetch:自定义的 fetch 方法,用于加载子应用的静态资源。

如果只有一个子项目,要想启用预加载,可以这样使用 start 函数:

代码语言:javascript
复制
start({ prefetch: 'all' });

这样,主应用 start 之后会预加载子应用的所有静态资源,无论子应用是否激活。

在使用 qiankun 时,你如何处理 js 沙箱不能解决的 js 污染问题?

qiankunjs 沙箱机制主要是通过代理 window 对象来实现的,它可以有效地隔离子应用的全局变量,防止子应用之间的全局变量污染。然而,这种机制并不能解决所有的 js 污染问题。例如,如果我们使用 onclickaddEventListener<body> 添加了一个点击事件,js 沙箱并不能消除它的影响。

对于这种情况,我们需要依赖于良好的代码规范和开发者的自觉。在开发子应用时,我们需要避免直接操作全局对象,如 windowdocument。如果必须要操作,我们应该在子应用卸载时,清理掉这些全局事件和全局变量,以防止对其他子应用或主应用造成影响。

例如,如果我们在子应用中添加了一个全局的点击事件,我们可以在子应用的 unmount 生命周期函数中移除这个事件:

代码语言:javascript
复制
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 的需求有一定的挑战性。这是因为 qiankun 的设计理念是在子应用卸载时,将环境还原到子应用加载前的状态,以防止子应用对全局环境造成污染。这种设计理念与 keep-alive 的需求是相悖的,因为 keep-alive 需要保留子应用的状态,而不是在子应用卸载时将其状态清除。

然而,我们可以通过一些技巧来实现 keep-alive 的效果。一种可能的方法是在子应用的生命周期函数中保存和恢复子应用的状态。例如,我们可以在子应用的 unmount 函数中保存子应用的状态,然后在 mount 函数中恢复这个状态:

代码语言:javascript
复制
// 伪代码
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-spaParcel 功能。Parcelsingle-spa 的一个功能,它允许你在一个应用中挂载另一个应用,并且可以控制这个应用的生命周期。通过 Parcel,我们可以将子应用挂载到一个隐藏的 DOM 元素上,从而实现 keep-alive 的效果。然而,这种方法需要对 qiankun 的源码进行修改,因为 qiankun 目前并不支持 Parcel

你能解释一下 qiankuniframe 在微前端实现方式上的区别和优劣吗?在什么情况下,你会选择使用 iframe 而不是 qiankun

qiankuniframe 都是微前端的实现方式,但它们在实现原理和使用场景上有一些区别。

qiankun 是基于 single-spa 的微前端解决方案,它通过 JavaScript 的 import 功能动态加载子应用,然后在主应用的 DOM 中挂载子应用的 DOM。qiankun 提供了一种 JavaScript 沙箱机制,可以隔离子应用的全局变量,防止子应用之间的全局变量污染。此外,qiankun 还提供了一种样式隔离机制,可以防止子应用的 CSS 影响其他应用。这些特性使得 qiankun 在处理复杂的微前端场景时具有很高的灵活性。

iframe 是一种较为传统的前端技术,它可以在一个独立的窗口中加载一个 HTML 页面。iframe 本身就是一种天然的沙箱,它可以完全隔离子应用的 JavaScript 和 CSS,防止子应用之间的相互影响。然而,iframe 的这种隔离性也是它的缺点,因为它使得主应用和子应用之间的通信变得困难。此外,iframe 还有一些其他的问题,比如性能问题、SEO 问题等。

在选择 qiankuniframe 时,需要根据具体的使用场景来决定。如果你的子应用是基于现代前端框架(如 React、Vue、Angular 等)开发的单页应用,那么 qiankun 可能是一个更好的选择,因为它可以提供更好的用户体验和更高的开发效率。如果你的子应用是基于 jQuery 或者其他传统技术开发的多页应用,或者你需要在子应用中加载一些第三方的页面,那么 iframe 可能是一个更好的选择,因为它可以提供更强的隔离性。

在使用 qiankun 时,你如何处理多个子项目的调试问题?

在使用qiankun处理多个子项目的调试问题时,通常的方式是将每个子项目作为一个独立的应用进行开发和调试。每个子项目都可以在本地启动,并通过修改主应用的配置,让主应用去加载本地正在运行的子应用,这样就可以对子应用进行调试了。这种方式的好处是,子应用与主应用解耦,可以独立进行开发和调试,不会相互影响。

对于如何同时启动多个子应用,你可以使用npm-run-all这个工具。npm-run-all是一个CLI工具,可以并行或者串行执行多个npm脚本。这个工具对于同时启动多个子应用非常有用。使用方式如下:

  1. 首先,你需要在你的项目中安装npm-run-all,可以通过下面的命令进行安装:
代码语言:javascript
复制
npm install --save-dev npm-run-all
  1. 然后,在你的package.json文件中定义你需要并行运行的脚本。比如,你有两个子应用,分别为app1app2,你可以定义如下的脚本:
代码语言:javascript
复制
"scripts": {
    "start:app1": "npm start --prefix ./app1",
    "start:app2": "npm start --prefix ./app2",
    "start:all": "npm-run-all start:app1 start:app2"
}

在这个例子中,start:app1start:app2脚本分别用于启动app1app2应用,start:all脚本则用于同时启动这两个应用。

  1. 最后,通过执行npm run start:all命令,就可以同时启动app1app2这两个应用了。

npm-run-all不仅可以并行运行多个脚本,还可以串行运行多个脚本。在某些情况下,你可能需要按照一定的顺序启动你的应用,这时你可以使用npm-run-all-s选项来串行执行脚本,例如:npm-run-all -s script1 script2,这将会先执行script1,然后再执行script2

qiankun是如何实现CSS隔离的,该方案有什么缺点,还有其它方案么

qiankun主要通过使用Shadow DOM来实现CSS隔离。

  1. Shadow DOMShadow DOM是一种浏览器内置的Web标准技术,它可以创建一个封闭的DOM结构,这个DOM结构对外部是隔离的,包括其CSS样式。qiankun在挂载子应用时,会将子应用的HTML元素挂载到Shadow DOM上,从而实现CSS的隔离。
代码语言:javascript
复制
// 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模块:

代码语言:javascript
复制
/* Button.module.css */
.button {
    background-color: blue;
}

在你的JavaScript文件中,你可以这样引入并使用这个模块:

代码语言:javascript
复制
import styles from './Button.module.css';

function Button() {
    return <button className={styles.button}>Click me</button>;
}

在这个例子中,button类名会被转换成一个唯一的名字,如Button_button__xxx,这样就可以避免全局样式冲突了。

3.BEM命名规范隔离

qiankun中如何实现父子项目间的通信?如果让你实现一套通信机制,你该如何实现?

  • Actions 通信:qiankun 官方提供的通信方式,适合业务划分清晰,较简单的微前端应用。这种通信方式主要通过 setGlobalState 设置 globalState,并通过 onGlobalStateChangeoffGlobalStateChange 来注册和取消 观察者 函数,从而实现通信。
  • 自己实现一套通信机制(可以思考一下如何追踪State状态,类似Redux模式)
  1. 全局变量:在全局(window)对象上定义共享的属性或方法。这种方式简单明了,但有可能导致全局污染,需要注意变量命名以避免冲突。
  2. 自定义事件:使用原生的 CustomEvent 或类似的第三方库来派发和监听自定义事件。这种方式避免了全局污染,更加符合模块化的原则,但可能需要更复杂的事件管理。
    • 2.1. 定义一个全局的通信对象,例如 window.globalEvent,这个对象提供两个方法,emit 和 on。
    • 2.2. emit 方法用于派发事件,接收事件名称和可选的事件数据作为参数。
    • 2.3. on 方法用于监听事件,接收事件名称和回调函数作为参数。当相应的事件被派发时,回调函数将被执行。
代码语言:javascript
复制
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);
  },
};

1.在主项目中使用qiankun注册子项目时,如何解决子项目路由的hash与history模式之争?

如果主项目使用 history 模式,并且子项目可以使用 historyhash 模式,这是 qiankun 推荐的一种形式。在这种情况下,子项目可以选择适合自己的路由模式,而且对于已有的子项目不需要做太多修改。但是子项目之间的跳转需要通过父项目的 router 对象或原生的 history 对象进行。

2. 如果主项目和所有子项目都采用 hash 模式,可以有两种做法:

  • 使用 path 来区分子项目:这种方式不需要对子项目进行修改,但所有项目之间的跳转需要借助原生的 history 对象。
  • 使用 hash 来区分子项目:这种方式可以通过自定义 activeRule 来实现,但需要对子项目进行一定的修改,将子项目的路由加上前缀。这样的话,项目之间的跳转可以直接使用各自的 router 对象或 <router-link>

3. 如果主项目采用 hash 模式,而子项目中有些采用 history 模式,这种情况下,子项目间的跳转只能借助原生的 history 对象,而不使用子项目自己的 router 对象。对于子项目,可以选择使用 pathhash 来区分不同的子项目。

在qiankun中,如果实现组件在不同项目间的共享,有哪些解决方案?

在项目间共享组件时,可以考虑以下几种方式:

  1. 父子项目间的组件共享:主项目加载时,将组件挂载到全局对象(如window)上,在子项目中直接注册使用该组件。
  2. 子项目间的组件共享(弱依赖):通过主项目提供的全局变量,子项目挂载到全局对象上。子项目中的共享组件可以使用异步组件来实现,在加载组件前先检查全局对象中是否存在,存在则复用,否则加载组件。
  3. 子项目间的组件共享(强依赖):在主项目中通过loadMicroApp手动加载提供组件的子项目,确保先加载该子项目。在加载时,将组件挂载到全局对象上,并将loadMicroApp函数传递给子项目。子项目在需要使用共享组件的地方,手动加载提供组件的子项目,等待加载完成后即可获取组件。

需要注意的是,在使用异步组件或手动加载子项目时,可能会遇到样式加载的问题,可以尝试解决该问题。另外,如果共享的组件依赖全局插件(如storei18n),需要进行特殊处理以确保插件的正确初始化。

在qiankun中,应用之间如何复用依赖,除了npm包方案外?

  1. 在使用webpack构建的子项目中,要实现复用公共依赖,需要配置webpackexternals,将公共依赖指定为外部依赖,不打包进子项目的代码中。
  2. 子项目之间的依赖复用可以通过保证依赖的URL一致来实现。如果多个子项目都使用同一份CDN文件,加载时会先从缓存读取,避免重复加载。
  3. 子项目复用主项目的依赖可以通过给子项目的index.html中的公共依赖的scriptlink标签添加自定义属性ignore来实现。在qiankun运行子项目时,qiankun会忽略这些带有ignore属性的依赖,子项目独立运行时仍然可以加载这些依赖。
  4. 在使用qiankun微前端框架时,可能会出现子项目之间和主项目之间的全局变量冲突的问题。这是因为子项目不配置externals时,子项目的全局Vue变量不属于window对象,而qiankun在运行子项目时会先找子项目的window,再找父项目的window,导致全局变量冲突。
  5. 解决全局变量冲突的方案有三种:
    • 方案一是在注册子项目时,在beforeLoad钩子函数中处理全局变量,将子项目的全局Vue变量进行替换,以解决子项目独立运行时的全局变量冲突问题。
    • 方案二是通过主项目将依赖通过props传递给子项目,子项目在独立运行时使用传递过来的依赖,避免与主项目的全局变量冲突。
    • 方案三是修改主项目和子项目的依赖名称,使它们不会相互冲突,从而避免全局变量冲突的问题。

说说webpack5联邦模块在微前端的应用

Webpack 5 的联邦模块(Federation Module)是一个功能强大的特性,可以在微前端应用中实现模块共享和动态加载,从而提供更好的代码复用和可扩展性

1. 模块共享

Webpack 5 的联邦模块允许不同的微前端应用之间共享模块,避免重复加载和代码冗余。通过联邦模块,我们可以将一些公共的模块抽离成一个独立的模块,并在各个微前端应用中进行引用。这样可以节省资源,并提高应用的加载速度。

代码语言:javascript
复制
// main-app webpack.config.js
const 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.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  // ...其他配置

  plugins: [
    new ModuleFederationPlugin({
      name: 'shared_module',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
      },
    }),
  ],
};

在上述示例中,main-appshared-module 分别是两个微前端应用的 webpack 配置文件。通过 ModuleFederationPlugin 插件,shared-moduleButton 组件暴露给其他应用使用,而 main-app 则通过 remotes 配置引入了 shared-module

2. 动态加载

Webpack 5 联邦模块还支持动态加载模块,这对于微前端应用的按需加载和性能优化非常有用。通过动态加载,可以在需要时动态地加载远程模块,而不是在应用初始化时一次性加载所有模块。

代码语言:javascript
复制
// main-app
const remoteModule = () => import('shared_module/Button');

// ...其他代码

// 在需要的时候动态加载模块
remoteModule().then((module) => {
  // 使用加载的模块
  const Button = module.default;
  // ...
});

在上述示例中,main-app 使用 import() 函数动态加载 shared_module 中的 Button 组件。通过动态加载,可以在需要时异步地加载远程模块,并在加载完成后使用模块。

在微前端应用中可以实现模块共享和动态加载,提供了更好的代码复用和可扩展性。通过模块共享,可以避免重复加载和代码冗余,而动态加载则可以按需加载模块,提高应用的性能和用户体验。

说说qiankun的资源加载机制(import-html-entry)

qiankun import-html-entry 是qiankun 框架中用于加载子应用的 HTML 入口文件的工具函数。它提供了一种方便的方式来动态加载和解析子应用的 HTML 入口文件,并返回一个可以加载子应用的 JavaScript 模块。

具体而言,import-html-entry 实现了以下功能:

    1. 加载 HTML 入口文件:import-html-entry 会通过创建一个 <link> 标签来加载子应用的 HTML 入口文件。这样可以确保子应用的资源得到正确加载,并在加载完成后进行处理。
    1. 解析 HTML 入口文件:一旦 HTML 入口文件加载完成,import-html-entry 将解析该文件的内容,提取出子应用的 JavaScript 和 CSS 资源的 URL。
    1. 动态加载 JavaScript 和 CSS 资源:import-html-entry 使用动态创建 <script><link> 标签的方式,按照正确的顺序加载子应用的 JavaScript 和 CSS 资源。
    1. 创建沙箱环境:在加载子应用的 JavaScript 资源时,import-html-entry 会创建一个沙箱环境(sandbox),用于隔离子应用的全局变量和运行环境,防止子应用之间的冲突和污染。
    1. 返回子应用的入口模块:最后,import-html-entry 返回一个可以加载子应用的 JavaScript 模块。这个模块通常是一个包含子应用初始化代码的函数,可以在主应用中调用以加载和启动子应用。

通过使用 qiankun import-html-entry,开发者可以方便地将子应用的 HTML 入口文件作为模块加载,并获得一个可以加载和启动子应用的函数,简化了子应用的加载和集成过程。

说说现有的几种微前端框架,它们的优缺点?

以下是对各个微前端框架优缺点的总结:

  1. qiankun 方案 优点
    • 降低了应用改造的成本,通过html entry的方式引入子应用;
    • 提供了完备的沙箱方案,包括js沙箱和css沙箱;
    • 支持静态资源预加载能力。

    缺点

    • 适配成本较高,包括工程化、生命周期、静态资源路径、路由等方面的适配;
    • css沙箱的严格隔离可能引发问题,js沙箱在某些场景下执行性能下降;
    • 无法同时激活多个子应用,不支持子应用保活;
    • 不支持vite等esmodule脚本运行。
  2. micro-app 方案 优点
    • 使用 webcomponent 加载子应用,更优雅;
    • 复用经过大量项目验证过的 qiankun 沙箱机制,提高了框架的可靠性;
    • 支持子应用保活;
    • 降低了子应用改造的成本,提供了静态资源预加载能力。

    缺点

    • 接入成本虽然降低,但路由依然存在依赖;
    • 多应用激活后无法保持各子应用的路由状态,刷新后全部丢失;
    • css 沙箱无法完全隔离,js 沙箱做全局变量查找缓存,性能有所优化;
    • 支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离;
    • 对于不支持 webcomponent 的浏览器没有做降级处理。
  3. EMP 方案 优点
    • webpack 联邦编译可以保证所有子应用依赖解耦;
    • 支持应用间去中心化的调用、共享模块;
    • 支持模块远程 ts 支持。

    缺点

    • 对 webpack 强依赖,对于老旧项目不友好;
    • 没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉;
    • 子应用保活、多应用激活无法实现;
    • 主、子应用的路由可能发生冲突。
  4. 无界方案 优点
    • 基于 webcomponent 容器和 iframe 沙箱,充分解决了适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite框架支持、应用共享等问题。

    缺点

    • 在继承了iframe优点的同时,缺点依旧还是存在

组件库

为什么需要二次封装组件库?

实际工作中,我们在项目中需要自定义主题色更改按钮样式自定义图标,自定义table组件等等,这些都可以基于antd组件库进行二次封装,减少重复工作,提升开发效率。

所以我们在封装的时候按照下面这四个原则进行思考就行了,另外本身封装组件库对于项目来说也是没有任何风险,因为一开始我们把PropsType直接进行转发,内部再进行增加业务的功能,这样就是达到完全的解耦

  • 统一风格:在一个大的项目或者多个相关的项目中,保持一致的界面风格和交互方式是非常重要的。通过二次封装,我们可以定义统一的样式和行为,减少不一致性。
  • 降低维护成本:当底层的组件库更新时,我们可能需要在项目的多个地方进行修改。但是如果我们有了自己的封装,只需要在封装层面进行更新即可,这大大降低了维护成本。
  • 增加定制功能:有些时候,我们需要在原有组件库的基础上增加一些特定的功能,如特定的验证、错误处理等。二次封装提供了这样的可能。
  • 提高开发效率:在一些常用的功能(如表单验证、全局提示等)上,二次封装可以提供更方便的API,提高开发效率。

请结合一个组件库设计的过程,谈谈前端工程化的思想

当我们结合一个组件库设计的过程来谈论前端工程化的思想时,需要理清这些要点:

1. 使用 Lerna 进行多包管理:通过 Lerna 来管理多个包(组件),实现组件级别的解耦、独立版本控制、按需加载等特性。

代码语言:javascript
复制
# 安装 Lerna
npm install -g lerna

# 初始化一个 Lerna 仓库
lerna init

# 创建 "Button" 组件包
lerna create button --yes

2. 规范化提交:使用规范化的提交信息可以提高 Git 日志的可读性,并且可以通过 conventional commits 自动生成 CHANGELOG。可以使用 commitizen、commitlint 等工具来配置。

代码语言:javascript
复制
# 安装相关工具
npm install commitizen cz-conventional-changelog --save-dev
代码语言:javascript
复制
// package.json
{
  "scripts": {
    "commit": "git-cz"
  },
  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }
}

3. 代码规范化:通过 ESLint、Prettier 等工具实现代码规范化和格式化,并封装为自己的规范预设。

代码语言:javascript
复制
# 安装相关工具
npm install eslint prettier eslint-plugin-prettier eslint-config-prettier --save-dev
代码语言:javascript
复制
// .eslintrc.js
module.exports = {
  extends: ['eslint:recommended', 'plugin:prettier/recommended'],
};

// .prettierrc.js
module.exports = {
  singleQuote: true,
  trailingComma: 'es5',
};

4. 组件开发调试:需要考虑热更新编译、软链接引用等问题,以方便在开发过程中进行组件的调试。

代码语言:javascript
复制
// packages/button/src/Button.js
import React from 'react';

const Button = ({ type = 'primary', onClick, children }) => {
  return (
    <button className={`button ${type}`} onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;

5. 文档站点:可以基于 dumi 搭建文档站点,并实现 CDN 加速、增量发布等优化。可以使用 surge 实现 PR 预览。

代码语言:javascript
复制
<!-- packages/button/docs/index.md -->
# Button

A simple button component.

## Usage

import { Button } from 'button-library';

const MyComponent = () => {
  return <Button onClick={() => alert('Button clicked!')}>Click Me</Button>;
};

### Props

| Name     | Type                   | Default | Description                   |
| -------- | ---------------------- | ------- | ----------------------------- |
| type     | `primary` \| `secondary` | `primary` | The type of the button. |
| onClick  | `function`             |         | Event handler for click event. |

6. 单元测试:需要考虑 jest、enzyme 等工具的配合使用,生成测试覆盖率报告。

代码语言:javascript
复制
# 安装相关工具
npm install jest enzyme enzyme-adapter-react-16 react-test-renderer --save-dev
代码语言:javascript
复制
// packages/button/src/Button.test.js
import React from 'react';
import { mount } from 'enzyme';
import Button from './Button';

describe('Button', () => {
  it('renders without crashing', () => {
    const wrapper = mount(<Button>Click Me</Button>);
    expect(wrapper.exists()).toBe(true);
  });

  it('calls onClick function when clicked', () => {
    const onClickMock = jest.fn();
    const wrapper = mount(<Button onClick={onClickMock}>Click Me</Button>);

    wrapper.find('button').simulate('click');
    expect(onClickMock).toHaveBeenCalledTimes(1);
  });
});

7. 按需加载:需要配合 babel-plugin-import 实现按需加载,即在编译时修改导入路径来实现组件的按需加载。

代码语言:javascript
复制
# 安装相关工具
npm install babel-plugin-import --save-dev
代码语言:javascript
复制
// .babelrc
{
  "plugins": [
    [
      "import",
      {
        "libraryName": "button-library",
        "style": "css"
      }
    ]
  ]
}

8. 组件设计:需要考虑响应式、主题、国际化、TypeScript 支持等问题,以保证组件的灵活性和可扩展性。

代码语言:javascript
复制
// packages/button/src/Button.js
import React from 'react';
import PropTypes from 'prop-types';

const Button = ({ type = 'primary', onClick, children }) => {
  return (
    <button className={`button ${type}`} onClick={onClick}>
      {children}
    </button>
  );
};

Button.propTypes = {
  type: PropTypes.oneOf(['primary', 'secondary']),
  onClick: PropTypes.func,
  children: PropTypes.node.isRequired,
};

export default Button;

9. 发布前的自动化脚本:需要编写自动化脚本来规范发布流程,确保发布的一致性和可靠性。

代码语言:javascript
复制
// package.json
{
  "scripts": {
    "prepublish": "npm run lint && npm run test",
    "lint": "eslint .",
    "test": "jest"
  }
}

10. 发布后的处理:考虑补丁升级、文档站点同步发布等问题,以便及时修复问题并提供最新的文档。

11. 制定 Contributing 文档:制定 Contributing 文档可以降低开源社区贡献的门槛,并确保社区成员了解如何参与项目。处理 issues 和 PR 需要有专人负责。

如何对一个组件库进行测试?

首先需要明确,组件库的测试大致可以分为两类:一类是针对组件本身的功能和性能的测试(例如,单元测试、性能测试),另一类是针对组件在集成环境下的行为和性能的测试(例如,集成测试、系统测试)。

1. 功能测试(单元测试)

通常来说,组件的功能测试可以通过单元测试来完成。单元测试的目的是验证组件的单个功能是否按照预期工作。这通常可以通过编写测试用例来完成,每个测试用例针对一个特定的功能。

代码语言:javascript
复制
import { Button } from '../src/Button';

test('Button should do something', () => {
    const component = new YourComponent();
    // your test logic here
    expect(component.doSomething()).toBe('expected result');
});

2. 边界测试

边界测试是一种特殊的功能测试,用于检查组件在输入或输出达到极限或边界条件时的行为。

代码语言:javascript
复制
test('Button should handle boundary condition', () => {
    const component = new YourComponent();
    // test with boundary value
    expect(component.handleBoundaryCondition('boundary value')).toBe('expected result');
});

3. 响应测试

响应测试通常涉及到 UI 组件在不同的设备或屏幕尺寸下的行为。这可能需要使用端到端(E2E)测试工具,如 Puppeteer、Cypress 等。

代码语言:javascript
复制
import { test } from '@playwright/test';

test('Button should be responsive', async ({ page }) => {
    await page.goto('http://localhost:3000/your-component');
    const component = await page.$('#your-component-id');
    expect(await component.isVisible()).toBe(true);

    // Simulate a mobile device
    await page.setViewportSize({ width: 375, height: 812 });
    // Check the component under this condition
    // your test logic here
});

4. 交互测试

交互测试也可以通过端到端(E2E)测试工具来完成。

代码语言:javascript
复制
test('Button should handle interactions', async ({ page }) => {
    await page.goto('http://localhost:3000/your-component');
    const component = await page.$('#your-component-id');

    // Simulate a click event
    await component.click();
    // Check the result of the interaction
    // your test logic here
});

5. 异常测试

异常测试用于验证组件在遇到错误或非法输入时能否正确处理。这通常可以通过在测试用例中模拟错误条件来完成。

代码语言:javascript
复制
test('Button should handle errors', () => {
    const component = new YourComponent();
    // Test with illegal argument
    expect(() => {
        component.doSomething('illegal argument');
    }).toThrow('Expected error message');
});

6. 性能测试

性能测试用于验证组件的性能,例如,加载速度、内存消耗等。

代码语言:javascript
复制
import { performance } from 'perf_hooks';

test('Button should have good performance', () => {
    const start = performance.now();
    const component = new YourComponent();
    component.doSomething();
    const end = performance.now();
    const duration = end - start;
    expect(duration).toBeLessThan(50);  // Expect the operation to finish within 50 ms
});

7. 自动化测试

单元测试、集成测试和系统测试都可以通过自动化测试工具进行。例如,Jest 和 Mocha 可以用于自动化运行 JavaScript 单元测试,Puppeteer 和 Selenium 可以用于自动化运行端到端测试。

代码语言:javascript
复制
module.exports = {
    roots: ['<rootDir>/src'],
    testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
    transform: {
        '^.+\\.(ts|tsx)$': 'ts-jest'
    }
};

Element-UI 的多语言方案是怎么设计的?

Element UI 使用了 Vue 的插件 vue-i18n 实现多语言支持,具体的设计和实现过程如下:

1. 定义语言包

首先,Element UI 定义了一个 JavaScript 对象作为语言包。每种语言都有一个对应的语言包,例如:

代码语言:javascript
复制
export default {
  el: {
    colorpicker: {
      confirm: 'OK',
      clear: 'Clear'
    },
    // ...other components
  }
};

2. 加载语言包

Element UI 提供了一个 i18n 方法用于加载语言包。

代码语言:javascript
复制
import ElementUI from 'element-ui';
import locale from 'element-ui/lib/locale/lang/en';

Vue.use(ElementUI, { locale });

3. 使用语言包

Element UI 的组件会使用 $t 方法获取语言包中的文本。例如:

代码语言:javascript
复制
<template>
  <el-button>{{ $t('el.button.confirm') }}</el-button>
</template>

在这个例子中,按钮的文本会根据当前的语言包来显示。

4. 集成 vue-i18n

如果你的项目中已经使用了 vue-i18n,Element UI 会优先使用 vue-i18n 提供的 $t 方法。你可以这样配置:

代码语言:javascript
复制
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import ElementUI from 'element-ui';
import enLocale from 'element-ui/lib/locale/lang/en';
import zhLocale from 'element-ui/lib/locale/lang/zh-CN';

Vue.use(VueI18n);

const messages = {
  en: {
    message: 'hello',
    ...enLocale // 或者用 Object.assign({ message: 'hello' }, enLocale)
  },
  zh: {
    message: '你好',
    ...zhLocale // 或者用 Object.assign({ message: '你好' }, zhLocale)
  }
};

const i18n = new VueI18n({
  locale: 'zh', // set locale
  messages, // set locale messages
});

Vue.use(ElementUI, {
  i18n: (key, value) => i18n.t(key, value)
});

在这个例子中,我们先加载了 vue-i18n,然后定义了两种语言的语言包(英文和中文)。最后,我们配置了 Element UI 使用 vue-i18n$t 方法。

这样,Element UI 的组件就能够根据 vue-i18n 的语言设置显示对应的文本。

组件库如何实现在线主题定制的?

1. 使用 CSS 变量定义样式

将组件的样式使用 CSS 变量定义,这样可以通过改变 CSS 变量的值来修改样式。

代码语言:javascript
复制
:root {
  --primary-color: #1890ff;
}

.btn {
  background: var(--primary-color); 
}

2. 提供主题文件进行配置

让用户可以通过导入自定义的主题文件来覆盖默认样式。

代码语言:javascript
复制
// theme.js
export default {
  '--primary-color': '#409eff'
}

3. 在线主题编辑器

提供一个在线工具,用户可以在工具中配置主题,生成主题文件。

工具会提交主题配置,服务器端接收后动态编译生成新的样式,并返回给前端。

4. 前端应用新样式

前端通过加载服务器返回的 CSS 文件来应用新的主题样式,实现样式更新而无需重新打包。

代码语言:javascript
复制
// 请求主题文件
fetchTheme(theme).then(css => {
  // 动态创建style标签,插入css
  const style = document.createElement('style');
  style.innerHTML = css;
  document.head.appendChild(style);  
})

5. 持久化主题配置

将用户主题配置持久化本地存储,这样每次访问都可以应用上次选定的主题。

组件库的类型定义应该怎样设计?

组件库的类型定义设计取决于很多因素,包括库的大小、复杂度、可能的使用场景等。

1. 定义全局类型 versus 定义组件Props类型

在组件库中,我们经常需要定义一些可以在多个组件之间共享的全局类型,以及针对特定组件的props类型。例如:

代码语言:javascript
复制
// 全局类型
export interface Size {
  width: number;
  height: number;
}

// 组件Props类型
export interface ButtonProps {
  size?: Size;
  label: string;
  onClick?: () => void;
}

2. 类型导出应该集中还是分散?

是否集中导出类型取决于组件库的大小和复杂度。对于小型库,可以在一个单独的文件中集中导出所有类型;对于大型库,可能需要将类型定义分散在各个组件文件中,然后在一个单独的文件中重新导出它们。例如:

代码语言:javascript
复制
// 在各个组件文件中定义和导出类型
// button.ts
export interface ButtonProps { /*...*/ }

// 在一个单独的文件中重新导出所有类型
// types.ts
export type { ButtonProps } from './button';

3. 如何设计类型层级关系?类型复用?

在设计类型时,应尽可能地利用 TypeScript 的类型系统来构建类型层级关系,并复用类型。例如,你可以使用类型交叉(&)和类型联合(|)来复用类型:

代码语言:javascript
复制
type SmallSize = { width: number; height: number };
type LargeSize = SmallSize & { depth: number };

type Size = SmallSize | LargeSize;

4. 类型定义要充分还是精简?

类型定义应尽可能精简,同时提供足够的信息来描述类型的形状和行为。避免使用 anyunknown 类型,除非有特别的理由。例如:

代码语言:javascript
复制
// 不好的类型定义
interface ButtonProps {
  [key: string]: any;  // 这不提供任何有关props的信息
}

// 好的类型定义
interface ButtonProps {
  size?: Size;
  label: string;
  onClick?: () => void;
}

总的来说,设计好的类型定义可以提高代码的可读性和可维护性,同时减少运行时错误。

组件库的渐进升级策略应该怎么设计?

组件库的渐进升级策略通常会涉及到版本控制、向下兼容性、废弃通知以及旧版本的兼容性等多个方面。这种策略的主要目的是在保持库的稳定性和功能性的同时,尽可能地减少对用户的影响。

1. 版本控制策略

组件库通常遵循语义化版本 (SemVer) 规范进行版本控制。在语义化版本中,每个版本号都由三部分组成:主版本号、次版本号和补丁版本号。

例如,版本号为 1.2.3 表示主版本号为 1,次版本号为 2,补丁版本号为 3。

  • 主版本号(Major): 当你做了不兼容的 API 修改
  • 次版本号(Minor): 当你做了向下兼容的功能性新增
  • 补丁版本号(Patch): 当你做了向下兼容的问题修复

2. 向下兼容处理

向下兼容性是指在升级组件库时,保证新版本不会破坏旧版本的功能。例如,如果新版本的一个组件删除了一个属性,而这个属性在旧版本中是必需的,那么这个变化就不是向下兼容的。

在进行不向下兼容的变化时,应在主版本号上进行增加,以警告用户可能需要修改他们的代码。

3. 功能被废弃怎么通知用户升级?

当一个功能或者组件被废弃时,应在库的文档、更新日志以及相关的 API 文档中明确注明。在代码中,可以通过添加警告或者错误信息来提醒用户:

代码语言:javascript
复制
function deprecatedFunction() {
  console.warn('Warning: deprecatedFunction is deprecated and will be removed in the next major version.');
  // 功能的原始实现
}

4. 兼容旧版本的方案

兼容旧版本的策略取决于特定的需求和资源。一种常见的策略是在主版本升级后,继续维护旧版本的一个分支,以便在必要时进行修复和改进。例如,如果当前版本是 2.x.x,那么可以维护一个 1.x.x 的分支。

在实践中,以上的策略和方法可能需要根据具体的情况进行调整。一个好的渐进升级策略应能够平衡新功能的引入、旧功能的废弃以及向下兼容性的维护。

组件库的按需加载实现中存在哪些潜在问题,如何解决?

按需加载(也称为代码拆分)是现代前端开发中常见的一种优化手段,可以有效地减少应用的初始加载时间。对于组件库来说,它使用户只加载和使用他们真正需要的组件,而不是加载整个库。

babel-plugin-import

Babel 插件: 使用如 babel-plugin-import 的 Babel 插件可以在编译时将导入整个库的语句转换为仅导入使用的组件。

代码语言:javascript
复制
```javascript
import { Button } from 'your-ui-lib';
// 在编译时,babel-plugin-import 将上面的语句转换为以下语句:
// import Button from 'your-ui-lib/button';
```

tree-shaking

Webpack、Rollup 等工具都已经支持了 Tree shaking。在项目的配置中开启 Tree shaking,然后使用 ES Modules 的导入导出语法,即可实现按需加载。

但是在使用 Tree shaking 的时候,有一个需要特别注意的地方,就是“副作用(side effects)”。

有些模块的代码可能会在导入时执行一些副作用,例如改变全局变量、改变导入模块的状态等。这种情况下,即使模块中的部分导出没有被使用,由于其副作用,也不能被 Tree shaking 移除。否则,可能会导致程序运行出错。

例如,在 CSS in JS 的库中,可能存在这样的代码:

代码语言:javascript
复制
import './styles.css'; // 有副作用,改变了全局的样式

在这种情况下,你需要在 package.json 中显式地指定模块的副作用,以防止它们被错误地移除:

代码语言:javascript
复制
{
  "name": "your-library",
  "sideEffects": [
    "./src/styles.css"
  ]
}

如果你的库没有任何副作用,你可以将 sideEffects 设置为 false

代码语言:javascript
复制
{
  "name": "your-library",
  "sideEffects": false
}

样式如何实现真正的按需加载?避免样式重复打包?

image.png
image.png

样式和逻辑分离

样式和逻辑结合

样式和逻辑关联

开发打包流程

中等

简单

复杂

输出文件

JS 文件和 CSS 文件

JS 文件

JS 文件和 CSS 文件

使用方法

分别引入 JS 和 CSS

只引入 JS

只引入 JS

按需加载

需要额外支持

支持

支持

性能影响

带额外 runtime,可能有影响

SSR

支持

需要额外支持(部分方案不支持)

支持(可能需要使用者调整配置)

支持写法

常规 CSS / 零运行时 CSS in JS

常规 CSS / CSS in JS

常规 CSS / 零运行时 CSS in JS

关键样式提取

自行处理

支持

自行处理

样式和逻辑分离

这种方案中,组件的CSS和JS在代码层面上是分离的,开发时写在不同的文件里。在打包时生成独立的逻辑文件和样式文件。

优点:

  • 适用面广,可以支持不同的框架和技术栈。
  • 支持SSR,样式处理留给使用者。
  • 可以直接提供源码,便于主题定制。

缺点:

适合需要高适用性和灵活性的组件库。

样式和逻辑结合

这种方案将CSS和JS打包在一起,输出单一的JS文件。主要有两种实现形式:

  1. CSS in JS:样式以对象或字符串形式存在在JS中。
  2. 将CSS打包进JS:通过构建工具,将CSS文件内容注入到JS中。

优点:

  • 使用简单,只需要引入JS即可。
  • 天然支持按需加载。

缺点:

  • 需要额外的runtime,可能影响性能。
  • 难以利用浏览器缓存。
  • SSR需要框架额外支持。

样式和逻辑关联

这种方案下,虽然CSS和JS在源码层分离,但组件内会直接引用样式,且输出文件中保留import语句。

优点:

  • 使用简单,只引入JS即可。
  • 支持按需加载。

缺点:

  • 对构建和SSR都有一定要求。
  • 样式编译复杂。

设计一个组件库的 CI/CD 和发布流程。

可以参考antd

当你设计一个组件库的 CI/CD 和发布流程时,可以考虑以下步骤:

1. 分支管理:

开发者在开发新特性或修复 bug 时,应该在新的分支(通常称为 feature 分支)上进行开发。完成开发后,提交一个 pull request 到 mainmaster 分支,并进行代码审查。

代码语言:javascript
复制
git checkout -b feature/new-component
# 开发过程...
git add .
git commit -m "Add new component"
git push origin feature/new-component

2. 代码检查:

使用如 ESLint、Stylelint 等工具进行代码检查,使用 Jest 等工具进行单元测试和覆盖率检查。这些步骤可以在提交代码时或者 pull request 的过程中自动进行。

例如,可以在 package.json 中添加如下 scripts:

代码语言:javascript
复制
{
  "scripts": {
    "lint": "eslint --ext .js,.jsx,.ts,.tsx src",
    "test": "jest"
  }
}

并在 CI/CD 工具中(如 GitHub Actions、Jenkins 等)配置相应的任务:

代码语言:javascript
复制
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '14'
      - name: Install dependencies
        run: npm ci
      - name: Run lint
        run: npm run lint
      - name: Run tests
        run: npm run test

3. 版本管理:

在合并代码并发布新版本前,需要确认新的版本号,并生成相应的 changelog。可以使用如 standard-version 这样的工具自动化这个过程。

代码语言:javascript
复制
npx standard-version

4. 构建:

使用如 Webpack、Rollup 等工具进行构建,生成可以在不同环境(如浏览器、Node.js)下使用的代码。

代码语言:javascript
复制
npm run build

5. 发布:

将构建好的代码发布到 npm,同时更新文档网站。

代码语言:javascript
复制
npm publish

6. 部署:

部署到github pages或者自建服务

如何实现button按钮

代码语言:javascript
复制
import React, { CSSProperties, FC, MouseEvent, ReactNode } from 'react';

interface ButtonProps {
  lock?: boolean;
  classNames?: Record<string, string>;
  danger?: boolean;
  disabled?: boolean;
  ghost?: boolean;
  href?: string;
  htmlType?: 'button' | 'submit' | 'reset';
  icon?: ReactNode;
  loading?: boolean | { delay: number };
  shape?: 'default' | 'circle' | 'round';
  size?: 'large' | 'middle' | 'small';
  styles?: Record<string, CSSProperties>;
  target?: string;
  type?: 'primary' | 'dashed' | 'link' | 'text' | 'default';
  onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
  children?: ReactNode;
}

const Button: FC<ButtonProps> = ({
  lock,
  classNames,
  danger,
  disabled,
  ghost,
  href,
  htmlType = 'button',
  icon,
  loading,
  shape,
  size,
  styles,
  target,
  type = 'default',
  onClick,
  children
}) => {
  const baseClassName = 'button';

  const className = [
    baseClassName,
    type && `${baseClassName}--${type}`,
    size && `${baseClassName}--${size}`,
    shape && `${baseClassName}--${shape}`,
    disabled && `${baseClassName}--disabled`,
    danger && `${baseClassName}--danger`,
    ghost && `${baseClassName}--ghost`,
    loading && `${baseClassName}--loading`,
    lock && `${baseClassName}--lock`,
  ].filter(Boolean).join(' ');

  const handleClick = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
    if (disabled) {
      e.preventDefault();
    } else if (onClick) {
      onClick(e);
    }
  };

  return href ? (
    <a
      className={className}
      href={href}
      target={target}
      onClick={handleClick}
    >
      {children}
    </a>
  ) : (
    <button
      className={className}
      type={htmlType}
      disabled={disabled}
      onClick={handleClick}
    >
      {children}
    </button>
  );
};

export default Button;

如何实现modal组件

代码语言:javascript
复制
interface IModalProps {
  afterClose?: () => void;
  bodyStyle?: CSSProperties;
  cancelButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
  cancelText?: ReactNode;
  centered?: boolean;
  closeIcon?: boolean | ReactNode;
  confirmLoading?: boolean;
  destroyOnClose?: boolean;
  focusTriggerAfterClose?: boolean;
  footer?: ReactNode;
  forceRender?: boolean;
  getContainer?: HTMLElement | (() => HTMLElement) | string | false;
  keyboard?: boolean;
  mask?: boolean;
  maskClosable?: boolean;
  maskStyle?: CSSProperties;
  modalRender?: (node: ReactNode) => ReactNode;
  okButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
  okText?: ReactNode;
  okType?: string;
  style?: CSSProperties;
  title?: ReactNode;
  open?: boolean;
  width?: string | number;
  wrapClassName?: string;
  zIndex?: number;
  onCancel?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onOk?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  afterOpenChange?: (open: boolean) => void;
}

const Modal: React.FC<IModalProps> = ({
  children,
  title = '',
  onCancel,
  onOk,
  open = false,
  mask = true,
}) => {
  return (
    <>
      {mask && <div className="modal-mask" style={{display: open ? 'block' : 'none'}}></div>}
      {open && (
        <div className="modal" style={{display: 'block'}}>
          <h2 className="modal-title">{title}</h2>
          <div className="modal-body">{children}</div>
          <div className="modal-footer">
            <button className="modal-footer-cancel" onClick={onCancel}>
              Cancel
            </button>
            <button className="modal-footer-ok" onClick={onOk}>
              OK
            </button>
          </div>
        </div>
      )}
    </>
  );
};

Modal.info = function(props: IModalProps) {
  const div = document.createElement('div');
  document.body.appendChild(div);

  function remove() {
    ReactDOM.unmountComponentAtNode(div);
    document.body.removeChild(div);
  }

  function onCancel(e: React.MouseEvent<HTMLButtonElement>) {
    if (props.onCancel) {
      props.onCancel(e);
    }
    remove();
  }

  function onOk(e: React.MouseEvent<HTMLButtonElement>) {
    if (props.onOk) {
      props.onOk(e);
    }
    remove();
  }

  ReactDOM.render(
    <Modal {...props} onCancel={onCancel} onOk={onOk} open={true} />,
    div
  );
};

如何实现高性能Tree组件

实现Tree组件的核心思路是什么?

Tree组件的核心思路是将原始的嵌套children数据结构平铺成一维数组,然后通过计算每个节点的深度(deep)、层级关系等信息,在渲染时动态计算缩进宽度、连接线等,从而实现树形结构的可视化。

Tree组件如何实现高性能大数据渲染?

  • 将原始树形数据平铺为一维数组,便于后续计算
  • 计算出实际需要渲染的节点数据,过滤隐藏的节点
  • 利用虚拟列表技术只渲染可视区域的数据,实现大数据量的高效渲染
代码语言:javascript
复制
function flattenTreeData(treeData = [], parent = null) {
  const nodes = [];

  treeData.forEach((node) => {
    const newNode = {
      ...node,
      parent,
    };

    nodes.push(newNode);

    if (newNode.children) {
      nodes.push(...flattenTreeData(newNode.children, newNode));
    }
  });

  return nodes;
}

如何计算Tree组件中节点的各种状态(展开/折叠、选中等)?

  • 展开/折叠状态根据ExpandedKeys计算
  • 复选框选中状态需要考虑受控/非受控,严格受控模式,及父子节点关联
  • 需要递归计算父节点和子节点的状态
  • 利用平铺后的索引进行相关节点查询
代码语言:javascript
复制
function flattenTreeData(treeData = [], parent = null) {
  const nodes = [];

  treeData.forEach((node) => {
    const newNode = {
      ...node,
      parent,
    };

    nodes.push(newNode);

    if (newNode.children) {
      nodes.push(...flattenTreeData(newNode.children, newNode));
    }
  });

  return nodes;
}

Tree组件的交互如何实现?点击节点展开折叠,复选框状态切换等

  • 点击展开折叠通过更新节点自身状态、可视状态及ExpandedKeys实现
  • 点击复选框需要递归更新父子节点的状态,及相关keys
  • 计算并保存实时状态,通过回调函数通知外部
代码语言:javascript
复制
function toggleExpanded(nodes, node) {
  return nodes.map((currentNode) => {
    if (currentNode === node) {
      return {
        ...currentNode,
        expanded: !currentNode.expanded,
      };
    }

    return currentNode;
  });
}

// 在渲染时计算缩进:
function renderNode(node) {
  const indentLevel = getIndentLevel(node);
  const style = {
    paddingLeft: `${indentLevel * 16}px`,
  };

  return (
    <div style={style} onClick={() => handleNodeClick(node)}>
      {node.label}
    </div>
  );
}

如何实现高性能表格Table组件?

可参考ali-react-table:高性能 React 表格组件

表格组件的性能瓶颈主要在哪里?

  • 渲染大量 DOM;
  • 频繁的更新渲染,如选中行状态改变引起整个表格重新渲染。

如何优化表格组件的渲染性能?

  1. 只渲染必要的列:
代码语言:javascript
复制
const columnsToRender = columns.filter(column => column.shouldRender);

return (
  <table>
    <thead>
      <tr>
        {columnsToRender.map(column => (
          <th key={column.key}>{column.title}</th>
        ))}
      </tr>
    </thead>
    <tbody>
      {data.map(row => (
        <tr key={row.id}>
          {columnsToRender.map(column => (
            <td key={column.key}>{row[column.key]}</td>
          ))}
        </tr>
      ))}
    </tbody>
  </table>
);
  1. 细粒度更新,只更新变化行/列。在React中,可以使用React.memo或者shouldComponentUpdate来避免不必要的重渲染:
代码语言:javascript
复制
function Row({ data, columns }) {
  return (
    <tr>
      {columns.map(column => (
        <Cell key={column.key} data={data[column.key]} />
      ))}
    </tr>
  );
}

const areEqual = (prevProps, nextProps) => {
  return prevProps.data === nextProps.data && prevProps.columns === nextProps.columns;
};

export default React.memo(Row, areEqual);
  1. 采用虚拟化技术,只渲染可视区的行。可以使用第三方库如react-window或者react-virtualized来实现:
代码语言:javascript
复制
import { FixedSizeList as List } from "react-window";

function Table({ data, columns }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {columns.map(column => (
        <Cell key={column.key} data={data[index][column.key]} />
      ))}
    </div>
  );

  return (
    <List
      height={500}
      itemCount={data.length}
      itemSize={35}
    >
      {Row}
    </List>
  );
}
  1. 使用Web Workers来处理数据处理或计算密集型任务:
代码语言:javascript
复制
// 创建一个新的 worker
const worker = new Worker('worker.js');

// 向 worker 发送数据
worker.postMessage(data);

// 监听 worker 的消息
worker.addEventListener('message', (event) => {
  // 更新表格数据
  updateTable(event.data);
});

worker.js中:

代码语言:javascript
复制
self.addEventListener('message', (event) => {
  // 处理数据
  const processedData = processData(event.data);

  // 发送处理后的数据
  self.postMessage(processedData);
});

基于Web Components封装组件库

这个可以当做拓展了解一下,目前有越来越多的开源组件库往这个方向发展,可以参考这篇文章如何基于 WebComponents 封装 UI 组件库

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 微前端
  • 为什么选择微前端作为项目亮点
    • 面试官:你为什么选择用微前端做管理端升级,你的项目很庞大么?
      • 候选人: 不是的,其实是我们把两个管理端合并,让用户方便使用。
        • 面试官:咦,竟然这样你们还不如用a标签链接或者nginx转发一下就行了,更加方便,杀鸡焉用牛刀啊
          • 候选人:为了让客户体验到单页面的感觉,体验感更好
            • 面试官:enen....
              • 方案一:使用公共路径
              • 方案二:劫持标签插入函数
              • 方案三:给 jQuery 项目加上 webpack 打包
          • 请解释一下微前端的概念以及它的主要优点和挑战?
          • 你能详细描述一下 qiankun 微前端框架的工作原理吗?
          • 在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,你会如何处理静态资源的加载问题?
          • 在使用 qiankun 时,如果子应用动态插入了一些标签,你会如何处理?
          • 在使用 qiankun 时,你如何处理老项目的资源加载问题?你能给出一些具体的解决方案吗?
          • 你能解释一下 qiankun 的 start 函数的作用和参数吗?如果只有一个子项目,你会如何启用预加载?
          • 在使用 qiankun 时,你如何处理 js 沙箱不能解决的 js 污染问题?
          • 你能解释一下 qiankun 如何实现 keep-alive 的需求吗?
          • 你能解释一下 qiankun 和 iframe 在微前端实现方式上的区别和优劣吗?在什么情况下,你会选择使用 iframe 而不是 qiankun?
          • 在使用 qiankun 时,你如何处理多个子项目的调试问题?
          • qiankun是如何实现CSS隔离的,该方案有什么缺点,还有其它方案么
          • qiankun中如何实现父子项目间的通信?如果让你实现一套通信机制,你该如何实现?
            • 1.在主项目中使用qiankun注册子项目时,如何解决子项目路由的hash与history模式之争?
              • 如果主项目使用 history 模式,并且子项目可以使用 history 或 hash 模式,这是 qiankun 推荐的一种形式。在这种情况下,子项目可以选择适合自己的路由模式,而且对于已有的子项目不需要做太多修改。但是子项目之间的跳转需要通过父项目的 router 对象或原生的 history 对象进行。
                • 2. 如果主项目和所有子项目都采用 hash 模式,可以有两种做法:
                  • 3. 如果主项目采用 hash 模式,而子项目中有些采用 history 模式,这种情况下,子项目间的跳转只能借助原生的 history 对象,而不使用子项目自己的 router 对象。对于子项目,可以选择使用 path 或 hash 来区分不同的子项目。
                  • 在qiankun中,如果实现组件在不同项目间的共享,有哪些解决方案?
                  • 在qiankun中,应用之间如何复用依赖,除了npm包方案外?
                  • 说说webpack5联邦模块在微前端的应用
                    • 1. 模块共享
                      • 2. 动态加载
                      • 说说qiankun的资源加载机制(import-html-entry)
                      • 说说现有的几种微前端框架,它们的优缺点?
                      • 组件库
                      • 为什么需要二次封装组件库?
                      • 请结合一个组件库设计的过程,谈谈前端工程化的思想
                        • 1. 使用 Lerna 进行多包管理:通过 Lerna 来管理多个包(组件),实现组件级别的解耦、独立版本控制、按需加载等特性。
                          • 2. 规范化提交:使用规范化的提交信息可以提高 Git 日志的可读性,并且可以通过 conventional commits 自动生成 CHANGELOG。可以使用 commitizen、commitlint 等工具来配置。
                            • 3. 代码规范化:通过 ESLint、Prettier 等工具实现代码规范化和格式化,并封装为自己的规范预设。
                              • 4. 组件开发调试:需要考虑热更新编译、软链接引用等问题,以方便在开发过程中进行组件的调试。
                                • 5. 文档站点:可以基于 dumi 搭建文档站点,并实现 CDN 加速、增量发布等优化。可以使用 surge 实现 PR 预览。
                                  • 6. 单元测试:需要考虑 jest、enzyme 等工具的配合使用,生成测试覆盖率报告。
                                    • 7. 按需加载:需要配合 babel-plugin-import 实现按需加载,即在编译时修改导入路径来实现组件的按需加载。
                                      • 8. 组件设计:需要考虑响应式、主题、国际化、TypeScript 支持等问题,以保证组件的灵活性和可扩展性。
                                        • 9. 发布前的自动化脚本:需要编写自动化脚本来规范发布流程,确保发布的一致性和可靠性。
                                          • 10. 发布后的处理:考虑补丁升级、文档站点同步发布等问题,以便及时修复问题并提供最新的文档。
                                            • 11. 制定 Contributing 文档:制定 Contributing 文档可以降低开源社区贡献的门槛,并确保社区成员了解如何参与项目。处理 issues 和 PR 需要有专人负责。
                                            • 如何对一个组件库进行测试?
                                              • 1. 功能测试(单元测试)
                                                • 2. 边界测试
                                                  • 3. 响应测试
                                                    • 4. 交互测试
                                                      • 5. 异常测试
                                                        • 6. 性能测试
                                                          • 7. 自动化测试
                                                          • Element-UI 的多语言方案是怎么设计的?
                                                            • 1. 定义语言包
                                                              • 2. 加载语言包
                                                                • 3. 使用语言包
                                                                  • 4. 集成 vue-i18n
                                                                  • 组件库如何实现在线主题定制的?
                                                                    • 1. 使用 CSS 变量定义样式
                                                                      • 2. 提供主题文件进行配置
                                                                        • 3. 在线主题编辑器
                                                                          • 4. 前端应用新样式
                                                                            • 5. 持久化主题配置
                                                                            • 组件库的类型定义应该怎样设计?
                                                                              • 1. 定义全局类型 versus 定义组件Props类型
                                                                                • 2. 类型导出应该集中还是分散?
                                                                                  • 3. 如何设计类型层级关系?类型复用?
                                                                                    • 4. 类型定义要充分还是精简?
                                                                                    • 组件库的渐进升级策略应该怎么设计?
                                                                                      • 1. 版本控制策略
                                                                                        • 2. 向下兼容处理
                                                                                          • 3. 功能被废弃怎么通知用户升级?
                                                                                            • 4. 兼容旧版本的方案
                                                                                            • 组件库的按需加载实现中存在哪些潜在问题,如何解决?
                                                                                              • babel-plugin-import
                                                                                                • tree-shaking
                                                                                                • 样式如何实现真正的按需加载?避免样式重复打包?
                                                                                                  • 样式和逻辑分离
                                                                                                    • 样式和逻辑结合
                                                                                                      • 样式和逻辑关联
                                                                                                      • 设计一个组件库的 CI/CD 和发布流程。
                                                                                                        • 1. 分支管理:
                                                                                                          • 2. 代码检查:
                                                                                                            • 3. 版本管理:
                                                                                                              • 4. 构建:
                                                                                                                • 5. 发布:
                                                                                                                  • 6. 部署:
                                                                                                                  • 如何实现button按钮
                                                                                                                  • 如何实现modal组件
                                                                                                                  • 如何实现高性能Tree组件
                                                                                                                    • 实现Tree组件的核心思路是什么?
                                                                                                                      • Tree组件如何实现高性能大数据渲染?
                                                                                                                        • 如何计算Tree组件中节点的各种状态(展开/折叠、选中等)?
                                                                                                                          • Tree组件的交互如何实现?点击节点展开折叠,复选框状态切换等
                                                                                                                          • 如何实现高性能表格Table组件?
                                                                                                                            • 表格组件的性能瓶颈主要在哪里?
                                                                                                                              • 如何优化表格组件的渲染性能?
                                                                                                                              • 基于Web Components封装组件库
                                                                                                                              相关产品与服务
                                                                                                                              内容分发网络 CDN
                                                                                                                              内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
                                                                                                                              领券
                                                                                                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档