标签: 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.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页面是这样的。可现在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仓库的实时构建结果。

从零开始,做一个NodeJS博客(四):服务器渲染页面与Pjax的更多相关文章

  1. 从零开始,做一个NodeJS博客(零):整体规(chui)划(niu)

    标签:NodeJS,Heroku 0 搭建一个个人独立博客,这是我好久之前就在计划的一件事了. 这个暑假,我学习了廖雪峰老师的NodeJS教程,又偶然在V2EX上发现了Heroku这个平台,可以免费在 ...

  2. 从零开始,做一个NodeJS博客(一):Heroku上的最简NodeJS服务器

    标签:NodeJS,Heroku 0 这里是这个伪系列的第一篇,因为我也不知道自己能不能做完,然后到底能做成什么样子.总之,尽力而为吧,加油. 1 Heroku App 的构成 Heroku 所谓的 ...

  3. 从零开始,做一个NodeJS博客(三):API实现-加载网易云音乐听歌排行

    标签: NodeJS 0 研究了一天,翻遍了GitHub上各种网易云API库,也没有找到我想要的听歌排行API,可能这功能比较小众吧.但收获也不是没有,在 这里 明白了云音乐API加密的凶险,我等蒟蒻 ...

  4. 从零开始,做一个NodeJS博客(二):实现首页-加载文章列表和详情

    标签: NodeJS 0 这个伪系列的第二篇,不过和之前的几篇是同一天写的.三分钟热度貌似还没过. 1 静态资源代理 上一篇,我们是通过判断请求的路径来直接返回结果的.简单粗暴,缺点明显:如果url后 ...

  5. 这几天有django和python做了一个多用户博客系统(可选择模板)

    这几天有django和python做了一个多用户博客系统(可选择模板) 没完成,先分享下 断断续续2周时间吧,用django做了一个多用户博客系统,现在还没有做完,做分享下,以后等完善了再慢慢说 做的 ...

  6. 如何搭建一个独立博客——简明Github Pages与Hexo教程

    摘要:这是一篇很详尽的独立博客搭建教程,里面介绍了域名注册.DNS设置.github和Hexo设置等过程,这是我写得最长的一篇教程.我想将我搭建独立博客的过程在一篇文章中尽可能详细地写出来,希望能给后 ...

  7. 使用前端技术和MySQL+PHP制作自己的一个个人博客网站

    源代码地址:https://github.com/YauCheun/BlogCode 我的博客网站地址:http://www.yublog.fun/ 制作前景: 想拥有一个自己独自开发的一个小型博客网 ...

  8. RSP小组——团队冲刺博客四

    RSP小组--团队冲刺博客四 冲刺日期:2018年12月13日 前言 问题已经明确,经过今天的努力,部分已近得到解决,所以,今天是一个值得庆祝的日子. 各成员今日(12.13)完成的任务 李闻洲对音乐 ...

  9. 推荐一个静态博客兼笔记的工具:WDTP

    简介 WDTP(山湖录)不止是一款开源免费的GUI桌面单机版静态网站生成器和简单方便的前端开发工具,更是一款跨平台的集笔记.录音.个人知识管理.写作/创作.博客/网站内容与样式管理等功能于一体的多合一 ...

随机推荐

  1. Java多线程系列--“基础篇”10之 线程优先级和守护线程

    概要 本章,会对守护线程和线程优先级进行介绍.涉及到的内容包括:1. 线程优先级的介绍2. 线程优先级的示例3. 守护线程的示例 转载请注明出处:http://www.cnblogs.com/skyw ...

  2. Dapper的基本使用

    Dapper是.NET下一个micro的ORM,它和Entity Framework或Nhibnate不同,属于轻量级的,并且是半自动的.也就是说实体类都要自己写.它没有复杂的配置文件,一个单文件就可 ...

  3. AngularJS in Action读书笔记3——走近Services

    试着想想这些问题:如果一个controller只关心自己所控制的view页面,那么对于整个application来说,你如何调用想要的function:如果controller从来都不会和其他cont ...

  4. 12个新潮的 HTML5 & CSS3 网站设计欣赏

    响应式设计和基于 HTML5 & CSS3 编码的网站是为网站制作的理想解决方案. HTML5 & CSS3 制作出来的网站结构良好,有很多惊人的效果,并能够在任何设备上浏览. 今天, ...

  5. PHP面向对象中的重要知识点(一)

    1. __construct: 内置构造函数,在对象被创建时自动调用.见如下代码: <?php class ConstructTest { private $arg1; private $arg ...

  6. Android之Activity的生命周期

    PS:写一发关于Activity的生命周期,也算是面试的重点内容. 学习内容: 1.Activity的生命周期 2.面对多种情况的时候Activity的生命周期 3.onSaveInstanceSta ...

  7. Windows Azure Active Directory (3) China Azure AD增加新用户

    <Windows Azure Platform 系列文章目录> 本文介绍的是国内由世纪互联运维的China Azure. 本文是对笔者之前的文档:Windows Azure Active ...

  8. 微软官方网站线上兼容测试平台-Browser screenshots

    前端开发时最不想做的就是在不同浏览器.平台和分辨率测试网页显示效果,通常这会浮现许多问题,尤其浏览器版本就可能让显示成效完全不同,也只好尽力维持让每一种设备都能正常浏览网页.修改到完全没有问题必须投入 ...

  9. Emit学习(4) - Dapper解析之数据对象映射(二)

    承接着上一篇, 这一篇主要以堆栈的方式来演示一下, db数据转换到类中去的一个过程. 一.先看第一张图 程序在运行到176行(上一篇贴出的代码)的时候, 就会出现上图中的第一个栈. 那在此之前, Da ...

  10. 使用VS Code 开发.NET Core 应用程序 部署到Linux 跨平台

    使用VS Code 开发.NET Core 应用程序 部署到Linux 跨平台. 前面讲解了VSCode开发调试 .NET Core.都只是在windows下运行. .NET Core真正的核心是跨平 ...