本文實現了一個簡單的個人漫畫網站,配合之前的漫畫爬蟲使用。享受無處不在的漫畫生活。 github地址:https://github.com/miaoerduo/cartoon-cat-server 歡迎大家star、fork和指教。 ...
小喵的嘮叨話:寒假的時候寫了一個漫畫爬蟲,爬取了好幾個漫畫,不過一直沒有找到合適的漫畫閱讀的工具。因此最近就試著自己寫一個漫畫的網站,放在公網上或者區域網里,這樣就能隨時隨地用手機、Pad看漫畫了。
先放上項目的地址:https://github.com/miaoerduo/cartoon-cat-server ,歡迎大家隨時star、fork和指教。
關於漫畫爬蟲的內容,請參看之前的博客:http://www.miaoerduo.com/python/爬蟲-漫畫喵的100行逆襲.html
本文系原創,轉載請註明出處~
小喵的博客:http://www.miaoerduo.com
博客原文:http://www.miaoerduo.com/nodejs/簡易漫畫網站搭建-漫畫喵server版.html
寫在前面
這裡,我們先試著思考幾個問題:
1、為什麼要做一個漫畫網站,而不是APP呢?
小喵最開始其實是打算做一個QT的漫畫閱讀軟體,這樣能夠在各種操作系統上用。開發了一小段時間之後,覺得用手機或者Pad看漫畫的情況會更多。難道要給手機、平板甚至是PC都編寫一套軟體嗎?這個小喵確實能力上達不到。其次是,如果是本地的APP的話,圖片可能還是需要存到設備上,這樣同樣很麻煩。於是,漫畫網站就成為最適合的選擇,只要在聯網的情況下(或者在同一個區域網中),只要有瀏覽器,就能夠看漫畫。這才是真正的跨平臺,也是小喵要編寫這個工具的原因。
2、那麼,一個簡單的漫畫網站可以怎麼去實現呢?
講道理的話,其實單純的靜態的網頁可能就足夠了,給每個漫畫圖片編寫一個頁面,包含圖片、下一頁、上一頁等部分就可以。頁面的生成可以是一個腳本程式。不過這樣的話,也有點麻煩,而且最終的頁面的數目可能會很多。小喵不喜歡這種方式。
這裡,小喵採用的方式是前、後端分離的方式。
整個網站由三個部分組成:
- 數據:也就是漫畫本身。
- 後臺:後臺程式的功能是根據請求返回用戶漫畫的列表、章節和圖片URL等信息。
- 前端:調用後臺的程式,得到漫畫的信息並友好的進行顯示。
3、最後,我們使用什麼技術來實現呢?
前端的話,使用標準的Html,Css和Js就可以。後臺是個API Server,Python + Flask 或者 Nodejs + Express 都是不錯的選擇(使用起來很簡單。。。),小喵這裡就選用 Nodejs + Express 的方式編寫後臺(一直寫Js就可以了 :P )。另外,大型的漫畫網站,漫畫的信息應該會存到資料庫中,這樣會方便查找和管理。這裡考慮到漫畫數目比較少,就去掉了資料庫這一步驟,直接通過文件操作來得到漫畫信息,工作量也大大減少了。
先看一下最終的效果圖(雖然界面很簡單):

