Node.js硬实战 - 构建精简网络应用

杨旭 bio photo By 杨旭

网络:构建精简的网络应用


第九章 构建精简的网络应用


  • 使用Node开发客户端
  • 浏览器的Node
  • 服务端技术和WebSockets
  • 应用从Express3迁移到Express4
  • 测试Web应用
  • 全栈框架和实时服务

前端技术

快速的静态网站服务器

仅仅希望构建一个服务器,提供静态网站访问能力或者单页面Web应用

大多数web开发任务都是从服务器上获取某些文件开始

在所谓的无服务应用上,通常是通过工程化构建工具,将客户端文件打包处理后,通过简单的静态服务器提供web访问能力

方案一:简单的connect脚本(connect 2.30.2)

提供当前目录的静态访问能力。

const connect = require('connect')

connect.createServer(
    connect.static(__dirname)
).listen(8080)

方案二:通过命令行工具

$ yarn global add glance
$ glance 

方案三:使用Grunt的任务执行器

  • 安装依赖
    $ yarn global add grunt-cli
    $ yarn add grunt grunt-contrib-connect --dev
    
  • 创建Gruntfile.js文件
    module.exports = function (grunt) {
      grunt.loadNpmTasks('grunt-contrib-connect')
    
      grunt.initConfig({
          connect: {
              server: {
                  options: {
                      port: 8080,
                      base: './',
                      keepalive: true
                  }
              }
          }
      })
    
      grunt.registerTask('default', ['connect:server'])
    }
    
  • 命令行执行grunt调用默认的default任务

在Node中使用DOM

在Node中模拟浏览器,例如要编写Web爬虫,浏览器处理提供JavaScript运行环境,更重要的是提供了文档对象模型(DOM)的API

使用cheerio在Node中复用依赖于DOM的客户端代码,或者渲染整个web页面内容

const cheerio = require('cheerio')

const $ = cheerio.load('<p class="info">Welcome</p>')
console.log($('.info').text())

服务端技术

Express的路由分离

随着应用规模的扩大,将所有路由都定义在app.js中会导致文件臃肿并且难以维护。

基于express内置的路由系统,可以按模块来组织路由。

  • 新建birds.js文件 ```js var express = require(‘express’); var router = express.Router();

// middleware that is specific to this router router.use(function timeLog(req, res, next) { console.log(‘Time: ‘, Date.now()); next(); }); // define the home page route router.get(‘/’, function(req, res) { res.send(‘Birds home page’); }); // define the about route router.get(‘/about’, function(req, res) { res.send(‘About birds’); });

module.exports = router;


- 在app.js中应用路由
```js
var express = require('express');
var birds = require('./birds')

var app = express();

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

app.use('/birds', birds);

app.listen(3000, function () {
    console.log('Example app listening on port 3000!');
});

自动重启服务器

使用第三方工具检查文件变更并重启web应用

  • 原生实现
const fs = require('fs')
const exec = require('child_process').exec

function watch() {
    const child = exec('node index.js')
    const watcher = fs.watch(__dirname + '/index.js', function(event) {
        console.log('file changed, reloading')
        child.kill()
        watcher.close()
        watch()
    })
}

watch()
  • 但是,web应用通常由多个文件组成,递归以及多文件监听会变得复杂,需要其他三方件的帮助

使用nodemon

nodemon拥有良好的特性和众多配置项来控制web应用的重启

  • 创建nodemon.json
{
  "ignore": [
    ".git",
    "node_modules"
  ],
  "execMap": {
    "js": "node --harmony"
  },
  "watch": [
    "index.js",
    "birds.js"
  ],
  "env": {
    "NODE_ENV": "development"
  },
  "ext": "js json"
}
  • 执行nodemon index.js启动应用

配置web应用

在开发环境、测试环境、生产环境使用不同的配置项

使用JSON配置文件、环境变量或者一个模块来管理不同的配置

基于环境变量

if (app.get('env') === 'development') {
    // ...
} else {
    // ...
}

根据NODE_ENV变量区分配置项

$ NODE_ENV=production node index.js

使用配置文件管理

const config = {
    development: require('./development.json'),
    production: require('./production.json'),
    test: require('./test.json')
}

module.exports = config[process.env.NODE_ENV || 'development']

使用第三方模块

$ yarn add nconf
var nconf = require('nconf')

// 命令行 > 环境变量 > 配置文件
nconf.argv().env().file({file: 'config.json'})

app.listen(nconf.get('port'), function () {
    console.log('Example app listening on port 3000!');
});

