Mark Brown , Jani Hartikainen和Joan Yin通过同行评审Git和Markdown使用Node.js构建微博。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
在现代编程中,“微型”一词泛滥了很多:微型框架,微型服务等。对我而言,这意味着无所solving形地解决眼前的问题。 所有这些都解决了一个简单的问题。 这意味着将重点放在眼前的问题上,减少不必要的依赖。
我觉得Node在网络上遵循Goldilocks原则 。 从低级库获得的一组API对于构建微型网站很有用。 这些API不太复杂,也不太简单,仅适合构建Web解决方案。
在本文中,让我们探索使用Node,Git和一些依赖项构建微博。 该应用程序的目的是从提交到存储库的文件中提供静态内容。 您将学习如何构建和测试应用程序,并深入了解提供解决方案的过程。 到最后,您将可以建立一个极简的工作博客应用程序。
微博的主要成分
要建立一个很棒的博客,首先,您需要一些要素:
- 一个用于发送HTTP消息的库
- 用于存储博客文章的存储库
- 单元测试运行器或库
- Markdown解析器
要发送HTTP消息,我选择“节点”,因为这正好满足了我从服务器发送超文本消息所需的条件。 特别感兴趣的两个模块是http和fs 。
http
模块将创建一个Node HTTP服务器。 fs
模块将读取一个文件。 Node具有使用HTTP构建微博客的库。
为了存储博客文章的存储库,我将选择Git而不是完整的数据库。 这样做的原因是,Git已经是带有版本控制的文本文档存储库。 这就是我存储博客文章数据所需要的。 无需将数据库添加为依赖项,这使我无需编写大量问题。
我选择存储在Markdown格式的博客文章,并分析它们使用标记 。 如果我以后决定这样做,则可以自由地逐步增强原始内容。 Markdown是纯HTML的一种不错的轻量级替代方案。
对于单元测试,我选择了称为roast.it的出色测试运行程序 。 我将选择这种替代方法,因为它没有依赖关系并且可以解决我的单元测试需求。 您可以选择另一个测试运行程序,例如taper ,但它大约有八个依赖项。 我喜欢roast.it
是它没有依赖性。
有了这个成分列表,我就拥有了构建微博客所需的所有依赖项。
选择依赖项不是一件容易的事。 我认为关键是紧迫问题之外的任何事情都可能成为依赖。 例如,我不是在构建测试运行程序或数据仓库,因此会将其附加到列表中。 任何给定的依赖项都不得吞噬解决方案并保持代码人质。 因此,仅挑选轻量级组件是有意义的。
本文假定您对Node , npm和Git以及各种测试方法有所了解。 我不会逐步完成构建微博客的所有步骤,而是会专注于并讨论代码的特定领域。 如果您想在家中继续学习,则代码已在GitHub上发布 ,您可以尝试显示的每个代码段。
测试中
测试使您对代码充满信心,并缩短了反馈循环。 编程中的反馈循环是编写任何新代码并运行它之间所花费的时间。 在任何Web解决方案中,这都意味着要跳过许多层以获得任何反馈。 例如,浏览器,Web服务器,甚至是数据库。 随着复杂性的增加,这可能意味着几分钟甚至一个小时才能获得反馈。 通过单元测试,我们可以放下这些层次并获得快速反馈。 这将重点放在手头的问题上。
我喜欢通过编写快速的单元测试来开始任何解决方案。 这使我有了为任何新代码编写测试的心态。 这就是您使用roast.it进行启动和运行的方式。
在package.json
文件中,添加:
"scripts": {
"test": "node test/test.js"
},
"devDependencies": {
"roast.it": "1.0.4"
}
在test.js
文件中,您可以引入所有单元测试并运行它们。 例如,一个人可以做:
var roast = require('roast.it');
roast.it('Is array empty', function isArrayEmpty() {
var mock = [];
return mock.length === 0;
});
roast.run();
roast.exit();
要运行测试,请执行npm install && npm test
。 让我感到高兴的是,我不再需要测试新代码。 这就是测试的全部内容:一个快乐的编码人员将获得信心,并专注于解决方案。
如您所见,测试运行程序希望调用roast.it(strNameOfTest, callbackWithTest)
。 每个测试结束时的return
值都必须解析为true
才能通过测试。 在实际应用中,您不想将所有测试写在一个文件中。 要解决此问题,您可以在Node中require
单元测试,然后将它们放在另一个文件中。 如果您在微博中查看test.js ,您会发现这正是我所做的。
提示 :您可以使用
npm run test
。 这可以缩写为npm test
甚至npm t
。
骷髅
该微博客将使用Node响应客户端请求。 一种有效的方法是通过http.CreateServer()
节点API。 可以从以下app.js摘录中看到:
/* app.js */
var http = require('http');
var port = process.env.port || 1337;
var app = http.createServer(function requestListener(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8'});
res.end('A simple micro blog website with no frills nor nonsense.');
});
app.listen(port);
console.log('Listening on http://localhost:' + port);
通过package.json
的npm脚本运行此命令:
"scripts": {
"start": "node app.js"
}
现在, http://localhost:1337/
成为默认路由,并通过一条消息返回给客户端。 想法是添加更多返回其他响应的路由,例如响应博客文章内容。
资料夹结构
为了构建应用程序的结构,我决定了以下主要部分:
我将使用这些文件夹来组织代码。 以下是每个文件夹的用途概述:
-
blog
:将原始博客帖子存储在纯Markdown中 -
message
:可重用的模块,用于建立对客户端的响应消息 -
route
:超出默认路由的路由 -
test
:编写单元测试的地方 -
view
:放置HTML模板的位置
如前所述,请随时关注,代码位于GitHub上 。 您可以尝试显示的每个代码段。
带有测试的更多路线
对于第一个用例,我将介绍博客文章的其他方法。 我选择将此路由放入可测试的组件BlogRoute
。 我喜欢的是您可以将依赖项注入其中。 将单元及其依赖项之间的关注点分离可以进行单元测试。 每个依赖项在一个隔离的测试中都会得到一个模拟。 这使您可以编写不可变,可重复和快速的测试。
例如,构造函数如下所示:
/* route/blogRoute.js */
var BlogRoute = function BlogRoute(context) {
this.req = context.req;
};
有效的单元测试是:
/* test/blogRouteTest.js */
roast.it('Is valid blog route', function isValidBlogRoute() {
var req = {
method: 'GET',
url: 'http://localhost/blog/a-simple-test'
};
var route = new BlogRoute({ req: req });
return route.isValidRoute();
});
目前, BlogRoute
需要一个req
对象,该对象来自Node API。 要使测试通过,就足够了:
/* route/blogRoute.js */
BlogRoute.prototype.isValidRoute = function isValidRoute() {
return this.req.method === 'GET' && this.req.url.indexOf('/blog/') >= 0;
};
这样,我们可以将其连接到请求管道。 您可以在app.js中执行以下操作 :
/* app.js */
var message = require('./message/message');
var BlogRoute = require('./route/BlogRoute');
// Inside createServer requestListener callback...
var blogRoute = new BlogRoute({ message: message, req: req, res: res });
if (blogRoute.isValidRoute()) {
blogRoute.route();
return;
}
// ...
关于测试的好处是我不必担心实施细节。 我会尽快定义message
。 res
和req
对象来自http.createServer()
节点API。
随时在route / blogRoute.js中浏览博客路由。
仓库
下一个要解决的问题是在BlogRoute.route()
读取原始博客文章数据。 Node提供了一个fs
模块,您可以使用该模块从文件系统读取。
例如:
/* message/readTextFile.js */
var fs = require('fs');
var path = require('path');
function readTextFile(relativePath, fn) {
var fullPath = path.join(__dirname, '../') + relativePath;
fs.readFile(fullPath, 'utf-8', function fileRead(err, text) {
fn(err, text);
});
}
此代码段位于message / readTextFile.js中 。 解决方案的核心是读取存储库中的文本文件。 注意fs.readFile()
是异步操作。 这就是需要fn
回调并使用文件数据进行调用的原因。 此异步解决方案使用谦虚的回调。
这提供了文件IO的需求。 我喜欢它的地方只是解决了一个问题。 由于这是一个贯穿各领域的问题,例如读取文件,因此不需要进行单元测试。 单元测试应该仅隔离地测试您自己的代码,而不是其他人的代码。
从理论上讲,您可以在内存中模拟文件系统并以这种方式编写单元测试,但是解决方案将开始在所有地方泄漏问题并变成糊涂。
横切关注点(例如读取文件)不在代码的范围内。 例如,读取文件取决于您无法直接控制的子系统。 这会使测试变脆,并给反馈回路增加时间和复杂性。 这是一个必须与您的解决方案分开的问题。
现在可以在BlogRoute.route()
函数中执行以下操作:
/* route/bogRoute.js */
BlogRoute.prototype.route = function route() {
var url = this.req.url;
var index = url.indexOf('/blog/') + 1;
var path = url.slice(index) + '.md';
this.message.readTextFile(path, function dummyTest(err, rawContent) {
this.res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
this.res.end(rawContent);
}.bind(this));
};
请注意, message
和res
通过BlogRoute
构造函数注入,如下所示:
this.message = context.message;
this.res = context.res;
从请求中获取req
对象,并读取Markdown文件。 不用担心dummyTest()
。 现在,将其像处理响应的任何其他回调一样对待。
要对该BlogRoute.route()
函数进行单元测试:
/* test/blogRouteTest.js */
roast.it('Read raw post with path', function readRawPostWithPath() {
var messageMock = new MessageMock();
var req = {
url: 'http://localhost/blog/a-simple-test'
};
var route = new BlogRoute({ message: messageMock, req: req });
route.route();
return messageMock.readTextFileCalledWithPath === 'blog/a-simple-test.md' &&
messageMock.hasCallback;
});
message
模块被注入到BlogRoute
以模拟message.readTextFile()
。 这样,我可以验证被测系统(即BlogRoute.route()
)是否通过了。
您不想在这里需要它们的代码中直接require
模块。 原因是,您正在热粘合依赖项。 这使任何类型的测试都变成了完全集成测试–例如, message.readTextFile()
将读取实际文件。
这种方法称为依赖反转 ,这是SOLID原理之一 。 这将解耦软件模块并启用依赖项注入。 单元测试基于此原理并具有模拟依赖性。 例如, messageMock.readTextFileCalledWithPath
测试该单元本身的行为是否正确。 它没有跨越功能边界。
不要害怕嘲笑。 这是用于测试事物的轻量级对象。 例如,您可以使用sinon ,并将此依赖项添加到模拟中 。
我喜欢的是自定义模拟,因为这为处理许多用例提供了灵活性。 定制模拟提供的一个优势是它们使测试代码杂乱无章。 这为单元测试增加了准确性和清晰度。
MessageMock
现在所做的所有MessageMock
是:
/* test/mock/messageMock.js */
var MessageMock = function MessageMock() {
this.readTextFileCalledWithPath = '';
this.hasCallback = false;
};
MessageMock.prototype.readTextFile = function readTextFile(path, callback) {
this.readTextFileCalledWithPath = path;
if (typeof callback === 'function') {
this.hasCallback = true;
}
};
您可以在test / mock / messageMock.js中找到此代码。
注意,该模拟不需要具有任何异步行为。 实际上,它甚至从不调用回调。 目的是确保以符合用例的方式使用它。 确保message.readTextFile()
被调用并具有正确的路径和回调。
注入到BlogRoute
的实际message
对象来自message / message.js 。 它的作用是将所有可重用组件合并到一个实用程序对象中。
例如:
/* message/message.js */
var readTextFile = require('./readTextFile');
module.exports = {
readTextFile: readTextFile
};
这是可以在Node中使用的有效模式。 在文件夹之后命名文件,并从一个位置导出文件夹内的所有组件。
至此,该应用程序已全部连接好,可以发送回原始的Markdown数据。 是时候进行一次端到端测试以验证这一工作了。
键入npm start
然后在单独的命令行窗口中执行curl -v http://localhost:1337/blog/my-first-post
:
发布数据通过Git进入存储库。 您可以通过git commit
保留博客文章更改。
Markdown解析器
对于下一个问题,是时候将原始Markdown数据从存储库转换为HTML了。 此过程有两个步骤:
- 从
view
文件夹中获取HTML模板 - 将Markdown解析为HTML并填充模板
在声音编程中,其想法是解决一个大问题并将其分解成小块。 让我们解决第一个问题:如何根据BlogRoute
内容获取HTML模板?
一种方法可能是:
/* route/blogRoute.js */
BlogRoute.prototype.readPostHtmlView = function readPostHtmlView(err, rawContent) {
if (err) {
this.res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
this.res.end('Post not found.');
return;
}
this.rawContent = rawContent;
this.message.readTextFile('view/blogPost.html', this.renderPost.bind(this));
};
请记住,这将替换上一节中使用的哑回调,即dummyTest
。
要替换回调dummyTest
,请执行以下操作:
this.message.readTextFile(path, this.readPostHtmlView.bind(this));
是时候编写一个快速的单元测试了:
/* test/blogRouteTest.js */
roast.it('Read post view with path', function readPostViewWithPath() {
var messageMock = new MessageMock();
var rawContent = 'content';
var route = new BlogRoute({ message: messageMock });
route.readPostHtmlView(null, rawContent);
return messageMock.readTextFileCalledWithPath !== '' &&
route.rawContent === rawContent &&
messageMock.hasCallback;
});
我只是在这里测试了幸福的道路。 万一找不到博客文章,还有另一项测试。 所有BlogRoute
单元测试都在test / blogRouteTest下 。 如果有兴趣的话,可以在这里随意逛逛。
至此,您已经通过了测试! 即使无法验证整个请求管道,您也有足够的信心继续前进。 再次,这就是测试的全部内容:呆在区域中,专注并保持快乐。 编程时没有理由感到难过或沮丧。 我当然认为您应该快乐而不是悲伤。
请注意,实例将原始Markdown发布数据存储在this.rawContent
。 还有更多工作要做,您可以在下一个回调中看到它(即this.renderPost()
)。
如果您不熟悉.bind(this)
,那么在JavaScript中,这是一种范围回调函数的有效方法。 默认情况下,回调的作用域范围是外部作用域,在这种情况下这是不好的。
将Markdown解析为HTML
下一个问题是将HTML模板和原始内容数据整合在一起。 我将在BlogRoute.renderPost()
用作回调的BlogRoute.renderPost()
此操作。
这是一种可能的实现:
/* route/blogRoute.js */
BlogRoute.prototype.renderPost = function renderPost(err, html) {
if (err) {
this.res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
this.res.end('Internal error.');
return;
}
var htmlContent = this.message.marked(this.rawContent);
var responseContent = this.message.mustacheTemplate(html, { postContent: htmlContent });
this.res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
this.res.end(responseContent);
};
再次,我将测试幸福的道路:
/* test/blogRouteTest.js */
roast.it('Respond with full post', function respondWithFullPost() {
var messageMock = new MessageMock();
var responseMock = new ResponseMock();
var route = new BlogRoute({ message: messageMock, res: responseMock });
route.renderPost(null, '');
return responseMock.result.indexOf('200') >= 0;
});
您可能想知道responseMock
来源。 请记住,模拟是用于测试事物的轻量级对象。 使用ResponseMock
确保res.writeHead()
和res.end()
被调用。
在此模拟中,这是我输入的内容:
/* test/mock/responseMock.js */
var Response = function Response() {
this.result = '';
};
Response.prototype.writeHead = function writeHead(returnCode) {
this.result += returnCode + ';';
};
Response.prototype.end = function end(body) {
this.result += body;
};
如果它提高了置信度,则此响应模拟将起作用。 就信心而言,这取决于作者。 单元测试告诉您编写代码的人在想什么。 这使您的程序更加清晰。
代码在这里: test / mock / responseMock.js 。
自从我引入了message.marked()
(将Markdown转换为HTML)和message.mustacheTemplate()
(轻量级的模板函数)以来,我就可以对它们进行模拟。
它们被附加到MessageMock
:
/* test/mock/messageMock.js */
MessageMock.prototype.marked = function marked() {
return '';
};
MessageMock.prototype.mustacheTemplate = function mustacheTemplate() {
return '';
};
在这一点上,每个组件返回什么内容都没有关系。 我主要关心的是确保两者都是模拟的一部分。
拥有很棒的模拟游戏的好处是,您可以迭代并使其变得更好。 当发现错误时,您可以增强单元测试,并将更多用例添加到反馈循环中。
这样,您就可以通过测试。 是时候将其连接到请求管道了。
在message/message.js
执行以下操作:
/* message/message.js */
var mustacheTemplate = require('./mustacheTemplate');
var marked = require('marked');
// ...
module.exports = {
mustacheTemplate: mustacheTemplate,
// ...
marked: marked
};
marked
是我选择添加为依赖项的Markdown解析器。
将其添加到package.json
:
"dependencies": {
"marked": "0.3.6"
}
mustacheTemplate
是位于message / mustacheTemplate.js中的message文件夹内的可重用组件。 我决定不将此添加为另一个依赖项,因为鉴于我需要的功能列表,这似乎有些过分。
胡子模板功能的关键是:
/* message/mustacheTemplate.js */
function mustache(text, data) {
var result = text;
for (var prop in data) {
if (data.hasOwnProperty(prop)) {
var regExp = new RegExp('{{' + prop + '}}', 'g');
result = result.replace(regExp, data[prop]);
}
}
return result;
}
有单元测试可以验证这项工作。 也可以随意浏览: test / mustacheTemplateTest.js 。
您仍然需要添加HTML模板或视图。 在view / blogPost.html中执行以下操作:
<!-- view/blogPost.html -->
<body>
<div>
{{postContent}}
</div>
</body>
有了这个,现在该在浏览器中进行演示了。
要尝试,请键入npm start
然后转到http://localhost:1337/blog/my-first-post
:
永远不要忽视软件中模块化,可测试和可重用的组件。 实际上,不要让任何人说服您使用对此有敌意的解决方案。 即使紧密耦合到框架,任何代码库都可以拥有干净的代码,所以不要失去希望!
期待
这样就可以给您一个可以正常工作的应用程序。 从这一点来看,有很多方法可以使它投入生产。
可能的改进示例包括:
没有限制,在您的世界中,您可以随心所欲地使用此应用程序。
包起来
希望您能看到如何在仅具有一些轻量级依赖项的Node.js中构建解决方案。 您所需要的只是一点点想象力和对当前问题的关注。 您可以使用的API集足以构建一些令人惊奇的东西。
很高兴看到KISS原则对任何解决方案都非常重要。 仅解决眼前的问题,并保持尽可能低的复杂性。
该工作解决方案在具有依赖项的磁盘上总计约172KB。 如此大小的解决方案几乎在任何Web主机上都具有令人难以置信的性能。 快速响应的轻量级应用程序将使用户满意。 最好的部分是,您现在可以使用一个不错的微博客,甚至可以做得更多。
我很想阅读您对这种方法的评论和问题,并听听您的想法!
From: https://www.sitepoint.com/build-microblog-node-js-git-markdown/