Better

Ethan的博客,欢迎访问交流

Node与Express开发之进阶篇(三)

最近准备系统的学习Node和Express,学习笔记-进阶篇!主要内容有REST API、静态内容管理优化以及MVC开发通用模式等。

REST API

现在我们将注意力转移到将数据和功能提供给其他程序上。渐渐地,互联网不再是各自为政的网站集合了,而是一个真正的网:网站为了给用户提供更丰富的体验而相互通信。

“Web 服务”是一个通用术语,指任何可以通过 HTTP 访问的应用程序编程界面(API) 。Web 服务的想法已经出现相当长的时间了,但直到不久之前,那些实现 Web服务的技术还是沉闷的、错乱的、过于复杂的。现在仍然有使用那些技术(比如 SOAP 和 WSDL)的系统,也有帮你与这些系统交互的 Node 包。不过我们不会讲到这些,相反,我们的重点是提供“REST 风格”的服务,与其交互要更直接得多。

JSON和XML

提供 API 的关键是有相通的语言。通信部分已经决定了,我们必须用 HTTP 方法跟服务器通信。但在那之后,我们可以用任何数据语言。传统上 XML 是非常流行的选择,并且是很重要的标记语言。尽管 XML 不是特别复杂,但还可以做得更轻量,因此 JavaScript 对象标记(JSON)诞生了。

除了对 JavaScript 非常友善(但它绝不是专有的,它是任何语言都可以解析的简单格式) ,它还有个优势,即一般手写起来也比XML 更容易。

API错误报告

HTTP API 的错误报告一般是通过 HTTP 状态码实现的:如果返回的响应码是 200(OK) ,则客户端知道请求成功了;如果响应码是 500(服务器内部错误),则请求失败了。然而在大多数应用程序中,并不是所有事情都可以(或者应该)粗略地划分成“成功”或“失败” 。比如说,你用 ID 请求某件东西,但如果那个 ID 不存在怎么办?这不是服务器错误:客户端请求了不存在的东西。一般来说,错误可以分为以下几类。

  • 灾难性错误
    导致服务器的状态不稳定或不可知的错误。这种错误一般是未处理异常导致的。从灾难性错误中恢复的唯一办法是重启服务器。理想情况下,所有挂起的请求都会收到响应码500,但如果故障很严重,服务器可能根本无法响应,请求会超时。
  • 可恢复的服务器错误
    可恢复错误不需要服务器重启,或其他任何壮烈的动作。这种错误一般是服务器上未预料到的错误条件导致的(比如不可用的数据库连接) 。问题可能是暂时的或永久的。这种情况下应该返回响应码 500。
  • 客户端错误
    客户端错误是客户端犯了错误,一般是参数漏掉了或参数无效。这时不应该用响应码500,毕竟服务器没有故障。一切都正常,只是客户端没有正确使用 API。此时你有两个选择:可以用状态码200,并在响应体中描述错误,或者你可以尝试额外用恰当的HTTP 状态码描述错误。我建议用后一种方式。这种情况下最合适的响应码是 404(未找到) 、400(错误的请求)和 401(未授权)。此外,响应体中应该有错误具体情况的说明。

跨域资源共享

如果你发布了一个 API,应该很想让其他人能够访问这个 API。这会导致跨站 HTTP 请求。跨站 HTTP 请求一直是很多攻击的对象,因此受到了同源策略的限制,限制可以从哪里加载脚本。具体来说就是协议、域和端口必须匹配。

这使得其他网站不可能使用你的 API,所以有了跨域资源共享(CORS) 。CORS 允许你针对个案解除这个限制,甚至允许你列出具体哪些域可以访问这个脚本。CORS 是通过 Access-Control-Allow-Origin 响应头实现的。

在 Express 程序中最容易的实现方式是用 cors 包( npm install --save cors ) 。要在程序中启用 CORS:

app.use(require('cors')());

