整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Express 的使用

          下內容,基于 Express 4.x 版本

          Node.js 的 Express

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

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

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

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

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

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

          Hello World

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

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

          新建一個 app.js :

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

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

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

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

          應用 Application

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

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

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

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

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

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

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

          路由 - Handler 映射

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

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

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

          對于上面的代碼,因為重復調用 send() 會報錯。

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

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

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

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

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

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

          Middleware

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

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

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

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

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

          功能開關,變量容器

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

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

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

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

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

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

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

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

          模板引擎

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

          const process = require('process');
          const express = require('express');
          const app = express();
          app.set('views', process.cwd() + '/template');
          app.engine('t2t', (path, options, callback) => {
           console.log(path, options);
           callback(false, '123');
          });
          app.all('/', (req, res) => {
           res.render('demo.t2t', {title: "標題"}, (err, html) => {
           res.send(html)
           });
          });
          app.listen(8888);
          

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

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

          端口監聽

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

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

          注意, host 是第二個參數。

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

          請求 Request

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

          GET 參數

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

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

          請求:

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

          POST 參數

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

          const express = require('express');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.all('/', (req, res) => {
           console.log(req.body);
           res.send('ok');
          });
          app.listen(8888);
          # -*- coding: utf-8 -*-
          import requests
          requests.post('http://localhost:8888', data={"a": '中文'})
          

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

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

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

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

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

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

          Cookie

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

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

          請求:

          # -*- coding: utf-8 -*-
          import requests
          import json
          requests.post('http://localhost:8888', data={'a': '中文'},
           headers={'Cookie': 'a=1'})
          

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

          來源 IP

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

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

          請求:

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

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

          響應 Response

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

          普通響應

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

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

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

          模板渲染

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

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

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

          Cookie

          res.cookie 來處理 Cookie 頭:

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

          請求:

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

          注意三點:

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

          頭和其它

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

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

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

          Chunk 響應

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

          const process = require('process');
          const express = require('express');
          const cookieParser = require('cookie-parser');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.use(express.json());
          app.use(cookieParser("key"))
          app.set('trust proxy', false);
          app.set('views', process.cwd() + '/template');
          app.set('view engine', 'html');
          app.engine('html', (path, options, callback) => {
           callback(false, '<h1>Hello</h1>');
          });
          app.all('/', (req, res) => {
           const f = () => {
           const t = new Date().getTime() + '\n';
           res.write(t);
           console.log(t);
           setTimeout(f, 1000);
           }
           setTimeout(f, 1000);
          });
          app.listen(8888);
          

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

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

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

          zys@zys-alibaba:/home/zys/temp >>> telnet localhost 8888
          Trying 127.0.0.1...
          Connected to localhost.
          Escape character is '^]'.
          GET / HTTP/1.1
          Host: localhost
          HTTP/1.1 200 OK
          X-Powered-By: Express
          Date: Thu, 20 Jun 2019 08:11:40 GMT
          Connection: keep-alive
          Transfer-Encoding: chunked
          e
          1561018300451
          e
          1561018301454
          e
          1561018302456
          e
          1561018303457
          e
          1561018304458
          e
          1561018305460
          e
          1561018306460
          

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

          Tornado 中的類似實現是:

          # -*- coding: utf-8 -*-
          import tornado.ioloop
          import tornado.web
          import tornado.gen
          import time
          class MainHandler(tornado.web.RequestHandler):
           @tornado.gen.coroutine
           def get(self):
           while True:
           yield tornado.gen.sleep(1)
           s = time.time()
           self.write(str(s))
           print(s)
           yield self.flush()
          def make_app():
           return tornado.web.Application([
           (r"/", MainHandler),
           ])
          if __name__ == "__main__":
           app = make_app()
           app.listen(8888)
           tornado.ioloop.IOLoop.current().start()
          

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

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

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

          const process = require('process');
          const express = require('express');
          const cookieParser = require('cookie-parser');
          const app = express();
          app.use(express.urlencoded({ extended: true }));
          app.use(express.json());
          app.use(cookieParser("key"))
          app.set('trust proxy', false);
          app.set('views', process.cwd() + '/template');
          app.set('view engine', 'html');
          app.engine('html', (path, options, callback) => {
           callback(false, '<h1>Hello</h1>');
          });
          app.all('/', (req, res) => {
           let close = false;
           const f = () => {
           const t = new Date().getTime() + '\n';
           res.write(t);
           console.log(t);
           if(!close){
           setTimeout(f, 1000);
           }
           }
           req.on('close', () => {
           close = true;
           });
           setTimeout(f, 1000);
          });
          app.listen(8888);
          

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

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

          我還是習慣這樣:

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

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

          我會怎么用 Express

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

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

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

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

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

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

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

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

          BaseHandler 的實現:

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

          要用的話,比如 index.js :

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

          url.js 中的樣子:

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

          日志

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

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

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

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

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

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

          說幾點:

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

          ini 格式配置

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

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

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

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

          WebSocket

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

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

          const express = require('express');
          const ws = require('ws');
          const app = express();
          app.all('/', (req, res) => {
           console.log('/');
           res.send('hello');
          });
          const server = app.listen(8888);
          const wss = new ws.Server({server, path: '/ws'});
          wss.on('connection', conn => {
           conn.on('message', msg => {
           console.log(msg);
           conn.send(new Date().toISOString());
           });
          });
          

          對應的一個客戶端實現,來自: 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請求

          視頻地址: https://pan.baidu.com/s/1nvop1nN 密碼: s9iy

          在很多場景中,我們的服務器都需要跟用戶的瀏覽器打交道,如表單提交。

          表單提交到服務器一般都使用 GET/POST 請求。

          本章節我們將為大家介紹 Node.js GET/POS T請求。


          獲取GET請求內容

          由于GET請求直接被嵌入在路徑中,URL是完整的請求路徑,包括了?后面的部分,因此你可以手動解析后面的內容作為GET請求的參數。

          node.js 中 url 模塊中的 parse 函數提供了這個功能。

          實例

          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 然后查看返回結果:

          獲取 URL 的參數

          我們可以使用 url.parse 方法來解析 URL 中的參數,代碼如下:

          實例

          varhttp = require('http');varurl = require('url');varutil = require('util'); http.createServer(function(req, res){res.writeHead(200, {'Content-Type': 'text/plain'}); // 解析 url 參數varparams = url.parse(req.url, true).query; res.write("網站名:" + params.name); res.write("\n"); res.write("網站 URL:" + params.url); res.end(); }).listen(3000);

          在瀏覽器中訪問 http://localhost:3000/user?name=編程改變未來&url=www.biancheng.com 然后查看返回結果:


          獲取 POST 請求內容

          POST 請求的內容全部的都在請求體中,http.ServerRequest 并沒有一個屬性內容為請求體,原因是等待請求體傳輸可能是一件耗時的工作。

          比如上傳文件,而很多時候我們可能并不需要理會請求體的內容,惡意的POST請求會大大消耗服務器的資源,所以 node.js 默認是不會解析請求體的,當你需要的時候,需要手動來做。

          基本語法結構說明

          varhttp = require('http');varquerystring = require('querystring'); http.createServer(function(req, res){// 定義了一個post變量,用于暫存請求體的信息varpost = ''; // 通過req的data事件監聽函數,每當接受到請求體的數據,就累加到post變量中req.on('data', function(chunk){post += chunk; }); // 在end事件觸發后,通過querystring.parse將post解析為真正的POST請求格式,然后向客戶端返回。req.on('end', function(){post = querystring.parse(post); res.end(util.inspect(post)); });}).listen(3000);

          以下實例表單通過 POST 提交并輸出數據:

          實例

          varhttp = require('http');varquerystring = require('querystring'); varpostHTML = '<html><head><meta charset="utf-8"><title>編程改變未來Node.js 實例</title></head>' + '<body>' + '<form method="post">' + '網站名: <input name="name"><br>' + '網站 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(){// 解析參數body = querystring.parse(body); // 設置響應頭部信息及編碼res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'}); if(body.name && body.url){// 輸出提交的數據res.write("網站名:" + body.name); res.write("<br>"); res.write("網站 URL:" + body.url); }else{// 輸出表單res.write(postHTML); }res.end(); });}).listen(3000);

          Node.js Web 模塊


          什么是 Web 服務器?

          Web服務器一般指網站服務器,是指駐留于因特網上某種類型計算機的程序,Web服務器的基本功能就是提供Web信息瀏覽服務。它只需支持HTTP協議、HTML文檔格式及URL,與客戶端的網絡瀏覽器配合。

          大多數 web 服務器都支持服務端的腳本語言(php、python、ruby)等,并通過腳本語言從數據庫獲取數據,將結果返回給客戶端瀏覽器。

          目前最主流的三個Web服務器是Apache、Nginx、IIS。


          Web 應用架構

          • Client - 客戶端,一般指瀏覽器,瀏覽器可以通過 HTTP 協議向服務器請求數據。

          • Server - 服務端,一般指 Web 服務器,可以接收客戶端請求,并向客戶端發送響應數據。

          • Business - 業務層, 通過 Web 服務器處理應用程序,如與數據庫交互,邏輯運算,調用外部程序等。

          • Data - 數據層,一般由數據庫組成。


          使用 Node 創建 Web 服務器

          Node.js 提供了 http 模塊,http 模塊主要用于搭建 HTTP 服務端和客戶端,使用 HTTP 服務器或客戶端功能必須調用 http 模塊,代碼如下:

          var http = require('http');

          以下是演示一個最基本的 HTTP 服務器架構(使用8081端口),創建 server.js 文件,代碼如下所示:

          var http = require('http');
          var fs = require('fs');
          var url = require('url');
          // 創建服務器
          http.createServer( function (request, response) { 
          // 解析請求,包括文件名
          var pathname = url.parse(request.url).pathname;
          // 輸出請求的文件名
          console.log("Request for " + pathname + " received.");
          // 從文件系統中讀取請求的文件內容
          fs.readFile(pathname.substr(1), function (err, data) {
          if (err) {
          console.log(err);
          // HTTP 狀態碼: 404 : NOT FOUND
          // Content Type: text/plain
          response.writeHead(404, {'Content-Type': 'text/html'});
          }else{ 
          // HTTP 狀態碼: 200 : OK
          // Content Type: text/plain
          response.writeHead(200, {'Content-Type': 'text/html'}); 
          // 響應文件內容
          response.write(data.toString()); 
          }
          // 發送響應數據
          response.end();
          }); 
          }).listen(8081);
          // 控制臺會輸出以下信息
          console.log('Server running at http://127.0.0.1:8081/');

          接下來我們在該目錄下創建一個 index.htm 文件,代碼如下:

          <html>
          <head>
          <title>Sample Page</title>
          </head>
          <body>
          Hello World!
          </body>
          </html>

          執行 server.js 文件:

          $ node server.js
          Server running at http://127.0.0.1:8081/

          接著我們在瀏覽器中打開地址:http://127.0.0.1:8081/index.htm,顯示如下圖所示:

          執行 server.js 的控制臺輸出信息如下:

          Server running at http://127.0.0.1:8081/
          Request for /index.htm received. # 客戶端請求信息

          Gif 實例演示


          使用 Node 創建 Web 客戶端

          Node 創建 Web 客戶端需要引入 http 模塊,創建 client.js 文件,代碼如下所示:

          var http = require('http');
          // 用于請求的選項
          var options = {
          host: 'localhost',
          port: '8081',
          path: '/index.htm' 
          };
          // 處理響應的回調函數
          var callback = function(response){
          // 不斷更新數據
          var body = '';
          response.on('data', function(data) {
          body += data;
          });
          response.on('end', function() {
          // 數據接收完成
          console.log(body);
          });
          }
          // 向服務端發送請求
          var req = http.request(options, callback);
          req.end();

          新開一個終端,執行 client.js 文件,輸出結果如下:

          $ node client.js
          <html>
          <head>
          <title>Sample Page</title>
          </head>
          <body>
          Hello World!
          </body>
          </html>

          執行 server.js 的控制臺輸出信息如下:

          Server running at http://127.0.0.1:8081/
          Request for /index.htm received. # 客戶端請求信息

          Gif 實例演示


          Express 簡介

          Express 是一個簡潔而靈活的 node.js Web應用框架, 提供了一系列強大特性幫助你創建各種 Web 應用,和豐富的 HTTP 工具。

          使用 Express 可以快速地搭建一個完整功能的網站。

          Express 框架核心特性:

          • 可以設置中間件來響應 HTTP 請求。

          • 定義了路由表用于執行不同的 HTTP 請求動作。

          • 可以通過向模板傳遞參數來動態渲染 HTML 頁面。


          安裝 Express

          安裝 Express 并將其保存到依賴列表中:

          $ cnpm install express --save

          以上命令會將 Express 框架安裝在當前目錄的 node_modules 目錄中, node_modules 目錄下會自動創建 express 目錄。以下幾個重要的模塊是需要與 express 框架一起安裝的:

          • body-parser - node.js 中間件,用于處理 JSON, Raw, Text 和 URL 編碼的數據。

          • cookie-parser - 這就是一個解析Cookie的工具。通過req.cookies可以取到傳過來的cookie,并把它們轉成對象。

          • multer - node.js 中間件,用于處理 enctype="multipart/form-data"(設置表單的MIME編碼)的表單數據。

          $ cnpm install body-parser --save
          $ cnpm install cookie-parser --save
          $ cnpm install multer --save

          安裝完后,我們可以查看下 express 使用的版本號:

          $ cnpm list express/data/www/node└── express@4.15.2 -> /Users/tianqixin/www/node/node_modules/.4.15.2@express

          第一個 Express 框架實例

          接下來我們使用 Express 框架來輸出 "Hello World"。

          以下實例中我們引入了 express 模塊,并在客戶端發起請求后,響應 "Hello World" 字符串。

          創建 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("應用實例,訪問地址為 http://%s:%s", host, port)})

          執行以上代碼:

          $ node express_demo.js
          應用實例,訪問地址為 http://0.0.0.0:8081

          在瀏覽器中訪問 http://127.0.0.1:8081,結果如下圖所示:


          請求和響應

          Express 應用使用回調函數的參數: requestresponse 對象來處理請求和響應的數據。

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

          requestresponse 對象的具體介紹:

          Request 對象 - request 對象表示 HTTP 請求,包含了請求查詢字符串,參數,內容,HTTP 頭部等屬性。常見屬性有:

          1. req.app:當callback為外部文件時,用req.app訪問express的實例

          2. req.baseUrl:獲取路由當前安裝的URL路徑

          3. req.body / req.cookies:獲得「請求主體」/ Cookies

          4. req.fresh / req.stale:判斷請求是否還「新鮮」

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

          6. req.originalUrl:獲取原始請求URL

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

          8. req.path:獲取請求路徑

          9. req.protocol:獲取協議類型

          10. req.query:獲取URL的查詢參數串

          11. req.route:獲取當前匹配的路由

          12. req.subdomains:獲取子域名

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

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

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

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

          Response 對象 - response 對象表示 HTTP 響應,即在接收到請求時向客戶端發送的 HTTP 響應數據。常見屬性有:

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

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

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

          4. res.cookie(name,value [,option]):設置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響應

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

          11. res.location():只設置響應的Location HTTP頭,不設置狀態碼或者close response

          12. res.redirect():設置響應的Location HTTP頭,并且設置狀態碼302

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

          14. res.sendFile(path [,options] [,fn]):傳送指定路徑的文件 -會自動根據文件extension設定Content-Type

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

          16. res.status():設置HTTP狀態碼

          17. res.type():設置Content-Type的MIME類型


          路由

          我們已經了解了 HTTP 請求的基本應用,而路由決定了由誰(指定腳本)去響應客戶端請求。

          在HTTP請求中,我們可以通過路由提取出請求的URL以及GET/POST參數。

          接下來我們擴展 Hello World,添加一些功能來處理更多類型的 HTTP 請求。

          創建 express_demo2.js 文件,代碼如下所示:

          express_demo2.js 文件代碼:

          varexpress = require('express');varapp = express(); // 主頁輸出 "Hello World"app.get('/', function(req, res){console.log("主頁 GET 請求"); res.send('Hello GET');})// POST 請求app.post('/', function(req, res){console.log("主頁 POST 請求"); res.send('Hello POST');})// /del_user 頁面響應app.get('/del_user', function(req, res){console.log("/del_user 響應 DELETE 請求"); res.send('刪除頁面');})// /list_user 頁面 GET 請求app.get('/list_user', function(req, res){console.log("/list_user GET 請求"); res.send('用戶列表頁面');})// 對頁面 abcd, abxcd, ab123cd, 等響應 GET 請求app.get('/ab*cd', function(req, res){console.log("/ab*cd GET 請求"); res.send('正則匹配');})varserver = app.listen(8081, function(){varhost = server.address().addressvarport = server.address().portconsole.log("應用實例,訪問地址為 http://%s:%s", host, port)})

          執行以上代碼:

          $ node express_demo2.js
          應用實例,訪問地址為 http://0.0.0.0:8081

          接下來你可以嘗試訪問 http://127.0.0.1:8081 不同的地址,查看效果。

          在瀏覽器中訪問 http://127.0.0.1:8081/list_user,結果如下圖所示:

          在瀏覽器中訪問 http://127.0.0.1:8081/abcd,結果如下圖所示:

          在瀏覽器中訪問 http://127.0.0.1:8081/abcdefg,結果如下圖所示:


          靜態文件

          Express 提供了內置的中間件 express.static 來設置靜態文件如:圖片, CSS, JavaScript 等。

          你可以使用 express.static 中間件來設置靜態文件路徑。例如,如果你將圖片, CSS, JavaScript 文件放在 public 目錄下,你可以這么寫:

          app.use(express.static('public'));

          我們可以到 public/images 目錄下放些圖片,如下所示:

          node_modules
          server.jspublic/public/imagespublic/images/logo.png

          讓我們再修改下 "Hello World" 應用添加處理靜態文件的功能。

          創建 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("應用實例,訪問地址為 http://%s:%s", host, port)})

          執行以上代碼:

          $ node express_demo3.js
          應用實例,訪問地址為 http://0.0.0.0:8081

          執行以上代碼:

          GET 方法

          以下實例演示了在表單中通過 GET 方法提交兩個參數,我們可以使用 server.js 文件內的 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("應用實例,訪問地址為 http://%s:%s", host, port)})

          執行以上代碼:

          node server.js
          應用實例,訪問地址為 http://0.0.0.0:8081

          瀏覽器訪問 http://127.0.0.1:8081/index.htm,如圖所示:

          現在你可以向表單輸入數據,并提交,如下演示:


          POST 方法

          以下實例演示了在表單中通過 POST 方法提交兩個參數,我們可以使用 server.js 文件內的 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'); // 創建 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("應用實例,訪問地址為 http://%s:%s", host, port)})

          執行以上代碼:

          $ node server.js應用實例,訪問地址為 http://0.0.0.0:8081

          瀏覽器訪問 http://127.0.0.1:8081/index.htm,如圖所示:

          現在你可以向表單輸入數據,并提交,如下演示:


          文件上傳

          以下我們創建一個用于上傳文件的表單,使用 POST 方法,表單 enctype 屬性設置為 multipart/form-data。

          index.htm 文件代碼:

          <html><head><title>文件上傳表單</title></head><body><h3>文件上傳:</h3>選擇一個文件上傳: <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("應用實例,訪問地址為 http://%s:%s", host, port)})

          執行以上代碼:

          $ node server.js
          應用實例,訪問地址為 http://0.0.0.0:8081

          瀏覽器訪問 http://127.0.0.1:8081/index.htm,如圖所示:

          現在你可以向表單輸入數據,并提交,如下演示:


          Cookie 管理

          我們可以使用中間件向 Node.js 服務器發送 cookie 信息,以下代碼輸出了客戶端發送的 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)

          執行以上代碼:

          $ node express_cookie.js

          現在你可以訪問 http://127.0.0.1:8081 并查看終端信息的輸出,如下演示:


          相關資料

          • Express官網: http://expressjs.com/

          • Express4.x API 中文版: Express4.x API Chinese

          • Express4.x API:http://expressjs.com/zh-cn/4x/api.html

          載說明:原創不易,未經授權,謝絕任何形式的轉載

          如何使您的網站呈現最佳狀態?這個問題有很多答案,本文介紹了當前框架中應用最廣泛的十種渲染設計模式,讓您能夠選擇最適合您的方式。

          近年來,網絡開發的迅速演變,尤其是在前端開發領域。這種轉變主要歸功于無數涌現的框架和技術,它們旨在簡化和增強構建引人入勝的用戶界面的過程。然而,由于現有框架的豐富多樣以及不斷涌現的新框架,跟上前端趨勢已成為一項艱巨的任務。對于新手來說,很容易感到不知所措,仿佛迷失在廣闊的選擇海洋中。

          渲染是前端開發的核心挑戰,它將數據和代碼轉化為可見且可交互的用戶界面。雖然大多數框架以類似的方式應對這一挑戰,通常比之前的方法更簡潔,但也有一些框架選擇了全新的解決方案。在本文中,我們將研究流行框架中使用的十種常見渲染模式,通過這樣做,無論是初學者還是專家都將獲得對新舊框架的扎實基礎理解,同時也能對解決應用程序中的渲染問題有新的見解。

          在本文的結尾,您將會:

          • 對于當今網頁開發中最常見的渲染模式有基本的了解
          • 了解不同渲染模式的優勢和劣勢
          • 了解在你的下一個大項目中使用哪種渲染模式和框架

          什么是UI渲染模式?

          在前端開發的背景下,渲染是將數據和代碼轉換為對最終用戶可見的HTML。UI渲染模式是指實現渲染過程可以采用的各種方法。這些模式概述了不同的策略,用于描述轉換發生的方式以及呈現出的用戶界面。正如我們很快會發現的那樣,根據所實現的模式,渲染可以在服務器上或瀏覽器中進行,可以部分或一次性完成。

          選擇正確的渲染模式對開發人員來說至關重要,因為它直接影響到Web應用程序的性能、成本、速度、可擴展性、用戶體驗,甚至開發人員的體驗。

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

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

          在每個案例中,我們將研究渲染模式的概念、優點和缺點、使用案例、相關的框架,并提供一個簡單的代碼示例來闡明觀點。

          代碼示例

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

          所有示例的全局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;
          }
          

          靜態網站

          靜態網站是最原始、最基本、最直接的UI渲染方法。它通過簡單地編寫HTML、CSS和JavaScript來創建網站。一旦代碼準備好,它會被上傳為靜態文件到托管服務(如Netlify),并指向一個域名。通過URL請求時,靜態文件會直接提供給用戶,無需服務器端處理。靜態網站渲染非常適合沒有交互性和動態內容的靜態網站,比如落地頁和文檔網站。

          優點

          • 非常簡單
          • 快速
          • 廉價(無服務器)
          • SEO友好

          缺點

          • 不適用于數據頻繁變動的情況(動態數據)
          • 不適用于互動應用程序
          • 沒有直接的數據庫連接
          • 當數據發生變化時,需要手動更新和重新上傳

          相關框架

          • 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實現的應用程序。下面是應用程序。

          第一頁:顯示所有可用的虛擬幣

          第2頁:從Coingecko API獲取的不同交易所的BTC價格。

          請注意,在使用靜態網站時,每個幣種的價格頁面必須手動編寫。

          多頁面應用程序(MPAs)

          這種渲染模式是為了處理我們網站上的動態數據而出現的解決方案,并導致了今天許多最大、最受歡迎的動態Web應用程序的創建。在MPA中,渲染由服務器完成,服務器會重新加載以基于當前底層數據(通常來自數據庫)生成新的HTML,以響應瀏覽器發出的每個請求。這意味著網站可以根據底層數據的變化而改變。最常見的用例是電子商務網站、企業應用程序和新聞公司博客。

          優點

          • 簡單直接
          • 處理動態數據非常出色
          • SEO友好
          • 良好的開發者體驗
          • 高度可擴展的

          缺點

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

          相關框架

          • 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"));

          注意:在這里,每個頁面都將由服務器自動生成,不同于靜態網站,靜態網站需要手動編寫每個文件。

          單頁應用程序(SPA)

          單頁應用程序(SPA)是2010年代創建高度交互式Web應用程序的解決方案,至今仍在使用。在這里,SPA通過從服務器獲取HTML外殼(空白HTML頁面)和JavaScript捆綁包來處理渲染到瀏覽器。在瀏覽器中,它將控制權(水合)交給JavaScript,動態地將內容注入(渲染)到外殼中。在這種情況下,渲染是在客戶端(CSR)上執行的。使用JavaScript,這些SPA能夠在不需要完整頁面重新加載的情況下對單個頁面上的內容進行大量操作。它們還通過操作URL欄來創建多個頁面的幻覺,以指示加載到外殼上的每個資源。常見的用例包括項目管理系統、協作平臺、社交媒體Web應用、交互式儀表板或文檔編輯器,這些應用程序受益于SPA的響應性和交互性。

          優點

          • 高度互動
          • 在瀏覽多個頁面時,用戶體驗無縫銜接
          • 手機友好

          缺點

          • 由于JavaScript捆綁包過大,加載時間較慢
          • SEO能力差
          • 由于客戶端上的代碼執行,存在高安全風險
          • 可擴展性差

          相關框架

          • 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;

          靜態網站生成(SSG)

          靜態網站生成(SSG)是一種利用構建網站的原始靜態網站模式的渲染模式。在構建過程中,從源代碼中預先構建和渲染了所有可能的網頁,生成靜態HTML文件,然后將其存儲在存儲桶中,就像在典型靜態網站的情況下原始上傳靜態文件一樣。對于基于源代碼可能存在的任何路由的請求,將向客戶端提供相應的預構建靜態頁面。因此,與SSR或SPA不同,SSG不依賴于服務器端渲染或客戶端JavaScript來動態渲染內容。相反,內容是提前生成的,并且可以被緩存和高性能地傳遞給用戶。這適用于中度交互的網站,其數據不經常更改,例如作品集網站、小型博客或文檔網站。

          優點

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

          缺點

          • 有限互動
          • 數據更改后需要重新構建和重新上傳

          相關框架

          • Nextjs (默認情況下)
          • 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>
           );
          }

          服務器端渲染(SSR)

          服務器端渲染(SSR)是一種渲染模式,它結合了多頁面應用(MPA)和單頁面應用(SPA)的能力,以克服兩者的局限性。在這種模式下,服務器生成網頁的HTML內容,填充動態數據,并將其發送給客戶端進行顯示。在瀏覽器上,JavaScript可以接管已經渲染的頁面,為頁面上的組件添加交互性,就像在SPA中一樣。SSR在將完整的HTML交付給瀏覽器之前,在服務器上處理渲染過程,而SPA完全依賴于客戶端JavaScript進行渲染。SSR特別適用于注重SEO、內容傳遞或具有特定可訪問性要求的應用,如企業網站、新聞網站和電子商務網站。

          優點

          • 適度互動
          • SEO友好
          • 快速加載時間
          • 對動態數據的良好支持

          缺點

          • 復雜的實施
          • 成本(需要服務器)

          相關框架

          • Next.js
          • Nuxt.js

          Demo (Nextjs)

          在NEXT.js上實現SSR的代碼與SSG演示幾乎相同。這里,唯一的變化在于 getCurrentPrice 函數。使用帶有 no-cache 選項的fetch API,頁面將不會被緩存;相反,服務器將需要在每個請求上創建一個新頁面。

          //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;
          }

          增量靜態生成(ISG)

          增量靜態生成是一種生成靜態網站的方法,它結合了靜態網站生成的優點,能夠更新和重新生成網站的特定頁面或部分,而無需重建整個網站。增量靜態生成允許自動增量更新,從而減少了重建整個應用程序所需的時間,并通過僅在必要時從服務器請求新數據,更有效地利用服務器資源。這對于國際多語言網站、企業網站和發布平臺網站非常實用。

          優點

          • 靜態網站的實時自動更新支持
          • 性價比高
          • SEO友好
          • 良好的性能和可擴展性

          缺點

          • 實施中的復雜性
          • 不適用于高度動態的數據應用

          相關框架

          • Next.js
          • Nuxt.js

          Demo (Nextjs)

          在NEXT.js上實現ISR的代碼與SSG演示幾乎相同。唯一的變化在于 getCurrentPrice 函數。使用fetch API并使用指定條件的選項從服務器獲取數據,當滿足我們定義的條件時,頁面將自動更新。在這里,我們說底層數據應該每60秒進行驗證,并且UI應該根據數據中的任何變化進行更新。

          //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)框架中用于解決加載時間緩慢問題的一種技術。使用這種技術,CSR框架將選擇性地首先渲染和水合具有交互性的網頁的最重要部分,而不是整個頁面。最終,當滿足特定條件時,較不重要的交互組件可以通過水合來實現其交互性。通過優先處理關鍵或可見組件的水合,而推遲處理非關鍵或在折疊區域下的組件的水合,它可以更有效地利用資源,并通過優先處理關鍵或可見組件的水合來加快初始頁面渲染速度。部分水合可以使任何具有多個交互組件的復雜CSR或SPA受益。

          優點

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

          缺點

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

          相關框架

          • 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 位于頁面底部,只有當它進入視口時才會被激活。

          Island Architecture(Astro)

          島嶼架構是Astro框架開發者倡導的一種有前途的UI渲染模式。Web應用程序在服務器上被劃分為多個獨立的小組件,稱為島嶼。每個島嶼負責渲染應用程序UI的特定部分,并且它們可以獨立地進行渲染。在服務器上被劃分為島嶼后,這些多個島嶼包被發送到瀏覽器,框架使用一種非常強大的部分加載形式,只有帶有交互部分的組件由JavaScript接管并啟用其交互性,而其他非交互式組件保持靜態。最常見的用例是構建內容豐富的網站。Astro是構建專注于內容的網站的不錯選擇,例如博客、作品集和文檔網站。Astro的島嶼架構模式可以幫助提高這些網站的性能,尤其是對于網絡連接較慢的用戶來說。

          優點

          • 性能(當今最快的框架之一)
          • 更小的捆綁尺寸
          • 易學易懂,易于維護
          • 良好的SEO表現
          • 良好的開發者體驗

          缺點

          • 有限互動
          • 由于組件數量極多,導致調試困難

          相關框架

          • 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是一個以重用性為核心的全新渲染方式的元框架。該渲染模式基于兩種主要策略:

          在服務器上序列化應用程序和框架的執行狀態,并在客戶端上恢復。

          水合

          這段來自Qwik文檔的摘錄很好地介紹了可重用性。

          監聽器 - 在DOM節點上定位事件監聽器并安裝它們,使應用程序具有交互性。組件樹 - 構建表示應用程序組件樹的內部數據結構。應用程序狀態 - 恢復在服務器上存儲的任何獲取或保存的數據。總體而言,這被稱為水合。所有當前的框架都需要這一步驟來使應用程序具有交互性。

          水合作用之所以昂貴,有兩個原因:

          • 框架必須下載與當前頁面相關的所有組件代碼。
          • 框架必須執行與頁面上的組件相關聯的模板,以重建監聽器位置和內部組件樹。

          在序列化中, Qwik 顯示了在服務器上開始構建網頁的能力,并在從服務器發送捆綁包后繼續在客戶端上執行構建,節省了其他框架重新初始化客戶端的時間。

          就懶加載而言, Qwik 將通過極度懶加載來確保Web應用程序盡快加載,只加載必要的JavaScript捆綁包,并在需要時加載其余部分。 Qwik 可以在開箱即用的情況下完成所有這些操作,無需進行太多開發者配置。

          這適用于復雜的博客應用和企業網站的發布。

          優點

          • 由于可恢復性而對網絡中斷具有彈性
          • 快速加載時間
          • 友好的搜索引擎優化

          缺點

          • 復雜的實施
          • 更高的帶寬使用

          相關框架

          • 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",
             },
           ],
          };
          

          流式服務器端渲染(Streaming SSR)

          流式服務器端渲染(Streaming SSR)是一種相對較新的用于渲染Web應用程序的技術。流式SSR通過將應用程序的用戶界面分塊在服務器上進行渲染。每個塊在準備好后立即進行渲染,然后流式傳輸到客戶端。客戶端在接收到塊時顯示和填充它們。這意味著客戶端在應用程序完全渲染之前就可以開始與其進行交互,無需等待。這提高了Web應用程序的初始加載時間,尤其適用于大型和復雜的應用程序。流式SSR最適用于大規模應用,如電子商務和交易應用程序。

          優點

          • Performance
          • 實時更新

          缺點

          • 復雜性

          相關框架

          • Next.js
          • Nuxt.js

          Demo

          很遺憾,我們的應用程序不夠復雜,無法提供一個合適的例子。

          結束

          在本文中,我們探討了當今前端網頁開發中最流行的十種UI渲染模式。在這個過程中,我們討論了每種方法的優勢、局限性和權衡。然而,重要的是要注意,沒有一種適用于所有情況的渲染模式或普遍完美的渲染方法。每個應用都有其獨特的需求和特點,因此選擇合適的渲染模式對于開發過程的成功至關重要。

          由于文章內容篇幅有限,今天的內容就分享到這里,文章結尾,我想提醒您,文章的創作不易,如果您喜歡我的分享,請別忘了點贊和轉發,讓更多有需要的人看到。同時,如果您想獲取更多前端技術的知識,歡迎關注我,您的支持將是我分享最大的動力。我會持續輸出更多內容,敬請期待。

          參考文獻

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

          主站蜘蛛池模板: 91在线一区二区三区| 日韩免费视频一区二区| 成人久久精品一区二区三区| 久久青草精品一区二区三区| 国产精品无码一区二区三级| 精品无码国产一区二区三区51安 | 国产在线精品一区二区三区不卡 | 久久国产精品一区| 国产福利91精品一区二区| 亚洲成a人一区二区三区| 日韩AV片无码一区二区不卡 | 国产视频一区在线播放| 亚洲欧洲精品一区二区三区| 亚洲中文字幕在线无码一区二区| 在线视频一区二区日韩国产| 熟妇人妻系列av无码一区二区| 精品少妇人妻AV一区二区三区| 亚洲AV色香蕉一区二区| 国产大秀视频在线一区二区| 亚洲av成人一区二区三区 | 性无码一区二区三区在线观看| 国产精品一区二区在线观看| 国产肥熟女视频一区二区三区| 精品女同一区二区三区免费播放| 久99精品视频在线观看婷亚洲片国产一区一级在线 | 久久se精品一区二区影院| 深夜福利一区二区| 久久久综合亚洲色一区二区三区| 亚洲av无码片区一区二区三区| 国产乱码精品一区二区三区麻豆 | 国产精品一区二区久久乐下载| 国产精品第一区第27页| 韩日午夜在线资源一区二区| 99久久人妻精品免费一区| 亚洲AV无码一区二区三区鸳鸯影院| 激情爆乳一区二区三区| 亚洲AV无码一区二区二三区软件| 国产一区在线mmai| 97一区二区三区四区久久| 国产一区二区三区在线免费| 亚洲AⅤ无码一区二区三区在线|