express session 不同步问题

情况是这样的,在开发的过程中出现了一个很奇怪的 BUG,现象是:在输入验证码的时候,不论怎么样第一次输入的验证码总是错误的。

需要了解的问题是 express 中的 session 的解析依赖 cookieParser,首先是从 cookie 中读取加密 connect sid,再通过 cookieParser 解析成一个对应的 session id,该 session id 保存在 req.sessionID 中(因此 cookieParser中间件应该放到 session 之前)。在通过 session 中间件的时候,首先通过 session 的 Store 对象来读取当前的 session 数据,所以当多个请求并发过来的时候,他们拿到的会是同一份 session 数据。每个协议调用在 res.end() 的时候这个阶段 session 的数据会被自动 save 一次(req.session.save() 可以主动保存)。

也就是说在获取验证码的协议(该协议在 session 中特别保存了一次验证码的数据),之后其他请求其他资源(比如js、图片之类)的协议中附带的 session(没有带验证码的数据)在 res.end() 的时候重复保存然后把开始的验证码数据给覆盖掉了。看起来就好像是一个不同步的问题一样,其实就是并发的时候数据重复写入了。。

最后博主的验证码解决方案是,当用户 focus 到验证码输入框的时候再去请求验证码的图片。当然还有一些其他关于这个问题可以作为解决方案的建议:

将静态资源的处理前置

将处理静态资源的 handler 放在 session 中间件的前面。例如:

