编程中的序

Table of Contents

架构1

该章节内容均来自百度百科词条。

架构,又名 软件架构 (Software architecture),是有关软件整体结构与组件的抽象描述,用于指导大型软件系统各个方面的设计。架构描述语言用于表述软件的体系架构,如 ADL ,C2 等,ADL 的基本构成包括组件、连接器和配置。

软件架构,是一个系统的草图,描述的对象是直接构成系统的抽象组件,各个组件之间的连接则明确和相对细致地描述组件之间的通讯。在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口(计算机科学)来实现。软件体系结构是构建计算机软件实践的基础。与建筑师设定建筑项目的设计原则和目标,作为绘图员画图的基础一样,一个软件架构师或者系统架构师陈述软件构架以作为满足不同客户需求的实际系统设计方案的基础。

在“软件构架简介”中,David Garlan 和 Mary Shaw 认为软件构架是有关如下问题的设计层次:“在计算的算法和数据结构之外,设计并确定系统整体结构成为了新的问题。结构问题包括总体组织结构和全局控制结构;通信、同步和数据访问的协议;设计元素的功能分配;物理分布;设计元素的组成;定标与性能;备选设计的选择。”

软件架构的要素

一般而言,软件系统的架构有两个要素:

  1. 它是一个软件系统从整体到部分的最高层次的划分。一个系统通常是由元件组成的,而这些元件如何形成、相互之间如何发生作用,则是关于这个系统本身结构的重要信息。详细地说,就是要包括架构元件(Architecture Component)、联结器(Connector)、任务流(Task-flow)。所谓架构元素,也就是组成系统的核心“砖瓦”,而联结器则描述这些元件之间通讯的路径、通讯的机制、通讯的预期结果,任务流则描述系统如何使用这些元件和联结器完成某一项需求。
  2. 建造一个系统所作出的最高层次的、以后难以更改的,商业的和技术的决定,必须经过非常慎重的研究和考察。

软件架构的目标

正如同软件本身有其要到达的目标,软件架构设计要达到如下的目标:

  1. 可靠性(Reliable),软件系统对于用户的商业经营和管理来说极为重要,因此软件系统必须非常可靠;
  2. 安全性(Secure),软件系统所承担的交易的商业价值极高,系统的安全性非常重要;
  3. 可扩展性(Scalable),软件必须能够在用户的使用率、用户的数目增加很快的情况下,保持合理的性能。只有这样,才能适应用户的市场扩展的可能性;
  4. 可定制化(Customizable),同样的一套软件,可以根据客户群的不同和市场需求的变化进行调整;
  5. 可伸缩(Extensible),在新技术出现的时候,一个软件系统应当允许导入新技术,从而对现有系统进行功能和性能扩展;
  6. 可维护性(Maintainable),软件系统的维护包括两方面,一是排除现有的错误,二是将新的软件需求反映到现有系统中去。一个易于维护的系统可以有效地降低技术支持的花费;
  7. 客户体验(Customer Experience),软件系统必须易于使用;
  8. 市场时机(Time to Market),软件用户要面临同业竞争,软件提供商也要面临同业竞争,以最快的速度争夺市场先机非常重要。

前端架构2

什么是 架构 ?无非就是:分层模块化 + 通信方式。

浅谈通信方式:

  • 模块化的通信方式,无非是相互引入,如抽取了 common ,其他模块使用自然要引入这个 module ;
  • 组件化的通信方式,主流是隐式和路由。隐式的存在使解耦与灵活性大大降低,路由是主流;
  • 插件化的通信方式,不同插件本身就是不同的进程,因此通信方式偏向于 Binder 机制类似的进程间通信。

前端架构的演变3

DOM → MVC → MV* → MNV*

1. DOM

这个阶段,是原生操作 DOM 和 jQuery 等 DOM 操作封装库的时代,所有的效果都是直接操作 DOM (前端直接操作或者框架封装了操作 DOM 的过程)。这个阶段,业务逻辑通常比较简单,对于比较复杂的逻辑通常交给后端。

