GithubHelp home page GithubHelp logo

vue-tutorial's Introduction

本教程教大家一步一步用vue来写一个web App, 从简单到复杂,所有步骤都有详细解释。

Table of Contents

  1. Step1: vue-cli
  2. Step2: 编写一个简单的Counter组件
  3. Step3: vuex
  4. Step4: 调用后端 Restful API
  5. Step5: vue-router
  6. Step6: 编写可复用组件
  7. Step7: element-ui

Step1: vue-cli

全局安装 vue-cli,

npm install -g vue-cli

使用 vue-cli 创建一个空项目,

vue init webpack step1
? Project name vue-starter-kit
? Project description A Vue.js project
? Author soulmachine <[email protected]>
? Vue build standalone
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? Yes

编译并运行,

 cd step1
 npm install
 npm run dev

会自动弹出浏览器并打开 http://localhost:8080/ ,可以效果了。

配置 WebStorm 开发环境

本教程使用WebStorm 作为开发工具,首先要安装Vue插件,点击 WebStorm->Preferences->Plugins,在右边的窗口点击Browse repositories,搜索 Vue, 会看到两个插件,vue-for-ideaVue.js,我们选择前者,虽然后者的下载次数多,但是很久没有更新了。不过目前两个插件都无法识别 <button v-on:click="decrement">-</button> 里面的v-on指令,会报错。

把刚刚创建的项目工程导入 WebStorm,

  1. 启动WebStorm ,安装 Vue.js 插件

  2. 点击菜单 File->Open,浏览到项目根目录,打开

  3. 在左上角点击菜单 Preferences->Language & Frameworks->JavaScript,在右边的窗口中,JavaScript language version 选择ECMAScript 6,勾选Perfer strict mode

  4. 浏览到 Language & Frameworks->JavaScript->Libraries,在右边的窗口中,勾选ECMAScript 6

  5. 浏览到 Language & Frameworks->JavaScript->Code Quality Tools->ESLint,在右边的窗口中勾选Enable

  6. 浏览到 Language & Frameworks->Node.jsJ and NPM,在右边的窗口中找到 Node.js Core library,点击 Enable按钮

  7. 为了让 webstorm 能够识别 v-on这类标签,点击 Preferences->Editor->Inspections,在右边的窗口点击HTML->Unkown HTML tag attribute,在Custom HTML tag attributes中添加以下属性:

     v-on,v-on:click,v-on:change,v-on:focus,v-on:blur,v-on:keyup,:click,@click,v-model,v-text,v-bind,:disabled,@submit,v-class,:class,v-if,:value,v-for,scoped,@click.prevent,number,:readonly,@input,@click,v-show,v-else,readonly,v-link,:title,:for,tab-index,:name,:id,:checked,transition,@submit.prevent,autocapitalize,autocorrect,slot,v-html,:style
    

以上配置参考了这个答案 http://stackoverflow.com/a/36929396/381712

Step2: 编写一个简单的Counter组件

先把 step1 的项目拷贝过来

cp -r step1 step2
cd step2

添加一个文件 src/components/Counter.vue,内容如下:

<template>
  <div id="counter">
    <p>Current count: {{count}}</p>
    <button v-on:click="increment">+</button>
    <button v-on:click="decrement">-</button>
    <button v-on:click="incrementIfOdd">Increment if odd</button>
  </div>
</template>

