# 1.axios原理

Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境。

  • 浏览器环境:使用 XMLHttpRequest(XHR)
  • Node.js 环境:使用 http/https 模块

与 Fetch API 的对比

特性 Axios Fetch API
​本质​ 第三方库(需要安装) 浏览器原生 API
浏览器支持​ 广泛(通过 XHR 回退) 现代浏览器
​​请求取消​ 支持完善 通过 AbortController 支持
拦截器​ 内置请求/响应拦截器 需要自行实现
Node.js支持​ 支持 不支持(需使用node-fetch等polyfill)
进度监控​ 支持 不支持
​​超时控制​ 内置 需要自行实现
​​CSRF防护​ 内置(XSRF-TOKEN) 需要自行实现
数据转换​ 自动 需要手动处理

# 一、整体架构设计

Axios 采用了分层架构设计,主要分为以下几层:

    1. ​核心层(Core)​​:提供基础请求能力
    1. ​适配器层(Adapter)​​:处理环境差异(浏览器/XHR 和 Node.js/http)
    1. ​拦截器层(Interceptors)​​:请求/响应拦截机制
    1. 配置层(Config)​​:统一的配置管理
    1. 取消请求层(Cancel)​​:请求取消功能

# 二、核心实现原理

# 1. 请求发送流程

// 简化版请求流程
function Axios(config) {
  // 1. 合并配置
  this.defaults = mergeConfig(defaults, config);
  // 2. 初始化拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function(config) {
  // 1. 参数处理(支持 axios('url', config) 形式)
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  }
  // 2. 合并配置
  config = mergeConfig(this.defaults, config);
  // 3. 转换请求数据
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  // 4. 构建请求链
  const chain = [dispatchRequest, undefined];
  // 5. 添加请求拦截器(后添加的先执行)
  this.interceptors.request.forEach(interceptor => {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 6. 添加响应拦截器(先添加的先执行)
  this.interceptors.response.forEach(interceptor => {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // 7. 执行链
  let promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 2. 适配器实现原理

// 适配器选择逻辑
function getDefaultAdapter() {
  if (typeof XMLHttpRequest !== 'undefined') {
    // 浏览器环境使用 XHR 适配器
    return require('./adapters/xhr');
  } else if (typeof process !== 'undefined') {
    // Node.js 环境使用 http 适配器
    return require('./adapters/http');
  }
}

// 浏览器 XHR 适配器核心实现
module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();
    // 初始化请求
    request.open(
      config.method.toUpperCase(),
      buildURL(config.url, config.params),
      true
    );
    // 设置超时
    request.timeout = config.timeout;
    // 设置响应类型
    if (config.responseType) {
      request.responseType = config.responseType;
    }
    // 设置请求头
    if (config.headers) {
      for (let key in config.headers) {
        request.setRequestHeader(key, config.headers[key]);
      }
    }
    // 处理取消
    if (config.cancelToken) {
      config.cancelToken.promise.then(reason => {
        request.abort();
        reject(reason);
      });
    }
    // 处理请求数据
    let requestData = config.data;
    if (requestData && !isStream(requestData)) {
      requestData = transformRequestData(requestData, config.headers);
    }
    // 请求完成处理
    request.onreadystatechange = function() {
      if (!request || request.readyState !== 4) return;
      const response = {
        data: transformResponseData(request.response, config),
        status: request.status,
        statusText: request.statusText,
        headers: parseHeaders(request.getAllResponseHeaders()),
        config: config,
        request: request
      };
      settle(resolve, reject, response);
      request = null;
    };
    // 错误处理
    request.onerror = function() {
      reject(createError('Network Error', config));
      request = null;
    };
    // 超时处理
    request.ontimeout = function() {
      reject(createError(`timeout of ${config.timeout}ms exceeded`, config));
      request = null;
    };
    // 发送请求
    request.send(requestData);
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 三、关键特性实现原理

# 1. 拦截器机制

拦截器采用链式 Promise 实现:

function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function(fulfilled, rejected) {
  this.handlers.push({ fulfilled, rejected });
  return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
// 请求链构建逻辑
const chain = [dispatchRequest, undefined];
this.interceptors.request.forEach(interceptor => {
  chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(interceptor => {
  chain.push(interceptor.fulfilled, interceptor.rejected);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2. 取消请求实现

AbortController 实现

// 适配 AbortController
if (config.cancelToken) {
  config.cancelToken.promise.then(reason => {
    request.abort();
    reject(reason);
  });
}
if (config.signal) {
  config.signal.aborted || config.signal.addEventListener('abort', () => {
    request.abort();
    reject(new Cancel('canceled'));
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3. 数据转换机制

// 默认转换函数
transformRequest: [
  function(data, headers) {
    // 处理 Content-Type
    if (isFormData(data) || isArrayBuffer(data) || isStream(data)) {
      return data;
    }
    if (isArrayBufferView(data)) {
      return data.buffer;
    }
    if (isURLSearchParams(data)) {
      setContentType(headers, 'application/x-www-form-urlencoded');
      return data.toString();
    }
    if (isObject(data)) {
      setContentType(headers, 'application/json');
      return JSON.stringify(data);
    }
    return data;
  }
],
transformResponse: [
  function(data) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) {}
    }
    return data;
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 四、性能优化设计

    1. ​配置合并优化​​:采用深度合并策略
    1. ​​请求数据转换优化​​:按需转换
    1. 适配器懒加载​​:运行时按需加载
    1. ​​内存管理​​:及时清理引用

# 2.i18n原理

# 一、核心概念

  • ​国际化(i18n)​​:使应用支持多语言的过程(i和n之间有18个字母)
  • ​​本地化(l10n)​​:为特定地区适配内容的过程(l和n之间有10个字母)
  • 语言环境(Locale)​​:由语言代码和国家代码组成(如zh-CN, en-US)
  • ​​翻译键(Translation Key)​​:用于标识文本片段的唯一键
  • ​复数形式(Pluralization)​​:不同语言有不同的复数规则
  • ​​文本方向(Text Direction)​​:LTR(左到右)和RTL(右到左)布局

# 二、常见问题与解决方案

# 1. 翻译管理问题

现象​​:不同开发者使用不同命名风格,如home.titlehomeHeaderhome_title

  • 制定命名规范文档,例如:
    • 模块化命名:模块.功能.元素(login.form.submitButton
    • 统一分隔符:强制使用点号.或下划线_

使用TS类型约束:

type I18nKeys = {
  login: {
    form: {
      submitButton: string;
    };
  };
};
declare module 'i18next' {
  interface CustomTypeOptions {
    resources: I18nKeys;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 3.移动端常见问题

# 一、移动端1px边框问题

参考 (opens new window)

移动端CSS里面写了1px,实际上看起来比1px粗;
UI设计师要求的1px是指设备的物理像素,而CSS里记录的像素是逻辑像素
它们之间存在一个比例关系,可以用javascript中的window.devicePixelRatio来获取,也可以用媒体查询的-webkit-min-device-pixel-ratio来获取。

在手机上border无法达到我们想要的效果。这是因为devicePixelRatio特性导致,iPhone的devicePixelRatio==2,而border-width: 1px描述的是设备独立像素。
所以,border被放大到物理像素2px显示,在iPhone上就显得较粗。

  • ​设备像素比(DPR)​​:
    • 普通屏幕:DPR = 1(1个CSS像素 = 1个物理像素)
    • Retina屏幕:DPR = 2或3(1个CSS像素 = 2或3个物理像素)
  • 解决方案:

# 1、媒体查询 + transfrom,对第一个方案的优化

/* 2倍屏 */
@media only screen and (-webkit-min-device-pixel-ratio: 2.0) {
    .border-bottom::after {
        -webkit-transform: scaleY(0.5);
        transform: scaleY(0.5);
    }
}
/* 3倍屏 */
@media only screen and (-webkit-min-device-pixel-ratio: 3.0) {
    .border-bottom::after {
        -webkit-transform: scaleY(0.33);
        transform: scaleY(0.33);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2、使用 SVG 矢量图

  • 原理​​:通过SVG的rect绘制1px线条。
  • 优点​​:支持任意缩放。
.svg-1px {
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' stroke='#000' fill='none'/></svg>");
}
1
2
3

# 3、PostCSS插件(自动化方案)​

工具​​:使用PostCSS插件(如postcss-write-svg)自动生成1px边框代码。

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-write-svg': {
      utf8: false
    }
  }
}
1
2
3
4
5
6
7
8
@svg 1px-border {
  width: 4px;
  height: 4px;
  @rect {
    fill: transparent;
    width: 100%;
    height: 100%;
    stroke-width: 25%;
    stroke: currentColor;
  }
}
.border {
  border: 1px solid transparent;
  border-image: svg(1px-border param(--color #000)) 1 stretch;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 二、H5页面窗口自动调整到设备宽度,并禁止用户缩放页面

# 1. 基础设置:Viewport Meta 标签​​

在HTML的<head>中添加以下meta标签,这是所有解决方案的基础:

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

# ​2. 兼容性增强方案​

​**(1) 禁止iOS双击缩放​​** 某些iOS版本会忽略user-scalable=no,需通过JavaScript补充禁止手势:

document.addEventListener('touchstart', function(event) {
  if (event.touches.length > 1) {
    event.preventDefault(); // 阻止双指缩放
  }
}, { passive: false });

let lastTouchEnd = 0;
document.addEventListener('touchend', function(event) {
  const now = Date.now();
  if (now - lastTouchEnd <= 300) {
    event.preventDefault(); // 阻止双击缩放
  }
  lastTouchEnd = now;
}, { passive: false });
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​​​(2) 禁止Android手势缩放​​​

document.addEventListener('gesturestart', function(e) {
  e.preventDefault(); // 阻止Android手势缩放
});
1
2
3

# 三、移动端click屏幕产生300 ms的延迟响应

参考 (opens new window)

当用户一次点击屏幕之后,浏览器并不能立刻判断用户是要进行双击缩放,还是想要进行单击操作。因此,就会等待300毫秒,以判断用户是否再次点击了屏幕。

# 解决方案:

  • 1、通过 meta 标签禁用网页的缩放
  • 2、通过 meta 标签将网页的 viewport 设置为 ideal viewport。
  • 3、FastClick

FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。简而言之,FastClick 在检测到 touchend 事件的时候,会通过 DOM 自定义事件立即触发一个模拟click事件的自定义事件,并把浏览器在 300 毫秒之后真正触发的 click 事件阻止掉。

# 四、iphoneX的“刘海”,底部的安全区域。让页面不能占满屏幕

参考 (opens new window)

# 解决方案:

添加viewport-fit=cover meta标签,使页面占满整个屏幕

<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
1

# 五、移动端滚动穿透问题

参考 (opens new window)

移动端滚动穿透是指当页面弹窗(如模态框、菜单)出现时,用户在弹窗上滑动会导致​​底层页面跟随滚动​​的现象。

# 根本原因

  • (1) 事件冒泡机制​​
    • 弹窗的touchmove事件默认冒泡到document,触发底层页面滚动。
    • ​关键点​​:浏览器默认允许滚动事件穿透层级。
  • ​(2) 滚动链(Scroll Chaining)​​
    • 当弹窗内容滚动到顶部/底部时,继续滑动会触发浏览器的滚动链机制,将剩余滚动量传递给父容器(如body)。
  • ​(3) 平台差异​​
    • ​​iOS橡皮筋效果​​:滑动到边界时触发弹性滚动,直接作用于document。
    • ​Android WebView​​:默认优先触发页面级滚动

# 解决方案

  • (1) 基础方案:阻止默认事件​

适用场景​​:弹窗​​无需内部滚动​​时

const modal = document.querySelector('.modal');
modal.addEventListener('touchmove', (e) => {
  e.preventDefault(); // 阻止默认滚动
}, { passive: false }); // 必须设置 passive: false
1
2
3
4

缺点​​:会禁用弹窗内所有滚动

  • (2) 动态阻止边界穿透​,复杂弹窗(推荐)

适用场景​​:弹窗​​需要内部滚动​​时

let startY = 0;
modal.addEventListener('touchstart', (e) => {
  startY = e.touches[0].clientY;
}, { passive: true });

modal.addEventListener('touchmove', (e) => {
  const currentY = e.touches[0].clientY;
  const scrollTop = modal.scrollTop;
  const isTop = (scrollTop <= 0 && currentY > startY); // 向下滑到顶部
  const isBottom = (scrollTop + modal.offsetHeight >= modal.scrollHeight && currentY < startY); // 向上滑到底部

  if (isTop || isBottom) {
    e.preventDefault(); // 阻止边界穿透
  }
}, { passive: false });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

优点​​:精准控制滚动行为

  • (3) 锁定body滚动​​

​​适用场景​​:弹窗覆盖全屏时

let scrollTop = 0;
function openModal() {
  scrollTop = window.scrollY;
  document.body.style.position = 'fixed';
  document.body.style.top = `-${scrollTop}px`;
}
function closeModal() {
  document.body.style.position = '';
  window.scrollTo(0, scrollTop);
}
1
2
3
4
5
6
7
8
9
10

​缺点​​:页面会轻微抖动(跳转位置)

  • ​(4) CSS现代属性:overscroll-behavior

适用场景​​:兼容现代浏览器(Chrome 63+、Firefox 59+)

.modal {
  overscroll-behavior: contain; /* 阻止滚动链 */
}
1
2
3

缺点​​:iOS Safari不支持

  • (5) 终极方案:框架集成(如Vant Popup)​,工程化项目(最佳实践)

原理​​:动态判断滚动边界,仅在必要时阻止默认事件。

// React示例
useEffect(() => {
  const body = document.body;
  const originalStyle = window.getComputedStyle(body).overflow;
  body.style.overflow = 'hidden';
  return () => { body.style.overflow = originalStyle; };
}, []);
1
2
3
4
5
6
7

# 六、​​移动端软键盘遮挡输入框

# 1. 问题原因分析​​

  • ​(1) 视口(Viewport)变化​​
    • 软键盘弹出后,浏览器视口高度减少(通常为原高度的50%-70%)。
    • 页面未自动滚动到输入框可见区域。
  • ​(2) 输入框位置问题​​
    • 绝对定位(position: fixed)的输入框可能被键盘覆盖。
    • 输入框位于页面底部时风险更高。
  • ​(3) 平台差异​​
    • ​iOS​​:键盘弹出会触发resize事件,但可能不会自动滚动。
    • ​Android​​:行为碎片化,部分机型需手动处理。

# 2. 解决方案

  • (1) 自动滚动输入框(通用方案)​
function focusInput(inputElement) {
  inputElement.addEventListener('focus', () => {
    setTimeout(() => {
      // 滚动到输入框位置(兼容iOS/Android)
      inputElement.scrollIntoView({ 
        behavior: 'smooth', 
        block: 'center' // 或 'start'/'end'
      });
    }, 300); // 延迟确保键盘已弹出
  });
}
// 对所有输入框生效
document.querySelectorAll('input, textarea').forEach(focusInput);
1
2
3
4
5
6
7
8
9
10
11
12
13
  • (2) 动态调整布局(针对底部输入框)​
/* CSS:预留键盘高度 */
body {
  padding-bottom: 50vh; /* 初始预留空间 */
}

.keyboard-active {
  padding-bottom: 0;
}
1
2
3
4
5
6
7
8
// JS:检测键盘状态
const inputs = document.querySelectorAll('input, textarea');
inputs.forEach(input => {
  input.addEventListener('focus', () => {
    document.body.classList.add('keyboard-active');
    window.scrollTo(0, document.body.scrollHeight);
  });
  input.addEventListener('blur', () => {
    document.body.classList.remove('keyboard-active');
  });
});
1
2
3
4
5
6
7
8
9
10
11
  • (3) iOS专属修复​
// 修复iOS键盘收起后页面空白
function fixIOSKeyboard() {
  if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
    document.addEventListener('focusout', () => {
      setTimeout(() => {
        window.scrollTo(0, Math.max(document.body.scrollHeight - window.innerHeight, 0));
      }, 100);
    });
  }
}
fixIOSKeyboard();
1
2
3
4
5
6
7
8
9
10
11

# 七、​​虚拟滚动

  • 基本概念
    • ​可视窗口(Viewport)​​:用户实际看到的区域
    • ​滚动容器(Scroll Container)​​:具有固定高度和滚动条的容器
    • ​内容容器(Content Container)​​:包含所有元素的虚拟高度容器
    • ​渲染窗口(Render Window)​​:实际被渲染的可视区域及缓冲区域

核心计算公式

// 计算可见项起始索引
const startIndex = Math.floor(scrollTop / itemHeight)
// 计算可见项结束索引
const endIndex = Math.min(
  startIndex + Math.ceil(viewportHeight / itemHeight),
  totalItems - 1
)
// 计算内容容器偏移量
const offset = startIndex * itemHeight
1
2
3
4
5
6
7
8
9

主流框架实现

React实现(使用react-window)

import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);
const Example = () => (
  <List
    height={500}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Vue实现(使用vue-virtual-scroller)

<template>
  <RecycleScroller
    class="scroller"
    :items="items"
    :item-size="50"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="item">
      {{ item.name }}
    </div>
  </RecycleScroller>
</template>
<script>
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
export default {
  components: { RecycleScroller },
  data() {
    return {
      items: Array(1000).fill().map((_, i) => ({
        id: i,
        name: `Item ${i}`
      }))
    };
  }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 4.H5和App的Webview交互

# 一、H5通过UA判断是否在webview环境(追加UA)

web页面通过脚本能够很容易的拿到浏览器的ua属性,那么在app启动的时候,自定义添加一个ua属性,那么web页面就能够根据这个自定义的ua属性,轻而易举的判断出是否在app内了。

# 二、JS和Native交互(JSBridge)

参考 (opens new window)

Hybrid APP 即混合模式开发的 APP,它可以把低成本、高效率、跨平台的 H5 技术和追求极致用户体验的 Native 技术混合在一起来为用户提供服务。而 Hybrid 技术实施的前提,是实现 H5 和 Native APP 之间的通信,这个实现,我们称之为 JSBridge。

# H5 调用 Native( 2 种方式)

  • 1.理论上,无论是 iOS 还是 Android,提供的 WebView 容器是可以拦截一切 H5 发起的请求的,无论是标准协议(如 http://、https:// 等)还是私有协议(如 weixin:// )。基于这个原理,H5 采用私有协议模拟发起 URL 请求,Native 解析这类 URL 并定制相应的处理函数,这就实现了 H5 调用 Native。
  • 2.在 Native 的开发中,开发者可以给 WebView 容器注入全局变量并挂载在 window 对象上,这样前端 js 就可以通过 window 上全局对象方法 来调用一些 Native 的方法。这里需要注意的是方法注入的时机,一般是 WebView 一旦加载页面就需要注入变量。

# Native 调用 H5

上面提到的给 WebView 容器注入全局变量并挂载在 window 对象上,实际上是 Native 代码执行了一个 evaluateJavaScript 函数,直接运行 js 字符串代码(类似 js 的 eval 函数),从而实现注入。所以 Native 代码也一样可以通过这个 evaluateJavaScript 函数来调用约定好的 js 函数来实现 H5 的调用。

# 消息都是单向的,调用 Native 功能时 Callback 的实现原理

JavaScript 是运行在一个单独的 JS Context 中。由于这些 Context 与原生运行环境的天然隔离,我们可以将这种情况与 RPC(Remote Procedure Call,远程过程调用)通信进行类比,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。

可以把前端看做 RPC 的客户端,把 Native 端看做 RPC 的服务器端

对于 JSBridge 的 Callback ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:

当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。

由此可见,callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。

完整的H5与原生APP通信的JSBridge解决方案

  • 一. 通信架构设计

  • 双向通道​​:

    • ​H5→APP​​:通过callNative方法调用原生功能
    • ​​APP→H5​​:通过onCallback和全局事件监听实现回调
  • ​​平台适配​​:

// Android调用方式
prompt(JSON.stringify(msgJson));  // 通过prompt协议通信
// iOS调用方式
iframe.src = 'kcnative://go';     // 通过URL Scheme触发
1
2
3
4
平台 触发方式 特点
​​Android​ prompt(JSON.stringify(msg)) 依赖WebView的prompt拦截
​​iOS​ 动态创建iframe触发URL Scheme 通过webkit.messageHandlers
  • 二. 核心通信流程
  1. H5调用原生方法(以获取用户token为例)
kerkee.initData(function(res) {
  console.log('获取到的token:', res.key); 
});
// 实际调用链路:
ApiBridge.callNative('jsBridgeClient', 'initData', {}, callback)// Android端:
prompt('{"clz":"jsBridgeClient","method":"initData","args":{}}')// iOS端:
iframe.src = 'kcnative://go?msg=' + encodeURIComponent(JSON.stringify(msg))
1
2
3
4
5
6
7
8
9
10
11
  1. 原生调用H5回调
// 原生调用H5注册的回调
ApiBridge.onCallback(callbackId, responseData);// 触发H5预先注册的回调函数
callbackCache[callbackId](responseData);
1
2
3
4
5
  • 三. 关键技术实现
  1. 消息队列管理
var ApiBridge = {
  msgQueue: [],       // 待发送消息队列
  callbackCache: [],  // 回调函数缓存
  callbackId: 0,      // 自增ID保证唯一性

  callNative: function(clz, method, args, callback) {
    const msg = { clz, method, args };
    if(callback) {
      const callbackId = this.callbackId++;
      this.callbackCache[callbackId] = callback;
      msg.args.callbackId = callbackId;
    }
    this.msgQueue.push(msg);
    this.flushMessages(); // 触发消息发送
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 四. 完整代码示例
import { _base64ToArrayBuffer } from 'UTILS/utils'
import pushOtions from 'UTILS/pushOtions'
import { decode } from 'UTILS/Axios'

/**
 * 客户端通信交互
 */
; (function (window) {
  // 避免多次初始化
  if (window.WebViewJSBridge) return
  window.WebViewJSBridge = {}

  var browser = {
    versions: (function () {
      var u = navigator.userAgent
      const agents = [
        'miya',
      ]

      let result = agents.some(item => u.indexOf(item) != -1)

      return {
        isLocal: /(localhost|10\.|0\.0|192\.168)/.test(location.origin),
        inApp: result ? 1 : 0,
        trident: u.indexOf('Trident') > -1, //IE
        presto: u.indexOf('Presto') > -1, //opera
        webKit: u.indexOf('AppleWebKit') > -1, //apple&google kernel
        gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //firfox
        mobile: !!u.match(/AppleWebKit.*Mobile.*/), //is Mobile
        ios:
          !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/i) ||
          !!u.match(/\(Maci[^;]+; .+Mac OS X/i), //is ios
        android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android
        iPhone: u.indexOf('iPhone') > -1, //iPhone or QQHD
        iPad: u.indexOf('iPad') > -1, //iPad
        iPod: u.indexOf('iPod') > -1, //iPod
        webApp: u.indexOf('Safari') == -1, //is webapp,no header and footer
        weixin: u.indexOf('MicroMessenger') > -1, //is wechat
        qq: u.match(/\sQQ/i) == ' qq', //is qq
        PCMiz: /PCMiz/i.test(u) // mizpc
      }
    })(),
    language: (
      navigator.browserLanguage || navigator.language
    ).toLowerCase()
  }

  function getQueryString(name, isUrl) {
    var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')
    var r = window.location.search.substr(1).match(reg)
    if (r != null) return isUrl ? decodeURIComponent(r[2]) : unescape(r[2])
    return null
  }

  var global = this || window
  var ApiBridge = {
    msgQueue: [],
    callbackCache: [],
    callbackId: 0,
    processingMsg: false,
    isReady: false,
    isNotifyReady: false
  }

  ApiBridge.create = function () {
    ApiBridge.bridgeIframe = document.createElement('iframe')
    ApiBridge.bridgeIframe.style.display = 'none'
    document.documentElement.appendChild(ApiBridge.bridgeIframe)
  }

  ApiBridge.prepareProcessingMessages = function () {
    ApiBridge.processingMsg = true
  }

  ApiBridge.fetchMessages = function () {
    if (ApiBridge.msgQueue.length > 0) {
      var messages = JSON.stringify(ApiBridge.msgQueue)
      ApiBridge.msgQueue.length = 0
      return messages
    }
    ApiBridge.processingMsg = false
    return null
  }

  ApiBridge.callNative = function (clz, method, args, callback) {
    var msgJson = {}
    msgJson.clz = clz
    msgJson.method = method
    if (args != undefined) msgJson.args = args
    if (callback) {
      var callbackId = ApiBridge.getCallbackId()
      ApiBridge.callbackCache[callbackId] = callback
      if (msgJson.args) {
        msgJson.args.callbackId = callbackId.toString()
      } else {
        msgJson.args = {
          callbackId: callbackId.toString()
        }
      }
    }

    if (
      browser.versions.inApp ||
      window.isInApp ||
      (!browser.versions.weixin &&
        !browser.versions.qq &&
        getQueryString('id') !== null &&
        !/.*#\/share?/gi.test(location.href))
    ) {
      if (browser.versions.ios) {
        if (ApiBridge.bridgeIframe == undefined) {
          ApiBridge.create()
        }
        ApiBridge.msgQueue.push(msgJson)
        if (!ApiBridge.processingMsg)
          ApiBridge.bridgeIframe.src = 'kcnative://go'
      } else if (browser.versions.android) {
        // 兼容马甲包
        const agents = ['miya', 'myyw', 'buou', 'ppyy', 'ppkh'],
          ua = window.navigator.userAgent

        let result = agents.some(item => ua.indexOf(item) > -1)

        // 添加判断,是app可进行跳转。浏览器则不显示弹窗
        result && prompt(JSON.stringify(msgJson))
        // browser.versions.inApp && prompt(JSON.stringify(msgJson));
        // android
        // prompt(JSON.stringify(msgJson));
        // prompt("弹窗")
      }
    }
  }

  ApiBridge.getCallbackId = function () {
    return ApiBridge.callbackId++
  }

  ApiBridge.onCallback = function (callbackId, obj) {
    if (ApiBridge.callbackCache[callbackId]) {
      ApiBridge.callbackCache[callbackId](obj)
    }
  }

  ApiBridge.onBridgeInitComplete = function (callback) {
    ApiBridge.callNative(
      'ApiBridge',
      'onBridgeInitComplete',
      {},
      callback
    )
  }

  ApiBridge.onNativeInitComplete = function (callback) {
    ApiBridge.isReady = true
    if (callback) {
      callback()
      ApiBridge.isNotifyReady = true
    }
  }

  ApiBridge.compile = function (aIdentity, aJS) {
    var value
    var error
    try {
      value = eval(aJS)
    } catch (e) {
      var err = {}
      err.name = e.name
      err.message = e.message
      err.number = e.number & 0xffff
      error = err
    }

    ApiBridge.callNative('ApiBridge', 'compile', {
      identity: aIdentity,
      returnValue: value,
      error: error
    })
  }

  var _Configs = {
    isOpenJSLog: false,
    sysLog: {},
    isOpenNativeXHR: true,
    sysXHR: {}
  }
  _Configs.sysLog = global.console.log
  _Configs.sysXHR = global.XMLHttpRequest

  // js通信交互方法
  var kerkee = {}

  /*****************************************
   * 事件监听
   *****************************************/
  kerkee.Event = {}
  kerkee.addEventListener = function (event, callback) {
    ApiBridge.callNative(
      'event',
      'addEventListener',
      {
        event: event
      },
      callback
    )
  }

  kerkee.registerHitPageBottomListener = function (callback, threshold) {
    ApiBridge.callNative('ApiBridge', 'setHitPageBottomThreshold', {
      threshold: threshold
    })
    kerkee.onHitPageBottom = callback
  }

  /**
   * 设备已准备完成
   */
  kerkee.onDeviceReady = function (handler) {
    ApiBridge.onDeviceReady = handler

    if (ApiBridge.isReady && !ApiBridge.isNotifyReady && handler) {
      console.log('-- device ready --')
      handler()
      ApiBridge.isNotifyReady = true
    }
  }

  /*****************************************
   * 接口
   *****************************************/
  /**
   * 获取客户端用户token
   * @params { Function }, callback 回调
   * @return { Object } ,  { key, player }
   *
   */
  kerkee.initData = function (callback) {
    ApiBridge.callNative('jsBridgeClient', 'initData', {}, callback)
  }

  global.ApiBridge = ApiBridge
  global.kerkee = kerkee
  global.jsBridgeClient = kerkee

  // 注册事件,绑定到全局方法
  kerkee.register = function (_window) {
    _window.ApiBridge = window.ApiBridge
    _window.kerkee = window.kerkee
    _window.console.log = window.console.log
    _window.XMLHttpRequest = window.XMLHttpRequest
    _window.open = window.open
  }

  // 客户端配置 TODO
  ApiBridge.onBridgeInitComplete(function (aConfigs) {
    if (aConfigs) {
      if (aConfigs.hasOwnProperty('isOpenNativeXHR')) {
        _Configs.isOpenNativeXHR = aConfigs.isOpenNativeXHR
      }
    }
    ApiBridge.onNativeInitComplete(ApiBridge.onDeviceReady)
  })

  /**
   * h5注册监听服务端⼴播
   * @params { Obejct }, optinos 配置项
   * @params { Function }, callback 回调
   * @return { Object }
   *
   */
  kerkee.subscribeAppPush = function (cmdId, callback) {
    if (!window.onAppPush) {
      window.onAppPush = function (resCmdId, data) {
        // 客户端返回base64转字节数组
        const arrayBuffer = _base64ToArrayBuffer(data)

        const index = pushOtions.findIndex(
          item => item.cmdId === resCmdId
        )
        let resData = null
        if (index > -1) {
          resData = decode(
            pushOtions[index].resDesction,
            new Uint8Array(arrayBuffer)
          )
        } else {
          resData = '无法解析数据'
        }

        callback(resCmdId, resData)
      }
    }

    ApiBridge.callNative('jsBridgeClient', 'subscribeAppPush', {
      cmdId: cmdId
    })
  }
})(window)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298

# 5.H5唤起APP

参考地址 (opens new window) github地址 (opens new window)

唤端一般包含了 唤起App下载App 以及 唤起App失败后自动下载 这三个功能。

唤端所使用的技术,通常可以统称为Deep Link(深度链接)技术。不同平台对这项技术有着不同的实现,主流的有这几种:

  • URL Scheme(全平台通用)
  • Universal Link(通用链接,iOS系统专属)
  • App Links 以及衍生的 Chrome Intents(安卓系统专属)

当然,以上只是国际通用标准,我们还需要考虑到“国内特色”,包括但不限于微信爸爸、微博、UC浏览器等这些环境的唤端,对于这些App,主流的唤端方式有这么几种

  • Universal Link(部分App可用,快被国内主流App禁干净了)
  • 微信API launchApplication 和 getInstallState (需要申请白名单,难度很高)
  • 微信开放标签 wx-open-launch-app (微信推荐方式,需要审核,流程较繁琐)
  • 跳转到应用宝,然后通过应用宝唤端 (适用于微信安卓环境)
  • 弹出个蒙层,友好的提示用户,点击右上角按钮,然后选择在浏览器中打开 (easy~)

主要介绍下 URL SchemeUniversal Link微信唤端自家公司App 四种技术场景下的唤端实现

  • URL Scheme:

URL Scheme其实就是一个URL前面的协议部分,比如这个地址 https://m.zhuanzhuan.com,其中 https就是一个Scheme,代表这是一个https的地址。

我们再看一个地址 zhuanzhuan://jump/core/myBuyList/jump?tab=0,当我们访问这个地址的时候,如果系统中有注册 zhuanzhuan:// => 转转App,那么系统便会使用转转App打开这个链接了,接着App会解析URL的path和search部分,执行对应的操作。

那么如何向系统注册 zhuanzhuan 这个Scheme呢,安卓可以在 manifest 里通过 intent-filter 配置,iOS 则可以在 info.plist 文件中添加 URL types 来注册一个 Scheme。

  • Scheme的调用方法:

Scheme的调用也十分简单,我们可以通过 location.hrefiframea标签 来调用 这三种方式并无本质区别,都是让系统访问一个URL,只不过在某些环境下,当方法失效时,需要采用另一种形式。

// SCHEMA_PATH 为 URL Scheme 地址

// location.href 形式
window.top.location.href = SCHEMA_PATH;

// a标签形式
const a = document.createElement("a");
a.setAttribute("href", SCHEMA_PATH);
a.click();

// iframe形式
iframe = document.createElement('iframe');
iframe.frameBorder = '0';
iframe.style.cssText = 'display:none;border:0;width:0;height:0;';
document.body.append(iframe);
iframe.src = SCHEMA_PATH;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 唤端失败自动下载:

使用URL Scheme会遇到一个棘手的问题,那就是我们无法得知是否成功唤起App。这个也可以理解,因为这个方式本质上就是访问一个URL,并没有特别的地方。

但是唤端失败(通常是用户没有装这个App),我们需要引导用户去下载,这就要求我们需要通过一些手段,去检测唤端是否成功。

常见的做法是通过监听页面是否在 n秒内 隐藏来判断是否成功唤起App,因为如果唤起,那当前页面必然已经退到后台去了。

所以我们处理的流程是这样:

1、当点击唤端后,定时器延时n秒(我们设定的是2.5秒)后执行下载操作
2、监听 visibilitychange 事件,如果页面隐藏,则表示唤端成功,清除定时器
3、如果 visibilitychange 事件没有被触发,那么就代表唤端失败,n秒后就会执行下载的操作

// n秒后执行下载
const timer = setTimeout(() => {
  this.__download(options);
}, options.delay);

// 页面隐藏,那么代表已经调起了app,就清除下载的定时器
const visibilitychange = function() {
  const tag = document.hidden || document.webkitHidden;
  tag && clearTimeout(timer);
};

document.addEventListener("visibilitychange", visibilitychange, false);
document.addEventListener("webkitvisibilitychange", visibilitychange, false);
window.addEventListener(
  "pagehide",
  function() {
    clearTimeout(timer);
  },
  false
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

不过这个方法有个致命问题,就是 n 应该取多少,不同的手机,唤端的时间并不一样。

时间设置太长,用户下载等待太久,会导致用户还没看到下载就退出了。 时间设置太短,可能会导致还没有唤起App,就又同时执行了下载的逻辑。

这里我们经过测试,觉得n设置为2500ms-3000ms,是一个比较合适的值

# 6.Web Workers 解决主线程阻塞问题

Web Workers 主要是为了解决 JS 单线程模型带来的性能瓶颈问题

  1. 解决主线程阻塞问题​​:JavaScript 是单线程的,当执行耗时操作(如复杂计算、大数据处理)时会阻塞主线程,导致页面卡顿和无响应。

  2. 提升用户体验​​:通过将耗时任务(如图像处理、数据排序、加密解密等)转移到 Worker 线程,确保用户交互(如点击、滚动)能即时响应,避免页面冻结

  3. 利用多核 CPU 性能​​:现代浏览器支持多线程并行处理,Web Workers 可以充分发挥多核 CPU 的优势,加速任务执行(如科学计算、3D 渲染)

限制​​:Worker 线程无法直接操作 DOM,需通过 postMessage 与主线程通信,且受同源策略约束

典型应用案例包括:Bilibili 的弹幕处理、在线图片编辑器、金融数据分析工具等

# 示例代码 - 没做任何优化

<!DOCTYPE html>
<html>
<head>
  <title>未优化的阻塞示例</title>
</head>
<body>
  <h1>同步阻塞演示</h1>
  <!-- 这个按钮在计算完成前不会响应 -->
  <button id="testBtn">点击我测试响应</button>
  <div id="output">准备开始计算...</div>
  <script>
    // 获取DOM元素
    const btn = document.getElementById('testBtn');
    const output = document.getElementById('output');
    // 按钮点击事件
    btn.addEventListener('click', () => {
      output.textContent += '\n按钮点击已响应!';
      console.log('按钮点击事件处理');
    });
    // 长时间同步计算函数
    function blockingCompute() {
      const start = Date.now();
      output.textContent = '计算开始...';
      // 模拟5秒的同步计算
      while (Date.now() - start < 5000) {
        // 纯粹的忙等待循环
      }
      output.textContent += '\n计算完成!';
    }
    // 执行计算
    blockingCompute();
    // 这行代码在计算完成后才会执行
    output.textContent += '\n页面脚本执行完毕';
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 代码行为说明

    1. ​​页面加载流程​​:
    • 浏览器解析HTML/CSS并渲染初始页面
    • 执行JavaScript代码时遇到blockingCompute()
    • 主线程被5秒的同步计算完全阻塞
    • 计算完成后才会继续执行后续代码
    1. ​用户交互表现​​:
    • 在5秒计算期间点击按钮​​不会有任何反应​​
    • 控制台​​不会​​立即输出点击日志
    • 所有DOM更新​​冻结​​直到计算完成
    1. ​​控制台输出顺序​​:
    • 计算开始...
    • 计算完成!
    • 页面脚本执行完毕
    • (之后点击按钮才会看到)
    • 按钮点击已响应!

# 示例代码 - Web Workers优化

TIP

注意:需要使用本地服务器运行html文件

<!-- 主线程代码 (main.js) -->
<!DOCTYPE html>
<html>
<head>
  <title>非阻塞页面示例</title>
</head>
<body>
  <h1>页面内容立即显示</h1>
  <p>这个段落和按钮应该立即显示,不受计算影响</p>
  <button id="myBtn">测试按钮</button>
  <div id="result"></div>
  <script>
    // 页面元素立即渲染
    document.getElementById('result').textContent = '页面已加载,计算进行中...';
    // 创建Web Worker
    const worker = new Worker('compute.js');
    // 监听计算结果
    worker.onmessage = (e) => {
      document.getElementById('result').textContent = `计算结果: ${JSON.stringify(e.data)}`;
    };
    // 按钮点击事件 - 会立即响应
    document.getElementById('myBtn').addEventListener('click', () => {
      console.log('按钮点击立即响应!');
      console.log('UI交互未被阻塞');
    });
    // 启动计算
    worker.postMessage('start');
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Web Worker代码 (compute.js)
// 模拟长时间计算
function heavyCompute() {
  const start = Date.now();
  let progress = 0;
  // 模拟5秒计算
  while (progress < 100) {
    // 每100ms更新一次进度
    if (Date.now() - start > progress * 50) {
      progress++;
      // 向主线程发送进度更新
      self.postMessage({ type: 'progress', value: progress });
    }
  }
  return "计算完成";
}
// 监听主线程消息
self.onmessage = (e) => {
  if (e.data === 'start') {
    const result = heavyCompute();
    self.postMessage({ type: 'result', value: result });
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lastUpdate: 6/3/2025, 11:52:51 AM