前端组件化开发指南(一)

564 阅读7分钟

如何写出高质量的前端代码》学习笔记

WHAT什么是组件化开发?

组件化开发是一种将 UI 界面拆分成可重用代码块的开发方式。一个完整的页面可以由多个组件构成:

    <!-- 一个典型的页面组件结构 -->
    <template>
      <div>
        <Header/>
        <main>
          <Banner/>
          <AboutUs/>
          <Services/> 
          <ContactUs/>
        </main>
        <Footer/>
      </div>
    </template>

WHY为什么要使用组件化开发?

1. 提高代码可读性

未组件化的代码,当修改"关于我们"部分的内容时,很难一眼找到你要修改的内容,你需要滚动鼠标滚轮来回查找,也需要阅读与"关于我们"无关的代码。

    <div>
        <div class="header">
            <img src="logo.png"/>
            <h1>网站名称</h1>
            <!--  其他头部代码    -->
        </div>
        <div class="main-content">
            <div class="banner">
                <ul>
                    <li><img src="banner1.png"></li>
                    <!--   省略n行代码      -->
                </ul>
            </div>
            <div class="about-us">
                <!--   省略n行代码      -->
            </div>
        </div>
    </div>

组件化后的代码,页面结构简洁,修改"关于我们"方便,避免干扰,聚焦组件。可直接进入"AboutUs"组件,删除、增加、编辑某模块,代码可读性增强,维护难度降低。

    <div>
        <Header/>
        <main>
            <Banner/>
            <AboutUs/>
            <Services/>
            <ContactUs/>
        </main>
        <Footer/>
    </div>

2. 提升代码复用性

抽取组件可以提高开发效率和降低维护成本,因为它消除了重复。不再需要重新造轮子,拿来就用,需求变更时只需修改组件一处即可。

    <!-- 可复用的表单标题组件 -->
    <template>
      <div class="form-title">
        <div class="line"></div>
        <h3>{{ title }}</h3>
        <slot></slot>
      </div>
    </template>

    <script>
    export default {
      props: {
        title: String
      }
    }
    </script>

3. UI更一致

协作时,若没有共用组件,各自开发的页面可能有差异,即使有UI规范也难以严格执行。例如标题,大小、颜色、padding、margin可能各异。使用组件化可避免这一问题,使开发人员只需使用组件,无需记忆规范。

    <!-- 不同级别的表单标题 -->
    <FormTitle :level="1"/>
    <FormTitle :level="2"/>

4. 提升可测试性

每个组件有自己的职责和功能,可以定义和实现,容易测试,比大页面更简单纯粹。

WHEN何时进行组件抽取?

组件抽取的时机可以从以下4个角度来考虑:

1. 复用性

当一个功能在多个地方重复出现或预期会被重复使用时,应考虑抽取为组件。

最佳实践:

  • 发现代码有复用机会时及时抽取
  • 开发新功能前先重构已有代码
  • 遵循"先重构,再开发"的原则

2. 复杂度

当组件变得过于复杂时,需要拆分成更小的组件以提升可维护性。

判断标准:

  • 单个文件代码建议不超过300行
  • 功能过于复杂难以维护
  • 代码可读性严重下降

3. 结构化编程

通过组件拆分使代码结构更清晰,即使代码量不大也可以拆分。

目的:

  • 清晰展示功能主结构
  • 隐藏实现细节
  • 提升代码可读性
  • 类似PPT目录,先概览后细节

4. 分离关注点

每个组件应该只关注自己的核心任务,不同的功能关注点应该分离。

HOW如何写出好组件?

一个好的组件应该从使用者和维护者两个视角来评价:

使用者视角

好组件评价指标 = 复用性 + 扩展性 + 易用性 + 可读性 + 正交性

1. 提升复用性

用抽象代替具体

假设用户列表上有搜索区域,可输入姓名和手机号进行搜索:

反面示例 - 耦合具体业务,UserSearch组件只适用于用户列表搜索场景,复用性较差。如果需要搜索产品列表,则需要再次创建ProductSearch组件。

    <!-- UserSearch.vue -->
    <template>
        <div>
            <input v-model="name" placeholder="输入姓名筛选"/>
            <input v-model="phone" placeholder="输入手机号筛选"/>
            <button @click="emitSearch">搜索</button>
        </div>
    </template>

    <script>
    export default {
        name: 'UserSearch',
        data(){
            return {
                name: '',
                phone: ''
            }
        },
        methods:{
            emitSearch(){
                this.$emit('search', {
                    name: this.name,
                    phone: this.phone
                })
            }
        }
    }
    </script>

正面示例 - 抽象通用搜索,如果我们从项目角度出发考虑搜索功能,会发现各个列表页都需要搜索功能。搜索组件可以抽象为通用的SearchForm,可配置表单内容不固定,通过抛出search事件,可以在各个页面复用。

    <!-- SearchForm.vue -->
    <template>
        <div class="search-form">
            <div v-for="field in fields" :key="field.key">
                <component 
                    :is="field.component"
                    v-model="formData[field.key]"
                    v-bind="field.props"
                />
            </div>
            <button @click="handleSearch">搜索</button>
        </div>
    </template>

    <script>
    export default {
        name: 'SearchForm',
        props: {
            fields: {
                type: Array,
                required: true
            }
        },
        data() {
            return {
                formData: {}
            }
        },
        methods: {
            handleSearch() {
                this.$emit('search', this.formData)
            }
        }
    }
    </script>

