从零开始的 Vue.js(全概括长篇)

Vue.js

  1. Vue.js(读音 /vjuː/, 类似于 view),是一套构建用户界面的 渐进式框架。
  2. Vue 的核心库只关注视图层。
  3. Vue 完全有能力驱动采用单文件组件和 Vue 生态系统支持的库开发的复杂单页应用。
    — 官网描述

这里还有跟其他框架的对比,包括 React、Angular 和 Ember 等。 我看来,Vue 和上面对比的框架其实都是想解决一个问题,只是程度不同,那就是用框架的抽象能力应对复杂和样板的功能需求。Vue 算是比较灵活的一种,例如 Angular 相比不那么灵活,因为框架自身把控的点更加多。而 Ember 的程度更加深,不仅将更多的点把控下来并且暴露为固定配置形式的“约定”让开发者遵守,框架自身则相当于一个大的场景模板,用配置来驱动。
我指的就是这种“程度”上的深浅,所以在前端领域百花齐放的年代 它们并没有那么的冲突,总有更适合不同场景不同人群的框架选择。
我学习 Vue 的过程中,起初觉得和 Angular1 相似,后来发现和 React 才是异曲同工。所以这里我会从 Vue 的入门的过程中逐渐的和 React 作对比。
为了更加深入和确切的感受到这两门框架的不同,我的博客项目前端的前台(就是你们看到的所有页面)是纯 React 构建,而后台(管理员才能看到的页面)是 Vue 构建,并且它们都是单页的。也就是说,我的整个博客站只有两个页面,一个是前台,一个是后台。

Vue 起步

Vue 分两种开发模式:

一是 “独立构建”。以模板 + Javascript(JSX) 的方式构建 View,需要集成 Webpack 和相应 loader 进行预编译工作,才能使用。也就是说这种方式需要搭建一个前端脚手架平台。
第二种是“运行时”构建。就跟写 Angular1 一样,直接依赖 vue.js 然后写 Javascript 和 HTML 元素。这种方式无需前端脚手架。

我们首先从第二种开始入门,下载 vue.js 的开发版本(开发版本体积较大,有错误提示):

http://vuejs.org/js/vue.js

当然你也可以直接依赖这个在线资源,但是为了避免网络问题,我还是建议下载到本地。

运行时方式入门

请大家创建 lib 目录存放 vue.js 文件,便可以直接 COPY 我展示的完整代码运行了。

Vue.js(以下简称 Vue)最基本的模板用法:

<div id="example">
    <span>{{ text }}</span>
</div>

创建 Vue 示例:

var vue = new Vue({
    el: '#example',
    data: {
        text : 'Hello Vue!'
    }
});

双大括号和 Angular 是一样的,除了没有像 Angular 的那样分层设计 乍一看是大同小异的。
el 选择相应 DOM 元素作为局部实例的根,{{ text }} 表示使用该 vue 实例的 text 属性,相当于调用了 vue.getText() 方法。
data 对象里面的所有属性都会被 vue 实例代理,所以 data 对象中的 text 可以被 vue.text 访问和修改。

结果当然就是 span 标签里边会被填充“Hello Vue!”文本内容。下面是完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <title>模板语法</title>
    <meta charset="utf-8">
    <script src="./lib/vue.js"></script>
</head>

<body>
    <div id="example">
        <span>{{ text }}</span>
    </div>
</body>
<script>
new Vue({
    el: '#example',
    data: {
        text : 'Hello Vue!'
    }
});
</script>

</html>

绑定事件

用 Vue 来给 DOM 元素绑定事件,通常(用通常是因为不是只有 menthods 与之相关)需要用到 methods 对象。我们创建一个按钮设置它的 onClick 事件。

在 el 元素里边再添加一个 button 标签:

<button v-on:click="clickBtn">点我</button>

在 Vue 实例里边添加一个 methods 属性,内容是包含函数的对象:

new Vue({
    // ...
    methods: {
        clickBtn: function(e){
            console.log('click!');
        }
    }
});

