从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(三)

8,078 阅读14分钟

这篇文章中,我们将讲解 Vue 实例的 Props 和 Methods,接着我们又讲解了最常见的 Vue 模板语法,并通过实例的方式将这些模板语法都实践了一番,最后我们讲解了 Vue 组件的组合,并完成了我们的发表商品页面。

欢迎阅读《从零到部署:用 Vue 和 Express 实现迷你全栈电商应用》系列:

如果您觉得我们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励我们写出更好的教程💪

用模板语法和双向绑定实现数据的添加

当我们完成了商城应用的基本页面框架之后,我们就可以开始考虑具体页面的内容了。首先我们要考虑的就是数据的来源,即添加商品页面。有了添加商品的入口,我们就可以展示商品列表,获取商品详情,甚至是修改商品信息。

不过在此之前,我们打算先复习一下 Vue 的一些重要知识点。如果你已经很熟悉了,可以直接跳到下面实现 ProductForm.vue 的代码部分。

Vue 实例:Props 和 Methods

Props

props 是 Vue 进行组件之间传参的形式。比如我们有如两个组件 New.vueProductForm.vue,在 New.vue 组件中需要使用到 ProductForm.vue 组件。其中 New.vue 组件是用来创建商品的,它的代码大致是这样的:

import ProductForm from '@/components/ProductForm.vue';

<ProductForm :manufacturers="manufacturers" />

它需要给 ProductForm.vue 组件传递一个 manufacturers 属性,以确保我们在创建商品时,可以选择这个商品所属的制造商,接着我们就可以在 ProductForm.vue 中的 props 中取到这个 manufacturers 属性。ProductForm.vue 的代码大致是这样的:

<template>
<!-- 模板部分 -->
</template>

<script>
export default {
  props: ['manufacturers'],
}
</script>

可以看到,我们在 ProductForm.vuescript 部分导出的对象里面找到 props 属性,然后取到 manufacturers 属性。

Methods

然后是 methodsmethods 是用来定义在组件中会用到的一些方法,如果说我们前面提到的 data ,是从数据从逻辑层(JS)向视图层(Html)流动的话,那么这里的 methods 就是视图层触发事件,如 click、submit等,反过来修改逻辑层的数据的方法,methods 使得数据可以双向流动。

让我们在完善一下我们的 ProductForm.vue ,看一下 Methods 在 Vue 中是如何运作的:

<template>
<form @submit.prevent="saveProduct">
<!-- 其他表单,如 input 等 -->

<div class="form-group new-button">
  <button class="button">Add Product</button>
</div>
</form>
</template>

<script>
export default {
  data: { isSaved: false },
  props: ['manufacturers'],
  methods: {
    saveProduct() {
       this.isSaved = true;

      // 完成一些保存创建商品的逻辑 ...
    }
  }
}
</script>

可以看到,我们可以通过在 template (视图层)通过点击提交按钮,发起表单提交事件,进而调用在 script 中定义在 methods 属性中的 saveProduct 方法,这个方法可以进一步修改定义在定义在 data 属性中的数据;甚至如果父组件 New.vue 传递了方法(以 props 的形式)给 ProductForm.vue 组件,我们可在 saveProduct 调用这个传递下来的方法,进而可以影响到父组件 New.vue 中的数据。我们将在后面的正式实现 ProductForm 组件时讲解到它。

模板语法:v-on

接下来我们再来谈一谈 v-bind 和 v-on 。

在 Vue 中,我们通过 v-on 的方式接管了之前在 HTML 中 onEvent

比如之前我们在 HTML 中的写法是这样的:

<div onclick="alert('I love tuture')">
Hello Tuture
</div>

现在在 Vue 的模板语法中我们需要写出这样:

<div v-on:click="alert('I love tuture')">
Hello Tuture
</div>

类似的 onEvent 都要改成 v-on:Event。然后这样写显得比较冗余,所以 Vue 支持简化写法,用 @ 替换 v-on: 部分,我们就可以写出这样:

<div @click="alert('I love tuture')">
Hello Tuture
</div>

调用事件之后我们一般有一些这样的操作,比如禁用浏览器默认行为,然后自己去处理事件,获取后端数据,以前我们会这样写:

<div onclick="saveProduct()">
Hello Tuture

