Pinvon's Blog

所见, 所闻, 所思, 所想

Express框架

声明

安装

创建工程目录

mkdir test
cd test

配置

在项目根目录新建文件 package.json, 添加基本配置.

{
    "name": "hello-world",
    "description": "hello world test app",
    "version": "0.0.1",
    "private": true,
    "dependencies": {
        "express": "4.x"
    }
}

安装

npm install

启动文件

在项目根目录新建文件 index.js, 作为启动文件.

var express = require('express');
var app = express();
app.get('/', function (req, res) {
    res.send('Hello world');
});
app.listen(3000);

运行.

node index

打开浏览器, 输入地址: http://localhost:3000 , 网页将会显示Hello world.

合理的结构

合理的目录结构至关重要, 方便项目管理.

路由(用于指定不同访问路径所对应的回调函数)应该放在一个单独的目录中. 新建 routes 子目录, 创建文件 index.js, 编辑如下:

module.exports = function (app) {
  app.get('/', function (req, res) {
    res.send('Hello world');
  });
  app.get('/customer', function(req, res){
    res.send('customer page');
  });
  app.get('/admin', function(req, res){
    res.send('admin page');
  });
};

而原本的 index.js 文件则修改如下:

var express = require('express');
var app = express();
var routes = require('./routes')(app);
app.listen(3000);

运行原理

底层: http模块

Node.jshttp模块 生成服务器的代码如下:

var http = require("http");
var app = http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello world!");
});
app.listen(3000, "localhost");

代码的关键是 http.createServer(), 表示生成一个HTTP服务器实例. 该方法接受一个回调函数, 参数分别代表HTTP请求和HTTP回应的request对象和response对象.

Express 框架对其进行了再包装, 上面的代码用 Express 改写如下:

var express = require('express');
var app = express();
app.get('/', function (req, res) {
  res.send('Hello world!');
});
app.listen(3000);

原来用 http.createServer() 方法创建的app实例, 现在改成用 Express 的构造方法来生成. Express框架 等于是在 http模块 上加了一个中间层.

中间件

中间件是处理HTTP请求的函数. 它的特点是, 一个中间件处理完后, 才会传递给下一个中间件处理. 一种清晰的写法如下:

var express = require("express");
var http = require("http");

var app = express();

app.use("/home", function(request, response, next) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  response.end("Welcome to the homepage!\n");
});

app.use("/about", function(request, response, next) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  response.end("Welcome to the about page!\n");
});

app.use(function(request, response) {
  response.writeHead(404, { "Content-Type": "text/plain" });
  response.end("404 error!\n");
});

http.createServer(app).listen(1337);

Express的方法

all()和HTTP动词方法

因为HTTP有多种请求, 如: GET, POST, PUT, DELETE, 为了使程序更加清晰, Express框架不建议统一使用 use(), 它提供了 use() 方法的一些别名, 根据不同的请求进行调用. 因此, 上面的代码还可以改成如下形式:

var express = require("express");
var http = require("http");
var app = express();

app.all("*", function(request, response, next) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  next();
});

app.get("/", function(request, response) {
  response.end("Welcome to the homepage!");
});

app.get("/about", function(request, response) {
  response.end("Welcome to the about page!");
});

app.get("*", function(request, response) {
  response.end("404!");
});

http.createServer(app).listen(1337);

all() 表示, 所有请求都必须通过该中间件, 参数中的 * 表示对所有路径都有效. 这样其他的中间件可以省去很多重复的代码. get() 表示只有HTTP请求方式为GET时, 才通过该中间件, 它的第一个参数是请求的路径, 由于 get() 的回调函数没有调用 next(), 所以只要有一个中间件被调用了, 后面的中间件就不会再被调用.

对于请求的路径, 除了使用绝对匹配外, 还可以模式匹配. 如:

app.get("/hello/:who", function(req, res) {
    res.end("hello, " + req.params.who + ".");
});

上面的代码可以匹配"/hello/alice"网址, 网址中的alice将被捕获, 作为 req.params.who 属性的值. 需要注意的是, 捕获后一般需要对网址进行检查, 过滤不安全字符, 上面的写法只是为了演示, 实际生产中不应该这样直接使用用户提供的值.

如果在模式参数后面加上问号, 表示该参数可选.

app.get('/hello/:who?',function(req,res) {
    if(req.params.id) {
        res.end("Hello, " + req.params.who + ".");
    }
    else {
        res.send("Hello, Guest.");
    }
});

更复杂的例子:

app.get('/forum/:fid/thread/:tid', middleware)

