首页首页
前端
非前端
辅助
Github
前端
非前端
辅助
Github
  • 前端基础

    • HTML基础
    • CSS基础
    • JS基础
    • ES6基础
    • HTTP基础
    • 前端缓存
    • 页面性能
    • 数据结构基础
    • 我的文章
  • 前端进阶

    • Vue2基础
    • Vue2进阶
    • Vue3基础
    • Vue3进阶
    • React基础
    • React进阶
    • React18新特性
    • Vue和React对比
    • RN基础
    • RN环境搭建和打包发布
    • 打包工具
    • TS基础
    • Nuxt基础
    • 小程序基础
    • 微前端基础
    • uni-app基础
    • 业务相关
    • 低代码相关
  • 前端代码练习

    • CSS代码练习
    • JS代码练习
    • 算法代码练习
  • 前端代码技巧

    • 工具库
    • 工具函数
    • CSS动画库
    • CSS代码技巧
    • JS代码技巧
    • 项目技巧

1.axios原理

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

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

与 Fetch API 的对比

特性AxiosFetch 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;
};

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;
  }
]

四、性能优化设计

    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.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和全局事件监听实现回调
  • ​​平台适配​​:

// 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
  • 二. 核心通信流程
  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. 原生调用H5回调
// 原生调用H5注册的回调
ApiBridge.onCallback(callbackId, responseData);
↓
// 触发H5预先注册的回调函数
callbackCache[callbackId](responseData);
  • 三. 关键技术实现
  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(); // 触发消息发送
  }
}
  • 四. 完整代码示例
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

参考地址github地址

唤端一般包含了 唤起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 单线程模型带来的性能瓶颈问题

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

示例代码 - 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.图片懒加载实现与原理

  • 核心原理
      1. 图片不直接设置 src,使用 data-src
      1. 检测图片是否进入可视区域
      1. 进入时,将 data-src 的值赋给 src
      1. 图片开始加载

现代方案推荐使用 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"]
    }
  ]
}
最后更新: 2026/1/8 20:58
Prev
uni-app基础
Next
低代码相关