微信小程序面试大全
来源:微信小程序八股
1. 小程序路由及区别(navigateTo、redirectTo 等)
小程序共有 5 种路由跳转方式:
| 方法 | 说明 | 页面栈表现 | 能否回退 |
|---|---|---|---|
wx.navigateTo | 保留当前页面,跳转到应用内某个页面 | 新页面入栈(栈+1) | ✅ 可以 |
wx.redirectTo | 关闭当前页面,跳转到应用内某个页面 | 当前页出栈,新页入栈(栈不变) | ❌ 不可以 |
wx.switchTab | 跳转到 tabBar 页面 | 关闭所有非 tab 页,tab 页入栈 | ❌ 不可以 |
wx.navigateBack | 关闭当前页面,返回上一页面或多级页面 | 页面出栈 | — |
wx.reLaunch | 关闭所有页面,打开应用内某个页面 | 清空页面栈,新页入栈 | ❌ 不可以 |
核心区别:
navigateTo:最常用,会压栈,右上角有返回按钮
redirectTo:替换当前页,无法返回到被替换的页面(适用于登录过期跳转等场景)
reLaunch:最暴力,清空整个页面栈(适用于切换身份等场景)
switchTab:只能跳 tabBar 页面,且会关闭所有非 tab 页
路由传参方式:
// 跳转
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:冒泡阶段的绑定方式,但处理器执行后会阻止事件继续向上冒泡;它不是“捕获型事件”
<!-- 父元素 -->
<view bindtap="parentTap">
<!-- 子元素 -->
<view catchtap="childTap">按钮</view>
</view>点击"按钮":只触发
childTap,不触发parentTap(catch 阻止了冒泡)如果子元素用
bindtap,则先触发childTap,再冒泡触发parentTap
补充: 还有 capture-bind 和 capture-catch 用于捕获阶段:
<view capture-bind:tap="captureTap">
父
<view catchtap="childTap">子</view>
</view>事件触发顺序:captureTap(捕获阶段)→ childTap(目标阶段),冒泡被 catch 阻止。
3. 小程序页面栈层级限制
小程序的页面栈最多 10 层。
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 的区别
| 维度 | OpenID | UnionID |
|---|---|---|
| 定义 | 用户在某个小程序/公众号下的唯一标识 | 用户在同一开放平台账号下所有应用的唯一标识 |
| 作用域 | 单个小程序 | 同一开放平台绑定的所有应用(公众号、小程序、APP等) |
| 获取方式 | wx.login 取得一次性 code,后端调用 code2Session 换取 | 小程序绑定开放平台账号后,code2Session 可能一并返回 |
| 是否需要授权 | 获取登录 code 不要求用户资料授权 | 不使用公众号网页 OAuth 的 snsapi_login;返回条件以开放平台绑定和微信规则为准 |
// 获取 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 页面:
<web-view src="https://example.com/page"></web-view>使用场景:
需要快速上线但来不及开发小程序原生页面时,用 H5 作为过渡
需要展示复杂的富文本或第三方内容
活动页、营销页等临时页面
限制条件:
域名必须在小程序后台配置白名单(业务域名)
不支持个人小程序(需要企业认证)
<web-view>会占满整个页面,无法与小程序原生组件混用部分功能受限:无法使用 wx.login、wx.share 等原生 API(需要通过 JS-SDK 桥接)
6. 小程序原生页和嵌入的 H5 Webview 如何通信?
方式一:URL 参数传递(简单数据)
<!-- 小程序页面 -->
<web-view src="https://h5.example.com/page?userId=456"></web-view>不要把访问令牌放入 URL,URL 可能出现在日志、分享内容、Referer 或截图中。需要认证时应建立短期的一次性交换码,或让 H5 自行完成安全登录。
方式二:wx.miniProgram.postMessage(H5 → 小程序)
// H5 页面中(通过微信 JS-SDK)
wx.miniProgram.postMessage({
data: { action: 'updateUser', userId: 789 }
});
// 小程序页面中接收
Page({
onMessage(event) {
console.log(event.detail.data); // 多次 postMessage 数据组成的数组
}
});bindmessage 不是实时消息通道。官方文档说明,消息会在小程序后退、组件销毁、分享、复制链接等特定时机触发接收;需要实时同步时应使用受控后端通道等其他方案。
<web-view src="https://h5.example.com" bindmessage="onMessage"></web-view>方式三:wx.miniProgram.navigateTo(H5 跳转小程序页面)
// H5 页面中
wx.miniProgram.navigateTo({ url: '/pages/detail/detail?id=123' });方式四:官方 URL Scheme / URL Link
外部 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)的使用和兼容性处理
常见困难挑战:
兼容性问题:不同基础库版本(如 2.x vs 3.x)API 差异大,需要做版本判断和降级方案
性能瓶颈:长列表渲染卡顿(需要虚拟列表)、 setData 数据量过大导致通信开销
登录态维护:wx.login 获取 code → 后端换 token → 前端存 token,流程复杂且易出错
分包加载配置:主包 2MB 限制,分包总共 20MB 限制,需要合理规划路由和资源
审核风险:微信审核标准不透明,经常被拒需要反复修改
8. H5 压缩介绍
H5 压缩通常指以下几个方面:
静态资源压缩
HTML/CSS/JS 压缩:使用 UglifyJS、Terser(JS)、cssnano(CSS)去除空格、注释、缩短变量名
图片压缩:使用 TinyPNG、imagemin 等工具,将 PNG/JPG 压缩 30%~70%
Gzip/Brotli 压缩:服务端配置,对文本类资源压缩率可达 60%~80%
// 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 体积
性能优化经历(常见场景)
首屏加载优化
路由懒加载:
const Home = () => import('./views/Home.vue')图片懒加载 + WebP 格式替换
第三方库按需引入(如 Element-UI 按需加载组件)
SSR / 预渲染关键页面
CDN 加速静态资源
运行时性能优化
虚拟列表处理长列表(如
vue-virtual-scroller)防抖节流处理高频事件(scroll、resize、input)
Web Worker 处理耗时计算,避免阻塞主线程
v-ifvsv-show合理使用Object.freeze()冻结大数据对象避免响应式开销
网络优化
HTTP/2 多路复用
资源预加载(
<link rel="preload">)DNS 预解析(
<link rel="dns-prefetch">)接口缓存策略(ETag、Cache-Control)
11. Vue 2 和 Vue 3 的区别
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式原理 | Object.defineProperty(需要递归遍历) | Proxy(更高效,支持动态新增属性) |
| API 风格 | Options API | Composition API + Options API |
| TypeScript | 支持较差 | 原生支持更好 |
| 生命周期 | beforeDestroy / destroyed | onBeforeUnmount / onUnmounted |
| 模板根节点 | 必须单根节点 | 支持多根节点(Fragment) |
| 性能 | 较好 | 更好(编译优化、Tree-shaking、静态提升) |
| 体积 | 取决于构建方式与功能使用情况 | 支持 Tree-shaking,实际体积取决于构建产物 |
| Teleport | 不支持 | 内置 <Teleport> |
| Suspense | 不支持 | 内置 <Suspense>,当前仍需关注其实验性状态与版本文档 |
| 多根节点 | 不支持 | 支持 |
| 事件总线 | new Vue() 做事件总线 | 推荐 mitt 库 |
| 过滤器 | 支持 filter | 移除了 filter,用方法或 computed 替代 |
| $set / $delete | 需要 | 不需要(Proxy 原生支持) |
Composition API 的优势:
// 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 的区别
| 维度 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型 + 对象类型 | 仅对象/数组类型 |
| 访问方式 | 需要 .value | 直接访问属性 |
| 模板使用 | 模板中自动解包,不需要 .value | 直接使用 |
| 解构 | 解构出 ref 对象本身仍可响应;直接取出 .value 只是普通值 | 直接解构属性会丢失响应性(需 toRefs) |
| 重新赋值 | 可以 .value = newVal | 不能直接替换整个对象 |
| 底层原理 | ref 自己跟踪 .value,对象值会再做深层响应式转换 | 对对象使用 Proxy |
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 2 | Vue 3 |
|---|---|---|
| 事件总线 | new Vue() 或 this.$bus | mitt 库 |
| 状态管理 | Vuex(this.$store) | Pinia(推荐)或 Vuex 4 |
| 依赖注入 | provide / inject | 同样支持,且 Composition API 中更灵活 |
| v-model | 一个组件只能一个 v-model | 支持多个 v-model:v-model:name |
| .sync | :prop.sync="val" | 移除,用 v-model:prop 替代 |
| $attrs / $listeners | 都有 | $listeners 合并到 $attrs 中 |
| 跨组件通信 | Mixins | Composables(推荐) |
<!-- 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)
class Database {
static instance = null;
static getInstance() {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
}前端场景:Vuex/Pinia Store、全局 EventBus、连接池。
2. 工厂模式(Factory)
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)
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)
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 入门教程》—— 阮一峰著,免费在线版
学习路径建议:
先掌握基础语法(变量、函数、循环、对象)
深入理解原型链、闭包、作用域、this 指向
学习异步编程(Promise、async/await、事件循环)
掌握 DOM 操作和 BOM API
学习设计模式和工程化实践
持续关注 ECMAScript 新标准
16. Web Worker
什么是 Web Worker
Web Worker 是浏览器提供的多线程机制,允许在后台线程中运行 JavaScript,不阻塞主线程(UI 线程)。
// 主线程
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);
};缺点/限制
通信限制:通常通过
postMessage传递数据并使用结构化克隆算法;函数和 DOM 节点不可克隆,现代环境可克隆常见Error数据,但自定义属性和堆栈等细节不应假定完整保留不能操作 DOM:Worker 中无法访问
document、window等 API加载限制:
Worker()的普通 URL 通常受同源/CORS 与脚本类型规则约束,也可在符合策略时使用blob:URL;不能简单概括为所有 Worker 脚本都必须同源数据传输开销:大数据量传递时,结构化克隆的性能开销较大
调试与兼容性:现代浏览器通常支持 Worker 的断点和
console,但独立线程让时序和性能问题更难复现,仍需检查目标环境能力内存消耗:每个 Worker 都有独立的全局作用域和内存空间
资源限制:浏览器可按设备资源限制或回收 Worker,但标准没有固定“4 到 8 个”的通用上限;应使用 worker pool 控制并发
Transferable Objects(零拷贝传输)
// 用 ArrayBuffer 做零拷贝,避免克隆开销
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage(buffer, [buffer]);
// 传输后 buffer 在主线程中不可用(被"转移"了)17. JS 安全相关——表单提交的安全问题
两种主要安全问题
1. XSS(Cross-Site Scripting,跨站脚本攻击)
攻击者在表单中注入恶意脚本代码:
<!-- 用户在表单输入 -->
<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,跨站请求伪造)
攻击者诱导用户在已登录的状态下,向目标网站发送伪造请求:
<!-- 攻击者网站 -->
<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 Cookie:
SameSite=Lax或SameSite=Strict验证 Referer / Origin 头
双重 Cookie 验证
关键操作二次确认(如输入密码)
18. Cookie 可以存多大内容?
单个 Cookie 大小限制:约 4KB(不同浏览器略有差异)
Cookie 数量、每个域名/站点的总量和超限淘汰策略由浏览器实现决定,不存在可靠的“所有域名总共约 4MB”规则
约 4KB 的计算口径在浏览器间有差异,有的主要限制 name/value,有的还计算属性;工程上应让 Cookie 尽可能小并实测目标浏览器。
19. 什么数据适合放 Cookie / localStorage / sessionStorage?
Cookie(~4KB)
适合放:
认证信息(token、sessionId)
用户偏好设置(语言、主题)
跟踪标识(用户ID、来源标记)
需要在匹配 Domain/主机、Path、Secure、SameSite 等条件时自动发送到服务端的小型数据
特点: 可设置过期时间;HttpOnly 降低被脚本直接读取的风险但不能防止 XSS 发起操作;只有匹配请求条件的 Cookie 才会自动携带
localStorage(配额由浏览器实现与存储策略决定)
适合放:
用户配置(主题、布局偏好)
缓存数据(接口数据本地缓存)
离线数据
大段文本(编辑器草稿)
不频繁变更的数据
特点: 永久存储、同源共享、同步 API、不随请求发送
sessionStorage(配额由浏览器实现与存储策略决定)
适合放:
表单临时数据(防刷新丢失)
单页面应用的临时状态
页面会话内的临时非敏感数据;敏感凭证即使放在 sessionStorage 也会暴露给同源 JavaScript
分页/筛选条件的临时保存
特点: 会话级存储、标签页隔离、关闭即清除
20. localStorage 可以放多大内容?
Web Storage 配额由浏览器、源、存储分区、隐私模式和用户设置决定,常见为数 MiB,但没有可跨浏览器依赖的精确
5,242,880字节上限超出限制会报错:
QuotaExceededErrorWeb Storage 是同步 API,大对象的序列化与读写会阻塞主线程;应控制数据量并捕获配额错误
try {
localStorage.setItem('data', JSON.stringify(bigObject));
} catch (e) {
if (e.name === 'QuotaExceededError') {
// 存储空间不足的处理
console.warn('localStorage 空间已满');
}
}扩展: 更大的结构化数据可使用 IndexedDB。它仍受浏览器配额和清理策略限制,并非“无大小限制”;大文件还可评估 OPFS 等持久化存储能力。
21. sessionStorage 适用场景
表单数据防丢失:用户填写长表单,页面刷新后恢复
页面间传参:多步骤表单(如注册流程),步骤间传数据
单标签页状态:用户在一个标签页登录,在另一个标签页不共享登录态
临时缓存:当前页面会话内的接口数据缓存(减少重复请求)
操作日志:记录当前标签页的操作历史,关闭即清除
权限展示缓存:可缓存服务端返回的 UI 权限信息,但真正的授权判断必须由服务端执行,不能信任 sessionStorage
22. JavaScript 实现异步的方式
| 方式 | 说明 | 出现时间 |
|---|---|---|
| 回调函数(Callback) | 最原始的异步方式 | ES5 |
| Promise | 链式调用,解决回调地狱 | ES6 (2015) |
| async/await | 语法糖,让异步代码像同步 | ES2017 |
| Generator | function* + yield,可暂停/恢复 | ES6 (2015) |
| 事件监听 | addEventListener + on 触发 | — |
| 发布/订阅模式 | 自定义事件,解耦发布和订阅 | — |
| setTimeout / setInterval | 定时器实现异步 | — |
| requestAnimationFrame | 浏览器渲染帧回调 | — |
| Web Worker | 多线程异步 | HTML5 |
事件循环(Event Loop)
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) 限制的。同源要求协议、域名、端口三者都相同。
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 代理配置
location /api/ {
proxy_pass http://backend-server:3000/api/;
proxy_set_header Host $host;
}// 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. 小程序访问接口连不通的可能问题
域名未配置:没有在「小程序后台 → 开发管理 → 服务器域名」中添加
域名不是 HTTPS:小程序强制要求 HTTPS(开发阶段可关闭校验)
ICP 备案问题:域名未完成备案,微信会拦截
SSL 证书问题:证书过期、自签名证书、证书链不完整
接口请求方法不匹配:如配置了 GET 但代码发了 POST
超时问题:请求超时默认 60 秒
wx.request 域名数量限制:每个域名有请求频率限制
开发工具校验问题:开发工具中"不校验合法域名"选项的状态
DNS 解析问题:服务器域名无法解析
IP 黑名单:服务器 IP 被微信封禁
排查步骤:
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 的缺点
服务器压力大:每个请求都需要服务端渲染 HTML,CPU 和内存消耗大
开发复杂度高:需要同时维护服务端和客户端代码,注意同构
服务器成本高:需要 Node.js 服务器,不能纯静态托管
兼容性问题:某些浏览器 API 在服务端不可用
状态管理复杂:服务端渲染的状态需要序列化传给客户端,避免 Hydration 不一致
缓存策略复杂:需要合理缓存 SSR 页面,平衡新鲜度和性能
调试困难: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)是关系型数据库的标准查询语言。
-- 查询
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, count或WHERE id > lastId LIMIT count(游标分页)子查询:
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)事务:
BEGIN; UPDATE ... ; COMMIT;
微信小程序开发全面讲解
一、页面生命周期
小程序的页面生命周期由框架自动管理,每个页面都有一组回调函数,在特定时机被触发。
1. 生命周期函数一览
| 生命周期 | 触发时机 | 典型用途 |
|---|---|---|
onLoad(query) | 页面首次创建时触发(仅一次) | 初始化数据、接收路由参数、请求接口 |
onShow() | 页面显示/从后台切到前台 | 刷新数据、恢复状态 |
onHide() | 页面隐藏/切到后台 | 暂停定时器、保存草稿 |
onUnload() | 页面销毁(navigateBack 或 redirectTo) | 清理定时器、取消订阅 |
onReady() | 页面初次渲染完成(仅一次,晚于onShow) | 操作 DOM / Canvas 等需要渲染完成的场景 |
2. 执行顺序
onLoad → onShow → onReady → (用户操作) → onHide → onShow → ... → onUnload3. 注意事项
onLoad** 只执行一次**:通过navigateBack返回页面时不会再次触发onLoad,但会触发onShow。所以需要刷新的数据应放在onShow中。onShow** vs **onReady:onShow时 DOM 可能还未就绪,需要操作组件(如createSelectorQuery)应放在onReady中。路由跳转与生命周期的关系:
wx.navigateTo→ 新页面onLoad,旧页面onHidewx.redirectTo→ 当前页面onUnload,新页面onLoadwx.switchTab→ 切到 Tab 页,原页面onUnloadwx.navigateBack→ 当前页面onUnload,上一页面onShow
4. App 级别生命周期
App({
onLaunch(options) { }, // 小程序初始化(仅一次)
onShow(options) { }, // 小程序切到前台
onHide() { }, // 小程序切到后台
onError(err) { }, // 脚本错误
onPageNotFound(res) { } // 页面不存在
})关键区别:onLaunch 和 onShow 的参数不同。onLaunch 的 options 包含完整的启动参数,而 onShow 在冷启动时与 onLaunch 同时触发但参数可能不一致(场景一)。
二、组件化开发(Component 构造器)
1. 基本结构
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 |
attached | onLoad | 进入节点树,可以 setData |
ready | onReady | 渲染完成 |
detached | onUnload | 销毁离开 |
3. WXS 与组件
WXS(WeiXin Script)是小程序的脚本语言,可以用于模板中的数据过滤,避免在 js 层做大量字符串处理再 setData。WXS 运行在视图层,不阻塞逻辑层。
三、状态管理
小程序没有内置的状态管理库,常见方案如下:
1. globalData(原生方案)
// app.js
App({
globalData: {
userInfo: null,
token: ''
}
})
// 页面中使用
const app = getApp()
app.globalData.userInfo = data适用场景:简单应用,少量全局数据。
缺点:没有响应式,修改后页面不会自动更新,需要手动 setData。
2. MobX-miniprogram(推荐原生小程序)
// 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)
// 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 / Zustand | Taro/uni-app React 项目 | ✅ | 中高 |
Pinia / Vuex | Taro/uni-app Vue 项目 | ✅ | 中 |
四、自定义组件通信
1. properties(父 → 子)
// 父组件 WXML
<my-component title="{{pageTitle}}" bind:change="onChildChange" />
// 子组件
Component({
properties: {
title: { type: String, value: '' }
}
})注意:properties 传递的是单向数据流,子组件不应直接修改 properties 值。若需要双向绑定,可使用 model 属性(基础库 2.8.3+):
Component({
properties: {
value: {
type: String,
value: ''
}
},
observers: {
'value'(val) {
this.triggerEvent('update:value', val)
}
}
})2. triggerEvent(子 → 父)
// 子组件
this.triggerEvent('myevent', { detail: 'data' }, { bubbles: true, composed: true })
// 父组件
<my-component bind:myevent="onMyEvent" />事件选项:
bubbles: 事件是否冒泡composed: 跨组件边界触发(穿越 Shadow DOM)capturePhase: 是否在捕获阶段触发
3. selectComponent(父主动获取子组件实例)
// 父组件
this.selectComponent('#child') // 返回子组件实例
this.selectAllComponents('.child-class') // 返回数组
// 然后直接调用子组件方法
childComp.someMethod()注意:selectComponent 在 onReady 之后才能使用。
4. 其他通信方式
| 方式 | 适用场景 | 说明 |
|---|---|---|
| 全局事件总线 | 无父子关系的组件/页面 | 手动实现发布订阅 |
| 共享 Behavior | 多个组件共享逻辑 | 类似 Mixin |
| shared(共享组件数据) | 微信插件内组件 | exposeInjection: true |
externalClasses | 样式传递 | 父组件向子组件传递 CSS 类名 |
| slot(插槽) | 内容分发 | 默认插槽 / 具名插槽 |
5. Behavior(类似 Mixin)
const myBehavior = Behavior({
data: { sharedData: 0 },
methods: {
sharedMethod() { /* ... */ }
}
})
Component({
behaviors: [myBehavior],
// 继承了 myBehavior 的 data 和 methods
})五、性能优化
1. 分包加载
原理:主包限制 2MB,整个小程序上限 20MB。通过分包将不常用的页面拆分出去。
// 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)
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["sub1"]
}
}进阶:通过 wx.preload API 动态预加载分包:
wx.preload({
subpackage: 'shop'
})3. 骨架屏
方案一:原生骨架屏
在 page 配置中启用:
{
"skeleton": "pages/skeleton/skeleton"
}方案二:组件实现
<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>.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. 图片懒加载
<image src="{{item.image}}" lazy-load bind:load="onImageLoad" />进阶方案(IntersectionObserver):
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 体系)
<!-- 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>条件编译(核心能力):
<!-- #ifdef MP-WEIXIN -->
<view>仅微信小程序显示</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view>仅H5显示</view>
<!-- #endif -->JS 中:
// #ifdef MP-WEIXIN
wx.login({ success(res) { } })
// #endif
// #ifdef APP-PLUS
plus.runtime.login(...)
// #endif2. Taro(React / Vue 体系)
// 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 语法):
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-app | Taro |
|---|---|---|
| 技术栈 | 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. 跨端兼容性处理要点
API 差异:使用平台判断包装统一 API,如
Taro.getStorage/uni.getStorageSync内部已处理组件差异:不同平台对组件支持不同(如
map在 H5 和小程序中表现不同)样式差异:使用
rpx统一单位,避免使用平台特有 CSS生命周期差异:各框架已做适配,但需注意细微差别(如 Taro 的
useDidShow对应onShow)导航差异:小程序
navigateTovs H5window.history
七、小程序八股文精选
Q1: 小程序的双线程架构是什么?
答:小程序采用双线程模型:
┌─────────────┐ 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: 小程序的安全机制?
答:
JS 沙箱:逻辑层与渲染层隔离,JS 不能操作 DOM
HTTPS 强制:所有网络请求必须 HTTPS
域名白名单:需在后台配置合法域名
内容安全:
wx.getFileSystemManager受限,文件系统不可随意读写登录态安全:
wx.login获取code→ 后端换session_key,前端拿token代码审计:发布前需微信审核
Q6: 如何实现小程序的登录?
答:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 小程序 │ │ 开发者服务器│ │ 微信服务器│
│ 前端 │ │ 后端 │ │ │
└────┬────┘ └────┬────┘ └────┬────┘
│ 1. wx.login() │ │
│ ─────────────────────────────────────→ │
│ 2. 返回 code │ │
│ ←──────────────────────────────────── │
│ 3. 发送 code │ │
│ ──────────────→ │ │
│ │ 4. code2Session │
│ │ ────────────────→ │
│ │ 5. openid+session_key │
│ │ ←─────────────── │
│ 6. 返回 token │ │
│ ←───────────── │ │Q7: wx.request 的封装与封装方案?
答:
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: 小程序的自动更新机制?
答:
// 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(() => {
// 下载失败,提示用户删除小程序重新搜索
})更新原理:
小程序冷启动时检查版本
下载新包(静默)
下载完成后等待下次冷启动或手动重启
冷启动时用新包替换旧包
Q9: 小程序的页面栈问题?
答:
当页面栈达到 10 层限制时:
navigateTo会失败解决方案:
用
redirectTo替代(关闭当前页再打开)用
reLaunch重定向重新设计导航流程(如 Tab 页 +
navigateTo)
Q10: WXML 模板编译的原理?
答:
WXML 与页面数据 → 视图层渲染 → 数据变化后更新界面WXML 会经过平台的编译和运行时处理;具体中间表示、差量算法和节点实现属于内部细节,不应写死为“WaxML”、Vue render 函数或某一种 Virtual DOM
运行时生成 虚拟节点树
setData触发新的虚拟节点树生成,与旧树 diff只更新变化的节点(最小化更新)