Home >Web Front-end >JS Tutorial >Create a content management system: nodePress

Create a content management system: nodePress

PHPz
PHPzOriginal
2023-09-03 13:53:051441browse

You have successfully created a flat file system content management system (CMS) using Go. The next step is to take the same idea and make a web server using Node.js. I'll show you how to load the library, create the server, and run the server.

This CMS will use the site data structure introduced in the first tutorial, "Building a CMS: Structure and Style." So, download this basic structure and install it in a new directory.

Get nodes and node libraries

The easiest way to install Node.js on Mac is to use Homebrew. If you don’t have Homebrew installed yet, the tutorial Homebrew Revealed: The Ultimate Package Manager for OS X will show you how to install it.

To install Node.js using Homebrew, enter the following command in the terminal:

brew install node

When completed, Node and npm commands will be fully installed on your Mac. For all other platforms, follow the instructions on the Node.js website.

Please note: Many package managers are currently installing Node.js version 0.10. This tutorial assumes you have version 5.3 or higher. You can check your version by typing:

node --version
The

node command runs the JavaScript interpreter. The npm command is the package manager for Node.js and is used to install new libraries, create new projects, and run project scripts. Envato Tuts has many great tutorials and courses on Node.js and NPM.

To install the web server's libraries, you must run the following command in the Terminal.app or iTerm.app program:

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 is a web application development platform. It is similar to the goWeb library in Go. Handlebars is a template engine for creating pages. Moment is a library for working with dates. Marked is a great Markdown to HTML converter in JavaScript. Jade is an HTML shorthand language that makes it easy to create HTML. Morgan is a middleware library for Express that generates Apache standard log files.

Another way to install the library is to download the source files for this tutorial. After downloading and unzipping, enter:

in the home directory
npm --install

This will install everything needed to create the project.

nodePress.js

Now you can start creating the server. In the top-level directory of your project, create a file called nodePress.js, open it in the editor of your choice, and start adding the following code. I'll explain the code I put in the file.

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

The server code starts by initializing all libraries used to create the server. Libraries without annotations with URLs are internal Node.js libraries.

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

Next, I set up all global variables and library configuration. Using global variables is not the best software design practice, but it does work and helps in rapid development.

parts The variable is a hash array containing all the parts of the web page. Each page references the contents of this variable. It starts with the contents of the server.json file at the top of the server directory.

I then used the information in the server.json file to create the full paths to the styles and layouts directories for this site.

Then set three variables to empty values: siteCSS, siteScripts, and mainPage. These global variables will contain all CSS, JavaScript, and main index page content. These three projects are the most requested on any web server. Therefore, keeping them in memory saves time. If the Cache variable in the server.json file is false, these items will be re-read with every request.

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

This code block is used to configure the Marked library to generate HTML from Markdown. Mostly I turn on tables and smartLists support.

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)));
});
The

parts variable further loads parts from the styles and layout directories. Each file in the parts directory within the site directory is also loaded into the parts global variable. The filename without extension is the name used to store the contents of the file. These names are expanded in the Handlebars macro.

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

The next piece of code defines the Handlebars helpers I defined for use in the web server: save, date, and cdate. The save assistant allows creating variables within the page. This version supports the goPress version where the name and value of the parameter are separated together with "|". You can also specify saving using two parameters. For example:

{{save "name|Richard Guay"}}
{{save "newName" "Richard Guay"}}

Name is: {{name}}
newName is: {{newName}}

This will produce the same result. I prefer the second approach, but the Handlebars library in Go doesn't allow multiple arguments.

datecdate 帮助程序格式化当前日期 (date) 或给定日期 (cdate)根据 moment.js 库格式化规则。 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() 函数,而所有针对 post 类型页面的请求都会调用 posts() 函数。

Content-Type 的默认值为 HTML。因此,对于 CSS、JavaScript 和图像,Content-Type 显式设置为其适当的值。

您还可以使用 putdeletepost 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() 函数,不同之处在于帖子有更多项目来定义每个帖子。在这个系列的服务器中,一个post包含一个type、category,以及实际的post。类型为 blogsnews。类别是 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 全局变量的本地副本,并添加“contents”主题标签以及调用 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]-

此代码在 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 + "
"); }, 'js': function(args, inside) { return ("
" + inside + "
"); }, 'html': function(args, inside) { return ("
" + inside + "
"); }, 'css': function(args, inside) { return ("
" + inside + "
"); } };

下一节定义 shortcodes json 结构,该结构定义与其函数关联的短代码的名称。所有短代码函数都接受两个参数:argsinsideargs 是名称和空格之后、标签结束之前的所有内容。 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 处理器将所有引号翻译为 ",因此我在传回之前将它们翻译回来。

//
// 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 脚本。

"Create

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

结论

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

The above is the detailed content of Create a content management system: nodePress. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn