Express 的使用

以下內容,基於 Express 4.x 版本

Node.js 的 Express

Express 估計是那種你第一次接觸,就會喜歡上用它的框架。因爲它真的非常簡單,直接。

在當前版本上,一共才這麼幾個文件:

lib/
├── application.js
├── express.js
├── middleware
│   ├── init.js
│   └── query.js
├── request.js
├── response.js
├── router
│   ├── index.js
│   ├── layer.js
│   └── route.js
├── utils.js
└── view.js

這種程度,說它是一個“框架”可能都有些過了,幾乎都是工具性質的實現,只限於 Web 層。

當然,直接了當地實現了 Web 層的基本功能,是得益於 Node.js 本身的 API 中,就提供了 nethttp 這兩層, Expresshttp 的方法包裝一下即可。

不過,本身功能簡單的東西,在 package.json 中卻有好長一串 dependencies 列表。

Hello World

在跑 Express 前,你可能需要初始化一個 npm 項目,然後再使用 npm 安裝 Express

mkdir p
cd p
npm init
npm install express --save

新建一個 app.js

const express = require('express');
const app = express();
app.all('/', (req, res) => res.send('hello') );
app.listen(8888);

調試信息是通過環境變量 DEBUG 控制的:

const process = require('process');
process.env['DEBUG'] = 'express:*';

這樣就可以在終端看到帶顏色的輸出了,嗯,是的,帶顏色控制字符,vim 中直接跑就 SB 了。

應用 Application

Application 是一個上層統籌的概念,整合“請求-響應”流程。 express() 的調用會返回一個 application ,一個項目中,有多個 app 是沒問題的:

const express = require('express');

const app = express();
app.all('/', (req, res) => res.send('hello'));
app.listen(8888);

const app2 = express();
app2.all('/', (req, res) => res.send('hello2'));
app2.listen(8889);

多個 app 的另一個用法,是直接把某個 path 映射到整個 app

const express = require('express');

const app = express();

app.all('/', (req, res) => {
    res.send('ok');
});

const app2 = express();
app2.get('/xx', (req, res, next) => res.send('in app2') )
app.use('/2', app2)

app.listen(8888);

這樣,當訪問 /2/xx 時,就會看到 in app2 的響應。

前面說了 app 實際上是一個上層調度的角色,在看後面的內容之前,先說一下 Express 的特點,整體上來說,它的結構基本上是“回調函數串行”,無論是 app ,或者 routehandlemiddleware 這些不同的概念,它們的形式,基本是一致的,就是 (res, req, next) => {} ,串行的流程依賴 next() 的顯式調用。

我們把 app 的功能,分成五個部分來說。

路由 - Handler 映射

app.all('/', (req, res, next) => {});
app.get('/', (req, res, next) => {});
app.post('/', (req, res, next) => {});
app.put('/', (req, res, next) => {});
app.delete('/', (req, res, next) => {});

上面的代碼就是基本的幾個方法,路由的匹配是串行的,可以通過 next() 控制:

const express = require('express');

const app = express();

app.all('/', (req, res, next) => {
    res.send('1 ');
    console.log('here');
    next();
});

app.get('/', (req, res, next) => {
    res.send('2 ');
    console.log('get');
    next();
});

app.listen(8888);

對於上面的代碼,因爲重複調用 send() 會報錯。

同樣的功能,也可以使用 app.route() 來實現:

const express = require('express');

const app = express();

app.route('/').all( (req, res, next) => {
    console.log('all');
    next();
}).get( (req, res, next) => {
    res.send('get');
    next();
}).all( (req, res, next) => {
    console.log('tail');
    next();
});

app.listen(8888);

app.route() 也是一種抽象通用邏輯的形式。

還有一個方法是 app.params ,它把“命名參數”的處理單獨拆出來了(我個人不理解這玩意兒有什麼用):

const express = require('express');

const app = express();

app.route('/:id').all( (req, res, next) => {
    console.log('all');
    next();
}).get( (req, res, next) => {
    res.send('get');
    next()
}).all( (req, res, next) => {
    console.log('tail');
});

app.route('/').all( (req, res) => {res.send('ok')});

app.param('id', (req, res, next, value) => {
    console.log('param', value);
    next();
});

app.listen(8888);

app.params 中的對應函數會先行執行,並且,記得顯式調用 next()

Middleware

