下內(nèi)容,基于 Express 4.x 版本
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
這種程度,說它是一個“框架”可能都有些過了,幾乎都是工具性質(zhì)的實現(xiàn),只限于 Web 層。
當然,直接了當?shù)貙崿F(xiàn)了 Web 層的基本功能,是得益于 Node.js 本身的 API 中,就提供了 net 和 http 這兩層, Express 對 http 的方法包裝一下即可。
不過,本身功能簡單的東西,在 package.json 中卻有好長一串 dependencies 列表。
在跑 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);
調(diào)試信息是通過環(huán)境變量 DEBUG 控制的:
const process = require('process'); process.env['DEBUG'] = 'express:*';
這樣就可以在終端看到帶顏色的輸出了,嗯,是的,帶顏色控制字符,vim 中直接跑就 SB 了。
Application 是一個上層統(tǒng)籌的概念,整合“請求-響應”流程。 express() 的調(diào)用會返回一個 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 實際上是一個上層調(diào)度的角色,在看后面的內(nèi)容之前,先說一下 Express 的特點,整體上來說,它的結(jié)構(gòu)基本上是“回調(diào)函數(shù)串行”,無論是 app ,或者 route, handle, middleware這些不同的概念,它們的形式,基本是一致的,就是 (res, req, next) => {} ,串行的流程依賴 next() 的顯式調(diào)用。
我們把 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);
對于上面的代碼,因為重復調(diào)用 send() 會報錯。
同樣的功能,也可以使用 app.route() 來實現(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() 也是一種抽象通用邏輯的形式。
還有一個方法是 app.params ,它把“命名參數(shù)”的處理單獨拆出來了(我個人不理解這玩意兒有什么用):
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 中的對應函數(shù)會先行執(zhí)行,并且,記得顯式調(diào)用 next() 。
Middleware
其實前面講了一些方法,要實現(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)用,同時,注意定義的順序, 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);
功能開關(guān),變量容器
app.set() 和 app.get() 可以用來保存 app 級別的變量(對, app.get() 還和 GET 方法的實現(xiàn)名字上還沖突了):
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 是沒有內(nèi)容的,先訪問 / 再訪問 /t 才可以看到內(nèi)容。
對于變量名, Express 預置了一些,這些變量的值,可以叫 settings ,它們同時也影響整個應用的行為:
具體的作用,可以參考 https://expressjs.com/en/4x/api.html#app.set 。
(上面這些值中,干嘛不放一個最基本的 debug 呢……)
除了基本的 set() / get() ,還有一組 enable() / disable() / enabled() / disabled() 的包裝方法,其實就是 set(name, false) 這種。 set(name) 這種只傳一個參數(shù),也可以獲取到值,等于 get(name) 。
模板引擎
Express 沒有自帶模板,所以模板引擎這塊就被設(shè)計成一個基礎(chǔ)的配置機制了。
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', ...) 是配置模板在文件系統(tǒng)上的路徑, app.engine() 是擴展名為標識,注冊對應的處理函數(shù),然后, res.render() 就可以渲染指定的模板了。 res.render('demo') 這樣不寫擴展名也可以,通過 app.set('view engine', 't2t') 可以配置默認的擴展名。
這里,注意一下 callback() 的形式,是 callback(err, html) 。
端口監(jiān)聽
app 功能的最后一部分, app.listen() ,它完成的形式是:
app.listen([port[, host[, backlog]]][, callback])
注意, host 是第二個參數(shù)。
backlog 是一個數(shù)字,配置可等待的最大連接數(shù)。這個值同時受操作系統(tǒng)的配置影響。默認是 512 。
這一塊倒沒有太多可以說的,一個請求你想知道的信息,都被包裝到 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);
請求:
# -*- coding: utf-8 -*- import requests requests.get('http://localhost:8888', params={"a": '中文'.encode('utf8')})
POST 參數(shù)
POST 參數(shù)的獲取,使用 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 的內(nèi)容。另外,對于 POST 提交的編碼數(shù)據(jù), Express 只支持 UTF-8 編碼。
如果你要處理文件上傳,嗯, Express 沒有現(xiàn)成的 Middleware ,額外的實現(xiàn)在 https://github.com/expressjs/multer 。( Node.js 天然沒有“字節(jié)”類型,所以在字節(jié)級別的處理上,就會感覺很不順啊)
Cookie
Cookie 的獲取,也跟 POST 參數(shù)一樣,需要外掛一個 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 處理簽名,并獲取結(jié)果。
來源 IP
Express 對 X-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 的值。
Express 的響應,針對不同類型,本身就提供了幾種包裝了。
普通響應
使用 res.send 處理確定性的內(nèi)容響應:
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 那節(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);
這里有一個坑點,就是必須在對應的目錄下,有對應的文件存在,比如上面例子的 template/index.html ,那么 app.engine() 中的回調(diào)函數(shù)才會執(zhí)行。都自定義回調(diào)函數(shù)了,這個限制沒有任何意義, 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);
請求:
# -*- 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)
注意三點:
頭和其它
res.set() 可以設(shè)置指定的響應頭, 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為空的情況,會造成循環(huán)重復重定向的后果。
Chunk 響應
Chunk 方式的響應,指連接建立之后,服務(wù)端的響應內(nèi)容是不定長的,會加個頭: Transfer-Encoding: chunked ,這種狀態(tài)下,服務(wù)端可以不定時往連接中寫入內(nèi)容(不排除服務(wù)端的實現(xiàn)會有緩沖區(qū)機制,不過我看 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);
上面的代碼,訪問之后,每過一秒,都會收到新的內(nèi)容。
大概是 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
每行前面的一個字節(jié)的 e ,為 16 進制的 14 這個數(shù)字,也就是后面緊跟著的內(nèi)容的長度,是 Chunk 格式的要求。具體可以參考 HTTP 的 RFC , https://tools.ietf.org/html/rfc2616#page-2 。
Tornado 中的類似實現(xiàn)是:
# -*- 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 中的實現(xiàn),有個大坑,就是:
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)斷了的情況下,并不會停止,還是會永遠執(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 事件來得到當前連接是否已經(jīng)關(guān)閉了。
req 上直接掛連接事件,從 net http Express 這個層次結(jié)構(gòu)上來說,也很,尷尬了。 Web 層不應該關(guān)心到網(wǎng)絡(luò)連接這么底層的東西的。
我還是習慣這樣:
app.all('/', (req, res) => { res.write('<h1>123</h1>'); res.end(); });
不過 res.write() 是不能直接處理 json 對象的,還是老老實實 res.send() 吧。
先說一下,我自己,目前在 Express 運用方面,并沒有太多的時間和復雜場景的積累。
即使這樣,作為技術(shù)上相對傳統(tǒng)的人,我會以我以往的 web 開發(fā)的套路,來使用 Express 。
我不喜歡日常用 app.all(path, callback) 這種形式去組織代碼。
首先,這會使 path 定義散落在各處,方便了開發(fā),麻煩了維護。
其次,把 path 和具體實現(xiàn)邏輯 callback 綁在一起,我覺得也是反思維的。至少,對于我個人來說,開發(fā)的過程,先是想如何實現(xiàn)一個 handler ,最后,再是考慮要把這個 handle 與哪些 path 綁定。
再次,單純的 callback 缺乏層次感,用 app.use(path, callback) 這種來處理共用邏輯的方式,我覺得完全是扯談。共用邏輯是代碼之間本身實現(xiàn)上的關(guān)系,硬生生跟網(wǎng)絡(luò)應用層 HTTP 協(xié)議的 path 概念抽上關(guān)系,何必呢。當然,對于 callback 的組織,用純函數(shù)來串是可以的,不過我在這方面并沒有太多經(jīng)驗,所以,我還是選擇用類繼承的方式來作層次化的實現(xiàn)。
我自己要用 Express ,大概會這樣組件項目代碼(不包括關(guān)系數(shù)據(jù)庫的 Model 抽象如何組織這部分):
./ ├── config.conf ├── config.js ├── handler │ ├── base.js │ └── index.js ├── middleware.js ├── server.js └── url.js
BaseHandler 的實現(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)容了,只是我個人,隨便想到的一些東西。
找一個日志模塊的實現(xiàn),功能上,就看這么幾點:
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');
總的來說,還是很好用的,但是官網(wǎng)的文檔不太好讀,有些細節(jié)的東西沒講,好在源碼還是比較簡單。
說幾點:
json 作配置文件,功能上沒問題,但是對人為修改是不友好的。所以,個人還是喜歡用 ini 格式作項目的環(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);
它擴展了 array[] 這種格式,但沒有對類型作處理(除了 true false),比如,獲取 port ,結(jié)果是 "5432" 。簡單夠用了。
Node.js 中的 WebSocket 實現(xiàn),可以使用 ws 模塊, https://github.com/websockets/ws 。
要把 ws 的 WebSocket Server 和 Express 的 app 整合,需要在 Express 的 Server 層面動手,實際上這里說的 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()); }); });
對應的一個客戶端實現(xiàn),來自: 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)
作者:zephyr
當今的前端開發(fā)中,了解后端技術(shù)對于全棧工程師來說至關(guān)重要。Express.js,作為Node.js的一個輕量級框架,以其簡單、快速和靈活的特性受到了廣大開發(fā)者的青睞。本文旨在通過15分鐘的閱讀,幫助你快速理解Express.js,掌握其基本用法,為全棧之路打下堅實基礎(chǔ)。
一、Express.js簡介
Express.js是一個基于Node.js平臺的極簡、靈活的web開發(fā)框架,它提供了一系列強大的特性,幫助開發(fā)者快速構(gòu)建Web和移動應用程序。通過Express.js,我們可以輕松創(chuàng)建Web服務(wù)器,處理HTTP請求和響應,以及構(gòu)建RESTful API等。
二、安裝與設(shè)置
首先,確保你已經(jīng)安裝了Node.js。然后,通過npm(Node.js的包管理器)安裝Express.js:
接下來,創(chuàng)建一個新的JavaScript文件(例如app.js),并引入Express模塊:
三、基本路由
路由是Express.js的核心功能之一。它允許我們定義應用程序如何響應客戶端發(fā)送的HTTP請求。下面是一個簡單的路由示例:
上述代碼定義了一個GET請求路由,當訪問應用程序的根路徑(/)時,服務(wù)器將返回"Hello World!"。
四、中間件
Express.js中的中間件是一種函數(shù),它可以處理請求和響應,或者終止請求-響應周期。中間件在Express.js中扮演著非常重要的角色,用于執(zhí)行各種任務(wù),如日志記錄、身份驗證、錯誤處理等。
以下是一個簡單的中間件示例,用于記錄每個請求的URL:
app.use((req, res, next) => {
console.log(`Request URL: ${req.url}`);
next();
});
五、靜態(tài)文件服務(wù)
Express.js還提供了靜態(tài)文件服務(wù)功能,可以方便地為用戶提供圖片、CSS和JavaScript等靜態(tài)資源。例如,以下代碼將設(shè)置一個靜態(tài)文件目錄:
app.use(express.static('public'));
在上述設(shè)置中,Express.js將自動為public目錄下的文件提供路由。
六、啟動服務(wù)器
最后,我們需要監(jiān)聽一個端口以啟動服務(wù)器。以下代碼將啟動一個監(jiān)聽3000端口的服務(wù)器:
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
七、總結(jié)
通過本文的介紹,你應該已經(jīng)對Express.js有了一個初步的了解。當然,Express.js的功能遠不止于此,還有更多高級特性和用法等待你去探索。不過,通過這15分鐘的閱讀,你已經(jīng)邁出了全棧開發(fā)的重要一步。現(xiàn)在,你可以嘗試使用Express.js構(gòu)建一個簡單的Web應用程序,將所學知識付諸實踐。記住,全棧之路雖然充滿挑戰(zhàn),但只要勇敢邁出第一步,就會發(fā)現(xiàn)其實并沒有那么難。加油!
●express 是什么?
○是一個 node 的第三方開發(fā)框架
■把啟動服務(wù)器包括操作的一系列內(nèi)容進行的完整的封裝
■在使用之前, 需要下載第三方
■指令: npm install express
1.基本搭建
// 0. 下載: npm install express
// 0. 導入
const express = express();
// 1. 創(chuàng)建服務(wù)器
const server = express();
// 2. 給服務(wù)器配置監(jiān)聽端口號
server.listen(8080, () => {
console.log("服務(wù)器啟動成功");
});
2.配置靜態(tài)資源
a.之前:
i.約定:
1.所有靜態(tài)資源以 /static 開頭
2.按照后面給出的文件名自己去組裝的路徑
ii.組裝:
1.準備了初始目錄 './client/'
2.按照后綴去添加二級目錄
3.按照文件名去查找內(nèi)容
iii.例子: /static/index.html
1.自動去 './client/views/index.html'
b.現(xiàn)在:
i.約定:
1.所有靜態(tài)資源以 /static 開頭
2.按照 /static 后面的路徑去訪問指定文件
3.要求: 在 /static 以后的內(nèi)容需要按照 二級路徑的正確方式書寫
a. 假設(shè)你需要請求的是 './client/views/index.html' 文件
b.你的請求地址需要書寫 '/static/views/index.html'
c.語法:
i. express.static('開放的靜態(tài)目錄地址')
ii.server.use('訪問這個地址的時候', 去到開放的靜態(tài)目錄地址)
// 0. 下載: npm install express
// 0. 導入
// 1. 創(chuàng)建服務(wù)器
// 1.1 配置靜態(tài)資源
server.use("/static", express.static("./client/"));
// 2. 給服務(wù)器配置監(jiān)聽端口號
3.配置接口服務(wù)器
// 0. 下載: npm install express
// 0. 導入
// 1. 創(chuàng)建服務(wù)器
// 1.1 配置靜態(tài)資源
// 1.2 配置服務(wù)器接口
server.get("/goods/list", (req, res) => {
/**
* req(request): 本次請求的相關(guān)信息
* res(response): 本次響應的相關(guān)信息
*
* req.query: 對 GET 請求體請求參數(shù)的解析
* 如果有參數(shù), req.query 就是 {a:xxx, b:yyy}
* 如果沒有參數(shù), req.query 就是 {}
*/
console.log(req.query);
// res.end(JSON.stringify({code: 1, msg: '成功'}))
res.send({ code: 1, msg: "成功" });
});
server.post("/users/login", (req, res) => {
console.log(req.query);
// 注意! express 不會自動解析 post 請求的 請求體
res.send({
code: 1,
msg: "接收 POST 請求成功, 但是還沒有解析請求體, 參數(shù)暫時不能帶回",
});
});
// 2. 給服務(wù)器配置監(jiān)聽端口號
●express 提供了一個方法能夠讓我們制作一張 "路由表"
●目的就是為了幫助我們簡化 服務(wù)器index.js 內(nèi)部的代碼量
●服務(wù)器根目錄/router/goods.js
// 專門存放于 goods 相關(guān)的路由表
const express = require("express");
// 創(chuàng)建一個路由表
const Router = express.Router();
// 向表上添加內(nèi)容, 添加內(nèi)容的語法, 向服務(wù)上添加的語法一樣
Router.get("/info", (req, res) => {
res.send({
code: 1,
message: "您請求 /goods/list 成功",
});
});
// 導出當前路由表
module.exports.goodsRouter = Router
●服務(wù)器根目錄/router/index.js
const express = require("express");
// 0. 導入處理函數(shù)
const { goodsRouter } = require("./goods");
// 創(chuàng)建路由總表
const Router = express.Router();
// 向路由總表上添加路由分表
Router.use("/goods", goodsRouter);
// 導出路由總表
module.exports = Router
●服務(wù)器根目錄/index.js
// 0. 下載并導入 express
const express = require("express");
const router = require("./router"); // 相當于 ./router/index.js
// 1. 創(chuàng)建服務(wù)器
const server = express();
// 1.1 配置靜態(tài)資源
server.use("/static", express.static("./client"));
// 1.2 配置接口
server.use("/api", router);
// 2. 給服務(wù)器監(jiān)聽端口號
server.listen(8080, () => {
console.log("服務(wù)啟動成功, 端口號8080~~~");
});
express 的中間件
●概念
○在任意兩個環(huán)節(jié)之間添加的一個環(huán)節(jié), 就叫做中間件
●分類
○全局中間件
■語法: server.use(以什么開頭, 函數(shù))
●server: 創(chuàng)建的服務(wù)器, 一個變量而已
●以什么開頭: 可以不寫, 寫的話需要是字符串
●函數(shù): 你這個中間件需要做什么事
// 0. 下載并導入第三方模塊
const express = require("express");
// 0. 引入路由總表
const router = require("./router");
// 0. 引入內(nèi)置的 fs 模塊
const fs = require("fs");
// 1. 開啟服務(wù)器
const app = express();
// 1.1 開啟靜態(tài)資源
app.use("/static", express.static("./client/"));
// 1.2 添加一個 中間件, 讓所有請求進來的時候, 記錄一下時間與請求地址
app.use(function (req, res, next) {
fs.appendFile("./index.txt", `${new Date()} --- ${req.url} \n`, () => {});
next(); // 運行完畢后, 去到下一個中間件
});
// 1.3 開啟路由表
app.use("/api", router);
// 2. 給服務(wù)添加監(jiān)聽
app.listen(8080, () => console.log("服務(wù)器開啟成功, 端口號8080~"));
○路由級中間件
■語法: router.use(以什么開頭, 函數(shù))
●router: 創(chuàng)建的路由表, 一個變量而已
●以什么開頭: 可以不寫, 寫的話需要是字符串
●函數(shù): 你這個中間件需要做什么事
// 路由分表
const router = require("express").Router();
// 導入 cart 中間件
const cartMidd = require("../middleware/cart");
// 添加路由級中間件
router.use(function (req, res, next) {
/**
* 1. 驗證 token 存在并且沒有過期才可以
* 規(guī)定: 請求頭內(nèi)必須有 authorization 字段攜帶 token 信息
*/
const token = req.headers.authorization;
if (!token) {
res.send({
code: 0,
msg: "沒有 token, 不能進行 該操作",
});
}
next();
});
router.get("/list", cartMidd.cartlist, (req, res) => {
res.send({
code: 1,
msg: "請求 /cart/list 接口成功",
});
});
router.get("/add", (req, res) => {
res.send({
code: 1,
msg: "請求 /cart/add 接口成功",
});
});
module.exports.cartRouter = router;
○請求級中間件
■直接在請求路由上, 在路由處理函數(shù)之前書寫函數(shù)即可
// 路由分表
const router = require("express").Router();
// 導入 cart 中間件
const cartMidd = require("../middleware/cart");
router.get("/list", cartMidd.cartlist, (req, res) => {
res.send({
code: 1,
msg: "請求 /cart/list 接口成功",
});
});
router.get("/add", (req, res) => {
res.send({
code: 1,
msg: "請求 /cart/add 接口成功",
});
});
module.exports.cartRouter = router;
// ../middleware/cart.js
const cartlist = (req, res, next) => {
// 1. 判斷參數(shù)是否傳遞
const { current, pagesize } = req.query;
if (!current || !pagesize) {
res.send({
code: 0,
msg: "參數(shù)current或者參數(shù)pagesize沒有傳遞",
});
return;
}
if (isNaN(current) || isNaN(pagesize)) {
res.send({
code: 0,
msg: "參數(shù)current或者參數(shù)pagesize 不是 數(shù)字類型的, 請?zhí)幚?#34;,
});
return;
}
next();
};
module.exports.cartlist = cartlist
○錯誤中間件
■本質(zhì)上就是一個全局中間件, 只不過處理的內(nèi)容
// 0. 下載并導入第三方模塊
const express = require("express");
// 0. 引入路由總表
const router = require("./router");
// 0. 引入內(nèi)置的 fs 模塊
const fs = require("fs");
// 1. 開啟服務(wù)器
const app = express();
// 1.1 開啟靜態(tài)資源
app.use("/static", express.static("./client/"));
// 1.2 開啟路由表
app.use("/api", router);
// 1.3 注冊全局錯誤中間件(必須接收四個參數(shù))
app.use(function (err, req, res, next) {
if (err === 2) {
res.send({
code: 0,
msg: "參數(shù)current或者參數(shù)pagesize沒有傳遞",
});
} else if (err === 3) {
res.send({
code: 0,
msg: "參數(shù)current或者參數(shù)pagesize 不是 數(shù)字類型的, 請?zhí)幚?#34;,
});
} else if (err === 4) {
res.send({
code: 0,
msg: "沒有 token, 不能進行 該操作",
});
}
});
// 2. 給服務(wù)添加監(jiān)聽
app.listen(8080, () => console.log("服務(wù)器開啟成功, 端口號8080~"));
/*
* 4. 錯誤中間件
* 為了統(tǒng)一進行錯誤處理
*
* 例子:
* 接口參數(shù)少
* 請求 /goods/list 參數(shù)少
* 請求 /cart/list 參數(shù)少
* 請求 /news/list 參數(shù)少
* res.send({code: 0, msg: '參數(shù)數(shù)量不對'})
* 接口參數(shù)格式不對
* 請求 /users/login 格式不對
* 請求 /goods/list 格式不對
* res.send({code: 0, msg: '參數(shù)格式不對})
*
* 思考:
* 正確的時候, 直接返回結(jié)果給前端
* 只要出現(xiàn)了錯誤, 統(tǒng)一回到全局路徑上
*
* 操作:
* 當你在任何一個環(huán)節(jié)的中間件內(nèi)
* => 調(diào)用 next() 的時候, 表示的都是去到下一個環(huán)節(jié)
* => 調(diào)用 next(參數(shù)) 的時候, 表示去到的都是全局錯誤環(huán)節(jié)
* 參數(shù):
* 參數(shù)的傳遞需要自己和自己約定一些暗號
* 2: 表示 接口參數(shù)少
* 3: 表示 接口參數(shù)格式不對
* 4: 表示沒有token
* 5: XXXX....
*/
token 的使用
●token 的使用分為兩步
○加密
■比如用戶登陸成功后, 將一段信息加密生成一段 token, 然后返回給前端
○解密
■比如用戶需要訪問一些需要登陸后才能訪問的接口, 就可以把登錄時返回的token保存下來
■在訪問這些接口時, 攜帶上token即可
■而我們接收到token后, 需要解密token, 驗證是否為正確的 token 或者 過期的 token
1.加密
/**
* 使用一個 第三方包 jsonwebtoken
*/
const jwt = require("jsonwebtoken");
/**
* 1. 加密
* 語法: jwt.sign(你要存儲的信息, '密鑰', {配置信息})
*/
const info = { id: 1, nickname: "腸旺面" };
const token = jwt.sign(info, "XXX", { expiresIn: 60 });
// console.log(token);
/*
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MSwibmlja25hbWUiOiLogqDml7rpnaLliqDnjKrohJoiLCJpYXQiOjE2NzAxNTYwMDgsImV4cCI6MTY3MDE1NjA2OH0.
12-87hSrMYmpwXRMuYAbf08G7RDSXM2rEI49jaK5wMw
*/
2.解密
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。