圖1 網站的最終效果圖
現在,喵粉們是不是已經迫不及待的想要動手寫代碼了呢?
一、目錄結構
下麵是小喵的目錄結構:
$ tree blog -N -L 2 blog ├── README.md ├── main.js ├── package.json └── public ├── api.html ├── cartoon.html ├── chapter.html ├── css ├── img ├── index.html ├── js └── store
main.js
這裡就是後臺API的程式。因為功能很簡單,所以就放在一個文件中。
package.json
是nodejs的包管理器,在這裡可以定義依賴。我們這裡只依賴Express。
public
這個文件夾用來存放靜態的資源,包括Html、Css、js、image以及漫畫資源(store)。
二、後臺程式
1. 依賴安裝
原生的 Nodejs 就已經十分適合寫API程式了,Express 只是讓它更加的方便了而已(至少對於這個項目來說)。
Nodejs
的安裝十分簡單,在官網上 https://nodejs.org 有下載鏈接,伺服器上使用 apt-get
等工具也很容易安裝。
Express
的話,可以使用npm install express —save
來安裝。這裡小喵使用的package的方式來安裝。在項目根目錄新建package.json文件,寫入配置信息:
{ "name": "cartoon-cat-server", "version": "0.0.1", "dependencies": { "express": "visionmedia/express" } }
之後使用 npm install
命令就可以完成安裝。安裝完成之後會發現根目錄多了一個 node_modules
文件夾,裡面就是我們的依賴庫了。喵粉們如果下載了我的這個項目的話,第一步也是要進入項目目錄然後輸入 npm install
。
2. 漫畫的文件結構
我們的漫畫資源都是通過 漫畫喵
這個爬蟲工具下載下來的,因此漫畫都是每個章節都是一個文件夾,每個章節的漫畫圖片都放在對應的文件夾中,而且按照頁碼來命名。
這樣通過遍歷文件夾似乎就能獲取漫畫的信息了!
漫畫列表和章節中的圖片列表都可以通過上述的方式來解決,但是章節的列表卻不行。因為漫畫的章節有時候並沒有明確的順序(比如突然出現一個番外篇啥的),這樣遍歷文件夾預設的順序(按名稱)就可能是錯誤的。
我們有兩個解決辦法:
- 按照文件夾的創建時間來顯示文件名。這樣有點不靈活。
- 在每個漫畫的根目錄建一個文本文件,用來存放章節的信息。
小喵選擇第二種策略,創建這個list的方法十分簡單粗暴,在漫畫目錄下麵使用:
ls -t -r > index
ls
是linux上面的顯示目錄的工具,-t
表示按時間排序(最上面是最新的),-r
表示倒序,>
是重定向,最終輸出到index這個文件。然後編輯這個文件,刪掉index這一行(系統貌似是先生成index這個文件,然後再執行ls,最後把結果輸入到文件中,因此文件裡面多了一個index的文件名),再做一些必要的調整。
Windows上可以使用:
dir /OD /B > index
dir
是windows的查看目錄的命令,/OD
表示按照時間排序,/B
表示只顯示文件名,>
重定向到index。windows上的這個列表文件中也會出現index這個文件名(看來各種操作系統都一樣)。另外需要註意的是windows的換行和linux或mac不一樣。
這樣,我們就可以通過讀這個index文件來獲取章節的信息了。
最終的漫畫的結構(為了顯示的方便,刪除了很多圖片和章節)如下:
$ tree store -N -L 3 store ├── 犬夜叉 │ ├── index │ ├── 第1章 │ │ ├── 00001.jpg │ │ └── 00002.jpg │ └── 第2章 │ ├── 00001.jpg │ └── 00002.jpg └── 極黑的布倫希爾特 ├── index ├── 第1章 │ ├── 00001.jpg │ └── 00002.jpg └── 第2章 ├── 00001.jpg └── 00002.jpg
3. API 編寫
Express十分的容易使用。這裡小喵給一個官網的Hello World的教程讓大家看一下:
var express = require('express') var app = express() app.get('/', function (req, res) { res.send('Hello World!') }) app.listen(3000, function () { console.log('Example app listening on port 3000!') })
將上述代碼保存成 main.js,使用 node main.js
就可以啟動這個程式,然後用瀏覽器訪問 http://localhost:3000
,就能看到一個顯示著 hello world 的頁面。
require
語句用來引入依賴,app
是express的封裝的對象。通過 app.get
方法就可以給指定的url(官方說法叫route)綁定相應的處理方法(GET方法的請求)。處理函數有2個參數 req
表示request,也就是用戶的請求,通過這個對象我們可以獲取用戶的輸入的參數,res
表示response,是一個向用戶返回數據的對象。
listen
用來監聽一個埠啟動服務。
這裡小喵先給出自己定義的一些輔助的函數,定義錯誤信息和參數校驗,後面會使用到:
// 引入依賴 var express = require('express'); var fs = require("fs"); // 即file system,用來進行文件操作 var app = express(); /** * 錯誤提示 */ var ErrorHelper = { 'internal_error': function () { return { 'msg': 'something wrong with server', 'code': 1 }; }, 'missing_param': function (param) { return { 'msg': 'missing param: ' + param, 'code': 2 }; }, 'error_param': function (param, data) { return { 'msg': 'the param ' + param + '(' + data + ') is illegal', 'code': 3 } }, 'not_found': function (param) { return { 'msg': 'cannot find ' + param, 'code': 4 }; } }; /** * 檢查參數格式,只能輸入字母,數字和漢字 */ function checkParam(param) { return /^[\u4e00-\u9fa5_a-zA-Z0-9]+$/.test(param); }
1) get_cartoon_list
這個介面用來獲取所有的漫畫列表。
/** * 獲取漫畫列表 */ app.get('/get_cartoon_list', function (req, res) { fs.readdir(__dirname + '/public/store', function (err , files) { if (err) { res.jsonp(ErrorHelper.internal_error()); } res.jsonp({'cartoon': files, 'code': 0}); }); });
這個函數十分的簡單,通過 fs
讀取store中的文件名,然後用json的格式返回回去。這裡小喵用的jsonp,為瞭解決跨域請求的問題,不過我們的頁面和服務是一臺機器的,所以這部分並不需要。
2) get_chapter_list
這個介面用來獲取漫畫的章節的信息,所以需要輸入參數,這裡定為cartoon。
/** * 獲取章節信息 */ app.get('/get_chapter_list', function (req, res) { var cartoon = req.query.cartoon; if (!cartoon) { res.jsonp(ErrorHelper.missing_param('cartoon')); return; } if (!checkParam(cartoon)) { res.jsonp(ErrorHelper.error_param('cartoon', cartoon)); return; } var cartoon_dir = __dirname + '/public/store/' + cartoon; fs.exists(cartoon_dir + '/index', function (exists) { if (!exists) { res.jsonp(ErrorHelper.not_found(cartoon)); return; } fs.readFile(cartoon_dir + '/index', function (err, data) { if (err) { res.jsonp(ErrorHelper.internal_error()); return; } var chapter_list = data.toString().split('\n').filter(function (d) { return d.length > 0; }); res.jsonp({'chapter': chapter_list, 'code': 0}); }); }); });
首先判斷輸入的參數,之後判斷對應漫畫的文件夾中是否有index這個文件,如果有的話就讀取然後返回給用戶。
3) get_img_list
這個介面用來返回漫畫的具體章節的圖片的URL,用戶需要輸入漫畫名(cartoon)和章節名(chapter)。註意要修改自己的HOST的地址。
var HOST = "localhost"; // 如果不是在本機上使用,請改成實際的ip地址 // 後面的圖片的URL會使用這個變數來構造 var PORT = 3000; app.get('/get_img_list', function (req, res) { var cartoon = req.query.cartoon; if (!cartoon) { res.jsonp(ErrorHelper.missing_param('cartoon')); return; } if (!checkParam(cartoon)) { res.jsonp(ErrorHelper.error_param('cartoon', cartoon)); return; } var chapter = req.query.chapter; if (!chapter) { res.jsonp(ErrorHelper.missing_param('chapter')); return; } if (!checkParam(chapter)) { res.jsonp(ErrorHelper.error_param('chapter', chapter)); return; } var cartoon_dir = __dirname + '/public/store/' + cartoon; fs.exists(cartoon_dir + '/index', function (exists) { if (!exists) { res.jsonp(ErrorHelper.not_found(cartoon)); return; } fs.readdir(cartoon_dir + '/' + chapter, function (err, images) { if (err) { res.jsonp(ErrorHelper.error_param('chapter', chapter)); return; } // 按名字排序 images.sort(function (lhs, rhs) { return parseInt(lhs.split('.')[0]) - parseInt(rhs.split('.')[0]); }); var urls = images.map(function (image) { return 'http://' + HOST + ':' + PORT + '/store/' + cartoon + '/' + chapter + '/' + image; }); res.jsonp({'img': urls, 'code': 0}); }); }); });
這是目前最複雜的函數了,先檢查參數,然後判斷漫畫是否存在,再判斷章節是否存在,列出章節文件夾裡面的圖片名,並按數字的順序排序。最終構造成URL,返回給用戶。
4) 靜態資源
public文件夾中的資源都是靜態資源,用戶可以通過URL訪問。在這裡Nodejs也是支持的:
app.use('/', express.static('public'));
不過Nodejs本身並不適這種靜態資源的工作,所以如果是生產環境中,建議大家還是使用Nginx等工具,讓Nodejs安心的處理業務邏輯吧。
5) 啟動服務
var server = app.listen(PORT, function () { console.log("應用實例,訪問地址為 http://%s:%s", HOST, PORT); });
三、前端
1. Ajax
前端使用Ajax就可以很容易完成,相信即使是前端小白也能實現,而且還比小喵做的好看(無奈臉)。小喵使用了JQuery 來處理Ajax的內容,界面庫使用了Metro,然而即使這樣也沒有提高網站的顏值。
源碼可以從github上下載到,所以小喵就不重點介紹前端了。
2. 懶載入
有一點需要註意,在漫畫圖片的頁面中,通常會出現大量的圖片,如果只是簡單的使用 img
標簽的話,可能會導致瀏覽器同時載入所有的圖片,如果網速不好的話,我們的體驗也會相當的差(區域網請無視)。所以我們使用一種懶載入的策略,只有可見的圖片才會載入。然後小喵就從github上找相關的插件,然後發現了一個使用比較方便的代碼,還有詳細的原理介紹,感興趣的話大家可以看一下。
圖片懶載入插件實戰:http://www.cnblogs.com/beidan/p/5648240.html
插件的github:https://github.com/beidan/lazeLoadImg
四、寫在後面
至此,我們就搭建好了一個可以隨時玩耍的個人漫畫網站了。喵粉們感興趣可以star、fork這個項目,如果喜歡開發的話,能幫忙一起提高網站的顏值就更好了 O(∩_∩)O哈!
項目地址:https://github.com/miaoerduo/cartoon-cat-server ,歡迎大家隨時star、fork和指教。
PS. 請搭配漫畫喵爬蟲版一起食用:https://github.com/miaoerduo/cartoon-cat
使用的話,按如下的流程:
git clone [email protected]:miaoerduo/cartoon-cat-server.git cd cartoon-cat-server npm install node main.js
另外,為了避免程式突然崩掉,建議大家使用forever這個工具。上面的流程的最後一句node main.js
就可以改成下麵的。
npm install forever -g
forever start main.js
這樣,我們的程式就更健壯了。
最後,小喵再說一句,這個項目有很多的缺陷,比如直接訪問文件,Nodejs直接管理靜態文件,需要提供奇怪的參數,沒有驗證用戶等等。因此不適合真的生成中的使用。不過,自娛自樂應該是足夠了。小喵自己是放在宿舍的樹莓派里,睡前躺床上看會兒漫畫。
覺得不錯的話,請點個大大的推薦~~
希望小喵能和大家一起學習和進步~~
轉載請註明出處~