# Project
# 1. 项目概览
# 1.1 组件架构
# 1.2 组件逻辑
# 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分钟,切换了两次账号)
- 再次测试(9分钟,切换了五次账号)
快照测试
概览
- 看得出来似乎还是有一些内存泄漏的,打开比较模式,并且意外在每个快照对比上一个快照的时候,都产生了新的闭包
- 查看闭包之前,先看看这个占用新增的内存的大头array里面都有什么
接着,再来仔细看看闭包的内容
- 关闭比较模式,来看一下快照本身的元素
# 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
← Interviews Vue →