前端框架的工程化之路

34次阅读

共计 5172 个字符,预计需要花费 13 分钟才能阅读完成。

前端框架工程化之路
人类的发展动力源于一个“懒”字,就如现在的大前端正是史前那群“懒”而聪明的“切图仔”进了软件工程的施工现场,怀揣着更少代码、更少沟通、更少错误、更少维护的梦想奔袭而来。从框架齐放闹革命到三大框架三足鼎立,从构建工具争鸣到 webpack 一统江湖,从 Javascript 遵循 ES5 长达 7 年统治到向 ES6 的自我进化。前端的发展与它们的成功都离不开一个“术”,工程化。
模块的进化
在没有框架的史前
我们面临的问题:1. 全局变量污染:各个文件的变量都是挂载到 window 对象上,污染全局变量。2. 变量重名:不同文件中的变量如果重名,后面的会覆盖前面的,造成程序运行错误。3. 文件依赖顺序:多个文件之间存在依赖关系,需要保证一定加载顺序问题严重。于是老王想出使用自执行函数的方法去解决问题
var foo = (function(cNum){
var aStr = ‘aa’;
var aNum = cNum + 1;
return {
aStr: aStr,
aNum: aNum
};
})(cNum);

前端最初始的模块诞生了,这个模块有问题吗?有!虽然模块内部的变量对全局不可见了,但暴露出来的 foo 是一个全局变量,这样的模块多了全局变量也会很多。
老李在老王的办法基础上添加命名空间去解决问题:
app.util.modA = xxx;
app.common.modA = xxx;
app.tools.modA.format = xxx;
除了写法丑陋外,这样的模块约束力极低,很容易遭到不遵守的开发者破坏,需要开发者有一定的划分不同模块的能力,更大的问题是需要人为的解决模块加载、初始化等管理问题。
框架加冕时代
2009 年横空出世的前端框架 angular 的模块机制
angular.module(‘ConfigModule’).service(“TextConfig”, function () {
this.headerText = {
};
});

angular.module(‘HeaderModule’, [‘ConfigModule’]).controller(‘HeaderCtr’, [‘$scope’, ‘TextConfig’, function ($scope, textConfig) {
$scope.headerText = textConfig.headerText;
}]);

Angularjs 的模块机制相比老王、老李的解决方案上增强了模块的约束性,和帮助开发者划分模块外,最重要的是解决了模块的运行时管理问题(模块的初始化顺序问题和依赖的模块自动初始化问题。再被多个模块依赖的情况,模块仅且只加载一次的问题、统一的输入输出 api 问题)。看似完美的方案,但仍有问题。
构建工具辅政
angularjs 的模块机制只解决了运行时管理问题,但没有解决加载管理问题。这让使用者不得不去链式在页面引用模块文件。所以在那个时候出现了一些构建工具与相应的插件来帮助我们 比如 gulp、grunt、插件 browserify。实际上 angularjs 的模块机制也只是一定程度的解决了运行时管理问题,了解的同学应该知道在 angularjs 里做模块异步懒加载是非常困难的。在 angular 2 及以上版本加入了动态加载模块的支持,其它框架,例如 vue 组件(这里暂且把 vue 的组件当作模块看待,后面会进行区分)也加了相应的支持,这得益于框架的组件或模块的 factor 机制的支持和 webpack code splitting 功能特性的支持。
const Foo = resolve => {
require.ensure([‘./Foo.vue’], () => {
resolve(require(‘./Foo.vue’))
})
}

const router = new VueRouter({
routes: [
{path: ‘/foo’, component: Foo}
]
})
一直在接近,但从未实现的组件化
前面谈到模块化发展中谈到了 vue 的组件,组件是一种模块,但又超越模块。模块是逻辑单元的封装,让开发及维护成本更低。那么组件则是更高一层的抽象,是一个业务单元的封装,能够独立运行的软件单元。组件需要解决的问题:隔离、去污染问题(模块解决的隔离只限于 js 变量的隔离、而组件还需要解决 css 的隔离)、状态管理问题、与其它组件通讯问题,生命周期问题,一个好的组件设计还需要遵循软件设计的一些原则。
不得不改源码的 jquery 组件
我们先来看看 jquery 的年代组件长什么样?以前的代码一般是用自执行函数作为一个类,这里为方便理解,我用 ts 展示一下。
export class component {
selector:Element;
options:any
static defaultOption = {
‘color’: ‘red’,
‘fontSize’: ’12px’,
‘textDecoration’:’none’}
constructor(selector,opt) {
this.selector = selector;
this.options = $.extend({}, this.defaultOption, opt)
}
beautify () {
return this. selector.css({
‘color’: this.options.color,
‘fontSize’: this.options.fontSize,
‘textDecoration’: this.options.textDecoration
});
}
}

组件使用者拿到这个组件并初始化,根据组件上层的一些交互,调用组件方法,改动组件内部。我们可以看到组件上层依赖这个组件,且依赖的是 beautify()的具体实现。根据 OCP 原则,对扩展开放,对修改关闭。当我们需求变化时,我们的 beautify 需要把背景色改一下,只能对组件内部逻辑做修改。很显然这样的组件不是一个好设计。
数据驱动让组件高可用性
进入有前端框架的时代,angular 使用数据驱动改变视图的状态,这是很大的一个进步,数据驱动解耦了组件外层对组件的依赖关系,将真正的依赖抛向外层的传给组件的数据(有点类似依赖倒置的意思),组件内部负责根据数据的变化改变 UI 状态。(React Virtual DOM 驱动视图实际也是一种数据驱动,只是一个是找到数据最小粒度的变化直接改动对应的视图,一个是数据生成 Virtual DOM 找到最小粒度的 Virtual DOM 变化,改动对应的视图。本质上都是数据驱动视图)。数据驱动视图解耦了组件与组件的依赖问题。但同时引入了一个问题,状态混乱问题。写过 angularjs 的同学应该知道状态混乱之痛,当我们在 angularjs 的多个组件依赖同一份数据时,当一个组件树中某一个组件将该数据更改时,整颗组件树中使用该数据的组件都会跟着共振。但实际情况是,有树当中有一部分组件不需要跟着某一次的该数据变化,引起自身的变化。

