开发一个 React 和 Vue 都能用的组件?基于 Lit 和 Tailwind

2,489 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 28 天,点击查看活动详情

背景

最近在开发一个 HugGroup 组件。
你这么一听,可能会对这个名字可能有些疑惑。我说出它的另外一个名字,你可能就懂了,它还叫做 OnlineUser。
其实它就长这样。
image.png
HugGroup 是一个更专业的称呼。
因为这是一个开源项目,所以考虑多个框架的通用性,比如如何与 React、Solid、Angular 等框架进行结合。
其实这个需求应该是应用的开发者来考虑的。但是最近这两年,我们这类做底层库的人为了取悦做应用的开发者,把他们的心智负担都给分担没了。
所以我要先实现一个原生的 JavaScript 库,然后再创建几个基于框架的库,就像胶水一样把它们给粘起来。
所以在 npm 可能会有这么几个包。

  • @xx/hug-group 核心库,支持原生 JS。
  • @xx/hug-group-react:适配 React。
  • @xx/hug-group-solid:适配 Solid。
  • @xx/hug-group-angular:适配 Angular。
  • @xx/hug-group-vue:适配 Vue。
  • ......

有些恶心,但是没办法。
我也希望未来某个框架能够一统天下。

技术选型

既然是 Online 组件,那一定会有个服务器支撑了?
当然会有,但是不需要自己搭建,我们团队有开源免费的 presence.js。
通讯技术是用 WebTransport 和 presence.js 来做的,WebTransport 是一个没有线头阻塞;支持 QUIC(HTTP3) 和 UDP 的新一代通讯技术,但是国内知道 WebTransport 的人很少,大家还在玩 HTTP1/1 和 WebSocket。
除此之外,我还搞了个 webtransport-polyfill 和 react-cursor-chat,一个是在没实现 web transport API 的浏览器里面直接跑 WebTransport 代码,比如 Firefox。
另一个是用鼠标聊天的组件,在 VSCode 掘金插件中就用到了这个组件。
image.png
如果你用过 Figma,应该看到过这类组件。
对应的链接我都挂在下面了,有兴趣的同学可以去看下。

你可能会问了,这些东西既然没人用,你还搞它们干什么?国内确实没人用,国外可是不少人在关注呢。说不定几年之后 WebTransport 就成了未来前后端通讯的最佳实践了,具有前沿性的创新型公司当然要提前布好局啊。
当然,本文中通讯技术不是重点,重点是前端组件的开发。
我知道这才是大多数人喜欢看的东西,让我们快点儿开始吧,不然又有人要骂我标题党了。
项目原来是用 tsdx 做构建工具的,但是 tsdx 对 umd 和 iife 这类构建目标并不是很友好,它更适合 cjs 和 esm 这类构建目标。
于是我换了一套技术栈:

  • rollup:其实 tsdx 内部也包了 rollup,但我还是更喜欢直接写 rollup 配置。
  • babel:用 babel 做 ts 的转译。
  • typescript:写代码的基础语言。
  • lit:这个库出来好几年了,更专注于封装组件。Google 一直在推,可惜一直不温不火。
  • tailwindcss:写样式的基础库。
  • ESLint:用来检测代码。
  • prettier:用来格式化代码。

技术栈介绍完毕,开始搭建!

安装配置 Rollup

其实很多人不知道 rollup 有一个项目模板的,它很好用,只是 rollup 没有怎么介绍它而已。
这个仓库帮我们处理了一些事,这样我们就不需要从头开始了。
克隆这个仓库:github.com/rollup/roll…

git clone git@github.com:rollup/rollup-starter-app.git

之后进行以下几步操作:

  1. 修改项目名:mv rollup-starter-app hug-group。
  2. 进入项目:cd hug-group。
  3. 安装依赖:npm i。
  4. 启动开发服务器:npm dev。
  5. 打开浏览器,访问命令行的地址。看到下图,意味着 rollup 的 hello world 就跑起来了。

image.png
这个项目把最基本的 rollup 配置设置好了,并且用 serve 作为简单的开发服务器。

安装配置 Babel 和 TypeScript

babel 有支持 rollup 的插件,我们要用这个插件让 rollup 去调用 babel。然后通过 babel 去把 TypeScript 代码转换成 JavaScript 代码。
安装一堆依赖:

  • @babel/core
  • @rollup/plugin-babel
  • @babel/preset-env
  • typescript
  • @rollup/plugin-typescript
  • @babel/preset-typescript
pnpm --filter @yomo/hug-group i -D @babel/core @rollup/plugin-babel @babel/preset-env typescript @rollup/plugin-typescript @babel/preset-typescript

创建 src/main.ts 文件,并且把原来 src 目录的内容删掉。
在 rollup.config.js 中导入 babel 插件,把入口设置为 main.ts。
这是完整配置:

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import typescript from '@rollup/plugin-typescript';
import { babel } from '@rollup/plugin-babel';

// `npm run build` -> `production` is true
// `npm run dev` -> `production` is false
const production = !process.env.ROLLUP_WATCH;

export default {
  input: 'src/main.ts',
  output: {
    file: 'public/bundle.js',
    format: 'es',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
    typescript({
      include: ['src/**/*.ts'],
    }),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
    }),
    production && terser({ format: { comments: false } }), // minify, but only  in production
  ],
};

创建 .babelrc 文件,这是具体配置:

