再谈业务逻辑

我面试过一些岗位,也面试过很多人,基础的岗位问的最多的大概就是对框架的理解,对语言基础的掌握等等,但如果是高级岗位往往问到对市场大环境的感悟和对业务逻辑的理解,体现了工具的应用必然是为完成目的服务的标准。

实际开发中,由于前后端现在工种的分离,业务逻辑的归属是很大的问题。

后端面向数据库和第三方服务集成,数据库总是以最小业务模型为基准,考虑扩展性进行建模,而第三方服务与领域模型融合则需要对业务逻辑进行抽象化,比如支付接口需要考虑本身提供的商品种类和第三方支付的种类进行分别编码并抽取公共逻辑,常见的形式有 Adapter(适配器模式)等。

前端当下也很复杂,虽然有了一些基础组件,但随着业务的深入,组件上难免多出很多随着各种特殊使用场景分别配置的参数,甚至一个组件接受几十个参数来配置,可能也有同学一看这不行啊,感觉封装了几个配置好的组件供业务使用,但上面的过程多了以后,组件变得又深又长,深体现在封装层级上,长体现在单个组件的长度上,比如封装一个电子书的展示列表。

图片 -> 封面 -> 带标签的封面 -> 电子书 -> 书籍列表 -> 可无限加载的书籍列表

这时如果要把「图片的尺寸」或者「可点击与否」传递给最内部的组件,就需要一层层从上面传下来,React 有 context 方法来在组件间传递参数,但这毕竟是不可见的,需要自己去 useContext 来得到。ES6 中提供了 Reflect 对象,比如 Reflect.getMetadata('design:paramtypes', target) 可以得到在参数里定义的参数类型,Angular 的控制反转和依赖注入就是依靠实现的。这为我们提供了一些可以想象的思路,当然本质上它只能解决编码上的便利性,组件的层级和复杂度并没有因此降下来。

当前前后端通讯主要靠两种方式

  1. 首屏页面内 script 注入初始数据
  2. AJAX 接口

阅读站新的接口优先考虑 GraphQLGraphQL 是一种面向领域模型的非常好的查询语言,我们不再需要多次往返前后端通讯获得更多数据,或者是重复定义各种 build_xxx_entity,只需要一次定义,任何用到单个字段的地方都只用写字段名就可以使用,可以说极大地降低了边际成本,而且越用收益越高。当然有同学会问那如果大批量查询的问题呢?Facebook 给出了 DataLoader 的方案,但我们的 peewee 版本以及早期的 hack 写法限制了我们真正将 ORM <==> GraphQL 的过程,现在其实是手动 ORM <==> 字段,带缓存的字段 <==> GraphQL

当后端和接口都做到了尽量少耦合业务逻辑时,业务逻辑就大量堆积在表单 POST 接口和前端展示逻辑上,又因为表单校验因 js 和 python 没法互通,需要前后端分别实现,而展示逻辑则是大多硬编码在组件内。这点其实很吓人。表单方面有 JSON Schema 的方案,但因为 Python 这种胶水语言快读开发的节奏每每难以落地,只是可惜展示上没法脱离业务组件进行编码,之前有过在 App 上设计根据 type 来组合 widget 的展示形式,但一旦需求变动代码就变得非常臃肿。

一种现有的整理业务逻辑的形式是把 Business Logic 存到数据库中 …… 简单来说,如果数据库可以方便地表达因果关系,或许这是一种可行的方案。还有就是很通用的配置文件方式,也是我们搭建开发环境的方式,通过脚本配置在环境初始化时载入一系列的变量最终构建出符合业务需要的开发环境。

理想的情形可能是,前端只提供组件库和组件间的组合逻辑,后端只提供数据的存储和加工工具,业务逻辑由第三方提供配置,设置极限值和用户可选值,用户可以在享有最多自主权的情形下使用 web 产品,当然这也只是一种美好的愿望……毕竟当下都是想把产品做到尽善尽美,只给用户一种最好的选择,可这是不是用户想要的呢?做得越多也许越错呢?没有人真的会去想,大家都是假装想想罢了。

类型定义即是量纲

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 终于不卡了!

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