Skip to content

微信小程序面试大全

作者:青见春山

来源:微信小程序八股

1. 小程序路由及区别(navigateTo、redirectTo 等)

小程序共有 5 种路由跳转方式:

方法说明页面栈表现能否回退
wx.navigateTo保留当前页面,跳转到应用内某个页面新页面入栈(栈+1)✅ 可以
wx.redirectTo关闭当前页面,跳转到应用内某个页面当前页出栈,新页入栈(栈不变)❌ 不可以
wx.switchTab跳转到 tabBar 页面关闭所有非 tab 页,tab 页入栈❌ 不可以
wx.navigateBack关闭当前页面,返回上一页面或多级页面页面出栈
wx.reLaunch关闭所有页面,打开应用内某个页面清空页面栈,新页入栈❌ 不可以

核心区别:

  • navigateTo:最常用,会压栈,右上角有返回按钮

  • redirectTo:替换当前页,无法返回到被替换的页面(适用于登录过期跳转等场景)

  • reLaunch:最暴力,清空整个页面栈(适用于切换身份等场景)

  • switchTab:只能跳 tabBar 页面,且会关闭所有非 tab 页

路由传参方式:

JavaScript
// 跳转
wx.navigateTo({ url: '/pages/detail/detail?id=1&name=test' })

// 接收
Page({
  onLoad(options) {
    console.log(options.id)   // '1'
    console.log(options.name) // 'test'
  }
})

注意:路由参数会成为 URL 字符串,应使用 encodeURIComponent 编码。不要依赖某个未经官方保证的固定长度,也不要在 URL 中传 Token 等敏感信息;复杂数据可通过 EventChannel、状态管理或 Storage 传递。


2. bindtap 和 catchtap 的区别

  • bindtap:事件冒泡型,点击子元素时事件会向上冒泡到父元素

  • catchtap:冒泡阶段的绑定方式,但处理器执行后会阻止事件继续向上冒泡;它不是“捕获型事件”

HTML
<!-- 父元素 -->
<view bindtap="parentTap">
  <!-- 子元素 -->
  <view catchtap="childTap">按钮</view>
</view>
  • 点击"按钮":只触发 childTap不触发 parentTap(catch 阻止了冒泡)

  • 如果子元素用 bindtap,则先触发 childTap,再冒泡触发 parentTap

补充: 还有 capture-bindcapture-catch 用于捕获阶段:

HTML
<view capture-bind:tap="captureTap">

  <view catchtap="childTap">子</view>
</view>

事件触发顺序:captureTap(捕获阶段)→ childTap(目标阶段),冒泡被 catch 阻止。


3. 小程序页面栈层级限制

小程序的页面栈最多 10 层

JavaScript
wx.navigateTo({ url: '/pages/a/a' }); // 栈深度 +1
// 如果超过 10 层,navigateTo 会失败

// 获取当前页面栈
const pages = getCurrentPages();
console.log('当前页面栈深度:', pages.length);

应对策略:

  • 对于深层级跳转(如分类→子分类→商品列表→商品详情),在第 8~9 层时用 redirectTo 替代 navigateTo

  • 或者使用 reLaunch 清空栈后跳转


4. 小程序生态、OpenID 与 UnionID 的区别

小程序生态

微信小程序生态包括:微信开发者工具、云开发(云函数+云数据库+云存储)、微信支付、分享能力、订阅消息、插件系统、第三方服务商(SaaS)等。

OpenID 与 UnionID 的区别

维度OpenIDUnionID
定义用户在某个小程序/公众号下的唯一标识用户在同一开放平台账号下所有应用的唯一标识
作用域单个小程序同一开放平台绑定的所有应用(公众号、小程序、APP等)
获取方式wx.login 取得一次性 code,后端调用 code2Session 换取小程序绑定开放平台账号后,code2Session 可能一并返回
是否需要授权获取登录 code 不要求用户资料授权不使用公众号网页 OAuth 的 snsapi_login;返回条件以开放平台绑定和微信规则为准
JavaScript
// 获取 OpenID
wx.login({
  success(res) {
    if (res.code) {
      // 发送 code 到后端,后端调用微信 API 换取 openid
      // https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=CODE&grant_type=authorization_code
      // 返回 { openid, session_key, unionid? }
    }
  }
});

UnionID 的价值: 如果一个公司有多个小程序和公众号,通过 UnionID 可以打通用户体系,识别出是同一个人。


5. 小程序中 Webview 的使用

小程序支持通过 <web-view> 组件嵌入 H5 页面:

HTML
<web-view src="https://example.com/page"></web-view>

使用场景:

  • 需要快速上线但来不及开发小程序原生页面时,用 H5 作为过渡

  • 需要展示复杂的富文本或第三方内容

  • 活动页、营销页等临时页面

限制条件:

  • 域名必须在小程序后台配置白名单(业务域名)

  • 不支持个人小程序(需要企业认证)

  • <web-view> 会占满整个页面,无法与小程序原生组件混用

  • 部分功能受限:无法使用 wx.login、wx.share 等原生 API(需要通过 JS-SDK 桥接)


6. 小程序原生页和嵌入的 H5 Webview 如何通信?

方式一:URL 参数传递(简单数据)

HTML
<!-- 小程序页面 -->
<web-view src="https://h5.example.com/page?userId=456"></web-view>

不要把访问令牌放入 URL,URL 可能出现在日志、分享内容、Referer 或截图中。需要认证时应建立短期的一次性交换码,或让 H5 自行完成安全登录。

方式二:wx.miniProgram.postMessage(H5 → 小程序)

JavaScript
// H5 页面中(通过微信 JS-SDK)
wx.miniProgram.postMessage({
  data: { action: 'updateUser', userId: 789 }
});

// 小程序页面中接收
Page({
  onMessage(event) {
    console.log(event.detail.data); // 多次 postMessage 数据组成的数组
  }
});