基于同源 API 存在的原因(防止攻击) ,我建议只在必要时应用 CORS。就我们的情况而言,想要输出整个 API(但只有 API) ,所以要将 CORS 限制在以“/api”开头的路径上:

app.use('/api',require('cors')());

书写API

Express 十分擅长提供 API。我们也可以使用Node模块connect-rest。

Express提供API

Express本身支持http.VERB,因此没什么好啰嗦的。

使用REST插件

只用 Express 写 API 很容易。然而用 REST 插件有些优势。接下来我们用健壮的 connect-rest 让 API 可以面向未来。

API 不应该跟网站的常规路由冲突(确保你没有创建任何以“/api”开头的网站路由) 。我建议把 API 路由放在网站路由后面: connect-rest 模块会检查每一个请求,向请求对象中添加属性,还会做额外的日志记录。因此把它放在网站路由后面更好,但要在 404 处理器之前:

// 网站路由在这里

// 在这里用 rest.VERB 定义 API 路由……

// API 配置
var apiOptions = {
context: '/api',
domain: require('domain').create(),
};
// 将 API 连入管道
app.use(rest.rester(apiOptions));
// 404 处理器在这里

connect-rest 已经提高了一点效率:我们可以自动给所有 API 调用加上前缀“/api” 。这减少了手误的几率,并且可以在需要时轻松修改根 URL。

rest.VERB添加api和express有什么不同呢?

rest.get('/attractions', function(req, content, cb){
    Attraction.find({ approved: true }, function(err, attractions){
        if(err) return cb({ error: 'Internal error.' });
        cb(null, attractions.map(function(a){
            return {
                name: a.name,
                description: a.description,
                location: a.location,
            };
        }));
    });
});

REST 函数不是只有常见的请求 / 响应两个参数,而是有三个:一个请求(跟平常一样) ;一个内容对象,是请求被解析的主体;一个回调函数,可以用于异步 API 的调用。因为我们用了数据库,这是异步的,所以必须用回调将响应发给客户端。

注意,我们在创建 API 时还指定了一个域。这样我们可以孤立 API 错误并采取相应的行动。当在那个域中检测到错误时, connect-rest 会自动发送一个响应码 500,你所要做的只是记录日志并关闭服务器。

apiOptions.domain.on('error', function(err){
    console.log('API domain error.\n', err.stack);
    setTimeout(function(){
      console.log('Server shutting down after API domain error.');
      process.exit(1);
    }, 5000);
    server.close();
    var worker = require('cluster').worker;
    if(worker) worker.disconnect();
});

使用子域名

因为 API 实质上是不同于网站的,所以很多人都会选择用子域将 API 跟网站其余部分分开。

var apiOptions = {
    context: '/',
    domain: require('domain').create(),
};
app.use(vhost('api.*', rest.rester(apiOptions));

静态内容

静态内容是指应用程序不会基于每个请求而去改变的资源。下面这些一般都应该是静态内容。

  • 多媒体
    图片、视频和音频文件
  • CSS
  • JavaScript
  • 二进制下载文件

性能考虑

如何处理静态资源对网站的性能有很大影响,特别是网站有很多多媒体内容时。在性能上主要考虑两点:减少请求次数和缩减内容的大小

其中减少(HTTP)请求的次数更关键,特别是对移动端来说(通过蜂窝网络发起一次HTTP 请求的开销要高很多) 。有两种办法可以减少请求的次数:合并资源和浏览器缓存。

合并资源主要是架构和前端问题:要尽可能多地将小图片合并到一个子画面中。然后用CSS 设定偏移量和尺寸只显示图片中需要展示的部分。我强烈推荐用SpritePad的免费服务创建子画面。它让子画面的生成容易得不可思议,并 且还会生成 CSS。

浏览器缓存会在客户端浏览器中存储通用的静态资源,这对减少 HTTP 请求有帮助。尽管浏览器做了很大努力让缓存尽可能自动化,但它也不是完美的:在让浏览器缓存静态资源方面,还有很多你能做也应该做的工作。

最后,我们可以通过缩减静态资源的大小来提升性能。有些技术是无损的(不丢失任何数据就可以实现资源大小的缩减) ,有些技术是有损的(通过降低静态资源的品质实现资源大小的缩减) 。无损技术包括 JavaScript 和 CSS 的缩小化,以及 PNG 图片的优化。有损技术包括增加 JPEG 和视频的压缩等级。

面向未来的网站

在将网站放到生产环境中时,静态资源必须放在互联网中的某个地方。你过去可能习惯于把它们放在生成动态 HTML 的服务器上。

如果你想让网站的性能最佳(或者在将来可以这样做) ,应该希望能轻易地将你的静态资源托管给内容发布网络(CDN) 。CDN 是专为提供静态资源而优化的服务器,它利用特殊的头信息(我们马上就会讲到)启用浏览器缓存。另外 CDN 还能基于地理位置进行优化,也就是说它们可以从地理位置上更接近客户端的服务器发布静态内容。

大部分静态资源都是在 HTML 视图中引用的(指向 CSS 文件的 <link> 元素,指向JavaScript 文件的 <script> ,指向图片的 <img> 标签,以及嵌入多媒体的标签) 。然后 CSS 中一般也有静态引用,一般是 background-image 属性。最后,有时在 JavaScript 中也会引用静态资源,比如动态修改或插入 <img> 标签或 background-image 属性。

静态映射

让静态资源可重定位、对缓存友善的策略核心是映射的概念:在编写 HTML 时,我们真的没必要担心静态资源将会放在哪里这种具体细节。我们要关心的是静态资源的逻辑组织。我们的重点是在指定静态资源时让使用这 种组织结构变得容易。

我们会用“协议相对 URL”指向静态资源。即 URL 仅以“//”开头,不用“http://”或“https://”。这样浏览器用什么协议都可以。

映射的问题:将不太具体的路径映射到更加具体的路径。更进一步讲我们希望可以随意修改映射。

基准映射:如果实现映射只是在路径开头添加些东西,我们称之为基准URL。然而你的映射模式可能会更复杂。

使用文件名和路径是相当标准和普遍的内容组织方式,如果要偏离这种方式,你应该有充分的理由。更有实际意义的、更复杂的映射模式案例是采用资产版本化

眼下我们只使用非常简单的映射模式:只添加基准URL。比如创建文件lib/static.js,在这里我们假定所有静态资产都是以斜杠开头诶。

var baseUrl = '';
exports.map = function(name){
    return baseUrl + name; 
}

你可能很想给映射器添加一个功能,让它检查资产名称是不是以斜杆开头的,如果不是就加一个,但不要忘了,资产映射器到处都要用,所有已经尽可能的快!

接下来我们需要在下列地方使用我们的映射器即可:

  • 视图
    • 使用中静态资源最容易处理,Handlebars辅助函数即可帮助我们
  • CSS中静态资源
    • 需要预处理器(LESS,Sass,Stylus)
    • 如何解决CSS中URL的问题呢,可以使用Grunt将静态映射器作为LESS的定制函数,那样就可以在LESS文件中使用static函数
  • 服务端JavaScript中静态资源
    • 这就比较简单了,直接在代码中使用映射器加工即可
  • 客户端JavaScript中的静态资源
    • 你可能直接的认为将静态映射放到客户端即可,的确对我们这种简单的情况,的确可以胜任
    • 但是随着映射器变得越来越复杂,他很快就会奔溃了,必须开始用数据库实现更复杂的映射,浏览器便不能再用啦,尽管可以使用Ajax,但是很大程度减慢速度
    • 不优雅解决方案:在服务端做映射,然后设定定制的JavaScript变量。
      var IMG_CART_EMPTY = '{{static '/img/a.png'}}';
      var IMG_CART_FULL = '{{static '/img/b.png'}}';
      // 然后在代码中使用这些变量即可
      

静态资源操作

静态资源如何提供,如何修改以及优化呢?

提供静态资源

存储静态资产的最佳方式?这里需要了解浏览器用来确定如何缓存的响应头会有帮助:

  • Expires/Cache-Contorl
  • Last-Modified/Etag

如果你选择把静态资源放在CDN上,好处就是它们会帮你处理好大部分细节。如果不想把静态资源放到CDN上,但想要比Express内置的connect中间件更健壮的方案,可以考虑代理服务器,比如Nginx,他完全可以胜任。

修改静态内容

缓存极大的提升了网站的性能,但也不是没有代价的。可能到更新不及时的问题!很明显我们不想出现这种局面,但你也不能告诉用户让他们清楚缓存。解决方案是指纹法。指纹法只是在资源名上加上某种版本信息,你更新资产后,资源名称会变,浏览器就知道需要下载这个资源了。

明白指纹法的原理,但是在静态资产文件很多的时候,我们就需要把静态映射起做的更精巧,比如,可以在数据库中保存所有的数据资产的当前版本,然后静态映射器可以查找资产名称,然后返回最新版本的URL。

除了单个文件的指纹,另外一个流行的解决方案就是资源打包。

打包和缩小

在减少HTTP请求次数和缩减通过网络发送数据的努力中,打包和缩小流行了起来。打包和缩小还有一个额外的好处就是指纹处理的资产数量变少。

小结

对于看起来简单的时候,处理静态资源麻烦够多的,但是它们代表着真正传输给访问着的大量数据,所以花点时间优化会产生可观的回报。

不管选择什么技术提供静态资源,强烈建议把它们单独部署,并优选CDN。

MVC

MVC最大的优势之一是它减少了项目的学习时间。比如一个熟悉MVC的框架的PHP开发人员可以非常轻松的进入一个.NET MVC项目。实际上编程语言一般并不能行成什么障碍,只要知道在哪里找东西就行。

MVC将功能分解到有明确定义的领域中,给了我们一个通用的软件开发框架。

  • 模型:纯粹的数据和逻辑,根本不关心自己和用户之间的交互
  • 视图:将模型传递给用户
  • 控制器:接受用户的输入,处理模型,选择要显示哪个视图

MVC繁衍出数不清的变体。微软的模型-视图-视图模型(MVVM)特别引入可以个重要概念:视图模型。视图模型的想法是说它是模型的转化,单个视图模型可能由不止一个模型组成,或者是几个模型的部分,或者是单个模型的部分。你可能觉得没必要搞这么复杂,但是这个概念的价值在于可以保护模型。在纯粹的MVC中会引诱你对模型做只对视图来说有必要的转换或改进。模型视图可以解救你,如果你需要一个只用来展示的数据视图,他就属于视图模型。

模型

如果你的迷行足够健壮且设计优良,你总能废掉表示层。

千万不要用表示代码或用户交互代码污染了你的模型。

在理想情况下,模型和持久层是可以完全分开的,这肯定可以达到的,但通常需要付出很大的代价,而且普遍的情况是,模型中的逻辑严重依赖于持久层,把这两着分开得不偿失

用Mongoose来定义模型,如果不想绑定到持久化技术上,可能要考虑使用原生的MongoDB驱动(不需要对象映射),并把你的模型跟持久层分开。

视图模型

如果你因为要在视图中显示什么就忍不住要修改你的模型,那么肯定会建议你创建一个视图模型。视图模型是保持模型抽象性的方法,同时还能为视图提供有意义的数据。

控制器

控制器负责处理用户交互,并根据用户交互选择恰当的视图来显示,听起来是不是很像路由?实际上,控制器和路由器之间唯一的区别是控制器一般会把相关功能归组



留言