其實前面講了一些方法,要實現 Middleware 功能,只需要 app.all(/.*/, () => {}) 就可以了, Express 還專門提供了 app.use() 做通用邏輯的定義:

const express = require('express');

const app = express();

app.all(/.*/, (req, res, next) => {
    console.log('reg');
    next();
});

app.all('/', (req, res, next) => {
    console.log('pre');
    next();
});

app.use((req, res, next) => {
    console.log('use');
    next();
});

app.all('/', (req, res, next) => {
    console.log('all');
    res.send('/ here');
    next();
});

app.use((req, res, next) => {
    console.log('use2');
    next();
});

app.listen(8888);

注意 next() 的顯式調用,同時,注意定義的順序, use()all() 順序上是平等的。

Middleware 本身也是 (req, res, next) => {} 這種形式,自然也可以和 app 有對等的機制——接受路由過濾, Express 提供了 Router ,可以單獨定義一組邏輯,然後這組邏輯可以跟 Middleware 一樣使用。

const express = require('express');
const app = express();
const router = express.Router();

app.all('/', (req, res) => {
    res.send({a: '123'});
});

router.all('/a', (req, res) => {
    res.send('hello');
});

app.use('/route', router);

app.listen(8888);

功能開關,變量容器

app.set()app.get() 可以用來保存 app 級別的變量(對, app.get() 還和 GET 方法的實現名字上還衝突了):

const express = require('express');

const app = express();

app.all('/', (req, res) => {
    app.set('title', '標題123');
    res.send('ok');
});

app.all('/t', (req, res) => {
    res.send(app.get('title'));
});


app.listen(8888);

上面的代碼,啓動之後直接訪問 /t 是沒有內容的,先訪問 / 再訪問 /t 纔可以看到內容。

對於變量名, Express 預置了一些,這些變量的值,可以叫 settings ,它們同時也影響整個應用的行爲:

  • case sensitive routing
  • env
  • etag
  • jsonp callback name
  • json escape
  • json replacer
  • json spaces
  • query parser
  • strict routing
  • subdomain offset
  • trust proxy
  • views
  • view cache
  • view engine
  • x-powered-by

具體的作用,可以參考 https://expressjs.com/en/4x/api.html#app.set

(上面這些值中,幹嘛不放一個最基本的 debug 呢……)

除了基本的 set() / get() ,還有一組 enable() / disable() / enabled() / disabled() 的包裝方法,其實就是 set(name, false) 這種。 set(name) 這種只傳一個參數,也可以獲取到值,等於 get(name)

模板引擎

Express 沒有自帶模板,所以模板引擎這塊就被設計成一個基礎的配置機制了。

const process = require('process');
const express = require('express');
const app = express();

app.set('views', process.cwd() + '/template');

app.engine('t2t', (path, options, callback) => {
    console.log(path, options);
    callback(false, '123');
});

app.all('/', (req, res) => {
    res.render('demo.t2t', {title: "標題"}, (err, html) => {
        res.send(html)
    });
});

app.listen(8888);

app.set('views', ...) 是配置模板在文件系統上的路徑, app.engine() 是擴展名爲標識,註冊對應的處理函數,然後, res.render() 就可以渲染指定的模板了。 res.render('demo') 這樣不寫擴展名也可以,通過 app.set('view engine', 't2t') 可以配置默認的擴展名。

這裏,注意一下 callback() 的形式,是 callback(err, html)

端口監聽

app 功能的最後一部分, app.listen() ,它完成的形式是:

app.listen([port[, host[, backlog]]][, callback])

注意, host 是第二個參數。

backlog 是一個數字,配置可等待的最大連接數。這個值同時受操作系統的配置影響。默認是 512 。

請求 Request

這一塊倒沒有太多可以說的,一個請求你想知道的信息,都被包裝到 req 的屬性中的。除了,頭。頭的信息,需要使用 req.get(name) 來獲取。

GET 參數

使用 req.query 可以獲取 GET 參數:

const express = require('express');
const app = express();

app.all('/', (req, res) => {
    console.log(req.query);
    res.send('ok');
});

app.listen(8888);

請求:

# -*- coding: utf-8 -*-
import requests
requests.get('http://localhost:8888', params={"a": '中文'.encode('utf8')})

POST 參數

POST 參數的獲取,使用 req.body ,但是,在此之前,需要專門掛一個 Middleware , req.body 纔有值:

const express = require('express');
const app = express();

