17

从零开始为Web项目定制构建系统(四)——Hello XXXX的SPA版(下)

 4 years ago
source link: http://nakeman.cn/engineering/webprogramming/customize-build-system-from-bottom-up-4-basic-spa2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前一课小结SPA理论和工作前提,现在开始本课正题——多页网站改造为单页应用(MPA的SPA改造)。

根据前面的理论的指引,我们的改造概略以下几点:

  • 第一,将网页(或部分交互功能)转换为JS view对象,「界面渲染」 转到前端(使用JS 动态操作DOM);
  • 第二,创建全局的JS router对象,负责应用会话导航,将 「会话路由」 转到前端;
  • 第三,创建一个app的对象,表征应用程序主体,管理(初始化)前两个对象;

我们再看看这个常规完整的SPA体系结构图:

2M3ANfu.png!web 服务端交换和 Model都不是必须,本课简单交互功能不需这些组件。

Table of Contents

功能需求与模型设计

在开始改造和编码之前,我们走一次正式的项目流程,先做一些简要的需求分析和概念模型设计。

需求分析

由于前一版本的交互功能过于简单,只有一张网页,一个交互输入输出功能(hello xxx),会话过程过于简单,不足于表征完整的SPA,所以在需求上需「增加一步交互功能」,增加一个goodbye的交互页,这样我们有了一个两步的简单交互会话(称为 greeting )。看如下的sitemap图:

YbAnM3J.png!web

可以看到简单会话有两个view,以及它们的流程关系:

  • 第一,一个起点,程序的载入;
  • 第二,自渲染或局部重渲染,例如Hello view的input name;
  • 第三,整view渲染,例如Hello view到Goodbye view;

由此我们也可以看「会话交互操作」有两类,一类是当前view「交互输入输出」操作,一类是「跳转新view」的操作。传统的MVC中的C就是负责这种逻辑。

PS 这里可补一份SRS 软件需求说明书(从用户的角度说程序提供的功能)

架构设计

有了需求 , 我们(作为WGP架构设计师)可以将其转换为架构设计图。本项目功能需求是很简单的,然,其有完整SPA构架的基础,包括 A R V等重要组件。看下图:

bYjQf2U.png!web

此图可看到“app有一个会话(router),会话中有两个交互页或步骤(view)”。我们使用了UML符号标出了它们类间关系,这样更方便 更技术的指导开发。这里值得注意几点:

  • 第一,本课初始实现的代码未对view对象进行抽象,抽象出view基类,图解只是为了方便;
  • 第二,SPA有机组件之间 多是 聚合关系(虚棱)——业务或逻辑上的计算协作关系,App和Router 成份关系(实棱)是有待验证的;

代码开发

有了这个简单SPA 架构设计案,我们可以(更容易)开始改造了。

一 创建前端app的架子

前一版greeting网站程序只有一个hello页,所以页面(index.html)和代码(app.js)都是hello页的逻辑,我们第一步要做的是 将它抽取出来 ,单独创建一个 hello页的JS对象(Hello view),留下一个app的架子。

index.html
...
<h1>This is a simple interaction SPA</h1>
<div id="main">
</div>
...
app.js
// hello spa
var HELLOSPA = HELLOSPA || {};

接下来的改动较多,先提交一个版本。

// 没添加新,也没删旧
$ git add -u .
$ git commit -m "set up the SPA base"

二 创建可运行的SPA

我们可对照着架构图,一个一个将它转换为JS模块(或者类),不过从哪个先,和具体实现方式,可能因人而异。我们遵循最简单和基础的方式,就是搭建一个架构的架子, 让程序能运行起来 ,再一点一点的丰富需要的功能。

完整的架构架子 是一体的,所创建哪个先不重要。重要的是知道它们的形式功能。

作为WGP开发者,我们揭示一下他 转译架构模型 所需的技术技能包括:

  • 第一,WGP各有机组件(A R V M等)的形式;
  • 第二,用什么JS技术表达这些组件,是一次性对象(对象字面量),类(JS构造函数),还是抽象类(原型继承);
  • 第三,怎样组织这些组件(包括环境和第三方的库)的依赖关系;
  • 第四,对环境功能的掌握,例如BOM(Fetch API),DOM和第三方的库(EJS);