<script>
var saveProduct = function (e) {
  e.preventDefault();

  // do something you like
}
</script>

但是这样写又显得特别繁琐了,Vue 也觉得这样可以简化,于是我们直接将这些禁止默认行为的调用作为绑定事件的属性来进行处理,于是乎在 Vue 中我们可以写出这样:

<template>
  <div @click.prevent="saveProduct">
  Hello Tuture
  </div>
</template>

<script>
export default {
  methods: {
    saveProduct() {
      // do something you like
    }
  }
}
</script>

不知道看了上面的长文,你有没有一点晕,不管你晕不晕,我是得喝口水缓一下。 - v -

模板语法:v-bind

我们已经看到在 Vue 模板中我们可以使用如下的功能:

  • {{}} 插值语法将 data 渲染到 HTML 元素内容中
  • v-on 或者简化写法 @ ,等用来取代 HTML 的事件绑定

有了上面的功能,我们可以让 HTML 动起来了,但是还缺点什么,比如我们的 HTML 属性,如 idclass 等,是不是也能动态的获取变化值,你还别说,还真的可以,Vue 模板语法为我们提供了 v-bind 用于动态绑定属性值,我们来看个例子:

<template>
<option v-bind:id="_id"  v-bind:value="value" />
</template>

<script>
export default {
  data: { _id: '1', value: "Xiaomi" },
}
</script>

可以看到,我们在 script 中导出的对象属性 data 中,定义了 _idvalue 值,然后我们通过在 <template> 模板中使用 v-bind 语法动态的给 option 标签的 idvalue 属性赋值,最后的结果看起来是这样的:

<option id="1" value="Xiaomi" />

当然,当需要绑定的属性多了,每次都写 v-bind 显得相当繁琐,所以 Vue 为我们提供了 v-bind 的简洁语法 :,即我们之前的绑定语法从 v-bind:id="_id" 变成了 :id="_id"

上面的代码用简洁语法改写如下:

<template>
<option :id="_id"  :value="value" />
</template>

<script>
export default {
  data: { _id: '1', value: "Xiaomi" },
}
</script>

模板语法:v-model 双向绑定

前面我们提到通过 {{}} 插值语法渲染来自 data 的数据实现了逻辑层向视图层的数据流动,通过 methods 在视图层操作逻辑层的数据,实现了视图层的数据向逻辑层的数据流动,从而达到了双向绑定,当我们的应用越来越复杂,我们会发现这样的数据双向流动会越来越频繁,而且粒度也会大小不一,有很多单纯修改某个值的方法调用就会显得特别繁杂,因此 Vue 通过提供 v-model 进行了视图层和逻辑层的双向绑定,让我们来看个例子:

<template>
<!-- 其他代码 ... -->
<input
  type="text"
  placeholder="Name"
  v-model="name"
/>
<!-- 其他代码 ... -->
</template>

<script>
export default {
  data: { name: 'ProductForm' },
}
</script>

这里我们通过申明 v-mode 将此 input 的值和我们在 Vue 实例中的 modelname 属性进行了双向绑定,即当 data 中的 name 发生变化,input 的值也会跟着变化,当 input 的值发生变化,我们 data 中的 name 的值也会被修改,这一切都是自动发生的,不需要我们额外的添加 methods 里面的方法调用来手动修改。

模板语法:循环

好了,Vue 替我们接管了 HTML 元素属性值、事件处理、元素内容,这些都还只属于原来 HTML 的部分,它更强大的一点就是将 JS 的功能引入了模板语法中,使得我们可以实现类似循环,条件选择操作等功能。

接下来我们先来看一下 Vue 为我们提供的 “循环” 模板语法, 它使得我们可以快速渲染大量具有相似结构的数据,比如渲染一个数组的数据,生成一个 HTML 元素列表,这在我们平时看到的新闻 App 里面很常见,我们浏览新闻时,发现其实每条新闻的结构都很相似,并且有很多条新闻(可能多大几百上千条),如果每一条我们都手动写 HTML 代码的话,无疑显得相当繁琐,并且数据一多,我们手动就显得无能为力了,而 Vue 为我们提供的 “循环” 模板语法,使得我们可以通过非常简单的写法就可以渲染大量数据,我们来看个例子:

<!--
manufacturers = [
  { _id: 1, name: 'Apple' },
  { _id: 2, name: 'Xiaomi' }
]