bindmessage 不是实时消息通道。官方文档说明,消息会在小程序后退、组件销毁、分享、复制链接等特定时机触发接收;需要实时同步时应使用受控后端通道等其他方案。

HTML
<web-view src="https://h5.example.com" bindmessage="onMessage"></web-view>

方式三:wx.miniProgram.navigateTo(H5 跳转小程序页面)

JavaScript
// H5 页面中
wx.miniProgram.navigateTo({ url: '/pages/detail/detail?id=123' });

外部 H5 唤起小程序应使用微信后台或服务端 API 生成的 URL Scheme / URL Link。不要自行拼接未公开的 wx-weixin:// 地址;这类链接用于外部页面打开小程序,也不是已嵌入 web-view 与宿主页之间的实时通信机制。

方式五:服务端中转

H5 和小程序都通过同一后端 API 读写数据,间接实现通信。


7. 小程序开发的熟悉程度和困难挑战

熟悉的方面:

  • 页面生命周期(onLoad、onShow、onHide、onUnload)

  • 组件化开发(Component 构造器)

  • 状态管理(globalData、MobX-miniprogram、Taro 的 Redux)

  • 自定义组件通信(properties、triggerEvent、selectComponent)

  • 性能优化(分包加载、预加载、骨架屏、图片懒加载)

  • 跨端框架(uni-app、Taro)的使用和兼容性处理

常见困难挑战:

  1. 兼容性问题:不同基础库版本(如 2.x vs 3.x)API 差异大,需要做版本判断和降级方案

  2. 性能瓶颈:长列表渲染卡顿(需要虚拟列表)、 setData 数据量过大导致通信开销

  3. 登录态维护:wx.login 获取 code → 后端换 token → 前端存 token,流程复杂且易出错

  4. 分包加载配置:主包 2MB 限制,分包总共 20MB 限制,需要合理规划路由和资源

  5. 审核风险:微信审核标准不透明,经常被拒需要反复修改


8. H5 压缩介绍

H5 压缩通常指以下几个方面:

静态资源压缩

  • HTML/CSS/JS 压缩:使用 UglifyJS、Terser(JS)、cssnano(CSS)去除空格、注释、缩短变量名

  • 图片压缩:使用 TinyPNG、imagemin 等工具,将 PNG/JPG 压缩 30%~70%

  • Gzip/Brotli 压缩:服务端配置,对文本类资源压缩率可达 60%~80%

JavaScript
// webpack 配置示例
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new TerserPlugin()]
  },
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240 // 大于 10KB 才压缩
    })
  ]
};

图片格式优化

  • WebP 格式比 JPEG 小 25%~35%

  • AVIF 格式更先进,压缩率更高

  • 使用 <picture> 标签做格式降级

代码层面压缩

  • Tree Shaking(摇树优化):移除未使用的代码

  • Code Splitting(代码分割):按需加载

  • Dynamic Import(动态导入):import() 懒加载

传输层压缩

  • HTTP/2 多路复用减少请求

  • Content-Encoding: gzip / br 响应头


9. 压缩后性能是否提升?性能优化经历

压缩后的性能提升

是的,压缩能显著提升性能:

  • Gzip 压缩 JS/CSS 文件通常减少 60%~80% 体积

  • 图片压缩减少 30%~50% 体积

  • Tree Shaking 减少 10%~30% JS 体积

