标签: 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. jQuery.noConflict()

    转载:http://blog.163.com/mjuxiaom@126/blog/static/13397047120117324320858/ 运行这个函数将变量$的控制权让渡给第一个实现它的那个库 ...

  2. 赞!15个来自 CodePen 的酷炫 CSS 动画效果

    CodePen 是一个在线的前端代码编辑和展示网站,能够编写代码并即时预览效果.你在上面可以在线分享自己的 Web 作品,也可以欣赏到世界各地的优秀开发者在网页中实现的各种令人惊奇的效果. 今天这篇文 ...

  3. 使用 CSS3 伪元素实现立体的照片堆叠效

    CSS3 里引入的伪元素让 Web 开发人员能够在不需要额外添加 HTML 标签的情况下制作出复杂的视觉效果.例如,:before 和 :after 这个两个 CSS3 伪元素就可以帮助你实现很多有趣 ...

  4. Jquery基本用法总结

    选择器$("#mydiv") 通过ID$("p#myp") 选择id=myp 的所有p元素(组合型)$(".mydiv") 通过 class ...

  5. Android正则表达式使用及性能隐患分析

    场景:找出一个关键词在一条短信中出现的次数 使用正则的实现方式: public static int findKeyWordCount(String srcText, String keyword) ...

  6. 配置springmvc在其他类中(spring容器外)获取注入bean

    学习https://github.com/thinkgem/jeesite 今天在写JedisUtils的时候要注入JedisPool,而这个属性被设置为static,@Resource和@Autow ...

  7. WebGL实现HTML5贪吃蛇3D游戏

    js1k.com收集了小于1k的javascript小例子,里面有很多很炫很酷的游戏和特效,今年规则又增加了新花样,传统的classic类型基础上又增加了WebGL类型,以及允许增加到2K的++类型, ...

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

    感觉好久没有写博客了, 这几天有点小忙, 接下来会更忙, 索性就先写一篇吧. 后面估计会有更长的一段时间不会更新博客了. 废话不多说, 先上菜. 一.示例 1. 先建类, 类的名称与读取的表名并没有什 ...

  9. C#:Func的同步、异步调用

    using System; namespace ActionDemo { class Program { static void Main(string[] args) { Console.Write ...

  10. 十年微软(Microsoft)MVP

    十年微软(Microsoft)MVP,七月一日收到邮件,今早收到从美国微软寄过来的证书!