Vue的结构
前端的开发由三部分构成:
- HTML(模板template)
- 样式(CSS)
- 逻辑(js)
vue
便是按照这样的划分来整个三部分。更精妙的整合,源自于引入 单组件文件 .vue
,它能够在一个component中整合这三部分,进而把它当做一个微小的unit,供其他地方使用。这里涉及了非常微妙的平衡之道:
- 如果 display template、display style、data logic 都是由同一门语言完成,那就根本没有这些复杂的事情了,所谓的整合和component,不过就是一个提取出来的函数、或者一个提取出来的公共类。
- 将这三部分分离开来,我想最开始的想法是为了解耦,即让三个不同职能的部分,由三种不同类别的文件来负责。
- 但显然,这样做不容易做更好的权限控制与模块控制,动不动就是global,造成各种namespace的冲突和不同模块之间无权限的互相影响。
- 所以需要整合。但很微妙地将小范围的三套件组合到一起。这充分体现了,「分离思想」不等同于「分离文件」。做好的分隔,是在逻辑上做恰当的分隔,而不是在文件上做分隔。
- 提取出component的概念,充分展示了创作者精深的分离思想,将耦合的切割做到恰到好处。
- 通过使用
vue-cli
可以在template、style部分分别设置相应的预编译工具,如template的pug、style的scss或less。通过vue-cli
这个脚手架将这些模块化的工具整合到一起,能够快速提高你的开发效率,极大地增强代码的可维护性和健壮性。
目录结构:vue
这个单组件文件,通常会把template(HTML)、script(js)、style(css)放到一起。但这不一定是一个好的practice。更好的方式是把css单独提取成一个文件,进而通过import的方式将其引入到 .vue
文件中。
.
├── index.scss
└── index.vue
这么做的理由是,不同于HTML的少量代码,css的代码量通常会急剧膨胀,而这就会影响可读性。将css和vue文件放于同一个目录,保证了它们作为一个整体的完整性,同时为后期维护css和debug提供了方便:可以根据视图快速定位到相应的位置。
我们一般根据视图区域的划分来提取component,每个视图区域单独命名为一个文件夹,其 src
的目录结构如下:
.
├── App.scss
├── App.vue
├── assets
│ ├── logo.png
│ └── style.scss
├── main.js
└── pages
└── first_page
├── components
│ ├── query-nav
│ │ ├── index.scss
│ │ └── index.vue
│ └── res-content
│ ├── index.scss
│ └── index.vue
├── index.scss
├── index.vue
└── utils
├── gen_test_data.js
└── first_page_util.js
vue单组件文件示例:
<!------ html ------->
<template lang=pug>
#app
#vue-learning
ol
li(v-for="todo in todos") {{ todo.text }}
span(v-bind:title="message") Hover your mouse over me for a few seconds
p
span(v-if="seen") Now you see me.
p {{message}}
button(@click="reverseMessage") Reverse Message
input(v-model="message")
ol
todo_item(v-for="item in groceryList" :todo="item" :keyc="item.id")
</template>
<!------ js ------->
<script>
import todo_item from "./components/todo-item";
export default {
name: "hello",
components: {
todo_item
},
data() {
return {
msg: "Welcome to Your Vue.js App",
message: "Hello Vue.js!",
tt: "myTT",
seen: true,
todos: [
{ text: "Learn JavaScript" },
{ text: "Learn Vue" },
{ text: "Build something awesome" }
],
groceryList: [
{ id: 0, text: "Vegetables" },
{ id: 1, text: "Cheese" },
{ id: 2, text: "Whatever else humans are supposed to eat" }
]
};
},
methods: {
reverseMessage: function() {
this.message = this.message
.split("")
.reverse()
.join("");
}
}
};
</script>
<!------ css ------->
<style lang="sass">
@import './App.scss'
</style>
可以看到,在script部分,我们会export default一个对象。这个对象里定义了vue所需要的所有代码,并且分门别类地放好:
- data
- methods
- components
- props
- created
- mounted
- …
从这种角度看,这样的结构为每部分的代码做好分类,能够更加清晰地构建复杂工程。
component之间的消息传递
Parent/child component的关系可以总结为:props down, events up. 父组件通过 props 向下传递数据给子组件;子组件通过 events 给父组件发送消息。
当我们把整个项目按照视图区域分解为各个component后,各个component就相当于是一个孤岛或node。如果一个component的操作需要调用另外一个的方法时,就需要相应的消息传递机制。从这个角度讲,引入component之后的前端开发,其实同后端开发就没有多大区别了。你需要在微观考虑每个component的实现细节,在宏观给出合适的架构来高效整合component之间的通信。至于作为展示内容的骨架(HTML)和内容的样式(CSS)都已经被完全分离出来了,你只需要集中精力使用js将相应的交互逻辑、数据流逻辑实现即可。不得不说component的引入,让曾经混乱不堪、类似汇编语言的前端开发一下子从原始时代推进到了蒸汽时代。
component之间的调用,可以通过event-bus,或者通过将信息传递到parent component来统一完成。个人更加偏好后一种方式,因为它逼迫你对整个工程做出更加清晰的逻辑拆分,否则你很难实现通过parent component来统一完成对各个子组件的消息分发。
parent component 使用 :foo="foo_val"
( v-bind:foo="foo"
的简写方式) 将parent的foo_val传递给child component。对应的完成pug语句可以写为:childComp(:foo="foo_val")
。注意到,此时parent component的data部分并不需要做任何设置。
但更好的方式,是将这个foo_val的值设置在parent component中,如此child component所接收到的foo值就能够随着parent component中的foo_val的值的变动而变动,也即是实现了动态绑定。
父组件代码:
<template lang=pug>
div(class="par-container")
childComp(:foo="foo_val")
</template>
<script>
import childComp from "./components/childComp/index";
export default {
...
data: () => {
return {
foo_val: []
};
},
components: {
childComp
}
}
</script>
<style lang=scss>
@import "./index.scss";
</style>
注意,这里之所以在最外层定义一个 par-container
,是因为在template中只允许有一个root node。所以通常我们在template中做的第一件事,便是定义一个根节点container。
相应的子组件代码:
<template lang=pug>
div(class="child-container")
...
table
tbody
template(v-for="task in foo")
tr
td {{task.id}}
td {{task.name}}
</template>
<script>
export default {
...
props: {
foo: {
type: Array,
default: () => []
}
}
}
</script>
<style lang=scss>
@import "./index.scss";
</style>
- 子组件只需要在
props
中声明好接受的参数。 - 子组件可以直接使用这个声明好的变量。
如此,父组件便把自己所控制的一个数据,传递到了子组件,进而子组件的视图便和父组件的这个数据动态绑定上了。这也就是所谓的props down。
再来是events up,也即是子组件触发父组件的一个事件。它的使用场景是:子组件所控制的视图区域有一个交互事件想要改变某个数据的值,但这个值却定义在父组件中。于是,就必须通过event emit的方式来触发父组件的某个事件,进而修改对应的数据。
首先父组件需要将对应的事件传递给子组件,同上述的 :foo="foo_val"
类似,使用 @bar="bar_func"
来传递:
<template lang=pug>
div(class="par-container")
childComp(:foo="foo_val" @bar="bar_func")
</template>
<script>
import childComp from "./components/childComp/index";
export default {
...
data: () => {
return {
foo_val: []
};
},
methods: {
bar_func: function(bar_param) {
// ...
}
},
components: {
childComp
}
}
</script>
<style lang=scss>
@import "./index.scss";
</style>
子组件则是通过 this.$emit()
方法来直接触发。通常,我们还会在子组件中再定义一个函数来表示对应事件的相应,进而在这个相应事件的函数中,再调用 this.$emit()
方法来实现对父组件事件的触发:
<template lang=pug>
div(class="par-container")
...
table
tbody
template(v-for="task in foo")
tr
td {{task.id}}
i(@click="change_bar")
td {{task.name}}
</template>
<script>
export default {
...
props: {
foo: {
type: Array,
default: () => []
}
},
methods: {
change_bar: function() {
let change_val = "";
// ...
this.$emit('bar', change_val);
}
}
}
</script>
<style lang=scss>
@import "./index.scss";
</style>
可以看到,这里的 change_bar
是子组件中click的响应事件函数。在它的实现中,我们调用了从父组件注册进来的事件 bar
。
有了 props down 和 events up ,我们便能自由地在component之间交换数据。这里要注意整个逻辑拆分和项目设计的原则:
- parent component应该控制核心的数据结构,而子组件只是负责对核心数据的展示(通过props来动态绑定)。即:类似于调用getter。
- 子组件所触发的对核心数据的修改,都应该触发相应的parent component事件来完成,而不是在子组件里做修改。即:调用的setter应该由parent component来提供。
- 之所以在父组件中写成使用闭包来构造
data()
部分的数据,就是为了防止子组件会对它做修改,从而不得不依靠父组件的事件方法来修改数据。如此就控制父组件中核心数据的访问控制权限,使其具备更完整的封装性。
至于vue的其它诸如双向绑定、computed、created、mounted等有用的hook方法,都可以在相应文档中找到使用方法,没有本质的难度。因为这些方法都是在一个单独的vue component中使用,而不必考虑component之间的调用问题。