背景
Javascript 是一门弱类型语言。与 Java、C++ 等强类型语言不同。Javascript 通过 var 关键字声明变量(ECMAScript 6 开始允许使用 let / const 声明变量),系统会在赋值语句中自动判断其数据类型。
优点:
书写简洁灵活
var a = 1;
var b = 'hello';
缺点:
类型不可预测,易造成程序运行结果不符合预期
var a = 2;
var b = '1';
var plusResult = plus(a, b);
var minusResult = minus(a, b);
console.log('相加结果: ', plusResult); // 相加结果: 21
console.log('相减结果: ', minusResult); // 相减结果: 1
// 相加方法
function plus (a, b) {
return a + b;
}
// 相减方法
function minus (a, b) {
return a - b;
}
解决方案
静态类型检查
大多数时候,类型产生的错误是由开发人员编码不规范造成的。这些问题通常可以在编码阶段解决(非运行时)。可以使用的工具有:Facebook 出品的 flow
、Microsoft 出品的 Typescript
。
示例
TypeScript 是 JavaScript 的强类型版本。在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。TypeScript 是 JavaScript 的超集,这意味着它支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。
- 安装
Typescript
编译器
npm install -g typescript
- 新建
demo.ts
var a: number = 2;
var b: string = '1';
var plusResult:number = plus(a, b);
var minusResult:number = minus(a, b);
console.log('相加结果: ', plusResult); // 相加结果: 21
console.log('相减结果: ', minusResult); // 相减结果: 1
// 相加方法
function plus (a: number, b: number): number {
return a + b;
}
// 相减方法
function minus (a: number, b: number): number {
return a - b;
}
- 编译
ts
文件
$> tsc demo.ts
$> demo(4,33): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
demo(5,35): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
编译过程中抛出了错误警示,原因是:
在声明 plus
、minus
函数时,对参数进行了类型标注,且都是 number
类型。在调用函数 plus
时,变量 b
作为第二个参数传入。由于变量 b
在声明时被标注为 string
类型,编译器判定此处代码有误,并在控制台输出错误警示。
尽管使用 Typescript
可以避免大部分弱类型带来的副作用。但仍有一部分场景 Typescript
是无法解决的
场景复现
小明是公司的前端开发人员,小王是后端开发人员。
他们使用了目前流行的前后端分离模式,独立开发各自模块,通过协商通信协议,来完成前后端数据交互,输出产品。
接口协议 get_company_info
- 基本信息
接口名称:获取公司信息
接口路径:GET
https://api.my-service.io/get_company_info/
- 请求参数
| 参数名称 | 是否必须 | 示例 | 备注 | | - | - | - | - | | sid | 是 | xxxxx | 用户登录态 |
- 返回数据
{
"data": {
"company_id": 1234,
"company_name": "xxx有限公司",
"person": [
{
"name": "张三",
"age": 24
},
{
"name": "李四",
"age": 22
},
{
"name": "王五",
"age": 26
}
]
},
"errcode": 0,
"msg": "success"
}
协议达成一致后,小明使用 Typescript
声明了接口类型:
interface Person {
name: string,
age: number
}
interface Company {
company_id: number,
company_name: string,
person: Array<Person>
}
获取数据渲染视图
// 代码片段
const companyInfo: Company = await Service.getCompanyInfo();
const person = companyInfo.person
renderPerson(person)
一切都很完美,直到有一天...
服务器出现了点异常,返回的数据变成了这样:
{
"data": {
"company_id": 1234,
"company_name": "xxx有限公司",
"person": null
},
"errcode": 0,
"msg": "success"
}
小明的前端界面瞬间挂掉了,打开调试器一看控制台:
VM218:1 Uncaught TypeError: Cannot read property 'person' of null
at <anonymous>:1:3
小明傻了眼,原来遵守契约有时候也不靠谱啊!
之后,小明吸取了教训,把代码做了改进:
// 代码片段
const companyInfo: Company = await Service.getCompanyInfo();
const person: Person = companyInfo.person || [];
renderPerson(person);
之后基本上就没有出现什么问题了。
直到有一天,协议中 person
新增了一个字段 device
:
// 公用设备
interface Device {
id: number,
name: string,
sn_code: string
}
interface Person {
name: string,
age: number,
device: Array<Device>
}
小明想:为了避免上次的坑,得做好容错处理,契约不一定靠得住,于是改了下代码:
// 代码片段
const companyInfo: Company = await Service.getCompanyInfo();
const person: Person = companyInfo.person || [];
person.forEach(p => {
if (isPlainObject(p)) {
p.device = p.device || [];
}
})
renderPerson(person);
就这样,又过了段时间...
代码变成了这样
// 代码片段
const companyInfo: Company = await Service.getCompanyInfo();
const person: Person = companyInfo.person || [];
person.forEach(p => {
if (isPlainObject(p)) {
p.device = p.device || [];
p.device.forEach(i => {
i.xxx = xxx || [];
// ...
})
}
})
renderPerson(person)
最终小明因代码不可维护崩溃住院,同时小王被打住院。
复盘反思
大部分前端应用并不是纯粹的离线系统,而是需要频繁地与其它模块(角色)进行数据交互。使用 Typescript
固然可以避开同一模块中的数据弱类型问题,涉及到与未知系统的数据交互,只能依靠契约。一旦契约被破坏,运行时没有做相应的容错处理,同样会造成不符预期的结果。
小明在第一次踩坑后,明白完全依赖契约是靠不住的,蝴蝶效应的放大,对整个系统是破坏性的。之后便在运行时做了容错处理。随着项目迭代,协议变得越来越复杂,最终导致容错代码过多,代码可读性变差。
动态类型增强
在运行时处理类型,能够让程序更加稳定,随之带来的是性能的下降,代码维护性易变差等问题。
let id = sth.id
let list = sth.list
const result1 = await fetch('/api/foo', {
id: id && !isNaN(Number(id)) && Number(id) || DEFAULT_ID,
list: list || []
})
const params = result1.data
const result2 = await fetch('/api/bar', {
id: params && params.id && !isNaN(Number(params.id)) && Number(params.id) || DEFAULT_ID,
})
为了解决可维护性的问题,我写了一套运行时的类型系统。
源码地址:https://github.com/dlhandsome/return-correct-data-model
使用方法很简单:
- 安装
rcdm
模块
npm i rcdm --save
- 使用
import { defineModel } from 'rcdm'
// define your own models
const people = defineModel({
name: String,
age: { type: Number, default: 18 }
})
const company = defineModel({
list: { type: Array, extend: people }
})
// return the data you expected
const p0 = people() // => { name: '', age: 18 }
const p1 = people({ name: 'Tony', age: '19' }) // => { name: 'Tony', age: 19 }
const p2 = people({ name: 'Tom', age: '20a' }) // => { name: 'Tom', age: 18 }
const c0 = company() // => { list: [] }
const c1 = company({ list: [{}] }) // => { list: [{ name: '', age: 18 }] }
const c2 = company({ list: [{ name: 'Tony', age: '19' }] }) // => { list: [{ name: 'Tony', age: 19 }] }
终极方案
静态类型检查 + 动态类型增强
使用 Typescript
做为开发语言,编译时做静态类型检查。对于外部契约式协议,可以通过 rcdm
模块初始化协议数据,得到预期的数据格式与类型。