首页 > 其他分享 >OpenTiny Vue组件库实现跨框架(vue2、vue3、react、solid)

OpenTiny Vue组件库实现跨框架(vue2、vue3、react、solid)

时间:2023-09-12 21:02:31浏览次数:42  
标签:Vue 框架 solid renderless react vue 组件 opentiny

本文由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环境下运行

mkdir cross-framework-component

cd 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": {
      "[email protected]": {
        "peerDependencies": {
          "vue": "2.6.14"
        }
      },
      "@opentiny/[email protected]": {
        "peerDependencies": {
          "vue": "2.6.14"
        }
      },
      "@opentiny/[email protected]": {
        "peerDependencies": {
          "vue": "2.6.14"
        }
      }
    }
  }
}

3、在根目录创建pnpm-workspace.yaml文件并配置如下:

packages:
  - packages/**    # packages文件夹下所有包含package.json的文件夹都是子包

4、创建组件源代码目录

cd packages
mkdir components

二、 创建react框架的common适配层

将整个工程创建好之后,我们需要抹平不同框架之间的差异,这样才能实现一套代码能够去支持不同的框架,那如何来抹平不同框架之间的差异呢?这里出现一个重要概念--common适配层 。它用来对接纯函数renderless无渲染逻辑层。

下面以react框架为例详细介绍如何构造react框架的common适配层(solid、vue的原理可以类比)

1、在上文创建的components文件夹中创建react文件夹,并初始化package.json

mkdir react
cd react
npm init -y

package.json的内容主要是把dependencies项中@opentiny/react-button和@opentiny/react-countdown两个依赖指向本地组件包,这是pnpm提供的本地包加载方式。

具体的配置如下所示:

{
  "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)

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具体内容如下所示:

{
  "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)

mkdir src
cd src
touch index.js

具体的目录结构如下:

├─ 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):

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

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为例,采用无渲染逻辑的复用方式

首先看下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是@opentiny/react-button组件的renderless入口文件,它负责去对接react的适配层@opentiny/react-common,主要功能是去调用一些react相关的api,比如生命周期函数等,在renderless函数最后返回了state响应式对象和一些方法,提供给react的函数式组件使用。

文件主要有两个需要注意的点:

(1)使用common适配层传递过来的useReactive函数返回基于react的响应式数据,对齐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
}

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 = true
      state.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
│  ├─ 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):

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克隆到本地:

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个子工程分别引入了不同框架的组件库,但是不同框架的组件库复用了同一份交互逻辑代码和样式文件。

效果如下图所示:

OpenTiny Vue组件库实现跨框架(vue2、vue3、react、solid)_json

如何证明vue2、vue3、react、solid都共用了一套逻辑了呢?

我们可以点击按钮然后会在控制台打印,当前复用逻辑层是来自哪个框架的:

OpenTiny Vue组件库实现跨框架(vue2、vue3、react、solid)_json_02

可以看到不同框架代码都已触发。

感兴趣的朋友可以持续关注我们TinyVue组件库。也欢迎给 TinyVue 开源项目点个 Star

标签:Vue,框架,solid,renderless,react,vue,组件,opentiny
From: https://blog.51cto.com/u_16152776/7448274

相关文章

  • vue3.*安装axios具体步骤
    在项目的命令行处使用命令进行axios的安装npminstallaxiosvue-axios--legacy-peer-deps--save其余的命令可能会报错;......
  • 基于vue制作的动画组件loading起来
    ......
  • vue实现动态导航栏的设置
    1、点击某个导航栏即切换到某个页面1、为el-menu标签加上router属性2、在页面中添加router-view标签,动态渲染我们自己选择的router3、el-menu-item标签的index值即为要跳转的页面地址呈现效果:2、为页面设置选中状态--此时点击选中是有状态的,但是初始化的时候,就不会有什......
  • React框架下如何集成H.265网页流媒体EasyPlayer.js视频播放器?
    H5无插件流媒体播放器EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器,可支持多种流媒体协议播放,可支持H.264与H.265编码格式,性能稳定、播放流畅,能支持WebSocket-FLV、HTTP-FLV,HLS(m3u8)、WebRTC等格式的视频流。在功能上,EasyPlayer支持直播、点播、录像、快照截图、MP4播放......
  • react native项目安装需求
    1.创建RN项目2.安装路由依赖3.redux需求安装redux依赖4.第三方组件库5.打包及各种其他操作 安装指定依赖注意:**link**链接库已在RN(reactnative)0.60版本开始支持自动链接,不需要再手动link!!! ###2.安装路由依赖#####安装所需包npminstall@react-navigation/native##......
  • vue 嵌套全屏iframe 能有效避开返回两次才能返回上一个路由的问题
    <template> <divclass="home">  <iframeref="iframe"class="iframe"frameborder="no"></iframe> </div></template><script>import{ get_doctor_info, statistics, ......
  • vue 学习
    1.给对象动态添加属性和值varobj={   name:"jack",   age:"18"}第一种:Vue.set(obj,'sex','18');第二种:this.$set(this.obj,'score',90);第三种:obj.score=100;直接赋值的方式不能触发Vue的响应式系统。如果我们需要在模板中使用动态添加的属性,建议使用Vue.set......
  • Vue3中使用pinia全局状态管理库
    ❝本期介绍一下pinia在vue3中的简单使用,以及如何使用pinia实现多页面状态共享。❞什么是piniaPinia是一个用于Vue应用程序的轻量级状态管理库。与vuex的区别pinia是vue3的官方状态管理库,vuex是vue2的官方状态管理库pinia更加轻量级pinia能更好的配合Vue3与TSpinia的使用比Vuex简......
  • docker部署springboot+vue项目环境安装及部署流程
    后端项目打jar包修改及配置项1、修改配置文件application-prod.yml中的mysql配置和redis配置2、切换Maven为生产模式3、Maven打包4、拿到打包后的jar 包5、拿到jar包之后在Linux中使用nohupjava-jarjeecg-system-start-3.5.0.jar>catalina.out2>&1&命令即可启动项......
  • Ant Design Vue Table 嵌套子表格的数据刷新方法
    父子组件各自负责,在table中嵌套了子表格后,首次加载表格时,父组件会实例化子组件并传递参数,折叠后再次展开时,只会传递参数,子组件的数据刷新就属于子表格了。如@@@code<template#expandedRowRender="{record}"><originIndexs......