如何用 Vue 构建大型单页面应用

3,929 阅读11分钟
原文链接: zhuanlan.zhihu.com

之前在HTML5梦工场微软的开发者沙龙分享过这个课题,这次就整理了一下分享出来


技术栈:

服务端:Node.js

前端框架:Vue (v2.x)

前端构建工具:webpack

代码检查:eslint

状态管理:Vuex

服务端通信:axios

进程管理:pm2

项目自动化部署:jenkins


首先先说一下单页面的优缺点:


单页面开发的优点:


  1. 良好的用户体验

    用户不需要重新刷新页面,减少TTFB的请求耗时,获取数据也是通过Ajax异步获取,页面显示流畅。

  2. 前后端分离

    前端负责界面显示,后端负责数据存储和计算,各司其职,不会把前后端的逻辑混杂在一起。


  3. 减轻服务端压力

    减轻服务器压力,服务器只需要提供API接口,不用管页面逻辑和页面的拼接,吞吐能力会提高几倍。


  4. 共用一套后端程序代码,适配多端

    同一套后端程序代码,不用修改就可以适用于Web、手机、平板。


单页面开发的缺点:

  1. 首屏加载过慢

    单页面首次加载,需要将所有页面所依赖的css和js 合并后统一加载,所 以css和js文件会较大,影响页面首次打开时间。

  2. SEO

    因为页面数据都是前端异步加载的方式,不利于搜索引擎的抓取。


基于以上的原因,所以我们引入了Vue SSR技术


引入Vue SSR

  1. 服务端预渲染

    vue2.0引入了虚拟DOM,实现原理就是vue的编译器在编译模板之后, 会将这些模板编译成一个渲染函数,也就是render方法, 函数被调用的时 候就会渲染并且返回一个虚拟DOM的树,在交给一个patch函数,把虚拟 DOM施加到真实的DOM上。这样做的主要原因是因为js的运算是非常快 的,而在浏览器中直接操作DOM会对性能有一定损耗。


  2. 流式渲染

    服务端渲染支持流式渲染,因为http请求也是流式的,在渲染组件时返回一个可读的 stream 流,然后直接写入 到 HTTP 响应中。流式渲染 能够确保服务端的响应度,也能让用户更快地获得渲染内容。


  3. 对组件进行缓存


  4. 前后端复用一套代码

    前端和服务端可以复用一套代码,提升开发效率和维护性


Vue SSR流程图

<img src="https://pic4.zhimg.com/v2-0e50e752acfb11405f05571d44333fbf_b.png" data-rawwidth="980" data-rawheight="450" class="origin_image zh-lightbox-thumb" width="980" data-original="https://pic4.zhimg.com/v2-0e50e752acfb11405f05571d44333fbf_r.png">

从图中可以看出,ssr有两个入口文件,client-entry 和 server-entry.js 也就是客户端和服务端入口文件,都包含了同一套应用代码,webpack 通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行对比,也就是判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告。



项目目录结构:

|— api api 接口文件

|— assets 静态资源目录

|— components vue组件

|— filters 过滤器文件

|— router 路由文件

|— views 页面模板文件

|— utils 工具方法目录

|— store vuex相关文件

|— app.js vue入口文件

|— client-entry.js 客户端入口文件

|— server.entry.js 服务端入口文件

|— index.html HTML入口文件



开启Vue SSR


  1. 使用服务端框架 express 或 koa

    服务端框架,我们使用的是express,之前也使用过了koa,有了一些未知的坑,koa是不错,但是生态圈不如express,express有不少比较有特色的模块并不支持koa,并且官方的例子使用的就是express,所以我们为了方便就选择了express作为我们的ssr服务器。


  2. 安装配置webpack 和 vue


  3. 拆分入口文件,服务端和浏览器要分开渲染

    vue2使用了虚拟DOM, 因此对浏览器环境和服务端环境要分开渲染, 要创建两个对应的入口文件。


    server-entry: 使用 vue ssr 功能将虚拟DOM渲染成网页


    client-entry: 使用 $mount 直接将应用挂载到DOM上


server-entry 服务端入口文件

<img src="https://pic2.zhimg.com/v2-410f491d8e55880e3d3be14fe29864bd_b.png" data-rawwidth="860" data-rawheight="540" class="origin_image zh-lightbox-thumb" width="860" data-original="https://pic2.zhimg.com/v2-410f491d8e55880e3d3be14fe29864bd_r.png">

server.js 返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,将 vue 实例通过 promise 返回。 context 一般包含 当前页面的url,首先我们调用 vue-router 的 router.push(url) 切换到到对应的路由, 然后调用getMatchedComponents 方法返回对应要渲染的组件, 这里会检查组件是否有 preFetch 方法,如果有就会执行它。


