Статьи

Создание микроблога с использованием Node.js, Git и Markdown

Создание микроблога с использованием Node.js, Git и Markdown были рецензированы Марком Брауном , Яни Хартикайнен и Джоан Инь . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Писатель спит на своем столе, в окружении компонентов ее микроблога

В современном программировании слово «микро» часто используется: микро-фреймворки, микро-сервисы и т. Д. Для меня это означает решение стоящей перед нами задачи без раздувания. Все, пока решается для единой заботы. Это означает, что нужно сосредоточиться на проблеме и избавиться от ненужных зависимостей.

Я чувствую, что Node следует принципу Златовласки, когда дело доходит до Интернета. Набор API, которые вы получаете из низкоуровневых библиотек, полезен для создания микро-сайтов. Эти API-интерфейсы не слишком сложны и не слишком просты, а просто подходят для создания веб-решений.

В этой статье мы рассмотрим создание микроблога с Node, Git и несколькими зависимостями. Цель этого приложения — обслуживать статический контент из файлов, переданных в хранилище. Вы узнаете, как создать и протестировать приложение, и получите представление о процессе предоставления решения. К концу у вас будет минималистичное рабочее приложение для блога, на котором вы сможете построить.

Основные ингредиенты для микроблога

Чтобы создать потрясающий блог, сначала вам нужно несколько ингредиентов:

  • Библиотека для отправки HTTP-сообщений
  • Репозиторий для хранения сообщений в блоге
  • Модульный тест бегун или библиотека
  • Анализатор Markdown

Чтобы отправить HTTP-сообщение, я выбираю Node, так как это дает мне именно то, что мне нужно для отправки гипертекстового сообщения с сервера. Особый интерес представляют два модуля: http и fs .

Модуль http создаст HTTP-сервер Node. Модуль fs прочитает файл. У Node есть библиотека для создания микроблога с использованием HTTP.

Для хранения репозитория постов в блоге я выберу Git вместо полноценной базы данных. Причиной этого является то, что Git уже является хранилищем текстовых документов с контролем версий. Это как раз то, что мне нужно для хранения данных постов в блоге. Свобода от добавления базы данных в качестве зависимости освобождает меня от написания множества проблем.

Я предпочитаю хранить сообщения в блоге в формате Markdown и анализировать их, используя отмеченные . Это дает мне свободу в отношении постепенного улучшения необработанного контента, если я решу сделать это позже. Markdown — это хорошая, легкая альтернатива простому HTML.

Для юнит-тестов я выбрал отличного раннера roast.it . Я выберу эту альтернативу, потому что она не имеет зависимостей и решает мои проблемы модульного тестирования. Вы можете выбрать другого бегуна, такого как конус , но он имеет около восьми зависимостей. Что мне нравится в roast.it это то, что он не имеет зависимостей.

С этим списком ингредиентов у меня есть все зависимости, необходимые для создания микроблога.

Выбор зависимостей не является тривиальным вопросом. Я думаю, что ключ к чему-либо, что находится вне непосредственной проблемы, может стать зависимостью. Например, я не создаю ни тестера, ни хранилища данных, поэтому он добавляется в список. Любая данная зависимость не должна поглощать решение и удерживать код в заложниках. Таким образом, имеет смысл выбирать только легкие компоненты.

Эта статья предполагает некоторое знакомство с Node , npm и Git , а также с различными методиками тестирования. Я не буду проходить через каждый шаг, связанный с созданием микроблога, скорее я сосредоточусь на конкретных областях кода и буду обсуждать их. Если вы хотите следовать по домам, код находится на GitHub, и вы можете попробовать каждый фрагмент кода, как показано.

тестирование

Тестирование дает вам уверенность в вашем коде и усиливает цикл обратной связи. Цикл обратной связи в программировании — это время, которое требуется между написанием любого нового кода и его выполнением. В любом веб-решении это означает, что нужно прыгать через множество слоев, чтобы получить обратную связь. Например, браузер, веб-сервер и даже база данных. По мере увеличения сложности это может означать минуты или даже час, чтобы получить обратную связь. Благодаря модульным тестам мы отбрасываем эти слои и получаем быструю обратную связь. Это держит акцент на проблеме под рукой.

Мне нравится начинать любое решение с написания быстрого юнит-теста. Это заставляет меня задуматься о написании тестов для любого нового кода. Вот как вы можете начать работать с 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 . В реальном приложении вам не захочется писать все тесты в одном файле. Чтобы обойти это, вам могут require модульные тесты в Node и поместить их в другой файл. Если вы посмотрите на test.js в микроблоге, вы увидите, что именно это я и сделал.

