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之间的调用问题。


geekartt
14 声望3 粉丝

Let's geek and art.