前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue之学会编写可复用性模块 函数组件与插件三种粒度

首页2019-06-02 00:28:32Front-End
Vue组件化代码复用

同一段字符串处理,在模板里写了两遍;同一个 loading 遮罩,在五个页面各复制了一份;同一个确认弹窗,每次用都要先 import 再注册再声明一个 visible 变量。这些重复不会让你的代码跑不起来,但会在需求变更的时候集中爆发,改一处漏三处。Vue 项目里的复用其实有三个粒度,函数、组件、插件,各自解决不同大小的重复。这篇把这三层挨个讲清楚:怎么写、什么时候该往上升一级、以及每一级各自的坑。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 把重复的表达式封装成方法和过滤器,以及过滤器在 Vue 3 里的下场
  • 组件封装的两种形态,整体封装和用插槽做定制化封装
  • 用 install 把公共逻辑封装成插件,全局挂一个 $toast
  • 原文那份 toast 插件代码里的一个 bug,以及为什么它一直不显示
  • 三种粒度怎么选,以及组合式 API 出现后这套划分有什么变化

在 Vue 项目里,每一个页面都可以看作是由大大小小的模块构成的,一行代码、一个函数、一个组件都算模块。提高复用性的关键就在于把这些模块写得可复用。

# 一、封装成一个函数

开发过程中最常见的重复不是数据的重复读取,而是功能点的重复。比如下面这段:

<template>
    <div>
        <input type="text" v-model="str1">
        <input type="text" v-model="str2">
        <div>{{ str1.slice(1).toUpperCase() }}</div>
        <div>{{ str2.slice(1).toUpperCase() }}</div>
    </div>
</template>
@前端进阶之旅: 代码已经复制到剪贴板

原文这段的开标签写成了 tempalte,是个 typo,上面改回来了。

重复的功能点是「截取输入框第二个字符开始到最后的值,并转成大写」。这么短的表达式写两遍看着没什么,但只要它出现在模板里,就有三个隐患:一是逻辑分散,改规则要满模板找;二是模板里塞表达式会让这一行的可读性迅速下降,再加两个条件判断就没法看了;三是这段逻辑没法单独测试。

所以把它提成方法:

