Статьи

Мои мысли о Node.js и Express

Некоторое время назад я написал о том, как я начал видеть Node в новом свете. (Люди называют его «просто» Node или вы всегда включаете JS в конце?) У меня было время на этой неделе, и я решил попробовать создать реальное, хотя и простое, приложение. Поскольку у меня было много данных для моего блога, я подумал, что простое приложение для просмотра блога будет хорошим приложением для постройки. Вот некоторые случайные, рассеянные мысли о том, на что был похож этот процесс.

Прежде всего — я решил использовать Express для создания веб-приложения. Express — это инфраструктура приложений Node, специально созданная для веб-приложений. Я видел Сима Бейтменапродемонстрируйте это на cfObjective в этом году, и это выглядело довольно круто. Что мне сразу понравилось, так это то, как я полностью контролировал обработку запросов. Я легко смог указать папку для статического дерьма, как CSS и JavaScript. Затем я мог бы указать URL-адреса и способы их обработки. В некотором смысле это немного похоже на onRequestStart ColdFusion, но на гораздо более глубоком уровне. Вы получаете возможность, например, сказать, когда приходит запрос на / ray / bio, запускать такой-то код. Но вы также можете легко добавлять динамические шаблоны. Если бы я хотел сопоставить / display / X, где X был динамическим, я мог бы достаточно легко настроить код, и Express автоматически предоставил бы мне доступ к динамической части в качестве переменной. (Прежде чем идти дальше — позвольте мне указать, что я могу спутать то, что дает вам Express, с тем, что просто есть в Node.Пожалуйста, прости меня, если я совершу эту ошибку.)

Вот пример всех трех выше. Из того, что я знаю, шаблоны проверяются в порядке их кодирования.

//To use with static crap, like images, jquery, etc
app.use('/', express.static(__dirname + "/public",{maxAge:86400000}));

//default request for home page
app.get("/", function(req, res) {
   
    blog.getLatestEntries(function(rows) {
        res.render("home",{entries:rows});
    });
    
});

//blog entry by id
app.get("/entry/:id", function(req, res) {
    blog.getBlogEntryById(req.params.id,function(entry) {
        res.render("entry", {entry:entry});
    },function() {
        res.writeHead(302, {
            'Location': '/'
        });
        res.end();
    });
});

Как я уже сказал, первый блок обрабатывает мои статические вещи. Я указал папку с именем public, и внутри нее я бы разместил свои файлы CSS и JS. Когда я вставляю файл стиля, я обращаюсь к нему, просто указывая URL на /public/style.css.

Второй блок обрабатывает запрос домашней страницы. Пока не беспокойтесь о «блоге», я объясню это немного. Но в основном вы можете видеть, что я запрашиваю некоторые данные и затем отображаю их. (Опять же, об этом в секунду.)

Наконец — я добавил поддержку для загрузки одной конкретной записи в блоге. Обратите внимание, что я смог точно определить шаблон URL / entry / X. Я мог бы сделать что-нибудь здесь вообще. Я люблю эту свободу. Чтобы быть ясным, вы можете сделать то же самое в ColdFusion, если вы добавите переписчик URL, как то, что встроено в Apache. Но мне нравится иметь это прямо здесь, в моем приложении. Это немного облегчает умственное понимание того, что происходит в приложении.

Если вы часто посещаете мой блог, вы знаете, что я использую слегка скользкую схему URL. Мне потребовалось 5 минут, чтобы добавить это к моему заявлению:

//blog entry by fancy SES url
app.get("/:year/:month/:day/:alias", function(req, res) {
    var entryDate = new Date(req.params.year,req.params.month-1,req.params.day);
    blog.getBlogEntry(entryDate,req.params.alias,function(entry,comments) {
        res.render("entry", {entry:entry,comments:comments});
    },function() {
        res.writeHead(302, {
            'Location': '/'
        });
        res.end();
    });
});