model = { _id: 1, name: 'Apple' }

 -->
<template v-for="manufacturer in manufacturers">
  <option :value="manufacturer._id" :selected="manufacturer._id == model._id">{{manufacturer.name}}</option>
</template>

最后渲染的结果为:

<option value="1" selected="true">Apple</option>
<option value="2" selected="false">Xiaomi</option>

注意到,如果我们在写 “循环” 语法时,使用了一个额外的标签 template 来包裹我们需要渲染的 HTML 元素,这也是 Vue 推荐的写法;我们在 template 标签的属性上添加 v-for 然后给它赋值 "manufacturer in manufacturers",通过这样的形式进行列表数据的遍历,每次从 manufacturers 中取一个元素,并赋值给 manufacturer ,然后我们就可以在 option 标签中使用 manufacturer 和我们定义的 model 进行比较。

因为我们的 model._id1,它和 manufacturers 数组的第一项元素的 _id 一致,所以我们返回的两个 option 标签,第一个 selected 属性为 true,第二个为 false

模板语法:条件选择

上面的讲述了循环是如何在 Vue 中使用的,下面我们来看一看条件语法是如何在 Vue 中使用的:

<span v-if="isEditing">Update Product</span>
<span v-else>Add Product</span>

<script>
export default {
  data: { isEditing: false },
}
</script>

我们可以看到,通过在标签上加 v-if 并后面紧跟加 v-else 的标签我们可以判断最终渲染的标签,比如我们这里 isEditingfalse,那么我们最终渲染的结果为:

<span>Add Product</span>

当然你可以添加诸如 v-else-if 的标签来做多重判断。

提示

这里的带 v-ifv-else-ifv-else 的标签需要依次紧跟着前面的标签,不能在这些带条件属性的标签中插入其他不带条件的标签,比如下面这段代码就是错误的:

<span v-if="isEditing">Update Product</span>
<span>我是错误插入的标签</span>
<span v-else>Add Product</span>

<script>
export default {
  data: { isEditing: false },
}
</script>

动手实现

讲解完 Vue 的基础知识之后,我们马上将所有的知识运用起来,来编写我们的 ProductForm.vue 组件,它用来添加或者更新商品的信息。

我们在 src/components 中创建 ProductForm.vue 表单组件,代码如下:

<template>
  <form @submit.prevent="saveProduct">
    <div class="col-lg-5 col-md-5 col-sm-12 col-xs-12">
      <div class="form-group">
        <label>Name</label>
        <input
          type="text"
          placeholder="Name"
          v-model="model.name"
          name="name"
          class="form-control" />
      </div>
      <div class="form-group">
        <label>Price</label>
        <input
          type="number"
          class="form-control"
          placeholder="Price"
          v-model="model.price"
          name="price" />
      </div>
      <div class="form-group">
        <label>Manufacturer</label>
        <select
          type="text"
          class="form-control"
          v-model="model.manufacturer"
          name="manufacturer">
          <template v-for="manufacturer in manufacturers">
            <option :value="manufacturer._id" :selected="manufacturer._id == (model.manufacturer && model.manufacturer._id)">{{manufacturer.name}}</option>
          </template>
        </select>
      </div>
    </div>

    <div class="col-lg-4 col-md-4 col-sm-12 col-xs-12">
      <div class="form-group">
        <label>Image</label>
        <input
          type="text"
          lass="form-control"
          placeholder="Image"
          v-model="model.image"
          name="image"
          class="form-control" />
      </div>
      <div class="form-group">
        <label>Description</label>
        <textarea
          class="form-control"
          placeholder="Description"
          rows="5"
          v-model="model.description"
          name="description"
         ></textarea>
      </div>
      <div class="form-group new-button">
        <button class="button">
          <i class="fa fa-pencil"></i>
          <!-- Conditional rendering for input text -->
          <span v-if="isEditing">Update Product</span>
          <span v-else>Add Product</span>
        </button>
      </div>
    </div>
  </form>
</template>

<script>
export default {
  props: ['model', 'manufacturers', 'isEditing'],
  methods: {
    saveProduct() {
      this.$emit('save-product', this.model)
    }
  }
}
</script>

这段代码看起来很长,你可能被吓到了,让我们一段一段来拆解它。

script 部分