export default {
    methods: {
        sliceUpperCase(val) {
            return val.slice(1).toUpperCase()
        }
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

用到的地方调用一下,传值进去拿新值出来。

如果这个功能点只出现在双花括号插值和 v-bind 表达式里,Vue 2 还给了一个更贴合的选择,过滤器:

// 单文件组件注册过滤器
filters: {
    sliceUpperCase(val) {
        return val.slice(1).toUpperCase()
    }
}

// 全局注册过滤器
Vue.filter('sliceUpperCase', function (val) {
    return val.slice(1).toUpperCase()
})
@前端进阶之旅: 代码已经复制到剪贴板

然后在模板里用「管道」符调用:

<div>{{ str1 | sliceUpperCase }}</div>
<div>{{ str2 | sliceUpperCase }}</div>
@前端进阶之旅: 代码已经复制到剪贴板

过滤器的好处是读起来顺,从左到右就是数据的流向。金额格式化、时间格式化这类场景用它特别舒服。

这里必须补一句时效性:过滤器在 Vue 3 里被移除了,全局的 Vue.filter 和组件内的 filters 选项都没有了。官方给的迁移建议是改用计算属性或者直接调方法。原因也说得很直白,管道语法是一套 Vue 自己发明的、只在模板里成立的规则,学习成本和收益不成正比,而方法调用是标准 JavaScript,谁都看得懂。

所以老项目里的过滤器不用急着改,Vue 2 里它是好用的;但新写的代码,尤其是准备往 Vue 3 迁的项目,直接用方法或计算属性,别再新增过滤器了。

不管是过滤器还是普通方法,底下都是函数封装这一件事。函数是复用粒度里最小、也最该优先考虑的一级。一段逻辑如果不依赖组件的状态,也不产生 DOM 相关的副作用,就把它抽成纯函数放进 utils,连组件都不用碰。

# 二、封装成一个组件

比函数大一号的粒度是组件。组件包含模板、脚本和样式三部分,是 Vue 里复用的主力。项目中每个页面都可以看作一个父组件,里面包含很多子组件,子组件接收父组件的值来渲染,父组件通过响应子组件的回调来触发事件。

封装组件主要有两种方式。

第一种是整体封装,使用者通过改变数据源来呈现不同状态,代码结构不可定制:

<div>
    <my-component data="我是父组件传入子组件的数据"></my-component>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

这种封装最简单,也最容易翻车。翻车的方式永远是同一种:需求方说「这里能不能加个图标」,你加了个 showIcon prop;下次说「图标想换成头像」,你加了个 iconType;再下次说「这块想放一个按钮」,你加了个 extraButton。半年之后这个组件有二十个 prop,里面全是 v-if。

这就是第二种方式存在的理由,用插槽做自定义封装。开放一部分槽位给父组件,让它自己往里塞结构:

<div>
    <my-component data="我是父组件传入子组件的数据">
        <template slot="customize">
            <span>这是定制化的数据</span>
        </template>
    </my-component>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

组件内部用 slot 标签的 name 接住:

<div class="container">
    <span>{{ data }}</span>
    <slot name="customize"></slot>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

原文这段的结尾写成了 <div> 而不是 </div>,少了一个斜杠,上面补上了。

子组件里 slot 的 name 定为 customize,父组件在 template 上写 slot="customize",两边对上,父组件里那段结构就会被渲染到对应位置。不同的父组件可以塞不同的内容,实现差异化。最终渲染出来是这样:

<div>
    <div class="container">
        <span>我是父组件传入子组件的数据</span>
        <span>这是定制化的数据</span>
    </div>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

这里的插槽语法也要更新一下。slot="customize" 这种写在标签属性上的老语法从 Vue 2.6 起就被废弃了,取而代之的是 v-slot:customize,简写成 #customize。Vue 3 直接把老语法移除了,写了不生效。所以上面那段父组件的代码,现在应该写成 <template #customize>。

还有一种插槽值得提一句,作用域插槽。上面这种插槽传的是「结构」,父组件塞什么子组件就渲染什么,但父组件拿不到子组件内部的数据。作用域插槽解决的就是这个:子组件把数据绑在 slot 标签上,父组件用 v-slot 的值接住。表格组件的自定义列、列表组件的自定义 item 全靠它。插槽的三种形态和组件之间九种通信方式,我在 Vue组件化实践详解 里完整写过一遍。

封装好的组件,页面里 import 进来注册一下就能用,也可以全局注册:

import myComponent from '../myComponent.vue'

// 全局
Vue.component('my-component', myComponent)
@前端进阶之旅: 代码已经复制到剪贴板

全局注册用起来省事,代价是这个组件无论用不用都会被打进主包,而且 tree-shaking 拿它没办法。所以全局注册只留给真正每个页面都用的东西,其余的老老实实局部注册。

# 三、封装成一个插件

再往上一级是插件。有些公共逻辑,使用者根本不需要了解内部结构,只要知道调什么方法、传什么参数。loading、toast、确认弹窗都属于这一类,它们的共同点是「随手就要能调,不想为它在模板里预留标签」。

Vue 2 提供了 install 方法来编写插件,第一个参数是 Vue 构造器,拿到它就能给项目添加全局方法、资源和选项:

/* toast.js */
import ToastComponent from './toast.vue' // 引入组件

let $vm

export default {    
    install(Vue, options) {
        
        // 判断实例是否存在
        if (!$vm) {            
            const ToastPlugin = Vue.extend(ToastComponent); // 创建一个「扩展实例构造器」
            
            // 创建 $vm 实例
            $vm = new ToastPlugin({                
                el: document.createElement('div')  // 声明挂载元素          
            });            
            
            document.body.appendChild($vm.$el); // 把 toast 组件的 DOM 添加到 body 里
        } 
        
        // 给 toast 设置自定义文案和时间
        let toast = (text, duration) => {
            $vm.text = text;
            $vm.duration = duration;
            $vm.isShow = true;   // 原文漏了这一行,不置为 true 的话 toast 永远不显示
            
            // 在指定 duration 之后让 toast 消失
            setTimeout(() => {
                $vm.isShow = false;  
            }, $vm.duration);
        }
        
        // 判断 Vue.$toast 是否存在
        if (!Vue.$toast) {            
            Vue.$toast = toast;        
        }        
        
        Vue.prototype.$toast = Vue.$toast; // 全局添加 $toast 事件
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

这段代码里我改了一处。原文的 toast 函数只设置了 text 和 duration,然后开个定时器把 isShow 设成 false,但从头到尾没有把 isShow 设成 true。组件初始的 isShow 是 false,调用之后还是 false,结果就是调了没反应,页面上什么都不出现。这种 bug 很典型,代码逻辑通顺、控制台干净,就是没效果。

再拆解一下这段的几个关键点。

Vue.extend(ToastComponent) 是把一个组件配置对象变成可以 new 的构造函数。直接 new 一个组件配置对象是不行的,必须先 extend 一次。

el: document.createElement('div') 这一手是让实例挂载到一个还没进入文档的游离节点上。这样组件会正常完成渲染流程,DOM 就挂在 $vm.$el 上,之后我们自己决定 append 到哪里。这也是所有「脱离组件树的弹窗」的共同做法。

if (!$vm) 这个判断保证了全局只有一个 toast 实例,反复调用只是改文案,不会往 body 里堆一堆节点。

这里也有个坑要注意:连续快速调用两次 $toast,第一次的定时器还没到点,第二次又开了一个,第一个定时器到点时会把正在显示的第二条也关掉。真要做严谨的话,每次调用前先 clearTimeout 掉上一个,把定时器 id 存起来。原文这版没处理,用在低频提示上问题不大,但列表里循环触发就会看到闪烁。

写完插件脚本,在入口文件里用 Vue.use() 注册:

import Toast from '@/widgets/toast/toast.js'

Vue.use(Toast); // 注册 Toast
@前端进阶之旅: 代码已经复制到剪贴板
fe
  • 一、封装成一个函数
  • 二、封装成一个组件
  • 三、封装成一个插件
  • 四、三种粒度怎么选
  • 总结
  • 参考

← Vue之合理划分容器组件与展示组件Vue编码技巧与规范 九条能落到代码里的实践 →