日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

从零开始,做一个NodeJS博客(四):服务器渲染页面与Pjax

發布時間:2023/12/10 javascript 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从零开始,做一个NodeJS博客(四):服务器渲染页面与Pjax 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

標簽: NodeJS


0

一個星期沒更新了 = =
一直在忙著重構代碼,以及解決重構后出現的各種bug
現在CSS也有一點了,是時候把遇到的各種坑盤點一下了

1 聽歌排行 API 修復與重構

1.1 修復

在加載云音樂聽歌排行的時候,有時會出現一個奇怪的bug:json數據無法被解析。如下圖:

在刷新頁面后,問題就會得到解決。此后無論怎么刷新,問題也不會出現。

過一段時間再次打開頁面,會出現相同的問題,刷新之后也可以解決。此時換用其他各種瀏覽器,都不會出現問題;但一段時間之后仍會重現一次。。。

那肯定不是瀏覽器的鍋了。把Response的內容復制出來看看。

粘貼,格式化。VSCode報出了4個警告和一個錯誤;再仔細看一眼,哎,怎么中途截斷了?難道是收到的請求不全?

返回去看看接收請求收到的JSON文件:沒錯啊,是全的。當然了,因為接下來刷新幾次之后就不會在遇到此問題了。在本地測試中也發現,只有服務器啟動之后的第一次訪問,才會出現這個問題。

找到輸出的位置,在這里下斷點,開始調試。

從server.js進來的時候,文件還沒有被創建;到36行,建立請求;38行,綁定事件回調;49行,發送。

接收到數據,觸發response事件,命中斷點。

解壓縮,輸出,這時候檢查一下輸出的文件,0 KB。跑到下一步callback,傳出文件名,這時候檢查輸出文件,0 KB。

等下!怎么會是0 KB!這時文件還沒有寫入完成,就已經把文件名傳給回調函數,然后開始讀取了?!

然后就進入了各種不明所以的內部庫調用,跳出之后,檢查輸出文件,37KB。這里才剛剛寫入完成!自然,瀏覽器那邊還是沒法解析,傳出來的數據還是不完整,即使輸出文件已經是完整的了。

有沒有聯想到一些東西?是IO效率的問題,或者說,文件操作也是異步的,需要等待一個事件?

好,馬上去查一下Stream的API文檔,找到了Stream.Writable的finish事件。這個事件在所有數據寫入完成之后被觸發。好,要的就是你。

將代碼修改如下:

response.pipe(zlib.createGunzip()).pipe(output); // wait for file operation output.on('finish',() => {fs.readFile(outputFileName, (err, data) => {var buf = JSON.parse(data.toString())['/api/user/detail/76980626'].listenedSongs;bufJSON = new Array();buf.forEach((value, index) => {if (index > 9) return;bufJSON.push({ id: value.id, name: value.name, artistName: value.artists[0].name });});}); });

在等待文件操作完成之后才讀取數據,而且讀到數據后,只取出自己需要用到的部分,存在全局數組bufJSON中當作緩存,順便提高一下API響應速度。

1.2 重構

之前,API獲取的聽歌排行目標用戶是寫死在代碼里的。可以寫一個init()函數,初始化它的獲取目標用戶。

function init(id) {userId = id;outputFileName = `netease_music_record_${id}.json`; }

在寫入請求body的時候,要把請求數據轉化成QueryString的格式。Node.js提供的QueryString模塊可以接受一個Obejct作為參數,輸出字符串;不過可變值的多行字符串并不能作為對象的屬性名。也就是說:

var postData = {`/api/user/detail/${id}`: '{\'all\':true}' }

是會報錯的,對象屬性名非法。這下我們就需要引入Map這個數據類型了,只要是合法的字符串,就可以當作數據的鍵和值。像這樣:

var req = http.request(options); var qString = new Map(); qString[`/api/user/detail/${userId}`] = '{\'all\':true}'; req.write(qs.stringify(qString));

嗯,API的優化就說到這里了,代碼都在文章最下方的Git倉庫里,我也會時不時進行一些抽風似的重構,不可能一一講述了。

2 服務器端頁面渲染

說到動態頁面,直接用JS在瀏覽器里操作不就行了,還關服務器什么事?這樣雖然很方便,不過有一個弊端:不利于搜索引擎爬蟲的索引。自己博客里寫了這么多文章,當然希望更多的人可以通過搜索引擎找到,而不是整天放在那里無人問津吧。

好,那就來動態的構建一個404頁面,可以顯示當然服務器正在運行的Node版本。

之前我們的404頁面是這樣的。可現在Node.js的current版本已經到6.4.0了,就先從這里下手吧。

通過Node.jsAPI文檔,了解到,要獲取當前node版本號,只需要使用porcess.version。如何吧這個版本號替換進404頁面的html文件中去呢?我想到的方法是,把html中的版本號改成一段特殊的字符串,然后用正則表達式去唯一的匹配他。比如這樣:

<p>Node.js - ${process.version}</p>

