类型定义即是量纲

2019 年阅读的前端项目终于吃上了 TypeScript,群众纷纷反映真香!

我在思考,类型定义对于程序语言来说究竟算是什么呢?我们大家都知道编译型语言类型检查不通过是会编译失败的,返回一个错误信息,那么我们为什么要保证变量类型呢?
我不想先谈那些内存结构啊,指针之类的,我想说的是类型定义就好比物理学里的“量纲”。

我们会说 1 立方米的木头,1 吨的木头,都是可以的。但我们不会说 1 安培的木头,或者 1 摄氏度的木头。因为这些量纲没法衡量木头的多少,或者说即使确实有 1 安培电流通过的木头,表面温度为 1 摄氏度的木头,我们也没法了解它到底意味着什么(能换多少钱,需要多少辆车才能拉动?)。

计算机程序是简单的,每个变量,每个函数,他们都只会做两件事,那就是被写入内存和从内存读取,量纲是他们在内存里分配的形式,u32i64 就是不同的变量,长得不一样,功能也不一样。

JavaScript 是弱类型的语言,它容忍隐式类型转换。但 TypeScript 不允许这样做。我们不能把 string 类型的变量传给一个接受 number 类型参数的函数,就好像我们没法把木头接到灯泡上指望它发光一样(好吧,如果这是一段带点的木头就当我在放屁),尤其是你接到的还是一段看起来很像电池的木头,比如名为 battery,但类型是 wood

另外,保证量纲被定义的好处是我们拥有了更好的接口提示,即使之前就有 document comment,但我们依然只能通过 description 来辨识变量类型和猜测该用的参数,有 TypeScript 以后 ……

https://juejin.im/post/5d8efeace51d45782b0c1bd6

最后,我想说的是,即使 TypeScript 给开发过程带来了巨大的便利,但我们仍应该清楚地认识到它只是一门 JavaScript 的预处理语言,运行时仍是那个不带类型检查的玩具语言(?),当 API 接口返回的数据不同于类型定义的 Interface 怎么办?

Runtime Type Check

一种方案是 WebAssembly,在 Rust 代码里用 Serde 之类的序列化工具来整理 JSON 数据,如果遇到不存在的属性会直接报错

另一种就是 JS 方式:invariant

1
2
3
4
5
6
7
var invariant = require('invariant');

invariant(someTruthyVal, 'This will not throw');
// No errors

invariant(someFalseyVal, 'This will throw an error with this message');
// Error: Invariant Violation: This will throw an error with this message

前端文件打包优化

前端开发除了 HTML 模板外最重要的就是 JS/CSS 文件了,现今开发者都是本地书写 ES6/Stylus/Sass 然后经打包发布至 CDN 等环境,于此带来的问题是一些不需要的代码被打包了进来,甚至更严重的是一份代码被打包几遍。当然,Webpack 这样的工具出现就是为了解决这些问题,不过考虑到打包过程是一次性的编译,运行时代码的区别仍然需要开发者手动对待。

JavaScript

1. 重复打包了类库文件

backbone、underscore 被同时打包进了 vendor 和 setup 入口文件

上图中可以看到 vendor 和 setup 里都有 backbone、underscore 和 zepto 等库文件,这是为什么呢?

原因是当我们引入 splitChunksPlugin 时仅将 /node_modules/ 下的文件纳入 vendor 范围,但在 entry 处又定义了包含通过 bower 安装的 backbone、underscore 等类库的 vendor,见下面两图。

mobile config 中定义了 vendor

base config 中定义了 cacheGroup.vendor.test 为 /node_modules/

如图所示将 /public/js/lib 写入 optimization.splitChunks.cacheGroups.vendor.test 就可以了,结果如下图。

backbone、underscore 等库乖乖地呆在了 vendor 里面

2. 类库文件无法自动剔除无用代码

date-fnsRxJS 这样 battle tested 的库,从早期原型链(prototype)实现到现在拥抱函数式的历史进程中无可避免地引入了很多历史负担,比如下图中 date-fns/esm 就非常巨大,里面很多都是我们暂时不需要的功能。

date-fns/esm

好在它们文档都比较全:

如下图一顿修改。
import function from 'date-fns/function' directly

见证奇迹的时刻!
date-fns/esm mini

3. 运行时才能确认使用的重依赖模块

membership app 内出现了 jQuery 等依赖

一个 React App 内竟然出现了 jQuery 依赖 …… 一定是哪里出了问题,经过不懈的努力,终于找到了是在 web 和 mobile 公用的组件里用了微信支付的 module,而这个 module 开头就直接引入了 backbone/underscore/zepto 三大金刚 ……

weixin_wap_payment 依赖了 backbone 等库

Webpack 编译时并不能知道运行时究竟在不在手机环境,怎么办呢?我们可以通过 webpack require AMD 模式 来拆分代码。

AMD require weixin_wap_payment

其实这个打出来的 34 号包永远也不会被 import ……
jQuery/backbone/underscore 等都被打包出去了

CSS

1. 重复 CSS Variables 定义

开发小王接到了一个任务,改进项目 css 代码以支持当下新出的黑夜模式(Dare Mode),小王犯了愁,一个个颜色变量替换也太苦逼了,小王挠挠头想出了一个在 source 文件定义 CSS Variables 的方法。

duplicate CSS Variables

看起来非常完美,但不足之处是这堆 CSS Variables 每次 import 都会被定义一遍,结果就是有多少 stylus 文件就被重复定义了多少遍 …… 不要问我为什么要用 import 而不是 require ……

我们知道 Stylus 是一种 css 的预编译器,它的变量和 CSS Variables 是不一样的,CSS Variables 是会编译到生成的 CSS 文件里,而 Stylus 变量则会在编译中承担一次桥梁的作用之后悄悄消失。可以通过比对 Stylus 变量是否已经赋值 var(--css-variable) 来判断是否需要定义 CSS Variables。

is css variables defined?

成果就是从有多少 stylus 文件就有多少次 CSS Variables 定义缩减到入口文件那么多个数的定义,足足降低了一个数量级,打开 Chrome DevTools > Elements > Styles 终于不卡了!

注意观察右侧滚动条的宽度!

排序

算法中的排序算是老生常谈的问题了,不过我的问题是生活中真的有那么简单只需要按照一个值从小到大排列的需求吗?

如下图
web
在 App 端变成了这样
App

显然这些已注销用户是在不同时间关注的,web 上将他们混排了,App 上则是单列在最后,前一种强调了关注顺序,后一种强调了社交性(注销账号当然再也没有了社交价值)。并且 App 理论上不太会有用户真的去翻到最后一条,Elastic Search 默认最大条数是 10000,恐怕意义也是如此。

当然排序在程序上而言也就一行代码的区别,不过在产品上区别可是很大,是在从一个笔记本在向电话簿转变。

这一切是否预示着我们每个人之间的关系都是从路人成为朋友,然而最终将深埋通讯录底不见天日,直到天人永隔。

谁都只能陪谁走一段路,但这条路毕竟是要靠自己走完。

行色秋将晚,交情老更亲。