Vue3.0权限管理实现流程【实践】
作者:lxcan
转发链接:https://segmentfault.com/a/1190000022431839
后端返回用户权限,前端根据用户权限处理得到左侧菜单;所有路由在前端定义好,根据后端返回的用户权限筛选出需要挂载的路由,然后使用 addRoutes 动态挂载路由。
(1)路由定义,分为初始路由和动态路由,一般来说初始路由只有 login,其他路由都挂载在 home 路由之下需要动态挂载。
(2)用户登录,登录成功之后得到 token,保存在 sessionStorage,跳转到 home,此时会进入路由拦截根据 token 获取用户权限列表。、
(3)全局路由拦截,根据当前用户有没有 token 和 权限列表进行相应的判断和跳转,当没有 token 时跳到 login,当有 token 而没有权限列表时去发请求获取权限等等逻辑。
(4)处理用户权限,在 store.js 定义一个模块 permission.js,专门用于处理用户权限相关的逻辑,用户权限列表、菜单列表都保存在此模块;
(5)用户权限列表、菜单列表的处理,前端的路由要和后端返回的权限有一个唯一标识(一般用路由名做标识符),根据此标识筛选出对应的路由。
(6)左侧菜单,要和用户信息、用户管理模块使用的菜单信息一致,统一使用保存在 store 中的变量。
系统主要页面的路由,后续会将这些路由经过权限筛选,添加到 home 路由的 children 里面
用户进入登录页,输入用户名、密码、验证码,点击登录,发送登录请求,登录成功之后,将 token 保存在 sessionStorage,然后跳转到首页 /home ,进入路由拦截的逻辑。
首先从打开本地服务 http://localhost:2001 开始,打开后会进入 login 页面,那么判断的依据是什么?首先是 token。没有登录的用户是拿不到 token 的,而登录后的用户我们会将 token 存到 seesionStorage,因此,根据当前有没有 token 即可知道是否登录。
(1)当用户打开 localhost,此时还没有 token,匹配的是空路由,我们重定向到登录页 next({ path: \’/login\’ });(2)用户在登录页刷新页面,也会进入路由拦截,此时匹配的是 login 路由,而 login 路由是不需要登录验证的(requiresAuth 为空或者 false),所以直接跳过执行 next();(3)用户在登录页输入了用户名和密码,登录成功,保存了 token,跳转到 /home 路由;(4)此时进入路由拦截,已经有 token了,但是还没有用户权限 permissionList,然后发请求获取用户权限列表,得到权限后 next({ path: to.path, query: to.query }); 继续往下走;(5)再次进入路由拦截,此时有 token 和 permissionList 了,就可以根据实际业务进行跳转了。上面的代码是判断当前是不是 login 路由,如果用户登录后手动在地址栏输入 /login,则清除 token 跳转到登录页。其他的逻辑就跟具体业务相关了,就不细讲了。
处理用户权限,在 store.js 定义一个模块 permission.js,专门用于处理用户权限相关的逻辑,用户权限列表、菜单列表都保存在此模块;来看看 permission.js 主要做了什么:
(1)首先,let data = await getUserByToken(); 发请求获取用户权限,得到 data,data.userPopedoms 格式大致如下:
(2)然后,根据我们写好的路由数组,进行对比,过滤得到我们要的路由。路由格式在上文“路由定义”的 router/router.js 已经提到。还要根据用户权限处理得到侧边栏菜单。
为此,我们需要两个处理函数,一个根据用户权限列表和路由数组过滤得到最终路由,另一个根据用户权限处理得到侧边栏菜单。所以另外专门创建了一个文件 handle-module.js 存放这两个函数。
(3)上面得到过滤后的路由数组后,加入到 path 为 \’/\’ 的 children 下面
(4)上面根据权限生成侧边栏菜单之后,保存在 store 待用。
(5)上面第三步将动态路由加入到 home 的 children 之后,就可以将 dynamicRoutes 加入到路由中了。router.addRoutes(dynamicRoutes);
(6)到了这里,路由就添加完了,也就是 FETCH_PERMISSION 操作完毕了,就可以在 action.then 里面调用 next({ path: to.path, query: to.query }); 进去路由,也就是进入 home。我们上面已经将 home 路由重定向为菜单的第一个路由信息,所以会进入系统菜单的第一个页面。
刷新页面后,根据 router.beforeEach 的判断,有 token 但是没有 permissionList ,会重新触发 action 去发请求获取用户权限,之前的逻辑会重新走一遍,所以没有问题。
退出登录后,需要清除 token 并刷新页面。因为是通过 addRoutes 添加路由的,而 vue-router 没有删除路由的 api,所以清除路由、清除 store 中存储的各种信息,刷新页面是最保险的。
相关文件的目录截图:
缺点:全局路由守卫里,每次路由跳转都要做判断;每次刷新页面,需要重新发请求获取用户权限;退出登录时,需要刷新一次页面将动态添加的路由以及权限信息清空;
优点:菜单与路由分离,菜单的修改、添加、删除由后端控制,利于后期维护;使用 addRoutes 动态挂载路由,可控制用户不能在 url 输入相关地址进行跳转;
vue权限管理还有其他实现方式,大家可以根据实际业务考虑做调整,以上的实现方式是比较适合我们现有项目的需求的。以上,有问题欢迎提出交流,喜欢的话点个赞哦~
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
作者:lxcan
转发链接:https://segmentfault.com/a/1190000022431839
Vue常见的面试知识点汇总(上)「附答案」
作者:东起
转发链接:https://zhuanlan.zhihu.com/p/103763164
Vue常见的面试知识点汇总(上)【附答案】本篇
1.页面中定义一个定时器,在哪个阶段清除?
答案:在 beforeDestroy 中销毁定时器。
①为什么销毁它:
在页面a中写了一个定时器,比如每隔一秒钟打印一次1,当我点击按钮进入页面b的时候,会发现定时器依然在执行,这是非常消耗性能的。
②解决方案1:
方案1 有两点不好的地方,引用尤大的话来说就是:
它需要在这个组件实例中保存这个 timer,如果可以的话最好只有生命周期钩的可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化的清理我们建立的所有东西。
方案2(推荐):该方法是通过$once这个事件侦听器在定义完定时器之后的位置来清除定时器
官网参考链接:https://cn.vuejs.org/v2/guide/components-edge-cases.html
2.附组件如何获取的组件的数据,子组件如何获取父组件的数据,父子组件如何传值?
① 先说,父组件如何主动获取子组件的数据?
方案1:$children
$children用来访问子组件实例,要知道一个组件的子组件可能是不唯一的,所以它的返回值是数组。
现在,我们定义Header,HelloWorld两个组件
打印的是一个数组,可以用foreach分别得到所需要的的数据
缺点:
无法确定子组件的顺序,也不是响应式的。如果你确切的知道要访问子组件建议使用$refs。
方案2 : $refs
调用helloworld的组件的时候直接定义一个ref,这样就可以通过this.$refs获取所需要的的数据。
②子组件如何主动获取父组件中的数据?
通过 : $parent
$parent用来访问父组件实例,通常父组件都是唯一确定的,跟$children类似
父子组件通信除了以上三种,还有props 和$emit 这两种比较常用就不介绍了,除此之外,还有inheritAttrs和 $attrs
③ inheritAttrs
这是@2.4新增的属性和接口。inheritAttrs属性控制子组件html属性上是否显示父组件的提供的属性。
如果我们将父组件Index中的属性desc、keysword、message三个数据传递到子组件HelloWorld中的话,如下
父组件Index部分
子组件:HelloWorld,props中只接受了message
实际情况,我们只需要message,那其他两个属性则会被当做普通的html元素插在子组件的根元素上。
如图
这样做会使组件预期功能变得模糊不清,这个时候,在子组件中写入,inheritAttrs:false ,这些没用到的属性便会被去掉,true的话,就会显示。
如果,父组件中没被需要的属性,跟子组件本来的属性冲突的时候,则依据父组件
子组件:HelloWorld
这个时候父组件中type=“text”,而子组件中type=”number”,而实际中最后显示的是type=”text”,这并不是我们想要的,所以只要设置:inheritAttrs:false,type便会成为number
上述这些没被用到的属性,如何被获取呢?这就用到了$attrs
③ $attrs
作用:可以获取到没有使用的注册属性,如果需要,我们在这也可以往下继续传递。
就上上述没有被用到的desc和keysword就能通过$attrs获取到。
通过$attrs的这个特性可以父组件传递到孙组件,免除父组件传递到子组件,再从子组件传递到孙组件的麻烦
代码如下附组件Index部分
子组件HelloWorld部分
孙子组件sunzi部分
可以看出通过 v-bind=”$attrs”将数据传到孙组件中
除了以上,provide / inject 也适用于 隔代组件通信,尤其是获取祖先组件的数据,非常方便。
简单的说,当组件的引入层次过多,我们的子孙组件想要获取祖先组件的资源,那么怎么办呢,总不能一直取父级往上吧,而且这样代码结构容易混乱。这个就是provide / inject要干的事情。
在这里我们在父组件中provide for这个变量,然后直接设置三个组件(childOne、childTwo 、childThird)并且一层层不断内嵌其中, 而在最深层的childThird组件中我们可以通过inject获取for这个变量
3.自定义指令如何定义,它的生命周期是什么?
通过Vue.directive() 来定义全局指令
有几个可用的钩子(生命周期), 每个钩子可以选择一些参数. 钩子如下:
bind: 一旦指令附加到元素时触发
inserted: 一旦元素被添加到父元素时触发
update: 每当元素本身更新(但是子元素还未更新)时触发
componentUpdate: 每当组件和子组件被更新时触发
unbind: 一旦指令被移除时触发。
bind和update也许是这五个里面最有用的两个钩子了
每个钩子都有el, binding, 和vnode参数可用.
update和componentUpdated钩子还暴露了oldVnode, 以区分传递的旧值和较新的值.
el就是所绑定的元素.
binding是一个保护传入钩子的参数的对象. 有很多可用的参数, 包括name, value, oldValue, expression, arguments, arg及修饰语.
vnode有一个更不寻常的用例, 它可用于你需要直接引用到虚拟DOM中的节点.
binding和vnode都应该被视为只读.
现在,自定义一个指令,添加一些样式,表示定位的距离
假设我们想要区分从顶部或者左侧偏移70px, 我们可以通过传递一个参数来做到这一点
也可以同时传入不止一个值
4、vue生命周期,各个阶段简单讲一下?
breforeCreate():实例创建前,这个阶段实例的data和methods是读不到的。
created():实例创建后,这个阶段已经完成数据观测,属性和方法的运算,watch/event事件回调,mount挂载阶段还没有开始。$el属性目前不可见,数据并没有在DOM元素上进行渲染。
created完成之后,进行template编译等操作,将template编译为render函数,有了render函数后才会执行beforeMount()
beforeMount():在挂载开始之前被调用:相关的 render 函数首次被调用
mounted():挂载之后调用,el选项的DOM节点被新创建的 vm.$el 替换,并挂载到实例上去之后调用此生命周期函数,此时实例的数据在DOM节点上进行渲染
后续的钩子函数执行的过程都是需要外部的触发才会执行
有数据的变化,会调用beforeUpdate,然后经过Virtual Dom,最后updated更新完毕,当组件被销毁的时候,会调用beforeDestory,以及destoryed。
5、watch 和 computed的区别?
computed:
①有缓存机制;②不能接受参数;③可以依赖其他computed,甚至是其他组件的data;④不能与data中的属性重复
watch:
①可接受两个参数;②监听时可触发一个回调,并做一些事情;③监听的属性必须是存在的;④允许异步
watch配置:handler、deep(是否深度)、immeditate (是否立即执行)
总结:
当有一些数据需要随着另外一些数据变化时,建议使用computed
当有一个通用的响应数据变化的时候,要执行一些业务逻辑或异步操作的时候建议使用watch
6、请说一下computed中的getter和setter
① computed 中可以分成 getter(读取) 和 setter(设值)
② 一般情况下是没有 setter 的,computed 预设只有 getter ,也就是只能读取,不能改变设置。
一、默认只有 getter的写法
注意:不是说我们更改了getter里使用的变量,就会触发computed的更新,前提是computed里的值必须要在模板里使用才行。如果将{{fullName}}去掉,get()方法是不会触发的。
二、setter的写法,可以设置
在这里,我们修改fullName的值,就会触发setter,同时也会触发getter。
注意:并不是触发了setter也就会触发getter,他们两个是相互独立的。我们这里修改了fullName会触发getter是因为setter函数里有改变firstName 和 lastName 值的代码,这两个值改变了,fullName依赖于这两个值,所以便会自动改变。
7、导航钩子有哪几种,分别如何用,如何将数据传入下一个点击的路由页面?
① 全局导航守卫
前置守卫
后置钩子(没有next参数)
②路由独享守卫
顺便看一下路由里面的参数配置:
③ 组件内的导航钩子
组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。他们是直接在路由组件内部直接进行定义的
beforeRouteEnter
注:beforeRouteEnter 不能获取组件实例 this,因为当守卫执行前,组件实例被没有被创建出来,我们可以通过给 next 传入一个回调来访问组件实例。在导航被确认时,会执行这个回调,这时就可以访问组件实例了
仅仅是 beforRouteEnter 支持给 next 传递回调,其他两个并不支持,因为剩下两个钩子可以正常获取组件实例 this
如何通过路由将数据传入下一个跳转的页面呢?
答: params 和 query
params
query
那query和params什么区别呢?
① params只能用name来引入路由,query既可以用name又可以用path(通常用path)
② params类似于post方法,参数不会在地址栏中显示
query类似于get请求,页面跳转的时候,可以在地址栏看到请求参数
那刚才提到的this.$router 和this.$route有何区别?
先打印出来看一下
$router为VueRouter实例,想要导航到不同URL,则使用$router.push方法
$route为当前router跳转对象,里面可以获取name、path、query、params等
8、es6 的特有的类型, 常用的操作数组的方法都有哪些?
es6新增的主要的特性:
① let const 两者都有块级作用域
② 箭头函数
③ 模板字符串
④ 结构赋值
⑤ for of循环
⑥ import 、export 导入导出
⑦ set数据结构
⑧ …展开运算符
⑨ 修饰器 @
⑩ class类继承
⑪ async、await
⑫ promise
⑬ Symbol
⑭ Proxy代理
操作数组常用的方法:
es5:concat 、join 、push、pop、shift、unshift、slice、splice、substring和substr 、sort、 reverse、indexOf和lastIndexOf 、every、some、filter、map、forEach、reduce
es6:find、findIndex、fill、copyWithin、Array.from、Array.of、entries、values、key、includes
9、vue双向绑定原理?
通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
10、vue-router的实现原理,history和hash模式有什么区别?
vue-router有两种模式,hash模式和history模式
hash模式
url中带有#的便是hash模式,#后面是hash值,它的变化会触发hashchange 这个事件。
通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作:
另外,hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。
history模式
history api可以分为两大部分,切换和修改
① 切换历史状态
包括back,forward,go三个方法,对应浏览器的前进,后退,跳转操作
② 修改历史状态
包括了pushState,replaceState两个方法,这两个方法接收三个参数:stateObj,title,url
通过pushstate把页面的状态保存在state对象中,当页面的url再变回这个url时,可以通过event.state取到这个state对象,从而可以对页面状态进行还原,这里的页面状态就是页面字体颜色,其实滚动条的位置,阅读进度,组件的开关的这些页面状态都可以存储到state的里面。
history缺点:
1:hash 模式下,仅hash符号之前的内容会被包含在请求中,如http://www.a12c.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误。
2:history模式下,前端的URL必须和实际向后端发起请求的URL一致。如http://www.a12c.com/book/a。如果后端缺少对/book/a 的路由处理,将返回404错误
11、怎么在vue中点击别的区域输入框不会失去焦点?
答:阻止事件的默认行为
具体操作:监听你想点击后不会丢失 input 焦点的那个元素的 mousedown 事件,回调里面调用 event.preventDefault(),会阻止使当前焦点丢失这一默认行为。
12、vue中data的属性可以和methods中的方法同名吗?为什么?
答:不可以
因为,Vue会把methods和data的东西,全部代理到Vue生成的对象中,会产生覆盖所以最好不要同名
13、怎么给vue定义全局的方法?
Vue.prototype.方法名称
14、Vue 2.0 不再支持在 v-html 中使用过滤器怎么办?
解决方法:
①全局方法(推荐)
②computed方法
③$options.filters(推荐)
14、怎么解决vue打包后静态资源图片失效的问题?
答:将静态资源的存放位置放在src目录下
16、怎么解决vue动态设置img的src不生效的问题?
因为动态添加src被当做静态资源处理了,没有进行编译,所以要加上require
17、跟keep-alive有关的生命周期是哪些?描述下这些生命周期
activated和deactivated两个生命周期函数
1.activated:当组件激活时,钩子触发的顺序是created->mounted->activated
2.deactivated: 组件停用时会触发deactivated,当再次前进或者后退的时候只触发activated
18、你知道vue中key的原理吗?说说你对它的理解
暂时没弄明白,等会儿写
19、vue中怎么重置data?
答:Object.assign()
Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
注意,具有相同属性的对象,同名属性,后边的会覆盖前边的。
由于Object.assign()有上述特性,所以我们在Vue中可以这样使用:
Vue组件可能会有这样的需求:在某种情况下,需要重置Vue组件的data数据。此时,我们可以通过this.$data获取当前状态下的data,通过this.$options.data()获取该组件初始状态下的data。
然后只要使用Object.assign(this.$data, this.$options.data())就可以将当前状态的data重置为初始状态。
20、vue怎么实现强制刷新组件?
答:① v-if ② this.$forceUpdate
v-if
this.$forceUpdate
21、vue如何优化首页的加载速度?
① 第三方js库按CDN引入(一、cdn引入 二、去掉第三方库引入的import 三、把第三方库的js文件从打包文件里去掉)
② vue-router路由懒加载
③ 压缩图片资源
④ 静态文件本地缓存
http缓存:推荐网站:https://www.cnblogs.com/chinajava/p/5705169.html
service worker离线缓存:,缺点:需要在HTTPS站点下,推荐:http://lzw.me/a/pwa-service-worker.html
本篇未完结,请见更精彩的下一篇
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
作者:东起
转发链接:https://zhuanlan.zhihu.com/p/103763164
带你五步学会Vue SSR
作者:liuxuan 前端名狮
转发链接:https://mp.weixin.qq.com/s/6K6GUHcLwLG4mzfaYtVMBQ
SSR大家肯定都不陌生,通过服务端渲染,可以优化SEO抓取,提升首页加载速度等,我在学习SSR的时候,看过很多文章,有些对我有很大的启发作用,有些就只是照搬官网文档。通过几天的学习,我对SSR有了一些了解,也从头开始完整的配置出了SSR的开发环境,所以想通过这篇文章,总结一些经验,同时希望能够对学习SSR的朋友起到一点帮助。
我会通过五个步骤,一步步带你完成SSR的配置:
- 纯浏览器渲染
- 服务端渲染,不包含Ajax初始化数据
- 服务端渲染,包含Ajax初始化数据
- 服务端渲染,使用serverBundle和clientManifest进行优化
- 一个完整的基于Vue + VueRouter + Vuex的SSR工程
如果你现在对于我上面说的还不太了解,没有关系,跟着我一步步向下走,最终你也可以独立配置一个SSR开发项目,所有源码我会放到github上,大家可以作为参考。
地址:https://github.com/leocoder351/vue-ssr-demo
这个配置相信大家都会,就是基于weback + vue的一个常规开发配置,这里我会放一些关键代码,完整代码可以去github查看。
最终效果截图:
完整代码查看:https://github.com/leocoder351/vue-ssr-demo/tree/master/01
服务端渲染SSR,类似于同构,最终要让一份代码既可以在服务端运行,也可以在客户端运行。如果说在SSR的过程中出现问题,还可以回滚到纯浏览器渲染,保证用户正常看到页面。
那么,顺着这个思路,肯定就会有两个webpack的入口文件,一个用于浏览器端渲染weboack.client.config.js,一个用于服务端渲染webpack.server.config.js,将它们的公有部分抽出来作为webpack.base.cofig.js,后续通过webpack-merge进行合并。同时,也要有一个server来提供http服务,我这里用的是koa。
我们来看一下新的目录结构:
在纯客户端应用程序(client-only app)中,每个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,我们也希望如此:每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染(cross-request state pollution)。
所以,我们要对app.js做修改,将其包装为一个工厂函数,每次调用都会生成一个全新的根组件。
app.js
在浏览器端,我们直接新建一个根组件,然后将其挂载就可以了。
entry-client.js
在服务器端,我们就要返回一个函数,该函数的作用是接收一个context参数,同时每次都返回一个新的根组件。这个context在这里我们还不会用到,后续的步骤会用到它。
entry-server.js
然后再来看一下index.ssr.html
index.ssr.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)应用程序:
由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要\”激活\”这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。
如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:
Vue在浏览器端就依靠这个属性将服务器吐出来的html进行激活,我们一会自己构建一下就可以看到了。
接下来我们看一下webpack相关的配置:
webpack.base.config.js
webpack.client.config.js
注意,这里的入口文件变成了entry-client.js,将其打包出的client.bundle.js插入到index.html中。
webpack.server.config.js
这里有几个点需要注意一下:
- 入口文件是 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
接下来我们看server端关于http服务的代码:
server/server.js
这里对两个端口进行监听,3000端口是服务端渲染,3001端口是直接输出index.html,然后会在浏览器端走Vue的那一套,主要是为了和服务端渲染做对比使用。
这里的关键代码是如何在服务端去输出html`字符串。
可以看到,server.bundle.js在这里被使用了,因为它的入口是一个函数,接收context作为参数(非必传),输出一个根组件app。
这里我们用到了vue-server-renderer插件,它有两个方法可以做渲染,一个是createRenderer,另一个是createBundleRenderer。
createRenderer无法接收为服务端打包出的server.bundle.js文件,所以这里只能用createBundleRenderer。
serverBundle 参数可以是以下之一:
- 绝对路径,指向一个已经构建好的 bundle 文件(.js 或 .json)。必须以 / 开头才会被识别为文件路径。
- 由 webpack + vue-server-renderer/server-plugin 生成的 bundle 对象。
- JavaScript 代码字符串(不推荐)。
这里我们引入的是.js文件,后续会介绍如何使用.json文件以及有什么好处。
使用createRenderer和createBundleRenderer返回的renderer函数包含两个方法renderToString和renderToStream,我们这里用的是renderToString成功后直接返回一个完整的字符串,renderToStream返回的是一个Node流。
renderToString支持Promise,但是我在使用Prmoise形式的时候样式会渲染不出来,暂时还不知道原因,如果大家知道的话可以给我留言哦。
配置基本就完成了,来看一下如何运行。
最终效果展示:
访问http://localhost:3000/index
我们看到了前面提过的data-server-rendered=\”true\”属性,同时会加载client.bundle.js文件,为了让Vue在浏览器端做后续接管。
访问http://localhost:3001/index还和第一步实现的效果一样,纯浏览器渲染,这里就不放截图了。
完整代码查看:https://github.com/leocoder351/vue-ssr-demo/tree/master/02
如果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更新。
更新后的目录结构:
先来看一下store.js:
store/store.js
如果不太了解Vuex,可以去Vuex官网先看一些基本概念。
这里fetchBar可以看成是一个异步请求,这里用setTimeout模拟。在成功回调中commit相应的mutation进行状态修改。
这里有一段关键代码:
因为store.js同样也会被打包到服务器运行的server.bundle.js中,所以运行环境不一定是浏览器,这里需要对window做判断,防止报错,同时如果有window.__INITIAL_STATE__属性,说明服务器已经把所有初始化需要的异步数据都获取完成了,要对store中的状态做一个替换,保证统一。
components/Bar.vue
这里在Bar组件的默认导出对象中增加了一个方法asyncData,在该方法中会dispatch相应的action,进行异步数据获取。
需要注意的是,我在mounted中也写了获取数据的代码,这是为什么呢? 因为想要做到同构,代码单独在浏览器端运行,也应该是没有问题的,又由于服务器没有mounted生命周期,所以我写在这里就可以解决单独在浏览器环境使用也可以发起同样的异步请求去初始化数据。
components/Foo.vue
这里我对两个组件都添加了一个点击事件,为的是证明在服务器吐出首页html后,后续的步骤都会被浏览器端的Vue接管,可以正常执行后面的操作。
app.js
在建立根组件的时候,要把Vuex的store传进去,同时要返回,后续会用到。
最后来看一下entry-server.js,关键步骤在这里:
entry-server.js
我们通过导出的App拿到了所有它下面的components,然后遍历,找出哪些component有asyncData方法,有的话调用并传入store,该方法会返回一个Promise,我们使用Promise.all等所有的异步方法都成功返回,才resolve(app)。
context.state = store.state作用是,当使用createBundleRenderer时,如果设置了template选项,那么会把context.state的值作为window.__INITIAL_STATE__自动插入到模板html中。
这里需要大家多思考一下,弄清楚整个服务端渲染的逻辑。
如何运行:
最终效果截图:
服务端渲染:打开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,在这个时间用户也是看不到页面的。没错,接口的时间我们无法避免,就算是纯浏览器渲染,首页该调接口还是得调,如果接口响应慢,那么纯浏览器渲染看到完整页面的时间会更慢。
完整代码查看:https://github.com/leocoder351/vue-ssr-demo/tree/master/03
前面我们创建服务端renderer的方法是:
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.js
webpack.server.config.js
因为是服务端引用模块,所以不需要打包node_modules中的依赖,直接在代码中require引用就好,所以配置externals: [nodeExternals()]。
两个配置文件会分别生成vue-ssr-client-manifest.json和vue-ssr-server-bundle.json。作为createBundleRenderer的参数。
来看server.js
server.js
效果和第三步就是一样的啦,就不截图了,完整代码查看github。
这里和第四步不一样的是引入了vue-router,更接近于实际开发项目。
在src下新增router目录。
router/index.js
这里我们把Foo组件作为一个异步组件引入,做成按需加载。
在app.js中引入router,并导出:
app.js
修改App.vue引入路由组件:
App.vue
最重要的修改在entry-server.js中,
entry-server.js
这里前面提到的context就起了大作用,它将用户访问的url地址传进来,供vue-router使用。因为有异步组件,所以在router.onReady的成功回调中,去找该url路由所匹配到的组件,获取异步数据那一套还和前面的一样。
于是,我们就完成了一个基本完整的基于Vue + VueRouter + VuexSSR配置,完成代码查看github。
最终效果演示:
访问http://localhost:3000/bar:
完整代码查看github
上面我们通过五个步骤,完成了从纯浏览器渲染到完整服务端渲染的同构,代码既可以运行在浏览器端,也可以运行在服务器端。那么,回过头来我们再看一下是否有优化的空间,又或者有哪些扩展的思考。
- 我们目前是使用renderToString方法,完全生成html后,才会向客户端返回,如果使用renderToStream,应用bigpipe技术可以向浏览器持续不断的返回一个流,那么文件的加载浏览器可以尽早的显示一些东西出来。
返回的值是 Node.js stream:
在流式渲染模式下,当 renderer 遍历虚拟 DOM 树(virtual DOM tree)时,会尽快发送数据。这意味着我们可以尽快获得\”第一个 chunk\”,并开始更快地将其发送给客户端。
然而,当第一个数据 chunk 被发出时,子组件甚至可能不被实例化,它们的生命周期钩子也不会被调用。这意味着,如果子组件需要在其生命周期钩子函数中,将数据附加到渲染上下文(render context),当流(stream)启动时,这些数据将不可用。这是因为,大量上下文信息(context information)(如头信息(head information)或内联关键 CSS(inline critical CSS))需要在应用程序标记(markup)之前出现,我们基本上必须等待流(stream)完成后,才能开始使用这些上下文数据。
因此,如果你依赖由组件生命周期钩子函数填充的上下文数据,则不建议使用流式传输模式。
- webpack优化
webpack优化又是一个大的话题了,这里不展开讨论,感兴趣的同学可以自行查找一些资料,后续我也可能会专门写一篇文章来讲webpack优化。
- 是否必须使用vuex?
答案是不用。Vuex只是为了帮助你实现一套数据存储、更新、获取的机制,如果你不用Vuex,那么你就必须自己想一套方案可以将异步获取到的数据存起来,并且在适当的时机将它注入到组件内,有一些文章提出了一些方案,我会放到参考文章里,大家可以阅读一下。
- 是否使用SSR就一定好?
这个也是不一定的,任何技术都有使用场景。SSR可以帮助你提升首页加载速度,优化搜索引擎SEO,但同时由于它需要在node中渲染整套Vue的模板,会占用服务器负载,同时只会执行beforeCreate和created两个生命周期,对于一些外部扩展库需要做一定处理才可以在SSR中运行等等。
本文通过五个步骤,从纯浏览器端渲染开始,到配置一个完整的基于Vue + vue-router + Vuex的SSR环境,介绍了很多新的概念,也许你看完一遍不太理解,那么结合着源码,去自己手敲几遍,然后再来看几遍文章,相信你一定可以掌握SSR。
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。