拒绝代码堆砌:一套优雅的微信小程序登录页面组件化方案

28 阅读7分钟

登录页该怎么写?

这是一个看似简单,但很多开发者都答不好的问题。

我见过代码臃肿的登录页,也见过简洁优雅的登录页。它们功能相同,但可维护性天差地别。

今天分享一套登录组件架构,用合理的设计让代码变得简洁清晰。

先上图看效果,然后慢慢展开

1e14c4aa-4173-40b7-b7eb-37ab9e22c8a9.png

一、设计理念

我的核心设计理念可以概括为三个关键词:

  1. 职责单一 每个组件只做一件事,做好一件事。输入组件只负责数据录入,按钮组件只负责状态反馈,布局组件只负责视觉框架。

  2. 数据驱动 状态由父组件控制,组件只负责展示和事件转发。组件内部不维护业务状态,避免数据不一致。

  3. 约定优于配置 提供合理的默认值,减少不必要的参数传递。比如登录按钮默认显示"登录",密码输入框默认是密码模式。

二、组件架构

整个登录模块包含 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 架构优势

通过这套组件化架构:

代码简洁:模板结构清晰,易于理解

高复用性:组件可独立使用,也可组合使用

职责分离:布局、输入、交互各司其职

易于扩展:新增功能只需扩展组件