app.use(express.urlencoded({ extended: true }));
app.all('/', (req, res) => {
    console.log(req.body);
    res.send('ok');
});

app.listen(8888);
# -*- coding: utf-8 -*-

import requests

requests.post('http://localhost:8888', data={"a": '中文'})

如果你是整塊扔的 json 的話:

# -*- coding: utf-8 -*-

import requests
import json

requests.post('http://localhost:8888', data=json.dumps({"a": '中文'}),
              headers={'Content-Type': 'application/json'})

Express 中也有對應的 express.json() 來處理:

const express = require('express');
const app = express();

app.use(express.json());
app.all('/', (req, res) => {
    console.log(req.body);
    res.send('ok');
});

app.listen(8888);

Express 中處理 body 部分的邏輯,是單獨放在 body-parser 這個 npm 模塊中的。 Express 也沒有提供方法,方便地獲取原始 raw 的內容。另外,對於 POST 提交的編碼數據, Express 只支持 UTF-8 編碼。

如果你要處理文件上傳,嗯, Express 沒有現成的 Middleware ,額外的實現在 https://github.com/expressjs/multer 。( Node.js 天然沒有“字節”類型,所以在字節級別的處理上,就會感覺很不順啊)

Cookie

Cookie 的獲取,也跟 POST 參數一樣,需要外掛一個 cookie-parser 模塊才行:

const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser())
app.all('/', (req, res) => {
    console.log(req.cookies);
    res.send('ok');
});

app.listen(8888);

請求:

# -*- coding: utf-8 -*-

import requests
import json

requests.post('http://localhost:8888', data={'a': '中文'},
              headers={'Cookie': 'a=1'})

如果 Cookie 在響應時,是配置 res 做了簽名的,則在 req 中可以通過 req.signedCookies 處理簽名,並獲取結果。

來源 IP

ExpressX-Forwarded-For 頭,做了特殊處理,你可以通過 req.ips 獲取這個頭的解析後的值,這個功能需要配置 trust proxy 這個 settings 來使用:

const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser())
app.set('trust proxy', true);
app.all('/', (req, res) => {
    console.log(req.ips);
    console.log(req.ip);
    res.send('ok');
});

app.listen(8888);

請求:

# -*- coding: utf-8 -*-

import requests
import json

#requests.get('http://localhost:8888', params={"a": '中文'.encode('utf8')})
requests.post('http://localhost:8888', data={'a': '中文'},
              headers={'X-Forwarded-For': 'a, b, c'})

如果 trust proxy 不是 true ,則 req.ip 會是一個 ipv4 或者 ipv6 的值。

響應 Response

Express 的響應,針對不同類型,本身就提供了幾種包裝了。

普通響應

使用 res.send 處理確定性的內容響應:

res.send({ some: 'json' });
res.send('<p>some html</p>');
res.status(404); res.end();
res.status(500); res.end();

res.send() 會自動 res.end() ,但是,如果只使用 res.status() 的話,記得加上 res.end()

模板渲染

模板需要預先配置,在 Request 那節已經介紹過了。

const process = require('process');
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser())

app.set('trust proxy', false);
app.set('views', process.cwd() + '/template');
app.set('view engine', 'html');
app.engine('html', (path, options, callback) => {
    callback(false, '<h1>Hello</h1>');
});

app.all('/', (req, res) => {
    res.render('index', {}, (err, html) => {
        res.send(html);
    });
});

app.listen(8888);

這裏有一個坑點,就是必須在對應的目錄下,有對應的文件存在,比如上面例子的 template/index.html ,那麼 app.engine() 中的回調函數纔會執行。都自定義回調函數了,這個限制沒有任何意義, path, options 傳入就好了,至於是不是要通過文件系統讀取內容,怎麼讀取,又有什麼關係呢。

Cookie

res.cookie 來處理 Cookie 頭:

const process = require('process');
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser("key"))

app.set('trust proxy', false);
app.set('views', process.cwd() + '/template');
app.set('view engine', 'html');
app.engine('html', (path, options, callback) => {
    callback(false, '<h1>Hello</h1>');
});
app.all('/', (req, res) => {
    res.render('index', {}, (err, html) => {
        console.log('cookie', req.signedCookies.a);
        res.cookie('a', '123', {signed: true});
        res.cookie('b', '123', {signed: true});
        res.clearCookie('b');
        res.send(html);
    });
});

app.listen(8888);

請求:

# -*- coding: utf-8 -*-

import requests
import json

