同构应用开发实践与JavaScript发展趋势
1. 模块对象与数据传递
模块对象并非全局对象,当需要将数据传递给特定模块且不暴露给其他模块时会用到它。例如,在处理条目中的项目列表(如图像库)时,为每个项目渲染一个图像模块,但使用不同的数据。此时,可通过模块对象将必要数据直接传递给模块,而非让模块以索引为键从条目中提取数据。
2. 转译视图模型
- 定义内部结构 :在定义了顶级数据结构后,需要定义每个对象的内部结构。由于C#是强类型语言,不能像JavaScript那样随意传递动态对象字面量。每个视图模型都需要严格定义属性及其类型。为了让前端负责视图模型设计,决定用JavaScript编写视图模型,这样前端团队成员可以轻松测试模板渲染,而无需与.NET后端集成。
- JavaScript构造函数示例 :以下是一个典型的JavaScript视图模型示例,描述了一个Bundle并继承自另一个名为Product的模型:
var Bundle = function() {
Product.apply(this);
this.Title = '';
this.Director = '';
this.Synopsis = '';
this.Artwork = new Image();
this.Trailer = new Video();
this.Film = new Video();
this.Extras = [new Extra()];
this.IsNew = false;
};
- 模板逻辑处理 :模板通常需要在显示或隐藏特定元素之前检查多个数据。为保持模板简洁,Handlebars中的#if语句只能评估单个属性,没有自定义辅助函数则不允许进行比较。通过ES5 JavaScript中的“getters”,可以轻松为构造函数添加动态评估属性,用于模板中的复杂评估和比较。示例如下:
var Bundle = function() {
// ...
this.Trailer = new Video();
this.IsNew = false;
// ...
Object.defineProperty(this, 'HasWatchTrailerBadge', {
get: function() {
return this.IsNew && this.Trailer !== null;
}
});
};
- JavaScript转译到C# :虽然所有视图模型都已用类型化属性和getters定义,但它们仅存在于JavaScript中。最初尝试手动将它们重写为C#,但发现这是重复工作且不可扩展。于是创建了一个简单的Node.js应用,使用枚举、类型检查和原型继承为每个构造函数创建描述性“清单”。将每个视图模型解析为清单后,可将数据输入到C#类的Handlebars模板中,生成后端的.cs文件。例如,上述Bundle视图模型转译为C#如下:
namespace Colony.Website.Models
{
public class Bundle : Product
{
public string Title { get; set; }
public string Director { get; set; }
public string Synopsis { get; set; }
public Image Artwork { get; set; }
public Video Trailer { get; set; }
public Video Film { get; set; }
public List<Extra> Extras { get; set; }
public Boolean IsNew { get; set; }
public bool HasWatchTrailerBadge
{
get
{
return this.IsNew && this.Trailer != null;
}
}
}
}
3. 布局定义
- 简单布局示例 :需要一种方法来定义特定视图应渲染哪些模块以及渲染顺序。为避免在C#和JavaScript中重复模块列表和相关逻辑,考虑将每个视图表示为JSON文件,可在前端和后端共享。例如,描述主页可能布局的简单JSON文件如下:
[
"Head",
"Header",
"Welcome",
"FilmCollection",
"SignUpCta",
"Footer",
"Foot"
]
- 条件渲染模块 :希望能够根据特定条件有条件地渲染模块。受Handlebars逻辑的启发,可在JSON中引用全局数据结构中的属性来表达所需逻辑。示例如下:
[
// ...
{
"Name": "UserSidebar",
"If": ["User.IsSignedIn"]
},
{
"Name": "Modal",
"If": ["ViewState.IsTrailerOpen"],
"Import": "Entry.Trailer"
}
]
- 复杂视图结构 :进一步使用这种格式描述更复杂的视图结构,模块可以嵌套在其他模块中。示例如下:
[
// ...
{
"Name": "TabsNav",
"Unless": ["Entry.UserAccess.IsLocked"]
},
{
"Name": "TabContainer",
"Unless": ["Entry.UserAccess.IsLocked"],
"Modules": [
{
"Name": "ExploreTab",
"If": ["ViewState.IsExploreTabActive"],
"Modules": [
{
"Name": "Extra",
"ForEach": "Entry.Extras"
}
]
},
{
"Name": "AboutTab",
"If": ["ViewState.IsAboutTabActive"]
}
]
}
// ...
]
4. 页面生成器
为了在栈的任一侧得到最终渲染的HTML,需要将模板、布局和数据组合在一起生成视图。为此设计了一个名为“Page Maker”的类,它需要遍历给定的布局文件,根据布局文件中的条件用相应数据渲染每个模块,最后返回渲染后的HTML字符串。为确保C#和JavaScript实现返回相同的输出,将两者作为团队编程练习编写,这也为前后端团队成员提供了学习彼此技术的机会。
5. 前端单页应用
开发团队倾向于自主构建技术。在单页应用方面,没有选择现成的框架,而是决定自己构建解决方案。遵循的原则包括:UI行为不应与特定标记紧密耦合;应用状态的变化和模板及布局中已定义的Handlebars逻辑应足以实现页面上任何元素的动态重新渲染。最终的解决方案不仅轻量级,而且高度模块化。
-
状态管理
:最低层是从服务器传递的状态和完全渲染的视图,可指定任意标记部分“订阅”状态变化。状态变化通过DOM事件发出信号,比Angular的摘要循环或各种实验性的“可观察”实现更高效。当状态发生变化时,DOM的相应部分会重新渲染并替换。
-
用户界面行为
:用户界面“行为”与状态管理过程完全分离,可逐步增强任意标记部分。例如,可将相同的“滑块”UI行为应用于不同组件。
-
路由管理
:最高层是启用了History API的路由器,用于拦截所有链接点击并确定渲染结果视图所需的布局文件。为减少前端和后端资源的差异,决定扩展REST API以提供模块模板和JSON布局。
以下是一个简单的流程图,展示前端单页应用的基本架构:
graph LR
A[服务器] --> B[状态和视图]
B --> C[DOM订阅状态变化]
C --> D[状态变化触发DOM重新渲染]
E[用户界面行为] --> F[增强标记]
G[链接点击] --> H[路由器确定布局文件]
I[REST API] --> J[提供模块模板和JSON布局]
6. 最终架构
初始服务器端请求和所有后续客户端请求的完整生命周期如图所示(此处省略原书中的图),涉及各个组件的使用。
综上所述,在同构应用开发中,通过模块对象、转译视图模型、布局定义、页面生成器和前端单页应用的设计,实现了前后端代码的有效共享和高效开发。同时,JavaScript的发展为同构应用提供了更多可能性,未来有望在更多技术领域看到同构应用的发展。
同构应用开发实践与JavaScript发展趋势
7. 后续改进方向
虽然通过共享各种组件大大减少了应用中的代码重复,但仍存在一些重复的地方,例如控制器和路由。基于布局文件的经验,可以将路由和部分控制器逻辑表示为共享的JSON文件,然后输入到C#和JavaScript实现的轻量级解析器中。
8. 同构JavaScript的独特优势
与一些允许在非JavaScript引擎上渲染Angular应用和React组件的解决方案不同,当前的解决方案在ASP.NET生态系统中具有独特性,不依赖特定框架。后端团队在开发过程中只需编写一些额外代码(如Page Maker),数据服务、控制器和应用逻辑基本保持不变。通过解耦,后端能够移除大量前端UI代码,使后端应用更加精简,目标更加明确。该应用在一定程度上实现了“同构”,同时保持了清晰的关注点分离。
9. 设计模式与同构JavaScript的演变
- 设计模式的家族性 :软件设计模式的发展类似于宗教,同一模式的核心思想有多种解释,导致不同的具体实现,这就是“设计模式家族”原则。以MVC模式为例,Xerox Palo Alto Research Center(PARC)在SmallTalk中的原始实现与Ruby on Rails中的Model2-like实现有很大差异,主要是因为Ruby on Rails是基于服务器的框架,而原始MVC实现侧重于前端桌面GUI。
- Flux模式的兴起 :随着同构JavaScript的发展,设计模式和软件架构需要不断演变。向同构JavaScript和单向不可变数据流的转变通过Flux模式家族的出现验证了“设计模式家族”原则。Flux催生了大量实现,其中大多数是同构的。这些实现的流行程度与它们支持客户端和服务器端渲染的能力相关。React、Flux和Redux等流行实现的兴起,验证了同构JavaScript的重要性。
以下是一个简单的表格,对比不同实现的MVC模式:
| 实现 | 特点 | 应用场景 |
| ---- | ---- | ---- |
| SmallTalk MVC | 侧重于前端桌面GUI | 早期桌面应用开发 |
| Ruby on Rails MVC | 基于服务器的框架 | 现代Web应用开发 |
10. JavaScript的发展趋势
- 无处不在的JavaScript :自2011年10月首次提及同构JavaScript以来,JavaScript的应用范围不断扩大。以npm模块总数为增长指标,2011年末至2016年年中,JavaScript的增长超过了50倍,且增长速度没有放缓的迹象。JavaScript几乎无处不在,为众多平台、设备和终端用途提供支持。
- 工具链的变革 :Traceur等JavaScript转译器和npm、browserify、webpack等工具的出现,使转译变得更加普遍和容易。在需要使用自定义语法(如React和JSX)的框架中,使用转译器变得必不可少。这为同构JavaScript创造了友好的环境,即使库不完全适合应用环境,转译和打包工具链也通常能使其可用。
- WebAssembly的潜力 :WebAssembly是一种新的可移植、高效的格式,适合编译到Web。它为许多语言提供了可行的编译目标,对于同构代码的发展具有无限可能,超越了同构JavaScript的范畴。
11. 术语与理解
- 术语的由来 :有人会问,为什么要为在客户端和服务器上运行相同代码创造一个新术语。原因是当时没有合适的术语来描述这一现象。而将其与数学中的“同构”概念联系起来,是因为前端构建系统和转译器在某种程度上体现了“同构”的含义。
- 同构与通用JavaScript的区别 :经常会有关于“同构JavaScript”和“通用JavaScript”的争论。实际上,同构JavaScript可以看作一个频谱,不同类型的同构JavaScript有不同的复杂度和环境适应性:
- 环境无关的JavaScript可以“通用”运行,无需修改。
- 为每个环境进行填充的JavaScript只有在修改环境后才能在不同环境中运行。
- 使用填充语义的JavaScript可能需要修改环境或在不同环境中表现略有不同。
- 因此,如果“通用JavaScript”指的是无需修改代码或环境即可实现同构的代码,那么环境无关的JavaScript显然是通用的。随着对代码或运行环境的修改需求增加,代码就更倾向于同构。
以下是一个mermaid流程图,展示同构与通用JavaScript的关系:
graph LR
A[环境无关JavaScript] --> B[通用JavaScript]
C[需填充环境的JavaScript] --> D[同构JavaScript]
E[使用填充语义JavaScript] --> D
12. 总结与展望
对于开发者来说,技术术语的争论并不重要,关键是利用已有的理解去做有趣或创新的事情。同构应用的发展表明,同构原则可以应用于任何技术栈,无需依赖可能在一年内过时的前端框架,也无需从头重建应用。希望其他开发团队能从这些经验中获得启发,未来能在更多技术领域看到同构应用的发展。
综上所述,同构应用开发结合了模块管理、视图模型转译、布局设计等多种技术手段,同时受益于JavaScript的发展和新兴技术的出现。随着技术的不断进步,同构应用有望在更多场景中得到应用,为开发者带来更多便利和创新机会。
超级会员免费看
47

被折叠的 条评论
为什么被折叠?