Совет : вы запускаете тесты, используя npm run test . Это может быть сокращено до npm test или даже npm t .

Скелет

Микроблог будет отвечать на запросы клиентов, используя Node. Один из эффективных способов сделать это — через API-интерфейс узла http.CreateServer() . Это можно увидеть в следующем фрагменте из 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); 

Запустите это через скрипт npm в package.json :

 "scripts": { "start": "node app.js" } 

Теперь http://localhost:1337/ становится маршрутом по умолчанию и отвечает сообщением обратно клиенту. Идея состоит в том, чтобы добавить больше маршрутов, которые возвращают другие ответы, например, ответы на сообщения в блоге.

Структура папок

Чтобы создать структуру приложения, я выбрал следующие основные разделы:

Микроблог Скелет

Я буду использовать эти папки для организации кода. Вот обзор того, для чего каждая папка:

  • blog : сохраняет необработанные сообщения в простом уценке
  • 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 поступают из API узла http.createServer() .

Не стесняйтесь изучать маршрут блога в файле 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 и вызывает его с данными файла. Это асинхронное решение использует простой обратный вызов.

Это обеспечивает потребности файла ввода-вывода. Что мне нравится в этом, так это то, что он решает только одну проблему. Поскольку это междисциплинарный вопрос, такой как чтение файла, нет необходимости в модульных тестах. Модульный тест должен проверять только ваш собственный код изолированно, а не чей-либо другой.

Теоретически вы могли бы смоделировать файловую систему в памяти и написать модульные тесты таким образом, но тогда решение повсюду начнет вызывать проблемы и превратится в кашу.

Междисциплинарная проблема, такая как чтение файла, выходит за рамки вашего кода. Например, чтение файла зависит от подсистем, которые находятся вне вашего прямого контроля. Это делает тесты хрупкими и добавляет время и сложность в цикл обратной связи. Это проблема, которая должна быть отделена от вашего решения.

Внутри функции 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() вызывается и имеет правильный путь и обратный вызов.

Фактический объект message который вставляется в BlogRoute происходит из 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 :

Curl Command Demo

Почтовые данные попадают в хранилище через Git. Вы можете сохранить изменения в блоге с помощью git commit .

Уценочный парсер

Для следующей проблемы, время, чтобы превратить необработанные данные Markdown из репозитория в HTML. Есть два шага к этому процессу:

  • Получить шаблон HTML из папки view
  • Разобрать Markdown в HTML и заполнить шаблон

В звуковом программировании идея состоит в том, чтобы взять большую проблему и разбить ее на мелкие кусочки. Давайте решим первую проблему: как мне получить шаблон HTML на основе того, что у меня есть в BlogRoute ?

Одним из подходов может быть:

 /* 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 тесты BlogRoute проходят тестирование / blogRouteTest . Не стесняйтесь осматриваться там, если интересно.

На данный момент у вас есть прохождение тестов! Даже если невозможно проверить весь конвейер запросов, у вас достаточно уверенности, чтобы продолжать работу. Опять же, вот что такое тестирование: оставаться в зоне, сосредоточиться и быть счастливым. Нет причин для печали или разочарования при программировании. Я, конечно, думаю, что вы должны быть счастливым, а не грустным.

Обратите внимание, что экземпляр хранит необработанные данные this.rawContent Markdown в this.rawContent . В работе еще много работы, и вы можете увидеть это в следующем this.renderPost() то есть this.renderPost() ).

В случае, если вы не знакомы с .bind(this) , в JavaScript это эффективный способ расширить функции обратного вызова. По умолчанию обратный вызов переносится во внешнюю область, что в данном случае не годится.

Разбор уценки в HTML

Следующая проблема размера укуса состоит в том, чтобы взять шаблон HTML и необработанные данные контента и собрать их все вместе. Я сделаю это в 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/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 для любого решения. Решение только для неотложных задач и поддержание сложности на минимально возможном уровне.

Это рабочее решение составляет около 172 КБ на диске с зависимостями. Решение такого размера будет иметь невероятную производительность практически на любом веб-хостинге. Адаптивное и легкое приложение порадует пользователей. Самое приятное то, что теперь у вас есть хороший микроблог, с которым можно поиграть и продолжить.

Я хотел бы прочитать ваши комментарии и вопросы о подходе и услышать, что вы думаете!