原来 TinyVue 组件库跨框架(Vue2、Vue3、React、Solid)是这样实现的?
本文由 TinyVue 组件库核心成员郑志超分享,首先分享了实现跨框架组件库的必要性,同时通过演示Demo和实际操作向我们介绍了如何实现一个跨框架的组件库。
前言
前端组件库跨框架是什么?
前端组件库跨框架是指在不同的前端框架(如 React、Vue、Solid 等)之间共享和复用组件的能力。这种能力可以让开发者在不同的项目中使用同一套组件库,从而提高开发效率和代码复用性。
为什么需要做前端组件库跨框架?
首先,不同的前端框架有不同的语法和 API,如果每个框架都要写一套组件库,那么开发成本和维护成本都会很高。其次,跨框架的组件库可以让开发者更加灵活地选择框架,而不必担心组件库的兼容性问题。
而 TinyVue 组件库在实现跨框架之前也经历了三个阶段。
第一个阶段:
2019年初,当时 Vue 3.0 还未发布,TinyVue创始团队 率先使用了 @vue/composition-api 和 renderless 无渲染函数隔离模板、样式和逻辑代码;经过两年的发展,支持的项目达到了800+,同时因为组件功能的丰富,代码量也达到了20w+。
第二个阶段:
2021年初,当时 Vue 3.0 已经发展了半年有余,各个方面已经逐步完善,TinyVue 支持的项目由 Vue2.0 切换 Vue3.0 的意愿日渐强烈;但是又苦于没有支持 Vue 3.0 的组件库; 于是 TinyVue 基于@vue/composition-api 和 renderless的架构的巨大优势体现了出来,在短短两个月通过适配层 vue-common 将 20w+ 行代码全部适配了 Vue3.0, 极大的减少了开发成本。2021年10月 TinyVue 组件库实现了一套代码同时支持 Vue2.0 和 Vue3.0 。
第三个阶段:
2023年6月,TinyVue 团队需要和开源的 openInula(完全兼容 React )框架合作共同开发 Inula 组件库,并且通过中科院软件所的开源之夏活动与开发者共建 OpenTiny React 组件库。在此过程中,充分利用 TinyVue 的模板与逻辑分离的架构,完成了开发可以适配 React 的 common 适配层,并已完成 4 个 React 组件的开发,并且完全复用了 renderless 无渲染层的逻辑。
为了更好的理解,可以参考以下 TinyVue 组件库的架构图:

