您已经使用Go成功创建了平面文件系统内容管理系统(CMS)。 下一步是采用同样的理想,并使用Node.js制作Web服务器。 我将向您展示如何加载库,创建服务器以及运行服务器。
该CMS将使用第一个教程“ 构建CMS:结构和样式”中列出的站点数据结构。 因此,下载此基本结构并将其安装在新目录中。
获取节点和节点库
在Mac上安装Node.js的最简单方法是使用Homebrew 。 如果尚未安装Homebrew,教程Homebrew Demystified:OS X的Ultimate Package Manager将向您展示如何。
要将Node.js与Homebrew一起安装,请在终端中输入以下说明:
brew install node
完成后,您将在Mac上完全安装node和npm命令。 对于所有其他平台,请遵循Node.js网站上的说明。
注意:许多软件包管理器当前正在安装Node.js版本0.10。 本教程假定您具有5.3或更高版本。 您可以通过键入以下内容检查版本:
node --version
node
命令运行JavaScript解释器。 npm
命令是Node.js的软件包管理器,用于安装新库,创建新项目和运行项目脚本。 Envato Tuts +上有许多关于Node.js和NPM的出色教程和课程。
要安装Web服务器的库,必须在Terminal.app或iTerm.app程序中运行以下命令:
npm install express --save
npm install handlebars --save
npm install moment --save
npm install marked --save
npm install jade --save
npm install morgan --save
Express是一个Web应用程序开发平台。 它类似于Go中的goWeb库。 把手是用于创建页面的模板引擎。 Moment是一个用于处理日期的库。 Marked是JavaScript中很棒的Markdown到HTML转换器。 Jade是一种HTML速记语言,可轻松创建HTML。 Morgan是Express的中间件库,可生成Apache标准日志文件 。
安装库的另一种方法是下载本教程的源文件 。 下载并解压缩后,在主目录中键入以下内容:
npm --install
这将安装创建该项目所需的一切。
nodePress.js
现在您可以开始创建服务器了。 在项目的顶部目录中,创建一个名为nodePress.js的文件,在您选择的编辑器中将其打开,然后开始添加以下代码。 我将解释放置在文件中的代码。
//
// Load the libraries used.
//
var fs = require('fs');
var path = require("path");
var child_process = require('child_process');
var process = require('process');
var express = require('express'); // http://expressjs.com/en/
var morgan = require('morgan'); // https://github.com/expressjs/morgan
var Handlebars = require("handlebars"); // http://handlebarsjs.com/
var moment = require("moment"); // http://momentjs.com/
var marked = require('marked'); // https://github.com/chjj/marked
var jade = require('jade'); // http://jade-lang.com/
服务器代码从用于制造服务器的所有库的初始化开始。 没有网址注释的库是内部Node.js库。
//
// Setup Global Variables.
//
var parts = JSON.parse(fs.readFileSync('./server.json', 'utf8'));
var styleDir = process.cwd() + '/themes/styling/' + parts['CurrentStyling'];
var layoutDir = process.cwd() + '/themes/layouts/' + parts['CurrentLayout'];
var siteCSS = null;
var siteScripts = null;
var mainPage = null;
接下来,我设置所有全局变量和库配置。 全局变量的使用不是最佳的软件设计实践,但它确实有效并且可以快速开发。
parts
变量是包含网页所有部分的哈希数组。 每个页面都引用此变量的内容。 它以服务器目录顶部的server.json文件的内容开头。
然后,我使用server.json文件中的信息来创建用于此站点的styles
和layouts
目录的完整路径。
然后将三个变量设置为空值: siteCSS
, siteScripts
和mainPage
。 这些全局变量将包含所有CSS,JavaScript和主索引页面的内容。 这三个项目是任何Web服务器上最需要的项目。 因此,将它们保留在内存中可以节省时间。 如果server.json文件中的Cache
变量为false,则每次请求都将重新读取这些项目。
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
});
此代码块用于配置Marked库以从Markdown生成HTML。 通常,我会启用表和smartLists支持。
parts["layout"] = fs.readFileSync(layoutDir + '/template.html', 'utf8');
parts["404"] = fs.readFileSync(styleDir + '/404.html', 'utf8');
parts["footer"] = fs.readFileSync(styleDir + '/footer.html', 'utf8');
parts["header"] = fs.readFileSync(styleDir + '/header.html', 'utf8');
parts["sidebar"] = fs.readFileSync(styleDir + '/sidebar.html', 'utf8');
//
// Read in the page parts.
//
var partFiles = fs.readdirSync(parts['Sitebase'] + "parts/");
partFiles.forEach(function(ele, index, array) {
parts[path.basename(ele, path.extname(ele))] = figurePage(parts['Sitebase'] + "parts/" + path.basename(ele, path.extname(ele)));
});
parts
变量进一步从styles
和layout
目录中装入了零件。 site
目录内parts
目录中的每个文件也都加载到parts
全局变量中。 不带扩展名的文件名是用于存储文件内容的名称。 这些名称在Handlebars宏中展开。
//
// Setup Handlebar's Helpers.
//
//
// HandleBars Helper: save
//
// Description: This helper expects a
// "<name>" "<value>" where the name
// is saved with the value for future
// expansions. It also returns the
// value directly.
//
Handlebars.registerHelper("save", function(name, text) {
//
// Local Variables.
//
var newName = "", newText = "";
//
// See if the name and text is in the first argument
// with a |. If so, extract them properly. Otherwise,
// use the name and text arguments as given.
//
if(name.indexOf("|") > 0) {
var parts = name.split("|");
newName = parts[0];
newText = parts[1];
} else {
newName = name;
newText = text;
}
//
// Register the new helper.
//
Handlebars.registerHelper(newName, function() {
return newText;
});
//
// Return the text.
//
return newText;
});
//
// HandleBars Helper: date
//
// Description: This helper returns the date
// based on the format given.
//
Handlebars.registerHelper("date", function(dFormat) {
return moment().format(dFormat);
});
//
// HandleBars Helper: cdate
//
// Description: This helper returns the date given
// in to a format based on the format
// given.
//
Handlebars.registerHelper("cdate", function(cTime, dFormat) {
return moment(cTime).format(dFormat);
});
下一部分代码定义了我定义用于Web服务器的Handlebars帮助器: save
, date
和cdate
。 保存帮助器允许在页面内创建变量。 此版本支持goPress版本,其中参数的名称和值一起用“ |”分隔。 您还可以使用两个参数指定保存。 例如:
{{save "name|Richard Guay"}}
{{save "newName" "Richard Guay"}}
Name is: {{name}}
newName is: {{newName}}
这将产生相同的结果。 我更喜欢第二种方法,但是Go中的Handlebars库不允许使用多个参数。
date
和cdate
帮助器根据moment.js库格式化规则来格式化当前日期( date
)或给定日期( cdate
)。 cdate
帮助器希望将要呈现的日期作为第一个参数,并具有ISO 8601格式。
//
// Create and configure the server.
//
var nodePress = express();
//
// Configure middleware.
//
nodePress.use(morgan('combined'))
现在,代码创建了一个Express实例,用于配置实际的服务器引擎。 nodePress.use()
函数设置中间件软件。 中间件是在每次调用服务器时得到的任何代码。 在这里,我设置了Morgan.js库来创建正确的服务器日志输出。
//
// Define the routes.
//
nodePress.get('/', function(request, response) {
setBasicHeader(response);
if((parts["Cache"] == true) && (mainPage != null)) {
response.send(mainPage);
} else {
mainPage = page("main");
response.send(mainPage);
}
});
nodePress.get('/favicon.ico', function(request, response) {
var options = {
root: parts['Sitebase'] + 'images/',
dotfiles: 'deny',
headers: {
'x-timestamp': Date.now(),
'x-sent': true
}
};
response.set("Content-Type", "image/ico");
setBasicHeader(response);
response.sendFile('favicon.ico', options, function(err) {
if (err) {
console.log(err);
response.status(err.status).end();
} else {
console.log('Favicon was sent:', 'favicon.ico');
}
});
});
nodePress.get('/stylesheets.css', function(request, response) {
response.set("Content-Type", "text/css");
setBasicHeader(response);
response.type("css");
if((parts["Cache"] == true) && (siteCSS != null)) {
response.send(siteCSS);
} else {
siteCSS = fs.readFileSync(parts['Sitebase'] + 'css/final/final.css');
response.send(siteCSS);
}
});
nodePress.get('/scripts.js', function(request, response) {
response.set("Content-Type", "text/javascript");
setBasicHeader(response);
if((parts["Cache"] == true) && (siteScripts != null)) {
response.send(siteScripts);
} else {
siteScripts = fs.readFileSync(parts['Sitebase'] + 'js/final/final.js', 'utf8');
response.send(siteScripts);
}
});
nodePress.get('/images/:image', function(request, response) {
var options = {
root: parts['Sitebase'] + 'images/',
dotfiles: 'deny',
headers: {
'x-timestamp': Date.now(),
'x-sent': true
}
};
response.set("Content-Type", "image/" + path.extname(request.params.image).substr(1));
setBasicHeader(response);
response.sendFile(request.params.image, options, function(err) {
if (err) {
console.log(err);
response.status(err.status).end();
} else {
console.log('Image was sent:', request.params.image);
}
});
});
nodePress.get('/posts/blogs/:blog', function(request, response) {
setBasicHeader(response);
response.send(post("blogs", request.params.blog, "index"));
});
nodePress.get('/posts/blogs/:blog/:post', function(request, response) {
setBasicHeader(response);
response.send(post("blogs", request.params.blog, request.params.post));
});
nodePress.get('/posts/news/:news', function(request, response) {
setBasicHeader(response);
response.send(post("news", request.params.news, "index"));
});
nodePress.get('/posts/news/:news/:post', function(request, response) {
setBasicHeader(response);
response.send(post("news", request.params.news, request.params.post));
});
nodePress.get('/:page', function(request, response) {
setBasicHeader(response);
response.send(page(request.params.page));
});
这部分代码定义了实现Web服务器所需的所有路由。 所有路由都运行setBasicHeader()
函数来设置适当的标头值。 对页面类型的所有请求都将唤起page()
函数,而对帖子类型页面的所有请求都将唤起posts()
函数。
Content-Type
的默认值为HTML。 因此,对于CSS,JavaScript和图像, Content-Type
被显式设置为适当的值。
您还可以使用put
, delete
和post
REST动词定义路由。 这个简单的服务器仅使用get
动词。
//
// Start the server.
//
var addressItems = parts['ServerAddress'].split(':');
var server = nodePress.listen(addressItems[2], function() {
var host = server.address().address;
var port = server.address().port;
console.log('nodePress is listening at http://%s:%s', host, port);
});
定义所使用的不同功能之前,最后要做的就是启动服务器。 server.json文件包含DNS名称(此处为localhost
)和服务器的端口。 解析后,服务器的listen()
函数将使用端口号启动服务器。 打开服务器端口后,脚本将记录服务器的地址和端口。
//
// Function: setBasicHeader
//
// Description: This function will set the basic header information
// needed.
//
// Inputs:
// response The response object
//
function setBasicHeader(response) {
response.append("Cache-Control", "max-age=2592000, cache");
response.append("Server", "nodePress - a CMS written in node from Custom Computer Tools: http://customct.com.");
}
定义的第一个函数是setBasicHeader()
函数。 此函数设置响应头,以告知浏览器将页面缓存一个月。 它还告诉浏览器该服务器是nodePress服务器。 如果还需要其他标准标头值,则可以在其中添加它们与response.append()
函数。
//
// Function: page
//
// Description: This function processes a page request
//
// Inputs:
// page The requested page
//
function page(page) {
//
// Process the given page using the standard layout.
//
return (processPage(parts["layout"], parts['Sitebase'] + "pages/" + page));
}
page()
函数将page()
的布局模板以及页面在服务器上的位置发送到processPage()
函数。
//
// Function: post
//
// Description: This function processes a post request
//
// Inputs:
// type The type of post.
// cat The category of the post.
// post The requested post
//
function post(type, cat, post) {
//
// Process the post given the type and the post name.
//
return (processPage(parts["layout"], parts['Sitebase'] + "posts/" + type + "/" + cat + "/" + post));
}
post()
函数与page()
函数类似,除了post具有更多项来定义每个post。 在这一系列服务器中,帖子包含type
,类别和实际post
。 类型是blogs
或news
。 类别为flatcms
。 由于它们代表目录名,因此您可以根据需要进行设置。 只需将命名与文件系统中的名称匹配即可。
//
// Function: processPage
//
// Description: This function processes a page for the CMS.
//
// Inputs:
// layout The layout to use for the page.
// page Path to the page to render.
//
function processPage(layout, page) {
//
// Get the pages contents and add to the layout.
//
var context = {};
context = MergeRecursive(context, parts);
context['content'] = figurePage(page);
context['PageName'] = path.basename(page, path.extname(page));
//
// Load page data.
//
if(fileExists(page + ".json")) {
//
// Load the page's data file and add it to the data structure.
//
context = MergeRecursive(context, JSON.parse(fs.readFileSync(page + '.json', 'utf8')));
}
//
// Process Handlebars codes.
//
var template = Handlebars.compile(layout);
var html = template(context);
//
// Process all shortcodes.
//
html = processShortCodes(html);
//
// Run through Handlebars again.
//
template = Handlebars.compile(html);
html = template(context);
//
// Return results.
//
return (html);
}
processPage()
函数获取要呈现的页面内容的布局和路径。 该函数首先创建parts
全局变量的本地副本,然后在调用figurePage()
函数的结果中添加“内容”主题标签。 然后,将PageName
哈希值设置为PageName
的名称。
然后,此功能使用“把手”将页面内容编译到布局模板中。 之后, processShortCodes()
函数将展开页面上定义的所有短代码。 然后,Handlebars模板引擎再次遍历代码。 浏览器然后接收结果。
//
// Function: processShortCodes
//
// Description: This function takes a string and
// processes all of the shortcodes in
// the string.
//
// Inputs:
// content String to process
//
function processShortCodes(content) {
//
// Create the results variable.
//
var results = "";
//
// Find the first match.
//
var scregFind = /\-\[([^\]]*)\]\-/i;
var match = scregFind.exec(content);
if (match != null) {
results += content.substr(0,match.index);
var scregNameArg = /(\w+)(.*)*/i;
var parts = scregNameArg.exec(match[1]);
if (parts != null) {
//
// Find the closing tag.
//
var scregClose = new RegExp("\\-\\[\\/" + parts[1] + "\\]\\-");
var left = content.substr(match.index + 4 + parts[1].length);
var match2 = scregClose.exec(left);
if (match2 != null) {
//
// Process the enclosed shortcode text.
//
var enclosed = processShortCodes(content.substr(match.index + 4 + parts[1].length, match2.index));
//
// Figure out if there were any arguments.
//
var args = "";
if (parts.length == 2) {
args = parts[2];
}
//
// Execute the shortcode.
//
results += shortcodes[parts[1]](args, enclosed);
//
// Process the rest of the code for shortcodes.
//
results += processShortCodes(left.substr(match2.index + 5 + parts[1].length));
} else {
//
// Invalid shortcode. Return full string.
//
results = content;
}
} else {
//
// Invalid shortcode. Return full string.
//
results = content;
}
} else {
//
// No shortcodes found. Return the string.
//
results = content;
}
return (results);
}
processShortCodes()
函数将网页内容作为字符串,并搜索所有短代码。 简码是类似于HTML标签的代码块。 一个例子是:
-[box]-
<p>This is inside a box</p>
-[/box]-
此代码在HTML段落周围有一个用于box
的简码。 HTML使用<
和>
,简码使用-[
和]-
。 名称之后,包含或不包含简码参数的字符串。
processShortCodes()
函数查找简码,获取其名称和参数,查找末尾以获取内容,处理短码的内容,执行包含参数和内容的简码,将结果添加至完成的页面,然后搜索页面其余部分中的下一个简码。 通过递归调用函数来执行循环。
//
// Define the shortcodes function array.
//
var shortcodes = {
'box': function(args, inside) {
return ("<div class='box'>" + inside + "</div>");
},
'Column1': function(args, inside) {
return ("<div class='col1'>" + inside + "</div>");
},
'Column2': function(args, inside) {
return ("<div class='col2'>" + inside + "</div>");
},
'Column1of3': function(args, inside) {
return ("<div class='col1of3'>" + inside + "</div>");
},
'Column2of3': function(args, inside) {
return ("<div class='col2of3'>" + inside + "</div>");
},
'Column3of3': function(args, inside) {
return ("<div class='col3of3'>" + inside + "</div>");
},
'php': function(args, inside) {
return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + inside + "</pre></div>");
},
'js': function(args, inside) {
return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: javascript'>" + inside + "</pre></div>");
},
'html': function(args, inside) {
return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: html'>" + inside + "</pre></div>");
},
'css': function(args, inside) {
return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: css'>" + inside + "</pre></div>");
}
};
下一部分定义了shortcodes
json结构,该结构定义了与其功能相关联的shortcode的名称。 所有shortcode函数都接受两个参数: args
和inside
。 args
是名称和空格之后以及标签关闭之前的所有内容。 inside
是开始和结束shortcode标记所包含的所有内容。 这些功能是基本功能,但是您可以创建一个简码来执行您在JavaScript中可以想到的任何事情。
//
// Function: figurePage
//
// Description: This function figures the page type
// and loads the contents appropriately
// returning the HTML contents for the page.
//
// Inputs:
// page The page to load contents.
//
function figurePage(page) {
var result = "";
if (fileExists(page + ".html")) {
//
// It's an HTML file. Read it in and send it on.
//
result = fs.readFileSync(page + ".html");
} else if (fileExists(page + ".amber")) {
//
// It's a jade file. Convert to HTML and send it on. I
// am still using the amber extension for compatibility
// to goPress.
//
var jadeFun = jade.compileFile(page + ".amber", {});
// Render the function
var result = jadeFun({});
} else if (fileExists(page + ".md")) {
//
// It's a markdown file. Convert to HTML and send
// it on.
//
result = marked(fs.readFileSync(page + ".md").toString());
//
// This undo marked's URI encoding of quote marks.
//
result = result.replace(/\"\;/g,"\"");
}
return (result);
}
figurePage()
函数接收服务器上页面的完整路径。 然后,此功能会根据扩展名将其测试为HTML,Markdown或Jade页面。 我仍在将.amber用于Jade,因为那是我与goPress服务器一起使用的库。 在将Markdown和Jade的所有内容传递给调用例程之前,都将其翻译为HTML。 由于Markdown处理器会将所有引号翻译为"
,我先将它们翻译回来,然后再传回。
//
// Function: fileExists
//
// Description: This function returns a boolean true if
// the file exists. Otherwise, false.
//
// Inputs:
// filePath Path to a file in a string.
//
function fileExists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
}
fileExists()
函数替代了fs.exists()
函数,该函数曾经是Node.js fs
库的一部分。 它使用fs.statSync()
函数尝试获取文件的状态。 如果发生错误,则返回false
。 否则,它返回true
。
//
// Function: MergeRecursive
//
// Description: Recursively merge properties of two objects
//
// Inputs:
// obj1 The first object to merge
// obj2 The second object to merge
//
function MergeRecursive(obj1, obj2) {
for (var p in obj2) {
try {
// Property in destination object set; update its value.
if (obj2[p].constructor == Object) {
obj1[p] = MergeRecursive(obj1[p], obj2[p]);
} else {
obj1[p] = obj2[p];
}
} catch (e) {
// Property in destination object not set; create it and set its value.
obj1[p] = obj2[p];
}
}
return obj1;
}
最后一个函数是MergeRecursive()
函数。 它将第二个传递对象复制到第一个传递的对象中。 在添加页面特定的部分之前,我利用它将主要parts
全局变量复制到本地副本中。
本地运行
保存文件后,可以使用以下命令运行服务器:
node nodePress.js
或者,您可以使用package.json文件中的npm
脚本。 您可以这样运行npm脚本:
npm start
这将运行package.json文件中的start
脚本。
将您的网络浏览器指向http://localhost:8080
,您将看到上面的页面。 您可能已经注意到,我在主页上添加了更多测试代码。 页面的所有更改都在本教程的下载中。 它们只是一些小调整,可以更完整地测试功能并适应使用不同库所带来的任何差异。 最明显的区别是Jade库不使用$
来命名变量,而Amber则使用$
来命名变量。
结论
现在,您在Go和Node.js中拥有完全相同的平面文件系统CMS。 这仅是您可以使用此平台构建的内容的起点。 实验并尝试一些新的东西。 那是创建自己的Web服务器的最佳部分。
翻译自: https://code.tutsplus.com/tutorials/build-a-cms-nodepress--cms-25633