<script>
  export default {
    name: 'Counter',
    data () {
      return {
        count: 0
      }
    },
    methods: {
      increment (event) {
        this.count++
      },
      decrement (event) {
        this.count--
      },
      incrementIfOdd (event) {
        if (this.count % 2 === 1) this.count++
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
p {
  text-align: center;
  font-size: 40px;
  padding: 40px 0;
}

button {
  width: 100px;
  height: 40px;
  background: #42B983;
  color: #fff;
}
</style>

<script> 中的 name 字段是可选的,一般还是加上,方便 debug 的时候知道是哪个组件出了问题。

App.vue中引入 Counter组件,需要修改三处,首先在 <script>的开头import,

import Counter from './components/Counter'

然后在 components里加入 Counter组件,

components: {
  Hello,
  Counter
}

<template>里使用 Counter组件,

<counter></counter>

最终 App.vue内容如下,

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <hello></hello>
    <counter></counter>
  </div>
</template>

<script>
import Hello from './components/Hello'
import Counter from './components/Counter'

export default {
  name: 'app',
  components: {
    Hello,
    Counter
  }
}
</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>

运行 npm run dev 应该可以看到Counter组件了。

Step3: vuex

状态管理是非常核心的功能,vuex 是尤大大专门为vue打造的状态管理框架,类似于 React 社区里的Redux. Vuex 最好的文档就是官方的文档 https://vuex.vuejs.org/,只看这一份文档就足够了,官方的文档写的简洁然而又通俗易懂。

这一节我们将使用 vuex 来改造 Counter 组件,把它的状态放入到 vuex的单根树里,因此Counter变成了一个无状态的组件。 大部分代码参考了官方的Counter例子

首先拷贝项目,

cp -r step2 step3
cd step3

然后安装 vuex,

npm install --save vuex

首先创建一个全局的 store, 新建一个文件 src/store/index.js,

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// root state object.
// each Vuex instance is just a single state tree.
const state = {
  count: 0
}

// mutations are operations that actually mutates the state.
// each mutation handler gets the entire state tree as the
// first argument, followed by additional payload arguments.
// mutations must be synchronous and can be recorded by plugins
// for debugging purposes.
const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}

// actions are functions that causes side effects and can involve
// asynchronous operations.
const actions = {
  increment: ({ commit }) => commit('increment'),
  decrement: ({ commit }) => commit('decrement'),
  incrementIfOdd ({ commit, state }) {
    if ((state.count + 1) % 2 === 0) {
      commit('increment')
    }
  },
  incrementAsync ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('increment')
        resolve()
      }, 1000)
    })
  }
}

// getters are functions
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

// A Vuex instance is created by combining the state, mutations, actions,
// and getters.
export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations
})

然后我们需要将 store 注入到所有组件中,只需要在 src/main.js 中将这个全局的store对象传递给根组件,

import Vue from 'vue'
import App from './App'
import store from './store/index'

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

然后所有App的子组件都可以用 this.$store 来访问这个状态树了。

最后我们开始改造 Counter组件,将其变为一个无状态组件, src/components/Counter.vue 内容如下:

<template>
  <div id="counter">
    <p>Current count: {{ count }}, the count is {{ evenOrOdd }}</p>
    <button v-on:click="increment">+</button>
    <button v-on:click="decrement">-</button>
    <button v-on:click="incrementIfOdd">Increment if odd</button>
    <button v-on:click="incrementAsync">Increment async</button>
  </div>
</template>

<script>
import { mapGetters, mapState, mapActions } from 'vuex'
export default {
  computed: {
    ...mapGetters([
      'evenOrOdd'
    ]),
    ...mapState([
      'count'
    ])
  },
  methods: mapActions([
    'increment',
    'decrement',
    'incrementIfOdd',
    'incrementAsync'
  ])
}
</script>


<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
p {
  text-align: center;
  font-size: 40px;
  padding: 40px 0;
}

button {
  width: 100px;
  height: 40px;
  background: #42B983;
  color: #fff;
}
</style>

可以看到Counter 组件的内部状态,即 data 消失了,取而代之的是数据直接从 this.$store 获取。不过要注意,local state is fine, 能用局部状态就没必要放入 vuex的全局树中,这里Counter组件用局部状态其实挺好,只是这一节为了学习目的,故意放入 vuex 中了。

这一节我们把 incrementIfOdd 变为了一个 action, 同时增加了一个 incrementAsync。关于 action 和 mutation 的区别请看官网 https://vuex.vuejs.org/en/actions.html

Step4: 调用后端 Restful API

单页面应用SPA免不了需要向后端服务请求数据,这一节我们选择 vue-resource 这个 HTTP客户端。

后端服务就用豆瓣的这个公开API, https://api.douban.com/v2/movie/top250

首先拷贝项目,

cp -r step3 step4
cd step4

然后安装 axios,

