Vue.js提供了插件机制,可以在全局添加一些功能。它们可以简单到几个方法、属性,也可以很复杂,比如一整套组件库。
注册插件需要一个公开的方法install,它的第一个参数是Vue构造器,第二个参数是一个可选的选项对象。示例如下:
MyPlugin.install = function(Vue,options){ //全局注册组件(指令等功能资源类似) Vue.component('component-name',{ //组件内容 }); //添加实例方法 Vue.prototype.$Notice = function() { //逻辑。。。 }; //添加全局混合 Vue.mixin({ mounted:function() { //逻辑。。 } }) }
通过Vue.use(MyPlugin)或者
Vue.use(MyPlugin,{
//参数
})
来使用插件。绝大多数情况下,开发插件主要是通过NPM发布后给别人使用的。在自己的项目中可以直接在入口调用以上的方法。
前端路由
webpack主要使用场景是单页面富应用(SPA),而SPA的核心就是前端路由。什么是路由,通俗的讲就是网址。专业一点就是每次get或post等请求在服务端有一个专门的正则配置列表,然后匹配到具体的一条路径后,分发到不同controller,进行各种操作,最终将html或数据返回给前端,这就完成了一侧IO。
目前绝大多是的网站都是后端路由,也就是多页面的,这样的好处很多,比如可以在服务端渲染好直接返回给浏览器,不用等待前端加载任何js和css就可以直接显示网页内容,对SEO的友好等。缺点也很明显,就是模板由后端累维护或改写html结构,所以html和数据、逻辑混为一谈,维护起来臃肿麻烦。
然后就有了前后端分离的开发模式。后端只提供API来返回数据,前端通过AJAX请求获取数据,在用一定的方式渲染都页面中。这样出现很多前端技术栈,比如jQuery + artTemplate +requirejs + gulp为主的开发模式可谓是万金油了。
在Node.js出现后,这种现象有了改善,html模板可以完全由前端累控制,同步或异步渲染完全由前端自由决定,并且由前端维护一套模板。
SPA就是在前后端分离的基础上加上路由。
vue-router的基本用法
新建一个项目router,复制上一章代码并安装完成后,再通过NPM来安装vue-router:
npm install --save vue-router
在main.js里使用Vue.use()加载插件:
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './app.vue';
Vue.use(VueRouter);
每个页面对应一个组件,也就是对应一个.vue文件。在router目录下创建views目录,用于存放所有页面,然后在views里创建index.vue和about.vue两个文件:
// index.vue
<template> <div>首页</div> </template> <script> export default { } </script> <style scoped> </style>
// about.vue
<template> <div>介绍页</div> </template> <script> export default { } </script> <style scoped> </style>
回到main.js完成路由剩余配置,创建一个数组来制定路由匹配列表,每一个路由映射一个组件:
const Routers = [ { path: '/index', component: (resolve) => require(['./views/index.vue'],resolve) }, { path: '/about', component: (resolve) => require(['./views/about.vue'],resolve) } ]
Routers里每一项的path属性就是指定当前匹配的路径 ,component 是映射的组件。上面的写法,webpack会把每个路由都打包为一个js文件,在请求到该页面时,才回去加载这个页面的js,也就是异步实现的懒加载。如果想要一次性加载,可以这样写:
{ path: '/about', component: require(['./views/index.vue']) }
使用异步路由后,便一处的每个页面的js都叫做chunk(块),它们命名默认是0.main.js、1.main.js....可以在webpack配置出口里通过设置chunkFilename字段修改chunk命名,例如:
output:{ publicPath:'/dist/', filename: '[name].js', chunkFilename: '[name].chunk.js' }
有了chunk后,在每个页面(.vue文件)里写的样式也需要配置后才会打包进main.css,否则仍然会通过javascript动态创建<style>标签写入,配置插件:
//webpack.config.js plugins: [ new ExtractTextPlugin({ filename:'[name].css', allChunk:true }) ]
然后继续在main.js里完成配置和路由实例:
const RouterConfig = { //使用html5的history路由模式 mode: 'history', routes: Routers }; const router = new VueRouter(RouterConfig); //创建Vue实例 new Vue({ el: "#app", router: router, render: h => h(App) });
在RouterConfig里,设置mode为history会开启HTML5的history路由模式,通过"/"设置路径。如果不配置mode,就会使用"#"来设置路径。开启history路由,在生产环境时服务端必须进行配置,将所有路由都指向同一个html。
webpack-dev-server也要配置下来支持history路由,在package.json中修改dev命令:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "webpack-dev-server --open --history-api-fallback --config webpack.config.js", "build": "webpack --mode production" },
增加了-history-api-fallback,所有的路由都会指向index.html。
配置好了这些,最后在app.vue添加一个<router-view>来挂载所有路由组件:
<template> <div> <router-view></router-view> </div> </template> <script> export default { } </script> <style scoped> div { color: #f60; font-size: 24px; } </style>
运行网页时,<router-view>会根据当前路由动态渲染不同的页面组件。网页中的一些公共部分,比如顶部的导航栏,侧边导航栏,底部的版权信息,这些也可以直接写在app.vue里,与<router-view>同级。路由切换时,切换的是<router-view>挂载的组件,其他内容并不会变化。
运行npm run dev启动服务,然后访问127.0.0.1:8080/index和127.0.0.1/about就可以访问这两个页面了。
在列表里,可以在最后新加一项,当访问路径不存在时,重定向到首页:
{ path: '*', redirect: '/index' }
路由列表的path也可以带参数,比如“个人主页”的场景,路由的一部分时固定的,一部分时动态的:/user/123456,其中用户id"123456"就是动态的,但是路由到同一个页面,这个页面需要获取到这个id,请求对应的数据,在路由里可以这样配置参数:
{ path: '/user/:id', component: (resolve) => require(['./views/user.vue'],resolve) },
在views目录下新建user.vue文件
<template> <div>{{$route.params.id}}</div> </template> <script> export default { mounted () { console.log(this.$route.params.id); } } </script> <style scoped> </style>
这里的this.$router可以访问到当前路由的很多信息。
跳转
vue-router有两种跳转页面的方法,第一种是内置的<router-link>组件,它会被焕然成一个a标签。
<template> <div> <h1>首页</h1> <router-link to="/about">跳转到about</router-link> </div> </template>
用法和一般的组件一样,to是一个prop,指定需要跳转的路径,也可用v-bind来绑定动态设置。在html5的histtory模式下会拦截点击,避免浏览器重新加载页面。<router-link>还有其他一些prop,常用的有:
tag
tag可以指定渲染成什么标签,比如<router-link to='/about' tag='li'></router-link>渲染的结果就是<li>
replace
使用replace不会留下history记录,所以导航后不能后退返回上一个页面,<router-link to='/about' replace>
active-class
当router-link 对应的路由匹配成功时,会自动给当前元素设置一个名为router-link-active的clas,设置prop:active-class可以修改默认的名称。在做类似的导航栏时,可以使用该功能高亮显示当前页面对应的导航菜单项,但是一个不修改active-class
如果跳转需要在javascript里进行,可以使用第二种方法
<template> <div> <h1>介绍页</h1> <button @click="handleRouter">跳转到user</button> </div> </template> <script> export default { methods:{ handleRouter() { this.$router.push('/user/123') } } } </script> <style scoped> </style>
添加点击事件,this.$router.push就可以。
$router还有一些其他的方法:
replace
this.$router.replace('/user/123'),替换当前的history记录。
go
类似前进和后退多少步。
this.$router.go(-1);
高级用法
如何修改网页标题?
vue-router提供了导航钩子beforeEach和afterEach,它们会在路由即将改变前和改变和触发,所以设置标题可以在beforeEach钩子完成。
const Routers = [ { path: '/index', meta:{ title:'首页' }, component: (resolve) => require(['./views/index.vue'],resolve) }, { path: '/about', meta:{ title:'关于' }, component: (resolve) => require(['./views/about.vue'],resolve) }, { path: '/user/:id', meta:{ title:'个人主页' }, component: (resolve) => require(['./views/user.vue'],resolve) }, { path: '*', redirect: '/index' } ]; const RouterConfig = { //使用html5的history路由模式 mode: 'history', routes: Routers }; const router = new VueRouter(RouterConfig); router.beforeEach((to, from, next) => { window.document.title = to.meta.title; next(); console.log('before') });
导航钩子有三个参数:
to 即将要进入的目标路由对象。
form 当前导航即将要离开的路由对象。
next 调用该方法后,才能进入下一个钩子。
路由列表的meta字段可以自定义一些信息,比如将title写入了meta来统一维护,beforeEach钩子可以从路由对象to里获取meta信息,从而改变标题。
有了这两个钩子,还能做很多来提升用户体验。比如页面较长,滚动到某个位置,在跳到另一个页面,滚动条默认实在上一个页面停留的位置,而最好是能返回顶部,通过钩子afterEach就可以实现
router.afterEach((to,from,next) => { window.scroll(0,0) })
类似的需求还有从一个页面过度到另一个页面时,可以吹安一个全局的loading动画。到新页面加载完成后再结束动画。
next()方法还可以设置参数。
某些页面需要检验是否登录,登录了就可以访问,不然就打登录页面。
router.beforeEach((to,form,next) => { if(window.localStorage.getItem('token')){ next(); }else{ next('/login'); } })
状态管理与Vuex
非父子组件通信时,使用了bus的一个方法,用来触发和接收事件,进一步起到了通信的作用。Vuex所解决的问题与bus类似,作为一个插件来使用,可以更好的维护整个项目的组件状态。
Vuex的基本用法
npm install --save vuex
用法和vue-router类似,在main.js里,通过Vue.use()使用Vuex。
import Vuex from 'vuex'
Vue.use('Vuex')
const store = new Vuex.Store({ //vuex配置 })
new Vue({ el: "#app", router: router, store:store, render: h => h(App) });
仓库store包含了应用的数据和操作过程。Vuex里的数据都是响应式的,任何组件使用同一store的数据时,只要store的数据变化,对应的组件也会立即更新。
数据保存在Vuex选项的state字段内,实现一个计数器,定义一个count,初始值为0:
const store = new Vuex.Store({ state: { count:0 }, })
在任何组件内,可以直接通过$store.state.count读取:
<template> <div> <h1>首页</h1> {{count}} </div> </template> <script> export default { computed: { count () { return this.$store.state.count; } } }
mutations是Vuex的第二个选项,用来直接修改state里的数据。我们给计数器增加2个mutations,用来加1或者减1
const store = new Vuex.Store({ state: { count:0 }, mutations: { increment (state) { state.count ++; }, decrease (state) { state.count --; } }, })
然后在组件内,通过this.$store.commit方法来执行mutations。在index.vue中添加按钮用于加和减:
<template> <div> <h1>首页</h1> {{ count }} <button @click="handleIncrement">+1</button> <button @click="handleDecrease">-1</button> </div> </template> <script> export default { computed:{ count () { return this.$store.state.count; }, }, methods: { handleIncrement () { this.$store.commit('increment'); }, handleDecrease () { this.$store.commit('decrease'); }, } } </script> <style scoped> </style>
mutations还可以接受第二个参数,可以是数字、字符串或对象等类型。
increment (state, n=1) { state.count += n; },
然后在组件里面
handleIncrement () { this.$store.commit('increment', 5); },
高级用法
Vuex还有其他3个选项可以使用:getters、actions、modules。
const store = new Vuex.Store({ state: { count:0 }, getters: { filteredList: state => { return state.list.filter(item => item < 10); }, listCount: (state, getters) => { return getters.filteredList.length; } }, })
//getters可以写一些通用的方法在里面,方便各个组件使用
<template> <div> <h1>首页</h1> <div>{{ list }}</div> <div>{{listCount}}</div> </div> </template> <script> export default { computed:{ count () { return this.$store.state.count; }, list () { return this.$store.getters.filteredList; }, listCount () { return this.$store.getters.listCount; } }, } </script> <style scoped> </style>
getters也可依赖其他的getter,把getter作为第二个参数。
mutationi不应该异步操作数据,所以有了actions选项。action与mutation很像,不同的是action里面提交的是mutation。并且可以异步操作业务逻辑。
action在组件内通过$store.dispatch触发,主要在异步的时候使用。
actions: { asyncIncrement (context) { return new Promise(resolve => { setTimeout(() => { context.commit('increment'); resolve(); },1000) }) } }
handleActionIncrement () { this.$store.dispatch('increment').then(() => { console.log(this.$store.state.count); }) }
mutations、actions看起来很相似,Vuex很像是一种与开发者的约定,涉及改变数据的使用mutations,存在业务逻辑的就用actions。
最后一个选项是modules,它用来将store分给到不同的模块中。当项目足够大时,store里的state、getters、mutations、actions会非常多,都放在main.js不是很好,使用modules可以将它们写到不同的文件中,每个module拥有自己的state、getters、mutatios、actions,而且可以多层嵌套。
const moduleA = {
state:{....},
mutations:{.....},
actions:{....},
getters:{.....}
}
const moduleB = {
state:{....},
mutations:{.....},
actions:{....},
getters:{.....}
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a //moduleA的状态
store.state.b //moduleB的状态
const moduleA = { state: { count:0 }, getters: { sumCount (state, getters, rootState) { return state.count + rootState.count; } } }
module的mutation和getter接收的第一个参数state是当前模块的状态。在actions和getters中,还可以接收一个参数rootState,来访问根节点的状态。