Хорошо, вот несколько примеров ответа на URL. У меня есть еще кое-что, но давайте поговорим о другом — рендеринг страниц. Я люблю JavaScript, но, черт возьми, я не собираюсь создавать HTML-представления в JavaScript. В последнее время я старался использовать как можно больше шаблонизаторов. Во время исследования я обнаружил, что библиотека EJS работает с Express. Подключить его к Express было довольно просто. Я сказал своему приложению, что мне нужен EJS, и попросил Express использовать его для разбора HTML-файлов. Честно говоря, я не на 100% понимаю эту часть, но это сработало:

app.engine('.html', require('ejs').__express);
app.set('view engine', 'html');

If you look at the app.get(«/») block above, you can see where I run res.render(). The first argument is the name of a template to run. By default this will be in a subdirectory called views. The second argument is data I’m passing to the view. Here is what home.html looks like — and remember — I did this very fast and kinda ugly. Normally you would have a bit more HTML in there:

<h2>Blog</h2>

<ul>
<% 
    entries.forEach(function(entry) { 
        var d = new Date(entry.posted);
        var year = d.getFullYear();
        var month = d.getMonth()+1;
        var day = d.getDate();
%>
    <li><a href="/<%= year + '/' + month + '/' + day %>/<%= entry.alias %>"><%= entry.title %></a> (<%= entry.posted %>)</li>
<% }) %>
</ul>
      

I am not a fan of the template syntax. Frankly it felt like I was writing classic ASP. That being said — it did work. Note that I’m doing a bit of work to create the fancy URL. EJS supports (I believe) writing your own helper functions. Normally I’d have built something to simplify that so that the view had much less logic in it. Just assume that as my first view I didn’t write this as nicely as I would if given more time. As a comparison, here is the view for an individual blog entry:

<h1><%= entry.title %></h1>

<%- entry.body %>
<% if(entry.morebody) { %>
    <%- entry.morebody %>
<% } %>

<% if(comments.length) { %>
    <h2>Comments</h2>
    
    <% comments.forEach(function(comment) { %>
        <p>
        Comment posted by <%= comment.name %><br/>
        Posted at <%= comment.posted %><br/>
        <%= comment.comment %>
        </p>
    <% }) %>
    
<% } %>

As I mentioned, I’m not really a fan of EJS. There are other alternatives. Right now I’m considering Dust, but as I ran into problems with that, I couldn’t actually use it.

Hopefully at this point you have a rough feel for how Express lets you handle requests and specify views to actually render them. Let’s talk about the database layer. After I figured out how to do my views and pass parameters around, I needed to get database support. This is where I got to play around more with NPM. NPM, or the Node Package Manager, is an incredibly powerful tool and probably one of the main reasons Node is so popular. (Other platforms have similar support.) From the command line you can tell Node to get a package (think open source project focused on adding a particular feature) and install it to your system. You can also tell your application itself that it requires a package. So for example, my application needs Express and MySQL support. I can use a special file (package.json) to note these requirements, run one command, and all the supporting libraries just magically come in.

But… this isn’t all rainbows and unicorns. When I decided to add RSS support to my application, I used NPM to search for an RSS library. If I remember right, about 30 or so packages showed up. I froze like a deer in headlights. Don’t get me wrong, I like options, but I had absolutely no idea which one to pick. This is very much like the problem you may have with jQuery plugins. You can almost always count on jQuery having a plugin to do X, but finding out what the «best» one is can be a laborious process. I feel like I’m going to get some criticism on this, but I do wish people would keep this in mind when praising Node. For me, I picked the package with the name «rss» just because it had the simplest name. Luckily, the one I chose worked great. I was able to add RSS support soon after:

