Better

Ethan的博客,欢迎访问交流

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

最近准备系统的学习Node和Express,学习笔记-进阶篇!主要内容有持久化、路由组织管理等。

持久化

所有网站和Web应用程序(除了最简单的)都需要某种持久化方式,即某种比易失性内存更持久的数据存储方式,这样当遇到服务器宕机、断电、升级和迁移等情况时数据才能保存下来。

文件系统持久化

实现持久化的一种方式是将数据存到扁平文件中(“扁平”的意思是文件没有内在结构,只是一串字节)。Node通过fs(文件系统)模块实现文件系统持久化。

文件系统持久化有一些不足之处,特别是它的扩展性不好。

  1. 当你需要不止一台服务器以满足流量的需求时,除非所有服务器都能访问一个共享的文件系统,否则就会遇到文件系统持久化的问题。
  2. 因为扁平文件没有内在结构,定位、排序和过滤数据就变成了应用程序的负担。
    • 出于这些原因,你应该用数据库而不是文件系统来做数据排序。
    • 排序二进制文件是个例外,比如图片、音频文件或视频。尽管很多数据库可以处理这类数据,但极少能达到文件系统那种效率

文本文件和二进制文件的区别?

  1. 文本文件是指完全由可见字符组成的文件。所谓可见字符是指ASCII码为32到126的字符、回车符(ASCII码13)、换行符(ASCII码10)、制表符(ASCII码9)、以及所有汉字字符(当然也包括其他字符集如韩文、日文、阿拉伯文等等)。如果是Unicode文本,则还包括ASCII码0。
  2. 二进制文件则有多种定义方式,广义的二进制文件是指电脑中的所有文件(包括文本文件),因为电脑中的所有文件其实都是以二进制方式存储的,也就是说每个字符(包括可见字符、控制字符)最终都是以0和1的形式存储在硬盘等介质中的(这也就是为什么说电脑只认识0和1这两个数字的原因);而狭义的二进制文件则是相对于文本文件而言的,即只要文件中含有除可见字符之外的其他字符(主要是控制字符),就是二进制文件;而比狭义的二进制文件更有特指性的定义方式则是指可执行文件(EXE)、库函数文件(DLL)、图片视频、数据库文件等等一切由程序代码、机器码、特定的二进制代码和数据等组成的有实际意义的文件。

那么如果确实需要存储二进制文件,记得文件系统依然有扩展性不好的问题,如果主机不能访问共享的文件系统,可以考虑如下方案:

  1. 将二进制文件存储到数据库中(一般要做些配置,以免数据库被拖垮)。
  2. 基于云的存储服务,比如亚马逊S3或者微软的Azure存储。

fs基本API

通过简单的代码摘要来学习:

// 是否存在,不存在创建
fs.existsSync(dataDir) || fs.mkdirSync(dataDir);
// 修改文件名称,可更改文件的存放路径。
fs.renameSync(photo.path, dir + '/' + photo.name, [callback(err)]);
// 读取文件内容
fs.readFileSync(__dirname + '/tmp/ + filename)

一般来说,你不应该信任用户上传的任何东西,因为它可能是攻击你网站的载体。比如,一个恶意用户可能轻易地将一个有害的可执行文件重命名为.jpg文件,然后上传,作为攻击的第一步(然后再找办法执行它)。同样,我们用浏览器提供的name 属性命名这个文件也是有风险的,有些人可能会在文件名中插入一些特殊字符来滥用它。要让这段代码完全安全,我们会给这个文件一个随机名,只接受扩展名(确保它仅由字母数字字符组成)。

云持久化

云存储越来越流行了,我强烈建议你利用这些便宜又好用的服务。不同服务API方式不同,这里没必须学习,只要有这么个玩意就行。

数据库持久化

所有网站和 Web 应用程序(除了最简单的)都需要数据库。即便你的数据是二进制的,并且你用共享的文件系统或云存储,你也很有可能需要一个数据库来做那些二进制数据的目录。

依照传统, “数据库”是“关系型数据管理系统” (RDBMS)的简称。关系型数据库,比如 Oracle、MySQL、PostgreSQL 或 SQL Server,基于几十年的研究和正规的数据库原理。现在它是一种十分成熟的技术,这些数据库的能量是毋庸置疑的。

最近几年兴起了 NoSQL 数据库,它们正在挑战互联网数据存储现状。两种最流行的 NoSQL 数据库是文档数据库和键 - 值数据库。文档数据库善于存储对象,这使得它们非常适合 Node 和 JavaScript。键 - 值数据库如其名所示,极其简单,对于数据 模式可以轻松映射到键 - 值对的程序来说是很好的选择。

我觉得文档数据库代表了关系型数据库的限制和键 - 值数据库的简单性两者之间的最佳折中,MongoDB 是文档数据库中的佼佼者,现在也非常健壮和成熟。

Mongoose

尽管有底层的MongoDB驱动,但你可能还是想用对象文档映射(ODM)。有官方支持的 MongoDB ODM 是 Mongoose。

JavaScript的优势之一是它的对象模型极其灵活。如果你想给一个对象添加属性或方法,尽管去做,并且不用担心要修改类。可惜那种随心所欲的灵活性可能会对数据库产生负面影响,因为它们会变得零碎和难以调优。Mongoose试图确立平衡,它引入了模式和模型(联合的,模式和模型类似于传统面向对象编程中的)。模式很灵活,但仍为数据库提供了一些必要的结构。

通过简单的代码摘要学习mongoose的基本API

var mongoose = require('mongoose');
// 连接
mongoose.connect(credentials.mongo.development.connectionString, opts);
// 创建模式
var vacationSchema = mongoose.Schema({
    name: String,
    slug: String
    ...
});
vacationSchema.methods.getDisplayPrice = function(){
    return '$' + (this.priceInCents / 100).toFixed(2);
};
// 创建模型
var Vacation = mongoose.model('Vacation', vacationSchema);
module.exports = Vacation;
// 引入模型对象
var Vacation = require('./models/vacation.js');
// 查找数据
Vacation.find({name:'lxq'}},function(err, vacations){

});
// 添加数据
new Vacation({}).save([callback])
// 更新数据 
Vacation.update(conditions,update,callback)
// 删除数据
Vacation.remove(conditions,callback)
// upsert
VacationInSeasonListener.update(
    { email: req.body.email },
    { $push: { skus: req.body.sku } },
    { upsert: true },
    function(err){

    }
);

