【分久必合】构建下一代前端组件
TIP
🎄Hi~ 大家好,我是小鑫同学,资深 IT 从业者,InfoQ 的签约作者,擅长前端开发并在这一领域有多年的经验,致力于分享我在技术方面的见解和心得
最近两年一直在迭代一套由 MicroApp 搭建的微前端的项目,整个项目由一套 Angualr8.x 的主项目 和 两套 Vue3.0 的子项目组成,虽然三套项目均采用了 Ant Design 组件库,但目前主流的组件库大多都严重依赖技术框架及技术框架的不同版本,在微前端项目中极容易造成页面或组件的开发和维护工作量翻倍,而且还极容易造成全局样式的污染。
为什么选择 Quarkc ?
在最近的一次技术大会上接触到了由 哈啰技术团队 带来的 Quarkc 和 Quarkd。Quarkc 是由哈啰平台前端团队开发的一套面向未来的 无框架 组件构建工具!底层基于浏览器原生 API: Web components。纯利用 Web components API 开发组件存在以下几个痛点:
- 大量的 DOM 操作;
- 处理版本和浏览器兼容;
- 没有代码高亮提示;
- 样式自适应;
- 。。。
正如哈啰技术团队的徐顺发老师讲的 陌生的API + 原生的写法 增加了学习成本、导致未知问题的出现、还会存在性能问题。所以选择合适的框架来开发 Web components 组件尤为重要,从框架的学习成本、维护积极性、开发体验等因素考虑 Quarkc 可能更加符合这些条件。
体验 Quarkc 开发组件:
我在 1024code 平台准备好了由 quarkc
创建的 quark-component-template 模板项目,你可以 Fork 此项目在线体验 Quarkc 开发组件。
下图是 Github Discussions 的常驻公告栏卡片,接下来就使用quarkc
来开发跨平台的公告栏卡片组件。
认识 Quark-Component:
目录结构:
quark-component-template
├── sec
│ ├── index.less // 组件样式
│ ├── index.tsx // 组件源码
│ └── vite-env.d.ts
├── index.html
├── index.vue.html
├── package.json
├── README.md
├── tsconfig.json
├── vite.config.build.ts
└── vite.config.ts
quark-component-template
├── sec
│ ├── index.less // 组件样式
│ ├── index.tsx // 组件源码
│ └── vite-env.d.ts
├── index.html
├── index.vue.html
├── package.json
├── README.md
├── tsconfig.json
├── vite.config.build.ts
└── vite.config.ts
在目录中主要关注 src/index.tsx
文件,在该文件中定义的组件类需要继承自 QuarkElement
对象并重写 render
函数来实现组件的结构, 使用 @customElement
定义组件的 tag
及引用的 style
。
import { QuarkElement, property, customElement } from 'quarkc';
import style from './index.less?inline';
declare global {
interface HTMLElementTagNameMap {
'discussion-spotlight-container': DiscussionSpotlightContainer;
}
}
@customElement({ tag: 'discussion-spotlight-container', style })
class DiscussionSpotlightContainer extends QuarkElement {
render() {
return (
<>
DiscussionSpotlightContainer
</>
);
}
}
export default DiscussionSpotlightContainer;
import { QuarkElement, property, customElement } from 'quarkc';
import style from './index.less?inline';
declare global {
interface HTMLElementTagNameMap {
'discussion-spotlight-container': DiscussionSpotlightContainer;
}
}
@customElement({ tag: 'discussion-spotlight-container', style })
class DiscussionSpotlightContainer extends QuarkElement {
render() {
return (
<>
DiscussionSpotlightContainer
</>
);
}
}
export default DiscussionSpotlightContainer;
开发常驻公告栏卡片:
通过分析卡片的构成,它主要包含了动态的头像、标题和通知三部分,那么给出了下面这段 tsx
结构代码。
<>
<div className="discussion-wapper">
<div className="discussion-container">
<img className="avatar" src={this.avatarUrl} />
<h3 className="title">{this.title}</h3>
<div className="notice">
<span>{this.notice}</span>
<span>·</span>
<span>xsf0105</span>
</div>
</div>
</div>
</>
<>
<div className="discussion-wapper">
<div className="discussion-container">
<img className="avatar" src={this.avatarUrl} />
<h3 className="title">{this.title}</h3>
<div className="notice">
<span>{this.notice}</span>
<span>·</span>
<span>xsf0105</span>
</div>
</div>
</div>
</>
期间三个动态的内容需要组件在被使用时,有使用者决定传入的内容,也就是要接收一些属性,这里会用到 @property
装饰器,完善后的代码如下。
@customElement({ tag: 'discussion-spotlight-container', style })
class DiscussionSpotlightContainer extends QuarkElement {
@property({ type: String })
avatarUrl = '';
@property({ type: String })
title = '';
@property({ type: String })
notice = '';
render() {
return (
<>
<div className="discussion-wapper">
<div className="discussion-container">
<img className="avatar" src={this.avatarUrl} />
<h3 className="title">{this.title}</h3>
<div className="notice">
<span>{this.notice}</span>
<span>·</span>
<span>xsf0105</span>
</div>
</div>
</div>
</>
);
}
}
@customElement({ tag: 'discussion-spotlight-container', style })
class DiscussionSpotlightContainer extends QuarkElement {
@property({ type: String })
avatarUrl = '';
@property({ type: String })
title = '';
@property({ type: String })
notice = '';
render() {
return (
<>
<div className="discussion-wapper">
<div className="discussion-container">
<img className="avatar" src={this.avatarUrl} />
<h3 className="title">{this.title}</h3>
<div className="notice">
<span>{this.notice}</span>
<span>·</span>
<span>xsf0105</span>
</div>
</div>
</div>
</>
);
}
}
还有样式的部分代码:
.discussion-wapper {
height: 150px;
overflow: hidden;
cursor: pointer;
background-image: linear-gradient(to right, #58a6ff, #56d364);
border-radius: 6px;
border: 1px solid #d0d7de;
}
.discussion-container {
margin-top: 32px;
padding: 0 16px;
height: 100%;
background-color: #fff;
}
.avatar {
height: 32px;
aspect-ratio: auto 32 / 32;
width: 32px;
border: 1px solid #d0d7de;
border-radius: 50%;
margin-top: calc(-24px);
border-color: #fff;
border-width: 3px;
}
.title {
margin-bottom: 8px;
font-size: 20px;
font-weight: 600;
}
.notice span {
max-width: fit-content;
min-width: 1ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notice span:first-child {
font-weight: 600;
color: #57606A;
}
.notice span:nth-child(2) {
margin-left: 0.25rem;
}
.notice span:nth-child(3) {
margin-left: 0.25rem;
color: #57606A;
}
.discussion-wapper {
height: 150px;
overflow: hidden;
cursor: pointer;
background-image: linear-gradient(to right, #58a6ff, #56d364);
border-radius: 6px;
border: 1px solid #d0d7de;
}
.discussion-container {
margin-top: 32px;
padding: 0 16px;
height: 100%;
background-color: #fff;
}
.avatar {
height: 32px;
aspect-ratio: auto 32 / 32;
width: 32px;
border: 1px solid #d0d7de;
border-radius: 50%;
margin-top: calc(-24px);
border-color: #fff;
border-width: 3px;
}
.title {
margin-bottom: 8px;
font-size: 20px;
font-weight: 600;
}
.notice span {
max-width: fit-content;
min-width: 1ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notice span:first-child {
font-weight: 600;
color: #57606A;
}
.notice span:nth-child(2) {
margin-left: 0.25rem;
}
.notice span:nth-child(3) {
margin-left: 0.25rem;
color: #57606A;
}
至此这个公告栏卡片组件就已经开发完成了,它将可以使用在普通的 HTML 网页,也可以使用在 Vue、React、Angular 技术框架的任何项目中,使用的方式同样要比目前常用的组件库使用方式要简单,我们接着往下看。
预览常驻公告栏卡片:
在执行 npm run dev
会启动组件的测试页面,对应的文件是 index.html
,是一个不包含任何框架的纯 HTML 文件,仅通过导入组件文件(./src/index.tsx
)就可以使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello Quark</title>
<script type="module" src="./src/index.tsx"></script>
</head>
<body>
<discussion-spotlight-container
avatarUrl="https://avatars.githubusercontent.com/u/14307551?s=32&v=4"
title="Next quark design 邀您共建 🚀"
notice="📣 Announcements"
></discussion-spotlight-container>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello Quark</title>
<script type="module" src="./src/index.tsx"></script>
</head>
<body>
<discussion-spotlight-container
avatarUrl="https://avatars.githubusercontent.com/u/14307551?s=32&v=4"
title="Next quark design 邀您共建 🚀"
notice="📣 Announcements"
></discussion-spotlight-container>
</body>
</html>
另外在根目录下的 index.vue.html
文件还提供了一份在 Vuejs 中使用组件的方式,同样是仅仅导入组件文件(import './src/index.tsx';
)就可以使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Test in Vue.js</title>
</head>
<body>
<div id="app">
<discussion-spotlight-container
:avatarUrl="avatarUrl"
:title="title"
:notice="notice"
></discussion-spotlight-container>
</div>
</body>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="module">
import './src/index.tsx';
const { createApp } = Vue;
createApp({
data() {
return {
avatarUrl:
'https://avatars.githubusercontent.com/u/14307551?s=32&v=4',
title: 'Next quark design 邀您共建 🚀',
notice: '📣 Announcements',
};
},
}).mount('#app');
</script>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Test in Vue.js</title>
</head>
<body>
<div id="app">
<discussion-spotlight-container
:avatarUrl="avatarUrl"
:title="title"
:notice="notice"
></discussion-spotlight-container>
</div>
</body>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="module">
import './src/index.tsx';
const { createApp } = Vue;
createApp({
data() {
return {
avatarUrl:
'https://avatars.githubusercontent.com/u/14307551?s=32&v=4',
title: 'Next quark design 邀您共建 🚀',
notice: '📣 Announcements',
};
},
}).mount('#app');
</script>
</html>
这是预览的效果: PS:https://1024code.com/codecubes/dx5axr8 《DiscussionSpotlightCard》,请点击链接或复制链接到浏览器打开,在线查看和运行
打包 & 再次测试组件:
执行 npm run build
命令来打包组件产物将在lib
目录中获得下面的内容,组件产物暂时不考虑发布 NPM,所以在 1024code 中在项目右上角的选择导出文件。
lib
├── types
| └── index.d.ts
├── index.js
└── index.umd.js
lib
├── types
| └── index.d.ts
├── index.js
└── index.umd.js
为了再次测试组件,我们依旧可以选择在 1024code 平台,新建 Vuejs 模板项目并随意选择版本并创建代码空间: 接着创建 webcomponents
目录并上传打包的组件产物,此 Vuejs 项目在构建时应排除此目录。 执行 npm i quarkc
安装唯一的依赖模块后就可以开始使用组件了:
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<discussion-spotlight-container
:avatarUrl="data.avatarUrl"
:title="data.title"
:notice="data.notice"
></discussion-spotlight-container>
</template>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<discussion-spotlight-container
:avatarUrl="data.avatarUrl"
:title="data.title"
:notice="data.notice"
></discussion-spotlight-container>
</template>
<script setup>
// 导入组件文件
import './webcomponents/discussion-spotlight-container/index';
// 定义组件属性数据
const data = {
avatarUrl: 'https://avatars.githubusercontent.com/u/14307551?s=32&v=4',
title: 'Next quark design 邀您共建 🚀',
notice: '📣 Announcements',
};
</script>
<script setup>
// 导入组件文件
import './webcomponents/discussion-spotlight-container/index';
// 定义组件属性数据
const data = {
avatarUrl: 'https://avatars.githubusercontent.com/u/14307551?s=32&v=4',
title: 'Next quark design 邀您共建 🚀',
notice: '📣 Announcements',
};
</script>
总结:
QuarkC 使用面向 Class 并配合装饰器的来提供组件开发的方式,对于习惯 Angualr 和 React 开发风格的前端更加的友好,在案例中仅仅使用了定义组件的装饰器 @customElement
和 对外提供属性的 @property
,在 QuarkC 中还提供了内部状态@state
、自定义事件 $emit
、插槽 slot
以及生命周期等。Quarkd 就是哈啰技术团队基于 Web Components 的跨框架 UI 组件库。 分久必合,可能 Web Components 的跨框架 UI 组件库就是下一代的前端组件,在后面的微前端项目迭代中将有必要考虑使用 QuarkC 构建跨平台的业务组件。你觉得 QuarkC 怎么样?