res = requests.post('http://localhost:8888', data={'a': '中文'},
                    headers={'X-Forwarded-For': 'a, b, c',
                             'Cookie': 'a=s%3A123.p%2Fdzmx3FtOkisSJsn8vcg0mN7jdTgsruCP1SoT63z%2BI'})
print(res, res.text, res.headers)

注意三點:

  • app.use(cookieParser("key")) 這裏必須要有一個字符串做 key ,纔可以正確使用簽名的 cookie 。
  • clearCookie() 仍然是用“設置過期”的方式來達到刪除目的,cookie()clearCookie() 並不會整合,會寫兩組 b=xx 進頭。
  • res.send() 會在連接上完成一個響應,所以,與頭相關的操作,都必須放在 res.send() 前面。

頭和其它

res.set() 可以設置指定的響應頭, res.rediect(301, 'http://www.zouyesheng.com') 處理重定向, res.status(404); res.end() 處理非 20 響應。

const process = require('process');
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser("key"))

app.set('trust proxy', false);
app.set('views', process.cwd() + '/template');
app.set('view engine', 'html');
app.engine('html', (path, options, callback) => {
    callback(false, '<h1>Hello</h1>');
});

app.all('/', (req, res) => {
    res.render('index', {}, (err, html) => {
        res.set('X-ME', 'zys');
        //res.redirect('back');
        //res.redirect('http://www.zouyesheng.com');
        res.status(404);
        res.end();
    });
});

app.listen(8888);

res.redirect('back') 會自動獲取 referer 頭作爲 Location 的值,使用這個時,注意 referer 爲空的情況,會造成循環重複重定向的後果。

Chunk 響應

Chunk 方式的響應,指連接建立之後,服務端的響應內容是不定長的,會加個頭: Transfer-Encoding: chunked ,這種狀態下,服務端可以不定時往連接中寫入內容(不排除服務端的實現會有緩衝區機制,不過我看 Express 沒有)。

const process = require('process');
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser("key"))

app.set('trust proxy', false);
app.set('views', process.cwd() + '/template');
app.set('view engine', 'html');
app.engine('html', (path, options, callback) => {
    callback(false, '<h1>Hello</h1>');
});

app.all('/', (req, res) => {
    const f = () => {
        const t = new Date().getTime() + '\n';
        res.write(t);
        console.log(t);
        setTimeout(f, 1000);
    }

    setTimeout(f, 1000);
});

app.listen(8888);

上面的代碼,訪問之後,每過一秒,都會收到新的內容。

大概是 res 本身是 Node.js 中的 stream 類似對象,所以,它有一個 write() 方法。

要測試這個效果,比較方便的是直接 telet:

zys@zys-alibaba:/home/zys/temp >>> telnet localhost 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Host: localhost

HTTP/1.1 200 OK
X-Powered-By: Express
Date: Thu, 20 Jun 2019 08:11:40 GMT
Connection: keep-alive
Transfer-Encoding: chunked

e
1561018300451

e
1561018301454

e
1561018302456

e
1561018303457

e
1561018304458

e
1561018305460

e
1561018306460

每行前面的一個字節的 e ,爲 16 進制的 14 這個數字,也就是後面緊跟着的內容的長度,是 Chunk 格式的要求。具體可以參考 HTTP 的 RFC , https://tools.ietf.org/html/rfc2616#page-2

Tornado 中的類似實現是:

# -*- coding: utf-8 -*-

import tornado.ioloop
import tornado.web
import tornado.gen
import time

class MainHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def get(self):
        while True:
            yield tornado.gen.sleep(1)
            s = time.time()
            self.write(str(s))
            print(s)
            yield self.flush()


def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Express 中的實現,有個大坑,就是:

app.all('/', (req, res) => {
    const f = () => {
        const t = new Date().getTime() + '\n';
        res.write(t);
        console.log(t);
        setTimeout(f, 1000);
    }

    setTimeout(f, 1000);
});

這段邏輯,在連接已經斷了的情況下,並不會停止,還是會永遠執行下去。所以,你得自己處理好:

const process = require('process');
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser("key"))

app.set('trust proxy', false);
app.set('views', process.cwd() + '/template');
app.set('view engine', 'html');
app.engine('html', (path, options, callback) => {
    callback(false, '<h1>Hello</h1>');
});

