从零开始,做一个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.mzph.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系统无法上网显…

硬件编程: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、概念结构设计 概念结构设计主要…

坎蒂雷赋权法 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 类中使用 final 关键字字符数组保存字符串&#xff0c; private final char value[] &#xff0c;所以 String 对象是不可变的。 StringBuilder 与 StringBuffer 都继承自 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 工作空间…

事务管理:事务的基本概念笔记

1、事务的意义事务管理是对于一系列数据库操作进行操作。针对多个事务并发执行的数据库当中&#xff0c;如果对共享的数据进行更新操作不进行控制&#xff0c;很有可能会产生数据的不一致性&#xff0c;造成数据库存储无效甚至错误的数据。数据库在运行过程中会受到很多方面的因…

从零开始攻略PHP(8)——面向对象(下)

8.编写代码类 每个分离的函数可以执行一个明确的任务。任务越简单&#xff0c;编写与测试这个函数就越简单&#xff0c;当然也不要将这个函数分得太小——若将程序分成太多的小个体&#xff0c;读起来就会很困难。 使用继承可以重载操作。我们可以替换成一个大的Display()函数&…

vb treeview 展开子节点_C# / VB.NET 在PPT中创建、编辑PPT SmartArt图形

本文介绍通过C#和http://VB.NET程序代码来创建和编辑PPT文档中的SmartArt图形。文中将分两个操作示例来演示创建和编辑结果。使用工具&#xff1a;Spire.Presentation for .NET hotfix 5.9.5dll文件引用&#xff1a;方式1&#xff1a;下载包&#xff1b;Spire.Presentation for…

mysql安装教程8.0.21安装,Windows系统下MySQL8.0.21安装教程(图文详解)

安装建议&#xff1a;尽量不要用.exe进行安装&#xff0c;用压缩包安装&#xff0c;对日后的卸载/版本升级更为方便下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/1、点击上面的下载地址得到zip压缩包2、解压到要安装的目录我这里是E:\database\mysql8\mysql-8.0…