目录标题
- 1、跨域问题及解决方法
- 同源策略
- 生产环境解决方案
- 开发环境解决方案
- 其他解决方案
- 2、组件间通信方式
- Vue2中的组件通信方式
- Vue3中的组件通信方式
- 通用注意事项
- 3、微信小程序生命周期
- 微信小程序原生生命周期
- UniApp生命周期
- 4、微信小程序授权登录流程
- 登录流程
- 手机号获取
- 5、防抖与节流
- 防抖(Debounce)示例
- 节流(Throttle)示例
- 注意事项
- 拓展
1、跨域问题及解决方法
同源策略
同源策略是浏览器的重要安全机制,限制来自不同源的文档或脚本进行交互。"同源"指协议、域名、端口完全相同。该策略会阻止跨域AJAX请求、Cookie访问等行为,但允许嵌入资源(如<img>
、<script>
标签)。
生产环境解决方案
① CORS(跨域资源共享)
CORS是W3C标准,通过HTTP头部实现跨域访问控制。后端需配置响应头:服务器在响应头中设置 Access-Control-Allow-Origin 允许特定域或所有域(*)访问。需处理预检请求(OPTIONS)。
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
示例Node.js实现:
// 中间件设置CORS头
// Node.js Express示例
const express = require('express');
const app = express();
// 中间件设置CORS头
app.use((req, res, next) => {
// 允许的源(实际项目应配置允许的具体域名)
res.header('Access-Control-Allow-Origin', 'https://your-client-domain.com');
// 允许的HTTP方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// 允许的请求头
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 允许携带cookie(需前端同时设置withCredentials)
res.header('Access-Control-Allow-Credentials', 'true');
// 预检请求缓存时间
res.header('Access-Control-Max-Age', '86400');
next();
});
// 处理OPTIONS预检请求
app.options('*', (req, res) => {
res.sendStatus(200);
});
app.listen(3000, () => console.log('Server running with CORS enabled'));
② Nginx反向代理
通过Nginx将不同域的请求代理到同源地址:
server {
listen 80;
server_name your-domain.com;
location /api/ {
# 反向代理到实际后端服务
proxy_pass http://backend-service:3000;
# 添加CORS头
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,Content-Type';
# 处理OPTIONS预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
}
开发环境解决方案
代理服务器
Vite/webpack等工具支持代理设置(vite.config.js示例):
以下是对 vue.config.js
文件中代理配置的详细代码注释:
代理配置代码注释
// vue.config.js
module.exports = {
// 开发服务器配置
devServer: {
// 代理配置
proxy: {
// 代理规则名称,可以自定义,这里使用 '/api' 作为示例
'/api': {
// 目标服务器地址,这里是后端服务的实际地址
target: 'http://localhost:3000',
// 是否修改请求头中的host为目标URL
// 设置为true时,请求头中的host会改为target的域名
changeOrigin: true,
// 路径重写规则
pathRewrite: {
// 正则表达式,匹配请求路径中的/api前缀并替换为空字符串
// 例如:/api/user => /user
'^/api': ''
}
}
}
}
}
配置说明
target
: 指定后端API服务器的地址,所有匹配的请求都会被转发到该地址。changeOrigin
: 修改请求头中的host
字段为目标URL。某些后端服务可能会验证host
字段,开启此选项可避免被拦截。pathRewrite
: 重写请求路径,常用于去除或替换前缀。^/api
表示匹配以/api
开头的路径,替换为空字符串后,实际请求的路径将不再包含/api
。
使用场景示例
假设前端请求/api/users
,代理会将请求转发到http://localhost:3000/users
(路径中的/api
被移除)。
其他解决方案
① JSONP
利用<script>
标签不受同源限制的特性,仅支持GET请求:
function handleResponse(data) {
console.log(data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
② WebSocket 协议代码注释
WebSocket 协议允许客户端与服务器建立双向通信通道,不受同源策略限制。以下代码注释详细说明了其使用方法:
// 创建 WebSocket 连接,协议为 wss(WebSocket Secure)
const socket = new WebSocket('wss://example.com');
// 监听来自服务器的消息事件
socket.onmessage = (event) => {
// 将接收到的数据(event.data)解析为 JSON 对象并打印
console.log(JSON.parse(event.data));
};
postMessage 跨窗口通信代码注释
postMessage
是一种跨窗口通信的 API,允许不同源窗口之间安全传递数据:
// 发送方代码
// 向父窗口(window.parent)发送数据 'data',目标源为 'https://target-origin.com'
window.parent.postMessage('data', 'https://target-origin.com');
// 接收方代码
// 监听 'message' 事件
window.addEventListener('message', (event) => {
// 验证消息来源是否为可信源 'https://trusted-origin.com'
if (event.origin === 'https://trusted-origin.com') {
// 打印接收到的数据
console.log(event.data);
}
});
③ document.domain 的基本概念
document.domain
是浏览器同源策略(Same-Origin Policy)中的一个属性,允许具有相同顶级域名的子域之间通过脚本进行通信。通过设置该属性,子域可以放宽同源限制,实现跨子域的数据交互。
适用场景
- 父子域通信:例如
a.example.com
需要与b.example.com
共享数据。 - 跨子域窗口操作:如
iframe
嵌入不同子域的页面时访问彼此的内容。 - 仅限于同一顶级域名(如
.com
、.org
),且需双方页面主动设置相同值。
代码示例与注释
// 假设当前页面为 a.example.com,需与 b.example.com 通信
// 将文档域设置为顶级域名,允许跨子域访问
document.domain = 'example.com';
// 验证是否设置成功
console.log(document.domain); // 输出: example.com
注意事项
- 仅适用于子域间通信:无法用于完全不同的域名(如
example.com
与other.com
)。 - 需双方页面同步设置:通信的双方必须同时设置相同的
document.domain
值。 - 安全性限制:现代浏览器可能要求域名包含有效的公共后缀(如
example.com
而非自定义的localhost
)。
总结
CORS需后端配合,简单请求与预检请求的处理逻辑不同。
JSONP仅支持GET请求,安全性较低。
生产环境中避免使用 Access-Control-Allow-Origin: *,应指定具体域名。
大部分浏览器仍支持 document.domain
,但部分新版本可能限制其使用。 优先考虑 PostMessage
或 CORS
等更安全的方案。
2、组件间通信方式
在Vue2和Vue3中,组件间通信方式既有共性也有差异。以下是常见方法的分类和实现示例:
Vue2中的组件通信方式
**Props / $emit**
父组件通过props
向子组件传递数据,子组件通过$emit
触发事件通知父组件。
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMsg" @update="handleUpdate"/>
</template>
<script>
export default {
data() {
return { parentMsg: 'Hello' }
},
methods: {
handleUpdate(val) {
this.parentMsg = val
}
}
}
</script>
<!-- 子组件 -->
<script>
export default {
props: ['message'],
methods: {
sendMessage() {
this.$emit('update', 'New message')
}
}
}
</script>
$parent / $children
直接访问父/子组件实例(不推荐,破坏组件封装性)。
// 子组件中访问父组件
this.$parent.someMethod()
// 父组件中访问子组件
this.$children[0].someMethod()
$refs
通过引用直接操作子组件。
<template>
<ChildComponent ref="child"/>
</template>
<script>
export default {
methods: {
callChildMethod() {
this.$refs.child.someMethod()
}
}
}
</script>
Event Bus
创建一个中央事件总线。
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 组件A
EventBus.$emit('event-name', data)
// 组件B
EventBus.$on('event-name', data => {})
Vuex
状态管理库,适合复杂应用。
// store.js
export default new Vuex.Store({
state: { count: 0 },
mutations: {
increment(state) {
state.count++
}
}
})
// 组件中使用
this.$store.commit('increment')
Vue3中的组件通信方式
Props / emits
Vue3中更规范的写法,需要显式声明emits
。
父传子基础数据传递
<!-- Parent.vue -->
<template>
<Child title="用户信息" :userData="user" />
</template>
<script setup>
import { ref } from 'vue'
const user = ref({ name: '张三', age: 25 })
</script>
<!-- Child.vue -->
<script setup>
defineProps({
title: String,
userData: Object
})
</script>
子传父事件触发
<!-- Parent.vue -->
<template>
<Counter @increment="handleIncrement" />
</template>
<script setup>
const handleIncrement = (step) => {
console.log('收到步长:', step)
}
</script>
<!-- Counter.vue -->
<script setup>
const emit = defineEmits(['increment'])
const clickHandler = () => {
emit('increment', 5)
}
</script>
provide / inject
跨层级组件通信。
// 祖先组件
import { provide } from 'vue'
provide('key', value)
// 后代组件
import { inject } from 'vue'
const value = inject('key')
mitt
替代Event Bus的方案。
// event.js
import mitt from 'mitt'
export const emitter = mitt()
// 组件A
emitter.emit('event', data)
// 组件B
emitter.on('event', data => {})
Pinia
Vue3推荐的状态管理库。
// store.js
export const useStore = defineStore('main', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
// 组件中使用
const store = useStore()
store.increment()
Teleport
DOM结构层面的"通信"。
<teleport to="#modal-container">
<ModalComponent/>
</teleport>
通用注意事项
- 简单父子通信优先使用
props/emits
- 跨层级通信考虑
provide/inject
- 全局状态管理在复杂场景下使用Vuex(Pinia)
- 事件总线适用于小型应用,但要注意及时清理监听
- Vue3的组合式API提供了更灵活的代码组织方式
3、微信小程序生命周期
微信小程序原生生命周期
微信小程序的生命周期分为应用级、页面级和组件级三种。
应用生命周期
App({
onLaunch(options) {}, // 初始化时触发,全局只触发一次
onShow(options) {}, // 小程序启动或从后台进入前台时触发
onHide() {}, // 小程序从前台进入后台时触发
onError(error) {}, // 脚本错误或API调用失败时触发
onPageNotFound(res) {} // 页面不存在时触发
})
页面生命周期
Page({
data: {}, // 页面初始数据
onLoad(options) {}, // 页面加载时触发,一个页面只调用一次
onShow() {}, // 页面显示/切入前台时触发
onReady() {}, // 页面初次渲染完成时触发
onHide() {}, // 页面隐藏/切入后台时触发
onUnload() {}, // 页面卸载时触发
onPullDownRefresh() {}, // 下拉刷新时触发
onReachBottom() {}, // 页面上拉触底时触发
onShareAppMessage() {} // 用户点击右上角转发时触发
})
组件生命周期
Component({
lifetimes: {
attached() {}, // 组件实例进入页面节点树时触发
detached() {}, // 组件实例被从页面节点树移除时触发
},
pageLifetimes: {
show() {}, // 组件所在页面显示时触发
hide() {} // 组件所在页面隐藏时触发
}
})
UniApp生命周期
UniApp支持微信小程序的生命周期,同时扩展了Vue的生命周期。
应用生命周期
export default {
onLaunch(options) {}, // 初始化时触发
onShow(options) {}, // 启动或从后台进入前台时触发
onHide() {}, // 从前台进入后台时触发
globalData: {} // 全局数据
}
页面生命周期
export default {
data() { return {} },
onLoad(options) {}, // 页面加载时触发
onShow() {}, // 页面显示时触发
onReady() {}, // 页面初次渲染完成时触发
onHide() {}, // 页面隐藏时触发
onUnload() {}, // 页面卸载时触发
onPullDownRefresh() {}, // 下拉刷新时触发
onReachBottom() {}, // 页面上拉触底时触发
onShareAppMessage() {}, // 用户点击右上角转发时触发
methods: {} // 自定义方法
}
组件生命周期
export default {
props: {},
data() { return {} },
created() {}, // 组件实例刚创建时触发
mounted() {}, // 组件挂载到DOM时触发
destroyed() {}, // 组件销毁时触发
methods: {}
}
生命周期执行顺序
原生小程序页面首次加载时:
onLoad → onShow → onReady
UniApp页面首次加载时:
onLoad → onShow → created → mounted → onReady
组件挂载时:
原生:attached
UniApp:created → mounted
注意事项
- 原生小程序的onLoad和onShow参数包含场景值scene,而UniApp需要通过uni.getLaunchOptionsSync()获取。
- UniApp的页面生命周期与Vue组件生命周期共存,但微信小程序原生组件没有created等Vue生命周期。
- 跨平台开发时建议优先使用UniApp生命周期,如需使用平台特定生命周期,需通过条件编译实现。
4、微信小程序授权登录流程
登录流程
- 自动调用wx.login获取临时code(无需用户授权)
- 用户点击按钮触发wx.getUserProfile获取用户信息(需用户授权)
- 将code和用户加密数据发送到后端换取token
- 保存token和用户信息到本地存储
手机号获取
- 用户点击特殊按钮(open-type=“getPhoneNumber”)触发
- 通过事件对象获取加密数据
- 发送到后端解密获取真实手机号
- 保存手机号到本地存储
uniapp+vue3实现:
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const userInfo = ref(null)
const phoneNumber = ref('')
const token = ref('')
// 微信登录
const handleLogin = async () => {
try {
// 1. 获取登录凭证
const loginRes = await uni.login()
// 2. 获取用户信息(需用户主动触发)
const userRes = await uni.getUserProfile({
desc: '用于完善会员资料',
lang: 'zh_CN'
})
// 3. 调用后端登录接口
const { data } = await uni.request({
url: 'YOUR_API_URL/login',
method: 'POST',
data: {
code: loginRes.code,
rawData: userRes.rawData,
encryptedData: userRes.encryptedData,
iv: userRes.iv,
signature: userRes.signature
}
})
// 4. 保存登录状态
if(data.code === 200) {
token.value = data.token
userInfo.value = userRes.userInfo
uni.setStorageSync('token', data.token)
uni.setStorageSync('userInfo', userRes.userInfo)
uni.showToast({ title: '登录成功' })
}
} catch (error) {
uni.showToast({ title: '登录失败', icon: 'error' })
}
}
// 获取手机号
const getPhoneNumber = async (e) => {
if(!e.detail.code) return
try {
const { data } = await uni.request({
url: 'YOUR_API_URL/getPhone',
method: 'POST',
data: { code: e.detail.code }
})
if(data.code === 200) {
phoneNumber.value = data.phone
uni.setStorageSync('phoneNumber', data.phone)
}
} catch (error) {
uni.showToast({ title: '获取手机号失败', icon: 'none' })
}
}
</script>
<template>
<view class="container">
<button @click="handleLogin">微信登录</button>
<button
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>获取手机号</button>
</view>
</template>
微信小程序原生实现
// app.js
App({
globalData: {
userInfo: null,
phoneNumber: '',
token: ''
}
})
// pages/login/login.js
Page({
data: {
loading: false,
userInfo: null,
phoneNumber: '',
token: wx.getStorageSync('token') || ''
},
onLoad() {
if (this.data.token) {
this.setData({
userInfo: wx.getStorageSync('userInfo')
})
}
},
handleLogin() {
this.setData({ loading: true })
wx.login({
success: (res) => {
if (res.code) {
this._code = res.code
wx.getUserProfile({
desc: '用于完善会员资料',
lang: 'zh_CN',
success: (userRes) => {
this._requestLogin(userRes)
},
fail: (err) => {
console.error('获取用户信息失败', err)
this.setData({ loading: false })
}
})
}
},
fail: (err) => {
console.error('登录失败', err)
this.setData({ loading: false })
}
})
},
_requestLogin(userRes) {
wx.request({
url: 'https://your-api.com/login',
method: 'POST',
data: {
code: this._code,
rawData: userRes.rawData,
encryptedData: userRes.encryptedData,
iv: userRes.iv
},
success: (res) => {
if (res.data.code === 200) {
this.setData({
token: res.data.token,
userInfo: userRes.userInfo
})
wx.setStorageSync('token', res.data.token)
wx.setStorageSync('userInfo', userRes.userInfo)
}
},
complete: () => {
this.setData({ loading: false })
}
})
},
handleGetPhone(e) {
if (!e.detail.code) return
if (!this.data.token) return
wx.request({
url: 'https://your-api.com/getPhone',
method: 'POST',
data: { code: e.detail.code },
success: (res) => {
if (res.data.code === 200) {
this.setData({ phoneNumber: res.data.phone })
wx.setStorageSync('phoneNumber', res.data.phone)
}
}
})
}
})
pages/login/login.wxml
<view class="container">
<button
bindtap="handleLogin"
disabled="{{loading}}"
type="primary"
>
{{loading ? '登录中...' : '微信登录'}}
</button>
<button
open-type="getPhoneNumber"
bindgetphonenumber="handleGetPhone"
disabled="{{!token}}"
type="primary"
style="margin-top: 20px;"
>
获取手机号
</button>
<view wx:if="{{userInfo}}" class="user-info">
<image src="{{userInfo.avatarUrl}}" mode="aspectFill"></image>
<text>{{userInfo.nickName}}</text>
</view>
<view wx:if="{{phoneNumber}}" class="phone-info">
<text>手机号: {{phoneNumber}}</text>
</view>
</view>
5、防抖与节流
防抖(Debounce)和节流(Throttle)都是用于优化高频率执行事件的技术,特别是在浏览器环境中处理用户交互(如滚动、输入、调整窗口大小等)时非常有用。
防抖(Debounce)示例
防抖适用于频繁触发的事件(如输入框搜索),只在最后一次操作后延迟执行。
<template>
<input @input="handleInput" placeholder="输入搜索内容" />
</template>
<script>
import { debounce } from 'lodash-es';
export default {
methods: {
// 使用lodash的debounce
handleInput: debounce(function(event) {
console.log('防抖处理:', event.target.value);
// 实际搜索逻辑
}, 500),
// 手动实现防抖
manualDebounce(event) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
console.log('手动防抖:', event.target.value);
}, 500);
}
},
beforeDestroy() {
clearTimeout(this.timeout); // 清除定时器
}
};
</script>
节流(Throttle)示例
节流适用于连续触发但需要限制频率的事件(如滚动、窗口大小调整)。
<template>
<div @scroll="handleScroll" style="height: 200px; overflow-y: scroll">
<div style="height: 1000px">滚动区域</div>
</div>
</template>
<script>
import { throttle } from 'lodash-es';
export default {
methods: {
// 使用lodash的throttle
handleScroll: throttle(function(event) {
console.log('节流滚动事件:', event.target.scrollTop);
}, 300),
// 手动实现节流
manualThrottle(event) {
if (!this.throttleFlag) {
this.throttleFlag = true;
setTimeout(() => {
console.log('手动节流:', event.target.scrollTop);
this.throttleFlag = false;
}, 300);
}
}
},
data() {
return {
throttleFlag: false
};
}
};
</script>
注意事项
性能优化:推荐使用 lodash-es 的 debounce 或 throttle,避免重复创建函数实例。
生命周期:在 beforeDestroy 或 unmounted 钩子中清除定时器,防止内存泄漏。
Vue 3:在 Composition API 中可结合 onUnmounted 清理:
<script setup>
import { ref, onUnmounted } from 'vue';
import { debounce } from 'lodash-es';
const inputValue = ref('');
const debouncedSearch = debounce(() => {
console.log('搜索:', inputValue.value);
}, 500);
onUnmounted(() => {
debouncedSearch.cancel(); // 取消lodash的防抖
});
</script>
拓展
安装 lodash-es 的方法
在 Vue 项目中安装 lodash-es 可以通过 npm 或 yarn 进行安装,以下是具体步骤。
npm install lodash-es
或者使用 yarn:
yarn add lodash-es
使用 lodash-es
安装完成后,可以在 Vue 项目中按需引入 lodash-es 的功能模块。
import { debounce, throttle } from 'lodash-es';
按需加载优化
为了减少打包体积,可以通过 babel 插件 babel-plugin-lodash 实现按需加载。
npm install babel-plugin-lodash --save-dev
然后在 babel.config.js 中配置:
module.exports = {
plugins: ['lodash']
};
全局引入(可选)
如果需要全局使用 lodash-es,可以在 Vue 项目的入口文件(如 main.js)中引入并挂载到 Vue 原型上。
import _ from 'lodash-es';
Vue.prototype._ = _;
之后在组件中可以通过 this._ 调用 lodash 的方法。
this._.debounce(() => {
// 你的逻辑
}, 300);
注意事项
lodash-es 是 lodash 的 ES 模块版本,更适合现代前端项目。
按需加载可以有效减小打包体积,提升性能。
全局引入会增加打包体积,建议仅在必要时使用。