標簽: 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 visibility
showArticleContnet();
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 advance
plainViewPage = data.toString();
});
之後每次請求時,只要復制存在全局變量裏的字符串,然後修改副本:
fs.stat(filePath, (err, stats) => {
// no error occured, read file
if (!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倉庫的實時構建結果。
Tags: 瀏覽器 下一步 服務器 從零開始 音樂
文章來源: