响应式设计完全指南
更新时间:2025-02
目录
什么是响应式设计
响应式设计(Responsive Web Design,RWD)是一种网页设计方法,使网站能够在不同设备和屏幕尺寸上提供最佳的浏览体验。
核心原则
javascript
// 响应式设计三大核心原则
const responsivePrinciples = {
流式布局: {
描述: '使用相对单位(%、vw、vh)而非固定单位(px)',
示例: 'width: 100%; max-width: 1200px;'
},
弹性图片: {
描述: '图片能够根据容器大小自动缩放',
示例: 'img { max-width: 100%; height: auto; }'
},
媒体查询: {
描述: '根据设备特性应用不同的样式',
示例: '@media (max-width: 768px) { ... }'
}
}
// 响应式设计的优势
const advantages = {
用户体验: '在任何设备上都能获得良好体验',
SEO友好: 'Google 推荐的移动优化方案',
维护成本低: '一套代码适配所有设备',
未来友好: '能够适应新的设备和屏幕尺寸'
}移动端适配方案
1. Viewport 设置
html
<!-- 基础 viewport 设置 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 完整的 viewport 设置 -->
<meta name="viewport" content="
width=device-width,
initial-scale=1.0,
maximum-scale=1.0,
minimum-scale=1.0,
user-scalable=no,
viewport-fit=cover
">
<!-- viewport 属性说明 -->
<!--
width: 视口宽度(device-width 表示设备宽度)
initial-scale: 初始缩放比例
maximum-scale: 最大缩放比例
minimum-scale: 最小缩放比例
user-scalable: 是否允许用户缩放(yes/no)
viewport-fit: 视口适配(cover 表示覆盖整个屏幕,包括刘海区域)
-->2. rem 适配方案
javascript
// rem 适配原理
// 1rem = 根元素(html)的 font-size
// 方案 1:动态设置 rem 基准值
(function() {
// 设计稿宽度
const designWidth = 750
// 基准字体大小
const baseFontSize = 100
function setRem() {
// 获取当前设备宽度
const deviceWidth = document.documentElement.clientWidth || window.innerWidth
// 计算 rem 基准值
const rem = (deviceWidth / designWidth) * baseFontSize
// 设置根元素字体大小
document.documentElement.style.fontSize = rem + 'px'
}
// 初始化
setRem()
// 监听窗口大小变化
window.addEventListener('resize', setRem)
window.addEventListener('orientationchange', setRem)
})()
// 使用示例
// 设计稿:750px
// 元素宽度:375px
// 转换:375 / 100 = 3.75rem
// CSS
.box {
width: 3.75rem; /* 375px */
height: 2rem; /* 200px */
font-size: 0.16rem; /* 16px */
}
// 方案 2:使用 postcss-pxtorem 自动转换
// postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 100, // 根元素字体大小
propList: ['*'], // 需要转换的属性
selectorBlackList: ['.no-rem'], // 不转换的选择器
minPixelValue: 2 // 最小转换值
}
}
}
// CSS(编译前)
.box {
width: 375px;
height: 200px;
font-size: 16px;
}
// CSS(编译后)
.box {
width: 3.75rem;
height: 2rem;
font-size: 0.16rem;
}3. vw/vh 适配方案
css
/* vw/vh 适配原理 */
/* 1vw = 视口宽度的 1% */
/* 1vh = 视口高度的 1% */
/* 设计稿:750px */
/* 元素宽度:375px */
/* 转换:375 / 750 * 100 = 50vw */
.box {
width: 50vw; /* 375px */
height: 26.67vw; /* 200px */
font-size: 2.13vw; /* 16px */
}
/* 使用 postcss-px-to-viewport 自动转换 */
/* postcss.config.js */
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 750, // 设计稿宽度
viewportHeight: 1334, // 设计稿高度
unitPrecision: 5, // 转换精度
viewportUnit: 'vw', // 转换单位
selectorBlackList: ['.no-vw'], // 不转换的选择器
minPixelValue: 1, // 最小转换值
mediaQuery: false // 是否转换媒体查询中的 px
}
}
}
/* CSS(编译前) */
.box {
width: 375px;
height: 200px;
font-size: 16px;
}
/* CSS(编译后) */
.box {
width: 50vw;
height: 26.66667vw;
font-size: 2.13333vw;
}
/* vw/vh 的优势 */
/* 1. 不需要 JavaScript */
/* 2. 计算简单 */
/* 3. 兼容性好 */
/* vw/vh 的问题 */
/* 1. 无法设置最大最小值 */
/* 解决方案:结合 calc() 和 clamp() */
.box {
/* 最小 16px,最大 24px */
font-size: clamp(16px, 2.13vw, 24px);
/* 或使用 calc() + min/max */
font-size: min(max(16px, 2.13vw), 24px);
}4. flexible 方案(淘宝)
javascript
// lib-flexible 原理
(function(win, lib) {
const doc = win.document
const docEl = doc.documentElement
let metaEl = doc.querySelector('meta[name="viewport"]')
let flexibleEl = doc.querySelector('meta[name="flexible"]')
let dpr = 0
let scale = 0
let tid
// 获取 dpr
if (metaEl) {
const match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/)
if (match) {
scale = parseFloat(match[1])
dpr = parseInt(1 / scale)
}
} else if (flexibleEl) {
const content = flexibleEl.getAttribute('content')
if (content) {
const initialDpr = content.match(/initial\-dpr=([\d\.]+)/)
const maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/)
if (initialDpr) {
dpr = parseFloat(initialDpr[1])
scale = parseFloat((1 / dpr).toFixed(2))
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1])
scale = parseFloat((1 / dpr).toFixed(2))
}
}
}
if (!dpr && !scale) {
const isAndroid = win.navigator.appVersion.match(/android/gi)
const isIPhone = win.navigator.appVersion.match(/iphone/gi)
const devicePixelRatio = win.devicePixelRatio
if (isIPhone) {
// iOS 下,对于 2 和 3 的屏,用 2 倍的方案,其余的用 1 倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2
} else {
dpr = 1
}
} else {
// 其他设备下,仍旧使用 1 倍的方案
dpr = 1
}
scale = 1 / dpr
}
docEl.setAttribute('data-dpr', dpr)
if (!metaEl) {
metaEl = doc.createElement('meta')
metaEl.setAttribute('name', 'viewport')
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no')
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl)
} else {
const wrap = doc.createElement('div')
wrap.appendChild(metaEl)
doc.write(wrap.innerHTML)
}
}
function refreshRem() {
let width = docEl.getBoundingClientRect().width
if (width / dpr > 540) {
width = 540 * dpr
}
const rem = width / 10
docEl.style.fontSize = rem + 'px'
lib.rem = win.rem = rem
}
win.addEventListener('resize', function() {
clearTimeout(tid)
tid = setTimeout(refreshRem, 300)
}, false)
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid)
tid = setTimeout(refreshRem, 300)
}
}, false)
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px'
} else {
doc.addEventListener('DOMContentLoaded', function() {
doc.body.style.fontSize = 12 * dpr + 'px'
}, false)
}
refreshRem()
lib.dpr = win.dpr = dpr
lib.refreshRem = refreshRem
lib.rem2px = function(d) {
let val = parseFloat(d) * this.rem
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px'
}
return val
}
lib.px2rem = function(d) {
let val = parseFloat(d) / this.rem
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem'
}
return val
}
})(window, window['lib'] || (window['lib'] = {}))
// 使用示例
// HTML
<html data-dpr="2">
<head>
<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
</head>
<body>
<div class="box"></div>
</body>
</html>
// CSS
.box {
width: 3.75rem; /* 375px */
height: 2rem; /* 200px */
font-size: 0.16rem; /* 16px */
}
/* 根据 dpr 设置不同的字体大小 */
[data-dpr="1"] .box { font-size: 16px; }
[data-dpr="2"] .box { font-size: 32px; }
[data-dpr="3"] .box { font-size: 48px; }5. 适配方案对比
javascript
// 适配方案对比
const comparisonTable = {
rem: {
原理: '动态设置根元素字体大小',
优点: [
'兼容性好',
'可以设置最大最小值',
'计算简单'
],
缺点: [
'需要 JavaScript',
'首屏可能闪烁'
],
适用场景: '大部分移动端项目'
},
vw_vh: {
原理: '使用视口单位',
优点: [
'不需要 JavaScript',
'计算简单',
'兼容性好'
],
缺点: [
'无法设置最大最小值(需要配合 clamp)',
'某些浏览器有兼容性问题'
],
适用场景: '简单的移动端项目'
},
flexible: {
原理: 'rem + dpr',
优点: [
'解决 1px 问题',
'适配各种屏幕',
'淘宝方案,成熟稳定'
],
缺点: [
'需要 JavaScript',
'配置复杂'
],
适用场景: '对视觉要求高的项目'
},
媒体查询: {
原理: '根据屏幕宽度应用不同样式',
优点: [
'灵活',
'可以针对不同设备定制'
],
缺点: [
'代码量大',
'维护成本高'
],
适用场景: '响应式网站'
}
}媒体查询最佳实践
1. 常用断点
css
/* 移动优先(Mobile First) */
/* 默认样式(移动端) */
.container {
width: 100%;
padding: 0 15px;
}
/* 平板(≥768px) */
@media (min-width: 768px) {
.container {
max-width: 750px;
margin: 0 auto;
}
}
/* 桌面(≥992px) */
@media (min-width: 992px) {
.container {
max-width: 970px;
}
}
/* 大屏(≥1200px) */
@media (min-width: 1200px) {
.container {
max-width: 1170px;
}
}
/* 超大屏(≥1400px) */
@media (min-width: 1400px) {
.container {
max-width: 1320px;
}
}
/* 桌面优先(Desktop First) */
/* 默认样式(桌面) */
.container {
max-width: 1170px;
margin: 0 auto;
}
/* 平板(≤992px) */
@media (max-width: 992px) {
.container {
max-width: 750px;
}
}
/* 移动端(≤768px) */
@media (max-width: 768px) {
.container {
width: 100%;
padding: 0 15px;
}
}
/* Bootstrap 断点 */
/* xs: <576px */
/* sm: ≥576px */
/* md: ≥768px */
/* lg: ≥992px */
/* xl: ≥1200px */
/* xxl: ≥1400px */
/* Tailwind CSS 断点 */
/* sm: 640px */
/* md: 768px */
/* lg: 1024px */
/* xl: 1280px */
/* 2xl: 1536px */2. 媒体查询类型
css
/* 1. 屏幕宽度 */
@media (min-width: 768px) { }
@media (max-width: 768px) { }
@media (min-width: 768px) and (max-width: 992px) { }
/* 2. 屏幕高度 */
@media (min-height: 600px) { }
@media (max-height: 600px) { }
/* 3. 设备方向 */
@media (orientation: portrait) { /* 竖屏 */ }
@media (orientation: landscape) { /* 横屏 */ }
/* 4. 设备像素比 */
@media (-webkit-min-device-pixel-ratio: 2),
(min-resolution: 192dpi) {
/* Retina 屏幕 */
}
/* 5. 颜色深度 */
@media (min-color: 8) { }
/* 6. 悬停能力 */
@media (hover: hover) {
/* 支持悬停(鼠标) */
.button:hover {
background: blue;
}
}
@media (hover: none) {
/* 不支持悬停(触摸屏) */
.button:active {
background: blue;
}
}
/* 7. 指针精度 */
@media (pointer: fine) {
/* 精确指针(鼠标) */
.button {
padding: 5px 10px;
}
}
@media (pointer: coarse) {
/* 粗糙指针(手指) */
.button {
padding: 10px 20px;
}
}
/* 8. 深色模式 */
@media (prefers-color-scheme: dark) {
body {
background: #000;
color: #fff;
}
}
@media (prefers-color-scheme: light) {
body {
background: #fff;
color: #000;
}
}
/* 9. 减少动画 */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
/* 10. 打印样式 */
@media print {
body {
font-size: 12pt;
}
.no-print {
display: none;
}
}3. 容器查询(Container Queries)
css
/* 容器查询(CSS Container Queries) */
/* 根据容器大小而非视口大小应用样式 */
/* 定义容器 */
.container {
container-type: inline-size; /* 或 size、normal */
container-name: sidebar; /* 可选 */
}
/* 容器查询 */
@container (min-width: 400px) {
.card {
display: flex;
}
}
/* 命名容器查询 */
@container sidebar (min-width: 400px) {
.card {
display: flex;
}
}
/* 实际示例 */
.sidebar {
container-type: inline-size;
}
.card {
display: block;
}
/* 当容器宽度 ≥ 400px 时 */
@container (min-width: 400px) {
.card {
display: flex;
gap: 1rem;
}
.card-image {
width: 40%;
}
.card-content {
width: 60%;
}
}
/* 容器查询单位 */
/* cqw: 容器宽度的 1% */
/* cqh: 容器高度的 1% */
/* cqi: 容器内联尺寸的 1% */
/* cqb: 容器块尺寸的 1% */
/* cqmin: cqi 和 cqb 中较小的值 */
/* cqmax: cqi 和 cqb 中较大的值*/
.card-title {
font-size: clamp(1rem, 5cqi, 2rem);
}响应式图片
1. srcset 和 sizes
html
<!-- 基础用法 -->
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt="响应式图片"
/>
<!-- srcset 说明 -->
<!-- 400w 表示图片宽度为 400px -->
<!-- 浏览器会根据设备像素比和 sizes 选择合适的图片 -->
<!-- sizes 说明 -->
<!-- (max-width: 600px) 400px: 视口宽度 ≤600px 时,图片显示宽度为 400px -->
<!-- (max-width: 1000px) 800px: 视口宽度 ≤1000px 时,图片显示宽度为 800px -->
<!-- 1200px: 默认显示宽度为 1200px -->
<!-- 使用 x 描述符(设备像素比) -->
<img
src="image-1x.jpg"
srcset="
image-1x.jpg 1x,
image-2x.jpg 2x,
image-3x.jpg 3x
"
alt="响应式图片"
/>
<!-- 完整示例 -->
<img
src="fallback.jpg"
srcset="
small.jpg 300w,
medium.jpg 600w,
large.jpg 1200w
"
sizes="
(max-width: 320px) 280px,
(max-width: 640px) 580px,
1000px
"
alt="响应式图片"
loading="lazy"
/>2. picture 元素
html
<!-- 基础用法 -->
<picture>
<source srcset="image.webp" type="image/webp" />
<source srcset="image.jpg" type="image/jpeg" />
<img src="image.jpg" alt="图片" />
</picture>
<!-- 根据屏幕宽度选择图片 -->
<picture>
<source media="(max-width: 600px)" srcset="small.jpg" />
<source media="(max-width: 1200px)" srcset="medium.jpg" />
<img src="large.jpg" alt="图片" />
</picture>
<!-- 结合 srcset -->
<picture>
<source
media="(max-width: 600px)"
srcset="small-1x.jpg 1x, small-2x.jpg 2x"
/>
<source
media="(max-width: 1200px)"
srcset="medium-1x.jpg 1x, medium-2x.jpg 2x"
/>
<img
src="large-1x.jpg"
srcset="large-1x.jpg 1x, large-2x.jpg 2x"
alt="图片"
/>
</picture>
<!-- 艺术指导(Art Direction) -->
<!-- 不同屏幕显示不同裁剪的图片 -->
<picture>
<!-- 移动端:竖版图片 -->
<source
media="(max-width: 600px)"
srcset="portrait.jpg"
/>
<!-- 桌面端:横版图片 -->
<source
media="(min-width: 601px)"
srcset="landscape.jpg"
/>
<img src="landscape.jpg" alt="图片" />
</picture>
<!-- 深色模式 -->
<picture>
<source
srcset="dark.jpg"
media="(prefers-color-scheme: dark)"
/>
<img src="light.jpg" alt="图片" />
</picture>3. CSS 响应式背景图
css
/* 基础用法 */
.hero {
background-image: url('small.jpg');
}
@media (min-width: 768px) {
.hero {
background-image: url('medium.jpg');
}
}
@media (min-width: 1200px) {
.hero {
background-image: url('large.jpg');
}
}
/* 使用 image-set() */
.hero {
background-image: image-set(
url('image-1x.jpg') 1x,
url('image-2x.jpg') 2x,
url('image-3x.jpg') 3x
);
}
/* WebP 支持检测 */
.hero {
background-image: url('image.jpg');
}
@supports (background-image: image-set(url('image.webp') type('image/webp'))) {
.hero {
background-image: image-set(
url('image.webp') type('image/webp'),
url('image.jpg') type('image/jpeg')
);
}
}
/* 深色模式 */
.hero {
background-image: url('light.jpg');
}
@media (prefers-color-scheme: dark) {
.hero {
background-image: url('dark.jpg');
}
}4. 图片懒加载
html
<!-- 原生懒加载 -->
<img src="image.jpg" alt="图片" loading="lazy" />
<!-- 浏览器支持检测 -->
<script>
if ('loading' in HTMLImageElement.prototype) {
// 支持原生懒加载
const images = document.querySelectorAll('img[loading="lazy"]')
images.forEach(img => {
img.src = img.dataset.src
})
} else {
// 不支持,使用 polyfill
const script = document.createElement('script')
script.src = 'lazysizes.min.js'
document.body.appendChild(script)
}
</script>
<!-- 使用 Intersection Observer -->
<img data-src="image.jpg" alt="图片" class="lazy" />
<script>
const lazyImages = document.querySelectorAll('.lazy')
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.remove('lazy')
observer.unobserve(img)
}
})
})
lazyImages.forEach(img => imageObserver.observe(img))
</script>移动端性能优化
1. 首屏优化
javascript
// 1. 关键资源内联
// 将关键 CSS 内联到 HTML 中
<style>
/* 关键 CSS */
body { margin: 0; font-family: sans-serif; }
.header { height: 60px; background: #fff; }
</style>
// 2. 预加载关键资源
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="hero.jpg" as="image">
// 3. 延迟加载非关键资源
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
// 4. 使用骨架屏
const SkeletonScreen = () => {
return (
<div className="skeleton">
<div className="skeleton-header"></div>
<div className="skeleton-content">
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
</div>
</div>
)
}
// CSS
.skeleton {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
// 5. 代码分割
// React
const LazyComponent = React.lazy(() => import('./Component'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
)
}
// Vue
const AsyncComponent = () => import('./Component.vue')
export default {
components: {
AsyncComponent
}
}2. 图片优化
javascript
// 1. 使用 WebP 格式
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="图片">
</picture>
// 2. 图片压缩
// 使用 imagemin
const imagemin = require('imagemin')
const imageminWebp = require('imagemin-webp')
await imagemin(['images/*.{jpg,png}'], {
destination: 'build/images',
plugins: [
imageminWebp({ quality: 75 })
]
})
// 3. 响应式图片
<img
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
src="medium.jpg"
alt="图片"
>
// 4. 懒加载
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">
// 5. 使用 CDN
<img src="https://cdn.example.com/image.jpg" alt="图片">3. 网络优化
javascript
// 1. 使用 HTTP/2
// 服务器配置(Nginx)
server {
listen 443 ssl http2;
# ...
}
// 2. 启用压缩
// Gzip
server {
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
}
// Brotli
server {
brotli on;
brotli_types text/plain text/css application/json application/javascript;
}
// 3. 使用 Service Worker 缓存
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'/script.js',
'/image.jpg'
])
})
)
})
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
})
)
})
// 4. 预连接
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
// 5. 资源提示
<link rel="prefetch" href="next-page.html">
<link rel="prerender" href="next-page.html">PWA 渐进式 Web 应用
1. Manifest 配置
json
// manifest.json
{
"name": "我的应用",
"short_name": "应用",
"description": "这是一个 PWA 应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3eaf7c",
"orientation": "portrait",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}html
<!-- HTML 引入 -->
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#3eaf7c">
<link rel="apple-touch-icon" href="/icon-192.png">2. Service Worker
javascript
// 注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW 注册成功:', registration)
})
.catch(error => {
console.log('SW 注册失败:', error)
})
})
}
// sw.js
const CACHE_NAME = 'my-app-v1'
const urlsToCache = [
'/',
'/styles.css',
'/script.js',
'/image.jpg'
]
// 安装
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(urlsToCache)
})
)
})
// 激活
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName)
}
})
)
})
)
})
// 拦截请求
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// 缓存命中
if (response) {
return response
}
// 网络请求
return fetch(event.request).then((response) => {
// 检查是否是有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response
}
// 克隆响应
const responseToCache = response.clone()
// 缓存响应
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache)
})
return response
})
})
)
})3. 离线功能
javascript
// 离线页面
// sw.js
const OFFLINE_URL = '/offline.html'
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.add(OFFLINE_URL))
)
})
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.match(event.request)
.then((response) => {
return response || caches.match(OFFLINE_URL)
})
})
)
})
// offline.html
<!DOCTYPE html>
<html>
<head>
<title>离线</title>
</head>
<body>
<h1>您当前处于离线状态</h1>
<p>请检查网络连接</p>
</body>
</html>4. 推送通知
javascript
// 请求通知权限
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
console.log('通知权限已授予')
}
})
// 订阅推送
navigator.serviceWorker.ready.then((registration) => {
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'YOUR_PUBLIC_KEY'
}).then((subscription) => {
console.log('订阅成功:', subscription)
// 发送订阅信息到服务器
fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
})
})
})
// sw.js - 接收推送
self.addEventListener('push', (event) => {
const data = event.data.json()
const options = {
body: data.body,
icon: '/icon-192.png',
badge: '/badge.png',
data: {
url: data.url
}
}
event.waitUntil(
self.registration.showNotification(data.title, options)
)
})
// 点击通知
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(
clients.openWindow(event.notification.data.url)
)
})响应式布局技巧
1. Flexbox 布局
css
/* 基础 Flex 布局 */
.container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.item {
flex: 1 1 300px; /* flex-grow flex-shrink flex-basis */
}
/* 响应式 Flex 布局 */
.container {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.container {
flex-direction: row;
}
}
/* 自适应卡片布局 */
.cards {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card {
flex: 1 1 calc(33.333% - 1rem);
min-width: 250px;
}
@media (max-width: 768px) {
.card {
flex: 1 1 calc(50% - 1rem);
}
}
@media (max-width: 480px) {
.card {
flex: 1 1 100%;
}
}2. Grid 布局
css
/* 基础 Grid 布局 */
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
/* 响应式 Grid 布局 */
.container {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.container {
grid-template-columns: repeat(3, 1fr);
}
}
/* 自适应 Grid 布局 */
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
/* auto-fit vs auto-fill */
/* auto-fit: 拉伸列以填充空间 */
/* auto-fill: 保持列宽,留空 */
/* Grid 区域布局 */
.container {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
grid-template-columns: 200px 1fr 1fr;
gap: 1rem;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
@media (max-width: 768px) {
.container {
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
grid-template-columns: 1fr;
}
}3. 多列布局
css
/* CSS 多列布局 */
.container {
column-count: 3;
column-gap: 2rem;
column-rule: 1px solid #ddd;
}
/* 响应式多列 */
.container {
column-count: 1;
}
@media (min-width: 768px) {
.container {
column-count: 2;
}
}
@media (min-width: 1024px) {
.container {
column-count: 3;
}
}
/* 或使用 column-width */
.container {
column-width: 250px;
column-gap: 2rem;
}
/* 防止元素被分割 */
.item {
break-inside: avoid;
}触摸优化
1. 触摸事件
javascript
// 基础触摸事件
element.addEventListener('touchstart', (e) => {
console.log('触摸开始', e.touches)
})
element.addEventListener('touchmove', (e) => {
console.log('触摸移动', e.touches)
})
element.addEventListener('touchend', (e) => {
console.log('触摸结束', e.changedTouches)
})
element.addEventListener('touchcancel', (e) => {
console.log('触摸取消')
})
// 滑动检测
class SwipeDetector {
constructor(element) {
this.element = element
this.startX = 0
this.startY = 0
this.threshold = 50 // 最小滑动距离
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this))
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this))
}
handleTouchStart(e) {
this.startX = e.touches[0].clientX
this.startY = e.touches[0].clientY
}
handleTouchEnd(e) {
const endX = e.changedTouches[0].clientX
const endY = e.changedTouches[0].clientY
const deltaX = endX - this.startX
const deltaY = endY - this.startY
// 水平滑动
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > this.threshold) {
if (deltaX > 0) {
this.onSwipeRight?.()
} else {
this.onSwipeLeft?.()
}
}
// 垂直滑动
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > this.threshold) {
if (deltaY > 0) {
this.onSwipeDown?.()
} else {
this.onSwipeUp?.()
}
}
}
}
// 使用示例
const detector = new SwipeDetector(document.querySelector('.swipeable'))
detector.onSwipeLeft = () => console.log('向左滑动')
detector.onSwipeRight = () => console.log('向右滑动')
detector.onSwipeUp = () => console.log('向上滑动')
detector.onSwipeDown = () => console.log('向下滑动')2. 触摸优化
css
/* 1. 增大触摸区域 */
.button {
min-width: 44px;
min-height: 44px;
padding: 12px 24px;
}
/* 2. 禁用点击延迟 */
* {
touch-action: manipulation;
}
/* 3. 禁用文本选择 */
.no-select {
user-select: none;
-webkit-user-select: none;
}
/* 4. 禁用长按菜单 */
.no-context-menu {
-webkit-touch-callout: none;
}
/* 5. 平滑滚动 */
.scroll-container {
-webkit-overflow-scrolling: touch;
overflow-y: scroll;
}
/* 6. 触摸反馈 */
.button {
transition: transform 0.1s;
}
.button:active {
transform: scale(0.95);
}
/* 7. 禁用双击缩放 */
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">3. 手势库
javascript
// 使用 Hammer.js
import Hammer from 'hammerjs'
const element = document.querySelector('.gesture')
const hammer = new Hammer(element)
// 滑动
hammer.on('swipeleft', () => console.log('向左滑动'))
hammer.on('swiperight', () => console.log('向右滑动'))
// 捏合
hammer.get('pinch').set({ enable: true })
hammer.on('pinch', (e) => {
console.log('捏合', e.scale)
})
// 旋转
hammer.get('rotate').set({ enable: true })
hammer.on('rotate', (e) => {
console.log('旋转', e.rotation)
})
// 双击
hammer.on('doubletap', () => console.log('双击'))
// 长按
hammer.on('press', () => console.log('长按'))常见问题
1. 1px 边框问题
css
/* 问题:在 Retina 屏幕上,1px 边框看起来很粗 */
/* 解决方案 1:使用 transform */
.border {
position: relative;
}
.border::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background: #000;
transform: scaleY(0.5);
transform-origin: 0 0;
}
/* 解决方案 2:使用 box-shadow */
.border {
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
}
/* 解决方案 3:使用 SVG */
.border {
border: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='1'%3E%3Cline x1='0' y1='0' x2='100%25' y2='0' stroke='%23000' stroke-width='1'/%3E%3C/svg%3E");
background-repeat: repeat-x;
background-position: bottom;
}
/* 解决方案 4:使用 viewport + rem */
/* 设置 viewport 的 scale 为 1/dpr */
<meta name="viewport" content="width=device-width, initial-scale=0.5">
.border {
border: 1px solid #000;
}2. 安全区域适配(刘海屏)
css
/* iOS 11+ 安全区域 */
body {
/* env() 函数获取安全区域 */
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
}
/* 或使用 constant()(iOS 11.0-11.2) */
body {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
/* 固定定位元素 */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 44px;
padding-top: env(safe-area-inset-top);
}
/* viewport-fit=cover */
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">3. 横竖屏切换
javascript
// 监听屏幕方向变化
window.addEventListener('orientationchange', () => {
if (window.orientation === 90 || window.orientation === -90) {
console.log('横屏')
} else {
console.log('竖屏')
}
})
// 使用 Screen Orientation API
screen.orientation.addEventListener('change', () => {
console.log('屏幕方向:', screen.orientation.type)
// portrait-primary, portrait-secondary
// landscape-primary, landscape-secondary
})
// 锁定屏幕方向
screen.orientation.lock('portrait').then(() => {
console.log('锁定为竖屏')
}).catch((error) => {
console.error('锁定失败:', error)
})
// CSS 媒体查询
@media (orientation: portrait) {
/* 竖屏样式 */
}
@media (orientation: landscape) {
/* 横屏样式 */
}最佳实践
1. 响应式设计清单
javascript
const responsiveChecklist = {
布局: [
'✅ 使用流式布局',
'✅ 使用 Flexbox 或 Grid',
'✅ 设置合理的断点',
'✅ 移动优先设计'
],
图片: [
'✅ 使用响应式图片',
'✅ 使用 WebP 格式',
'✅ 实现图片懒加载',
'✅ 压缩图片'
],
性能: [
'✅ 优化首屏加载',
'✅ 使用代码分割',
'✅ 启用缓存',
'✅ 使用 CDN'
],
交互: [
'✅ 增大触摸区域',
'✅ 优化触摸反馈',
'✅ 禁用点击延迟',
'✅ 实现手势操作'
],
兼容性: [
'✅ 测试多种设备',
'✅ 处理安全区域',
'✅ 解决 1px 问题',
'✅ 适配横竖屏'
]
}2. 性能优化建议
javascript
const performanceTips = {
关键渲染路径: [
'内联关键 CSS',
'延迟加载非关键 CSS',
'预加载关键资源',
'使用骨架屏'
],
资源优化: [
'压缩图片',
'使用 WebP',
'实现懒加载',
'使用 CDN'
],
代码优化: [
'代码分割',
'Tree Shaking',
'压缩代码',
'使用 HTTP/2'
],
缓存策略: [
'Service Worker',
'HTTP 缓存',
'本地存储',
'CDN 缓存'
]
}面试要点
核心概念
响应式设计三大原则
- 流式布局、弹性图片、媒体查询
移动端适配方案
- rem、vw/vh、flexible、媒体查询
响应式图片
- srcset、sizes、picture、懒加载
PWA
- Manifest、Service Worker、离线功能、推送通知
实战经验
如何实现移动端适配?
- 选择适配方案(rem/vw)
- 设置 viewport
- 使用 postcss 自动转换
- 处理 1px 问题
如何优化移动端性能?
- 首屏优化(骨架屏、代码分割)
- 图片优化(WebP、懒加载)
- 网络优化(HTTP/2、压缩、CDN)
- 使用 PWA
如何处理响应式图片?
- 使用 srcset 和 sizes
- 使用 picture 元素
- 实现懒加载
- 使用 WebP 格式
参考资料
官方文档
工具库
- postcss-pxtorem - px 转 rem
- postcss-px-to-viewport - px 转 vw
- lib-flexible - 淘宝适配方案
- Hammer.js - 手势库
学习资源
💡 提示:响应式设计是现代 Web 开发的基础,掌握移动端适配和性能优化技巧可以大大提升用户体验。