通过前端组件库跨框架,可以达到以下效果:
-
提高开发效率和代码复用性,减少重复开发的工作量。
-
统一 UI 风格和交互体验,提高产品的一致性和可用性。
-
支持多种前端框架,让开发者更加灵活地选择框架。
-
降低维护成本,减少代码冗余和重复的工作。
总之,前端组件库跨框架可以帮助开发者更加高效地开发和维护前端应用,提高产品的质量和用户体验。
如何开发
要实现前端组件库跨框架,需要使用一些技术手段。本文将要演示如何通过 common 适配层和 renderless 无渲染逻辑层实现跨框架组件库。
温馨提示: 本文涉及到的代码较多,所以无法将所有代码都罗列出来,因此演示流程主要以分析思路为主,如果想要运行完整流程建议下载演示 Demo 查看源码和展示效果(文章最后会介绍如何下载和运行)
因为 TinyVue 组件库已具备同时兼容 Vue2 和 Vue3 的能力,所以本文以 React 和 Solid 为例,介绍如何开发一套复用现有 TinyVue 代码逻辑的跨框架组件库
首先开发 React 和 Solid 跨框架组件库主要分为几个步骤:
1、使用 pnpm 管理 monorepo 工程的组件库,可以更好的管理本地和线上依赖包。
2、创建 React 框架和 Solid 框架的 common 适配层,目的是抹平不同框架之间的差异,并对接 renderless 无渲染逻辑层。
3、实现无渲染逻辑层 renderless,目的是抽离与框架和渲染无关的业务逻辑,然后复用这部分逻辑。
4、创建模板层去对接 common 适配层和 renderless 无渲染层,从而实现了框架、模板和业务逻辑的分离。
下面演示下如何开发一个跨框架的组件库
一、使用 pnpm 管理 monorepo 工程的组件库
1、创建 monorepo 工程文件夹,使用 gitbash 输入以下命令(以下所有命令均在 gitbase 环境下运行)
mkdir cross-framework-componentcd cross-framework-component# 创建多包目录
mkdir packages
2、在根目录下创建 package.json,并修改其内容
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 内容如下:
{"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 文件并配置如下:
packages:- packages/** # packages文件夹下所有包含package.json的文件夹都是子包
4、创建组件源代码目录
cd packages
mkdir components
二、 创建 React 框架和 Solid 框架的 common 适配层
将整个工程创建好之后,我们需要抹平不同框架之间的差异,这样才能实现一套代码能够去支持不同的框架,那如何来抹平不同框架之间的差异呢?这里出现一个重要概念–common 适配层 。它用来对接纯函数 renderless 无渲染逻辑层。
下面以 React 框架及 Solid 框架为例详细介绍如何构造两个框架的 common 适配层(Vue 的原理可以类比)
1、在上文创建的 components 文件夹中创建 React 和 Solid 文件夹,并初始化 package.json
mkdir react
mkdir solid
cd react
npm init -y
cd ../solid
npm init -y
package.json 的内容主要是把 dependencies 项中@opentiny/react-button 、@opentiny/react-countdown、@opentiny/solid-button、@opentiny/solid-countdown 4个依赖指向本地组件包,这是 pnpm 提供的本地包加载方式。
具体的配置如下所示:
@opentiny/react
{"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:~"}
}
@opentiny/solid
{"name": "@opentiny/solid","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo "Error: no test specified" && exit 1"},"keywords": [],"author": "","license": "ISC","dependencies": {"@opentiny/solid-button": "workspace:~","@opentiny/solid-countdown": "workspace:~"}
}
2、在上文创建的 React 和 Solid 文件夹中创建适配层文件夹 common 并初始化package.json(路径:packages/components/react/common、packages/components/solid/common)
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 具体内容如下所示:
@opentiny/react-comon
{"name": "@opentiny/react-common","version": "1.0.0","description": "","main": "src/index.js","keywords": [],"author": "","license": "ISC","dependencies": {"@opentiny/renderless": "workspace:~","@opentiny/theme": "workspace:~","// ---- 处理html标签的class类名 ----": "","classnames": "^2.3.2","// ---- 提供react响应式数据能力,对齐vue的响应式数据 ----": "","ahooks": "3.7.8","react": "18.2.0"}
}
@opentiny/solid-common
{"name": "@opentiny/solid-common","version": "1.0.0","description": "","main": "src/index.js","keywords": [],"author": "","license": "ISC","dependencies": {"@opentiny/renderless": "workspace:~","@opentiny/theme": "workspace:~","// ---- 处理html标签的class类名 ----": "","classnames": "^2.3.2","solid-js": "^1.7.8"}
}
3、在上文创建的 common 文件夹中继续创建适配层逻辑页面(路径:packages/components/react/common、packages/components/solid/common)
mkdir src
cd src
touch index.js
React 具体的目录结构如下:
├─ 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
Solid 具体的目录结构如下:
├─ solid
│ ├─ common # solid适配层
│ │ ├─ package.json
│ │ └─ src
│ │ ├─ index.js
│ ├─ index.js
│ ├─ package.json
│ ├─ README.md
│ ├─ README.zh-CN.md
│ └─ src
│ ├─ button # solid框架button组件的模板层
│ │ ├─ package.json
│ │ └─ src
│ │ └─ pc.jsx
│ └─ countdown # solid框架倒计时组件的模板层
│ ├─ package.json
│ └─ src
│ └─ pc.jsx
4、最后把 props 和无渲染逻辑层 renderless 导出的 api 进行适配 React 的处理,以下这两段代码主要是分别从三个方面来处理这个问题。
- 抹平响应式数据: 为 React(Solid 本身具有响应式能力)提供响应式数据能力,从而可以复用 OpentinyVue 已经写好组件的 state 数据响应能力,React 使用了 ahooks 去模拟了 Vue 的响应式数据,并且可以在响应式数据变化的时候调用 React 的setState方法,从而触发了视图的渲染;而 Solid 只需要使用 createSignal 方法去创建响应式对象,并且在模板中使用 state().xxx去使用 Solid 自带的响应式能力,从而触发视图渲染。
- 抹平 Vue 的 nextTick: 使用微任务 queueMicrotask 模拟 Vue 框架的 nextTick。
- 抹平事件触发机制: 使用自定义方法模拟 Vue 框架的事件触发机制 emit。
其中 React 具体代码如下所示(路径:packages/components/react/common/src/index.js):
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: renderlessconst utils = {parent: {},emit: emit(props)}const sdk = render(props,{ ...hooks, useReactive, useNextTick },utils,extendOptions)return {...sdk,type: props.type ?? 'default'}
}
其中 Solid 具体代码如下所示(路径:packages/components/solid/common/src/index.js):
import * as hooks from 'solid-js'
import { createSignal } from 'solid-js'
import '@opentiny/theme/base/index.less'const EVENTS_PREFIX = 'on'// 处理solid事件触发机制
export const emit =(props) =>(evName, ...args) => {const eventsName = `${EVENTS_PREFIX}${evName[0].toLocaleUpperCase()}${evName.slice(1)}`if (props[eventsName] && typeof props[eventsName] === 'function') {props[eventsName](...args)}}export const useSetState = (initialState) => {// equals: false 配置非常重要,保证state对象属性发生变化后视图可以更新const [state, setState] = createSignal(initialState, { equals: false })return [state, setState]
}// props 应该不用做处理, props 都是 . 访问。
export const useReactive = (staticObject) => {const [state, setState] = useSetState(staticObject)return {state,// 这里提供代理对象提供给renderless无渲染层使用proxy: new Proxy(state(), {get(target, property) {if (typeof target[property] === 'function') {return target[property](target)} else {return target[property]}},set(target, property, value) {Reflect.set(target, property, value)setState((val) => val)return true}})}
}// nextTick, 等待 dom 更新后触发回调
export const useNextTick = (callback) => {queueMicrotask(callback)
}// emitEvent, dispath, broadcast
export const emitEvent = () => {const broadcast = () => {return ''}return {dispatch: () => {return ''},broadcast}
}export const useSetup = ({props,renderless,extendOptions = { framework: 'Solid' }
}) => {const render =typeof props.tiny_renderless === 'function'? props.tiny_renderless: renderlessconst 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
mkdir renderless
npm init -y
package.json 文件内容如下所示(其中 exports 项表示所有加载的资源都会从 randerless 目录下的 src 文件夹中按文件路径寻找):
{"name": "@opentiny/renderless","version": "3.9.0","sideEffects": false,"type": "module","exports": {"./package.json": "./package.json","./*": "./src/*"}
}
2、以 React 和 Solid 为例,采用无渲染逻辑的复用方式
首先看下 renderless 需要创建的文件夹和文件(注意:这里只是罗列了 renderless 文件夹中的文件结构,外部文件结构省略了):
├─ 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 和solid.js 是@opentiny/react-button 组件和 @opentiny/solid-button组件的 renderless 入口文件,它负责去对接 React 和 Solid 的适配层@opentiny/react-common,主要功能是去调用一些 React 和 Solid 相关的 api,比如生命周期函数等,在 renderless 函数最后返回了 state 响应式对象和一些方法,提供给 React 和 Solid 的函数式组件使用。
文件主要有两个需要注意的点:
(1)使用 common 适配层传递过来的 useReactive 函数返回基于 React 和 Solid 的响应式数据,对齐 Vue 的响应式数据
(2)使用双层函数(闭包)保存了一些组件状态,方便用户和模板层调用方法。
react.js 具体代码内容如下所示:
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
}
solid.js具体代码内容如下所示:
import { handleClick, clearTimer } from './index'export const api = ['state', 'handleClick']export default function renderless(props,{ useReactive },{ emit },{ framework }
) {// prox是state执行时候的原始对象的代理const { state, proxy } = useReactive({timer: null,disabled: !!props.disabled,plain: props.plain})const api = {state,clearTimer: clearTimer(proxy),handleClick: handleClick({ emit, props, state: proxy, framework })}return api
}
index.js 是和 React、Solid、Vue 三大框架无关只和业务逻辑有关的公共逻辑层,因此这部分代码是和框架无关的纯业务逻辑代码。
index.js 逻辑层一般都是双层函数(闭包:函数返回函数),第一层函数保存了一些组件状态,第二层函数可以很方便的让用户和模板层调用。
这里介绍下 button 组件的纯逻辑层的两个函数:
(1)handleClick:当点击按钮时会触发 handleClick 内层函数,如果用户传递的重置时间大于零,则在点击之后会设置按钮的 disabled 属性为 true 禁用按钮,并在重置时间后解除按钮禁用,然后打印出当前逻辑触发是来自哪个框架,并向外抛出 click 点击事件;
(2)clearTimer:调用 clearTimer 方法可以快速清除组件的 timer 定时器。
具体内容如下所示:
export const handleClick =({ emit, props, state, framework }) =>(event) => {if (props.nativeType === 'button' && props.resetTime > 0) {state.disabled = truestate.timer = setTimeout(() => {state.disabled = false}, props.resetTime)}console.log(`${framework}框架代码已触发!!!!!!!!!`)emit('click', event)}export const clearTimer = (state) => () => clearTimeout(state.timer)
四、创建模板层去对接 common 适配层和 renderless 无渲染层
由于需要创建的文件太多,为了方便操作,可以直接参考我们提供的示例源码工程查看 (https://github.com/opentiny/cross-framework-component/tree/master/packages/components/react/src )
React 具体的目录结构如下:
├─ 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
(https://github.com/opentiny/cross-framework-component/tree/master/packages/components/solid/src)
Solid 具体的目录结构如下:
├─ solid
│ ├─ common # solid适配层
│ │ ├─ package.json
│ │ └─ src
│ │ ├─ index.js
│ ├─ index.js
│ ├─ package.json
│ ├─ README.md
│ ├─ README.zh-CN.md
│ └─ src
│ ├─ button # solid框架button组件的模板层
│ │ ├─ package.json
│ │ └─ src
│ │ └─ pc.jsx
│ └─ countdown # solid框架倒计时组件的模板层
│ ├─ package.json
│ └─ src
│ └─ pc.jsx
这里创建的模板层和一般的 React 和 Solid 函数式组件类似,都是接受使用组件的用户传递过来的属性,并返回需要渲染的 jsx 模板。不一样的地方是:jsx 绑定的数据是通过适配层和 renderless 无渲染层处理后的数据,并且数据发生变化的时候会触发视图渲染,比如下面代码中 useSetup 方法。
pc.jsx 的具体实现如下所示(React 路径:packages/components/react/src/button/src/pc.jsx):
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// 通过common适配层的useSetup处理props和renderless无渲染层const { handleClick, state, tabindex, type, $attrs } = useSetup({props: { nativeType: 'button', resetTime: 1000, ...props },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 (<buttonclassName={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>)
}
(Solid 路径:packages/components/solid/src/button/src/pc.jsx):
import renderless from '@opentiny/renderless/button/solid' // renderless无渲染层
import { useSetup } from '@opentiny/solid-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// 通过common适配层的useSetup处理props和renderless无渲染层const { handleClick, state, tabindex, type, $attrs } = useSetup({props: { nativeType: 'button', resetTime: 1000, ...props },renderless})// 这里需要注意在模板中需要调用state函数才能正常使用solid的响应式能力return (<buttonclassName={['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()}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 克隆到本地:
git clone https://github.com/opentiny/cross-framework-component.git
2、使用 pnpm 下载依赖:
pnpm i# 如果没有pnpm需要执行以下命令
npm i pnpm -g
3、工程目录结构分析
整个工程是基于 pnpm 搭建的多包 monorepo 工程,演示环境为无界微前端环境,整体工程的目录架构如下所示(本文主要介绍 packages/components 文件夹):
├─ 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 🌟支持下:https://github.com/opentiny/tiny-vue
关于 OpenTiny

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架、跨版本的 TinyVue 组件库,包含基于 Angular+TypeScript 的 TinyNG 组件库,拥有灵活扩展的低代码引擎 TinyEngine,具备主题配置系统TinyTheme / 中后台模板 TinyPro/ TinyCLI 命令行等丰富的效率提升工具,可帮助开发者高效开发 Web 应用。
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~更多视频内容也可关注B站、抖音、小红书、视频号
OpenTiny 也在持续招募贡献者,欢迎一起共建
OpenTiny 官网:https://opentiny.design/
OpenTiny 代码仓库:https://github.com/opentiny/
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~
如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~
相关文章:
原来 TinyVue 组件库跨框架(Vue2、Vue3、React、Solid)是这样实现的?
本文由 TinyVue 组件库核心成员郑志超分享,首先分享了实现跨框架组件库的必要性,同时通过演示Demo和实际操作向我们介绍了如何实现一个跨框架的组件库。 前言 前端组件库跨框架是什么? 前端组件库跨框架是指在不同的前端框架(如…...
自定义label组件
自定义label组件 支持边框绘制 支持shape背景(按指定圆角裁剪,矩形,圆角矩,圆形),支持指定角圆角 支持自定义阴影(颜色,偏移,深度) 边框颜色支持状态选择器 预览 核心绘制辅助类 public class LabelHelper {private final Paint paint;private Paint shadowPaint;private fina…...
【Linux】使用Makefile自动化编译项目:简化开发流程、提高效率
文章目录 示例一:编译一个进度条程序示例二:编译一个简单的程序gcc的几个选项结论 当你开始一个新的软件项目时,编写一个好的Makefile是非常重要的。Makefile是一个文本文件,用于指定如何构建和编译项目。它定义了目标文件、依赖关…...
浅谈开源和闭源的认知
目录 在大型模型的发展中,开源和闭源两种截然不同的开发模式扮演着关键的角色。开源模式通过促进技术共享,吸引了大量优秀人才的加入,从而推动了大模型领域的不断创新。与此相反,闭源模式则着重于保护商业利益和技术优势ÿ…...
你了解Postman 变量吗?
变量是在Postman工具中使用的一种特殊功能,用于存储和管理动态数据。它们可以用于在请求的不同部分、环境或集合之间共享和重复使用值。 Postman变量有以下几种类型: 1、环境变量(Environment Variables): 环境变量是在Postman…...
ArmSoM-RK3588编解码之mpp编码demo解析:mpi_enc_test
一. 简介 [RK3588从入门到精通] 专栏总目录mpi_enc_test 是rockchip官方编码 demo本篇文章进行mpi_enc_test 的代码解析,编码流程解析 二. 环境介绍 硬件环境: ArmSoM-W3 RK3588开发板 软件版本: OS:ArmSoM-W3 Debian11 三. …...
【ES6.0】-详细模块化、export与Import详解
【ES6.0】-详细模块化、export与Import详解 文章目录 【ES6.0】-详细模块化、export与Import详解一、模块化概述二、ES6模块化的语法规范三、export导出模块3.1 单变量导出3.2 导出多个变量3.3 导出函数3.4 导出对象第一种第二种: 3.5 类的导出第一种第二种 四、imp…...
网工内推 | Base北京,国企网工运维,最高30k*14薪,IE认证优先
01 万方数据股份有限公司 招聘岗位:网络工程师 职责描述: 1.负责完成基础网络组网工作; 2.负责网络对象的访问控制及安全策略,配置VLan,黑白名单、地址转换、故障排查及网络安全监控工作; 3.负责对操作系…...
SQL LIKE 运算符:用法、示例和通配符解释
SQL中的LIKE运算符用于在WHERE子句中搜索列中的指定模式。通常与LIKE运算符一起使用的有两个通配符: 百分号 % 代表零个、一个或多个字符。下划线 _ 代表一个单个字符。 以下是LIKE运算符的用法和示例: 示例 选择所有以字母 “a” 开头的客户&#x…...
编译原理Lab1-用FLEX构造C-Minus-f词法分析器
HNU编译原理lab1实验–根据cminux-f的词法补全lexical_analyer.l文件,完成词法分析器。 本文没有添加任何图片,但是以复制输出的形式展现出来了实验结果。 实验要求: 根据cminux-f的此法补全lexical_analyer.l文件,完成词法分析…...
网络安全之渗透测试入门准备
渗透测试入门所需知识 操作系统基础:Windows,Linux 网络基础:基础协议与简单原理 编程语言:PHP,python web安全基础 渗透测试入门 渗透测试学习: 1.工具环境准备:①VMware安装及使用;…...
【MySQL】宝塔面板结合内网穿透实现公网远程访问
文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar3.2 创建HTTP隧道4.远程连接5.固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单几步,通过宝塔面板cpo…...
通过AX6000路由器,实现外部访问内网的任意主机
概述 这里遇到一个场景,就是需要外部的人员,访问我内网的一台设备,进行内外部的设备联调。 这也是实际环境中,很常见的一种场景。 之前的做法是子设备上运行edge节点,可以直接访问。 但有的设备无法运行edge节点,那么可以参考一下这个方案来实现。 此方案可以摒弃了…...
如何应用ChatGPT撰写、修改论文及工作报告,提供写作能力及优化工作??
如果我想让gpt从pdf文档中提取相关关键词的内容,可以怎么做呢??我们评论区讨论 ChatGPT 在论文写作与编程方面也具备强大的能力。无论是进行代码生成、错误调试还是解决编程难题,ChatGPT都能为您提供实用且高质量的建议和指导&am…...
camera-caps:Jetson设备上的一种实用的V4L2可视化界面
camera-caps:Jetson设备上的一种实用的V4L2可视化界面 github地址是: https://github.com/jetsonhacks/camera-caps 注意:Jetpack5.x需要选择tag 5.x版本...
CAN基础知识
CAN 简介 CAN 是 Controller Area Network 的缩写(以下称为 CAN),是 ISO 国际标准化的串行通信 协议。在当前的汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种 各样的电子控制系统被开发了出来…...
vue3跨域怎么解决?
其实很简单 假设一个接口; http://101.42.170.68:10000/open/mockData/test1 首先,看自己项目中有没有vue.config.js文件,如果没有自己创建一个,如果有那吗在其中写。 vue.config.js: //固定格式,修改一部分就行了 const { def…...
强化学习小笔记 —— 如何选择合适的更新步长
在强化学习中,动作价值函数的更新可以使用增量法,如下所示: Q k 1 k ∑ i 1 k r i 1 k ( r k ∑ i 1 k − 1 r i ) 1 k ( r k ( k − 1 ) Q k − 1 ) 1 k ( r k k Q k − 1 − Q k − 1 ) Q k − 1 1 k [ r k − Q k − 1 ] \beg…...
容斥 C. Strange Function改编题
补题: 题目详情 - 9.段坤爱取模%%% - SUSTOJ 本题或许是参考 Problem - C - Codeforces 根据题意,f(i)就是不能被整除的最小的一个质因子。 打表发现,当15个质因子相乘后,长度就大于18。 因此可以知道小于等于1e16内的正整数x…...
C++笔记
文章目录 类模板类函数什么是友元函数?什么是内联函数?VECTOR哈希表栈队列映射与解除映射mmap()munmap可变参数 va_start()-va_send()vsnprintf()C/C++异常处理list红黑树类 基类、父类、顶层类、抽象类 子类、派生类 模板类 在C++中,模板类(Template Class)是一种通用…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
Bean 作用域有哪些?如何答出技术深度?
导语: Spring 面试绕不开 Bean 的作用域问题,这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开,结合典型面试题及实战场景,帮你厘清重点,打破模板式回答,…...
pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)
目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 (1)输入单引号 (2)万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...