Mongoose 方便的 upsert ( “更新”和“插入”的混成词) 。基本上就相当于,如果给定的记录不存在,就会创建它。如果记录存在,就更新它。用魔法变量$push表明我们想添加一个值到数组中。

不要将未映射的数据库对象直接传给视图。视图会得到一堆它可能不需要的属性,并且可能是以它不能兼容的格式。这样还很容易暴露机密信息,或者威胁网站安全的信息。因此我建议将数据库中返回的数据映射一下,并且只传递视图需要的数据。

在某些 MVC 架构的变体中,引入了一种称为视图模型的组件。视图模型本质上就是对模型的抽取和转换,从而让模型(或多个模型)更适合在视图中显示。

MongoDB存储会话数据

用内存存储会话数据不适用于生产环境。好在设置 MongoDB 用来存储会话非常容易。我们会用 session-mongoose 包提供 MongoDB 会话存储。

var MongoSessionStore = require('session-mongoose')(require('connect'));
var sessionStore = new MongoSessionStore({ url:
credentials.mongo.connectionString });
app.use(require('cookie-parser')(credentials.cookieSecret));
app.use(require('express-session')({ store: sessionStore }));

MongoDB 不一定是会话存储的最佳选择,它有点杀鸡用牛刀的意味。另外一个流行又易用的会话持久化方案是用 Redis。

路由

路由是网站或 Web 服务中最重要的一个方面;路由是将请求(由 URL 和 HTTP 方法指定)路由到处理它们的代码去的一种机制。

在探讨路由技术之前,我们应该讨论下信息架构(IA)的概念。IA 是指内容的概念性组织。在考虑路由之前有一个可扩展(但不过于复杂的)IA 会为你的后续工作提供巨大的好处。

网站管理员有责任让分配的 URI 保持 2 年、20 年、200 年不变。这需要思考、组织和决心。

认真考虑内容的分解。按逻辑归类,尽量别把自己逼入死角。这是科学,但也是艺术。

可能最重要的是跟其他人合作设计你的 URL。即便你是方圆几公里内最好的信息架构师,可能也会惊异地看到人们对相同内容的观点有多么不同。我的意思不是让你做一个从每个人的观点来看都有意义的 IA(因为那一般是不可能的),而是说以多种观点看待问题能让你产生更好的想法,并且暴露你自己的 IA 中的缺陷。

关于实现持久IA的建议:

  • 绝不在URL中暴露技术细节
  • 避免在URL中出现无意义的单词
    • 比如/home,因为严格来说根路由就是首页
  • 避免无谓的长URL
    • 但不应该为了缩短URL从而牺牲清晰性和SEO
  • 单词分隔符保持一致
    • 用连字符分割单词的情况使用普遍,而用下划线的不太多。一般认为连字符比下划线更美观,并且SEO专家都建议使用连字符
    • 关键是保持一致
  • 绝不要用空格或不可录入字符
    • 空格会转换成+号,从而引起困惑
  • 在URL中使用小写字母

路由和SEO

这两者有什么关系呢?那就需要知道URL会如何影响到SEO,如果某些关键字特别重要并且有意义,就考虑把它变成URL的一部分。良好的URL设计会提高SEO。

子域名

