name: 最佳实践 description: 应用现代网页开发最佳实践,涵盖安全、兼容性和代码质量。 license: MIT metadata: author: web-quality-skills version: ‘1.0’
最佳实践
基于Lighthouse最佳实践审核的现代网页开发标准。涵盖安全、浏览器兼容性和代码质量模式。
安全
HTTPS无处不在
强制执行HTTPS:
<!-- ❌ 混合内容 -->
<img src="http://example.com/image.jpg" />
<script src="http://cdn.example.com/script.js"></script>
<!-- ✅ 仅HTTPS -->
<img src="https://example.com/image.jpg" />
<script src="https://cdn.example.com/script.js"></script>
<!-- ✅ 协议相对(使用页面协议) -->
<img src="//example.com/image.jpg" />
HSTS头:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
内容安全策略 (CSP)
<!-- 通过meta标签的基本CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;"
/>
<!-- 更好:HTTP头 -->
CSP头(推荐):
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123' https://trusted.com;
style-src 'self' 'nonce-abc123';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
使用nonce处理内联脚本:
<script nonce="abc123">
// 这个内联脚本被允许
</script>
安全头
# 防止点击劫持
X-Frame-Options: DENY
# 防止MIME类型嗅探
X-Content-Type-Options: nosniff
# 启用XSS过滤器(旧浏览器)
X-XSS-Protection: 1; mode=block
# 控制引用信息
Referrer-Policy: strict-origin-when-cross-origin
# 权限策略(原Feature-Policy)
Permissions-Policy: geolocation=(), microphone=(), camera=()
无漏洞库
# 检查漏洞
npm audit
yarn audit
# 自动修复可能的问题
npm audit fix
# 检查特定包
npm ls lodash
保持依赖更新:
// package.json
{
"scripts": {
"audit": "npm audit --audit-level=moderate",
"update": "npm update && npm audit fix"
}
}
避免已知漏洞模式:
// ❌ 原型污染漏洞模式
Object.assign(target, userInput)
_.merge(target, userInput)
// ✅ 更安全的替代方法
const safeData = JSON.parse(JSON.stringify(userInput))
输入消毒
// ❌ XSS漏洞
element.innerHTML = userInput
document.write(userInput)
// ✅ 安全文本内容
element.textContent = userInput
// ✅ 如果需要HTML,消毒
import DOMPurify from 'dompurify'
element.innerHTML = DOMPurify.sanitize(userInput)
安全cookie
// ❌ 不安全cookie
document.cookie = "session=abc123";
// ✅ 安全cookie(服务器端)
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/
浏览器兼容性
文档类型声明
<!-- ❌ 缺少或无效doctype -->
<html>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<!-- ✅ HTML5 doctype -->
<!DOCTYPE html>
<html lang="en"></html>
</html>
字符编码
<!-- ❌ 缺少或延迟字符集 -->
<html>
<head>
<title>页面</title>
<meta charset="UTF-8" />
</head>
<!-- ✅ 字符集作为head中第一个元素 -->
<html>
<head>
<meta charset="UTF-8" />
<title>页面</title>
</head>
</html>
</html>
视口meta标签
<!-- ❌ 缺少视口 -->
<head>
<title>页面</title>
</head>
<!-- ✅ 响应式视口 -->
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>页面</title>
</head>
功能检测
// ❌ 浏览器检测(脆弱)
if (navigator.userAgent.includes('Chrome')) {
// Chrome特定代码
}
// ✅ 功能检测
if ('IntersectionObserver' in window) {
// 使用IntersectionObserver
} else {
// 后备方案
}
// ✅ 在CSS中使用@supports
@supports (display: grid) {
.container {
display: grid;
}
}
@supports not (display: grid) {
.container {
display: flex;
}
}
补丁(如果需要)
<!-- 有条件加载补丁 -->
<script>
if (!('fetch' in window)) {
document.write('<script src="/polyfills/fetch.js"><\/script>')
}
</script>
<!-- 或使用polyfill.io -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,IntersectionObserver"></script>
已弃用API
避免这些
// ❌ document.write(阻塞解析)
document.write('<script src="..."></script>');
// ✅ 动态脚本加载
const script = document.createElement('script');
script.src = '...';
document.head.appendChild(script);
// ❌ 同步XHR(阻塞主线程)
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // false = 同步
// ✅ 异步fetch
const response = await fetch(url);
// ❌ 应用缓存(已弃用)
<html manifest="cache.manifest">
// ✅ 服务工作者
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
事件监听器被动模式
// ❌ 非被动触摸/滚轮(可能阻塞滚动)
element.addEventListener('touchstart', handler)
element.addEventListener('wheel', handler)
// ✅ 被动监听器(允许平滑滚动)
element.addEventListener('touchstart', handler, { passive: true })
element.addEventListener('wheel', handler, { passive: true })
// ✅ 如果需要preventDefault,明确指定
element.addEventListener('touchstart', handler, { passive: false })
控制台和错误
无控制台错误
// ❌ 生产环境中的错误
console.log('调试信息') // 在生产环境中移除
throw new Error('未处理') // 捕获所有错误
// ✅ 适当错误处理
try {
riskyOperation()
} catch (error) {
// 记录到错误跟踪服务
errorTracker.captureException(error)
// 显示用户友好消息
showErrorMessage('出错了。请重试。')
}
错误边界(React)
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, info) {
errorTracker.captureException(error, { extra: info })
}
render() {
if (this.state.hasError) {
return <FallbackUI />
}
return this.props.children
}
}
// 用法
;<ErrorBoundary>
<App />
</ErrorBoundary>
全局错误处理程序
// 捕获未处理错误
window.addEventListener('error', (event) => {
errorTracker.captureException(event.error)
})
// 捕获未处理的promise拒绝
window.addEventListener('unhandledrejection', (event) => {
errorTracker.captureException(event.reason)
})
源映射
生产配置
// ❌ 生产环境中暴露源映射
// webpack.config.js
module.exports = {
devtool: 'source-map', // 暴露源代码
}
// ✅ 隐藏源映射(上传到错误跟踪器)
module.exports = {
devtool: 'hidden-source-map',
}
// ✅ 或生产环境中无源映射
module.exports = {
devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
}
性能最佳实践
避免阻塞模式
// ❌ 阻塞脚本
<script src="heavy-library.js"></script>
// ✅ 延迟脚本
<script defer src="heavy-library.js"></script>
// ❌ 阻塞CSS导入
@import url('other-styles.css');
// ✅ 链接标签(并行加载)
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="other-styles.css">
高效事件处理程序
// ❌ 每个元素上的处理程序
items.forEach((item) => {
item.addEventListener('click', handleClick)
})
// ✅ 事件委托
container.addEventListener('click', (e) => {
if (e.target.matches('.item')) {
handleClick(e)
}
})
内存管理
// ❌ 内存泄漏(从未移除)
const handler = () => {
/* ... */
}
window.addEventListener('resize', handler)
// ✅ 完成后清理
const handler = () => {
/* ... */
}
window.addEventListener('resize', handler)
// 稍后,当组件卸载时:
window.removeEventListener('resize', handler)
// ✅ 使用AbortController
const controller = new AbortController()
window.addEventListener('resize', handler, { signal: controller.signal })
// 清理:
controller.abort()
代码质量
有效HTML
<!-- ❌ 无效HTML -->
<div id="header">
<div id="header">
<!-- 重复ID -->
<ul>
<div>项目</div>
<!-- 无效子元素 -->
</ul>
<a href="/"><button>点击</button></a>
<!-- 无效嵌套 -->
<!-- ✅ 有效HTML -->
<header id="site-header"></header>
<ul>
<li>项目</li>
</ul>
<a href="/" class="button">点击</a>
</div>
</div>
语义HTML
<!-- ❌ 非语义 -->
<div class="header">
<div class="nav">
<div class="nav-item">首页</div>
</div>
</div>
<div class="main">
<div class="article">
<div class="title">标题</div>
</div>
</div>
<!-- ✅ 语义HTML5 -->
<header>
<nav>
<a href="/">首页</a>
</nav>
</header>
<main>
<article>
<h1>标题</h1>
</article>
</main>
图像宽高比
<!-- ❌ 扭曲图像 -->
<img src="photo.jpg" width="300" height="100" />
<!-- 如果实际比例是4:3,这会压缩图像 -->
<!-- ✅ 保持宽高比 -->
<img src="photo.jpg" width="300" height="225" />
<!-- 实际4:3尺寸 -->
<!-- ✅ 使用CSS object-fit实现灵活性 -->
<img src="photo.jpg" style="width: 300px; height: 200px; object-fit: cover;" />
权限和隐私
正确请求权限
// ❌ 页面加载时请求(不良UX,常被拒绝)
navigator.geolocation.getCurrentPosition(success, error)
// ✅ 在上下文中请求,用户操作后
findNearbyButton.addEventListener('click', async () => {
// 解释为何需要
if (await showPermissionExplanation()) {
navigator.geolocation.getCurrentPosition(success, error)
}
})
权限策略
<!-- 限制强大功能 -->
<meta http-equiv="Permissions-Policy" content="geolocation=(), camera=(), microphone=()" />
<!-- 或允许特定源 -->
<meta http-equiv="Permissions-Policy" content="geolocation=(self 'https://maps.example.com')" />
审核清单
安全(关键)
- [ ] HTTPS启用,无混合内容
- [ ] 无漏洞依赖(
npm audit) - [ ] CSP头配置
- [ ] 安全头存在
- [ ] 无暴露源映射
兼容性
- [ ] 有效HTML5 doctype
- [ ] 字符集在head中首先声明
- [ ] 视口meta标签存在
- [ ] 无使用已弃用API
- [ ] 滚动/触摸事件使用被动事件监听器
代码质量
- [ ] 无控制台错误
- [ ] 有效HTML(无重复ID)
- [ ] 使用语义HTML元素
- [ ] 适当错误处理
- [ ] 组件中内存清理
UX
- [ ] 无侵入性插页
- [ ] 权限请求在上下文中
- [ ] 清晰错误消息
- [ ] 适当图像宽高比
工具
| 工具 | 用途 |
|---|---|
npm audit |
依赖漏洞检查 |
| SecurityHeaders.com | 头分析 |
| W3C Validator | HTML验证 |
| Lighthouse | 最佳实践审核 |
| Observatory | 安全扫描 |