乐趣区

关于软件工程:我来聊聊前端应用表现层抽象

本文首发于欧雷流。因为我会时不时对文章进行补充、修改和润色,为了保障所看到的是最新版本,请浏览原文。

咱们处于变动很快的时代,无论是商业还是科技。一家公司看上去商业很胜利,兴许前脚刚上市,后脚就因为什么而退市,甚至开张;一项看似高大上的技术横空出世,各类媒体争先恐后地撰文介绍,热度炒得老高,没准没多久就呈现了竞争者、替代者。

在这样的大环境下,传统的「web 前端开发」演变成了「泛客户端开发」,前端开发者从「配置工程师」被「逼」成了「软件工程师」。开发变得更简单了,要解决的问题更多了,从业难度不知晋升了多少倍——前端早就不再简略。

在泛滥必须要解决的问题中的一个,就是体现层运行环境的兼容问题,像跨浏览器和跨端、平台、技术栈。留神,这里说的是「体现层」而不是「视图层」。

「体现层」与「视图层」

「体现层」的英文是「presentation tier」或「presentation layer」,具体是哪个取决于是物理上还是逻辑上划分;而「视图层」的英文是「view」。「体现层」是「视图层」的超集,依据前端利用的架构设计,它们既能够不等又能够相等。

体现层

「体现层」这个词出自经典的三层架构(或多层架构),是其中一个分层。三层架构包含数据层、逻辑层和体现层,个别用在 C/S 架构中。

为什么会在这篇讲前端开发的文章中提到它?这是因为,尽管在一些前端利用中用不到,尤其是快餐式利用,但在企业级简单前端利用中就非常须要一个前端的「三层架构」。

视图层

「视图层」则来自体现层罕用的「model-view-whatever」模式中的「view」,即「视图」。至于说的时候在「视图」前面加个「层」字合不适合,就不在这里探讨了,文中皆应用「视图层」这个词。

运行环境兼容

跨浏览器

因为各浏览器厂商对规范实现的不统一以及浏览器的版本等起因,会导致个性反对不同、界面显示 bug 等问题的呈现。但庆幸的是,他们根本是依照规范来的,所以在开发时源码的语法简直没什么不同。

所谓的「跨浏览器」实际上就是利用浏览器额定的公有个性和技术或辅以 JS 对浏览器的 bug 进行「修改」与性能反对。

跨端、平台、技术栈

当初,绝大部分的前端开发者是在做泛客户端开发——开发 web 利用、客户端利用和各类小程序。

在做 web 利用时须要思考 PC 端和挪动端是离开还是适配?技术选型是用 React、Vue?还是用 Web Components?或是用其余的?做客户端利用、各类小程序时这些也会面临技术选型的问题。

如果公司某个业务的性能笼罩了上述所有场景,该如何去撑持?与跨浏览器不同的是,不同端、平台、技术栈的源码语法不一样,要满足业务需要就得各开发一遍。然而,这显然老本过高,并且危险也有些大。

那么,要怎么解决这个问题呢?从源头登程。基本的源头是业务场景,而后是产品设计,但这些都不是开发人员可掌控的,简直无奈扭转。可能齐全被开发人员所左右的根本只有开发阶段的事件,那就从这个阶段的源头动手——源码编写。

若是与业务相干的代码只需编写一次就能运行在不同的端、平台、技术栈上,那真是太棒了!这将会大大地降低成本并缩小危险!

体现层的形象

为了达到跨端、平台、技术栈的目标,须要将体现层再划分为形象层、运行层和适配层。其中,形象层是为了对立源码的编写形式,能够是 DSL、配置等,它是一种协定或约定;运行层就是须要被「跨」的端、平台、技术栈;适配层则是将形象层的产物转换为运行层失常运行所须要的模式。

体现层中能够被形象的大略有视图构造、组件外观、组件行为等。

视图构造

在 web 前端开发中,HTML 就是一种视图构造的形象,形容了界面中都有什么,以及它们之间的层级关系。最终的显示须要浏览器解析 HTML 后调用操作系统的 GUI 工具库。

对于业务撑持来说,无论是 HTML 还是其余什么拼凑界面的形式,相对来说比拟低级(是「low level」而不是「low」),视图单元的划分粒度比拟细,在开发界面时就会破费更多的工夫。

咱们须要一种可能屏蔽一些不用关注的细节的视图构造形象,在这个形象中,每个视图单元都有着其在业务上的意义,而不是有没有都能够的角色。具体做法请看下文。

组件外观

大部分已存在的组件的视觉出现是固定的,即某个组件的尺寸、形态、色彩、字体等无奈被定制。如果同样的交互只是因为视觉上有所差别就要从新写组件,或者在组件内部从新写份款式进行笼罩,那未免也太苦楚了……

咱们能够将那些心愿可能被定制的视觉出现形象成「主题」的一部分,这部分能够被叫做「皮肤」。在进行定制时,分为线下和线上两种形式。

