本文介紹了一個簡單的靜態資源服務器的實例項目,但願能給Node.js初學者帶來幫助。項目涉及到http、fs、url、path、zlib、process、child_process等模塊,涵蓋大量經常使用api;還包括了基於http協議的緩存策略選取、gzip壓縮優化等;最終咱們會發布到npm上,作成一個能夠全局安裝、使用的小工具。麻雀雖小,五臟俱全,一想是否是還有點小激動?話很少說,放碼過來。javascript
文中源碼地址在最後附錄中。
可先行體驗項目效果:
安裝:npm i -g here11
任意文件夾地址輸入命令:herecss
由於咱們要發佈到npm上,因此咱們先按照國際慣例,npm init,走你!在命令行能夠一路回車,有些配置會在最後的發佈步驟中細說。html
目錄結構以下:
bin文件夾存放咱們的執行代碼,web做爲一個測試文件夾,裏面放了些網頁。前端
靜態資源服務器,通俗講就是咱們在瀏覽器地址欄輸入形如「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
功能實現了,可是咱們能夠在某些方面作作優化,提高實用性,順便多學習幾個api(裝逼技巧)。linux
咱們目前讀取文件返回給瀏覽器的操做是經過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
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。不然瀏覽器會把一堆亂碼展現出來。
緩存這個東西讓人又愛又恨,用得好,能夠提高用戶體驗,減輕服務器壓力;用得很差,可能就會面臨各類各樣奇奇怪怪的問題。通常來說瀏覽器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連接等)清單會更新,保證用戶可以實時獲得最新的資源。
固然,緩存之路千萬條,適合業務才重要,你們能夠靈活制定。
做爲一個在命令行執行的工具,怎麼能不象徵性的支持幾個參數呢?
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}`); });
這裏就簡單的舉個栗子啦,你們能夠自由發揮!
雖然沒太大卵用,但仍是要加。我就是要讓大家知道,我加完以後什麼樣,大家就是什麼樣 :-( 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}`); });
用process.cwd()代替__dirname。
咱們最終要作成一個全局而且能夠在任意目錄下調用的命令,因此拼接path的代碼修改以下:
//__dirname是當前文件的目錄地址,process.cwd()返回的是腳本執行的路徑 _path = path.join(process.cwd(), _path);
基本上咱們的代碼都寫完了,能夠考慮發佈了!(不發佈到npm上何以顯示逼格?)
獲得一個配置相似下面所示的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」這個名字能夠隨意起。
在index.js文件的開頭加上:#!/usr/bin/env node
不然linux上運行會報錯。
勉強貼一手命令,還不清楚自行百度:
沒有帳號的先添加一個,執行:
npm adduser
而後依次填入
Username: your name
Password: your password
Email: yourmail
npm會給你發一封驗證郵件,記得點一下,否則會發布失敗。
執行登陸命令:
npm login
執行發佈命令:
npm publish
發佈的時候記得把項目名字、版本號、做者、倉庫啥的改一下,別填成個人。
還有readme文件寫一下,好歹告訴別人咋用,基本上和文首所說的用法是同樣的。
好了,齊活。
還等啥啊,趕快把npm i -g xxx 這行命令發給你的小夥伴啊。什麼?你沒有小夥伴?告辭!
本文項目源碼地址:https://github.com/gww666/here若是對你有幫助,還請不吝star!