從零開始,做一個NodeJS博客(四):服務器渲染頁面與Pjax

標簽: NodeJS


0

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

1 聽歌排行 API 修復與重構

1.1 修復

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

JSON Parse ERROR

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

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

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

JSON 片段

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

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

打下斷點

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

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

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

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

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

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

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

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

將代碼修改如下:

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

之前我們的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靠前的部分,相當于變量初始化的位置,然后運行測試吧:

動態的404頁面

好的,效果達到了。

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被改變了,內容也成功加載出來。可是如果現在后退的話,雖然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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/271065.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/271065.shtml
英文地址,請注明出處:http://en.pswp.cn/news/271065.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

java 簡單事件的使用,如何正確的使用Java事件通知(1)

如何正確的使用Java事件通知(1)通過實現觀察者模式來提供 Java 事件通知(Java event notification)似乎不是件什么難事兒&#xff0c;但這過程中也很容易就掉進一些陷阱。本文介紹了我自己在各種情形下&#xff0c;不小心制造的一些常見錯誤。Java 事件通知讓我們從一個最簡單的…

Java 自動裝箱與拆箱

Java 自動裝箱與拆箱 裝箱就是自動將基本數據類型轉換為包裝器類型&#xff08;int–>Integer&#xff09;&#xff1b;調用方法&#xff1a;Integer 的 valueOf(int) 方法 拆箱就是自動將包裝器類型轉換為基本數據類型&#xff08;Integer–>int&#xff09;。調用方法…

基本系統設備感嘆號_win7系統網絡圖標顯示感嘆號的問題

有系統之家的小伙伴&#xff0c;在使用win764位純凈版系統上網的時候&#xff0c;出現網絡圖標顯示感嘆號的問題。這種問題我們可以通過在網絡檢測修復中進行自行檢測。或者是檢查一下是不是硬件設備的問題。詳細解決步驟就來看下系統哥小編是怎么做的吧~win7 64系統無法上網顯…

LVM邏輯卷,RAID磁盤陣列

磁盤管理&#xff1a; 有關硬盤的識別&#xff0c;linux根據設備類型對存儲設備進行識別&#xff0c;如果是IDE設備&#xff0c; 在計算機中會被識別為hd&#xff0c;第一個IDE設備會被識別為hda&#xff0c;第二個IDE設備會被識別為hdb&#xff0c;依次類推。如果是SATA,USB,S…

硬件編程:STM32串口發送數據和接收數據方式總結!

串口發送數據1、串口發送數據最直接的方式就是標準調用庫函數 。void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);第一個參數是發送的串口號&#xff0c;第二個參數是要發送的數據&#xff0c;但是用過的朋友應該覺得不好用&#xff0c;一次只能發送單個字符&#…

Java編程題修院子,了解java虛擬機—JVM相關參數設置(2)

1. JVM相關參數設置JVM相關配置-XX:PrintGC兩次次YoungGC&#xff0c;兩次FullGC。-XX:PrintGCDetails打印GC時的內存&#xff0c;并且在程序結束時打印堆內存使用情況-XX:PrintHeapAtGC每次GC時會分別打印回收前與回收后堆信息-XX:PrintGCTimeStamps選擇打印GC的方式后&…

此異常最初是在此調用堆棧中引發的:_【8】進大廠必須掌握的面試題Java面試異常和線程...

點擊上方“全棧程序員社區”&#xff0c;星標公眾號重磅干貨&#xff0c;第一時間送達Q1。錯誤和異常有什么區別&#xff1f;錯誤是在運行時發生的不可恢復的情況。如OutOfMemory錯誤。這些JVM錯誤無法在運行時修復。盡管可以在catch塊中捕獲錯誤&#xff0c;但是應用程序的執行…

高仿帶感魔性病毒源碼+成品(最近很火的)

高仿帶感魔性病毒源碼成品&#xff08;最近很火的&#xff09;娛樂使用。沒破壞性 會改壁紙和打亂桌面圖標順序 自己改回來就好 演示地址&#xff1a; 下載地址&#xff1a;鏈接: http://pan.baidu.com/s/1dF2ZlU5 密碼: m95p轉載于:https://www.cnblogs.com/blogwy/p/5804711.…

數據庫設計:數據庫設計的基本步驟介紹

數據庫設計主要包括用戶需求分析、概念結構設計、邏輯結構設計、物理結構設計、數據庫實施階段、數據庫運行和維護階段等六個階段。1、用戶需求分析 數據庫設計人員采用相應的輔助工具對應用對象的功能、性能、限制等要求進行科學實際的分析。2、概念結構設計 概念結構設計主要…

