# Project

# 1. 项目概览

# 1.1 组件架构

image

# 1.2 组件逻辑

image

# 2. 项目细节

# 2.1 我负责部分

  • 主页面的三个模块:车次、我的车票、个人中心
    • 车次
    • 我的车票:这里又有一个子路由,四个页面之间跳转
    • 个人中心
    • 以及网络请求模块

# 2.2 一些小的亮点

  • 路由懒加载:增加首屏的加载速度

    • 将app.js拆分,多出几个js文件,也就是其他路由页面的js文件
  • Vuex来管理 bus 的座位信息和 user 的 localStorage 中的 token

  • vh来实现了响应式的布局

  • 导航守卫:(router.beforeEach)

    • 来保证每次切换页面之前,都要检查是否已经登陆
    • 如果是从「确认订单」的页面跳转回「车次信息」(选座位的页面),还要恢复之前已经选中的座位的被选中的状态
  • 封装、复用的思想:

    • 比如 getOrder 这个获取订单的函数的复用,只需要根据参数不同,来获取不同的订单,不需要再写多个函数了。
    • 订单详情页面的复用,已支付 / 未支付的订单用同一个组件,只是根据状态,下面是候车时间 / 前往支付
  • 跨域

    • vue的devServer.proxy
    • 本身应该是来自于一个叫做 http-proxy-middleware的集成
    • 参数中,开启了WebSocket,并且changeOrigin。
    // vue.config.js
    devServer: {
      proxy: {
        '/apis': {
          target: 'http://sb.dreamcat.ink:2020/',
            ws: true,
              changeOrigin: true,
                pathRewrite: {
                  '^/apis': ''
                }
        }
      }
    }
    

# 3. 难点与解决方案

# 3.1 CSS的布局与居中问题

  • 作为前端的新手,经常分不清 vertical-middle / text-align 之类的是什么作用
  • 百分比完全分不清楚是根据谁的百分比
  • 经常会写了样式,发现为什么不生效呢,或者生效了也不知道为什么

# 3.2 前后端分离的业务逻辑问题

  • 一个任务,到底是前端做呢,还是交给后端做呢?
  • 尽可能地分担后端的压力,并且减少网络上的流量:
    • 这个思想跟缓存很接近(感叹,很多东西本质上都是想通的)
    • 但是也不能什么都前端实现,客户端臃肿,发送的流量大,违背了初衷
    • 比如:
      • 注册时候的确认密码判断:没必要发给客户端,或者检查注册信息中是否有不合法的字符
      • 登陆的时候要显示账号或密码错误,而不是很直白的账号不存在或者密码错误,数据库里面是用哈希算法存的摘要,做摘要这个几乎是不可逆的计算,所以一般网络上见到的只有重置密码,没有找回原密码。

# 4. TypeScript重构

# 4.1 概览

  • 主要的页面已经基本能跑起来

# 4.2 难点

# 4.2.1 ref的问题

  • 主要还是vue2.x对于ts的支持不是很好,比如用的比较多的vue-property-decorator是vue社区出品。(所以期待vue3.0的ts)

  • scroll绑定了ref属性,方便调用,但是typescript里面不支持直接获取ref属性,需要单独声明一个属性去调用它

    // 1. 声明一个属性
    class YourComponent extends Vue {
      $refs: {
        checkboxElement: HTMLFormElement
      }
    
      someMethod () {
        this.$refs.checkboxElement.checked
      }
    }
    
    // 2. 声明计算属性
    import { Vue, Component, Ref } from 'vue-property-decorator'
    
    import AnotherComponent from '@/path/to/another-component.vue'
    
    @Component
    export default class YourComponent extends Vue {
      @Ref() readonly anotherComponent!: AnotherComponent
      @Ref('aButton') readonly button!: HTMLButtonElement
    }
    
    // 3. 使用vuevue-property-decorator提供的Ref属性
     get refs():any {
        return this.$refs;
      }
    this.refs().clildGreet.hello();
    
  • StackOverflow是真的靠谱!

# 4.2.2 项目比较庞大不知道怎么入手

  • 很低效的方式是对比法:跟别人的项目比较,看自己缺了什么配置,就补充什么配置,但其实项目不同,配置依赖等都不同
  • 比较一目了然的方法是:跟着别人写的经验、或者官方文档去初始化一个新的项目:
    • 一方面,可以了解TypeScript和Vue在初始化的时候是怎么结合起来的,便于理解和记忆怎么去配置,使二者合作
    • 另一方面,保证项目中不会缺失一些配置,或者可以根据自己的需要去配置一些东西,而不是跟着别人的项目走,并且看到一大堆的配置文件还是云里雾里的。
  • 因为是强类型,有时候确实是不确定一个数据的类型的时候,只能用any,但是总是用any的话,感觉就失去了使用typeScript的意义。为什么叫typescript,重点在 type
  • 过程中我也学到了很多东西:
    • 入口很重要,就有点像之前跟着网上的一个教程简单实现一个vue(这里要好好去看下vue的基本原理和那份代码)
    • 将大的模块拆分成小的,这里也体现了封装、模块化、强内聚低耦合的优越性,最重要的是写注释极大提高了代码的可读性,便于维护。
    • 先重构路由相关的几个主要展示的组件,然后再对于每个组件下的子组件及其相关的部分进行重构,包括网络请求和Vuex状态管理

# 4.3 对于TypeScript的理解

  • 强类型语言,但仅仅是编程时候,或者说是提供了一种js下强类型的编程范式,但是也可以不遵守
  • 因为最终ts的代码是被转化为es5的js代码,然后被浏览器执行的,所以非要说的话,其实本质上并没有发生改变
  • 即使IDE/tsLint可以检查出ts中的语法错误,但仍然是可以执行的,这些都是由于TS基于JS,而JS的本质是弱类型语言以及JS的一些其他的特定,决定了运行时的一些效果。
  • 重构代码的过程中,但是避免了在类型上出错,同时这种面向对象的编程思想,也增强了代码的可读性,对于大型项目而言降低了维护成本,虽然重构的时候ESLint搞得人很痛苦,并且至于性能还有待测试。
  • 目前在第三方组件vant的引入上还是有点问题,组件引入了但是显示的其中嵌入的内容不正确。

# 5. 测试

# 5.1 灰盒测试

# 5.1.1 概念

  • Jest:一个JavaScript测试框架(或者说,一款比较主流的 JavaScript 测试运行器),支持Vue,提供简单易用的api,支持生成代码覆盖率报告
    • 匹配器:toBe、toBeCloseTo、toEqual、toBeNull等
    • 钩子函数:beforeAll、afterAll、beforeEach、afterEach
    • 同步测试/异步测试
    • shallowMount/mount:
      • mount:计算机中指 挂载
      • shallowMount:渲染的子组件是假的,只mount了当前组件(可能单元测试应该用这个)
      • mount:完整的渲染(可能适合集成测试的时候使用)
  • vue-test-util:Vue Test Utils 是Vue.js 官方的单元测试实用工具库
  • babel:Jest不支持ES6语法,需要用babel转化一下

