Better

Ethan的博客,欢迎访问交流

Node与Express开发之尾声篇

有关Node和Express开发学习进入尾声了,我很感谢作者Ethan Brown让我看到一本这么棒的书,这本书的特点不是仅仅介绍Express相关的API(如果是那样的话,我估计也不会有耐心看完),更多的是讲述一个成熟项目从零到一的诞生过程,注意我用的是成熟,因为它不仅仅实现功能,更多的关注点还有测试、安全和维护等,同时他所传达的一个Web编程思想是不区分语言的,只是书本以Express来实现而言,让作为有Java Web经验的我对Web有了更深的理解,作为尾声篇,主要内容有调试、集成第三方、启用以及维护等。

第三方集成

渐渐地,成功的网站不再是完全独立的了。为了留住已有用户,找到新用户,跟社交媒体集成是网站必须要做的工作。

本章会讨论两种最常见的集成需求:社交媒体和地理定位

社交媒体

社交媒体对产品和服务的推广非常有帮助。由于是外国友人的书,集成第三方AAPI总是以facebook和google等为例,这在我国的大环境下是不适用,而且API方面的东西都没什么难度,就不说了,在这里简单的对一些共性的东西做些总结。

性能考虑

大多数社交媒体集成都是前端事务。你在你的页面中引用恰当的JavaScript文件,它就能实现内容的流入(比如从你的Facebook页面上抓来三个头条)和内容的流出(比如就你所在的页面发送推文)。尽管这一般代表着社交媒体集成最容易的路径,但它也是有代价的:我曾经见过因为额外的 HTTP 请求占用了两倍甚至三倍加载时间的页面。如果你很看重页面的性能(应该是这样,特别对于移动端用户来说) ,应该认真考虑一下如何集成社交媒体。

Token问题

Token认证导致我们每个请求必须Token参数才能请求成功,首先Token是有效期的(可能永久),甚至请求Token的次数都是有限制的,因此我们必须缓存Token,而不是每次请求就重新获取Token,然后请求Token是异步的,我们必须在请求Token得到之后才能发送我们正式的请求。代码架构如下:

var https = require('https');
module.exports = function(twitterOptions){
    // 这个变量在模块外是不可见的
    var accessToken;
    // 这个函数在模块外是不可见的
    function getAccessToken(cb){
        if(accessToken) return cb(accessToken);
        // TODO: 获取访问令牌
    }
    return {
        search: function(query, count, cb){
            // TODO
        },
    };
};

中间参数缺省

由于js的灵活性,参数传入不一致可以成功调用方法,利用这个特点,我们可以达到参数缺省的目的。如果参数缺省最后一个,那么处理十分简单,如果我想省略中间一个呢,比如三个参数,我要省略第二个参数,那么如何处理呢?很明显这里需要用到参数的数据类型进行处理啦。

function embed(statusId, options, cb){
    if(typeof options==='function') {
        cb = options;
        options = {};
    }
}

地理编码

地理编码是指将街道地址或地名转换为地理坐标的过程,如果你的应用程序准备做地理位置计算(距离或方向) ,或者要显示地图,那你就需要地理坐标。

使用限制

地图应用对地理编码 API 的使用都有限制,以防止出现滥用的情况,但限制非常高。在编写本书时,谷歌的限制是每 24 小时不超过 2500 次请求。

天气数据

用 WeatherUnderground 的免费 API 获取当地的天气数据。你得创建一个免费账号,在 http://www.wunderground.com/weather/api/ 注册。

免费 API 有使用限制(在编写本书时每天不能超过 500 次请求,每分钟不能超过 10 次) 。为了不超出使用限制,我们会按小时缓存数据。

这里可以学习一下缓存策略:

var getWeatherData = (function(){
// 天气缓存
var c = {
  refreshed: 0,// 上一次刷新时间
  refreshing: false,// 是否正在刷新
  updateFrequency: 360000, // 1 小时
  locations: [
    { name: 'Portland' },
    { name: 'Bend' },
    { name: 'Manzanita' },
  ]
};
return function() {
  if( !c.refreshing && Date.now() > c.refreshed + c.updateFrequency ){
    c.refreshing = true;
    var promises = [];
    c.locations.forEach(function(loc){
      var deferred = Q.defer();
      var url = 'http://api.wunderground.com/api/' +
        credentials.WeatherUnderground.ApiKey +
        '/conditions/q/OR/' + loc.name + '.json'
      http.get(url, function(res){
        var body = '';
        res.on('data', function(chunk){
          body += chunk;
        });
        res.on('end', function(){
          body = JSON.parse(body);
          loc.forecastUrl = body.current_observation.forecast_url;
          loc.iconUrl = body.current_observation.icon_url;
          loc.weather = body.current_observation.weather;
          loc.temp = body.current_observation.temperature_string;
          deferred.resolve();
        });
      });
      promises.push(deferred);
    });
    Q.all(promises).then(function(){
      c.refreshing = false;
      c.refreshed = Date.now();
    });
  }
  return { locations: c.locations };
}
})();
// 初始化天气缓存
getWeatherData()

