从零开始,做一个NodeJS博客,二:实现首页-加载文章列表和详情

标签: NodeJS


0

这个伪系列的第二篇,不过和之前的几篇是同一天写的。三分钟热度貌似还没过。

1 静态资源代理

上一篇,我们是通过判断请求的路径来直接返回结果的。简单粗暴,缺点明显:如果url后面加杂了queryString,因为判断逻辑中没有处理,那么将直接返回404页面(其实也没有其他的页面)。

难道要一个一个加queryString的处理?有可行性,但麻烦。

再者,如果要添加新的html页面,那么也要在处理逻辑中依次添加?html页面还要引用js脚本,css样式,样式表中还要引用图片,字体。。。。难道就要这样无休止的添加下去?显然是不可能的。

借助于NodeJS提供的urlAPI,我们可以提取请求url中的pathName部分,也就是忽略了域名以及queryString的部分,然后将他交给另一个pathAPI。他负责将pathName解析为当前操作系统可读的路径。

至此,答案就很明显了:拿到路径后,利用fs模块读取,如果成功,则返回文件内容;否则直接404就好了。

这里我们只用到了urlpath模块的很小一部分功能,我也就只写这一点了。具体的定义及应用请参考NodeJS官方API文档

url.prase( urlString )

接收一个url字符串,将它转为URL对象。

url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash');

它的返回值将会是一个 Object:

Url {
    protocol: 'http:',
    slashes: true,
    auth: 'user:pass',
    host: 'host.com:8080',
    port: '8080',
    hostname: 'host.com',
    hash: '#hash',
    search: '?query=string',
    query: 'query=string',
    pathname: '/path/to/file',
    path: '/path/to/file?query=string',
    href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' 
 }

看看这个结构图,应该更容易理解:

┌─────────────────────────────────────────────────────────────────────────────┐
│                                    href                                     │
├──────────┬┬───────────┬─────────────────┬───────────────────────────┬───────┤
│ protocol ││   auth    │      host       │           path            │ hash  │
│          ││           ├──────────┬──────┼──────────┬────────────────┤       │
│          ││           │ hostname │ port │ pathname │     search     │       │
│          ││           │          │      │          ├─┬──────────────┤       │
│          ││           │          │      │          │ │    query     │       │
"  http:   // user:pass @ host.com : 8080   /p/a/t/h  ?  query=string   #hash "
│          ││           │          │      │          │ │              │       │
└──────────┴┴───────────┴──────────┴──────┴──────────┴─┴──────────────┴───────┘

以上抄自:NodeJS官方API文档:URL

所以,要拿到相对路径,我们只需要取 Url.pathname 就可以了。

path.resolve( path[, ...] )

接收一组按顺序排好的路径,并把它组合为一个当前操作系统可识别的绝对路径。

path.resolve('.', '/archive/my-first-article')

如果在windows中,当前工作目录为 C:\Users\rocka,它将返回

'C:\Users\rocka\archive\my-first-article'

而在linux环境,/home/rocka目录中,它的返回值是

'/home/rocka/archive/my-first-article'

与当前操作系统直接关联。至于为何要跟当前工作目录关联,因为此时,它的第一个参数是 . ,不就是当前目录嘛。

path.join( path[, ...] )

接收一组路径字符串,然后将他们拼起来。可以用 .. 这样的相对路径。

返回值仍是字符串哦。

path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')

将返回:

'/foo/bar/baz/asdf'

直接贴代码咯(代码片段,省略了require和listen等):

var root = path.resolve('.');

var server = http.createServer((request, response) => {
    console.log(`[Rocka Node Server] ${request.method}: ${request.url}`);
    // path name in url
    var pathName = url.parse(request.url).pathname;
    // file path based on operation system
    var filePath = path.join(root, pathName);
    console.log(`[Rocka Node Server] pathName: ${pathName}, filePath: ${filePath}`);
    if (request.method === 'GET') {
        // try to find and read local file
        fs.stat(filePath, (err, stats) => {
            // no error occured, read file
            if (!err && stats.isFile()) {
                response.writeHead(200, { 'Content-Type': 'text/html' });
                fs.createReadStream(filePath).pipe(response);
            // cannot find file, but received index request
            } else if (!err && pathName == '/') {
                response.writeHead(200, { 'Content-Type': 'text/html' });
                fs.createReadStream('./page/index.html').pipe(response);
            // file not found
            } else if (!err && !stats.isFile()) {
                response.writeHead(200, { 'Content-Type': 'text/html' });
                fs.createReadStream('./page/404.html').pipe(response);
            // error :(
            } else if (err) {
                response.writeHead(500);
                response.end(err.toString());
            }
        });
    }
});

嗯,看起来很工整。下面我们来试着跑一下。

哦,等会,根据刚才404页面和首页的位置,我的目录结构应该重构了,就像这样

  • page
    • 404.html
    • index.html
  • favicon.ico
  • package.json
  • Procfile
  • server.js

之后打开监听的端口,看到首页了,favicon也正常加载,完成!

2 读取文章列表

上一篇我说过,暂时使用单独的文件来存放文章。所以文件名不仅要是文章的标题,还唯一标识着这篇文章对应的文件。