1 创建 App 对象

我们可从依赖的末端开始工作,例如view,也可以从最直观的App,我们选择后者。

为了程序完整性,需要制作一个App对象,表征程序主体,App 对象主要负责初始化程序,管理包括一些全局配置数据,和启动R对象等。故App 的形式主要是1) 监测 PageOnload,2) 加载R对象;

由此可知,App 对象是依赖浏览器BOM API 和R 对象,并且只有一个单例(使用对象字面理实现),只使用一次(启动会话,暂时没有重启会话的需求);

依赖管理上提供一个全局对象(HELLOSPA)作为名字空间,大部分对象位置这个对象之下(作为它的属性),这样它们之间的依赖得到满足(虽然像R并不依赖A):

// 1 hello spa
// 明示有一个全局的 HELLOSPA 对象存在(windows.HELLOSPA)
var HELLOSPA = HELLOSPA || {};
.......
// 2.2 将这个即时类对象 赋给 HELLOSPA的属性,让其全局可用(只用了一次,安装加载事件处理)
HELLOSPA.App = App;

最后在全局上安装App的程序加载事件处理

// 在全局环境上,执行事件安装
window.addEventListener("load",HELLOSPA.App.startSession());

App 类图如下:

zQVZviz.png!web

2 创建 Router 对象

接着制作 Router,制作R组件需要熟悉 R的组件形式,和具体的会话导航任务。R组件是管理WGP 会话的功能对象,它的形式主要是

  • 1) 监测 URL跳转事件,根据route记录
  • 2) 加载相应的view对象;

故,R对象是依赖浏览器BOM API,并且一般只一个单例,但是会被执行多次,每导航一次执行一次;

用户(点击)导航产生多次的hashchange事件,所以最重要是安装hashchange事件处理:

window.addEventListener('hashchange',HELLOSPA.Router.routing,false);

3 创建 View 对象

View 对象形式是SPA中最为复杂的一个,也是最重要一个。我们先创建两个“空白” View组件:HelloView.js和GoodbyeView.js。

view对象主要形式功能是在DOM上渲染交互UI,为了测试,这里我们让两个view打印一些字符,并提供会话导航链接。

3.1 V组件形式

「制作(创建)具体V组件」需要多种基础和前提,这也是 WGP构架师 的专业所在;包括 V组件形式,和具体的View的交互功能;

V组件(形式)包括:

  • 1) 渲染交互界面
  • 2) 交互结果输出
  • 和3)接受交互输入;

V组件可以没有交互输入,但必须有一定的渲染输出。

从开发者技术上,渲染 通过操作DOM,接受输入 通过 绑定浏览器交互事件。所以V组件是强依赖浏览器DOM API的。

3.2 模板引擎

虽然交互view功能比较简单,但是它HTML结构代码还是比较多,所以为了后面扩展准备,现在就开始引入 模板引擎 ,将 View组件的「渲染」任务专业独立出来 [注] 。

