在React或Vue应用中集成用Svelte创建的Web组件
04/05/2021 更新 1):
- 修复 React app 中的 bug;
- 增加 bolt 分支,使用 Bolt 对项目进行管理,简单测试可操作如下:
git clone https://github.com/vulcangz/svelte-webcomponent-in-react-vue.git -b bolt test1 cd test1 bolt install bolt svelte bolt vue # 在另一个终端窗口执行 bolt react
前言
近来读到一篇文章——《制作 Web 组件的所有方法》,其中比较了 51 种制作 Web 组件的不同方法的编码风格、包大小和性能。结论有:“GitHub 的 Catalyst 是新的冠军。"“由于出色的摇树优化,Svelte 在很小的库运行时成本下也做得很好。”,等等......来看 "Get" 到的关键点:1)现在浏览器已经很好地支持 Web 组件了;2)很多JS 框架和库支持 Web 组件(以上 51 种);3)有些库的大小竟然比裸 HTMLElement 还要小,这意味着 -- 潜力!感兴趣的可以去了解一下,在这里就不多说了。
上述文章着重描述的是各种方法的具体实现、和横向比较。而对于现实的开发人员而言,可能更关心于 Web 组件的可访问性。换个说法:我用 Svelte 开发的组件,能否正常在 React 或者 Vue 应用中使用?或者反过来使用。带着这个问题,借用了上述文章中的 Svelte 计数器代码,来创建一个名为 “my-counter” 的、实现计数器功能的、简单的 Web 组件,来尝试下在 React 和 Vue 中去使用这个组件。
需求与用例
- 1)Svelte 组件 UI 中包含 3 个按钮和 1 个计数器值显示:点击 ‘+’ 号计数值加 1;点击 '-' 号计数值减 1;点击最右边的自定义按钮触发 'customOnClick' 自定义事件。
- 2)在 React 或 Vue 应用中,引入 Svelte 组件、并在 HTML 页面显示,对于 '+'、'-' 操作,结果应该与原生组件表现一致;同时设置一个计数器,对于 'customOnClick' 自定义事件,设置相应的监听器和回调函数,实现点击 Web 组件的自定义按钮时,操作本应用的 '计数器' 加 1。
- 3)简单比较构建后的捆绑包的大小。
- 4)参考 UI 如下图所示。
![]() |
![]() |
实现过程(Step by step)
以下就如何创建 Svelte Web 组件、如何在 React 应用中集成 Svelte Web 组件、如何在 Vue 应用中集成 Svelte Web 组件分别进行实现,并写出了主要的步骤。对这些方面很熟悉的,就不需要花时间去细看了。我已将代码上传到 GitHub,Repo 地址是:https://github.com/vulcangz/svelte-webcomponent-in-react-vue。如果这篇文章和代码对您有帮助,还请给个'star',谢谢!
1、Web 组件简介
Web 组件 —— 采用一套不同的技术,允许您创建可重复使用的自定义元素,并在您的 Web 应用中使用它们。详细介绍请参见 MDN - Web Components。
2、使用 Svelte 创建 Web 组件
Svelte 组件可以编译成自定义元素(也就是 Web 组件),我们来实际测试一下。示例是一个计数器,基本元素包括3个按钮。创建完成后的组件效果如下所示:
1)使用 Svelte 官方组件模板 创建组件
npx degit sveltejs/component-template svelte-webcomponent
其中 svelte-webcomponent 是你的组件目录。
2)定制生成 Web 组件
a.先修改包名: package.json --> "name": "MyCounter"
b.修改组件名:src\Component.svelte --> src\MyCounter.svelte
c.代码内容改为:
<svelte:options tag="my-counter" /> <script> import { createEventDispatcher } from 'svelte'; // Event handling const dispatch = createEventDispatcher(); export let title = '?'; let count = 0; let ref; function dispatchClick(e) { // dispatch("customOnClick", { text: "Sir, I don't understand your command, I only know that the current count value is: " + count }); const event = new CustomEvent("customOnClick", { detail: { text: "Hi, I'm custom event from Svelte web component! My own count value is: " + count }, bubbles: true, cancelable: true, composed: true, // this line allows the event to leave the Shadow DOM }); // console.log(event) ref.dispatchEvent(event); } function inc() { count++; } function dec() { count--; } </script> <style> * { font-size: 200%; } span { width: 4rem; display: inline-block; text-align: center; } button { width: 64px; height: 64px; border: none; border-radius: 10px; background-color: seagreen; color: white; } </style> <button on:click={dec}> - </button> <span>{count}</span> <button on:click={inc}> + </button> <button on:click={dispatchClick} bind:this={ref}> {title} </button>
d.修改编译配置文件 rollup.config.js,增加编译选项:customElement: true
最终的 rollup.config.js 是这样的:
import svelte from 'rollup-plugin-svelte'; import resolve from '@rollup/plugin-node-resolve'; import pkg from './package.json'; const name = pkg.name .replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3') .replace(/^\w/, m => m.toUpperCase()) .replace(/-\w/g, m => m[1].toUpperCase()); export default { input: 'src/index.js', output: [ { file: pkg.module, 'format': 'es' }, { file: pkg.main, 'format': 'umd', name } ], plugins: [ svelte({ customElement: true, }), resolve() ] };
e. 编译生成 Web 组件
cd svelte-webcomponent yarn yarn build
生成的组件代码在 dist/ 目录下。
3)验证该 Web 组件。
在根目录下创建一个 index.html 文件,内容是这样的:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>My Counter</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <my-counter title="咣"></my-counter> <script type="text/javascript" src="dist/index.js"></script> <script type="text/javascript"> function handleClick(e) { console.log(e.detail.text) } document .querySelector("my-counter") .addEventListener('customOnClick', handleClick) </script> </body> </html>
4)用你常用的 http server 测试该 Web 组件。
比如 Live Server(https://www.npmjs.com/package/live-server)。我用的是 Serv:
cd svelte-webcomponent live-server
命令执行后会自动打开浏览器访问 http://127.0.0.1:8080/,分别点击"+"、"-"号、或 "咣" 按钮,查看计数器变化。同时你可以 F12 打开开发者工具,在 console 查看输出信息。
2、创建 React 应用并集成 my-counter 组件
1)创建新的 React 应用
npx create-react-app react-with-webcomponent
2)分别修改 src 目录下的 App.js、App.css,最终代码如下:
App.jsimport React, { useState, useEffect, useRef } from "react"; import "MyCounter"; import './App.css'; function App() { const [count, setCount] = useState(0); const buttonRef = useRef(); // console.log(buttonRef) useEffect(() => { buttonRef.current.addEventListener("customOnClick", (e) => { setCount(count + 1) console.log(e.detail.text) }); }); return ( <div> <ul class="list-group"> <li>React counter <span class="badge">{count}</span> </li> </ul> <my-counter title="咣" ref={buttonRef} // the classic onClick also works onClick={(e) => { console.log("on click", e); }} /> </div> ); } export default App;
App.css
/* https://github.com/mdn/css-examples/blob/master/css-cookbook/list-group-badges--download.html */ body { background-color: #fff; color: #333; font: 1.2em / 1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 0; margin: 1em; } .list-group { list-style: none; margin: 0 0 0 1.6em; padding: 0; border: 1px solid #ccc; border-radius: .5em; width: 14em; } .list-group li { border-top: 1px solid #ccc; padding: .5em; display: flex; justify-content: space-between; align-items: center; } .list-group li:first-child { border-top: 0; } .list-group .badge { background-color: rebeccapurple; color: #fff; font-weight: bold; font-size: 80%; border-radius: 10em; min-width: 1.5em; padding: .25em; text-align: center }
3)链接 MyCounter 包到本项目
注意到上面 App.js 中的 `import "MyCounter";` 了吗?运行之前,我们需要将之前编译好的包链接过来:
// 在 svelte-webcomponent 执行 cd svelte-webcomponent npm link // 在 react-with-webcomponent 执行 cd react-with-webcomponent npm link MyCounter npm start
命令执行后会自动打开浏览器访问 http://localhost:3000/,分别点击"+"、"-"号、或 "咣" 按钮,查看计数器变化。同时你可以 F12 打开开发者工具,在 console 查看输出信息。
3、创建 Vue 应用并集成 my-counter 组件
1)创建新的 Vue 应用
// 如果还没有 vue 命令,用 yarn global add @vue/cli 安装。 vue create vue-with-webcomponent cd vue-with-webcomponent npm link MyCounter yarn serve
命令执行后默认打开浏览器窗口,访问 http://localhost:8080/
2)修改 src 目录下的 App.vue,最终代码如下:
App.vue<template> <div id="app"> <ul class="list-group"> <li> Vue counter <span class="badge">{{ count }}</span> </li> <li> <my-counter :title="myTitle" @click="handleClick" @customOnClick="handleCustomClick" ></my-counter> </li> </ul> </div> </template> <script> import "MyCounter"; import "./App.css"; export default { name: "App", data() { return { count: 0, myTitle: `咣`, }; }, methods: { handleClick: function (event) { console.log(event); this.show = event.detail; }, handleCustomClick: function (event) { this.count++; console.log(event.detail.text); }, }, }; </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
增加 App.css 样式文件
同上述 React 应用中的 app.css 相同。
3)计数器测试
由于当前处于编译和热加载状态:在浏览器访问 http://localhost:8080/,即可看到计数器界面。
分别点击"+"、"-"号、或 "咣" 按钮,查看计数器变化。同时你可以 F12 打开开发者工具,在 console 查看输出信息。
构建后的目标代码比较
React 与 Vue 应用构建后的目标代码对比
框架(Framework) | 文件名(File) | 大小 KB(Size) | 压缩后(Gzipped) |
---|---|---|---|
React | build\static\js\2.cb6ed4ec.chunk.js build\static\js\main.d13c8b99.chunk.js |
136 8.0 |
42.65 2.96 KB (+6 B) |
Vue | dist\js\chunk-vendors.b5503860.js | 114.84 | 40.42 |
React | build\static\js\main.d13c8b99.chunk.js | 8.0 | 2.96 KB (+6 B) |
Vue | dist\js\app.4230b026.js | 8.34 | 3.66 |
React | build\static\js\runtime-main.0b036b94.js | 4.0 | 1.18 |
Vue | |||
React | build\static\css\main.baa84833.chunk.css | 4.0 | 519 B (-5 B) |
Vue | dist\css\app.c8a0ffb5.css | 0.67 | 0.36 |
从大小比较,在 JS 方面,两者大小差不多,Vue 略小一点。在 CSS 方面,React 压缩之后要小一些。
参考链接
- All the Ways to Make a Web Component - January 2021 Update on WebComponents.dev By <div>riots
- USING SVELTE IN PRODUCTION By Daniel Mies
- Integrate Web Components with Your Vue.js App By Joshua Bemenderfer