「线下」是指在利用部署前的开发阶段进行解决。在前端构建工具丰盛的当初,写页面款式时曾经不会去间接写 CSS,而是像 Sass 这种可编程式的预处理器。这样就能够抽取出一些管制视觉出现的 Sass 变量,须要定制时通过在内部对变量赋值进行笼罩,而不须要吃力重写组件或款式。

「线上」则是部署后依据运行时数据动静扭转。在皮肤定制即时预览和低代码平台等场景,是根本没机会去批改 Sass 变量并走一遍构建流程的,即便技术上可能办到。借助 CSS 自定义属性(CSS 变量)的力量能够较为不便地做到视觉出现的运行时变更。

组件行为

组件除了外观,其行为也该当是能够定制的。看到「行为」这个词,第一反馈就是跟用户操作相干的事件,然而这里还包含与组件内部结构相干的。

对于组件的内部来说,组件外部就是个黑盒子,其本身构造的组成部分有的能够被上文所说的视图构造所管制,有的则无能为力:

上图是一个比较复杂的搜寻组件,尽管外观和布局看起来有所不同,但「它们」的确是同一个组件。外观不同的解决方案下面曾经大体阐明,这类视图构造无法控制的布局问题,须要枚举场景后在组件内进行反对,而后作为「主题」的一部分存在。

跟用户操作相干的行为有组件本身的交互规定及与业务逻辑的联合这两类。

交互规定又有两种:一种是像表单是在字段值产生扭转时就校验还是在点击按钮时校验这样;另一种是像字段值是在输入框的值扭转(input 事件)时更新还是失焦(change 事件)时更新这样,或是像下拉菜单的弹出层是在悬停(hover 事件)时呈现还是点击(click 事件)时呈现这样。

前者的解决形式与下面说的视图构造无法控制的布局问题差不多,后者则是须要组件反对事件映射,即内部能够指定组件某些交互的触发事件。当然,这两者同样也能够作为「主题」的一部分。

咱们在写组件时有件事是须要竭力防止却往往难以避免——组件中耦合业务逻辑。组件决定的应该只是外貌与交互状态,外面只有交互逻辑及管制展示的状态,不应该牵扯到任何具体业务相干的逻辑。只有长得一样、操作一样,那么就应该是同一个组件,具体业务相干的逻辑注入进去。

这段非常「个性化」的业务逻辑,说白了就是响应用户操作的变动以及业务数据的变动去更改组件外部的状态:

{
  // 组件事件
  events: {
    // 组件的一个点击事件
    'click-a': function() {},
    // 组件的另一个点击事件
    'click-b': function() {},
    // 组件的一个扭转事件
    'change-c': function() {},
  },
  // 业务数据变动的回调
  watch: function(contextValue) {},}

运行时会注入一个上下文给上述对象办法的 this,组件还能够增加工具办法给上下文。该上下文的内置属性与办法有:

interface IDomainSpecificComponentContext {getState(key: string): any;
  setState(key: string, value: any): void;
  setState(stateMap: { [key: string]: any }): void;
}

视图构造形容

下面说了咱们须要一种比 HTML 之类的更进一步的视图构造形象,上面就来说说这部分的大体思路。

技术选型

在做视图构造形象时最罕用到的技术就是 XML-based 或 XML-like 以及 JSON-based 的某种技术。XML-base 和 XML-like 的技术都是合乎 XML 语法的,惟一的区别是前者要完全符合 XML 的标准规范,像 Angular 和 Vue 的模板就是后者;同样的,JSON-based 的技术是完全符合 JSON 的标准规范的技术,像 JSON Schema。

自从 React 问世以来,其带来的 XML-like 的 JSX 也会被用于视图构造形象,但根本仅限于编辑时(edit time)。一段 JSX 代码并不是纯申明式的,作为视图构造形容来说可读性较低,解析难度较高,并且通用性很低。

JSON-based 的技术对前端运行时最为敌对,解析老本简直为零;相同的,其可读性很低,JSON 构造是纵向增长的,指定区域内的表达力非常受限,无奈很直观地看出层级关系与视图单元的属性:

{
  "tag": "view",
  "attrs": {"widget": "form"},
  "children": [{
    "tag": "group",
    "attrs": {
      "title": "根本信息",
      "widget": "fieldset"
    },
    "children": [{
      "tag": "field",
      "attrs": {
        "name": "name",
        "label": "姓名",
        "widget": "input"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "gender",
        "label": "性别",
        "widget": "radio"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "age",
        "label": "年龄",
        "widget": "number"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "birthday",
        "label": "生日",
        "widget": "date-picker"
      }
    }]
  }, {
    "tag": "group",
    "attrs": {
      "title": "宠物",
      "widget": "fieldset"
    },
    "children": [{
      "tag": "field",
      "attrs": {
        "name": "dogs",
        "label": "????",
        "widget": "select"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "cats",
        "label": "????",
        "widget": "select"
      }
    }]
  }]
}

如果一个利用的设计是不须要人工写视图构造形容的话,能够思考应用 JSON-based 的技术。

像 Angular 和 Vue 的模板那种 XML-like 的技术是相对来说最适宜做视图构造形容的——纯申明式,构造是向程度与垂直两个方向增长,无论是可读性还是表达力都更强,解析难度适中,并且具备通用性。

