頁面模板
可能是因為公司裡寫代碼用velocity習慣了,找了google的ctemplate來實作類別似的功能。ctemplate相對velocity比較簡單,只有變數替換,簡單的引用、迴圈。這裡有ctemplate的詳細文檔,相對來說,就是不能在模板上有太多的邏輯。
首先,前一篇裡面貼了dispatcher,通過apache中設定環境變數,設定模板跟路徑:
| 代碼如下 |
複製代碼 |
| ctemplate::Template::SetTemplateRootDirectory(input.getenv("TEMPLATE_PATH")); |
然後,通過綁定的處理函數,調用真正的處理uri的函數後,將輸出的dictionary對象,和模板合并:
| 代碼如下 |
複製代碼 |
std::string templatePath;
if(context.dict) //has dict
{
//get template
templatePath = context.dict->name() + ".tpl";
//expend
std::string out;
bool expandResult = ctemplate::ExpandTemplate(templatePath, ctemplate::STRIP_WHITESPACE, context.dict.get(), &out);
if(expandResult) //good, we expend template success
{
context.ostream << cgicc::HTTPHTMLHeader();
context.ostream << out;
}
else //oops, we response 500
{
context.ostream << cgicc::HTTPStatusHeader(500, "Internal Server Error");
context.ostream << "fail to expand template: " << templatePath;
}
}
else
{
//如果沒有字典,由對應的函數自己輸出
} |
這裡是有字典輸出的邏輯。擷取根字典的名字,加上尾碼,在根路徑中尋找對應的模板。然後調用ctemplate的函數,將模板展開。最後,將展開後的內容,通過流輸出。
如果沒有字典輸出,預設當成處理函數中已經自己進行了輸出(這裡主要是為了讓處理函數自己輸出json內容)
另外一個分支,如果沒有綁定,那麼直接輸出模板內容:
| 代碼如下 |
複製代碼 |
// find if there is a template
std::string templatePath = path.substr(1) + ".tpl";
if(ctemplate::mutable_default_template_cache()->LoadTemplate(templatePath, ctemplate::STRIP_WHITESPACE))
{
//expend
std::string out;
bool expandResult = ctemplate::ExpandTemplate(templatePath, ctemplate::STRIP_WHITESPACE,
_emptyDict.get(), &out);
if(expandResult) //good, we expend template success
{
context.ostream << cgicc::HTTPHTMLHeader();
context.ostream << out;
}
else //oops, we response 500
{
context.ostream << cgicc::HTTPStatusHeader(500, "Internal Server Error");
context.ostream << "fail to expand template: " << templatePath;
}
}
else //not bind and not find a template file
{
context.ostream << cgicc::HTTPStatusHeader(404, "not find");
context.ostream << "not find";
} |
如果沒有綁定,用一個空的字典,展開模板,也就是直接輸出模板。如果這都沒有找到,那麼就返回404。
一個綁定函數的例子:
| 代碼如下 |
複製代碼 |
void handle(Context &context)
{
std::vector<Volume> volumes;
//FIXME: mock資料
Volume v1 ("1", "a", "cover_1.png", 5);
Volume v2 ("2", "b", "cover_2.png", 1);
volumes.push_back(v1);
volumes.push_back(v2);
boost::shared_ptr<ctemplate::TemplateDictionary> listPageDict(new ctemplate::TemplateDictionary("list"));
for(int i = 0; i < volumes.size(); ++i)
{
ctemplate::TemplateDictionary *listSection = listPageDict->AddSectionDictionary("LIST_SECTIONS");
listSection->SetIntValue("id", volumes[i].id());
listSection->SetValue("name", volumes[i].name());
listSection->SetValue("cover_img_path", volumes[i].cover_path());
}
context.dict = listPageDict;
} |
對應的模板檔案:
| 代碼如下 |
複製代碼 |
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>title</title>
</head>
<body>
<ol>
{{#LIST_SECTIONS}}
<li>name: {{name}}</li>
{{/LIST_SECTIONS}}
</ol>
</body>
</html> |
最後輸出就是剛在Volume對象的name欄位。
uri綁定
前面兩篇部落格講了從uri到模板輸出,還沒有提到中間處理參數的部分。
首先,參數綁定簡單的放在一個map中,這裡用的是boost的unordered_map(也就是hashmap),其實已經可以使用c++11提供的unordered_map了。
boost::unordered::unordered_map<std::string, RequestHandleFunc> _mapping;
這個map的value,是一個functor,具體定義為:
typedef boost::function<void (Context &context)> RequestHandleFunc;
也就是沒有傳回值,參數是Context的函數。
Context結構非常簡單,主要是封裝了輸入和輸出,也就是cgi、ostream、dict等對象,沒有進行太多的抽象。
| 代碼如下 |
複製代碼 |
struct Context
{
cgicc::Cgicc &cgi;
cgicc::CgiInput &input;
std::ostream &ostream;
boost::shared_ptr<ctemplate::TemplateDictionary> dict;
}; |
這裡增加了cgiInput,主要是因為cgicc封裝了常用cgi環境變數,沒法取到自訂環境變數(前一篇部落格介紹過)。
綁定函數非常簡單,就是直接插入,需要注意的是,unordered_map非安全執行緒,沒有線程類似java的concurrent hashmap(google了下intel tbb庫有類似的資料結構),所以採用了boost thread庫中的互斥變數:
| 代碼如下 |
複製代碼 |
void bind(const std::string &path, RequestHandleFunc func)
{
boost::unique_lock<boost::shared_mutex> lock(_mutex);
_mapping.insert(std::pair<std::string, RequestHandleFunc>(path, func));
} |
注意,boost的鎖分為unique_lock和shared_lock,這裡是“寫者”,所以需要獨佔鎖。
處理方法前文已經貼了代碼了,同樣要注意的是,需要在搜尋map的時候加鎖。這裡是“讀者”,所以使用shared_lock即可:
| 代碼如下 |
複製代碼 |
| boost::shared_lock<boost::shared_mutex> lock(_mutex); |
為了方便綁定,寫了個宏,直接綁定。
| 代碼如下 |
複製代碼 |
#define REGISTER_URL(URL, CLASS, METHOD)
CLASS c##CLASS; Handler::instance().bind(URL, boost::bind(&CLASS::METHOD, &c##CLASS, _1))
No Comments |
檔案上傳和cgicc
html中上傳檔案,只需要表單裡面放一個input type=file即可,如果要使用ajax非同步上傳(下文基於jquery),就需要注意幾點(以下操作,部分需要基於html5定義的api):
1、頁面上建立一個input元素,type是file。如果一個input需要支援選擇多個檔案,在標籤中增加屬性:multiple=”multiple”。(注意,這需要瀏覽器支援html5,具體文檔可以見)
2、擷取這個元素繫結的已上傳檔案:
var files = document.getElementById('xxx').files;
先擷取元素,通過讀取元素的files屬性,擷取所有已經選擇的檔案。
3、組裝formdata,因為上傳檔案可能會比較大,所以即使支援多選檔案,這裡還是分成不同的請求發送。
| 代碼如下 |
複製代碼 |
for(var i=0;i<array.length;i++) {
var file = files[index];
var formData = new FormData();
formData.append("file", file);
formData.append("id", i+1);
} |
這裡將每個檔案單獨組裝成一個formdata,裡麵包含檔案內容和一個id。
4、通過jquery發起post請求:
| 代碼如下 |
複製代碼 |
$.ajax({
type: 'POST',
url: "file/new",
data: formData,
async: true,
cache: false,
processData: false,
contentType: false,
xhr: function() {
myXhr = $.ajaxSettings.xhr();
if(myXhr.upload){
myXhr.upload.addEventListener('progress',progressHandlerFunction, false);
}
return myXhr;
},
success: function(data){
if(!data.result) {
progressTag.remove();
$("#" + index).after($('<span style="color:red">'+data.msg+'</span>'));
}
}
}); |
其中的xhr部分稍後會提到,這裡還有兩個需要特別注意的地方。首先是processData屬性,必須設定為false,其次是contentType必須設定為false。其他後端不清楚,cgicc在處理檔案上傳(既multipart方式post的時候),必須從http頭中讀取Content-Disposition屬性,只有在jquery裡面設定了前面提到的兩個屬性,才能正確的按照標準協議進行封裝,讓cgicc讀取到正確的內容分隔字串。
5、錦上添花,增加進度條:
| 代碼如下 |
複製代碼 |
var progressTag = $("<progress/>", {
value: 0,
min: 0,
max: file.size
});
$("#" + index).after(progressTag);
function progressHandlerFunction(evt) {
if (evt.lengthComputable) {
progressTag.attr("max", evt.total);
progressTag.attr("value", evt.loaded);
}
} |
這裡通過jquery動態增加了一個progress標籤(html5中新增),然後定義了一個handler。之前jquery的ajax函數中看見了,通過擷取原生xhr對象,在upload事件上增加handler函數回調,在上傳的同時,將具體進度反饋給使用者。當然,如果需要,這裡也可以綁定download事件。
6、cgicc處理檔案:
cgicc處理檔案和普通表單元素類似,唯一的區別是需要通過cgi.getFile函數來擷取,而不是cgi.getElement。
| 代碼如下 |
複製代碼 |
cgicc::form_iterator idIter = cgi.getElement("id");
cgicc::file_iterator fileIter = cgi.getFile("file");
if(idIter == cgi.getElements().end())
{
//handle error
}
int id = (int)idIter->getIntegerValue();
if(fileIter == cgi.getFiles().end())
{
//handle error
}
std::ofstream of(boost::lexical_cast<std::string>(id));
fileIter->writeToStream(of); |
這裡忽略了錯誤處理,通過擷取表單元素,作為檔案名稱,直接將檔案通過標準庫檔案輸出資料流ofstream寫到磁碟上。
這樣,就完成了從頁面上選擇檔案,到後台儲存的簡單流程。