然后我們建立正則表達式,去匹配那個字符串。但千萬不要在html文檔的其他地方使用這個“占位符”,它會被全部替換成版本號。也可以再在后面加一些其他無意義內容,反正要避免正常的代碼或文字與它重復。

fs.readFile(path.join(root, '/page/404.html'), (err, data) => {var versionRegex = /\$\{process\.version\}/;var nodeVersion = process.version;var current404 = data.toString().replace(versionRegex, nodeVersion);var page404 = fs.createWriteStream(path.join(root, '/page/current404.html'));page404.end(current404, 'utf8'); });

讀取文件,轉換字符串,然后生成了新的current404.html文件。之后發送404頁面的響應也要改成發送剛剛生成的current404.html。

把這段代碼放在server.js靠前的部分,相當于變量初始化的位置,然后運行測試吧:

好的,效果達到了。

3 使用 history.pushState(),改變 URL 并局部刷新頁面

Ajax都很熟悉吧,Asynchronous Javascript And XML,再加上pushState,就變成了Pjax。

沒什么神秘的,history.pushState()的作用就是,改變頁面的URL,并將一個state對象儲存起來。這個state對象是自己定義的。在事件window.onpopstate的回調函數中,傳入的參數的state屬性,是之前儲存起來的state對象。

簡單來說,使用history.pushState(),會改變當前頁面的URL,但僅僅是改變,瀏覽器并不嘗試去加載他,只是擺在那里;同時會將URL與傳入的state對象一起壓入歷史紀錄棧中。當用戶操作瀏覽器前進或后退時,如果操作后當前頁面的URL是由history.pushState()方法壓入棧中的,那么頁面將不會被重新加載,window.onpopstate的回調函數會被執行。

有關更詳細的介紹,請看操縱瀏覽器的歷史記錄 - DOM | MDN。

我的目的是,在用戶單擊了首頁的標題文章標題時,URL改變,但以Ajax的方法從服務器加載文章內容,顯示在頁面上。而當用戶直接訪問這個URL時,又能提供完整文章瀏覽的頁面。

為此,先要在主頁上動動手腳,使得點擊文章之后讓他看起來像一個瀏覽頁面:

<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><title>Rocka's Node Blog</title> </head><body><h1>Rocka's Node Blog</h1><hr><h3 id="index-article-title" style="display:none;">Title should be shown here.</h3><blockquote id="index-article-content" style="display:none;">Article should be shown here.</blockquote><h3 id="index-article-header">Blog Archive</h3><ul id="index-article-list"></ul><h3>Rcecntly Listened</h3><ul id="index-music-record"></ul> </body></html>

新加入的元素被設置為了不顯示,我們總不能在一個主頁上就顯示文章內容吧。在用戶點擊文章之后,再改變歷史記錄,同時變更頁面的樣式,讓它看起來像一個文章瀏覽頁面。于是,在loadArticleContent的success回調中,我們這樣寫:

