页面模板
可能是因为公司里写代码用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写到磁盘上。
这样,就完成了从页面上选择文件,到后台保存的简单流程。
|