上面的模板代码所形容的内容与下面那段 JSON 代码截然不同,深呼吸,好好感受一下两者之间的差别:

<view widget="form">
  <group title="根本信息" widget="fieldset">
    <field name="name" label="姓名" widget="input" />
    <field name="gender" label="性别" widget="radio" />
    <field name="age" label="年龄" widget="number" />
    <field name="birthday" label="生日" widget="date-picker" />
  </group>
  <group title="宠物" widget="fieldset">
    <field name="dogs" label="????" widget="select" />
    <field name="cats" label="????" widget="select" />
  </group>
</view>

至此,视图构造形容最终该选用哪种技术,想必毋庸多言。

设计思路

毋庸置疑,模板的语法要合乎 XML 语法是前提,再在此基础上依据需要进行定制、扩大。首先要定义标签集。所谓的「标签集」就是一个元素库,其中的每个元素都要具备肯定语义,使其在业务上有存在意义。而后是制订形容元素的 schema 并实现其对应的解析、校验等逻辑。

元素 schema 大略是长这样:

// 属性值类型
type PropType = 'boolean' | 'number' | 'string' | 'regexp' | 'json';

// 属性描述符
type PropDescriptor = {type: PropType | PropType[];
  required: boolean; // 是否必须
};

// 元素 schema
type ElementSchema = {
  name: string; // 元素名
  tag?: string; // 标签名,不指定时取元素名
  props?: {[key: string]: PropDescriptor;
  };
  attrs?: {resolve: (key: string, val: any) => any;
  };
  // 节点行为,是作为父节点的子节点还是属性存在
  behavior?: {
    type: 'append' | 'attach';
    // 以下都用于 `type` 是 `'attach'` 时
    host?: string; // 宿主(属性名)keyed?: boolean; // 是否为键值对汇合,值为 `true` 且 `merge` 为 `false` 时以节点 ID 为键
    merge?: boolean; // 当值为 `true` 时将 `reduce` 的返回值与 `host` 指定的属性的值进行合并后从新赋值给 `host`
    reduce?: (node: ITemplateNode) => any; // 转换节点信息
    restore?: (reduced: any, node?: ITemplateNode) => ITemplateNode | Partial<ITemplateNode>;
  };
};

能够看到 schema 中有 propsattrs,它们独特组成了模板元素的属性(XML attributes),区别是:模板解析后的属性如果是在 props 中定义的并且满足属性描述符的 typerequired 所指定的限度条件,会成为模板节点的 props 属性;残余没在 props 中定义的则成为模板节点的 attrs 属性,通过 resolve 办法可能对属性依据本人的规定进行值的转换。

尽管在模板中元素总是以嵌套的模式展现出层级关系,但一个元素并不一定就是其父级的构造,还可能是配置。因而,元素 schema 中的 behavior 用于设置以后元素在模板解析后是作为一个节点的子节点存在还是作为某个属性存在。

上述的模板设计是纯视图构造形容的,并且只对元素这种「块」进行解决,我认为这样够用了。依据状况,能够扩大为像 Angular 和 Vue 的模板那样反对文本、插值和指令等。

如果懒癌发生并且没什么非凡需要,模板解析的工作能够交给魔改后的 Vue 2.6 编译器,再适配为模板节点树。

每个模板节点的构造大抵为:

interface ITemplateNode {
  id: string;
  name: string;
  tag: string;
  props: {[key: string]: any;
  };
  attrs: {[key: string]: any;
  };
  parent: ITemplateNode | null;
  children: ITemplateNode[];}

最初,通过适配层将模板节点树转为运行层的组件树,并把渲染的控制权也转交给了最终的运行环境。

总结

在一个简单的前端利用中,如果不对其进行分层,那它的扩展性和可维护性等真的会不忍直视……通常是采纳经典的三层架构,从下到上别离为数据层、逻辑层和体现层。本文以体现层为例,将其再次划分出形象层、运行层和适配层这三层,实际上数据层和逻辑层也能够套用这种模式——就像在生日蛋糕上切上四刀——我称其为「九宫格」模型。

在体现层的各种形象中,本文着重论述了视图构造形容的技术选型与设计思路,能够看出 XML-like 的模板从编写到解析再到渲染这一整条流程,与 Angular 和 Vue 的模板及 HTML 大体上统一;其余形象只是略微提了提,当前有机会再开展来说。

之前也写过几篇与模板相干的文章:从提效角度与「面向组件」做比照的《我来聊聊面向模板的前端开发》;从可定制性角度讲的《我来聊聊配置驱动的视图开发》;从低代码平台的核心理念「模型驱动」登程的《我来聊聊模型驱动的前端开发》。能够说,本文的内容是它们无关体现层形容的「根基」。

无论一家公司是不是做低代码平台的,或者外部有没有低代码平台,都应该从体现层形象出视图构造形容,至多要有如此意识。


欢送关注微信公众号以及时浏览最新的技术文章:

退出移动版