npm install --save vue-resource

src/main.js 引入并注册 vue-resource:

import VueResource from 'vue-resource'
Vue.use(VueResource)

创建一个组件 src/components/Douban.vue,

<template>
  <div id="douban">
    <ul>
      <li v-for="article in articles">
        {{article.title}}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data () {
    return {
      articles: []
    }
  },
  mounted () {
    this.$http.jsonp('https://api.douban.com/v2/movie/top250?count=10', {}, {
      headers: {},
      emulateJSON: true
    }).then(function (response) { // success callback
      this.articles = response.data.subjects
    }, function (response) { // error callback
      console.log(response)
    })
  }
}
</script>

注意不能用 get 只能用jsonp,因为跨域请求的缘故。

最后,在 App.vue 中使用这个组件,需要添加三行代码, 在 <template>里添加一行 <douban></douban>,在 <script>里引入这个组件,import Douban from './components/Douban' 并添加到 components 字段。

Step5: vue-router

现在所有的组件都挤在首页上,很难看,需要把它们分散到多个页面,这时候就需要引入客户端路由,使用官方的 vue-router。 官方文档 http://router.vuejs.org/

先拷贝项目,

cp -r step4 step5
cd step5

安装 vue-router,

npm install --save vue-router

src/main.js 引入并注册 vue-router,创建一个路由实例并传递给Vue实例,

import Vue from 'vue'
import App from './App'
import Counter from './components/Counter'
import Douban from './components/Douban'
import store from './store/index'
import VueResource from 'vue-resource'
import VueRouter from 'vue-router'

Vue.use(VueResource)
Vue.use(VueRouter)

const routes = [
  { path: '/counter', component: Counter },
  { path: '/douban', component: Douban }
]

const router = new VueRouter({
  mode: 'history',
  routes // short for routes: routes
})

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

然后在 src/App.vue<template>节点删除原来的Counter和Douban两个组件,添加 <router-link><router-view>

<p>
  <router-link to="/counter">Go to Counter</router-link>
  <router-link to="/douban">Go to Douban</router-link>
</p>
<!-- component matched by the route will render here -->
<router-view></router-view>

Step6: 编写可复用组件

在第3步中,我们的 Counter 组件,内部直接使用了Vuex 的东西,跟 Vuex 是强耦合的。如果我们想要把Counter组件开源出去,做成一个可复用的组件,该怎么办? 需要去掉 Vuex 依赖,仅仅依赖Vue, 只使用 props 和 events 来实现父子组件通讯。

在官方文档这里,已经说到,父子组件之间,一般用 props down, events up 来实现通讯。

接下来我们来改造 Counter 组件,让它拜托 Vuex 的依赖。

<template>
  <div id="counter">
    <p>Current count: {{ count }}, the count is {{ evenOrOdd }}</p>
    <button v-on:click="increment">+</button>
    <button v-on:click="decrement">-</button>
    <button v-on:click="incrementIfOdd">Increment if odd</button>
    <button v-on:click="incrementAsync">Increment async</button>
  </div>
</template>

<script>
export default {
  name: 'counter',
  props: {
    count: Number
  },
  computed: {
    evenOrOdd () {
      return this.count % 2 === 0 ? 'even' : 'odd'
    }
  },
  methods: {
    increment () {
      this.$emit('incrementEvent')
    },
    decrement () {
      this.$emit('decrementEvent')
    },
    incrementIfOdd () {
      this.$emit('incrementIfOddEvent')
    },
    incrementAsync () {
      this.$emit('incrementAsyncEvent')
    }
  }
}
</script>


<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
p {
  text-align: center;
  font-size: 40px;
  padding: 40px 0;
}

button {
  width: 100px;
  height: 40px;
  background: #42B983;
  color: #fff;
}
</style>

现在 Counter 组件的数据来源只有一个,就是 props 里的 count 了,同时 Counter 定义了四个自定义事件,向上冒泡给父组件,由父组件来响应。

