vue

前端框架 Vue 学习笔记

之前在网页里小范围的用了 vue, 感觉用起来非常爽,现在打算做自己的笔记应用。这次打算做成 SPA 的形式,前端全部用 vue 来写。需要 node 的一些东西,记录下学习过程。迈向全栈,哈哈 ^_^

安装

首先,全局安装 vue:

npm install --global @vue/cli

data 属性

只有在创建时提供的 data 属性才是响应式的,在创建之后在添加新元素就不管用了。

当然还可以使用 app.$watch 方法显式创建一些监听器

// $watch 是一个实例方法
vm.$watch("a", function (newValue, oldValue) {
  // 这个回调将在 `vm.a` 改变后调用
})

methods 和 computed

methods 定义了一些可以调用的方法,他的值也可以用来插值。但是最好使用 computed,因为 computed 是有缓存的。

computed 属性也可以设置 setter,所以实际上,computed 属性相当于对现有属性的一种映射和变化。

v-bind 和 v-model

  • v-bind 用于单项绑定:在 HTML 属性中使用 v-bind 绑定,标签中使用 {{ }}。只能使用表达式,而不能使用语句。
  • v-model 用于双向绑定:在 input 这类用户可以输入的组件中,需要双向绑定,使用 v-model.

v-bind 类似的指令还有 v-once 和 v-html

v-model 实际上等价于

<input v-model="searchText">
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

要想在自定义输入组件中支持 v-model 的话,就需要使用 v-bind 和 v-on 两个方法了,而不能直接使用 v-model。

v-if 和 v-for

这两个就和所有模板系统中的 if 和 for 一样。vue 中提供的额外方便之处是,可以使用 template 标签,这样就不会多一个标签了

vue 是懒渲染的,因此会尽可能地复用组件,可以使用 key 来区分

v-if 是真正的条件式渲染,v-show 则只是在切换 display 属性

vue 没有代理数组的赋值方法,所以需要使用 app.$set 方法

@

如果只是指定一个事件处理函数的话,那么参数就是 event。如果自己指定了参数的话,可以使用 $event 来代表 event

还可以使用 .prevent 和 .stop 等修饰符

component

vue 中最终要的概念就是组件了。使用组件来模块式得构建应用。需要通过 props 属性来定义组件中的属性

<div id="app-7">
  <ol>
    <!--
      现在我们为每个 todo-item 提供 todo 对象
      todo 对象是变量,即其内容可以是动态的。
      我们也需要为每个组件提供一个“key”,稍后再
      作详细解释。
    -->
    <todo-item
      v-for="item in groceryList"
      v-bind:todo="item"
      v-bind:key="item.id">
    </todo-item>
  </ol>
</div>

Vue.component("todo-item", {
  // todo-item 组件现在接受一个
  // "prop",类似于一个自定义特性。
  // 这个 prop 名为 todo。
  props: ["todo"],
  template: "<li>{{ todo.text }}</li>"
})

props 是一个数组,用来声明组建的属性。然后通过属性来传递。

组件还可以通过 $emit 来发送事件,这些事件可以被所有的组件监听到,就像普通的 DOM 事件一样。

声明周期函数

在固定的周期,vue 会调用的一些函数 created, mounted 等。需要注意的是,不要使用胖箭头函数。

new Vue({
  data: {
    a: 1
  },
  created: function () {
    // &#x60;this&#x60; 指向 vm 实例
    console.log("a is: " + this.a)
  }
})
// => "a is: 1"

创建 vue 应用

因为我们会直接通过*.vue文件来编写 vue 的组件,因此需要使用 webpack 打包编译。另外我们需要使用官方的 vue-router 来

vue init webpack notelet

这条命令基于 webpack 这个模板创建了 notelet 这个应用,也就是我们的笔记应用。

打开 src/router/index.js 可以看到 vue 创建的 router 的代码,其中@src目录的缩写。

import Vue from "vue"
  import Router from "vue-router"
import Hello from "@/components/Hello"

Vue.use(Router)

export default new Router({
  routes: [
    {
    ¦ path: "/",
    ¦ name: "Hello",
    ¦ component: Hello
    }
  ]
})

然后打开 src/main.js, 可以看到在里面使用了 App 来作为我们的跟组件

