• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

构建一个 CMS:nodePress

您已经使用Go成功创建了一个平面文件系统内容管理系统 (CMS) 。下一步是采取同样的理想并使用node.js制作一个 Web 服务器。我将向您展示如何加载库、创建服务器和运行服务器。

此 CMS 将使用第一个教程“构建 CMS:结构和样式”中所述的站点数据结构。因此,下载这个基本结构并将其安装在一个新目录中。

获取节点和节点库

mac上安装 Node.js 的最简单方法是使用homebrew。如果您还没有安装Homebrew,教程Homebrew Demystified: OS X's Ultimate Package Manager将向您展示如何安装。

要使用 Homebrew 安装 Node.js,请在终端中输入以下指令:

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 库。Handlebars是用于创建页面的模板引擎。Moment是一个处理日期的库。Marked是一个很棒的Markdown到JavaScript中的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/

服务器代码从用于创建服务器的所有库的初始化开始。没有带有 Web 地址的注释的库是内部 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 服务器上请求最多的项目。因此,将它们保存在内存中可以节省时间。如果Cacheserver.json 文件中的变量为 false,则每次请求都会重新读取这些项目。

marked.setOptions({
  renderer: new marked.Renderer(),
  gfm: true,
  tables: true,
  breaks: false,
  pedantic: false,
  sanitize: false,
  smartLists: true,
  smartypants: false
});

此代码块用于配置 Marked 库以从 Markdown 生成 HTML。大多数情况下,我正在打开 table 和 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目录中的部分。parts目录内的每个文件site也被加载到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。save 助手允许在页面内创建变量。此版本支持 goPress 版本,其中参数的名称和值以“|”分隔。您还可以使用两个参数指定保存。例如:

{{save "name|Richard Guay"}}
{{save "newName" "Richard Guay"}}
 
Name is: {{name}}
newName is: {{newName}}

这将产生相同的结果。我更喜欢第二种方法,但 Go 中的 Handlebars 库不允许多个参数。

date和助手根据moment.js库格式化规则格式化cdate当前日期 ( date) 或给定日期 ( cdate) 。帮助程序希望呈现的日期是第一个参数并具有 ISO 8601 格式。cdate

//
// 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-TypeHTML。因此,对于 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()函数将页面的布局模板和页面在服务器上的位置发送给该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()只是帖子有更多的项目来定义每个帖子。在这一系列服务器中,一个帖子包含一个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 散列值设置为页面的名称。

然后,此函数使用 Handlebars 将页面内容编译为布局模板。之后,该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]-

此代码有一个box围绕 HTML 段落的简码。HTML 使用<and的地方>,短代码使用-[and ]-。在名称之后,包含短代码参数的字符串可以存在或不存在。

该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>");
   }
};

下一部分定义了shortcodesjson 结构,该结构定义了与其功能关联的短代码的名称。所有简码函数都接受两个参数:args和inside. args是名称和空格之后以及标签关闭之前的所有内容。这inside是开始和结束短代码标签所包含的一切。这些功能是基本的,但您可以创建一个短代码来执行您在 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(/\&quot\;/g,"\"");
   }
 
   return (result);
}

该figurePage()函数接收服务器上页面的完整路径。然后,此函数会根据扩展测试它是否为 HTML、Markdown 或 Jade 页面。我仍在为 Jade 使用 .amber,因为那是我与 goPress 服务器一起使用的库。所有 Markdown 和 Jade 内容在传递给调用例程之前都会被翻译成 HTML。由于 Markdown 处理器将所有引号翻译为&quot;,因此我在将其传回之前将它们翻译回来。

//
// 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;
   }
}

该函数是对曾经是Node.js 库的一部分的fileExists()函数的替代。它使用该函数来尝试获取文件的状态。如果发生错误, 则返回 a。否则,它返回.fs.exists()fsfs.statSync()falsetrue

//
//  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

或者,您可以使用npmpackage.json 文件中的脚本。你像这样运行 npm 脚本:

npm start

这将运行startpackage.json 文件中的脚本。

构建一个 CMS:nodePress  第1张nodePress 服务器主页

将您的网络浏览器指向http://localhost:8080,您将看到上面的页面。您可能已经注意到我在主页中添加了更多测试代码。对页面的所有更改都在本教程的下载中。它们大多只是一些小的调整,以更完整地测试功能并适应使用不同库的任何差异。最显着的区别是 Jade 库不使用$Amber 来命名变量。

结论

现在,您在 Go 和 Node.js 中拥有完全相同的平面文件系统 CMS。这只是触及了您可以使用此平台构建的内容的表面。尝试并尝试一些新的东西。这是创建自己的 Web 服务器的最佳部分。




文章目录
  • 获取节点和节点库
  • nodePress.js
    • 本地运行
  • 结论