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 综合久久精品,久久亚洲综合,91香蕉国产亚洲一区二区三区

          整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          Express 的使用

          下內(nèi)容,基于 Express 4.x 版本

          Node.js 的 Express

          Express 估計(jì)是那種你第一次接觸,就會(huì)喜歡上用它的框架。因?yàn)樗娴姆浅:?jiǎn)單,直接。

          在當(dāng)前版本上,一共才這么幾個(gè)文件:

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

          這種程度,說它是一個(gè)“框架”可能都有些過了,幾乎都是工具性質(zhì)的實(shí)現(xiàn),只限于 Web 層。

          當(dāng)然,直接了當(dāng)?shù)貙?shí)現(xiàn)了 Web 層的基本功能,是得益于 Node.js 本身的 API 中,就提供了 net 和 http 這兩層, Express 對(duì) http 的方法包裝一下即可。

          不過,本身功能簡(jiǎn)單的東西,在 package.json 中卻有好長(zhǎng)一串 dependencies 列表。

          Hello World

          在跑 Express 前,你可能需要初始化一個(gè) npm 項(xiàng)目,然后再使用 npm 安裝 Express:

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

          新建一個(gè) app.js :

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

          調(diào)試信息是通過環(huán)境變量 DEBUG 控制的:

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

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

          應(yīng)用 Application

          Application 是一個(gè)上層統(tǒng)籌的概念,整合“請(qǐng)求-響應(yīng)”流程。 express() 的調(diào)用會(huì)返回一個(gè) application ,一個(gè)項(xiàng)目中,有多個(gè) app 是沒問題的:

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

          多個(gè) app 的另一個(gè)用法,是直接把某個(gè) path 映射到整個(gè) app :

          const express = require('express');
          const app = express();
          app.all('/', (req, res) => {
           res.send('ok');
          });
          const app2 = express();
          app2.get('/xx', (req, res, next) => res.send('in app2') )
          app.use('/2', app2)
          app.listen(8888);
          

          這樣,當(dāng)訪問 /2/xx 時(shí),就會(huì)看到 in app2 的響應(yīng)。

          前面說了 app 實(shí)際上是一個(gè)上層調(diào)度的角色,在看后面的內(nèi)容之前,先說一下 Express 的特點(diǎn),整體上來說,它的結(jié)構(gòu)基本上是“回調(diào)函數(shù)串行”,無論是 app ,或者 route, handle, middleware這些不同的概念,它們的形式,基本是一致的,就是 (res, req, next) => {} ,串行的流程依賴 next() 的顯式調(diào)用。

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

          路由 - Handler 映射

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

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

          const express = require('express');
          const app = express();
          app.all('/', (req, res, next) => {
           res.send('1 ');
           console.log('here');
           next();
          });
          app.get('/', (req, res, next) => {
           res.send('2 ');
           console.log('get');
           next();
          });
          app.listen(8888);
          

          對(duì)于上面的代碼,因?yàn)橹貜?fù)調(diào)用 send() 會(huì)報(bào)錯(cuò)。

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

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

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

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

          const express = require('express');
          const app = express();
          app.route('/:id').all( (req, res, next) => {
           console.log('all');
           next();
          }).get( (req, res, next) => {
           res.send('get');
           next()
          }).all( (req, res, next) => {
           console.log('tail');
          });
          app.route('/').all( (req, res) => {res.send('ok')});
          app.param('id', (req, res, next, value) => {
           console.log('param', value);
           next();
          });
          app.listen(8888);
          

          app.params 中的對(duì)應(yīng)函數(shù)會(huì)先行執(zhí)行,并且,記得顯式調(diào)用 next() 。

          Middleware

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

          const express = require('express');
          const app = express();
          app.all(/.*/, (req, res, next) => {
           console.log('reg');
           next();
          });
          app.all('/', (req, res, next) => {
           console.log('pre');
           next();
          });
          app.use((req, res, next) => {
           console.log('use');
           next();
          });
          app.all('/', (req, res, next) => {
           console.log('all');
           res.send('/ here');
           next();
          });
          app.use((req, res, next) => {
           console.log('use2');
           next();
          });
          app.listen(8888);
          

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

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

          const express = require('express');
          const app = express();
          const router = express.Router();
          app.all('/', (req, res) => {
           res.send({a: '123'});
          });
          router.all('/a', (req, res) => {
           res.send('hello');
          });
          app.use('/route', router);
          app.listen(8888);
          

          功能開關(guān),變量容器

          app.set() 和 app.get() 可以用來保存 app 級(jí)別的變量(對(duì), app.get() 還和 GET 方法的實(shí)現(xiàn)名字上還沖突了):

          const express = require('express');
          const app = express();
          app.all('/', (req, res) => {
           app.set('title', '標(biāo)題123');
           res.send('ok');
          });
          app.all('/t', (req, res) => {
           res.send(app.get('title'));
          });
          app.listen(8888);
          

          上面的代碼,啟動(dòng)之后直接訪問 /t 是沒有內(nèi)容的,先訪問 / 再訪問 /t 才可以看到內(nèi)容。

          對(duì)于變量名, Express 預(yù)置了一些,這些變量的值,可以叫 settings ,它們同時(shí)也影響整個(gè)應(yīng)用的行為:

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

          具體的作用,可以參考 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 。

          請(qǐng)求 Request

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

          GET 參數(shù)

          使用 req.query 可以獲取 GET 參數(shù):

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

          請(qǐng)求:

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

          POST 參數(shù)

          POST 參數(shù)的獲取,使用 req.body ,但是,在此之前,需要專門掛一個(gè) Middleware , req.body才有值:

          const express = require('express');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.all('/', (req, res) => {
           console.log(req.body);
           res.send('ok');
          });
          app.listen(8888);
          # -*- coding: utf-8 -*-
          import 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 的值。

          響應(yīng) Response

          Express 的響應(yīng),針對(duì)不同類型,本身就提供了幾種包裝了。

          普通響應(yīng)

          使用 res.send 處理確定性的內(nèi)容響應(yīng):

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

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

          模板渲染

          模板需要預(yù)先配置,在 Request 那節(jié)已經(jīng)介紹過了。

          const process = require('process');
          const express = require('express');
          const cookieParser = require('cookie-parser');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.use(express.json());
          app.use(cookieParser())
          app.set('trust proxy', false);
          app.set('views', process.cwd() + '/template');
          app.set('view engine', 'html');
          app.engine('html', (path, options, callback) => {
           callback(false, '<h1>Hello</h1>');
          });
          app.all('/', (req, res) => {
           res.render('index', {}, (err, html) => {
           res.send(html);
           });
          });
          app.listen(8888);
          

          這里有一個(gè)坑點(diǎn),就是必須在對(duì)應(yīng)的目錄下,有對(duì)應(yīng)的文件存在,比如上面例子的 template/index.html ,那么 app.engine() 中的回調(diào)函數(shù)才會(huì)執(zhí)行。都自定義回調(diào)函數(shù)了,這個(gè)限制沒有任何意義, path, options 傳入就好了,至于是不是要通過文件系統(tǒng)讀取內(nèi)容,怎么讀取,又有什么關(guān)系呢。

          Cookie

          res.cookie 來處理 Cookie 頭:

          const process = require('process');
          const express = require('express');
          const cookieParser = require('cookie-parser');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.use(express.json());
          app.use(cookieParser("key"))
          app.set('trust proxy', false);
          app.set('views', process.cwd() + '/template');
          app.set('view engine', 'html');
          app.engine('html', (path, options, callback) => {
           callback(false, '<h1>Hello</h1>');
          });
          app.all('/', (req, res) => {
           res.render('index', {}, (err, html) => {
           console.log('cookie', req.signedCookies.a);
           res.cookie('a', '123', {signed: true});
           res.cookie('b', '123', {signed: true});
           res.clearCookie('b');
           res.send(html);
           });
          });
          app.listen(8888);
          

          請(qǐng)求:

          # -*- coding: utf-8 -*-
          import requests
          import 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):

          • app.use(cookieParser("key")) 這里必須要有一個(gè)字符串做 key ,才可以正確使用簽名的 cookie 。
          • clearCookie() 仍然是用“設(shè)置過期”的方式來達(dá)到刪除目的,cookie() 和 clearCookie() 并不會(huì)整合,會(huì)寫兩組 b=xx 進(jìn)頭。
          • res.send() 會(huì)在連接上完成一個(gè)響應(yīng),所以,與頭相關(guān)的操作,都必須放在 res.send() 前面。

          頭和其它

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

          const process = require('process');
          const express = require('express');
          const cookieParser = require('cookie-parser');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.use(express.json());
          app.use(cookieParser("key"))
          app.set('trust proxy', false);
          app.set('views', process.cwd() + '/template');
          app.set('view engine', 'html');
          app.engine('html', (path, options, callback) => {
           callback(false, '<h1>Hello</h1>');
          });
          app.all('/', (req, res) => {
           res.render('index', {}, (err, html) => {
           res.set('X-ME', 'zys');
           //res.redirect('back');
           //res.redirect('http://www.zouyesheng.com');
           res.status(404);
           res.end();
           });
          });
          app.listen(8888);
          

          res.redirect('back') 會(huì)自動(dòng)獲取 referer 頭作為 Location 的值,使用這個(gè)時(shí),注意 referer為空的情況,會(huì)造成循環(huán)重復(fù)重定向的后果。

          Chunk 響應(yīng)

          Chunk 方式的響應(yīng),指連接建立之后,服務(wù)端的響應(yīng)內(nèi)容是不定長(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() 吧。

          我會(huì)怎么用 Express

          先說一下,我自己,目前在 Express 運(yùn)用方面,并沒有太多的時(shí)間和復(fù)雜場(chǎng)景的積累。

          即使這樣,作為技術(shù)上相對(duì)傳統(tǒng)的人,我會(huì)以我以往的 web 開發(fā)的套路,來使用 Express 。

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

          首先,這會(huì)使 path 定義散落在各處,方便了開發(fā),麻煩了維護(hù)。

          其次,把 path 和具體實(shí)現(xiàn)邏輯 callback 綁在一起,我覺得也是反思維的。至少,對(duì)于我個(gè)人來說,開發(fā)的過程,先是想如何實(shí)現(xiàn)一個(gè) handler ,最后,再是考慮要把這個(gè) handle 與哪些 path 綁定。

          再次,單純的 callback 缺乏層次感,用 app.use(path, callback) 這種來處理共用邏輯的方式,我覺得完全是扯談。共用邏輯是代碼之間本身實(shí)現(xiàn)上的關(guān)系,硬生生跟網(wǎng)絡(luò)應(yīng)用層 HTTP 協(xié)議的 path 概念抽上關(guān)系,何必呢。當(dāng)然,對(duì)于 callback 的組織,用純函數(shù)來串是可以的,不過我在這方面并沒有太多經(jīng)驗(yàn),所以,我還是選擇用類繼承的方式來作層次化的實(shí)現(xiàn)。

          我自己要用 Express ,大概會(huì)這樣組件項(xiàng)目代碼(不包括關(guān)系數(shù)據(jù)庫(kù)的 Model 抽象如何組織這部分):

          ./
          ├── config.conf
          ├── config.js
          ├── handler
          │ ├── base.js
          │ └── index.js
          ├── middleware.js
          ├── server.js
          └── url.js
          
          • config.conf 是 ini 格式的項(xiàng)目配置。
          • config.js 處理配置,包括日志,數(shù)據(jù)庫(kù)連接等。
          • middleware.js 是針對(duì)整體流程的擴(kuò)展機(jī)制,比如,給每個(gè)請(qǐng)求加一個(gè) UUID ,每個(gè)請(qǐng)求都記錄一條日志,日志內(nèi)容有請(qǐng)求的細(xì)節(jié)及本次請(qǐng)求的處理時(shí)間。
          • server.js 是主要的服務(wù)啟動(dòng)邏輯,整合各種資源,命令行參數(shù) port 控制監(jiān)聽哪個(gè)端口。不需要考慮多進(jìn)程問題,(正式部署時(shí) nginx 反向代理到多個(gè)應(yīng)用實(shí)例,多個(gè)實(shí)例及其它資源統(tǒng)一用 supervisor 管理)。
          • url.js 定義路徑與 handler 的映射關(guān)系。
          • handler ,具體邏輯實(shí)現(xiàn)的地方,所有 handler 都從 BaseHandler 繼承。

          BaseHandler 的實(shí)現(xiàn):

          class BaseHandler {
           constructor(req, res, next){
           this.req = req;
           this.res = res;
           this._next = next;
           this._finised = false;
           }
           run(){
           this.prepare();
           if(!this._finised){
           if(this.req.method === 'GET'){
           this.get();
           return;
           }
           if(this.req.method === 'POST'){
           this.post();
           return;
           }
           throw Error(this.req.method + ' this method had not been implemented');
           }
           }
           prepare(){}
           get(){
           throw Error('this method had not been implemented');
           }
           post(){
           throw Error('this method had not been implemented');
           }
           render(template, values){
           this.res.render(template, values, (err, html) => {
           this.finish(html);
           });
           }
           write(content){
           if(Object.prototype.toString.call(content) === '[object Object]'){
           this.res.write(JSON.stringify(content));
           } else {
           this.res.write(content);
           }
           }
           finish(content){
           if(this._finised){
           throw Error('this handle was finished');
           }
           this.res.send(content);
           this._finised = true;
           if(this._next){ this._next() }
           }
          }
          module.exports = {BaseHandler};
          if(module === require.main){
           const express = require('express');
           const app = express();
           app.all('/', (req, res, next) => new BaseHandler(req, res, next).run() );
           app.listen(8888);
          }
          

          要用的話,比如 index.js :

          const BaseHandler = require('./base').BaseHandler;
          class IndexHandler extends BaseHandler {
           get(){
           this.finish({a: 'hello'});
           }
          }
          module.exports = {IndexHandler};
          

          url.js 中的樣子:

          const IndexHandler = require('./handler/index').IndexHandler;
          const Handlers = [];
          Handlers.push(['/', IndexHandler]);
          module.exports = {Handlers};
          

          日志

          后面這幾部分,都不屬于 Express 本身的內(nèi)容了,只是我個(gè)人,隨便想到的一些東西。

          找一個(gè)日志模塊的實(shí)現(xiàn),功能上,就看這么幾點(diǎn):

          • 標(biāo)準(zhǔn)的級(jí)別: DEBUG,INFO,WARN, ERROR 這些。
          • 層級(jí)的多個(gè) logger 。
          • 可注冊(cè)式的多種 Handler 實(shí)現(xiàn),比如文件系統(tǒng),操作系統(tǒng)的 rsyslog ,標(biāo)準(zhǔn)輸出,等。
          • 格式定義,一般都帶上時(shí)間和代碼位置。

          Node.js 中,大概就是 log4js 了, 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):

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

          ini 格式配置

          json 作配置文件,功能上沒問題,但是對(duì)人為修改是不友好的。所以,個(gè)人還是喜歡用 ini 格式作項(xiàng)目的環(huán)境配置文件。

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

          const s = `
          [database]
          host = 127.0.0.1
          port = 5432
          user = dbuser
          password = dbpassword
          database = use_this_database
          [paths.default]
          datadir = /var/lib/data
          array[] = first value
          array[] = second value
          array[] = third value
          `
          const fs = require('fs');
          const ini = require('ini');
          const config = ini.parse(s);
          console.log(config);
          

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

          WebSocket

          Node.js 中的 WebSocket 實(shí)現(xiàn),可以使用 ws 模塊, 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)
          

          其它

          • 命令行解析, yargs ,https://github.com/yargs/yargs
          • UUID, uuid , https://github.com/kelektiv/node-uuid

          作者:zephyr

          ode.js GET/POST請(qǐng)求

          視頻地址: 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);

          Node.js Web 模塊


          什么是 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ù): requestresponse 對(duì)象來處理請(qǐng)求和響應(yīng)的數(shù)據(jù)。

          app.get('/', function (req, res) {
           // --})

          requestresponse 對(duì)象的具體介紹:

          Request 對(duì)象 - request 對(duì)象表示 HTTP 請(qǐng)求,包含了請(qǐng)求查詢字符串,參數(shù),內(nèi)容,HTTP 頭部等屬性。常見屬性有:

          1. req.app:當(dāng)callback為外部文件時(shí),用req.app訪問express的實(shí)例

          2. req.baseUrl:獲取路由當(dāng)前安裝的URL路徑

          3. req.body / req.cookies:獲得「請(qǐng)求主體」/ Cookies

          4. req.fresh / req.stale:判斷請(qǐng)求是否還「新鮮」

          5. req.hostname / req.ip:獲取主機(jī)名和IP地址

          6. req.originalUrl:獲取原始請(qǐng)求URL

          7. req.params:獲取路由的parameters

          8. req.path:獲取請(qǐng)求路徑

          9. req.protocol:獲取協(xié)議類型

          10. req.query:獲取URL的查詢參數(shù)串

          11. req.route:獲取當(dāng)前匹配的路由

          12. req.subdomains:獲取子域名

          13. req.accepts():檢查可接受的請(qǐng)求的文檔類型

          14. req.acceptsCharsets / req.acceptsEncodings / req.acceptsLanguages:返回指定字符集的第一個(gè)可接受字符編碼

          15. req.get():獲取指定的HTTP請(qǐng)求頭

          16. req.is():判斷請(qǐng)求頭Content-Type的MIME類型

          Response 對(duì)象 - response 對(duì)象表示 HTTP 響應(yīng),即在接收到請(qǐng)求時(shí)向客戶端發(fā)送的 HTTP 響應(yīng)數(shù)據(jù)。常見屬性有:

          1. res.app:同req.app一樣

          2. res.append():追加指定HTTP頭

          3. res.set()在res.append()后將重置之前設(shè)置的頭

          4. res.cookie(name,value [,option]):設(shè)置Cookie

          5. opition: domain / expires / httpOnly / maxAge / path / secure / signed

          6. res.clearCookie():清除Cookie

          7. res.download():傳送指定路徑的文件

          8. res.get():返回指定的HTTP頭

          9. res.json():傳送JSON響應(yīng)

          10. res.jsonp():傳送JSONP響應(yīng)

          11. res.location():只設(shè)置響應(yīng)的Location HTTP頭,不設(shè)置狀態(tài)碼或者close response

          12. res.redirect():設(shè)置響應(yīng)的Location HTTP頭,并且設(shè)置狀態(tài)碼302

          13. res.send():傳送HTTP響應(yīng)

          14. res.sendFile(path [,options] [,fn]):傳送指定路徑的文件 -會(huì)自動(dòng)根據(jù)文件extension設(shè)定Content-Type

          15. res.set():設(shè)置HTTP頭,傳入object可以一次設(shè)置多個(gè)頭

          16. res.status():設(shè)置HTTP狀態(tài)碼

          17. 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ì):

          • 對(duì)于當(dāng)今網(wǎng)頁(yè)開發(fā)中最常見的渲染模式有基本的了解
          • 了解不同渲染模式的優(yōu)勢(shì)和劣勢(shì)
          • 了解在你的下一個(gè)大項(xiàng)目中使用哪種渲染模式和框架

          什么是UI渲染模式?

          在前端開發(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)。

          在本文中,我們將介紹下面列出的前十種渲染模式:

          • 1、靜態(tài)網(wǎng)站(Static Site)
          • 2、多頁(yè)面應(yīng)用(Multi-Page Applications(MPA))
          • 3、單頁(yè)應(yīng)用程序(Single Page Applications (with Client Side Rendering CSR))
          • 4、服務(wù)器端渲染(erver Side Rendering (SSR))
          • 5、靜態(tài)網(wǎng)站生成(Static Site Generation (SSG))
          • 6、增量靜態(tài)生成(Incremental Static Generation (ISG))
          • 7、部分水合(Partial Hydration)
          • 8、Island Architectur
          • 9、Resumability
          • 10、 SSR

          在每個(gè)案例中,我們將研究渲染模式的概念、優(yōu)點(diǎn)和缺點(diǎn)、使用案例、相關(guān)的框架,并提供一個(gè)簡(jiǎn)單的代碼示例來闡明觀點(diǎn)。

          代碼示例

          • 第一頁(yè)將顯示可用的貨幣類型
          • 第二頁(yè)將顯示從Coingecko API獲取的特定幣種在不同交易所的價(jià)格。
          • 第二頁(yè)還將提供深色和淺色模式。
          • 各種框架的實(shí)施可能會(huì)有輕微的差異。

          所有示例的全局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)站

          靜態(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)

          • 非常簡(jiǎn)單
          • 快速
          • 廉價(jià)(無服務(wù)器)
          • SEO友好

          缺點(diǎn)

          • 不適用于數(shù)據(jù)頻繁變動(dòng)的情況(動(dòng)態(tài)數(shù)據(jù))
          • 不適用于互動(dòng)應(yīng)用程序
          • 沒有直接的數(shù)據(jù)庫(kù)連接
          • 當(dāng)數(shù)據(jù)發(fā)生變化時(shí),需要手動(dòng)更新和重新上傳

          相關(guān)框架

          • Hugo
          • Jekyll
          • HTML/CSS/純JavaScript(無框架)

          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)編寫。

          多頁(yè)面應(yīng)用程序(MPAs)

          這種渲染模式是為了處理我們網(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)

          • 簡(jiǎn)單直接
          • 處理動(dòng)態(tài)數(shù)據(jù)非常出色
          • SEO友好
          • 良好的開發(fā)者體驗(yàn)
          • 高度可擴(kuò)展的

          缺點(diǎn)

          • 適度支持用戶界面的交互性
          • 由于多次重新加載而導(dǎo)致用戶體驗(yàn)差
          • 昂貴的(需要服務(wù)器)

          相關(guān)框架

          • Express 和 EJS (node.js)
          • Flask (Python)
          • Spring boot (java)

          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)

          單頁(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)

          • 高度互動(dòng)
          • 在瀏覽多個(gè)頁(yè)面時(shí),用戶體驗(yàn)無縫銜接
          • 手機(jī)友好

          缺點(diǎn)

          • 由于JavaScript捆綁包過大,加載時(shí)間較慢
          • SEO能力差
          • 由于客戶端上的代碼執(zhí)行,存在高安全風(fēng)險(xiǎn)
          • 可擴(kuò)展性差

          相關(guān)框架

          • React
          • Angular
          • Vue

          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)

          靜態(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)

          • SEO友好
          • 快速加載頁(yè)面
          • 高性能
          • 提高安全性(由于代碼既不在客戶端上運(yùn)行也不在服務(wù)器上運(yùn)行)

          缺點(diǎn)

          • 有限互動(dòng)
          • 數(shù)據(jù)更改后需要重新構(gòu)建和重新上傳

          相關(guān)框架

          • Nextjs (默認(rèn)情況下)
          • Gatsby
          • Hugo
          • Jekyll

          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)

          服務(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)

          • 適度互動(dòng)
          • SEO友好
          • 快速加載時(shí)間
          • 對(duì)動(dòng)態(tài)數(shù)據(jù)的良好支持

          缺點(diǎn)

          • 復(fù)雜的實(shí)施
          • 成本(需要服務(wù)器)

          相關(guān)框架

          • Next.js
          • Nuxt.js

          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)生成(ISG)

          增量靜態(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)

          • 靜態(tài)網(wǎng)站的實(shí)時(shí)自動(dòng)更新支持
          • 性價(jià)比高
          • SEO友好
          • 良好的性能和可擴(kuò)展性

          缺點(diǎn)

          • 實(shí)施中的復(fù)雜性
          • 不適用于高度動(dòng)態(tài)的數(shù)據(jù)應(yīng)用

          相關(guān)框架

          • Next.js
          • Nuxt.js

          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)

          • 由于減少了初始的JavaScript捆綁包,加載時(shí)間更快
          • 性能提升了
          • 優(yōu)化的搜索引擎優(yōu)化
          • 資源效率

          缺點(diǎn)

          • 增加的復(fù)雜性和代碼
          • 不一致的用戶界面可能性

          相關(guān)框架

          • React
          • Vue

          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ì)被激活。

          Island Architecture(Astro)

          島嶼架構(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)

          • 性能(當(dāng)今最快的框架之一)
          • 更小的捆綁尺寸
          • 易學(xué)易懂,易于維護(hù)
          • 良好的SEO表現(xiàn)
          • 良好的開發(fā)者體驗(yàn)

          缺點(diǎn)

          • 有限互動(dòng)
          • 由于組件數(shù)量極多,導(dǎo)致調(diào)試?yán)щy

          相關(guān)框架

          • Astro

          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>

          Resumability (withQwik)

          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è)原因:

          • 框架必須下載與當(dāng)前頁(yè)面相關(guān)的所有組件代碼。
          • 框架必須執(zhí)行與頁(yè)面上的組件相關(guān)聯(lián)的模板,以重建監(jiān)聽器位置和內(nèi)部組件樹。

          在序列化中, 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)

          • 由于可恢復(fù)性而對(duì)網(wǎng)絡(luò)中斷具有彈性
          • 快速加載時(shí)間
          • 友好的搜索引擎優(yōu)化

          缺點(diǎn)

          • 復(fù)雜的實(shí)施
          • 更高的帶寬使用

          相關(guān)框架

          • Qwik

          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)

          流式服務(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)

          • Performance
          • 實(shí)時(shí)更新

          缺點(diǎn)

          • 復(fù)雜性

          相關(guān)框架

          • Next.js
          • Nuxt.js

          Demo

          很遺憾,我們的應(yīng)用程序不夠復(fù)雜,無法提供一個(gè)合適的例子。

          結(jié)束

          在本文中,我們探討了當(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)期待。

          參考文獻(xiàn)

          • 有關(guān)可恢復(fù)性的更多信息,請(qǐng)參閱文檔:https://qwik.builder.io/docs/concepts/think-qwik/ https://qwik.builder.io/docs/concepts/resumable/
          • 有關(guān)島嶼架構(gòu)的更多信息,請(qǐng)參閱文檔 https://docs.astro.build/en/getting-started/
          • 關(guān)于渲染的簡(jiǎn)要文檔 https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic-rendering
          • 流式服務(wù)器端渲染(SSR) https://blog.logrocket.com/streaming-ssr-with-react-18/

          主站蜘蛛池模板: 99久久精品费精品国产一区二区| 美女免费视频一区二区| 国产精品久久亚洲一区二区 | 99精品国产一区二区三区2021 | 日本一区二区三区在线视频| 亚洲欧洲一区二区| 高清精品一区二区三区一区| 国产日韩AV免费无码一区二区 | 国产精品毛片VA一区二区三区| 精品一区二区久久久久久久网站| 国产成人欧美一区二区三区 | 国产精品主播一区二区| 伦理一区二区三区| 国产伦精品一区二区| 国产午夜精品一区理论片| 一区二区中文字幕在线观看| 久久久91精品国产一区二区三区 | 欧美日韩精品一区二区在线视频| 国产成人无码一区二区在线播放| 亚洲AV无码一区二三区| 少妇精品无码一区二区三区 | 国模大尺度视频一区二区| 欧美日韩精品一区二区在线观看| 91精品乱码一区二区三区| 国产高清一区二区三区| 中文字幕日韩欧美一区二区三区| 精品深夜AV无码一区二区| 国产91精品一区| 视频一区二区在线观看| 一区二区三区在线播放| 亚洲一区精品伊人久久伊人| 日韩人妻无码一区二区三区久久99| 中文字幕一区二区三区永久| 国产无线乱码一区二三区 | 国产成人精品一区二区A片带套 | 国产精品免费综合一区视频| 在线观看一区二区三区视频| 久久精品国产一区二区三| 日韩精品中文字幕无码一区| 一区二区网站在线观看| 国产剧情国产精品一区|