接下来我们来改在父组件,即 src/App.vue,

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <hello></hello>
    <p>
      <counter :count="count" v-on:incrementEvent="increment" v-on:decrementEvent="decrement" v-on:incrementIfOddEvent="incrementIfOdd" v-on:incrementAsyncEvent="incrementAsync" to="/counter" ></counter>
      <router-link to="/douban">Go to Douban</router-link>
    </p>
    <router-view></router-view>
  </div>
</template>

<script>
import Hello from './components/Hello'
import Counter from './components/Counter'
import Douban from './components/Douban'
import { mapState, mapActions } from 'vuex'

export default {
  name: 'app',
  computed: mapState([
    'count'
  ]),
  methods: mapActions([
    'increment',
    'decrement',
    'incrementIfOdd',
    'incrementAsync'
  ]),
  components: {
    Hello,
    Counter,
    Douban
  }
}
</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>

基本上,把第三步中的Counter里关于 Vuex的部分,原封不动的搬到了App组件里了,现在 App 组件是跟Vuex耦合的了,这是无可避免的,一个 Web 应用,至少根组件是要跟 Vuex 耦合的。

template 节点, 我们监听Counter 组件的四个事件。

目前有两个问题还没有解决:

  • 当前的代码,比step3 中的相比, boilplate 代码多了很多,是我用的姿势不对吗?
  • Counter组件如果放在 <router-link> 里,怎么向它传递 props 数据,以及怎么用v-on监听事件? 现在只能把它暂时从 <router-link>移出来了。

Step7: element-ui

现在的界面太难看了,自己写UI组件又很费劲,我们就用饿了么公司开源的UI库来美化一下界面吧, element-ui,最近刚刚发布 1.0 正式版。

先拷贝项目,从 step5 拷贝,感觉 step6 的写法太繁琐了,

cp -r step5 step7
cd step7

安装 element-ui,

npm install --save element-ui

src/main.js 中引入并注册 element-ui,

import Element from 'element-ui'
import 'element-ui/lib/theme-default/index.css'
Vue.use(Element)

给豆瓣的电影列表套一个CSS 样式,将 src/components/Douban.vue 中的 <template> 换成如下内容,

<template>
  <el-card class="box-card">
    <div slot="header" class="clearfix">
        <h1 style="line-height: 36px; color: #20A0FF">豆瓣电影排行榜</h2>
    </div>
    <div v-for="article in articles" class="text item">
        {{article.title}}
    </div>
  </el-card>
</template>

运行 npm run dev 就可以看到效果了。

接下来给 Counter 组件美化一下,同样的,只需要修改src/components/Counter.vue里的 <template>部分,

<template>
  <div id="counter">
    <p>Current count: {{ count }}, the count is {{ evenOrOdd }}</p>
    <el-button v-on:click="increment">+</el-button>
    <el-button v-on:click="decrement">-</el-button>
    <el-button v-on:click="incrementIfOdd">Increment if odd</el-button>
    <el-button v-on:click="incrementAsync">Increment async</el-button>
  </div>
</template>

最后,我们来改善一下页面布局,并加一个导航菜单, src/App.vue的内容如下,

<template>
  <div id="app">
    <el-menu router default-active="/" class="el-menu-demo" mode="horizontal" @select="handleselect">
      <el-menu-item index="/">Home</el-menu-item>
      <el-menu-item index="counter">Go to Counter</el-menu-item>
      <el-menu-item index="douban">Go to Douban</el-menu-item>
    </el-menu>
    <router-view></router-view>
  </div>
</template>

<script>
import Hello from './components/Hello'
import Counter from './components/Counter'
import Douban from './components/Douban'

export default {
  name: 'app',
  components: {
    Hello,
    Counter,
    Douban
  },
  methods: {
    handleselect (index) {
      console.log(index)
    }
  }
}
</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>

上面的代码中我们还把 <style> 的最后一行 margin-top: 60px; 删除了,这样顶部就不再有空白了。

上述代码里,我们给 el-menu 传入了一个参数 router,从而启用了vue-router模式, 并在 <methods> 添加了一个 handleselect 方法。

同时,由于在导航菜单里我们把Hello 组件做为默认的首页,因此我们还要修改 src/main.js,添加一条路由,

{ path: '/', component: Hello },

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.