Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
下內(nèi)容,基于 Express 4.x 版本
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 中卻有好長(zhǎng)一串 dependencies 列表。
在跑 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 了。
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)用的行為:
具體的作用,可以參考 https://expressjs.com/en/4x/api.html#app.set 。
(上面這些值中,干嘛不放一個(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 。
這一塊倒沒有太多可以說的,一個(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 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 中也有對(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)在 https://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 json requests.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 的值。
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 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)
注意三點(diǎn):
頭和其它
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)容是不定長(zhǎng)的,會(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: 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
每行前面的一個(gè)字節(jié)的 e ,為 16 進(jìn)制的 14 這個(gè)數(shù)字,也就是后面緊跟著的內(nèi)容的長(zhǎng)度,是 Chunk 格式的要求。具體可以參考 HTTP 的 RFC , https://tools.ietf.org/html/rfc2616#page-2 。
Tornado 中的類似實(shí)現(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 中的實(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() 吧。
先說一下,我自己,目前在 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ù)庫(kù)的 Model 抽象如何組織這部分):
./ ├── config.conf ├── config.js ├── handler │ ├── base.js │ └── index.js ├── middleware.js ├── server.js └── url.js
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):
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)的文檔不太好讀,有些細(xì)節(jié)的東西沒講,好在源碼還是比較簡(jiǎn)單。
說幾點(diǎn):
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)單夠用了。
Node.js 中的 WebSocket 實(shí)現(xiàn),可以使用 ws 模塊, https://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),來自: 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
視頻地址: https://pan.baidu.com/s/1nvop1nN 密碼: s9iy
在很多場(chǎng)景中,我們的服務(wù)器都需要跟用戶的瀏覽器打交道,如表單提交。
表單提交到服務(wù)器一般都使用 GET/POST 請(qǐng)求。
本章節(jié)我們將為大家介紹 Node.js GET/POS T請(qǐng)求。
獲取GET請(qǐng)求內(nèi)容
由于GET請(qǐng)求直接被嵌入在路徑中,URL是完整的請(qǐng)求路徑,包括了?后面的部分,因此你可以手動(dòng)解析后面的內(nèi)容作為GET請(qǐng)求的參數(shù)。
node.js 中 url 模塊中的 parse 函數(shù)提供了這個(gè)功能。
實(shí)例
varhttp = require('http');varurl = require('url');varutil = require('util'); http.createServer(function(req, res){res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}); res.end(util.inspect(url.parse(req.url, true)));}).listen(3000);
在瀏覽器中訪問 http://localhost:3000/user?name=編程改變未來&url=www.biancheng.com 然后查看返回結(jié)果:
獲取 URL 的參數(shù)
我們可以使用 url.parse 方法來解析 URL 中的參數(shù),代碼如下:
實(shí)例
varhttp = require('http');varurl = require('url');varutil = require('util'); http.createServer(function(req, res){res.writeHead(200, {'Content-Type': 'text/plain'}); // 解析 url 參數(shù)varparams = url.parse(req.url, true).query; res.write("網(wǎng)站名:" + params.name); res.write("\n"); res.write("網(wǎng)站 URL:" + params.url); res.end(); }).listen(3000);
在瀏覽器中訪問 http://localhost:3000/user?name=編程改變未來&url=www.biancheng.com 然后查看返回結(jié)果:
獲取 POST 請(qǐng)求內(nèi)容
POST 請(qǐng)求的內(nèi)容全部的都在請(qǐng)求體中,http.ServerRequest 并沒有一個(gè)屬性內(nèi)容為請(qǐng)求體,原因是等待請(qǐng)求體傳輸可能是一件耗時(shí)的工作。
比如上傳文件,而很多時(shí)候我們可能并不需要理會(huì)請(qǐng)求體的內(nèi)容,惡意的POST請(qǐng)求會(huì)大大消耗服務(wù)器的資源,所以 node.js 默認(rèn)是不會(huì)解析請(qǐng)求體的,當(dāng)你需要的時(shí)候,需要手動(dòng)來做。
基本語(yǔ)法結(jié)構(gòu)說明
varhttp = require('http');varquerystring = require('querystring'); http.createServer(function(req, res){// 定義了一個(gè)post變量,用于暫存請(qǐng)求體的信息varpost = ''; // 通過req的data事件監(jiān)聽函數(shù),每當(dāng)接受到請(qǐng)求體的數(shù)據(jù),就累加到post變量中req.on('data', function(chunk){post += chunk; }); // 在end事件觸發(fā)后,通過querystring.parse將post解析為真正的POST請(qǐng)求格式,然后向客戶端返回。req.on('end', function(){post = querystring.parse(post); res.end(util.inspect(post)); });}).listen(3000);
以下實(shí)例表單通過 POST 提交并輸出數(shù)據(jù):
實(shí)例
varhttp = require('http');varquerystring = require('querystring'); varpostHTML = '<html><head><meta charset="utf-8"><title>編程改變未來Node.js 實(shí)例</title></head>' + '<body>' + '<form method="post">' + '網(wǎng)站名: <input name="name"><br>' + '網(wǎng)站 URL: <input name="url"><br>' + '<input type="submit">' + '</form>' + '</body></html>'; http.createServer(function(req, res){varbody = ""; req.on('data', function(chunk){body += chunk; }); req.on('end', function(){// 解析參數(shù)body = querystring.parse(body); // 設(shè)置響應(yīng)頭部信息及編碼res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'}); if(body.name && body.url){// 輸出提交的數(shù)據(jù)res.write("網(wǎng)站名:" + body.name); res.write("<br>"); res.write("網(wǎng)站 URL:" + body.url); }else{// 輸出表單res.write(postHTML); }res.end(); });}).listen(3000);
什么是 Web 服務(wù)器?
Web服務(wù)器一般指網(wǎng)站服務(wù)器,是指駐留于因特網(wǎng)上某種類型計(jì)算機(jī)的程序,Web服務(wù)器的基本功能就是提供Web信息瀏覽服務(wù)。它只需支持HTTP協(xié)議、HTML文檔格式及URL,與客戶端的網(wǎng)絡(luò)瀏覽器配合。
大多數(shù) web 服務(wù)器都支持服務(wù)端的腳本語(yǔ)言(php、python、ruby)等,并通過腳本語(yǔ)言從數(shù)據(jù)庫(kù)獲取數(shù)據(jù),將結(jié)果返回給客戶端瀏覽器。
目前最主流的三個(gè)Web服務(wù)器是Apache、Nginx、IIS。
Web 應(yīng)用架構(gòu)
Client - 客戶端,一般指瀏覽器,瀏覽器可以通過 HTTP 協(xié)議向服務(wù)器請(qǐng)求數(shù)據(jù)。
Server - 服務(wù)端,一般指 Web 服務(wù)器,可以接收客戶端請(qǐng)求,并向客戶端發(fā)送響應(yīng)數(shù)據(jù)。
Business - 業(yè)務(wù)層, 通過 Web 服務(wù)器處理應(yīng)用程序,如與數(shù)據(jù)庫(kù)交互,邏輯運(yùn)算,調(diào)用外部程序等。
Data - 數(shù)據(jù)層,一般由數(shù)據(jù)庫(kù)組成。
使用 Node 創(chuàng)建 Web 服務(wù)器
Node.js 提供了 http 模塊,http 模塊主要用于搭建 HTTP 服務(wù)端和客戶端,使用 HTTP 服務(wù)器或客戶端功能必須調(diào)用 http 模塊,代碼如下:
var http = require('http');
以下是演示一個(gè)最基本的 HTTP 服務(wù)器架構(gòu)(使用8081端口),創(chuàng)建 server.js 文件,代碼如下所示:
var http = require('http'); var fs = require('fs'); var url = require('url'); // 創(chuàng)建服務(wù)器 http.createServer( function (request, response) { // 解析請(qǐng)求,包括文件名 var pathname = url.parse(request.url).pathname; // 輸出請(qǐng)求的文件名 console.log("Request for " + pathname + " received."); // 從文件系統(tǒng)中讀取請(qǐng)求的文件內(nèi)容 fs.readFile(pathname.substr(1), function (err, data) { if (err) { console.log(err); // HTTP 狀態(tài)碼: 404 : NOT FOUND // Content Type: text/plain response.writeHead(404, {'Content-Type': 'text/html'}); }else{ // HTTP 狀態(tài)碼: 200 : OK // Content Type: text/plain response.writeHead(200, {'Content-Type': 'text/html'}); // 響應(yīng)文件內(nèi)容 response.write(data.toString()); } // 發(fā)送響應(yīng)數(shù)據(jù) response.end(); }); }).listen(8081); // 控制臺(tái)會(huì)輸出以下信息 console.log('Server running at http://127.0.0.1:8081/');
接下來我們?cè)谠撃夸浵聞?chuàng)建一個(gè) index.htm 文件,代碼如下:
<html> <head> <title>Sample Page</title> </head> <body> Hello World! </body> </html>
執(zhí)行 server.js 文件:
$ node server.js Server running at http://127.0.0.1:8081/
接著我們?cè)跒g覽器中打開地址:http://127.0.0.1:8081/index.htm,顯示如下圖所示:
執(zhí)行 server.js 的控制臺(tái)輸出信息如下:
Server running at http://127.0.0.1:8081/ Request for /index.htm received. # 客戶端請(qǐng)求信息
Gif 實(shí)例演示
使用 Node 創(chuàng)建 Web 客戶端
Node 創(chuàng)建 Web 客戶端需要引入 http 模塊,創(chuàng)建 client.js 文件,代碼如下所示:
var http = require('http'); // 用于請(qǐng)求的選項(xiàng) var options = { host: 'localhost', port: '8081', path: '/index.htm' }; // 處理響應(yīng)的回調(diào)函數(shù) var callback = function(response){ // 不斷更新數(shù)據(jù) var body = ''; response.on('data', function(data) { body += data; }); response.on('end', function() { // 數(shù)據(jù)接收完成 console.log(body); }); } // 向服務(wù)端發(fā)送請(qǐng)求 var req = http.request(options, callback); req.end();
新開一個(gè)終端,執(zhí)行 client.js 文件,輸出結(jié)果如下:
$ node client.js <html> <head> <title>Sample Page</title> </head> <body> Hello World! </body> </html>
執(zhí)行 server.js 的控制臺(tái)輸出信息如下:
Server running at http://127.0.0.1:8081/ Request for /index.htm received. # 客戶端請(qǐng)求信息
Gif 實(shí)例演示
Express 簡(jiǎn)介
Express 是一個(gè)簡(jiǎn)潔而靈活的 node.js Web應(yīng)用框架, 提供了一系列強(qiáng)大特性幫助你創(chuàng)建各種 Web 應(yīng)用,和豐富的 HTTP 工具。
使用 Express 可以快速地搭建一個(gè)完整功能的網(wǎng)站。
Express 框架核心特性:
可以設(shè)置中間件來響應(yīng) HTTP 請(qǐng)求。
定義了路由表用于執(zhí)行不同的 HTTP 請(qǐng)求動(dòng)作。
可以通過向模板傳遞參數(shù)來動(dòng)態(tài)渲染 HTML 頁(yè)面。
安裝 Express
安裝 Express 并將其保存到依賴列表中:
$ cnpm install express --save
以上命令會(huì)將 Express 框架安裝在當(dāng)前目錄的 node_modules 目錄中, node_modules 目錄下會(huì)自動(dòng)創(chuàng)建 express 目錄。以下幾個(gè)重要的模塊是需要與 express 框架一起安裝的:
body-parser - node.js 中間件,用于處理 JSON, Raw, Text 和 URL 編碼的數(shù)據(jù)。
cookie-parser - 這就是一個(gè)解析Cookie的工具。通過req.cookies可以取到傳過來的cookie,并把它們轉(zhuǎn)成對(duì)象。
multer - node.js 中間件,用于處理 enctype="multipart/form-data"(設(shè)置表單的MIME編碼)的表單數(shù)據(jù)。
$ cnpm install body-parser --save $ cnpm install cookie-parser --save $ cnpm install multer --save
安裝完后,我們可以查看下 express 使用的版本號(hào):
$ cnpm list express/data/www/node└── express@4.15.2 -> /Users/tianqixin/www/node/node_modules/.4.15.2@express
第一個(gè) Express 框架實(shí)例
接下來我們使用 Express 框架來輸出 "Hello World"。
以下實(shí)例中我們引入了 express 模塊,并在客戶端發(fā)起請(qǐng)求后,響應(yīng) "Hello World" 字符串。
創(chuàng)建 express_demo.js 文件,代碼如下所示:
express_demo.js 文件代碼:
//express_demo.js 文件varexpress = require('express');varapp = express(); app.get('/', function(req, res){res.send('Hello World');})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應(yīng)用實(shí)例,訪問地址為 http://%s:%s", host, port)})
執(zhí)行以上代碼:
$ node express_demo.js 應(yīng)用實(shí)例,訪問地址為 http://0.0.0.0:8081
在瀏覽器中訪問 http://127.0.0.1:8081,結(jié)果如下圖所示:
請(qǐng)求和響應(yīng)
Express 應(yīng)用使用回調(diào)函數(shù)的參數(shù): request 和 response 對(duì)象來處理請(qǐng)求和響應(yīng)的數(shù)據(jù)。
app.get('/', function (req, res) { // --})
request 和 response 對(duì)象的具體介紹:
Request 對(duì)象 - request 對(duì)象表示 HTTP 請(qǐng)求,包含了請(qǐng)求查詢字符串,參數(shù),內(nèi)容,HTTP 頭部等屬性。常見屬性有:
req.app:當(dāng)callback為外部文件時(shí),用req.app訪問express的實(shí)例
req.baseUrl:獲取路由當(dāng)前安裝的URL路徑
req.body / req.cookies:獲得「請(qǐng)求主體」/ Cookies
req.fresh / req.stale:判斷請(qǐng)求是否還「新鮮」
req.hostname / req.ip:獲取主機(jī)名和IP地址
req.originalUrl:獲取原始請(qǐng)求URL
req.params:獲取路由的parameters
req.path:獲取請(qǐng)求路徑
req.protocol:獲取協(xié)議類型
req.query:獲取URL的查詢參數(shù)串
req.route:獲取當(dāng)前匹配的路由
req.subdomains:獲取子域名
req.accepts():檢查可接受的請(qǐng)求的文檔類型
req.acceptsCharsets / req.acceptsEncodings / req.acceptsLanguages:返回指定字符集的第一個(gè)可接受字符編碼
req.get():獲取指定的HTTP請(qǐng)求頭
req.is():判斷請(qǐng)求頭Content-Type的MIME類型
Response 對(duì)象 - response 對(duì)象表示 HTTP 響應(yīng),即在接收到請(qǐng)求時(shí)向客戶端發(fā)送的 HTTP 響應(yīng)數(shù)據(jù)。常見屬性有:
res.app:同req.app一樣
res.append():追加指定HTTP頭
res.set()在res.append()后將重置之前設(shè)置的頭
res.cookie(name,value [,option]):設(shè)置Cookie
opition: domain / expires / httpOnly / maxAge / path / secure / signed
res.clearCookie():清除Cookie
res.download():傳送指定路徑的文件
res.get():返回指定的HTTP頭
res.json():傳送JSON響應(yīng)
res.jsonp():傳送JSONP響應(yīng)
res.location():只設(shè)置響應(yīng)的Location HTTP頭,不設(shè)置狀態(tài)碼或者close response
res.redirect():設(shè)置響應(yīng)的Location HTTP頭,并且設(shè)置狀態(tài)碼302
res.send():傳送HTTP響應(yīng)
res.sendFile(path [,options] [,fn]):傳送指定路徑的文件 -會(huì)自動(dòng)根據(jù)文件extension設(shè)定Content-Type
res.set():設(shè)置HTTP頭,傳入object可以一次設(shè)置多個(gè)頭
res.status():設(shè)置HTTP狀態(tài)碼
res.type():設(shè)置Content-Type的MIME類型
路由
我們已經(jīng)了解了 HTTP 請(qǐng)求的基本應(yīng)用,而路由決定了由誰(shuí)(指定腳本)去響應(yīng)客戶端請(qǐng)求。
在HTTP請(qǐng)求中,我們可以通過路由提取出請(qǐng)求的URL以及GET/POST參數(shù)。
接下來我們擴(kuò)展 Hello World,添加一些功能來處理更多類型的 HTTP 請(qǐng)求。
創(chuàng)建 express_demo2.js 文件,代碼如下所示:
express_demo2.js 文件代碼:
varexpress = require('express');varapp = express(); // 主頁(yè)輸出 "Hello World"app.get('/', function(req, res){console.log("主頁(yè) GET 請(qǐng)求"); res.send('Hello GET');})// POST 請(qǐng)求app.post('/', function(req, res){console.log("主頁(yè) POST 請(qǐng)求"); res.send('Hello POST');})// /del_user 頁(yè)面響應(yīng)app.get('/del_user', function(req, res){console.log("/del_user 響應(yīng) DELETE 請(qǐng)求"); res.send('刪除頁(yè)面');})// /list_user 頁(yè)面 GET 請(qǐng)求app.get('/list_user', function(req, res){console.log("/list_user GET 請(qǐng)求"); res.send('用戶列表頁(yè)面');})// 對(duì)頁(yè)面 abcd, abxcd, ab123cd, 等響應(yīng) GET 請(qǐng)求app.get('/ab*cd', function(req, res){console.log("/ab*cd GET 請(qǐng)求"); res.send('正則匹配');})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應(yīng)用實(shí)例,訪問地址為 http://%s:%s", host, port)})
執(zhí)行以上代碼:
$ node express_demo2.js 應(yīng)用實(shí)例,訪問地址為 http://0.0.0.0:8081
接下來你可以嘗試訪問 http://127.0.0.1:8081 不同的地址,查看效果。
在瀏覽器中訪問 http://127.0.0.1:8081/list_user,結(jié)果如下圖所示:
在瀏覽器中訪問 http://127.0.0.1:8081/abcd,結(jié)果如下圖所示:
在瀏覽器中訪問 http://127.0.0.1:8081/abcdefg,結(jié)果如下圖所示:
靜態(tài)文件
Express 提供了內(nèi)置的中間件 express.static 來設(shè)置靜態(tài)文件如:圖片, CSS, JavaScript 等。
你可以使用 express.static 中間件來設(shè)置靜態(tài)文件路徑。例如,如果你將圖片, CSS, JavaScript 文件放在 public 目錄下,你可以這么寫:
app.use(express.static('public'));
我們可以到 public/images 目錄下放些圖片,如下所示:
node_modules server.jspublic/public/imagespublic/images/logo.png
讓我們?cè)傩薷南?"Hello World" 應(yīng)用添加處理靜態(tài)文件的功能。
創(chuàng)建 express_demo3.js 文件,代碼如下所示:
express_demo3.js 文件代碼:
varexpress = require('express');varapp = express(); app.use(express.static('public')); app.get('/', function(req, res){res.send('Hello World');})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應(yīng)用實(shí)例,訪問地址為 http://%s:%s", host, port)})
執(zhí)行以上代碼:
$ node express_demo3.js 應(yīng)用實(shí)例,訪問地址為 http://0.0.0.0:8081
執(zhí)行以上代碼:
GET 方法
以下實(shí)例演示了在表單中通過 GET 方法提交兩個(gè)參數(shù),我們可以使用 server.js 文件內(nèi)的 process_get 路由器來處理輸入:
index.htm 文件代碼:
<html><body><formaction="http://127.0.0.1:8081/process_get"method="GET">First Name: <inputtype="text"name="first_name"><br> Last Name: <inputtype="text"name="last_name"><inputtype="submit"value="Submit"></form></body></html>
server.js 文件代碼:
varexpress = require('express');varapp = express(); app.use(express.static('public')); app.get('/index.htm', function(req, res){res.sendFile(__dirname + "/" + "index.htm");})app.get('/process_get', function(req, res){// 輸出 JSON 格式varresponse = {"first_name":req.query.first_name, "last_name":req.query.last_name}; console.log(response); res.end(JSON.stringify(response));})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應(yīng)用實(shí)例,訪問地址為 http://%s:%s", host, port)})
執(zhí)行以上代碼:
node server.js 應(yīng)用實(shí)例,訪問地址為 http://0.0.0.0:8081
瀏覽器訪問 http://127.0.0.1:8081/index.htm,如圖所示:
現(xiàn)在你可以向表單輸入數(shù)據(jù),并提交,如下演示:
POST 方法
以下實(shí)例演示了在表單中通過 POST 方法提交兩個(gè)參數(shù),我們可以使用 server.js 文件內(nèi)的 process_post 路由器來處理輸入:
index.htm 文件代碼:
<html><body><formaction="http://127.0.0.1:8081/process_post"method="POST">First Name: <inputtype="text"name="first_name"><br> Last Name: <inputtype="text"name="last_name"><inputtype="submit"value="Submit"></form></body></html>
server.js 文件代碼:
varexpress = require('express');varapp = express();varbodyParser = require('body-parser'); // 創(chuàng)建 application/x-www-form-urlencoded 編碼解析varurlencodedParser = bodyParser.urlencoded({extended: false})app.use(express.static('public')); app.get('/index.htm', function(req, res){res.sendFile(__dirname + "/" + "index.htm");})app.post('/process_post', urlencodedParser, function(req, res){// 輸出 JSON 格式varresponse = {"first_name":req.body.first_name, "last_name":req.body.last_name}; console.log(response); res.end(JSON.stringify(response));})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應(yīng)用實(shí)例,訪問地址為 http://%s:%s", host, port)})
執(zhí)行以上代碼:
$ node server.js應(yīng)用實(shí)例,訪問地址為 http://0.0.0.0:8081
瀏覽器訪問 http://127.0.0.1:8081/index.htm,如圖所示:
現(xiàn)在你可以向表單輸入數(shù)據(jù),并提交,如下演示:
文件上傳
以下我們創(chuàng)建一個(gè)用于上傳文件的表單,使用 POST 方法,表單 enctype 屬性設(shè)置為 multipart/form-data。
index.htm 文件代碼:
<html><head><title>文件上傳表單</title></head><body><h3>文件上傳:</h3>選擇一個(gè)文件上傳: <br/><formaction="/file_upload"method="post"enctype="multipart/form-data"><inputtype="file"name="image"size="50"/><br/><inputtype="submit"value="上傳文件"/></form></body></html>
server.js 文件代碼:
<pre>varexpress = require('express');varapp = express();varfs = require("fs"); varbodyParser = require('body-parser');varmulter = require('multer'); app.use(express.static('public'));app.use(bodyParser.urlencoded({extended: false}));app.use(multer({dest: '/tmp/'}).array('image')); app.get('/index.htm', function(req, res){res.sendFile(__dirname + "/" + "index.htm");})app.post('/file_upload', function(req, res){console.log(req.files[0]); // 上傳的文件信息vardes_file = __dirname + "/" + req.files[0].originalname; fs.readFile(req.files[0].path, function(err, data){fs.writeFile(des_file, data, function(err){if(err){console.log(err); }else{response = {message:'File uploaded successfully', filename:req.files[0].originalname}; }console.log(response); res.end(JSON.stringify(response)); }); });})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應(yīng)用實(shí)例,訪問地址為 http://%s:%s", host, port)})
執(zhí)行以上代碼:
$ node server.js 應(yīng)用實(shí)例,訪問地址為 http://0.0.0.0:8081
瀏覽器訪問 http://127.0.0.1:8081/index.htm,如圖所示:
現(xiàn)在你可以向表單輸入數(shù)據(jù),并提交,如下演示:
Cookie 管理
我們可以使用中間件向 Node.js 服務(wù)器發(fā)送 cookie 信息,以下代碼輸出了客戶端發(fā)送的 cookie 信息:
express_cookie.js 文件代碼:
// express_cookie.js 文件varexpress = require('express')varcookieParser = require('cookie-parser')varapp = express()app.use(cookieParser())app.get('/', function(req, res){console.log("Cookies: ", req.cookies)})app.listen(8081)
執(zhí)行以上代碼:
$ node express_cookie.js
現(xiàn)在你可以訪問 http://127.0.0.1:8081 并查看終端信息的輸出,如下演示:
相關(guān)資料
Express官網(wǎng): http://expressjs.com/
Express4.x API 中文版: Express4.x API Chinese
Express4.x API:http://expressjs.com/zh-cn/4x/api.html
載說明:原創(chuàng)不易,未經(jīng)授權(quán),謝絕任何形式的轉(zhuǎn)載
如何使您的網(wǎng)站呈現(xiàn)最佳狀態(tài)?這個(gè)問題有很多答案,本文介紹了當(dāng)前框架中應(yīng)用最廣泛的十種渲染設(shè)計(jì)模式,讓您能夠選擇最適合您的方式。
近年來,網(wǎng)絡(luò)開發(fā)的迅速演變,尤其是在前端開發(fā)領(lǐng)域。這種轉(zhuǎn)變主要?dú)w功于無數(shù)涌現(xiàn)的框架和技術(shù),它們旨在簡(jiǎn)化和增強(qiáng)構(gòu)建引人入勝的用戶界面的過程。然而,由于現(xiàn)有框架的豐富多樣以及不斷涌現(xiàn)的新框架,跟上前端趨勢(shì)已成為一項(xiàng)艱巨的任務(wù)。對(duì)于新手來說,很容易感到不知所措,仿佛迷失在廣闊的選擇海洋中。
渲染是前端開發(fā)的核心挑戰(zhàn),它將數(shù)據(jù)和代碼轉(zhuǎn)化為可見且可交互的用戶界面。雖然大多數(shù)框架以類似的方式應(yīng)對(duì)這一挑戰(zhàn),通常比之前的方法更簡(jiǎn)潔,但也有一些框架選擇了全新的解決方案。在本文中,我們將研究流行框架中使用的十種常見渲染模式,通過這樣做,無論是初學(xué)者還是專家都將獲得對(duì)新舊框架的扎實(shí)基礎(chǔ)理解,同時(shí)也能對(duì)解決應(yīng)用程序中的渲染問題有新的見解。
在本文的結(jié)尾,您將會(huì):
在前端開發(fā)的背景下,渲染是將數(shù)據(jù)和代碼轉(zhuǎn)換為對(duì)最終用戶可見的HTML。UI渲染模式是指實(shí)現(xiàn)渲染過程可以采用的各種方法。這些模式概述了不同的策略,用于描述轉(zhuǎn)換發(fā)生的方式以及呈現(xiàn)出的用戶界面。正如我們很快會(huì)發(fā)現(xiàn)的那樣,根據(jù)所實(shí)現(xiàn)的模式,渲染可以在服務(wù)器上或?yàn)g覽器中進(jìn)行,可以部分或一次性完成。
選擇正確的渲染模式對(duì)開發(fā)人員來說至關(guān)重要,因?yàn)樗苯佑绊懙絎eb應(yīng)用程序的性能、成本、速度、可擴(kuò)展性、用戶體驗(yàn),甚至開發(fā)人員的體驗(yàn)。
在本文中,我們將介紹下面列出的前十種渲染模式:
在每個(gè)案例中,我們將研究渲染模式的概念、優(yōu)點(diǎn)和缺點(diǎn)、使用案例、相關(guān)的框架,并提供一個(gè)簡(jiǎn)單的代碼示例來闡明觀點(diǎn)。
所有示例的全局CSS如下
/* style.css or the name of the global stylesheet */
h1,
h2 {
color: purple;
margin: 1rem;
}
a {
color: var(--text-color);
display: block;
margin: 2rem 0;
}
body {
font-family: Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
}
.dark-mode {
--background-color: #333;
--text-color: #fff;
}
.light-mode {
--background-color: #fff;
--text-color: #333;
}
.toggle-btn{
background-color: yellow;
padding: 0.3rem;
margin: 1rem;
margin-top: 100%;
border-radius: 5px;
}
靜態(tài)網(wǎng)站是最原始、最基本、最直接的UI渲染方法。它通過簡(jiǎn)單地編寫HTML、CSS和JavaScript來創(chuàng)建網(wǎng)站。一旦代碼準(zhǔn)備好,它會(huì)被上傳為靜態(tài)文件到托管服務(wù)(如Netlify),并指向一個(gè)域名。通過URL請(qǐng)求時(shí),靜態(tài)文件會(huì)直接提供給用戶,無需服務(wù)器端處理。靜態(tài)網(wǎng)站渲染非常適合沒有交互性和動(dòng)態(tài)內(nèi)容的靜態(tài)網(wǎng)站,比如落地頁(yè)和文檔網(wǎng)站。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (HTML/CSS/JavaScript)
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Cryptocurrency Price App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Cryptocurrency Price App</h1>
<ol>
<li><a href="./btcPrice.html">Bitcoin </a></li>
<li><a href="./ethPrice.html">Ethereum </a></li>
<li><a href="./xrpPrice.html">Ripple </a></li>
<li><a href="./adaPrice.html">Cardano </a></li>
</ol>
</body>
</html>
<!-- btcPrice.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h2>BTC</h2>
<ul>
<li id="binance">Binance:</li>
<li id="kucoin">Kucoin:</li>
<li id="bitfinex">Bitfinex:</li>
<li id="crypto_com">Crypto.com:</li>
</ul>
<script src="fetchPrices.js"></script>
<button class="toggle-btn">Toggle Mode</button>
<script src="darkMode.js"></script>
</body>
</html>
//fetchPrices.js
const binance = document.querySelector("#binance");
const kucoin = document.querySelector("#kucoin");
const bitfinex = document.querySelector("#bitfinex");
const crypto_com = document.querySelector("#crypto_com");
// Get the cryptocurrency prices from an API
let marketPrices = { binance: [], kucoin: [], bitfinex: [], crypto_com: [] };
async function getCurrentPrice(market) {
if (
`${market}` === "binance" ||
`${market}` === "kucoin" ||
`${market}` === "crypto_com" ||
`${market}` === "bitfinex"
) {
marketPrices[market] = [];
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=bitcoin%2Cripple%2Cethereum%2Ccardano`
);
if (res) {
let data = await res.json();
if (data) {
for (const info of data.tickers) {
if (info.target === "USDT") {
let name = info.base;
let price = info.last;
if (`${market}` === "binance") {
marketPrices.binance = [
...marketPrices.binance,
{ [name]: price },
];
}
if (`${market}` === "kucoin") {
marketPrices.kucoin = [...marketPrices.kucoin, { [name]: price }];
}
if (`${market}` === "bitfinex") {
marketPrices.bitfinex = [
...marketPrices.bitfinex,
{ [name]: price },
];
}
if (`${market}` === "crypto_com") {
marketPrices.crypto_com = [
...marketPrices.crypto_com,
{ [name]: price },
];
}
}
}
}
}
}
}
async function findPrices() {
try {
const fetched = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
if (fetched) {
binance ? (binance.innerHTML += `${marketPrices.binance[0].BTC}`) : null;
kucoin ? (kucoin.innerHTML += `${marketPrices.kucoin[0].BTC}`) : null;
bitfinex
? (bitfinex.innerHTML += `${marketPrices.bitfinex[0].BTC}`)
: null;
crypto_com
? (crypto_com.innerHTML += `${marketPrices.crypto_com[0].BTC}`)
: null;
}
} catch (e) {
console.log(e);
}
}
findPrices();
//darkMode.js
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
document.body.classList.add("dark-mode");
} else if (preferredMode === "light") {
document.body.classList.add("light-mode");
}
});
// Check the user's preferred mode on page load (optional)
function toggleMode() {
const body = document.body;
body.classList.toggle("dark-mode");
body.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
toggleBtn.addEventListener("click", () => {
toggleMode();
});
上面的代碼塊展示了我們使用HTML/CSS/JavaScript實(shí)現(xiàn)的應(yīng)用程序。下面是應(yīng)用程序。
第一頁(yè):顯示所有可用的虛擬幣
第2頁(yè):從Coingecko API獲取的不同交易所的BTC價(jià)格。
請(qǐng)注意,在使用靜態(tài)網(wǎng)站時(shí),每個(gè)幣種的價(jià)格頁(yè)面必須手動(dòng)編寫。
這種渲染模式是為了處理我們網(wǎng)站上的動(dòng)態(tài)數(shù)據(jù)而出現(xiàn)的解決方案,并導(dǎo)致了今天許多最大、最受歡迎的動(dòng)態(tài)Web應(yīng)用程序的創(chuàng)建。在MPA中,渲染由服務(wù)器完成,服務(wù)器會(huì)重新加載以基于當(dāng)前底層數(shù)據(jù)(通常來自數(shù)據(jù)庫(kù))生成新的HTML,以響應(yīng)瀏覽器發(fā)出的每個(gè)請(qǐng)求。這意味著網(wǎng)站可以根據(jù)底層數(shù)據(jù)的變化而改變。最常見的用例是電子商務(wù)網(wǎng)站、企業(yè)應(yīng)用程序和新聞公司博客。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (ExpressandEJS)
npm i express and ejs
<!-- views/index.ejs -->
<!-- css file should be in public folder-->
<!DOCTYPE html>
<html>
<head>
<title>Cryptocurrency Price App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Cryptocurrency Price App</h1>
<ol>
<li><a href="./price/btc">Bitcoin </a></li>
<li><a href="./price/eth">Ethereum </a></li>
<li><a href="./price/xrp">Ripple </a></li>
<li><a href="./price/ada">Cardano </a></li>
</ol>
</body>
</html>
<!-- views/price.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cryptocurrency Price App</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<h2><%- ID %></h2>
<ul>
<li id="binance">Binance:<%- allPrices.binance[0][ID] %></li>
<li id="kucoin">Kucoin:<%- allPrices.kucoin[0][ID] %></li>
<li id="bitfinex">Bitfinex:<%- allPrices.bitfinex[0][ID] %></li>
<li id="crypto_com">Crypto.com:<%- allPrices.crypto_com[0][ID] %></li>
</ul>
<button class="toggle-btn">Toggle Mode</button>
<script src="/darkMode.js"></script>
</body>
</html>
// public/darkMode.js
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
document.body.classList.add("dark-mode");
} else if (preferredMode === "light") {
document.body.classList.add("light-mode");
}
});
// Check the user's preferred mode on page load (optional)
function toggleMode() {
const body = document.body;
body.classList.toggle("dark-mode");
body.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
toggleBtn.addEventListener("click", () => {
toggleMode();
});
// utils/fetchPrices.js
async function getCurrentPrice(market) {
let prices = [];
if (
`${market}` === "binance" ||
`${market}` === "kucoin" ||
`${market}` === "crypto_com" ||
`${market}` === "bitfinex"
) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=bitcoin%2Cripple%2Cethereum%2Ccardano`
);
const data = await res.json();
for (const info of data.tickers) {
if (info.target === "USDT") {
let name = info.base;
let price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
}
module.exports = getCurrentPrice;
//app.js.
const getCurrentPrice = require("./utils/fetchPrices");
const express = require("express");
const ejs = require("ejs");
const path = require("path");
const app = express();
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use(express.static("public"));
app.get("/", (req, res) => {
res.render("index");
});
app.get("/price/:id", async (req, res) => {
let { id } = req.params;
let ID = id.toUpperCase();
let allPrices;
try {
const fetched = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
if (fetched) {
allPrices = {};
allPrices.binance = fetched[0];
allPrices.kucoin = fetched[1];
allPrices.bitfinex = fetched[2];
allPrices.crypto_com = fetched[3];
console.log(allPrices);
res.render("price", { ID, allPrices });
}
} catch (e) {
res.send("server error");
}
});
app.listen(3005, () => console.log("Server is running on port 3005"));
注意:在這里,每個(gè)頁(yè)面都將由服務(wù)器自動(dòng)生成,不同于靜態(tài)網(wǎng)站,靜態(tài)網(wǎng)站需要手動(dòng)編寫每個(gè)文件。
單頁(yè)應(yīng)用程序(SPA)是2010年代創(chuàng)建高度交互式Web應(yīng)用程序的解決方案,至今仍在使用。在這里,SPA通過從服務(wù)器獲取HTML外殼(空白HTML頁(yè)面)和JavaScript捆綁包來處理渲染到瀏覽器。在瀏覽器中,它將控制權(quán)(水合)交給JavaScript,動(dòng)態(tài)地將內(nèi)容注入(渲染)到外殼中。在這種情況下,渲染是在客戶端(CSR)上執(zhí)行的。使用JavaScript,這些SPA能夠在不需要完整頁(yè)面重新加載的情況下對(duì)單個(gè)頁(yè)面上的內(nèi)容進(jìn)行大量操作。它們還通過操作URL欄來創(chuàng)建多個(gè)頁(yè)面的幻覺,以指示加載到外殼上的每個(gè)資源。常見的用例包括項(xiàng)目管理系統(tǒng)、協(xié)作平臺(tái)、社交媒體Web應(yīng)用、交互式儀表板或文檔編輯器,這些應(yīng)用程序受益于SPA的響應(yīng)性和交互性。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (ReactandReact-router)
// pages/index.jsx
import { Link } from "react-router-dom";
export default function Index() {
return (
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<Link to="./price/btc">Bitcoin </Link>
</li>
<li>
<Link to="./price/eth">Ethereum </Link>
</li>
<li>
<Link to="./price/xrp">Ripple </Link>
</li>
<li>
<Link to="./price/ada">Cardano </Link>
</li>
</ol>
</div>
);
}
//pages/price.jsx
import { useParams } from "react-router-dom";
import { useEffect, useState, useRef, Suspense } from "react";
import Btn from "../components/Btn";
export default function Price() {
const { id } = useParams();
const ID = id.toUpperCase();
const [marketPrices, setMarketPrices] = useState({});
const [isLoading, setIsLoading] = useState(true);
const containerRef = useRef(null);
function fetchMode() {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.current.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.current.classList.add("light-mode");
}
}
useEffect(() => {
fetchMode();
}, []);
async function getCurrentPrice(market) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
useEffect(() => {
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
setMarketPrices(allPrices);
setIsLoading(false);
console.log(allPrices); // Log the fetched prices to the console
} catch (error) {
console.log(error);
setIsLoading(false);
}
}
fetchMarketPrices();
}, []);
return (
<div className="container" ref={containerRef}>
<h2>{ID}</h2>
{isLoading ? (
<p>Loading...</p>
) : Object.keys(marketPrices).length > 0 ? (
<ul>
{Object.keys(marketPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {marketPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
<Btn container={containerRef} />
</div>
);
}
//components/Btn.jsx
export default function Btn({ container }) {
function toggleMode() {
container.current.classList.toggle("dark-mode");
container.current.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = container.current.classList.contains("dark-mode")
? "dark"
: "light";
localStorage.setItem("mode", currentMode);
}
// Check the user's preferred mode on page load (optional)
return (
<div>
<button
className="toggle-btn"
onClick={() => {
toggleMode();
}}
>
Toggle Mode
</button>
</div>
);
}
// App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Index from "./pages";
import Price from "./pages/Price";
const router = createBrowserRouter([
{
path: "/",
element: <Index />,
},
{
path: "/price/:id",
element: <Price />,
},
]);
function App() {
return (
<>
<RouterProvider router={router}></RouterProvider>
</>
);
}
export default App;
靜態(tài)網(wǎng)站生成(SSG)是一種利用構(gòu)建網(wǎng)站的原始靜態(tài)網(wǎng)站模式的渲染模式。在構(gòu)建過程中,從源代碼中預(yù)先構(gòu)建和渲染了所有可能的網(wǎng)頁(yè),生成靜態(tài)HTML文件,然后將其存儲(chǔ)在存儲(chǔ)桶中,就像在典型靜態(tài)網(wǎng)站的情況下原始上傳靜態(tài)文件一樣。對(duì)于基于源代碼可能存在的任何路由的請(qǐng)求,將向客戶端提供相應(yīng)的預(yù)構(gòu)建靜態(tài)頁(yè)面。因此,與SSR或SPA不同,SSG不依賴于服務(wù)器端渲染或客戶端JavaScript來動(dòng)態(tài)渲染內(nèi)容。相反,內(nèi)容是提前生成的,并且可以被緩存和高性能地傳遞給用戶。這適用于中度交互的網(wǎng)站,其數(shù)據(jù)不經(jīng)常更改,例如作品集網(wǎng)站、小型博客或文檔網(wǎng)站。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Nextjs)
// components/Btn.js
export default function Btn({ container }) {
function toggleMode() {
container.current.classList.toggle("dark-mode");
container.current.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = container.current.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
// Check the user's preferred mode on page load (optional)
return (
<div>
<button className="toggle-btn" onClick={() => {toggleMode()}}>
Toggle Mode
</button>
</div>
);
}
// components/Client.js
"use client";
import { useEffect, useRef } from "react";
import Btn from "@/app/components/Btn";
import { usePathname } from "next/navigation";
export default function ClientPage({ allPrices }) {
const pathname = usePathname();
let ID = pathname.slice(-3).toUpperCase();
const containerRef = useRef(null);
function fetchMode() {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.current.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.current.classList.add("light-mode");
}
}
useEffect(() => {
fetchMode();
}, []);
return (
<div className="container" ref={containerRef}>
<h2>{ID}</h2>
{Object.keys(allPrices).length > 0 ? (
<ul>
{Object.keys(allPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {allPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
<Btn container={containerRef} />
</div>
);
}
//price/[id]/page.js
import ClientPage from "../../components/Client";
async function getCurrentPrice(market) {
const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
console.log("fetched");
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
export default async function Price() {
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
return allPrices;
// Log the fetched prices to the console
} catch (error) {
console.log(error);
}
}
const allPrices = await fetchMarketPrices();
return (
<div>
{allPrices && Object.keys(allPrices).length > 0 ? (
<ClientPage allPrices={allPrices} />
) : (
<p>No data available.</p>
)}
</div>
);
}
//page.js
import Link from "next/link";
export default function Index() {
return (
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<Link href="./price/btc">Bitcoin </Link>
</li>
<li>
<Link href="./price/eth">Ethereum </Link>
</li>
<li>
<Link href="./price/xrp">Ripple </Link>
</li>
<li>
<Link href="./price/ada">Cardano </Link>
</li>
</ol>
</div>
);
}
服務(wù)器端渲染(SSR)是一種渲染模式,它結(jié)合了多頁(yè)面應(yīng)用(MPA)和單頁(yè)面應(yīng)用(SPA)的能力,以克服兩者的局限性。在這種模式下,服務(wù)器生成網(wǎng)頁(yè)的HTML內(nèi)容,填充動(dòng)態(tài)數(shù)據(jù),并將其發(fā)送給客戶端進(jìn)行顯示。在瀏覽器上,JavaScript可以接管已經(jīng)渲染的頁(yè)面,為頁(yè)面上的組件添加交互性,就像在SPA中一樣。SSR在將完整的HTML交付給瀏覽器之前,在服務(wù)器上處理渲染過程,而SPA完全依賴于客戶端JavaScript進(jìn)行渲染。SSR特別適用于注重SEO、內(nèi)容傳遞或具有特定可訪問性要求的應(yīng)用,如企業(yè)網(wǎng)站、新聞網(wǎng)站和電子商務(wù)網(wǎng)站。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Nextjs)
在NEXT.js上實(shí)現(xiàn)SSR的代碼與SSG演示幾乎相同。這里,唯一的變化在于 getCurrentPrice 函數(shù)。使用帶有 no-cache 選項(xiàng)的fetch API,頁(yè)面將不會(huì)被緩存;相反,服務(wù)器將需要在每個(gè)請(qǐng)求上創(chuàng)建一個(gè)新頁(yè)面。
//price/[id]/page.js
async function getCurrentPrice(market)
const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`,
{ cache: "no-store" }
);
console.log("fetched");
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
增量靜態(tài)生成是一種生成靜態(tài)網(wǎng)站的方法,它結(jié)合了靜態(tài)網(wǎng)站生成的優(yōu)點(diǎn),能夠更新和重新生成網(wǎng)站的特定頁(yè)面或部分,而無需重建整個(gè)網(wǎng)站。增量靜態(tài)生成允許自動(dòng)增量更新,從而減少了重建整個(gè)應(yīng)用程序所需的時(shí)間,并通過僅在必要時(shí)從服務(wù)器請(qǐng)求新數(shù)據(jù),更有效地利用服務(wù)器資源。這對(duì)于國(guó)際多語(yǔ)言網(wǎng)站、企業(yè)網(wǎng)站和發(fā)布平臺(tái)網(wǎng)站非常實(shí)用。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Nextjs)
在NEXT.js上實(shí)現(xiàn)ISR的代碼與SSG演示幾乎相同。唯一的變化在于 getCurrentPrice 函數(shù)。使用fetch API并使用指定條件的選項(xiàng)從服務(wù)器獲取數(shù)據(jù),當(dāng)滿足我們定義的條件時(shí),頁(yè)面將自動(dòng)更新。在這里,我們說底層數(shù)據(jù)應(yīng)該每60秒進(jìn)行驗(yàn)證,并且UI應(yīng)該根據(jù)數(shù)據(jù)中的任何變化進(jìn)行更新。
//price/[id]/page.js
async function getCurrentPrice(market)
const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`,
{ next: { revalidate: 60 } }
);
console.log("fetched");
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
部分水合是客戶端渲染(CSR)框架中用于解決加載時(shí)間緩慢問題的一種技術(shù)。使用這種技術(shù),CSR框架將選擇性地首先渲染和水合具有交互性的網(wǎng)頁(yè)的最重要部分,而不是整個(gè)頁(yè)面。最終,當(dāng)滿足特定條件時(shí),較不重要的交互組件可以通過水合來實(shí)現(xiàn)其交互性。通過優(yōu)先處理關(guān)鍵或可見組件的水合,而推遲處理非關(guān)鍵或在折疊區(qū)域下的組件的水合,它可以更有效地利用資源,并通過優(yōu)先處理關(guān)鍵或可見組件的水合來加快初始頁(yè)面渲染速度。部分水合可以使任何具有多個(gè)交互組件的復(fù)雜CSR或SPA受益。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (React)
//pages/price.jsx
import { useParams } from "react-router-dom";
import React, { useEffect, useState, useRef, Suspense } from "react";
const Btn = React.lazy(() => import("../components/Btn"));
import getCurrentPrice from "../utils/fetchPrices";
export default function Price() {
const { id } = useParams();
const ID = id.toUpperCase();
const [marketPrices, setMarketPrices] = useState({});
const [isLoading, setIsLoading] = useState(true);
const containerRef = useRef(null);
// Wrapper component to observe if it's in the viewport
const [inViewport, setInViewport] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const [entry] = entries;
setInViewport(entry.isIntersecting);
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
if (containerRef.current) {
observer.unobserve(containerRef.current);
}
};
}, []);
function fetchMode() {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.current.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.current.classList.add("light-mode");
}
}
useEffect(() => {
fetchMode();
}, []);
useEffect(() => {
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
setMarketPrices(allPrices);
setIsLoading(false);
console.log(allPrices); // Log the fetched prices to the console
} catch (error) {
console.log(error);
setIsLoading(false);
}
}
fetchMarketPrices();
}, []);
return (
<div className="container" ref={containerRef}>
<h2>{ID}</h2>
{isLoading ? (
<p>Loading...</p>
) : Object.keys(marketPrices).length > 0 ? (
<ul>
{Object.keys(marketPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {marketPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
{inViewport ? (
// Render the interactive component only when it's in the viewport
<React.Suspense fallback={<div>Loading...</div>}>
<Btn container={containerRef} />
</React.Suspense>
) : (
// Render a placeholder or non-interactive version when not in the viewport
<div>Scroll down to see the interactive component!</div>
)}
</div>
);
}
在上面的演示中,我們代碼的交互組件 Btn 位于頁(yè)面底部,只有當(dāng)它進(jìn)入視口時(shí)才會(huì)被激活。
島嶼架構(gòu)是Astro框架開發(fā)者倡導(dǎo)的一種有前途的UI渲染模式。Web應(yīng)用程序在服務(wù)器上被劃分為多個(gè)獨(dú)立的小組件,稱為島嶼。每個(gè)島嶼負(fù)責(zé)渲染應(yīng)用程序UI的特定部分,并且它們可以獨(dú)立地進(jìn)行渲染。在服務(wù)器上被劃分為島嶼后,這些多個(gè)島嶼包被發(fā)送到瀏覽器,框架使用一種非常強(qiáng)大的部分加載形式,只有帶有交互部分的組件由JavaScript接管并啟用其交互性,而其他非交互式組件保持靜態(tài)。最常見的用例是構(gòu)建內(nèi)容豐富的網(wǎng)站。Astro是構(gòu)建專注于內(nèi)容的網(wǎng)站的不錯(cuò)選擇,例如博客、作品集和文檔網(wǎng)站。Astro的島嶼架構(gòu)模式可以幫助提高這些網(wǎng)站的性能,尤其是對(duì)于網(wǎng)絡(luò)連接較慢的用戶來說。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Astro)
---
// components/Btn.astro
---
<div>
<button class="toggle-btn"> Toggle Mode</button>
</div>
<script>
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
document.body.classList.add("dark-mode");
} else if (preferredMode === "light") {
document.body.classList.add("light-mode");
}
});
// Check the user's preferred mode on page load (optional)
function toggleMode() {
const body = document.body;
body.classList.toggle("dark-mode");
body.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
toggleBtn.addEventListener("click", () => {
toggleMode();
});
</script>
---
// pages/[coin].astro
import Layout from "../layouts/Layout.astro";
import Btn from "../components/Btn.astro";
export async function getStaticPaths() {
return [
{ params: { coin: "btc" } },
{ params: { coin: "eth" } },
{ params: { coin: "xrp" } },
{ params: { coin: "ada" } },
];
}
const { coin } = Astro.params;
async function getCurrentPrice(market) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
return allPrices;
// Log the fetched prices to the console
} catch (error) {
console.log(error);
return null;
}
}
const allPrices = await fetchMarketPrices();
---
<Layout title="Welcome to Astro.">
<div>
<h2>{coin}</h2>
{
allPrices && Object.keys(allPrices).length > 0 ? (
<ul>
{Object.keys(allPrices).map((exchange) => (
<li>
{exchange}: {allPrices[exchange][0][coin]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)
}
<Btn />
</div>
</Layout>
---
//pages/index.astro
import Layout from "../layouts/Layout.astro";
---
<Layout title="Welcome to Astro.">
<main>
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<a href="./btc">Bitcoin</a>
</li>
<li>
<a href="./eth">Ethereum</a>
</li>
<li>
<a href="./xrp">Ripple</a>
</li>
<li>
<a href="./ada">Cardano</a>
</li>
</ol>
</div>
</main>
</Layout>
Qwik是一個(gè)以重用性為核心的全新渲染方式的元框架。該渲染模式基于兩種主要策略:
在服務(wù)器上序列化應(yīng)用程序和框架的執(zhí)行狀態(tài),并在客戶端上恢復(fù)。
水合
這段來自Qwik文檔的摘錄很好地介紹了可重用性。
監(jiān)聽器 - 在DOM節(jié)點(diǎn)上定位事件監(jiān)聽器并安裝它們,使應(yīng)用程序具有交互性。組件樹 - 構(gòu)建表示應(yīng)用程序組件樹的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。應(yīng)用程序狀態(tài) - 恢復(fù)在服務(wù)器上存儲(chǔ)的任何獲取或保存的數(shù)據(jù)。總體而言,這被稱為水合。所有當(dāng)前的框架都需要這一步驟來使應(yīng)用程序具有交互性。
水合作用之所以昂貴,有兩個(gè)原因:
在序列化中, Qwik 顯示了在服務(wù)器上開始構(gòu)建網(wǎng)頁(yè)的能力,并在從服務(wù)器發(fā)送捆綁包后繼續(xù)在客戶端上執(zhí)行構(gòu)建,節(jié)省了其他框架重新初始化客戶端的時(shí)間。
就懶加載而言, Qwik 將通過極度懶加載來確保Web應(yīng)用程序盡快加載,只加載必要的JavaScript捆綁包,并在需要時(shí)加載其余部分。 Qwik 可以在開箱即用的情況下完成所有這些操作,無需進(jìn)行太多開發(fā)者配置。
這適用于復(fù)雜的博客應(yīng)用和企業(yè)網(wǎng)站的發(fā)布。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Qwik)
//components/Btn.tsx
import { $, component$, useStore, useVisibleTask$ } from "@builder.io/qwik";
export default component$(({ container }) => {
const store = useStore({
mode: true,
});
useVisibleTask$(({ track }) => {
// track changes in store.count
track(() => store.mode);
container.value.classList.toggle("light-mode");
container.value.classList.toggle("dark-mode");
// Save the user's preference in localStorage (optional)
const currentMode = container.value.classList.contains("dark-mode")
? "dark"
: "light";
localStorage.setItem("mode", currentMode);
console.log(container.value.classList);
});
return (
<div>
<button
class="toggle-btn"
onClick$={$(() => {
store.mode = !store.mode;
})}
>
Toggle Mode
</button>
</div>
);
});
//components/Client.tsx
import { component$, useVisibleTask$, useSignal } from "@builder.io/qwik";
import { useLocation } from "@builder.io/qwik-city";
import Btn from "./Btn";
export default component$(({ allPrices }) => {
const loc = useLocation();
const ID = loc.params.coin.toUpperCase();
const containerRef = useSignal<Element>();
useVisibleTask$(() => {
if (containerRef.value) {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.value.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.value.classList.add("light-mode");
}
}
});
return (
<div class="container" ref={containerRef}>
<h2>{ID}</h2>
{Object.keys(allPrices).length > 0 ? (
<ul>
{Object.keys(allPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {allPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
<Btn container={containerRef} />
</div>
);
});
export const head: DocumentHead = {
title: "Qwik",
};
// routes/price/[coin]/index.tsx
import { component$, useVisibleTask$, useSignal } from "@builder.io/qwik";
import { type DocumentHead } from "@builder.io/qwik-city";
import Btn from "../../../components/Btn";
import Client from "../../../components/Client";
export default component$(async () => {
async function getCurrentPrice(market) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
return allPrices;
// Log the fetched prices to the console
} catch (error) {
console.log(error);
}
}
const allPrices = await fetchMarketPrices();
return (
<div>
{allPrices && Object.keys(allPrices).length > 0 ? (
<Client allPrices={allPrices} />
) : (
<p>No data available.</p>
)}
</div>
);
});
export const head: DocumentHead = {
title: "Qwik Flower",
};
//routes/index.tsx
import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { Link } from "@builder.io/qwik-city";
export default component$(() => {
return (
<>
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<Link href="./price/btc">Bitcoin </Link>
</li>
<li>
<Link href="./price/eth">Ethereum </Link>
</li>
<li>
<Link href="./price/xrp">Ripple </Link>
</li>
<li>
<Link href="./price/ada">Cardano </Link>
</li>
</ol>
</div>
</>
);
});
export const head: DocumentHead = {
title: "Welcome to Qwik",
meta: [
{
name: "description",
content: "Qwik site description",
},
],
};
流式服務(wù)器端渲染(Streaming SSR)是一種相對(duì)較新的用于渲染W(wǎng)eb應(yīng)用程序的技術(shù)。流式SSR通過將應(yīng)用程序的用戶界面分塊在服務(wù)器上進(jìn)行渲染。每個(gè)塊在準(zhǔn)備好后立即進(jìn)行渲染,然后流式傳輸?shù)娇蛻舳恕?蛻舳嗽诮邮盏綁K時(shí)顯示和填充它們。這意味著客戶端在應(yīng)用程序完全渲染之前就可以開始與其進(jìn)行交互,無需等待。這提高了Web應(yīng)用程序的初始加載時(shí)間,尤其適用于大型和復(fù)雜的應(yīng)用程序。流式SSR最適用于大規(guī)模應(yīng)用,如電子商務(wù)和交易應(yīng)用程序。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo
很遺憾,我們的應(yīng)用程序不夠復(fù)雜,無法提供一個(gè)合適的例子。
在本文中,我們探討了當(dāng)今前端網(wǎng)頁(yè)開發(fā)中最流行的十種UI渲染模式。在這個(gè)過程中,我們討論了每種方法的優(yōu)勢(shì)、局限性和權(quán)衡。然而,重要的是要注意,沒有一種適用于所有情況的渲染模式或普遍完美的渲染方法。每個(gè)應(yīng)用都有其獨(dú)特的需求和特點(diǎn),因此選擇合適的渲染模式對(duì)于開發(fā)過程的成功至關(guān)重要。
由于文章內(nèi)容篇幅有限,今天的內(nèi)容就分享到這里,文章結(jié)尾,我想提醒您,文章的創(chuàng)作不易,如果您喜歡我的分享,請(qǐng)別忘了點(diǎn)贊和轉(zhuǎn)發(fā),讓更多有需要的人看到。同時(shí),如果您想獲取更多前端技術(shù)的知識(shí),歡迎關(guān)注我,您的支持將是我分享最大的動(dòng)力。我會(huì)持續(xù)輸出更多內(nèi)容,敬請(qǐng)期待。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。