第 8 章 - 網誌系統設計
簡易伺服器
檔案: staticServer.js
var koa = require('koa');
var fs = require('fs');
var app = module.exports = koa();
var path = require('path');
var extname = path.extname;
app.use(function *() {
var path = __dirname + this.path;
var fstat = yield stat(path);
if (fstat.isFile()) {
this.type = extname(path);
this.body = fs.createReadStream(path);
}
});
if (!module.parent) app.listen(3000);
function stat(file) {
return function (done) {
fs.stat(file, done);
};
}
markdown 格式
Markdown 是目前程式人最常用的書寫格式,這種格式容易寫有容易讀,最後通常會被轉換為 HTML 呈現,目前已經有很多網誌系統都支援 markdown 格式,您現在所看到的這本 gitbook 書,也是用 markdown格式撰寫的!
以下是一個 markdown 的範例
markdown | 顯示結果 |
---|---|
# 標題 ## 子標題 ### 子子標題 內文, __底線__ , *斜體*, **粗體** . 水平線 --- 無號項目 * 項目1 * 項目2 * 項目3 編號項目 1. 項目1 2. 項目2 3. 項目3 yahoo: [Yahoo](http://tw.yahoo.com/). |
標題子標題子子標題內文, 底線 , 斜體, 粗體 . 水平線 無號項目
編號項目
yahoo: Yahoo. |
若您想更進一步了解 markdown 的寫法,請參考下列連結。
showdown 套件: 將 markdown轉為html
範例: showdown_embed.html
<html>
<body>
<table width="100%"><tr><td style="vertical-align:top">
<textarea id="mdBox" style="width:90%; height:300px">
# 標題
## 子標題
### 子子標題
內文, __底線__ ,
*斜體*, **粗體** .
水平線
---
無號項目
* 項目1
* 項目2
* 項目3
編號項目
1. 項目1
2. 項目2
3. 項目3
yahoo: [Yahoo](http://tw.yahoo.com/).
</textarea>
</td><td>
<div id="htmlBox">
htmlBox
</div>
</td></tr></table>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.5.0/showdown.js"></script>
<script>
var mdBox = document.getElementById('mdBox');
var htmlBox = document.getElementById('htmlBox');
var converter = new showdown.Converter();
converter.setOption('tables', true);
var html = converter.makeHtml(mdBox.value);
htmlBox.innerHTML = html;
</script>
</body>
</html>
呈現結果:
showdown 套件 AJAX 顯示檔案
檔案: showdown_ajax.html
<html>
<body>
<table width="100%"><tr><td style="vertical-align:top">
<textarea id="mdBox" style="width:90%; height:300px">
</textarea>
</td><td>
<div id="htmlBox">
htmlBox
</div>
</td></tr></table>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.5.0/showdown.js"></script>
<script>
var load=function(path) {
if (!path.startsWith("/")) path="/"+path;
return $.ajax({
type: "GET",
url: path,
timeout: this.timeout,
data: {}
});
}
var loadFile = function(path) {
load(path).done(function(content) {
var md;
if (typeof content === 'string')
md = content;
else
md = JSON.stringify(content, null, ' ');
$('#mdBox').val(md);
var html = converter.makeHtml(md);
$('#htmlBox').html(html);
})
.fail(function() {
$('#mdBox').val("error!");
});
}
window.onhashchange = function() {
var filepath = window.location.hash.substring(1);
loadFile(filepath);
}
var converter = new showdown.Converter();
converter.setOption('tables', true);
window.location = '#test.md';
</script>
</body>
</html>
呈現結果
專案:wikidown 網誌系統
專案網址: https://github.com/ccckmit/wikidown
安裝方法:
$ git clone https://github.com/ccckmit/wikidown.git
$ cd wikidown
$ node wdserver
Server started: http://localhost:80
Ssl Server started: https://localhost:443
db connect fail
Wikidown 系統用到了上一章的 fdbserver,執行時由於沒有啟動 mongod ,所以只能使用檔案系統,不能用資料庫。
不過 wikidown 並沒有使用到資料庫,而是將資料完全儲存在檔案系統中,所以上述執行方法,雖然沒有啟動 mongodb 伺服器,但功能卻是完全正常的。(因為根本沒用到)
以下是我在 windows 8 上執行此程式的結果:
1 瀏覽畫面
2 https 進入時會顯示可編輯的瀏覽畫面
3 點選 Edit 後會顯示 markdown 格式原始碼的編輯畫面
關於完整的原始碼,請直接閱讀 https://github.com/ccckmit/wikidown 專案。
wikidown 是一個單頁式應用,由於伺服端是採用上一章的 fdbserver 模組,所以在此我們僅列出 client 重要的程式碼部分。
檔案: wd.html
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="web/favicon.ico" rel="icon">
<link href="web/css/bootstrap.min.css" rel="stylesheet">
<link href="web/css/highlight.default.min.css" rel="stylesheet">
<link href="web/css/fileinput.css" media="all" rel="stylesheet"/>
<link href="web/editor.css" rel="stylesheet">
<title></title>
</head>
<body onload="E.init()">
<div id="fb-root"></div>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span id="titleHead" class="titleLink"></span>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right hide" id="menuLogin">
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown">Login</a>
<ul class="dropdown-menu" role="menu">
<li><a onclick="E.login()">Login</a></li>
<li><a onclick="E.logout()">Logout</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-share"></span>
<ul class="dropdown-menu" role="menu">
<li><a onclick="E.facebookShare()">Facebook</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-right">
<input id="staticUrl" type="text" placeholder="static url" class="form-control">
</form>
<form class="navbar-form navbar-right hide" id="menuEdit">
<div class="form-group">
<input id="filepath" type="hidden" class="form-control" placeholder="filepath" aria-describedby="basic-addon1">
<button class="btn btn-success" type="button" onclick="E.show()">Show</button>
<button class="btn btn-success" type="button" onclick="E.edit()">Edit</button>
<button class="btn btn-success" type="button" onclick="E.save()">Save</button>
<button class="btn btn-success" type="button" onclick="E.upload()">Upload</button>
</div>
</form>
</div>
</div>
</nav>
<div id="panelShow" class="tab-pane panel container-fluid"> <!-- Show -->
<div class="row">
<div id="sidebar" class="col-sm-3 col-md-2 sidebar">
<ul id="side" class="nav nav-sidebar"></ul>
</div>
<div id="htmlBox" class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main" style="height:90%;padding:10px;">
{%=html%}
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
</div>
</div>
</div>
<div id="panelEdit" class="tab-pane panel container-fluid"> <!-- 編輯 -->
<br/>
<textarea id="editBox" class="form-control" style="width:100%; height:80%">
{%=text%}
</textarea>
</div>
<div id="panelUpload" class="tab-pane panel container-fluid" style="height:75%">
<center>
<div class="kv-main">
<form enctype="multipart/form-data">
<div class="form-group">
<input id="imageUpload" name="imageUpload" type="file" multiple=true class="file-loading">
</div>
</form>
</div>
</center>
</div>
<div id="panelLogin" class="tab-pane panel container-fluid"> <!-- 登入 -->
<form class="form-signin" role="form">
<input type="text" id="loginUser" class="form-control" required autofocus data-mt="User" placeholder="Account"/>
<input type="password" id="loginPassword" class="form-control" required data-mt="Password" placeholder="Password"/>
<button class="btn btn-lg btn-primary btn-block" type="button" data-mt="Login" onclick="E.Server.login()">Login</button>
</form>
</div>
<script src="web/js/jquery.min.js"></script>
<script src="web/js/bootstrap.min.js"></script>
<script src="web/js/fileinput.js"></script>
<script src="web/js/showdown.js"></script>
<script src="web/js/highlight.min.js"></script>
<script src="web/editor.js"></script>
<script src="web/config.js"></script>
<script src="web/wdlib.js"></script>
<script src="web/wd.js"></script>
</body>
</html>
檔案: web/editor.js
var E = {
path:null,
msgNewFile:'File not found!',
loadCompleted:false,
}
E.init = function() {
$('.panel').css( "display", "none");
E.onhashchange();
if (window.location.protocol === 'https:') {
$('#menuLogin').removeClass('hide');
$('#menuEdit').removeClass('hide');
}
}
E.isLogin = function() {
if (localStorage.wd_login !== "true") {
// 注意:sessionStorage 不能跨頁面持續,所以得用 localStorage
alert('You can not save & edit before login. Please login now !');
E.Server.login();
return false;
}
return true;
}
E.Server = {
timeout : 4000
};
E.Server.save=function(path, text) {
if (!path.startsWith("/")) path="/"+path;
console.log("save");
$.ajax({
type: "POST",
url: "/file"+path,
timeout: this.timeout,
data: { text: text },
statusCode: {
401: function() { // 401:Unauthorized
localStorage.wd_login = "false";
E.isLogin();
}
}
})
.done(function(data) {
console.log("save success");
alert("Save success!");
})
.fail(function() {
alert("Save fail!");
});
}
E.Server.load=function(path) {
if (!path.startsWith("/")) path="/"+path;
return $.ajax({
type: "GET",
url: "/file"+path,
timeout: this.timeout,
data: {}
});
}
E.Server.login=function() {
$.ajax({
type: "POST",
url: "/login",
timeout: this.timeout,
data: { user:$('#loginUser').val(), password:$('#loginPassword').val() },
})
.done(function(data) {
localStorage.wd_login = "true";
alert( "Login success!");
$('#loginPassword').val('');
E.edit();
})
.fail(function() {
localStorage.wd_login = "false";
alert("Login fail!\nDefault : user=root , password=123\nModify or add (user, password) in setting.js for security");
E.login();
});
}
E.Server.logout=function() {
$.ajax({
type: "POST",
url: "/logout",
timeout: this.timeout,
data: {},
})
.done(function(data) {
localStorage.wd_login = "false";
alert( "Logout success!");
E.switchPanel('panelShow');
})
.fail(function() {
alert( "Logout fail!" );
});
}
E.switchPanel=function(name) {
$('.panel').css( "display", "none");
$('#'+name).css( "display", "block");
}
E.login=function() {
E.switchPanel('panelLogin');
}
E.logout=function() {
E.Server.logout();
}
E.edit=function () {
E.switchPanel('panelEdit');
}
E.upload=function() {
if (!E.isLogin()) return;
E.switchPanel('panelUpload');
$("#imageUpload").fileinput({
uploadUrl: "/upload/"+E.path,
maxFileCount: 10,
uploadAsync: false,
uploadExtraData: { path: E.path }
});
}
E.show=function() {
var editText = $('#editBox').val();
var content;
try {
content = JSON.parse(editText);
} catch (e) {
content = editText;
}
var html = E.render(content);
// http://stackoverflow.com/questions/12449890/reload-content-in-modal-twitter-bootstrap
$('#htmlBox').html(html);
$("#htmlBox").animate({ scrollTop: 0 }, "fast");
E.switchPanel('panelShow');
}
E.render=function(content) {
if (typeof content === 'string') {
return "<pre>\n"+content+"</pre>\n";
} else if (content.type==="directory"){
var files = content.files;
var html = "<UL>\n";
var path = E.path;
if (!path.endsWith("/")) path=path+"/";
for (var i=0; i<files.length; i++) {
var rewritePath = path;
html += "<LI><a href='#"+rewritePath+files[i]+"\'>"+files[i]+"</a></LI>\n";
}
html += "</UL>\n";
return html;
}
}
E.loadFile=function(path) {
E.path = path;
E.Server.load(path)
.done(function(content) {
if (typeof content === 'string')
editText = content;
else
editText = JSON.stringify(content, null, ' ');
$('#editBox').val(editText);
E.show();
})
.fail(function() {
$('#editBox').val(E.msgNewFile);
E.show();
});
}
E.onhashchange = window.onhashchange = function() {
var filepath = window.location.hash.substring(1);
E.loadFile(filepath);
}
E.save=function() {
if (!E.isLogin()) return;
var editText = $('#editBox').val();
E.Server.save(E.path, editText);
}