先了解一下,微信小程序 Component 多单词命名规范(官方 + 行业最佳实践)
- 组件标签名:短横线 kebab-case(必须)
- 文件 / 文件夹名:短横线 kebab-case(推荐)
- JS 内部构造名:大驼峰 PascalCase(推荐)
1、自定义登录Component组件 login-popup
1.1、组件目录
components/
└─ login-popup/
├─ login-popup.js
├─ login-popup.json
├─ login-popup.wxml
└─ login-popup.wxss
1.2、login-popup.wxml
<!-- 遮罩 -->
<view class="login-mask {{show ? 'mask-show' : ''}}" bindtap="close">
<!-- 底部弹窗容器 带动画 -->
<view class="login-popup-wrap {{show ? 'popup-show' : ''}}" catchtap="preventTouch">
<view class="login-header">
<!-- 顶部圆角拖拽条 -->
<view class="drag-bar"></view>
<view class="login-title">快捷登录</view>
</view>
<view class="login-content">
<text class="tip-message">为优先保存您的数据,建议您登录</text>
<!-- 微信授权登录(头像昵称) -->
<button class="wx-login-btn" bindtap="wxAuthorLogin">
微信账号登录
</button>
<!-- 微信一键获取手机号登录 -->
<!-- <button class="phone-login-btn" open-type="getPhoneNumber" bindgetphonenumber="wxGetPhone">
手机号一键登录
</button> -->
<!-- 协议勾选 -->
<view class="agree-box">
<checkbox class="checkbox" checked="{{isAgree}}" bindtap="toggleAgree" />
<view class="agree-text">
我已阅读并同意
<text class="link" bindtap="goUserAgree">《用户协议》</text>
及
<text class="link" bindtap="goPrivacy">《隐私政策》</text>
</view>
</view>
</view>
<!-- 适配底部安全区 -->
<view class="safe-area"></view>
</view>
</view>
1.3、login-popup.wxss
/* components/login-popup/login-popup.wxss */
/* 遮罩 */
.login-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.login-mask.mask-show {
opacity: 1;
visibility: visible;
}
/* 底部弹窗容器 初始在屏幕外下方 */
.login-popup-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
transform: translateY(100%);
transition: transform 0.3s ease-out;
z-index: 10000;
}
/* 滑入动画 */
.login-popup-wrap.popup-show {
transform: translateY(0);
}
.login-header {
}
/* 顶部拖拽条 */
.drag-bar {
width: 80rpx;
height: 8rpx;
background: #e5e5e5;
border-radius: 4rpx;
margin: 20rpx auto;
}
/* 标题 */
.login-title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 20rpx;
color: #333;
background-color: #ffffff;
}
.login-content {
padding: 40rpx 40rpx 20rpx;
background-color: #f5f5f5;
}
.tip-message {
display: block;
color: #C6c6c6;
text-align: center;
font-size: 28rpx;
padding-bottom: 20rpx;
}
/* 微信账号登录按钮 */
.wx-login-btn {
background: #07c160;
color: #fff;
border-radius: 10rpx;
font-size: 32rpx;
margin: 0 0 30rpx;
}
.wx-login-btn::after {
border: none;
}
/* 手机号一键登录按钮 */
.phone-login-btn {
background: #fff;
color: #07c160;
border: 1rpx solid #07c160;
border-radius: 10rpx;
font-size: 32rpx;
margin: 0 0 40rpx;
}
.phone-login-btn::after {
border: none;
}
/* 协议区域 */
.agree-box {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.checkbox {
transform: scale(0.7);
margin-right: 10rpx;
}
.agree-text {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.link {
color: #07c160;
}
/* 底部安全区适配 */
.safe-area {
height: env(safe-area-inset-bottom);
background: #f5f5f5;
}
1.4、login-popup.js(核心:获取手机号 + 授权逻辑)
获取手机号如果不需要的话,可以忽略
// components/login-popup/login-popup.js
const app = getApp()
Component({
properties: {
show: {
type: Boolean,
value: false
}
},
data: {
isAgree: false
},
methods: {
// 阻止冒泡
preventTouch() {},
// 关闭弹窗
close() {
app.$closeLogin()
setTimeout(() => {
this.setData({
isAgree: false
})
}, 300)
},
// 切换协议勾选
toggleAgree() {
this.setData({
isAgree: !this.data.isAgree
})
},
// 跳转协议页面
goUserAgree() {
wx.navigateTo({ url: '/pages/agreement/agreement' })
},
goPrivacy() {
wx.navigateTo({ url: '/pages/privacy/privacy' })
},
// 微信授权 获取昵称头像
wxAuthorLogin() {
if (!this.data.isAgree) {
wx.showToast({ title: '请先勾选同意协议', icon: 'none' })
return
}
wx.getUserProfile({
desc: '用于完善用户个人资料',
success: (res) => {
wx.login({
success: loginRes => {
// 回调给页面:微信账号登录
app.globalData.loginCallback?.({
type: 'userInfo',
userInfo: res.userInfo,
code: loginRes.code
})
this.close()
}
})
},
fail: () => {
wx.showToast({ title: '授权已取消', icon: 'none' })
}
})
},
// 微信一键获取手机号
wxGetPhone(e) {
if (!this.data.isAgree) {
wx.showToast({ title: '请先勾选同意协议', icon: 'none' })
return
}
// 用户拒绝授权手机号
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
// 拿到加密的 phoneCode,传给后端解密拿真实手机号
const phoneCode = e.detail.code
wx.login({
success: loginRes => {
app.globalData.loginCallback?.({
type: 'phoneNumber',
phoneCode: phoneCode,
wxCode: loginRes.code
})
this.close()
}
})
}
}
})
1.5、login-popup.json
{
"component": true,
"usingComponents": {}
}
1.6、app.json 全局注册
{
"usingComponents": {
"login-popup": "/components/login-popup/login-popup"
}
}
1.7、app.js 全局方法
App({
globalData: {
loginCallback: null
},
$login(callback) {
this.globalData.loginCallback = callback
const pages = getCurrentPages()
const currPage = pages[pages.length - 1]
currPage.setData({ showGlobalLogin: true })
},
$closeLogin() {
const pages = getCurrentPages()
const currPage = pages[pages.length - 1]
currPage.setData({ showGlobalLogin: false })
}
})
1.8、页面调用
页面 wxml 加一行即可:
<login-popup show="{{showGlobalLogin}}" />
1.9、页面Js调用
// 打开登录弹窗
openLogin() {
getApp().$login(res => {
console.log('登录回调结果', res)
if (res.type === 'userInfo') {
// 微信登录:res.userInfo、res.code
console.log('微信用户信息', res.userInfo)
console.log('wx.login code', res.code)
// 传给后端登录
} else {
// 手机号验证码登录
console.log('手机号', res.phone, '验证码', res.code)
}
})
}
2、小程序登录流程
2.1、登录完成流程(时序)
- 小程序前端:调用
wx.login(),拿到临时 code(5 分钟有效期、一次性)。 - 前端→开发者服务器:把
code发给自己后端。 - 开发者服务器→微信接口:请求
jscode2session,参数appid+secret+code+grant_type=authorization_code。- 微信返回:
openid(用户唯一标识)、session_key(会话密钥,不可下发前端)、unionid(开放平台绑定后才有)。
- 微信返回:
- 服务器生成登录态:用
openid生成自定义token(如 JWT),关联用户会话。 - 服务器→前端:返回
token,前端存到localStorage或cookie。 - 后续请求:前端带
token访问业务接口,服务器验证token识别用户。
2.2、关键概念
- code:临时凭证,前端获取,一次性有效。
- openid:用户在当前小程序的唯一 ID,不可跨小程序。
- unionid:用户在开放平台下的唯一 ID,可跨小程序 / 公众号(需绑定开放平台)。
- session_key:微信加密密钥,用于解密用户敏感数据(如手机号),仅后端存储。
2.3、常见变体
- 静默登录:只拿
openid,不获取用户信息,自动完成。 - 用户信息授权:调用
wx.getUserProfile,前端拿到用户昵称 / 头像,后端用session_key校验签名。 - 手机号登录:调用
wx.getPhoneNumber拿到加密数据,后端用session_key解密获取手机号。
2.4、安全要点
- code 只能用一次,防止重放攻击。
- session_key 绝不返回前端,避免泄露。
- token 建议短期有效,配合刷新机制。
3、登录(获取昵称/头像) | 退出 功能实现
上面登录弹窗已经写好了,流程也清楚了,下面开始写登录及退出功能,由于登录可能在任意页面,其它页面页有可能要判断登录状态,所以登录功能要写成全局的。
3.1、app.js
在globalData中新增userInfo字段,提供setUserInfo(userInfo)用户设置用户信息到本地,提供logout()函数用于退出登录
globalData: {
userInfo: null,
loginCallback: null
},
onLaunch() {
// 小程序启动时,读取本地缓存,初始化全局用户信息
const storageUser = wx.getStorageSync('userInfo')
if (storageUser) {
this.globalData.userInfo = storageUser
}
},
// 登录:设置用户信息
setUserInfo(userInfo) {
this.globalData.userInfo = userInfo;
console.log(userInfo);
wx.setStorageSync('userInfo', userInfo);
this._refreshAllPages();
},
// 退出登录:设置为 null
logout() {
this.globalData.userInfo = null;
wx.removeStorageSync('userInfo');
this._refreshAllPages();
},
// 刷新所有页面(核心:自动更新 WXML)
_refreshAllPages() {
const pages = getCurrentPages();
const user = this.globalData.userInfo;
pages.forEach(page => {
if (page.setData) {
page.setData({
userInfo: user
});
}
});
},
3.2、utils/watchUserInfo.js
这个js是用来观测用户信息变化的,这样可以及时更新用户状态
export function watchUserInfo(page) {
const app = getApp();
const oldOnShow = page.onShow || function () {};
page.onShow = function () {
this.setData({
userInfo: app.globalData.userInfo
});
oldOnShow.call(this);
};
return page;
}
3.3、页面js调用
import { watchUserInfo } from '../../utils/watchUserInfo.js';
Page(watchUserInfo({
data: {},
getUserProfile() {
wx.getUserProfile({
desc: '登录',
success: (res) => {
console.log('微信返回:', res.userInfo);
const app = getApp();
app.setUserInfo(res.userInfo);
}
});
},
logout() {
const app = getApp();
app.logout();
}
}));
3.4、结合 login-popup 组件调用
上面3.3的代码没有结合 login-popup 组件,这里结合 第一部分写的login-popup组件来登录
// 点击用户信息卡片,没有登录时,弹出登录窗;已登录状态则跳转到个人信息页面
tapUserCard() {
const app = getApp()
if (app.globalData.userInfo) {
console.log("has logined") // 可调转到个人资料页面,也可啥都不做直接返回
} else {
getApp().$login((res) => {
console.log('登录成功', res)
if (res.type === 'userInfo') {
// ✅ 只需要这一句!
// 全局更新 + 本地缓存 + 当前页面自动刷新,这是最新版的
app.setUserInfo(res.userInfo);
}
if (res.type === 'phone') {
// 手机号登录
}
})
}
},