如何在客户端用Vanilla JS渲染Astro组件并实现动态搜索?
解决方案
方案一:用Vanilla JS手动渲染组件(适合快速实现)
1. 提取HTML模板
把Astro组件的HTML结构转成可复用的模板,用<template>标签存放在页面中:
<template id="job-card-template"> <div class='transition ease-in-out delay-150 flex overflow-hidden border border-zinc-300 rounded-md ring-slate-800 shadow-md duration-300 hover:border-zinc-500 hover:shadow-lg hover:scale-105'> <img src="" onerror="this.onerror=null; this.src='/images/joblogo.png'" alt="logo" class=" bg-white w-28 sm:w-32 object-cover object-center"> <div class="flex flex-col text-[#002838] mx-2 my-2 sm:mx-4 sm:my-4 text-sm w-full"> <h1 class="font-bold"></h1> <div class="text-xs mt-1"> <h2 class="font-medium mt-1"></h2> <p class="mb-1"></p> <p></p> </div> <div class="flex mt-6 items-center"> <ul class="flex flex-wrap gap-2 text-[8px] sm:text-xs font-medium"></ul> </div> <div class="apply-buttons"></div> </div> </div> </template>
2. 编写渲染函数
实现Vanilla JS函数,接收单个Job数据并生成对应DOM元素:
function renderJobCard(jobData) { const template = document.getElementById('job-card-template'); const card = template.content.cloneNode(true); // 填充基础信息 card.querySelector('img').src = jobData.logo; card.querySelector('h1').textContent = jobData.title; card.querySelector('h2').textContent = jobData.company; card.querySelector('.text-xs p:first-of-type').textContent = jobData.type; // 处理截止日期和close状态 const deadlineEl = card.querySelector('.text-xs p:last-of-type'); deadlineEl.textContent = `Apply before: ${jobData.deadline}`; if (jobData.close) { deadlineEl.classList.add('text-[#ED0000]', 'flex', 'relative'); // 插入告警元素(提前把Siren组件的SVG转成字符串) const alertHTML = ` <span class="flex"> <span class="animate-ping mx-[0.68rem] absolute inline-flex h-3 w-3 rounded-full bg-[#ED0000] opacity-75"></span> <span class="relative mx-2 -my-1">${sirenSvgString}</span> </span> `; deadlineEl.insertAdjacentHTML('beforeend', alertHTML); } // 渲染技能标签 const tagsList = card.querySelector('ul'); jobData.skills.split(',').forEach(tag => { const li = document.createElement('li'); li.className = 'rounded-sm bg-[#002838] text-white text-start px-1 hover:bg-[#00283896] duration-150'; li.textContent = tag.trim(); tagsList.appendChild(li); }); // 生成申请按钮 const buttonsContainer = card.querySelector('.apply-buttons'); if (jobData.link) { const linkBtn = document.createElement('button'); linkBtn.className = 'self-end mt-4 rounded-lg bg-[#0374E2] text-white w-16 sm:w-20 item-center text-md h-6 hover:text-zinc-800 hover:bg-accent duration-150 hover:shadow-md'; linkBtn.innerHTML = `<a href="${jobData.link}" target="_blank">Apply</a>`; buttonsContainer.appendChild(linkBtn); } if (jobData.email) { const emailBtn = document.createElement('button'); emailBtn.className = 'self-end mt-4 rounded-lg bg-[#0374E2] text-white w-16 sm:w-20 item-center text-md h-6 hover:text-zinc-800 hover:bg-accent duration-150 hover:shadow-md'; emailBtn.textContent = 'Apply'; emailBtn.onclick = () => openModal(jobData.email); buttonsContainer.appendChild(emailBtn); } return card; } // 提前将Siren.astro编译后的SVG内容转为字符串 const sirenSvgString = `<!-- 此处粘贴Siren组件的SVG代码 -->`;
3. 搜索与动态更新逻辑
实现API调用和渲染更新的绑定:
async function fetchJobs(searchQuery = '') { const res = await fetch(`/api/jobs?search=${encodeURIComponent(searchQuery)}`); return res.json(); } async function renderJobs(searchQuery) { const container = document.getElementById('jobs-container'); container.innerHTML = ''; const jobs = await fetchJobs(searchQuery); jobs.forEach(job => { container.appendChild(renderJobCard(job)); }); } // 绑定搜索框事件 document.getElementById('search-input').addEventListener('input', (e) => { renderJobs(e.target.value); }); // 页面加载时渲染初始数据 window.addEventListener('load', () => renderJobs());
方案二:封装为Web Component(Astro原生支持)
1. 修改Astro组件为Web Component
调整Astro组件,添加自定义元素声明和属性监听:
--- import Siren from '@components/Icons/Siren.astro'; import type { IJob } from '@util/job.interface'; export interface Props extends IJob{} const { company, title, email, type, deadline, skills, link, logo, close=false } = Astro.props; const tags = skills.split(","); --- <siren-job-card is:inline> <div class='transition ease-in-out delay-150 flex overflow-hidden border border-zinc-300 rounded-md ring-slate-800 shadow-md duration-300 hover:border-zinc-500 hover:shadow-lg hover:scale-105'> <img src={logo} onerror="this.onerror=null; this.src='/images/joblogo.png'" alt="logo" class=" bg-white w-28 sm:w-32 object-cover object-center"> <div class="flex flex-col text-[#002838] mx-2 my-2 sm:mx-4 sm:my-4 text-sm w-full"> <h1 class="font-bold">{title}</h1> <div class="text-xs mt-1"> <h2 class="font-medium mt-1">{company}</h2> <p class="mb-1">{type}</p> <p class={close && 'text-[#ED0000] flex relative'}>Apply before: {deadline} {close && <span class="flex"> <span class="animate-ping mx-[0.68rem] absolute inline-flex h-3 w-3 rounded-full bg-[#ED0000] opacity-75"></span> <span class="relative mx-2 -my-1"><Siren/></span> </span> } </p> </div> <div class="flex mt-6 items-center"> <ul class="flex flex-wrap gap-2 text-[8px] sm:text-xs font-medium"> {tags.map(tag=>{ return <li class="rounded-sm bg-[#002838] text-white text-start px-1 hover:bg-[#00283896] duration-150">{tag}</li> })} </ul> </div> {link && <button type="button" class="self-end mt-4 rounded-lg bg-[#0374E2] text-white w-16 sm:w-20 item-center text-md h-6 hover:text-zinc-800 hover:bg-accent duration-150 hover:shadow-md"> <a href={link} target="_blank">Apply</a> </button>} {email && <button type="button" onclick={`openModal("${email}")`} class="self-end mt-4 rounded-lg bg-[#0374E2] text-white w-16 sm:w-20 item-center text-md h-6 hover:text-zinc-800 hover:bg-accent duration-150 hover:shadow-md"> Apply </button>} </div> </div> </siren-job-card> <script> class SirenJobCard extends HTMLElement { static get observedAttributes() { return ['company', 'title', 'email', 'type', 'deadline', 'skills', 'link', 'logo', 'close']; } constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = this.innerHTML; } attributeChangedCallback(name, oldVal, newVal) { this.updateContent(); } updateContent() { // 动态更新技能标签 const skills = this.getAttribute('skills'); if (skills) { const tagsList = this.shadowRoot.querySelector('ul'); tagsList.innerHTML = ''; skills.split(',').forEach(tag => { const li = document.createElement('li'); li.className = 'rounded-sm bg-[#002838] text-white text-start px-1 hover:bg-[#00283896] duration-150'; li.textContent = tag.trim(); tagsList.appendChild(li); }); } } } customElements.define('siren-job-card', SirenJobCard); </script>
2. 客户端传递Props并渲染
通过HTML属性或JS动态设置组件属性:
<!-- 直接用HTML属性传递数据 --> <siren-job-card company="Example Corp" title="Frontend Developer" email="jobs@example.com" type="Full-time" deadline="2024-12-31" skills="React, Astro, Tailwind" link="https://example.com/apply" logo="/images/corp-logo.png" close="true" ></siren-job-card> <!-- JS动态创建组件 --> <script> async function renderJobs(searchQuery) { const container = document.getElementById('jobs-container'); container.innerHTML = ''; const jobs = await fetch(`/api/jobs?search=${encodeURIComponent(searchQuery)}`); const jobsData = await jobs.json(); jobsData.forEach(job => { const card = document.createElement('siren-job-card'); card.setAttribute('company', job.company); card.setAttribute('title', job.title); card.setAttribute('email', job.email); card.setAttribute('type', job.type); card.setAttribute('deadline', job.deadline); card.setAttribute('skills', job.skills); card.setAttribute('link', job.link); card.setAttribute('logo', job.logo); if (job.close) card.setAttribute('close', 'true'); container.appendChild(card); }); } </script>
3. 搜索逻辑同方案一
绑定搜索框事件,调用API后动态创建自定义元素即可。
内容的提问来源于stack exchange,提问作者Mahmoud Galal
相关产品推荐
相关产品推荐