性能优化经历(常见场景)

  1. 首屏加载优化

    • 路由懒加载:const Home = () => import('./views/Home.vue')

    • 图片懒加载 + WebP 格式替换

    • 第三方库按需引入(如 Element-UI 按需加载组件)

    • SSR / 预渲染关键页面

    • CDN 加速静态资源

  2. 运行时性能优化

    • 虚拟列表处理长列表(如 vue-virtual-scroller

    • 防抖节流处理高频事件(scroll、resize、input)

    • Web Worker 处理耗时计算,避免阻塞主线程

    • v-if vs v-show 合理使用

    • Object.freeze() 冻结大数据对象避免响应式开销

  3. 网络优化

    • HTTP/2 多路复用

    • 资源预加载(<link rel="preload">

    • DNS 预解析(<link rel="dns-prefetch">

    • 接口缓存策略(ETag、Cache-Control)


11. Vue 2 和 Vue 3 的区别

维度Vue 2Vue 3
响应式原理Object.defineProperty(需要递归遍历)Proxy(更高效,支持动态新增属性)
API 风格Options APIComposition API + Options API
TypeScript支持较差原生支持更好
生命周期beforeDestroy / destroyedonBeforeUnmount / onUnmounted
模板根节点必须单根节点支持多根节点(Fragment)
性能较好更好(编译优化、Tree-shaking、静态提升)
体积取决于构建方式与功能使用情况支持 Tree-shaking,实际体积取决于构建产物
Teleport不支持内置 <Teleport>
Suspense不支持内置 <Suspense>,当前仍需关注其实验性状态与版本文档
多根节点不支持支持
事件总线new Vue() 做事件总线推荐 mitt
过滤器支持 filter移除了 filter,用方法或 computed 替代
$set / $delete需要不需要(Proxy 原生支持)

Composition API 的优势:

JavaScript
// Vue 3 Composition API
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    onMounted(() => { /* ... */ })
    return { count, double }
  }
}
  • 逻辑复用更灵活(Composables 替代 Mixins)

  • 相关逻辑聚合在一起,而不是分散在 data、methods、computed 中

  • 更好的类型推断


12. ref 和 reactive 的区别

维度refreactive
适用类型基本类型 + 对象类型仅对象/数组类型
访问方式需要 .value直接访问属性
模板使用模板中自动解包,不需要 .value直接使用
解构解构出 ref 对象本身仍可响应;直接取出 .value 只是普通值直接解构属性会丢失响应性(需 toRefs
重新赋值可以 .value = newVal不能直接替换整个对象
底层原理ref 自己跟踪 .value,对象值会再做深层响应式转换对对象使用 Proxy
JavaScript
import { ref, reactive, toRefs } from 'vue'

// ref
const count = ref(0)
count.value++  // 必须 .value

// reactive
const state = reactive({ name: '张三', age: 25 })
state.name = '李四'  // 直接修改

// reactive 解构会丢失响应性
const { name, age } = state  // ❌ 丢失响应性
const { name, age } = toRefs(state)  // ✅ 保持响应性

使用建议:

  • 基本类型用 ref

  • 对象类型用 reactive(但要注意解构问题)

  • 组件 defineProps 推荐用 defineProps<{ ... }>()


13. Vue 2 和 Vue 3 的通信方式区别

共同的通信方式

  • Props / $emit:父子组件通信(最常用)

  • EventBus / mitt:跨层级通信

  • provide / inject:依赖注入(祖先→后代)

  • Vuex / Pinia:全局状态管理

区别

维度Vue 2Vue 3
事件总线new Vue()this.$busmitt
状态管理Vuex(this.$store)Pinia(推荐)或 Vuex 4
依赖注入provide / inject同样支持,且 Composition API 中更灵活
v-model一个组件只能一个 v-model支持多个 v-modelv-model:name
.sync:prop.sync="val"移除,用 v-model:prop 替代
$attrs / $listeners都有$listeners 合并到 $attrs
跨组件通信MixinsComposables(推荐)
Plain
<!-- Vue 3 多 v-model -->
<my-component v-model:name="name" v-model:age="age" />

<!-- Vue 3 Composables 逻辑复用 -->
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>

14. 设计模式

常见设计模式在前端中的应用

1. 单例模式(Singleton)

JavaScript
class Database {
  static instance = null;
  static getInstance() {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }
}

前端场景:Vuex/Pinia Store、全局 EventBus、连接池。

2. 工厂模式(Factory)

JavaScript
function createComponent(type) {
  switch(type) {
    case 'button': return new Button();
    case 'input': return new Input();
    case 'modal': return new Modal();
  }
}

前端场景:Vue 的 createApp()、React 的 createElement

3. 观察者模式(Observer)

JavaScript
class EventEmitter {
  constructor() { this.events = {}; }
  on(event, fn) { (this.events[event] = this.events[event] || []).push(fn); }
  emit(event, ...args) { this.events[event]?.forEach(fn => fn(...args)); }
}

前端场景:EventBus、Vue 的响应式系统、DOM 事件监听。

4. 发布-订阅模式(Pub/Sub)

与观察者模式类似,但有中间的事件调度中心(解耦发布者和订阅者)。

5. 策略模式(Strategy)

JavaScript
const strategies = {
  required: (val) => val !== '',
  minLength: (val, len) => val.length >= len,
  email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
};

前端场景:表单校验、动画缓动函数、路由策略。

6. 代理模式(Proxy)

前端场景:Vue 3 的 Proxy 响应式、ES6 的 Proxy、图片懒加载(代理图片加载)。

7. 装饰器模式(Decorator)

前端场景:TypeScript 装饰器、Vue 的 mixins(类似装饰器概念)。

8. 适配器模式(Adapter)

前端场景:接口数据格式适配、不同框架间 API 适配。


15. 如何学习 JavaScript / 经典书籍

经典书籍推荐

入门级:

  • 《JavaScript 高级程序设计》(红宝书)—— 最全面的 JS 经典

  • 《你不知道的 JavaScript》(上/中/下)—— 深入理解 JS 核心机制

  • 《JavaScript 语言精粹》(蝴蝶书)—— Douglas Crockford 的精炼总结

进阶级:

  • 《JavaScript 设计模式与开发实践》—— 曾探著,前端设计模式经典

  • 《ES6 入门教程》—— 阮一峰著,免费在线版

学习路径建议:

  1. 先掌握基础语法(变量、函数、循环、对象)

  2. 深入理解原型链、闭包、作用域、this 指向

  3. 学习异步编程(Promise、async/await、事件循环)

  4. 掌握 DOM 操作和 BOM API

  5. 学习设计模式和工程化实践

  6. 持续关注 ECMAScript 新标准


16. Web Worker

什么是 Web Worker

Web Worker 是浏览器提供的多线程机制,允许在后台线程中运行 JavaScript,不阻塞主线程(UI 线程)。

JavaScript
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
  console.log('计算结果:', e.data);
};

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

缺点/限制

  1. 通信限制:通常通过 postMessage 传递数据并使用结构化克隆算法;函数和 DOM 节点不可克隆,现代环境可克隆常见 Error 数据,但自定义属性和堆栈等细节不应假定完整保留

  2. 不能操作 DOM:Worker 中无法访问 documentwindow 等 API

  3. 加载限制Worker() 的普通 URL 通常受同源/CORS 与脚本类型规则约束,也可在符合策略时使用 blob: URL;不能简单概括为所有 Worker 脚本都必须同源

  4. 数据传输开销:大数据量传递时,结构化克隆的性能开销较大

  5. 调试与兼容性:现代浏览器通常支持 Worker 的断点和 console,但独立线程让时序和性能问题更难复现,仍需检查目标环境能力

  6. 内存消耗:每个 Worker 都有独立的全局作用域和内存空间

  7. 资源限制:浏览器可按设备资源限制或回收 Worker,但标准没有固定“4 到 8 个”的通用上限;应使用 worker pool 控制并发

Transferable Objects(零拷贝传输)

JavaScript
// 用 ArrayBuffer 做零拷贝,避免克隆开销
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage(buffer, [buffer]);
// 传输后 buffer 在主线程中不可用(被"转移"了)

17. JS 安全相关——表单提交的安全问题

两种主要安全问题

1. XSS(Cross-Site Scripting,跨站脚本攻击)

攻击者在表单中注入恶意脚本代码:

HTML
<!-- 用户在表单输入 -->
<script>alert('盗取你的Cookie:' + document.cookie)</script>
<img src="x" onerror="stealData()">

<!-- 如果服务端不做过滤,直接渲染到页面 -->
<div>用户评论:<script>恶意代码</script></div>

防范措施:

  • 输入过滤/转义:服务端对 <>"'& 等字符转义

  • CSP(Content Security Policy):限制可执行脚本来源

  • HttpOnly Cookie:防止 JS 读取 Cookie

  • 使用 **textContent** 而非 **innerHTML**

  • 前端框架自带防 XSS:Vue、React 默认对模板输出做转义

2. CSRF(Cross-Site Request Forgery,跨站请求伪造)

攻击者诱导用户在已登录的状态下,向目标网站发送伪造请求:

HTML
<!-- 攻击者网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">
<!-- 或者自动提交的隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf-form').submit();</script>

防范措施:

  • CSRF Token:表单中带一个随机 token,服务端验证

  • SameSite CookieSameSite=LaxSameSite=Strict

  • 验证 Referer / Origin 头

  • 双重 Cookie 验证

  • 关键操作二次确认(如输入密码)


  • 单个 Cookie 大小限制:约 4KB(不同浏览器略有差异)

  • Cookie 数量、每个域名/站点的总量和超限淘汰策略由浏览器实现决定,不存在可靠的“所有域名总共约 4MB”规则

约 4KB 的计算口径在浏览器间有差异,有的主要限制 name/value,有的还计算属性;工程上应让 Cookie 尽可能小并实测目标浏览器。


适合放:

  • 认证信息(token、sessionId)

  • 用户偏好设置(语言、主题)

  • 跟踪标识(用户ID、来源标记)

  • 需要在匹配 Domain/主机、Path、Secure、SameSite 等条件时自动发送到服务端的小型数据

特点: 可设置过期时间;HttpOnly 降低被脚本直接读取的风险但不能防止 XSS 发起操作;只有匹配请求条件的 Cookie 才会自动携带

localStorage(配额由浏览器实现与存储策略决定)

适合放:

  • 用户配置(主题、布局偏好)

  • 缓存数据(接口数据本地缓存)

  • 离线数据

  • 大段文本(编辑器草稿)

  • 不频繁变更的数据

特点: 永久存储、同源共享、同步 API、不随请求发送

sessionStorage(配额由浏览器实现与存储策略决定)

适合放:

  • 表单临时数据(防刷新丢失)

  • 单页面应用的临时状态

  • 页面会话内的临时非敏感数据;敏感凭证即使放在 sessionStorage 也会暴露给同源 JavaScript

  • 分页/筛选条件的临时保存

特点: 会话级存储、标签页隔离、关闭即清除


20. localStorage 可以放多大内容?

  • Web Storage 配额由浏览器、源、存储分区、隐私模式和用户设置决定,常见为数 MiB,但没有可跨浏览器依赖的精确 5,242,880 字节上限

  • 超出限制会报错QuotaExceededError

  • Web Storage 是同步 API,大对象的序列化与读写会阻塞主线程;应控制数据量并捕获配额错误

JavaScript
try {
  localStorage.setItem('data', JSON.stringify(bigObject));
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    // 存储空间不足的处理
    console.warn('localStorage 空间已满');
  }
}