使用示例:

    <template>
        <SearchForm
            :fields="fields"
            @search="search"
        />
    </template>

    <script>
    export default {
        data(){
            return {
                fields:[
                    {
                        key: 'name',
                        label:'姓名',
                        component: 'input',
                        props:{
                            placeholder: '输入姓名筛选'
                        }
                    },
                    {
                        key: 'phone',
                        label:'手机',
                        component: 'input',
                        props:{
                            placeholder: '输入手机号筛选'
                        }
                    }
                ]
            }
        },
        methods:{
            search(data){
                console.log('搜索条件:', data)
            }
        }
    }
    </script>

单一原则

组件就像积木,通过组合成功能模块或页面。复杂的多功能积木用途狭窄,功能单一的积木用途广泛。像ElementUI中的icon、button、input等组件,越小单一的功能,通过组合能发挥更大的威力。适用于各种功能、各种页面、各种行业。

组件封装应遵循单一职责原则,尤其是通用组件和项目基础组件。对于业务领域组件,尽量完成单一功能以提高复用性。

2. 扩展性

组件由DOM、逻辑和样式三部分组成,要提升组件的扩展性,需要从这三个方面同时考虑:

扩展组件 = 扩展DOM + 扩展逻辑 + 扩展样式

DOM扩展 - 插槽机制

通过插槽机制让用户能够在不修改组件源码的情况下扩展组件的DOM结构。

以一个表单标题组件(FormTitle)为例:

  • 基础场景:标题前有个绿色竖线
  • 特殊场景:需要在标题旁添加提示图标

错误做法:

  • 添加一个属性控制是否显示图标
  • 导致组件与具体业务耦合
  • 违背通用组件设计原则

正确做法:

  • 提供插槽供用户扩展
  • 保持组件的通用性
  • 给插槽设置默认内容提升易用性

逻辑扩展 - 钩子函数

通过预留钩子函数,让用户能够介入组件的处理逻辑。

以一个通用表格组件(CommonTable)为例:

初始设计:

    <CommonTable api="/api/v1/user"/>

问题:只支持固定的API格式和返回值结构

改进方案1 - 数据转换钩子:

    <template>
        <CommonTable api="/other-api/user" :parseData="parseData" />
    </template>
    <script>
    export default {
        methods:{
            parseData(data){
                //对数据格式进行处理
                return data;
            }
        }
    }
    </script>

作用:处理不同格式的返回数据

改进方案2 - 函数式配置:

    <template>
        <CommonTable :api="getData" />
    </template>
    <script>
    export default {
        methods:{
            getData(pageNumber, pageSize){
                return request('/other/user',{
                    params:{
                        num: pageNumber,
                        size: pageSize
                    }
                }).then(data=>{
                    //数据处理
                    return data
                })
            }
        }
    }
    </script>

作用:完全自定义数据获取逻辑,更灵活地处理各种接口情况

样式扩展 - 自定义样式

组件除了扩展DOM和逻辑外,通常还需要支持样式扩展,可通过接收用户传递的style或class实现。例如,ElementUI的Popover组件可通过popover-class属性设置弹窗内容的类名。

最佳实践:

  1. 支持传入自定义class和style
  2. 避免使用!important
  3. 通过合理的CSS优先级控制样式
  4. 预留样式变量供用户定制

3. 易用性

易用性解决的是组件"好不好用"的问题,特别是对新手来说尤为重要。一个易用性好的组件应该具备以下特点:

傻瓜式使用

  • 用最简单的方式解决问题
  • 降低使用门槛
  • 减少学习成本

实际案例: vue-office

问题背景: 文件预览需要写大量代码、配置复杂、新手使用困难

解决方案:

    <!-- 一行代码实现文件预览 -->
    <vue-office-docx src="file.docx"/>

成功原因:

  • 极简使用方式
  • 封装复杂性
  • 提供完整解决方案

合理的默认值

  • 分析使用频率
  • 常用场景零配置
  • 特殊场景再配置

示例:按钮组件

    <!-- 基础使用 - 零配置 -->
    <Button>确定</Button>

    <!-- 特殊场景 - 按需配置 -->
    <Button 
      type="primary"
      size="large"
      :loading="true"
    >
      提交
    </Button>

符合用户习惯

命名规范,遵循主流组件库的命名习惯,直观、一致、可预测

    <!-- 好的命名示例 -->
    <Dialog :visible="true"/>  // 与ElementUI保持一致
    <Modal v-model="show"/>    // 与AntDesign保持一致

    <!-- 不好的命名示例 -->
    <Dialog :isHide="false"/>  // 不符合常见习惯
    <Modal :popup="true"/>     // 命名不够直观

总结要点

  1. 简单优于复杂
  2. 默认值优于配置
  3. 习惯优于创新

最佳实践

  1. 组件文件结构:
    components/
      ├── common/          # 通用基础组件
      │   ├── Button/
      │   └── Input/
      ├── business/        # 业务组件
      │   ├── UserForm/
      │   └── OrderList/
      └── layout/          # 布局组件
          ├── Header/
          └── Footer/

2. 组件命名规范:

    // 组件名使用PascalCase
    export default {
        name: 'SearchForm',
        // ...
    }

3. 提供完整的文档:

    // 组件props文档
    export default {
        props: {
            /**
             * 按钮大小
             * @values small, medium, large
             * @default medium
             */
            size: {
                type: String,
                default: 'medium',
                validator: value => ['small', 'medium', 'large'].includes(value)
            }
        }
    }

如何提升组件的可读性和正交性,以及组件开发应该遵循怎样的流程,咱们下一章再聊聊。