前端

Vue 学习笔记

2019-02-16  本文已影响238人  地平线0530

1. 使用vue-cli3构建项目

全局安装 vue-cli3

npm i -g @vue/cli

查看 vue-cli 版本

vue -V

快速构建项目

使用 vue ui 创建项目

vue ui

使用图形化界面管理和创建项目,一目了然,很方便

使用命令行创建项目

vue create xxx

参考:创建一个项目

项目配置

编辑器打开项目,然后添加一些文件夹和文件如下:

项目目录

将 router.js 和 store.js 分别放到相应目录里面,改名为 index.js
vue.config.js 配置参考

运行项目

方式一:命令行

npm run serve

方式二:图形界面

运行项目g

2. vue 路由

router-link 和 router-view 组件

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>

路由文件做如下配置:

// 0. 导入 Vue 和 VueRouter
import Vue from 'vue'
import Router from 'vue-router'

// 1. 导入自定义的页面组件
import Foo from './views/Foo.vue'
import Bar from './views/Bar.vue'

// 2. 定义路由
const routes = [
  {
    path: '/foo',
    name: 'foo',
    component: Foo
  },
  {
    path: '/bar',
    name: 'bar',
    component: Bar
  }
]

// 3. 加载 Router
Vue.use(Router)

// 4. 创建 router 实例
const router = new Router({
  routes
})

// 5. 暴露 router 实例
export default router

路由配置

起步

上面的例子比较简单,在实际工程中,页面较多,需要将路由配置信息和加载分开来写:
新建 router,js

路由目录

将 index.js 中的路由配置信息全部剪切到 router.js 中:
index.js 文件将如下:

import Vue from 'vue'
import Router from 'vue-router'
import routes from './router'

Vue.use(Router)

export default new Router({
  routes
})

router.js 如下:

import Foo from './views/Foo.vue'
import Bar from './views/Bar.vue'

const routes = [
  {
    path: '/foo',
    name: 'foo',
    component: Foo
  },
  {
    path: '/bar',
    name: 'bar',
    component: Bar
  }
]

export default routes

动态路由

import User from './view/User.vue'

{
  path: '/user/:name',
  name: 'user',
  component: User
}

User.vue 中:

<template>
  <div>
    {{ $route.params.name }}
  </div>
</template>

当我们在地址栏输入: http://localhost:8080/#/user/dog 时,页面显示了 dog

嵌套路由

/parent/child

{
  path: '/parent',
  name: 'parent',
  component: () => import('@/views/Parent.vue'),  // 路由懒加载
  children: [
    {
      path: 'child',
      name: 'child',
      component: () => import('@/views/Child.vue')
    }
  ]
}

编程式导航

例子:

<div id="app">
  <router-link to="/foo">Go to Foo</router-link>
  <router-link to="/bar">Go to Bar</router-link>
  <router-link @click="handleClick">User</router-link>
    
  <router-view></router-view> 
</div>

<script>
export default {
  name: '#app',
  methods: {
    handleClick() {
      this.$router.push({
        name: 'user',
        params: { name: 'dog' }
      })
    }
  }
}
</script>

方式一览:

// 字符串
this.$router.push('bar')

// 对象
this.$router.push({ path: 'bar' })

// 命名路由
this.$router.push({ name: 'user', params: { name: 'dog' }})

// 带查询参数: /xxx?name=dog
this.$router.push({ path: 'xxx', query: { name: 'dog' }})

// 含有变量的写法,实际中常见
const userName = 'dog'
this.$router.push({ name: 'user', params: { userName }})
this.$router.push({ path: `/user${userName}` })

// 错误写法:
this.$router.push({ path: '/user', params: { userName }})  // 结果为:/user
router.go(n)

在 history 记录中向前或向后跳转 n 步

// 在浏览器记录中前进一步,等同于 history.forward()
this.$router.go(1)
this.$router.forward()

// 后退一步记录,等同于 history.back()
this.$router.go(-1)
this.$router.back()

// 前进 3 步记录
this.$router.go(3)

// 如果 history 记录不够用,那就默默地失败呗
this.$router.go(-100)
this.$router.go(100)

// 跳转页面,并替换记录
this.$router.replace({name: 'foo'})

vue中操纵 history 效仿的原生API,详细参考:MDN 操作浏览器历史记录

命名路由

