乐趣区

关于vue.js:Vue学习笔记五-长乐无极

下一篇是长乐未央,其实是我记错了,将长乐未央、长毋相忘,记成了长乐无极,长乐未央。在汉代,长乐未央和长毋相忘,是两句罕用的祝福语,意思是短暂高兴,永远不要遗记对方。前面的主线就是通过一些例子来介绍 Vue 的一些罕用概念。以后这个例子仍然来自 mdn web docs。加上了我本人的了解。

进化: 组件列表

书接上回(有种章回体的感觉了,哈哈),上回咱们构建了一个待办组件,并借助 props 来实现父子组件之间进行通信。父子组件简略的说就是假如一个组件,咱们权且称之为 A,应用了另一个组件,咱们权且称之为 B。那么咱们称 A 为父组件,B 为子组件,也就是父子组件。然而咱们的待办通常不会只有一个,假如咱们有 10 个待办,咱们不会想将组件标签写 10 次,咱们会想到循环,没错这就是咱们前文中提到的 v -for 指令,首先咱们在 App.vue 中申明一个 data 函数,而后在函数中咱们返回的对象中蕴含一个数组。

data() {
    return {
      ToDoItems: [{ label: 'JavaFX 教程(一)', done: false },
        {label: '应用 JavaFX 打包一个平台包', done: true},
        {label: '写 C 语言教程', done: true},
        {label: 'SDL 库', done: false}
      ]
};

目前如同没问题,然而咱们并不想每次批改待办,都会将所有的待办都从新创立一遍,为了帮忙 Vue 优化列表中元素的出现,Vue 须要在 v -for 创立进去的元素有惟一的键,这些键最好是字符串或数值,这里咱们就能够抉择应用第三方库来产生惟一的键,也就是 lodash 包的 uniqueid()办法来产生惟一键。首先让咱们进行服务,而后再终端中输出以下命令:

npm install --save lodash.uniqueid
# 如果报 找不到 uniqueid
npm i --save-dev @types/lodash.uniqueid

而后在 app.vue 中引入这个这个办法,

import uniqueId from 'lodash.uniqueid';

而后咱们的 app.vue 就变成了上面这样:

<template>
  <div id="app">
    <h1>To-Do List</h1>
  <ul>
    <li v-for="item in ToDoItems" :key="item.id">
      <to-do-item :label="item.label"  :done="item.done" ></to-do-item>
    </li>
  </ul>
  </div>
</template>

<script>
import ToDoItem from './components/ToDoItem.vue'
import uniqueId from 'lodash.uniqueid'
export default {
  name: 'App',
  components: {ToDoItem},
  data () {
    return {
      ToDoItems: [{ id: uniqueId('todo-'), label: 'JavaFX 教程(一)', done: false },
        {id: uniqueId('todo-'), label: '应用 JavaFX 打包一个平台包', done: true },
        {id: uniqueId('todo-'), label: '写 C 语言教程', done: true },
        {id: uniqueId('todo-'), label: 'SDL 库', done: false }
      ]
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

目前 app 的 css 不会发生变化,前面的代码示例就临时不展现。咱们当初曾经为每一个待办产生了一个 id,那待办组件的 id 就显的有点多余了,咱们将待办组件的 id 申明为 prop,同时移除掉 lodash 相干的代码, 当初待办组件的 script 就变成了上面这样:

<script>
export default {
  name: 'ToDoItem',
  props: {label: {required: true, type: String},
    done: {default: false, type: Boolean},
    id: {required: true, type: String}
  },
  data () {
    return {isDone: this.done}
  }
}
</script>

app.vue 中将 id 传递给待办组件:

<template>
  <div id="app">
    <h1>To-Do List</h1>
  <ul>
    <li v-for="item in ToDoItems" :key="item.id" >
      <to-do-item :label="item.label"  :done="item.done" :id="item.id" ></to-do-item>
    </li>
  </ul>
  </div>
</template>

进化: 待办表单

当初咱们的程序依然可交互性较差,待办的数据都被固定在程序外面,不能增加。为了让咱们的程序交互性更强,让咱们制作一个待办表单组件。固定的步骤,咱们首先在 src/components 上面增加一个 ToDoForm.vue,而后咱们在 template 外面增加一个待办表单,表单外面蕴含 label,input、button。如下所示:

<template>
  <form>
      <label for = "new-todo-input">
        还有哪些没做
      </label>
      <input type="text" id="new-todo-input" name = "new-todo" autocomplete="off"/>
      <button type="submit"> 增加待办 </button>
  </form>
</template>

<script>
export default {name: 'ToDoForm'}
</script>

而后咱们在 App 组件外面,引入并注册这个组件。

import ToDoForm from './components/ToDoForm.vue'
components: {
    ToDoItem,
    ToDoForm
},

当初 template 变成了上面这样:

<template>
  <div id="app">
    <h1>To-Do List</h1>
    <to-do-form></to-do-form>
  <ul>
    <li v-for="item in ToDoItems" :key="item.id" >
      <to-do-item :label="item.label"  :done="item.done" :id="item.id" ></to-do-item>
    </li>
  </ul>
  </div>
</template>

页面成果如下:

对于表单的按钮来说点击 submit 的默认行为是将表单外面的数据发送回服务器,当初咱们还没有服务器。咱们只能来模仿,咱们在 submit 事件上绑定一个办法,该办法将输出的待办退出到待办列表里。这里咱们回顾一下 vue 实例的几个属性:

  • data 属性 用于建设双向绑定,单文件组件中必须申明为一个函数
  • compute 计算属性,用于计算 data 属性。
  • methods 办法属性,在这外面咱们申明事件产生时触发的办法。

当初咱们须要监听事件,那就是须要 methods 属性, 首先咱们在表单外面申明咱们关注的事件和事件产生后触发的办法

 <form @submit="onSubmit"></form>
<script>
export default {
  name: 'ToDoForm',
  methods: {onSubmit () {}}
}
</script>

然而下面咱们提到表单外面的 submit 事件会默认将表单的数据提交到服务器,这目前不是咱们所须要的,所以咱们须要阻止事件的默认行为,在原生 JavaScript 中咱们应用的是 event.preventDefault 来阻止,在 Vue 中也对此做了包装,在 Vue 中这种语法咱们称之为 event modifiers (事件修饰符),修饰符被增加到事件的开端,和事件之间通过点来进行连贯。像上面这样:

<template>
  <form @submit.prevent="onSubmit">
      <label for = "new-todo-input">
        还有哪些没做
      </label>
      <input type="text" id="new-todo-input" name = "new-todo" autocomplete="off"/>
      <button type="submit"> 增加待办 </button>
  </form>
</template>

<script>
export default {
  name: 'ToDoForm',
  methods: {onSubmit () {}}
}
</script>

上面是修饰符列表:

  • .stop:进行流传事件。等效于惯例 JavaScript 事件中的 Event.stopPropagation()。
  • .prevent:阻止事件的默认行为。等效于 Event.preventDefault()
  • .self:仅当事件是从该确切元素分派时触发处理程序。
  • {.key}:仅通过指定键触发事件处理程序。多词键只需转换为 kebab 大小写(例如 page-down)。
  • .native:监听组件根(最外层的包装)元素上的原生事件。
  • .once:监听事件,直到它被触发一次,而后不再触发。
  • .left:仅通过鼠标左键事件触发处理程序。
  • .right:仅通过鼠标右键事件触发处理程序。
  • .middle:仅通过鼠标中键事件触发处理程序。
  • .passive:等效于在 vanilla JavaScript 中应用 addEventListene 创立事件监听器时传入 {passive: true} 参数。4

有了监听事件咱们就能够进行下一步,接管表单的值并将其同步到待办列表上,咱们能够用双向绑定来天然的实现这个实现,也就是 v -model。当初咱们的表单标签如下所示:

<form @submit.prevent="onSubmit">

script 标签中的值如下所示:

export default {
  name: 'ToDoForm',
  data () {
    return {label: ''}
  },
  methods: {onSubmit () {console.log(this.label)
    }
  }
}

当初你在待办表单输出值,点击提交,控制台就会输入你输出的值。但咱们的监听事件目前依然存在一些缺点,对于空格这样的输出咱们一惯是不关怀的,在 Vue 中咱们能够通过 trim 来去除前后的空格。v-model 以后是通过 input 事件来更新 data 变量中的值,这意味着咱们每次点击按键都会同步数据,但咱们心愿的是在点击提交按钮或不再输出之后再同步值,所以咱们须要批改 v -model 所关怀的事件,也就是 change 事件。咱们通过 lazy 修饰符能够实现这个扭转,当初咱们的输入框变成了上面这样:

  <input type="text" id="new-todo-input" name = "new-todo" autocomplete="off" v-model.trim.lazy="label"/>

下一个问题是当增加待办这个按钮被点击,也就是说一个待办被提交,咱们该怎么告诉待办列表组件。在 Vue 的世界里,这个答案叫自定义事件。在咱们的程序中待办列表的组件是 App 组件来传递给它的,所以咱们应该是在 App 组件中监听这个音讯。要实现这一件事首先咱们要扭转下面 onSubmit 事件中的内容,在 Vue 中收回自定义事件的办法是 emit,这是 Vue 内置的办法,咱们在调用的时候须要在办法名上加上 $ , 规范的语法如下所示:

#eventName 事件名称 args 是附加参数 都会传递给监听此事件的办法
vm.$emit(eventName, […args] )
this.$emit('todo-added', this.label);

值得注意的是事件处理程序辨别大小写并且不能蕴含空格,Vue 在进行转换的时候会将事件名转为小写,所以在 Vue 外面无奈监听以大写字母命名的事件。子组件收回此事件之后,咱们在父组件应用的中央监听此事件:

<template>
  <div id="app">
    <h1>To-Do List</h1>
    <to-do-form @todo-added="addToDo"></to-do-form>
  <ul>
    <li v-for="item in ToDoItems" :key="item.id" >
      <to-do-item :label="item.label"  :done="item.done" :id="item.id" ></to-do-item>
    </li>
  </ul>
  </div>
</template>

而后再 addToDo 外面将待办表单组件传递过去的数据更新到页面, 像上面这样:

<script>
import ToDoItem from './components/ToDoItem.vue'
import uniqueId from 'lodash.uniqueid'
import ToDoForm from './components/ToDoForm.vue'
export default {
  name: 'App',
  components: {
    ToDoItem,
    ToDoForm
  },
  data () {
    return {
      ToDoItems: [{ id: uniqueId('todo-'), label: 'JavaFX 教程(一)', done: false },
        {id: uniqueId('todo-'), label: '应用 JavaFX 打包一个平台包', done: true },
        {id: uniqueId('todo-'), label: '写 C 语言教程', done: true },
        {id: uniqueId('todo-'), label: 'SDL 库', done: false }
      ]
    }
  },
  methods: {addToDo (toDoLabel) {this.ToDoItems.push({id: uniqueId('todo-'), label: toDoLabel, done: false})
    }
  }
}
</script>

当初咱们的待办就有了交互性,咱们在输入框中输出,点击提交之后,页面就会呈现新的待办。但目前咱们的程序依然不太欠缺,咱们在输出实现,点击提交,输入框的内容没被清空。除此之外,对于空格咱们尽管做了去除,然而咱们输出空格最初达到监听办法的是一个长度为 0 的字符串,咱们不应该解决这类数据。所以咱们待办表单的输出该当在点击提交之后清空输入框的内容,对于空输出,咱们该当间接返回,像上面这样:

onSubmit () {if (this.label === '') {return}
      this.$emit('todo-added', this.label)
      this.label = ''
}

当初咱们构建的待办开始感觉有交互性,然而她不难看,咱们须要丑化它。

进化: CSS 款式化组件

在 Vue 中应用 CSS 款式的办法有三种:

  • 内部 CSS 文件
  • 单个文件组件 (.vue 文件) 中的全局款式。
  • 单个文件组件中组件范畴的款式。

咱们将别离演示这三种应用办法。

内部 CSS 文件的款式

首先,咱们在 src/assets 目录下建一个名为 reset.css 的文件。Webpack 将解决此文件夹的文件。这意味着咱们能够应用 CSS 预处理器 (如 SCSS) 或后处理器(如 PostCSS)。而后将下列内容增加进去:

/*reset.css*/
/* RESETS */
*,
*::before,
*::after {box-sizing: border-box;}
*:focus {outline: 3px dashed #228bec;}
html {font: 62.5% / 1.15 sans-serif;}
h1,
h2 {margin-bottom: 0;}
ul {
  list-style: none;
  padding: 0;
}
button {
  border: none;
  margin: 0;
  padding: 0;
  width: auto;
  overflow: visible;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: normal;
  -webkit-font-smoothing: inherit;
  -moz-osx-font-smoothing: inherit;
  -webkit-appearance: none;
}
button::-moz-focus-inner {border: 0;}
button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}
button,
input {
  /* 1 */
  overflow: visible;
}
input[type="text"] {border-radius: 0;}
body {
  width: 100%;
  max-width: 68rem;
  margin: 0 auto;
  font: 1.6rem/1.25 "Helvetica Neue", Helvetica, Arial, sans-serif;
  background-color: #f5f5f5;
  color: #4d4d4d;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
}
@media screen and (min-width: 620px) {
  body {
    font-size: 1.9rem;
    line-height: 1.31579;
  }
}
/*END RESETS*/

而后在 src/main.js 中引入:

import './assets/reset.css';

当初页面变成了上面这样:

单个文件组件 (.vue 文件) 中的全局款式

下面咱们用 reset.css 对立了款式,咱们也心愿在咱们的应用程序外面所有的组件局部款式对立,尽管将这些款式增加到 reset.css 是能够的,然而将它增加到 style 标签也是能够的,留神要在 App.vue 中演示。当初咱们更新 App.vue 中的 style 标签,更新内容如下:

#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
/* Global styles */
.btn {
  padding: 0.8rem 1rem 0.7rem;
  border: 0.2rem solid #4d4d4d;
  cursor: pointer;
  text-transform: capitalize;
}
.btn__danger {
  color: #fff;
  background-color: #ca3c3c;
  border-color: #bd2130;
}
.btn__filter {border-color: lightgrey;}
.btn__danger:focus {outline-color: #c82333;}
.btn__primary {
  color: #fff;
  background-color: #000;
}
.btn-group {
  display: flex;
  justify-content: space-between;
}
.btn-group > * {flex: 1 1 auto;}
.btn-group > * + * {margin-left: 0.8rem;}
.label-wrapper {
  margin: 0;
  flex: 0 0 100%;
  text-align: center;
}
[class*="__lg"] {
  display: inline-block;
  width: 100%;
  font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {margin-bottom: 1rem;}
@media screen and (min-width: 620px) {[class*="__lg"] {font-size: 2.4rem;}
}
.visually-hidden {
  position: absolute;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px);
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}
[class*="stack"] > * {
  margin-top: 0;
  margin-bottom: 0;
}
.stack-small > * + * {margin-top: 1.25rem;}
.stack-large > * + * {margin-top: 2.5rem;}
@media screen and (min-width: 550px) {
  .stack-small > * + * {margin-top: 1.4rem;}
  .stack-large > * + * {margin-top: 2.8rem;}
}
/* End global styles */
#app {
  background: #fff;
  margin: 2rem 0 4rem 0;
  padding: 1rem;
  padding-top: 0;
  position: relative;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
@media screen and (min-width: 550px) {
  #app {padding: 4rem;}
}
#app > * {
  max-width: 50rem;
  margin-left: auto;
  margin-right: auto;
}
#app > form {max-width: 100%;}
#app h1 {
  display: block;
  min-width: 100%;
  width: 100%;
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
}

当初你会发现咱们的待办列表变成了一个卡片。

为了让他更好看一些,咱们将 app.vue 中申明的按钮款式利用 ToDoForm 的按钮,在.vue 中应用 css 和在规范 html 中增加款式的做法雷同,通过 class 属性选中款式。所以咱们的待办表单按钮变成了上面这样:

<button type="submit"  class="btn btn__primary btn__lg">
           点击增加
</button>

但目前 label 的间隔还待办之间的间隔还太过紧凑,咱们看待办表单的革新如下:

<template>
    <form @submit.prevent="onSubmit">
        <h2 class="label-wrapper">
        <label for="new-todo-input" class="label__lg">
          还有什么没做?
        </label>
        </h2>
      <input
        type="text"
        id="new-todo-input"
        name="new-todo"
        autocomplete="off"
        v-model.lazy.trim="label"
        class="input__lg"
      />
      <button type="submit"  class="btn btn__primary btn__lg">
           点击增加
      </button>
    </form>
</template>

当初咱们的待办列表变成了上面这个样子:

单个文件组件中组件范畴的款式

当初还剩待办列表组件咱们没有丑化,为了让这个款式仅利用待办列表组件,所以咱们须要在待办列表的 style 标签外面加上 scope,而后加上上面的款式:

<style scoped>
.custom-checkbox > .checkbox-label {
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-weight: 400;
  font-size: 16px;
  font-size: 1rem;
  line-height: 1.25;
  color: #0b0c0c;
  display: block;
  margin-bottom: 5px;
}
.custom-checkbox > .checkbox {
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-weight: 400;
  font-size: 16px;
  font-size: 1rem;
  line-height: 1.25;
  box-sizing: border-box;
  width: 100%;
  height: 40px;
  height: 2.5rem;
  margin-top: 0;
  padding: 5px;
  border: 2px solid #0b0c0c;
  border-radius: 0;
  appearance: none;
}
.custom-checkbox > input:focus {
  outline: 3px dashed #fd0;
  outline-offset: 0;
  box-shadow: inset 0 0 0 2px;
}
.custom-checkbox {
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-weight: 400;
  font-size: 1.6rem;
  line-height: 1.25;
  display: block;
  position: relative;
  min-height: 40px;
  margin-bottom: 10px;
  padding-left: 40px;
  clear: left;
}
.custom-checkbox > input[type="checkbox"] {
  -webkit-font-smoothing: antialiased;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -2px;
  left: -2px;
  width: 44px;
  height: 44px;
  margin: 0;
  opacity: 0;
}
.custom-checkbox > .checkbox-label {
  font-size: inherit;
  font-family: inherit;
  line-height: inherit;
  display: inline-block;
  margin-bottom: 0;
  padding: 8px 15px 5px;
  cursor: pointer;
  touch-action: manipulation;
}
.custom-checkbox > label::before {
  content: "";
  box-sizing: border-box;
  position: absolute;
  top: 0;
  left: 0;
  width: 40px;
  height: 40px;
  border: 2px solid currentcolor;
  background: transparent;
}
.custom-checkbox > input[type="checkbox"]:focus + label::before {
  border-width: 4px;
  outline: 3px dashed #228bec;
}
.custom-checkbox > label::after {
  box-sizing: content-box;
  content: "";
  position: absolute;
  top: 11px;
  left: 9px;
  width: 18px;
  height: 7px;
  transform: rotate(-45deg);
  border: solid;
  border-width: 0 0 5px 5px;
  border-top-color: transparent;
  opacity: 0;
  background: transparent;
}
.custom-checkbox > input[type="checkbox"]:checked + label::after {opacity: 1;}
@media only screen and (min-width: 40rem) {
  label,
  input,
  .custom-checkbox {
    font-size: 19px;
    font-size: 1.9rem;
    line-height: 1.31579;
  }
}
</style>

当初它变的更加丑陋:

进化: 计算属性

让咱们来接着欠缺这个待办,它看起来还是有些不完满的中央,没有显示出曾经已办的和未半的。一种间接的思路是在插值表达式中进行运算像上面这样:

<h2>{{ToDoItems.filter(item => item.done).length}} out of {{ToDoItems.length}} items completed</h2>

但咱们不应该在插值表达式中写入太多计算逻辑,一方面这不利于保护,另一方面页面每次渲染都要从新再进行计算,对于更为简单的页面或更为简单的表达式这会重大影响性能。更好的解决方案是计算属性。咱们首先在 App 组件中申明一个计算属性,像上面这样:

computed: {listSummary () {const completeNum = this.ToDoItems.filter(item => item.done).length
      return `${completeNum} out of ${this.ToDoItems.length} items completed`
    }
}

而后在 template 取出计算属性:

  <div id="app">
    <h1>To-Do List</h1>
    <to-do-form @todo-added="addToDo"></to-do-form>
    <h2 id="list-summary">{{listSummary}}</h2>
   <ul aria-labelledby="list-summary" class="stack-large">
    <li v-for="item in ToDoItems" :key="item.id" >
      <to-do-item :label="item.label"  :done="item.done" :id="item.id" ></to-do-item>
    </li>
  </ul>
  </div>

当初咱们的利用变成了上面这样:

但当咱们增加待办会发现总的待办数量会发生变化,当咱们点击实现待办,已实现的待办数量没有发生变化。起因在于,点击实现的时候没有更新待办数据的状态。当初咱们看待办组件进行革新,当咱们实现了待办,待办组件须要告知父组件 App,让父组件及时的更新 data 中的数据。仍旧是 emit。

<template>
  <div class="custom-checkbox">
     <input type="checkbox" :id="id" :checked="isDone" class="checkbox" @change="$emit('todo-complete')"/>
     <label :for="id" class="checkbox-label"> {{label}}</label>
  </div>
</template>

咱们在 App.vue 中解决该事件:

<div id="app">
    <h1>To-Do List</h1>
    <to-do-form @todo-added="addToDo"></to-do-form>
    <h2 id="list-summary">{{listSummary}}</h2>
   <ul aria-labelledby="list-summary" class="stack-large">
    <li v-for="item in ToDoItems" :key="item.id" >
      <to-do-item :label="item.label"  :done="item.done" :id="item.id" @todo-complete="updateToDoStatus(item.id)"></to-do-item>
    </li>
  </ul>
</div>

在 methods 申明解决该事件的办法:

updateToDoStatus (toDoId) {const toDoUpdate = this.ToDoItems.find(item => item.id === toDoId)
      toDoUpdate.done = !toDoUpdate.done
}

总结一下

到当初咱们的待办利用大抵曾经成型,但还是不够完满,咱们还要持续欠缺上来。但都塞在一篇,那这篇的篇幅就太长了。咱们来回顾一下本篇咱们讲了什么,上一篇咱们讲了父组件通过 prop 来向子组件传递数据,这一篇咱们讲了子组件向父组件传递数据的形式和计算属性,以及在 Vue 中如何应用 CSS 款式。打个预报,这些款式在 JavaFX 中也会用到,咱们也会用 JavaFX 构建一个待办,如果你对 JavaFX 不敢趣味,那么你能够疏忽这句话。

参考资料

  • mdn web docs https://developer.mozilla.org…
退出移动版