随着 AJAX 技术的盛行,SPA 应用开始被广泛应用。SPA 的引入将整个应用的内容都在一个页面中进行异步交互。如此以来,原有的 DOM 交互方式开发就显得不好管理,例如某 SPA 页面上交互和异步加载的内容很多,我们的做法是每一次请求渲染后做事件绑定,异步请求后再做另一部分事件绑定,以此类推。当所有异步页面全部调用完成,页面上的绑定将变得十分混乱,各种元素绑定,渲染后的视图内容逻辑不清,又不得不声明各种变量保存每次异步加载时返回的数据,因为页面交互需要对这些数据做操作,最后写完,项目代码就成了一锅粥。

2. MVC

这个阶段,并没有本质的差别,只不过将操作 DOM 的代码换了位置而已。

为了更方便的统一管理页面上的事件、数据和视图内容,就有了早期 MVC 的框架设计。MVC 可以认为是一种设计模式,其 基本思路 是将 DOM 交互的过程分为调用事件、获取数据、管理视图。即将之前所有的事件、数据、视图分别做统一管理,用 model 来存放数据请求和数据操作,view 来存放对视图的操作和改变,controller 用来管理各种事件绑定。

例如,SPA 中的每个异步页面可以看成是一个组件,之前的做法是每个组件独立完成自己的数据请求操作、渲染和数据绑定,到了 MVC 里面,所有的组件数据请求、渲染、数据绑定都到一个统一的 model、view、controller 注册管理,后面调用必须要通过统一的 model、view、controller 来调。通俗来说,就是组件交出自己控制权到统一的地方注册调用。

3. MVP

MVP 可以跟 MVC 对照起来看,其中的 P 代表 Presenter ,它与 Controller 有点相似。不容的是,在 MVC 中 V 会直接展现 M ,而在 MVP 中 V 会把所有的任务都委托给 P 。V 和 P 会互相持有 reference ,因此可以互相调用。

例如,我们可以在 MVC 代码上做一点改变,如下:

<div controller="Controller.vp" id="text">html</div>
var Controller = new Controller();
Controller['vp'] = new VP({
    $el: $('text'),
    click: function(e) {
        console.log(self.$el.html());
    },
    mouseenter: function(e) {
        console.debug(self.$el.html());
    },
    mouseleave: function(e) {
        console.info(self.$el.html());
    }
});

4. MV*

MV* 包括 react + 类 flux 架构,都将 DOM 做了一层抽象,并不是简单的封装。它们的理念是数据驱动 DOM ,同时借助一定的手段(如虚拟 DOM)将 DOM 操作最小化(DOM 操作非常昂贵)。

为什么 DOM 操作比较昂贵?一方面,DOM API 非常多;另一方面,DOM 操作通常伴随着浏览器的重排和重绘。

拿 react 来举例, react 的核心思路可以用下面的伪代码表示:

const oldTree = render(oldState, oldProps);
const newTree = render(newState, newProps);

const patches = diff(oldTree, newTree); // 比较,计算 patches

doPatch(patches);                       // 根据 patches 应用补丁,将 DOM 以最小化更新

可以看出 react 封装了 DOM 操作,将 DOM 创建(render)和 DOM 修改(doPatches)封装了起来,对开发者来说是透明的,而 jQuery 则只是将开发者从 DOM 复杂的 API 和浏览器兼容性泥潭中拉了出来,并没有将开发者彻底从 DOM 关注点中解放出来。

5. MNV*

MVN* 是什么? Model-Native-View-* ,后面则可以人为是 Virtual DOM 或 mvvm 中的 ViewModel ,或者是自己使用 Controller 来实现的调用方式。

目前许多大公司的核心 APP 都是基于这种混合式开发完成,如支付宝、微信、钉钉等。这种开发方式的好处在于结合了 H5 开发的速度和原生开发的性能的优势,它的原理很简单 – 在原生 APP 中嵌入 webview 并基于一定的协议,完成客户端和 H5 页面的双向通信。比如 flex , flex 可以将 flex 内部应用通过接口暴露给 window 对象,js 通过 window 对象访问 flex 中的方法,同时 flex 可以调用 window 上的方法,从而实现 flex 和 js 的双向绑定。 MVN* 的交互模式有点类似, H5 和 Native 直接的交互基于 JSBridge

前端开发的核心

技术架构在不断的演进,目录结构、代码层次、公共逻辑抽离都不一样,那么,架构发展中的不变量是什么?如何根据不变量来规划软件架构?要回答这个问题,首先要明白软件开发的核心关注点,前端开发的核心关注点有哪些呢?

1. 页面结构