可以发现,我们在配置路由信息时,给每个路由指定了一个 name,这种就是命名路由,要访问命名路由,只需访问其 name 就可以了:

<router-link to="/foo">Go to Foo</router-link>
<router-link :to="{ name: 'foo' }">Go to Foo</router-link>

以上两个效果相同。
还可以传递一些参数:

<router-link :to="{ name: 'user', params: {name: 'cat'} }">User</router-link>

命名视图

在同级展示多个页面

<router-view></router-view>
<router-view name="a"></router-view>
<router-view name="b"></router-view>

路由配置

{
  path: '/',
  components: {
    default: () => import('@/views/Foo.vue'),
    a: () => import('@/views/A.vue'),
    b: () => import('@/views/B.vue'),
  }
}

重定向和别名

重定向

当用户访问 '/a' 页面时,URL 被自动替换为 '/b'
方式一: 从 /a 重定向到 /b

{
  path: '/a',
  redirect: '/b'
}

方式二:重定向到命名路由

{
  path: '/a',
  redirect: {
    name: 'foo'
  }
}

方法三:动态返回重定向目标

{
  path: '/a',
  redirect: to => {
    return {
      name: 'foo'
    }
  }
}
别名

当用户访问 '/''/home' 时显示的是想用的页面

{ 
  path: '/',
  alias: '/home',
  component: Home,
}

组件传参

在前面,我们这样进行路由传参:

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})

直接使用 $route 会使之与对应路由形成高度的耦合,限制了其灵活性。
我们可以通过 props 解耦:

const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true }
  ]
})

也可以设置接收参数的数据类型和默认值:

const User = {
  props: {
    type: [Number, String],  // 或者只有一种数据类型,可以直接写 String
    default: 123
  },
  template: '<div>User {{ id }}</div>'
}

还可以创建一个函数返回 props,这样就可以对参数做更多的事情:

const router = new VueRouter({
  routes: [
    {
      path: '/search',
      component: SearchUser,
      props: (route) => ({ query: route.query.q }) }
  ]
})

此时:/search?q=vue 将会以 {query: 'vue'} 为属性传递给 SearchUser 组件。

HTML5 History 模式

使用 history 模式,链接:http://localhost:8080/#/blog/123 就会像正常链接一样:http://localhost:8080/blog/123
但是这种模式需要后台配置支持,以防访问页面出现 404 错误。

nginx
location / {
  try_files $uri $uri/ /index.html;
}
原生 Node.js
const http = require('http')
const fs = require('fs')
const httpPort = 80

http.createServer((req, res) => {
  fs.readFile('index.htm', 'utf-8', (err, content) => {
    if (err) {
      console.log('We cannot open "index.htm" file.')
    }

    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })

    res.end(content)
  })
}).listen(httpPort, () => {
  console.log('Server listening on: http://localhost:%s', httpPort)
})
警告

这样做,服务器就不再返回 404 错误页面了,这时我们需要在 vue 中返回一个 404 页面:

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
})

路由匹配优先级按照配置文件,从上向下,所以最好将上面的配置写在所有路由配置的最后。

导航守卫

有时候,我们需要判断用户的登录状态,来决定其是否有权限访问一些页面,这时我们就需要导航守卫了。

配置全局前置守卫

import Vue from 'vue'
import Router from 'vue-router'
import routes from './router'

Vue.use(Router)

const router = new Router({
  routes
})

const HAS_LOGINED = true  // 判断用户是否登录

router.beforeEach((to, from ,next) => {
  if (to.name !== 'login') {
    if (HAS_LOGINED) next()
    else next({ name: 'login' })
  }
  else {
    if (HAS_LOGINED) next({ name: 'home' })
    else next()
  }
})

export default router

参数说明:

路由元信息

自定义路由的 meta 字段
比如,根据不同路由,改变 title
先给路由添加 meta 信息

{
  path: '/',
  alias: '/home',  // 别名
  name: 'home',
  component: Home,
  meta: {
    title: '首页'
  }
}

我们这里在 lib/util.js 文件中定义了一个改变 title 的函数:

export const setTitle = (title) => {
  window.document.title = title || '欢迎来到我的博客'
}

最后在全局导航守卫处进行判断:

router.beforeEach((to, from ,next) => {
  to.meta && setTitle(to.meta.title)
})

这里利用了短路逻辑原理,如果设置了 meta 字段,则运行 setTitle 函数,否则不做任何修改。