app.all('/', (req, res) => {
    let close = false;
    const f = () => {
        const t = new Date().getTime() + '\n';
        res.write(t);
        console.log(t);
        if(!close){
            setTimeout(f, 1000);
        }
    }

    req.on('close', () => {
        close = true;
    });

    setTimeout(f, 1000);
});

app.listen(8888);

req 掛了一些事件的,可以通過 close 事件來得到當前連接是否已經關閉了。

req 上直接掛連接事件,從 net http Express 這個層次結構上來說,也很,尷尬了。 Web 層不應該關心到網絡連接這麼底層的東西的。

我還是習慣這樣:

app.all('/', (req, res) => {
    res.write('<h1>123</h1>');
    res.end();
});

不過 res.write() 是不能直接處理 json 對象的,還是老老實實 res.send() 吧。

我會怎麼用 Express

先說一下,我自己,目前在 Express 運用方面,並沒有太多的時間和複雜場景的積累。

即使這樣,作爲技術上相對傳統的人,我會以我以往的 web 開發的套路,來使用 Express

我不喜歡日常用 app.all(path, callback) 這種形式去組織代碼。

首先,這會使 path 定義散落在各處,方便了開發,麻煩了維護。

其次,把 path 和具體實現邏輯 callback 綁在一起,我覺得也是反思維的。至少,對於我個人來說,開發的過程,先是想如何實現一個 handler ,最後,再是考慮要把這個 handle 與哪些 path 綁定。

再次,單純的 callback 缺乏層次感,用 app.use(path, callback) 這種來處理共用邏輯的方式,我覺得完全是扯談。共用邏輯是代碼之間本身實現上的關係,硬生生跟網絡應用層 HTTP 協議的 path 概念抽上關係,何必呢。當然,對於 callback 的組織,用純函數來串是可以的,不過我在這方面並沒有太多經驗,所以,我還是選擇用類繼承的方式來作層次化的實現。

我自己要用 Express ,大概會這樣組件項目代碼(不包括關係數據庫的 Model 抽象如何組織這部分):

./
├── config.conf
├── config.js
├── handler
│   ├── base.js
│   └── index.js
├── middleware.js
├── server.js
└── url.js
  • config.conf 是 ini 格式的項目配置。
  • config.js 處理配置,包括日誌,數據庫連接等。
  • middleware.js 是針對整體流程的擴展機制,比如,給每個請求加一個 UUID ,每個請求都記錄一條日誌,日誌內容有請求的細節及本次請求的處理時間。
  • server.js 是主要的服務啓動邏輯,整合各種資源,命令行參數 port 控制監聽哪個端口。不需要考慮多進程問題,(正式部署時 nginx 反向代理到多個應用實例,多個實例及其它資源統一用 supervisor 管理)。
  • url.js 定義路徑與 handler 的映射關係。
  • handler ,具體邏輯實現的地方,所有 handler 都從 BaseHandler 繼承。

BaseHandler 的實現:

class BaseHandler {
    constructor(req, res, next){
        this.req = req;
        this.res = res;
        this._next = next;
        this._finised = false;
    }

    run(){
        this.prepare();
        if(!this._finised){
            if(this.req.method === 'GET'){
                this.get();
                return;
            }
            if(this.req.method === 'POST'){
                this.post();
                return;
            }
            throw Error(this.req.method + ' this method had not been implemented');
        }
    }

    prepare(){}
    get(){
        throw Error('this method had not been implemented');
    }
    post(){
        throw Error('this method had not been implemented');
    }

    render(template, values){
        this.res.render(template, values, (err, html) => {
            this.finish(html);
        });
    }

    write(content){
        if(Object.prototype.toString.call(content) === '[object Object]'){
            this.res.write(JSON.stringify(content));
        } else {
            this.res.write(content);
        }
    }

    finish(content){
        if(this._finised){
            throw Error('this handle was finished');
        }
        this.res.send(content);
        this._finised = true;
        if(this._next){ this._next() }
    }

}

module.exports = {BaseHandler};

if(module === require.main){
    const express = require('express');
    const app = express();
    app.all('/', (req, res, next) => new BaseHandler(req, res, next).run() );
    app.listen(8888);
}

要用的話,比如 index.js

const BaseHandler = require('./base').BaseHandler;

class IndexHandler extends BaseHandler {
    get(){
        this.finish({a: 'hello'});
    }
}

module.exports = {IndexHandler};

url.js 中的樣子:

const IndexHandler = require('./handler/index').IndexHandler;