扩展: 更大的结构化数据可使用 IndexedDB。它仍受浏览器配额和清理策略限制,并非“无大小限制”;大文件还可评估 OPFS 等持久化存储能力。


21. sessionStorage 适用场景

  1. 表单数据防丢失:用户填写长表单,页面刷新后恢复

  2. 页面间传参:多步骤表单(如注册流程),步骤间传数据

  3. 单标签页状态:用户在一个标签页登录,在另一个标签页不共享登录态

  4. 临时缓存:当前页面会话内的接口数据缓存(减少重复请求)

  5. 操作日志:记录当前标签页的操作历史,关闭即清除

  6. 权限展示缓存:可缓存服务端返回的 UI 权限信息,但真正的授权判断必须由服务端执行,不能信任 sessionStorage


22. JavaScript 实现异步的方式

方式说明出现时间
回调函数(Callback)最原始的异步方式ES5
Promise链式调用,解决回调地狱ES6 (2015)
async/await语法糖,让异步代码像同步ES2017
Generatorfunction* + yield,可暂停/恢复ES6 (2015)
事件监听addEventListener + on 触发
发布/订阅模式自定义事件,解耦发布和订阅
setTimeout / setInterval定时器实现异步
requestAnimationFrame浏览器渲染帧回调
Web Worker多线程异步HTML5

事件循环(Event Loop)

JavaScript
console.log('1');                    // 同步

setTimeout(() => console.log('2'), 0);  // 宏任务

Promise.resolve().then(() => console.log('3'));  // 微任务

console.log('4');                    // 同步

// 输出:1 → 4 → 3 → 2

任务(常俗称宏任务):定时器、消息、部分 I/O 事件等来自不同任务源;浏览器不是只有一个统一宏任务队列

渲染相关回调requestAnimationFrame 在渲染更新阶段调用,不宜简单归类为普通宏任务;渲染本身也不是一项 JavaScript 宏任务

微任务(Micro Task):Promise.then、MutationObserver、queueMicrotask


23. 浏览器跨域问题

为什么会有跨域问题?

跨域是浏览器的同源策略(Same-Origin Policy) 限制的。同源要求协议、域名、端口三者都相同。

Plain
https://www.example.com:443/page
协议     域名              端口  路径

任何一项不同都会触发跨域。

目的:防止恶意网站窃取其他网站的数据,是浏览器的安全机制。

解决方案