调试

“调试”也许是个不幸的词,因为它是跟缺陷相关联的。然而实际上我们所说的“调试”是你一直在做的事情,无论是实现新特性,学习某些东西如何工作,还是真的在解决一个bug。

首要原则

调试中的首要原则是排除的过程。

排除可能有很多种形态。这里有些常见的例子:

  • 系统化地注释掉或禁用代码块。
  • 编写能被单元测试覆盖的代码;单元测试本身提供了一个用于排除的框架。
  • 分析网络数据流,确定问题是出在客户端还是服务器端。
  • 测试系统中跟第一个相似但不同的部分。
  • 使用之前能用的输入,并一点一点地修改输入,直到问题呈现。
  • 用版本控制逐次回退,直到问题消失。
  • “模拟”功能以排除复杂子系统的干扰。

利用好REPL和控制台

在浏览器中,JavaScript 控制台就是你的 REPL。在 Node 中,不带参数运行 node 就是进入REPL 模式。

控制台日志也是你的朋友。它可能是一种粗糙的调试技术,但很简单。在 Node 中调用 console.log 会以一种易读的格式输出对象中的内容,所以你更容易发现问题。

Node内置的调试器

Node 有个内置的调试器,允许你逐步执行程序,就好像你在用 JavaScript 解释器一样。你只需要在启动程序时加上 debug 参数就可以开始调试了:

node debug xxx.js

命令行调试器还有很多功能,但一般你应该不太会去用它。命令行有很多擅长做的事情,但调试不在其列。你应该更常用图形界面的调试器,比如 Node 探查器。

Node探查器

除非是别无他法,否则你可能不会想用命令行调试器,实际上 Node 通过 Web 服务提供了调试控制,所以你还有个可选项-Node 探查器。有了它,你可以在调试客户端 JavaScript 代码的界面里调试 Node 程序。

安装 Node 探查器:

npm install -g node-inspector

装好之后需要启动它。如果你愿意,可以在另一个窗口中运行,但除了一个提示性的启动消息,它不会输出太多日志,所以我一般让它在后台运行:

node-inspector &

在 Linux 或 OS X 中,命令最后加一个 & 符号就是让它在后台运行。如果你要把它带回到前台,可以输入 fg 。如果你一开始运行时没把它放到后台,可以按 Ctrl-Z 让它暂停,然后输入 bg 把它切到后台去继续运行。

既然 Node 探查器运行起来了,会自动附着到运行在调试模式的任何程序上,你可以用调试模式启动程序:

node --debug xxx.js

注意,我们用了 --debug ,而不仅仅是 debug ;这样你的程序是以调试模式运行的,但不调用命令行调试器。

设置断点

  1. 恢复脚本执行(F8)
    你不再单步执行代码,直到遇到另一个断点停下来。
  2. 经过下一个函数调用(F10)
    如果当前这行代码调用了一个函数,调试器不会进入这个函数中。即这个函数会执行,而调试器会在函数调用完后接着到下一行代码。
  3. 进入下一个函数调用(F11)
    这个命令会进入所调用函数的内部,你可以一览无余。如果只用了这个动作,你最终能见到所要执行的一切代码。
  4. 步出当前函数(Shift-F11)
    将会执行你当前所在函数的剩余部分,并在这个函数的调用者中的下一行代码中再次开始调试模式。

除了所有这些控制动作,你还可以访问控制台:这个控制台在你程序的当前上下文中。所以你可以探查变量,甚至修改它们,或者调用函数。右侧有一些对你来说有价值的数据。从顶部开始依次是:

  1. 观测表达式
    可以自己定义的JavaScript表达式,可以随着你单步执行程序实时更新。
  2. 调用栈
    可以从中看出自己是从哪里到达目前这个位置的,列表中最上面那一行是你当前所在的函数。紧接着下面那行是调用这个函数的函数。如果你点击这个列表中的任何一项,就会被神奇地传送到对应的上下文中:你所有的观测和控制台上下文都是在那个上下文中的了。
  3. 作用域变量
    目前在作用域中的变量(包括在父作用域中对我们可见的变量)。如果你有很多变量,这个列表会变得臃肿,你最好将你感兴趣的变量定义为观测表达式。
  4. 断点列表
    真的只是起到记录的作用,点击其中一个会把你直接带到那里(但不像在调用栈中那样,它不会改变上下文,这是因为并不是每个断点都一定表示一个活动的上下文,而调用栈中的却一定是)。
  5. DOM、XHR 和事件监听器断点