在then里会将服务端获取到的数据挂载到 context 对象上,后面会把这些数据直接发送到浏览器端与客户端的vue实例进行数据(状态)同步。


client-entry 客户端入口文件

<img src="https://pic2.zhimg.com/v2-7225fca49facb7ec2a25094f85e78949_b.png" data-rawwidth="752" data-rawheight="271" class="origin_image zh-lightbox-thumb" width="752" data-original="https://pic2.zhimg.com/v2-7225fca49facb7ec2a25094f85e78949_r.png">

客户端入口文件很简单,同步服务端发送过来的数据,会和服务端生成的DOM 进行对比,也就是判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到服务端渲染的DOM上, 否则会提示警告。


组件化


将组件分为:基础组件、业务组件、页面组件


基础组件:

与业务低耦合,可复用


业务组件:

与业务深耦合,复用难度大,难抽象


页面组件

页面是组件的容器,将组件组合可以形式一个完整的界面


├── components

│ ├── business # 基础组件

│ └── base # 业务组件

├── header

├── header.vue

├── logo.png

├── header.scss

├── views # 页面组件

│ ├── index

├── index.vue

└── comment

<img src="https://pic1.zhimg.com/v2-4bb753489ac8ed750dfe195498b86f2c_b.png" data-rawwidth="623" data-rawheight="716" class="origin_image zh-lightbox-thumb" width="623" data-original="https://pic1.zhimg.com/v2-4bb753489ac8ed750dfe195498b86f2c_r.png">

Vue的组件引入构建工具之后有一个单文件组件概念。就是眼前的这个Vue文件,在同一个Vue文件里,可以同时写 模板、脚本 和 样式,三个东西放在一个里面。 使用webpack进行打包,编译成js模块。可以使用多种预处理器Babel、ts、sass、postcss,同时可以使用方便的热重载。


在Vue中,父子组件之间的通信是通过 props 传递。从父向子单向传递;每次父组件更新时,子组件的所有 prop 都会更新为最新值。


如果想要子组件把数据传递给父组件,就需要在子组件上绑定自定义事件,然后在子组件使用emit去派发事件


但开发中大型项目会遇到以下的问题


  • 多个视图依赖于同一状态

  • 兄弟组件间的状态传递无能为力

  • 传参的方法对于多层嵌套的组件将会非常繁琐


所以这个时候,我们需要用Vuex负责多组件的状态管理


Vuex


  • Vuex 是专门为 Vue.js 设计的状态管理库

  • 结合Vue实现页面的展示更新

  • 统一页面状态管理以及操作处理


Vuex 使用 单一状态树,通俗理解就是一个应用的数据集合,可以想象为一个“前端数据库”,让其在各个页面上实现数据的共享,并且可操作


<img src="https://pic1.zhimg.com/v2-96cb87f73f32562dfbe95e63e6abeac0_b.png" data-rawwidth="701" data-rawheight="551" class="origin_image zh-lightbox-thumb" width="701" data-original="https://pic1.zhimg.com/v2-96cb87f73f32562dfbe95e63e6abeac0_r.png">从左到右,从组件出发,组件中调用 action,在 action 这一层级我们可以和后台数据交互,比如获取初始化的数据源,或者中间数据的过滤等。然后在 action 中去派发 Mutation。Mutation 去触发状态的改变,状态的改变,将触发视图的更新。

从左到右,从组件出发,组件中调用 action,在 action 这一层级我们可以和后台数据交互,比如获取初始化的数据源,或者中间数据的过滤等。然后在 action 中去派发 Mutation。Mutation 去触发状态的改变,状态的改变,将触发视图的更新。

Vuex项目结构


├── index.html # HTML模板

├── app.js

├── api # API配置文件

├── components # 组件

└── store

├── index.js # 入口文件,提供store的module构建

└── modules

├── index.js # 首页模块

├── list.js # 列表模块

└── common.js # 通用模块


<img src="https://pic3.zhimg.com/v2-adeaa4a67d0385d9476e3d418c4f5c7a_b.png" data-rawwidth="598" data-rawheight="604" class="origin_image zh-lightbox-thumb" width="598" data-original="https://pic3.zhimg.com/v2-adeaa4a67d0385d9476e3d418c4f5c7a_r.png">

使用单一状态树,导致应用的所有状态集中到一个对象中。所以当应用变得很大时,store对象会变得臃肿不堪


为了解决以上问题,Vuex 允许我们将 store 分割到模块(module)。每个模块拥有自己的 state、mutation、action、getters,让代码结构更清晰, 多人开发时,每个人只需要负责开发自己的模块即可。


state:单一状态树


getters:状态的获取


mutations:触发同步事件


action:提交mutation可以包含异步操作


数据处理


Axios


现在vue-resource,现在官方宣布不在维护,