方案说明适用场景
CORS服务端设置 Access-Control-Allow-Origin最标准的方案
代理服务器Nginx 反向代理 / webpack devServer proxy开发环境
JSONP利用 <script> 标签不受同源限制只支持 GET
document.domain已弃用且会削弱源隔离,不应作为新项目方案仅维护旧系统
WebSocket不走 CORS,但握手会携带 Origin,服务端必须校验并鉴权实时通信
postMessage跨窗口/iframe 通信iframe 场景
Nginx
# Nginx 代理配置
location /api/ {
  proxy_pass http://backend-server:3000/api/;
  proxy_set_header Host $host;
}
JavaScript
// CORS 服务端设置(Node.js/Express)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://www.example.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

24. 服务端环境下有没有跨域问题?

服务端请求不受浏览器同源策略或 CORS 响应检查约束,但仍受网络 ACL、DNS、TLS、代理和应用自身的出站策略限制,并要防范 SSRF。

服务端可以自由地请求任何地址的 API,不需要 CORS、JSONP 等方案。


25. 微信小程序有跨域问题吗?

原生 wx.request 不经过浏览器同源策略,因此没有浏览器 Fetch 那种 CORS 检查;但它受微信小程序的服务器域名、TLS、端口和平台策略约束。web-view 内的 H5 仍运行在 WebView 中,照常受 Web 同源策略影响。

但需要注意:

  • 域名必须在小程序后台配置(request 合法域名)

  • 不支持 HTTP,必须 HTTPS

  • 不支持 IP 地址,必须域名


26. 小程序访问接口连不通的可能问题

  1. 域名未配置:没有在「小程序后台 → 开发管理 → 服务器域名」中添加

  2. 域名不是 HTTPS:小程序强制要求 HTTPS(开发阶段可关闭校验)

  3. ICP 备案问题:域名未完成备案,微信会拦截

  4. SSL 证书问题:证书过期、自签名证书、证书链不完整

  5. 接口请求方法不匹配:如配置了 GET 但代码发了 POST

  6. 超时问题:请求超时默认 60 秒

  7. wx.request 域名数量限制:每个域名有请求频率限制

  8. 开发工具校验问题:开发工具中"不校验合法域名"选项的状态

  9. DNS 解析问题:服务器域名无法解析

  10. IP 黑名单:服务器 IP 被微信封禁

排查步骤:

Plain
1. 开发工具中关闭"不校验合法域名",看是否报错
2. 用 Postman / curl 直接请求接口,确认接口本身正常
3. 检查域名是否已配置、是否通过备案
4. 检查 SSL 证书是否有效
5. 查看小程序后台请求日志

27. SSR 项目

什么是 SSR?

SSR(Server-Side Rendering)即服务端渲染,页面的 HTML 在服务端生成后直接返回给浏览器。

什么项目适合 SSR?

适合的场景原因
SEO 要求高的网站搜索引擎能直接抓取完整 HTML
首屏加载要求高的页面不需要等 JS 下载执行就能渲染内容
新闻/内容类网站SEO + 首屏速度双重需求
电商产品页SEO + 转化率
社交媒体页面分享链接需要有预览内容(Open Graph)

SSR 的缺点

  1. 服务器压力大:每个请求都需要服务端渲染 HTML,CPU 和内存消耗大

  2. 开发复杂度高:需要同时维护服务端和客户端代码,注意同构

  3. 服务器成本高:需要 Node.js 服务器,不能纯静态托管

  4. 兼容性问题:某些浏览器 API 在服务端不可用

  5. 状态管理复杂:服务端渲染的状态需要序列化传给客户端,避免 Hydration 不一致

  6. 缓存策略复杂:需要合理缓存 SSR 页面,平衡新鲜度和性能

  7. 调试困难:Node.js 端和浏览器端的调试环境不同

技术栈

  • Vue:Nuxt.js

  • React:Next.js

  • 通用:Vite SSR、VitePress


28. Node.js 使用经验

Node.js 简介

Node.js 是基于 Chrome V8 引擎的 JavaScript 运行时,让 JS 可以在服务端运行。

常见使用场景

  • 后端 API 服务:Express、Koa、Fastify、NestJS

  • 构建工具:Webpack、Vite、Rollup 底层都是 Node.js

  • 前端工程化:脚手架(CLI)、代码生成、自动化任务

  • SSR:Nuxt.js、Next.js

  • 工具库开发:npm 包、CLI 工具

  • 中间层/BFF:Backend For Frontend,为前端定制 API

  • 实时应用:WebSocket 服务、Socket.io


30. MySQL 使用

SQL 基本操作

SQL(Structured Query Language)是关系型数据库的标准查询语言。

SQL
-- 查询
SELECT name, age FROM users WHERE age > 18 ORDER BY age DESC LIMIT 10;

-- 联表查询
SELECT u.name, o.product
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- 聚合函数
SELECT department, COUNT(*) as count, AVG(salary) as avg_salary
FROM employees
GROUP BY department
HAVING count > 5;

-- 插入
INSERT INTO users (name, age) VALUES ('张三', 25);

-- 更新
UPDATE users SET age = 26 WHERE name = '张三';

-- 删除
DELETE FROM users WHERE age < 18;

常用查询技巧

  • 索引优化CREATE INDEX idx_name ON users(name) 提升查询性能

  • 分页LIMIT offset, countWHERE id > lastId LIMIT count(游标分页)

  • 子查询SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)

  • 事务BEGIN; UPDATE ... ; COMMIT;


微信小程序开发全面讲解


一、页面生命周期

小程序的页面生命周期由框架自动管理,每个页面都有一组回调函数,在特定时机被触发。

1. 生命周期函数一览