如何划分结构,如何组织页面。前端发展前期,页面结构划分的方式就是 DIV ,前端通过将一个个组件通过 DIV 构建出来,复用性几乎不存在。随着技术进一步发展,组件的思想深入人心,人们通过组件描述应用的结构,将程序看做是一个个组件组成的,进而有了一大批关于组件化开发的最佳实践,其中 web-components 就是组件化思想的官方表现。

2. 页面样式

如何实现十几稿的效果。早起大家通过 css2 实现基本的网站布局。随着网站发展,交互越来越复杂,动画的要求越来越高。诸如 css3 , canvas 动画技术浮出水面,这些技术都是为了更好的实现网站的样式。为了提高书写效率,有了一批 css 预处理和后处理引擎。为了解决 css 全局覆盖的问题,又出现了 CSS Module 的方案。为了是组件和样式更便宜复用,又出现了 Style Component 技术等,不胜枚举。

3. 页面交互

如何完成产品的交互需求。网站早起页面的交互就是简单的登录和提交留言等最基础的功能。现在网页做的越来越像一个 App ,同一时刻,可能存在多种状态,复杂性可想而知。那么如何维持越来越复杂的状态呢?由于状态之间不是独立存在的,某些状态可以依赖于别的状态,各种状态交织在一起,令人烦扰。为了解决这个问题,出现了诸如 Redux 、Vuex 、Mobx 等数据管理软件,用来做状态管理。

前端工程4

本章节主要关于工程化、模块化、组件化等。

架构的目的是什么? 提升质量和效率 。应该怎么进行架构呢? 制定标准

架构是一个抽象的过程,它是架构师根据自己的经验对大量具体的业务项目进行分析,发现其中的规律,抽象出具体的规范,最终又应用到具体的业务项目中去。比如 MVVM 就是这一种规范。

要把跟业务无关的问题都在架构层面处理掉。如代码压缩、打包这种工程化的问题都要在架构层面统一解决,要做到业务的归业务,架构的归架构。

架构要考虑到可以方便团队成员提供和使用通用技术解决方案,比如分页组件。

架构设计的时候要考虑当前的主流技术跟自己业务系统的实际情况。因为前端正处在高速发展,各种新技术,工具,插件,框架层出不穷,要结合项目实际情况运用已经成熟的技术,避免跳坑。

那么,合理的架构应该是怎么样的呢?概括几点就是: 模块化,组件化,工程化,规范化

浅谈工程化

工程化就像是百叶箱一样,减少人的操作,把工作所需要的 工具 做到的标准化,工作的 流程 做到的标准化,同时把很多重复的工作交给代码来做,保证高质,标准统一。

工具,工程化包括哪些方面:

  • 模块化与组件化:NPM,ES6,Seajs,React/Angular/Vue ;
  • 代码版本管理:git ;
  • 代码风格管理:jscs,editorconfig ;
  • 代码编译:Babel,Less,Sass,Imgmin,CSSsrpit,inline-svg ;
  • 代码质量管理(QA):Eslint,Mocha ;
  • 代码构建:Webpack ;
  • 项目脚手架:Yeoman ;
  • 持续集成/持续交付/持续部署:Jenkins ;
  • 本地化与国际化。

进行工程化:

  • 在配置初始项目文件结构和基本文件依靠命令行(工具)自动生成;
  • 确定代码规范,缩进,换行,以及各种预编译工具 Less,Coffee,保证输出代码的标准一致;
  • 对 JS 文件是否规范化,进行单元测试,不用手动复制到 JsHint 上去检测,现在配置 Grunt 监听文件变动自动检验显示检验结果还可以通过配置构建工具自动刷新浏览器实现文件实时变动监听;
  • 以前压缩合并文件用手工复制到压缩工具然后复制到一个文件里面,现在配置一下 Grunt,Gulp 可以做到自动任务,实时编译,并且监测文件改变而做出响应;
  • 以前发布到服务器上,要手动使用 FTP 软件上传,现在也可以用工具自动打包上传。

浅谈模块化

通行的 JavaScript 模块规范主要有两种: CommonJS 和 AMD 。

模块化开发,一个模块就是一个实现特定功能的文件,有了模块就可以更方便地使用别人的代码,要用什么功能就加载什么模块。