# 5.1.2 测试内容

  • 主要测试了登陆模块

  • 同步测试

    • 主要是测试了一些组件生成之后内容是否对应

    • 因为组件本身比较简单,所以就相当于操作了一些怎么写测试。

    • 示例代码

    • // ./src/components/hello-world/hello-world.spec.js
      import { shallowMount } from '@vue/test-utils';
      import tabBar from './tabBar';
      
      describe('<tab-bar/>', () => {
        const wrapper = shallowMount(tabBar);
        it('should render correct contents', () => {
          expect(wrapper.find('h1').isVisible()).toBe(false);
        });
      });
      
  • 异步测试

    • mock掉原有的网络请求模块:比如jest.mock('../../apis/user');

    • 然后 shallowMount 现有的组件,调用mock下的原本的网络请求函数

    • 返回预设好的值,判断是否通过用例

    • 示例代码:

    • // ./src/components/user-info/user-info.spec.js
      import { shallowMount } from '@vue/test-utils';
      import UserInfo from './user-info';
      import UserApi from '../../apis/user';
      
      // mock 掉 user 模块
      jest.mock('../../apis/user');
      
      // 指定 getUserInfo 方法返回假数据
      UserApi.getUserInfo.mockResolvedValue({
        name: 'olive',
        desc: 'software engineer',
      });
      
      describe('<user-info/>', () => {
        const wrapper = shallowMount(UserInfo);
        test('getUserInfo 有且只 call 了一次', () => {
          expect(UserApi.getUserInfo.mock.calls.length).toBe(1);
        });
        it('用户信息渲染正确', () => {
          expect(wrapper.find('.name').text()).toEqual('olive');
          expect(wrapper.find('.desc').text()).toEqual('software engineer');
        });
      });
      
  • 快照测试

    • SnapShot

    • 第一次运行快照测试时会生成一个快照文件。

    • 之后每次执行测试的时候,会生成一个快照,然后对比最初生成的快照文件:

      • 如果没有发生改变,则通过测试。
      • 否则测试不通过,同时会输出结果,对比不匹配的地方。
    • Test Suites: 1 passed, 1 total
      Tests:       2 passed, 2 total
      Snapshots:   2 passed, 2 total
      Time:        1s
      Ran all test suites.
      Done in 1.02s.
      

# 5.1.3 遇到的困难

  • 主要还是因为项目比较小,不能够很好地体现使用工具进行自动化测试的优秀之处。
  • 当然,实践之后总还是有一些体会:一旦项目比较大了,就没办法只是通过人工测试、或者手工输入一个一个的用例

# 5.1.3 代码覆盖率

  • 概念:简单来说,就是 被测试执行的代码的数量 / 代码总数量
  • 对于覆盖率的思考:
    • 是一个辩证看待的指标:
      • 代码覆盖并不等于测试质量、代码质量,甚至有可能测试用例本身就是错误的
      • 但是反过来,覆盖率低,代码质量可能就不一定会很好。
    • 不必强求100%覆盖率:
      • 有个边际效用递减问题,为了追求数字上的完美,你可能要付出过多的代价,得不偿失,还不如省下点力气花在其它地方。
      • 达到固然可嘉,但是随着项目的增大,达到100%覆盖的代价很大,有时候项目需要尽快上线盈利,可能就没有足够的时间测试所有的模块,只能先测试最核心、最重要的部分。(主要的业务功能要能够正常执行)
    • 当然这里只是很笼统地说覆盖,详细的也分为:语句覆盖、判定覆盖、条件覆盖、路径覆盖

# 5.1.4 个人的思考

  • 前言:其实最开始是打算做性能测试的,结果学习到后面这个测试工具,更像是专门用来做单元测试、集成测试的,主要是来测试功能而不是性能。
  • 很像是灰盒测试,其核心是通关断言的方式来测试输入输出是否正确匹配,即是否完成了指定的功能。同时,生成的覆盖率报告又涉及到了白盒测试的思想,它会检查有哪些分支覆盖了。
  • 为什么要做测试?因为现学前端,短期内不光要掌握前端各方面的基础知识,同时如果还需要脱颖而出,就需要一些亮点,而我的想法是做一些折腾的事情、剑走偏锋,当然毕竟是现学,基础薄弱的这个弱点还是没办法掩盖的,但是我还是比较希望尽可能从这个项目中展现我的能力和思路。

# 5.2 性能测试

# 6. 优化

  • 其实最大的问题还是做的比较泛,都是浅尝辄止,没有针对哪一个方向深入下去。有自身原因,也有客观原因。
  • 或者说,策略是:先将基本的能够优化的部分做了,然后再把某个方向做到极致,就类似于,九门课,先都保证能够考及格,然后再考虑把某一门课加强,甚至是做到考满分。