这里我们的 props 接收来自父组件的三个参数:modelmanufacturersisEditing

然后我们定义了一个 saveProduct 方法,就是当用户填写完商品信息的表单之后,点击提交按钮会触发的方法,在 saveProduct 内部,我们调用了父组件的 save-product 方法,并把修改后的 this.model 变量内容传给父组件。所以这里我们还可以看到,methods 不仅可以使得数据可以双向流动,而且还可以在子组件反向操作父组件的内容,使得数据还可以上下流动。

template 部分

接下来我们再来谈一谈 template 里面发生的事情。

可以看到 template 里面就是一个表单,这个表单定义了一个 submit 事件,并且使用了禁用默认事件的简洁写法 @submit.prevent。 这个事件触发会调用我们上面提到的 saveProduct 方法。

接着我们定义了好几个 classform-group 的元素块,每个块代表我们创建商品所需要填写的相关信息,我们注意到,前两个 form-group 使用 v-model 双向绑定语法分别绑定了 modelnameprice 属性。

第三个 form-group 我们首先在 select 标签中使用 v-model 双向绑定了 model.manufacturer,表示我们在视图里面进行选择时,会修改对应的 model.manufacturer 属性。

接着我们对 manufacturers 进行循环遍历,构造多个 option 标签选项,然后使用了属性绑定语法的简洁写法绑定了 optionvalueselected 属性,value 属性赋值为 manufacturer._idselected 属性会进行判断,model.manufacturer && model.manufacturer._id 表示首先检验 modelmanufacturer 属性是否存在,正常情况下它应该是一个对象,如果 model.manufacturer 属性存在,那么获取 model.manufacturer._id,然后用获取到的这个 model.manufacturer._idmanufacturer._id 进行比较,如果一致,那么 selected 属性为 true,不一致就为 false

然后我们来看一下第二段 form-group,也就是第 4-6 个 form-group

可以看到前两个 form-group 使用 v-model 双向绑定了 model.imagemodel.description ,表示当用户上传了商品图片和描述之后,对应的 model.image 就会变成用户上传的商品图片,model.description 就会变成用户撰写的商品描述。

最后一个 form-group 我们使用了条件选择语法,判断 isEditing,来渲染不同的按钮文案。

Vue 组件组合

编写完上面的表单之后,我们在 New.vue 中引入我们创建的表单组件:

<template>
  <product-form
    @save-product="addProduct"
    :model="model"
    :manufacturers="manufacturers"
  >
  </product-form>
</template>

<script>
import ProductForm from '@/components/products/ProductForm.vue';
export default {
  data() {
    return {
      model: {},
      manufacturers: [
        {
          _id: 'sam',
          name: 'Samsung',
        },
        {
          _id: 'apple',
          name: 'Apple',
        },
      ],
    };
  },
  methods: {
    addProduct(model) {
      console.log('model', model);
    },
  },
  components: {
    'product-form': ProductForm
  }
}
</script>

当一个组件要在模板语法中使用另外一个组件时,需要申明此组件,即在组件的 components 属性中申明要使用的组件,比如我们上面使用名为 'product-form' 的名称来申明使用 ProductForm 组件,这样在 template 中我们就可以以 <product-form /> 形式使用导入的表单组件。

同时我们在组件的 data 中定义了 modelmanufacturers 以及在 methods 中定义了 addProduct 方法,并将它们以属性绑定 :model="model":manufacturers="manufacturers" 和事件绑定 @save-product="saveProduct" 的方式传递给表单组件。

当保存上面编写的代码之后,我们打开浏览器,点击导航链接 Admin,然后点击子导航链接 New Products,切换到我们的 New.vue 添加商品页面,我们可以看到如下的效果:

小结

到现在为止,我们已经了解了 Vue 的基础部分,包括:

  • 用路由进行多页面的跳转和导航
  • 用嵌套路由和动态路由合理组织页面
  • Vue 组件和 Vue 实例
  • Vue 模板语法

掌握了这些知识后,我们已经可以实现很多前端的功能,完成一些简单的 Vue 应用了。但是如果要完成数据逻辑复杂的大型应用,目前学到的知识就力不从心了。但是没关系,我们将在后面学习 Vuex 这一前端状态管理工具,有了 Vuex 的加持,我们就能用 Vuex 写出任意复杂的应用了。

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。