前端跨框架组件解决方案

跨框架组件的开发,是一个兼顾用户体验(UX)开发者体验(DX)的重要问题。随着前端技术生态的快速发展,如何开发能够适配不同框架的组件已成为UI一致性、业务多技术栈需求以及渐进式重构中的重要挑战。

一、跨框架组件的使用场景

1. 大型业务的多技术栈需求

大型业务企业的前端团队可能使用不同的技术栈(如 React 和 Vue)。通过跨框架组件库,可以实现组件复用,减少重复开发的成本,同时保持设计一致性。

2.UI交互的一致性

一个成功的设计系统需要在不同框架和平台中提供一致的用户体验。跨框架组件能确保设计一致性和交互统一,从而提高用户满意度。

3.第三方库或插件

第三方组件库(如日期选择器、图表库等)需要适配多种框架以覆盖更广泛的用户群体,提升组件的可复用性。

4.渐进式重构

在技术栈迁移过程中(如从 AngularJS 迁移到 React),跨框架组件可以帮助团队在不同框架之间平滑过渡,同时兼容旧框架和新框架。

二、解决方案

在实际开发中,有以下四种常见方案可以帮助我们实现跨框架组件的目标:

1. Web Components

Web Components 是浏览器原生支持的技术,它由 Shadow DOM、Custom Elements 和 HTML Templates 组成,可以在任何框架中使用。

优点

  • 原生支持,无需依赖框架。
  • 跨框架兼容性好。
  • 可封装复杂逻辑并隐藏实现细节。

缺点

  • 与框架的状态管理和事件机制集成较复杂。
  • 学习曲线较高,部分 API 浏览器兼容性有限。

代码示例

class MyButton extends HTMLElement {
  // 监听 label 属性变化
  static get observedAttributes() {
    return ['label'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // 创建样式
    const style = document.createElement('style');
    style.textContent = `
        button {
          background-color: var(--button-bg-color, #007bff);
          color: var(--button-text-color, white);
          border: none;
          border-radius: var(--button-border-radius, 4px);
          padding: var(--button-padding, 10px 20px);
          font-size: var(--button-font-size, 16px);
          cursor: pointer;
        }
        button:hover {
          background-color: var(--button-hover-bg-color, #0056b3);
        }
      `;

    // 创建按钮元素
    this.button = document.createElement('button');

    // 添加样式和按钮到 Shadow DOM
    this.shadowRoot.appendChild(style);
    this.shadowRoot.appendChild(this.button);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'label') {
      this.button.textContent = newValue;
    }
  }

  set label(value) {
    this.setAttribute('label', value);
  }

  get label() {
    return this.getAttribute('label');
  }
}

customElements.define('my-button', MyButton);

在 React 中使用:

import { useEffect, useRef } from 'react';
import '../components/UserCard/index.js'

const Home = () => {
    const buttonRef = useRef();

    useEffect(() => {
        const handleClick = () => {
            alert('React handled click!');
        };

        const button = buttonRef.current;
        if (button) {
            button.addEventListener('click', handleClick);
            return () => {
                button.removeEventListener('click', handleClick);
            };
        }
    }, []);

    return <my-button ref={buttonRef} label="React Button"></my-button>;
};

export default Home;

2. Mitosis

官方Slogan:Write components once, run everywhere.

Mitosis 是一个开源工具,它能将 JSX 组件转换为适用于诸如 Angular、React、Qwik、Vue、Svelte、Solid 以及 React Native 等框架的全功能组件。开发者编写一次代码,可以生成多种框架的实现。

Mitosis 有点类似于 Taro 在小程序开发中的存在,一次React或Vue开发和编译后,可以在多个不同的小程序中运行。

优点

  • 一次开发,多框架生成。
  • 集成现有技术栈,降低维护成本。

缺点

  • 生成的代码可能较复杂,不适合高度定制化场景。
  • 需要依赖 Mitosis 的生态, 有自身的语法,有一定的学习成本。

代码示例

编写一个 Mitosis 组件:

import { useState } from "@builder.io/mitosis";

export default function MyComponent(props) {
  const [name, setName] = useState("Steve");

  return (
    <div>
      <input
        css={{
          color: "red",
        }}
        value={name}
        onChange={(event) => setName(event.target.value)}
      />
      Hello! I can run natively in React, Vue, Svelte, Qwik, and many more frameworks!
    </div>
  );
}

可以编译成Vue组件代码:

<template>
  <div>
    <input
      class="input"
      :value="name"
      @change="async (event) => (name = event.target.value)"
    />
    Hello! I can run natively in React, Vue, Svelte, Qwik, and many more
    frameworks!
  </div>
</template>

<script setup>
import { ref } from "vue";

const name = ref("Steve");
</script>

<style scoped>
.input {
  color: red;
}
</style>

编译成 Angular 组件代码:

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";

import { Component } from "@angular/core";

@Component({
  selector: "my-component",
  template: `
    <div>
      <input
        class="input"
        [attr.value]="name"
        (change)="name = $event.target.value"
      />

      Hello! I can run natively in React, Vue, Svelte, Qwik, and many more
      frameworks!
    </div>
  `,
  styles: [
    `
      :host {
        display: contents;
      }
      .input {
        color: red;
      }
    `,
  ],
})
export default class MyComponent {
  name = "Steve";
}

@NgModule({
  declarations: [MyComponent],
  imports: [CommonModule],
  exports: [MyComponent],
})
export class MyComponentModule {}

3. Ark UI

Ark UI 是一个无框架依赖的组件库,提供框架无关的核心逻辑和 API,开发者可以将它使用到 React、Vue 或 Solid 开发中。

优点

  • 提供基础逻辑,适配各框架。
  • 社区支持活跃,文档完善。

缺点

  • 需要手动适配 UI 层。
  • 对新手开发者不够友好。

比较类似的一个UI组件库是 Park UI

官方介绍:Beautifully designed components built with  Ark UI and  Panda CSS that work with a variety of JS frameworks.

4. Zag.js

Slogan:UI components powered by Finite State Machines.

Zag.js 是一个声明式的 UI 组件逻辑框架,专注于逻辑与 UI 分离。它提供了框架无关的状态管理和交互逻辑,开发者可以通过适配器将逻辑集成到 React、Vue、Solid 和 Svelte中。

优点

  • 将复杂组件的逻辑抽象为状态机(State Machines),提供清晰的状态管理和事件处理流程,更易于调试和维护复杂的组件交互。

缺点

  • 有特定的语法,有一定学习成本

代码示例

实现一个跨框架的数字输入组件:

React代码示例:

import * as numberInput from "@zag-js/number-input"
import { useMachine, normalizeProps } from "@zag-js/react"

export function NumberInput() {
    const [state, send] = useMachine(numberInput.machine({ id: "1" }))  
    const api = numberInput.connect(state, send, normalizeProps)
    return (
        <div {...api.getRootProps()}>
            <label {...api.getLabelProps()}>Enter number:</label>
            <div>
                <button {...api.getDecrementTriggerProps()}>DEC</button>
                <input {...api.getInputProps()} />
                <button {...api.getIncrementTriggerProps()}>INC</button>
            </div>
        </div>
    )
}

Solid代码示例:

import * as numberInput from "@zag-js/number-input"
import { normalizeProps, useMachine } from "@zag-js/solid"
import { createMemo, createUniqueId } from "solid-js"

export function NumberInput() {
    const [state, send] = useMachine(numberInput.machine({ id: createUniqueId() }))
    const api = createMemo(() => numberInput.connect(state, send, normalizeProps))
    
    return (
        <div {...api().getRootProps()}>
          <label {...api().getLabelProps()}>Enter number:</label>
          <div>
            <button {...api().getDecrementTriggerProps()}>DEC</button>
            <input {...api().getInputProps()} />
            <button {...api().getIncrementTriggerProps()}>INC</button>
          </div>
        </div>
    )
}

Vue 代码示例:

<script setup>
import * as numberInput from "@zag-js/number-input";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed } from "vue";

const [state, send] = useMachine(numberInput.machine({ id: "1" }));
const api = computed(() =>
  numberInput.connect(state.value, send, normalizeProps)
);
</script>

<template>
  <div ref="ref" v-bind="api.getRootProps()">
    <label v-bind="api.getLabelProps()">Enter number</label>
    <div>
      <button v-bind="api.getDecrementTriggerProps()">DEC</button>
      <input v-bind="api.getInputProps()" />
      <button v-bind="api.getIncrementTriggerProps()">INC</button>
    </div>
  </div>
</template>

三、总结

在实际开发中,选择跨框架组件的实现方式需要根据项目业务特点、团队技术背景和未来的可维护性等方面来权衡。

希望这篇文章能帮助你更好地理解如何开发和使用跨框架组件,并为你的项目选择最优方案!

欢迎关注工种号【码界交流圈】好文第一时间分享不迷路!~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值