除了路径,子域名一般也是URL中用来路由请求的部分。子域名最好保留给程序中显著不同的部分, 比如RESTAPI(api..com 或管理界面(admin..com)。

用子域名分割内容时一般会影响 SEO,所以一般应该留给 SEO 不重要的区域,比如管理区域和 API。记住这一点,并且只有在确实没有其他选择时,才给对于 SEO 方案来说比较重要的内容使用子域名。

Express 中的路由机制默认不会把子域名考虑在内: app.get(/about) 会处理对 http://*.com/abouthttp:// www.*.com/abouthttp://admin.*.com/about 的请求。如果你想分开处理子域名,可以用 vhost 包(表示 “虚拟主机” ,源自 Apache 的机制,一般用来处理子域名) 。

// 创建子域名 "admin" ……它应该出现在所有其他路由之前
var admin = express.Router();
app.use(vhost('admin.*', admin));
// 创建 admin 的路由;它们可以在任何地方定义
admin.get('/', function(req, res){
    res.render('admin/home');
});
admin.get('/users', function(req, res){
    res.render('admin/users');
});

路由处理器是中间件

在学习中间件的时候就说过,路由处理器本身也是中间件,但有一点我们可能不知道,同一个路由可以有多个处理器,会直接传入第三个,第四个甚至更多的处理器即可,这让你可以创建可以用在任何路由中的通用函数

最基本的通用函数就是页面授权验证,比如说我们的用户授权代码会设定一个会话变量req.session.authorized ,则可以像下面这样做一个可重复使用的授权过滤器:

function authorize(req, res, next){
    if(req.session.authorized) return next();
    res.render('not-authorized');
}
app.get('/secret', authorize, function(){
    res.render('secret');
})
app.get('/sub-rosa', authorize, function(){
    res.render('sub-rosa');
});

路由路径和正则表达式

路由中指定的路径(比如 /foo)最终会被 Express 转换成一个正则表达式。某些正则表达式中的元字符可以用在路由路径中: + 、 ? 、 * 、 ( 和 ) 。

// 处理user和username
app.get('/user(name)?', function(req,res){
    res.render('user');
});
// 处理多个n
app.get('/khaa+n', function(req,res){
    res.render('khaaan');
})

并不是所有的常规正则表达式元字符在路由路径中都有含义,虽然只有前面列出来的那些。如果你的路由真的需要功能完整的正则表达式,也可以支持的:

app.get(/crazy|mad(ness)?|lunacy/, function(req,res){
    res.render('madness');
});

路由参数

在你日常使用的 Expression 工具箱中可能很少发现正则路由,但路由参数很可能要经常用。这是一种把变量参数放到路由中成为其一部分的办法。

app.get('/staff/:name', function(req, res){});

它会跟任何字符串匹配(不包括反斜杠) ,并将其跟键 name 一起放到 req.params 对象中。路由中可以有多个参数。

组织路由

在主应用程序文件中定义所有路由太笨重了。那样不仅会导致那个文件一直增长,还不利于功能的分离。

Express 对于你如何组织路由没有意见,所以怎么做完全是你的事情。

四条组织路由的指导原则:

  • 给路由处理器用命名函数
    • 而不是在路由中直接使用匿名函数作为处理器
  • 路由不应该神秘
  • 路由组织应该是可扩展的
  • 不要忽视自动化的基于视图的路由处理器

在模块中声明路由

组织路由的第一步是把它们都放到它们自己的模块中。

  • 你的模块做成一个函数,让它返回包含“方法”和“处理器”属性的对象数组。然后你可以这样在应用程序文件中定义路由
    var routes = require('./routes.js')();
    routes.forEach(function(route){
    app[route.method](route.handler);
    })
    
    这种方式有它的优势,并且可能非常适合动态地存储路由,比如在数据库或 JSON 文件中。如果你不需要那样的功能,建议使用第二种方式。
  • 将 app 实例传给模块,然后让它添加路由。
    module.exports = function(app){
      app.get('/', function(req,res){
          app.render('home');
      }))
      //...
    };
    

按逻辑对处理器分组

要满足第一条指导原则(给路由处理器用命名函数) ,我们需要找地方放那些处理器。以某种方式将相关功能分组更好。那样不仅更容易利用共享的功能,并且更容 易修改相关的方法。

自动化渲染视图

如果你希望回到以前,只要把 HTML 文件放到一个目录中,然后很快你的网站就能提供它的旧时光,那么有这样想法的人不止你一个。如果你的网站有很多内容,但功能不多,你可能发现给每个视图添加一个路由是不必要的麻烦。我们只需要在404之前添加如下处理器即可。

var autoViews = {};
var fs = require('fs');
app.use(function(req,res,next){
    var path = req.path.toLowerCase();
    // 检查缓存;如果它在那里,渲染这个视图
    if(autoViews[path]) return res.render(autoViews[path]);
    // 如果它不在缓存里,那就看看有没有 .handlebars 文件能匹配
    if(fs.existsSync(__dirname + '/views' + path + '.handlebars')){
        autoViews[path] = path.replace(/^\//, '');
        return res.render(autoViews[path]);
    }
    // 没发现视图;转到 404 处理器
    next();
});

其他的路由组织方式

最流行的两种路由组织方式是命名空间路由(namespaced routing)和随机应变路由(resourceful routing) 。当很多路由都以相同的前缀开始时,命名空间路由很不错(比如 /vacations) 。有个 Node 模块叫 express-namespace,它让这种方式变得很容易。随机应变路由基于一个对象中的方法自动添加路由。如果网站的逻辑是天然面向对象的,这项技术就很好用。 express-resource 包是如何实现这种路由组织风格的范例。



留言