# 移动端开发

# 基础知识

# Webview

Webview 是一个基于 webkit 的引擎,可以解析 DOM 元素,展示 html 页面的控件,它和浏览器展示页面的原理是相同的,所以可以把它当做浏览器看待。

# WebKit

WebKit是一个开源的Web浏览器引擎(Web browser engine)[3] (opens new window)。它被用于Apple (opens new window) Safari (opens new window)。其分支Blink (opens new window)被用于基于Chromium (opens new window)的网页浏览器,如Microsoft Edge (opens new window)Google Chrome (opens new window)

一个浏览器至少离不开一个渲染DOM的引擎和一个运行 JavaScript 的引擎。WebView 使用了 WebKit 内核。在安卓里面以 V8 作为 JS 引擎,在 iOS 里面以 JavaScriptCore 作为 JS 引擎。

由于渲染 DOM 和操作 JS 的是两个引擎,因此当我们用 JS 去操作 DOM 的时候,JS 引擎通过调用桥接方法来获取访问 DOM 的能力。这里就会涉及到两个引擎通信带来的性能损失,这也是为什么频繁操作 DOM 会导致性能低下。

React 和 Vue 这些框架都是在这一层面进行了优化,只去修改差异部分的 DOM,而非全量替换 DOM。

# 离线包方案

Hybrid App 同时拥有 Native 和 Web 的优点

本质:提前下载打包好的 zip 文件,每次访问的是 App 本地的资源,加载速度可以得到质的提升。

更新:如果文件有更新,那么客户端就去拉取远程版本,和本地版本进行对比,如果版本有更新,那就去拉取差量部分的文件,用二进制 diff 算法 patch 到原来的文件中,这样可以做到热更新。需要在服务端进行一次文件差分,还需要公司内部提供一套热更新发布平台。

# JSBridge 通信原理

# 布局

# REM 布局

使用 js 动态设置 html 的 font-size,css 使用 rem 单位,文本大小可选择使用 px

js 设置 viewport 的 scale 以支持高清设备的 1px

可设置页面最大最小宽度

# VW 布局

css 使用 vw 单位,文本大小可选择使用 px

使用 transform 以支持高清设备的边框 1px(包括圆角),使用 @mixin ./vw/scss/_border.scss

可设置容器固定纵横比,不可设置页面最大最小宽度

# REM + VW 布局

html 的 font-size 使用 vw 单位,css 使用 rem 单位,文本大小可选择使用 px

使用 transform 以支持高清设备的边框 1px(包括圆角),使用 @mixin ./vw-rem/scss/_border.scss

可设置容器固定纵横比,可设置页面最大最小宽度

# HTML

# 唤醒原生应用

通过location.href与原生应用建立通讯渠道,这种页面与客户端的通讯方式称为URL Scheme,其基本格式为scheme://[path][?query]

  • scheme:应用标识,表示应用在系统里的唯一标识
  • path:应用行为,表示应用某个页面或功能
  • query:应用参数,表示应用页面或应用功能所需的条件参数

例如:

<!-- 打开微信 -->
<a href="weixin://">打开微信</a>

<!-- 打开支付宝 -->
<a href="alipays://">打开支付宝</a>

<!-- 打开支付宝的扫一扫 -->
<a href="alipays://platformapi/startapp?saId=10000007">打开支付宝的扫一扫</a>

<!-- 打开支付宝的蚂蚁森林 -->
<a href="alipays://platformapi/startapp?appId=60000002">打开支付宝的蚂蚁森林</a>

# 禁止页面缩放
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">
# 禁止页面缓存
<meta http-equiv="Cache-Control" content="no-cache">
# 自动适应布局

通常将rem布局比例设置成1rem=100px,即在设计图上100px长度在CSS代码上使用1rem表示。

给HTML上设置 `style="font-size:xxpx"

function AutoResponse(width = 750) {
    const target = document.documentElement;
    if (target.clientWidth >= 600) {
        target.style.fontSize = "80px";
    } else {
        target.style.fontSize = target.clientWidth / width * 100 + "px";
    }
}
AutoResponse();
window.addEventListener("resize", () => AutoResponse());

另一种方案:

当然还可依据屏幕宽度设计图宽度的比例使用calc()动态声明<html>font-size

html {
    font-size: calc(100vw / 7.5);
}

若以iPad Pro分辨率1024px为移动端和桌面端的断点,还可结合媒体查询做断点处理。1024px以下使用rem布局,否则不使用rem布局

@media screen and (max-width: 1024px) {
    html {
        font-size: calc(100vw / 7.5);
    }
}
# 监听屏幕旋转
/* 横屏 */
@media all and (orientation: landscape) { 
  
	/* 自定义样式 */
  
}
/* 竖屏 */
@media all and (orientation: portrait) { 
  
 /* 自定义样式 */
  
} 
# 支持弹性滚动
body {
    -webkit-overflow-scrolling: touch;
}
.elem {
    overflow: auto;
}

# 禁止滚动传播

注意:Safari 暂时不支持

.elem {
    overscroll-behavior: contain;
}

# 美化滚动占位
  • ::-webkit-scrollbar:滚动条整体部分
  • ::-webkit-scrollbar-track:滚动条轨道部分
  • ::-webkit-scrollbar-thumb:滚动条滑块部分
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
    background-color: transparent;
}
::-webkit-scrollbar-track {
    background-color: transparent;
}
::-webkit-scrollbar-thumb {
    border-radius: 3px;
    background-image: linear-gradient(135deg, #09f, #3c9);
}

# 美化输入占位
input::-webkit-input-placeholder {
    color: #66f;
}
# 描绘像素边框

1px

.elem {
    position: relative;
    width: 200px;
    height: 80px;
    &::after {
        position: absolute;
        left: 0;
        top: 0;
        border: 1px solid #f66;
        width: 200%;
        height: 200%;
        content: "";
        transform: scale(.5);
        transform-origin: left top;
    }
}
# 控制溢出文本
.elem {
    width: 400px;
    line-height: 30px;
    font-size: 20px;
    &.sl-ellipsis {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    &.ml-ellipsis {
        display: -webkit-box;
        overflow: hidden;
        text-overflow: ellipsis;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
    }
}
# 禁止点击穿透

fastclick (opens new window)有现成的NPM包

import Fastclick from "fastclick";

FastClick.attach(document.body);

css方案:

touch-action 的默为 auto,将其置为 none,即可移除目标元素的 300 毫秒延迟; 缺点: 新属性 ,可能存在浏览器兼容问题

使用 touchstarttouchend 来模拟代click

el.addEventListener("touchstart", () => { console.log("ok"); }, false);
# 禁止滑动穿透
  • 弹窗打开后内部内容无法滚动
  • 弹窗关闭后页面滚动位置丢失
  • Webview能上下滑动露出底色

css 代码

body.static {
    position: fixed;
    left: 0;
    width: 100%;
}

js 代码

const body = document.body;
const openBtn = document.getElementById("open-btn");
const closeBtn = document.getElementById("close-btn");
openBtn.addEventListener("click", e => {
    e.stopPropagation();
    const scrollTop = document.scrollingElement.scrollTop;
    body.classList.add("static");
    body.style.top = `-${scrollTop}px`;
});
closeBtn.addEventListener("click", e => {
    e.stopPropagation();
    body.classList.remove("static");
    body.style.top = "";
});
# 修复高度坍塌

当输入完成键盘占位消失后,页面高度有可能回不到原来高度。

  • 页面高度过小
  • 输入框在页面底部或视窗中下方
  • 输入框聚焦输入文本

只要保持前后滚动条偏移量一致就不会出现上述问题。在输入框聚焦时获取页面当前滚动条偏移量,在输入框失焦时赋值页面之前获取的滚动条偏移量,这样就能间接还原页面滚动条偏移量解决页面高度坍塌。

# 简化回到顶部

函数scrollIntoView (opens new window),它会滚动目标元素的父容器使之对用户可见,简单概括就是相对视窗让容器滚动到目标元素位置。它有三个可选参数能让scrollIntoView滚动起来更优雅。

behavior:动画过渡效果,默认auto无,可选smooth平滑

inline:水平方向对齐方式,默认nearest就近对齐,可选start顶部对齐center中间对齐end底部对齐

block:垂直方向对齐方式,默认start顶部对齐,可选center中间对齐end底部对齐nearest就近对齐

const gotopBtn = document.getElementById("gotop-btn");
openBtn.addEventListener("click", () => document.body.scrollIntoView({ behavior: "smooth" }));
# 简化懒性加载

函数就是IntersectionObserver (opens new window),它提供一种异步观察目标元素及其祖先元素或顶级文档视窗交叉状态的方法。详情可参照MDN文档 (opens new window)

图片懒加载

<img data-src="pig.jpg">
<!-- 很多<img> -->
const imgs = document.querySelectorAll("img.lazyload");
const observer = new IntersectionObserver(nodes => {
    nodes.forEach(v => {
        if (v.isIntersecting) { // 判断是否进入可视区域
            v.target.src = v.target.dataset.src; // 赋值加载图片
            observer.unobserve(v.target); // 停止监听已加载的图片
        }
    });
});
imgs.forEach(v => observer.observe(v));

下拉加载

<ul>
    <li></li>
    <!-- 很多<li> -->
</ul>
<!-- 也可将#bottom以<li>的形式插入到<ul>内部的最后位置 -->
<div id="bottom"></div>
const bottom = document.getElementById("bottom");
const observer = new IntersectionObserver(nodes => {
    const tgt = nodes[0]; // 反正只有一个
    if (tgt.isIntersecting) {
        console.log("已到底部,请求接口");
        // 执行接口请求代码
    }
})
observer.observe(bottom);

# 元素的高度

  • clientHeight : 可理解为内部可视区高度,样式的 height + padding
  • scrollHeight 网页正文全文高
  • offsetHeight 网页可见区域高 ,元素的height + padding + border+ 水平滚动条
  • scrollTop 页面被卷上去的高度

# 键盘弹出

IOS:IOS 的键盘处在窗口的最上层,当键盘弹起时,webview 的高度 height 并没有改变,只是 scrollTop 发生变化,页面可以滚动。且页面可以滚动的最大限度为弹出的键盘的高度,而只有键盘弹出时页面恰好也滚动到最底部时,scrollTop 的变化值为键盘的高度,其他情况下则无法获取。这就导致在 IOS 情况下难以获取键盘的真实高度。

Android: webview 中留出空间,该空间小于等于的键盘空间,变化的高度差会随着布局而不同,有的认为 键盘高度 + 页面高度 = 原页面高度; 是错误的误导,只有在某种很巧合的布局情况下才可套用此公式。

# 键盘收起

IOS:触发键盘上的按钮收起键盘或者输入框以外的页面区域时,输入框会失去焦点,因此会触发输入框的 blur 事件。

Android: 触发键盘上的按钮收起键盘时,输入框并不会失去焦点,因此不会触发页面的 blur 事件;触发输入框以外的区域时,输入框会失去焦点,触发输入框的 blur 事件。

# 监听键盘的弹出与收起

const ua = window.navigator.userAgent.toLocaleLowerCase();
const isIOS = /iphone|ipad|ipod/.test(ua);
const isAndroid = /android/.test(ua);


// ios & android 键盘弹出:
const $input = document.getElementById('input');
$input.addEventListener('focus', () => {
	// 处理键盘弹出后所需的页面逻辑
}, false);

// ios 键盘收起:
const $input = document.getElementById('input');
$input.addEventListener('blur', () => {
	// 处理键盘收起后所需的页面逻辑
}, false);

/*键盘弹起后页面高度变小*/
const originHeight = document.documentElement.clientHeight || document.body.clientHeight;
window.addEventListener('resize', () => {
	const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
	if (resizeHeight < originHeight) {
		// 键盘弹起所后所需的页面逻辑
	} else {
		// 键盘弹起所后所需的页面逻辑
	}
}, false);


# IOS 键盘输入确认搜索后键盘不消失

解决办法:input 提交的时候触发 blur 事件,让 input 失去焦点。

# 移动端底部 input 被弹出的键盘遮挡

因为在iOS下,会先平移webview,后在移动webview里面的H5,而平移webview的前提是H5的页面足够的高,底部可滚动的高度大于弹出键盘的高度,这个时候才会平移,不然就遮挡。

最新版的 iOS 没有这个问题,为了兼容老系统,

下面方法二选一就行:

Element.scrollIntoView() (opens new window):方法让当前的元素滚动到浏览器窗口的可视区域内

:Element.scrollIntoViewIfNeeded() (opens new window) :方法用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域

document.querySelector('#inputId').scrollIntoView();
//只要在input的点击事件,或者获取焦点的事件中,加入这个api就好了

如果还是不行参考这里 (opens new window)

总结一下:

  1. iOS:使用 WKWebView (opens new window),同时用最新版的 XCode 来编译;
  2. Android:可能需要打 AndroidBug5497Workaround (opens new window) 补丁;
  3. H5:分情况处理 scrollIntoView。

H5 的 js 完整代码可以查看 ChatUI (opens new window) Composer 组件的 riseInput (opens new window) 方法,你可以这样使用:

const input = document.querySelector('textarea');
const target = document.querySelector('form');

// input 是输入框
// target 是需要对齐的容器,可选
riseInput(input, target);

riseInput

const ua = navigator.userAgent;
const iOS = /iPad|iPhone|iPod/.test(ua);

function uaIncludes(str: string) {
  return ua.indexOf(str) !== -1;
}

function testScrollType() {
  if (iOS) {
    if (uaIncludes('Safari/') || /OS 11_[0-3]\D/.test(ua)) {
      /**
       * 不处理
       * - Safari
       * - iOS 11.0-11.3(`scrollTop`/`scrolIntoView` 有 bug)
       */
      return 0;
    }
    // 用 `scrollTop` 的方式
    return 1;
  }
  // 其它的用 `scrollIntoView` 的方式
  return 2;
}

export default function riseInput(input: HTMLElement, target: HTMLElement) {
  const scrollType = testScrollType();
  let scrollTimer: ReturnType<typeof setTimeout>;

  if (!target) {
    // eslint-disable-next-line no-param-reassign
    target = input;
  }

  const scrollIntoView = () => {
    if (scrollType === 0) return;
    if (scrollType === 1) {
      document.body.scrollTop = document.body.scrollHeight;
    } else {
      target.scrollIntoView(false);
    }
  };

  input.addEventListener('focus', () => {
    setTimeout(scrollIntoView, 300);
    scrollTimer = setTimeout(scrollIntoView, 1000);
  });

  input.addEventListener('blur', () => {
    clearTimeout(scrollTimer);

    // 某些情况下收起键盘后输入框不收回,页面下面空白
    // 比如:闲鱼、大麦、乐动力、微信
    if (scrollType && iOS) {
      // 以免点击快捷短语无效
      setTimeout(() => {
        document.body.scrollIntoView();
      });
    }
  });
}

# IOS 移动端页面根容器可拖拽滑动

js 方案

document.body.addEventListener("touchmove", bodyScroll, false);
function bodyScroll(event) {
  event.preventDefault();
}

vue 方案

@touchmove.prevent

Css 方案

body {
    position: fixed;
    overflow: hidden;
}

# iOS safari浏览器上overflow: scroll元素无法滚动 (opens new window)

**问题描述:**当 iOS safari 浏览器上出现大于父容器的子元素,想给父容器加上overflow: scroll实现内部滚动效果而失败。

剖析: safari浏览器在渲染页面元素的时候,会预先走webkit浏览器的渲染流程:

  1. 构建DOM tree
  2. 构建CSS rule tree
  3. 根据DOM和CSS tree来构建render tree
  4. 根据render tree计算页面的layout
  5. render页面

在第三步和第四步的时候,safari浏览器在构建render tree的时候,会预先找到相应的overflow: scroll元素,在计算页面layout的时候,会计算父元素的高度与子元素的高度,若子元素高于父元素,则在render页面时为其建立一个原生的scrollView。

这个scrollView有什么用的?其实就是为了给其一个弹弹乐的效果(但确实用户体验不错)。

当子元素是某个媒体格式时,比如img、object(svg)等,safari在加载完成之前是不会在计算在layout之内的,也就是高度为0,则子元素的高度就一定小于父元素的高度,safari不会给父元素一个原生的scrollView。

# 解决办法:

方法一:给子元素一个包裹元素,包裹元素设置一个min-height大于父元素的高度,让父元素有scrollView。当子元素加载完成时,将包裹元素撑开,父元素便可以自由滚动了。不一定成功

方法二:通过 document.addEventListener("scroll", this.onScroll, true); 监听

// 以上拉刷新举例 
onScroll(e) {
  clearTimeout(this.timer);
  this.timer = setTimeout(() => {
    var clientHeight = document.documentElement.clientHeight; 
    var scrollTop = document.documentElement.scrollTop; 
    var scrollHeight = document.documentElement.scrollHeight; 
	// 
    if (clientHeight + scrollTop + 20 >= scrollHeight) {
      this.bottomCallback();
    }
  }, 200);
},

浏览器视口

  • 布局视口,原始视口也就是原始的 width ,document.documentElement.clientWidth
  • 视觉视口,用户看到的真实区域,它决定了用户能够看到多少东西,window.innerWidth
  • 理想视口,网页在移动端展示的最佳大小,window.innerWidth

通常网页上的

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

上面的 width=device-width, initial-scale=1, maximum-scale=1 的目的在于让三者统一,

即:布局视口 = 视觉视口 = 理想视口

# scroll 事件失效

**问题描述:**在Vue中的组件绑定scroll事件,事件处理函数似乎不会触发。

# 前置知识 JavaScript事件模型

capture-bubble-phase

  • 捕获阶段
    • 事件从document到传递到目标元素的过程
  • 冒泡阶段
    • 事件从目标元素传递到document的过程

我们一般监听事件的冒泡阶段:elem.addEventListener('scroll', handler,false)

addEventListener第三个参数可能值:

  • true - 事件句柄在捕获阶段执行
  • false- false- 默认。事件句柄在冒泡阶段执行

# 失效原因

** elementscroll事件不冒泡, 但是documentdefaultViewscroll事件冒泡。**

如果scroll的目标元素是一个元素的话,比如说是一个div元素。那么此时事件只有从documentdiv的捕获阶段以及div的冒泡阶段。如果尝试在父级监视scroll的冒泡阶段监视这一事件是无效的。如果scroll是由document.defaultView(目前document关联的window对象)产生的有冒泡阶段。但是由于其本身就是DOM树里最顶级的对象,因此只能在window里监视scroll的捕获阶段以及冒泡阶段。

scroll-event-flow

如图所示在目标元素的父级监听是监听不到的。因为只冒泡到了他自己上就断了,不往上冒泡了。

# 解决办法:

方法一:要确定目标元素是谁,谁产生了scroll事件,是谁就在谁上进行监听。

方法二:就是在window上监听scroll的捕获阶段,即window.addEventListener('scroll', handler, true)

# new Date 的兼容性 iOS

问题描述:使用new Date(time).getTime() 方法转换时间戳。iOS 获取不到。

# 解决办法

格式一:

2018-03-05 00:00:02 如果 time 是这种格式,需要把 “-” 转换成 “/” str.replace(/-/g,"/")

格式二:

2018-03-05 00:00:02.55 这种截取前19位str.substring(0,19) 进行转换。

# 吸顶的写法

# 粘性定位

.sticky {
  position: sticky;
  top: 10px;
}
  • 必须指定 top、right、bottom、left 四个阈值的其中之一,粘性定位才会生效。
  • 父容器的高度,应大于粘性定位的元素,粘性布局是对父级滚动元素定位。

# 粘性事件

浏览器暂无提供粘性定位事件,可以用模拟的方式。

监听 scroll 事件,获取目标元素的信息boundingClientRect,和设定的值进行比较,然后触发自定义事件。

# 兼容 iPhone X 刘海屏 底部显示

第一步:设置网页在可视窗口的布局方式

<meta name='viewport'  content="width=device-width, viewport-fit=cover"  />

第二步:页面主体内容限定在安全区域内

body {
  /* 适配齐刘海*/
  padding-top: constant(safe-area-inset-top);  
 /* 适配底部黑条*/
  padding-bottom: constant(safe-area-inset-bottom);
}

fixed 元素的适配

bottom 不是0的情况

/* bottom 不是0的情况 */
.fixed {
  bottom: calc(50px(原来的bottom值) + constant(safe-area-inset-bottom));
}

吸底的情况(bottom=0)

/* 方法1 :设置内边距 扩展高度*/
/* 这个方案需要吸底条必须是有背景色的,因为扩展的部分背景是跟随外容器的,否则出现镂空情况。*/
.fix {
  padding-bottom: constant(safe-area-inset-bottom);
}
/* 直接扩展高度*/
.fix {
  height: calc(60px(原来的高度值) + constant(safe-area-inset-bottom));
}

/* 方法2 :在元素下面用一个空div填充, 但是背景色要一致 */
.blank {
  position: fixed;
  bottom: 0;
  height: 0;
  width: 100%;
  height: constant(safe-area-inset-bottom);
  background-color: #fff;
}
/* 吸底元素样式 */
.fix {
  margin-bottom: constant(safe-area-inset-bottom);
}

第三步:使用 @supports

@supports (bottom: constant(safe-area-inset-bottom)) {
  body {
    padding-bottom: constant(safe-area-inset-bottom);
  }
}

完整 less

@mixin iphonex-css {
  padding-top: constant(safe-area-inset-top); //为导航栏+状态栏的高度 88px
  padding-top: env(safe-area-inset-top); //为导航栏+状态栏的高度 88px
  padding-left: constant(safe-area-inset-left); //如果未竖屏时为0
  padding-left: env(safe-area-inset-left); //如果未竖屏时为0
  padding-right: constant(safe-area-inset-right); //如果未竖屏时为0
  padding-right: env(safe-area-inset-right); //如果未竖屏时为0
  padding-bottom: constant(safe-area-inset-bottom); //为底下圆弧的高度 34px
  padding-bottom: env(safe-area-inset-bottom); //为底下圆弧的高度 34px
}

/* iphonex 适配 */
@mixin iphonex-media {
  @media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) {
    body.iphonex {
      @include iphonex-css;
    }
  }
}

# iOS垂直居中不生效

Safari中必须设置 父元素 position:relative

# 上拉下拉有空白

在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器 ,容器自然会被拖动,剩下的部分会成空白。

解决方案:

1、preventDefault 方法,阻止同一触点上所有默认行为,包括滚动。 通过监听 touchmove,让需要滑动的地方滑动,不需要滑动的地方禁止滑动注意要过滤掉具有滚动容器的元素。

document.body.addEventListener('touchmove', function(e) {
    if(e._isScroller) return;
    // 阻止默认事件
    e.preventDefault();
}, {
    passive: false
});

2、填充空白, 装饰成其他功能,或者设置背景图。

# 生成图片

1、 生成画布

import html2canvas from 'html2canvas';

html2canvas(document.body).then(function(canvas) {
    document.body.appendChild(canvas);
});

2、上述生成的图片较为模糊,生成多倍图

// 根据需要设置 scaleSize 大小
const scaleSize = 2;
const newCanvas = document.createElement("canvas");
const target = document.querySelector('div');
const width = parseInt(window.getComputedStyle(target).width);
const height = parseInt(window.getComputedStyle(target).height);
newCanvas.width = width * scaleSize;
newCanvas.height = height * scaleSize;
newCanvas.style.width = width + "px";
newCanvas.style.height = height + "px";
const context = newCanvas.getContext("2d");
context.scale(scaleSize, scaleSize);

html2canvas(document.querySelector('.demo'), { canvas: newCanvas }).then(function(canvas) {
  // 简单的通过超链接设置下载功能
  document.querySelector(".btn").setAttribute('href', canvas.toDataURL());
}

# touch

# 设置偏移量带来的页面卡顿

const getX = e => e.touches[0].pageX

const onTouchStart = () => {
    const startX = getX(e);
}

const onTouchMove = (e: TouchEvent) => {
    const curX = getX(e);
    // 当前偏移量
    const offsetX = curX - startX;
}

# offsetX 为响应式,假如执行js会带来页面的卡顿 

作用到视图

<div
    className="touch-wrapper"
    style={{
        // 0.5 是一个阻力值,比如滑动 100px,实际 tab 只移动 50px
        transform: `translate(${offsetX * 0.5}px, 0)`,
    }}
>
    ...
</div>

解决办法: 直接设置元素的 style,避免执行额外的 JS

const setPosition = 
    (offset: number) => {
        if (wrapperRef.current) {
            wrapperRef.current.style.transform = `translate(${-offset}px, 0)`;
        }
    }

# 其他touch问题的解决方案

https://juejin.cn/post/7259927532249694269

# CSS 内在尺寸

CSS 存在内在尺寸(intrinsic)和外在尺寸 (extrinsic),内在为元素自己的内容决定,外在就是元素设置的宽高。

# 内在尺寸

  • min-content 内在的最小尺寸,这个元素的宽度等于内容里面最长的那个单词的宽。
  • max-content 就是内容的宽,是动态的,随着内容的改变而改变。
  • Fit-content 是二者的结合,若内容尺寸小于 min-content 就用min-content,反之同理。