有时候你需要调试的是应用程序的设置(比如在你连接中间件到 Express 中去的时候)。像我们之前那样运行调试器,在我们有机会设断点之前,一眨眼的工夫全都发生了。好在这有办法解决。只需要用 --debug-brk 代替 --debug 就行了:

node --debug-brk xxx.js

调试器会在程序的第一行停住,然后你就可以单步执行,或者设置合适的断点。

正式启用

主要介绍域名注册和托管服务,从临时环境向生产环境迁移的技术,部署技术,以及选择生产服务时应该考虑的问题。

域名

域名:互联网上所有的网站和服务都可以由一个互联网协议IP地址标识。但是人们不太容易记住这些数字,但计算机最终需要这些数字来显示网页,所以就有了域名。作用如下:

  1. 将一个人类容易记住的名字和IP地址映射起来
  2. 有助于组织迁移,即使搬到新的物理地址去,仍然可以找到它
  3. 有利用推广

域名安全

有些观念必须形成:

  1. 时刻牢记域名的价值
    如果你的托管服务被黑客完全攻破,托管主机被控制了,但只要你还能控制自己的域名,就可以找一台新的托管主机,把域名转过去。如果域名被攻破那就真的很麻烦。
  2. 你的名声是跟域名绑定在一起的,并且好域名都要非常认真的保护好
  3. 考虑到保护域名注册所有权的重要性,你应该采用跟域名注册相称的安全实践
  4. 注册域名时,必须提供一个跟域名关联的第三方邮件地址

顶级域名

域名的结尾部分(比如.com或.net)叫作顶级域名(TLD),一般来说有两类RLD:国家代码TLD和通用TLD。国家代码TLD(.us,.es和.uk)用来用地理区域分类的。通用TLD包括大家熟悉的.com,.net,.gov,.fed,.mil,.edu等。

顶级域名的管理是由互联网名称与数字地址分配机构最终负责,不过他把大部分实际管理工作交给其他组织代理。

子域名

TLD在域名后面,子域名在域名前面,目前最常见的子域名是www。对于这个子域名,大家都十分熟悉,建议主域名别用子域名,用http://xxx.comhttp://www.xxx.com,他更短更轻松,并且因为有重定向机制,也不用担心那些习惯用www开头输入网址的用户访问不到你的网站。

子域名也用于其他用途,像blog、api、m(移动站点)之类的网址很常见。一个好的代理既能根据子域名重定向流量,也能根据路径重定向,因此选择子域名还是路径应该更侧重于内容而不是技术。

建议用子域名给网站或服务有显著区别的部分分区。比如

  1. 用api.xxx.com提供对外api
  2. 跟其余部分外观不同的站点也应该用子域名
  3. 管理界面和公众界面分开

除非特别指明,否则域名注册商会忽略子域名将所有请求都重定向到你的服务器,然后由服务器或代理根据子域名采取相应动作。

托管

托管服务器描述的是运行网站的真实计算机。

传统托管还是云托管

“云”是最近几年突然出现的含义最模糊的技术术语之一。真的,它只是用一种很炫的方式说“互联网”或者“互联网的一部分”。不过这个术语也不是毫无用处,尽管这个术语中没有技术定义部分,托管在云中的一般表示计算资源在一定程度上的商品化。

也就是说我们不在把服务器当作独立的物理实体,他只是在云中某处的一个同质化资源。

把应用部署在真正服务器上和把它部署在云中的服务器上两者的区别是,应用能在你不知情(或关心)的情况下轻松迁移到不同的云服务器上。有人照顾那些让你的Web应用程序跑起来的物理硬件和网络,唯一改变的只是你离硬件更远了。

XaaS

在考察云托管时,你会遇到Saas、Paas、Iaas这几个缩写。

  • 软件即服务(SaaS)
    提供给你软件:你只是使用它们
  • 平台即服务(PaaS)
    提供所有的基础设施(操作系统,网络等),你只需要编写应用程序。
  • 架构即服务(IaaS)
    IaaS最灵活,但也是有代价的。它只是提供虚拟机和基本的网络连接。然后你负责安装和维护操作系统、数据库和网络策略。除非你需要对环境做这种层面的控制,否则一般选择PaaS。PaaS确实允许你控制操作系统和网络配置的选择,只是你不必亲自动手实现。

