Node.js動(dòng)手?jǐn)]一個(gè)靜態(tài)資源服務(wù)器的方法
簡(jiǎn)介
本文介紹了一個(gè)簡(jiǎn)單的靜態(tài)資源服務(wù)器的實(shí)例項(xiàng)目,希望能給Node.js初學(xué)者帶來幫助。項(xiàng)目涉及到http、fs、url、path、zlib、process、child_process等模塊,涵蓋大量常用api;還包括了基于http協(xié)議的緩存策略選取、gzip壓縮優(yōu)化等;最終我們會(huì)發(fā)布到npm上,做成一個(gè)可以全局安裝、使用的小工具。麻雀雖小,五臟俱全,一想是不是還有點(diǎn)小激動(dòng)?話不多說,放碼過來。
文中源碼地址在最后附錄中。
可先行體驗(yàn)項(xiàng)目效果:
安裝:npm i -g here11
任意文件夾地址輸入命令:here
step1 新建項(xiàng)目
因?yàn)槲覀円l(fā)布到npm上,所以我們先按照國際慣例,npm init,走你!在命令行可以一路回車,有些配置會(huì)在最后的發(fā)布步驟中細(xì)說。
目錄結(jié)構(gòu)如下:

bin文件夾存放我們的執(zhí)行代碼,web作為一個(gè)測(cè)試文件夾,里面放了些網(wǎng)頁。
step2 碼碼
step2.1 雛形
靜態(tài)資源服務(wù)器,通俗講就是我們?cè)跒g覽器地址欄輸入形如“http://域名/test/index.html”的一個(gè)地址,服務(wù)器從根目錄下的對(duì)應(yīng)文件夾找到index.html,讀出文件內(nèi)容并返回給瀏覽器,瀏覽器渲染給用戶。
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}" rel="external nofollow" >${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 {
//拿到路徑所對(duì)應(yīng)的文件描述對(duì)象
let stats = fs.statSync(_path);
if (stats.isFile()) {
//是文件,返回文件內(nèi)容
let file = fs.readFileSync(_path);
res.end(file);
} else if (stats.isDirectory()) {
//是目錄,返回目錄列表,讓用戶可以繼續(xù)點(diǎn)擊
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就是我們的核心代碼了,已經(jīng)實(shí)現(xiàn)了核心功能,本地運(yùn)行即可看到返回了文件目錄,點(diǎn)擊文件名便可瀏覽對(duì)應(yīng)的網(wǎng)頁、圖片、文本啦。
step2.2 優(yōu)化
功能實(shí)現(xiàn)了,但是我們可以在某些方面做做優(yōu)化,提升實(shí)用性,順便多學(xué)習(xí)幾個(gè)api(裝逼技巧)。
1. stream
我們目前讀取文件返回給瀏覽器的操作是通過readFile一次性讀出來,一次性返回,這樣當(dāng)然可以實(shí)現(xiàn)功能,但我們有更好的方式——用stream(流)進(jìn)行IO操作。stream并不是node.js獨(dú)有的概念,而是操作系統(tǒng)最基本的一種操作形式,所以理論上講,任何一門server端語言都實(shí)現(xiàn)了stream的API。
為什么講用stream是一種更好的方式?因?yàn)橐淮涡宰x取、操作大文件,內(nèi)存和網(wǎng)絡(luò)是吃不消的,尤其在用戶訪問量比較大的情況下更為明顯;而借助stream可以讓數(shù)據(jù)流動(dòng)起來,一點(diǎn)一點(diǎn)操作,從而提升性能。代碼修改如下:
if (stats.isFile()) {
//是文件,返回文件內(nèi)容
//在createServer時(shí)傳入的回調(diào)函數(shù)被添加到了"request"事件上,回調(diào)函數(shù)的兩個(gè)形參req和res
//分別為http.IncomingMessage對(duì)象和http.ServerResponse對(duì)象
//并且它們都實(shí)現(xiàn)了流接口
let readStream = fs.createReadStream(_path);
readStream.pipe(res);
}
編碼實(shí)現(xiàn)非常簡(jiǎn)單,在需要返回文件內(nèi)容時(shí),我們創(chuàng)建了一個(gè)可讀流,并把它直接導(dǎo)向了res對(duì)象。
2. gzip壓縮
gzip壓縮帶來的性能(用戶訪問體驗(yàn))提升是非常明顯的,尤其在當(dāng)下spa應(yīng)用大行其道的時(shí)代,開啟gzip壓縮,可以大幅減小js、css等文件資源的體積,提升用戶訪問速度。作為一個(gè)靜態(tài)資源服務(wù)器,我們當(dāng)然要加上這個(gè)功能。
node中有一個(gè)zlib的模塊,提供了很多壓縮相關(guān)的api,我們就用它來實(shí)現(xiàn):
const zlib = require("zlib");
if (stats.isFile()) {
//是文件,返回文件內(nèi)容
res.setHeader("content-encoding", "gzip");
const gzip = zlib.createGzip();
let readStream = fs.createReadStream(_path);
readStream.pipe(gzip).pipe(res);
}
有了stream的使用經(jīng)驗(yàn),我們?cè)倏催@段代碼的時(shí)候就好理解多了。把文件流先導(dǎo)向gzip對(duì)象,再導(dǎo)向res對(duì)象。此外,使用gzip壓縮的時(shí)候還需要注意一點(diǎn):需要把響應(yīng)頭里的content-encoding設(shè)置為gzip。否則瀏覽器會(huì)把一堆亂碼展示出來。
3. http緩存
緩存這個(gè)東西讓人又愛又恨,用得好,可以提升用戶體驗(yàn),減輕服務(wù)器壓力;用得不好,可能就會(huì)面臨各種各樣奇奇怪怪的問題。一般來講瀏覽器http緩存分為強(qiáng)緩存(非驗(yàn)證性緩存)和協(xié)商緩存(驗(yàn)證性緩存)。
什么叫強(qiáng)緩存呢?強(qiáng)緩存是由cache-control和expires兩個(gè)首部字段控制的,現(xiàn)在一般用cache-control。比如我們?cè)O(shè)置了cache-control: max-age=31536000的響應(yīng)頭,就是告訴瀏覽器這個(gè)資源有一年的緩存期,一年內(nèi)不用向服務(wù)端發(fā)送請(qǐng)求,直接從緩存中讀取資源。
而協(xié)商性緩存是使用if-modified-since/last-modified、if-none-match/etag等首部字段,配合強(qiáng)緩存,在強(qiáng)緩存沒有命中(或告知瀏覽器no-cache)的時(shí)候,向服務(wù)器發(fā)送請(qǐng)求,確認(rèn)資源的有效性,決定從緩存中讀取或是返回新的資源。
有了以上概念,我們便可以制定我們的緩存策略:
if (stats.isFile()) {
//是文件,返回文件內(nèi)容
//增加判斷文件是否有改動(dòng),沒有改動(dòng)返回304的邏輯
//從請(qǐng)求頭獲取modified時(shí)間
let IfModifiedSince = req.headers["if-modified-since"];
//獲取文件的修改日期——時(shí)間戳格式
let mtime = stats.mtime;
//如果服務(wù)器上的文件修改時(shí)間小于等于請(qǐng)求頭攜帶的修改時(shí)間,則認(rèn)定文件沒有變化
if (IfModifiedSince && mtime <= new Date(IfModifiedSince).getTime()) {
//返回304
res.writeHead(304, "not modify");
return res.end();
}
//第一次請(qǐng)求或文件被修改后,返回給客戶端新的修改時(shí)間
res.setHeader("last-modified", new Date(mtime).toString());
res.setHeader("content-encoding", "gzip");
let reg = /\.html$/;
//不同的文件類型設(shè)置不同的cache-control
if (reg.test(_path)) {
//我們對(duì)html文件執(zhí)行每次必須向服務(wù)器驗(yàn)證資源有效性的策略
res.setHeader("cache-control", "no-cache");
} else {
//我們對(duì)其余的靜態(tài)資源文件采取強(qiáng)緩存策略,一個(gè)月內(nèi)無需向服務(wù)器索取
res.setHeader("cache-control", `max-age=${1 * 60 * 60 * 24 * 30}`);
}
//執(zhí)行g(shù)zip壓縮
const gzip = zlib.createGzip();
let readStream = fs.createReadStream(_path);
readStream.pipe(gzip).pipe(res);
}
這樣一套緩存策略在現(xiàn)代前端項(xiàng)目體系下還是比較合適的,尤其是對(duì)于spa應(yīng)用來講。我們希望index.html能夠保證每次向服務(wù)器驗(yàn)證是否有更新,而其余的文件統(tǒng)一本地緩存一個(gè)月(自己定);通過webpack打包或其他工程化方式構(gòu)建之后,js、css內(nèi)容如果發(fā)生變化,文件名相應(yīng)更新,index.html插入的manifest(或script鏈接、link鏈接等)清單會(huì)更新,保證用戶能夠?qū)崟r(shí)得到最新的資源。
當(dāng)然,緩存之路千萬條,適合業(yè)務(wù)才重要,大家可以靈活制定。
4. 命令行參數(shù)
作為一個(gè)在命令行執(zhí)行的工具,怎么能不象征性的支持幾個(gè)參數(shù)呢?
const config = {
//從命令行中獲取端口號(hào),如果未設(shè)置采用默認(rèn)
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}`);
});
這里就簡(jiǎn)單的舉個(gè)栗子啦,大家可以自由發(fā)揮!
5. 自動(dòng)打開瀏覽器
雖然沒太大卵用,但還是要加。我就是要讓你們知道,我加完之后什么樣,你們就是什么樣 :-( 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。
我們最終要做成一個(gè)全局并且可以在任意目錄下調(diào)用的命令,所以拼接path的代碼修改如下:
//__dirname是當(dāng)前文件的目錄地址,process.cwd()返回的是腳本執(zhí)行的路徑 _path = path.join(process.cwd(), _path);
step3 發(fā)布
基本上我們的代碼都寫完了,可以考慮發(fā)布了!(不發(fā)布到npm上何以顯示逼格?)
step3.1 package.json
得到一個(gè)配置類似下面所示的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較為重要,其余的按照自己的項(xiàng)目情況填寫。
bin這個(gè)配置代表的是npm i -g xxx之后,我們運(yùn)行here命令所執(zhí)行的文件,“here”這個(gè)名字可以隨意起。
step3.2 聲明腳本執(zhí)行類型
在index.js文件的開頭加上:#!/usr/bin/env node
否則linux上運(yùn)行會(huì)報(bào)錯(cuò)。
step3.3 注冊(cè)npm賬號(hào)
勉強(qiáng)貼一手命令,還不清楚自行百度:
沒有賬號(hào)的先添加一個(gè),執(zhí)行:
npm adduser
然后依次填入
Username: your name
Password: your password
Email: yourmail
npm會(huì)給你發(fā)一封驗(yàn)證郵件,記得點(diǎn)一下,不然會(huì)發(fā)布失敗。
執(zhí)行登錄命令:
npm login
執(zhí)行發(fā)布命令:
npm publish
發(fā)布的時(shí)候記得把項(xiàng)目名字、版本號(hào)、作者、倉庫啥的改一下,別填成我的。
還有readme文件寫一下,好歹告訴別人咋用,基本上和文首所說的用法是一樣的。
好了,齊活。
step3.4
還等啥啊,趕快把npm i -g xxx 這行命令發(fā)給你的小伙伴啊。什么?你沒有小伙伴?告辭!
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解如何使用Node.js連接數(shù)據(jù)庫ORM
這篇文章主要為大家介紹了詳解如何使用Node.js連接數(shù)據(jù)庫ORM示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Node.js使用supervisor進(jìn)行開發(fā)中調(diào)試的方法
今天小編就為大家分享一篇關(guān)于Node.js使用supervisor進(jìn)行開發(fā)中調(diào)試的方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03
nodejs簡(jiǎn)單實(shí)現(xiàn)TCP服務(wù)器端和客戶端的聊天功能示例
這篇文章主要介紹了nodejs簡(jiǎn)單實(shí)現(xiàn)TCP服務(wù)器端和客戶端的聊天功能,結(jié)合實(shí)例形式分析了nodejs基于TCP協(xié)議實(shí)現(xiàn)的聊天程序客戶端與服務(wù)器端具體步驟與相關(guān)操作技巧,代碼備有較為詳盡的注釋便于理解,需要的朋友可以參考下2018-01-01
詳解如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Node.js腳手架
本篇文章主要介紹了如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Node.js腳手架,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12
詳解express + mock讓前后臺(tái)并行開發(fā)
這篇文章主要介紹了詳解express + mock讓前后臺(tái)并行開發(fā),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-06-06