//RSS
app.get("/blog.rss", function(req, res) {

    /* lets create an rss feed */
    var feed = new rss({
            title: 'My Blog',
            description: 'This is my blog',
            feed_url: 'http://www.raymondcamden.com/rss.xml',
            site_url: 'http://www.raymondcamden.com',
            image_url: 'http://example.com/icon.png',
            author: 'Raymond Camden'
        });


    blog.getLatestEntries(function(rows) {
        rows.forEach(function(entry) {
            feed.item({
                title: entry.title,
                description: entry.body,
                url: 'http://localhost:3000/' + entry.posted.getFullYear() + '/' + ((entry.posted.getMonth())+1) + '/' + (entry.posted.getDate()) + '/' + entry.alias, // This can be done better
                date: entry.posted // any format that js Date can parse.
            });
        });

        // cache the xml
        var xml = feed.xml();
        res.send(xml);
    });
    
});

But it didn’t always work out well. I mentioned above that I tried to use another templating engine. The first one I tried, Dust, didn’t work because it wasn’t supported on Windows. That really surprised me. Shouldn’t JavaScript work everywhere? To be fair, the reason the project wasn’t supported on Windows was because the author didn’t have an environment to test with (and he did the right thing then in marking it not supported), but I ended up getting stuck for a while.

So going back to database support, it turns out there are a few options for working with MySQL. Unfortunately, none of them really instilled a great deal of confidence in me. In fact, the solution I went with didn’t even support bound parameters! Yes, you could use them in your code, and yes, your data would be escaped, but it wasn’t truly using a bound parameter in it’s communication to the database. And frankly — as much as I like JavaScript, I’m not sure how much I’d trust a database library written in it. I haven’t done any performance tests, but out of everything I did, this was the one area that gave me the most doubt.

With all that being said, using MySQL was pretty easy. I began by just setting up the connection like so:

var mysql = require("mysql");
var con = mysql.createConnection({
        host:"localhost",
        user:"root",
        password:"passwordsareforwimps",
        database:"myblog_dbo"
});
con.connect();

I then created a module called blog that would handle my service layer. Since I had a «con» object that represented my database connection, I exposed an API where I could pass it in to the blog:

var blog = require("./blog.js");
blog.setConnection(con);

Here then is my blog module. A ‘real’ blog engine would have quite a bit more of course but you get the idea.

var CONN;

exports.setConnection = function(con) {
    CONN=con;
}

exports.getLatestEntries = function(cb) {
    CONN.query("select id, title, alias, posted from tblblogentries order by posted desc limit 0,10", function(err, rows, fields) {
        cb(rows);
    });
}

exports.getBlogEntryById = function(id,success,fail) {
    CONN.query("select id, title, body, morebody, posted from tblblogentries where id = ?",[id], function(err, rows, fields) {
        if(rows.length == 1) success(rows[0]);
        else fail();
    });
}

exports.getBlogEntry = function(date,alias,success,fail) {
    var year = date.getFullYear();
    var month = date.getMonth()+1;
    var day = date.getDate();
    CONN.query("select id, title, body, morebody, posted from tblblogentries where year(posted) = ? and month(posted) = ? and dayofmonth(posted) = ? and alias = ?",
    [year,month,day,alias], function(err, rows, fields) {
        if(rows && rows.length == 1) {
            exports.getCommentsForBlogEntry(rows[0].id, function(comments) {
                success(rows[0],comments);
            });
        }
        else fail();
    });
}

exports.getCommentsForBlogEntry = function(id,success) {
    CONN.query("select id, name, email, comment, posted, website from tblblogcomments where entryidfk = ?", [id], function(err, rows, fields) {
        if(!rows || !rows.length) rows = [];
        success(rows);
    });
}

So what do I think? I love it. Once I got my environment running (and be sure to use nodemon to make reloading automatic) I was able to rapidly build out a simple application. I loved the level of control I had over the request and how quick it was to get up and running. I didn’t love the fact that the quality wasn’t quite consistent across various modules.

p.s. One more code snippet. I demonstrated the RSS support above. But I also built in a quick JSON view as well. It was incredibly difficult. Honest. This took me hours to write. (Heh…)

//API
app.get("/entries.json", function(req, res) {
    blog.getLatestEntries(function(rows) {
        res.writeHead(200, {'Content-Type':'application/json'});
        res.write(JSON.stringify(rows));
        res.end();
    });    
});