// The Vue build version to load with the &#x60;import&#x60; command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from "vue"
import App from "./App"
import router from "./router"

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: "#app",
  router,
  template: "<App/>",
  components: { App }
})

执行 npm run dev, 然后打开 http://localhost:8080/ 就可以看到我们的 vue 应用了。注意,在写这篇文章的时候 node 8.x 下似乎有 bug, 导致 app.js 加载不出来,安装 6.x 就好了。该死的 node.

打开 App.vue, 也就是我们的根组件,可以看到下面的内容

<template>
  <div id="app">
  ¦ <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "app"
}
</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>

注意其中的 router-view 便签,意思就是路由的内容都在 router-view 中显示。

添加一个新的组件和路由

接下来我们添加一个”关于”页面。打开src/router/index.js, 改成下面这样:

export default new Router({
  routes: [
  ¦ {
  ¦ ¦ path: "/",
  ¦ ¦ name: "Hello",
  ¦ ¦ component: Hello
  ¦ },
  ¦ {
  ¦ ¦ path: "/about",
  ¦ ¦ name: "About",
  ¦ ¦ component: About
  ¦ }
  ]
})

然后添加 src/components/About.vue 文件

<template>
  <div class="hello">
  ¦ <h1>About Notelet</h1>
  ¦ <p>This is a simple note app</p>
  </div>
</template>

<script>
export default {
  name: "About",
  data () {
  ¦ return {
  ¦ ¦ msg: "Hello Vue"
  ¦ }
  }
}
</script>

然后更改 App.vue 文件

<template>
  <div id="app">
  ¦ <router-link :to="{name: "Hello"}">Home</router-link>
  ¦ <router-link to="/about">About</router-link>
  ¦ <router-view></router-view>
  </div>
</template>

注意,我们使用 about 指向了 About 这个组件,而使用 hello 指向了 Hello 这个组件,注意其中还动态传递了参数。

[1] https://scotch.io/tutorials/getting-started-with-vue-router

thinking in vue

basic usage

The created vue instance will proxy its data member

<div id="app">
  <ol>
    <li v-for="company in companies">
      <a v-bind:href="company.link">{{ company.text }}</a>  // bind 更新参数
            <button v-on:click="reverseText">逆转消息</button>
    </li>
  </ol>
</div>

let vm = new Vue({
    el: '#app',
    data: {
        companies: [
                {text: 'Google', link: 'http://google.com'},
                    {text: 'fackbook', link: 'http://facebook.com'},
                    {text: 'apple', link: 'http://apple.com'}
            ]
    },
    methods: {
        reverseText: function() {
        // 注意 this 绑定到了触发这个事件的元素内部
                this.company.text = this.company.text.split('').reverse.join('');
            }
    }
});

vm.companies.push({text: 'Amazon', link: 'http://amazon.com'});
// computed 属性可以绑定一个虚拟的属性到几个不同的属性上,有点类似 python 的 @property
// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

# 适合用来做自动保存文档等工作
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 question 发生改变,这个函数就会运行
    question: function (newQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.getAnswer()
    }
  },
  methods: {
    // _.debounce 是一个通过 lodash 限制操作频率的函数。
    // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
    // ajax 请求直到用户输入完毕才会发出
    // 学习更多关于 _.debounce function (and its cousin
    // _.throttle),参考:https://lodash.com/docs#debounce
    getAnswer: _.debounce(
      function () {
        if (this.question.indexOf('?') === -1) {
          this.answer = 'Questions usually contain a question mark. ;-)'
          return
        }
        this.answer = 'Thinking...'
        var vm = this
        axios.get('https://yesno.wtf/api')
          .then(function (response) {
            vm.answer = _.capitalize(response.data.answer)
          })
          .catch(function (error) {
            vm.answer = 'Error! Could not reach the API. ' + error
          })
      },
      // 这是我们为用户停止输入等待的毫秒数
      500
    )
  }
})
</script>

flow control

v-if

v-else 元素必须紧跟在 v-if 或者 v-else-if 元素的后面——否则它将不会被识别。v-else-if 也是。

可以使用 template 来包装多个元素:

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-for

v-for 的基本语法如前所述,另外还可以采用可选参数 key. <li v-for="(item, index) in items">.

v-for 还可以遍历对象,<li v-for="value in obj">

v-for 还可以直接遍历 range, <li v-for="n in 10">

事件

在 vue 中,绑定的事件如果需要参数,可以使用