模块化开发的好处:

  • 避免变量污染,命名冲突;
  • 提高代码复用率;
  • 提高可维护性;
  • 依赖关系管理。

那具体什么是 模块化 呢?如要写一个实现 A 功能的 JS 代码,这个功能在项目其他位置也需要用到,就可以把这个功能看成一个模块采用一定的方式进行模块化编写,既能实现复用,还可以分而治之。同理,如需要某种特殊的样式,会在很多地方应用,就可以采用一定的方式进行 CSS 的模块化。

具体来说,JavaScript 模块化方案很多,包括 AMD/CommonJS/UMD/ES6 Module 等;CSS 模块化开发大多是在 Less/Sass/Stylus 等预处理其的 import/mixin 特性支持下实现的。

总体而言,模块化不难理解,重点是要学习相关的技术并且灵活运用。

CommonJS

JavaScript 在 web 端发展这么多年,第一个流行的模块化规范却是由服务器端的 JavaScript 应用带来的, CommonJS 规范是由 NodeJS 发扬光大,标志着 JavaScript 模块化编程正式登上舞台。

为什么 JavaScript 不是首先在 web 端实现模块化?一是,初期阶段 web 端是轻逻辑的;二是,即使后来逻辑比重增加,但仍然可以工作下去。但是服务器端却一定要有模块的。

定义模块

根据 CommonJS 规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,即在模块内部定义的变量,无法被其他模块读取,除非定义为 global 对象的属性。

模块输出

模块只有一个出口 – module.exports 对象,需要把模块希望输出的内容放入该对象。

加载模块

加载模块使用 require 方法,该方法读取一个文件并执行,返回文件内部的 module.exports 对象。

不同的实现对 require 时的路径有不同要求,一般情况可以省略 .js 扩展名,可以使用相对路径,也可以使用绝对路径,甚至可以省略路径直接使用模块名(当模块是系统内置模块时)。

AMD

AMD(Asynchronous Module Definition),异步模块定义,它是一个在浏览器端模块化开发的规范。目前不是 JavaScript 原生支持,使用 AMD 规范进行页面开发需要用到对应的函数库 – RequireJS ,主要解决两个问题:

  • 多个 js 文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器;
  • js 加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。

实际上,AMD 本身就是 RequireJS 在推广过程中对模块定义的规范化的产出。

RequireJS 定义了一个全局函数 define ,用来定义模块。

define(id, dependencies, factory)

  • id 可选参数,用来定义模块的表示,缺省为脚本文件名;
  • dependencies ,是当前模块依赖的模块名成数组;
  • factory ,工厂方法,模块初始化要执行的函数或对象(模块的输出值)。

在页面上使用 require 函数加载模块, require([dependencies], callback)

  • 第一个参数是所依赖模块的数组;
  • 第二个参数是一个回调函数,当前面指定的模块都加载成功后调用,加载的模块会以参数形式传入该函数。

AMD 推崇的是 依赖前置 ,在定义模块的时候就要声明其依赖的模块,依赖提前罗列出来会被提前下载被执行,后来做了改进,可以不用罗列,允许在回调函数中就近使用 require 引入并下载执行模块。

CMD

CMD 推崇 就近依赖 ,只有在用到某模块的时候再去 require 。如同 AMD 和 RequireJS ,CMD 也是在 Sea.js 的推广过程中规范化的产出, Sea.js 和 Require.js 要解决的问题一样,只不过在模块定义方式和模块加载时机上有所不同。

Seal.js 也定义了一个全局函数 define ,用来定义模块。

define(factory))

  • factory 参数既可以是函数,又可以是字符串或对象;
  • 如果 factory 是字符串或对象,表示该模块的接口就是该对象或字符串;
  • 如果 factory 是函数,此函数就是模块的构造方法,该函数提供三个参数 – define(function(require, exports.module) {})

AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同。很多人说 RequireJS 是异步加载模块, SeaJS 是同步加载模块,这种理解实际上是不准确的,其实加载模块都是异步的,只不过 AMD 依赖前置, JS 可以方便知道依赖模块是谁,立即加载,而 CMD 就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了哪些模块。这也是很多人诟病 CMD 的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

Webpack

