使用Node.js, Git和M必威滚arkdown构建微博
使用Node.js, Git和M必威滚arkdown构建微博马克。布朗,贾尼Hartikainen而且琼阴.感谢所有SitePoint的同行审必威西盟体育网页登录稿人,让SitePoint的内容成为最好的!
“微”这个词在现代编程中经常出现:微框架、微服务等等。对我来说,这意味着在不膨胀的情况下解决手头的问题。同时解决一个简单明了的问题。这意味着专注于手头的问题,减少不必要的依赖。
我觉得Node遵循了金发女孩原则说到网络。从低级库中获得的api集对于构建微型网站非常有用。这些api不太复杂,也不太简单,只是适合构建web解决方案。
在本文中,我们将探讨如何使用Node、Git和一些依赖项构建微博。必威滚这个应用程序的目的是为提交到存储库的文件提供静态内容。您将学习如何构建和测试应用程序,并深入了解交付解决方案的过程。到最后,你将有一个极简的工作博客应用程序,你可以构建。必威滚
微博的主要成分必威滚
要创建一个很棒的博客,首先,你需要一些要必威滚素:
- 用于发送HTTP消息的库
- 存储博客文章的存储库必威滚
- 单元测试运行程序或库
- Markdown解析器
要发送HTTP消息,我选择Node,因为这正好提供了从服务器发送超文本消息所需的内容。两个特别有趣的模块是http而且fs.
的http
模块将创建一个Node HTTP服务器。的fs
模块将读取一个文件。Node拥有使用HTTP构建微博的库。必威滚
要存储博客文章的存储库,我将选择Git而不是成熟的必威滚数据库。原因在于,Git已经是一个带有版本控制的文本文档存储库。这正是我需要存储博客文章数据。必威滚将数据库作为依赖项添加的自由使我从编码中摆脱了大量的问题。
我选择以Markdown格式存储博客必威滚文章,并使用解析它们标志着.这让我可以自由地逐步增强原始内容,如果我决定以后这样做的话。Markdown是普通HTML的一个不错的轻量级替代方案。
对于单元测试,我选择称为的优秀测试运行器roast.it.我将选择这个替代方案,因为它没有依赖关系,并且解决了我的单元测试需求。你可以选另一个测试者,比如锥形,但它有大约8个依赖项。我喜欢什么roast.it
它没有依赖关系。
有了这个成分列表,我就有了创建微博所需的所有依赖项。必威滚
选择依赖关系不是一件小事。我认为关键在于,任何与眼前问题无关的事情都可能成为一种依赖。例如,我没有构建测试运行程序,也没有构建数据存储库,因此会将其追加到列表中。任何给定的依赖关系都不能吞下解决方案并劫持代码。因此,只挑选轻量级组件是有意义的。
本文假设您对节点,npm而且Git,以及各种测试方法。我不会详细介绍构建微博所涉及的每个步骤,而是将重点讨论代码的特定区域。必威滚如果你想在家里跟着做,代码已经打开了GitHub您可以尝试如下所示的每个代码片段。
测试
测试让你对你的代码有信心,并加强反馈循环。编程中的反馈循环是指编写新代码和运行新代码之间所花费的时间。在任何web解决方案中,这意味着要跨越许多层来获得任何反馈。例如,浏览器,网络服务器,甚至数据库。随着复杂性的增加,这可能意味着需要几分钟甚至一个小时才能获得反馈。通过单元测试,我们可以去掉这些层并获得快速反馈。这样就能把注意力集中在手头的问题上。
我喜欢通过编写一个快速的单元测试来开始任何解决方案。这让我有了为任何新代码编写测试的心态。这就是你开始做烤肉的方法。
在package.json
文件,添加:
“脚本”:{“测试”:“节点测试/ . js”},“devDependencies”:{“roast.it”:“1.0.4”}
的. js
文件是引入所有单元测试并运行它们的地方。例如,可以这样做:
var烤=需要(“roast.it”);烤.它('数组为空',函数isArrayEmpty(){var模拟=[];返回模拟.长度= = =0;});烤.运行();烤.退出();
要运行测试请执行NPM安装& NPM测试
.让我高兴的是,我不再需要为了测试新代码而经历重重困难。这就是测试的全部内容:一个快乐的程序员获得信心并专注于解决方案。
如您所见,测试运行程序期望调用roast.it(strNameOfTest, callbackWithTest)
.的返回
在每次测试结束时都必须解决真正的
以便测试通过。在真实的应用程序中,您不希望将所有测试都写在一个文件中。要绕开这个问题,你可以需要
并将它们放在不同的文件中。如果你看一下. js在微博中,你会看到这正是我必威滚所做的。
提示:使用运行测试
NPM运行测试
.这可以缩写为npm测试
甚至npm t
.
骨骼
微博将使用Node响必威滚应客户端请求。一个有效的方法是通过http.CreateServer ()
节点API。这可以从以下摘录中看出app.js:
/* app.js */varhttp=需要(“http”);var港口=过程.env.港口||1337;var应用程序=http.createServer(函数requestListener(要求的事情,res){res.writeHead(200,{“内容类型”:“文本/平原;charset = utf - 8 '});res.结束(“一个简单的微博网站,没有虚饰和必威滚废话。”);});应用程序.听(港口);控制台.日志(“收听http://localhost:”+港口);
通过npm脚本运行package.json
:
“脚本”:{“开始”:“节点app.js”}
现在,http://localhost:1337/
成为默认路由,并向客户端返回消息。这个想法是添加更多返回其他响应的路由,比如用博客帖子内容进行响应。必威滚
文件夹结构
为了构建应用程序的结构,我决定使用以下主要部分:
我将使用这些文件夹来组织代码。下面是每个文件夹的用途概述:
必威滚
:在普通Markdown中必威滚存储原始博客文章消息
:用于向客户端构建响应消息的可重用模块路线
:缺省路由以外的路由测验
:编写单元测试的地方视图
:放置HTML模板的位置
如前所述,请随意跟随,代码已经启动GitHub.您可以尝试如下所示的每个代码片段。
更多带有测试的路由
对于第一个用例,我将为博客文章介绍进一步的路径。必威滚我选择将此路由放在名为必威滚BlogRoute
.我喜欢的是你可以把依赖注入到这个。单元及其依赖项之间的这种关注点分离使单元测试成为可能。每个依赖项在孤立的测试中得到一个模拟。这允许您编写不可变、可重复和快速的测试。
例如,构造函数是这样的:
/* route/必威滚blogRoute.js */var必威滚BlogRoute=函数必威滚BlogRoute(上下文){这.要求的事情=上下文.要求的事情;};
一个有效的单元测试是:
/* test/必威滚blogRouteTest.js */烤.它(“是有效的博客路由”必威滚,函数isValid必威滚BlogRoute(){var要求的事情={方法:“得到”,url:“http://localhost/必威滚blog/a-simple-test”};var路线=新必威滚BlogRoute({要求的事情:要求的事情});返回路线.isValidRoute();});
就目前而言,必威滚BlogRoute
预计要求的事情
对象,它来自Node API。为了通过测试,需要做以下工作:
/* route/必威滚blogRoute.js */必威滚BlogRoute.原型.isValidRoute=函数isValidRoute(){返回这.要求的事情.方法= = =“得到”& &这.要求的事情.url.indexOf(“/必威滚博客/”)> =0;};
这样我们就可以将它连接到请求管道。你可以在室内这样做app.js:
/* app.js */var消息=需要(“。/信息/消息”);var必威滚BlogRoute=需要(“/线路/ Blo必威滚gRoute。”);//内部createServer的requestListener回调…var必威滚blogRoute=新必威滚BlogRoute({消息:消息,要求的事情:要求的事情,res:res});如果(必威滚blogRoute.isValidRoute()){必威滚blogRoute.路线();返回;}/ /……
有测试的好处是我不必预先担心实现细节。我将定义消息
很快。的res
而且要求的事情
对象来自http.createServer ()
节点API。
请随意浏览博客路线,进入必威滚路线/ bl必威滚ogRoute.js.
存储库
下一个要解决的问题是读取原始博客文章数据必威滚必威滚BlogRoute.route ()
.节点提供一个fs
模块,您可以使用它从文件系统中读取。
例如:
/* message/readTextFile.js */varfs=需要(“fs”);var路径=需要(“路径”);函数readTextFile(relativePath,fn){varfullPath=路径.加入(__dirname,“. . /”)+relativePath;fs.readFile(fullPath,“utf - 8”,函数fileRead(犯错,文本){fn(犯错,文本);});}
此代码片段已导入消息/ readTextFile.js.解决方案的核心是读取存储库中的文本文件。请注意fs.readFile ()
是异步操作。这就是为什么它需要afn
回调函数用文件数据调用它。这个异步解决方案使用了一个简单的回调。
这提供了文件IO需求。我喜欢它的地方在于它只解决了一个问题。由于这是一个横切问题,例如读取文件,因此不需要单元测试。单元测试应该只测试您的自己的独立的代码,而不是别人的代码。
理论上,您可以在内存中模拟文件系统并以这种方式编写单元测试,但是解决方案将开始到处泄漏关注点并变成混乱。
横切关注点(比如读取文件)超出了代码的范围。例如,读取文件依赖于超出您直接控制的子系统。这使得测试变得脆弱,并增加了反馈循环的时间和复杂性。这是一个必须与您的解决方案分离的关注点。
在必威滚BlogRoute.route ()
函数我现在可以做:
/* route/bogRoute.js */必威滚BlogRoute.原型.路线=函数路线(){varurl=这.要求的事情.url;var指数=url.indexOf(“/必威滚博客/”)+1;var路径=url.片(指数)+“。海事”;这.消息.readTextFile(路径,函数dummyTest(犯错,rawContent){这.res.writeHead(200,{“内容类型”:text / html;charset = utf - 8 '});这.res.结束(rawContent);}.绑定(这));};
请注意,消息
而且res
通过注射必威滚BlogRoute
构造函数,如:
这.消息=上下文.消息;这.res=上下文.res;
取要求的事情
对象,并读取Markdown文件。不要担心dummyTest ()
.现在,像对待任何其他处理响应的回调一样对待它。
进行单元测试必威滚BlogRoute.route ()
功能:
/* test/必威滚blogRouteTest.js */烤.它(“读取带有路径的原始帖子”,函数readRawPostWithPath(){varmessageMock=新MessageMock();var要求的事情={url:“http://localhost/必威滚blog/a-simple-test”};var路线=新必威滚BlogRoute({消息:messageMock,要求的事情:要求的事情});路线.路线();返回messageMock.readTextFileCalledWithPath= = =“必威滚博客/ a-simple-test.md”& &messageMock.hasCallback;});
的消息
模块注入到必威滚BlogRoute
嘲笑message.readTextFile ()
.有了这个,我可以验证测试中的系统(即。必威滚BlogRoute.route ()
)通过。
你不会想要的需要
模块就在需要它们的代码中。原因是,你是热粘合依赖。这使得任何类型的测试都变成了完全的集成测试message.readTextFile ()
,例如,将读取实际文件。
这种方法叫做依赖倒置,其中一个坚实的原则.这将解耦软件模块并支持依赖项注入。单元测试建立在这个原则之上,并带有一个模拟依赖项。messageMock.readTextFileCalledWithPath
,例如,测试该单元是否单独运行正常。它没有跨越功能界限。
不要怕讥诮。它是用来测试事物的轻量级对象。你可以用西农,并为mock添加此依赖项。
我喜欢的是自定义模拟,因为这在处理许多用例方面提供了灵活性。自定义模拟提供的一个优点是它们从测试代码中整理模拟。这增加了单元测试的精确性和清晰度。
所有MessageMock
现在做的是:
/* test/mock/messageMock.js */varMessageMock=函数MessageMock(){这.readTextFileCalledWithPath=”;这.hasCallback=假;};MessageMock.原型.readTextFile=函数readTextFile(路径,回调){这.readTextFileCalledWithPath=路径;如果(typeof回调= = =“函数”){这.hasCallback=真正的;}};
注意,模拟不需要任何异步行为。事实上,它甚至从不调用回调。目的是确保以一种满足用例的方式使用它。确保message.readTextFile ()
被调用并具有正确的路径和回调。
实际的消息
对象中注入的必威滚BlogRoute
来自消息/ message.js.它所做的是将所有可重用组件整合到单个实用程序对象中。
例如:
/* message/message.js */varreadTextFile=需要(”。/ readTextFile ');模块.出口={readTextFile:readTextFile};
这是可以在Node中使用的有效模式。以文件夹命名文件,并从单一位置导出文件夹内的所有组件。
在这一点上,应用程序都连接好了,并准备发回原始Markdown数据。该进行端到端测试以验证这是可行的。
类型npm开始
然后,在单独的命令行窗口中执行旋度-v http://localhost:1337/blo必威滚g/my-first-post
:
发布数据通过Git进入存储库。你可以坚持博客文章的变化必威滚git提交
.
Markdown解析器
对于下一个问题,是时候将存储库中的原始Markdown数据转换为HTML了。这个过程有两个步骤:
- 中获取HTML模板
视图
文件夹 - 将Markdown解析为HTML并填写模板
在声音编程中,想法是把一个大问题分解成小块。让我们解决第一个问题:我如何获得基于我在HTML模板必威滚BlogRoute
?
一种方法是:
/* route/必威滚blogRoute.js */必威滚BlogRoute.原型.readPostHtmlView=函数readPostHtmlView(犯错,rawContent){如果(犯错){这.res.writeHead(404,{“内容类型”:“文本/平原;charset = utf - 8 '});这.res.结束(“邮件未找到。”);返回;}这.rawContent=rawContent;这.消息.readTextFile(“视图/ b必威滚logPost.html”,这.renderPost.绑定(这));};
记住,这将替换前一节中使用的虚拟回调dummyTest
.
替换回调函数dummyTest
做:
这.消息.readTextFile(路径,这.readPostHtmlView.绑定(这));
是时候写一个快速的单元测试了:
/* test/必威滚blogRouteTest.js */烤.它(“读取带有路径的post视图”,函数readPostViewWithPath(){varmessageMock=新MessageMock();varrawContent=“内容”;var路线=新必威滚BlogRoute({消息:messageMock});路线.readPostHtmlView(零,rawContent);返回messageMock.readTextFileCalledWithPath= = !”& &路线.rawContent= = =rawContent& &messageMock.hasCallback;});
这里我只测试了一条快乐之路。还有另一个测试,以防它找不到博客文章。必威滚所有必威滚BlogRoute
单元测试正在进行测试/ b必威滚logRouteTest.如果有兴趣的话,可以随便看看。
至此,您已经通过了测试!尽管不可能验证整个请求管道,但您有足够的信心继续进行下去。再次强调,这就是测试的意义所在:保持专注,保持快乐。在编程时没有理由感到悲伤或沮丧。我当然认为你应该高兴而不是难过。
注意实例中存储原始Markdown post数据this.rawContent
.还有更多的工作正在进行中,您可以在下一个回调(即。this.renderPost ()
).
以防你不熟悉.bind(这)
在JavaScript中,这是定义回调函数作用域的有效方法。默认情况下,回调的作用域是外部作用域,这在本例中是不好的。
将Markdown解析为HTML
下一个小问题是将HTML模板和原始内容数据结合在一起。我把这个画进去必威滚BlogRoute.renderPost ()
上面我们用它作为回调函数。
这里有一个可能的实现:
/* route/必威滚blogRoute.js */必威滚BlogRoute.原型.renderPost=函数renderPost(犯错,超文本标记语言){如果(犯错){这.res.writeHead(500,{“内容类型”:“文本/平原;charset = utf - 8 '});这.res.结束(内部错误。);返回;}varhtmlContent=这.消息.标志着(这.rawContent);varresponseContent=这.消息.mustacheTemplate(超文本标记语言,{postContent:htmlContent});这.res.writeHead(200,{“内容类型”:text / html;charset = utf - 8 '});这.res.结束(responseContent);};
再次,我将测试愉快的路径:
/* test/必威滚blogRouteTest.js */烤.它(“回复全文”,函数respondWithFullPost(){varmessageMock=新MessageMock();varresponseMock=新ResponseMock();var路线=新必威滚BlogRoute({消息:messageMock,res:responseMock});路线.renderPost(零,”);返回responseMock.结果.indexOf(“200”)> =0;});
你可能想知道在哪里responseMock
的来源。记住,模拟是用于测试的轻量级对象。使用ResponseMock
为了确保res.writeHead ()
而且res.end ()
被调用。
在这个mock中,我这样写:
/* test/mock/responseMock.js */var响应=函数响应(){这.结果=”;};响应.原型.writeHead=函数writeHead(returnCode){这.结果+ =returnCode+“;”;};响应.原型.结束=函数结束(身体){这.结果+ =身体;};
如果这个响应模拟提高了信心水平,那么它就可以了。至于自信,这对作者来说是主观的。单元测试告诉你编写代码的人在想什么。这增加了程序的清晰度。
代码在这里:测试/嘲笑/ responseMock.js.
自从我介绍了message.marked ()
(转换Markdown到HTML)和message.mustacheTemplate ()
(一个轻量级的模板函数),我可以模拟那些。
它们被附加到MessageMock
:
/* test/mock/messageMock.js */MessageMock.原型.标志着=函数标志着(){返回”;};MessageMock.原型.mustacheTemplate=函数mustacheTemplate(){返回”;};
此时,每个组件返回什么内容并不重要。我主要关心的是确保两者都是模拟的一部分。
拥有很棒的模拟的好处在于,你可以迭代并让它们变得更好。当您发现bug时,您可以加强单元测试,并向反馈循环中添加更多用例。
有了这个,你就能通过测试。是时候将其连接到请求管道了。
在消息/ message.js
做的事:
/* message/message.js */varmustacheTemplate=需要(”。/ mustacheTemplate ');var标志着=需要(“标记”);/ /……模块.出口={mustacheTemplate:mustacheTemplate,/ /……标志着:标志着};
标志着
是我选择作为依赖项添加的Markdown解析器。
添加到package.json
:
“依赖”:{“标记”:“0.3.6”}
mustacheTemplate
消息文件夹中的可重用组件是否位于消息/ mustacheTemplate.js.我决定不将它作为另一个依赖项添加,因为考虑到我需要的特性列表,它似乎有些多余。
胡子模板函数的关键是:
/* message/mustacheTemplate.js */函数胡子(文本,数据){var结果=文本;为(var道具在数据){如果(数据.hasOwnProperty(道具)){var正则表达式=新正则表达式(“{{”+道具+‘}},‘g’);结果=结果.取代(正则表达式,数据[道具]);}}返回结果;}
有单元测试来验证这是可行的。你也可以随便看看这些:测试/ mustacheTemplateTest.js.
您仍然需要添加HTML模板或视图。在视图/ b必威滚logPost.html你可以这样做:
<!——view/b必威滚logPost.html——><身体><div>{{postContent}}div>身体>
完成这些之后,就可以在浏览器中进行演示了。
要尝试,请键入npm开始
然后转到http://localhost:1337/必威滚blog/my-first-post
:
永远不要忽视软件中的模块化、可测试和可重用组件。事实上,不要让任何人说服你接受一个与此敌对的解决方案。任何代码库都可以有干净的代码,即使与框架紧密耦合,所以不要失去希望!
期待
这只是给你一个工作的应用程序。从这一点上,有很多可能性让它准备生产。
一些可能改进的例子包括:
没有限制,在你的世界里,你可以把这个应用带到你想去的地方。
总结
我希望你能看到如何在Node.js中使用一些轻量级依赖来构建解决方案。你所需要的只是一点想象力和对手头问题的关注。您可以使用的api集足以构建一些令人惊叹的东西。
很高兴看到KISS原则是任何解。只解决眼前的问题,并尽可能降低复杂性。
这个工作解决方案在磁盘上的依赖项加起来大约为172KB。这种规模的解决方案在任何web主机上都有令人难以置信的性能。一个反应灵敏、轻量级的应用程序会让用户满意。最棒的是,你现在有了一个不错的微博可以玩,甚至更进一步。必威滚
我很乐意阅读您对该方法的评论和问题,并听听您的想法!