开发一个 React 和 Vue 都能用的组件?基于 Lit 和 Tailwind
背景
最近在开发一个 HugGroup 组件。你这么一听,可能会对这个名字可能有些疑惑。我说出它的另外一个名字,你可能就懂了,它还叫做 OnlineUser。其实它就长这样。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。
- ......
技术选型
- WebTransport Web.dev 的教程:web.dev/i18n/zh/web…
- webtransport-polyfill 的仓库:github.com/yomorun/pre…
- react-cursor-chat 的仓库:github.com/yomorun/rea…
你可能会问了,这些东西既然没人用,你还搞它们干什么?国内确实没人用,国外可是不少人在关注呢。说不定几年之后 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
之后进行以下几步操作:
- 修改项目名:mv rollup-starter-app hug-group。
- 进入项目:cd hug-group。
- 安装依赖:npm i。
- 启动开发服务器:npm dev。
- 打开浏览器,访问命令行的地址。看到下图,意味着 rollup 的 hello world 就跑起来了。
这个项目把最基本的 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。没安装之前:安装之后:
安装 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"
}
}
安装配置 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>
`;
}
}
最后回到浏览器中,查看效果。恭喜,现在我已经有了一个 Lit 和 Tailwind 脚手架。经过一系列配置,我终于可以聚焦于开发这个 HugGroup 组件了。于是,我开始开发 HugGroup。好了,开发完了。
总结
可以看到,要想搭建一套稍微完善些的前端工程化是非常复杂的,这个过程需要借助大量的工具、库、插件来处理各种各样的问题。一个不算复杂的 OnlineUser 组件,哦不,HugGroup 组件。就需要将近 20 个包来支撑它的开发,这还没算测试等工作。等它的功能完善后,基于它封装适配 React、Solid、Svelte 和 Vue 等框架的组件。这又是个枯燥无味的过程,但是为了简化上层应用开发者的体验,我又不得不去做这些事。最后差点儿忘记了解释一下标题,为什么这个组件,React、Solid、Svelte、Vue、Angular 们都能用呢?因为用 lit 做的组件,它本质上只是个标准的 WebComponent,像原生标签一样平平无奇。所以和框架无关。如果你也想要构建与框架无关的通用组件,不妨试试我的这套技术栈。如果你想低成本构建实时应用,比如不考虑服务器的实现,不妨试试我们团队开源的 presence.js。算是打个广告,哈哈!
转载自:https://juejin.cn/post/7136002792304902175