生命周期触发时机典型用途
onLoad(query)页面首次创建时触发(仅一次初始化数据、接收路由参数、请求接口
onShow()页面显示/从后台切到前台刷新数据、恢复状态
onHide()页面隐藏/切到后台暂停定时器、保存草稿
onUnload()页面销毁(navigateBackredirectTo清理定时器、取消订阅
onReady()页面初次渲染完成(仅一次,晚于onShow)操作 DOM / Canvas 等需要渲染完成的场景

2. 执行顺序

Plain
onLoad → onShow → onReady → (用户操作) → onHide → onShow → ... → onUnload

3. 注意事项

  • onLoad** 只执行一次**:通过 navigateBack 返回页面时不会再次触发 onLoad,但会触发 onShow。所以需要刷新的数据应放在 onShow 中。

  • onShow** vs **onReadyonShow 时 DOM 可能还未就绪,需要操作组件(如 createSelectorQuery)应放在 onReady 中。

  • 路由跳转与生命周期的关系

    • wx.navigateTo → 新页面 onLoad,旧页面 onHide

    • wx.redirectTo → 当前页面 onUnload,新页面 onLoad

    • wx.switchTab → 切到 Tab 页,原页面 onUnload

    • wx.navigateBack → 当前页面 onUnload,上一页面 onShow

4. App 级别生命周期

JavaScript
App({
  onLaunch(options) { },  // 小程序初始化(仅一次)
  onShow(options) { },    // 小程序切到前台
  onHide() { },           // 小程序切到后台
  onError(err) { },       // 脚本错误
  onPageNotFound(res) { } // 页面不存在
})

关键区别onLaunchonShow 的参数不同。onLaunchoptions 包含完整的启动参数,而 onShow 在冷启动时与 onLaunch 同时触发但参数可能不一致(场景一)。


二、组件化开发(Component 构造器)

1. 基本结构

JavaScript
Component({
  properties: {    // 外部传入的属性
    title: {
      type: String,
      value: '',
      observer(newVal, oldVal) { }  // 属性变化监听
    }
  },
  data: {          // 组件内部状态
    count: 0
  },
  lifetimes: {     // 组件生命周期
    created() { },       // 组件实例创建(内存中,还未渲染)
    attached() { },      // 进入页面节点树(可操作 setData)
    ready() { },         // 布局完成
    detached() { },      // 离开页面节点树
    error(err) { }       // 组件方法出错
  },
  pageLifetimes: { // 监听所在页面的生命周期
    show() { },
    hide() { },
    resize(size) { }
  },
  methods: {
    handleTap() {
      this.setData({ count: this.data.count + 1 })
      // 触发事件给父组件
      this.triggerEvent('change', { count: this.data.count })
    }
  }
})

2. 组件生命周期 vs 页面生命周期

组件生命周期页面生命周期说明
created实例刚创建,不能 setData
attachedonLoad进入节点树,可以 setData
readyonReady渲染完成
detachedonUnload销毁离开

3. WXS 与组件

WXS(WeiXin Script)是小程序的脚本语言,可以用于模板中的数据过滤,避免在 js 层做大量字符串处理再 setData。WXS 运行在视图层,不阻塞逻辑层。


三、状态管理

小程序没有内置的状态管理库,常见方案如下:

1. globalData(原生方案)

JavaScript
// app.js
App({
  globalData: {
    userInfo: null,
    token: ''
  }
})

// 页面中使用
const app = getApp()
app.globalData.userInfo = data

适用场景:简单应用,少量全局数据。

缺点:没有响应式,修改后页面不会自动更新,需要手动 setData

2. MobX-miniprogram(推荐原生小程序)

JavaScript
// store.js
import { observable, action } from 'mobx-miniprogram'

export const store = observable({
  count: 0,
  get doubled() {         // 计算属性
    return this.count * 2
  },
  increment: action(function() {
    this.count++
  })
})

// 页面中使用
import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store'

Page({
  onLoad() {
    this.storeBindings = createStoreBindings(this, {
      store,
      fields: ['count', 'doubled'],
      actions: ['increment']
    })
  },
  onUnload() {
    this.storeBindings.destroyStoreBindings()
  }
})

核心优势

  • 响应式store 中的数据变化会自动触发页面/组件的 setData

  • 计算属性get 定义的字段自动缓存

  • 易于测试:store 与视图分离

3. Taro 的状态管理(Redux / Zustand / MobX)

TypeScript
// Taro + Redux Toolkit 示例
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { useSelector, useDispatch } from 'react-redux'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment(state) { state.value++ }
  }
})

const store = configureStore({ reducer: { counter: counterSlice.reducer } })

// 组件中
const Counter = () => {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()
  return <Button onClick={() => dispatch(counterSlice.actions.increment())}>{count}</Button>
}

4. 状态管理方案对比

方案适用场景响应式学习成本
globalData简单应用
mobx-miniprogram中小型原生小程序
eventBus跨页面通信
Redux / ZustandTaro/uni-app React 项目中高
Pinia / VuexTaro/uni-app Vue 项目

四、自定义组件通信

1. properties(父 → 子)

JavaScript
// 父组件 WXML
<my-component title="{{pageTitle}}" bind:change="onChildChange" />

// 子组件
Component({
  properties: {
    title: { type: String, value: '' }
  }
})

注意:properties 传递的是单向数据流,子组件不应直接修改 properties 值。若需要双向绑定,可使用 model 属性(基础库 2.8.3+):

JavaScript
Component({
  properties: {
    value: {
      type: String,
      value: ''
    }
  },
  observers: {
    'value'(val) {
      this.triggerEvent('update:value', val)
    }
  }
})

2. triggerEvent(子 → 父)

JavaScript
// 子组件
this.triggerEvent('myevent', { detail: 'data' }, { bubbles: true, composed: true })

// 父组件
<my-component bind:myevent="onMyEvent" />

事件选项

  • bubbles: 事件是否冒泡

  • composed: 跨组件边界触发(穿越 Shadow DOM)

  • capturePhase: 是否在捕获阶段触发

3. selectComponent(父主动获取子组件实例)

JavaScript
// 父组件
this.selectComponent('#child')  // 返回子组件实例
this.selectAllComponents('.child-class')  // 返回数组

// 然后直接调用子组件方法
childComp.someMethod()

注意selectComponentonReady 之后才能使用。

4. 其他通信方式