目录又要改了:

  • archive
    • my-first-article
    • my-second-article
  • page
    • 404.html
    • index.html
  • script
    • index.js
  • favicon.ico
  • package.json
  • Procfile
  • server.js

读取目录内所有文件,又要用到fs模块了。这次用的是 fs.readdir(path[, options], callback)

它读取指定目录内所有文件,并把两个参数 (err, files) 传递给回调函数。files 是一个包含目录内所有文件名的数组。

这样思路就很清晰了!我们读取 archive 下的所有文件,然后解析成json发出去就好。我说过要用Ajax的嘛。

(先别管怎么接收)

这个请求不同于文件请求,是一个“莫须有”的路径,需要我们来定义一下。我说了:

/api/index-article-list

这就是API路径。好了,现在把他写到服务器中去:

var server = http.createServer((request, response) => {
    // parse the path
    var pathName = url.parse(request.url).pathname;
    var filePath = path.join(root, pathName);
    if (request.method === 'GET') {
        // this is a api request
        if (pathName.indexOf('/api/') >= 0) {
            switch (pathName) {
                // api address
                case '/api/index-article-list':
                    // read all archives
                    fs.readdir('./archive', (err, files) => {
                        if (err) {
                            console.log(err);
                        } else {
                            response.writeHead(200, { 'Content-Type': 'application/json' });
                            // parse the Array<String> to json string
                            response.end(JSON.stringify(files));
                        }
                    });
                    break;
                default:
                    break;
            }
        } else {
            // read local file
            });
        }
    }
});

收到请求时,看看它是不是有定义的API请求。如果是的话,执行并返回,否则才去找文件。

服务端准备完成了,接下来是页面的动态加载:

贴一下我的 index.html

<!DOCTYPE html>
<html >

<head>
    <meta charset="UTF-8">
    <title>Rocka's Node Blog</title>
    <script src="../script/index.js"></script>
</head>

<body>
    <h1>Rocka's Node Blog</h1>
    <hr>
    <h3>Blog Archive</h3>
    <ul >
    </ul>
    <blockquote >Article should be shown here.</blockquote>
</body>

</html>

接下来是 index.js

'use strict';

function loadArticleList() {
    var ul = document.getElementById('index-article-list');

    function success(response) {
        var resData = JSON.parse(response);
        resData.forEach((title) => {
            var newLine = document.createElement('li');
            newLine.innerHTML = `<a href="javascript:void(0);">${title}</a>`;
            ul.appendChild(newLine);
        });
    }

    function fail(code) {
        var newLine = document.createElement('li');
        newLine.innerText = `List Load Faild: Please Refresh Page And Try Again.`;
        ul.appendChild(newLine);
    }

    var request = new XMLHttpRequest(); // New XMLHttpRequest Object

    request.onreadystatechange = () => { // invoked when readyState changes
        if (request.readyState === 4) { // request succeed
            // response result:
            if (request.status === 200) {
                // succeed: update article
                return success(request.response);
            } else {
                // failed: show error code
                return fail(request.status);
            }
        }
    }

    // send request
    request.open('GET', '/api/index-article-list');
    request.send();
}

window.onload = () => {
    console.log('Welcome to Rocka\'s Node Blog! ');
    loadArticleList();
}

这里我用了js原生的 XMLHttpRequest ,并没有用jQuery提供的Ajax方法。

至于为什么,可能是之前一直用jQuery,有点审美疲劳了,想换个方法玩玩。

不过,一用上原生方法,我就发现,这是什么智障写法!!

一个破 Object ,还on这个on那个,自己还带那么多方法和属性,你以为你是谁啊,我凭什么要记这么多方法这么多属性!!!

果然jQuery大法好啊!!!!

Write less, do more. —— jQuery

3 读取文章详情

按照先前的定义,文章是存在单独的文件里的。里面只有一句简简单单的话:

my-first-articlemy-second-article
I'm Very Excited!Stay young, Stay simple.

于是这就很明显了!直接请求 /archive/${title} ,由于有这个文件的存在,内容就可以出来了!

这下连API都不用写了!

于是,愉快的在 index.js 中补刀一个函数(不要吐槽 XMLHttpRequest 的写法):

function loadArticleContent(articleTitle) {
    var bq = document.getElementById('index-article-content');

    function success(response) {
        bq.innerText = response;
    }

    function fail(code) {
        bq.innerText = 'Article Load Faild: Please Refresh Page And Try Again.';
        bq.innerText += `Error Code: ${code}`;
    }

    var request = new XMLHttpRequest();

    request.onreadystatechange = () => {
        if (request.readyState === 4) {
            if (request.status === 200) {
                return success(request.response);
            } else {
                return fail(request.status);
            }
        }
    }

    request.open('GET', `/archive/${articleTitle}`);
    request.send();
}

然后在加载标题时,把它的href指向这个函数就好了!

newLine.innerHTML = `<a href="javascript:loadArticleContent('${title}');">${title}</a>`;

保存,运行,刷新,提交,构建,完成!

愉快的一天就这么结束了233

仓库地址

BlogNode

主仓库,以后的代码都在这里更新。

rocka-blog-node

上面GitHub仓库的实时构建结果。