背景
最近在开发一个 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。
- ......
有些恶心,但是没办法。
我也希望未来某个框架能够一统天下。
技术选型
既然是 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 掘金插件中就用到了这个组件。
如果你用过 Figma,应该看到过这类组件。
对应的链接我都挂在下面了,有兴趣的同学可以去看下。
- 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。