function success(response) {history.pushState({originTitle: articleTitle,type: 'archive',originPathName: window.location.pathname},articleTitle,`/archive/${articleTitle}`);// switch element visibilityshowArticleContnet();document.getElementById('index-article-title').innerText = articleTitle;document.getElementById('index-article-content').innerText = response; }

showAtricleContent函數用來切換各種元素可見性,把#index-article-header和#index-article-list隱藏,#index-article-title和#index-article-contnet顯示,這里就不展開寫了。el.sytle.display='block'或者'none'就好。之后還會有一個showIndex函數,都懂這個意思,看看就好。

還有就是history.pushState()的三個參數,第一個是要壓入的state對象,第二個是名稱,可以傳入空字符串,或者當前文章名稱,因為這個屬性在現在并沒由什么用處(MDN是這么說的!)。第三個就是要變成的URL了,規定好自己的URL地址。我這里用的是與文章文件相同位置的地址。

然后,看看效果:

URL被改變了,內容也成功加載出來。可是如果現在后退的話,雖然URL會變回去,但卻不會產生任何效果。這時要給window.onpopstate綁定回調函數:

window.onpopstate = (e) => {if (e.state) {loadArticleContent(e.state.originTitle);} else {showIndex();} }

這個e.state是我們之前pushState的時候壓入歷史記錄棧中的,里面存儲的是跳轉到的標題。同樣,如果沒有state,應該是后退到了主頁上,顯示主頁。

現在測試,點擊,跳轉了,后退,正常;前進,正常;后退,后退。。。。哎,不對啊,怎么退不回主頁了?還記得loadArticleContent嗎?我們調用它的時候,直接使用了pushState。但在window.onpopstate的回調函數中,也是調用了它。這也就意味著,當我們操作頁面前進時,又會有一條歷史記錄被壓入棧中;然后再后退,又多了一條,每次后退,又會多一條。雖然我們的位置后退了,但在我們前面又增加了一條記錄,這樣永遠也回不到主頁。

所以,在加載文章內容時做出判斷:如果此次加載來自歷史記錄操作(加一個參數就好),那么不再增加歷史記錄:

function loadArticleContent(articleTitle, fromState) {function success(response) {if (!fromState) {history.pushState({originTitle: articleTitle,type: 'archive',originPathName: window.location.pathname},articleTitle,`/archive/${articleTitle}`);}showArticleContent();document.getElementById('index-article-title').innerText = articleTitle;document.getElementById('index-article-content').innerText = response;}// other more operations......// ...... }window.onpopstate = (e) => {if (!e.state) {showIndex();} else {loadArticleContent(e.state.originTitle, true);} }

至此,在不刷新的前提下主頁的操作正常了。

4 動態構建文章閱讀頁面

借助pushState,我們時可以改變URL了,可是這個頁面實際上是不存在的,一刷新就沒了。如果別人想要收藏你的博客文章,不就很尷尬了。。。所以我們要動態的構建一個閱讀頁面出來。

剛才在處理首頁的時候,把元素隱藏了一下就變成閱讀界面了。這里先把首頁復制一份,稍加改動,就變成了文章閱讀頁面view.html:

<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><title>Rocka's Node Blog</title> </head><body><h1>Rocka's Node Blog</h1><hr><h3 id="index-article-title">${article.title}</h3><blockquote id="index-article-content">${article.contnet}</blockquote><h3 id="index-article-header" style="display:none;">Blog Archive</h3><ul id="index-article-list" style="display:none;"></ul><h3>Rcecntly Listened</h3><ul id="index-music-record"></ul> </body></html>

這里我把對應元素的內容也都換成了“占位符”,方便匹配。接下來,當用戶請求文章頁面的時候,就像生成404頁面一樣,先讀取模板,然后將占位符用相應的數據替換。唯一不同的一點是,不要把輸出后的文件緩存到當前目錄,否則加載文章列表要讀取文件的時候,會多出一些奇怪的東西。

在服務器啟動監聽端口之前,先把原始的文章閱讀頁面存入全局變量,也是相當于變量初始化吧:

fs.readFile(path.join(root, '/page/view.html'), (err, data) => {// read origin page in advanceplainViewPage = data.toString(); });

之后每次請求時,只要復制存在全局變量里的字符串,然后修改副本:

fs.stat(filePath, (err, stats) => {// no error occured, read fileif (!err && stats.isFile()) {if (pathName.indexOf('/archive/') >= 0) {var archiveRegex = /archive\/(.+)/;var titleRegex = /\$\{archive\.title\}/;var contentRegex = /\$\{archive\.content\}/;var title = archiveRegex.exec(pathName)[1];fs.readFile(path.join(root, pathName), (err, data) => {var page = plainViewPage;var page = page.replace(titleRegex, title);var page = page.replace(contentRegex, data.toString());response.end(page);});} else {// normal file read}} else {// file not found} });

現在問題來了:上一步pjax的時候,請求文章內容的URL已經是文章的“真實”URL了。如果再把這個URL分給文章頁面,是否會產生沖突?

當然會了,不過我們有辦法避免。在我們異步請求文章內容的時候是一個GET請求;瀏覽器刷新頁面時也是。但在創建XMLHttpRequest的時候,可以給它設置一個特殊的請求頭,比如pushstate-ajax之類的,用于區分動態加載和頁面獲取。值得注意的是,只有在請求open之后,send之前,才能設置請求頭:

var request = new XMLHttpRequest();request.onreadystatechange = () => {if (request.readyState === 4) {if (request.status === 200) {// do sth with resopnse} else {// oops~~}} }request.open('GET', `/archive/${articleTitle}`); // set special request header request.setRequestHeader('pushstate-ajax', true); request.send();

同樣,在服務器端,也需要進行一些判斷:

  • 如果是正常的頁面請求(沒有特殊請求頭),就要返回替換了文章內容的查看頁面;
  • 否則只需要返回文章內容:
if (request.method === 'GET') {if (pathName.indexOf('/api/') >= 0) {// api request} else if (request.headers['pushstate-ajax']) {// return article coontent only} else {fs.stat(filePath, (err, stats) => {if (!err && stats.isFile()) {if (pathName.indexOf('/archive/') >= 0) {// return mixed view.html} else {// normal file}} else if (!err && pathName == '/') {// goto index} else {// return currnet404.html}});} }

5

好了,今天就寫到這里。其實我還落下了一次更新,現在的實際進度已經達到了,額,還是點開下面的App地址看一下吧,我也不好形容。我會抓緊把剩下的坑都填好的 ;)

倉庫地址

GitHub倉庫:BlogNode

主倉庫,以后的代碼都在這里更新。

HerokuApp:rocka-blog-node

上面GitHub倉庫的實時構建結果。

轉載于:https://www.cnblogs.com/rocket1184/p/nodejs-heroku-blog-4.html

總結

以上是生活随笔為你收集整理的从零开始,做一个NodeJS博客(四):服务器渲染页面与Pjax的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。