提前说声:不是 Web Component 方式实现的!!!
文章结尾附上源码地址
开篇:当我们在谈论跨框架组件时,究竟在解决什么?
在Vue、React、Angular三分天下的今天,你是否经历过这样的困境?
- 精心开发的组件库因技术栈更迭被迫重构
- 业务需求变更导致多框架版本维护成本激增
- 或是面对新兴框架时陷入重复造轮子的循环
据统计,超过67%的前端团队同时维护两个以上技术栈项目,而跨框架组件复用需求正以每年23%的速度增长。
本文将为你揭示一种突破性的解决方案——通过 编译时转换 实现真正意义上的 跨框架组件。不同于传统的Web Components方案,我们将借助 Svelte 的独特优势,打造一个能在React、Vue、Angular等任意框架中即插即用的画板组件。更令人振奋的是,这个方案 已在生产环境稳定运行。
一、跨框架组件的本质探索
1.1 概念重定义:不只是代码复用
跨框架组件绝非简单的代码拷贝复用,其核心在于运行时零依赖与框架特性解耦。真正的跨框架组件应具备:
- 原子化运行能力:不依赖任何框架运行时
- 标准化接口规范:统一的API设计与事件机制
- 自适应渲染机制:自动适配宿主框架生命周期
- 样式沙箱隔离:CSS作用域自动管控
1.2 价值矩阵:从成本控制到架构演进
| 维度 | 传统方案痛点 | 跨框架组件优势 |
|---|---|---|
| 开发成本 | 多版本并行开发 | 单代码库维护 |
| 升级维护 | 版本碎片化严重 | 统一技术债务管理 |
| 团队协作 | 技术栈壁垒阻碍协作 | 标准化组件接口 |
| 技术演进 | 框架绑定风险 | 渐进式架构升级 |
| 性能表现 | 运行时开销叠加 | 编译时优化输出 |
二、方案选型:从Web Components到编译时转换
2.1 传统方案的局限性
Web Components的三大困局:
- 事件系统割裂:需手动注册Custom Events
- 属性传递障碍:复杂对象序列化难题
- 样式污染风险:Shadow DOM的封闭性反噬
开发跨框架的 展示类组件 还行(例如: ),涉及到多事件组件或者暴露组件内部方法等复杂组件就捉襟见肘。
适配器模式的隐藏成本:
- 底层逻辑适配层膨胀
- 框架特性差异导致API妥协
- 版本迭代的连锁更新压力
看着好像只需要关注各框架等渲染层,但是共享底层逻辑因为要适配,所以需要各种兼容,得不偿失!
2.2 Svelte的破局之道
Svelte编译时革命的三大杀器
- 零运行时开销:将组件编译为Vanilla JS,消除框架
运行时包袱 - 原子化打包:按需生成UMD/IIFE/ES模块,
适配任意构建体系 - 原生DOM操控:直接操作原生DOM元素,
规避虚拟DOM的性能损耗
// 核心架构示意图
[ Svelte组件 ] -> [ Svelte编译器 ] -> [ Vite打包: 纯净JS Bundle ] -> [ 任意框架环境 ]
三、实战:跨框架画板组件开发全流程
3.1 组件展示环节
线上Vue项目引入使用的实例!
方法一:使用 npm/yarn安装,各框架中实例化使用
npm install usb-paint
import USBPaint from "usb-paint";
new USBPaint({
target: document.querySelector("#root"),
});
方法二:使用 CDN 直接插入到 HTML,实例化使用
<script src="<https://cdn.jsdelivr.net/npm/usb-paint@latest/dist/usbpaint.iife.js>" type="text/javascript"></script>
<script>
let a = new USBPaint({
target: document.querySelector("#root"),
});
</script>
3.2 核心实现解析
3.2.1 框架适配层
通过类封装实现框架无关实例化(core.ts)
import Core from "./core.svelte";
import type { SvelteComponent } from "svelte";
import type { USBPaintOptionsType } from "./core.interface";
export class USBPaint {
public option: USBPaintOptionsType = {};
protected mainInstance: SvelteComponent | undefined;
constructor(opt?: USBPaintOptionsType) {
// 接受参数
this.option = opt || {};
this._initRun();
}
// init || 初始化
private _initRun(): void {
// 节点
let target = document.querySelector(this.option.target) as HTMLElement
// 设置参数
···
// 挂载并实例化 Svelte实现的画板功能组件(import Core from "./core.svelte";)
this.mainInstance = new Core({
target: target, // 挂载的DOM节点
props: {
parentDom: target,
scale: this.option.scale || 10,
···
} // 传递参数
});
}
}
3.2.2 画板功能实现
在具体功能实现组件(core.svelte)中接收参数,实例化canvas,实现画板的各种功能()
-
接收参数(通过export修饰变量直接接收core.ts实例化组件过程中放到props中的参数)
<script lang="ts"> ··· export let scale: number = 10; export let parentDom: HTMLElement; </script> -
canvas实例化画板(Canvas渲染优化);
<div style={usbdScale} class="usb-paint-container"> <canvas id="usb-paint-canvas" style="touch-action: none;"></canvas> </div> <style lang="scss"> .usb-paint-container { height: 100%; width: 100%; } </style><script lang="ts"> onMount(() => { canvasEl = document.getElementById("usb-paint-canvas") as HTMLCanvasElement; if (canvasEl) { // 实例化canvas元素 ctx2d = canvasEl.getContext("2d") as CanvasRenderingContext2D; transRatio(canvasEl, ctx2d); } }); // Canvas渲染优化:设置canvas的分辨率,使线条清晰 let transRatio = (cvs: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => { const width = parentDom.offsetWidth; const height = parentDom.offsetHeight; const ratio = window.devicePixelRatio || 1; cvs.width = width * ratio; cvs.height = height * ratio; cvs.style.width = width + "px"; cvs.style.height = height + "px"; ctx.scale(ratio, ratio); }; </script> -
实现绘画、橡皮擦、删除、导出图片等功能;
-
画笔工具(笔触算法优化)
import { getStroke } from "perfect-freehand"; // 画笔工具 const freedomPaint = () => { if (canvasEl && ctx2d) { colorBrightness = calcColorBrightness(canvasEl, ctx2d); let points: number[][] = []; ctx2d.globalCompositeOperation = "source-over"; // 设置绘制样式 ctx2d.fillStyle = "xxx"; ctx2d.lineCap = "round"; ctx2d.lineJoin = "round"; ctx2d.imageSmoothingEnabled = true; // 开始绘制 canvasEl.addEventListener("pointerdown", e => { e.preventDefault(); points = [[e.offsetX, e.offsetY]]; // 捕获指针事件,以便在指针移出画布时仍然能继续绘制 canvasEl?.setPointerCapture(e.pointerId); }); // 绘制过程 canvasEl.addEventListener("pointermove", e => { e.preventDefault(); points.push([e.offsetX, e.offsetY]); // 笔触算法优化:手绘风格画笔样式设置 const pathData = getStroke(points, { simulatePressure: true, size: isEraser ? eraserLineWidth : lineOptions.lineWidth, // 线条宽度 smoothing: lineOptions.lineSmooth, // 平滑度 thinning: lineOptions.lineThin, // 线条随压力变化 streamline: lineOptions.lineStream, // 线条流畅度 start: { taper: lineOptions.lineStart }, // 开始处圆滑过渡 end: { taper: lineOptions.lineEnd }, // 结束处圆滑过渡 easing: t => Math.sin((t * Math.PI) / 2), // 缓动函数 }); ctx2d.beginPath(); pathData.forEach(([x, y], index) => { if (index === 0) { ctx2d.moveTo(x, y); } else { ctx2d.lineTo(x, y); } }); ctx2d.fill(); ctx2d.closePath(); }); // 停止绘制 canvasEl.addEventListener("pointerup", e => { e.preventDefault(); canvasEl?.releasePointerCapture(e.pointerId); // 释放指针捕获 }); // 离开画布时停止绘制(可选,防止异常情况下未释放) canvasEl.addEventListener("pointerleave", e => { e.preventDefault(); }); } }; -
橡皮擦工具
// 橡皮工具 const eraserTool = () => { ctx2d.globalCompositeOperation = "destination-out"; }; -
删除工具
ctx2d.clearRect(0, 0, canvasEl.width, canvasEl.height); -
导出图片工具
// 导出为图片 const exportToFile = (canvas: HTMLCanvasElement) => { if (canvas) { let imgType = "image/png"; const imageData = canvas.toDataURL(imgType); return imageData; } else { return ""; } };
-
3.2.3 导出和多格式打包
-
组件导出供第三方实例化使用(main.ts)
// polyfill import "core-js/stable/symbol"; import "core-js/stable/promise"; import { USBPaint } from "./core.ts"; // export export default USBPaint; -
vite打包构建(vite.config.ts)
使用vite进行打包构建,设置打包的时候生成多格式js,满足不同引入的要求
import { defineConfig } from "vite"; ··· export default defineConfig({ ··· build: { minify: "terser", lib: { entry: resolve(__dirname, "./src/main.ts"), // 入口文件,根据你的项目路径调整 name: "USBPaint", fileName: "usbpaint", formats: ["umd", "iife", "es"], // 使用 IIFE 格式生成一个自包含的文件 }, rollupOptions: { // input: { // main: resolve(__dirname, "./src/main.ts"), // }, output: { inlineDynamicImports: true, // 确保动态导入也在同一个文件中 }, }, }, });运行
npm run build进行编译打包,生成"umd", "iife", "es"三种格式的纯javascript代码,最后发布的 npm 进行管理,大功告成!
当然,使用Svelte方案也是有局限性的,他开发的框架构组件无法像Web Components组件一样标签化使用,每次使用需要先实例化。
终极总结:跨框架组件的破局之道
通过本次实战,我们验证了编译时方案的关键优势:
✅ 真·零依赖:最终产物为纯净JS
✅ 开发友好:保留现代框架的开发体验
✅ 性能卓越:超越传统方案的渲染效率
✅ 生态兼容:无缝接入现有构建工具链
当你在为多框架支持苦恼时,不妨尝试这种**「一次编译,处处运行」**的新范式。这不仅是一个组件的实现方案,更是一种打破技术壁垒的思维方式。
技术没有银弹,但有最优解。选择能最大化技术投资回报的方案,才是工程智慧的体现。