3. 组件间通信

父子组件通信

父组件传参到子组件

新建子组件 child.vue

<template>
  <div>
    <h2>{{ title }}</h2>
  </div>
</template>

<script>
export default {
  name: 'child',
  props: {
    title: {
      type: String,
      default: 'aaa'
    }
  }
}
</script>

新建父组件 parent.vue

<template>
  <div>
    <Child :title="title"/>
  </div>
</template>

<script>
import Child from "@/components/Child.vue"

export default {
  name: 'parent',
  data() {
    return {
      title: 'bbb'
    }
  },
  components: {
    Child
  }
}
</script>

这时,当我们在父组件内改变 title 值时,子组件内的 title 也发生了变化,如果什么值都不传,则子组件 title 会显示默认值 aaa

子组件传参到父组件

继续改变上面的例子,我们给子组件添加一个 input 标签,父组件添加一个 p 标签,当我们在子组件内输入内容时,父组件可以显示出来。
修改子组件:

<template>
  <div>
    <h2>{{ title }}</h2>
    <input @input="handleInput" :value="value">
  </div>
</template>

<script>
export default {
  name: 'child',
  props: {
    title: {
      type: String,
      default: 'aaa'
    },
    value: {
      type: [String, Number],
      default: ''
    }
  },
  methods: {
    handleInput(event) {
      const value = event.target.value
      this.$emit('input', value)
    }
  }
}
</script>

修改父组件:

<template>
  <div>
    <Child :title="title" v-model="inputValue"/>
    <p>{{ inputValue }}</p>
  </div>
</template>

<script>
import Child from "@/components/Child.vue"

export default {
  name: 'parent',
  data() {
    return {
      title: 'bbb',
      inputValue: ''
    }
  },
  components: {
    Child
  }
}
</script>

参考:组件基础

bus

如何实现同级组件或是跨级组件之间的通信呢?我们可以通过全局注册一个空的 vue 实例暂存数据来实现。
我们在 /lib 文件夹中新建 bus.js 文件:

import Vue from 'vue'

const Bus = new Vue()

export default Bus

main.js 中引入:

import Bus from './lib/bus'

// 将 bus 注册到 Vue 原型上
Vue.prototype.$bus = Bus

我们新建一个 All.vue 用来展示:

<template>
  <div>
    <Aview/>
    <Bview/>
  </div>
</template>

<script>
import Aview from '@/components/Aview.vue'
import Bview from '@/components/Bview.vue'

export default {
  components: {
    Aview,
    Bview
  }
}
</script>

components 目录新建两个文件:
Aview.vue

<template>
  <div class="a_box">
    <h2>A页面</h2>
    <button @click="handleClick">点我</button>
  </div>
</template>

<script>
export default {
  name: 'Aview',
  data() {
    return {
      msg: '我来自A'
    }
  },
  methods: {
    handleClick() {
      // 将事件绑定到 $bus 上
      this.$bus.$emit('on-click', this.msg)
    }
  },
}
</script>

<style scoped>
.a_box {
  border: 1px solid red;
}
</style>

Bview.vue

<template>
  <div class="b_box">
    <h2>B页面</h2>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  name: 'Bview',
  data() {
    return {
      content: ''
    }
  },
  mounted() {
    // 监听 $bus 上的事件
    this.$bus.$on('on-click', msg => this.content = msg)
  },
}
</script>

<style scoped>
.b_box {
  border: 1px solid blue;
}
</style>

页面效果如下:

截图

我们可以点击 A页面的按钮,此时会在 B页面显示相应的信息,这就是利用 bus 来实现同级组件或跨级组件间通信。

4. Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,其包含以下几个部分:

vuex

项目中配置

在实际项目中,我们将 store.js 文件进行解耦,放在单独文件中,方便管理:

截图

index.js 配置如下;

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from "./mutations"
import actions from "./actions"
import getters from "./getters"
import user from './module/user'
import player from './module/player'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters,
  modules: {
    user,
    player
  }
})

我们新建三个文件,来做演示:
新建 views/All.vue 用来装载其他组件,配置如下:(记得配置下路由,这里就不写了)

<template>
  <div>
    <GroupOne/>
    <GroupTwo/>
  </div>
</template>

<script>
import GroupOne from '@/components/GroupOne.vue'
import GroupTwo from '@/components/GroupTwo.vue'