const Handlers = [];

Handlers.push(['/', IndexHandler]);

module.exports = {Handlers};

日誌

後面這幾部分,都不屬於 Express 本身的內容了,只是我個人,隨便想到的一些東西。

找一個日誌模塊的實現,功能上,就看這麼幾點:

  • 標準的級別: DEBUG,INFO,WARN, ERROR 這些。
  • 層級的多個 logger
  • 可註冊式的多種 Handler 實現,比如文件系統,操作系統的 rsyslog ,標準輸出,等。
  • 格式定義,一般都帶上時間和代碼位置。

Node.js 中,大概就是 log4js 了, https://github.com/log4js-node/log4js-node

const log4js = require('log4js');

const layout = {
    type: 'pattern',
    pattern: '- * %p * %x{time} * %c * %f * %l * %m',
    tokens: {
        time: logEvent => {
            return new Date().toISOString().replace('T', ' ').split('.')[0];
        }
    }
};
log4js.configure({
  appenders: {
        file: { type: 'dateFile', layout: layout, filename: 'app.log', keepFileExt: true },
        stream: { type: 'stdout', layout: layout }
  },
  categories: {
      default: { appenders: [ 'stream' ], level: 'info', enableCallStack: false },
      app: { appenders: [ 'stream', 'file' ], level: 'info', enableCallStack: true }
  }
});

const logger = log4js.getLogger('app');
logger.error('xxx');

const l2 = log4js.getLogger('app.good');
l2.error('ii');

總的來說,還是很好用的,但是官網的文檔不太好讀,有些細節的東西沒講,好在源碼還是比較簡單。

說幾點:

  • getLogger(name) 需要給一個名字,否則 default 的規則都匹配不到。
  • getLogger('parent.child') 中的名字,規則匹配上,可以通過 . 作父子繼承的。
  • enableCallStack: true 加上,才能拿到文件名和行號。

ini 格式配置

json 作配置文件,功能上沒問題,但是對人爲修改是不友好的。所以,個人還是喜歡用 ini 格式作項目的環境配置文件。

Node.js 中,可以使用 ini 模塊作解析:

const s = `
[database]
host = 127.0.0.1
port = 5432
user = dbuser
password = dbpassword
database = use_this_database

[paths.default]
datadir = /var/lib/data
array[] = first value
array[] = second value
array[] = third value
`

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(s);
console.log(config);

它擴展了 array[] 這種格式,但沒有對類型作處理(除了 true false),比如,獲取 port ,結果是 "5432" 。簡單夠用了。

WebSocket

Node.js 中的 WebSocket 實現,可以使用 ws 模塊, https://github.com/websockets/ws

要把 ws 的 WebSocket Server 和 Expressapp 整合,需要在 ExpressServer 層面動手,實際上這裏說的 Server 就是 Node.js 的 http 模塊中的 http.createServer()

const express = require('express');
const ws = require('ws');

const app = express();

app.all('/', (req, res) => {
    console.log('/');
    res.send('hello');
});

const server = app.listen(8888);

const wss = new ws.Server({server, path: '/ws'});
wss.on('connection', conn => {
    conn.on('message', msg => {
        console.log(msg);
        conn.send(new Date().toISOString());
    });
});

對應的一個客戶端實現,來自: https://github.com/ilkerkesen/tornado-websocket-client-example/blob/master/client.py

# -*- coding: utf-8 -*-

import time
from tornado.ioloop import IOLoop, PeriodicCallback
from tornado import gen
from tornado.websocket import websocket_connect

class Client(object):
    def __init__(self, url, timeout):
        self.url = url
        self.timeout = timeout
        self.ioloop = IOLoop.instance()
        self.ws = None
        self.connect()
        PeriodicCallback(self.keep_alive, 2000).start()
        self.ioloop.start()

    @gen.coroutine
    def connect(self):
        print("trying to connect")
        try:
            self.ws = yield websocket_connect(self.url)
        except Exception:
            print("connection error")
        else:
            print("connected")
            self.run()

    @gen.coroutine
    def run(self):
        while True:
            msg = yield self.ws.read_message()
            print('read', msg)
            if msg is None:
                print("connection closed")
                self.ws = None
                break

    def keep_alive(self):
        if self.ws is None:
            self.connect()
        else:
            self.ws.write_message(str(time.time()))


if __name__ == "__main__":
    client = Client("ws://localhost:8888/ws", 5)

其它

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章