// 匹配/commits/71dbb9c
// 或/commits/71dbb9c..4c084f9这样的git格式的网址
app.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){
  var from = req.params[0];
  var to = req.params[1] || 'HEAD';
  res.send('commit range ' + from + '..' + to);
});

set方法

用于指定变量的值.

app.set("views", __dirname + "/views");
app.set("view engine", "jade");

该代码使用 set(), 为系统变量"views"和"view engin"指定值.

response对象

response.redirect(): 网址重定向. 如: response.redirect("/hello/anime"); response.sendFile(): 发送文件. 如: response.sendFile("/path/to/anime.mp4"); response.render(): 渲染网页模板. 如:

app.get("/", function(request, response) {
  response.render("index", { message: "Hello World" });
});

该代码使用 render() 方法, 把 message 变量传入index模板, 渲染成HTML网页.

request对象

request.ip: 属性, 用于获得HTTP请求的IP地址. request.files: 用于获取上传的文件.

搭建HTTPs服务器

使用Express搭建HTTPs加密服务器.

var fs = require('fs');
var options = {
  key: fs.readFileSync('E:/ssl/myserver.key'),
  cert: fs.readFileSync('E:/ssl/myserver.crt'),
  passphrase: '1234'
};

var https = require('https');
var express = require('express');
var app = express();

app.get('/', function(req, res){
  res.send('Hello World Expressjs');
});

var server = https.createServer(options, app);
server.listen(8084);
console.log('Server is running on port 8084');

项目开发实例

首先创建工程目录, 配置, 配置文件如下:

{
   "name": "demo",
   "description": "My First Express App",
   "version": "0.0.1",
   "dependencies": {
      "express": "3.x"
   }
}

安装, 编写启动文件 app.js. 内容如下:

var express = require('express');
var path = require('path');
var app = express();

// 设定port变量,意为访问端口
app.set('port', process.env.PORT || 3000);

// 设定views变量,意为视图存放的目录
app.set('views', path.join(__dirname, 'views'));

// 设定view engine变量,意为网页模板引擎
app.set('view engine', 'jade');

app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);

// 设定静态文件目录,比如本地文件
// 目录为demo/public/images,访问
// 网址则显示为http://localhost:3000/images
app.use(express.static(path.join(__dirname, 'public')));

app.listen(app.get('port'));

set() 用于设定内部变量, use() 用于调用express的中间件.

在浏览器中访问: http://localhost:3000 , 网页提示"Cannot GET /", 表示没有为网站的根路径指定可以显示的内容. 所以下一步开始配置路由.

配置路由

所谓"路由", 就是指为不同的访问路径, 指定不同的处理方法.

app.js 中, 指定根路径的处理方法.

app.get('/', function(req, res) {
   res.send('Hello World');
});

再通过浏览器访问, 就会显示"Hello World".

如果需要指定HTTP头信息, 回调函数就必须换一种写法:

app.get('/', function(req, res){
  var body = 'Hello World';
  res.setHeader('Content-Type', 'text/plain');
  res.setHeader('Content-Length', body.length);
  res.end(body);
});

指定特定路径

假定用户访问 /api路径, 希望返回一个JSON字符串, 可以这么写:

app.get('/api', function(request, response) {
   response.send({name:"张三",age:40});
});

为了便于管理, 我们把路由的回调函数, 封装成模块, 在 routes目录 下建立一个 api.js文件.

exports.index = function (req, res) {
    res.json(200, {name:"张三", age:40});
}

然后 在 app.js 中加载这个模块:

var api = require('./routes/api');
app.get('/api', api.index);

此时, 在浏览器中访问 http://localhost:3000/api 就会有正确的文字显示出来.

静态网页模板

在项目目录中, 建立一个子目录 views, 用于存放网页模板. 假设该项目有三个路径: 根路径(/), 自我介绍(/about), 文章(/article). 修改 app.js 的中间件:

app.get('/', function (req, res) {
    res.sendfile(__dirname + '/views/index.html');
});

app.get('/about', (req, res) => {
    res.sendfile(__dirname + '/views/about.html');
});

app.get('/article', (req, res) => {
    res.sendfile(__dirname + '/views/article.html');
});

然后编辑 views/index.html:

<html>
<head>
   <title>首页</title>
</head>

<body>
<h1>Express Demo</h1>

<footer>
<p>
   <a href="/">首页</a> - <a href="/about">自我介绍</a> - <a href="/article">文章</a>
</p>
</footer>

</body>
</html>

如果想要展示动态内容, 就必须使用动态网页模板.

动态网页模板

安装模板引擎

Express支持多种模板引擎, 这里使用Handlebars模板引擎的服务器端版本.

npm install hbs --save-dev

save-dev 表示将依赖关系写入 package.json 文件.