# 6.1 性能优化

# 6.1.1 班车项目

优化
方法
打包
时间
打包
体积
DOM-Content-Loaded Load app.js chunk-vendors.js 初始
页面
登陆
下单
全优化 6.83s 928kB 0.97s 1.33s 305kB 1.4MB 2.5MB 4.2MB
无优化 7.54s 2.8MB 1.02s 1.45s 306kB 2.6MB 3.6MB 5.3MB
代码丑化 9.94s 1.0MB 0.97s 1.32s 396kB 2.5MB 3.5MB 5.1MB
cdn 5.07s 2.1MB 1.08s 1.34s 306kB 1.4MB 2.5MB 4.3MB
路由懒加载 8.99s 2.4MB 1.67s 2.01s 1.7MB 3.4MB 5.1MB 5.1MB
vant按需引入 6.23s 2.8MB 0.95s 1.38s 306kB 2.5MB 3.5MB 5.1MB
代码优化 7.48s 2.8MB 0.95s 1.38s 306kB 2.5MB 3.5MB 5.1MB
无图片懒加载* 9.95s 1.7MB 0.86s 2.41s 246kB 2.4MB 6.4MB N/A
图片懒加载* 9.93s 1.7MB 1.05s 2.04s 247kB 2.5MB 4.6MB N/A
  • 备注:
    • 代码层面的优化:
      • 冗余的div、无用的空格、换行符、注释。
      • 引入但未被使用的组件、变量
      • 文件命名规范、语义化
      • 代码风格:分号、驼峰变量命名、缩进
    • 概念:
      • chunk-venders.js是引用的第三方库的代码
      • app.js是开发者自己写的代码
  • 疑问:
    • 丑化后的dist只有1.0MB,但是初始页面仍然是3.5MB的大小
    • uglify没有完善的程序流分析。它可以简单的判断变量后续是否被引用、修改,但是不能判断一个变量完整的修改过程,不知道它是否已经指向了外部变量,所以很多有可能会产生副作用的代码,都只能保守的不删除。

# 6.1.2 内存泄漏检查

  • 理论上,内存泄漏的原因:

    • 未被销毁的DOM事件监听
    • 全局变量
    • 脱离DOM的引用
    • 闭包
    • 计时器或者回调函数
  • 时间线测试:

  • 初步测试(5分钟,切换了两次账号)

image

  • 再次测试(9分钟,切换了五次账号)

image

  • 快照测试

  • 概览

image

  • 看得出来似乎还是有一些内存泄漏的,打开比较模式,并且意外在每个快照对比上一个快照的时候,都产生了新的闭包

image

image

image

image

image

  • 查看闭包之前,先看看这个占用新增的内存的大头array里面都有什么

image

接着,再来仔细看看闭包的内容

image

image

image

  • 关闭比较模式,来看一下快照本身的元素

image

# 6.2 用户友好性

  • Toast 淡入淡出的效果
  • Loading加载组件
  • 骨架图组件

# 7. 还可以改进的地方

# 7.1 用户友好性

  • 下拉的时候「加载中」的提示,悬停半秒钟,然后显示加载成功。而不是直接就滑回上面去了。
  • toast的淡入淡出效果
  • 及时的反馈:登陆/注册 成功/失败的时候给一个toast提示
  • keep-alive只能保证不重新加载,但是如果一个页面比较长,还需要在 actived / deactivated 的钩子函数里面保存scroll的位置

# 7.2 文档

  • 开发模式基本上算是瀑布模型
  • 印象里这种模型是文档驱动的,需要保证每一步都稳健,防止在后期出现严重的需求错误/设计错误。

# 7.4 安全

  • XSS攻击,并没有在提交的时候对于提交的内容进行审查,查看是否包含了字符串形式的脚本文件。
  • CSRF攻击,虽然是服务器,但是没有做 - -

# 8. 插件

# 8.1 BetterScroll

# 8.2 FastClick