本文由TinyVue组件库核心成员郑志超分享,首先分享了实现跨框架组件库的必要性,同时通过演示demo和实际操作向我们介绍了如何实现一个跨框架的组件库。
前言
前端组件库跨框架是什么?
前端组件库跨框架是指在不同的前端框架(如React、Vue、Solid等)之间共享和复用组件的能力。这种能力可以让开发者在不同的项目中使用同一套组件库,从而提高开发效率和代码复用性。
为什么需要做前端组件库跨框架?
首先,不同的前端框架有不同的语法和API,如果每个框架都要写一套组件库,那么开发成本和维护成本都会很高。其次,跨框架的组件库可以让开发者更加灵活地选择框架,而不必担心组件库的兼容性问题。
如何开发
要实现前端组件库跨框架,需要使用一些技术手段。本文将要演示如何通过common适配层和renderless无渲染逻辑层实现跨框架组件库。
温馨提示:本文涉及到的代码较多,所以无法将所有代码都罗列出来,因此演示流程主要以分析思路为主,如果想要运行完整流程建议下载演示demo查看源码和展示效果(文章最后会介绍如何下载和运行)
因为OpenTinyVue已具备同时兼容vue2和vue3的能力,所以本文以react为例,介绍如何开发一套复用现有OpenTinyVue代码逻辑的跨框架组件库
首先开发react跨框架组件库主要分为几个步骤:
1、使用pnpm管理monorepo工程的组件库,可以更好的管理本地和线上依赖包。
2、创建react框架的common适配层,目的是抹平不同框架之间的差异,并对接renderless无渲染逻辑层。
3、实现无渲染逻辑层renderless,目的是抽离与框架和渲染无关的业务逻辑,然后复用这部分逻辑。
4、创建模板层去对接common适配层和renderless无渲染层,从而实现了框架、模板和业务逻辑的分离。
下面演示下如何开发一个跨框架的组件库
一、使用pnpm管理monorepo工程的组件库
1、创建monorepo工程文件夹,使用gitbash输入以下命令(以下所有命令均在gitbase环境下运行)
bash
mkdir cross-framework-component
cd cross-framework-component
# 创建多包目录
mkdir packages
2、在根目录下创建package.json,并修改其内容
csharp
npm init -y
package.json内容主要分为两块:
(1)定义包管理工具和一些启动工程的脚本:
- "preinstall": "npx only-allow pnpm" -- 本项目只允许使用pnpm管理依赖
- "dev": "node setup.js" -- 启动无界微前端的主工程和所有子工程
- "dev:home": "pnpm -C packages/home dev" -- 启动无界微前端的主工程(vue3框架)
- "dev:react": "pnpm -C packages/react dev" -- 启动无界微前端的react子工程
- "dev:solid": "pnpm -C packages/solid dev" -- 启动无界微前端的solid子工程
- "dev:vue2": "pnpm -C packages/vue2 dev" -- 启动无界微前端的vue2子工程
- "dev:vue3": "pnpm -C packages/vue3 dev" -- 启动无界微前端的vue3子工程
(2)解决一些pnpm针对vue不同版本(vue2、vue3)的依赖冲突,packageExtensions 项可以让vue2相关依赖可以找到正确的vue版本,从而可以正常加载vue2和vue3的组件。
package.json内容如下:
perl
{
"name": "@opentiny/cross-framework",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "node setup.js",
"dev:home": "pnpm -C packages/home dev",
"dev:react": "pnpm -C packages/react dev",
"dev:solid": "pnpm -C packages/solid dev",
"dev:vue2": "pnpm -C packages/vue2 dev",
"dev:vue3": "pnpm -C packages/vue3 dev"
},
"repository": {
"type": "git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"eslint": "8.48.0"
},
"pnpm": {
"packageExtensions": {
"vue-template-compiler@2.6.14": {
"peerDependencies": {
"vue": "2.6.14"
}
},
"@opentiny/vue-locale@2.9.0": {
"peerDependencies": {
"vue": "2.6.14"
}
},
"@opentiny/vue-common@2.9.0": {
"peerDependencies": {
"vue": "2.6.14"
}
}
}
}
}
3、在根目录创建pnpm-workspace.yaml文件并配置如下:
makefile
packages:
- packages/** # packages文件夹下所有包含package.json的文件夹都是子包
4、创建组件源代码目录
bash
cd packages
mkdir components
二、 创建react框架的common适配层
将整个工程创建好之后,我们需要抹平不同框架之间的差异,这样才能实现一套代码能够去支持不同的框架,那如何来抹平不同框架之间的差异呢?这里出现一个重要概念--common适配层 。它用来对接纯函数renderless无渲染逻辑层。
下面以react框架为例详细介绍如何构造react框架的common适配层(solid、vue的原理可以类比)
1、在上文创建的components文件夹中创建react文件夹,并初始化package.json
bash
mkdir react
cd react
npm init -y
package.json的内容主要是把dependencies项中@opentiny/react-button和@opentiny/react-countdown两个依赖指向本地组件包,这是pnpm提供的本地包加载方式。
具体的配置如下所示:
perl
{
"name": "@opentiny/react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@opentiny/react-button": "workspace:~",
"@opentiny/react-countdown": "workspace:~"
}
}
2、在上文创建的react文件夹中创建适配层文件夹common并初始化package.json(路径:packages/components/react/common)
arduino
mkdir common
npm init -y
package.json内容中的一些重要依赖项及其说明:
- "@opentiny/renderless": "workspace:~" -- 使用本地的renderless包
- "@opentiny/theme": "workspace:~" -- 使用本地的theme主题包
- "classnames": "^2.3.2" -- 处理html标签的class类名
- "ahooks": "3.7.8" -- 提供react响应式数据能力,对齐vue的响应式数据
package.json具体内容如下所示:
perl
{
"name": "@opentiny/react-common",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@opentiny/renderless": "workspace:~",
"@opentiny/theme": "workspace:~",
"classnames": "^2.3.2",
"ahooks": "3.7.8",
"react": "18.2.0"
}
}
3、在上文创建的common文件夹中继续创建适配层逻辑页面(路径:packages/components/common/src/index.js)
bash
mkdir src
cd src
touch index.js
具体的目录结构如下:
css
├─ react
│ ├─ common # react适配层
│ │ ├─ package.json
│ │ └─ src
│ │ ├─ index.js
│ ├─ index.js
│ ├─ package.json
│ ├─ README.md
│ ├─ README.zh-CN.md
│ └─ src
│ ├─ button # react框架button组件的模板层
│ │ ├─ package.json
│ │ └─ src
│ │ └─ pc.jsx
│ └─ countdown # react框架倒计时组件的模板层
│ ├─ package.json
│ └─ src
│ └─ pc.jsx
4、最后把props和无渲染逻辑层renderless导出的api进行适配react的处理,以下这段代码主要是分别从三个方面来处理这个问题。
- 抹平响应式数据: 为react和solid提供响应式数据能力,从而可以复用OpentinyVue已经写好组件的state数据响应能力,上面的代码就是使用了ahooks去模拟了vue的响应式数据,并且可以在响应式数据变化的时候调用react的setState方法,从而触发了视图的渲染。
- 抹平vue的nextTick: 使用微任务queueMicrotask模拟vue框架的nextTick。
- 抹平事件触发机制: 使用自定义方法模拟vue框架的事件触发机制emit。
具体代码如下所示(路径:packages/components/react/common/src/index.js):
javascript
import * as hooks from 'react'
import '@opentiny/theme/base/index.less'
import { useReactive } from 'ahooks' // 使用ahooks提供的useReactive抹平vue框架的响应式数据
// 抹平vue框架的事件触发机制
export const emit =
(props) =>
(evName, ...args) => {
if (props[evName] && typeof props[evName] === 'function') {
props[evName](...args)
}
}
// 抹平vue框架的nextTick,等待 dom 更新后触发回调
export const useNextTick = (callback) => {
queueMicrotask(callback)
}
export const useSetup = ({
props, // 模板层传递过来的props属性
renderless, // renderless无渲染函数
extendOptions = { framework: 'React' } // 模板层传递过来的额外参数
}) => {
const render =
typeof props.tiny_renderless === 'function'
? props.tiny_renderless
: renderless
const utils = {
parent: {},
emit: emit(props)
}
const sdk = render(
props,
{ ...hooks, useReactive, useNextTick },
utils,
extendOptions
)
return {
...sdk,
type: props.type ?? 'default'
}
}
三、无渲染逻辑层renderless实现
接下来介绍下实现跨端组件库的第二个重要概念:renderless无渲染层 -- 这块分为两部分:一个是与框架相关的入口函数文件(react.js、vue.js、solid.js)另外一个是与框架无关的纯函数文件(index.js)。
1、在components文件夹中创建renderless文件夹,并初始化package.json
arduino
mkdir renderless
npm init -y
package.json文件内容如下所示(其中exports项表示所有加载的资源都会从randerless目录下的src文件夹中按文件路径寻找):
json
{
"name": "@opentiny/renderless",
"version": "3.9.0",
"sideEffects": false,
"type": "module",
"exports": {
"./package.json": "./package.json",
"./*": "./src/*"
}
}
2、以react为例,采用无渲染逻辑的复用方式
首先看下renderless需要创建的文件夹和文件(注意:这里只是罗列了renderless文件夹中的文件结构,外部文件结构省略了):
bash
├─ renderless
│ ├─ package.json
│ ├─ README.md
│ ├─ README.zh-CN.md
│ └─ src
│ ├─ button
│ │ ├─ index.js # 公共逻辑层
│ │ ├─ react.js # react相关api层
│ │ ├─ solid.js # solid相关api层
│ │ └─ vue.js # vue相关api层
react.js是@opentiny/react-button组件的renderless入口文件,它负责去对接react的适配层@opentiny/react-common,主要功能是去调用一些react相关的api,比如生命周期函数等,在renderless函数最后返回了state响应式对象和一些方法,提供给react的函数式组件使用。
文件主要有两个需要注意的点:
(1)使用common适配层传递过来的useReactive函数返回基于react的响应式数据,对齐vue的响应式数据
(2)使用双层函数(闭包)保存了一些组件状态,方便用户和模板层调用方法。
react.js具体代码内容如下所示:
javascript
import { handleClick, clearTimer } from './index'
export const api = ['state', 'handleClick']
export default function renderless(
props,
{ useReactive },
{ emit },
{ framework }
) {
// 利用ahooks提供的useReactive模拟vue的响应式数据,并且使用react的useRef防止响应式数据被重复执行定义
const state = useReactive({
timer: null,
disabled: !!props.disabled,
plain: props.plain,
formDisabled: false
})
const api = {
state,
clearTimer: clearTimer(state),
handleClick: handleClick({ emit, props, state, framework })
}
return api
}
index.js是和react、solid、vue三大框架无关只和业务逻辑有关的公共逻辑层,因此这部分代码是和框架无关的纯业务逻辑代码。
index.js逻辑层一般都是双层函数(闭包:函数返回函数),第一层函数保存了一些组件状态,第二层函数可以很方便的让用户和模板层调用。
这里介绍下button组件的纯逻辑层的两个函数:
(1)handleClick:当点击按钮时会触发handleClick内层函数,如果用户传递的重置时间大于零,则在点击之后会设置按钮的disabled属性为true禁用按钮,并在重置时间后解除按钮禁用,然后打印出当前逻辑触发是来自哪个框架,并向外抛出click点击事件;
(2)clearTimer:调用clearTimer方法可以快速清除组件的timer定时器。
具体内容如下所示:
javascript
export const handleClick =
({ emit, props, state, framework }) =>
(event) => {
if (props.nativeType === 'button' && props.resetTime > 0) {
state.disabled = true
state.timer = setTimeout(() => {
state.disabled = false
}, props.resetTime)
}
console.log(`${framework}框架代码已触发!!!!!!!!!`)
emit('click', event)
}
export const clearTimer = (state) => () => clearTimeout(state.timer)
四、创建模板层去对接common适配层和renderless无渲染层
由于需要创建的文件太多,为了方便操作,可以直接参考我们提供的示例源码工程查看(github.com/opentiny/cr...
具体的目录结构如下:
css
├─ react
│ ├─ common # react适配层
│ │ ├─ package.json
│ │ └─ src
│ │ ├─ index.js
│ ├─ index.js
│ ├─ package.json
│ ├─ README.md
│ ├─ README.zh-CN.md
│ └─ src
│ ├─ button # react框架button组件的模板层
│ │ ├─ package.json
│ │ └─ src
│ │ └─ pc.jsx
│ └─ countdown # react框架倒计时组件的模板层
│ ├─ package.json
│ └─ src
│ └─ pc.jsx
这里创建的模板层和一般的react函数式组件类似,都是接受使用组件的用户传递过来的属性,并返回需要渲染的jsx模板。不一样的地方是:jsx绑定的数据是通过适配层和renderless无渲染层处理后的数据,并且数据发生变化的时候会触发视图渲染,比如下面代码中useSetup方法。
pc.jsx的具体实现如下所示(路径:packages/components/react/src/button/src/pc.jsx):
typescript
import renderless from '@opentiny/renderless/button/react' // renderless无渲染层
import { useSetup } from '@opentiny/react-common' // 抹平不同框架的适配层
import '@opentiny/theme/button/index.less' // 复用OpenTinyVue的样式文件
export default function Button(props) {
const {
children,
text,
autofocus,
round,
circle,
icon: Icon,
size,
nativeType = 'button',
} = props
const {
handleClick,
state,
tabindex,
type,
$attrs
} = useSetup({ // 通过common适配层的useSetup处理props和renderless无渲染层
props: { ...props, nativeType: 'button', resetTime: 1000 },
renderless
})
const className = [
'tiny-button',
type ? 'tiny-button--' + type : '',
size ? 'tiny-button--' + size : '',
state.disabled ? 'is-disabled' : '',
state.plain ? 'is-plain' : '',
round ? 'is-round' : '',
circle ? 'is-circle' : ''
].join(' ').trim()
return ( <button
className={className}
onClick={handleClick}
disabled={state.disabled}
autoFocus={autofocus}
type={nativeType}
tabIndex={tabindex}
{...$attrs}
>
{(Icon) ? <Icon className={(text || children) ? 'is-text' : ''} /> : ''} <span>{children || text}</span>
</button>
)
}
到此大体上描述了跨框架组件库的实现原理。
demo演示
如果想快速查看效果和源码,可以克隆我们提供的跨框架示例demo,具体操作步骤如下:
1、使用如下命令把演示demo克隆到本地:
bash
git clone https://github.com/opentiny/cross-framework-component.git
2、使用pnpm下载依赖:
css
pnpm i
# 如果没有pnpm需要执行以下命令
npm i pnpm -g
3、工程目录结构分析
整个工程是基于pnpm搭建的多包monorepo工程,演示环境为无界微前端环境,整体工程的目录架构如下所示(本文主要介绍packages/components文件夹):
bash
├─ package.json
├─ packages
│ ├─ components # 组件库文件夹
│ │ ├─ react # react组件库及其适配层
│ │ ├─ renderless # 跨框架复用的跨框架无渲染逻辑层
│ │ ├─ solid # solid组件库及其适配层
│ │ ├─ theme # 跨框架复用的pc端样式层
│ │ ├─ theme-mobile # 移动端模板样式层
│ │ ├─ theme-watch # 手表带模板样式层
│ │ └─ vue # vue组件库及其适配层
│ ├─ element-to-opentiny # element-ui切换OpenTiny演示工程
│ ├─ home # 基于vue3搭建无界微前端主工程
│ ├─ react # 基于react搭建无界微前端子工程
│ ├─ solid # 基于solid搭建无界微前端子工程
│ ├─ vue2 # 基于vue2搭建无界微前端子工程
│ └─ vue3 # 基于vue3搭建无界微前端子工程
├─ pnpm-workspace.yaml
├─ README.md
├─ README.zh-CN.md
└─ setup.js
4、启动本地的无界微前端本地服务
pnpm dev
启动后会总共启动5个工程,1个主工程和4个子工程,其中4个子工程分别引入了不同框架的组件库,但是不同框架的组件库复用了同一份交互逻辑代码和样式文件。
效果如下图所示:
如何证明vue2、vue3、react、solid都共用了一套逻辑了呢?
我们可以点击按钮然后会在控制台打印,当前复用逻辑层是来自哪个框架的:
可以看到不同框架代码都已触发。
感兴趣的朋友可以持续关注我们TinyVue组件库。也欢迎给 TinyVue 开源项目点个 Star 🌟支持下:github.com/opentiny/ti...
OpenTiny Vue招募贡献者啦!
OpenTiny Vue 正在招募社区贡献者,欢迎加入我们🎉
你可以通过以下方式参与贡献:
- 在 issue 列表中选择自己喜欢的任务
- 阅读贡献者指南,开始参与贡献
你可以根据自己的喜好认领以下类型的任务:
- 编写单元测试
- 修复组件缺陷
- 为组件添加新特性
- 完善组件的文档
如何贡献单元测试:
- 在
packages/vue
目录下搜索it.todo
关键字,找到待补充的单元测试 - 按照以上指南编写组件单元测试
- 执行单个组件的单元测试:
pnpm test:unit3 button
如果你是一位经验丰富的开发者,想接受一些有挑战的任务,可以考虑以下任务:
- ✨ [Feature]: 希望提供 Skeleton 骨架屏组件
- ✨ [Feature]: 希望提供 Divider 分割线组件
- ✨ [Feature]: tree树形控件能增加虚拟滚动功能
- ✨ [Feature]: 增加视频播放组件
- ✨ [Feature]: 增加思维导图组件
- ✨ [Feature]: 添加类似飞书的多维表格组件
- ✨ [Feature]: 添加到 unplugin-vue-components
- ✨ [Feature]: 兼容formily
参与 OpenTiny 开源社区贡献,你将收获:
直接的价值:
- 通过参与一个实际的跨端、跨框架组件库项目,学习最新的
Vite
+Vue3
+TypeScript
+Vitest
技术 - 学习从 0 到 1 搭建一个自己的组件库的整套流程和方法论,包括组件库工程化、组件的设计和开发等
- 为自己的简历和职业生涯添彩,参与过优秀的开源项目,这本身就是受面试官青睐的亮点
- 结识一群优秀的、热爱学习、热爱开源的小伙伴,大家一起打造一个伟大的产品
长远的价值:
- 打造个人品牌,提升个人影响力
- 培养良好的编码习惯
- 获得华为云 OpenTiny 团队的荣誉和定制小礼物
- 受邀参加各类技术大会
- 成为 PMC 和 Committer 之后还能参与 OpenTiny 整个开源生态的决策和长远规划,培养自己的管理和规划能力
- 未来有更多机会和可能
关于 OpenTiny
OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 移动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,拥有主题配置系统 / 中后台模板 / CLI 命令行等效率提升工具,可帮助开发者高效开发 Web 应用。
核心亮点:
跨端跨框架
:使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。组件丰富
:PC 端有100+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等配置式组件
:组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化周边生态齐全
:提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme
联系我们:
- 官方公众号:
OpenTiny
- OpenTiny 官网:opentiny.design/
- OpenTiny 代码仓库:github.com/opentiny/
- Vue 组件库:github.com/opentiny/ti... (欢迎 Star)
- Angluar组件库:github.com/opentiny/ng (欢迎 Star)
- CLI工具:github.com/opentiny/ti... (欢迎 Star)
更多视频内容也可以关注OpenTiny社区,B站/抖音/小红书/视频号。