闲话
上月空闲时间加入了FF14的一个攻略组的攻略网页开发,趁着7.2版本零式还没有开放,集中贡献了两周bug。到目前为止还是一个纯前端项目,技术栈主要包括Astro + VueJS + TailwindCSS,开源在GitHub上。毕竟是年轻人第一次参与开源项目开发,算是一次有趣的体验。
CollapseSection
开发途中我接到一个折叠块CollapseSection组件的需求。起初我简单的以为只需要在组件的astro文件中通过dom操作实现即可,提了pr才知道这样的用户点击一个展开按钮,就会使整个页面的折叠组件都被影响。也就是说,这个组件只能在一个页面中使用一次,不可复用。
(这样看,Astro好像让开发者又回到了写html + css + js三件套的时代。对于一个“出生”就在写ReactJS和VueJS的业余选手来说,还有一些不太习惯)

Web Component
为了解决这一问题,官方也给出了建议。在Web components with custom elements这一部分,介绍了如何通过定义一个自定义元素,为astro组件添加我们需要的交互功能,以达到过去在React或Vue实现组件的效果。
官方所述的优点如下:
在Web Component中可以使用
this.querySelector(),这一方法只在当前自定义元素实例中搜索。这使得一次只处理一个组件实例的子实例变得更容易。尽管组件中的
<script>只运行一次,但浏览器每次在页面上找到自定义标签(例如<web-component />)时都会运行这个元素的connectedCallback()方法。这意味着这个组件现在可以被复用了。
实现方案
总结来讲,在astro组件中创建一个web components需要以下步骤:
在astro组件的
<script>中定义一个自定义元素:js1class MyComponent extends HTMLElement { 2 constructor() { 3 super(); 4 // 初始化 5 } 6 connectedCallback() { 7 // 实现组件交互功能 8 } 9}既可以继承自
HTMLElement这一基类,也可以继承其他html元素(例如HTMLImageElement以及HTMLParagraphElement)。在
connectedCallback中,即可定义所需的交互功能,例如添加对组件中某一元素的监听等。在astro的
<script>部分中注册自定义元素,以后就可以在其他位置使用<my-component>这一元素。html1<script> 2 // ... 3 customElements.define('my-component', MyComponent) 4</script>
由于实际实现中需要配合页面设置适配多种模式,导致原本很简单的一个折叠组件被做得非常复杂,这里简化一个astro折叠组件的实现,仅供参考。
1---
2import RightArrowSVG from '@/assets/svg/right-arrow.svg'
3---
4
5<collapse-section>
6 <section class="collapse-summary mb-0 grid grid-cols-[16rem_1fr]">
7 <div class="self-center justify-self-end px-4">
8 <span class="collapse-toggle text-decimal inline-flex cursor-pointer items-center opacity-80 hover:underline">
9 <RightArrowSVG class="collapse-icon mr-1.5" />
10 <span class="collapse-open">展开</span>
11 <span class="collapse-close hidden">折叠</span>
12 细节
13 </span>
14 </div>
15 <div class="flex flex-wrap items-center gap-1 px-4 text-lg">
16 <slot name="summary" />
17 </div>
18 </section>
19 <div class="collapse-details relative flex max-h-0 flex-col gap-(--article-row-gap) opacity-0">
20 <div class="pointer-events-none absolute top-1/2 left-40 h-[calc(100%-1rem)] w-0.5 -translate-y-1/2 bg-teal-500">
21 </div>
22 <slot name="details" />
23 </div>
24</collapse-section>
25
26<script>
27 class CollapseSection extends HTMLElement {
28 private _isOpen: boolean
29 private _toggleButton!: HTMLSpanElement
30 private _icon!: SVGElement
31 private _detailsSection!: HTMLDivElement
32 private _openText!: HTMLSpanElement
33 private _closeText!: HTMLSpanElement
34
35 private _toggleView = (isOpen: boolean) => {
36 this._openText.classList.toggle('hidden', isOpen)
37 this._closeText.classList.toggle('hidden', !isOpen)
38 this._detailsSection.classList.toggle('hidden', !isOpen)
39 this._icon.classList.toggle('rotate-90', isOpen)
40 this._icon.classList.toggle('rotate-0', !isOpen)
41 }
42
43 constructor() {
44 super()
45 this._isOpen = false
46 }
47
48 connectedCallback() {
49 this._toggleButton = this.querySelector('.collapse-toggle') as HTMLSpanElement
50 this._icon = this.querySelector('.collapse-icon') as SVGElement
51 this._detailsSection = this.querySelector('.collapse-details') as HTMLDivElement
52 this._openText = this.querySelector('.collapse-open') as HTMLSpanElement
53 this._closeText = this.querySelector('.collapse-close') as HTMLSpanElement
54
55 this._toggleButton.addEventListener('click', () => {
56 this._isOpen = !this._isOpen
57 this._toggleView(this._isOpen)
58 })
59 }
60 }
61 customElements.get('collapse-section') || customElements.define('collapse-section', CollapseSection)
62</script>这里定义了一个私有方法_toggleView出来,用于控制展开/折叠时各元素的状态,便于后续添加其他功能(如在阅读模式改变时强制展开组件)时,能基于该函数暴露其他公有方法。