{
  "presets": ["@babel/preset-env"]
}

安装配置 ESLint 和 Prettier

安装一堆依赖:

  • eslint
  • prettier
  • eslint-config-prettier
  • eslint-plugin-prettier
pnpm --filter @yomo/hug-group i -D eslint prettier eslint-config-prettier eslint-plugin-prettier

然后运行以下命令来创建 eslint 的配置文件。

pnpm --filter @yomo/hug-group exec eslint --init

之后它会问一堆啰嗦的问题,你按照自己喜欢的内容去选择就好了。
之后会生成一个 .eslintrc.js 的配置文件。
在它的 plugin 中追加一个 prettier 就可以了。

安装插件

由于 Lit 用来表示 HTML 的方式是使用 JavaScript 模板字符串,在编辑器中并不会高亮。
为了实现在编辑器中高亮,需要下载对应的插件。
我使用的编辑器是 VSCode,安装的插件是 lit-element
没安装之前:
image.png
安装之后:
image.png

安装 Lit 和配置 TypeScript

现在我们已经把最基本工程化工具都配置好了,现在开始安装编写 UI 的 Lit 库。
Lit 就是在 Web Component 基础上的一个包装器,它为我们提供了一种将 UI 声明式地编写为状态函数的方法。
Lit 和 React 这类框架之间的区别是,Lit 是建立在 Web Component 之上的框架,并且它只专注于做组件。
第一步装包。

pnpm --filter @yomo/hug-group i lit

然后在 main.ts 里面编写一个 HelloWorld 组件。

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('hug-group')
export default class HugGroup extends LitElement {
  render() {
    return html` <h1>Hug Group</h1> `;
  }
}

最后修改 public/index.html 的内容。

<!DOCTYPE html>
<html>
  <head lang="en">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>rollup-starter-app</title>

    <style>
      body {
        font-family: 'Helvetica Neue', Arial, sans-serif;
        color: #333;
        font-weight: 300;
      }
    </style>
  </head>
  <body>
    <hug-group></hug-group>
    <script type="module" src="bundle.js"></script>
  </body>
</html>

这时应该会有一个错误,错误提示是无法使用 TypeScript 装饰器。
我们处理一下,在 tsconfig.json 中把 experimentalDecorators 设置为 true。
或者直接复制我的配置。

{
  "include": [
    "src",
    "types"
  ],
  "compilerOptions": {
    "target": "es6",
    "experimentalDecorators": true,
    "moduleResolution": "node"
  }
}

不出意外就跑起来了。
image.png

安装配置 Tailwind

安装一堆依赖:

  • rollup-plugin-postcss
  • tailwindcss
  • postcss
  • autoprefixer
pnpm --filter @yomo/hug-group i -D rollup-plugin-postcss tailwindcss postcss autoprefixer

运行以下命令生成 Tailwind 配置文件:

pnpm --filter @yomo/hug-group exec tailwindcss init

它会生成一个 tailwindcss.config.js 的配置文件。
稍微调整一下:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.ts'],
};

在 rollup.config.js 中导入 postcss 插件。

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import typescript from '@rollup/plugin-typescript';
import { babel } from '@rollup/plugin-babel';
import postcss from 'rollup-plugin-postcss'; // import postcss plugin

const production = !process.env.ROLLUP_WATCH;

export default {
  input: 'src/main.ts',
  output: {
    file: 'public/bundle.js',
    format: 'es',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
    postcss(), // use postcss plugin
    typescript({
      include: ['src/**/*.ts'],
    }),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
    }),
    production && terser({ format: { comments: false } }),
  ],
};

创建 postcss.config.js。

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

创建 src/styles.css 文件。

@tailwind base;
@tailwind components;
@tailwind utilities;

在 src/main.ts 中导入,并编写样式。

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import './styles.css';

@customElement('hug-group')
export default class HugGroup extends LitElement {
  createRenderRoot() {
    return this; // turn off shadow dom to access external styles
  }
  render() {
    return html`
    <h1 class="mx-auto my-4 py-4 text-center shadow-lg text-xl w-1/2">
    Hug Group
    </h1>
    `;
  }
}

最后回到浏览器中,查看效果。
image.png
恭喜,现在我已经有了一个 Lit 和 Tailwind 脚手架。
经过一系列配置,我终于可以聚焦于开发这个 HugGroup 组件了。
于是,我开始开发 HugGroup。
image.png
好了,开发完了。

总结

可以看到,要想搭建一套稍微完善些的前端工程化是非常复杂的,这个过程需要借助大量的工具、库、插件来处理各种各样的问题。
一个不算复杂的 OnlineUser 组件,哦不,HugGroup 组件。就需要将近 20 个包来支撑它的开发,这还没算测试等工作。
等它的功能完善后,基于它封装适配 React、Solid、Svelte 和 Vue 等框架的组件。这又是个枯燥无味的过程,但是为了简化上层应用开发者的体验,我又不得不去做这些事。
最后差点儿忘记了解释一下标题,为什么这个组件,React、Solid、Svelte、Vue、Angular 们都能用呢?因为用 lit 做的组件,它本质上只是个标准的 WebComponent,像原生标签一样平平无奇。所以和框架无关。
如果你也想要构建与框架无关的通用组件,不妨试试我的这套技术栈。
如果你想低成本构建实时应用,比如不考虑服务器的实现,不妨试试我们团队开源的 presence.js。
算是打个广告,哈哈!