vue router 路由鉴权(非动态路由)

概述角色:超级管理员、主题管理员、数据服务管理员权限:超级管理员:所有页面主题管理员:基础公共页面+主题设置页数据服务管理员:基础公共页面+数据服务设置页+数据服务审批页需求:角色菜单来自后端,当用户未通过页面菜单,直接从地址栏访问非权限范围内的url时,拦截用户访问并重定向到首页。实际系统中还有几种管理员,此处略去,以精简描述。 原本想用动态路由的思路去做,按权限加载对应路由表,但是由于权限可以交叉(比如一个人可以同时是主题管理员和数据服务管理员),导致权限路由表还是得去做判断组合。于是放弃了这个思路,索性就在beforeEach里直接判断了。实现路由概览// index.jsimport Vue from ‘vue’import Router from ‘vue-router’import LabelMarket from ‘./modules/label-market’import PersonalCenter from ‘./modules/personal-center’import SystemSetting from ‘./modules/system-setting’import API from ‘@/utils/api’Vue.use(Router)const routes = [ { path: ‘/label’, component: () => import(/* webpackChunkName: “index” / ‘@/views/index.vue’), redirect: { name: ‘LabelMarket’ }, children: [ { // 基础公共页面 path: ’label-market’, name: ‘LabelMarket’, component: () => import(/ webpackChunkName: “label-market” / ‘@/components/page-layout/OneColLayout.vue’), redirect: { name: ‘LabelMarketIndex’ }, children: LabelMarket }, { // 个人中心 path: ‘personal-center’, name: ‘PersonalCenter’, redirect: ‘/label/personal-center/my-apply’, component: () => import(/ webpackChunkName: “personal-center” / ‘@/components/page-layout/TwoColLayout.vue’), children: PersonalCenter }, { // 系统设置 path: ‘system-setting’, name: ‘SystemSetting’, redirect: ‘/label/system-setting/theme’, component: () => import(/ webpackChunkName: “system-setting” / ‘@/components/page-layout/TwoColLayout.vue’), children: SystemSetting }] }, { path: ‘’, redirect: ‘/label’ }]const router = new Router({ mode: ‘history’, routes })// personal-center.jsexport default [ … { // 我的审批 path: ‘my-approve’, name: ‘PersonalCenterMyApprove’, component: () => import(/* webpackChunkName: “personal-center” / ‘@/views/personal-center/index.vue’), children: [ { // 数据服务审批 path: ‘api’, name: ‘PersonalCenterMyApproveApi’, meta: { requireAuth: true, authRole: ‘dataServiceAdmin’ }, component: () => import(/ webpackChunkName: “personal-center” / ‘@/views/personal-center/api-approve/index.vue’) }, … ] }]export default [ … { // 数据服务设置 path: ‘api’, name: ‘SystemSettingApi’, meta: { requireAuth: true, authRole: ‘dataServiceAdmin’ }, component: () => import(/ webpackChunkName: “system-setting” / ‘@/views/system-setting/api/index.vue’) }, { // 主题设置 path: ’theme’, name: ‘SystemSettingTheme’, meta: { requireAuth: true, authRole: ’topicAdmin’ }, component: () => import(/ webpackChunkName: “system-setting” */ ‘@/views/system-setting/theme/index.vue’) }, …]鉴权判断用户登陆信息请求后端接口,返回菜单、权限、版权信息等公共信息,存入vuex。此处用到权限字段如下:_userInfo: { admin:false, // 是否超级管理员 dataServiceAdmin:true, // 是否数据服务管理员 topicAdmin:false // 是否主题管理员}权限判断逻辑如下:判断当前路由是否需要鉴权(router中meta字段下requireAuth是否为true),让公共页面直接放行;判断角色是超级管理员,直接放行;(本系统特殊逻辑)判断跳转路径是主题设置但角色不为主题管理员,继续判断角色是否为数据服务管理员,跳转数据服务设置页or重定向(‘系统设置’菜单’/label/system-setting’默认重定向到’/label/system-setting/theme’,其他菜单默认重定向的都是基础公共页面,故需要对这里的重定向鉴权。系统设置的权限不是主题管理员就一定是数据服务管理员,所以能这样做);判断路由需求权限是否符合,若不符合直接重定向。// index.jsrouter.beforeEach(async (to, from, next) => { try { // get user login info const _userInfo = await API.get(’/common/query/menu’, {}, false) router.app.$store.dispatch(‘setLoginUser’, _userInfo) if (_userInfo && Object.keys(_userInfo).length > 0 && to.matched.some(record => record.meta.requireAuth)) { if (_userInfo.admin) { // super admin can pass next() } else if (to.fullPath === ‘/label/system-setting/theme’ && !_userInfo.topicAdmin) { if (_userInfo.dataServiceAdmin) { next({ path: ‘/label/system-setting/api’ }) } else { next({ path: ‘/label’ }) } } else if (!(_userInfo[to.meta.authRole])) { next({ path: ‘/label’ }) } } } catch (e) { router.app.$message.error(‘获取用户登陆信息失败!’) } next()}) ...

February 20, 2019 · 2 min · jiezi

基于iview-ui的导航栏路径(面包屑)配置

起因上家公司的后台管理系统都是刷表刷出来的,所用很久很久没写后台管理系统了。换了工作后总算要开始捣腾router了,很久没用都快忘光了,所以把一些通用的模块记录一下,也分享给需要的朋友们。经过//router.jslet routes = [ { path: ‘/’, redirect: ‘/admin’, }, { path: ‘/login’, name: ’login’, meta: {title: ‘登录’}, component: () => import(’./components/login.vue’) }, { path: ‘/admin’, name: ‘admin’, meta: {title: ‘主页’}, component: () => import(’./components/admin.vue’), children: [ { path: ‘operation’, name: ‘operation’, meta: {title: ‘运营管理’}, component: () => import(’./components/admin/operation.vue’) }, { path: ‘order’, name: ‘order’, meta: {title: ‘订单中心’}, redirect: ‘order/index’, component: () => import(’./components/admin/order.vue’), children: [ { path: ‘index’, name: ‘index’, meta: {title: ‘’}, component: () => import(’./components/admin/ordercenter.vue’) }, { path: ‘detail’, name: ‘detail’, meta: {title: ‘订单详情’}, component: () => import(’./components/admin/orderdetail.vue’) }, ] }, ] },]export default routes这个是我部分的router路径配置表 /面包屑路径处理/ eve_breadcrumbItem_change(){ var list = this.$route.fullPath.split(’/’)//list[0]:是空格 this.BreadcrumbItem = [] function fn(obj, arr, index,self) { if (obj.hasOwnProperty(‘children’)&&obj[‘children’].length>0) { for (let one of obj.children) { if (one.name != ‘index’ && one.name == arr[index]) { self.BreadcrumbItem.push({’title’: one.meta.title, ‘path’: one.path}) return one.hasOwnProperty(‘children’)&&one[‘children’].length>0?fn(one,arr,index+1,self):false } } } } for(let one of this.$router.options.routes){ if(one.hasOwnProperty(’name’)&&one.name == list[1]){ this.BreadcrumbItem.push({’title’: one.meta.title, ‘path’: one.path}) fn(one,list,2,this) } } }这个是就是本文的重点,其实也简单,就是递归了下路径名重新组装了下数据给面包屑传过去watch: { ‘$route’(to, from) { this.eve_breadcrumbItem_change() }},…mounted() { this.eve_breadcrumbItem_change()},使用也简单,无非watch检测下路径变化,避免刷新页面时没路径,在mounted里再调用一下。结果结果嘛,自然就解决问题。不过路径的配置可能会和大家的不同,我喜欢在分组下默认弄个index路径,我觉得这样结构比较好,这里大家注意下。 如果对你有帮助的话,帮忙点个赞或者收藏下啦!感谢感谢 ...

February 20, 2019 · 1 min · jiezi

219. 单页应用 会话管理(session、cookie、jwt)

原文链接:https://github.com/ly525/blog…关键字:http-only, cookie,sessionid, vue-router, react-router, 安全,localStorage, jwt需求描述内部管理平台,需要用户登录之后才能访问。现在将 该平台地址(www.xxx.com/home) 直接发给新来的运营同学前端需要检测该用户是否已登录,如果未登录,则将其 redirect 到登录页面因为该页面为单页应用,路由跳转不涉及后端的 302 跳转,使用前端路由跳转实现思路实现代码// 以 vue-router 为例// 登录中间验证,页面需要登录而没有登录的情况直接跳转登录router.beforeEach((to, from, next) => { const hasToken = document.cookie.includes(‘sessionid’); // 如果采用 jwt,则同样 hasToken = localStorage.jwt const pathNeedAuth = to.matched.some(record => record.meta.requiresAuth); // 用户本地没有后端返回的 cookie 数据 && 前往的页面需要权限 // if (pathNeedAuth && !hasToken ) { next({ path: ‘/login’, query: { redirect: to.fullPath }, }); } else if (hasToken && to.name === ’login’) { // 已登录 && 前往登录页面, 则不被允许,会重定向到首页 next({ path: ‘/’, }); } else { next(); }});应该在进入任何页面之前,判断:该页面是否需要权限才能访问:登录、注册页面不需要权限用户是否已经登录:本地 cookie (或者 localStorage)包含 session 相关信息 Cookie: csrftoken=YaHb…; sessionid=v40ld3x….如果 A页面需要权限 且 本地 cookie中包含了 sessionid 字段,则允许访问A页面,否则跳转到登录页面备注:sessionid 该字段由用户在登录之后,由后端框架通过 response.setCookie 写入前端 ,因此该字段需要和后端同学确认需要后端同学在 response header 中配置cookie中该字段的 http-only属性,允许前端读取 cookie。否则前端通过 document.cookie 读取到的 cookie 将不包含 sessionid 字段这个时候,可能会存在 js 读取cookie 导致不安全的情况,后端同学可以把 cookie 中的某个字段设置为 允许读取,其他 cookie 设置不允许读取,这样即使被第三方不安全脚本获取,也无法产生负面影响。如果用户已登录 && 在浏览器中输入了登录页地址,则将其重定向到首页更多说明这样做,前端就不必再向后端发起 API 做权限鉴定了(后端返回401,前端跳转到 401),减少不必要的API 请求,特别是如果在API响应时间过长的情况下,体验不太友好。用户修改 cookie,伪造 sessionid这样的话,前端就无能为力了,前端鉴权此时认为该用户合法。此时访问首页,将会调用获取数据的API。浏览器会将 用户伪造的 sessionid 带给后端,这时候就需要后端对 sessionid 进行较验了。比如校验前端带来的 sessionid 与数据库中的 sessionid 是否一致用户伪造的数据 sessionid 和 后端数据库中 sessionid 的概率 非常非常低,可以忽略不计,因为 sessionid 的位数一般在 32 位以上,因为里面包含了字母和数字,也就由 32 ^ 36 种可能结论:伪造没有意义,即使用户可以看到页面的样子,但是看不到数据采用 localStorage 而不是 sessionStorage 的原因(SessionStorage 失效场景)原因:sessionStorage 无法跨 Tab 共享用户在新打开一个 Tab,输入已经登录之后的某个页面通过target="_blank" 来打开新页面的时候,会导致会话失效在当前页面执行 Duplicate (复制 Tab),sessionStorage 失效 ...

February 20, 2019 · 1 min · jiezi

你可能不清楚的 Vue Router 深度用法(二)

此为 Vue Router 深度用法的第二篇,主要讲述动态路由匹配用法。第一篇的链接在此: https://segmentfault.com/a/11…动态路由匹配是用于把某种模式匹配到的所有路由,全都映射到同个组件。通过给路由路径一个变量,即变成动态路由,把变化的内容赋值给变量即可。例如文章详情页是一个组件,只有一个路由,从文章列表页点进来,变化的只是文章 id 而已。将其赋予给设定的变量,然后通过 watch ‘$route’ 或者使用 beforeRouteUpdate 导航守卫监测路由变化,传递新的文章 id 获取文章详情即可。在组件里,可以通过 this.$route.params.xx 获取当前文章 id。一个路由地址可以设置多个变量,适合有分叉情况的内容。例如 path: ‘/params/:foo/:bar’从文章列表页点进来即传递路由变量,有三种方法: (1)<router-link to="/params/list/1">跳转到 /params/list/1</router-link> (2)this.$router.push({ name: ‘articles’, params: { foo: ’list’, bar: 1 } }) (3)this.$router.push({ path: ‘/params/list/1’ }) // path 不能与 params 同时使用高级匹配模式这里主要研究的是动态路由匹配的高级匹配模式,以达到合并差异不大的路由、减少路由数量的目的。高级匹配即结合简单的正则匹配方法,给予路由更多的限制和操作空间。1、可选路由参数路由参数可选,添加与否都对应同一个组件。可以在组件里使用 v-if / v-show 结合 $route.params.xx 展现不同的内容// a param can be made optional by adding “?”{ path: ‘/optional-params/:foo?’ }// 这两个链接都对应同个组件<li><router-link to="/optional-params">/optional-params</router-link></li><li><router-link to="/optional-params/foo">/optional-params/foo</router-link></li>2、精确匹配参数只有参数通过正则匹配,完全符合格式,才能会跳转。例如只有参数是数字/手机号才允许跳转。适用于对第三方不规范格式的数据进行筛选。// a param can be followed by a regex pattern in parens// this route will only be matched if :id is all numbers{ path: ‘/params-with-regex/:id(\d+)’ }// 只匹配数字<li><router-link to="/params-with-regex/123">/params-with-regex/123</router-link></li>// 只匹配手机号{ path: ‘/params-with-regex/:id(^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$)’ }<li><router-link to="/params-with-regex/abc">/params-with-regex/13800138000</router-link></li>3、匹配任意参数不对参数格式、数量进行限制,任意参数都可。// asterisk can match anything{ path: ‘/asterisk/*’ }// 这两个都是同一组件<li><router-link to="/asterisk/foo">/asterisk/foo</router-link></li><li><router-link to="/asterisk/foo/bar">/asterisk/foo/bar</router-link></li>4、部分可选参数结合可选路由参数与多路由参数,其中一部分参数可选。适用于分叉情况下不确定参数数量的情况。// make part of the path optional by wrapping with parens and add “?”{ path: ‘/optional-group/(foo/)?bar’ }// 这两个都是同一组件<li><router-link to="/optional-group/bar">/optional-group/bar</router-link></li><li><router-link to="/optional-group/foo/bar">/optional-group/foo/bar</router-link></li> ...

February 16, 2019 · 1 min · jiezi

你可能不清楚的 Vue Router 深度用法(一)

Vue Router 简单易上手,能实现大部分的需求。但是,如果在项目里需要更细致的控制路由,以实现与其同步的效果,就需要挖掘其文档里没详细提及的内容。第一章为路由元信息用途挖掘。路由元信息用途(1)验证用户身份,定义用户权限能访问的页面大部分项目,除了登录页、重置密码页、用户协议页以外,页面都需要验证用户身份进行访问。使用 Vue Router 可以配合后端进行双重验证。(登录)验证身份方法:1、给需要验证的路由对象添加 meta 字段,里面自定义一个代表验证的字段:const router = new VueRouter({ routes: [ { path: ‘/foo’, component: Foo, children: [ { path: ‘bar’, component: Bar, meta: { requiresAuth: true // 添加该字段,表示进入这个路由是需要登录的 } } ] } ]})2、在全局导航钩子里验证 requiresAuth 字段:注意事项:使用 beforeEach 在路由变化前验证。验证原理是在跳转前,访问目标路由对象的 requiresAuth 字段判断是否需要验证用户身份,如为是,检测是否有保存用户信息(即用户登录成功后前端保存的信息,例如 token)每个路由都有一个 $route.matched 数组,包含当前路由的父级路由对象和当前路由对象,在组件中可以通过 this.$route.matched 访问beforeEach 的 to 参数即目标路由对象 $route,to.matched 即是它的路由数组因此,使用 some 方法,只要路由数组里的任意路由对象需要验证身份,即进行验证验证成功跳转正确页面;失败则跳到登录页,将目标地址附在 url 的 query 里,登录成功就跳转到目标地址router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { if (!auth.loggedIn()) { // 没登录 next({ path: ‘/login’, query: { redirect: to.fullPath } }) } else { next() // 确保一定要调用 next() } } else { next() // 确保一定要调用 next() }})3、拦截 http 请求,验证用户身份为了防止本地保存的 token 过期,需要拦截 http 请求,为每次请求头加上 token ,然后拦截 http 响应,根据响应内容判断是否需要跳回登录页面重新登录。使用 axios 的方法如下:// http request 拦截器axios.interceptors.request.use( config => { if (auth.loggedIn()) { // 判断是否存在token,如果存在的话,则每个http header都加上token config.headers.Authorization = token ${auth.loggedIn()}; } return config; }, err => { return Promise.reject(err); });// http response 拦截器axios.interceptors.response.use( response => { return response; }, error => { if (error.response) { switch (error.response.status) { case 401: // Unauthorized // 返回 401 清除token信息并跳转到登录页面 auth.clear(); router.replace({ path: ’login’, query: { redirect: router.currentRoute.fullPath } }) } } return Promise.reject(error.response.data) // 返回接口返回的错误信息 });前端查看权限,也是配合后端进行某些页面的隐藏显示功能。一般应用于综合的办公系统,由 IT 部分配账号,不同部门的人只能看到自己负责的内容,例如行政部不会看到财务数据页面。实现方法:前端路由每个页面的 meta 对象添加 level 字段,设置 0 ~ n 级别登录成功,后台返回用户 token 的同时,返回其所属的 level 字段组件代码比较目标页面的 level 与用户 level,只显示目标 level 小于等于用户 level 的页面全局导航钩子 beforeEach 里比较目标页面的 level 与用户 level,小于等于目标 level 则正确跳转,反之取消跳转并提示权限不足上面第4步是为了防止用户直接在浏览器输入目标地址(2)其他内容控制可以控制显示路由固定的搭配,例如某个路由地址的 title 是固定的字符串、固定的欢迎语、固定的 favicon 等。在组件里通过 this.$route.meta.xxx 访问。const router = new VueRouter({ routes: [ { path: ‘/foo’, component: Foo, children: [ { path: ‘bar’, component: Bar, meta: { title: ‘标题’, message: ‘欢迎您’, requiresAuth: true // 添加该字段,表示进入这个路由是需要登录的 } } ] } ]}) ...

February 16, 2019 · 2 min · jiezi

vue2.0+vue-router构建一个简单的列表页

一: 环境搭建使用vue-cli脚手架工具构建安装 vue-clinpm install -g vue-cli使用vue-cli初始化项目vue init demo1进到目录cd demo1安装依赖 npm install开始运行 npm run dev浏览器访问http://localhost:80801、首先会打开首页 也就是我们看到的index.html文件2、使用webpack打包之后默认加载main.js文件并将其引入到index.html文件中 二: 开发在main.js中可以引入相关模块以及组件import Vue from ‘vue’import App from ‘./App’import router from ‘./router’ //这里引入的是router目录,会默认识别里面的index.js文件(不能是其他名字)// 引入并使用vue-resource网络请求模块import VueResource from ‘vue-resource’Vue.use(VueResource)实例化vue对象配置选项路由及渲染App组件 new Vue({ el: ‘#app’, //这里绑定的是index.html中的id为app的div元素 router, render: h => h(App) // 这里的render: h => h(App)是es6的写法 // 转换过来就是: 暂且可理解为是渲染App组件 // render:(function(h){ // return h(App); // });})App.vue文件是我们的组件入口,之后所有的开发在这里面进行<template> <div id=“app”> <div class=“nav”> <!– 使用 router-link 组件来导航. –> <!– 通过传入 to 属性指定链接. –> <!– <router-link> 默认会被渲染成一个 &lt;a&gt; 标签 –> <ul> <li><router-link to="/home">Home</router-link></li> <li><router-link to="/about">About</router-link></li> </ul> </div> <div class=“main”> <!– 路由匹配到的组件将渲染在这里 –> <router-view></router-view> </div> </div></template><script>export default { name: ‘app’, components: { }}</script><style>body{ background-color: #f8f8ff; font-family: ‘Avenir’, Helvetica, Arial, sans-serif; color: #2c3e50;}.nav{ position: fixed; width: 108px; left: 40px;}.nav ul{list-style: none; margin: 0; padding: 0;}.nav ul li{ width: 108px; height: 48px; line-height: 48px;border:1px solid #dadada;text-align: center;}.nav ul li a{ text-decoration: none;}.main{ height: 400px; margin-left: 180px; margin-right: 25px;}</style> 要使用路由我们首先要在router/index.js文件中创建路由并配置路由映射 ,并通过export输出router到main.js文件中// 这里面负责写路由映射,便于管理import About from ‘@/components/About’import Home from ‘@/components/Home’import VueRouter from ‘vue-router’Vue.use(VueRouter)// 创建路由实例并配置路由映射 const router = new VueRouter({ mode: ‘history’, routes: [ { path: ‘/’, name: ‘Home’, component: Home }, { path: ‘/’, name: ‘About’, component: About }, ]})// 输出routerexport default router;上面配置了2个组件映射 分别Hme.vue组件和About组件,配置好之后我们就可以开始使用路由了<!– 使用 router-link 组件来导航. –> <!– 通过传入 to 属性指定链接. –> <!– <router-link> 默认会被渲染成一个 &lt;a&gt; 标签 –> <ul> <li><router-link to="/home">Home</router-link></li> <li><router-link to="/about">About</router-link></li> </ul> <!– 路由匹配到的组件将渲染在这里 –> <router-view></router-view>点击home和about导航会映射到对应的组件,然后将组件渲染在</router-view>这里面到此,整个流程我们已经走通了。 接下来我们使用vue-resource网络插件动态加载数据并显示出来1、安装插件npm install vue-resource –save2、在main.js文件中引入并使用vue-resource网络请求模块import VueResource from ‘vue-resource’Vue.use(VueResource)3、创建Home组件我们需要在created钩子函数中去请求网络,这里我们使用豆瓣的API去请求电影列表数据,请求成功之后我们将其数据显示到页面中<template> <div class=“home”> <h1>{{ msg }}</h1> <ul> <li v-for=“article in articles”> <div class=“m-img inl-block”><img v-bind:src=“article.images.small”/></div> <div class=“m-content inl-block”> <div>{{article.title}}</div> <div>年份:{{article.year}}</div> <div>类型:{{article.subtype}}</div> </div> </li> </ul> </div></template><script>// mounted 钩子函数 这里去请求豆瓣数据export default { name: ‘home’, data () { return { msg: ‘电影列表’, articles:[] } }, created:function(){ //这里mounted和created生命周期函数区别 this.$http.jsonp(‘https://api.douban.com/v2/movie/top250?count=10', {}, { headers: { }, emulateJSON: true }).then(function(response) { // 这里是处理正确的回调 console.log(response); this.articles = response.data.subjects // this.articles = response.data[“subjects”] 也可以 }, function(response) { // 这里是处理错误的回调 console.log(response) }); }}</script><!– Add “scoped” attribute to limit CSS to this component only –><style scoped>ul{ list-style: none; margin: 0; padding: 0;}ul li{border-bottom: 1px solid #999;padding: 10px 0;}.inl-block{display: inline-block;}.m-img{ }.m-content{margin-left: 20px;}</style>4、最后查看运行结果 ...

February 12, 2019 · 2 min · jiezi

Vue-router基本学习(1)

多页面应用:网页HTML文件是请求后台发过来的。每次切换页面都会从后台把页面文件传输回来。单页面应用:网页只有在第一次进入页面的、的时候请求服务器的HTML文件,接下来的页面跳转是基于内部的router。两种应用的优缺点:多页面应用只需要加载当前页面所需要的资源,所以首屏时间快。但是每切换一次页面都要去请求一次服务器资源。单页面应用第一次要将所有的资源全部加载,所以首屏时间慢,但是后续的跳转不需要再次向服务器发请求。多页面应用可以直接实现SEO搜索,但是单页面得有一套单独的SEO方案。单页面可以实现局部刷新,多页面实现不了。这里稍微的讲了一些单页面和多页面的一些知识,大家要知道 Vue 是一个单页面应用,其页面的跳转需要通过路由:Vue-router!!! vue-router的安装我们已经在前面的文章里讲过了,今天这篇文章就讲vue-router的使用。基本使用src/router/index.jsimport Vue from ‘vue’import Router from ‘vue-router’import Parent from ‘@/components/Parent’import Childs1 from ‘@/components/Childs1’import Childs2 from ‘@/components/Childs2’Vue.use(Router)export default new Router({ mode:‘history’, routes: [ { path:’/parent’, name:‘Parent’, component:Parent }, { path:’/child1’, name:‘Childs1’, component: Childs1 }, { path: ‘/child2’, name:‘Childs2’, component:Childs2 } ]})运行结果如下图:我们输入不同的路由不同的组件被渲染出。首先我们将需要在路由里面渲染的组件引入,然后配置路由。path:是我们需要配置的路径名,component: 是我们需要在该路径下渲染的组件。路由嵌套我们在开发的过程中不应该只有一级路由。比如上面的例子,child应该放在`parent的下面,name我们将怎么样实现路由的嵌套呢?当然是用路由嵌套啦~src/router/index.jsimport Vue from ‘vue’import Router from ‘vue-router’import Parent from ‘@/components/Parent’import Childs1 from ‘@/components/Childs1’import Childs2 from ‘@/components/Childs2’Vue.use(Router)export default new Router({ mode:‘history’, routes: [ { path:’/parent’, name:‘Parent’, component:Parent, children: [ {path:‘child1’, component: Childs1}, {path:‘child2’, component: Childs2} ] } ]})Parent.vue<template><div> Parent <router-view> </router-view></div></template>运行结果如下图:Parent.vue 的 <router-view> </router-view>是渲染其组路由组件的地方。我们可以看到url为/parent的时候,页面上只有paernt的字符串,当我们路由为两层的时候,parent和child全部展示在页面上。vue-router 会根据不同的路由加载不同的组件。动态路由如果我们要将某一种模式下的路由全部映射到同一个组件上,比如我们要将’/user/tom’ 和 ‘/user/David’ 都匹配到同样组件下面,那么动态路由是我们不二的选择。 src/router/index.jsimport Vue from ‘vue’import Router from ‘vue-router’import Parent from ‘@/components/Parent’import Childs1 from ‘@/components/Childs1’Vue.use(Router)export default new Router({ mode:‘history’, routes: [ { path:’/parent’, name:‘Parent’, component:Parent, children: [ {path: ‘child1/:name’, component:Childs1} ] } ]})Parent.vue<template><div> Parent <router-view></router-view></div></template>Childs1.vue<template> <div> Childs1– -{{$route.params.name}} </div></template>运行结果如下图:我们虽然在/child1后面输入不同的路径,但是最后全部映射到同一个组件上。this.$route.params对象存放我们的动态路由的内容。动态路由引起的组件复用动态路由就是将不同的路由映射到同一个组件上,如果两个路由是匹配到同一组件,那么Vue不会将当前组件销毁重建,而是直接替换不一样的内容,实现组件的复用。 src/router/index.js 同上Parent.vue<template><div> Parent <router-view></router-view></div></template>Childs1.vue<template> <div> Childs1– -{{$route.params.name}} <button @click=“change”>点我去aa</button> </div></template><script>export default { name:‘Childs1’, data(){ return{ title: 1 } }, methods:{ change(){ this.$router.push(’/parent/child1/aa’ + this.title++); } }, mounted(){ console.log(‘child1 mounted’,new Date().toLocaleString()); }}</script>运行结果如下图:我们使用编程式导航来进行路由切换,title的初始值唯一,在我们点击按钮进行页面切换的时候,title没有变成初始值,而是复用了前一个页面的组件,在原来的基础上自增。第二章图片也显示,只有第一次进入的时候进入了生命周期钩子,以后的页面切换不再进入钩子。编程式导航和声明式导航编程式导航是调用方法push进行路由跳转,声明式导航是类似于a标签一样的<router-link to=’/parent’></router-link>的标签进行跳转。to属性的内容就是我们要跳转的目标路由。声明式导航最终渲染到页面也是a标签。 声明式导航在被点击的时候会调用编程式导航的方法。Parent.vue*<template> <div> <ul> <router-link to=’/parent/child1/bb’> <li>点我去bb</li> </router-link> <router-link to=’/parent/child1/cc’> <li>点我去cc</li> </router-link> <router-link to=’/parent/child1/dd’> <li>点我去dd</li> </router-link> </ul> <router-view></router-view> </div></template>Childs1.vue同上 运行结果如下图:li的外面包裹着router-link,当我们点击的时候,路由就会跳转到我们to指向的URL,我们点击按钮的时候,调用了’this.$router.push(url)‘方法来进行跳转。这两种方法没有好与坏的区别,只是使用于不同的场景。router.push()push往history栈中加入一条记录,所以当我们使用浏览器的后退按钮时,还能够回到这一页。router.replace()replace是替换栈中当前页的记录,意味着history栈中不会再有当前页的记录。这种方法通常用来授权页,这样就不会有二次授权的情况出现。router.go()go是告诉浏览器在history栈中前进或者后退几步,所以我们一般的页面跳转用push才能在栈中新增一条记录,便于go使用。 ...

January 29, 2019 · 1 min · jiezi

vue-router源码解析(四)路由匹配规则

前面我们讲过,在使用 vue-router 的时候,主要有以下几个步骤:// 1. 安装 插件Vue.use(VueRouter);// 2. 创建router对象const router = new VueRouter({ routes // 路由列表 eg: [{ path: ‘/foo’, component: Foo }]});// 3. 挂载routerconst app = new Vue({ router}).$mount(’#app’);然后再进行路由跳转的时候,我们会有以下几种使用方式 。 详细使用请查看官方文档// 字符串router.push(‘home’);// 对象router.push({ path: ‘home’ });// 命名的路由router.push({ name: ‘user’, params: { userId: ‘123’ } });// 带查询参数,变成 /register?plan=privaterouter.push({ path: ‘register’, query: { plan: ‘private’ } });那么,你有没有想过, push 进去的对象是如何与我们之前定义的 routes 相对应的 ??接下来,我们一步步来进行探个究竟吧!匹配路由入口之前我们说过 push 方法的具体实现, 里面主要是通过 transitionTo 来实现路由匹配并切换 // src/history/hash.js // 跳转到 push(location: RawLocation, onComplete ? : Function, onAbort ? : Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) }所以我们来看看 transitionTo// src/history/base.js// 切换路由 transitionTo(location: RawLocation, onComplete ? : Function, onAbort ? : Function) { // 匹配路由 // 根据路径获取到匹配的路径 const route = this.router.match(location, this.current) // 跳转路由 this.confirmTransition(route, () => { // …more }, err => { // …more }) }这里看到, transitionTo 主要处理两件事匹配路由将匹配到的路由作为参数,调用 confirmTransition 进行跳转我们来看看具体如何匹配路由的 , 这里直接调用了匹配器的 match 方法// 获取匹配的路由对象 match( raw: RawLocation, current ? : Route, redirectedFrom ? : Location ): Route { // 直接调用match方法 return this.matcher.match(raw, current, redirectedFrom) }匹配器export default class VueRouter { constructor() { // …more // 创建匹配器 this.matcher = createMatcher(options.routes || [], this); // …more }}创建匹配器在 VueRouter 实例化的时候, 会通过我们之前设置的 routers , 以及 createMatcher 创建一个匹配器, 匹配器包含一个 match 方法,用于匹配路由// 文件位置: src/create-matcher.js// 创建匹配export function createMatcher( routes: Array<RouteConfig>, router: VueRouter): Matcher { // 创建 路由映射的关系 ,返回对应的关系 const { pathList, pathMap, nameMap } = createRouteMap(routes); // 添加 路由 function addRoutes(routes) { createRouteMap(routes, pathList, pathMap, nameMap); } // 匹配规则 function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 路径 const location = normalizeLocation(raw, currentRoute, false, router); const { name } = location; // 如果存在 name if (name) { // 找出匹配的 const record = nameMap[name]; if (!record) return _createRoute(null, location); // …more if (record) { location.path = fillParams( record.path, location.params, named route "${name}" ); return _createRoute(record, location, redirectedFrom); } } else if (location.path) { // 根据路径寻找匹配的路由 location.params = {}; for (let i = 0; i < pathList.length; i++) { const path = pathList[i]; const record = pathMap[path]; // 查找匹配的路由 if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom); } } } // no match return _createRoute(null, location); } // 创建路由 function _createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { // …more return createRoute(record, location, redirectedFrom, router); } return { match, addRoutes };}获取路由映射关系 createRouteMapexport function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord>): { pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>} { // the path list is used to control path matching priority // 数组,包括所有的 path const pathList: Array<string> = oldPathList || []; // $flow-disable-line // 对象 , key 为 path , 值为 路由对象 const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null); // $flow-disable-line // 对象 , key 为 name , 值为 路由对象 const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null); // 循环遍历 routes ,添加路由记录 routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route); }); // ensure wildcard routes are always at the end // 确保 * 匹配符放到最后面 for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === ‘*’) { pathList.push(pathList.splice(i, 1)[0]); l–; i–; } } return { pathList, pathMap, nameMap };}addRouteRecord 主要完成了几项工作生成 normalizedPath 复制给 record.path通过 compileRouteRegex 生成 record.regex , 用于后期的路由匹配将 record 分别加入到 pathMap 、 pathList、nameMap 里面// 添加路由记录对象function addRouteRecord( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string) { const { path, name } = route; // … const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}; const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ); if (typeof route.caseSensitive === ‘boolean’) { pathToRegexpOptions.sensitive = route.caseSensitive; } // 路由记录对象 const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }; // … if (!pathMap[record.path]) { pathList.push(record.path); pathMap[record.path] = record; } if (name) { if (!nameMap[name]) { nameMap[name] = record; } // … }}创建路由对象// 文件位置: src/util/route.js// 创建路由对象export function createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter): Route { const stringifyQuery = router && router.options.stringifyQuery; // 请求参数 let query: any = location.query || {}; try { query = clone(query); } catch (e) {} // 生成路由对象 const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || ‘/’, hash: location.hash || ‘’, query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), matched: record ? formatMatch(record) : [] }; if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery); } // 冻结路由对象,防止篡改 return Object.freeze(route);}createRoute 生成的对象,便是是我们经常用到的路由对象。 当前激活的路由信息对象则是this.$route路由匹配规则路由是否匹配 , 主要是通过 path-to-regexp , 来创建一个正则表达式 , 然后 , 通过这个正则来检查是否匹配import Regexp from ‘path-to-regexp’;// …more// 编译路径,返回一个正则function compileRouteRegex( path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp { const regex = Regexp(path, [], pathToRegexpOptions); // …more return regex;}关于 path-to-regexp ,这里主要讲几个例子。import Regexp from ‘path-to-regexp’;// 假如我们页面 path 为 /aboutlet reg = Regexp(’/about’, [], {}); // reg ==> /^/about(?:/(?=$))?$/i’/about’.match(reg); // ["/about", index: 0, input: “/about”, groups: undefined]’/home’.match(reg); // null// 假如我们页面 path 为 /about/:idlet reg = Regexp(’/about/:id’, [], {}); // reg ==> /^/about/((?:[^/]+?))(?:/(?=$))?$/i’/about’.match(reg); // null’/about/123’.match(reg); //["/about/123", “123”, index: 0, input: “/about/123”, groups: undefined]具体文档可参照这里 : path-to-regexp最后通过正则检查路由是否匹配, 匹配结果非 null 则表示路由符合预先设定的规则// 匹配路由规则function matchRoute(regex: RouteRegExp, path: string, params: Object): boolean { const m = path.match(regex); if (!m) { return false; } else if (!params) { // 没参数直接返回true return true; } // …more, 这里对参数做了一些处理 return true;}总结最后,对路由匹配做一个总结 。 路由匹配具体的步骤有:实例化的时候,创建匹配器 ,并生成路由的映射关系 。匹配器中包含 match 方法push 的时候,调用到 match 方法match 方法里面,从路由的映射关系里面,通过编译好的正则来判定是否匹配,返回最终匹配的路由对象transitionTo 中,拿到匹配的路由对象,进行路由跳转其他系列文章列表个人博客 ...

January 29, 2019 · 5 min · jiezi

vueSSR: 从0到1构建vueSSR项目 --- node以及vue-cli3的配置

前言上一次做了路由的相关配置,原本计划今天要做vuex部分,但是想了想,发现vuex单独的客户端部分穿插解释起来很麻烦,所以今天改做服务端部分。服务端部分做完,再去做vuex的部分,这样就会很清晰。vue ssr是分两个端,一个是客户端,一个是服务端。所以要做两个cli3的配置。那么下面就直接开始做吧。修改package.json的命令//package.json :client代表客户端 :server代表服务端//使用VUE_NODE来作为运行环境是node的标识//cli3内置 命令 –no-clean 不会清除dist文件 “scripts”: { “serve:client”: " vue-cli-service serve", “build”:“npm run build:server – –silent && npm run build:client – –no-clean –silent”, “build:client”: “vue-cli-service build”, “build:server”: “cross-env VUE_NODE=node vue-cli-service build”, “start:server”: “cross-env NODE_ENV=production nodemon nodeScript/index” }修改vue.config.js配置添加完相关脚本命令之后,我们开始改造cli3配置。首先要require(‘vue-server-renderer’)然后再根据VUE_NODE环境变量来决定编译的走向以及生成不同的环境清单先做cli3服务端的入口文件// src/entry/server.jsimport { createApp} from ‘../main.js’export default context => { return new Promise((resolve, reject) => { const { app, router } = createApp(context.data) //根据node传过来的路由 来调用router路由的指向 router.push(context.url) router.onReady(() => { //获取当前路由匹配的组件数组。 const matchedComponents = router.getMatchedComponents() //长度为0就是没找到该路由所匹配的组件 //可以路由设置重定向或者传回node node来操作也可以 if (!matchedComponents.length) { return reject({ code: 404 }) } resolve(app) }, reject) })}这里是cli3的配置//vue.config.jsconst ServerPlugin = require(‘vue-server-renderer/server-plugin’),//生成服务端清单 ClientPlugin = require(‘vue-server-renderer/client-plugin’),//生成客户端清单 nodeExternals = require(‘webpack-node-externals’),//忽略node_modules文件夹中的所有模块 VUE_NODE = process.env.VUE_NODE === ’node’, entry = VUE_NODE ? ‘server’ : ‘client’;//根据环境变量来指向入口module.exports = { css: { extract: false//关闭提取css,不关闭 node渲染会报错 }, configureWebpack: () => ({ entry: ./src/entry/${entry}, output: { filename: ‘js/[name].js’, chunkFilename: ‘js/[name].js’, libraryTarget: VUE_NODE ? ‘commonjs2’ : undefined }, target: VUE_NODE ? ’node’ : ‘web’, externals: VUE_NODE ? nodeExternals({ //设置白名单 whitelist: /.css$/ }) : undefined, plugins: [//根据环境来生成不同的清单。 VUE_NODE ? new ServerPlugin() : new ClientPlugin() ] }), chainWebpack: config => { config.resolve .alias .set(‘vue$’, ‘vue/dist/vue.esm.js’) config.module .rule(‘vue’) .use(‘vue-loader’) .tap(options => { options.optimizeSSR = false; return options; }); config.module .rule(‘images’) .use(‘url-loader’) .tap(options => { options = { limit: 1024, fallback:‘file-loader?name=img/[path][name].[ext]’ } return options; }); }}node相关配置用于node渲染 必然要拦截get请求的。然后根据get请求地址来进行要渲染的页面。官方提供了vue-server-renderer插件大概的方式就是 node拦截所有的get请求,然后将获取到的路由地址,传给前台,然后使用router实例进行push再往下面看之前 先看一下官方文档创建BundleRenderercreateBundleRenderer将 Vue 实例渲染为字符串。renderToString渲染应用程序的模板template生成所需要的客户端或服务端清单clientManifest先创建 服务端所需要的模板//public/index.nodeTempalte.html<!DOCTYPE html><html> <head> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <meta name=“viewport” content=“width=device-width,initial-scale=1.0”> <link rel=“icon” href="/favicon.ico"> <meta charset=“utf-8”> <title>vuessr</title> </head> <body> <!–vue-ssr-outlet–> </body></html>node部分先创建三个文件index.js //入口proxy.js //代理server.js //主要配置//server.jsconst fs = require(‘fs’);const { resolve } = require(‘path’);const express = require(’express’);const app = express();const proxy = require(’./proxy’);const { createBundleRenderer } = require(‘vue-server-renderer’)//模板地址const templatePath = resolve(__dirname, ‘../public/index.nodeTempalte.html’)//客户端渲染清单const clientManifest = require(’../dist/vue-ssr-client-manifest.json’)//服务端渲染清单const bundle = require(’../dist/vue-ssr-server-bundle.json’)//读取模板const template = fs.readFileSync(templatePath, ‘utf-8’)const renderer = createBundleRenderer(bundle,{ template, clientManifest, runInNewContext: false})//代理相关proxy(app);//请求静态资源相关配置app.use(’/js’, express.static(resolve(__dirname, ‘../dist/js’)))app.use(’/css’, express.static(resolve(__dirname, ‘../dist/css’)))app.use(’/font’, express.static(resolve(__dirname, ‘../dist/font’)))app.use(’/img’, express.static(resolve(__dirname, ‘../dist/img’)))app.use(’.ico’, express.static(resolve(__dirname, ‘../dist’)))//路由请求app.get(’’, (req, res) => { res.setHeader(“Content-Type”, “text/html”) //传入路由 src/entry/server.js会接收到 使用vueRouter实例进行push const context = { url: req.url } renderer.renderToString(context, (err, html) => { if (err) { if (err.url) { res.redirect(err.url) } else { res.status(500).end(‘500 | 服务器错误’); console.error(${req.url}: 渲染错误 ); console.error(err.stack) } } res.status(context.HTTPStatus || 200) res.send(html) })})module.exports = app;//proxy.jsconst proxy = require(‘http-proxy-middleware’);function proxyConfig(obj){ return { target:’localhost:8081’, changeOrigin:true, …obj }}module.exports = (app) => { //代理开发环境 if (process.env.NODE_ENV !== ‘production’) { app.use(’/js/main*’, proxy(proxyConfig())); app.use(’/hot-update’,proxy(proxyConfig())); app.use(’/sockjs-node’,proxy(proxyConfig({ws:true}))); }}//index.jsconst app = require(’./server’)app.listen(8080, () => { console.log(’\033[42;37m DONE \033[40;33m localhost:8080 服务已启动\033[0m’)})做完这一步之后,就可以预览基本的服务渲染了。后面就只差开发环境的配置,以及到node数据的传递(vuex) npm run build npm run start:server 打开localhost:8080 F12 - Network - Doc 就可以看到内容最终目录结构|– vuessr |– .gitignore |– babel.config.js |– package-lock.json |– package.json |– README.md |– vue.config.js |– nodeScript //node 渲染配置 | |– index.js | |– proxy.js | |– server.js |– public//模板文件 | |– favicon.ico | |– index.html | |– index.nodeTempalte.html |– src |– App.vue |– main.js |– router.config.js//路由集合 |– store.config.js//vuex 集合 |– assets//全局静态资源源码 | |– 备注.txt | |– img | |– logo.png |– components//全局组件 | |– Head | |– index.js | |– index.scss | |– index.vue | |– img | |– logo.png |– entry//cli3入口 | |– client.js | |– server.js | |– 备注.txt |– methods//公共方法 | |– 备注.txt | |– mixin | |– index.js |– pages//源码目录 | |– home | | |– index.js | | |– index.scss | | |– index.vue | | |– img | | | |– flow.png | | | |– head_portrait.jpg | | | |– logo.png | | | |– vuessr.png | | |– vue | | | |– index.js | | | |– index.scss | | | |– index.vue | | |– vueCli3 | | | |– index.js | | | |– index.scss | | | |– index.vue | | |– vueSSR | | | |– index.js | | | |– index.scss | | | |– index.vue | | |– vuex | | |– index.js | | |– index.scss | | |– index.vue | |– router//路由配置 | | |– index.js | |– store//vuex配置 | |– all.js | |– gather.js | |– index.js |– static//cdn资源 |– 备注.txtgithub欢迎watch ...

January 28, 2019 · 3 min · jiezi

vue路由篇(动态路由、路由嵌套)

什么是路由?网络原理中,路由指的是根据上一接口的数据包中的IP地址,查询路由表转发到另一个接口,它决定的是一个端到端的网络路径。web中,路由的概念也是类似,根据URL来将请求分配到指定的一个’端’。(即根据网址找到能处理这个URL的程序或模块)使用vue.js构建项目,vue.js本身就可以通过组合组件来组成应用程序;当引入vue-router后,我们需要处理的是将组件(components)映射到路由(routes),然后在需要的地方进行使用渲染。一、基础路由1、创建vue项目,执行vue init webpack projectName命令,默认安装vue-router。项目创建后,在主组件App.vue中的HTML部分:<template> <div id=“app”> <router-view/> </div></template>上述代码中,<router-view/>是路由出口,路由匹配到的组件将渲染在这里。2、文件router/index.js中:import Vue from ‘vue’ // 导入vue插件import Router from ‘vue-router’ // 导入vue-routerimport HelloWorld from ‘@/components/HelloWorld’ // 导入HelloWorld组件Vue.use(Router) // 引入vue-routerexport default new Router({ routes: [ { path: ‘/’, // 匹配路由的根路径 name: ‘HelloWorld’, component: HelloWorld } ]})以上代码比较简单,一般的导入、引用操作,其中Vue.use()具体什么作用?个人理解:Vue.use(plugin, arguments)就是执行一个plugin函数,或执行plugin的install方法进行插件注册(如果plugin是一个函数,则执行;若是一个插件,则执行plugin的install方法…);并向plugin或其install方法传入Vue对象作为第一个参数;如果有多个参数,use的其它参数作为plugin或install的其它参数。(具体需要分析源码,在此不再过多解释)二、动态路由什么是动态路由?动态路由是指路由器能够自动的建立自己的路由表,并且能够根据实际情况的变化实时地进行调整。1、在vue项目中,使用vue-router如果进行不传递参数的路由模式,则称为静态路由;如果能够传递参数,对应的路由数量是不确定的,此时的路由称为动态路由。动态路由,是以冒号为开头的(:),例子如下:export default new Router({ routes: [ { path: ‘/’, name: ‘HelloWorld’, component: HelloWorld }, { path: ‘/RouterComponents/:id’, name: ‘RouterComponents’, component: RouterComponents } ]})2、路由跳转,执行方式有两大类;第一大类:router-link模式,直接把参数写在to属性里面:<router-link :to="{name:‘RouterComponents’,params:{id:110}}">跳转</router-link>第二大类:$router.push()模式,代码如下:methods: { changeFuc (val) { this.$router.push({ name: ‘RouterComponents’, params: {id: val} }) }}或者:methods: { changeFuc (val) { this.$router.push({ path: /RouterComponents/${val}, }) }}三、嵌套路由vue项目中,界面通常由多个嵌套的组件构成;同理,URL中的动态路由也可以按照某种结构对应嵌套的各层组件:export default new Router({ routes: [ { path: ‘/’, // 根路由 name: ‘HelloWorld’, component: HelloWorld }, { path: ‘/RouterComponents/:id’, component: RouterComponents, children: [ { path: ‘’, // 默认路由 name: ‘ComponentA’, // 当匹配上RouterComponents后,默认展示在<router-view>中 component: ComponentA }, { path: ‘/ComponentB’, name: ‘ComponentB’, component: ComponentB }, ] } ]}) ...

January 26, 2019 · 1 min · jiezi

vue-router 一些容易被忽略的知识点

本文适用于对 Vue.js 和 vue-router 有一定程度了解的开发者除特殊说明,vue-router 版本为 3.0.2正文路由 class 匹配<router-link> 路由匹配后会给该标签添加 class 属性值 .router-link-active,该功能在嵌套路由中十分方便class 的实际属性值可以通过 <router-link> 的 active-class 来控制,全局默认值则通过路由构造选项 linkActiveClass 来控制默认情况下,当前路由的所有父级会默认添加 active-class ,即 当前处于/user/1 会给当前页面的 <router-link to="/"> 添加 active-class ,如果不需要此项,给 <router-link> 添加 exact 即可,精准匹配的 class 通过 exact-active-class 控制示例:JSFiddle通配符路由路由配置: {path: ‘/user-’} ,访问 /user-admin 路由,可在 $route.params.pathMatch 获取到 ‘admin’ (pathMatch 仅在 vue-router@3.0.2+ 可用,低于此版本使用 $route.params[0] 尝试获取)示例:JSFiddle文档:捕获所有路由或 404 Not found 路由路由高级匹配模式vue-router 使用 path-to-regexp 作为路由匹配引擎,该库可以通过输入的路径生成匹配规则的正则表达式,从而实现路由匹配功能。path-to-regexp 中常用的方法 pathToRegexp(path, keys, options) 第三个参数为 pathToRegexpOptions 编译正则的选项,在 vue-router@2.6.0+ 版本可以通过配置 route 的 pathToRegexpOptions 参数添加高级配选项。参考例子,其可通过 ‘/optional-params/:foo?’ 实现可选 param ,也可通过 ‘/params-with-regex/:id(\d+)’ 实现仅匹配数字 param(非命中路由向后匹配)。pathToRegexpOptions 的内容为:sensitive 大小写敏感 (default: false)strict 末尾斜杠是否精确匹配 (default: false)end 全局匹配 (default: true)start 从开始位置展开匹配 (default: true)delimiter 指定其他分隔符 (default: ‘/’)endsWith 指定标准的结束字符whitelist 指定分隔符列表 (default: undefined, any character)源码:vue-router/src/create-route-map.js:154 文档:高级匹配模式编程式导航的钩子处理在 vue-router@2.2.0+,可选的在 router.push 或 router.replace 中提供 onComplete 和 onAbort 回调作为第二个和第三个参数。这些回调将会在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。该功能可用在少数埋点场景,而不用配置复杂的路由钩子。路由重定向给 route 配置 redirect 属性可使路由重定向到指定路由,该属性支持 String/Object/Function 三种类型的值,其中 Function 的参数为 to 对象给重定向的中间路由添加 beforeEach 和 beforeLeave 不会有效果,给 router 添加的钩子也不能检测到此次重定向,如果需要判断重定向来源,可使用路由对象 $route.redirectedFrom 判断该功能适合路由 path 修改后保留原路由的重定向文档:重定向嵌套命名视图在平级展示多个视图时(单个视图使用多个平级的<router-view>),可以用到 <router-view> 的 name prop例如在 sidebar/list 的布局页面上,不用在父级视图容器去书写许多子组件的逻辑,只需要在路由配置中配置好相关页面组件,从而进行组件关系解耦,也能高效控制子视图渲染例子:JSFiddle文档:嵌套命名视图路由别名 给 route 配置 alias 属性可以使访问者保持原有 url 却访问到指定路由中去。该属性支持 String 和 Array 两种类型,当 alias 与其他路由重复时,以先申明的路由为准,同时别名不会进行路由 class 匹配 文档:别名路由组件传参该功能旨在给组件与路由解除耦合关系,给 route 配置 props: true 同时组件内 props 配置与 prams 相同的变量,可以直接通过访问 props 而不用通过 $route.params 去访问参数如果 props 是一个对象,对象内容会当作静态内容传入组件作为 props 当 props 为一个函数,函数接收一个 route 参数,可以使 query 作为 props 传入组件或实现更多高级功能文档:路由组件传参完整的导航解析流程导航被触发。在失活的组件里调用离开守卫。调用全局的 beforeEach 守卫。在重用的组件里调用 beforeRouteUpdate 守卫 (vue-router@2.2+)。在路由配置里调用 beforeEnter。解析异步路由组件。在被激活的组件里调用 beforeRouteEnter。调用全局的 beforeResolve 守卫 (vue-router@2.5+)。导航被确认。调用全局的 afterEach 钩子。触发 DOM 更新。用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。文档:完整的导航解析流程滚动行为创建 Router 实例,可以提供一个 scrollBehavior 方法,该方法接收 to 、from 、savedPosition(该页面原存在的xy值,仅在通过浏览器前进后退中可用)在该方法中返回 {selector:to.hash} 还可实现类似于“滚动到锚点”的行为,vue-router@2.6.0+ 还可返回 {offset?:{x,y}} 进行位置偏移,注意该偏移负值为向负方向偏移其 异步滚动 通常用于小众的过渡组件(transition)和滚动行为同时进行的情况下,官方实例未给太多相关信息文档:滚动行为组件懒加载-按组分块SPA(single page application)由于 All in JS 的特性,会使得首屏加载比较慢,很多人都推荐使用 Webpack 的 代码分割功能减小单个 JS 体积,当所有页面组件使用动态加载则会使页面请求过多而得不偿失,所以组件按组分块则应运而生: const Foo = () => import(/ webpackChunkName: “group-foo” / ‘./Foo.vue’) const Bar = () => import(/ webpackChunkName: “group-foo” / ‘./Bar.vue’) const Baz = () => import(/ webpackChunkName: “group-foo” */ ‘./Baz.vue’)该功能需要 webpack@2.4+ 支持 文档:把组件按组分块获取路由匹配组件router.getMatchedComponents(location?)该函数可以获取传入参数在路由表中匹配的路由对象数组,官方文档中写到通常在服务端渲染数据预加载的时候,也可用于在获取当前路由对象数组的时候如果需要获取当前路由记录(就是路由构造选项 routes 配置数组中的对象副本,包含children 数组),可用 route.matched文档:getMatchedComponents文档:$route.matched解析路由router.resolve(location, current?, append?)该函数可同时导出一个类似浏览器的 location 对象和一个根据匹配到的路由记录 resolved ,如果没有匹配到对应的对象,resolved 字段默认返回 404 组件或错误数据文档:router.resolve添加路由 router.addRoutes(routes:Array<RouteConfig>)该函数可以用户触发添加路由到路由表中,可以尝试在用户权限控制中使用文档:router.addRoutes建议简单按钮的路由跳转逻辑不使用 v-on:click 事件,多使用 <router-link> 标签。如果 SPA 放置路径处于域名的子目录中,不要按照一些网络教程写的去修改 webpack 配置,应该修改 Router 构建选项 中的 base 值,这样能避免一些不必要的问题不要尝试改变组件内的 $route 的内容,这个属性是只读,里面的属性是 immutable 状态,但你可以 watch 这个参考资料url 的正则表达式:path-to-regexp - 简书vue-router Documentvue-router Source Code本文首发地址blog.shoyuf.top第一次在 segmentfault 上发文章,欢迎各位评论区中吐槽指正 ...

January 25, 2019 · 2 min · jiezi

基础知识:vue-cli项目搭建及配置

vue-cli脚手架搭建// 准备 npm install vue-cli -g确保已全局安装node、npm // 下载 vue init webpack my-project下载脚手架项目// 进入 cd my-project // 运行 npm start把服务跑起来 // 访问 localhost:8080// 了解 以下为vue-cli的结构图config/index.js端口// 更改端口port: 3000,打开浏览器// 是否在编译完成后,// 自动打开浏览器访问http://localhost:3000autoOpenBrowser: false, 代理proxyTable: { // 接口跨域 // 解决跨域的问题 ‘/lots-web/**’: { target: ‘http://localhost:8080’ // 接口的域名 }, // 开发环境本地数据挂载 // json数据文件路径:static/mock/index.json ‘/api’: { target: ‘http://localhost:7000’, pathRewrite: { ‘^/api’: ‘/static/mock’ } }}build/webpack.base.conf.js路径配置alias: { ‘vue$’: ‘vue/dist/vue.esm.js’, ‘@’: resolve(‘src’), // styles ‘styles’: resolve(‘src/assets/styles’)} scss预编译// 安装模块:// npm install –save-dev sass-loader// npm install –save-dev node-sass{ test: /.sass$/, loaders: [‘style’, ‘css’, ‘sass’]}修改文件:xxx.vue<style lang=‘scss’ scoped><style>build/utils.jsvariables.scss预编译// 全局引用variables.scss变量文件 // 安装模块:// npm install –save-dev sass-resources-loaderscss: generateLoaders(‘sass’).concat( { loader: ‘sass-resources-loader’, options: { resources: path.resolve(__dirname, ‘../src/assets/styles/variables.scss’) } })variables.styl预编译// 全局引用variables.styl变量文件 // 安装模块:// npm install –save-dev stylus// npm install –save-dev stylus-loaderconst stylusOptions = { import: [ path.join(__dirname, “../src/assets/styles/variables.styl”) ]}return { stylus: generateLoaders(‘stylus’, stylusOptions), styl: generateLoaders(‘stylus’, stylusOptions)}src/main.js初始 主文件import Vue from ‘vue’import App from ‘./App’import router from ‘./router’ // 路由// 设置为 false 以阻止 vue 在启动时生成生产提示Vue.config.productionTip = false new Vue({ el: ‘#app’, router, components: {App}, template: ‘’}) 详细 主文件// 入口 import router from ‘./router’ // 路由import store from ‘./store’ // 状态import api from ‘./utility/api’ // 接口 // 模块 import axios from ‘axios’ // 接口调用import ‘babel-polyfill’ // ie9和一些低版本的浏览器对es6新语法的不支持问题import fastClick from ‘fastclick’ // 延迟300毫秒import VueAwesomeSwiper from ‘vue-awesome-swiper’ // 图片轮播import BScroll from ‘better-scroll’ // 拖动插件 // 公用样式 import ‘styles/reset.css’ // 重置import ‘styles/common.css’ // 公共import ‘styles/border.css’ // 1像素边import ‘styles/iconfont.css’ // 文字图标import ‘swiper/dist/css/swiper.css’ // 图片轮播// 公用组件 import Fade from ‘@/pages/common/fade’ // 显隐过渡import Gallery from ‘@/pages/common/gallery’ // 画廊Vue.component(‘c-fade’, Fade)Vue.component(‘c-gallery’, Gallery) // 全局调用 Vue.use(VueAwesomeSwiper) // 图片轮播Vue.prototype.$scroll = BScrollVue.prototype.$http = axiosVue.prototype.$api = api // 其他 fastClick.attach(document.body) // 为消除移动端浏览器,从物理触摸到触发点击事件之间的300ms延时的问题 // 创建Vue new Vue({ el: ‘#app’, router, store, components: {App}, template: ‘<App/>’})src/App.vuevue入口文件// keep-alive数据缓存 <keep-alive :exclude=“exclude”> <router-view/></keep-alive>data(){ return { exclude: [ ‘Detail’ ] }}// 用法 // 1、如不填,则缓存全部组件// 2、keep-alive include=“City”,缓存name=‘City’的组件// 3、keep-alive exclude=“Detail”,不缓存name=‘Detail’的组件// 生命周期 // 当时用keep-alive的时候,会触发activated和deactivated生命周期// activated 当组件被激活的时候调用// deactivated 当组件被移除的时候调用src/router/index.js路由文件人口// 初始 import Vue from ‘vue’import Router from ‘vue-router’import Home from ‘@/pages/home/home’export default new Router({ routes: [ { path: ‘/home’, component: Home } ])} // 组件和路由路径 import Home from ‘@/pages/home/home’const path = { home: ‘/’} // 路由和组件渲染 routes: [ { path: path.home, component: Home }] // 路由"#“号去除 mode: ‘history’, // 当前路由添加.activelinkActiveClass: ‘active’,linkExactActiveClass: ‘active’, // 切换路由时界面始终显示顶部 scrollBehavior(to, from, savePosition){ return { x: 0, y: 0 }} // vue用$route获取router // $router// 路由:’/detail/:name’ // ‘/detail/lzm’// this.$route.params.name// 路由:’/detail’ // ‘detail?name=lzm’// this.$route.query.name src/store/index.js状态管理文件入口// 状态管理 import Vue from ‘vue’import Vuex from ‘vuex’import state from ‘./state’import mutations from ‘./mutations’import actions from ‘./actions’Vue.use(Vuex)export default new Vuex.Store({ state, mutations, actions}) // 第四步state.js let defaultCity = { id: 1, spell: ‘beijing’, name: ‘北京’}if(localStorage.city){ defaultCity = JSON.parse(localStorage.city) // 字符串转对象}const state = { city: defaultCity}export default state// 第三步mutations.js const mutations = { changeCity(state, payload){ state.city = payload localStorage.city = JSON.stringify(payload) // 当为对象是,要转字符串 }}export default mutations// 第二步actions.js const actions = { changeCity(context, payload){ context.commit(‘changeCity’, payload) }}export default actions// 第一步xxx.vue // template部分:<div @click=“handleClick(item)">{{city}}</div>// script部分:import { mapState, mapActions} from ‘vuex’export default { computed: { …mapState([‘city’]) }, methods: { …mapActions([‘changeCity’]) handleClick(city){} this.changeCity(city) } }}axios$http调用接口// 全局调用 // 修改main.js文件Vue.prototype.$http = axios// xxx.vue文件:this.$http .get(’/api/index.json’, { params: {…} }) .then( fn(data) ).catch( fn(data )); // 局部调用 // xxx.vue文件:import axios from ‘axios’axios.get(’/api/index.json’) .then( fn(data) ) .catch( fn(data) ); // 案例 getHomeData() { this.$http .get(this.$api.home, { params: { name: this.city.name } }) .then(res => { let resData = res.data; if (resData.ret && resData.data) { let data = resData.data this.bannerList = data.bannerList this.iconList = data.iconList this.likeList = data.recommendList this.weekendList = data.weekendList } }) .catch((err) => { console.log(err) })}移动端移动端初始配置// 移动端访问 // 修改package.json文件:–host 0.0.0.0// mac查看ip: ifconfig// windows查看ip: ipconfig// 手机访问地址: ip:7000 // 缩放比例 // 修改文件:index.html<meta minimum-scale=1.0, maximum-scale=1.0, user-scalable=no><meta width=“device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no”> // 初始化样式 reset.css // 1px像素设备不一致 borer.css // 延迟300ms // 模块 fastclick 解决移动端事件延迟300ms// main.js文件加入:fastClick.attach(document.body) // stylus运用 // 模块 stylus stylus-loader 类似scss预编译// xxx.vue文件:style lang=“stylus"iconfont文字图标// 网址 [http://www.iconfont.cn][2] // 步骤 // 选择图标加入购物车 →// 添加到项目 →// 下载到本地 →// 字体和css复制到项目 →// 更改iconfont目录和删除代码 →// 引入iconfont.css →<span class=“iconfont user-icon”>&#x e624;><span>better-scroll拖动使界面滚动// 全局 // 修改main.js文件:import BScroll from ‘better-scroll’Vue.prototype.$scroll= BScrollxxx.vue文件:mounted() { this.scroll = new this.$scroll(elevent); // element为dom或$refs}, // 局部 // xxx.vue文件:import BScroll from ‘better-scroll’this.scrolll = new BScroll(this.$refs.wrapper)// 定位到某个元素 this.scroll.scrollToElement(element) vue-awesome-swiper图片轮播// 网址 [https://www.swiper.com.cn/][3] // 步骤 // 安装模块 vue-awesome-swiper 轮播插件(2.6.7)// 修改main.js:import “swiper/dist/css/swiper.css"Vue.use(VueAwesomeSwiper)xxx.vue文件:<swiper :options=“swiperOption” v-if=“hasBanner”> <swiper-slide v-for=“item in list”> <img :src=“item.imgUrl”> </swiper-slide> <div class=“swiper-pagination” slot=“pagination”></div></swiper>swiperOption: { pagination: ‘.swiper-pagination’, autoplay: 3000, paginationType: ‘fraction’, loop: true}router-link路由链接// router-link组件的active设置 // 全局设置:export default new VueRouter({ linkActiveClass: ‘active’, linkExactActiveClass: ‘active’, routes : [ … 省略 ]});// 局部设置:<router-link :to="‘home’” tag=“li” active-class=“active” exact-active-class=“active” exact><标题router-link>// 加上exact 则遵循路由完全一致模式,该路由下的路径不匹配 // 标准 <router-link :to=”’/detail?name=’ + item.name” tag=“div”></router-link> // 路由跳转 this.$router.push(’/’)、this.$router.replace(’/’) fade过渡动画// template <transition> <slot></slot></transition> // style <style lang=“stylus” scoped>.v-enter, .v-leave-to opacity 0.v-enter-active, .v-leave-active transition opacity .5s</style>// name name=“fade”.fade-entertouch拖动事件// touchstart/touchmove <div class=“letter” @click=“handleClickLetter” @touchstart.prevent=“handleTouchStart” @touchmove=“handleTouchMove” ref=“letter”> <div class=“letter-cont” ref=“letterCont”> <span class=“letter-item” v-for=“item in list” :key=“item”>{{item}}</span> </div></div>methods: { handleClickLetter(e) { const letter = e.target.innerHTML this.$emit(‘change’, letter) }, handleTouchStart() { this.letterHeight = this.$refs.letterCont.clientHeight / this.letterLen this.letterOffsetTop = this.$refs.letter.offsetTop + this.$refs.letterCont.offsetTop }, handleTouchMove(e) { let touchY = e.touches[0].clientY let letterScope = (touchY - this.letterOffsetTop) / this.letterHeight if (letterScope > 0 && letterScope < this.letterLen) { if (this.timer) clearTimeout(this.timer) this.timer = setTimeout(() => { this.letterIndex = Math.floor(letterScope) }, 16) } }}其他组件里name的作用// 1、递归组件会用到// 2、取消缓存的时候会用到// 3、浏览器vue插件显示组件的时候用到子父组件传值// 子组件:handleClick(){ this.$emit(‘change’, value)}// 父组件:<component @change=“handleClick”></component>handleClick(){} $refs// this.$refs.msg 普通元素,引用指向dom:<div ref=‘msg’>Hello, world<div>// this.$refs.child 组件,引用指向组件实例:<c-child ref=‘child’>Hello, world<c-child>$route获取url数据// 1. /detail/:id// /detail/1389435894this.$route.params.id// 2. /detail// /detail?id=1389435894this.route.query.id利用setTimeout节流if(this.timer) clearTimeout(timer);this.timer = setTimeout(()=>{ console.log(‘‘xxx’)}, 16);滚动监听// 1.window.onscroll = this.handleScroll// 2.window.addEventListener(‘scroll’, this.handleScroll)浏览器cookielocalStorage.city = citylocalStorage.clear() ...

January 25, 2019 · 4 min · jiezi

收集vue2精佳文章,一月下半月 - 岂敢定居,一月三捷

一月: 一年之计在于春,一月是一年的开始一月下半月-岂敢定居,一月三捷。(01.16~01.31):寄徐丞 [宋] 赵师秀 病不窥园经一月,更无人迹损青苔。 池禽引子衡鱼去,野蔓开花上竹来。 亦欲鬓毛休似雪,争如丹汞只为灰。 秋风昨夜吹寒雨,有梦南游到海回。文章列表【收藏】2019年最新Vue相关精品开源项目库汇总创建vue-cli框架项目VuePress博客搭建笔记(二)个性化配置【前端笔记】Vuex快速使用vue -on如何绑定多个事件基于vue的验证码组件Vue自定义Toast插件Vue添加数据视图不更新问题聊一聊Vue组件模版,你知道它有几种定义方式吗?深入学习Vue SSR服务端渲染 用Nuxt.js打造CNode社区vue 源码学习(二) 实例初始化和挂载过程使用NodeJS 生成Vue中文版 docSet 离线文档手牵手教你写 Vue 插件Vue项目部署遇到的问题及解决方案预计今年发布的 Vue 3.0 到底有什么不一样的地方?记一次 Vue 单页面上线方案的优化vue-cli3使用svg问题的简单解决办法从react转职到vue开发的项目准备基于 Vue-Cli3 构建的脚手架模版新手福音用vue-cli3从0到1做一个完整功能手机站(一)vue-cli3 从搭建到优化Spring Security (三):与Vue.js整合电商网站项目总结:Vuex 带来全新的编程体验结合vue-cli来谈webpack打包优化vue开发环境配置跨域,一步到位Vue新手向:14篇教程带你从零撸一个Todo应用Vue2.0 核心之响应式流程基于Vue的任务节点图绘制插件(vue-task-node)Vue 实践小结vue项目接口管理【vue-cli3升级】老项目提速50%(二)Vuex 是什么,为什么需要【收藏】2019年最新Vue相关精品开源项目库汇总创建vue-cli框架项目VuePress博客搭建笔记(二)个性化配置RFCs for substantial changes / feature additions to Vue core 3.0February 14-15 - Vue.js AmsterdamMarch 25-27 - VueConf US in Tampa, FLApril 12th, VueDay 2019, Verona, Italy – Call for papers until 1st FebruaryIncoming workshop: Proven patterns for building Vue apps by Chris Fritz10 Best Tips for Learning Vue from Vue MastersImprove performance on large lists in VueBuild a Beautiful Website with VuePress and Tailwind.cssSelectively enable SSR or SPA mode in a Nuxt.js appGive Users Control Over App Updates in Vue CLI 3 PWAsWhen to “componentize” from the point of VueVue Route Component HooksWatch for Vuex State changes!GitHub - egoist/styled-vueGitHub - Justineo/vue-clampGitHub - edisdev/vue-datepicker-uiVue2+周报不积跬步,无以至千里;不积小流,无以成江海丁酉年【鸡年】/戊戌年【狗年】/己亥年【猪年】 对【Vue相关开源项目库汇总】的Star更新排名几个值得收藏的国外有关Vue.js网站(https://segmentfault.com/a/11… :conf.vuejs.org国外一个Vue.js视频教程scotch网站的技术视频教程vue-hackernews-2.0Weekly dose of handpicked Vue.js news!vuejsdevelopers-Vue开发者网站还是个人的?vuejsfeed-最新的Vue.js新闻、教程、插件等等vuecomponents-Vue.js组件集合社区madewithvuejs-收藏了用Vue.js实现的网站vuejsexamples-Vue.js的Demo满满的最新vueNYC资讯:VueNYCVueNYC - Vue.js: the Progressive Framework - Evan Youtwitter-search-VueNYC尤大大的PPT我已经上传了../PPT/Vue.js the Progressive Framework.pdf最新2018 VueConf资讯: (第二届VueConf将于2018年11月24日在杭州举行) ::资料:: ::PPT::更多资讯集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。:::【点击我】::: ...

January 25, 2019 · 1 min · jiezi

vue-router源码解析(三)路由模式

路由模式及降级处理vue-router 默认是 hash 模式 , 即使用 URL 的 hash 来模拟一个完整的 URL ,于是当 URL 改变时,页面不会重新加载。vue-router 还支持 history 模式,这种模式充分利用了 history.pushState 来完成 URL 跳转。在不支持 history.pushState 的浏览器 , 会自动会退到 hash 模式。是否回退可以通过 fallback 配置项来控制,默认值为 trueconst router = new VueRouter({ mode: ‘history’, // history 或 hash routes: […]});详细使用可参看文档: HTML5 History 模式根据 mode 确定类型首先看下 VueRouter 的构造方法 , 文件位置 src/index.jsimport { HashHistory } from ‘./history/hash’import { HTML5History } from ‘./history/html5’import { AbstractHistory } from ‘./history/abstract’ // … more constructor(options: RouterOptions = {}) { // … more // 默认hash模式 let mode = options.mode || ‘hash’ // 是否降级处理 this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false // 进行降级处理 if (this.fallback) { mode = ‘hash’ } if (!inBrowser) { mode = ‘abstract’ } this.mode = mode // 根据不同的mode进行不同的处理 switch (mode) { case ‘history’: this.history = new HTML5History(this, options.base) break case ‘hash’: this.history = new HashHistory(this, options.base, this.fallback) break case ‘abstract’: this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== ‘production’) { assert(false, invalid mode: ${mode}) } } }我们可以看到,会判断是否支持 history , 然后根据 fallback 来确定是否要降级。然后,根据不同的 mode , 分别实例化不同的 history 。 (HTML5History、HashHistory、AbstractHistory)history我们看到 , HTML5History、HashHistory、AbstractHistory都是来自 history 目录。├── history // 操作浏览器记录的一系列内容│ ├── abstract.js // 非浏览器的history│ ├── base.js // 基本的history│ ├── hash.js // hash模式的history│ └── html5.js // html5模式的history其中, base.js 里面定义了 History 类基本的关系如下图:base.js 里面定义了一些列的方法, hash 、html5 模式,分别继承了这些方法,并实现了自己特有的逻辑从外部调用的时候,会直接调用到 this.history , 然后,由于初始化对象的不同,而进行不同的操作。接下来, 我们挑选其中一个我们最常用到的 push 方法来解释一整个过程push 方法我们平时调用的时候, 一直都是用 this.$router.push(‘home’) , 这种形式调用。首先,在 VueRouter 对象上有一个 push 方法 。// 文件位置: src/index.jsexport default class VueRouter { // … more push(location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort); }}我们看到,其没有做任何处理,直接转发到 this.history.push(location, onComplete, onAbort)。上面我们讲到,这个处理,会根据 history 的初始化对象不同而做不同处理。我们来分别看看细节mode === hashexport class HashHistory extends History { // …more // 跳转到 push(location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this; this.transitionTo( location, route => { pushHash(route.fullPath); handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort ); }}// 切换路由// 会判断是否支持pushState ,支持则使用pushState,否则切换hashfunction pushHash(path) { if (supportsPushState) { pushState(getUrl(path)); } else { window.location.hash = path; }}mode === historyexport class HTML5History extends History { // …more // 增加 hash push(location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this; this.transitionTo( location, route => { pushState(cleanPath(this.base + route.fullPath)); handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort ); }}两种模式的 push 实现区别并不大,都是调用了 transitionTo , 区别在于: 一个调用 pushHash , 一个调用 pushState.其他的 go 、 replace 、getCurrentLocation 都是类似的实现方式。transitionTo的具体实现,这里就先不详聊了,后面聊到路由守护的时候,会细讲这一块内容。其他系列文章列表个人博客 ...

January 22, 2019 · 2 min · jiezi

vue-router源码解析(二)插件实现

vue-router 插件方式的实现vue-router 是作为插件集成到 vue 中的。我们使用 vue-router 的时候,第一部就是要 安装插件 Vue.use(VueRouter);关于插件的介绍可以查看 vue 的官方文档我们重点关注如何开发插件如何开发插件Vue.js 要求插件应该有一个公开方法 install。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。在 install 方法里面,便可以做相关的处理:添加全局方法或者属性添加全局资源:指令/过滤器/过渡等,通过全局 mixin 方法添加一些组件选项,添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。一个库,提供自己的 API,同时提供上面提到的一个或多个功能MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或属性 Vue.myGlobalMethod = function () { // 逻辑… } // 2. 添加全局资源 Vue.directive(‘my-directive’, { bind (el, binding, vnode, oldVnode) { // 逻辑… } … }) // 3. 注入组件 Vue.mixin({ created: function () { // 逻辑… } … }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑… }}在粗略了解了 vue.js 插件的实现思路之后,我们来看看 vue-router 的处理vue-router 的 install首先查看入口文件 src/index.jsimport { install } from ‘./install’;// …moreVueRouter.install = install;所以,具体的实现在 install里面。接下来我们来看具体做了些什么 ?install 实现install 相对来说逻辑较为简单。主要做了以下几个部分 :防止重复安装通过一个全局变量来确保只安装一次// 插件安装方法export let _Vue;export function install(Vue) { // 防止重复安装 if (install.installed && _Vue === Vue) return; install.installed = true; // …more}通过全局 mixin 注入一些生命周期的处理export function install(Vue) { // …more const isDef = v => v !== undefined; // 注册实例 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode; if ( isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance)) ) { i(vm, callVal); } }; // 混入生命周期的一些处理 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 如果 router 已经定义了,则调用 this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive( this, ‘_route’, this._router.history.current ); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } // 注册实例 registerInstance(this, this); }, destroyed() { // 销毁实例 registerInstance(this); } }); // …more}我们看到 , 利用mixin,我们往实例增加了 beforeCreate 以及 destroyed 。在里面注册以及销毁实例。值得注意的是 registerInstance 函数里的vm.$options._parentVnode.data.registerRouteInstance;你可能会疑惑 , 它是从哪里来的 。它是在 ./src/components/view.js , route-view 组件的 render 方法里面定义的。主要用于注册及销毁实例,具体的我们后期再讲~挂载变量到原型上通过以下形式,定义变量。我们经常使用到的 this.$router ,this.$route 就是在这里定义的。// 挂载变量到原型上Object.defineProperty(Vue.prototype, ‘$router’, { get() { return this._routerRoot._router; }});// 挂载变量到原型上Object.defineProperty(Vue.prototype, ‘$route’, { get() { return this._routerRoot._route; }});这里通过 Object.defineProperty 定义 get 来实现 , 而不使用 Vue.prototype.$router = this.this._routerRoot._router。是为了让其只读,不可修改注册全局组件import View from ‘./components/view’;import Link from ‘./components/link’;export function install(Vue) { // …more // 注册全局组件 Vue.component(‘RouterView’, View); Vue.component(‘RouterLink’, Link); // …more}最后附上 install.js 完整的代码import View from ‘./components/view’;import Link from ‘./components/link’;export let _Vue;// 插件安装方法export function install(Vue) { // 防止重复安装 if (install.installed && _Vue === Vue) return; install.installed = true; _Vue = Vue; const isDef = v => v !== undefined; // 注册实例 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode; if ( isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance)) ) { i(vm, callVal); } }; // 混入生命周期的一些处理 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 如果 router 已经定义了,则调用 this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive( this, ‘_route’, this._router.history.current ); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } // 注册实例 registerInstance(this, this); }, destroyed() { registerInstance(this); } }); // 挂载变量到原型上 Object.defineProperty(Vue.prototype, ‘$router’, { get() { return this._routerRoot._router; } }); // 挂载变量到原型上 Object.defineProperty(Vue.prototype, ‘$route’, { get() { return this._routerRoot._route; } }); // 注册全局组件 Vue.component(‘RouterView’, View); Vue.component(‘RouterLink’, Link); // 定义合并的策略 const strats = Vue.config.optionMergeStrategies; // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;}其他系列文章列表个人博客 ...

January 21, 2019 · 3 min · jiezi

vue-router源码解析(一)

准备工作Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。这里主要通过阅读 vue-router 的源码,对平时使用较多的一些特性以及功能,理解其背后实现的思路。阅读版本 : 3.0.2目录结构├── components // 组件│ ├── link.js // route-link的实现│ └── view.js // route-view的实现├── create-matcher.js // 创建匹配├── create-route-map.js // 创建路由的映射├── history // 操作浏览器记录的一系列内容│ ├── abstract.js // 非浏览器的history│ ├── base.js // 基本的history│ ├── hash.js // hash模式的history│ └── html5.js // html5模式的history├── index.js // 入口文件├── install.js // 插件安装的方法└── util // 工具类库 ├── async.js // 异步操作的工具库 ├── dom.js // dom相关的函数 ├── location.js // 对location的处理 ├── misc.js // 一个工具方法 ├── params.js // 处理参数 ├── path.js // 处理路径 ├── push-state.js // 处理html模式的 pushState ├── query.js //对query的处理 ├── resolve-components.js //异步加载组件 ├── route.js // 路由 ├── scroll.js //处理滚动 └── warn.js // 打印一些警告我们知道 , 我们在使用 vue-router 的时候 ,主要有以下几步:<div id=“app”> <!– 路由匹配到的组件将渲染在这里 –> <router-view></router-view></div>// 1. 安装 插件Vue.use(VueRouter);// 2. 创建router对象const router = new VueRouter({ routes // 路由列表 eg: [{ path: ‘/foo’, component: Foo }]});// 3. 挂载routerconst app = new Vue({ router}).$mount(’#app’);其中 VueRouter 对象,就在vue-router 的入口文件 src/index.jsVueRouter 原型上定义了一系列的函数,我们日常经常会使用到。主要有 : go 、 push 、 replace 、 back 、 forward 。以及一些导航守护 : beforeEach 、beforeResolve 、afterEach 等等上面html 中使用到的 router-view ,以及经常用到的 router-link 则存在 src/components 目录下。下一步到这里相信你对整个项目结构有一个大概的认识 。 接下来,我们会根据以下几点,一步步拆解 vue-router。vue 插件方式的实现路由模式及降级处理的实现导航守卫的原理路由匹配详解组件:route-view 和 route-link 都做了些什么 ?滚动行为的实现如何实现异步加载组件(路由懒加载)其他查看系列文章 ...

January 21, 2019 · 1 min · jiezi

创建vue-cli框架项目

1、安装最新版vue-cli 3.xnpm install -g @vue/clivue –version // 检测vue-cli版本号2、安装vue-cli框架vue init webpack project // 创建project项目项目目录|—–build|—–config|—–node_modules|—–src|—–static|—–test以上是基本文件夹目录结构3、测试项目框架是否可以正常运行cd project // 进入project项目目录npm run dev 出现以上提示,在浏览器输入:http://localhost:8080 或者 127.0.0.1:8080至此OK,可以静下心来专心撸代码了。

January 20, 2019 · 1 min · jiezi

VUE-Router路由懒加载,打包问题(下午更改)

1、路由懒加载配置1.1index路由1.2home组件的路由1.3 添加路由导航守卫1.4项目打包1.4.1提示不能直接访问index.html文件,需要放在服务器中才可以访问。根据搜索解决办法,在 config > index.js 文件//assetsPublicPath: ‘/’, 添加.assetsPublicPath: ‘./’,重新打包后,仍然有提示1.4.2暂时不管,将文件放到虚拟机中继续访问,页面可以正常加载,但只要刷新页面就报404错误。后经过查询,需要将项目放在服务器中运行。不能直接访问静态页面。第二步,虚拟机安装nginx2、nginx的安装2.1配置完成后,启动项目报错 1067报错原因很多,一个个排查,因为IP冲突了。将IIS端口换成8082再启动成功访问locahost可以运行页面2.2将文件扔到nginx文件html文件中文件回退有问题,因为没有进行配置nginx.conf 文件配置配置完成后,再次刷新新页面还有问题2.3解决文件配置问题。因为打包前配置了 config > index.js 文件将配置路径还原,再次打包运行就没问题了。

January 18, 2019 · 1 min · jiezi

spa 项目上线后程序刷新一次浏览器,来使用新上线资源

解决了什么问题?spa项目新上线后,登陆有效期内用户,可以马上使用新上线资源。原理:路由切换时,判断如果是新上线,程序刷新下浏览器。实现步骤:打包时产生一个json文件:static/json/build_str.jsonlocalStorage中存入值:build_str每个路由切换时,从接口获得新打包后json中的字符串,与localStorage中存的上次打包字符串比较,不相同时刷新vue 项目代码修改的地方:1、相应目录下,新建文件:static/json/build_str.json2、build/build.js 修改:// 将当前时间戳写入json文件let json_obj = {“build_str”: new Date().getTime().toString()}fs.writeFile(path.resolve(__dirname, ‘../static/json/build_str.json’), JSON.stringify(json_obj), function (err) { if (err) { return console.error(err); } console.log(“打包字符串写入文件:static/json/build_str.json,成功!”); realBuild()})3、src/main.js 修改:router.beforeEach((to, from, next) => { axios.get(’/static/json/build_str.json?v=’ + new Date().getTime().toString()) .then(res => { let newBuildStr = res.data.build_str let oldBuildStr = localStorage.getItem(‘build_str’) || ’’ if (oldBuildStr !== newBuildStr) { console.log(‘auto refresh’) localStorage.setItem(‘build_str’, newBuildStr) location.reload() } }) next()})项目demo:https://github.com/cag2050/vu…

January 16, 2019 · 1 min · jiezi

解决vue-router跳转页面,返回上一页跳回指定位置

项目要求(商品列表):pageA -> pageB -> pageA 常用的:keep-alive 路由缓存(不多解说) 但上面很多时候,因页面需求原因,不能使用这种实现方式,在网上找了很多方法,但是都不是很好用,现在自己实现了一个还算不错的。 首先,在vue-router中,scrollBehavior这个方法是可以打印访问过的页面,滚动的位置的。【注意:在刷新页面时,不会触发该事件】 我们可以使用vuex储存滚动的位置(因页面会做接口请求数据,所以要在数据渲染完成后,再进行跳转) store.commit('SET_ROUTER_POSITION', savedPosition || {}) 好。我们获取到滚动的位置并且储存后,我们在需要在指定页面进行页面渲染后,调用滚动事件。 封装mixin方法,免得每个页面都需要写一次 ...

January 10, 2019 · 1 min · jiezi

vue-router和webpack懒加载,页面性能优化篇

在vue单页应用中,当项目不断完善丰富时,即使使用webpack打包,文件依然是非常大的,影响页面的加载。如果我们能把不同路由对应的组件分割成不同的代码块,当路由被访问时才加载对应的组件(也就是按需加载),这样就更加高效了。——引自vue-router官方文档如何实现??vue异步组件vue-router配置路由,使用vue的异步组件技术,可以实现懒加载,代码如下:// 每个组件都会生成一个js文件import Vue from ‘vue’import Router from ‘vue-router’import Login from ‘../view/List.vue’;Vue.use(Router);export default new Router({routes: [ { path: ‘/home/list’, name: ’list’, components: resolve => require([’../view/List.vue’], resolve) }],动态import(webpack > 2.4)vue、webpack官方推荐情况一:每个组件都会打包生成一个js文件const List = () => import(’../view/List.vue’)// 在路由配置中什么都不需要改变,像往常一样使用组件:export default new Router({routes: [ { path: ‘/home/list’, name: ’login’, components: Login }, { path: ‘/home/user’, name: ‘user’, components: User }],情况二:所有组件合并打包在一个异步块chunk中const List = () => import(/* webpackChunkName: “home” / ‘./List.vue’)const User = () => import(/ webpackChunkName: “home” / ‘./User.vue’)// 在路由配置中什么都不需要改变,像往常一样使用组件:export default new Router({routes: [ { path: ‘/home/list’, name: ’list’, components: List }, { path: ‘/home/user’, name: ‘user’, components: User }],// 在webpack.base.config.js中配置 ChunkFileName:output: {path: config.build.assetsRoot,filename: ‘[name].js’,chunkFilename: ‘[name].js’,publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath},另一种写法,更简洁:同样需要在webpack.base.config.js中配置 ChunkFileName:{ path: ‘/home/list’, name: ’list’, component: () => import(/ webpackChunkName:“list”/ ‘../view/List.vue’)},{ path: ‘/home/user’, name: ‘user’, component: () => import(/ webpackChunkName:“user”*/ ‘../view/User.vue’)},webpack提供的require.ensure()语法如下:摘自官网require.ensure(dependencies: String[], callback: function(require), chunkName: String多个路由指定相同的chunkName,在这里chunkName为home,会合并打包成一个js文件。{ path: ‘/home/list’, name: ’list’, // component:list component: r => require.ensure([], () => r(require(’../view/Lst.vue’)), ‘home’)},{ path: ‘/home/user’, name: ‘user’, // component:user component: r => require.ensure([], () => r(require(’../view/User.vue’)), ‘home’)}// 在webpack.base.config.js中配置 ChunkFileName 和 publicPath:output: {path: config.build.assetsRoot,filename: ‘[name].js’,chunkFilename: ‘[name].js’,publicPath: ‘./’,publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath},在实践过程中应该会遇到各种问题,到时候再继续补充,前端新手,多多指教! ...

January 5, 2019 · 1 min · jiezi

最详细的Vue Hello World应用开发步骤

很多Vue的初学者想尝试这个框架时,都被webpack过于复杂的配置所吓倒,导致最后无法跑出一个期望的hello word效果。今天我就把我第一次使用webpack打包一个Vue Hello World应用的所有步骤详细记录下来,供Vue的初学者参考。安装nodejs和npm,这两个就不用说了,网上很多教程。本地随便新建一个文件夹,进入后运行命令npm init, 一路next下去,自动生成package.json。运行命令npm install –save-dev webpack-dev-server,安装一个轻量级的服务器,该服务器用于vue应用开发完毕后的本地测试。重复执行命令npm install –save-dev <name>,也就是把下列命令粘贴到cmd里进行执行:npm install –save-dev css-loadernpm install –save-dev vue-template-compilernpm install –save-dev webpacknpm install –save-dev vue-loadernpm install –save-devvue-router参数-save-dev的效果是让这些安装的module出现在package.json的devDependencies区域内,如下图红色区域所示:这些都是开发时依赖。我们再用下列命令安装运行时依赖:npm install –save vue vuex然后再在package.json里手动加入如下这一段内容:目的是开发完毕后,使用命令npm run dev可以启动webpack-dev-server,运行我们的vue应用,并带上参数–inline –hot。在项目文件夹根目录下创建一个名为src的文件夹,文件夹里新建一个文件index.vue,把如下内容拷贝进去:<style>h2{color: red;}</style><template><h2>Jerry: Hello, World!</h2></template><script>module.exports = {data: function(){return {};}}</script>再回到根目录下,新建一个文件main.js:import Vue from ‘vue’;import AppJerry from ‘./src/index.vue’new Vue({el: “#demo”,components: {app: AppJerry}});这段代码首先将我们在src文件夹的index.vue里实现的应用导出,存储到变量AppJerry里,再将这个应用安装到html页面id为demo的div标签里。安装是通过创建Vue实例并将div元素的id传入构造函数里进行的。当然,我们还没创建html文件,所以马上创建一个名为index.html的文件:<!DOCTYPE html><html lang=“en”><head><meta charset=“UTF-8”><title>hello world</title></head><body><div id=“demo”><app></app></div><script src=“dist/build.js”></script></body></html>我们注意到这个index.html里引用了一个dist/build.js的文件,这个文件用来干嘛的?这里就不得不提webpack在现代前端开发技术中起的重要作用了。WebPack可以看做是模块打包机:它做的事情是,分析我们的前端项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言,比如Scss,TypeScript等,并将其打包为合适的格式以供浏览器使用。具体到我们这个例子,就是说webpack把我们src文件夹下的index.vue打包转换成浏览器能识别的js文件,webpack的输出就是dist文件夹下的build.js文件。为了让webpack清楚地知道它要完成什么样的任务,我们通过创建一个配置文件webpack.config.js来完成webpack任务指定。这个配置文件的内容:var path = require(‘path’);module.exports = {entry: ‘./main.js’,output: {path: path.resolve(__dirname, ‘./dist’),publicPath: ‘/dist/’,filename: ‘build.js’},resolve: {alias: {‘vue$’: ‘vue/dist/vue.esm.js’}},module: {loaders: [{test: /.vue$/,loader: ‘vue-loader’},{test: /.(png|jpg|eot|svg|ttf|woff)/,loader: ‘url?limit=40000’}]}}里面定义了webpack执行任务的入口是main.js文件,任务输出的文件夹是项目文件夹里的dist目录,输出文件是build.js, webpack扫描的文件通过vue-loader指定,特征是以.vue结尾的文件。到目前为止,这个基于Vue的hello world应用的开发和配置都结束了,是不是很简单?我们可以来测试了。直接在命令行里敲webpack命令,就会自动执行打包操作,并在控制台上看到build.js文件成功生成的消息:这个打包后的文件尺寸很大,有323KB,包含了vue.js本身的内容和我们index.vue里的转换后的内容。下图高亮区域就是我们index.vue里的实现被webpack处理后生成对应的JavaScript代码。使用npm run dev启动webpack-dev-server,看到提示说在localhost:8080上可以访问我们的应用了。浏览器里访问,看到Hello World的输出,说明我们成功地走完了一个基于webpack的Vue应用开发流程。要获取更多Jerry的原创文章,请关注公众号"汪子熙":uto-orient/strip%7CimageView2/2/w/1240) ...

January 1, 2019 · 1 min · jiezi

PWA项目实战分享

PWA项目实战分享 - BookPlayer 每天听本书App因为自己有个需求,特别的痒,昼夜难免。第二天就开始起手做这个项目,利用业余时间,大概持续了10天时间(因为边学边做),从设计到数据(包括解析物理文件)到前端。总于把我想要的效果做出来了。因为数据涉及到版权问题,所以只搞了部分数据来做演示,哈哈。效果演示传送门项目地址传送门Android App 下载该项目实现了:播放器功能倍速播放连续播放播放列表…听书排序分月/区间浏览已读变灰色查看大图解析xmind文件为树结构文本书籍的搜索历史记录课程和听书类似没有xmind只有图片文稿AppPWA 集成可借助 Lavas 生成 Android App…技术栈vue + vuex + vue-router + vue cli3 + LeanCloud + PWA(可以借助Lavas生成AndroidApp) + 腾讯云对象存储项目运行git clone https://github.com/worklinwu/BookPlayer.gitcd BookPlayernpm i 或 yarnnpm run devLeanCloud 配置先注册 LeanCloud 账号创建应用,命名为 BookPlayer, 或者自己喜欢的进入应用后,在存储的创建 Class旁边有个加号,点击选择数据导入把目录下的 json 文件导入查看侧边栏的设置 -> 应用key,复制替换掉该项目的 .env 的 VUE_APP_LEANCLOUD_APP_ID 和 VUE_APP_LEANCLOUD_APP_KEY重启项目,看看效果吧

December 28, 2018 · 1 min · jiezi

vue之vue router

走在前端的大道上本篇将自己读过的相关 vue router 文章中,对自己有启发的章节片段总结在这(会对原文进行删改),会不断丰富提炼总结更新。文章内容 基于vue 2.01.vue router如何传参?1.1 params、query是什么?params:/router1/:id ,/router1/123,/router1/789 //这里的id叫做paramsquery:/router1?id=123 ,/router1?id=456 //这里的id叫做query。比如:跳转/router1/:id<router-link :to="{ name:‘router1’,params: { id: status}}" >正确</router-link><router-link :to="{ name:‘router1’,params: { id2: status}}">错误</router-link>1.2 html标签传参params、query不设置也可以传参,params不设置的时候,刷新页面或者返回参数会丢失路由界面:当你使用params方法传参的时候,要在url后面加参数名,并在传参的时候,参数名要跟url后面设置的参数名对应。params是路由的一部分,必须要有,否则会导致跳转失败或者页面会没有内容如图片 :id,// 上面的router-link传参,也可以使用编程式导航跳转this.$router.push({ name:‘router1’,params: { id: status ,id2: status3},query: { queryId: status2 }});//编程跳转写在一个函数里面,通过click等方法来触发query,是拼接在url后面的参数,就没有这种限制,直接在跳转里面用就可以,没有也没关系。使用路由上面的参数<template> <div class=“router1”> <h1>接收参数的路由</h1> <h1> params.id:{{ $route.params }}</h1> <h1>query.status:{{ $route.query.queryId }}</h1> <keep-alive> <router-view></router-view> </keep-alive> </div></template>注意:获取路由上面的参数,用的是$route,后面没有r本节参考文章:vue router 使用params query传参,以及有什么区别1.3 vue this.$router.push()传参params 传参注意⚠️:patams传参 ,路径不能使用path 只能使用name,不然获取不到传的数据this.$router.push({name: ‘dispatch’, params: {paicheNo: obj.paicheNo}})取数据:this.$route.params.paicheNoquery 传参this.$router.push({path: ‘/transport/dispatch’, query: {paicheNo: obj.paicheNo}})取数据:this.$route.query.paicheNo2.this.$router 与 this.$route 在登录页完成登录请求后进行下面的操作 获取路径中存放前一个路径的参数 ,然后跳转到该页面 loginSuccess() { const { params: { back } } = this.$route; const route = back || { name: ‘home’ }; const { name, params, query } = route; this.$router.replace({ name, params, query }); },在上面这段代码中出现了两个我们经常混淆的概念:我们知道this.$router是router实例,可以用来直接访问路由。我们称router配置中每一个对象为一个路由记录,this.$route是暴露出来用来访问每个路由记录的。因此我们获取参数时使用的是this.$route 跳转路由时使用的是道this.$router。this.$router.push 与 this.$router.replace上端代码中我们使用了replace而不是push来跳转路由,这两者的区别是会不会在history中产生记录。replace不会新增记录,而是直接替换掉了这条路由记录。本节参考文章:vue router+ vuex+ 首页登录判断逻辑3.路由懒加载路由懒加载应该是写大一点的项目都会用的一个功能,只有在使用这个component的时候才会加载这个相应的组件,这样写大大减少了初始页面 js 的大小并且能更好的利用游览器的缓存。首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):const Foo = () => Promise.resolve({ /* 组件定义对象 / })const Foo = resolve => require([’./Foo.vue’], resolve)//或者const Foo = () => import(’./Foo’);官网:详细4.单页及多页应用全局配置404页面4.1 SPA的404路由配置单页应用配置404页面,也区分两种情况:4.1.1 路由表固定的情况如果SPA的路由表是固定的,那么配置404页面就变得非常的简单。只需要在路由表中添加一个路径为404的路由,同时在路由表的最底部配置一个路径为的路由,重定向至404路由即可。(由于路由表是由上至下匹配的,一定要将任意匹配规则至于最底部,否则至于此路由规则下的路由将全部跳转至404,无法正确匹配。)// router.jsexport default new Router({ mode: ‘history’, routes: [ // … { name: ‘404’, path: ‘/404’, component: () => import(’@/views/notFound.vue’) }, { path: ‘’, // 此处需特别注意至于最底部 redirect: ‘/404’ } ],})4.1.2 路由表动态生成的情况路由表是动态生成的情况下,也就是说路由表分为两部分,一部分为基础路由表,另一部分是需要根据用户的权限信息动态生成的路由表。本项目中动态生成路由采用vue-router自带的addRoutes方法,该方法是会将新的路由规则在原路由表数组的尾部注入的。由于任意匹配重定向至404页面的规则必须至于路由表的最底部,所以此处我将重定向至404页面的规则抽出,在动态路由注入后,再注入重定向规则,以确保该规则至于路由表最底部。// router.jsexport default new Router({ mode: ‘history’, routes: [ // … { name: ‘404’, path: ‘/404’, component: () => import(’@/views/notFound.vue’) }, // …other codes ],})// notFoundRouterMap.jsexport default [ { name: ‘404’, path: ‘/404’, component: () => import(’@/views/notFound.vue’) }, }, { path: ‘’, redirect: ‘/404’ }]// main.js//…other codesrouter.beforeEach((to, from, next) => { new Promise((resolve, reject) => { if (getCookie(tokenName)) { if (!getInfo()) { Promise.all([store.dispatch(‘getBasicInfo’), store.dispatch(‘getUserDetail’)]).then(res => { store.dispatch(‘GenerateRoutes’, { roles }).then(() => { // 根据用户权限生成可访问的路由表 router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 router.addRoutes(NotFoundRouterMap) // 添加404及重定向路由规则 resolve({ …to, replace: true }) // 重新加载一次路由,让路由表更新成功后走下面else的判断 }) }) } else { // …other codes } } else { window.location.href = ‘/login.html’ } }).then(res => { if (res) { next(res) } else { next() } }).catch(err => { new Error(err) next(false) })4.2 多页应用的404路由配置多页应用区别于SPA的不同点是每个页面有自己的一套路由,并且每个页面可能有自己的一套404页面风格,当然也可能没有。这时候,就不能再采用动态添加路由规则的方法了。我采用的方案是在全局导航守卫beforeEach中对路由匹配的情况进行判断,这时候就需要用到vue导航守卫中的matched数组了。如果没有一个匹配上的,那么就重定向至404页面。当然,这个404页面也单独设置为一个页面。// permission.js//…other codesrouter.beforeEach((to, from, next) => { new Promise((resolve, reject) => { // …other codes }).then(res => { if (!to.matched.length) { window.location = ‘/error.html#/404’ return } if (res) { next(res) } else { next() } }).catch(err => { new Error(err) next(false) })本节参考文章:Vue单页及多页应用全局配置404页面实践5.后台系统权限控制具体实现思路创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。6.基于路由的动态过渡<!– 使用动态的 transition name –><transition :name=“transitionName”> <router-view></router-view></transition>// 接着在父组件内// watch $route 决定使用哪种过渡watch: { ‘$route’ (to, from) { const toDepth = to.path.split(’/’).length const fromDepth = from.path.split(’/’).length this.transitionName = toDepth < fromDepth ? ‘slide-right’ : ‘slide-left’ }}动手理解导航守卫(Vue)Vue动态路由的实现(后台传递路由,前端拿到并生成侧边栏)手摸手,带你用vue撸后台 系列二(登录权限篇)vue router原理前端的路由模式包括了 Hash 模式和 History 模式。vue-router 在初始化的时候,会根据 mode 来判断使用不同的路由模式,从而 new 出了不同的对象实例。例如 history 模式就用 HTML5History,hash 模式就用 HashHistory。vue-router 源码:前端路由vue-router 源码:路由模式 ...

December 27, 2018 · 2 min · jiezi

vue-router 启用 history 模式下的开发及非根目录部署

vue-router 的 history 模式是个提高颜值的好东西,没有了 hash 的路由看起来清爽许多。开发的时候,如果我们使用 devServer 来启动服务,由于一般不共用端口,我们一般不存在非根目录的问题。而刷新后 404 的问题可以借助 historyApiFallback 来解决。 但当我们项目对外开放时,往往无法在域名根目录下提供服务,这个时候资源的访问路径与开发时的根目录就有了区别。首先,我们通过 webpack 来配置一下项目中所有资源的基础路径,让这份代码在开发和生产环境中都可以正确找到资源。// config/index.jsmodule.exports = { dev: { … // 开发环境根目录 - 服务根目录 - 绝对路径 assetsPublicPath: ‘/’ … }, build: { … // 生产环境根目录 - 服务器访问路径 - 绝对路径 assetsPublicPath: ‘/test/project1/’ … }}// build/webpack.common.conf.jsconst config = require(’../config’)module.exports = { output: { publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath }}// build/webpack.dev.conf.jsconst common = require(’./webpack.common’)module.exports = merge(common, { devServer: { historyApiFallback: true }}然后在提供服务的服务器配置中做如下配置(以 nginx 为例):location /test/project1 { alias …/project1; // 项目的真实路径 index index.html; try_files $uri $uri/ /test/project1/index.html;}try_files 会按顺序检查参数中的资源是否存在,并返回第一个找到的资源,如果都没有找到,它会让 nginx 内部重定向到会后一个参数。对了,所以它的的作用是解决刷新 404 的问题。这里值得注意的是 try_files 的参数是绝对路径。至此,你开启 history 模式的项目就可以顺利的跑在任何路径了。欢迎大家点评指正点个赞~ wink原文链接 -《vue-router 启用 history 模式下的开发及非根目录部署》 ...

December 21, 2018 · 1 min · jiezi

vue 撸后台笔记一

前言本文是以 花裤衩 大佬的 vue-element-admin 项目为模板、结合公司需求开发的后台管理系统的学习笔记。原项目地址:vue-element-admin参考文章:手摸手用 vue 撸后台系列安装与配置新建 vue-cli 项目,相关安装及配置不多做介绍,有需要可自行搜索。接着是安装项目依赖。基本依赖库:Vue-Router Vue.js 官方的路由管理器Axios 基于promise 的 HTTP 库Element-UI 一套为开发者、设计师和产品经理准备的基于 Vue2.0 的桌面端组件库Vuex 一个专为 Vue.js 应用程序开发的状态管理模式扩展依赖库:node-sass css 扩展语言normalize.css 为默认的 HTML 元素样式上提供跨浏览器的高度一致性js-cookie 一款轻量级的 js 操作 cookie 的插件i18n Vue.js 的国际化插件,它可以轻松地将一些本地化特性集成到 Vue 中driver.js 一款轻量级、无需依赖但功能强大的原生 JavaScript,兼容所有主流浏览器,可帮助你将用户的注意力集中在页面上NProgress 细长的全站进度条SVG sprite loader 用于根据导入的 svg 文件自动生成 symbol 标签并插入 htmlSortable 一款轻量级的拖放排序列表的 js 插件ECharts 一款功能强大的图表和可视化库screenfull 一款全屏插件项目结构├── build // 构建相关├── config // 配置相关├── disk // 打包文件├── node_modules // 依赖项├── src // 源代码│ ├── api // 所有请求│ ├── assets // 主题 字体等静态资源│ ├── components // 全局公用组件│ ├── directive // 全局指令│ ├── waves // 水波纹指令│ ├── icons // 项目所有 svg icons│ ├── lang // 国际化 language│ ├── mock // 项目mock 模拟数据│ ├── roter // 路由│ ├── store // 全局 store管理│ ├── styles // 全局样式│ ├── utils // 全局公用方法│ ├── views // views 所有页面│ ├── account // 账户管理│ ├── court // 法院管理│ ├── dashboard // 功能主页│ ├── device // 设备管理│ ├── errorPage // 错误页面│ ├── layout // 整体布局│ ├── login // 登录页面│ ├── redirect // 重定向页面│ ├── statistics // 数据统计页面│ ├── versions // 版本管理页面│ ├── writs // 文书管理页面│ ├── App.vue // 入口页面│ ├── errorLog.js // 错误日志│ ├── main.js // 入口文件 加载组件 初始化等│ ├── permission.js // 权限管理├── static // 第三方不打包资源├── .babelrc // babel-loader 配置├── .eslintrc.js // eslint 配置项├── .gitignore // git 忽略项├── favicon.ico // favicon 图标├── index.html // html 模板├── package.json // 依赖项目录├── README.MD // 说明文档简单讲下 src 文件夹api 与 views根据项目的业务划分 views 页面展示部分,并将 api 接口请求与 views 一一对应,有利于迭代更新与后期维护。components将全局公用的模块与组件存放在 components 文件夹中,页面级的的组件建议还是放在各自的 views 文件夹下。store在 index 入口文件引入 modules 对象,独立封装各个模块状态。axios在 axios 配置档设置基础 URL,根据环境变量动态切换 api,需要在 config/dev.env.js 文件中配置接口路径。lang将中英文语言包各自封装并在入口 index.js 配置导入在 main.js 使用 i18n。 ...

December 21, 2018 · 2 min · jiezi

Vue全家桶 + webpack 构建单页应用初体验

文章指南主题 承接这上一篇Vue + Webpack 构建模块化开发框架详解,我们知道了如何使用webpack对vue进行打包,从而开始我们的前端模块化开发之路,这一篇在上一篇的基础上讲解 Vue全家桶(vue+vuex+vue-router+axios) + webpack 构建一个单页应用Demo前提 阅读本篇内容之前,除了需要掌握上一篇内容中的前提部分的知识,还需要了解以下内容????Vue全家桶系列,掌握vuex,vue-router以及axios ,不了解请移步官方教程axios-npm,vuex,vue-routervue全家桶简单介绍vuex : vuex是一种集中式状态管理模式,什么意思呢?我们在模块化开发过程中,我们以组件来作为模块单位,模块之间存在于不同的命名空间,作用域互不干预,这样保证了我们模块之间变量函数名称等不会冲突,但是有时候我们我们需要组件之间共享一些数据或者状态,我们通常的做法是传参,但是传参的做法至少有两个弊端,一是麻烦(尤其是当需要传递的参数很多时),二是不好管理且冗余(给多个组件传参就需要多份参数列表,而且容易出错)。vuex提供的集中式管理就解决了这个问题,通过把要共享的数据或状态集中起来管理,别的组件需要时就去访问变更,大大提高了可维护性和开发效率vue-router : vue-router是一个前端路由管理器,这个和后端常听说的路由有些不同(个人觉得),这里的路由管理器更像是一个组件注册器,vue-router为分散的组件注册一个路由或者叫地址也未尝不可,以方便我们控制组件的层级嵌套关系以及隐藏还是显示,这样我们可以很方便高效的构建单页应用axios : axios 和 jquery.ajax/vue-resource一样 , 都是HTTP异步请求的工具,axios和vue-resource的API很像,但是个人觉得,axios的API更丰富一些正文 本文的小Demo大概功能就是一个登陆的功能,我们这里app.vue封装了一个登陆组件,success.vue封装了一个提示面板,通过vuex来集中管理登陆状态,用vue-router来路由提示面板,用axios来异步提交到一个跨域的服务器(这里后台服务是nginx+php提供,所以在dev-server中要设置一个反向代理,也就是nginx来代理我们的dev-server的请求从而解决axios跨域问题)先来看看我们的项目结构 [自定义的]这里的目录设置,并不是一成不变的金科玉律,也不一定是最好的,读者可以根据自己的想法设定文件结构,了解如何配置单入口单出口的webpack.config.js,可以看我的上一篇文章(开头提到了),或者移步webpack官方网站。本篇内容,不再讲解过多webpack配置,和上一篇是基本相同的,下面主要讲解 Vue全家桶相关内容index.jsimport Vue from ‘vue’;import Router from ‘./routers/index-router’;import Store from ‘./stores/index-store’;import App from ‘./components/App.vue’;var app = new Vue({ //创建一个Vue实例 router : Router, //加入路由配置 store : Store, //加入状态管理 components : {App} //加入App组件}).$mount(’#app’); //挂载节点需要注意的是,路由和状态都需要加入一个vue实例中去才有意义。这里有一点很有意思,就是在路由router中注册的组件(success.vue)依然可以访问到状态store实例,而官方教程上并没有特别强调这一点,在下面我们可以看到这个有趣的事情stores/index-store.jsimport Vuex from ‘vuex’;import Vue from ‘vue’;import axios from ‘axios’;Vue.use(Vuex); //在vue中加入Vuex插件const Store = new Vuex.Store({ //实例化一个Store state : { //这里是我们需要集中管理的登陆状态 account : “ads”, password : “123456”, islogin : null }, mutations : { // 这是唯一可以变更state的途径,需要通过一个提交 updateAccount(state,payload){ //具有载荷的mutation state.account = payload.account }, updatePassword(state,payload){ state.password = payload.password }, islogin(state,payload){ if(payload == 1){ state.islogin = true }else if(payload == 0){ state.islogin = false } } }, actions : { //这是可以支持异步执行提交的actions login (context,payload){ //这里是去访问我们的反向代理服务器上的一个php文件 axios.get(’/index.php’,{ params : { account : payload.account, password : payload.password } }).then(function(response){ if(response.data.code == ‘success’){ //变更store状态 context.commit(‘islogin’,1) //这里做了在异步逻辑中的状态提交 }else{ context.commit(‘islogin’,0) } }).catch(function(error){ console.log(error) alert(‘fail’) }) } }})export default Store我们这里的Store有三个属性,state是我们需要集中管理的状态或者数据, mutations是维护变更我们状态的一个方式, actions是为了在异步逻辑中提交状态的一个中转站,需要注意的是:在实例化Store之前,必须先 Vue.use(Vuex) ,先在vue中加入Vuex插件这里需要说一下的是,如何开启dev-server的proxy,让nginx来代理axios的请求,也就是跨域访问了(因为dev-server跑的是8080端口,nginx跑的是80端口,不设置代理的话浏览器将拒绝跨域访问),感觉很深奥,其实很简单,我们需要修改一下webpack.config.js中devServer的配置 : webpack.config.js devServer : { contentBase : ‘./dist’, watchContentBase : true, compress : true, port : 8080, hot : true, inline : true, //开启页面自动刷新 open : true, proxy : { ‘/index.php’ : { target : ‘http://localhost:80/phpinfo.php’, secure : false } } }然后,我们的axios在配置url时使用 /index.php,就可以相当于访问http://localhost:80/phpinfo.phprouters/index-router.jsimport VueRouter from ‘vue-router’;import Vue from ‘vue’;import success from ‘../components/success.vue’;// 组件注册路由,这里success组件注册路由是’/app’var routes = [ { path : ‘/app’,component : success }]Vue.use(VueRouter); //在vue中加入VueRouter插件var Router = new VueRouter({ //实例化一个router routes //这里的写法相当于 routes:routes})export default Router;这里的路由只注册了一个组件,vue-router的能力远不止如此,还可以嵌套定义组件的层级关系,更多到官方文档了解 ,路由器和状态管理器一样都是vue官方核心插件,使用前都需要先 Vue.use(VueRouter) ,我们其实可以看到,这里把路由和状态分别从vue实例中抽离出来,单独管理,通过暴露实例Store Router,加入到vue实例中去,这样可以方便debug和后期维护components/App.vue 和 components/success.vueApp.vue<template> <div> <input type=“text” v-model=“account”> <input type=“password” v-model=“password”> <button type=“button” @click=“login”>show</button> </div></template><script> export default { data : function(){ return { account : “”, password : "" } }, created : function(){ this.account = this.$store.state.account this.password = this.$store.state.password }, watch : { //侦听属性 account : function(){ //当accout改变就立即提交状态 this.$store.commit(‘updateAccount’,{ account : this.account }) }, // 当password改变就立即提交状态 password : function(){ this.$store.commit(‘updatePassword’,{ password : this.password }) } }, methods : { login : function(){ //通过分发到action,来异步请求登录服务,并变更状态 this.$store.dispatch(’login’,{ account : this.account, password : this.password }) this.$router.push(’/app’);//同时显示登陆状态 } } }</script><style scoped> input { height: 40px; width: 300px; border: 1px solid blue; box-shadow: none; }</style>success.vue<template> <div> <p>{{islogin}}</p> </div></template><script> export default { computed : { //计算属性 //动态响应状态的变更 islogin : function(){ var islogin = this.$store.state.islogin if(islogin == null){ return ‘No option’ }else if(islogin == true){ return ‘Login Success’ }else if(islogin == false){ return ‘Login Fail’ } } } }</script><style scoped> p { height: 40px; text-align: center; }</style>templates/index.html<!DOCTYPE html><html><head> <meta charset=“utf-8”> <title>index</title></head><body> <div id=“app”> <!– App组件渲染出口 –> <App></App> <!– 注册路由的组件渲染出口 –> <router-view></router-view> </div></body></html>总结好了,到此为止,代码基本完成,我们来理一理,vue+vuex+vue-router以及组件之间是如何配合工作的?用户在App.vue组件中输入,v-model双向绑定了App组件中的data,App组件中的侦听属性发现data改动,立即向vuex的Store实例提交了状态变更,当用户点击show,App组件中methods.login事件被触发,它向Store实例发起一个分发,并导航到/app,Store的action收到分发调用axios去异步请求位于localhost:80下的php后台服务,得到的登陆状态立即提交,与此同时,Router去渲染/app路由对应的组件到模板中去,于是我们就可以看到一个动态响应式的状态提示了 ...

December 19, 2018 · 2 min · jiezi

Weex系列(4) —— 老生常谈的三端统一

目录Weex系列(序) —— 总要知道原生的一点东东(iOS)Weex系列(序) —— 总要知道原生的一点东东(Android)Weex系列(1) —— Hello World项目Weex系列(2) —— 页面跳转和通信Weex系列(3) —— 单页面还是多页面Weex系列(4) —— 老生常谈的三端统一[Weex系列(5) —— 封装原生组件和模块][Weex系列(6) —— css相关小结][Weex系列(7) —— web组件和webview][Weex系列(8) —— 是时候简析一下流程原理了][Weex系列(9) —— 踩坑填坑的集锦][Weex系列(10) —— 先这么多吧想到再写。。。]哎,手动捂脸,真的是好忙的两周,拔了一颗智齿、做了一个三端的投票活动、参加了微信马拉松比赛。还好都坚持过来了,我怎么这么优秀,还是手动doge一下吧。上面提到了一个三端投票活动,之前还想着怎么写这篇文章,做了这个活动后,感觉有千言万语。。场景附上我们的活动链接 https://tousu.sina.cn/activit… ,欢迎为自己喜欢的爱豆打call哦。APP端,欢迎搜索 黑猫投诉 或 新浪众测 呦,点击banner都可以双倍投票呢。对,这两个app都是基于weex做的。打开活动页,可以看到就三个页面,首页、明星详情页、明星列表页。刚看到这仨页面的时候,我就想着可以用路由,做成三端统一。配置看过我前一篇的文章,就知道我们的app都是多页面的,webpack只会打包成多个js,按照我上面的思路,这个时候需要修改配置,做过vue大型项目的应该遇到过吧,我之前是没有弄过,花了半天时间,参考的是已有的app多页面配置,和新建的只有单页面项目的配置,终于修改好了配置文件。(这里的单页多页可以参考我的前一篇文章)。然后就把静态的三个页面切好了,在app端和web样式基本都是正常的。如果你用的是最近的weex脚手架,web的index.html里面需要引入dist目录里面对应的index.web.js和vendor.web.js,而不是网上weex-hackernews-master项目里面引的weex-vue-render等js。(不然是不能单独运行的)vendor.web.js里面兼容了我们使用的weex组件和模块,有兴趣的可以去研究一下。开始其实还挺顺利的,但是中间遇到了很多问题,主要列出以下几点吧封装的模块和组件刚开始拿到项目的时候,想的还是少了。weex只是处理了他支持的组件和模块,所以我们自己封装的就需要自己做兼容了/(ㄒoㄒ)/。这里要说的一点是weex-ui也是处理过了,比如wxc-slider-bar三端基本无差异。比如我们这边的登录模块,h5是一套登录组件,app里面是微博的登录模块。由此还牵扯的有相关的请求方法、后端接口处理等。样式这部分真的三端基本是高度统一的,部分微调一下就可以了,也正是这样,我们后续才能迅速解决h5和pc。总结上面模块那部分由于涉及项目,我是简单几笔带过,其实这块真的是挺麻烦的,祝大家顺利吧。这次我们是有pc、h5、两个app的两端,其实是6端,时间也是挺紧的,所以最后基本还是h5、pc维护一套,app再维护一套。终于不是谈谈三端统一了,也是真的体验了一次,虽然最后有点出入,但是下次基本是没问题了(doge)。想用但还没有去实践过的,真的可以去试试了。最后欢迎评论交流学习啊,如果喜欢就请点个赞

December 19, 2018 · 1 min · jiezi

单页应用history路由改造

需求调用其它团队流量站埋点统计时会截取锚点#,导致单页应用分页面流量分析获取不到数据。而本项目迫切需要按菜单、特殊功能模块统计流量情况,等不及流量站的团队开发升级版,于是选择改造项目路由为hostory模式。单页应用路由实现思路SPA,只加载一个HTML,页面在用户与应用程序交互时动态更新该页。即理论上来说,只需加载一次页面就可以不再请求(首屏耗时过长需按模块chunk,预加载css,按需加载js),当点击其他子页面时只会有相应的URL改变而不会重新加载。这种情况下实现路由的过程分为两部分:更新URL页面不刷新URL变化时执行页面替换逻辑现在主流有2种实现方式:history.pushState等触发popstate事件location.hash的变化触发hashchange事件vue-router中提供了三种方式HTML5History(判断是否支持)、HashHistory、AbstractHistory(用于Node环境,因为不涉及和浏览器地址相关记录关联在一起;整体流程依旧和 HashHistory 是一样的,只是这里通过数组来模拟浏览器历史记录堆栈信息)默认为Hash模式,组件(components)映射到路由(routes),registerHook设置守卫入栈,在每次跳转的时候,递归守卫集合,将触发的守卫进行解析和执行。vue-router源码阅读实现过程 & 踩坑记录1. Router传入配置项,设置mode为’history’export default new Router({ mode: ‘history’, routes })2. 开发模式下,webpack 热启动配置 history 模式webpack属性中historyApiFallback默认能将当前找不到的目录重定向到主目录默认index.html。在 webpack 配置文件的devServer配置,将url重写到自己配置的目录:// webpack.dev.conf.jsdevServer: {… historyApiFallback: { rewrites: [ { from: /.*/, to: ‘/index.html’ } ], },…}3. 部署预发后,刷新时资源404开发过程一切正常,直到编译打包发布。状况:从域名点击菜单能正常跳转,但刷新当前页会报错404。(Eg:从test.com点击访问/111正常,但直接访问test.com/111报404)原因:在History mode下,直接通过地址栏访问url会被http server直接解析到该文件路径,但是spa的路由是虚拟的,并不能直接找到这个file,所以会404。解决思路:如果URL匹配不到任何静态资源,就跳转到默认的index.html,让router去解析url, nginx中需要配置try_files。# 当前项目使用了第一级’/‘来区分产品,所以这里匹配metric、stocktake# 一般直接’location ~ /‘即可location ~ ^(/metric|/stocktake) { try_files $uri $uri/ /index.html;}具体配置官方提供了:其它服务器配置4. 页面不再404,开始报语法错误报错如下DOM情况:资源情况:查看source,发现index.html中,静态资源vendor.dll.的引入路径有问题,将相对路径’./‘修改为绝对路径’/‘5. 页面正常加载,但后端接口被统一重定向原因:step3时在nginx处配置了try_files 加上后端接口也是/metric、/stocktake开头。于是就不幸地被nginx匹配到规则统一作跳转处理了。解决:区分接口与页面。在项目中统一对后端接口加’/api/’,nginx新建规则匹配’/api/‘并作相应跳转。// 在发请求的公共方法处添加’/api/‘function ajax(url, type, params, opt = ‘’) { return Q.Promise((resolve, reject) => { const config = { method: type === ‘get’ ? ‘get’ : ‘post’, // TODO: ‘/api/‘暂时作为nginx区分重定向的匹配字段 待与兆华协商统一 不与路由命名重复 url: C.HOST + ‘/api/’ + url, params: type === ‘get’ ? params : null, data: configData(type, params, opt) } axios(config).then(checkStatus).then(checkCode).then((response) => { resolve(response) }).catch((err) => { console.log(err) if (err.msg) Notice(err.msg) reject(err) }) })}nginx中添加如下匹配规则并重写url# 后台三类开头对应请求不同服务器location ~ ^/api/system/ { rewrite ^/api/(.*) /$1 break; proxy_pass http://assets-api-yf.jd.com; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}location ~ ^/api/metric/ { rewrite ^/api/(.*) /$1 break; proxy_pass http://metrics-api-yf2.jd.com; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}location ~ ^/api/stocktake/ { rewrite ^/api/(.*) /$1 break; proxy_pass http://meta-api-yf.jd.com; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}6. 切换产品菜单时会刷新因为使用的是window.location,替换成this.$router.push即可7. 非法输入路由时,未正常重定向到首页,直接404状况:地址栏输入test.com/ffsdfdsge,显示'404 Not Found’,并未重定向到首页。 解决:nginx配置error_page。server { … error_page 404 /index.html; …}前端小菜鸟一枚,如表述有误,恳请各位大神指正~ ...

December 18, 2018 · 1 min · jiezi

关于Vue2一些值得推荐的文章 -- 十二月份

十二月份查看github最新的Vue weekly;请::点击::集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。十二月上半月-幽州思妇十二月,停歌罢笑双蛾摧。(12.01~12.15):北风行北风行 [唐] 李白 烛龙栖寒门,光曜犹旦开。 日月照之何不及此,唯有北风号怒天上来。 燕山雪花大如席,片片吹落轩辕台。 幽州思妇十二月,停歌罢笑双蛾摧。 倚门望行人,念君长城苦寒良可哀。 别时提剑救边去,遗此虎纹金鞞靫。 中有一双白羽箭,蜘蛛结网生尘埃。 箭空在,人今战死不复回。 不忍见此物,焚之已成灰。 黄河捧土尚可塞,北风雨雪恨难裁。Vue中文推荐列表加快Vue项目的开发速度2019年Vue学习路线图Vue 性能优化之深挖数组几种常见的Vue组件间的传参方式前端错误收集(Vue.js、微信小程序)vue3.0 尝鲜 – 摒弃 Object.defineProperty,基于 Proxy 的观察者机制探索说说在 Vue.js 中如何实现组件间通信(高级篇)不吹不黑比对下React与Vue的差异与优劣尤雨溪:React 是不是比 Vue 牛,为什么?初探 Vue3.0 中的一大亮点——Proxyvue权限路由实现方式总结二从零实现Vue的组件库(零)-基本结构以及构建工具Vue 2.0学习笔记:Vue的transition推荐一个很好用的vscode插件:一个可以给出vuex中store定义信息的vscode插件vue轻量高效的前端组件化方案以及MVC MVVM思想vue的.vue文件是怎么run起来的(vue-loader)Vuex和Redux都参照了的Flux模式简单版实现vue中的computed的this指向问题Vue 进阶系列(一)之响应式原理及实现在Vue项目中加载krpano全景图如何用vue封装一个防用户删除的平铺页面的水印组件5个Vuex插件,让你下一个VueJS项目开发速度提升3倍Vue源码中为什么要const _toStr = Object.prototype.toString?初探 Vue3.0 中的一大亮点——Proxy !vue中async-await的使用误区快速利用 vue 或者 react 开发 chrome 插件Vue中的基础过渡动画原理解析浏览器事件循环机制与Vue nextTick的实现Vue 源码(一):响应式原理用 vue + d3 画一棵树为什么我会选择 React + Next.js,而不是 Vue 或 Angular?使用Golang的Gin框架和vue编写web应用Vue项目中使用better-scroll实现一个轮播图小白带你学习Vuex从零实现Vue的Toast插件如何在vue项目中优雅的使用SVG记一次简单的vue组件单元测试你可能需要的一本前端小册:Vue 项目构建与开发入门为什么Proxy可以优化vue的数据监听机制月下载量千万的 npm 包被黑客篡改,Vue 开发者可能正在遭受攻击基于vue-cli理解render函数Vue.js的复用组件开发流程Proxy实现vue MVVM实践Vue调试神器之Vue.js devToolsVue一个案例引发「动画」的使用总结Vue.js 3.0发布更新计划基于Vue组件化的日期联动选择器mpvue 单文件页面配置VueConf 杭州 PPT深入浅出Vue使用中的小技巧手把手教你使用 VuePress 搭建个人博客利用Vue原理实现一个mini版的MVVM框架Vue.js 图标选择组件实践逐行粒度的vuex源码分析Vue一个案例引发「内容分发slot」的最全总结Vue英文推荐列表Stories, Chapters and Paragraphs: Structuring Content with Storyblok and Vue.js – Markus OberlehnerStructuring a Vue project — Authentication – Boris SavicVue Development In 2019: What You Need To Know - Anthony Gore Let’s talk about an unnecessary but popular Vue plugin - heftyheadWorking with the Camera in a NativeScript Vue App – Raymond CamdenGitHub - f/vue-smart-routeGitHub - posva/vue-local-scopePromoted Get all products by Creative Tim including Vue premium dashboards 90% offBest resources to learn Vue.js in 2018The Vue.js Conference in Amsterdam will have everything you hope forOfficial Style Guide for Vue-specific codeLaravel Nova Administration Panel with Vue.jsVuePress: What is it and Why it is a great tool to useVue.js Frameworks & Libraries to use in your next project Integrating content management into your Vue.js projects with PrismicVue.js Amsterdam RecordingsiView UI framework 2.4State of Vue.js 2019 survey7 VueConf Toronto Talks Now Live to WatchVue.JS Components for building Search UIs – All thingsStructuring a Vue project — Authentication – Boris Savic Best Code Editor for Vue.js – Vue Mastery My Favorite Vue.js & Nuxt.js packages for 2019 – Nada Rifki Turn your Vue Web App into a PWA! – Bits and PiecesThe State of Javascript 2018: The View on VueVue 2 + Firebase: How to add Firebase Social Sign In into your Vue application更多推荐查看github最新的Vue weekly;请::点击::集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。 ...

December 18, 2018 · 2 min · jiezi

Vue2.0 + ElementUI 手写权限管理系统后台模板(三)——页面搭建

框架布局本章只介绍基础布局,和一些主要的js,页面上基本上都是些交互事件,项目代码上都有注释,不懂的地方debug跑一变就知道了,只是这些事件基本上没有独立存在的,相互之间都有关联框架风格新建页面:/src/views/layout/layout.vue<!– layout.vue –><template> <div id=“loyout”> <el-container> <layoutAside></layoutAside> <el-container> <layoutHeader></layoutHeader> <el-main id=“elmain”> <transition name=“main” mode=“out-in”> <router-view></router-view> </transition> </el-main> <el-footer> <Bottom></Bottom> </el-footer> </el-container> </el-container> </div></template>aside 无限级菜单组件新建页面:/src/views/layout/aside/aside.vue<!– aside.vue –><template> <div> <el-aside id=“asideNav”> <div class=“logo-name”> <p v-if="$store.getters.logoShow">XU</p> <p v-else>vue-xuAdmin后台模板</p> </div> <!– el-menu的属性查看官方文档 –> <el-menu :default-active="$route.path" class=“el-menu-vertical” @select=“selectmenu” :collapse="$store.getters.isCollapse" background-color="#03152A" text-color=“rgba(255,255,255,.7)” active-text-color="#ffffff" :router="$store.getters.uniquerouter" :unique-opened="$store.getters.uniquerouter" :collapse-transition=“true” > <!– 遍历根据权限生成的路由表生成菜单列表 –> <template v-for="(item,index) in $store.getters.routers" v-if="!item.hidden"> <!– 检查是否带有alone属性的一级菜单类似“主页”,还有子菜单的个数 –> <el-submenu v-if="!item.alone && item.children.length>0" :index=“index+’’"> <template slot=“title”> <!– 如果没有设置图标将会采用默认图标‘fa fa-server’ –> <i :class=“item.iconCls?item.iconCls:[fa,fa-server]"></i> <span slot=“title”>{{ $t(routeNmae.${item.name}) }}</span> </template> <!– 子菜单组件 –> <menu-tree :menuData=“item.children”></menu-tree> </el-submenu> <!– 一级菜单 –> <el-menu-item :index=“item.path” v-else> <i :class=“item.iconCls?item.iconCls:[fa,fa-file]"></i> <span slot=“title”>{{ $t(routeNmae.${item.name}) }}</span> </el-menu-item> </template> </el-menu> </el-aside> </div></template>点击菜单// aside.vuewatch: { // 监听浏览器直接输入路由,将此路由添加到tabnavBox ‘$route.path’: function (val) { this.selectmenu(val) } }, // 点击菜单把当前菜单的name和path添加到tabNavBox容器,生成tabNav标签页菜单selectmenu (key) { // 获取当前权限路由表 let router = this.$store.getters.routers let name = ’’ // 查找路由的name属性 let navTitle = function (path, routerARR) { for (let i = 0; i < routerARR.length; i++) { if (routerARR[i].children.length > 0 || routerARR[i].path === path) { if (routerARR[i].path === path && routerARR[i].children.length < 1) { name = routerARR[i].name break } // 递归查找 navTitle(path, routerARR[i].children) } } return name } // tabNavBox添加数据 this.$store.dispatch(‘addTab’, { title: navTitle(key, router), path: key }) }子菜单组件 menu-true新建页面:/src/views/layout/aside/menuTree.vue<!– menuTree.vue –><template> <div> <template v-for="(child,index) in menuData”> <el-submenu v-if=“child.children.length > 0” :index=“child.path”> <template slot=“title”> <i :class=“child.iconCls?child.iconCls:[fa,fa-file]"></i> <span slot=“title”>{{ $t(routeNmae.${child.name}) }}</span> </template> <!– 通过递归 menu-tree 生成无限级菜单 –> <menu-tree :menuData=“child.children”></menu-tree> </el-submenu> <el-menu-item v-else :index=“child.path”> <i :class=“child.iconCls?child.iconCls:[fa,fa-file]"></i> <span slot=“title”>{{ $t(routeNmae.${child.name}) }}</span> </el-menu-item> </template> </div></template>header头部这里没啥好说的,都是html布局,tabnav接下来说, i18n后面会讲新建页面:/src/views/layout/header/header.vue<!– header.vue –><template> <div> <el-header id=“header”> <span class=“hideAside” @click=“collapse”><i class=“fa fa-indent fa-lg”></i></span> <ul class=“personal”> <li class=“fullScreen” @click=“fullScreen”> <el-tooltip class=“item” effect=“dark” content=“全屏” placement=“bottom”><i class=“fa fa-arrows-alt fa-lg”></i></el-tooltip> </li> <li> <langSelect></langSelect> </li> <li>{{ $t(role.${this.$store.getters.info.role}) }}</li> <li> <el-dropdown @command=“handleCommand”> <span class=“el-dropdown-link”> 夏洛克丶旭<i class=“el-icon-arrow-down el-icon–right”></i> </span> <el-dropdown-menu slot=“dropdown”> <el-dropdown-item command=“a”>{{ $t(‘userDropdownMenu.basicInfor’) }}</el-dropdown-item> <el-dropdown-item command=“b”>{{ $t(‘userDropdownMenu.changePassword’) }}</el-dropdown-item> <el-dropdown-item command=“logout” divided>{{ $t(‘userDropdownMenu.logout’) }}</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </li> <li class=“icon”><img :src=“avatar”/></li> </ul> </el-header> <!– tabNav 组件,标签页菜单 –> <tabNav></tabNav> </div></template>tabNav 组件这里的tabNav标签动画和页面的动画是一样的,都是官方的demo稍微改一下,,只不过页面有mode=“out-in"所以动画时间需要快一点新建页面:/src/views/layout/header/tabNav.vue<!– tabNav.vue –><template> <div> <div class=“tabnavBox”> <transition-group name=“list” tag=“ul”> <!– tabnavBox 是存储所有tabNav的数据容器,每次点击左侧菜单就会把数据添加到tabnavBox –> <li v-for="(item, index) in $store.getters.tabnavBox” @contextmenu.prevent=“openMenu(item,$event,index)” :key=“item.title” class=“tabnav” :class=”{ active: $route.path === item.path }"> <router-link :to=“item.path”>{{ $t(routeNmae.${item.title}) }}</router-link> <i @click=“removeTab(item)” class=“el-icon-error” v-if=“index !== 0”></i> </li> </transition-group> </div> <!– 右击菜单 –> <ul v-show=“this.rightMenuShow” :style="{left:this.left+‘px’,top:this.top+‘px’}” class=“menuBox”> <li @click=“removeTab($store.getters.rightNav)"><i class=“fa fa-remove”></i>{{ $t(‘rightMenu.close’) }}</li> <li @click=“removeOtherTab($store.getters.rightNav)">{{ $t(‘rightMenu.closeOther’) }}</li> <li @click=“removeAllTab”>{{ $t(‘rightMenu.closeAll’) }}</li> </ul> </div></template> ...

December 17, 2018 · 2 min · jiezi

Vue2.0 + ElementUI 手写权限管理系统后台模板(二)——权限管理

权限验证页面级别权限路由:默认挂载不需要权限的路由,例如:登录、主页。需要权限的页面通过 router.addRoutes(点击查看官方文档) 动态添加更多的路由规则,404拦截页面需要放在路由表的最后,否则 /404 后面的路由会被404拦截,通过路由元信息meta(点击查看官方文档)记录路由需要的权限。为了菜单列表可以被翻译,路由表的 name 属性值通过 i18n 的英文对照表来获取,也可以直接写英文名称,如 name: routeNmae.builtInIcon 可以直接写成 name: “builtInIcon”,凭个人喜好// src/router/index.jsimport en from ‘../i18n/lang/en’ // 路由名字 name import Vue from ‘vue’import Router from ‘vue-router’import CommerViews from ‘@/views/commerViews’import Login from ‘@/views/login/index’import Layout from ‘@/views/layout/layout’import HomeMain from ‘@/views/index/mainIndex’// 不是必须加载的组件使用懒加载const Icon = () => import(’@/views/icon/index’)const Upload = () => import(’@/views/upload/upload’)const Markdown = () => import(’@/views/markdown/markdownView’)const NotFound = () => import(’@/page404’)Vue.use(Router)let routeNmae = en.routeNmae// 不需要权限的路由let defaultRouter = [ { path: ‘/’, redirect: ‘/index’, hidden: true, children: [] }, { path: ‘/login’, component: Login, name: ‘’, hidden: true, children: [] }, { path: ‘/index’, iconCls: ‘fa fa-dashboard’, // 菜单图标,直接填写字体图标的 class name: routeNmae.home, component: Layout, alone: true, children: [ { path: ‘/index’, iconCls: ‘fa fa-dashboard’, name: ‘主页’, component: HomeMain, children: [] } ] }, { path: ‘/404’, component: NotFound, name: ‘404’, hidden: true, children: [] },]// 需要 addRouters 动态加载的路由 let addRouter = [ { path: ‘/’, iconCls: ‘fa fa-server’, name: routeNmae.multiDirectory, component: Layout, children: [ { path: ‘/erji1’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu2-1’], component: Erji, children: [] }, { path: ‘/erji3’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu2-3’], component: CommerViews, // 无限极菜单的容器 超过三级菜单父级容器需要使用 CommerViews children: [ { path: ‘/sanji2’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu3-2’], component: Sanji2, children: [] }, { path: ‘/sanji3’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu3-3’], component: CommerViews, children: [ { path: ‘/siji’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu4-1’], component: Siji, children: [] }, { path: ‘/siji1’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu4-2’], component: CommerViews, children: [ { path: ‘/wuji’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu5-1’], component: Wuji, children: [] } ] } ] } ] } ] }, { path: ‘/’, iconCls: ’el-icon-edit’, // 图标样式class name: routeNmae.editor, component: Layout, meta: {role: [‘superAdmin’, ‘admin’]}, // 需要权限 ‘superAdmin’, ‘admin’。meta属性可以放在父级,验证父级和所有子菜单,也可以放在子级单独验证某一个子菜单 children: [ { path: ‘/markdown’, iconCls: ‘fa fa-file-code-o’, // 图标样式class name: routeNmae.markdown, component: Markdown, children: [] } ] }, { path: ‘*’, redirect: ‘/404’, hidden: true, children: [] },]export default new Router({ routes: defaultRouter})export {defaultRouter, addRouter}然后通过 token 获取当前登录用户的个人信息,在router被挂载到Vue之前和需要权限的路由表做对比,筛选出当前角色的动态路由表,// main.js// 获取角色信息,根据用户权限动态加载路由router.beforeEach((to, from, next) => { if (store.getters.token) { // 查看 token 是否存在 store.dispatch(‘setToken’, store.getters.token) // 每次操作都重新写入 token,延长有效会话时间 if (to.path === ‘/login’) { next({path: ‘/’}) } else { if (!store.getters.info.role) { // 查看是否有当前用户角色,如果没有则获取角色信息 !async function getAddRouters () { await store.dispatch(‘getInfo’, store.getters.token) // 通过token获取角色信息 await store.dispatch(’newRoutes’, store.getters.info.role) // 通过权限筛选新路由表 await router.addRoutes(store.getters.addRouters) // 动态加载新路由表 next({path: ‘/index’}) }() } else { let is404 = to.matched.some(record => { // 404页面拦截 if(record.meta.role){ // 没有权限的页面,跳转的404页面 return record.meta.role.indexOf(store.getters.info.role) === -1 } }) if(is404){ next({path: ‘/404’}) return false } next() } } } else { if (to.path === ‘/login’) { next() } next({path: ‘/login’}) }})actions: getInfo// src/vuex/modules/role.jsstate: { info: ’’ // 每次刷新都要通过token请求个人信息来筛选动态路由 }, mutations: { getInfo (state, token) { // 省略 axios 请求代码 通过 token 向后台请求用户权限等信息,这里用假数据赋值 state.info = { role: ‘superAdmin’, permissions: ‘超级管理员’ } // 将 info 存储在 sessionStorage里, 按钮指令权限将会用到 sessionStorage.setItem(‘info’, JSON.stringify(store.getters.info)) }, setRole (state, options) { // 切换角色,测试权限管理 state.info = { role: options.role, permissions: options.permissions } sessionStorage.setItem(‘info’, JSON.stringify(store.getters.info)); // 权限切换后要根据新权限重新获取新路由,再走一遍流程 store.dispatch(’newRoutes’, options.role) router.addRoutes(store.getters.addRouters) } }, actions: { getInfo ({commit}, token) { commit(‘getInfo’, token) }, setRole ({commit}, options){// 切换角色,测试权限管理,不需要可以删除 commit(‘setRole’, options) } }actions: newRoutes// src/vuex/modules/routerData.jsimport {defaultRouter, addRouter} from ‘@/router/index’const routerData = {state: { routers: [], addRouters: [] }, mutations: { setRouters: (state, routers) => { state.addRouters = routers // 保存动态路由用来addRouter state.routers = defaultRouter.concat(routers) // 所有有权限的路由表,用来生成菜单列表 } }, actions: { newRoutes ({commit}, role) { // 通过递归路由表,删除掉没有权限的路由 function eachSelect (routers, userRole) { for (let j = 0; j < routers.length; j++) { if (routers[j].meta && routers[j].meta.role.length && routers[j].meta.role.indexOf(userRole) === -1) { // 如果没有权限就删除该路由,如果是父级路由没权限,所有子菜单就更没权限了,所以一并删除 routers.splice(j, 1) j = j !== 0 ? j - 1 : j // 删除掉没有权限的路由后,下标应该停止 +1,保持不变,如果下标是 0的话删除之后依然等于0 } if (routers[j].children && routers[j].children.length) { // 如果包含子元素就递归执行 eachSelect(routers[j].children, userRole) } } } // 拷贝这个数组是因为做权限测试的时候可以从低级切回到高级角色,仅限演示,正式开发时省略这步直接使用 addRouter // 仅限演示 let newArr = […addRouter] eachSelect(newArr, role) commit(‘setRouters’, newArr) // 正式开发 // eachSelect(addRouter, role) // commit(‘setRouters’, addRouter) } }}export default routerData按钮级别权限验证通过自定义指令获取当前按钮所需的有哪些权限,然后和当前用户的权限对比,如果没有权限则删除按钮// btnPermission.jsimport Vue from ‘vue’Vue.directive(‘roleBtn’,{ bind:function (el,binding) { let roleArr = binding.value; // 获取按钮所需权限 let userRole = JSON.parse(sessionStorage.getItem(‘info’)).role // 获取当前用户权限 if (roleArr && roleArr.indexOf(userRole) !== -1) { return false } else { el.parentNode.removeChild(el); } }})export default Vue使用自定义指令权限<el-button type=“primary” plain size=“medium”>查看</el-button><el-button type=“primary” plain size=“medium” v-role-btn="[‘admin’]">添加</el-button><el-button type=“danger” plain size=“medium” v-role-btn="[‘superAdmin’]">删除</el-button><el-button type=“primary” plain size=“medium” v-role-btn="[‘superAdmin’,‘admin’]">修改</el-button> ...

December 17, 2018 · 4 min · jiezi

Vue2.0 + ElementUI 手写权限管理系统后台模板(四)——组件结尾

i18n国际化多语言翻译使用框架采用vue-i18n版本 8.4.0,使用npm安装新建文件夹src/i18n,目录如下i18n.js//i18n.jsimport Vue from ‘vue’import locale from ’element-ui/lib/locale’import VueI18n from ‘vue-i18n’import messages from ‘./lang’Vue.use(VueI18n)const i18n = new VueI18n({ locale: localStorage.lang || ‘cn’, messages})locale.i18n((key, value) => i18n.t(key, value))export default i18ni18n/lang/index.js//index.jsimport en from ‘./en’import cn from ‘./cn’export default { en, cn}i18n/lang/cn.jscn.js和en.js 需要要翻译的内容要一一对照,我这里这是参考示例只写了一部分//cn.jsimport zhLocale from ’element-ui/lib/locale/lang/zh-CN’const cn = { home: ‘主页’, routeNmae: { home: ‘主页’, article: ‘文章管理’, ‘menu2-2’: ‘二级-2’, ‘menu2-3’: ‘二级-3’, }, rightMenu: { close: ‘关闭’, closeOther: ‘关闭其他’, closeAll: ‘全部关闭’ } …zhLocale // 合并element-ui内置翻译}export default cni18n/lang/en.js//en.jsimport enLocale from ’element-ui/lib/locale/lang/en’const en = { home: ‘home’, routeNmae: { home: ‘home’, article: ‘article’, ‘menu2-2’: ‘menu2-2’, ‘menu2-3’: ‘menu2-3’ }, rightMenu: { close: ‘close’, closeOther: ‘closeOther’, closeAll: ‘closeAll’ } …enLocale // 合并element-ui内置翻译}export default en多语言切换组件新建src/components/lang/langSelect.vue<!– langSelect.vue –><template> <el-dropdown class=‘international’ @command=“handleSetLanguage”> <div> <span class=“el-dropdown-link”><i class=“fa fa-language fa-lg”></i>&nbsp;{{language}}<i class=“el-icon-arrow-down el-icon–right”></i> </span> </div> <el-dropdown-menu slot=“dropdown”> <el-dropdown-item command=“cn”>中文</el-dropdown-item> <el-dropdown-item command=“en”>English</el-dropdown-item> </el-dropdown-menu> </el-dropdown></template>main.jsimport Vue from ‘./btnPermission’import ElementUI from ’element-ui’import ’element-ui/lib/theme-chalk/index.css’import ‘font-awesome/css/font-awesome.css’import App from ‘./App.vue’import router from ‘./router’import store from ‘./vuex’import i18n from ‘./i18n/i18n’new Vue({ el: ‘#app’, router, store, i18n, render: h => h(App), components: {App}, template: ‘<App/>’})使用:<!– 翻译使用 –><p>message: {{ $t(‘home’) }}</p><p>message: {{ $t(‘routeNmae.article’) }}</p><!– 多语言切换组件调用 –><langSelect></langSelect>vue中使用ECharts具体使用方法可以查看ECharts官网,需要注意的地方就是响应屏幕大小代码如下,在调用组件的页面 mounted () { this.selfAdaption() }, methods: { // echart自适应 selfAdaption () { let that = this setTimeout(() => { window.onresize = function () { if (that.$refs.echarts) { that.$refs.echarts.chart.resize() } } }, 10) } }编辑器-markdown框架目前只封装了markdown,实时获取markdown,html,text三种格式文本,支持内容回填,默认初始值,可以编辑已发布的文章或者草稿引用的Editor.md,点击查看插件更多的使用方法结束vue-xuAdmin 只注重框架基础功能,这几个组件是我最近用到的,更多的组件内容根据项目需求可以自己去封装。如果你感觉这个框架或者这几篇文章对你有所帮助,请去项目git上给个星点个star,感谢!orz项目地址:github:https://github.com/Nirongxu/v…码云:https://gitee.com/nirongxu/xu… ...

December 17, 2018 · 1 min · jiezi

vue router的使用

vue router的使用1、安装:npm install vue-router –save2、新建router文件夹,建立index.js文件如下:import Vue from ‘vue’import Router from ‘vue-router’//路由懒加载const login = resolve => require([’../login.vue’], resolve);const Apps = resolve => require([’../App.vue’], resolve);const HelloWorld = resolve => require([’../components/HelloWorld’], resolve);const second = resolve => require([’../components/second’], resolve);const detail = resolve => require([’../components/detail’], resolve);Vue.use(Router)const routes = [ { path: “/login”, name: “login”, component: login }, { path: “/Apps”, name: “Apps”, component: Apps, children: [ { path: “/HelloWorld”, name: “HelloWorld”, component: HelloWorld }, { path: “/second”, name: “second”, component: second }, { path: “/detail”, name: “detail”, component: detail } ] }, { path: ‘/’, redirect: ‘/login’ }]var router = new Router({ linkActiveClass:’list-active’, //设置当前路由style routes})export default router;3、在main.js中应用import Vue from ‘vue’;import router from ‘./router’;var v = new Vue({ el: ‘#app’, router, components: {App}, template: ‘<App/>’, created: function () { }})4、在APP.vue中设置路由跳转<template> <div id=“apps”> <div class=“leftNav”> <router-link to="/HelloWorld">链接一</router-link> <router-link to="/second">链接二</router-link> </div> <div class=“rightCon”> <router-view/> </div> </div></template><script> import HelloWorld from ‘./components/HelloWorld’ import second from ‘./components/second’ export default { name: ‘Apps’, components: { HelloWorld, second }, data () { return { } } }</script>5、js跳转:this.$router.push({name: ‘detail’, query: {userInfo: thisName}})6、vue-router默认是hash模式,切换成history模式var router = new Router({ mode: ‘history’, linkActiveClass:’list-active’, //设置当前路由style routes})7、github:vue-router,欢迎star。 ...

December 17, 2018 · 1 min · jiezi

vue-router 模块化

在使用vue开发中,前期常常会将所有的路由写在一个文件中。但是当项目过大的时候,会面临路由文件过程,难以维护的问题。通过webpack的require.context()函数,可以自动导入vue-router模块。1.分割路由文件router // 路由文件夹 |__index.js // 路由组织器:用来初始化路由等等 |__modules // 业务逻辑模块:所以的业务逻辑模块 |__index.js // 自动化处理文件:自动引入路由的核心文件 |__a.js // 业务模块a |__b.js // 业务模块b |__c.js // 业务模块c 2.modules文件夹modules文件夹中存放着所有的业务模块与自动化引入的index.js。业务模块内容根据具体业务自行编写。本文代码仅为示例。// a模块export default [ { path: ‘/a’, name: ‘A’, component: A, meta: { name_str: ‘首页’ }]自动化导入模块index.jsconst manageFiles = require.context(’.’, true, /.js$/)console.log(manageFiles.keys()) // [’./a.js’] 返回一个数组,包含全部文件名let configRouters = []manageFiles.keys().forEach(key => { if (key === ‘./index.js’) return // 如果是当前文件,则跳过 configRouters = configRouters.concat(manageFiles(key).default) // 读取出文件中的default模块})export default configRouters // 抛出一个Vue-router期待的结构的数组路由初始化import Vue from ‘vue’import VueRouter from ‘vue-router’import RouterConfig from ‘./modules’ // 引入业务逻辑模块Vue.use(VueRouter)export default new VueRouter({ scrollBehavior: () => ({ y: 0 }), routes: RouterConfig}) ...

December 14, 2018 · 1 min · jiezi

解析Vue-router相关干货及工作原理

本文主要介绍了vue-router相关基础知识及单页面应用的工作原理,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。单页面工作原理是通过浏览器URL的#后面的hash变化就会引起页面变化的特性来把页面分成不同的小模块,然后通过修改hash来让页面展示我们想让看到的内容。那么为什么hash的不同,为什么会影响页面的展示呢?浏览器在这里面做了什么内容。以前#后面的内容一般会做锚点,但是会定位到一个页面的某个位置,这个是怎么做到的呢,和我们现在的路由有什么不同。(我能想到一个路由的展示就会把其他路由隐藏,是这样的吗)后面会看一看写一下这个疑惑,现在最重要的是先把基本概念弄熟。当你要把 vue-router 添加进来,我们需要做的是,将组件(components)映射到路由(routes),然后告诉 vue-router 在哪里渲染它们起步//*** router-link 告诉浏览器去哪个路由//*** router-view 告诉路由在哪里展示内容<div id=“app”> <h1>Hello App!</h1> <p> <!– 使用 router-link 组件来导航. –> <!– 通过传入 to 属性指定链接. –> <!– <router-link> 默认会被渲染成一个 &lt;a&gt; 标签 –> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> <!– 路由出口 –> <!– 路由匹配到的组件将渲染在这里 –> <router-view></router-view></div>// 1. 定义(路由)组件。// 可以从其他文件 import 进来const Foo = { template: ‘<div>foo</div>’ }const Bar = { template: ‘<div>bar</div>’ }// 2. 定义路由// 每个路由应该映射一个组件。 其中"component" 可以是// 通过 Vue.extend() 创建的组件构造器,// 或者,只是一个组件配置对象。// 我们晚点再讨论嵌套路由。const routes = [ { path: ‘/foo’, component: Foo }, { path: ‘/bar’, component: Bar }]//欢迎加入前端全栈开发交流圈一起学习交流:864305860// 3. 创建 router 实例,然后传 routes 配置// 你还可以传别的配置参数, 不过先这么简单着吧。const router = new VueRouter({ routes // (缩写)相当于 routes: routes})// 4. 创建和挂载根实例。// 记得要通过 router 配置参数注入路由,// 从而让整个应用都有路由功能const app = new Vue({ router}).$mount(’#app’)// 现在,应用已经启动了!动态路由匹配相当于同一个组件,因为参数不同展示不同的组件内容,其实就是在 vue-router 的路由路径中使用『动态路径参数』const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: ‘/user/:id’, component: User } ]})那么我们进入uesr/001 和 user/002 其实是进入的同一个路由,可以根据参数的不同在内容页展示不同的内容。一般适用场景:列表,权限控制定义的时候用: 表示是动态路由使用 {{ $route.params.id }} 来拿到本路由里面参数的内容当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch(监测变化) $route 对象const User = { template: ‘…’, watch: { ‘$route’ (to, from) { // 对路由变化作出响应… }//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }}有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。嵌套路由在路由里面嵌套一个路由//路由里面也会出现 <router-view> 这是嵌套路由展示内容的地方const User = { template: &lt;div class="user"&gt; &lt;h2&gt;User {{ $route.params.id }}&lt;/h2&gt; &lt;router-view&gt;&lt;/router-view&gt; &lt;/div&gt;}//定义路由的时候在 加children 子路由属性const router = new VueRouter({ routes: [ { path: ‘/user/:id’, component: User, children: [ { // 当 /user/:id/profile 匹配成功, // UserProfile 会被渲染在 User 的 <router-view> 中 path: ‘profile’, component: UserProfile },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 { // 当 /user/:id/posts 匹配成功 // UserPosts 会被渲染在 User 的 <router-view> 中 path: ‘posts’, component: UserPosts } ] } ]})设置空路由,在没有指定路由的时候就会展示空路由内容const router = new VueRouter({ routes: [ { path: ‘/user/:id’, component: User, children: [ // 当 /user/:id 匹配成功, // UserHome 会被渲染在 User 的 <router-view> 中 { path: ‘’, component: UserHome }, ]//欢迎加入前端全栈开发交流圈一起学习交流:864305860 } ]})编程式导航声明式:<router-link :to="…">编程式:router.push(…)可以想象编程式 push 可以理解为向浏览器历史里面push一个新的hash,导致路由发生变化router.replace() 修改路由但是不存在历史里面router.go(n) 有点像JS的window.history.go(n)命名路由 就是给每一个路由定义一个名字。命名视图有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar(侧导航) 和 main(主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。<router-view class=“view one”></router-view><router-view class=“view two” name=“a”></router-view><router-view class=“view three” name=“b”></router-view>一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置(带上 s):const router = new VueRouter({ routes: [ { path: ‘/’, components: { default: Foo, a: Bar, b: Baz } } ]})//欢迎加入前端全栈开发交流圈一起学习交流:864305860重定向和别名重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: ‘/b’ } ]})一般首页的时候可以重定向到其他的地方重定向的目标也可以是一个命名的路由:const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: { name: ‘foo’ }} ]//欢迎加入前端全栈开发交流圈一起学习交流:864305860})甚至是一个方法,动态返回重定向目标:const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: to => { // 方法接收 目标路由 作为参数 // return 重定向的 字符串路径/路径对象 }} ]})『重定向』的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么『别名』又是什么呢?/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。上面对应的路由配置为:const router = new VueRouter({ routes: [ { path: ‘/a’, component: A, alias: ‘/b’ } ]//欢迎加入前端全栈开发交流圈一起学习交流:864305860})『别名』的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。HTML5 History 模式ue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。const router = new VueRouter({ mode: ‘history’, routes: […]})//欢迎加入前端全栈开发交流圈一起学习交流:864305860当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,也好看!不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。给个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。const router = new VueRouter({ mode: ‘history’, routes: [//欢迎加入前端全栈开发交流圈一起学习交流:864305860 { path: ‘*’, component: NotFoundComponent } ]})或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。导航守卫我的理解 就是组件或者全局级别的 组件的钩子函数正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。全局守卫const router = new VueRouter({ … })router.beforeEach((to, from, next) => { // …})每个守卫方法接收三个参数:to: Route: 即将要进入的目标 路由对象from: Route: 当前导航正要离开的路由next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false): 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。next(‘/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。确保要调用 next 方法,否则钩子就不会被 resolved。全局后置钩子你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:router.afterEach((to, from) => { // …})\路由独享的守卫你可以在路由配置上直接定义 beforeEnter 守卫:const router = new VueRouter({ routes: [ { path: ‘/foo’, component: Foo, beforeEnter: (to, from, next) => { // …//欢迎加入前端全栈开发交流圈一起学习交流:864305860 } } ]})这些守卫与全局前置守卫的方法参数是一样的。组件内的守卫最后,你可以在路由组件内直接定义以下路由导航守卫:beforeRouteEnter beforeRouteUpdate (2.2 新增) beforeRouteLeave const Foo = { template: ..., beforeRouteEnter (to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不!能!获取组件实例 this // 因为当守卫执行前,组件实例还没被创建 },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 beforeRouteUpdate (to, from, next) { // 在当前路由改变,但是该组件被复用时调用 // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候, // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 this }, beforeRouteLeave (to, from, next) { // 导航离开该组件的对应路由时调用 // 可以访问组件实例 this }}beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。完整的导航解析流程导航被触发。在失活的组件里调用离开守卫。调用全局的 beforeEach 守卫。在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。在路由配置里调用 beforeEnter。解析异步路由组件。在被激活的组件里调用 beforeRouteEnter。调用全局的 beforeResolve 守卫 (2.5+)。导航被确认。调用全局的 afterEach 钩子。触发 DOM 更新。用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。路由元信息我的理解就是 他可以把路由的父路径都列举出来,完成一些任务,比如登录,user 组件需要登录,那么user下面的foo组件也需要,那么可以通过这个属性 来检测这个路由线上 的一些状态。定义路由的时候可以配置 meta 字段:const router = new VueRouter({ routes: [ {//欢迎加入前端全栈开发交流圈一起学习交流:864305860 path: ‘/foo’, component: Foo, children: [ { path: ‘bar’, component: Bar, // a meta field meta: { requiresAuth: true } } ] } ]})首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。一个路由匹配到的所有路由记录会暴露为 $route 对象(还有在导航守卫中的路由对象)的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。下面例子展示在全局导航守卫中检查元字段: if (to.matched.some(record => record.meta.requiresAuth)) { // this route requires auth, check if logged in // if not, redirect to login page. if (!auth.loggedIn()) { next({//欢迎加入前端全栈开发交流圈一起学习交流:864305860 path: ‘/login’, query: { redirect: to.fullPath } }) } else { next() } } else { next() // 确保一定要调用 next() }//欢迎加入前端全栈开发交流圈一起学习交流:864305860})数据获取我的理解就是在哪里获取数据,可以再组件里面,也可以在组件的守卫里面,也就是组件的生命周期里面。有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示『加载中』之类的指示。导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。导航完成后获取数据当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。假设我们有一个 Post 组件,需要基于 $route.params.id 获取文章数据:<template> <div class=“post”> <div class=“loading” v-if=“loading”> Loading… </div> <div v-if=“error” class=“error”> {{ error }} </div> <div v-if=“post” class=“content”> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div>//欢迎加入前端全栈开发交流圈一起学习交流:864305860 </div></template>export default { data () { return { loading: false, post: null, error: null } }, created () { // 组件创建完后获取数据, // 此时 data 已经被 observed 了 this.fetchData() }, watch: { // 如果路由有变化,会再次执行该方法 ‘$route’: ‘fetchData’ },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 methods: { fetchData () { this.error = this.post = null this.loading = true // replace getPost with your data fetching util / API wrapper getPost(this.$route.params.id, (err, post) => { this.loading = false if (err) { this.error = err.toString() } else {//欢迎加入前端全栈开发交流圈一起学习交流:864305860 this.post = post } }) } }}在导航完成前获取数据通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法。export default { data () { return { post: null, error: null } }, beforeRouteEnter (to, from, next) { getPost(to.params.id, (err, post) => { next(vm => vm.setData(err, post)) })//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }, // 路由改变前,组件就已经渲染完了 // 逻辑稍稍不同 beforeRouteUpdate (to, from, next) { this.post = null getPost(to.params.id, (err, post) => { this.setData(err, post) next()//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }) }, methods: { setData (err, post) { if (err) { this.error = err.toString() } else { this.post = post }//欢迎加入前端全栈开发交流圈一起学习交流:864305860 } }}在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 13, 2018 · 5 min · jiezi

深入了解最新的Vue Devtools v5.0

早些时候发布了Vue devtools 5.0beta版,为已经调试过的强大工具带来了惊人的新功能。一些新功能包括性能分析,路线跟踪,Vuex store的实时编辑以及新的设置面板。这些新的功能我觉得是肯定会出来的,在使用它们几周后,我分享一下对我们最直观感受的东西。让我们来了解一些新功能和在使用它们在调试过程中的新见解。路由Routing选项卡是devtools套件的全新选项。这里有两个不同的视图,“历史记录”和“路径”,可以通过单击“路由”选项卡标题进行交换。如果您在应用程序中使用vue-router,这些都会提供有用的信息。 历史视图有两个面板。左侧面板显示已经前往的路线的历史记录。单击其中一个历史记录条目将在右侧面板中显示该路径更改的详细信息。这些详细信息显示用户导航和导航的路线,以及任何伴随的路线参数。 路由视图还有两个面板,左侧面板显示应用程序中所有路径的映射。单击其中一条路线将在右侧面板中显示其详细信息。此处的详细信息与上一个视图略有不同,而是显示路径,任何子项(嵌套)路由和任何路由参数。VuexVuex已经是老功能了,但它有一个惊人的新功能; 您现在可以从devtools更新应用程序状态!这个功能一直期待已久。在更新之前,改变状态的过程要繁琐得多。您必须重新给一个真确的的Mutation以获得您想要的状态,或者您必须手动更新Vuex模块文件中的默认值。现在,您只需单击任何状态值,然后从那里更新或删除。此外,您甚至可以在现有对象上添加新属性!性能与路由选项卡一样,性能选项卡也是一个新增功能。此选项卡由两部分组成,“每秒帧数”和“组件渲染”。第一个选项卡“每秒帧数”显示一个实时源图表,其中包含应用程序的当前fps。这可用于查找减慢应用程序速度的某些操作或组件。在下图中,您可以看到图表中的第一个红色凹陷在其顶部有“M”,“E”和“R”图标。“M”表示发生Mutation,“E”表示事件被触发,“R”表示路径发生变化。我们可以预判应用程序的fps会暂时降低路径变化,但如果这是意外下降,那我们更加容易查出哪些组件消耗了比较多的资源。“Component Render”选项卡的第二张图片显示了组件生命周期方法的详细运行时间统计信息。左窗格显示组件的总渲染时间,而右窗格按生命周期方法提供细分。任何极慢的组件都会在这个左侧标签中脱颖而出,这再次为调查性能问题提供了一个良好的起点。设置最后但并非最不重要的是,有一个新的设置菜单!目前,此菜单包括以下选项:将显示密度更改为更紧凑的布局规范化组件名称(my-component将变为MyComponent)更新主题 - 打开或关闭新的黑暗主题选项结论Vue核心团队及其社区再一次提供了令人惊叹的开发人员工具体验。Vue devtools一直是开发人员体验中不可或缺的一部分,而且这次更新是朝着正确方向发展的巨大推动力。此版本的devtools与最近发布的vue-cli GUI相结合,提供了从创建到完成,令人惊讶的开发体验。下载地址Chrome官方地址(需自备梯子)

November 24, 2018 · 1 min · jiezi

Weex系列(3) —— 单页面还是多页面

目录Weex系列(序) —— 总要知道原生的一点东东(iOS)Weex系列(序) —— 总要知道原生的一点东东(Android)Weex系列(1) —— Hello World项目Weex系列(2) —— 页面跳转和通信Weex系列(3) —— 单页面还是多页面[Weex系列(4) —— 老生常谈的三端统一][Weex系列(5) —— 封装原生组件和模块][Weex系列(6) —— css相关小结][Weex系列(7) —— web组件和webview][Weex系列(8) —— 是时候简析一下流程原理了][Weex系列(9) —— 踩坑填坑的集锦][Weex系列(10) —— 先这么多吧想到再写。。。]时间总是过得那么快,一周又过去了。天越来越冷了,感觉跟要冬眠似的,越来越懒得动脑了,哈哈哈,下面开始进入我们的主题吧。单页面应用单页面应用(single page web application,SPA),大家应该很熟悉了,现在好多页面都采用的是这种模式,优缺点网上一搜一箩筐,支持的框架也有很多,react全家桶、vue全家桶等。Weex的上层语言有vue,所以我们是不是也可以用vue全家桶来打造一个App,官网的回答是可以的。用weex脚手架初始化项目,选项vue-router后面竟然跟了一个(not recommended)不推荐的。demo如下图,这个例子很简单,就不上传代码了,其实官网有一个很典型的例子weex-hackernews(https://github.com/weexteam/w…,用了vuex和vue-router,感觉入了weex这个坑的(doge),应该都看过研究过这个例子吧。官网有一个 使用 Vuex 和 vue-router ,大家也可以点进去看一下。然后我们来简单分析一下吧一个bundlejs上面的例子,虽然有三个tab,还有一个page3,感觉好多页面的样子,像web一样,最后打包只有一个js,是不是感觉到一丝不对的气息,是啊,这么一个大的app就这么一个js。1、首次打开白屏时间长2、不能按需加载对应页面js3、整个app使用相同的执行环境,隐患很多等一般app都是越做越大,越做越复杂,想想是不是有点可怕呢。所以官网也是引导我们集成Weex到已有的app。多页面应用其实原生app本就是多页面的场景,好比浏览器可以开很多窗口,上面那个例子就只是在一个窗口里来回折腾。说了这么多,那上面那个例子的底部tab1、2、3怎么实现呢,对,这就是多页面的成本,应该有好多跟我们一样,完全用Weex开发出一个从无到有的app,考虑了很多,底部这块我们还是决定用原生去做,这块我们是找了原生开发同学去做了一些支持的,这块据说是原生开发很基础很基础的一部分,大概半天就能搞定,可是后续的扩展性、性能优化、延展性等就好说多了,下面仅提供我们这边的一个思路。iOS: UITabBarController + UIViewController 把tab1、2、3.js的路径分别赋值给UIViewController,之前也有分析过WXDemoViewController大家可以去看看。UIViewController * weexVC = [[WXDemoViewController alloc] init];((WXDemoViewController *)weexVC).url = url;Android: 这个用的是Fragment,网上搜weex Fragment,会出来好多有参考价值的文章,大家可以去了解一下,我就不截图了,怕有版权之类的。navigator感觉这个词在我前面的文章里也是多次出现过了。是啊,底部tab1对应tab1.js渲染完页面,怎么进去到相应的page.js呢,就是我上一篇讲的了,用的基本就是navigator了,而且在page.js对应的页面,我们也是可以使用vue-router的。这个当然是用原生的模块组件封装的,有兴趣的可以看看WXNavigatorModule.m这个文件,所以页面的进退、切换等效果也都是极佳的,个人感觉完全超过单页面应用。小结读完文章的不难发现,我的观点就是偏向于多页面应用。各有所需,大家完全可以根据自己的场景来选择,如果你的app页面不多、轻量等,完全也是可以用单页面模式的。最后如果大家有一点点喜欢,对你有一点点的帮助,欢迎点赞收藏啊。

November 22, 2018 · 1 min · jiezi

Vue 全家桶实现一个移动端酷狗音乐

Vue 已经用了不少时间,最近抽空把以前的未完成的酷狗音乐做完了,过来分享下,也可以直接点这里预览,注意切换成手机模式。技术栈: vue-router、eventBus、vuex、vue-awesome-swiper整体功能 vs 酷狗官网:总体模拟官网,原来的亮点保留,如:图片懒加载除此之外,增加了加了全局的 Loading 组件,根据不同页面调整 Loading 尺寸搜索页面做了优化,可以在刷新时保留之前的搜索结果播放页面单独做了一个路由,可以在刷新时保留当前歌曲页面播放器的常驻以及滚动时最小化,避免遮挡歌曲名称部分可以重用,极少更新的数据,譬如主页四大栏,避免了数据的二次请求。增加了主页四栏手势滑动切换歌词滚动等…如果参考网易云,后续可以加的新功能还有一些,不过暂时我要先去做其他事了。CSS 觉得不难,都是手写的,采用的是 BEM 规范,js 是 ESLint。总体难度适中,只不过,如果规范化,组件化抽象,任务还是不少的,具体的坑我就不说了,源代码都在这里,推荐想要熟悉 vue 的同学也自己试下。作为一个练手项目,vue 全家桶都覆盖到了,当然,如果你只用 vue 和 vue-router 去实现,也不是不行,实现到一大半,就会明白为什么要全家桶了。至于酷狗的接口以及跨域的问题,解决方案都在 README 里,都是借用的其他作者的分享与整理,在此还是要感谢下 ecitlm 和 JsonBird。src/ 文件目录:|__ App.vue |__ assets |__ css |__ base.less |__ constant.less |__ iconfont.css |__ reset.css |__ images |__ logo__grey.png |__ logo__text.png |__ logo__theme.png |__ js |__ api.js |__ bus.js |__ globalEvent.js |__ mobileLayout.js |__ utils.js |__ components |__ Main.vue |__ new_song |__ NewSong.vue |__ Slider.vue |__ player |__ NextButton.vue |__ PlayButton.vue |__ PlayerLyrics.vue |__ PlayerMax.vue |__ PlayerMed.vue |__ PlayerProgress.vue |__ PrevButton.vue |__ public |__ AppHeader.vue |__ AppLoading.vue |__ AppMusicList.vue |__ AppNav.vue |__ PubList.vue |__ PubModuleDes.vue |__ PubModuleHead.vue |__ PubModuleTitle.vue |__ rank |__ RankInfo.vue |__ RankList.vue |__ search |__ Search.vue |__ singer |__ SingerCategory.vue |__ SingerInfo.vue |__ SingerList.vue |__ song |__ SongList.vue |__ SongListInfo.vue |__ main.js |__ mixins |__ index.js |__ loading.js |__ router |__ index.js |__ store |__ device.js |__ images.js |__ index.js |__ loading.js |__ newSong.js |__ player.js |__ rank.js |__ search.js |__ singer.js |__ song.js ...

November 16, 2018 · 1 min · jiezi

从头开始学习vue-router

一、前言要学习vue-router就要先知道这里的路由是什么?为什么我们不能像原来一样直接用标签编写链接哪?vue-router如何使用?常见路由操作有哪些?等等这些问题,就是本篇要探讨的主要问题。二、vue-router是什么这里的路由并不是指我们平时所说的硬件路由器,这里的路由就是SPA(单页应用)的路径管理器。再通俗的说,vue-router就是WebApp的链接路径管理系统。vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系。至于我们为啥不能用a标签,这是因为用Vue做的都是单页应用(当你的项目准备打包时,运行npm run build时,就会生成dist文件夹,这里面只有静态资源和一个index.html页面),所以你写的标签是不起作用的,你必须使用vue-router来进行管理。三、vue-router实现原理SPA(single page application):单一页面应用程序,只有一个完整的页面;它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面;vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式;根据mode参数来决定采用哪一种方式。1、Hash模式:vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。hash 模式的原理是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件。2、History模式:由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: ‘history’",这种模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。//main.js文件中const router = new VueRouter({ mode: ‘history’, routes: […]})当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,比较好看!不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。 export const routes = [ {path: “/”, name: “homeLink”, component:Home} {path: “/register”, name: “registerLink”, component: Register}, {path: “/login”, name: “loginLink”, component: Login}, {path: “”, redirect: “/”}]此处就设置如果URL输入错误或者是URL 匹配不到任何静态资源,就自动跳到到Home页面3、使用路由模块来实现页面跳转的方式方式1:直接修改地址栏方式2:this.$router.push(‘路由地址’)方式3:<router-link to=“路由地址”></router-link>四、vue-router使用方式 1:下载 npm i vue-router -S 2:在main.js中引入 import VueRouter from ‘vue-router’; 3:安装插件 Vue.use(VueRouter); 4:创建路由对象并配置路由规则 let router = new VueRouter({routes:[{path:’/home’,component:Home}]}); 5:将其路由对象传递给Vue的实例,options中加入 router:router 6:在app.vue中留坑 <router-view></router-view>具体实现请看如下代码://main.js文件中引入import Vue from ‘vue’;import VueRouter from ‘vue-router’;//主体import App from ‘./components/app.vue’;import Home from ‘./components/home.vue’//安装插件Vue.use(VueRouter); //挂载属性//创建路由对象并配置路由规则let router = new VueRouter({ routes: [ //一个个对象 { path: ‘/home’, component: Home } ]});//new Vue 启动new Vue({ el: ‘#app’, //让vue知道我们的路由规则 router: router, //可以简写router render: c => c(App),})最后记得在在app.vue中“留坑”//app.vue中<template> <div> <!– 留坑,非常重要 –> <router-view></router-view> </div></template><script> export default { data(){ return {} } }</script>五、vue-router核心要点1.vue-router参数传递声明式的导航<router-link :to="…">和编程式的导航router.push(…)都可以传参,本文主要介绍前者的传参方法,同样的规则也适用于编程式的导航。①用name传递参数在路由文件src/router/index.js里配置name属性routes: [ { path: ‘/’, name: ‘Hello’, component: Hello }]模板里(src/App.vue)用$route.name来接收比如:<p>{{ $route.name}}</p>②通过<router-link> 标签中的to传参这种传参方法的基本语法:<router-link :to="{name:xxx,params:{key:value}}">valueString</router-link>比如先在src/App.vue文件中<router-link :to="{name:‘hi1’,params:{username:‘jspang’,id:‘555’}}">Hi页面1</router-link>然后把src/router/index.js文件里给hi1配置的路由起个name,就叫hi1.{path:’/hi1’,name:‘hi1’,component:Hi1}最后在模板里(src/components/Hi1.vue)用$route.params.username进行接收.{{$route.params.username}}-{{$route.params.id}}③vue-router 利用url传递参数—-在配置文件里以冒号的形式设置参数。我们在/src/router/index.js文件里配置路由{ path:’/params/:newsId/:newsTitle’, component:Params}我们需要传递参数是新闻ID(newsId)和新闻标题(newsTitle).所以我们在路由配置文件里制定了这两个值。在src/components目录下建立我们params.vue组件,也可以说是页面。我们在页面里输出了url传递的的新闻ID和新闻标题。<template> <div> <h2>{{ msg }}</h2> <p>新闻ID:{{ $route.params.newsId}}</p> <p>新闻标题:{{ $route.params.newsTitle}}</p> </div></template><script>export default { name: ‘params’, data () { return { msg: ‘params page’ } }}</script>在App.vue文件里加入我们的<router-view>标签。这时候我们可以直接利用url传值了<router-link to="/params/198/jspang website is very good">params</router-link>④使用path来匹配路由,然后通过query来传递参数<router-link :to="{ name:‘Query’,query: { queryId: status }}" > router-link跳转Query</router-link>对应路由配置: { path: ‘/query’, name: ‘Query’, component: Query }于是我们可以获取参数:this.$route.query.queryId2.单页面多路由区域操作在一个页面里我们有2个以上<router-view>区域,我们通过配置路由的js文件,来操作这些区域的内容①App.vue文件,在<router-view>下面新写了两行<router-view>标签,并加入了些CSS样式<template> <div id=“app”> <img src="./assets/logo.png"> <router-link :to="{name:‘HelloWorld’}"><h1>H1</h1></router-link> <router-link :to="{name:‘H1’}"><h1>H2</h1></router-link> <router-view></router-view> <router-view name=“left” style=“float:left;width:50%;background-color:#ccc;height:300px;”/> <router-view name=“right” style=“float:right;width:50%;background-color:yellowgreen;height:300px;”/> </div></template>②需要在路由里配置这三个区域,配置主要是在components字段里进行export default new Router({ routes: [ { path: ‘/’, name: ‘HelloWorld’, components: {default: HelloWorld, left:H1,//显示H1组件内容’I am H1 page,Welcome to H1’ right:H2//显示H2组件内容’I am H2 page,Welcome to H2’ } }, { path: ‘/h1’, name: ‘H1’, components: {default: HelloWorld, left:H2,//显示H2组件内容 right:H1//显示H1组件内容 } } ] })上边的代码我们编写了两个路径,一个是默认的‘/’,另一个是‘/Hi’.在两个路径下的components里面,我们对三个区域都定义了显示内容。最后页面展示如下图:3.vue-router配置子路由(二级路由)实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL中各段动态路径也按某种结构对应嵌套的各层组件,例如:如何实现下图效果(H1页面和H2页面嵌套在主页中)?①首先用<router-link>标签增加了两个新的导航链接<router-link :to="{name:‘HelloWorld’}">主页</router-link><router-link :to="{name:‘H1’}">H1页面</router-link><router-link :to="{name:‘H2’}">H2页面</router-link>②在HelloWorld.vue加入<router-view>标签,给子模板提供插入位置<template> <div class=“hello”> <h1>{{ msg }}</h1> <router-view></router-view> </div></template>③在components目录下新建两个组件模板 H1.vue 和 H2.vue 两者内容类似,以下是H1.vue页面内容:<template> <div class=“hello”> <h1>{{ msg }}</h1> </div></template><script> export default { data() { return { msg: ‘I am H1 page,Welcome to H1’ } } }</script>④修改router/index.js代码,子路由的写法是在原有的路由配置下加入children字段。 routes: [ { path: ‘/’, name: ‘HelloWorld’, component: HelloWorld, children: [{path: ‘/h1’, name: ‘H1’, component: H1},//子路由的<router-view>必须在HelloWorld.vue中出现 {path: ‘/h2’, name: ‘H2’, component: H2} ] } ]4.$route 和 $router 的区别我们先将这两者console.log打印出来:$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。① $route.path字符串,对应当前路由的路径,总是解析为绝对路径,如 “/order”。② $route.params一个 key/value 对象,包含了 动态片段 和 全匹配片段,如果没有路由参数,就是一个空对象。③ $route.query一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user为1,如果没有查询参数,则是个空对象。④ $route.hash当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。⑤ $route.fullPath完成解析后的 URL,包含查询参数和 hash 的完整路径。⑥ $route.matched数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。⑦ $route.name 当前路径名字$router 是“路由实例”对象,即使用 new VueRouter创建的实例,包括了路由的跳转方法,钩子函数等。$router常见跳转方法<button @click=“goToMenu” class=“btn btn-success”>Let’s order!</button>…..<script> export default{ methods:{ goToMenu(){ this.$router.go(-1)//跳转到上一次浏览的页面 this.$router.replace(’/menu’)//指定跳转的地址 this.$router.replace({name:‘menuLink’})//指定跳转路由的名字下 this.$router.push(’/menu’)//通过push进行跳转 this.$router.push({name:‘menuLink’})//通过push进行跳转路由的名字下 } } }</script>$router.push和$router.replace的区别:使用push方法的跳转会向 history 栈添加一个新的记录,当我们点击浏览器的返回按钮时可以看到之前的页面。使用replace方法不会向 history 添加新记录,而是替换掉当前的 history 记录,即当replace跳转到的网页后,‘后退’按钮不能查看之前的页面。5.404页面的设置用户会经常输错页面,当用户输错页面时,我们希望给他一个友好的提示页面,这个页面就是我们常说的404页面。vue-router也为我们提供了这样的机制。①设置我们的路由配置文件(/src/router/index.js){ path:’’, component:Error}这里的path:’*‘就是输入地址不匹配时,自动显示出Error.vue的文件内容②在/src/components/文件夹下新建一个Error.vue的文件。简单输入一些有关错误页面的内容。<template> <div> <h2>{{ msg }}</h2> </div></template><script>export default { data () { return { msg: ‘Error:404’ } }}</script>此时我们随意输入一个错误的地址时,便会自动跳转到404页面如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!六、参考文章vue-router实现单页面路由原理Vue.js——vue-router 60分钟快速入门技术胖的Vue-router视频教程vue中$router以及$route的使用Vue2.0 探索之路——vue-router入门教程和总结vue-router 2.0一些区别 ...

November 8, 2018 · 3 min · jiezi

Vue-Router基础学习笔记

1、安装vue-routernpm install vue-routeryarn add vue-router2、引入注册vue-routerimport Vue from ‘vue’import VueRouter from ‘vue-router’Vue.use(VueRouter)3、链接跳转<router-link to=’/home’></router-link> //你可以在template中使用它实现一个可点击跳转到home.vue的 a 标签this.$router.push(’/about’); //在methods方法中跳转到about页面this.$router.go(’-1’); //在js中返回上一个页面4、经常用到this.$route.params.name //在js中获取路由的参数.router-link-active //当前选中路由的匹配样式$route.query //获取查询参数$route.hash //哈希5、路由配置export default new Router({ routes:[ { //第一层是顶层路由,顶层路由中的router-view中显示被router-link选中的子路由 path:’/’, name:‘Home’, component:‘Home’ },{ path:’/user/:id’, //www.xxx.com/user/cai name:‘user’, //:id是动态路径参数 component:‘user’, children:[ { path:‘userInfo’, //www.xxx.com/user/cai/userInfo component:‘userInfo’ //子路由将渲染到父组件的router-view中 },{ path:‘posts’, component:‘posts’ } ] } ]})Vue.use(Router);6、路由参数方式变化时,重新发出请求并更新数据//比如:用户一切换到用户二, 路由参数改变了,但组件是同一个,会被复用// 从 /user/cai 切到 /user/wan在User组件中://方法1: watch:{ ‘$route’(to,from){ //做点什么,比如:更新数据 } }//方法二: beforeRouteUpdate(to,from,next){ //同上 }7、编程式导航router.push({name:‘user’,params:{userId:‘123’}}) //命名路由导航到user组件<router-link :to=’{name:‘user’,params:{userId:‘123’}}’>用户</router-link>router.push({path:‘register’,query:{plan:‘cai’}}) //query查询参数router.push({path:/user/${userId}}) //queryrouter.push(location,onComplete,onAbort)router.replace() //替换router.go(-1)8、命名视图//当前组件中只有一个 router-view 时,子组件默认渲染到这里<router-view class=‘default’></router-view><router-view class=‘a’ name=‘left’></router-view><router-view class=‘b’ name=‘main’></router-view>routes:[ { path:’/’, components:{ default:header, left:nav, main:content //content组件会渲染在name为main的router-view中 } }]//嵌套命名视图就是:子路由+命名视图9、重定向与别名const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: ‘/b’ }, { path: ‘/b’, redirect: { name: ‘foo’ }}, //命名路由方式 { path: ‘/c’, redirect: to => { //动态返回重定向目标 // 方法接收 目标路由 作为参数 // return 重定向的 字符串路径/路径对象 }} ]})const router = new VueRouter({ routes: [ { path: ‘/a’, component: A, alias: ‘/b’ } //别名:当访问 /b 时也会使用A组件 ]})10、路由组件传参const User={ props:[‘id’], template:&lt;div&gt;{{id}}&lt;/div&gt;}const router = new VueRouter({ routes: [ { path: ‘/user/:id’, component: User, props: true }, // 对于包含命名视图的路由,你必须分别为每个命名视图添加 props 选项: { path: ‘/user/:id’, components: { default: User, sidebar: Sidebar }, props: { default: true, sidebar: false } } ]})11、HTML5的History模式下服务端配置const router = new VueRouter({ mode: ‘history’, routes: [ { path: ‘*’, component: 404} ]})后端配置://Nginx location / { try_files $uri $uri/ /index.html; } //Apache <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] </IfModule>//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(‘无法打开index.htm页面.’) } res.writeHead(200, { ‘Content-Type’: ’text/html; charset=utf-8’ }) res.end(content) }) }).listen(httpPort, () => { console.log(‘打开: http://localhost:%s’, httpPort) }) //使用了Node.js的Express [使用中间件][1] ...

October 15, 2018 · 2 min · jiezi

vue路由history模式刷新页面出现404问题

vue hash模式下,URL中存在’#’,用’history’模式就能解决这个问题。但是history模式会出现刷新页面后,页面出现404。解决的办法是用nginx配置一下。在nginx的配置文件中修改方法一:location /{ root /data/nginx/html; index index.html index.htm; if (!-e $request_filename) { rewrite ^/(.) /index.html last; break; }}方法二:vue.js官方教程里提到的https://router.vuejs.org/zh/g… server { listen 8081;#默认端口是80,如果端口没被占用可以不用修改 server_name myapp.com; root D:/vue/my_app/dist;#vue项目的打包后的dist location / { try_files $uri $uri/ @router;#需要指向下面的@router否则会出现vue的路由在nginx中刷新出现404 index index.html index.htm; } #对应上面的@router,主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件 #因此需要rewrite到index.html中,然后交给路由在处理请求资源 location @router { rewrite ^.$ /index.html last; } #…….其他部分省略 }

October 12, 2018 · 1 min · jiezi

带你五步学会Vue SSR

前言SSR大家肯定都不陌生,通过服务端渲染,可以优化SEO抓取,提升首页加载速度等,我在学习SSR的时候,看过很多文章,有些对我有很大的启发作用,有些就只是照搬官网文档。通过几天的学习,我对SSR有了一些了解,也从头开始完整的配置出了SSR的开发环境,所以想通过这篇文章,总结一些经验,同时希望能够对学习SSR的朋友起到一点帮助。我会通过五个步骤,一步步带你完成SSR的配置:纯浏览器渲染服务端渲染,不包含Ajax初始化数据服务端渲染,包含Ajax初始化数据服务端渲染,使用serverBundle和clientManifest进行优化一个完整的基于Vue + VueRouter + Vuex的SSR工程如果你现在对于我上面说的还不太了解,没有关系,跟着我一步步向下走,最终你也可以独立配置一个SSR开发项目,所有源码我会放到github上,大家可以作为参考。正文1. 纯浏览器渲染这个配置相信大家都会,就是基于weback + vue的一个常规开发配置,这里我会放一些关键代码,完整代码可以去github查看。目录结构- node_modules- components - Bar.vue - Foo.vue- App.vue- app.js- index.html- webpack.config.js- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignoreapp.jsimport Vue from ‘vue’;import App from ‘./App.vue’;let app = new Vue({ el: ‘#app’, render: h => h(App)});App.vue<template> <div> <Foo></Foo> <Bar></Bar> </div></template><script>import Foo from ‘./components/Foo.vue’;import Bar from ‘./components/Bar.vue’;export default { components: { Foo, Bar }}</script>index.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>纯浏览器渲染</title></head><body> <div id=“app”></div></body></html>components/Foo.vue<template> <div class=“foo”> <h1>Foo Component</h1> </div></template><style>.foo { background: yellowgreen;}</style>components/Bar.vue<template> <div class=“bar”> <h1>Bar Component</h1> </div></template><style>.bar { background: bisque;}</style>webpack.config.jsconst path = require(‘path’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const ExtractTextPlugin = require(’extract-text-webpack-plugin’);module.exports = { mode: ‘development’, entry: ‘./app.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘bundle.js’ }, module: { rules: [ { test: /.js$/, use: ‘babel-loader’ }, { test: /.css$/, use: [‘vue-style-loader’, ‘css-loader’, ‘postcss-loader’] // 如果需要单独抽出CSS文件,用下面这个配置 // use: ExtractTextPlugin.extract({ // fallback: ‘vue-style-loader’, // use: [ // ‘css-loader’, // ‘postcss-loader’ // ] // }) }, { test: /.(jpg|jpeg|png|gif|svg)$/, use: { loader: ‘url-loader’, options: { limit: 10000 // 10Kb } } }, { test: /.vue$/, use: ‘vue-loader’ } ] }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: ‘./index.html’ }), // 如果需要单独抽出CSS文件,用下面这个配置 // new ExtractTextPlugin(“styles.css”) ]};postcss.config.jsmodule.exports = { plugins: [ require(‘autoprefixer’) ]};.babelrc{ “presets”: [ “@babel/preset-env” ], “plugins”: [ // 让其支持动态路由的写法 const Foo = () => import(’../components/Foo.vue’) “dynamic-import-webpack” ]}package.json{ “name”: “01”, “version”: “1.0.0”, “main”: “index.js”, “license”: “MIT”, “scripts”: { “start”: “yarn run dev”, “dev”: “webpack-dev-server”, “build”: “webpack” }, “dependencies”: { “vue”: “^2.5.17” }, “devDependencies”: { “@babel/core”: “^7.1.2”, “@babel/preset-env”: “^7.1.0”, “babel-plugin-dynamic-import-webpack”: “^1.1.0”, “autoprefixer”: “^9.1.5”, “babel-loader”: “^8.0.4”, “css-loader”: “^1.0.0”, “extract-text-webpack-plugin”: “^4.0.0-beta.0”, “file-loader”: “^2.0.0”, “html-webpack-plugin”: “^3.2.0”, “postcss”: “^7.0.5”, “postcss-loader”: “^3.0.0”, “url-loader”: “^1.1.1”, “vue-loader”: “^15.4.2”, “vue-style-loader”: “^4.1.2”, “vue-template-compiler”: “^2.5.17”, “webpack”: “^4.20.2”, “webpack-cli”: “^3.1.2”, “webpack-dev-server”: “^3.1.9” }}命令启动开发环境yarn start构建生产环境yarn run build最终效果截图:完整代码查看github2. 服务端渲染,不包含Ajax初始化数据服务端渲染SSR,类似于同构,最终要让一份代码既可以在服务端运行,也可以在客户端运行。如果说在SSR的过程中出现问题,还可以回滚到纯浏览器渲染,保证用户正常看到页面。那么,顺着这个思路,肯定就会有两个webpack的入口文件,一个用于浏览器端渲染weboack.client.config.js,一个用于服务端渲染webpack.server.config.js,将它们的公有部分抽出来作为webpack.base.cofig.js,后续通过webpack-merge进行合并。同时,也要有一个server来提供http服务,我这里用的是koa。我们来看一下新的目录结构:- node_modules- config // 新增 - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js- src - components - Bar.vue - Foo.vue - App.vue - app.js - entry-client.js // 新增 - entry-server.js // 新增 - index.html - index.ssr.html // 新增- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignore在纯客户端应用程序(client-only app)中,每个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,我们也希望如此:每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染(cross-request state pollution)。所以,我们要对app.js做修改,将其包装为一个工厂函数,每次调用都会生成一个全新的根组件。app.jsimport Vue from ‘vue’;import App from ‘./App.vue’;export function createApp() { const app = new Vue({ render: h => h(App) }); return { app };}在浏览器端,我们直接新建一个根组件,然后将其挂载就可以了。entry-client.jsimport { createApp } from ‘./app.js’;const { app } = createApp();app.$mount(’#app’);在服务器端,我们就要返回一个函数,该函数的作用是接收一个context参数,同时每次都返回一个新的根组件。这个context在这里我们还不会用到,后续的步骤会用到它。entry-server.jsimport { createApp } from ‘./app.js’;export default context => { const { app } = createApp(); return app;}然后再来看一下index.ssr.htmlindex.ssr.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>服务端渲染</title></head><body> <!–vue-ssr-outlet–> <script type=“text/javascript” src="<%= htmlWebpackPlugin.options.files.js %>"></script></body></html><!–vue-ssr-outlet–>的作用是作为一个占位符,后续通过vue-server-renderer插件,将服务器解析出的组件html字符串插入到这里。<script type=“text/javascript” src="<%= htmlWebpackPlugin.options.files.js %>"></script>是为了将webpack通过webpack.client.config.js打包出的文件放到这里(这里是为了简单演示,后续会有别的办法来做这个事情)。因为服务端吐出来的就是一个html字符串,后续的Vue相关的响应式、事件响应等等,都需要浏览器端来接管,所以就需要将为浏览器端渲染打包的文件在这里引入。用官方的词来说,叫客户端激活(client-side hydration)。所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:// 这里假定 App.vue template 根元素的 id="app"app.$mount(’#app’)由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要"激活"这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:<div id=“app” data-server-rendered=“true”>Vue在浏览器端就依靠这个属性将服务器吐出来的html进行激活,我们一会自己构建一下就可以看到了。接下来我们看一下webpack相关的配置:webpack.base.config.jsconst path = require(‘path’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);module.exports = { mode: ‘development’, resolve: { extensions: [’.js’, ‘.vue’] }, output: { path: path.resolve(__dirname, ‘../dist’), filename: ‘[name].bundle.js’ }, module: { rules: [ { test: /.vue$/, use: ‘vue-loader’ }, { test: /.js$/, use: ‘babel-loader’ }, { test: /.css$/, use: [‘vue-style-loader’, ‘css-loader’, ‘postcss-loader’] }, { test: /.(jpg|jpeg|png|gif|svg)$/, use: { loader: ‘url-loader’, options: { limit: 10000 // 10Kb } } } ] }, plugins: [ new VueLoaderPlugin() ]};webpack.client.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { entry: { client: path.resolve(__dirname, ‘../src/entry-client.js’) }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.html’), filename: ‘index.html’ }) ]});注意,这里的入口文件变成了entry-client.js,将其打包出的client.bundle.js插入到index.html中。webpack.server.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { target: ’node’, entry: { server: path.resolve(__dirname, ‘../src/entry-server.js’) }, output: { libraryTarget: ‘commonjs2’ }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.ssr.html’), filename: ‘index.ssr.html’, files: { js: ‘client.bundle.js’ }, excludeChunks: [‘server’] }) ]});这里有几个点需要注意一下:入口文件是 entry-server.js因为是打包服务器端依赖的代码,所以target要设为node,同时,output的libraryTarget要设为commonjs2这里关于HtmlWebpackPlugin配置的意思是,不要在index.ssr.html中引入打包出的server.bundle.js,要引为浏览器打包的client.bundle.js,原因前面说过了,是为了让Vue可以将服务器吐出来的html进行激活,从而接管后续响应。那么打包出的server.bundle.js在哪用呢?接着往下看就知道啦package.json{ “name”: “01”, “version”: “1.0.0”, “main”: “index.js”, “license”: “MIT”, “scripts”: { “start”: “yarn run dev”, “dev”: “webpack-dev-server”, “build:client”: “webpack –config config/webpack.client.config.js”, “build:server”: “webpack –config config/webpack.server.config.js” }, “dependencies”: { “koa”: “^2.5.3”, “koa-router”: “^7.4.0”, “koa-static”: “^5.0.0”, “vue”: “^2.5.17”, “vue-server-renderer”: “^2.5.17” }, “devDependencies”: { “@babel/core”: “^7.1.2”, “@babel/preset-env”: “^7.1.0”, “autoprefixer”: “^9.1.5”, “babel-loader”: “^8.0.4”, “css-loader”: “^1.0.0”, “extract-text-webpack-plugin”: “^4.0.0-beta.0”, “file-loader”: “^2.0.0”, “html-webpack-plugin”: “^3.2.0”, “postcss”: “^7.0.5”, “postcss-loader”: “^3.0.0”, “style-loader”: “^0.23.0”, “url-loader”: “^1.1.1”, “vue-loader”: “^15.4.2”, “vue-style-loader”: “^4.1.2”, “vue-template-compiler”: “^2.5.17”, “webpack”: “^4.20.2”, “webpack-cli”: “^3.1.2”, “webpack-dev-server”: “^3.1.9”, “webpack-merge”: “^4.1.4” }}接下来我们看server端关于http服务的代码:server/server.jsconst Koa = require(‘koa’);const Router = require(‘koa-router’);const serve = require(‘koa-static’);const path = require(‘path’);const fs = require(‘fs’);const backendApp = new Koa();const frontendApp = new Koa();const backendRouter = new Router();const frontendRouter = new Router();const bundle = fs.readFileSync(path.resolve(__dirname, ‘../dist/server.js’), ‘utf-8’);const renderer = require(‘vue-server-renderer’).createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, ‘../dist/index.ssr.html’), ‘utf-8’)});// 后端ServerbackendRouter.get(’/index’, (ctx, next) => { // 这里用 renderToString 的 promise 返回的 html 有问题,没有样式 renderer.renderToString((err, html) => { if (err) { console.error(err); ctx.status = 500; ctx.body = ‘服务器内部错误’; } else { console.log(html); ctx.status = 200; ctx.body = html; } });});backendApp.use(serve(path.resolve(__dirname, ‘../dist’)));backendApp .use(backendRouter.routes()) .use(backendRouter.allowedMethods());backendApp.listen(3000, () => { console.log(‘服务器端渲染地址: http://localhost:3000’);});// 前端ServerfrontendRouter.get(’/index’, (ctx, next) => { let html = fs.readFileSync(path.resolve(__dirname, ‘../dist/index.html’), ‘utf-8’); ctx.type = ‘html’; ctx.status = 200; ctx.body = html;});frontendApp.use(serve(path.resolve(__dirname, ‘../dist’)));frontendApp .use(frontendRouter.routes()) .use(frontendRouter.allowedMethods());frontendApp.listen(3001, () => { console.log(‘浏览器端渲染地址: http://localhost:3001’);});这里对两个端口进行监听,3000端口是服务端渲染,3001端口是直接输出index.html,然后会在浏览器端走Vue的那一套,主要是为了和服务端渲染做对比使用。这里的关键代码是如何在服务端去输出html字符串。const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8');const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')});可以看到,server.bundle.js在这里被使用啦,因为它的入口是一个函数,接收context作为参数(非必传),输出一个根组件app。这里我们用到了vue-server-renderer插件,它有两个方法可以做渲染,一个是createRenderer,另一个是createBundleRenderer。const { createRenderer } = require('vue-server-renderer')const renderer = createRenderer({ /* 选项 */ })const { createBundleRenderer } = require('vue-server-renderer')const renderer = createBundleRenderer(serverBundle, { /* 选项 */ })createRenderer无法接收为服务端打包出的server.bundle.js文件,所以这里只能用createBundleRenderer。serverBundle 参数可以是以下之一:绝对路径,指向一个已经构建好的 bundle 文件(.js 或 .json)。必须以 / 开头才会被识别为文件路径。由 webpack + vue-server-renderer/server-plugin 生成的 bundle 对象。JavaScript 代码字符串(不推荐)。这里我们引入的是.js文件,后续会介绍如何使用.json文件以及有什么好处。renderer.renderToString((err, html) =&gt; { if (err) { console.error(err); ctx.status = 500; ctx.body = '服务器内部错误'; } else { console.log(html); ctx.status = 200; ctx.body = html; }});使用createRenderer和createBundleRenderer返回的renderer函数包含两个方法renderToString和renderToStream,我们这里用的是renderToString成功后直接返回一个完整的字符串,renderToStream返回的是一个Node流。renderToString支持Promise,但是我在使用Prmoise形式的时候样式会渲染不出来,暂时还不知道原因,如果大家知道的话可以给我留言啊。配置基本就完成了,来看一下如何运行。yarn run build:client // 打包浏览器端需要bundleyarn run build:server // 打包SSR需要bundleyarn start // 其实就是 node server/server.js,提供http服务最终效果展示:访问http://localhost:3000/index我们看到了前面提过的data-server-rendered="true"属性,同时会加载client.bundle.js文件,为了让Vue在浏览器端做后续接管。访问http://localhost:3001/index还和第一步实现的效果一样,纯浏览器渲染,这里就不放截图了。完整代码查看github3. 服务端渲染,包含Ajax初始化数据如果SSR需要初始化一些异步数据,那么流程就会变得复杂一些。我们先提出几个问题:服务端拿异步数据的步骤在哪做?如何确定哪些组件需要获取异步数据?获取到异步数据之后要如何塞回到组件内?带着问题我们向下走,希望看完这篇文章的时候上面的问题你都找到了答案。服务器端渲染和浏览器端渲染组件经过的生命周期是有区别的,在服务器端,只会经历beforeCreate和created两个生命周期。因为SSR服务器直接吐出html字符串就好了,不会渲染DOM结构,所以不存在beforeMount和mounted的,也不会对其进行更新,所以也就不存在beforeUpdate和updated等。我们先来想一下,在纯浏览器渲染的Vue项目中,我们是怎么获取异步数据并渲染到组件中的?一般是在created或者mounted生命周期里发起异步请求,然后在成功回调里执行this.data = xxx,Vue监听到数据发生改变,走后面的Dom Diff,打patch,做DOM更新。那么服务端渲染可不可以也这么做呢?答案是不行的。在mounted里肯定不行,因为SSR都没有mounted生命周期,所以在这里肯定不行。在beforeCreate里发起异步请求是否可以呢,也是不行的。因为请求是异步的,可能还没有等接口返回,服务端就已经把html字符串拼接出来了。所以,参考一下官方文档,我们可以得到以下思路:在渲染前,要预先获取所有需要的异步数据,然后存到Vuex的store中。在后端渲染时,通过Vuex将获取到的数据注入到相应组件中。把store中的数据设置到window.__INITIAL_STATE__属性中。在浏览器环境中,通过Vuex将window.__INITIAL_STATE__里面的数据注入到相应组件中。正常情况下,通过这几个步骤,服务端吐出来的html字符串相应组件的数据都是最新的,所以第4步并不会引起DOM更新,但如果出了某些问题,吐出来的html字符串没有相应数据,Vue也可以在浏览器端通过Vuex注入数据,进行DOM更新。更新后的目录结构:- node_modules- config - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js- src - components - Bar.vue - Foo.vue - store // 新增 store.js - App.vue - app.js - entry-client.js - entry-server.js - index.html - index.ssr.html- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignore先来看一下store.js:store/store.jsimport Vue from ‘vue’;import Vuex from ‘vuex’;Vue.use(Vuex);const fetchBar = function() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(‘bar 组件返回 ajax 数据’); }, 1000); });};function createStore() { const store = new Vuex.Store({ state: { bar: ’’ }, mutations: { ‘SET_BAR’(state, data) { state.bar = data; } }, actions: { fetchBar({ commit }) { return fetchBar().then((data) => { commit(‘SET_BAR’, data); }).catch((err) => { console.error(err); }) } } }); if (typeof window !== ‘undefined’ && window.INITIAL_STATE) { console.log(‘window.INITIAL_STATE’, window.INITIAL_STATE); store.replaceState(window.INITIAL_STATE); } return store;}export default createStore;typeof window如果不太了解Vuex,可以去Vuex官网先看一些基本概念。这里fetchBar可以看成是一个异步请求,这里用setTimeout模拟。在成功回调中commit相应的mutation进行状态修改。这里有一段关键代码:if (typeof window !== ‘undefined’ && window.INITIAL_STATE) { console.log(‘window.INITIAL_STATE’, window.INITIAL_STATE); store.replaceState(window.INITIAL_STATE);}因为store.js同样也会被打包到服务器运行的server.bundle.js中,所以运行环境不一定是浏览器,这里需要对window做判断,防止报错,同时如果有window.__INITIAL_STATE__属性,说明服务器已经把所有初始化需要的异步数据都获取完成了,要对store中的状态做一个替换,保证统一。components/Bar.vue<template> <div class=“bar”> <h1 @click=“onHandleClick”>Bar Component</h1> <h2>异步Ajax数据:</h2> <span>{{ msg }}</span> </div></template><script> const fetchInitialData = ({ store }) => { store.dispatch(‘fetchBar’); }; export default { asyncData: fetchInitialData, methods: { onHandleClick() { alert(‘bar’); } }, mounted() { // 因为服务端渲染只有 beforeCreate 和 created 两个生命周期,不会走这里 // 所以把调用 Ajax 初始化数据也写在这里,是为了供单独浏览器渲染使用 let store = this.$store; fetchInitialData({ store }); }, computed: { msg() { return this.$store.state.bar; } } }</script><style>.bar { background: bisque;}</style>这里在Bar组件的默认导出对象中增加了一个方法asyncData,在该方法中会dispatch相应的action,进行异步数据获取。需要注意的是,我在mounted中也写了获取数据的代码,这是为什么呢? 因为想要做到同构,代码单独在浏览器端运行,也应该是没有问题的,又由于服务器没有mounted生命周期,所以我写在这里就可以解决单独在浏览器环境使用也可以发起同样的异步请求去初始化数据。components/Foo.vue<template> <div class=“foo”> <h1 @click=“onHandleClick”>Foo Component</h1> </div></template><script>export default { methods: { onHandleClick() { alert(‘foo’); } },}</script><style>.foo { background: yellowgreen;}</style>这里我对两个组件都添加了一个点击事件,为的是证明在服务器吐出首页html后,后续的步骤都会被浏览器端的Vue接管,可以正常执行后面的操作。app.jsimport Vue from ‘vue’;import createStore from ‘./store/store.js’;import App from ‘./App.vue’;export function createApp() { const store = createStore(); const app = new Vue({ store, render: h => h(App) }); return { app, store, App };}在建立根组件的时候,要把Vuex的store传进去,同时要返回,后续会用到。最后来看一下entry-server.js,关键步骤在这里:entry-server.jsimport { createApp } from ‘./app.js’;export default context => { return new Promise((resolve, reject) => { const { app, store, App } = createApp(); let components = App.components; let asyncDataPromiseFns = []; Object.values(components).forEach(component => { if (component.asyncData) { asyncDataPromiseFns.push(component.asyncData({ store })); } }); Promise.all(asyncDataPromiseFns).then((result) => { // 当使用 template 时,context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中 context.state = store.state; console.log(222); console.log(store.state); console.log(context.state); console.log(context); resolve(app); }, reject); });}我们通过导出的App拿到了所有它下面的components,然后遍历,找出哪些component有asyncData方法,有的话调用并传入store,该方法会返回一个Promise,我们使用Promise.all等所有的异步方法都成功返回,才resolve(app)。context.state = store.state作用是,当使用createBundleRenderer时,如果设置了template选项,那么会把context.state的值作为window.__INITIAL_STATE__自动插入到模板html中。这里需要大家多思考一下,弄清楚整个服务端渲染的逻辑。如何运行:yarn run build:clientyarn run build:serveryarn start最终效果截图:服务端渲染:打开http://localhost:3000/index可以看到window.__INITIAL_STATE__被自动插入了。我们来对比一下SSR到底对加载性能有什么影响吧。服务端渲染时performance截图:纯浏览器端渲染时performance截图:同样都是在fast 3G网络模式下,纯浏览器端渲染首屏加载花费时间2.9s,因为client.js加载就花费了2.27s,因为没有client.js就没有Vue,也就没有后面的东西了。服务端渲染首屏时间花费0.8s,虽然client.js加载扔花费2.27s,但是首屏已经不需要它了,它是为了让Vue在浏览器端进行后续接管。从这我们可以真正的看到,服务端渲染对于提升首屏的响应速度是很有作用的。当然有的同学可能会问,在服务端渲染获取初始ajax数据时,我们还延时了1s,在这个时间用户也是看不到页面的。没错,接口的时间我们无法避免,就算是纯浏览器渲染,首页该调接口还是得调,如果接口响应慢,那么纯浏览器渲染看到完整页面的时间会更慢。完整代码查看github4. 使用serverBundle和clientManifest进行优化前面我们创建服务端renderer的方法是:const bundle = fs.readFileSync(path.resolve(__dirname, ‘../dist/server.js’), ‘utf-8’);const renderer = require(‘vue-server-renderer’).createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, ‘../dist/index.ssr.html’), ‘utf-8’)});serverBundle我们用的是打包出的server.bundle.js文件。这样做的话,在每次编辑过应用程序源代码之后,都必须停止并重启服务。这在开发过程中会影响开发效率。此外,Node.js 本身不支持 source map。vue-server-renderer 提供一个名为 createBundleRenderer 的 API,用于处理此问题,通过使用 webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。所创建的 bundle renderer,用法和普通 renderer 相同,但是 bundle renderer 提供以下优点:内置的 source map 支持(在 webpack 配置中使用 devtool: ‘source-map’)在开发环境甚至部署过程中热重载(通过读取更新后的 bundle,然后重新创建 renderer 实例)关键 CSS(critical CSS) 注入(在使用 *.vue 文件时):自动内联在渲染过程中用到的组件所需的CSS。更多细节请查看 CSS 章节。使用 clientManifest 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。preload和prefetch有不了解的话可以自行查一下它们的作用哈。那么我们来修改webpack配置:webpack.client.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const VueSSRClientPlugin = require(‘vue-server-renderer/client-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { entry: { client: path.resolve(__dirname, ‘../src/entry-client.js’) }, plugins: [ new VueSSRClientPlugin(), // 新增 new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.html’), filename: ‘index.html’ }) ]});webpack.server.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const nodeExternals = require(‘webpack-node-externals’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const VueSSRServerPlugin = require(‘vue-server-renderer/server-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { target: ’node’, // 对 bundle renderer 提供 source map 支持 devtool: ‘#source-map’, entry: { server: path.resolve(__dirname, ‘../src/entry-server.js’) }, externals: [nodeExternals()], // 新增 output: { libraryTarget: ‘commonjs2’ }, plugins: [ new VueSSRServerPlugin(), // 这个要放到第一个写,否则 CopyWebpackPlugin 不起作用,原因还没查清楚 new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.ssr.html’), filename: ‘index.ssr.html’, files: { js: ‘client.bundle.js’ }, excludeChunks: [‘server’] }) ]});因为是服务端引用模块,所以不需要打包node_modules中的依赖,直接在代码中require引用就好,所以配置externals: [nodeExternals()]。两个配置文件会分别生成vue-ssr-client-manifest.json和vue-ssr-server-bundle.json。作为createBundleRenderer的参数。来看server.jsserver.jsconst serverBundle = require(path.resolve(__dirname, ‘../dist/vue-ssr-server-bundle.json’));const clientManifest = require(path.resolve(__dirname, ‘../dist/vue-ssr-client-manifest.json’));const template = fs.readFileSync(path.resolve(__dirname, ‘../dist/index.ssr.html’), ‘utf-8’);const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, template: template, clientManifest: clientManifest});效果和第三步就是一样的啦,就不截图了,完整代码查看github。5. 配置一个完整的基于Vue + VueRouter + Vuex的SSR这里和第四步不一样的是引入了vue-router,更接近于实际开发项目。在src下新增router目录。router/index.jsimport Vue from ‘vue’;import Router from ‘vue-router’;import Bar from ‘../components/Bar.vue’;Vue.use(Router);function createRouter() { const routes = [ { path: ‘/bar’, component: Bar }, { path: ‘/foo’, component: () => import(’../components/Foo.vue’) // 异步路由 } ]; const router = new Router({ mode: ‘history’, routes }); return router;}export default createRouter;这里我们把Foo组件作为一个异步组件引入,做成按需加载。在app.js中引入router,并导出:app.jsimport Vue from ‘vue’;import createStore from ‘./store/store.js’;import createRouter from ‘./router’;import App from ‘./App.vue’;export function createApp() { const store = createStore(); const router = createRouter(); const app = new Vue({ router, store, render: h => h(App) }); return { app, store, router, App };}修改App.vue引入路由组件:App.vue<template> <div id=“app”> <router-link to="/bar">Goto Bar</router-link> <router-link to="/foo">Goto Foo</router-link> <router-view></router-view> </div></template><script>export default { beforeCreate() { console.log(‘App.vue beforeCreate’); }, created() { console.log(‘App.vue created’); }, beforeMount() { console.log(‘App.vue beforeMount’); }, mounted() { console.log(‘App.vue mounted’); }}</script>最重要的修改在entry-server.js中,entry-server.jsimport { createApp } from ‘./app.js’;export default context => { return new Promise((resolve, reject) => { const { app, store, router, App } = createApp(); router.push(context.url); router.onReady(() => { const matchedComponents = router.getMatchedComponents(); console.log(context.url) console.log(matchedComponents) if (!matchedComponents.length) { return reject({ code: 404 }); } Promise.all(matchedComponents.map(component => { if (component.asyncData) { return component.asyncData({ store }); } })).then(() => { // 当使用 template 时,context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中 context.state = store.state; // 返回根组件 resolve(app); }); }, reject); });}这里前面提到的context就起了大作用,它将用户访问的url地址传进来,供vue-router使用。因为有异步组件,所以在router.onReady的成功回调中,去找该url路由所匹配到的组件,获取异步数据那一套还和前面的一样。于是,我们就完成了一个基本完整的基于Vue + VueRouter + VuexSSR配置,完成代码查看github。最终效果演示:访问http://localhost:3000/bar:完整代码查看github后续上面我们通过五个步骤,完成了从纯浏览器渲染到完整服务端渲染的同构,代码既可以运行在浏览器端,也可以运行在服务器端。那么,回过头来我们在看一下是否有优化的空间,又或者有哪些扩展的思考。1. 优化我们目前是使用renderToString方法,完全生成html后,才会向客户端返回,如果使用renderToStream,应用bigpipe技术可以向浏览器持续不断的返回一个流,那么文件的加载浏览器可以尽早的显示一些东西出来。const stream = renderer.renderToStream(context)返回的值是 Node.js stream:let html = ‘‘stream.on(‘data’, data => { html += data.toString()})stream.on(’end’, () => { console.log(html) // 渲染完成})stream.on(’error’, err => { // handle error…})在流式渲染模式下,当 renderer 遍历虚拟 DOM 树(virtual DOM tree)时,会尽快发送数据。这意味着我们可以尽快获得"第一个 chunk",并开始更快地将其发送给客户端。然而,当第一个数据 chunk 被发出时,子组件甚至可能不被实例化,它们的生命周期钩子也不会被调用。这意味着,如果子组件需要在其生命周期钩子函数中,将数据附加到渲染上下文(render context),当流(stream)启动时,这些数据将不可用。这是因为,大量上下文信息(context information)(如头信息(head information)或内联关键 CSS(inline critical CSS))需要在应用程序标记(markup)之前出现,我们基本上必须等待流(stream)完成后,才能开始使用这些上下文数据。因此,如果你依赖由组件生命周期钩子函数填充的上下文数据,则不建议使用流式传输模式。webpack优化webpack优化又是一个大的话题了,这里不展开讨论,感兴趣的同学可以自行查找一些资料,后续我也可能会专门写一篇文章来讲webpack优化。2. 思考是否必须使用vuex?答案是不用。Vuex只是为了帮助你实现一套数据存储、更新、获取的机制,入股你不用Vuex,那么你就必须自己想一套方案可以将异步获取到的数据存起来,并且在适当的时机将它注入到组件内,有一些文章提出了一些方案,我会放到参考文章里,大家可以阅读一下。是否使用SSR就一定好?这个也是不一定的,任何技术都有使用场景。SSR可以帮助你提升首页加载速度,优化搜索引擎SEO,但同时由于它需要在node中渲染整套Vue的模板,会占用服务器负载,同时只会执行beforeCreate和created两个生命周期,对于一些外部扩展库需要做一定处理才可以在SSR中运行等等。结语本文通过五个步骤,从纯浏览器端渲染开始,到配置一个完整的基于Vue + vue-router + Vuex的SSR环境,介绍了很多新的概念,也许你看完一遍不太理解,那么结合着源码,去自己手敲几遍,然后再来看几遍文章,相信你一定可以掌握SSR。最后,本文所有源代码都放在我的github上,如果对你有帮助的话,就来点一个赞吧参考链接https://ssr.vuejs.org/zh/https://zhuanlan.zhihu.com/p/…http://www.cnblogs.com/qingmi...https://juejin.im/entry/590ca...https://github.com/youngwind/… ...

October 11, 2018 · 8 min · jiezi

vue组件通信全面总结

写在前面组件间的通信是是实际开发中非常常用的一环,如何使用对项目整体设计、开发、规范都有很实际的的作用,我在项目开发中对此深有体会,总结下vue组件间通信的几种方式,讨论下各自的使用场景文章对相关场景预览父->子组件间的数据传递子->父组件间的数据传递兄弟组件间的数据传递组件深层嵌套,祖先组件与子组件间的数据传递文章相关技术预览prop、emit、bus、vuex、路由URL、provide/inject注:以下介绍与代码环境:vue2.0+、vue-cli2父->子组件间的数据传递父子组件的通信是开发是最常用的也是最重要的,你们一定知道父子通信是用prop传递数据的,像这样://父组件,传递数据<editor :inputIndex=“data” :inputName=“王文健”></editor>//子组件,接受数据,定义传递数据的类型type与默认值default props: { inputIndex: { type: Object, default: function(){ return {} } }, inputName: { type: String, default: ’’ },注意项:父组件传递数据时类似在标签中写了一个属性,如果是传递的数据是data中的自然是要在传递属性前加v-bind:,如果传递的是一个已知的固定值呢字符串是静态的可直接传入无需在属性前加v-bind数字,布尔,对象,数组,因为这些是js表达式而不是字符串,所以即使这些传递的是静态的也需要加v-bind,把数据放到data中引用,如果prop传到子组件中的数据是一个对象的话,要注意传递的是一个对象引用,虽然父子组件看似是分离的但最后都是在同一对象下如果prop传到子组件的值只是作为初始值使用,且在父组件中不会变化赋值到data中使用如果传到子组件的prop的数据在父组件会被改变的,放到计算属性中监听变化使用。因为如果传递的是个对象的话,只改变下面的某个属性子组件中是不会响应式更新的,如果子组件需要在数据变化时响应式更新那只能放到computed中或者用watch深拷贝deep:true才能监听到变化当然如果你又需要在子组件中通过prop传递数据的变化做些操作,那么写在computed中会报警告,因为计算属性中不推荐有任何数据的改变,最好只进行计算。如果你非要进行数据的操作那么可以把监听写在watch(注意deep深拷贝)或者使用computed的get和set如下图:但问题又来了,如果你传进来的是个对象,同时你又需要在子组件中操作传进来的这个数据,那么在父组件中的这个数据也会改变,因为你传递的只是个引用, 即使你把prop的数据复制到data中也是一样的,无论如何赋值都是引用的赋值,你只能对对象做深拷贝创建一个副本才能继续操作,你可以用JSON的方法先转化字符串在转成对象更方便一点,所以在父子传递数据时要先考虑好数据要如何使用,否则你会遇到很多问题或子组件中修改了父组件中的数据,这是很隐蔽并且很危险的子->父组件间的数据传递在vue中子向父传递数据一般用$emit自定义事件,在父组件中监听这个事件并在回调中写相关逻辑// 父组件监听子组件定义的事件 <editor :inputIndex=“index” @editorEmit=‘editorEmit’></editor>// 子组件需要返回数据时执行,并可以传递数据this.$emit(’editorEmit’, data)那么问题来了,我是不是真的有必要去向父组件返回这个数据,用自定义事件可以在当子组件想传递数据或向子组件传递的数据有变化需要重新传递时执行,那么另外一种场景,父组件需要子组件的一个数据但子组件并不知道或者说没有能力在父组件想要的时候给父组件,那么这个时候就要用到组件的一个选项ref:<editor ref=“editor” @editorEmit=‘editorEmit’></editor>父组件在标签中定义ref属性,在js中直接调用this.$refs.editor就是调用整个子组件,子组件的所有内容都能通过ref去调用,当然我们并不推荐因为这会使数据看起来非常混乱,所以我们可以在子组件中定义一种专供父组件调用的函数,,比如我们在这个函数中返回子组件data中某个数据,当父组件想要获取这个数据就直接主动调用ref执行这个函数获取这个数据,这样能适应很大一部分场景,逻辑也更清晰一点另外,父向子传递数据也可以用ref,有次需要在一个父组件中大量调用同一个子组件,而每次调用传递的prop数据都不同,并且传递数据会根据之后操作变化,这样我需要在data中定义大量相关数据并改变它,我可以直接用ref调用子组件函数直接把数据以参数的形式传给子组件,逻辑一下子清晰了如果调用基础组件可以在父组件中调用ref执行基础组件中暴露的各种功能接口,比如显示,消失等兄弟组件间的数据传递vue中兄弟组件间的通信是很不方便的,或者说不支持的,那么父子组件中都有什么通信方式呢路由URL参数在传统开发时我们常常把需要跨页面传递的数据放到url后面,跳转到另外页面时直接获取url字符串获取想要的参数即可,在vue跨组件时一样可以这么做,// router index.js 动态路由{ path:’/params/:Id’, component:Params, name:Params}// 跳转路由<router-link :to="/params/12">跳转路由</router-link>在跳转后的组件中用$route.params.id去获取到这个id参数为12,但这种只适合传递比较小的数据,数字之类的Bus通信在组件之外定义一个bus.js作为组件间通信的桥梁,适用于比较小型不需要vuex又需要兄弟组件通信的bus.js中添加如下 import Vue from ‘vue’ export default new Vue组件中调用bus.js通过自定义事件传递数据 import Bus from ‘./bus.js’ export default { methods: { bus () { Bus.$emit(‘msg’, ‘我要传给兄弟组件们’) } } }兄弟组件中监听事件接受数据 import Bus from ‘./bus.js’ export default { mounted() { Bus.$on(‘msg’, (e) => { console.log(e) }) } }注:以上两种使用场景并不高所以只是简略提一下,这两点都是很久以前写过,以上例子网上直接搜集而来如有错误,指正Vuex集中状态管理vuex是vue的集中状态管理工具,对于大型应用统一集中管理数据,很方便,在此对vuex的用法并不过多介绍只是提一下使用过程中遇到的问题规范:对于多人开发的大型应用规范的制定是至关重要的,对于所有人都会接触到的vuex对其修改数据调用数据都应有一个明确严格的使用规范vuex分模块:项目不同模块间维护各自的vuex数据限制调用:只允许action操作数据,getters获取数据,使用mapGetters,mapActions辅助函数调用数据对于vuex的使用场景也有一些争论,有人认为正常组件之间就是要用父子组件传值的方式,即使子组件需要使vuex中的数据也应该由父组件获取再传到子组件中,但有的时候组件间嵌套很深,只允许父组件获取数据并不是一个方便的方法,所以对于祖先元组件与子组件传值又有了新问题,vue官网也有一些方法解决,如下祖先组件与子组件间的数据传递provide/inject除了正常的父子组件传值外,vue也提供了provide/inject这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效官网实例// 父级组件提供 ‘foo’var Provider = { provide: { foo: ‘bar’ }, // …}// 子组件注入 ‘foo’var Child = { inject: [‘foo’], created () { console.log(this.foo) // => “bar” }}provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。一个字符串数组,或一个对象,对象的 key 是本地的绑定名,value 是:在可用的注入内容中搜索用的 key (字符串或 Symbol),或 一个对象,该对象的:from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)default 属性是降级情况下使用的 value提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。具体细节移步vue相关介绍https://cn.vuejs.org/v2/api/#…provide/inject还未在项目中应用过,后面会做尝试写在结尾文章只是整理一下笔记,谈一谈遇到的问题和经验,并没有严谨的措辞和详细的过程,如有错误望指正原创文章转载引用请注明原文链接http://blog.wwenj.com/index.p… ...

October 10, 2018 · 1 min · jiezi

[vuejs 踩坑实战系列] keep-alive 被 beforeRouteEnter 骗了

大家中秋假期快乐,假期分享一些实战文章给大家,原创不易,欢迎转发,一起学习现在大家基本都在单页应用里面使用了 keep-alive 来缓存不活动的组件实例,而不是销毁它们。如果你还没有使用,可以看看官方的介绍(如果大家需要一些新手入门的文章可以留言哈):https://cn.vuejs.org/v2/api/#…用法很简单:主要是包裹<keep-alive> …</keep-alive>使用场景:和单页应用环境配合使用的:<keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view></keep-alive>有以下几个常识,如果你还没有使用 keep-alive 的话,可以记下来:1、组件内的第一次的生命周期:mounted ==> activated2、切换路由再次进来只会触发 activated3、可以通过 router 的钩子函数 beforeRouteEnter 来做一些辅助判断具体可以看看官方的这个的文档:https://router.vuejs.org/zh/g…不能获取组件实例 this比如你要设置 data 里面的变量,抱歉,这里操作不了,那如何做呢?很多熟悉的人会想到 next 操作 vm 对象:beforeRouteEnter (to, from, next) { next(vm => { // 通过 vm 访问组件实例 })}是的,这里你可以通过 from.name 来做一些判断,比如如下代码片段:需求很简单,判断一下从特定路由切换过来,做一个判断赋值给 data 的 isFromTesterbeforeRouteEnter (to, from, next) { console.log(to, from); if (from.name == ‘Tester’) { next(vm => { vm.isFromTester = true }) } else { next(vm => { vm.isFromTester = false }) }}然后你就可以在 activated 生命周期直接判断啦activated () { if (this.isFromTester) { //… }}大功告成啦抱歉,这里的 activated 不会那么及时地更新 isFromTester,所以第一次你获取的不是 true,第二次是可以的那我们就要来刨根问底了,到底为啥不是及时更新的呢?有没有人想到了 vue 里面一个很常见的 nextTick 这个东西?是滴,就是它,它骗了 activated,真相在这里:(我们省去了很多路由事件里面自己的处理逻辑和 vue activated 的 hook 的触发) ...

September 24, 2018 · 1 min · jiezi

[vuejs 踩坑实战系列] 路由场景下父子组件的生命周期顺序来个刨根问底

大家中秋假期快乐,假期分享一些原理设计文章给大家原创不易,欢迎转发,一起学习(凌晨写的,不容易哈,收藏或者点个赞吧)在常见的单页应用中,我们都会有一个根 App.vue 文件,里面放置一个 router-view 然后配置路由来切换.很多人在子父组件嵌套关系下的生命周期钩子函数如何应用,谁先谁后(比如哪个用来发送请求,数据传递)等有所疑问。本文聚焦 mounted 事件(需要 created 的可以留言哈),先抛结论:子组件一层一层往外触发,最终触发根 App.vue 的 mounted验证的做法很简单:你只需要在每一个组件里面的 mounted 增加打印日志就可以看到了,我们具体来看看设计原理现在假设我们配置了路由:一级是 /user/:id 二级是 profileconst router = new VueRouter({ routes: [ { path: ‘/user/:id’, component: User, children: [ { // 当 /user/:id/profile 匹配成功, // UserProfile 会被渲染在 User 的 <router-view> 中 path: ‘profile’, component: UserProfile } ] } ]})先看一下所有的 mounted 最终在各自组件里面是如何被调用的:通过 vm.$options 获取组件内部的配置,然后通过 call 方法下面的我们又会遇到 vnode(所以掌握它很重要,在前面的 vue.js 源码原创系列 ref 与 $refs 如何关联 也提到了一些 vnode,感兴趣可以看看),就是下面 componentVNodeHooks 里面的 insert 函数的参数var componentVNodeHooks = { insert: function insert (vnode) {}}里面呢,第一步:从 vnode 里面获取 componentInstancevar componentInstance = vnode.componentInstance;然后判断 _isMounted 是否已经执行过 mounted (很常用的状态二次确定的变量,前面的 _ 一般代表内部使用)if (!componentInstance._isMounted) { componentInstance._isMounted = true; callHook(componentInstance, ‘mounted’);}上面我们就用到了 callHook 函数了,传入的第二个参数也正是本文讨论的生命周期的 mounted再往后有一个 invokeInsertHook 函数function invokeInsertHook (vnode, queue, initial) {}注意一下源码里面的注释:delay insert hooks for component root nodes, invoke them after the element is really inserted设置了 pendingInsert(后面会在 initComponent 中使用),代码如下:if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue;}内部设计:循环 queue 这个包含 vnode 的数组对象,如图所示:注意一下标注的 data.hook,下面的代码片段会使用到,也就是调用上面提到的 componentVNodeHooks 对象的 insert:for (var i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]);}再往下,带着疑问:这个 queue 是如何生成 vnode 数组的呢?最开始定义一个空数组:var insertedVnodeQueue = [];在刚才的 对象中还有 initvar componentVNodeHooks = { init: function init () { // … }}init 函数内部定义一个 childvar child = vnode.componentInstance = createComponentInstanceForVnode(…)然后会调用一个 $mountchild.$mount(hydrating ? vnode.elm : undefined, hydrating);在函数 initComponent 中会用到之前的 pendingInsert,而且 insertedVnodeQueue 这个数组对象会调用 push 插入元素function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert); vnode.data.pendingInsert = null; }}在函数 invokeCreateHooks 内部insertedVnodeQueue 这个数组对象会调用 push 插入元素function invokeCreateHooks (vnode, insertedVnodeQueue) { i = vnode.data.hook; // Reuse variable if (isDef(i)) { if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); } }}在函数 mountComponent 内部当 vm.$vnode 为 null 也会调用 callHook,第二个参数传入 mountedfunction mountComponent () { if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, ‘mounted’); }} ...

September 24, 2018 · 2 min · jiezi