v-on: 前缀表示绑定 DOM 元素的对应事件,click 就是 DOM 的 onClick。基本上就是原生事件名去掉 on 首字母小写。
这里把 onClick 事件绑定到了一个名为 clickBtn 的函数上,即 vue 实例的 methons 对象中的 clickBtn 函数。点击控制台会输出 ‘click!‘。
PS:v-on 指令前缀可以用 @ 符号简写,此时无需冒号。例如 v-on:click 等同于 @click。
修改 clickBtn 函数内容,做点实际的事情:

clickBtn: function(e){
    this.text = '被点击了!';
}

然后会发现点击按钮之后 span 中的文本变成了 ‘被点击了!’字符串。有过 Angular 经验的一点也不会吃惊,因为修改绑定属性更新视图这样一个设计模式是一模一样的。 text 的变动会导致 span 视图被更新。
只不过跟 Angular1 在内部实现上是不同的,Angualr1 的双向绑定是用脏检查机制检测数据变动然后更新视图这样一个过程。而 Vue 没有脏检查机制,是利用的 setter 拦截的方式主动更新视图。也就是你修改 text 属性的同时就主动触发了对应视图的更新,比脏检查效率更高、消耗更小、更加的实时。

我们将 Vue 实例修改成下面这样:

var vue = new Vue({
    el: '#example',
    data: {
        text : 'Hello Vue!',
        counter: 0
    },
    methods: {
        clickBtn: function(e){
            this.text = '被点击了'+ (++this.counter) +'次!';
        }
    }
});

然后会发现,点击一次按钮 span 的文本内容都会不一样。因为 text 的内容会随着 counter 的增加而不同,所以每点击一次都会更新视图。这里的 counter 和 text 一样都是 vue 实例的属性(被 vue 实例直接代理),区别只是 text 被视图绑定,而 counter 没有。所以 counter 的变动不会导致界面的更新,只有 text 的变动才会。

条件渲染

如果页面上的元素根据某个值或者某个表达式的结果而决定呈现或者不呈现,例如:已登录的用户不显示登录按钮,反之则显示。
这时候就需要用到几个 Vue 内置的指令:v-if 和 v-else。创建新的 html 文件:

<!DOCTYPE html>
<html lang="en">

<head>
    <title>条件渲染</title>
    <meta charset="utf-8">
    <script src="./lib/vue.js"></script>
</head>

<body>
    <div id="example">
        <div id="guest" v-if="!login">
            <a href="javascript:">登录</a> |
            <a href="javascript:">注册</a>
        </div>
    </div>
</body>
<script>
var vue = new Vue({
    el: '#example',
    data: {
        login : false
    }
});
</script>

</html>

v-if 指令的值是 vue 实例的 login 属性取反的结果,因为 login 为 false,所以 v-if 成立 id 为 “guest” 的 div 就会渲染出来。既未登录的游客会看到的内容。
当然,v-if 指令是支持表达式的,所以你可以这样:

<div id="guest" v-if="login == false">
    <!-- ... -->
</div>

如果 login 状态是数字,1代表登录,0表示未登录,那么 v-if 的值就是 “login == 0”。
v-else 指令则是 v-if 不成立的情形下渲染的内容,不过要注意使用 v-else 的元素要挨着上面的 v-if 元素,否则无用。

<div id="guest" v-if="login == false">
    <!--省略内容-->
</div>
<div v-else>
    <a href="javascript:">注销</a>
</div>

然后把 login 的值改为 true 试试看,相反的,注销链接就显示出来了。当然你也可以加一个按钮动态的改变 login 属性的值,来动态的感受条件渲染的魅力。
不得不提一下,还有一个 v-show 指令,它跟 v-if 的功能一样。但是 v-show 还是会渲染出 DOM 结构,只不过 display:none。使用 v-show 仍然可以选择到该 DOM 元素,所以在某些情形下会有用。
注意:v-else 只能跟 v-if 连用,不能跟 v-show 连用。

绑定表单

一般来讲,表单的元素例如:input、select、checkbox、audio 等都不是直接用子元素作为值,而是属性作为值。并且由于双向绑定和单项数据流的特性,表单控件所输入的值需要同步到属性值上再更新表单的视图。
所以,绑定表单元素需要用 v-model 指令,而不能用文本绑定的形式:

<!DOCTYPE html>
<html lang="en">

<head>
    <title>绑定表单</title>
    <meta charset="utf-8">
    <script src="./lib/vue.js"></script>
</head>

<body>
    <div id="example">
        <input v-model="message" /><br>
        <span>{{ message }}</span>
    </div>
</body>
<script>
var vue = new Vue({
    el: '#example',
    data: {
        message : ''
    }
});
</script>

</html>

这段代码将 message 属性绑定到表单元素 input 上,并且绑定到视图 span 的子元素中。因为双向绑定的特性,input 中输入的内容会更新到 message 属性上,message 的变更会导致 input 和 span 视图发生更新,所 input 的内容跟 span 中显示的会是一致的。
注意:表单元素 textarea 由于其值是子元素,如果是传统的

<textarea>{{ message }}</textarea>
<span>{{ message }}</span>

这样的形式,message 不会是双向绑定的效果。因为子元素文本值的变动不会更新 message 属性。所以需要这样用 textarea:

<textarea v-model="message"></textarea>
<span>{{ message }}</span>

组件

上述内容跟 Angular 的确相似,但是这里以后会发现跟 React 才是一路的。
如果把 div、h1、ul 等看做 HTML 内置组件的话,那么 Vue 可以让你定制自己的组件。首先,创建一个简单的自定义组件:

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Component</title>
  <meta charset="utf-8">
  <script src="./lib/vue.js"></script>
</head>

<body>
  <div id="example">
    <my-component></my-component>
  </div>
</body>
<script>
Vue.component('my-component',{
  template: '<div>A custom component!</div>'
});

new Vue({
  el: '#example'
});
</script>

</html>

上面的代码创建了一个 my-component 组件,这个组件的内容是一个 div 包含文本’A custom component!‘。所以使用 my-component 标签被使用的时候,就会渲染出这样一个 DOM 结构:

<div id="example">
  <div>A custom component!</div>
</div>

绑定:

Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  }
})
new Vue({
  el: '#example'
})

跟 创建 Vue 实例 绑定不同的是, data 属性必须是一个函数,返回一个数据对象。

自定义组件包含子元素:

Vue.component('my-component',{
  template: '<div><h2>MyComponent</h2><slot></slot></div>'
});

slot 标签即表示子元素渲染位置,相当于 React 的 children 属性。

<div id="example">
  <my-component><span>我是子元素</span></my-component>
</div>

渲染为:

<div id="example">
  <div>
    <h2>MyComponent</h2><span>我是子元素</span>
  </div>
</div>

当我们定义好一个个组件后,会发现某些组件的内容是根据外部数据动态产生的,所以我们需要给组件传递参数。我们先创建组件:

Vue.component('my-component-params', {
    template: '<ul><li v-for="item in list">{{ item }}</li><ul/>',
    props: ['list']
});

props 中需要显式声明绑定的 prop 的名称。使用组件并且传递参数:

<my-component-params :list="['你好', '我是', '参数']" />

渲染结果:

<ul>
    <li>你好</li>
    <li>我是</li>
    <li>参数</li>
</ul>

此处的 :list 是就跟 @click 一样是简写,不过是 v-bind 指令的简写,等同于 v-bind:list 。
当然此处的 list 属性还可以绑定到 vue 实例的 data 数据中,而不是固定的数据字面量。

虽然组件是写出来了,但是这样子相当麻烦。因为要在字符串中拼接 template 标签,所以当然会有更方便的形式创建组件。但在这之前还要介绍另一种组件定义方式。

render 函数

template 在绝大部分场景中都是适用的,但在某些情况下可能不适合。于是便有了纯 Javascript 的形式构建组件的方式,也就是 render 函数:

<!--使用 render 定义的组件-->
<div id="example2">
  <render-component :level="1">Hello render!</render-component>
</div>
// 通过 render 定义的组件
Vue.component('render-component', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // tag name 标签名称
      this.$slots.default // 子组件中的阵列
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
new Vue({
    el: '#example2'
});

上面的代码渲染结果是:

<div id="example2">
  <h1>Hello render!</h1>
</div>

render 函数包含一个用于创建元素的函数参数,也就是上面的 createElement 函数。level 是组件的 props,也就是使用组件时设置的属性。 level 的值为1, ‘h’ + 1 于是便创建出了一个 h1 标签。this.$slots.default 相当于 template 中的 solt 标签,表示组件的子元素内容,也就是上面的 ‘Hello render!’ 文本。第二个参数 this.$slots.default 同时也表示组件顶级标签 h1 的 children 元素。 那么,如何创建有多层嵌套关系的组件呢?也就是有 children,并且有 children 的 children 的…

return createElement(
  'h' + this.level,
  [
    createElement('a', {
      attrs: {
        href: 'http://prd_blog.bluerain.io/'
      }
    }, this.$slots.default)
  ]
)

就跟上面代码那样,第二个参数可以接收一个数组。数组里边继续 createElement,若里层还有子元素则第二个参数继续 createElement …
上面的代码只是为了告诉大家可以接收数组,定义多个兄弟子元素。这里虽然只定义了一个 a 标签,并且 a 标签也没有继续在子级里边嵌套了,不过能这样使用就是了。

独立构建(非运行时)

上面的基本入门教程大概的介绍了 Vue 主要的几个点,这里要基于这些基础内容进入更高级的使用方式,也就是独立构建 Vue 应用。
在开始有介绍过,独立构建需要预编译,要搭建前端脚手架。这里假设大家有相关基础,我直接贴出额外需要的 webpack 配置项(非完全版):

resolve: {extensions: ['', '.js', '.jsx', '.vue'],
  alias: {
    'vue$': 'vue/dist/vue.js'
  }
},
module: {
  loaders: [
    {
      exclude: [nodeModulesPath],
      test: /\.vue$/,
      loader: 'vue'
    }, {
      exclude: [nodeModulesPath],
      test: /\.js$/,
      loader: 'babel',
      query: {
        presets: ['es2015', 'stage-3'],
        plugins: ['transform-vue-jsx']
      }
    }
    // ...
  ]
},
babel: {
  presets: ['es2015', 'stage-3']
}

增加了 vue 后缀文件类型的 loader。也就是说我们要基于跟 jsx 类似一种新的文件类型 .vue 来开发独立构建的 Vue 应用。
还在 babel-loader 的配置中添加 transform-vue-jsx 插件,增加了一次 babel 的独立配置。
所以我们要添加除 babel 外的:vue、vue-loader、babel-plugin-transform-vue-jsx、vue-template-compiler 等依赖,完整的 dependencies 如下:

"devDependencies": {
  "babel-core": "^6.18.2",
  "babel-helper-vue-jsx-merge-props": "^2.0.2",
  "babel-loader": "^6.2.7",
  "babel-plugin-syntax-jsx": "^6.18.0",
  "babel-plugin-transform-vue-jsx": "^3.2.0",
  "babel-polyfill": "^6.16.0",
  "babel-preset-es2015": "^6.18.0",
  "babel-preset-stage-3": "^6.17.0",
  "vue": "^2.0.7",
  "vue-loader": "^10.0.2",
  "vue-template-compiler": "^2.1.3",
  "webpack": "^1.13.3"
}

都配置好以后,我们创建 Button.vue 文件,假设我们要设计一个按钮组件。

<template>
  <button @click="handleClick">
    <span v-if="$slots.default">
      <slot></slot>
    </span>
    <span v-else>{{text }}</span>
  </button>
</template>
<script>
export default {
  name: 'MyButton',
  data(){
    return {
      text : '未命名按钮'
    }
  },
  methods:{
    handleClick(e){
      this.$emit('click', e);
    }
}
</script>
<style>
  .vue-btn {
    background: skyblue;
    border: none;
    cursor: pointer;
  }
  
  .vue-btn:hover {
    background: #F5DCC6;
  }
</style>

上面是 Button.vue 的完整内容,我们解析一下这个组件:
首先利用 template 标签创建出组件的基本 DOM 结构,绑定了 onClick 事件给 handleClick 方法,handleClick 发射了 click 方法。v-if 指令 和 v-else 的意思是如果有子元素那么按钮的内容是子元素。如果没有按钮的内容绑定到 text 上,默认是 ‘未命名按钮’,也就是说使用这个组件,子元素即是按钮的显示文本。并且用 style 标签为按钮添加了样式。
script 标签用了 es2015 的导出语法,导出这个组件对象。template 标签即相当于为这个对象设置了 template 属性。
在你的 entry.js 中导入这个组件,并且使用它:

const Vue = require('vue')
import Button from './Button.vue'
Vue.component(Button.name, Button)
// 创建 vue 实例省略

HTML(或者另一个 template)中使用组件:

<my-button>我是按钮<my-button/>

然后就能看到独立构建的 Vue 组件渲染结果。其实跟运行时构建一样,但是这种方式更加利于开发。在 JS 中写标签或者用纯 JS 写组件都不方便或者直观。那么 .vue 让标签化 template 和 JS 以及 style 结合在一起 + 加上 ES6,更优雅的开发 Vue 应用。

上面的所有内容大致上把 Vue 说了一个大概,是一个宏观的概括 Vue 核心元素的形式的教程。不过也可以应付一些入门级的 Vue 开发工作了。当然作为一个真正投入 Vue 的爱好者,是不可能仅限于此的,官方文档仍然需要细看和经常查阅。
虽然 Vue 的教程完了,但是仍然不具备第一段话的第三点:“ Vue 完全有能力驱动采用单文件组件和Vue生态系统支持的库开发的复杂单页应用。”的能力。
所以本文还需要介绍最后一个东西,那就是 Vue 单页应用的开发。

VueRouter

跟 React 一样,想开发单页应用单纯靠 Vue 还不够。React 是配合 ReactRouter,而 Vue 也有相应的 VueRouter。安装依赖:

yarn add vue-router --dev

创建 views 目录和 Index.vue 文件(放进 views 目录是为了让独立构建的页面和组件区分开),内容:

<template>
  <div>
    <h1>我是首页内容。</h1>
  </div>
</template>
<script>
export default {
  name: 'MyIndex'
}
</script>

上面的 vue 独立文件当作独立构建的首页,在 entry.jsx 中配置 vue-router:

const Vue = require('vue');
import VueRouter from 'vue-router'
import Index from './views/Index.vue'
Vue.component(Index.name, Index);
const router = new VueRouter({
  mode: 'history',
  routes: [{
    path: '/',
    component: {
      template: '<MyIndex></MyIndex>'
    }
  }]
});
Vue.use(VueRouter);new Vue({
  router,
  el: '#app',
  render(h) {
    return (
      <router-view></router-view>
    )
  }
});

通用的 HTML 模板非常简单:

<body>
    <div id="app">
        {{app}}
    </div>
</body>

router-view 表示进入 router 后 template 的填充位置。所以 {{ app }} 绑定处会填充上 Index 的内容。

我们再添加第二个页面组件(views/About.vue):

<template>
  <div>
    <h1>我是关于页面。</h1>
  </div>
</template>
<script>
export default {
  name: 'MyAbout'
}
</script>

在首页(Index.vue)的 h1 标签下添加:

<router-link to="/about">About</router-link>

router-link 就相当于 ReactRouter 中的 Link 了。 to 属性表示要访问的 route,所以再添加一个 About 的 route 项:

routes: [{
    path: '/',
    component: {
        template: '<MyIndex></MyIndex>'
    }
}, {
    path: '/about',
    component: {
        template: '<MyAbout></MyAbout>'
    }
}]

访问首页 About 链接,页面会变成 Aboue.vue 渲染出来的内容。并且页面不会刷新,地址栏却会变化。后退返回首页同样。这样不同的 view 之间就靠 vue-router 实现单页化了。
相比我的这篇专门介绍 ReactRouter 的文章,这篇的 vue-router 篇幅实在太少了。毕竟这是一个全概括性入门,不能面面具到,不然写着很累,看着也累。更多的 vue-router 基本构成参考文档

最后

至此,Vue 入门结束。