所以我这里使用的是Axios,它能够同时支持客户端和服务端请求,拦截请求和响应,支持 Promise API,客户端支持防止 CSRF 攻击,并且可以处理并发请求。


我们在项目里,主要 利用拦截器做预处理,对API进行封装,并且讲述下在组件中如何进行使用


利用拦截器做预处理


请求时的拦截器

<img src="https://pic3.zhimg.com/v2-1b3afd9635f2ea589ac6c9ffd5c2677e_b.png" data-rawwidth="760" data-rawheight="418" class="origin_image zh-lightbox-thumb" width="760" data-original="https://pic3.zhimg.com/v2-1b3afd9635f2ea589ac6c9ffd5c2677e_r.png">

请求完成后的拦截器

<img src="https://pic2.zhimg.com/v2-243688bc0cf5093b1fe9cb50850090a1_b.png" data-rawwidth="779" data-rawheight="438" class="origin_image zh-lightbox-thumb" width="779" data-original="https://pic2.zhimg.com/v2-243688bc0cf5093b1fe9cb50850090a1_r.png">

对API进行封装


对 服务端 和 客户端 API请求文件进行区分

要点:服务端 需要在headers里写入cookie

<img src="https://pic1.zhimg.com/v2-472508cb4c6df2be440e7528464fff90_b.png" data-rawwidth="748" data-rawheight="590" class="origin_image zh-lightbox-thumb" width="748" data-original="https://pic1.zhimg.com/v2-472508cb4c6df2be440e7528464fff90_r.png">

<img src="https://pic4.zhimg.com/v2-2e7f94e2834455ee719e2535e7e04357_b.png" data-rawwidth="904" data-rawheight="558" class="origin_image zh-lightbox-thumb" width="904" data-original="https://pic4.zhimg.com/v2-2e7f94e2834455ee719e2535e7e04357_r.png">

在组件中的使用


<img src="https://pic1.zhimg.com/v2-0d960168ea0e788b338970da2569828c_b.png" data-rawwidth="722" data-rawheight="314" class="origin_image zh-lightbox-thumb" width="722" data-original="https://pic1.zhimg.com/v2-0d960168ea0e788b338970da2569828c_r.png">

随着我们代码,项目越来越大,优化必不可少,下面介绍一下我们的优化策略


优化策略


serviceWorker 预缓存


  • 在serviceWorker进程中运行

  • 缓存必要的 JS、CSS、font

  • 大幅度减少CDN的压力

  • 资源是持久性存储


和indexDB、LocalStorage共用,存储空间约有50M


使用常规的方式,用LocalStorage来缓存JS以及CSS,会与业务代码耦合性很高,并且十分容易被XSS攻击,但使用serviceWorker不会有这样问题,我们只需要关注需要缓存的文件即可,缓存文件的安全全部由浏览器自行控制,用户无法手动删除这些缓存。

唯一的缺点就是,目前有些浏览器还不支持serviceWorker,只能等浏览器自行支持


我们可以使用webpack插件来实现

npm install --save-dev sw-precache-webpack-plugin


<img src="https://pic3.zhimg.com/v2-a81d77d8064967db89950a2465303356_b.png" data-rawwidth="712" data-rawheight="562" class="origin_image zh-lightbox-thumb" width="712" data-original="https://pic3.zhimg.com/v2-a81d77d8064967db89950a2465303356_r.png">

服务端API数据的缓存

<img src="https://pic4.zhimg.com/v2-2c20be08404c661ff6f60addc80b58af_b.png" data-rawwidth="592" data-rawheight="614" class="origin_image zh-lightbox-thumb" width="592" data-original="https://pic4.zhimg.com/v2-2c20be08404c661ff6f60addc80b58af_r.png">

组件的缓存

<img src="https://pic2.zhimg.com/v2-9d6ce56498707ac374317fb7106d6165_b.png" data-rawwidth="946" data-rawheight="209" class="origin_image zh-lightbox-thumb" width="946" data-original="https://pic2.zhimg.com/v2-9d6ce56498707ac374317fb7106d6165_r.png">
<img src="https://pic4.zhimg.com/v2-1dea0cec37c0f7da91b8253ae0626de3_b.png" data-rawwidth="949" data-rawheight="330" class="origin_image zh-lightbox-thumb" width="949" data-original="https://pic4.zhimg.com/v2-1dea0cec37c0f7da91b8253ae0626de3_r.png">

按需分块加载

后面会单独写一篇文章,来介绍和讲解下使用方法和原理


通用策略


  • 将静态资源上传到CDN

  • 使用Gzip压缩

  • 图片压缩、懒加载和base64

  • 首屏外使用异步加载


优化后,我们的项目的首页加载时间从700多ms,提升到了400多ms


欢迎大家关注:AutoHome车服务前端团队 微信公众号