登录页该怎么写?
这是一个看似简单,但很多开发者都答不好的问题。
我见过代码臃肿的登录页,也见过简洁优雅的登录页。它们功能相同,但可维护性天差地别。
今天分享一套登录组件架构,用合理的设计让代码变得简洁清晰。
先上图看效果,然后慢慢展开
一、设计理念
我的核心设计理念可以概括为三个关键词:
-
职责单一 每个组件只做一件事,做好一件事。输入组件只负责数据录入,按钮组件只负责状态反馈,布局组件只负责视觉框架。
-
数据驱动 状态由父组件控制,组件只负责展示和事件转发。组件内部不维护业务状态,避免数据不一致。
-
约定优于配置 提供合理的默认值,减少不必要的参数传递。比如登录按钮默认显示"登录",密码输入框默认是密码模式。
二、组件架构
整个登录模块包含 5 个组件,结构如下:
components/login/
├── input-icon/ # 通用输入组件
├── input-user/ # 用户名输入
├── input-password/ # 密码输入
├── btn-login/ # 登录按钮
└── layout/ # 布局容器
各组件职责清晰,相互独立,可以单独使用,也可以组合使用。
三、Layout 布局组件
3.1 组件设计
layout 组件提供统一的登录页视觉框架,包含三个可配置元素:
Logo 和公司名称:展示在页面顶部
欢迎语:个性化提示文案
插槽区域:动态插入登录表单
Component({
options: {
multipleSlots: true // 启用多插槽支持
},
properties: {
logoSrc: { type: String, value: '' },
companyName: { type: String, value: '' },
welcomeText: { type: String, value: '欢迎登录' }
}
})
设计亮点:
多插槽支持:multipleSlots: true 允许在组件中使用多个 ,提供更高的灵活性。
默认值友好:welcomeText 提供默认值,不传参数时自动显示"欢迎登录"。
职责单一:只负责布局和样式,不涉及任何业务逻辑。
3.2 模板结构
<view class="layout">
<view class="ll-top">
<layout-top>
<view class="ll-top-logo">
<image src="{{logoSrc}}"></image>
<view class="ll-top-logo-title">{{companyName}}</view>
</view>
</layout-top>
<view class="ll-top-welcome">{{welcomeText}}</view>
</view>
<view class="ll-main">
<slot></slot>
</view>
</view>
结构分析:
使用 layout-top 子组件封装顶部导航,增强复用性。 标签为内容提供插入点,父组件可动态注入登录表单。
3.3 使用示例
<layout
logoSrc="/asset/logo.png"
companyName="系统名称"
welcomeText="欢迎登录">
<view class="login-form">
<!-- 登录表单内容 -->
</view>
</layout>
使用优势:
视觉统一:不同登录页(如普通登录、第三方登录)保持一致风格。
配置灵活:Logo、公司名称、欢迎语均可动态配置。
内容自由:插槽区域可插入任意内容。
四、Input-Icon 通用输入组件
4.1 组件设计
input-icon 是最基础的输入组件,支持图标、占位符、密码模式,是整个登录模块的核心。
Component({
properties: {
// 图标路径
iconSrc: { type: String, value: '' },
// 占位文字
placeholder: { type: String, value: '' },
// 是否是密码输入
isPassword: { type: Boolean, value: false },
// 输入值
value: { type: String, value: '' }
},
methods: {
onInput(e) {
this.triggerEvent('input', { value: e.detail.value })
}
}
})
设计亮点:
通用性强:不限定具体场景,可用于用户名、密码、手机号、验证码等各种输入场景。
属性配置合理:四个属性覆盖了输入框的核心需求,没有冗余参数。
受控组件模式:通过 value 属性接收输入值,通过 input 事件向外传递,完全由父组件控制状态。
事件处理简洁:使用 triggerEvent 向上传递事件,符合微信小程序的事件机制。
4.2 模板结构
<view class="form-item">
<view class="form-icon">
<image src="{{iconSrc}}"></image>
</view>
<input
class="form-input"
placeholder="{{placeholder}}"
password="{{isPassword}}"
value="{{value}}"
bindinput="onInput"
placeholder-class="input-placeholder"/>
</view>
结构分析:
使用 Flex 布局,图标和输入框水平排列。
password="{{isPassword}}" 控制密码显示模式。
placeholder-class 自定义占位符样式。
4.3 设计总结
input-icon 体现了组件化的核心思想:
职责单一:只负责输入框的展示和事件转发
配置灵活:属性覆盖核心需求,无冗余
数据驱动:完全由父组件控制状态
易于测试:功能单一,单元测试简单
这是整个登录模块中最基础也最重要的组件,理解了它,后续的 input-user 和 input-password 就很容易了。
五、Input-User 和 Input-Password 场景专用组件
5.1 组件设计
这两个组件是基于 input-icon 的场景化封装,通过预设默认值实现"约定优于配置"。
input-user.js
Component({
properties: {
iconSrc: { type: String, value: '' },
placeholder: { type: String, value: '请输入用户名' },
value: { type: String, value: '' }
},
methods: {
onInput(e) {
this.triggerEvent('input', { value: e.detail.value })
}
}
})
input-password.js
Component({
properties: {
iconSrc: { type: String, value: '' },
placeholder: { type: String, value: '请输入密码' },
value: { type: String, value: '' }
},
methods: {
onInput(e) {
this.triggerEvent('input', { value: e.detail.value })
}
}
})
设计亮点:
语义化强:组件名称直观,一眼看出用途。
默认值友好:预设了合理的占位符,使用时无需重复配置。
简化使用:相比 input-icon,减少了一个 isPassword 参数,更聚焦于特定场景。
5.2 模板结构
input-user.wxml
<view class="form-item">
<view class="form-icon">
<image src="{{iconSrc}}"></image>
</view>
<input
class="form-input"
placeholder="{{placeholder}}"
value="{{value}}"
bindinput="onInput"
placeholder-class="input-placeholder"/>
</view>
input-password.wxml
<view class="form-item">
<view class="form-icon">
<image src="{{iconSrc}}"></image>
</view>
<input
class="form-input"
placeholder="{{placeholder}}"
password
value="{{value}}"
bindinput="onInput"
placeholder-class="input-placeholder"/>
</view>
结构分析:
input-password 直接使用 password 属性,无需传递参数。
其他结构与 input-icon 完全一致,保持视觉统一。 5.3 使用示例
<input-user
iconSrc="/asset/ico_user.png"
value="{{username}}"
bind:input="onUsernameInput"/>
<input-password
iconSrc="/asset/ico_pwd.png"
value="{{password}}"
bind:input="onPasswordInput"/>
使用对比:
代码更简洁,参数更少。
语义更清晰,阅读代码更容易理解。
5.4 设计总结
这两个组件体现了"约定优于配置"的设计理念:
减少重复配置:预设默认值,简化使用
语义化命名:组件名即用途
保持一致性:视觉和交互与基础组件一致
易于扩展:其他表单场景可以按此模式创建
从通用组件到场景专用组件的演变,展示了如何根据实际需求灵活调整抽象层次。
六、Btn-Login 登录按钮组件
6.1 组件设计
btn-login 是一个带加载状态的登录按钮,内置了禁用和加载状态管理。
Component({
properties: {
// 是否禁用
disabled: { type: Boolean, value: false },
// 是否加载中
loading: { type: Boolean, value: false },
// 加载文字
loadingText: { type: String, value: '登录中...' },
// 默认文字
defaultText: { type: String, value: '登 录' }
},
methods: {
onTap() {
if (!this.properties.disabled && !this.properties.loading) {
this.triggerEvent('tap')
}
}
}
})
设计亮点:
内置状态管理:按钮内部自己处理禁用和加载状态的防重复点击,父组件无需关心交互细节。
合理的默认值:loadingText 和 defaultText 都有默认值,使用时无需传递,符合"约定优于配置"理念。
防护逻辑清晰:点击时先判断 disabled 和 loading,只有都为 false 时才触发事件。
6.2 模板结构
<view
class="login-btn {{disabled || loading ? 'disabled' : ''}}"
bindtap="onTap"
disabled="{{disabled || loading}}">
<view wx:if="{{!loading}}">{{defaultText}}</view>
<view wx:else class="loading-text">{{loadingText}}</view>
</view>
结构分析:
根据状态动态添加 disabled 类名,改变按钮样式。
使用 wx:if 条件渲染,加载时显示 loadingText,否则显示 defaultText。
disabled="{{disabled || loading}}" 确保加载时按钮不可点击。
6.3 使用示例
<btn-login
disabled="{{!canLogin}}"
loading="{{isLoading}}"
bind:tap="onLogin"/>
页面实现:
Page({
data: {
username: '',
password: '',
canLogin: false,
isLoading: false
},
validateForm() {
const { username, password } = this.data
const canLogin = username.trim().length > 0 && password.length > 0
this.setData({ canLogin })
},
onLogin() {
if (!this.data.canLogin || this.data.isLoading) return
this.setData({ isLoading: true })
// 发起登录请求...
}
})
6.4 设计总结
btn-login 是整套组件中最优秀的部分,体现了良好的交互设计:
职责单一:只负责按钮展示和点击处理
防护完善:内置防重复点击逻辑
用户友好:加载状态清晰反馈
易于使用:最少只需传递两个参数
七、完整使用示例
7.1 模板结构分析
pages/login/index.wxml
<layout
logoSrc="/asset/logo.png"
companyName="系统名称"
welcomeText="欢迎登录">
<view class="login-form">
<input-user
value="{{username}}"
bind:input="onUsernameInput"/>
<input-password
value="{{password}}"
bind:input="onPasswordInput"/>
<btn-login
disabled="{{!canLogin}}"
loading="{{isLoading}}"
bind:tap="onLogin"/>
</view>
</layout>
模板分析:
布局层:layout 组件提供统一的视觉框架,包含 Logo、公司名称、欢迎语。
输入层:input-user 和 input-password 负责用户名和密码输入,通过 bind:input 事件将输入值传递给页面。
交互层:btn-login 负责登录操作,通过 disabled 和 loading 属性控制按钮状态,通过 bind:tap 事件响应点击。
7.2 数据流分析
code
用户输入 → input组件 → 触发input事件 → 页面更新data → 验证表单 → 更新canLogin → 按钮状态变化
属性下行:页面通过 value、disabled、loading 属性控制组件状态。
事件上行:组件通过 bind:input、bind:tap 事件将用户操作反馈给页面。
7.3 架构优势
通过这套组件化架构:
代码简洁:模板结构清晰,易于理解
高复用性:组件可独立使用,也可组合使用
职责分离:布局、输入、交互各司其职
易于扩展:新增功能只需扩展组件