(六)Nginx + Node.js + Java 的软件栈部署实践
最后更新于:2022-04-02 00:34:27
> 原文:http://ued.taobao.org/blog/2014/05/midway-deploy/
> 作者:渐飞
## 起
关于前后端分享的思考,我们已经有五篇文章阐述思路与设计。本文介绍淘宝网[收藏夹](http://shoucang.taobao.com/)将 Node.js 引入传统技术栈的具体实践。
淘宝网线上应用的传统软件栈结构为 Nginx + Velocity + Java,即:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce1c50363d.png)
在这个体系中,Nginx 将请求转发给 Java 应用,后者处理完事务,再将数据用 Velocity 模板渲染成最终的页面。
引入 Node.js 之后,我们势必要面临以下几个问题:
1. 技术栈的拓扑结构该如何设计,部署方式该如何选择,才算是科学合理?
2. 项目完成后,该如何切分流量,对运维来说才算是方便快捷?
3. 遇到线上的问题,如何最快地解除险情,避免更大的损失?
4. 如何确保应用的健康情况,在负载均衡调度的层面加以管理?
## 承
### 系统拓扑
按照我们在[前后端分离的思考与实践(二)- 基于前后端分离的模版探索](http://ued.taobao.org/blog/2014/04/xtpl/)一文中的思路,Velocity 需要被 Node.js 取代,从而让这个结构变成:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce1c718bed.png)
这当然是最理想的目标。然而,在传统栈中首次引入 Node.js 这一层毕竟是个新尝试。为了稳妥起见,我们决定只在收藏夹的宝贝收藏页面([shoucang.taobao.com/item_collect.htm](http://shoucang.taobao.com/item_collect.htm))启用新的技术,其它页面沿用传统方案。即,由 Nginx 判断请求的页面类型,决定这个请求究竟是要转发给 Node.js 还是 Java。于是,最后的结构成了:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce1c74da4d.png)
### 部署方案
上面的结构看起来没什么问题了,但其实新问题还等在前面。在传统结构中,Nginx 与 Java 是部署在同一台服务器上的,Nginx 监听 80 端口,与监听高位 7001 端口的 Java 通信。现在引入了 Node.js ,需要新跑一个监听端口的进程,到底是将 Node.js 与 Nginx + Java 部署在同一台机器,还是将 Node.js 部署在单独的集群呢?
我们来比较一下两种方式各自特点:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce1c77d208.png)
淘宝网收藏夹是一个拥有千万级日均 PV 的应用,对稳定性的要求性极高(事实上任何产品的线上不稳定都是不能接受的)。如果采用同集群部署方案,只需要一次文件分发,两次应用重启即可完成发布,万一需要回滚,也只需要操作一次基线包。性能上来说,同集群部署也有一些理论优势(虽然内网的交换机带宽与延时都是非常乐观的)。至于一对多或者多对一的关系,理论上可能做到服务器更加充分的利用,但相比稳定性上的要求,这一点并不那么急迫需要去解决。所以在收藏夹的改造中,我们选择了同集群部署方案。
### 灰度方式
为了保证最大程度的稳定,这次改造并没有直接将 Velocity 代码完全去掉。应用集群中有将近 100 台服务器,我们以服务器为粒度,逐渐引入流量。也就是说,虽然所有的服务器上都跑着 Java + Node.js 的进程,但 Nginx 上有没有相应的转发规则,决定了获取这台服务器上请求宝贝收藏的请求是否会经过 Node.js 来处理。其中 Nginx 的配置为:
~~~
location = "/item_collect.htm" {
proxy_pass http://127.0.0.1:6001; # Node.js 进程监听的端口
}
~~~
只有添加了这条 Nginx 规则的服务器,才会让 Node.js 来处理相应请求。通过 Nginx 配置,可以非常方便快捷地进行灰度流量的增加与减少,成本很低。如果遇到问题,可以直接将 Nginx 配置进行回滚,瞬间回到传统技术栈结构,解除险情。
第一次发布时,我们只有两台服务器上启用了这条规则,也就是说大致有不到 2% 的线上流量是走 Node.js 处理的,其余的流量的请求仍然由 Velocity 渲染。以后视情况逐步增加流量,最后在第三周,全部服务器都启用了。至此,生产环境 100% 流量的商品收藏页面都是经 Node.js 渲染出来的(可以查看源代码搜索 Node.js 关键字)。
## 转
灰度过程并不是一帆风顺的。在全量切流量之前,遇到了一些或大或小的问题。大部分与具体业务有关,值得借鉴的是一个技术细节相关的陷阱。
### 健康检查
在传统的架构中,负载均衡调度系统每隔一秒钟会对每台服务器 80 端口的特定 URL 发起一次 `get` 请求,根据返回的 HTTP Status Code 是否为 `200` 来判断该服务器是否正常工作。如果请求 1s 后超时或者 HTTP Status Code 不为 `200`,则不将任何流量引入该服务器,避免线上问题。
这个请求的路径是 Nginx -> Java -> Nginx,这意味着,只要返回了 `200`,那这台服务器的 Nginx 与 Java 都处于健康状态。引入 Node.js 后,这个路径变成了 Nginx -> Node.js -> Java -> Node.js -> Nginx。相应的代码为:
~~~
var http = require('http');
app.get('/status.taobao', function(req, res) {
http.get({
host: '127.1',
port: 7001,
path: '/status.taobao'
}, function(res) {
res.send(res.statusCode);
}).on('error', function(err) {
logger.error(err);
res.send(404);
});
});
~~~
但是在测试过程中,发现 Node.js 在转发这类请求的时候,每六七次就有一次会耗时几秒甚至十几秒才能得到 Java 端的返回。这样会导致负载均衡调度系统认为该服务器发生异常,随即切断流量,但实际上这台服务器是能够正常工作的。这显然是一个不小的问题。
排查一番发现,默认情况下, Node.js 会使用 `HTTP Agent` 这个类来创建 HTTP 连接,这个类实现了 socket 连接池,每个主机+端口对的连接数默认上限是 5。同时 `HTTP Agent` 类发起的请求中默认带上了 `Connection: Keep-Alive`,导致已返回的连接没有及时释放,后面发起的请求只能排队。
最后的解决办法有三种:
* 禁用 `HTTP Agent`,即在在调用 `get` 方法时额外添加参数 `agent: false`,最后的代码为:
~~~
var http = require('http');
app.get('/status.taobao', function(req, res) {
http.get({
host: '127.1',
port: 7001,
agent: false,
path: '/status.taobao'
}, function(res) {
res.send(res.statusCode);
}).on('error', function(err) {
logger.error(err);
res.send(404);
});
});
~~~
* 设置 `http` 对象的全局 socket 数量上限:
~~~
http.globalAgent.maxSockets = 1000;
~~~
* 在请求返回的时候及时主动断开连接:
~~~
http.get(options, function(res) {
}).on("socket", function (socket) {
socket.emit("agentRemove"); // 监听 socket 事件,在回调中派发 agentRemove 事件
});
~~~
实践上我们选择第一种方法。这么调整之后,健康检查就没有再发现其它问题了。
## 合
Node.js 与传统业务场景结合的实践才刚刚起步,仍然有大量值得深入挖掘的优化点。比比如,让 Java 应用彻底中心化后,是否可以考分集群部署,以提高服务器利用率。或者,发布与回滚的方式是否能更加灵活可控。等等细节,都值得再进一步研究。
';
(五)基于前后端分离的多终端适配
最后更新于:2022-04-02 00:34:25
> 原文:http://ued.taobao.org/blog/2014/05/cross-platform-tpl/
> 作者:筱谷
## 前言
近年来各站点基于 Web 的多终端适配进行得如火如荼,行业间也发展出依赖各种技术的解决方案。有如基于浏览器原生 CSS3 Media Query 的响应式设计、基于云端智能重排的「云适配」方案等。本文则主要探讨在前后端分离基础下的多终端适配方案。
> ### 关于前后端分离
>
> 关于前后端分离的方案,在[《前后端分离的思考与实践(一)》](http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/)中有非常清晰的解释。我们在服务端接口和浏览器之间引入 NodeJS 作为渲染层,因为 NodeJS 层彻底与数据抽离,同时无需关心大量的业务逻辑,所以十分适合在这一层进行多终端的适配工作。
## UA 探测
进行多终端适配首先要解决的是 UA 探测问题,对于一个过来的请求,我们需要知道这个设备的类型才能针对对它输出对应的内容。现在市面上已经有非常成熟的兼容大量设备的 User Agent 特征库和探测工具,[这里有 Mozilla 整理的一个列表](https://wiki.mozilla.org/Compatibility/UADetectionLibraries)。其中,既有运行在浏览器端的,也有运行在服务端代码层的,甚至有些工具提供了 Nginx/Apache 的模块,负责解析每个请求的 UA 信息。
实际上我们推荐最后一种方式。基于前后端分离的方案决定了 UA 探测只能运行在服务器端,但如果把探测的代码和特征库耦合在业务代码里并不是一个足够友好的方案。我们把这个行为再往前挪,挂在 Nginx/Apache 上,它们负责解析每个请求的 UA 信息,再通过如 HTTP Header 的方式传递给业务代码。
这样做有几点好处:
1. 我们的代码里面无需再去关注 UA 如何解析,直接从上层取出解析后的信息即可。
2. 如果在同一台服务器上有多个应用,则能够共同使用同一个 Nginx 解析后的 UA 信息,节省了不同应用间的解析损耗。
![https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce1565b106.png](http://gtms03.alicdn.com/tps/i3/T1s8L7FFNcXXbvv3MO-968-509.png)
来自天猫分享的基于 Nginx 的 UA 探测方案
淘宝的 Tengine Web 服务器也提供了类似的模块 [ngx_http_user_agent_module](http://tengine.taobao.org/document/http_user_agent.html)。
值得一提的是,选用 UA 探测工具时必须要考虑特征库的可维护性,因为市面上新增的设备类型越来越多,每个设备都会有独立的 User Agent,所以该特征库必须提供良好的更新和维护策略,以适应不断变化的设备。
## 建立在 MVC 模式中的几种适配方案
取得 UA 信息后,我们就要考虑如果根据指定的 UA 进行终端适配了。即使在 NodeJS 层,虽然没有了大部分的业务逻辑,但我们依然把内部区分为 Model / Controller / View 三个模型。
![https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce157f17cc.png](http://gtms04.alicdn.com/tps/i4/T1oGP_FIlbXXc1sab4-200-220.png)
我们先利用上面的图,去解析一些已有的多终端适配方案。
### 建立在 Controller 上的适配方案
![https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce158103eb.png](http://gtms03.alicdn.com/tps/i3/T1YkZcFMhXXXa36h6P-507-400.png)
这种方案应该是最简单粗暴的处理方法。通过路由(Router)将相同的 URL 统一传递到同一个控制层(Controller)。控制层再通过 UA 信息将数据和模型(Model)逻辑派发到对应的展现(View)进行渲染,渲染层则按预先的约定提供了适配几个终端的模板。
这种方案的好处是,保持了数据和控制层的统一性,业务逻辑只需处理一次遍可以应用在所有终端上。但这种场景只适合如展示型页面等低交互型的应用,一旦业务比较复杂,各个终端的 Controller 可能有各自的处理逻辑,如果还是共用一个 Controller ,会导致 Controller 非常的臃肿而且难以维护,这无疑是一个错误的选择。
### 建立在 Router 上的适配方案
为了解决上面遇到的问题,我们可以在 Router 上就将设备区分,针对不同的终端分发到不同的 Controller 上:
![https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce159b2741.png](http://gtms03.alicdn.com/tps/i3/T1UQr7FK8cXXXKYi.n-531-348.png)
这也是最常见的方案之一,大多表现在针对不同终端使用各自独立的一套应用。如 PC 淘宝首页和 WAP 版的淘宝首页,不同设备访问 www.taobao.com ,服务器会通过 Router 的控制,重定向到 WAP 版的淘宝首页或者 PC 版的淘宝首页,它们各自是完全独立的两套应用。
但这种方案无疑带来了数据和部分逻辑无法共用的问题,各种终端之间无法分享同一份数据和业务逻辑,产生大量重复性工作,效率低下。
为了缓解这个问题,有人提出了优化后的方案:依然是在同一套应用里面,各个数据来源抽象成各个 Model,提供给不同终端的 Controller 组合使用:
![https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce159cb601.png](http://gtms01.alicdn.com/tps/i1/T1pR_fFGxaXXXGaK2H-685-520.png)
这个方案解决了前面数据无法共用的问题。在 Controller 上各个终端还是相互独立,但能共同使用同一批数据源,至少在数据上无需再针对终端类型开发独立的接口了。
以上两种基于 Router 的方案,由于 Controller 的独立,各个终端可以为自己的页面实现不同的交互逻辑,保证了各终端自身足够的灵活度,这也是为什么大部分应用采用这种方案的主要原因。
### 建立在 View 层的适配方案
这是淘宝下单页面使用的方案,不过区别是下单页将整体的渲染层放在了浏览器端,而不是 NodeJS 层。不过无论是浏览器还是 NodeJS,整体设计思路还是一致的:
![https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bce159e52d1.png](http://gtms02.alicdn.com/tps/i2/T18IvnFMtXXXc.mb7O-1327-494.png)
在这个方案里面,Router、Controller 和 Model 都无需关注设备信息,终端类型的判断完全交给展现层来处理。图中主要的模块是「View Factory」,Model 和 Controller 将数据和渲染逻辑传递过来之后,通过 View Factory 根据设备信息和其它状态(不仅仅是 UA 信息、也可以是网络环境、用户地区等等)从一堆预设好的组件(View Component)中抓取特定的组件,再组合成最终的页面。
这种方案有几个优势:
1. 上层无需关注设备信息(UA),多终端的视频还是交由和最终展现最大关系的 View 层来处理;
2. 不仅仅是多终端适配,除了 UA 信息,各个 View Component 还可以根据用户状态决定自身输出何种模版,如低网速下默认隐藏图片、指定地区输出活动 Banner。
3. 每个 View Component 的不同模版间可以自行决定是否使用同一份数据、业务逻辑,提供十分灵活的实现方式。
但明显的是,这个方案也是最复杂的,尤其是要考虑一些富交互的应用场景时,Router 和 Controller 也许无法保持这么纯粹。特别对于一些整体性比较强的业务,本身无法被拆分成组件,这种方案也许并不适用;而且对于一些简单的业务,使用这种架构可能不是最佳的选择。
## 总结
以上几种方案,都各自体现在 MVC 模型中的一个或多个部分,在业务上如果一个方案不满足需求,更可以采取多个方案同时采用的方式。或是可以理解为,业务上的复杂度和交互属性决定了该产品更适合采用哪种多终端适配方案。
对比基于浏览器的响应式设计方案,因为绝大部分终端探测和渲染逻辑迁移到了服务端,所以在 NodeJS 层进行适配无疑带来了更好的性能和用户体验;另外,相对于一些所谓的「云适配」方案带来的转换质量问题,在基于前后端分离的「定制式」方案中也不会存在。前后端分离的适配方案在这些方面有着天然优势。
最后,为了适应更灵活的强大的适配需求,基于前后端分离的适配方案将会面临更多挑战!
';
(四)前后端分离模式下的安全解决方案
最后更新于:2022-04-02 00:34:23
原文:http://ued.taobao.org/blog/2014/05/midway-security/
作者:lorrylockie
> ## 前言
>
> 在前后端分离的开发模式中,从开发的角色和职能上来讲,一个最明显的变化就是:以往传统中,只负责浏览器环境中开发的前端同学,需要涉猎到服务端层面,编写服务端代码。而摆在面前的一个基础性问题就是如何保障Web安全?
本文就在前后端分离模式的架构下,针对前端在Web开发中,所遇到的安全问题以及应对措施和注意事项,并提出解决方案。
## 跨站脚本攻击(XSS)的防御
### 问题及解决思路
跨站脚本攻击(XSS,Cross-site scripting)是最常见和基本的攻击Web网站的方法。攻击者可以在网页上发布包含攻击性代码的数据,当浏览者看到此网页时,特定的脚本就会以浏览者用户的身份和权限来执行。通过XSS可以比较容易地修改用户数据、窃取用户信息以及造成其它类型的攻击,例如:CSRF攻击。
预防XSS攻击的基本方法是:确保任何被输出到HTML页面中的数据以HTML的方式进行转义(HTML escape)。例如下面的模板代码:
~~~
~~~
这段代码中的`$description`为模板的变量(不同模板中定义的变量语法不同,这里只是示意一下),由用户提交的数据,那么攻击者可以输入一段包含”JavaScript”的代码,使得上述模板语句的结果变成如下的结果:
~~~
~~~
上述代码,在浏览器中渲染,将会执行JavaScript代码并在屏幕上alert hello。当然这个代码是无害的,但攻击者完全可以创建一个JavaScript来修改用户资料或者窃取cookie数据。
解决方法很简单,就是将`$description`的值进行html escape,转义后的输出代码如下
~~~
~~~
以上经过转义后的HTML代码是没有任何危害的。
### Midway的解决方案
#### 转义页面中所有用户输出的数据
对数据进行转义有以下几种情况和方法:
##### 1\. 使用模板内部提供的机制进行转义
中途岛内部使用KISSY xtemplate作为模板语言。
在xtemplate实现中,语法上使用两个中括号( `{{val}}`)解析模板数据, ,默认既是对数据进行HTML转义的,所以开发者可以这样写模板:
~~~
~~~
在xtemplate中,如果不希望输出的数据被转义,需要使用三个中括号({{{val}}})。
##### 2\. 在Midway中明确的调用转义函数
开发者可以在Node.js程序或者模板中,直接调用Midway提供的HTML转义方法,显示的对数据进行转义,如下:
方法1:在Node.js程序中对数据进行HTML转义
~~~
var Security= require('midway-security');
//data from server,eg {html:'',other:""}
data.html =Security.escapeHtml(data.html);
xtpl = xtpl.render(data);
~~~
方法2:在模板中对HTML数据进行HTML转义
~~~
~~~
注意:只有当模板内部没有对数据进行转义的时候才使用Security.escapeHtml进行转义。 否则,模板内部和程序会两次转义叠加,导致不符合预期的输出。
推荐:如果使用xtemplate,建议直接使用模板内置的`{{}}`进行转义; 如果使用其他模板,建议使用`Security.escapeHtml`进行转义。
#### 过滤页面中用户输出的富文本
你可能会想到:“其实我就是想输出富文本,比如一些留言板、论坛给用户提供一些简单的字体大小、颜色、背景等功能,那么我该如何处理这样的富文本来防止XSS呢?”
##### 1\. 使用Midway中Security提供的richText函数
Midway中提供了richText方法,专门用来过滤富文本,防止XSS、钓鱼、cookie窃取等漏洞。
有一个留言板,模板代码可能如下:
~~~
我在留言中
~~~
上述的富文本数据如果直接输出到页面中,必然会导致eval.com站点的js注入到当前页面中,造成了XSS攻击。为了防止这个漏洞,我们只要在模板或者程序中,调用Security.richText方法,处理用户输入的富文本。
调用方法与escapeHtml类似,有如下两种方式
方法1: 直接在Node.js程序中调用
~~~
message =Security.richText(message);
var html = xtpl.render(message)
~~~
方法2: 在模板中调用
~~~
~~~
通过调用Security的richText方法后,最终的输出如下:
~~~
~~~
可以看出,首先:会造成XSS攻击的`script`标签被直接过滤掉;同时style标签中CSS属性`position:fixed;`样式也被过滤了。最终输出了无害的HTML富文本
#### 了解其他可能导致XSS攻击的途径
除了在页面的模板中可能存在XSS攻击之外,在Web应用中还有其他几个途径也可能会有风险。
##### 1\. 出错页面的漏洞
一个页面如果找不到,系统可能会报一个404 Not Found的错误,例如:[http://localhost/page/not/found](http://localhost/page/not/found)
~~~
404 NotFound
Page /page/not/found does not exsit
~~~
很显然:攻击者可以利用这个页面,构造一个类似这样的连接,`http://localhost/%3Cscript%3Ealert%28%27hello%27%29%3C%2Fscript%3E`,并引诱受害者点击 ;假如出错页面未对输出变量进行转义的话,那么连接中隐藏的 `` 将会被执行。
在express中,发送一个404页面的方法如下
~~~
res.send(404,'Sorry,we don\'t find that!')
~~~
这里就需要开发者注意错误页面(404或者其他错误状态)的处理方式。如果错误信息的返回内容带有路径信息(其实更准确的讲,是用户输入信息),就一定要进行escapeHtml了。
后续,错误处理的安全机制,会在Midway框架层面中完成。
### Midway解决方案的补充说明
#### 其他模板引擎
Midway默认支持xtemplate模板,但将来也有可能支持其他模板:如jade、mustache、ejs等。目前在主流模板中,都提供了默认转义和不转义的输出变量写法,需要开发者特别留意其安全性。
#### 关于escape的其他支持
除了对页面中输出的普通数据和富文本数据,一些场景中也还包含其他可能需要转义的情况,Midway提供了如下几个常用的转义方法,供开发者使用:
* escapeHtml 过滤指定的HTML中的字符,防XSS漏洞
* jsEncode 对输入的String进行JavaScript 转义 对中文进行unicode转义,单引号,双引号转义
* escapeJson 不破坏JSON结构的escape函数,只对json结构中name和vaule做escapeHtml处理
* escapeJsonForJsVar 可以理解就是jsEncode+escapeJson
例子如下
~~~
var jsonText ="{\"
';
~~~
因为message是用户的输入数据,其留言板的内容,包含了富文本信息,所以这里在xtemplate中,使用了三个大括号,默认不进行HTML转义;那么用户输入的数据假如如下:
~~~
(三)Midway-ModelProxy — 轻量级的接口配置建模框架
最后更新于:2022-04-02 00:34:20
> 原文:http://ued.taobao.org/blog/2014/04/modelproxy/
> 作者:善繁
## 前言
使用Node做前后端分离的开发模式带来了一些性能及开发流程上的优势(见[《前后端分离的思考与实践 一》](http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/)), 但同时也面临不少挑战。在淘宝复杂的业务及技术架构下,后端必须依赖Java搭建基础架构,同时提供相关业务接口供前端使用。Node在整个环境中最重要的工作之一就是代理这些业务接口,以方便前端(Node端和浏览器端)整合数据做页面渲染。如何做好代理工作,使得前后端开发分离之后,仍然可以在流程上无缝衔接,是我们需要考虑的问题。本文将就该问题做相关探讨,并提出解决方案。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcdebfd9bb6.jpg)
**由于**后端提供的接口方式可能多种多样,同时开发人员在编写Node端代码访问这些接口的方式也有可能多种多样。如果我们在接口访问方式及使用上不做统一架构处理,则会带来以下一些问题:
1\. 每一个开发人员使用各自的代码风格编写接口访问代码,造成工程目录及编码风格混乱,维护相对困难。
2\. 每一个开发人员编写自己的mock数据方式,开发完毕之后,需要手工修改代码移除mock。
3\. 每一个开发人员为了实现接口的不同环境切换(日常,预发,线上),可能各自维护了一些配置文件。
4\. 数据接口调用方式无法被各个业务model非常方便地复用。
5\. 对于数据接口的描述约定散落在代码的各个角落,有可能跟后端人员约定的接口文档不一致。
6\. 整个项目分离开发之后,对于接口的联调或者测试回归成本依然很高,需要涉及到每一个接口提供者和使用者。
于是我们希望有这样一个框架,通过该框架提供的机制去描述工程项目中依赖的所有外部接口,对他们进行统一管理,同时提供灵活的接口建模及调用方式,并且提供便捷的线上环境和生产环境切换方法,使前后端开发无缝结合。ModelProxy就是满足这样要求的轻量级框架,它是Midway Framework 核心构件之一,也可以单独使用。使用ModelProxy可以带来如下优点:
1\. 不同的开发者对于接口访问代码编写方式统一,含义清晰,降低维护难度。
2\. 框架内部采用工厂+单例模式,实现接口一次配置多次复用。并且开发者可以随意定制组装自己的业务Model(依赖注入)。
3\. 可以非常方便地实现线上,日常,预发环境的切换。
4\. 内置[river-mock](http://gitlab.alibaba-inc.com/river/mock/tree/master)和[mockjs](http://mockjs.com/)等mock引擎,提供mock数据非常方便。
5\. 使用接口配置文件,对接口的依赖描述做统一的管理,避免散落在各个代码之中。
6\. 支持浏览器端共享Model,浏览器端可以使用它做前端数据渲染。整个代理过程对浏览器透明。
7\. 接口配置文件本身是结构化的描述文档,可以使用[river](http://gitlab.alibaba-inc.com/river/spec/tree/master)工具集合,自动生成文档。也可使用它做相关自动化接口测试,使整个开发过程形成一个闭环。
## ModelProxy工作原理图及相关开发过程图览
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcdec00206c.jpg)
在上图中,开发者首先需要将工程项目中所有依赖的后端接口描述,按照指定的json格式,写入interface.json配置文件。必要时,需要对每个接口编写一个规则文件,也即图中interface rules部分。该规则文件用于在开发阶段mock数据或者在联调阶段使用River工具集去验证接口。规则文件的内容取决于采用哪一种mock引擎(比如 mockjs, river-mock 等等)。配置完成之后,即可在代码中按照自己的需求创建自己的业务model。
下面是一个简单的例子:
【例一】
* 第一步 在工程目录中创建接口配置文件interface.json, 并在其中添加主搜接口json定义
~~~
{
"title": "pad淘宝项目数据接口集合定义",
"version": "1.0.0",
"engine": "mockjs",
"rulebase": "./interfaceRules/",
"status": "online",
"interfaces": [ {
"name": "主搜索接口",
"id": "Search.getItems",
"urls": {
"online": "http://s.m.taobao.com/client/search.do"
}
} ]
}
~~~
* 第二步 在代码中创建并使用model
~~~
// 引入模块
var ModelProxy = require( 'modelproxy' );
// 全局初始化引入接口配置文件 (注意:初始化工作有且只有一次)
ModelProxy.init( './interface.json' );
// 创建model 更多创建模式请参后文
var searchModel = new ModelProxy( {
searchItems: 'Search.getItems' // 自定义方法名: 配置文件中的定义的接口ID
} );
// 使用model, 注意: 调用方法所需要的参数即为实际接口所需要的参数。
searchModel.searchItems( { q: 'iphone6' } )
// !注意 必须调用 done 方法指定回调函数,来取得上面异步调用searchItems获得的数据!
.done( function( data ) {
console.log( data );
} )
.error( function( err ) {
console.log( err );
} );
~~~
ModelProxy的功能丰富性在于它支持各种形式的profile以创建需要业务model:
* 使用接口ID创建>生成的对象会取ID最后’.’号后面的单词作为方法名
~~~
ModelProxy.create( 'Search.getItem' );
~~~
* 使用键值JSON对象>自定义方法名: 接口ID
~~~
ModelProxy.create( {
getName: 'Session.getUserName',
getMyCarts: 'Cart.getCarts'
} );
~~~
* 使用数组形式>取最后 . 号后面的单词作为方法名
下例中生成的方法调用名依次为: Cart_getItem, getItem, suggest, getName
~~~
ModelProxy.create( [ 'Cart.getItem', 'Search.getItem', 'Search.suggest', 'Session.User.getName' ] );
~~~
* 前缀形式>所有满足前缀的接口ID会被引入对象,并取其后半部分作为方法名
~~~
ModelProxy.create( 'Search.*' );
~~~
同时,使用这些Model,你可以很轻易地实现合并请求或者依赖请求,并做相关模板渲染
【例二】 合并请求
~~~
var model = new ModelProxy( 'Search.*' );
// 合并请求 (下面调用的model方法除done之外,皆为配置接口id时指定)
model.suggest( { q: '女' } )
.list( { keyword: 'iphone6' } )
.getNav( { key: '流行服装' } )
.done( function( data1, data2, data3 ) {
// 参数顺序与方法调用顺序一致
console.log( data1, data2, data3 );
} );
~~~
【例三】 依赖请求
~~~
var model = new ModelProxy( {
getUser: 'Session.getUser',
getMyOrderList: 'Order.getOrder'
} );
// 先获得用户id,然后再根据id号获得订单列表
model.getUser( { sid: 'fdkaldjfgsakls0322yf8' } )
.done( function( data ) {
var uid = data.uid;
// 二次数据请求依赖第一次取得的id号
this.getMyOrderList( { id: uid } )
.done( function( data ) {
console.log( data );
} );
} );
~~~
此外ModelProxy不仅在Node端可以使用,也可以在浏览器端使用。只需要在页面中引入官方包提供的modelproxy-client.js即可。
【例四】浏览器端使用ModelProxy
~~~
~~~
~~~
~~~
同时,ModelProxy可以配合Midway另一核心组件[Midway-XTPL](http://search.npm.taobao.net/package/midway-xtpl)一起使用,实现数据和模板以及相关渲染过程在浏览器端和服务器端的全共享。关于ModelProxy的详细教程及文档请移步[https://github.com/purejs/modelproxy](https://github.com/purejs/modelproxy)
## 总结
ModelProxy以一种配置化的轻量级框架存在,提供友好的接口model组装及使用方式,同时很好的解决前后端开发模式分离中的接口使用规范问题。在整个项目开发过程中,接口始终只需要定义描述一次,前端开发人员即可引用,同时使用River工具自动生成文档,形成与后端开发人员的契约,并做相关自动化测试,极大地优化了整个软件工程开发过程。
【注】River 是阿里集团研发的前后端统一接口规范及相关工具集合的统称
';
(二)基于前后端分离的模版探索
最后更新于:2022-04-02 00:34:18
> 原文:http://ued.taobao.org/blog/2014/04/xtpl/
> 作者:Herman
## 前言
在做前后端分离时,第一个关注到的问题就是 渲染,也就是 View 这个层面的工作。
在传统的开发模式中,浏览器端与服务器端是由不同的前后端两个团队开发,但是模版却又在这两者中间的模糊地带。因此模版上面总不可避免的越来越多复杂逻辑,最终难以维护。
而我们选择了NodeJS,作为一个前后端的中间层。试图藉由NodeJS,来疏理 View 层面的工作。
使得前后端分工更明确,让专案更好维护,达成更好的用户体验。
## 本文
渲染这块工作,对于前端开发者的日常工作来说,佔了非常大的比例,也是最容易与后端开发纠结不清的地方。
回首过去前端技术发展的这几年, View 这个层面的工作,经过了许多次的变革,像是:
1. Form Submit 全页刷新 => _AJAX局部刷新_
2. 服务端续染 + MVC => _客户端渲染 + MVC_
3. 传统换页跳转 => 单页面应用
可以观察到在这几年,大家都倾向将 渲染 这件事,从服务器端端移向了浏览器端。
而服务器端则专注于 服务化 ,提供数据接口。
### 浏览器端渲染的好处
浏览器端渲染的好处,我们都很清楚,像是
1. 摆脱业务逻辑与呈现逻辑在Java模版引擎中的耦合与混乱。
2. 针对多终端应用,更容易以接口化的形式。在浏览器端搭配不同的模版,呈现不同的应用。
3. 页面呈现本来就不仅是html,在前端的渲染可以更轻易的以组件化形式 (html + js + css)提供功能,使得前端组件不需依赖于服务端产生的html结构。
4. 脱离对于后端开发、发佈流程的依赖。
5. 方便联调。
### 浏览器端渲染造成的坏处
但是在享受好处的同时,我们同样的也面临了 浏览器端渲染 所带来的坏处,像是:
1. 模版分离在不同的库。有的模版放在服务端 (JAVA),而有的放在浏览器端 (JS)。前后端模版语言不相通。
2. 需要等待所有模版与组件在浏览器端载入完成后才能开始渲染,无法即开即看。
3. 首次进入会有白屏等待渲染的时间,不利于用户体验
4. 开发单页面应用时,前端Route与服务器端Route不匹配,处理起来很麻烦。
5. 重要内容都在前端组装,不利于SEO
### 反思前后端的定义
其实回头想想,在我们把渲染的工作从 服务端(Java) 抽出来到 浏览器端(JS) 的时候,我们的目的只是 明确的前后端职责划分,并不是非浏览器渲染不可 。
只是因为在传统的开发模式中,出了服务器就到了浏览器,所以前端的工作内容只能被限制在浏览器端。
也因此很多人认定了 `后端 = 服务端` `前端 = 浏览器端` ,就像下面这张图。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcde58e135f.png)
而在淘宝UED目前进行的 中途岛Midway 项目中,藉由在 JAVA – Browser中间搭建一个NodeJS中间层,试图把这个前后端的分割线,重新针对 工作职责 去区分,而非针对硬体环境去区分(服务器 & 浏览器)。
因此我们有机会做到模版与路由的共享,也是一个前后端分工中最理想的状态。
* * *
## 淘宝中途岛 Midway
在中途岛项目中,我们把前后端分界的那条线,从浏览器端移回到了服务器端。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcde58f3740.png)
藉由一个由前端 轻松掌控 且 与浏览器共通 的Nodejs层,可以更清晰的完成了前后端分离。
也可以让前端开发针对不同的情况,自行决定 最适当的解决方案 。而不是所有事情 都在浏览器端来处理 。
### 职责划分
中途岛并不是前端试图抢后端饭碗的项目,目的只是把 模版 这个模糊地带切割清楚,取得更明确的职责划分。
* 后端 (JAVA),专注于
1. 服务层
2. 数据格式、数据稳定
3. 业务逻辑
* 前端,专注于
1. UI层
2. 控制逻辑、渲染逻辑
3. 交互、用户体验
而不再拘泥于服务端或浏览器端的差异。
### 模版共享
在传统的开发模式中,浏览器端与服务器端是由不同的前后端两个团队开发,但是模版却又在这两者中间的模糊地带。因此模版上面总不可避免的越来越多复杂逻辑,最终难以维护。
有了NodeJS,后端同学可以在JAVA层专注于业务逻辑与数据的开发。而前端同学则专注于控制逻辑与渲染的开发。并且自行选择这些模版是要在 服务端 (NodeJS) 或是 浏览器端 做渲染。
用著一样的模版语言 _XTemplate_ ,一样的渲染引擎 _JavaScript_
在 不同的渲染环境 (Server-side、PC Browser、Mobile Browser、Web View、etc.) 渲染出 一样的结果 。
### 路由共享
也因为有了NodeJS这一层,可以更细致的控制路由。
假如需要在前端做浏览器端路由时,可以同时配置服务器端的路由,使其在 浏览器端换页 或是 服务端换页 ,都可以得到一致的渲染效果。
同时也处理了SEO的问题。
* * *
## 模版共享的实践
通常我们在浏览器端渲染一份模版时,流程不外乎是
1. 在浏览器端載入模版引擎 (xtmpleate, juicer, handlerbar, etc.)
2. 在浏览器端载入模版档案,方法可能有
* 使用 `` 印在页面上
* 使用模块载入工具,载入模版档案 (KISSY, requireJS, etc.)
* 其他
3. 取得数据,使用模版引擎产生html
4. 将html插入到指定位置。
從以上的流程可以觀察到,要想要做到模版的跨端共享,重点其实在 一致的模块选型 这件事。
市面上流行很多种模块标准,例如 KMD、AMD、CommonJS,只要能将NodeJS的模版档案透过一致模块规范输出到NodeJS端,就可以做基本的模版共享了。
而后续的系列文章会针对Model的proxy与共享,做进一步的探讨。
* * *
## 案例探讨
因为有了中途岛这中间层,针对过往的一些问题都有了更好的解答,例如说
### 案例一 复杂交互应用 (如购物车、下单页面)
* 状况:全部的HTML都是在前端渲染完成,服务端仅提供接口。
* 问题:进入页面时,会有短暂白屏。
* 解答:
1. 首次进入页面,在NodeJS端进行 全页渲染 ,并在背景下载相关的模版。
2. 后续交互操作,在浏览器端完成 局部刷新
3. 用的是 同一份模版 , 产生 一样的结果
### 案例二 单页面应用
* 状况:使用Client Side MVC框架,在浏览器换页。
* 问题:渲染与换页都在浏览器端完成,直接输入网址进入或f5刷新时,无法直接呈现同样的内容。
* 解答:
1. 在浏览器端与NodeJS端共享 同样的Route 设定
2. 浏览器端换页时,在浏览器端进行Route变更与 页面内容渲染
3. 直接输入同样的网址时,在NodeJS端进行 页面框架 + 页面内容渲染
4. 不管是浏览器端换页,或直接输入同样的网址,看到的内容都是 一样的 。
5. 除了增加体验、减少逻辑複杂度外。更解决了 SEO 的问题
### 案例三 纯浏览型页面
* 状况:页面仅提供资讯,较少或没有交互
* 问题:html在服务端产生,css与js放在另外一个位置,彼此间有依赖。
* 解答:
1. 透过NodeJS,统一管理html + css + js
2. 日后若需要扩展成复杂应用或是单页面应用,也可以轻易转移。
### 案例四 跨终端页面
* 状况:同样的应用要在不同端点呈现不同的介面与交互
* 问题:html管理不易,常常会在服务端产生不一样的html,浏览器端又要做不一样的处理
* 解答:
1. 跨终端的页面是渲染的问题,统一由前端来处理。
2. 透过NodeJS层与后端服务化,可以针对这类型复杂应用,设计最佳的解决方案。
* * *
## 总结
过去的AJAX、Client-side MVC、SPA、Two-way Data Binding 等技术的出现,都是试图要解决当时的前端开发遇到的瓶颈。
而NodeJS中间层的出现,也是在试图解决现今前端被侷限在浏览器端的一个限制。
这边文章专注于前后端模版共享,也希望能抛砖引玉,与大家一起讨论如何在NodeJS中间层这个架构下,我们可以怎样的改善我们的工作流程,怎样的跟 后端配合,来把 前端 这个工作做得更好。
';
(一)也谈基于NodeJS的全栈式开发
最后更新于:2022-04-02 00:34:16
> 原文:http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/
> 作者:常胤
## 前言
为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异。痛定思痛,今天我们重新思考了“前后端”的定义,引入前端同学都熟悉的NodeJS,试图探索一条全新的前后端分离模式。
随着不同终端(Pad/Mobile/PC)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本。为了提升开发效率,前后端分离的需求越来越被重视,后端负责业务/数据接口,前端负责展现/交互逻辑,同一份数据接口,我们可以定制开发多个版本。
这个话题最近被讨论得比较多,阿里有些BU也在进行一些尝试。讨论了很久之后,我们团队决定探索一套基于NodeJS的前后端分离方案,过程中有一些不断变化的认识以及思考,记录在这里,也希望看到的同学参与讨论,帮我们完善。
## 一、什么是前后端分离?
最开始组内讨论的过程中我发现,每个人对前后端分离的理解不一样,为了保证能在同一个频道讨论,先就什么是”前后端分离”达成一致。
大家一致认同的前后端分离的例子就是SPA(Single-page application),所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现。
从某种意义上来说,SPA确实做到了前后端分离,但这种方式存在两个问题:
* WEB服务中,SPA类占的比例很少。很多场景下还有同步/同步+异步混合的模式,SPA不能作为一种通用的解决方案。
* 现阶段的SPA开发模式,接口通常是按照展现逻辑来提供的,有时候为了提高效率,后端会帮我们处理一些展现逻辑,这就意味着后端还是涉足了View层的工作,不是真正的前后端分离。
SPA式的前后端分离,是从物理层做区分(认为只要是客户端的就是前端,服务器端的就是后端),这种分法已经无法满足我们前后端分离的需求,我们认为从职责上划分才能满足目前我们的使用场景:
* 前端:负责View和Controller层。
* 后端:只负责Model层,业务处理/数据等。
为什么去做这种职责的划分,后面会继续探讨。
## 二、为什么要前后端分离?
关于这个问题,玉伯的文章[Web研发模式演变](https://github.com/lifesinger/lifesinger.github.com/issues/184)中解释得非常全面,我们再大概理一下:
### 2.1 现有开发模式的适用场景
玉伯提到的几种开发模式,各有各的适用场景,没有哪一种完全取代另外一种。
* 比如后端为主的MVC,做一些同步展现的业务效率很高,但是遇到同步异步结合的页面,与后端开发沟通起来就会比较麻烦。
* Ajax为主SPA型开发模式,比较适合开发APP类型的场景,但是只适合做APP,因为SEO等问题不好解决,对于很多类型的系统,这种开发方式也过重。
### 2.2 前后端职责不清
在业务逻辑复杂的系统里,我们最怕维护前后端混杂在一起的代码,因为没有约束,M-V-C每一层都可能出现别的层的代码,日积月累,完全没有维护性可言。
虽然前后端分离没办法完全解决这种问题,但是可以大大缓解。因为从物理层次上保证了你不可能这么做。
### 2.3 开发效率问题
淘宝的Web基本上都是基于MVC框架webx,架构决定了前端只能依赖后端。
所以我们的开发模式依然是,前端写好静态demo,后端翻译成VM模版,这种模式的问题就不说了,被吐槽了很久。
直接基于后端环境开发也很痛苦,配置安装使用都很麻烦。为了解决这个问题,我们发明了各种工具,比如[VMarket](https://www.npmjs.org/package/vmarket),但是前端还是要写VM,而且依赖后端数据,效率依然不高。
另外,后端也没法摆脱对展现的强关注,从而专心于业务逻辑层的开发。
### 2.4 对前端发挥的局限
性能优化如果只在前端做空间非常有限,于是我们经常需要后端合作才能碰撞出火花,但由于后端框架限制,我们很难使用Comet、Bigpipe等技术方案来优化性能。
为了解决以上提到的一些问题,我们进行了很多尝试,开发了各种工具,但始终没有太多起色,主要是因为我们只能在后端给我们划分的那一小块空间去发挥。只有真正做到前后端分离,我们才能彻底解决以上问题。
## 三、怎么做前后端分离?
怎么做前后端分离,其实第一节中已经有了答案:
* 前端:负责View和Controller层。
* 后端:负责Model层,业务处理/数据等。
![MVC分](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcdde7474b8.png)
试想一下,如果前端掌握了Controller,我们可以做url design,我们可以根据场景决定在服务端同步渲染,还是根据view层数据输出json数据,我们还可以根据表现层需求很容易的做Bigpipe,Comet,Socket等等,完全是需求决定使用方式。
### 3.1 基于NodeJS“全栈”式开发
如果想实现上图的分层,就必然需要一种web服务帮我们实现以前后端做的事情,于是就有了标题提到的“基于NodeJS的全栈式开发”
![Node 带来的全栈时代](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcdde7747e8.png)
这张图看起来简单而且很好理解,但没尝试过,会有很多疑问。
* SPA模式中,后端已供了所需的数据接口,view前端已经可以控制,为什么要多加NodeJS这一层?
* 多加一层,性能怎么样?
* 多加一层,前端的工作量是不是增加了?
* 多加一层就多一层风险,怎么破?
* NodeJS什么都能做,为什么还要JAVA?
这些问题要说清楚不容易,下面说下我的认识过程。
### 3.2 为什么要增加一层NodeJS?
现阶段我们主要以后端MVC的模式进行开发,这种模式严重阻碍了前端开发效率,也让后端不能专注于业务开发。
解决方案是让前端能控制Controller层,但是如果在现有技术体系下很难做到,因为不可能让所有前端都学java,安装后端的开发环境,写VM。
NodeJS就能很好的解决这个问题,我们无需学习一门新的语言,就能做到以前开发帮我们做的事情,一切都显得那么自然。
### 3.3 性能问题
分层就涉及每层之间的通讯,肯定会有一定的性能损耗。但是合理的分层能让职责清晰、也方便协作,会大大提高开发效率。分层带来的损失,一定能在其他方面的收益弥补回来。
另外,一旦决定分层,我们可以通过优化通讯方式、通讯协议,尽可能把损耗降到最低。
举个例子:
淘宝宝贝详情页静态化之后,还是有不少需要实时获取的信息,比如物流、促销等等,因为这些信息在不同业务系统中,所以需要前端发送5,6个异步请求来回填这些内容。
有了NodeJS之后,前端可以在NodeJS中去代理这5个异步请求,还能很容易的做Bigpipe,这块的优化能让整个渲染效率提升很多。
可能在PC上你觉得发5,6个异步请求也没什么,但是在无线端,在客户手机上建立一个HTTP请求开销很大,有了这个优化,性能一下提升好几倍。
淘宝详情基于NodeJS的优化我们正在进行中,上线之后我会分享一下优化的过程。
### 3.4 前端的工作量是否增加了?
相对于只切页面/做demo,肯定是增加了一点,但是当前模式下有联调、沟通环节,这个过程非常花时间,也容易出bug,还很难维护。
所以,虽然工作量会增加一点,但是总体开发效率会提升很多。
另外,测试成本可以节省很多。以前开发的接口都是针对表现层的,很难写测试用例。如果做了前后端分离,甚至测试都可以分开,一拨人专门测试接口,一拨人专注测试UI(这部分工作甚至可以用工具代替)。
### 3.5 增加Node层带来的风险怎么控制?
随着Node大规模使用,系统/运维/安全部门的同学也一定会加入到基础建设中,他们会帮助我们去完善各个环节可能出现的问题,保障系的稳定性。
### 3.6 Node什么都能做,为什么还要JAVA?
我们的初衷是做前后端分离,如果考虑这个问题就有点违背我们的初衷了。即使用Node替代Java,我们也没办法保证不出现今天遇到的种种问题,比如职责不清。我们的目的是分层开发,专业的人,专注做专业的事。基于JAVA的基础架构已经非常强大而且稳定,而且更适合做现在架构的事情。
## 四、淘宝基于Node的前后端分离
![淘宝基于NodeJS的前后端分离](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-01_55bcdde7a71ca.jpg)
上图是我理解的淘宝基于Node的前后端分离分层,以及Node的职责范围。简单解释下:
* 最上端是服务端,就是我们常说的后端。后端对于我们来说,就是一个接口的集合,服务端提供各种各样的接口供我们使用。因为有Node层,也不用局限是什么形式的服务。对于后端开发来说,他们只用关心业务代码的接口实现。
* 服务端下面是Node应用。
* Node应用中有一层Model Proxy与服务端进行通讯。这一层主要目前是抹平我们对不同接口的调用方式,封装一些view层需要的Model。
* Node层还能轻松实现原来vmcommon,tms(引用淘宝内容管理系统)等需求。
* Node层要使用什么框架由开发者自己决定。不过推荐使用express+xTemplate的组合,xTemplate能做到前后端公用。
* 怎么用Node大家自己决定,但是令人兴奋的是,我们终于可以使用Node轻松实现我们想要的输出方式:JSON/JSONP/RESTful/HTML/BigPipe/Comet/Socket/同步、异步,想怎么整就怎么整,完全根据你的场景决定。
* 浏览器层在我们这个架构中没有变化,也不希望因为引入Node改变你以前在浏览器中开发的认知。
* 引入Node,只是把本该就前端控制的部分交由前端掌控。
这种模式我们已经有两个项目在开发中,虽然还没上线,但是无论是在开发效率,还是在性能优化方面,我们都已经尝到了甜头。
## 五、我们还需要要做什么?
* 把Node的开发流程集成到淘宝现有的SCM流程中。
* 基础设施建设,比如session,logger等通用模块。
* 最佳开发实践
* 线上成功案例
* 大家对Node前后端分离概念的认识
* 安全
* 性能
* …
技术上不会有太多需要去创新和研究的,已经有非常多现成的积累。其实关键是一些流程的打通和通用解决方案的积累,相信随着更多的项目实践,这块慢慢会变成一个稳定的流程。
## 六、“中途岛”
虽然“基于NodeJS的全栈式开发”模式很让人兴奋,但是把基于Node的全栈开发变成一个稳定,让大家都能接受的东西还有很多路要走,我们正在进行的“中途岛”项目就是为了解决这个问题。虽然我们起步不久,但是离目标已经越来越近!!
';