Hashcode 的作用

java 的集合有兩類&#xff0c;一類是 List&#xff0c;還有一類是 Set。前者有序可重復&#xff0c;后者無序不重復。當我們在 set 中插入的時候怎么判斷是否已經存在該元素呢&#xff0c;可以通過 equals 方法。但是如果元素太多&#xff0c;用這樣的方法就會比較滿。 于是有…

坎蒂雷賦權法 matlab,干貨 | 利用MATLAB實現FMCW雷達中的常用角度估計方法

其中在介紹角度估計中&#xff0c;通過對接收差頻信號在快慢時間維度的擴展&#xff0c;增加了空域的信息。擴展后的接收差頻信號可以表示為其中k表示接收天線的個數&#xff0c;d為天線間距。在“干貨|利用MATLAB實現FMCW雷達的角度估計”中&#xff0c;已經介紹了如何理解目標…

vscode 使用筆記

https://code.visualstudio.com/docs/setup/setup-overview#_proxy-server-support 如果使用代理上網時&#xff0c;需要配置&#xff1a; 在 settings.json 中這樣設定&#xff1a; // 將設置放入此文件中以覆蓋默認設置{"http.proxy": "http://用戶名:密碼IP:…

數據庫設計基礎:需求分析相關知識筆記

系統需求分析是用戶和相關設計人員對數據庫應用系統所涉及的內容和功能描述&#xff0c;主要是以用戶角度來了解系統&#xff0c;是數據庫邏輯設計和物理設計以及應用程序的涉及都根據系統分析的內容作為基礎。該階段是非常重要的環節&#xff0c;如果該階段設計的不好&#xf…

matlab 康托爾集,康托爾集的性質特點

康托爾集的性質特點康托三分集中有無窮多個點&#xff0c;所有的點處于非均勻分布狀態。此點集具有自相似性&#xff0c;其局部與整體是相似的&#xff0c;所以是一個分形系統。康托三分集具有(1)自相似性&#xff1b;(2)精細結構&#xff1b;(3)無窮操作或迭代過程&#xff1b…

String、StringBuuffer、StringBuilder三者的區別

可變性 String 類中使用 ?nal 關鍵字字符數組保存字符串&#xff0c; private final char value[] &#xff0c;所以 String 對象是不可變的。 StringBuilder 與 StringBu?er 都繼承自 AbstractStringBuilder 類&#xff0c;在 AbstractStringBuilder 中也是使用字符數組保存…

運算符和類型轉換

1.類型轉換&#xff1a; 分為自動轉換和強制轉換&#xff0c;一般用強制轉換。 其他類型轉換為整數&#xff1a;parseInt&#xff08;&#xff09;&#xff1b; 其他類型轉換為小數&#xff1a;parseFloat&#xff08;&#xff09;&#xff1b; 判斷是否是一個合法的數字類型&a…

數據庫設計基礎:數據字典相關知識筆記

1、數據字典的定義 數據字典&#xff08;Data Dictionary ,DD&#xff09;是各類數據描述的集合&#xff0c;它是關于數據庫中數據的描述&#xff0c;即元數據&#xff0c;而不是數據本身。2、數據字典的組成數據字典主要包括數據項、數據結構、數據流、數據存儲、處理過程等內…

用符號方法求下列極限或導數matlab,matlab實驗

3&#xff0c;設有矩陣A 和B 1234530166789101769A ,111213141502341617181920970212223242541311B ????????-????????-????????????????1、求它們的乘積C &#xff1b;2、將矩陣C 的右下角3*2子矩陣賦給D &#xff1b;3、察看matlab 工作空間…

Java基本數據類型及所占字節大小

一、Java基本數據類型 基本數據類型有8種&#xff1a;byte、short、int、long、float、double、boolean、char 分為4類&#xff1a;整數型、浮點型、布爾型、字符型。 整數型&#xff1a;byte、short、int、long 浮點型&#xff1a;float、double 布爾型&#xff1a;boolean 字…

事務管理:事務的基本概念筆記

1、事務的意義事務管理是對于一系列數據庫操作進行操作。針對多個事務并發執行的數據庫當中&#xff0c;如果對共享的數據進行更新操作不進行控制&#xff0c;很有可能會產生數據的不一致性&#xff0c;造成數據庫存儲無效甚至錯誤的數據。數據庫在運行過程中會受到很多方面的因…