这个系列的文章能够看做我的项目blog-node的记录,同时也算一个nodejs 进阶版hello world范例。但愿能够为像我同样想快速入门nodejs的你们一点小小的方向。本身也是刚刚开始进入nodejs世界,所以文中或有一些初级的,入门的,甚至多是错误的知识与观点,还请不吝指正。html
由于是“进阶版”hello world,所以本文会跳过一些我认为即便对我这样的新手来讲也很是基础的地方而略过不提,好比如何搭建环境,如何使用npm等等,网上关于这些的文章已经足够多了。node
因此,若是你对Nodejs还一无所知,强烈推荐这本小书:Node入门,篇幅不长但讲解详细而又浅显易懂。相信是个极好的学习开端。git
最后,文章内的全部代码几乎均可以在本项目中找到,或许偶有为了讲解方便而略做修改之处。欢迎任何的意见和批评~github
不知道你们有没有看过像黑客同样写博客或是相似的介绍利用Jekyll来进行博客写做的文章?本着不折腾会死不造轮子不幸福的码农精神。今天来试试从头搭建一个类jekyll的静态网站。这个网站应该至少能知足如下需求:web
若是您曾尝试过jekyll或相似系统,或许就会发现,以上列出的基本就是一个minimal jekyll website。 那么, 接下来分析一下如何实现。chrome
不消说咱们要开发的是一个网站。基础技术也已肯定NodeJs不作他选(题目就是这个嘛)。数据库
时下火热的 MEAN Web-Dev Stack一定值得一试。不过根据初期需求来看,静态博客暂时不须要引入数据库或复杂的页面交互结构,为了快速实现一个原型,目前仅需 M.E.A.N 中的 E[xpress] + N[odejs] 就很足够了。express
关于Express或是Nodejs的基础知识,例如开发环境搭建什么的,这里就再也不重复。我本身则是在Mac/Ubuntu/Windows下同步进行后文中的全部工做,基本就是Nodejs环境 + SublimeText/Vim + Git(跨平台的软件就是一个赞)通吃全部。npm
直接进入主题。json
撩起袖子大干一场以前,先给以后的工做作一个简单的分解和排序:细化一下上文中的需求列表,咱们的第一个目标就是把文章显示到页面上!
...确实简单了点儿,不过由浅入深嘛。借此机会了解一下Nodejs与Express的基本知识。
新建项目
|--myblog-node | |--app.js | |--package.json
package.json为咱们提供了一个统一控制包依赖关系以及程序自描述的入口,唔,你大概能够把他想像成C#项目中sln/csproj文件之类的东西(=。=暴露了,其实我是个.NET农民)。内容以下:
{ "name": "my-blog", "version": "0.0.1", "author": "NarK", "dependencies":{ "express": "4.x" } }
内容很清楚不解释,但在这里能够看到package.json所能作的远不止这些,咱们之后再谈。保存后运行npm install
等待依赖包安装完成。
app.js则是咱们从此的主程序文件。
//app.js var express = require('express'); var app = express(); app.get('/', function(req, res) { res.send('hello world'); }); app.listen(3000);
以上代码直接从Express官网API Doc复制,顺便一说,本项目采用目前最新的4.x版本。
这样实现了一个express下的最简server,打开浏览器访问 http://localhost:3000 便可见效。
OK,那么本文到此为止。
...
...
...
...开个玩笑。从上述代码可见,Express中的路由控制不须要咱们再去手动解析request,而后苦哈哈的写上一句又一句if (method === 'POST' && path === '/home')
之类的判断。取而代之的是至关形象而易写的 app.verb(path, callback)
方法。verb 能够是 post,get 等等。
而在请求处理方法中,res.send(content)
也提供了一个简单的响应方式,若是不显示指定Content-Type
,express会根据send方法的参数自动推定响应类型,列个表格出来:
Data Type Content-Type Buffer application/octet-stream String text/html Array/Object Json representation Number return a respond text: 200 <=> "OK" for example
参见res.send()。
因而很明显,接下来咱们无非是把文章内容从文件中读取出来,再res.send()一下就ok。
查阅一下nodejs中与文件系统相关的api,加上读取文件的代码后,app.js的内容以下:
//app.js var express = require('express'); var fs = require('fs'); var app = express(); app.get('/', function(req, res) { fs.readFile('./blogs/test.md', function (err, data) { if (err) res.send(err); res.send(data); }); }); app.listen(3000);
固然了,在运行前记得先建立blogs文件夹与test.md文件(随手写点内容咯,否则看不到效果)。
编码完工~切换到终端,输入 nodemon app.js
,咱们来看一下是否成功读取了本博客的第一篇文章。
哦对了,强烈推荐一个小工具 nodemon 。一句话简介:全局安装了nodemon后,咱们能够经过nodemon xxx.js的方式启动nodejs程序,而在此方式下启动的程序会自动侦测与本程序相关的文件,随时自动重启进程以反映最新的变化。实乃nodejs开发debug过程当中必备利器!
言归正传,我志得意满的打开chrome浏览器访问localhost:3000,意料中的文字却没有出现,反而弹出了一个文件下载询问框。shit!谁告诉我send()方法会自动推定Content-Type的!?打开网络侦测一看,果不其然,返回的Content-Type是 application/octet-stream
。(经测试,在FireFox中一样提示下载文件,有点搞笑的是,IE11却是老老实实的直接在页面显示了文件内容...IE大哥你怎么老跟别人不同啊...)
Well~我从新翻阅了nodejs的文档,对于fs.readFile(path, callback (err, data))
的解释最后有一句话:
If no encoding is specified, then the raw buffer is returned.
得得~这就是看文档不仔细的后果。查阅上文表格可见,buffer
对应的content-type
确实是application/octet-stream
来着,修改代码:
fs.readFile('./blogs/test.md', 'utf-8', function (err, data) { if (err) res.send(err); res.send(data); });
刷新页面(nodemon已经在咱们保存代码文件时自动重启: [nodemon] restarting due to changes...
),当当~成功显示!啥?你说这个页面一点儿都很差看?不要在乎这些细节...咱们根本就是原样输出了markdown文件内容,连html都没转换,固然好看不了,稍安勿躁~
以上,咱们创建了一个最简单的Hello, Express项目,介绍了package.json,express中的简单路由控制,res.send()
,fs.readFile()
,以此完成了一个读取本地文件显示到页面的功能。接下来,咱们会在此基础上,完成一个自动检测全部指定格式的blog文件,并一一映射到对应URL的功能。
很明显,咱们的网站不会只有一篇博文,网站的首页也不该该直挺挺的就打印出一篇文章来。因此下一步是构思一下网站的路由结构,
|-- Home | |--Blog | | |--blogA | | |--blogB | | |--blog... | |--xxx
那么,首页天然应该显示一个文章列表,点击文章后导向一个 host/blog/xxxxx
的url,显示对应文章的内容。至关常见的组织方式~
有了上节的经验,咱们很快就能写出相似于这样的代码:
//... app.get('/blog/blogA', function (req, res) { fs.readFile('./blogs/blogA.md', 'utf-8', function (err, data) { if (err) res.send(err); res.send(data); }); });
以此类推,有两百篇文章就写上两百个这样的路由方法=。=
固然不是这样...因而,为了能够批量读取到全部指定目录下的markdown文件,咱们势必要给它订立一个标准的命名格式,好比:*.md,只要是markdown文件的我都认;不过或许咱们能够把标准订的更严格一些。
好比:模仿jekyll的默认命名格式: yyyy-MM-dd-blog-title.md
这样一来咱们甚至能够依靠文件名就简单的为他们作一个按日期分组。或者直接就体如今url上,好比 host/blog/2014/04/30/express-plus-nodejs-making-my-own-blog
。
因而,咱们的路由方法能够从两百个变成一个 ;)
app.get('/blog/:year/:month/:day/:title', function (req, res) { var fileName = './blogs/' + req.params.year + '-' + req.params.month + '-' + req.params.day + '-' + req.params.title + '.md'; fs.readFile(fileName, 'utf-8', function (err, data) { if (err) { res.send(err); } res.send(data); }); });
这里咱们认识了Express又一个十分方便的路由功能:唔..我不知道它叫啥,姑且称为命名请求参数?总之,在 app.verb(url, callback (req, res))
中的url上,咱们可使用 :argname
的形式为url的部分字符命名,而后经过req.params.argname
获取,如上所示。
确保创建了正确的文件夹与按规则命名的markdown文件后,启动程序访问一下看看咯~
这样,利用 yyyy-MM-dd-blog-title.md
的命名规则,配合Express中的命名请求参数:形如 /blog/:year/:month/:day/:title
这样的url来解析指向相应的任意md文件。
接下来要作的,就是从本地已有的文件反向获得该文件的url,以今生成一个文章列表供用户点击。
function getBlogList(blogDir) { fs.readdir(blogDir, function (err, files) { var blogList = []; if (files && files.length) { files.forEach(function (filename) { //split file name and generate url... //... //create a blogItem { title: blogTitle, url: blogUrl } blogList.push(blogItem); }); } return blogList; }); }
限于篇幅,我去掉了关于文件名格式的正则验证,对文件名的解析生成url的过程(只是简单的截取字符串而已)等等细节的部分。总之,通过以上繁琐的字符串处理,咱们最终获得了一个形如
{[ { title: 'blogA', url: '/blog/2014/04/01/blogA'}, { title: 'blogB', url: '/blog/2014/05/08/blogB'}, ... ]}
这样的对象。
因而咱们就能够在首页显示全部文章的连接了:
app.get('/', function (req, res) { var html = ''; var blogList = getBlogList('./blog'); if (blogList && blogList.length) { blogList.forEach(function (blog) { html += '<a href="'+ blog.url +'">' + blog.title + '</a><br/>'; }); res.send(html); } else { res.send('No Blogs Found.'); } });
大功告成~如今用户能够经过首页上的列表访问任意一篇存在于blog文件夹下且命名符合规则的文章了。
终于来到了Express中,准确的说是Connect(Express的一个基础组件,固然也能够做为一个单独的框架使用,主要负责了中间件机制的实现)中激动人心的中间件部分。
有关中间件的解释,参见 A short guide to Connect Middleware。 我在这里就很少卖弄本身的浅薄看法了,简单来讲,能够把中间件机制想像成一个层层过滤的污水处理系统(=。=抱歉,可是这是我第一个想到的比喻...)。request
通过一个又一个的中间件,有的结束了处理response
到了客户端,有的则继续流入下一个中间件。
其实在咱们以前的代码中,已经在无心中使用了这一特性:
... app.get('/', function (req, res){ //index page }); app.get('/blog/:year/:month/:day/:title', function (req, res) { //blog page }); ...
app.verb()
其在本质上就是一个带有高级路由功能的中间件,request自上而下首先来到app.get('/')
,判断url是否匹配,匹配则进入处理方法,不然继续"流向"下一个中间件。
那么,若是咱们但愿加上一个自定义的404 Not Found页面的话,应该如何利用中间件的这一特性呢?
简单,只须要把它放在“过滤网”的最底层就行了:
... app.get('/', ...); app.get('/blog/', ...); app.get('/wiki/', ...); ... app.get('*', function (req, res) { res.send(404, "Oops! We didn't find it"); });
能够被解析匹配的请求路径在各自对应的中间件中被一一处理并返回告终果,剩下全部可以到达最底层的请求则是没法被已有路由解析的,因而返回404。
简洁而天然的处理方式!
而有的时候,一个请求在通过首个匹配的中间件处理后,咱们可能还但愿它继续行进到下一个匹配的中间件中去,在处理方法中显式使用next()
便可。
app.use(function (req, res, next) { console.log(req.method + ',' + req.url); next(); }); app.get('/', ...); ...
关于中间件的介绍就到此为止,更多的知识及应用会在后面逐一说起。咱们仍是把重心放回到当前的项目中来。
现在咱们的网站已经能够将用户从主页导向至任意一篇文章,接下来就该把markdown文件正式转换为html的格式以供读者阅读了...
第一篇结束,多谢观看。
请!
看!
下!
集!
有没有点黑猫警长的范儿~ ;)