状态管理让个体拒绝骚扰
react、angular 使用 immutablejs 强化单向数据流。这的确减轻了复杂度,但这种方式对于子组件想通过状态变更驱动父组件、兄弟组件变化的情况,只能通过注册事件通知的形式。首先这种形式会违背隔离性,有很高的耦合,组件内部必须知道外部想要知道我会有什么变化,预留订阅的钩子。其次对于一颗组件树跨了 N 层,极端点从叶子节点到根节点这样一个通知在每层订阅子组件事件,会显得非常不合理。于是衍生的一些 flux redux 库的状态集中式托管,让一个组件的数据驱动视图的变化,可以来源于任何一个组件树节点,又不会让变化成复杂的网状拓扑结构,而是成星型拓扑结构。这个发展过程实际也是为了解耦合,让组件更加独立,就好似以前一个人的每一个日常活动都推送出去让全人类知道,由于日常太过苦逼影起社会负面情绪暴增(无单向数据流的情况)。影响太大了。于是乎改为在推送前,大家先订阅建立关系,建立关系则推送,大家发现这套太麻烦了,需要个体知道别人关心我哪个日常活动,家人想知道我吃的什么我建立一个晒图发布方式,领导想知道工作情况,我建立一个写周报发布方式,于是乎我不停的改变自己适应社会(这就是一种耦合,单向数据流方式)。当个体丧失人性后,终于想到了一个更好的办法,做自己该做的事,把这种拔内裤的事情交给社会,我在办公室你就给我采集认真工作的照片,系统让领导看还是让其它同事看我不关心。我在吃大餐的时候你采集照片,系统要给哪些联系人,这由我此刻处的环境下社会关系决定。
组件进化之殇
组件的进化从未停下脚步,例如 css 隔离问题从依靠项目 wiki 中制订 css 命名规范到 css Modules 自动化解决 css 隔离问题。从 angular1 的混乱的网状状态管理(状态管理补充 1)到 react、angular 使用 immutablejs 强化单向数据流、和衍生的一些 flux redux 库的状态集中式托管。从 vue 的简单生命周期到 2.0 加入 keep-alive、activated、deactivated 使生命周期的增强。组件的发展一片欣欣向荣,但为什么我仍认为还没有实现组件化呢?
框架的出现让组件拥有了其应该有的特性,让开发者无需再重复造轮子解决这些问题。但也引入了新的问题,组件的独立性,前面提到组件应当是一个独立运行的软件单元。而实际的情况是,组件只是在某框架体系下独立运行的软件单元。而工程化也是一个去底层服务的趋势,我们可以看看近年的 docker 技术、云服务的 serverless 概念,都强调其无需关注底层执行环境,想象一下如果某天我们开发一个页面无论采用任何技术架构与框架,都只需引入一个个 Custom Element,把 dom attribute 作为 API(或者拿着各团队发布的在线运行的一个组件地址),去组装页面即可。这就是近几年前端的一个研究课题微前端技术。目前的微前端技术也有不错的发展,利用 Custom Element 的实现方案,解决了一些基本的问题,如前面提到的:隔离性、状态管理、通讯问题、生命周期问题、不依赖前端架构体系问题。但作为能独立服务端部署提供使用的一个 component 还有很多问题待解决,但这是一个组件化发展的方向。
这是一个不错的微前端实现方案 https://micro-frontends.org/
工程化的乌托邦——规范化
我们前面已经提到过一些古老“法典”,如 jquery 时代模块定义的规则、css 规范的规则,还有前面没提到的项目结构划分、代码书写规范。大家有没有发现它们都在历史的舞台中消失或者说们不必在为规范的实现耗费精力。为什么?当一个“法典”的受管控者和执法者都是自身的时候,那法典也就成了空谈。所以我们需要一个公正的执法者——机器(自动化)。
终会让项目 wiki 消失的自动化
代码规范,我们拥有 jslint 帮我们校验,有编辑器插件帮我们根据规范自动格式化。项目结构,我们有对应的 CLI 帮我们生成。模块的定义,我们有框架帮助划分解决运行时问题,有 webpack 帮助解决加载问题,等等。自动化并非真正让规范消失,而是对规范的更加强制化和易实施化,达到“无约”自制的效果。那些 wiki 里规范让程序要怎么怎么写代码的文章我觉得大可不必,话说:“能动手的不 BB,能自动化的不文档”。而对于项目 wiki 里那些让程序如何与其它人合作写代码的文档我也觉得大可不必。比如:我们与后端如何对接,我们可以使用 YAPI 这类工具,让前后端对接口定义及数据结构一目了然和保持实时稳定性。对于前端与前端之间如何互相调用模块或组件,我们可以利用 typescript,让模块组件接口更加清晰和强类型带来的稳定性。对于强类型带来的好处我想举个例子:这个是我写无 ts 的 vue 项目时一个很低级的 bug
export default {
props: {},
data() {
return {
isFullcreen: false;
}
},
methods: {
toggle() {
this.isFullCreen = true;
}
}
}

看似是个大小写问题,但完全是可以避免,如果这个组件是个强类型,IDE(支持 ts 的)会推断 this 类型,该字段是否声明过给予校验提示。这个是书写上的带来的好处。强类型还给我们带来很多好处与方便,比如可以很快的了解一个模块提供的 API,可以在多模块引用同一个数据,在某个时期对该数据结构进行一定调整后,能立刻知道那些陈旧的代码哪些需要随着这次改动一起调整等等。
除此之外 TS 还能在自动化文档上起到辅助作用。我们可以看一下 Angular 的自动生成的文档。https://compodoc.github.io/co… demo: https://compodoc.github.io/co…
未完待续 …
当我们站在巨人的肩膀上时,从未觉得向前走一步是如此轻松 … 愿,未来的前端走得更轻松。

正文完
 0