原文地址:
vue 单文件探索
以 vue 作为开发技术栈的前端开发者,往往会配合前端构建工具,进行项目的工程化管理。比如,大家常用的 vue 全家桶 + webpack 的方案进行一些中大型前端项目的开发。配合 webpack 后,vue 的组件化优势更加明显,我们可以通过单文件的组件化开发方式,在工作实践中搭建前端页面,从而提高开发效率。有这样一个问题:“当我们在写 vue 单文件时,我们在写什么?” 很多人可能会这样回答:template 负责模板,javascript 负责逻辑,style 负责样式。当回答到这里时,一个 vue 开发者的世界观基本上算是很明确了。我们要做的就是在一个单文件组件中写 template、javascript、style。如果仅仅局限于此,显然我们无法从更好的利用的单文件组件服务我们的整个开发流程。接下来我将和大家讨论在 vue 单文件开发中的一些方法论的问题。
vue 单文件本质
vue单文件是以特定文件扩展名 .vue
命名的文件。如下所示的代码:
ListDemo.vue
<template>
<div class="list-demo">
<ul>
<li v-for="item in list" :key="item.key">{{item.value}}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'ListNav',
data() {
return {
list: [
{ key: 'home', value: '首页' },
{ key: 'category', value: '文章分类' },
{ key: 'tags', value: '标签' },
{ key: 'about', value: '关于我' },
{ key: 'links', value: '友情链接'},
],
};
},
};
</script>
<style>
.list-demo {
font-size: 14px;
}
</style>
代码中含有 template,script,style。三者的作用此处就不在赘述,如上的结构展示了一个 vue 单文件基本的文件结构。其背后的理念就是一个单文件组件对应了一个功能性组件,该组件的模板,样式,业务逻辑都采用就近维护的思想。从组件的复用性,后期可维护性的角度上来说,这样的理念都大大的提高了组件化的开发效率。vue 的单文件,既不是 js,也不是 html,也不是 css 文件,这样的文件如何被应用到页面上,这也就是下面将会说到的一个问题,vue 单文件是如何被处理成页面中可用的资源。
vue 单文件被处理的流程
vue 单文件配合 webpack 构建工具,在 webpack 中会交由 vue-loader 来处理。如下所示:
{
test: /\.vue$/,
loader: 'vue-loader',
}
项目中通过 import 或者 require 引入的 vue 单文件,都会经过 vue-loader 处理,vue-loader 在这个过程中会将模板按照 template、script、style 解析并将处理结果返回,三种不同类型的文件交由接下来的loader 进行处理。如果该单文件组件在父组件中的 components 声明,则 components 中对应的该项会被插入解析后 script 代码。这个过程从入口文件 main.js
开始,所有涉及的被依赖单文件组件依次经历这样的处理过程。之后所有的组件的实例化将根据业务逻辑中的依赖关系进行,这个过程也是我们平时在开发中经常用到的一种方式。(这里可以单拉一篇文章详细讲述 vue-loader 的处理流程)
单文件的常用姿势
模板中的组件引用
一、使用方式
组件的拆分和嵌套:
- 将具体的业务按照功能以及后期复用性方面的考虑划分成更小的组件
- 通过一个容器组件(父组件)将小的功能组件(子组件)进行整合
操作手法:父组件中引入子组件,components 中注册,template 中添加相应的组件引用模板
这种方式也是我们在进行单文件的开发中常用的一种方式,所有组件的实例化,都被隐含在组件的嵌套关系和业务逻辑中。开发者只需要关心组件的引入,在父组件逻辑中注册该组件,并在父组件的模板中以标签的方式引入组件。这个过程中待引入的组件的实例化时机也可以通过 v-if 指令在业务逻辑中进行控制。
二、适用场景
大部分场景下我们都可以通过这样的方式进行组件化的开发。这种模式的有一个特点: 组件的引入通过组件注册和模板中写入对应的组件的标签来完成。模板中通过标签来引入组件这一步必不可少,这个特点在某些业务场景下可能给开发者带来了一定的重复工作量。
API 式的调用
API 式的调用指的是手动创建子组件的实例,业务逻辑中无需引入组件和模板标签占位,在暴露的 API 中控制组件的实例化与显示。
一、使用方式
- 功能模块提供一个入口 js 来控制该功能模块下单文件实例的所有功能逻辑
- 其他组件中使用该功能模块时,调用功能模块下的 js,传入部分参数
操作手法:
Confirm.vue
<template>
<el-dialg
title="test"
:visible.sync="visible">
{{content}}
<el-button @click="handleCancelClick">cancel</el-button>
<el-button @click="handleOkClick">ok</el-button>
</el-dialg>
</template>
<script>
export default {
name: 'Confirm',
data() {
return {
visible: false,
content: '这是一个confirm dialog',
callback: null,
};
},
methods: {
handleCancelClick() {
this.callback('cancel');
},
handleOkClick() {
this.callback('confirm');
},
},
};
</script>
confirm.js
import Vue from 'vue';
import Confirm from './confirm';
const ConfirmConstructor = Vue.extend(Confirm);
const confirm = (content) => {
let confirmInstance = new ConfirmConstructor({
data: {
content,
},
});
confirmInstance.vm = confirmInstance.$mount();
confirmInstance.vm.visible = true;
// 手动插入目的 dom
document.body.appendChild(confirmInstance.vm.$el);
confirmInstance.vm.callback = action => {
return new Promise((resolve, reject) => {
resolve(action);
});
};
return confirmInstance.vm;
};
如上所示,给出的是一个确认弹框的场景实现。确认弹框在很多用户交互中是一个必须的交互形式。很多组件库也采用上面这种 API 式的组件调用。调用方仅仅通过 api 的调用,就能实现该功能模块的引用。这样就避免了在 template 中通过标签占位的方式引用。实现原理就是手动接管单文件组件的实例化,通过 Vue.extend 获得该组件对应的 Vue 的子类,在暴露给调用的 api 中去实例化这个组件。这个过程中我们可能还要完成一些组件数据的注入,逻辑相关以及手动将该组件插入到目的 dom 中。手动的注入 dom 是该种方式的一个很大特点,通过在 api 中动态的注入目的 dom,避免我们在各个业务组件中调用该功能模块时重复性的在业务组件 template 中手写组件标签。
二、适用场景
- 功能聚合度高,组件内逻辑简单,输入输出较为单一,比如一些功能较为独立的弹框
- 一些特殊的自定义指令开发,比如在一些特殊场景的指令,可以复用一些单文件组件,通过在指令的钩子中实例化组件对应的 vue 子类,按照特定的逻辑插入到目的 dom 中(例如:element-ui的v-loading)
区别和共性
共性:通过实例化对应组件完成组件的功能逻辑
区别:实例化的时机和方式不同。模板式的引入通过组件注册和标签引入的方式来使用单文件组件。标签引入解决了子组件插入的 dom 位置问题,开发者无需关心。API 式的单文件组件使用,在 API 调用时手动实例化组件,需要手动控制插入到目的 dom。
总结
vue 的单文件组件提供了 vue 的组件化开发思路,其本质在导出 vue 的一些关键属性,比如生命周期函数,methods,computed, watch,props等。我们可以通过上述两种方式来使用单文件组件,目的在于工程内部尽量减少重复的模板代码,组件解耦。