export default {
  components: {
    GroupOne,
    GroupTwo
  }
}
</script>

components 目录下新建两个文件:GroupOne.vueGroupTwo.vue

文件直接复制就可以了,样式随便改改,用来做对比:

<template>
  <div class="group-one">
    <h2>Group One</h2>
    <ul>
      <li>
        <span class="name">这里显示名字</span>
        <span class="health">这里显示血量</span>
      </li>
    </ul>
  </div>
</template>

State

我们在 store/module/player.js 中配置一些数据:

const state = {
  group: [
    { name: '东方未明', health: 100 },
    { name: '谷月轩', health: 90 },
    { name: '荆棘', health: 80 },
    { name: '王蓉', health: 50 }
  ]
}

const getters = {
  //
}

const mutations = {
  //
}

const actions = {
  //
}

export default {
  state,
  getters,
  actions,
  mutations
}

在 vue 文件中,可以使用 computed 计算属性来获取 store 数据。
我们分别在 GroupOne.vueGroupTwo.vue 中的 script 标签中添加如下语句:

export default {
  name: "groupOne",
  computed: {
    group() {
      return this.$store.state.player.group
    }
  }
}

注意这里的 player 是指模块 player,如果这里获取的是根 state 数据,就不加模块名。
改写模板文件,使其显示数据:

<template>
  <div class="group-two">
    <h2>Group Two</h2>
    <ul>
      <li v-for="(player, index) of group" :key="index">
        <span class="name">{{ player.name }}</span>
        <span class="health">{{ player.health }}</span>
      </li>
    </ul>
  </div>
</template>

效果如下:

演示效果

不过这些数据是不可变的,有时我们需要派生一些状态,这时就需要 getter 了。

Getter

我们在 player 模块中定义一个方法,使 store 输出的数据都翻一倍:

const getters = {
  addHealth: state => {
    return state.player.group.map(item => {
      let { name, health } = item
      name = `${name}-加强版`
      health *=  2
      const obj = {
        name,
        health
      }
      return obj
    })
  }
}

我们在 GroupOne.vue 中调用:

<li v-for="(player, index) of addHealth" :key="index">
...

addHealth() {
  return this.$store.getters.addHealth
}

通过对比可以看出变化:

截图

我们可以认为 getter 就是 store 的计算属性,但是如果我们需要在组件中控制状态呢?比如我们添加一个按钮,点击按钮会对队伍群体伤害,血量 -1,这时我们就需要 mutation 了。

Mutation

player.js 中添加以下方法:

const mutations = {
  reduceAllHealth: (state, payload) => {
    state.group.forEach(item => {
      item.health -= payload
    })
  }
}

这里的 payload 是提交载荷,是由 store.commit 传入的额外参数。
GroupOne.vue 中做如下修改:

<button @click="reduceAllHealth(reduceNum)">群体攻击</button>
...

data() {
  return {
    reduceNum: 1
  }
},
methods: {
  reduceAllHealth(num) {
    this.$store.commit('reduceAllHealth', num)
  }
}

这时我们的页面如下:

效果图

点击一下按钮可见 Group One 的数值都减去了 2, Group Two 的数值都减去了 1。这是因为我们之前给 Group One 的模板添加了 getter 方法的原因。

Action

Action 类似于 mutation,不同在于:

截图

可以看到, action 不止可以提交 mutation,也可以直接改变 state
组件中使用 this.$store.dispatch() 方法来调用 action
比如我们定义一个 action 使其在触发时,提交一个 mutation

const actions = {
  reduceAllHealth: (state, payload) => {
    state.commit('reduceAllHealth', payload)
  }
}

GroupOne.vue 中调用:

this.$store.dispatch('reduceAllHealth', num)

其达到的效果和直接调用 mutation 一样。

辅助函数

Vuex还提供了一些辅助函数,帮助我们更为方便的调用 store
GroupOne.vue 中先引入辅助函数:

import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

然后修改上面所有的调用方法:

computed: {
  ...mapState({
    group: state => state.player.group
  }),
  ...mapGetters([
    'addHealth'
  ])
},
methods: {
  ...mapMutations([
    'reduceAllHealth'
  ]),
  ...mapActions({
    reduce: 'reduceAllHealth'
  })
}

使用数组形式,可以直接调用方法,使用对象形式,可以重命名。
Vuex 更多资料,参考官方文档

上一篇下一篇

猜你喜欢

热点阅读