Webpack 是的来临,先看一下 webpack 的优点:

  • RequireJS 的所有功能它都有;
  • 编译过程更快(因为 Require.js 会去处理不需要的文件);
  • 不需要再做一个封装的函数,如 RequireJS 的 define ,你需要一个很大的封装去定义每个模块,还需要在 RequireJS 的配置文件中把每个模块的路径都配出来,略繁琐。

对比 RequireJS 和 SeaJS 而言,它还有一些特有的属性:

  • 对 CommonJS、AMD、ES6 的语法做了兼容;
  • 对js、css、图片等资源文件都支持打包;
  • 串联式模块加载器以及插件机制,具有更好的灵活性和扩展性;
  • 有独立的配置文件 webpack.config.js
  • 可以将代码切割成不同的 chunk ,实现按需加载,降低初始化时间;
  • 支持 SourceUrls 和 SourceMaps ,易于调试;
  • 具有强大的 Plugin 接口,大多是内部插件,使用起来比较灵活;
  • 使用异步 IO 并具有多级缓存,使其很快且在增量编译上更加快;
  • 双服务器模式。

TODO 浅谈组件化

保证组件的封闭性, js 方面是模块化的,需要注意组件的功能界限问题,即什么是应该在组件内部实现,什么是应该由组件的调用者来实现的。对组件功能界限的定义是只负责 UI 相关的功能,所有的业务逻辑都是从调用者传递过的。

浅谈规范化

项目目录结构非常清晰。当进行开发的时候,哪些代码应该放到哪里进行了明确的规定,并且每个文件的功能都尽量清晰并且单一。

模块和组件

技术是服务业务的,业务依托于技术而存在。模块和组件的划分首先要站在业务角度划分,其次才是技术角度。

模块和组件的划分依据

根据业务划分

复杂的系统,最好先按业务领域横向拆分成可独立部署的子系统,每个子系统内部再按技术(主要是业务层和 Web 层)纵向拆分成不同的模块。需要注意的是,根据业务划分出模块,是面向系统的,还是面向用户的。

根据技术划分

已经根据业务划分了模块,此时需要将这些模块组合起来,对外提供服务。最终产出的是产品,是面向用户的。如何组织模块呢?一个好的思路是 分层 ,将基础服务下沉,将业务服务上浮。实现了分层, 模块间的通信 又成了一个问题。目前业界比较好的解决方案是通过 MQ 和配置中心解耦,模块之间并不知道自己依赖的系统或者被谁依赖,所有需要的东西都通过 MQ 注册,并通过配置中心管理。模块划分的另一个原则就是 单一职能原则

总结

其他的划分原则还有高内聚低耦合,模块大小规模适当,模块的依赖关系适当等。

物理学有一个名词 – ,是一种测量在动力学方面不能做功的能量总数,即当总体的熵增加,其做功能力下降,熵的量度正是能量退化的指标。熵亦被用于计算一个系统中的失序现象,也就是计算该系统混乱的程度。

软件开发本质上是处理复杂度的过程,软件总是趋向无序混乱发展,但是架构得当,模块划分合适,可以将软件腐败速度降低。

模块和组件的编写技巧

假设已经正确合理地划分了模块,是否就代表代码结构、复用性、调试等已经足够好了呢?不一定。原因在于组件内部通常也包含很多细小的逻辑,由于这些逻辑没有必要(或者我们认为没有必要)抽离出来,再加上模块和组件内部状态复杂多变,各自牵扯就会造成模块内部代码难以复用和修改。

为什么代码难以维护呢?其中一个重要原因就是 系统抽象级别 不够。那么如何编写抽象级别合适的代码呢?其中一个原则就是 代码描述的应该是一般逻辑 ,而 将硬编码和分支策略做成可配置 ,其中硬编码部分和分支策略可以称之为程序的 元数据

为一般逻辑编码,为特殊逻辑配置。

注意保持模块和组件的纯粹性,要求代码不依赖外部环境且不会对外界有影响,并且给定输入,输出一定。另外,还有一个重要特征就是 单一职责 。即一个功能函数只完成原子性功能,不要让函数做非常多的事情,以保证其功能的纯粹。单一职责原则描述的是如果一个模块承担的职责过多,就等于把这些职责耦合在一起了,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。此原则的核心就是 解耦和增强内聚性 ,如此代码才能够更易于被修改,扩展。

Footnotes:

Date: 2020-01-14 Tue 22:58

Author: Jack Liu

Created: 2020-02-08 Sat 21:27

Validate