本实验项目选择使用 EJS ,具体安装和使用请参考这里(https://ejs.co) 和这里(https://juejin.im/post/5ed3a1d7518825432a3595e8 )。

注:引入 模板引擎有两个意义,第一, UI渲染编程动态化(按需要生成不同的UI结构,例如数组->列表);第二,专业独立(将UI结构逻辑独立出交互逻辑);

3.3 View 类

V组件的复杂动态性适合使用 类现实( JS 构造函数 )。由于本 初始版本 只有简单的渲染输出,没有完整的交互功能,类只有几个属性,和一个render接口方法,测试成功成后 下一版 本才完整实现:

function HelloView(){
    
    this.el = document.querySelector("#main");
    this.templatefile = 'HelloView.ejs';
    this.template = "";

  }

  HelloView.prototype.render = function(){
    var that = this;

值得提及的是,render方法实现上,使用了EJS模板引擎,和fetch API异步取得外部模板。使用外部模板的理由是说,让UI结构的制作更独立。

通过测试,先提交一个版本(无删除,有新数据和更新,使用默认git add .):

[keminlau@localhost a-wgp]$ git status -sb
## fea-basic-SPA
M .gitignore
M src/app.js
M src/index.html
?? src/GoodbyeView.ejs
?? src/GoodbyeView.js
?? src/HelloView.ejs
?? src/HelloView.js
?? src/router.js

[keminlau@localhost a-wgp]$ git add .
[keminlau@localhost a-wgp]$ git status -sb
## fea-basic-SPA
M .gitignore
A src/GoodbyeView.ejs
A src/GoodbyeView.js
A src/HelloView.ejs
A src/HelloView.js
M src/app.js
M src/index.html
A src/router.js
[keminlau@localhost a-wgp]$ git commit -m "set a runnable SPA base"

三 完善 Greeting SPA程序

我们现在已经有了一个最精简的可运行的SPA程序,接下来只需将目标需求的功能分别“填上”两个交互页即可。

HelloView

HelloView 的交互功能可概述为“用户在 名字输入框 填上自己的名字,点击 进入按钮 ,程序在界面上打一声招呼,用户手动或者程序自动的转向下一交互页”。

交互页跳转的功能已经做好了,而界面UI也是极为简单,所主要任务是为UI安装事件处理。

谁负责安装事件处理程序?

这里有两个疑问:

  • 第一,交互事件处理程序的 安装 是谁的(形式)任务?
  • 第二,事件处理程序,与render是什么关系?

我们可以参考像backbone这样的框架,会发现事件处理程序的安装是V类的形式部分。而当我们再查阅一些老式源码( TodoMVC )会发现有一个Controller的类对象负责安装V的事件处理。这个疑问揭示了V和C之间的微妙关系。而经个人分析,有了一些初步的结论:

  • 第一,「事件处理程序的安装」 是不是V的形式任务,取决于V的定义;但是,将这个任务 和render 或 事件处理程序本身 并列 ,说是V对象的形式任务,那是不符合逻辑的;
  • 第二,所以,「事件处理安装」 严格上不属于V的形式任务,它要么是V的父R的责任,要么是一个独立的对象负责,例如传统的C的责任;
  • 第三,事件处理程序 和render方法是并列的,性质同类,是V的形式属性;当然,由于JS的动态性,我们并不少见 在事件处理程序中 安装另一个对象的事件处理;
  • 第四,V可以被用户交互触发,会有多个执行点(render可认为是加载触发,其它是事件触发),这是与其它组件最大的不同;每个执行事件点都可看成是 V 高级对象的输入输出 (相对于只一个执行点的CLI程序);
  • 第五,当然还有一个分法,就是render渲染界面和UI事件的安装,都在「 组装V的形式 」,性质更相似;而「 事件处理程序」 才是 V组件作为高级计算对象的输入输出(这个结论似乎更合理)。

由于本程序功能较简单,HelloView的事件处理由Router触发安装,不设独立的Controller对象;由上面的分析,我们将界面结构的渲染(render)和界面事件处理的安装(bind)归为V的形式初始化,放进V类的构造函数中。类图如下:

3QJ3qeQ.png!web

function HelloView(){
  ....
  //3 View Object UI
  // 构造函数里有数据,方法和UI,这些都是V类对象的形式部分,所以合理
  // 另,UI构造可分为 结构渲染和事件安装(绑定),此外由异步加载UI模板,而将bind串在render的后面
  this._render();
  
  
HelloView.prototype._render = function(){
    ....
      //而将bind串在render的后面
      that._bind();

  });

HelloView.prototype._bind = function(){

  var $btn = document.querySelector("#btsubmit");
  ....

值得提及的是,由于使用了异步加载UI模板数据,bind要串在render后,不能同步并列。因为render未完成,安装不了事件。

GoodbyeView

功能完满此版本,提交最后一个版本:

[keminlau@localhost a-wgp]$ git status -sb
## fea-basic-SPA
 M src/GoodbyeView.ejs
 M src/GoodbyeView.js
 M src/HelloView.ejs
 M src/HelloView.js
 M src/index.html
 M src/router.js
[keminlau@localhost a-wgp]$ git add -u .
[keminlau@localhost a-wgp]$ git commit -m "finished greeting function"

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK