设为首页 收藏本站
查看: 1330|回复: 0

[经验分享] 从零开始,做一个NodeJS博客(四):服务器渲染页面与Pjax

[复制链接]

尚未签到

发表于 2017-2-23 07:07:19 | 显示全部楼层 |阅读模式
  标签: NodeJS

0
  一个星期没更新了 = =
  
一直在忙着重构代码,以及解决重构后出现的各种bug
  
现在CSS也有一点了,是时候把遇到的各种坑盘点一下了

1 听歌排行 API 修复与重构

1.1 修复
  在加载云音乐听歌排行的时候,有时会出现一个奇怪的bug:json数据无法被解析。如下图:
DSC0000.png

  在刷新页面后,问题就会得到解决。此后无论怎么刷新,问题也不会出现。
  过一段时间再次打开页面,会出现相同的问题,刷新之后也可以解决。此时换用其他各种浏览器,都不会出现问题;但一段时间之后仍会重现一次。。。
  那肯定不是浏览器的锅了。把Response的内容复制出来看看。
DSC0001.png

  粘贴,格式化。VSCode报出了4个警告和一个错误;再仔细看一眼,哎,怎么中途截断了?难道是收到的请求不全?
  返回去看看接收请求收到的JSON文件:没错啊,是全的。当然了,因为接下来刷新几次之后就不会在遇到此问题了。在本地测试中也发现,只有服务器启动之后的第一次访问,才会出现这个问题。
DSC0002.png

  找到输出的位置,在这里下断点,开始调试。
  从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({>  });
  });
  
});
  在等待文件操作完成之后才读取数据,而且读到数据后,只取出自己需要用到的部分,存在全局数组bufJSON中当作缓存,顺便提高一下API响应速度。

1.2 重构
  之前,API获取的听歌排行目标用户是写死在代码里的。可以写一个init()函数,初始化它的获取目标用户。
function init(id) {
  userId =>  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版本。
DSC0003.png

  之前我们的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靠前的部分,相当于变量初始化的位置,然后运行测试吧:
DSC0004.png

  好的,效果达到了。

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=&quot;en&quot;>
  

  
<head>
  <meta charset=&quot;UTF-8&quot;>
  <title>Rocka's Node Blog</title>
  
</head>
  

  
<body>
  <h1>Rocka's Node Blog</h1>
  <hr>

  <h3>
  <blockquote>
  <h3>
  <ul>  <h3>Rcecntly Listened</h3>

  <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地址。我这里用的是与文章文件相同位置的地址。
  然后,看看效果:
DSC0005.png

  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=&quot;en&quot;>
  

  
<head>
  <meta charset=&quot;UTF-8&quot;>
  <title>Rocka's Node Blog</title>
  
</head>
  

  
<body>
  <h1>Rocka's Node Blog</h1>
  <hr>

  <h3>
  <blockquote>
  <h3>
  <ul>  <h3>Rcecntly Listened</h3>

  <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>  var contentRegex = /\$\{archive\.content\}/;

  var>  fs.readFile(path.join(root, pathName), (err, data) => {
  var page = plainViewPage;

  var page = page.replace(titleRegex,>  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仓库的实时构建结果。


运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-345831-1-1.html 上篇帖子: Nodejs学习路线图 下篇帖子: 使用Angular和Nodejs搭建聊天室
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表