构建restful服务

RESTful架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。

REST,即Representational State Transfer(表现层状态转化)

“表现层”其实指的是”资源”(Resources)的”表现层”。

所谓“资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务

可以用一个URI(统一资源定位符)指向它,我们把”资源”具体呈现出来的形式,叫做它的”表现层”(Representation)。

资源具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对”表现层”的描述

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”。

分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源

使用express构建restful服务

明确目标

  • 处理CRUD(下面讲用bears)
  • 加一个标准的URL(http://example.com/api/bears)和(http://example.com/api/bears/:bear_id)
  • 用相应的http的动词达到rest效果(GET, POST, PUT, DELETE)
  • 返回JSON数据
  • 在控制台打印所有的请求

使用express初始化项目

  • 初始化依赖,加入express
$ yarn init
$ yarn add express body-parser
  • 创建app.js文件,利用官方例子创建最简单的应用
var express = require('express');
var app = express();

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

app.listen(3000, function () {
    console.log('Example app listening on port 3000!');
});
  • 加入nodemon监听文件变化,重启服务
$ yarn add nodemon --dev
  • 创建nodemon.json文件
{
  "restartable": "rs",
  "verbose": true,
  "env": {
    "NODE_ENV": "development",
    "PORT": "3000"
  },
  "execMap": {
    "": "node --debug",
    "js": "node --debug"
  },
  "watch": [
    "app/",
    "bin/",
    "routes/",
    "views/",
    "app.js"
  ],
  "ignore": [
    ".git",
    ".idea",
    "node_modules"
  ],
  "ext": "js jade"
}
  • 执行nodemon启动应用(注意将package.json中的main字段修改为app.js)

添加bodyparser中间件来解析请求中的json数据

bodyParser = require('body-parser');

// 给app配置bodyParser中间件
// 通过如下配置再路由种处理request时,可以直接获得post请求的body部分
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

创建路由模块

// API路由配置
var router = express.Router();              // 获得express router对象
router.get('/', function(req, res) {
    res.json({ message: 'hooray! welcome to our api!' });
})

// 注册路由
// 所有的路由会加上“/api”前缀
app.use('/api', router)

使用POSTMAN来测试服务

创建数据库,用web应用连接数据库

$ yarn add mongoose

// app.js
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/bears');

创建数据模型

// model/bears.js
var mongoose   = require('mongoose');
var Schema     = mongoose.Schema;

var BearSchema = new Schema({
    name: String
});

module.exports = mongoose.model('Bear', BearSchema);

定义路由

新增router/bears.js文件,声明bears路由和数据保存操作

const express = require('express')
const Bear = require('../model/bears')

const router = express.Router()

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
    console.log('Time: ', Date.now());
    next();
});

router.route('/bears')
    .post((req, res) => {
        const bear = new Bear();      // 创建一个Bear model的实例
        bear.name = req.body.name;  // 从request取出name参数的值然后设置bear的name字段

        // 保存bear,加入错误处理,即把错误作为响应返回
        bear.save(function(err) {
            if (err)
                res.send(err);

            res.json({ message: 'Bear created!' });
        });

    })

module.exports = router

新增get方法

    .get(function(req, res) {
        Bear.find(function(err, bears) {
            if (err)
                res.send(err);

            res.json(bears);
        });
    });

为单条记录创建GET、DELETE和PUT请求


router.route('/bears/:bear_id')
    .get(function(req, res) {
        Bear.findById(req.params.bear_id, function(err, bear) {
            if (err) res.send(err);
            res.json(bear);
        });
    })
    .put((req, res) => {
        Bear.findById(req.params.bear_id, function(err, bear) {
            if (err) res.send(err);
            
            bear.name = req.body.name
            
            bear.save((err) => {
                if (err) res(err)
                res.json({ message: 'Bear updated!' });
            })
        });

    })
    .delete(function(req, res) {
        Bear.remove({
            _id: req.params.bear_id
        }, function(err, bear) {
            if (err)
                res.send(err);

            res.json({ message: 'Successfully deleted' });
        });
    });


全栈框架

  • MEAN: MongoDB, Express, Angular, Node
  • Linnovate 使用Mongoose处理数据层,Passport处理身份认证,使用Bootstrap作为UI
  • Derby 使用Racer代替Mongoose处理数据层
  • Kraken 基于express、子目录、控制器、Grunt和测试
  • Meteor 使用MongoDB,基于发布/订阅模式,拥抱反射范式,在桌面开发领域相当流行