十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Front-End-Checklist 实战:用 `<noscript>` 回退内容构建无 JavaScript 也可用的渐进增强站点

Front-End-Checklist 实战:用 `<noscript>` 回退内容构建无 JavaScript 也可用的渐进增强站点 Front-End-Checklist 实战用noscript回退内容构建无 JavaScript 也可用的渐进增强站点【免费下载链接】Front-End-Checklist The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist本篇技术指南围绕 Front-End-Checklist 仓库中noscript-tag规则展开它属于html分类、components子分类优先级 Medium、难度 Beginner、预计耗时 10 分钟。指南将完整讲解noscript回退内容的使用场景、完整 HTML/CSS/JS 示例、Next.js / React / Vue 框架实现、特性检测与 hydration 不匹配的避坑要点以及无 JS 环境下的测试与验证方法帮助你写出对禁用 JavaScript 的用户、企业防火墙拦截用户与脚本加载失败用户依然可用的网页。规则是什么为禁用 JavaScript 的用户提供回退内容一条noscript标签为禁用 JavaScript 的用户提供回退内容。noscript元素为 JavaScript 被禁用或不可用的用户提供回退内容从而保证可访问性与渐进增强progressive enhancement。根据 SKILL.md 的 Quick Reference实践该规则需要做到为依赖 JavaScript 的功能添加带帮助信息的noscript提供替代内容而不是简单一句请启用 JavaScript用于关键功能分析统计回退、懒加载图片等考虑渐进增强内容在无 JS 时也能正常工作重要内容应出现在初始 HTML 中而不是仅在 hydration 之后才被拉取。该规则元数据category:htmlsubcategory:componentspriority:mediumdifficulty:beginnerestimatedTime:10定义于规则源文件 packages/content/rules/en/html/noscript-tag.mdx完整实操细节见其引用文档 references/rule.md。在 Front-End-Checklist 的规则目录 docs/generated/rules-catalog.md 中该规则以Provide noscript fallback content条目出现说明其核心理念A noscript tag provides fallback content for users with JavaScript disabled为禁用 JavaScript 的用户提供回退内容的 noscript 标签。完整代码示例一个渐进增强的页面以下是一个完整的示例页面展示如何在关键位置使用noscript!DOCTYPE html html langen head meta charsetUTF-8 titleProgressive Enhancement Example/title !-- Critical CSS loaded normally -- link relstylesheet href/css/critical.css !-- Non-critical CSS with noscript fallback -- script // Load CSS asynchronously var link document.createElement(link); link.rel stylesheet; link.href /css/enhanced.css; document.head.appendChild(link); /script noscript link relstylesheet href/css/enhanced.css /noscript /head body nav ul lia href/Home/a/li lia href/aboutAbout/a/li lia href/contactContact/a/li /ul /nav !-- JavaScript-enhanced form with fallback -- form action/search methodGET input typesearch nameq placeholderSearch... required button typesubmitSearch/button noscript pstrongNote:/strong JavaScript is disabled. Search results will be displayed on a new page./p /noscript /form !-- Enhanced content with fallback -- div iddynamic-content noscript pThis content requires JavaScript to display properly. Please enable JavaScript or visit our a href/sitemapsitemap/a for all content./p /noscript /div /body /html这个示例体现了三个关键模式非关键 CSS 的异步加载回退用script动态注入link时同步在noscript中放置等价的link确保无 JS 用户同样获得增强样式表单的降级路径form保持原生action/method提交能力noscript提示用户搜索将在新页面展示结果动态内容的替代入口当内容依赖 JS 渲染时noscript内提供说明与指向/sitemap的链接作为替代入口。为什么它很重要禁用 JavaScript、被企业防火墙拦截或脚本加载失败的用户如果没有合适的 noscript 回退看到的将是空白页面。渐进增强从可用的 HTML开始而不是从客户端 JavaScript 开始。初始文档就应该包含重要的文案、链接与表单动作。JavaScript 应当增强搜索建议、筛选、无限滚动与校验而不是成为内容的唯一通路。常见使用场景的完整实现导航回退Navigation Fallbacks移动端菜单按钮由 JavaScript 增强noscript内用内联style强制显示完整导航链接nav !-- Mobile menu button (JavaScript enhanced) -- button idmobile-menu-btn classmobile-menu-toggle aria-expandedfalse Menu /button !-- Navigation menu -- ul idmain-nav classnav-menu lia href/Home/a/li lia href/productsProducts/a/li lia href/aboutAbout/a/li lia href/contactContact/a/li /ul noscript style .mobile-menu-toggle { display: none !important; } .nav-menu { display: block !important; } /style pemAll navigation links are available above./em/p /noscript /nav script // Enhanced mobile menu functionality document.getElementById(mobile-menu-btn).addEventListener(click, function() { const nav document.getElementById(main-nav); const expanded this.getAttribute(aria-expanded) true; this.setAttribute(aria-expanded, !expanded); nav.classList.toggle(open); }); /script要点aria-expanded状态由 JS 维护但导航本身是纯 HTML 链接列表无 JS 依然可达noscript内的style规则在无 JS 时让菜单按钮隐藏、导航始终展开。表单增强与回退Form Enhancement with Fallbacksform action/contact methodPOST idcontact-form div classform-group label fornameName */label input typetext idname namename required /div div classform-group label foremailEmail */label input typeemail idemail nameemail required div idemail-error classerror-message aria-livepolite/div /div div classform-group label formessageMessage */label textarea idmessage namemessage required/textarea div idchar-count classchar-counter0/500 characters/div /div button typesubmit idsubmit-btnSend Message/button div idform-status aria-livepolite/div noscript div classnoscript-notice pstrongJavaScript Disabled:/strong/p ul liForm will submit normally but without real-time validation/li liCharacter counting is not available/li liYoull be redirected to a confirmation page after submission/li /ul /div /noscript /form script // Enhanced form functionality const form document.getElementById(contact-form); const messageField document.getElementById(message); const charCount document.getElementById(char-count); // Character counter messageField.addEventListener(input, function() { const count this.value.length; charCount.textContent ${count}/500 characters; charCount.className count 500 ? char-counter over-limit : char-counter; }); // AJAX form submission form.addEventListener(submit, async function(e) { e.preventDefault(); // Enhanced submission logic here }); /script该示例展示了增强而非替代的原则表单始终以原生actionmethod提交JS 只负责字符计数、实时校验与 AJAX 提交noscript明确告知用户哪些增强能力不可用但提交链路本身不依赖 JS。内容加载回退Content Loading with Fallbackssection idproduct-gallery h2Product Gallery/h2 !-- Placeholder content for no-JS users -- div classstatic-gallery div classproduct-grid div classproduct-item img src/images/product-1.jpg altProduct 1 h3Product 1/h3 p$29.99/p a href/products/1 classbtnView Details/a /div div classproduct-item img src/images/product-2.jpg altProduct 2 h3Product 2/h3 p$39.99/p a href/products/2 classbtnView Details/a /div /div div classpagination a href/products?page11/a a href/products?page22/a a href/products?page33/a /div /div noscript pShowing all products. Enhanced filtering and infinite scroll require JavaScript./p /noscript /section script // Replace static gallery with enhanced version document.addEventListener(DOMContentLoaded, function() { const gallery document.getElementById(product-gallery); // Initialize enhanced gallery with filtering, search, infinite scroll initEnhancedGallery(gallery); }); /script关键设计静态内容默认在 HTML 中就存在产品网格 分页链接JS 加载后用增强版画廊替换即使脚本加载失败或被执行安全策略拦截用户依然能看到全部商品并通过分页浏览。这正是重要内容必须在 hydration 之前存在于初始 HTML的落地方式。优先使用特性检测而非 UA 嗅探浏览器或设备嗅探是脆弱的经常误判嵌入式浏览器、兼容模式与未来版本。应检测你真正需要的能力// ❌ Bad: user-agent sniffing if (/iPhone|Android/.test(navigator.userAgent)) { enableTouchMenu() } // ✅ Good: feature detection if (ontouchstart in window || navigator.maxTouchPoints 0) { enableTouchMenu() } if (IntersectionObserver in window) { enableInfiniteScroll() } else { showPaginationLinks() }特性检测的好处是不依赖厂商字符串直接判断浏览器是否支持所需 API对于IntersectionObserver这类渐进能力可以优雅降级为传统分页链接。避免 hydration 不匹配如果服务端渲染一个值、客户端挂载后立刻替换为另一个值用户可能丢失上下文辅助技术assistive technology也会播报过期内容。关键内容应保持服务端与客户端输出一致// ❌ Bad: server and client render different primary content function Greeting() { return h1{typeof window undefined ? Welcome : window.location.pathname}/h1 } // ✅ Good: server renders stable content, JS enhances after mount function Greeting({ path }: { path: string }) { return h1Viewing {path}/h1 }正确做法服务端渲染稳定的主内容客户端在挂载后增强如补充交互而不是替换主内容。框架级实现示例Next.js带 NoScript 支持的搜索组件Next.js 中isClient状态用于区分服务端渲染与客户端挂载form始终保留原生提交能力作为回退// components/EnhancedSearch.js import { useState, useEffect } from react export default function EnhancedSearch({ fallbackAction /search }) { const [query, setQuery] useState() const [results, setResults] useState([]) const [isClient, setIsClient] useState(false) useEffect(() { setIsClient(true) }, []) const handleSearch async (e) { if (!isClient) return // Let form submit normally e.preventDefault() try { const response await fetch(/api/search?q${encodeURIComponent(query)}) const data await response.json() setResults(data.results) } catch (error) { console.error(Search failed:, error) // Fallback to regular form submission window.location.href ${fallbackAction}?q${encodeURIComponent(query)} } } return ( div classNamesearch-container form action{fallbackAction} methodGET onSubmit{handleSearch} input typesearch nameq value{query} onChange{(e) setQuery(e.target.value)} placeholderSearch products... required / button typesubmitSearch/button /form {!isClient ( noscript div classNamenoscript-notice pJavaScript is disabled. Search results will open in a new page./p /div /noscript )} {isClient results.length 0 ( div classNamesearch-results {results.map(result ( div key{result.id} classNameresult-item h3{result.title}/h3 p{result.description}/p /div ))} /div )} /div ) }配合服务端渲染的回退页让搜索在无 JS 时走GET提交到独立结果页// pages/search.js (fallback page) export default function SearchResults({ query, results }) { return ( div h1Search Results for {query}/h1 {results.map(result ( div key{result.id} classNameresult-item h3{result.title}/h3 p{result.description}/p a href{result.url}View Details/a /div ))} /div ) } export async function getServerSideProps({ query }) { const searchQuery query.q || const results await searchProducts(searchQuery) return { props: { query: searchQuery, results } } }这段代码完整展示了渐进增强链路客户端可用时走fetch实时搜索客户端不可用时handleSearch直接 return 让表单正常提交JS 请求失败则跳转到fallbackAction结果页而服务端getServerSideProps保证无 JS 也能拿到完整搜索结果。React渐进增强的联系表单React 中通过isEnhanced状态决定是否启用增强校验与 AJAX 提交noscript在挂载后由 JS 隐藏import React, { useState, useEffect, useRef } from react function ProgressiveContactForm() { const [isEnhanced, setIsEnhanced] useState(false) const [formData, setFormData] useState({ name: , email: , message: }) const [errors, setErrors] useState({}) const [isSubmitting, setIsSubmitting] useState(false) const formRef useRef(null) useEffect(() { // Enable enhanced features after component mounts setIsEnhanced(true) // Hide noscript content when JS is available const noscriptElements document.querySelectorAll(noscript) noscriptElements.forEach(el { el.style.display none }) }, []) const validateField (name, value) { switch (name) { case email: return /^[^\s][^\s]\.[^\s]$/.test(value) ? : Please enter a valid email case name: return value.length 2 ? : Name must be at least 2 characters case message: return value.length 10 ? : Message must be at least 10 characters default: return } } const handleChange (e) { const { name, value } e.target setFormData(prev ({ ...prev, [name]: value })) if (isEnhanced) { const error validateField(name, value) setErrors(prev ({ ...prev, [name]: error })) } } const handleSubmit async (e) { if (!isEnhanced) { // Let the form submit normally return } e.preventDefault() setIsSubmitting(true) try { const response await fetch(/api/contact, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(formData) }) if (response.ok) { alert(Message sent successfully!) setFormData({ name: , email: , message: }) } else { throw new Error(Submission failed) } } catch (error) { alert(Failed to send message. Please try again.) } finally { setIsSubmitting(false) } } return ( form ref{formRef} action/contact methodPOST onSubmit{handleSubmit} div classNameform-group label htmlFornameName */label input typetext idname namename value{formData.name} onChange{handleChange} required / {isEnhanced errors.name ( span classNameerror{errors.name}/span )} /div div classNameform-group label htmlForemailEmail */label input typeemail idemail nameemail value{formData.email} onChange{handleChange} required / {isEnhanced errors.email ( span classNameerror{errors.email}/span )} /div div classNameform-group label htmlFormessageMessage */label textarea idmessage namemessage value{formData.message} onChange{handleChange} required / {isEnhanced errors.message ( span classNameerror{errors.message}/span )} {isEnhanced ( small{formData.message.length}/500 characters/small )} /div button typesubmit disabled{isSubmitting} {isSubmitting ? Sending... : Send Message} /button noscript div classNamenoscript-notice pstrongJavaScript is disabled:/strong/p ul liForm will submit to /contact endpoint/li liReal-time validation unavailable/li liYoull see a confirmation page after submission/li /ul /div /noscript /form ) }注意其中handleSubmit在!isEnhanced时直接 return 的写法这正是让表单走原生提交的 React 实现方式避免在 hydration 前拦截提交。Vue.js画廊的 NoScript 处理Vue 中利用isClientSide数据标志在模板里切换增强视图与回退视图同时保留noscript作为纯 HTML 版本template div classenhanced-component !-- Enhanced content shown when JS is available -- div v-ifisClientSide classjs-enhanced h2Enhanced Gallery/h2 div classfilters button v-forcategory in categories :keycategory clickfilterByCategory(category) :class{ active: selectedCategory category } {{ category }} /button /div div classimage-grid div v-forimage in filteredImages :keyimage.id classimage-item clickopenLightbox(image) img :srcimage.thumbnail :altimage.alt p{{ image.title }}/p /div /div div v-ifshowLightbox classlightbox clickcloseLightbox img :srcselectedImage.full :altselectedImage.alt /div /div !-- Fallback content structure -- div v-else classno-js-fallback h2Image Gallery/h2 div classstatic-grid div v-forimage in images :keyimage.id classstatic-item a :hrefimage.full target_blank img :srcimage.thumbnail :altimage.alt p{{ image.title }} ({{ image.category }})/p /a /div /div /div !-- NoScript element for pure HTML version -- noscript div classnoscript-gallery h2Image Gallery (JavaScript Disabled)/h2 pClick images to view full size in new window./p div classnoscript-grid !-- Server-rendered static content would go here -- /div /div /noscript /div /template script export default { data() { return { isClientSide: false, images: [ { id: 1, title: Sunset, category: Nature, thumbnail: /thumb1.jpg, full: /full1.jpg, alt: Beautiful sunset }, { id: 2, title: City, category: Urban, thumbnail: /thumb2.jpg, full: /full2.jpg, alt: City skyline } ], categories: [All, Nature, Urban, People], selectedCategory: All, showLightbox: false, selectedImage: null } }, computed: { filteredImages() { if (this.selectedCategory All) { return this.images } return this.images.filter(img img.category this.selectedCategory) } }, mounted() { // Enable client-side features this.isClientSide true // Handle keyboard navigation document.addEventListener(keydown, this.handleKeydown) }, beforeUnmount() { document.removeEventListener(keydown, this.handleKeydown) }, methods: { filterByCategory(category) { this.selectedCategory category }, openLightbox(image) { this.selectedImage image this.showLightbox true document.body.style.overflow hidden }, closeLightbox() { this.showLightbox false this.selectedImage null document.body.style.overflow auto }, handleKeydown(e) { if (e.key Escape this.showLightbox) { this.closeLightbox() } } } } /script该模式的核心是SSR/静态渲染时isClientSide为false输出回退视图mounted后切换为增强视图noscript则保证在框架 JS 完全未执行时也有说明性内容。noscript 的 CSS 样式方案/* Hide enhanced elements when JS is disabled */ .js-only { display: none; } /* Show enhanced elements when JS is available */ .js-enabled .js-only { display: block; } /* Style noscript notices */ noscript { display: block; background: #f0f0f0; border: 1px solid #ccc; padding: 1rem; margin: 1rem 0; border-radius: 4px; } .noscript-notice { background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 1rem; border-radius: 4px; margin: 1rem 0; } /* Progressive enhancement styles */ .enhanced-form { position: relative; } .enhanced-form .loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.8); display: flex; align-items: center; justify-content: center; display: none; } .enhanced-form.submitting .loading-overlay { display: flex; } /* Hide JS-dependent elements initially */ .char-counter, .real-time-validation, .auto-save-indicator { opacity: 0; transition: opacity 0.3s; } /* Show them when JS loads */ .js-loaded .char-counter, .js-loaded .real-time-validation, .js-loaded .auto-save-indicator { opacity: 1; }样式层面要点依赖 JS 的元素默认display: none.js-onlyJS 可用时通过根元素类名.js-enabled恢复显示noscript与.noscript-notice采用醒目的提示样式帮助无 JS 用户理解页面状态字符计数、实时校验等增强 UI 默认透明JS 加载后.js-loaded淡入避免闪烁。十条最佳实践提供有意义的回退不要只显示JavaScript is requiredJavaScript 必需保持核心功能可用核心功能应能在无 JavaScript 时工作渐进增强从可用的 HTML 开始用 JS 增强清晰沟通说明哪些功能依赖 JavaScript无 JS 测试定期在禁用 JS 的情况下测试站点使用语义化 HTML以原生 HTML 功能为基础服务端处理确保表单与关键功能可在服务端工作优雅降级增强功能失败时应优雅降级使用特性检测基于受支持的 API 分支而不是 UA 字符串保持 HTML 权威重要内容、链接与分页必须在 hydration 之前存在。测试 noscript 实现浏览器端测试// Programmatically disable JavaScript for testing Object.defineProperty(window, navigator, { value: { ...window.navigator, javaEnabled: () false } }); // Simulate noscript environment document.querySelectorAll(script).forEach(script { script.remove(); });自动化测试Puppeteer 禁用 JS// Puppeteer test with JavaScript disabled const puppeteer require(puppeteer); async function testNoScript() { const browser await puppeteer.launch(); const page await browser.newPage(); // Disable JavaScript await page.setJavaScriptEnabled(false); await page.goto(http://localhost:3000); // Test form submission await page.type(input[nameemail], testexample.com); await page.click(button[typesubmit]); // Verify redirect to success page await page.waitForNavigation(); const url page.url(); console.log(Redirected to:, url); await browser.close(); }验证清单Verification自动化检查测试一条主路径和一条受改动影响的边界路径尽可能使用浏览器或 CI 工具验证修复复查共享抽象shared abstractions确保修复被一致应用。手动检查在最终渲染输出或运行时行为中确认规则生效禁用 JavaScript确认主要内容、导航与分页仅凭 HTML 即可访问对比服务端渲染的 HTML 与 hydration 后的 UI确认标题、计数或正文在挂载时没有意外变化。规则在 Front-End-Checklist 项目中的落地方式Front-End-Checklist 将每一条前端检查规则同时沉淀为规则 MDX与Agent Skill两种形态规则本体packages/content/rules/en/html/noscript-tag.mdx 是权威规则源包含 title、description、categorieshtml / accessibility、subcategorycomponents、priority、difficulty、estimatedTime以及面向 LLM 的 promptscheck / fix / explain / codeReview与 aiContextSkill 封装SKILL.md 是面向 Agent 的精简入口其 frontmatter 要求 description 以 Use when 开头以便 Agent 进行意图匹配正文提供 Quick Reference并指引 Agent 在审查模板、服务端渲染 HTML 与共享组件时校验最终浏览器可见的标记而不是源码层的框架抽象完整参考文档references/rule.md 承载了本指南引用的全部代码示例、框架实现、测试与验证细节生成机制从仓库脚本 scripts/generate/generate-skills.ts 的注释可见每个规则 MDX 的 frontmatter 会生成对应的 skill 目录命令pnpm generate:skills可批量生成全部规则 skill也可针对单个.mdx文件生成由 lefthook 在提交时触发输出采用扁平结构skills/{skillName}/其中references/rule.md链接回规则文档。审查人员在使用该 Skill 时应遵循 SKILL.md 的四个动作Check验证页面是否为禁用/不可用 JS 的用户提供了合适回退、核心功能是否可访问、重要内容是否在初始 HTML 中而非阻塞于 hydration、Fix为关键功能添加替代内容、用特性检测替换 UA 嗅探、确保服务端 HTML 已包含重要内容与分页路径、Explain说明 noscript 回退对可访问性与渐进增强的价值、特性检测为何比 UA 嗅探更安全、hydration 不匹配为何会破坏渐进增强、Code Review审查模板与服务端渲染 HTML指出具体违反规则的元素、属性与路由。结语noscript元素是构建真正可访问、健壮 Web 应用的基础能力——它确保无论用户的 JavaScript 支持情况如何站点都能工作。结合本文的完整示例、框架实现与验证方法你可以在自己的项目中落实HTML 优先、JS 增强的渐进增强策略核心内容始终存在于初始 HTML增强能力在 JS 可用时渐进加载而noscript回退则兜底所有 JS 不可用的场景。【免费下载链接】Front-End-Checklist The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表