在 Preact 中使用 @lit/react 包装 Web Components:examples/preact 示例工程深度剖析

发布时间:2026/9/13 12:26:43
在 Preact 中使用 @lit/react 包装 Web Components:examples/preact 示例工程深度剖析 在 Preact 中使用 lit/react 包装 Web Componentsexamples/preact 示例工程深度剖析【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit导读lit/react的createComponent()可以把 Lit 自定义元素包装成可在 React JSX 中直接使用的 React 组件。本文以本仓库 examples/preact 示例工程为主体完整讲解这套「React 组件 Preact 运行时」的组合方案既给出可直接运行的 Vite Preact 工程骨架又从源码层面剖析createComponent()如何区分属性与事件、如何完成类型安全的 JSX 接入并介绍该工程同时承担的类型兼容性回归测试职责。读完本文你将掌握在 Preact 项目中正确引入、使用并验证lit/react包装组件的方法以及背后的类型系统设计。一、示例工程概述它解决什么问题examples/preact/README.md 对工程定位的描述非常精炼核心信息有两点演示用途展示由lit/react的createComponent()创建的 React 组件如何在 Preact 项目中被使用测试用途该工程同时作为测试项目用来确保包装后的组件与 Preact 保持兼容This also serves as a test project to make sure wrapped components remain compatible with Preact。这意味着examples/preact不是孤立的教学 Demo而是 packages/reactlit/react包在 Preact 这一「非 React」渲染环境下正确性验证的一环。它关注两个层次的问题运行时兼容Preact 能否像 React 一样渲染自定义元素并驱动其属性与事件类型系统兼容lit/react导出的组件类型基于 React 类型定义能否在 Preact 的 JSX 类型环境中通过编译。二、工程结构速览examples/preact是一个精简到极致的 Vite 工程全貌如下examples/preact/ ├── index.html # HTML 入口挂载 div idapp ├── package.json # 依赖与 wireit 测试脚本 ├── src/ │ └── index.tsx # 应用入口使用包装组件渲染 App / ├── tsconfig.json # 关键jsxImportSource 指向 preactreact 路径映射到 preact/compat ├── vite.config.ts # preact/preset-vite 插件 └── CHANGELOG.md入口 HTMLindex.html只做三件事声明字符集与视口、提供一个idapp的空容器、通过script typemodule加载 src/index.tsx。这是一个标准的 Preact Vite 挂载点。工程依赖见 package.json有两个关键项依赖版本范围作用preact^10.15.1Preact 运行时lit-internal/test-elements-react^1.0.1预先用createComponent()包装好的测试组件包开发依赖preact/preset-vite^2.5.0与vite^4.3.2用于构建与开发服务器vite.config.ts只注册了 preact 插件一行核心配置export default defineConfig({ plugins: [preact()], });三、入口源码包装组件在 Preact JSX 中的三种用法src/index.tsx 是整个示例的核心它把三类由lit/react包装的组件放进同一个 Preact 应用中渲染正好覆盖了日常开发中最典型的三种场景。3.1 基本属性与插槽ElementAimport {ElementA} from lit-internal/test-elements-react/element-a.js; ElementA foofoo onAChanged{() {}} This goes in default slot div slotstuffThis goes in stuff slot/div /ElementA这里展示了自定义元素的三个特性如何穿过 React/Preact 包装层字符串属性foofoo作为 prop 传入最终被createComponent设置为元素属性property而非 HTML 属性attribute事件监听onAChanged对应element-a元素派发的a-changed事件插槽内容默认插槽文本与slotstuff的命名插槽子元素经children透传给原生元素。ElementA的包装定义位于 packages/labs/test-projects/test-elements-react/src/element-a.tsexport const ElementA createComponent({ react: React, tagName: element-a, elementClass: ElementAElement, events: { onAChanged: a-changed as EventNameCustomEventunknown, }, });3.2 事件类型精确化ElementEventsElementEvents foofoo onStringCustomEvent{(e: CustomEventString) { console.log(e); }} onNumberCustomEvent{(e: CustomEventNumber) { console.log(e); }} onMyDetailCustomEvent{(e: CustomEventMyDetail) { console.log(e); }} onEventSubclass{(e: EventSubclass) { console.log(e); }} onSpecialEvent{(e: SpecialEvent) { console.log(e); }} /这段代码验证的是lit/react的EventNameT类型系统事件回调的参数类型由createComponent的events映射中as EventName...的类型断言决定而非一律退化为Event。CustomEventString、CustomEventNumber、CustomEventMyDetail、EventSubclass、SpecialEvent这些不同的事件载荷类型都能被完整保留并参与类型检查。底层实现见 packages/react/src/create-component.ts 中的类型推导链export type EventNameT extends Event Event string { __eventType: T; }; type EventListenersR extends EventNames { [K in keyof R]?: R[K] extends EventName ? (e: R[K][__eventType]) void : (e: Event) void; };即映射值若是EventNameT回调参数类型取T否则回退为Event。这正是 packages/react/README.md 中「Non-casted event names will fallback to an event type ofEvent」的源码依据。3.3 复杂数据属性ElementPropsElementProps aStraStr aNum{-1} aBool{false} aStrArray{[a, b]} aMyType{{ a: a, b: -1, c: false, d: [a, b], e: isUnknown, strOrNum: strOrNum, }} /这里验证的是包装组件对非字符串数据类型的透传数字aNum、布尔aBool、字符串数组aStrArray、复杂对象aMyType全部作为 property 直接赋值给元素实例而不是被序列化成 attribute 字符串。这正是 React 直接渲染自定义元素时做不到、必须借助createComponent才能获得的能力packages/react/README.md 明确说明了这一点。ElementProps的包装定义见 packages/labs/test-projects/test-elements-react/src/element-props.ts。3.4 类型负向测试ts-expect-error{/* ts-expect-error bar is not a valid prop */} ElementA barbar /这一行是示例工程中最容易被忽略、却最能体现其「类型回归测试」使命的代码。ElementA的合法属性只有foo与onAChanged传入不存在的bar应产生类型错误ts-expect-error要求 TypeScript必须在这一行报错否则tsc反而会失败。它保证包装组件的 props 类型不是宽泛的any而是由元素类属性与事件映射精确推导出的封闭集合。最后render(App /, document.getElementById(app))使用 Preact 的render函数完成挂载取代了 React 的createRoot/render调用。四、类型兼容的关键tsconfig 中的 Preact 映射examples/preact/tsconfig.json 是让整个方案在类型层面成立的核心配置{ compilerOptions: { target: ES2020, module: NodeNext, moduleResolution: NodeNext, noEmit: true, allowJs: true, checkJs: true, jsx: react-jsx, jsxImportSource: preact, skipLibCheck: true, paths: { react: [../../node_modules/preact/compat/src/index.d.ts] } }, include: [./node_modules/vite/client.d.ts, src/**/*] }逐项拆解它对兼容性的贡献jsx: react-jsxjsxImportSource: preact让 TSX 转译使用 Preact 的 JSX 运行时与 JSX 类型h/Fragment等而不是 React 的paths中react→preact/compat的类型声明这是最关键的一步。lit/react生成的组件类型如React.ForwardRefExoticComponent基于 React 的类型定义通过路径映射让import * as React from react解析到 Preact 兼容层preact/compat的类型从而让 React 风格组件类型与 Preact 的 JSX 环境相互兼容。文件头注释src/index.tsx 顶部也明言此文件用于验证「React components made withlit/reactcan be used in Preact projectswithout any type errors」noEmit: true本工程只做类型检查、不产出编译结果构建由 Vite 完成allowJs/checkJs允许并对 JS 文件做类型检查与lit-internal/test-elements-react包内的类型来源协作。五、作为测试工程wireit 与 tsc --noEmitexamples/preact的测试定位最终落实在 package.json 的wireit配置上wireit: { test: { dependencies: [test:ts] }, test:ts: { command: tsc --noEmit, dependencies: [../../packages/labs/test-projects/test-elements-react:build], files: [src/**/*], output: [] } }要点如下npm test经由 wireit 调度实际执行tsc --noEmit即对 src/index.tsx 做全量类型检查验证前文所有 JSX 用法含ts-expect-error都能通过编译test:ts声明了对../../packages/labs/test-projects/test-elements-react:build的依赖即先构建测试元素包再检查消费方源码保证类型检查基于最新构建产物files/output声明使 wireit 可以增量判断是否需要重新执行。因此任何对lit/react类型定义的改动只要破坏了与 Preact 的兼容性都会在这一步的tsc --noEmit中暴露。这就是「包装组件与 Preact 保持兼容」这一回归承诺的自动化保障。工程自身的变更记录见 examples/preact/CHANGELOG.md0.0.1 版本随lit-internal/test-elements-react1.0.1更新。六、底层原理createComponent 如何在 Preact 下工作示例工程的效果来自 packages/react/src/create-component.ts 的实现。它在运行时把「React/Preact props」拆分为两类reservedReactProperties之外的处理逻辑for (const [k, v] of Object.entries(props)) { if (reservedReactProperties.has(k)) { // className 转为 class其余如 ref/children 交给框架处理 reactProps[k className ? class : k] v; continue; } if (eventProps.has(k) || k in elementClass.prototype) { elementProps[k] v; // 元素属性 / 事件回调稍后直接赋值给元素 continue; } reactProps[k] v; // 其余交给框架作为普通 JSX 属性 }随后在useLayoutEffect中通过setProperty把elementProps应用到真实 DOM 节点事件名经events映射后走addOrUpdateEventListener维护WeakMapElement, Mapstring, EventListenerObject每个事件仅注册一个监听器并复用见 create-component.ts非事件属性则直接执行node[name] value赋值为 property当属性值变为undefined/null且该名字存在于HTMLElement.prototype时还会移除同名 attribute 以复刻 React 的清理行为create-component.ts。注意示例中的 Preact 路径与 React 路径共享同一份createComponent实现。由于lit/react的返回值是基于 React 类型构造的组件在 Preact 中运行时依赖preact/compat兼容层tsconfig 的paths映射正是为此服务这也是为什么本示例工程同时要承担类型回归验证职责。七、本地运行与验证工程不依赖额外安装步骤仓库为只读仅介绍运行方式。在仓库根目录安装好依赖后可进入 examples/preact 目录执行npm run dev # 启动 Vite 开发服务器打开浏览器观察页面 npm run build # 生产构建 npm run preview # 预览构建产物 npm test # 经由 wireit 执行 tsc --noEmit 类型回归测试其中npm test与 CI 中执行的是同一套检查是确认「包装组件与 Preact 兼容」最直接的验证手段。八、小结examples/preact示例工程用最少的文件回答了lit/react集成方案中的一个关键问题React 风格包装组件能否在 Preact 中既编译通过、又正确运行。答案分为三层运行时createComponent在useLayoutEffect阶段把属性赋值为 property、把事件注册为监听器Preact 的渲染流程可以无缝驱动类型层jsxImportSource: preact配合paths将react映射到preact/compat让 React 类型的包装组件落入 Preact 的 JSX 类型环境回归保障ts-expect-error负向用例与wireit调度的tsc --noEmit测试使兼容性承诺可持续验证。对于希望在 Preact 生态中复用 Lit Web Components 的开发者可直接参照 src/index.tsx 的三种用法起步若需深入包装细节可继续阅读 packages/react/src/create-component.ts 与 packages/react/README.md。【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考