Palemoons' Archive
Palemoons' Archive
在astro中使用web component
Astro项目初尝试
发布于 2025715|遵循 CC BY-NC-SA 4.0 许可

闲话

上月空闲时间加入了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实现组件的效果。

官方所述的优点如下:

  1. 在Web Component中可以使用 this.querySelector() ,这一方法只在当前自定义元素实例中搜索。这使得一次只处理一个组件实例的子实例变得更容易。
  2. 尽管组件中的 <script> 只运行一次,但浏览器每次在页面上找到自定义标签(例如<web-component />)时都会运行这个元素的 connectedCallback() 方法。这意味着这个组件现在可以被复用了。

实现方案

总结来讲,在astro组件中创建一个web components需要以下步骤:

  1. 在astro组件的<script>中定义一个自定义元素:

    js
    复制代码
    1class MyComponent extends HTMLElement { 2 constructor() { 3 super(); 4 // 初始化 5 } 6 connectedCallback() { 7 // 实现组件交互功能 8 } 9}

    既可以继承自HTMLElement这一基类,也可以继承其他html元素(例如HTMLImageElement以及HTMLParagraphElement)。

    connectedCallback中,即可定义所需的交互功能,例如添加对组件中某一元素的监听等。

  2. 在astro的<script>部分中注册自定义元素,以后就可以在其他位置使用<my-component>这一元素。

    html
    复制代码
    1<script> 2 // ... 3 customElements.define('my-component', MyComponent) 4</script>

由于实际实现中需要配合页面设置适配多种模式,导致原本很简单的一个折叠组件被做得非常复杂,这里简化一个astro折叠组件的实现,仅供参考。

vue
复制代码
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出来,用于控制展开/折叠时各元素的状态,便于后续添加其他功能(如在阅读模式改变时强制展开组件)时,能基于该函数暴露其他公有方法。

Comments