使用Node.js,Git和Markdown构建微博

Mark BrownJani HartikainenJoan Yin通过同行评审Git和Markdown使用Node.js构建微博。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

一个作家在她的桌子上睡着,周围被她的微博组件包围

在现代编程中,“微型”一词泛滥了很多:微型框架,微型服务等。对我而言,这意味着无所solving形地解决眼前的问题。 所有这些都解决了一个简单的问题。 这意味着将重点放在眼前的问题上,减少不必要的依赖。

我觉得Node在网络上遵循Goldilocks原则 。 从低级库获得的一组API对于构建微型网站很有用。 这些API不太复杂,也不太简单,仅适合构建Web解决方案。

在本文中,让我们探索使用Node,Git和一些依赖项构建微博。 该应用程序的目的是从提交到存储库的文件中提供静态内容。 您将学习如何构建和测试应用程序,并深入了解提供解决方案的过程。 到最后,您将可以建立一个极简的工作博客应用程序。

微博的主要成分

要建立一个很棒的博客,首先,您需要一些要素:

  • 一个用于发送HTTP消息的库
  • 用于存储博客文章的存储库
  • 单元测试运行器或库
  • Markdown解析器

要发送HTTP消息,我选择“节点”,因为这正好满足了我从服务器发送超文本消息所需的条件。 特别感兴趣的两个模块是httpfs

http模块将创建一个Node HTTP服务器。 fs模块将读取一个文件。 Node具有使用HTTP构建微博客的库。

为了存储博客文章的存储库,我将选择Git而不是完整的数据库。 这样做的原因是,Git已经是带有版本控制的文本文档存储库。 这就是我存储博客文章数据所需要的。 无需将数据库添加为依赖项,这使我无需编写大量问题。

我选择存储在Markdown格式的博客文章,并分析它们使用标记 。 如果我以后决定这样做,则可以自由地逐步增强原始内容。 Markdown是纯HTML的一种不错的轻量级替代方案。

对于单元测试,我选择了称为roast.it的出色测试运行程序 。 我将选择这种替代方法,因为它没有依赖关系并且可以解决我的单元测试需求。 您可以选择另一个测试运行程序,例如taper ,但它大约有八个依赖项。 我喜欢roast.it是它没有依赖性。

有了这个成分列表,我就拥有了构建微博客所需的所有依赖项。

选择依赖项不是一件容易的事。 我认为关键是紧迫问题之外的任何事情都可能成为依赖。 例如,我不是在构建测试运行程序或数据仓库,因此会将其附加到列表中。 任何给定的依赖项都不得吞噬解决方案并保持代码人质。 因此,仅挑选轻量级组件是有意义的。

本文假定您对NodenpmGit以及各种测试方法有所了解。 我不会逐步完成构建微博客的所有步骤,而是会专注于并讨论代码的特定领域。 如果您想在家中继续学习,则代码已在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;
  }
// ...

关于测试的好处是我不必担心实施细节。 我会尽快定义messageresreq对象来自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));
};

请注意, messageres通过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

浏览器视图演示

永远不要忽视软件中模块化,可测试和可重用的组件。 实际上,不要让任何人说服您使用对此有敌意的解决方案。 即使紧密耦合到框架,任何代码库都可以拥有干净的代码,所以不要失去希望!

期待

这样就可以给您一个可以正常工作的应用程序。 从这一点来看,有很多方法可以使它投入生产。

可能的改进示例包括:

  • 例如,Git部署使用GitFlow
  • 添加一种管理客户端资源的方法
  • 基本缓存,客户端和服务器端内容
  • 添加元数据(可能使用前事 )以使帖子适合SEO

没有限制,在您的世界中,您可以随心所欲地使用此应用程序。

包起来

希望您能看到如何在仅具有一些轻量级依赖项的Node.js中构建解决方案。 您所需要的只是一点点想象力和对当前问题的关注。 您可以使用的API集足以构建一些令人惊奇的东西。

很高兴看到KISS原则对任何解决方案都非常重要。 仅解决眼前的问题,并保持尽可能低的复杂性。

该工作解决方案在具有依赖项的磁盘上总计约172KB。 如此大小的解决方案几乎在任何Web主机上都具有令人难以置信的性能。 快速响应的轻量级应用程序将使用户满意。 最好的部分是,您现在可以使用一个不错的微博客,甚至可以做得更多。

我很想阅读您对这种方法的评论和问题,并听听您的想法!

From: https://www.sitepoint.com/build-microblog-node-js-git-markdown/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值