部署

到2014年还有人用FTP部署应用程序,如果你也是那样的话,请停下来吧,FTP绝对不安全,不仅你的所有文件传输都是未经加密的,连用户名和密码也是未经加密的,最起码你也应该用SFTP火FTPS。

Git部署

Git最强的是它的灵活性,他几乎可以适应你能想到的任何工作流。

当需要回滚变化时会怎么样?

  1. 逆向提交取消前面的提交-git revert,复杂且引发后续问题
  2. 使用productin当作一次性分支:知识你在master分支在不同时点的映像。当你需要回滚修改,只需要在production分支上做一次git rest --hard ,然后git push origin production --force,本质上是重写历史,经常被教条式的Git使用者描述为危险或高级的行为。在这里可以理解production是一个只读分支,开发人员绝不能向他提交代码,重写历史会给你带来麻烦。

维护

网站的维护在规划时一般会收到冷遇。

维护原则

  1. 有长远的规划
  2. 使用源码控制系统
  3. 使用问题追踪系统
  4. 良好的卫生习惯-版本控制、测试、代码审查和问题追踪
  5. 不要拖延-重构
  6. QA检查
  7. 监测分析
  8. 性能优化
  9. 潜在用户追踪优先
  10. 防止出现不可见的错误

代码重用及重构

  1. 私有npm
  2. 中间件
    • 如果你发现中间件的通用型比较弱,不足以放到可重用包中,则应该考虑重构中间件,让它可配置,变得更加通用。
    • 记住你可以将配置对象传到中间件中,让他们适用整个情况。

Node模块中输出中间件常用的办法:

  1. 模块直接输出中间件函数
    module.exports = function(req,res,next){
     next();
    }
    // 使用
    var stuff = require('stuff');
    app.use(stuff)
    
  2. 模块输出返回中间件的函数
    module.exports = function(config){
     if(!config) config = {};
     return function(req,res,next){
         next();
     }
    }
    var stuff = require('stuff')({option:'choice'});
    app.use(stuff)
    
  3. 输出模块包含中间件对象
    module.exports = function(config){
     if(!config) config = {};
     return {
         m1:function(req,res,next){
             next();
         },
         m2:function(req,res,next){
             next();
         }
     }
    }
    var stuff = require('stuff')({option:'choice'});
    app.use(stuff.m1);
    app.use(stuff.m2);
    
  4. 模块输出对象构造器
    // 可能最少见的中间件返回方式,适合用面向对象方式实现,同时也是实现中间件最需要技巧的方式,`因为你将中间件输出为实例方法,他们就不会被Express对象的实例调用,所以this就不是你想要的实例`。
    function Stuff(config){
     this.config = config || {};
    }
    Stuff.prototype.m1 = function(req,res,next){
     // this不是你想要的实例,不要用他
     next();
    }
    Stuff.prototype.m2 = function(req,res,next){
     // 用Function.prototype.bind将这个实例绑定在this属性上
     return (function(req,res,next){
         next();
     }).bind(this);
    }
    module.exports = Stuff;
    // 使用
    var Stuff = require('stuff');
    var stuff = new Stuff({option:'choice'});
    app.use(stuff.m1);
    app.use(stuff.m2());
    

其他资源

如何获取更多的资源呢?

  1. 在线文档
    • 对于JavaScript、CSS和HTML而言,无人能与Mozilla开发网络(MD)相媲美。如果需要查阅JavaScript文档,可以直接在MDN搜索或在搜索查询中加上mdn,否则w3schools肯定会出现在搜索结果中。负责w3schools搜索引擎优化工作的是个天才,但我建议你避开这个网站,因为他的文档经常严重匮乏。
    • Mark Pilgrim的《深入HTML5》
    • WHATWG维护这一个卓越HTML5规范的活标准https://developers.whatwg.org
    • ...
  2. 期刊
  3. Stack Overflow
    最重要的在线Q&A网站,如何提高获得优秀答案的机会
    • 成为一个了解SO的用户
    • 不要问已经回答过的问题
    • 不要让人替你写代码 - 描述尝试过的办法以及为什么不行
    • 一次问一个问题
    • 为你的问题做一个最精简的例子
    • 学会Markdown
    • 接受答案并投出赞成票
    • 如果你在别人给我答案之前自己解决了问题,那就自己回答那个问题
  4. 贡献npm包
    • 包名
    • 包介绍
    • 作者/贡献者
    • 许可
    • 版本
    • 依赖项
    • 关键字
    • 代码库
    • README.md


留言