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 采用了分层架构设计,主要分为以下几层:
- 核心层(Core):提供基础请求能力
- 适配器层(Adapter):处理环境差异(浏览器/XHR 和 Node.js/http)
- 拦截器层(Interceptors):请求/响应拦截机制
- 配置层(Config):统一的配置管理
- 取消请求层(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;
};
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. 拦截器机制
拦截器采用链式 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);
});
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'));
});
}
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;
}
]
四、性能优化设计
- 配置合并优化:采用深度合并策略
- 请求数据转换优化:按需转换
- 适配器懒加载:运行时按需加载
- 内存管理:及时清理引用
2.i18n原理
一、核心概念
- 国际化(i18n):使应用支持多语言的过程(i和n之间有18个字母)
- 本地化(l10n):为特定地区适配内容的过程(l和n之间有10个字母)
- 语言环境(Locale):由语言代码和国家代码组成(如zh-CN, en-US)
- 翻译键(Translation Key):用于标识文本片段的唯一键
- 复数形式(Pluralization):不同语言有不同的复数规则
- 文本方向(Text Direction):LTR(左到右)和RTL(右到左)布局
二、常见问题与解决方案
1. 翻译管理问题
现象:不同开发者使用不同命名风格,如home.title、homeHeader、home_title
- 制定命名规范文档,例如:
- 模块化命名:模块.功能.元素(
login.form.submitButton) - 统一分隔符:强制使用点号.或下划线_
- 模块化命名:模块.功能.元素(
使用TS类型约束:
type I18nKeys = {
login: {
form: {
submitButton: string;
};
};
};
declare module 'i18next' {
interface CustomTypeOptions {
resources: I18nKeys;
}
}
3.移动端常见问题
一、移动端1px边框问题
移动端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);
}
}
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>");
}
3、PostCSS插件(自动化方案)
工具:使用PostCSS插件(如postcss-write-svg)自动生成1px边框代码。
// postcss.config.js
module.exports = {
plugins: {
'postcss-write-svg': {
utf8: false
}
}
}
@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;
}
二、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">
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 });
(2) 禁止Android手势缩放
document.addEventListener('gesturestart', function(e) {
e.preventDefault(); // 阻止Android手势缩放
});
三、移动端click屏幕产生300 ms的延迟响应
当用户一次点击屏幕之后,浏览器并不能立刻判断用户是要进行双击缩放,还是想要进行单击操作。因此,就会等待300毫秒,以判断用户是否再次点击了屏幕。
解决方案:
- 1、通过 meta 标签禁用网页的缩放
- 2、通过 meta 标签将网页的 viewport 设置为 ideal viewport。
- 3、FastClick
FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。简而言之,FastClick 在检测到 touchend 事件的时候,会通过 DOM 自定义事件立即触发一个模拟click事件的自定义事件,并把浏览器在 300 毫秒之后真正触发的 click 事件阻止掉。
四、iphoneX的“刘海”,底部的安全区域。让页面不能占满屏幕
解决方案:
添加viewport-fit=cover meta标签,使页面占满整个屏幕
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
五、移动端滚动穿透问题
移动端滚动穿透是指当页面弹窗(如模态框、菜单)出现时,用户在弹窗上滑动会导致底层页面跟随滚动的现象。
根本原因
- (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
缺点:会禁用弹窗内所有滚动
- (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 });
优点:精准控制滚动行为
- (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);
}
缺点:页面会轻微抖动(跳转位置)
- (4) CSS现代属性:overscroll-behavior
适用场景:兼容现代浏览器(Chrome 63+、Firefox 59+)
.modal {
overscroll-behavior: contain; /* 阻止滚动链 */
}
缺点: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. 问题原因分析
- (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);
- (2) 动态调整布局(针对底部输入框)
/* CSS:预留键盘高度 */
body {
padding-bottom: 50vh; /* 初始预留空间 */
}
.keyboard-active {
padding-bottom: 0;
}
// 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');
});
});
- (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();
七、虚拟滚动
- 基本概念
- 可视窗口(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
主流框架实现
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>
);
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>
4.H5和App的Webview交互(⭐️)
一、H5通过UA判断是否在webview环境(追加UA)
web页面通过脚本能够很容易的拿到浏览器的ua属性,那么在app启动的时候,自定义添加一个ua属性,那么web页面就能够根据这个自定义的ua属性,轻而易举的判断出是否在app内了。
二、JS和Native交互(JSBridge)
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和全局事件监听实现回调
- H5→APP:通过
平台适配:
// Android调用方式
prompt(JSON.stringify(msgJson)); // 通过prompt协议通信
// iOS调用方式
iframe.src = 'kcnative://go'; // 通过URL Scheme触发
| 平台 | 触发方式 | 特点 |
|---|---|---|
| Android | prompt(JSON.stringify(msg)) | 依赖WebView的prompt拦截 |
| iOS | 动态创建iframe触发URL Scheme | 通过webkit.messageHandlers |
- 二. 核心通信流程
- 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))
- 原生调用H5回调
// 原生调用H5注册的回调
ApiBridge.onCallback(callbackId, responseData);
↓
// 触发H5预先注册的回调函数
callbackCache[callbackId](responseData);
- 三. 关键技术实现
- 消息队列管理
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(); // 触发消息发送
}
}
- 四. 完整代码示例
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)
5.H5唤起APP
唤端一般包含了 唤起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 Scheme、Universal 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.href 或 iframe 或 a标签 来调用 这三种方式并无本质区别,都是让系统访问一个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;
- 唤端失败自动下载:
使用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
);
不过这个方法有个致命问题,就是 n 应该取多少,不同的手机,唤端的时间并不一样。
时间设置太长,用户下载等待太久,会导致用户还没看到下载就退出了。 时间设置太短,可能会导致还没有唤起App,就又同时执行了下载的逻辑。
这里我们经过测试,觉得n设置为2500ms-3000ms,是一个比较合适的值
6.Web Workers 解决主线程阻塞问题
Web Workers 主要是为了解决 JS 单线程模型带来的性能瓶颈问题
解决主线程阻塞问题:JavaScript 是单线程的,当执行耗时操作(如复杂计算、大数据处理)时会阻塞主线程,导致页面卡顿和无响应。
提升用户体验:通过将耗时任务(如图像处理、数据排序、加密解密等)转移到 Worker 线程,确保用户交互(如点击、滚动)能即时响应,避免页面冻结
利用多核 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>
代码行为说明
- 页面加载流程:
- 浏览器解析HTML/CSS并渲染初始页面
- 执行JavaScript代码时遇到blockingCompute()
- 主线程被5秒的同步计算完全阻塞
- 计算完成后才会继续执行后续代码
- 用户交互表现:
- 在5秒计算期间点击按钮不会有任何反应
- 控制台不会立即输出点击日志
- 所有DOM更新冻结直到计算完成
- 控制台输出顺序:
- 计算开始...
- 计算完成!
- 页面脚本执行完毕
- (之后点击按钮才会看到)
- 按钮点击已响应!
示例代码 - Web Workers优化
提示
注意:需要使用本地服务器运行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>
// 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 });
}
};
7.大文件分片上传和断点续传
- 分片上传:大文件切小片并行上传提高成功率;
- 选择文件 → 计算文件hash → 分片切割 → 并行上传分片 → 服务器合并 → 完成
如何实现分片上传?
前端通过File.slice()实现分片
用SparkMD5计算文件唯一标识
通过Web Worker避免计算阻塞UI
控制并发数避免请求过多
- 断点续传:记录已传分片,中断后只传剩余部分。
- 选择文件 → 计算文件hash → 查询已上传分片 → 上传缺失分片 → 服务器合并 → 完成
如何实现断点续传?
1.文件唯一标识: 我们通过计算整个文件的hash(如MD5)来唯一标识这个文件。相同内容的文件hash相同,这还支持'秒传'功能。
2.缺失分片检测: 客户端上传前,会向服务器查询这个文件hash对应的上传状态。服务器返回一个数组,包含已成功上传的分片索引,如[0, 1, 2, 5, 6]。
3. 智能续传: 客户端对比总的分片数和已上传的分片索引,只上传缺失的分片。比如总共有10个分片,已上传了[0,1,2,5,6],那么只需要上传索引为3、4、7、8、9的分片。
8.从0到1搭建项目(H5和RN)
- 1、默认基本环境(nvm、node)安装好,node切换到脚手架要求指定的版本或者最低版本
- 2、npm全局安装对应的脚手架,根据技术栈来选
- 3、利用脚手架来创初始化项目
- 4、根据对应的项目功能,来安装对应的依赖
- 基本都要安装对应路由库、状态管理库(
pinia,vuex,redux)、UI库(如果涉及到)、网络请求库(如axios) - 根据业务需求,来安装对应的业务依赖,如时间处理库
day.js,轮播组件库swiper.js - 公共组件库,是否要抽离,是不是要搭一个私有npm库
- 基本都要安装对应路由库、状态管理库(
- 5、重构项目目录,建立对应业务需求的目录,也包括通用样式目录以及通用工具js目录
- 6、根据业务,搭建路由系统、封装好网络请求,以及对应的状态管理封装,
- 7、根据团队喜好,看是否要配置好
Eslint、Prettier、husky(git hooks)等 - 8、本地运行项目,看能否跑通
9.图片懒加载实现与原理
- 核心原理
- 图片不直接设置 src,使用 data-src
- 检测图片是否进入可视区域
- 进入时,将 data-src 的值赋给 src
- 图片开始加载
现代方案推荐使用 IntersectionObserverAPI,它性能更好,不阻塞主线程。兼容性要求高的项目可以使用传统的 scroll 事件计算位置 。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>最简单的图片懒加载</title>
<style>
.lazy-img {
width: 100%;
height: 300px;
margin: 20px 0;
background: #eee;
}
</style>
</head>
<body>
<div id="app">
<img class="lazy-img" data-src="image1.jpg" alt="">
<img class="lazy-img" data-src="image2.jpg" alt="">
<!-- 更多图片 -->
</div>
<script>
// 1. 创建观察器
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 设置真实 src
observer.unobserve(img); // 停止观察
}
});
});
// 2. 开始观察所有图片
document.querySelectorAll('.lazy-img').forEach(img => {
observer.observe(img);
});
</script>
</body>
</html>
对于Vue,组件库可以使用vue-lazyload。
10.浏览器关闭标签时发送分析数据或者日志
页面关闭时,只有特定 API 能可靠发送请求:
一、navigator.sendBeacon()
// 使用 sendBeacon
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/api/page-exit', blob);
}
navigator.sendBeacon() 利用了 HTTP 的 POST 请求,但与普通的 XMLHttpRequest 或 fetch API 不同,它在浏览器尝试卸载(例如,用户关闭标签页或导航到另一个页面)时发送数据。这意味着即使在页面卸载后,浏览器仍然会尝试完成这次数据传输。
二、fetch() keepalive: true
// 这个也能发送
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(data),
keepalive: true, // ✅ 关键参数
headers: { 'Content-Type': 'application/json' }
});
三、img图片请求
// 图片请求(最可靠)
function sendImageRequest(data) {
const params = new URLSearchParams();
params.append('exit_data', btoa(JSON.stringify(data)));
params.append('_', Date.now());
new Image().src = `/api/track.gif?${params}`;
}
- 现代浏览器方案
用visibilitychange + sendBeacon组合,sendBeacon有 64KB 数据的限制
// 用户离开页面时发送(切换标签、最小化、关闭等)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// 页面不可见时发送数据
sendHiddenData();
}
});
// 也监听 beforeunload 作为最后机会
window.addEventListener('beforeunload', () => {
sendHiddenData();
});
function sendHiddenData() {
const data = {
type: 'page_hidden',
timestamp: Date.now(),
url: window.location.href,
duration: calculatePageDuration(),
// 你的业务数据
};
// 使用 sendBeacon
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/api/page-exit', blob);
} else {
// 回退方案
sendSyncRequest(data);
}
}
function calculatePageDuration() {
if (!window.pageEnterTime) {
window.pageEnterTime = Date.now();
}
return Date.now() - window.pageEnterTime;
}
11.H5/小程序首屏优化手段
1. 关键CSS内联(H5)
将首屏必须的CSS样式直接内联到HTML中,避免CSS文件阻塞渲染:
<style>
/* 首屏关键样式 */
body { font-family: system-ui; }
.header { height: 60px; background: #fff; }
</style>
2. 资源预加载(H5)
使用preload预加载关键资源:
<link rel="preload" href="main.css" as="style">
<link rel="preload" href="main.js" as="script">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
3. 异步加载非关键JS(H5)
<script src="analytics.js" async></script>
<script src="lazy.js" defer></script>
4. 图片懒加载
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">
5. 骨架屏
在内容加载前显示骨架屏,提升感知速度:
<div class="skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-content"></div>
</div>
6. 虚拟滚动
对于长列表,只渲染可视区域内的元素:
使用 react-window 或 vue-virtual-scroller
7. 路由懒加载(H5)
使用动态导入 import()实现路由懒加载和组件懒加载,将代码拆分成多个块(chunk),让首屏只加载必需代码
8. 资源压缩
JS压缩:terser、uglify-js
CSS压缩:cssnano
图片压缩:WebP格式优先
9. CDN加速(H5)
将静态资源部署到CDN,减少网络延迟。
10. HTTP/2(H5)
开启HTTP/2多路复用,减少连接开销。
11. 开启Gzip/Brotli压缩(H5)
# nginx配置
gzip on;
gzip_types text/css application/javascript;
12.按需引入
避免全量引入第三方库:
// 按需引入
import { Button } from 'vant-weapp';
13.分包加载(小程序)
将非首屏页面和功能拆分成独立分包,减少主包体积:
{
"subPackages": [
{
"root": "pages/sub",
"pages": ["index", "detail"]
}
]
}
