Node.js新手上路——动手撸一个静态资源服务器

简介

本文介绍了一个简单的静态资源服务器的实例项目,但愿能给Node.js初学者带来帮助。项目涉及到http、fs、url、path、zlib、process、child_process等模块,涵盖大量经常使用api;还包括了基于http协议的缓存策略选取、gzip压缩优化等;最终咱们会发布到npm上,作成一个能够全局安装、使用的小工具。麻雀虽小,五脏俱全,一想是否是还有点小激动?话很少说,放码过来。javascript

文中源码地址在最后附录中。
可先行体验项目效果:
安装:npm i -g here11
任意文件夹地址输入命令:herecss

step1 新建项目

由于咱们要发布到npm上,因此咱们先按照国际惯例,npm init,走你!在命令行能够一路回车,有些配置会在最后的发布步骤中细说。html

目录结构以下:
图片描述
bin文件夹存放咱们的执行代码,web做为一个测试文件夹,里面放了些网页。前端

step2 码码

step2.1 雏形

静态资源服务器,通俗讲就是咱们在浏览器地址栏输入形如“http://域名/test/index.html”的一个地址,服务器从根目录下的对应文件夹找到index.html,读出文件内容并返回给浏览器,浏览器渲染给用户。java

const http = require("http");
const url = require("url");
const fs = require("fs");
const path = require("path");

const item = (name, parentPath) => {
    let path = parentPath = `${parentPath}/${name}`.slice(1);
    return `<div><a href="${path}">${name}</a></div>`;
}

const list = (arr, parentPath) => {
    return arr.map(name => item(name, parentPath)).join("");
}

const server = http.createServer((req, res) => {
    let _path = url.parse(req.url).pathname;//去掉search
    let parentPath = _path;
    _path = path.join(__dirname, _path);
    try {
        //拿到路径所对应的文件描述对象
        let stats = fs.statSync(_path);
        if (stats.isFile()) {
            //是文件,返回文件内容
            let file = fs.readFileSync(_path);
            res.end(file);
        } else if (stats.isDirectory()) {
            //是目录,返回目录列表,让用户能够继续点击
            let dirArray = fs.readdirSync(_path);
            res.end(list(dirArray, parentPath));
        } else {
            res.end();
        }
    } catch (err) {
        res.writeHead(404, "Not Found");
        res.end();
    }
});

const port = 2234;
const hostname = "127.0.0.1";
server.listen(port, hostname, () => {
    console.log(`server is running on http://${hostname}:${port}`);
});

以上这段code就是咱们的核心代码了,已经实现了核心功能,本地运行便可看到返回了文件目录,点击文件名即可浏览对应的网页、图片、文本啦。node

step2.2 优化

功能实现了,可是咱们能够在某些方面作作优化,提高实用性,顺便多学习几个api(装逼技巧)。linux

1. stream

咱们目前读取文件返回给浏览器的操做是经过readFile一次性读出来,一次性返回,这样固然能够实现功能,但咱们有更好的方式——用stream(流)进行IO操做。stream并非node.js独有的概念,而是操做系统最基本的一种操做形式,因此理论上讲,任何一门server端语言都实现了stream的API。webpack

为何讲用stream是一种更好的方式?由于一次性读取、操做大文件,内存和网络是吃不消的,尤为在用户访问量比较大的状况下更为明显;而借助stream可让数据流动起来,一点一点操做,从而提高性能。代码修改以下:git

if (stats.isFile()) {
    //是文件,返回文件内容
    //在createServer时传入的回调函数被添加到了"request"事件上,回调函数的两个形参req和res
    //分别为http.IncomingMessage对象和http.ServerResponse对象
    //而且它们都实现了流接口
    let readStream = fs.createReadStream(_path);
    readStream.pipe(res);
}

编码实现很是简单,在须要返回文件内容时,咱们建立了一个可读流,并把它直接导向了res对象。github

2. gzip压缩

gzip压缩带来的性能(用户访问体验)提高是很是明显的,尤为在当下spa应用大行其道的时代,开启gzip压缩,能够大幅减少js、css等文件资源的体积,提高用户访问速度。做为一个静态资源服务器,咱们固然要加上这个功能。

node中有一个zlib的模块,提供了不少压缩相关的api,咱们就用它来实现:

const zlib = require("zlib");

if (stats.isFile()) {
    //是文件,返回文件内容

    res.setHeader("content-encoding", "gzip");
    
    const gzip = zlib.createGzip();
    let readStream = fs.createReadStream(_path);
    readStream.pipe(gzip).pipe(res);
}

有了stream的使用经验,咱们再看这段代码的时候就好理解多了。把文件流先导向gzip对象,再导向res对象。此外,使用gzip压缩的时候还须要注意一点:须要把响应头里的content-encoding设置为gzip。不然浏览器会把一堆乱码展现出来。

3. http缓存

缓存这个东西让人又爱又恨,用得好,能够提高用户体验,减轻服务器压力;用得很差,可能就会面临各类各样奇奇怪怪的问题。通常来说浏览器http缓存分为强缓存(非验证性缓存)和协商缓存(验证性缓存)。

什么叫强缓存呢?强缓存是由cache-control和expires两个首部字段控制的,如今通常用cache-control。好比咱们设置了cache-control: max-age=31536000的响应头,就是告诉浏览器这个资源有一年的缓存期,一年内不用向服务端发送请求,直接从缓存中读取资源。

