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
方式二:图形界面
2. vue 路由
router-link 和 router-view 组件
Hello App!
Go to Foo
Go to Bar
路由文件做如下配置:
// 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 中:
{{ $route.params.name }}
当我们在地址栏输入: 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')
}
]
}
编程式导航
例子:
Go to Foo
Go to Bar
User
方式一览:
// 字符串
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
就可以了:
Go to Foo
Go to Foo
以上两个效果相同。
还可以传递一些参数:
User
命名视图
在同级展示多个页面
路由配置
{
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: 'User {{ $route.params.id }}'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})
直接使用 $route
会使之与对应路由形成高度的耦合,限制了其灵活性。
我们可以通过 props
解耦:
const User = {
props: ['id'],
template: 'User {{ id }}'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true }
]
})
也可以设置接收参数的数据类型和默认值:
const User = {
props: {
type: [Number, String], // 或者只有一种数据类型,可以直接写 String
default: 123
},
template: 'User {{ id }}'
}
还可以创建一个函数返回 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
参数说明:
-
to
:即将进入的目标路由 -
from
:正要离开的路由 -
next
:必须调用,才能执行接下来的程序-
next()
:进行管道中下一个钩子 -
next(false)
:中断当前的导航 -
next('/')
或next({path: '/'})
:跳转到指定地址 -
next(error)
:终止错误并传递给router.onError()
注册过的回调
-
路由元信息
自定义路由的 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. 组件间通信
父子组件通信
- 所有 prop 都使父子 prop之间形成一个单向下行绑定:父级 prop 更新会向下流动到子组件中,但反过来不行。
- 子组件通过自定义事件
this.$emit('myEvent')
的方式向父组件传参。
父组件传参到子组件
新建子组件 child.vue
:
{{ title }}
新建父组件 parent.vue
:
这时,当我们在父组件内改变 title
值时,子组件内的 title
也发生了变化,如果什么值都不传,则子组件 title
会显示默认值 aaa
子组件传参到父组件
继续改变上面的例子,我们给子组件添加一个 input
标签,父组件添加一个 p
标签,当我们在子组件内输入内容时,父组件可以显示出来。
修改子组件:
{{ title }}
修改父组件:
{{ inputValue }}
参考:组件基础
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
用来展示:
在 components
目录新建两个文件:
Aview.vue
A页面
Bview.vue
B页面
{{ content }}
页面效果如下:
我们可以点击 A页面的按钮,此时会在 B页面显示相应的信息,这就是利用 bus 来实现同级组件或跨级组件间通信。
4. Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,其包含以下几个部分:
- state:驱动应用的数据源
- view:以声明方式将 state 映射到视图
- actions:响应在 view 上的用户输入导致的状态变化
项目中配置
在实际项目中,我们将 store.js 文件进行解耦,放在单独文件中,方便管理:
- index.js:负责导入导出所有模块
- state.js:写入一些全局使用的数据
- actions.js:根级别的 action
- mutations.js:根级别的 mutation
- getters.js:根级别的 getter
- modules:存放各模块
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
用来装载其他组件,配置如下:(记得配置下路由,这里就不写了)
components
目录下新建两个文件:GroupOne.vue
、GroupTwo.vue
。
文件直接复制就可以了,样式随便改改,用来做对比:
Group One
-
这里显示名字
这里显示血量
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.vue
、GroupTwo.vue
中的 script
标签中添加如下语句:
export default {
name: "groupOne",
computed: {
group() {
return this.$store.state.player.group
}
}
}
注意这里的 player
是指模块 player
,如果这里获取的是根 state 数据,就不加模块名。
改写模板文件,使其显示数据:
Group Two
-
{{ player.name }}
{{ player.health }}
效果如下:
不过这些数据是不可变的,有时我们需要派生一些状态,这时就需要 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
中调用:
...
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
中做如下修改:
...
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,而不是直接变更状态。
- Action 可以包含任意异步操作。
我们用控制台打印action
:
可以看到, 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 更多资料,参考官方文档