這個系列的文章能夠看做我的項目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的格式以供讀者閱讀了...
第一篇結束,多謝觀看。
請!
看!
下!
集!
有沒有點黑貓警長的範兒~ ;)