方式适用场景说明
全局事件总线无父子关系的组件/页面手动实现发布订阅
共享 Behavior多个组件共享逻辑类似 Mixin
shared(共享组件数据)微信插件内组件exposeInjection: true
externalClasses样式传递父组件向子组件传递 CSS 类名
slot(插槽)内容分发默认插槽 / 具名插槽

5. Behavior(类似 Mixin)

JavaScript
const myBehavior = Behavior({
  data: { sharedData: 0 },
  methods: {
    sharedMethod() { /* ... */ }
  }
})

Component({
  behaviors: [myBehavior],
  // 继承了 myBehavior 的 data 和 methods
})

五、性能优化

1. 分包加载

原理:主包限制 2MB,整个小程序上限 20MB。通过分包将不常用的页面拆分出去。

JSON
// app.json
{
  "pages": ["pages/index/index"],
  "subpackages": [
    {
      "root": "packageA",
      "name": "shop",
      "pages": ["pages/detail/detail"],
      "independent": false
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["shop"]
    }
  }
}

关键点

  • preloadRule 配置预加载规则,在进入某个页面时预加载其他分包

  • independent: true 表示独立分包(可独立运行,不需要下载主包)

  • 分包之间不能互相引用,但可以通过 require 引用主包的 JS

2. 预加载(Preload)

JSON
"preloadRule": {
  "pages/index/index": {
    "network": "all",
    "packages": ["sub1"]
  }
}

进阶:通过 wx.preload API 动态预加载分包:

JavaScript
wx.preload({
  subpackage: 'shop'
})

3. 骨架屏

方案一:原生骨架屏

page 配置中启用:

JSON
{
  "skeleton": "pages/skeleton/skeleton"
}

方案二:组件实现

HTML
<view wx:if="{{loading}}" class="skeleton">
  <view class="skeleton-avatar"></view>
  <view class="skeleton-line"></view>
  <view class="skeleton-line short"></view>
</view>
<view wx:else>
  <!-- 真实内容 -->
</view>
CSS
.skeleton-avatar {
  width: 64px; height: 64px; border-radius: 50%;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

4. 图片懒加载

HTML
<image src="{{item.image}}" lazy-load bind:load="onImageLoad" />

进阶方案(IntersectionObserver)

JavaScript
onReady() {
  this.observer = wx.createIntersectionObserver(this)
  this.observer.relativeToViewport({ bottom: 100 })
    .select('.lazy-image')
    .boundingClientRect(rect => {
      if (rect) {
        this.setData({ imageSrc: actualUrl })
      }
    })
    .observe()
}

5. 其他性能优化手段

优化项措施
setData 优化减少每次传输数据量,避免频繁调用,使用路径更新 setData({'a.b.c': val})
长列表优化虚拟列表(只渲染可视区域)、分页加载、recycle-view(虚拟列表组件)
首屏优化骨架屏 + 分包 + 预加载 + 内联首屏数据(data-initial-data
图片优化使用 WebP 格式、CDN 裁剪、设置尺寸、懒加载
代码优化避免同步 API(如 wx.getStorageSync)、避免 eval、减少 WXML 层级
网络优化合并请求、缓存策略、减少 DNS 解析、HTTP/2

六、跨端框架:uni-app 和 Taro

1. uni-app(Vue 体系)

Plain
<!-- Vue 3 + uni-app -->
<template>
  <view>
    <text>{{ msg }}</text>
    <button @click="goDetail">跳转</button>
  </view>
</template>

<script setup>
import { ref } from 'vue'

const msg = ref('Hello')

const goDetail = () => {
  uni.navigateTo({ url: '/pages/detail/detail' })
}

// 生命周期
onLoad((query) => { })
onShow(() => { })
</script>

<style scoped>
/* rpx 适配 */
.text { font-size: 30rpx; }
</style>

条件编译(核心能力):

Plain
<!-- #ifdef MP-WEIXIN -->
<view>仅微信小程序显示</view>
<!-- #endif -->

<!-- #ifdef H5 -->
<view>仅H5显示</view>
<!-- #endif -->

JS 中:

JavaScript
// #ifdef MP-WEIXIN
wx.login({ success(res) { } })
// #endif

// #ifdef APP-PLUS
plus.runtime.login(...)
// #endif

2. Taro(React / Vue 体系)

TypeScript
// Taro + React
import { View, Text, Button } from '@tarojs/components'
import Taro, { useDidShow, useRouter } from '@tarojs/taro'

function Detail() {
  const router = useRouter()

  useDidShow(() => {
    console.log('页面显示')
  })

  return (
    <View className='detail'>
      <Text>ID: {router.params.id}</Text>
      <Button onClick={() => Taro.navigateTo({ url: '/pages/index/index' })}>
        返回首页
      </Button>
    </View>
  )
}

export default Detail

条件编译(Taro 语法):

TypeScript
import Taro from '@tarojs/taro'

// 编译时判断
if (process.env.TARO_ENV === 'weixin') {
  Taro.login({ ... })
} else if (process.env.TARO_ENV === 'h5') {
  // H5 逻辑
}

3. uni-app vs Taro 对比

维度uni-appTaro
技术栈Vue(推荐 3.x)React / Vue 都支持
底层原理条件编译 + 运行时适配编译时转换(JSX → WXML)
生态DCloud 插件市场,数量庞大npm 生态,插件相对少
跨端支持小程序/H5/App/RN/快应用小程序/H5/RN
性能Vue 3 响应式 + 原生编译React 虚拟 DOM + 编译优化
SSR支持(Nuxt-like)支持(Next.js-like)
调试HBuilderX 集成CLI + VS Code

4. 跨端兼容性处理要点

  1. API 差异:使用平台判断包装统一 API,如 Taro.getStorage / uni.getStorageSync 内部已处理

  2. 组件差异:不同平台对组件支持不同(如 map 在 H5 和小程序中表现不同)

  3. 样式差异:使用 rpx 统一单位,避免使用平台特有 CSS

  4. 生命周期差异:各框架已做适配,但需注意细微差别(如 Taro 的 useDidShow 对应 onShow

  5. 导航差异:小程序 navigateTo vs H5 window.history


七、小程序八股文精选

Q1: 小程序的双线程架构是什么?

:小程序采用双线程模型

Plain
┌─────────────┐     Native 层      ┌─────────────┐
│  逻辑线程     │ ←──── JSON ────→  │  渲染线程     │
│ (App Service)│   (setData数据)    │  (WebView)   │
│              │                    │              │
│  JS 执行     │                    │  WXML 渲染   │
│  网络请求     │                    │  WXSS 样式   │
│  数据处理     │                    │  事件处理     │
└─────────────┘                    └─────────────┘

为什么这样设计

  • 安全性:JS 不能直接操作 DOM,避免恶意操作

  • 逻辑与渲染隔离可以减少直接 DOM 操作带来的耦合,但跨线程通信、主线程资源竞争和大量更新仍会造成卡顿,不能说“不会互相阻塞”

  • 缺点setData 是跨线程通信(序列化 → 传输 → 反序列化),频繁调用有性能瓶颈

Q2: setData 的原理和优化?

原理setData 把逻辑层的数据变化跨越通信边界传给视图层,并在回调中表示本次界面更新完成。具体序列化格式和渲染实现属于平台内部细节,不应固定写成必然调用 JSON.stringify / JSON.parse 或等同于 Vue Virtual DOM。

优化策略

  • 合并多次 setData 调用

  • 减少传输数据量(只传变化的字段,使用路径更新 this.setData({ 'a.b': val })

  • 避免在 setData 中传递大对象

  • 避免在 scroll / touchmove 等高频事件中调用

Q3: 小程序的路由机制?

  • 小程序采用栈式路由(类似浏览器 history)

  • navigateTo → 入栈(最多 10 层)

  • redirectTo → 替换栈顶

  • navigateBack → 出栈

  • switchTab → 清空非 Tab 页栈

  • reLaunch → 清空所有页面,入栈新页面

路由与页面实例:页面实例在栈中被缓存,navigateBack 不会触发 onLoad

Q4: 小程序与 H5 的区别?

维度小程序H5
运行环境微信客户端容器浏览器
DOM 操作不可直接操作自由操作
路由框架管理(栈)URL 驱动
缓存wx.setStorage,配额以当前平台文档为准Web Storage / IndexedDB,配额由浏览器决定
网络wx.request(受域名白名单限制)fetch / axios(CORS 限制)
原生能力GPS、蓝牙、NFC 等有限(受浏览器权限控制)
更新需要发布版本部署即生效

Q5: 小程序的安全机制?

  1. JS 沙箱:逻辑层与渲染层隔离,JS 不能操作 DOM

  2. HTTPS 强制:所有网络请求必须 HTTPS

  3. 域名白名单:需在后台配置合法域名

  4. 内容安全wx.getFileSystemManager 受限,文件系统不可随意读写

  5. 登录态安全wx.login 获取 code → 后端换 session_key,前端拿 token

  6. 代码审计:发布前需微信审核

Q6: 如何实现小程序的登录?

Plain
┌─────────┐         ┌─────────┐         ┌─────────┐
│  小程序   │         │  开发者服务器│        │  微信服务器│
│  前端     │         │  后端     │         │         │
└────┬────┘         └────┬────┘         └────┬────┘
     │ 1. wx.login()     │                   │
     │ ─────────────────────────────────────→ │
     │ 2. 返回 code      │                   │
     │ ←──────────────────────────────────── │
     │ 3. 发送 code      │                   │
     │ ──────────────→   │                   │
     │                   │ 4. code2Session   │
     │                   │ ────────────────→ │
     │                   │ 5. openid+session_key │
     │                   │ ←─────────────── │
     │ 6. 返回 token     │                   │
     │ ←─────────────   │                   │

Q7: wx.request 的封装与封装方案?

JavaScript
const request = (url, options = {}) => {
  return new Promise((resolve, reject) => {
    const token = wx.getStorageSync('token')
    wx.request({
      url: `${BASE_URL}${url}`,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      success(res) {
        if (res.statusCode === 401) {
          // token 过期处理
          redirectToLogin()
          reject(new Error('Unauthorized'))
        } else if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
        } else {
          reject(res)
        }
      },
      fail: reject
    })
  })
}

Q8: 小程序的自动更新机制?

JavaScript
// app.js onLaunch 中
const updateManager = wx.getUpdateManager()

updateManager.onCheckForUpdate((res) => {
  if (res.hasUpdate) {
    // 发现新版本
  }
})

updateManager.onUpdateReady(() => {
  wx.showModal({
    title: '更新提示',
    content: '新版本已准备好,是否重启?',
    success(res) {
      if (res.confirm) {
        updateManager.applyUpdate()
      }
    }
  })
})

updateManager.onUpdateFailed(() => {
  // 下载失败,提示用户删除小程序重新搜索
})

更新原理

  1. 小程序冷启动时检查版本

  2. 下载新包(静默)

  3. 下载完成后等待下次冷启动或手动重启

  4. 冷启动时用新包替换旧包

Q9: 小程序的页面栈问题?

当页面栈达到 10 层限制时:

  • navigateTo 会失败

  • 解决方案:

    • redirectTo 替代(关闭当前页再打开)

    • reLaunch 重定向

    • 重新设计导航流程(如 Tab 页 + navigateTo

Q10: WXML 模板编译的原理?

Plain
WXML 与页面数据 → 视图层渲染 → 数据变化后更新界面
  • WXML 会经过平台的编译和运行时处理;具体中间表示、差量算法和节点实现属于内部细节,不应写死为“WaxML”、Vue render 函数或某一种 Virtual DOM

  • 运行时生成 虚拟节点树

  • setData 触发新的虚拟节点树生成,与旧树 diff

  • 只更新变化的节点(最小化更新