<button v-on:click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

的形式,其中 $event 指的是原声事件。

修饰符

vue 中的事件绑定函数可以使用一些修饰符来指定一些附加的效果。常用的有 .prevent, .stop, .self, .once 等。

像这样:<a v-on:click.stop="doThis"></a>

对于键盘时间,还可以使用修饰符来指定键值:<input v-on:keyup.enter="submit">

mustache vs v-bind

mustache can only be used in textContent of an element, v-bind is used for attribute.

<a v-bind:href="url">{{ link_text }}</a>

shortcut

<!-- 完整语法 -->
<a v-bind:href="url"></a>
<!-- 缩写 -->
<a :href="url"></a>
<!-- 完整语法 -->
<a v-on:click="doSomething"></a>
<!-- 缩写 -->
<a @click="doSomething"></a>

v-model 做双向绑定

use v-model to double bind data between input and js.

<div id="app-6">
  <p>{{ message }}</p>
  <input v-model="message">
</div>

var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})

值得注意的是,v-model 本质上只是一个语法糖。
<input v-model="something"> is just a syntax sugar to <input v-bind:value="something" v-on:input="something = $event.target.value">

text area

需要注意的是,textarea 时间上相当于一个 input 组件,不能在 testarea 内部使用 {{value}} 的语法,而应该使用 v-model

<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

修饰符

就像事件一样,也可以指定一些修饰符给 v-model, 常用的有 .trim

vue-component

vue 的 component 中三个重要的概念:props, events, slots.

其中 props 向下传递,用于 parent 组件向 child 组件传递值。child 组件对于 props 的访问只能是只读的。在 child 组件中使用v-bind:var="var"来访问定义的 props. 注意在组件中不能更改 props, 如果需要更改他,请把他赋值给其他变量,或者使用 computed 属性。

如果把模板直接放到 dom 中会有一些标签渲染不出来,建议放到

// 在 vue 中注册一个组件,大多数传递给 vue 实例的参数都可以使用,除了 data 必须是一个函数

Vue.component('todo-item', {
  // todo-item 组件现在接受一个"prop",类似于一个自定义属性。这个属性名为 todo。
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>',
  data: function() {},
})

<div id="app-7">
  <ol>
    <!-- 现在我们为每个 todo-item 提供待办项对象    -->
    <!-- 待办项对象是变量,即其内容可以是动态的 -->
    <todo-item v-for="item in groceryList" v-bind:todo="item"></todo-item>
  </ol>
</div>

Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})
var app7 = new Vue({
  el: '#app-7',
  data: {
    groceryList: [
      { text: '蔬菜' },
      { text: '奶酪' },
      { text: '随便其他什么人吃的东西' }
    ]
  }
})

事件

parent 组件可以监听子组件的事件,从而实现通信:

这个例子中,子组件通过 $emit() 函数发送 increment 事件。parent 组件通过监听子组件的 increment 事件,从而获得子组件的消息。

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

slots

slots vs props: slots 用于显示一大片的包含 html 代码的替换块,而 props 用于显示值,有点类似 v-bind 和 {{}} 的区别。

Props 允许外部环境传递数据给组件
Events 允许从外部环境在组件内触发副作用
Slots 允许外部环境将额外的内容组合在组件中。

vue router

basic: 将组件 (components) 映射到路由 (routes),然后告诉 vue-router 在哪里渲染它们。

<div id="app">
  <p>
    <router-link to="/user/foo">/user/foo</router-link>
    <router-link to="/user/bar">/user/bar</router-link>
  </p>
  <router-view></router-view>
</div>

<script>
const User = {
  template: `<div>User {{ $route.params.id }}</div>`
}

const router = new VueRouter({
  mode: 'history',  // 这样才能使用 html5 的 history api
  routes: [
    { path: '/user/:id', component: User }
  ]
})

const app = new Vue({ router }).$mount('#app')
</script>

vue lifecycle

using in chrome extension

chrome does not allow eval and new Function() in extensions, vue relies on it. you need to use CSP version of vue or relax the restriction by chrome.

See also:

  • https://developer.chrome.com/extensions/contentSecurityPolicy#relaxing-eval
  • https://stackoverflow.com/questions/34615503/vue-js-in-chrome-extension
  • https://vuejs.org/v2/guide/installation.html#CSP-environments