而协商性缓存是使用if-modified-since/last-modified、if-none-match/etag等首部字段,配合强缓存,在强缓存没有命中(或告知浏览器no-cache)的时候,向服务器发送请求,确认资源的有效性,决定从缓存中读取或是返回新的资源。

有了以上概念,咱们即可以制定咱们的缓存策略:

if (stats.isFile()) {
    //是文件,返回文件内容
    
    //增长判断文件是否有改动,没有改动返回304的逻辑
    
    //从请求头获取modified时间
    let IfModifiedSince = req.headers["if-modified-since"];
    //获取文件的修改日期——时间戳格式
    let mtime = stats.mtime;
    //若是服务器上的文件修改时间小于等于请求头携带的修改时间,则认定文件没有变化
    if (IfModifiedSince && mtime <= new Date(IfModifiedSince).getTime()) {
        //返回304
        res.writeHead(304, "not modify");
        return res.end();
    }
    //第一次请求或文件被修改后,返回给客户端新的修改时间
    res.setHeader("last-modified", new Date(mtime).toString());
    res.setHeader("content-encoding", "gzip");
    let reg = /\.html$/;
    //不一样的文件类型设置不一样的cache-control
    if (reg.test(_path)) {
        //咱们对html文件执行每次必须向服务器验证资源有效性的策略
        res.setHeader("cache-control", "no-cache");
    } else {
        //咱们对其他的静态资源文件采起强缓存策略,一个月内无需向服务器索取
        res.setHeader("cache-control", `max-age=${1 * 60 * 60 * 24 * 30}`);
    }
    
    //执行gzip压缩
    const gzip = zlib.createGzip();
    let readStream = fs.createReadStream(_path);
    readStream.pipe(gzip).pipe(res);
}

这样一套缓存策略在现代前端项目体系下仍是比较合适的,尤为是对于spa应用来说。咱们但愿index.html可以保证每次向服务器验证是否有更新,而其他的文件统一本地缓存一个月(本身定);经过webpack打包或其余工程化方式构建以后,js、css内容若是发生变化,文件名相应更新,index.html插入的manifest(或script连接、link连接等)清单会更新,保证用户可以实时获得最新的资源。

固然,缓存之路千万条,适合业务才重要,你们能够灵活制定。

4. 命令行参数

做为一个在命令行执行的工具,怎么能不象征性的支持几个参数呢?

const config = {
    //从命令行中获取端口号,若是未设置采用默认
    port: process.argv[2] || 2234,
    hostname: "127.0.0.1"
}
server.listen(config.port, config.hostname, () => {
    console.log(`server is running on http://${config.hostname}:${config.port}`);
});

这里就简单的举个栗子啦,你们能够自由发挥!

5. 自动打开浏览器

虽然没太大卵用,但仍是要加。我就是要让大家知道,我加完以后什么样,大家就是什么样 :-( duang~

const exec = require("child_process").exec;
server.listen(config.port, config.hostname, () => {
    console.log(`server is running on http://${config.hostname}:${config.port}`);
    exec(`open http://${config.hostname}:${config.port}`);
});

6. process.cwd()

用process.cwd()代替__dirname。
咱们最终要作成一个全局而且能够在任意目录下调用的命令,因此拼接path的代码修改以下:

//__dirname是当前文件的目录地址,process.cwd()返回的是脚本执行的路径
_path = path.join(process.cwd(), _path);

step3 发布

基本上咱们的代码都写完了,能够考虑发布了!(不发布到npm上何以显示逼格?)

step3.1 package.json

获得一个配置相似下面所示的json文件:

{
    "name": "here11",
    "version": "0.0.13",
    "private": false,
    "description": "a node static assets server",
    "bin": {
        "here": "./bin/index.js"
    },
    "repository": {
        "type": "git",
        "url": "https://github.com/gww666/here.git"
    },
    "scripts": {
        "test": "node bin/index.js"
    },
    "keywords": [
        "node"
    ],
    "author": "gw666",
    "license": "ISC"
}

其中bin和private较为重要,其他的按照本身的项目状况填写。
bin这个配置表明的是npm i -g xxx以后,咱们运行here命令所执行的文件,“here”这个名字能够随意起。

step3.2 声明脚本执行类型

在index.js文件的开头加上:#!/usr/bin/env node
不然linux上运行会报错。

step3.3 注册npm帐号

勉强贴一手命令,还不清楚自行百度:

没有帐号的先添加一个,执行:
npm adduser

而后依次填入
Username: your name
Password: your password
Email: yourmail

npm会给你发一封验证邮件,记得点一下,否则会发布失败。

执行登陆命令:
npm login

执行发布命令:
npm publish

发布的时候记得把项目名字、版本号、做者、仓库啥的改一下,别填成个人。
还有readme文件写一下,好歹告诉别人咋用,基本上和文首所说的用法是同样的。

好了,齐活。

step3.4

还等啥啊,赶快把npm i -g xxx 这行命令发给你的小伙伴啊。什么?你没有小伙伴?告辞!

本文项目源码地址:https://github.com/gww666/here若是对你有帮助,还请不吝star!

相关文章
相关标签/搜索