JavaScript 应用示例
控制页面滚动
页面滚动分两种:窗口滚动,以及设了 overflow: scroll 或 auto 的元素滚动。
JavaScript 里常用下面这些方法和属性。scroll()、scrollTo()、scrollBy() 既可以挂在 window 上,也可以挂在滚动元素上(Element.scrollTo 等)。
常用的滚动方法
scrollTo()
把滚动容器滚到指定坐标(相对该容器内容的左上角)。
window.scrollTo(x, y)
window.scrollTo(options)
element.scrollTo(x, y)
element.scrollTo(options)
scroll()
与 scrollTo() 相同,是同一个行为的别名。
window.scroll(x, y)
window.scroll(options)
scrollBy()
相对当前位置再滚一段距离。
window.scrollBy(x, y)
window.scrollBy(options)
x、y:水平、竖直方向上的像素。对scrollTo是目标坐标,对scrollBy是增量。options是ScrollToOptions:top:竖直方向的像素left:水平方向的像素behavior:smooth平滑滚;instant立刻跳;auto(默认)跟随该容器 CSS 的scroll-behavior,没有平滑声明时就是立刻跳。auto并不等同于永远instant。
scrollIntoView()
让当前元素滚进其最近滚动祖先的可视区域。
element.scrollIntoView()
element.scrollIntoView(alignToTop)
element.scrollIntoView(scrollIntoViewOptions)
无参等同于 scrollIntoView(true)。
alignToTop(布尔):true时元素顶边对齐可视区域顶边;false时元素底边对齐可视区域底边。scrollIntoViewOptions:behavior:auto或smooth,默认autoblock:竖直对齐,start、center、end、nearest,默认startinline:水平对齐,取值同上,默认nearest
常用的滚动属性
scrollLeft
读取或设置元素滚动条到内容左边的偏移。
element.scrollLeft
注意: 内容方向为 rtl 时,有的引擎里滚动条在右侧、起点处 scrollLeft 为 0,继续往内容末尾拖会变成负数。LTR 里设成小于 0 会被收成 0。实现并不完全统一,读写前要在目标引擎上确认。
其他边界:
- 内容没有溢出、不能滚时,值为
0 - LTR 下设成大于可滚最大值,会被收成最大值
窗口没有 scrollLeft。读窗口横向滚动用 window.scrollX(或 window.pageXOffset),也可以用 document.documentElement.scrollLeft。
scrollTop
读取或设置内容在竖直方向上已经卷过的像素:内容顶部到可见区域顶部的距离。
element.scrollTop
设值规则与 scrollLeft 类似。窗口用 window.scrollY(或 window.pageYOffset),或 document.documentElement.scrollTop。怪异模式里有的页面还写在 document.body.scrollTop 上。
其他方法
scrollByLines(): 按行数滚文档。非标准,目前主要是 Firefox。scrollByPages(): 按页数翻当前文档。非标准,目前主要是 Firefox。
注意问题
window.scrollTo、window.scroll、window.scrollBy很早就有。带options对象、以及元素上的element.scrollTo/element.scrollBy,IE 都不支持。- 部分旧机(例如华为麦芒 4,Android 6.0.1)上
element.scrollTo()无效,改设element.scrollLeft、element.scrollTop。 window没有scrollTop、scrollLeft,用document.documentElement或window.scrollY/window.scrollX。
蒙层底部页面跟随滚动
弹窗常用蒙层隔开页面交互。在蒙层里滑到尽头再继续滑,底层页面会跟着滚,这就是滚动穿透(scroll chaining)。
解决方案一
打开蒙层时给 body 加样式(有的机还要加在根元素上)。关掉时去掉这些样式。
body {
height: 100%;
overflow: hidden;
}
优点: 只改 CSS,没有脚本逻辑。
缺点: PC 上往往够用;部分 Android 和 Safari 仍挡不住底层滚动。
解决方案二
在蒙层上拦截触摸的默认滚动。
// node 为蒙层容器节点。必须 passive: false,否则 preventDefault 会被忽略
node.addEventListener("touchstart", (e) => {
e.preventDefault()
}, { passive: false })
蒙层自己也不会滚。内容不需要内滚时,这样足够。蒙层内部还要滚动时,就要判断是否已经滚到尽头:到尽头再 preventDefault,否则放行。逻辑会明显变长。
监听若写在 document 或 window 上,touchstart / touchmove 默认是被动的,不写 { passive: false } 时 preventDefault 无效。
解决方案三(锁住背景)
把页面用 position: fixed 钉在视口上,它就滚不了;关掉蒙层再松开,并回到原来的 scrollY。钉住之后内容会跳到视口顶部,所以要先记下滚动位置,写进 top。
let top = 0
function stopBodyScroll(isFixed) {
const bodyEl = document.body
if (isFixed) {
top = window.scrollY
bodyEl.style.position = "fixed"
bodyEl.style.top = -top + "px"
} else {
bodyEl.style.position = ""
bodyEl.style.top = ""
window.scrollTo(0, top)
}
}
现行还可以给蒙层自己的滚动容器加 overscroll-behavior: contain(或 none):滚到尽头后不再把滚动链到背后的页面。新项目优先试这一条,再按机型补方案三。
iPhone 安全区域
全面屏(刘海、底部横条、Android 挖孔)会在可视窗口四周留出系统占用的安全区。顶部刘海一般由浏览器自己垫过;网页更常处理的是底部:吸底导航、回到顶部等 fixed 元素。
viewport-fit
viewport 的 viewport-fit 控制网页如何铺进可视窗口(含圆角和安全区以外的区域):
contain:可视窗口完全包住网页内容(默认,与auto相同)cover:网页内容铺满可视窗口,可以画进安全区auto:与contain一致
env() 和 constant()
env() 读取环境变量。安全区四边距离:
safe-area-inset-leftsafe-area-inset-rightsafe-area-inset-topsafe-area-inset-bottom
viewport-fit 不是 cover 时,这四个值按 0 处理,写了 env() 也垫不出横条高度。浏览器不认识 env() 时会丢掉整条声明。
iOS 11 最初把这个函数做成 constant()。从 Safari Technology Preview 41 和 iOS 11.2 起改成 env()。要兼顾旧系统,两条都写,先 constant 后 env,顺序不能反。
padding-bottom: constant(safe-area-inset-bottom); /* iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); /* iOS >= 11.2,以及现行引擎 */
如何适配
<meta name="viewport" content="width=device-width, viewport-fit=cover">
body {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
常见问题
- iPhone X 一类机型上,
fixed元素滑到页面底部时会往上挪一点。常见原因是页面高度不到一屏(或把撑高度的元素写成fixed,脱离文档流)。给body或外层加min-height: 100%,或用空元素、伪元素把文档撑高。 height: 100%不含底部安全区。若再用padding-bottom: env(safe-area-inset-bottom)去补横条高度,该元素需要box-sizing: content-box,否则 padding 会吃进已经算死的 100% 高度里。
限制 input 输入
限制 input 只能进数字、中文、字母等。思路是:在输入过程中用正则丢掉非法字符,或在按键时取消默认插入。
三个键盘事件:keydown、keypress、keyup。粘贴走 paste 或随后的 input。下面内联写法能跑,但 keyCode、returnValue、onafterpaste、execCommand 都已过时。新代码用 input 事件改 value,或直接用 type、inputmode、pattern。
只能输入或粘贴数字:
<input onkeyup="this.value = this.value.replace(/\D/g,'')" onafterpaste="this.value = this.value.replace(/\D/g,'')">
只能输入数字(按键阶段拦下,少一次先插入再删掉的闪动):
<input type="text" onkeypress="if (event.keyCode < 48 || event.keyCode > 57) event.returnValue = false">
keyCode 48-57 是主键盘数字。小键盘、中文输入法、粘贴绕得过这层,所以仍要配合 input 过滤。现行用 event.key(例如 "0" 到 "9"),并 event.preventDefault()。
只能输入或粘贴数字和小数点:
<input onkeyup="if (isNaN(value)) execCommand('undo')" onafterpaste="if (isNaN(value)) execCommand('undo')">
document.execCommand('undo') 已废弃。同样情况改为在 input 里记下上次合法值,非法则回写。
只能输入或粘贴英文字母、数字、下划线、点、斜杠(\w 含下划线;正则里还放行了 . 和 /),不能输入中文:
<input onkeyup="this.value = this.value.replace(/[^\w.\/]/g,'')" onafterpaste="this.value = this.value.replace(/[^\w.\/]/g,'')">
只能输入或粘贴基本汉字区的中文:
<input type="text" onkeyup="this.value = this.value.replace(/[^\u4e00-\u9fa5]/g,'')" onafterpaste="this.value = this.value.replace(/[^\u4e00-\u9fa5]/g,'')">
\u4e00-\u9fa5 盖不住所有汉字扩展区。
用 CSS 关输入法:
<input style="ime-mode: disabled">
ime-mode 是非标准属性。旧版 IE、部分 Firefox 认;Chrome 当无效。不要指望它在 WebKit 里禁中文。
点击的 300ms 延迟
移动端单击曾被故意推迟约 300ms:浏览器要等用户会不会再点一次,以区分单击和双击缩放(double tap to zoom)。
视口设成 width=device-width 之后,Chrome、Safari、Firefox 已经去掉这层等待。新项目不必默认引入 FastClick。
仍觉得点击发黏时,给可点元素关掉双击缩放手势即可,不必禁用整页缩放:
a,
button,
[role="button"] {
-ms-touch-action: manipulation; /* IE10 */
touch-action: manipulation; /* IE11 与现行引擎 */
}
下面这些属于旧方案,不要当首选:
<!-- 禁用缩放:损害无障碍,且不是现行去掉 300ms 的必要条件 -->
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!-- Chrome 32 起:理想视口即可去掉延迟;不必把 maximum-scale 钉死 -->
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=3">
FastClick 在 touchend 时立刻派发模拟 click,并拦住大约 300ms 后浏览器自己的那次 click。只适合仍有延迟的老 WebView。部分机型上它会让 <input> 点不着;不要和 Zepto 的 tap(点击穿透)一起当通用方案。
文字无缝滚动
两份相同文案并排,容器持续 scrollLeft++;滚过第一份宽度时把 scrollLeft 往回减一份,看起来像无限循环。DEMO 里的类名如下。
<section class="box1">
<div class="container1">
<div class="item1 item11">【蒹葭】蒹葭苍苍,白露为霜。所谓伊人,在水一方,溯洄从之,道阻且长。溯游从之,宛在水中央。</div>
<div class="item1 item12"></div>
</div>
</section>
.box1 { overflow: hidden; border: 1px solid #0aa; }
.box1 .container1 { white-space: nowrap; }
.box1 .container1 .item1 { display: inline-block; color: #0aa; }
const boxDom01 = document.querySelector(".box1")
const itemDom01 = document.querySelector(".item11")
const itemDom02 = document.querySelector(".item12")
itemDom02.innerHTML = itemDom01.innerHTML
setInterval(() => {
if (boxDom01.scrollLeft - itemDom02.offsetWidth >= 0) {
boxDom01.scrollLeft -= itemDom01.offsetWidth
} else {
boxDom01.scrollLeft++
}
}, 20)
防抖和节流
防抖: 事件触发后等 n 秒再执行;n 秒内又触发,则重新计时。适合输入联想、窗口 resize 结束再算布局。
function debounce(fn, delay) {
var timer = null
return function () {
var context = this
var args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
这是尾随防抖:停稳之后才跑。还有一种是先立刻执行,再在冷却期内忽略后续触发。
节流: 单位时间内最多执行一次。适合 scroll、mousemove。
function throttle(fn, delay) {
var preTime = 0
return function () {
var context = this
var args = arguments
var nowTime = Date.now()
if (nowTime - preTime >= delay) {
preTime = nowTime
return fn.apply(context, args)
}
}
}
preTime 初始为 0,第一次触发会立刻执行。若写成 Date.now(),创建后 delay 毫秒内的第一次触发会被丢掉。
滚动监听还可以改用 requestAnimationFrame 对齐帧,而不是死盯一个固定 delay。
获取光标位置、聚焦、选择内容
获取 input 光标位置(插入点在值里的偏移):
function getCursor() {
const input = document.getElementById("input")
if (typeof input.selectionStart === "number") {
return input.selectionStart
}
const range = document.selection.createRange()
range.moveStart("character", -input.value.length)
return range.text.length
}
selectionStart 是现行标准。document.selection 只存在于旧 IE。
选中 input 里一段文本:先聚焦,再 setSelectionRange(start, end)。end 是结束位置的偏移(不含该下标之后的字符)。
function setSelection() {
const input = document.getElementById("input")
input.focus()
input.setSelectionRange(1, 8)
}