安装完成后, 需要改写 app.js:

var express = require('express');
var hbs = require('hbs');
var app = express();

// 指定模板文件的后缀名为html
app.set('view engine', 'html');

// 运行hbs模块
app.engine('html', hbs.__express);

app.get('/', function (req, res) {
    res.render('index');
});

app.get('/about', function (req, res) {
    res.render('about');
});

app.get('/article', function (req, res) {
    res.render('article');
});

上面的代码改用 render() 对网页模板进行渲染. render() 的参数就是模板的文件名, 默认放在子目录 views 之中, 后缀名已经在前面指定为html, 这里可以省略. 所以, res.render('index') 是指: 把子目录views下面的index.html文件, 交给模板引擎hbs渲染.

新建数据脚本

渲染是指将数据代入模板的过程. 在实际应用中, 数据是保存在数据库的, 这里为简化问题, 假定数据保存在一个脚本文件中.

在项目目录中, 新建一个文件 blog.js, 用于存放数据.

var entries = [
    {"id":1, "title":"第一篇", "body":"正文", "published":"6/2/2013"},
    {"id":2, "title":"第二篇", "body":"正文", "published":"6/3/2013"},
    {"id":3, "title":"第三篇", "body":"正文", "published":"6/4/2013"},
    {"id":4, "title":"第四篇", "body":"正文", "published":"6/5/2013"},
    {"id":5, "title":"第五篇", "body":"正文", "published":"6/10/2013"},
    {"id":6, "title":"第六篇", "body":"正文", "published":"6/12/2013"}
];

exports.getBlogEntries = function (){
   return entries;
}

exports.getBlogEntry = function (id){
   for(var i=0; i < entries.length; i++){
      if(entries[i].id == id) return entries[i];
   }
}

新建网页模板

在目录 views 里新建模板文件 index.html.

<!-- views/index.html文件 -->

<h1>文章列表</h1>

{{#each entries}}
   <p>
      <a href="/article/{{id}}">{{title}}</a><br/>
      Published: {{published}}
   </p>
{{/each}}

模板文件about.html:

<!-- views/about.html文件 -->

<h1>自我介绍</h1>

<p>正文</p>

模板文件article.html:

<!-- views/article.html文件 -->

<h1>{{blog.title}}</h1>
Published: {{blog.published}}

<p/>

{{blog.body}}

以上三个模板文件都只有网页主体, 因为网页布局是共享的, 所以布局的部分可以单独新建一个文件layout.html:

<!-- views/layout.html文件 -->

<html>

<head>
   <title>{{title}}</title>
</head>

<body>

    {{{body}}}

   <footer>
      <p>
         <a href="/">首页</a> - <a href="/about">自我介绍</a>
      </p>
   </footer>

</body>
</html>

渲染模板

改写 app.js:

var express = require('express');
var hbs = require('hbs');
var app = express();

// 加载数据模块
var blogEngine = require('./blog');

// 指定模板文件的后缀名为html
app.set('view engine', 'html');

// 运行hbs模块
app.engine('html', hbs.__express);
app.use(express.bodyParser());

app.get('/', function (req, res) {
    res.render('index', {title:"最近文章", entries:blogEngine.getBlogEntries()});
});

app.get('/about', function (req, res) {
    res.render('about', {title:"自我介绍"});
});

app.get('/article/:id', function (req, res) {
    var entry = blogEngine.getBlogEntry(req.params.id);
    res.render('article', {title:entry.title, blog:entry});
});

app.listen(3000);

此时可以用浏览器访问.

指定静态文件目录

模板文件默认存放在 views子目录. 这时, 如果要在网页中加载静态文件(如样式表, 图片等), 就需要另外指定一个存放静态文件的目录.

app.use(express.static('public'));

当浏览器发出非HTML文件请求时, 服务器就到 public 目录寻找这个文件, 比如浏览器发出如下的样式表请求:

<link href="/bootstrap/css/bootstrap.css" rel="stylesheet">

服务器就到 public/bootstrap/css 目录中寻找 bootstrap.css 文件.

Express.Router用法

从Express 4.0开始, 路由器功能成了一个单独的组件 Express.Router, 它就像小型的express应用程序一样, 有自己的use, get, param, route方法.

基本用法

Express.Router 是一个构造函数, 调用后返回一个路由器实例. 再使用该实例的HTTP动词方法, 为不同的访问路径, 指定回调函数, 最后挂载到某个路径.

var router = express.Router();
router.get('/', function (req, res) {
    res.send('首页');
});
router.get('/about', function (req, res) {
    res.send('关于');
});
app.use('/', router);

app.use('/', router) 表示将之前定义的路径挂载到根目录. 如果改成 app.use('/app', router) 表示将之前定义的路径挂载到 '/app' 目录, 相当于 '/app' 和 '/app/about' 这两个路径.

router.route()

使用 router.route(), 可以直接将访问路径作为参数, 且可以对同一个路径指定get和post方法的回调函数.

var router = express.Router();
router.route('/api')
            .post (function (req, res) { ...    })
            .get (function (req, res) { ... });
app.use('/', router);

router中间件

router.use(function (req, res, next) {
    console.log(req.method, req.url);
    next();
});

中间件的放置顺序很重要, 必须放在HTTP动词方法之前, 等同于执行顺序.

对路径参数的处理

router.param('name', function(req, res, next, name) {
    // 对name进行验证或其他处理……
    console.log(name);
    req.name = name;
    next();
});

router.get('/hello/:name', function(req, res) {
    res.send('hello ' + req.name + '!');
});

上面代码中, get方法为访问路径指定了name参数, param方法则是对name参数进行处理. 注意, param方法必须放在HTTP动词方法之前.

app.route

推荐这种写法:

var app = express();
app.route('/login')
    .get(function(req, res) {
        res.send('this is the login form');
    })
    .post(function(req, res) {
        console.log('processing');
        res.send('processing the login form!');
    });

上传文件到本地目录

在网页插入上传文件的表单.

<form action="/pictures/upload" method="POST" enctype="multipart/form-data">
  Select an image to upload:
  <input type="file" name="image">
  <input type="submit" value="Upload Image">
</form>

服务器脚本建立指向 /upload 目录的路由. 可以安装 multer模块, 它提供了上传文件的许多功能.

var express = require('express');
var router = express.Router();
var multer = require('multer');

var uploading = multer({
  dest: __dirname + '../public/uploads/',
  // 设定限制,每次最多上传1个文件,文件大小不超过1MB
  limits: {fileSize: 1000000, files:1},
})

router.post('/upload', uploading, function(req, res) {

})

module.exports = router

上传文件到Amazon S3

在S3上面新增CORS配置文件.

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
  </CORSRule>
</CORSConfiguration>

上面的配置允许任意电脑向你的bucket发送HTTP请求.

然后安装 aws-sdk:

npm install aws-sdk --save

安装服务器脚本:

var express = require('express');
var router = express.Router();
var aws = require('aws-sdk');

router.get('/', function(req, res) {
  res.render('index')
})

var AWS_ACCESS_KEY = 'your_AWS_access_key'
var AWS_SECRET_KEY = 'your_AWS_secret_key'
var S3_BUCKET = 'images_upload'

router.get('/sign', function(req, res) {
  aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});

  var s3 = new aws.S3()
  var options = {
    Bucket: S3_BUCKET,
    Key: req.query.file_name,
    Expires: 60,
    ContentType: req.query.file_type,
    ACL: 'public-read'
  }

  s3.getSignedUrl('putObject', options, function(err, data){
    if(err) return res.send('Error with S3')

    res.json({
      signed_request: data,
      url: 'https://s3.amazonaws.com/' + S3_BUCKET + '/' + req.query.file_name
    })
  })
})

module.exports = router

上面代码中,用户访问/sign路径,正确登录后,会收到一个JSON对象,里面是S3返回的数据和一个暂时用来接收上传文件的URL,有效期只有60秒。

浏览器代码如下。

// HTML代码为
// <br>Please select an image
// <input type="file" id="image">
// <br>
// <img id="preview">

document.getElementById("image").onchange = function() {
  var file = document.getElementById("image").files[0]
  if (!file) return

  sign_request(file, function(response) {
    upload(file, response.signed_request, response.url, function() {
      document.getElementById("preview").src = response.url
    })
  })
}

function sign_request(file, done) {
  var xhr = new XMLHttpRequest()
  xhr.open("GET", "/sign?file_name=" + file.name + "&file_type=" + file.type)

  xhr.onreadystatechange = function() {
    if(xhr.readyState === 4 && xhr.status === 200) {
      var response = JSON.parse(xhr.responseText)
      done(response)
    }
  }
  xhr.send()
}

function upload(file, signed_request, url, done) {
  var xhr = new XMLHttpRequest()
  xhr.open("PUT", signed_request)
  xhr.setRequestHeader('x-amz-acl', 'public-read')
  xhr.onload = function() {
    if (xhr.status === 200) {
      done()
    }
  }

  xhr.send(file)
}

上面代码首先监听file控件的change事件,一旦有变化,就先向服务器要求一个临时的上传URL,然后向该URL上传文件。

Comments

使用 Disqus 评论
comments powered by Disqus