app.use(express.static(__dirname + '/public')); // 读取静态资源在前
app.use(cookieParser('keyboard cat'));
app.use(session({ ... }); // session 在后

设置 session 的 ignore

如果你不能把一些 handler 移到 session 的前面,你也可以配置 session 中间件的 express.session.ignore 来忽略一些不使用 req.session 的路径
例如

express.session.ignore.push('/individual/path');
app.use(express.session({ ... }));

把 session 去掉

如果你的 handler 没有写入 session (例如只是读取)的话,可以在调用 res.end 之前设置 req.session = null,这样就不会导致 session 被重复保存。

总之,建议与 session 有关的操作尽量要放在 post 中处理,如果需要类似 get 的同时处理的话,请先 use 之后再来。或者可以考虑定义一个规则,不符合规的则路由则过滤掉,例如所有有 session 操作的路由可以把他们的 path 的结尾定义为 .php 然后可以通过 use 来将非 .php 结尾的请求中的 session 设置为 null 之类的。

node.js express 使用 redis 存储 session

使用 redis 存储 session 的好处在于:
1.多进程间 session 可以共存
2.网站重启用 session 依旧还在,测试的时候不用重新登录了

var express = require('express');
var RedisStore = require('connect-redis')(express.session);
var app = express();

// 设置 Cookie
app.use(express.cookieParser('keyboard cat'));

// 设置 Session
app.use(express.session({
    store: new RedisStore({
        host: "192.168.108.46",
        port: 6379,
        ttl: 1800 // 过期时间
    }),
    secret: 'keyboard cat'
}))

app.get("/", function(req, res) {
    var session = req.session;
    session.count = session.count || 0;
    var n = session.count++;
    res.send('hello, session id:' + session.id + ' count:' + n);
});

app.listen(3002);

console.log('Web server has started on http://127.0.0.1:3002/');

关于 RedisStore 的更多选项以及信息请参见 https://github.com/visionmedia/connect-redis

Jade 模板引擎使用

在 Express 中调用 jade 模板引擎

var express = require('express');
var http = require('http');
var app = express();
app.set('view engine', 'jade'); // 设置模板引擎
app.set('views', __dirname);  // 设置模板相对路径(相对当前目录)

app.get('/', function(req, res) {
	res.render('test'); // 调用当前路径下的 test.jade 模板
})

var server = http.createServer(app);
server.listen(3002);
console.log('server started on http://127.0.0.1:3002/');

test.jade

p hello jade

其上的 jade 模板会被解析成

<p>hello jade</p>

虽然平常我们修改 node.js 代码之后需要重启来查看变化,但是 jade 引擎不在此列,因为是动态加载的,所以我们修改完 jade 文件之后可以直接刷新网页来查看效果的。

如果文本比较长可以使用

p
  | foo bar baz
  | rawr rawr

或者

p.
  foo bar baz
  rawr rawr

两种情况都可以处理为

<p>foo bar baz rawr rawr</p>

jade 变量调用

jade 的变量调用有 3 种方式

  1. #{表达式}
  2. =表达式
  3. !=表达式

注意:- 开头在 jade 种属于服务端执行的代码

- console.log('hello'); // 这段代码在服务端执行
- var s = 'hello world' // 在服务端空间中定义变量
p #{s}
p= s

会被渲染成为

<p>hello world</p>
<p>hello world</p>

以下代码效果相同

- var s = 'world'
p hello #{s}
p= 'hello' + s

方式1可以自由的嵌入各个地方
方式2返回的是表达式的值
= 与 != 雷同,据说前者会编码字符串(如 <stdio.h> 变成 &lt;stdio.h&gt;),后者不会,不过博主没试出来不知道什么情况。

除了直接在 jade 模板中定义变量,更常见的应用是在 express 中调用 res.render 方法的时候将变量一起传递进模板的空间中,例如这样:

res.render(test, {
    s: 'hello world'
});

调用模板的时候,在 jade 模板中也可以如上方那样直接调用 s 这个变量

if 判断

方式1

- var user = { description: '我喜欢猫' }
- if (user.description)
    h2 描述
    p.description= user.description
- else
    h1 描述
    p.description 用户无描述

结果:

<div id="user">
  <h2>描述</h2>
  <p class="description">我喜欢猫</p>
</div>

方式2

上述的方式有种省略写法

- var user = { description: '我喜欢猫' }
#user
  if user.description
    h2 描述
    p.description= user.description
  else
    h1 描述
    p.description 用户无描述

方式3

使用 Unless 类似于 if 后的表达式加上了 ! 取反

- var user = { name: 'Alan', isVip: false }
unless user.isVip
  p 亲爱的 #{user.name} 您并不是 VIP

结果

<p>亲爱的 Alan 您并不是 VIP</p>

注意这个 unless 是 jade 提供的关键字,不是运行的 js 代码

循环

for 循环

- var array = [1,2,3]
ul
  - for (var i = 0; i < array.length; ++i) {
    li hello #{array[i]}
  - }

结果

<ul>
	<li>hello 1</li>
	<li>hello 2</li>
	<li>hello 3</li>
</ul>

each

同样的 jade 对于循环液提供了省略 “-” 减号的写法

ul
  each val, index in ['西瓜', '苹果', '梨子']
    li= index + ': ' + val

结果

<ul>
  <li>0: 西瓜</li>
  <li>1: 苹果</li>
  <li>2: 梨子</li>
</ul>

该方法同样适用于 json 数据

ul
  each val, index in {1:'苹果',2:'梨子',3:'乔布斯'}
    li= index + ': ' + val

结果:

<ul>
  <li>1: 苹果</li>
  <li>2: 梨子</li>
  <li>3: 乔布斯</li>
</ul>

Case

类似 switch 里面的结果,不过这里的 case 不支持case 穿透,如果 case 穿透的话会报错。

- var friends = 10
case friends
  when 0
    p you have no friends
  when 1
    p you have a friend
  default
    p you have #{friends} friends

结果:

<p>you have 10 friends</p>

简略写法:

- var friends = 1
case friends
  when 0: p you have no friends
  when 1: p you have a friend
  default: p you have #{friends} friends

结果:

<p>you have a friend</p>

在模板中调用其他语言

:markdown
  # Markdown 标题
  这里使用的是 MarkDown 格式
script
  :coffee
    console.log '这里是 coffee script'

结果:

<h1>Markdown 标题</h1>
<p>这里使用的是 MarkDown 格式</p>
<script>console.log('这里是 coffee script')</script>

可重用的 jade 块 (Mixins)

//- 申明可重用的块
mixin list
  ul
    li foo
    li bar
    li baz

//- 调用
+list()
+list()

结果:

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>
<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

你也可以给这个重用块制定参数

mixin pets(pets)
  ul.pets
    - each pet in pets
      li= pet

+pets(['cat', 'dog', 'pig'])

结果:

<ul class="pets">
  <li>cat</li>
  <li>dog</li>
  <li>pig</li>
</ul>

Mixins 同时也支持在外部传入 jade 块

mixin article(title)
  .article
    .article-wrapper
      h1= title
      //- block 为 jade 关键字代表外部传入的块
      if block
        block
      else
        p 该文章没有内容
        
+article('Hello world')

+article('Hello Jade')
  p 这里是外部传入的块
  p 再写两句

结果:

<div class="article">
  <div class="article-wrapper">
    <h1>Hello world</h1>
    <p>该文章没有内容</p>
  </div>
</div>
<div class="article">
  <div class="article-wrapper">
    <h1>Hello Jade</h1>
    <p>这里是外部传入的块</p>
    <p>再写两句</p>
  </div>
</div>

Mixins 同时也可以从外部获取属性。

mixin link(href, name)
  a(class!=attributes.class, href=href)= name
  
+link('/foo', 'foo')(class="btn")

结果:

<a href="/foo" class="btn">foo</a>

模板包含 (Includes)

你可以使用 Includes 在模板中包含其他模板的内容。就像 PHP 里的 include 一样。

index.jade

doctype html
html
  include includes/head
body
  h1 我的网站
  p 欢迎来到我的网站。
  include includes/foot

includes/head.jade

head
  title 我的网站
  script(src='/javascripts/jquery.js')
  script(src='/javascripts/app.js')

includes/foot.jade

#footer
  p Copyright (c) foobar

调用 index.jade 的结果:

<!doctype html>
<html>
  <head>
    <title>我的网站</title>
    <script src='/javascripts/jquery.js'></script>
    <script src='/javascripts/app.js'></script>
  </head>
  <body>
    <h1>我的网站</h1>
    <p>欢迎来到我的网站。</p>
    <div id="footer">
      <p>Copyright (c) foobar</p>
    </div>
  </body>
</html>

模板引用 (Extends)

就绝

layout.jade

doctype html
html
  head
    title 我的网站
    meta(http-equiv="Content-Type",content="text/html; charset=utf-8")
    link(type="text/css",rel="stylesheet",href="/css/style.css")
    script(src="/js/lib/jquery-1.8.0.min.js",type="text/javascript")
  body
    block content

article.jade

//- extends 拓展调用 layout.jade
extends ../layout
block content
  h1 文章列表
  p 习近平忆贾大山 李克强:将启动新核电项目
  p 朴槿惠:"岁月号"船长等人弃船行为等同于杀人
  p 普京疑换肤:眼袋黑眼圈全无 领导人整容疑云
  p 世界最大兔子重45斤长逾1米 1年吃2万元食物

res.render(‘article’) 的结果:

<html>
  <head>
    <title>我的网站</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>
    <link type="text/css" rel="stylesheet" href="/css/style.css"></head>
    <script src="/js/lib/jquery-1.8.0.min.js" type="text/javascript"></script>
  </head>
  <body>
    <h1>文章列表</h1>
    <p>习近平忆贾大山 李克强:将启动新核电项目</p>
    <p>朴槿惠:"岁月号"船长等人弃船行为等同于杀人</p>
    <p>普京疑换肤:眼袋黑眼圈全无 领导人整容疑云</p>
    <p>世界最大兔子重45斤长逾1米 1年吃2万元食物</p>
  </body>
</html>

node.js express 防盗链

本文代码已经上传 npm,通过如下代码安装:

npm install express-anti-leech

目前版本已经更新,新的成熟版本的使用参见 node.js express 防盗链模块 express-anti-leech

旧版的示意:

var express = require('express'),
var AntiLeech = require('express-anti-leech');

var app = express();

app.use(AntiLeech()); // 确保在 静态资源之前调用
app.use(express.static(path.join(__dirname, 'public')));

// ...

旧版实际原理很简单,所以除了核心内容就不赘述了,见如下:

antiLeech.js

var url = require('url');
var path = require('path');

// 防盗链类型
var img = [ '.png', '.jpg', '.jpeg', '.gif', '.swf', '.flv' ];
// 允许访问域名
var host = ['localhost:3001', 'localhost'];

module.exports = function(req, res, next) {
	var referer = req.headers.referer;
	var url = req.url;

	if (filter(url)) {
		if (!isTrustSite(referer)) {
			res.send('');
			return;
		}
	}

	next();
};

// 信任域名
var isTrustSite = function(referer) {
	// referer 未指定不信任
	if (!referer) {
		return false;
	}
	var url_obj = url.parse(referer);
	for (var i = 0; i < host.length; i++) {
		if (url_obj.host == host[i]) {
			return true;
		}
	}
	return false;
};

// 防止类型
var filter = function(url) {
	var ext = path.extname(url);
	for (var i = 0; i < img.length; i++) {
		if (ext == img[i]) {
			return true;
		}
	}
	return false;
};

在 express 的 app.js 中调用:

var express = require('express');
var app = express();
var antiLeech = require('./lib/antiLeech');

app.configure(function () {
    app.use(express.methodOverride());
    app.use(express.urlencoded());
    app.use(express.json());
    app.use(antiLeech);  // 调用防盗链
    // ...
});

node.js express 学习 (一) hello world

由于博主使用的是 win7 操作系统,所以有个地方要提一下,如果你的系统账户不是管理员账户(也就是说你安装软件的时候会弹出是否以管理员身份运行的选项)的话,那么在安装之前需要先以管理员身份运行 cmd.exe 或 npm , 否则执行 npm install 命令的时候有些东西由于权限的问题不会成功安装。

# 安装 express
E:node>npm install express

# 查看 express 版本 (如果没有以管理员身份安装就可能找不到 express)
E:node>express -V
3.4.8

# 新建一个测试实例
E:node>express mySite

   create : mySite
   create : mySite/package.json
   create : mySite/app.js
   create : mySite/public
   create : mySite/public/images
   create : mySite/routes
   create : mySite/routes/index.js
   create : mySite/routes/user.js
   create : mySite/views
   create : mySite/views/layout.jade
   create : mySite/views/index.jade
   create : mySite/public/javascripts
   create : mySite/public/stylesheets
   create : mySite/public/stylesheets/style.css

   install dependencies:
     $ cd mySite && npm install

   run the app:
     $ node app

# 安装 express 的模板引擎
E:node>npm install jade

# 运行 express
E:nodemySite>node app
Express server listening on port 3000

如果运行 express 实在是找不到命令,就考虑用全路径吧:

E:node>node E:nodenode_modulesexpressbinexpress mySite

   create : mySite
   create : mySite/package.json
   create : mySite/app.js
   create : mySite/public
   create : mySite/public/images
   create : mySite/routes
   create : mySite/routes/index.js
   create : mySite/routes/user.js
   create : mySite/views
   create : mySite/views/layout.jade
   create : mySite/views/index.jade
   create : mySite/public/javascripts
   create : mySite/public/stylesheets
   create : mySite/public/stylesheets/style.css

   install dependencies:
     $ cd mySite && npm install

   run the app:
     $ node app

弹出的提示说目前server 已经在 3000 端口上运行了,那么这个时候就已经可以打开浏览器访问 http://localhost:3000 或者 http://127.0.0.1:3000

当你看到 “Welcome to Express” 字样的时候表示你已经成功搭建了一个 express 站点。

目前 express 默认示例的 mySite/app.js 代码是:

/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');

var app = express();

// 环境配置
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

// 路由配置
app.get('/', routes.index);
app.get('/users', user.list);

// 开启服务
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

根据路由配置,我们知道有两个路径是可以访问的:
一个是 http://127.0.0.1:3000 即默认的根目录 ‘/’
还有一个是 http://127.0.0.1:3000/users 即 ‘/users’

我们可以访问 http://127.0.0.1:3000/users 看下, 页面上显示 respond with a resource。 这个地方

根据:
var user = require(‘./routes/user’);
app.get(‘/users’, user.list);

可以找到在 /routes/user.js

/*
 * GET users listing.
 */

exports.list = function(req, res){
  res.send("respond with a resource");
};

这里面的参数 req 是 request 的简写,即客户端发上来的请求
res 是 response 的简写,是服务端的响应。
res.send(“respond with a resource”); 是指服务端下发一个字符串,内容就是刚才页面上看到的了。

你也可以修改一下这个字符串看看,不过需要注意的时候修改之后,node app 需要重新启动一次(这个可以通过一些模块来解决重启的问题)。