Express 的使用
以下內(nèi)容,基于 Express 4.x 版本
Node.js 的 Express
Express?估計(jì)是那種你第一次接觸,就會(huì)喜歡上用它的框架。因?yàn)樗娴姆浅:?jiǎn)單,直接。
在當(dāng)前版本上,一共才這么幾個(gè)文件:
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這種程度,說它是一個(gè)“框架”可能都有些過了,幾乎都是工具性質(zhì)的實(shí)現(xiàn),只限于 Web 層。
當(dāng)然,直接了當(dāng)?shù)貙?shí)現(xiàn)了 Web 層的基本功能,是得益于?Node.js?本身的 API 中,就提供了?net?和?http?這兩層,?Express?對(duì)?http?的方法包裝一下即可。
不過,本身功能簡(jiǎn)單的東西,在?package.json?中卻有好長一串?dependencies?列表。
Hello World
在跑?Express?前,你可能需要初始化一個(gè)?npm?項(xiàng)目,然后再使用?npm?安裝?Express:
mkdir p cd p npm init npm install express --save新建一個(gè)?app.js?:
const express = require('express'); const app = express(); app.all('/', (req, res) => res.send('hello') ); app.listen(8888);調(diào)試信息是通過環(huán)境變量?DEBUG?控制的:
const process = require('process'); process.env['DEBUG'] = 'express:*';這樣就可以在終端看到帶顏色的輸出了,嗯,是的,帶顏色控制字符,vim 中直接跑就 SB 了。
應(yīng)用 Application
Application?是一個(gè)上層統(tǒng)籌的概念,整合“請(qǐng)求-響應(yīng)”流程。?express()?的調(diào)用會(huì)返回一個(gè)?application?,一個(gè)項(xiàng)目中,有多個(gè)?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);多個(gè)?app?的另一個(gè)用法,是直接把某個(gè)?path?映射到整個(gè)?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);這樣,當(dāng)訪問?/2/xx?時(shí),就會(huì)看到?in app2?的響應(yīng)。
前面說了?app?實(shí)際上是一個(gè)上層調(diào)度的角色,在看后面的內(nèi)容之前,先說一下?Express?的特點(diǎn),整體上來說,它的結(jié)構(gòu)基本上是“回調(diào)函數(shù)串行”,無論是?app?,或者?route,?handle,?middleware這些不同的概念,它們的形式,基本是一致的,就是?(res, req, next) => {}?,串行的流程依賴?next()?的顯式調(diào)用。
我們把?app?的功能,分成五個(gè)部分來說。
路由 - 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) => {});上面的代碼就是基本的幾個(gè)方法,路由的匹配是串行的,可以通過?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);對(duì)于上面的代碼,因?yàn)橹貜?fù)調(diào)用?send()?會(huì)報(bào)錯(cuò)。
同樣的功能,也可以使用?app.route()?來實(shí)現(xiàn):
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()?也是一種抽象通用邏輯的形式。
還有一個(gè)方法是?app.params?,它把“命名參數(shù)”的處理單獨(dú)拆出來了(我個(gè)人不理解這玩意兒有什么用):
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?中的對(duì)應(yīng)函數(shù)會(huì)先行執(zhí)行,并且,記得顯式調(diào)用?next()?。
Middleware
其實(shí)前面講了一些方法,要實(shí)現(xiàn)?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()?的顯式調(diào)用,同時(shí),注意定義的順序,?use()?和?all()?順序上是平等的。
Middleware?本身也是?(req, res, next) => {}?這種形式,自然也可以和?app?有對(duì)等的機(jī)制——接受路由過濾,?Express?提供了?Router?,可以單獨(dú)定義一組邏輯,然后這組邏輯可以跟?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);功能開關(guān),變量容器
app.set()?和?app.get()?可以用來保存?app?級(jí)別的變量(對(duì),?app.get()?還和?GET?方法的實(shí)現(xiàn)名字上還沖突了):
const express = require('express');const app = express();app.all('/', (req, res) => {app.set('title', '標(biāo)題123');res.send('ok'); });app.all('/t', (req, res) => {res.send(app.get('title')); });app.listen(8888);上面的代碼,啟動(dòng)之后直接訪問?/t?是沒有內(nèi)容的,先訪問?/?再訪問?/t?才可以看到內(nèi)容。
對(duì)于變量名,?Express?預(yù)置了一些,這些變量的值,可以叫?settings?,它們同時(shí)也影響整個(gè)應(yīng)用的行為:
- 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
(上面這些值中,干嘛不放一個(gè)最基本的?debug?呢……)
除了基本的?set() / get()?,還有一組?enable() / disable() / enabled() / disabled()?的包裝方法,其實(shí)就是?set(name, false)?這種。?set(name)?這種只傳一個(gè)參數(shù),也可以獲取到值,等于?get(name)?。
模板引擎
Express?沒有自帶模板,所以模板引擎這塊就被設(shè)計(jì)成一個(gè)基礎(chǔ)的配置機(jī)制了。
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: "標(biāo)題"}, (err, html) => {res.send(html)}); });app.listen(8888);app.set('views', ...)?是配置模板在文件系統(tǒng)上的路徑,?app.engine()?是擴(kuò)展名為標(biāo)識(shí),注冊(cè)對(duì)應(yīng)的處理函數(shù),然后,?res.render()?就可以渲染指定的模板了。?res.render('demo')?這樣不寫擴(kuò)展名也可以,通過?app.set('view engine', 't2t')?可以配置默認(rèn)的擴(kuò)展名。
這里,注意一下?callback()?的形式,是?callback(err, html)?。
端口監(jiān)聽
app?功能的最后一部分,?app.listen()?,它完成的形式是:
app.listen([port[, host[, backlog]]][, callback])注意,?host?是第二個(gè)參數(shù)。
backlog?是一個(gè)數(shù)字,配置可等待的最大連接數(shù)。這個(gè)值同時(shí)受操作系統(tǒng)的配置影響。默認(rèn)是 512 。
請(qǐng)求 Request
這一塊倒沒有太多可以說的,一個(gè)請(qǐng)求你想知道的信息,都被包裝到?req?的屬性中的。除了,頭。頭的信息,需要使用?req.get(name)?來獲取。
GET 參數(shù)
使用?req.query?可以獲取 GET 參數(shù):
const express = require('express'); const app = express();app.all('/', (req, res) => {console.log(req.query);res.send('ok'); });app.listen(8888);請(qǐng)求:
# -*- coding: utf-8 -*- import requests requests.get('http://localhost:8888', params={"a": '中文'.encode('utf8')})POST 參數(shù)
POST 參數(shù)的獲取,使用?req.body?,但是,在此之前,需要專門掛一個(gè) 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 requestsrequests.post('http://localhost:8888', data={"a": '中文'})如果你是整塊扔的 json 的話:
# -*- coding: utf-8 -*-import requests import jsonrequests.post('http://localhost:8888', data=json.dumps({"a": '中文'}),headers={'Content-Type': 'application/json'})Express?中也有對(duì)應(yīng)的?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?部分的邏輯,是單獨(dú)放在?body-parser?這個(gè) npm 模塊中的。?Express?也沒有提供方法,方便地獲取原始 raw 的內(nèi)容。另外,對(duì)于 POST 提交的編碼數(shù)據(jù),?Express?只支持 UTF-8 編碼。
如果你要處理文件上傳,嗯,?Express?沒有現(xiàn)成的 Middleware ,額外的實(shí)現(xiàn)在?github.com/expressjs/multer?。( Node.js 天然沒有“字節(jié)”類型,所以在字節(jié)級(jí)別的處理上,就會(huì)感覺很不順啊)
Cookie
Cookie 的獲取,也跟 POST 參數(shù)一樣,需要外掛一個(gè)?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);請(qǐng)求:
# -*- coding: utf-8 -*-import requests import jsonrequests.post('http://localhost:8888', data={'a': '中文'},headers={'Cookie': 'a=1'})如果 Cookie 在響應(yīng)時(shí),是配置?res?做了簽名的,則在?req?中可以通過?req.signedCookies?處理簽名,并獲取結(jié)果。
來源 IP
Express?對(duì)?X-Forwarded-For?頭,做了特殊處理,你可以通過?req.ips?獲取這個(gè)頭的解析后的值,這個(gè)功能需要配置?trust proxy?這個(gè)?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);請(qǐng)求:
# -*- 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?會(huì)是一個(gè) ipv4 或者 ipv6 的值。
響應(yīng) Response
Express?的響應(yīng),針對(duì)不同類型,本身就提供了幾種包裝了。
普通響應(yīng)
使用?res.send?處理確定性的內(nèi)容響應(yīng):
res.send({ some: 'json' }); res.send('<p>some html</p>'); res.status(404); res.end(); res.status(500); res.end();res.send()?會(huì)自動(dòng)?res.end()?,但是,如果只使用?res.status()?的話,記得加上?res.end()?。
模板渲染
模板需要預(yù)先配置,在?Request?那節(jié)已經(jīng)介紹過了。
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);這里有一個(gè)坑點(diǎn),就是必須在對(duì)應(yīng)的目錄下,有對(duì)應(yīng)的文件存在,比如上面例子的?template/index.html?,那么?app.engine()?中的回調(diào)函數(shù)才會(huì)執(zhí)行。都自定義回調(diào)函數(shù)了,這個(gè)限制沒有任何意義,?path, options?傳入就好了,至于是不是要通過文件系統(tǒng)讀取內(nèi)容,怎么讀取,又有什么關(guān)系呢。
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);請(qǐng)求:
# -*- coding: utf-8 -*-import requests import jsonres = 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)注意三點(diǎn):
- app.use(cookieParser("key"))?這里必須要有一個(gè)字符串做?key?,才可以正確使用簽名的 cookie 。
- clearCookie()?仍然是用“設(shè)置過期”的方式來達(dá)到刪除目的,cookie()?和?clearCookie()?并不會(huì)整合,會(huì)寫兩組?b=xx?進(jìn)頭。
- res.send()?會(huì)在連接上完成一個(gè)響應(yīng),所以,與頭相關(guān)的操作,都必須放在?res.send()?前面。
頭和其它
res.set()?可以設(shè)置指定的響應(yīng)頭,?res.rediect(301, 'http://www.zouyesheng.com')?處理重定向,?res.status(404); res.end()?處理非 20 響應(yīng)。
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')?會(huì)自動(dòng)獲取?referer?頭作為?Location?的值,使用這個(gè)時(shí),注意?referer為空的情況,會(huì)造成循環(huán)重復(fù)重定向的后果。
Chunk 響應(yīng)
Chunk?方式的響應(yīng),指連接建立之后,服務(wù)端的響應(yīng)內(nèi)容是不定長的,會(huì)加個(gè)頭:?Transfer-Encoding: chunked?,這種狀態(tài)下,服務(wù)端可以不定時(shí)往連接中寫入內(nèi)容(不排除服務(wù)端的實(shí)現(xiàn)會(huì)有緩沖區(qū)機(jī)制,不過我看?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);上面的代碼,訪問之后,每過一秒,都會(huì)收到新的內(nèi)容。
大概是?res?本身是 Node.js 中的?stream?類似對(duì)象,所以,它有一個(gè)?write()?方法。
要測(cè)試這個(gè)效果,比較方便的是直接 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: localhostHTTP/1.1 200 OK X-Powered-By: Express Date: Thu, 20 Jun 2019 08:11:40 GMT Connection: keep-alive Transfer-Encoding: chunkede 1561018300451e 1561018301454e 1561018302456e 1561018303457e 1561018304458e 1561018305460e 1561018306460每行前面的一個(gè)字節(jié)的?e?,為 16 進(jìn)制的 14 這個(gè)數(shù)字,也就是后面緊跟著的內(nèi)容的長度,是?Chunk?格式的要求。
Tornado 中的類似實(shí)現(xiàn)是:
# -*- coding: utf-8 -*-import tornado.ioloop import tornado.web import tornado.gen import timeclass MainHandler(tornado.web.RequestHandler):@tornado.gen.coroutinedef 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?中的實(shí)現(xiàn),有個(gè)大坑,就是:
app.all('/', (req, res) => {const f = () => {const t = new Date().getTime() + '\n';res.write(t);console.log(t);setTimeout(f, 1000);}setTimeout(f, 1000); });這段邏輯,在連接已經(jīng)斷了的情況下,并不會(huì)停止,還是會(huì)永遠(yuǎn)執(zhí)行下去。所以,你得自己處理好:
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?事件來得到當(dāng)前連接是否已經(jīng)關(guān)閉了。
req?上直接掛連接事件,從?net?http?Express?這個(gè)層次結(jié)構(gòu)上來說,也很,尷尬了。 Web 層不應(yīng)該關(guān)心到網(wǎng)絡(luò)連接這么底層的東西的。
我還是習(xí)慣這樣:
app.all('/', (req, res) => {res.write('<h1>123</h1>');res.end(); });不過?res.write()?是不能直接處理 json 對(duì)象的,還是老老實(shí)實(shí)?res.send()?吧。
我會(huì)怎么用 Express
先說一下,我自己,目前在?Express?運(yùn)用方面,并沒有太多的時(shí)間和復(fù)雜場(chǎng)景的積累。
即使這樣,作為技術(shù)上相對(duì)傳統(tǒng)的人,我會(huì)以我以往的 web 開發(fā)的套路,來使用?Express?。
我不喜歡日常用?app.all(path, callback)?這種形式去組織代碼。
首先,這會(huì)使?path?定義散落在各處,方便了開發(fā),麻煩了維護(hù)。
其次,把?path?和具體實(shí)現(xiàn)邏輯?callback?綁在一起,我覺得也是反思維的。至少,對(duì)于我個(gè)人來說,開發(fā)的過程,先是想如何實(shí)現(xiàn)一個(gè)?handler?,最后,再是考慮要把這個(gè)?handle?與哪些?path?綁定。
再次,單純的?callback?缺乏層次感,用?app.use(path, callback)?這種來處理共用邏輯的方式,我覺得完全是扯談。共用邏輯是代碼之間本身實(shí)現(xiàn)上的關(guān)系,硬生生跟網(wǎng)絡(luò)應(yīng)用層 HTTP 協(xié)議的?path?概念抽上關(guān)系,何必呢。當(dāng)然,對(duì)于?callback?的組織,用純函數(shù)來串是可以的,不過我在這方面并沒有太多經(jīng)驗(yàn),所以,我還是選擇用類繼承的方式來作層次化的實(shí)現(xiàn)。
我自己要用?Express?,大概會(huì)這樣組件項(xiàng)目代碼(不包括關(guān)系數(shù)據(jù)庫的?Model?抽象如何組織這部分):
./ ├── config.conf ├── config.js ├── handler │?? ├── base.js │?? └── index.js ├── middleware.js ├── server.js └── url.js- config.conf?是 ini 格式的項(xiàng)目配置。
- config.js?處理配置,包括日志,數(shù)據(jù)庫連接等。
- middleware.js?是針對(duì)整體流程的擴(kuò)展機(jī)制,比如,給每個(gè)請(qǐng)求加一個(gè) UUID ,每個(gè)請(qǐng)求都記錄一條日志,日志內(nèi)容有請(qǐng)求的細(xì)節(jié)及本次請(qǐng)求的處理時(shí)間。
- server.js?是主要的服務(wù)啟動(dòng)邏輯,整合各種資源,命令行參數(shù)?port?控制監(jiān)聽哪個(gè)端口。不需要考慮多進(jìn)程問題,(正式部署時(shí)?nginx?反向代理到多個(gè)應(yīng)用實(shí)例,多個(gè)實(shí)例及其它資源統(tǒng)一用?supervisor?管理)。
- url.js?定義路徑與?handler?的映射關(guān)系。
- handler?,具體邏輯實(shí)現(xiàn)的地方,所有?handler?都從?BaseHandler?繼承。
BaseHandler?的實(shí)現(xiàn):
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?本身的內(nèi)容了,只是我個(gè)人,隨便想到的一些東西。
找一個(gè)日志模塊的實(shí)現(xiàn),功能上,就看這么幾點(diǎn):
- 標(biāo)準(zhǔn)的級(jí)別: DEBUG,INFO,WARN, ERROR 這些。
- 層級(jí)的多個(gè)?logger?。
- 可注冊(cè)式的多種?Handler?實(shí)現(xiàn),比如文件系統(tǒng),操作系統(tǒng)的?rsyslog?,標(biāo)準(zhǔn)輸出,等。
- 格式定義,一般都帶上時(shí)間和代碼位置。
Node.js 中,大概就是?log4js?了,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');總的來說,還是很好用的,但是官網(wǎng)的文檔不太好讀,有些細(xì)節(jié)的東西沒講,好在源碼還是比較簡(jiǎn)單。
說幾點(diǎn):
- getLogger(name)?需要給一個(gè)名字,否則?default?的規(guī)則都匹配不到。
- getLogger('parent.child')?中的名字,規(guī)則匹配上,可以通過?.?作父子繼承的。
- enableCallStack: true?加上,才能拿到文件名和行號(hào)。
ini 格式配置
json 作配置文件,功能上沒問題,但是對(duì)人為修改是不友好的。所以,個(gè)人還是喜歡用 ini 格式作項(xiàng)目的環(huán)境配置文件。
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);它擴(kuò)展了?array[]?這種格式,但沒有對(duì)類型作處理(除了?true?false),比如,獲取?port?,結(jié)果是?"5432"?。簡(jiǎn)單夠用了。
WebSocket
Node.js 中的 WebSocket 實(shí)現(xiàn),可以使用?ws?模塊, github.com/websockets/ws?。
要把?ws?的 WebSocket Server 和?Express?的?app?整合,需要在?Express?的?Server?層面動(dòng)手,實(shí)際上這里說的?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());}); });對(duì)應(yīng)的一個(gè)客戶端實(shí)現(xiàn),來自:?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_connectclass Client(object):def __init__(self, url, timeout):self.url = urlself.timeout = timeoutself.ioloop = IOLoop.instance()self.ws = Noneself.connect()PeriodicCallback(self.keep_alive, 2000).start()self.ioloop.start()@gen.coroutinedef 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.coroutinedef run(self):while True:msg = yield self.ws.read_message()print('read', msg)if msg is None:print("connection closed")self.ws = Nonebreakdef 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)
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的Express 的使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开发者解读:为什么蚂蚁要用融合计算这种新
- 下一篇: ChaosBlade 发布对 C++ 应