在qiankun中,如果实现组件在不同项目间的共享,有哪些解决方案?
在项目间共享组件时,可以考虑以下几种方式:
- 父子项目间的组件共享:主项目加载时,将组件挂载到全局对象(如
window
)上,在子项目中直接注册使用该组件。 - 子项目间的组件共享(弱依赖):通过主项目提供的全局变量,子项目挂载到全局对象上。子项目中的共享组件可以使用异步组件来实现,在加载组件前先检查全局对象中是否存在,存在则复用,否则加载组件。
- 子项目间的组件共享(强依赖):在主项目中通过
loadMicroApp
手动加载提供组件的子项目,确保先加载该子项目。在加载时,将组件挂载到全局对象上,并将loadMicroApp
函数传递给子项目。子项目在需要使用共享组件的地方,手动加载提供组件的子项目,等待加载完成后即可获取组件。
需要注意的是,在使用异步组件或手动加载子项目时,可能会遇到样式加载的问题,可以尝试解决该问题。另外,如果共享的组件依赖全局插件(如store
和i18n
),需要进行特殊处理以确保插件的正确初始化。
在qiankun中,应用之间如何复用依赖,除了npm包方案外?
- 在使用
webpack
构建的子项目中,要实现复用公共依赖,需要配置webpack
的externals
,将公共依赖指定为外部依赖,不打包进子项目的代码中。 - 子项目之间的依赖复用可以通过保证依赖的URL一致来实现。如果多个子项目都使用同一份CDN文件,加载时会先从缓存读取,避免重复加载。
- 子项目复用主项目的依赖可以通过给子项目的
index.html
中的公共依赖的script
和link
标签添加自定义属性ignore
来实现。在qiankun
运行子项目时,qiankun
会忽略这些带有ignore
属性的依赖,子项目独立运行时仍然可以加载这些依赖。 - 在使用
qiankun
微前端框架时,可能会出现子项目之间和主项目之间的全局变量冲突的问题。这是因为子项目不配置externals
时,子项目的全局Vue
变量不属于window
对象,而qiankun
在运行子项目时会先找子项目的window
,再找父项目的window
,导致全局变量冲突。 - 解决全局变量冲突的方案有三种:
- 方案一是在注册子项目时,在
beforeLoad
钩子函数中处理全局变量,将子项目的全局Vue
变量进行替换,以解决子项目独立运行时的全局变量冲突问题。 - 方案二是通过主项目将依赖通过
props
传递给子项目,子项目在独立运行时使用传递过来的依赖,避免与主项目的全局变量冲突。 - 方案三是修改主项目和子项目的依赖名称,使它们不会相互冲突,从而避免全局变量冲突的问题。
说说webpack5联邦模块在微前端的应用
Webpack 5 的联邦模块(Federation Module)是一个功能强大的特性,可以在微前端应用中实现模块共享和动态加载,从而提供更好的代码复用和可扩展性
1. 模块共享
Webpack 5 的联邦模块允许不同的微前端应用之间共享模块,避免重复加载和代码冗余。通过联邦模块,我们可以将一些公共的模块抽离成一个独立的模块,并在各个微前端应用中进行引用。这样可以节省资源,并提高应用的加载速度。
// 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-app
和 shared-module
分别是两个微前端应用的 webpack 配置文件。通过 ModuleFederationPlugin
插件,shared-module
将 Button
组件暴露给其他应用使用,而 main-app
则通过 remotes
配置引入了 shared-module
。
2. 动态加载
Webpack 5 联邦模块还支持动态加载模块,这对于微前端应用的按需加载和性能优化非常有用。通过动态加载,可以在需要时动态地加载远程模块,而不是在应用初始化时一次性加载所有模块。
// 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
实现了以下功能:
- 加载 HTML 入口文件:
import-html-entry
会通过创建一个<link>
标签来加载子应用的 HTML 入口文件。这样可以确保子应用的资源得到正确加载,并在加载完成后进行处理。 - 解析 HTML 入口文件:一旦 HTML 入口文件加载完成,
import-html-entry
将解析该文件的内容,提取出子应用的 JavaScript 和 CSS 资源的 URL。 - 动态加载 JavaScript 和 CSS 资源:
import-html-entry
使用动态创建<script>
和<link>
标签的方式,按照正确的顺序加载子应用的 JavaScript 和 CSS 资源。 - 创建沙箱环境:在加载子应用的 JavaScript 资源时,
import-html-entry
会创建一个沙箱环境(sandbox),用于隔离子应用的全局变量和运行环境,防止子应用之间的冲突和污染。 - 返回子应用的入口模块:最后,
import-html-entry
返回一个可以加载子应用的 JavaScript 模块。这个模块通常是一个包含子应用初始化代码的函数,可以在主应用中调用以加载和启动子应用。
通过使用 qiankun import-html-entry
,开发者可以方便地将子应用的 HTML 入口文件作为模块加载,并获得一个可以加载和启动子应用的函数,简化了子应用的加载和集成过程。
说说现有的几种微前端框架,它们的优缺点?
以下是对各个微前端框架优缺点的总结:
- qiankun 方案优点
- 降低了应用改造的成本,通过html entry的方式引入子应用;
- 提供了完备的沙箱方案,包括js沙箱和css沙箱;
- 支持静态资源预加载能力。
- 缺点
- 适配成本较高,包括工程化、生命周期、静态资源路径、路由等方面的适配;
- css沙箱的严格隔离可能引发问题,js沙箱在某些场景下执行性能下降;
- 无法同时激活多个子应用,不支持子应用保活;
- 不支持vite等esmodule脚本运行。
- micro-app 方案优点
- 使用 webcomponent 加载子应用,更优雅;
- 复用经过大量项目验证过的 qiankun 沙箱机制,提高了框架的可靠性;
- 支持子应用保活;
- 降低了子应用改造的成本,提供了静态资源预加载能力。
- 缺点
- 接入成本虽然降低,但路由依然存在依赖;
- 多应用激活后无法保持各子应用的路由状态,刷新后全部丢失;
- css 沙箱无法完全隔离,js 沙箱做全局变量查找缓存,性能有所优化;
- 支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离;
- 对于不支持 webcomponent 的浏览器没有做降级处理。
- EMP 方案优点
- webpack 联邦编译可以保证所有子应用依赖解耦;
- 支持应用间去中心化的调用、共享模块;
- 支持模块远程 ts 支持。
- 缺点
- 对 webpack 强依赖,对于老旧项目不友好;
- 没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉;
- 子应用保活、多应用激活无法实现;
- 主、子应用的路由可能发生冲突。
- 无界方案优点
- 基于 webcomponent 容器和 iframe 沙箱,充分解决了适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite框架支持、应用共享等问题。
- 缺点
- 在继承了iframe优点的同时,缺点依旧还是存在
组件库
为什么需要二次封装组件库?
实际工作中,我们在项目中需要自定义
主题色
,更改按钮样式
,自定义图标
,自定义table组件
等等,这些都可以基于antd组件库进行二次封装,减少重复工作,提升开发效率。
所以我们在封装的时候按照下面这四个原则进行思考就行了,另外本身封装组件库对于项目来说也是没有任何风险,因为一开始我们把PropsType
直接进行转发
,内部再进行增加业务的功能,这样就是达到完全的解耦
- 统一风格:在一个大的项目或者多个相关的项目中,保持一致的界面风格和交互方式是非常重要的。通过二次封装,我们可以定义统一的样式和行为,减少不一致性。
- 降低维护成本:当底层的组件库更新时,我们可能需要在项目的多个地方进行修改。但是如果我们有了自己的封装,只需要在封装层面进行更新即可,这大大降低了维护成本。
- 增加定制功能:有些时候,我们需要在原有组件库的基础上增加一些特定的功能,如特定的验证、错误处理等。二次封装提供了这样的可能。
- 提高开发效率:在一些常用的功能(如表单验证、全局提示等)上,二次封装可以提供更方便的API,提高开发效率。
请结合一个组件库设计的过程,谈谈前端工程化的思想
当我们结合一个组件库设计的过程来谈论前端工程化的思想时,需要理清这些要点:
1. 使用 Lerna 进行多包管理:通过 Lerna 来管理多个包(组件),实现组件级别的解耦、独立版本控制、按需加载等特性。
# 安装 Lerna npm install -g lerna # 初始化一个 Lerna 仓库 lerna init # 创建 "Button" 组件包 lerna create button --yes
2. 规范化提交:使用规范化的提交信息可以提高 Git 日志的可读性,并且可以通过 conventional commits 自动生成 CHANGELOG。可以使用 commitizen、commitlint 等工具来配置。
# 安装相关工具 npm install commitizen cz-conventional-changelog --save-dev
// package.json { "scripts": { "commit": "git-cz" }, "config": { "commitizen": { "path": "cz-conventional-changelog" } } }
3. 代码规范化:通过 ESLint、Prettier 等工具实现代码规范化和格式化,并封装为自己的规范预设。
# 安装相关工具 npm install eslint prettier eslint-plugin-prettier eslint-config-prettier --save-dev
// .eslintrc.js module.exports = { extends: ['eslint:recommended', 'plugin:prettier/recommended'], }; // .prettierrc.js module.exports = { singleQuote: true, trailingComma: 'es5', };
4. 组件开发调试:需要考虑热更新编译、软链接引用等问题,以方便在开发过程中进行组件的调试。
// 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 预览。
<!-- 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 等工具的配合使用,生成测试覆盖率报告。
# 安装相关工具 npm install jest enzyme enzyme-adapter-react-16 react-test-renderer --save-dev
// 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 实现按需加载,即在编译时修改导入路径来实现组件的按需加载。
# 安装相关工具 npm install babel-plugin-import --save-dev
// .babelrc { "plugins": [ [ "import", { "libraryName": "button-library", "style": "css" } ] ] }
8. 组件设计:需要考虑响应式、主题、国际化、TypeScript 支持等问题,以保证组件的灵活性和可扩展性。
// 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. 发布前的自动化脚本:需要编写自动化脚本来规范发布流程,确保发布的一致性和可靠性。
// package.json { "scripts": { "prepublish": "npm run lint && npm run test", "lint": "eslint .", "test": "jest" } }
10. 发布后的处理:考虑补丁升级、文档站点同步发布等问题,以便及时修复问题并提供最新的文档。
11. 制定 Contributing 文档:制定 Contributing 文档可以降低开源社区贡献的门槛,并确保社区成员了解如何参与项目。处理 issues 和 PR 需要有专人负责。
如何对一个组件库进行测试?
首先需要明确,组件库的测试大致可以分为两类:一类是针对组件本身的功能和性能的测试(例如,单元测试、性能测试),另一类是针对组件在集成环境下的行为和性能的测试(例如,集成测试、系统测试)。
1. 功能测试(单元测试)
通常来说,组件的功能测试可以通过单元测试来完成。单元测试的目的是验证组件的单个功能是否按照预期工作。这通常可以通过编写测试用例来完成,每个测试用例针对一个特定的功能。
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. 边界测试
边界测试是一种特殊的功能测试,用于检查组件在输入或输出达到极限或边界条件时的行为。
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 等。
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)测试工具来完成。
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. 异常测试
异常测试用于验证组件在遇到错误或非法输入时能否正确处理。这通常可以通过在测试用例中模拟错误条件来完成。
test('Button should handle errors', () => { const component = new YourComponent(); // Test with illegal argument expect(() => { component.doSomething('illegal argument'); }).toThrow('Expected error message'); });
6. 性能测试
性能测试用于验证组件的性能,例如,加载速度、内存消耗等。
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 可以用于自动化运行端到端测试。
module.exports = { roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest' } };