在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.js
import 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 压缩之后要小一些。

参考链接


Like:
0
To the top