基於node.js 的 web server 實現

無依賴包,有nodejs即可,實現代碼

/**
 * web server.js node js 運行的一個web 服務器
 * 特點:
 * 1. 運行時指定端口 : node ./web-server.js 5001
 * 2. 開啓目錄瀏覽,帶參數?!dir: localhost:5001/pic/?!dir
 */
let http = require("http");
let url = require("url");
let fs = require("fs");
let path = require("path");

let config = {
    hostname: "127.0.0.1", //主機ip地址 127.0.0.1
    port: 5000, //端口值 80
    home: ".", //根目錄地址,默認是當前web-server.js目錄。默認值 '.',
    //指定目錄默認訪問頁
    defaultPages: ["index.html", "index.asp", "index.jsp", "index.cshtml"],
    /**指定無法識別的格式的元類型 text/plain application/octet-stream(默認)*/
    unmatchFileAs: "text/plain",
    /** 目錄映射 */
    dirMapping: {
        tool: "",
    },
};

// 0 node 1 file
if (process.argv.length > 2) {
    config.port = process.argv[2];
}

if (config.hostname === "") {
    config.hostname = "0.0.0.0";
}

/**
 * 通過擴展名獲取 mine type
 * @param {*} extName 擴展名 e.g. .txt .html
 * @returns
 */
const getContentType = (extName) => {
    //from nginx mime types
    let mimeTypes = {
        ".html": "text/html",
        ".htm": "text/html",
        ".shtml": "text/html",
        ".css": "text/css",
        ".xml": "text/xml",
        ".gif": "image/gif",
        ".jpeg": "image/jpeg",
        ".jpg": "image/jpeg",
        ".js": "application/javascript",
        ".atom": "application/atom+xml",
        ".rss": "application/rss+xml",
        ".mml": "text/mathml",
        ".txt": "text/plain",
        ".gitignore": "text/plain",
        ".jad": "text/vnd.sun.j2me.app-descriptor",
        ".wml": "text/vnd.wap.wml",
        ".htc": "text/x-component",
        ".png": "image/png",
        ".svg": "image/svg+xml",
        ".svgz": "image/svg+xml",
        ".tif": "image/tiff",
        ".tiff": "image/tiff",
        ".wbmp": "image/vnd.wap.wbmp",
        ".webp": "image/webp",
        ".ico": "image/x-icon",
        ".jng": "image/x-jng",
        ".bmp": "image/x-ms-bmp",
        ".woff": "application/font-woff",
        ".jar": "application/java-archive",
        ".war": "application/java-archive",
        ".ear": "application/java-archive",
        ".json": "application/json",
        ".hqx": "application/mac-binhex40",
        ".doc": "application/msword",
        ".pdf": "application/pdf",
        ".ps": "application/postscript",
        ".eps": "application/postscript",
        ".ai": "application/postscript",
        ".rtf": "application/rtf",
        ".m3u8": "application/vnd.apple.mpegurl",
        ".kml": "application/vnd.google-earth.kml+xml",
        ".kmz": "application/vnd.google-earth.kmz",
        ".xls": "application/vnd.ms-excel",
        ".eot": "application/vnd.ms-fontobject",
        ".ppt": "application/vnd.ms-powerpoint",
        ".odg": "application/vnd.oasis.opendocument.graphics",
        ".odp": "application/vnd.oasis.opendocument.presentation",
        ".ods": "application/vnd.oasis.opendocument.spreadsheet",
        ".odt": "application/vnd.oasis.opendocument.text",
        ".pptx":
            "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        ".xlsx":
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        ".docx":
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        ".wmlc": "application/vnd.wap.wmlc",
        ".7z": "application/x-7z-compressed",
        ".cco": "application/x-cocoa",
        ".jardiff": "application/x-java-archive-diff",
        ".jnlp": "application/x-java-jnlp-file",
        ".run": "application/x-makeself",
        ".pl": "application/x-perl",
        ".pm": "application/x-perl",
        ".prc": "application/x-pilot",
        ".pdb": "application/x-pilot",
        ".rar": "application/x-rar-compressed",
        ".rpm": "application/x-redhat-package-manager",
        ".sea": "application/x-sea",
        ".swf": "application/x-shockwave-flash",
        ".sit": "application/x-stuffit",
        ".tcltk": "application/x-tcl",
        ".der": "application/x-x509-ca-cert",
        ".pem": "application/x-x509-ca-cert",
        ".crt": "application/x-x509-ca-cert",
        ".xpi": "application/x-xpinstall",
        ".xhtml": "application/xhtml+xml",
        ".xspf": "application/xspf+xml",
        ".zip": "application/zip",
        ".msi": "application/octet-stream",
        ".msp": "application/octet-stream",
        ".msm": "application/octet-stream",
        ".mid": "audio/midi",
        ".midi": "audio/midi",
        ".kar": "audio/midi",
        ".mp3": "audio/mpeg",
        ".ogg": "audio/ogg",
        ".m4a": "audio/x-m4a",
        ".ra": "audio/x-realaudio",
        ".3gp": "video/3gpp",
        ".3gpp": "video/3gpp",
        ".ts": "video/mp2t",
        ".mp4": "video/mp4",
        ".mpeg": "video/mpeg",
        ".mpg": "video/mpeg",
        ".mov": "video/quicktime",
        ".webm": "video/webm",
        ".flv": "video/x-flv",
        ".m4v": "video/x-m4v",
        ".mng": "video/x-mng",
        ".asx": "video/x-ms-asf",
        ".asf": "video/x-ms-asf",
        ".wmv": "video/x-ms-wmv",
        ".avi": "video/x-msvideo",
        ".gitignore": "text/plain",
        ".gitattributes": "text/plain",
        "license": "text/plain",
    };
    let contentType = mimeTypes[extName];
    if (contentType === null || contentType === undefined) {
        contentType = config.unmatchFileAs;
    }
    return contentType;
};

/**
 * 獲取請求路徑
 * @param {IncomingMessage} req
 */
const getUrlObj = (req) => {
    let reqUrl = req.url;
    reqUrl = decodeURI(reqUrl);
    let urlObj = url.parse(reqUrl);
    return urlObj;
};

/**
 * 格式化字節單位
 * @param {*} bytes
 * @param {*} decimals 小數點位數
 * @returns
 */
const formatBytes = (bytes, decimals = 2) => {
    if (bytes === 0) return "0 Bytes";
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};

/**
 * 檢測路徑是否存在,同時判斷是否是目錄
 * @param {*} filePath 文件路徑
 * @returns
 */
const checkPathIsDirectory = (filePath) => {
    return fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
};

/**
 * 檢測路徑是否存在,同時判斷是否是文件
 * @param {*} filePath 文件路徑
 * @returns
 */
const checkPathIsFile = (filePath) => {
    return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
};

/**
 * 填充文本到預期長度
 * @param {*} text
 * @param {*} expectLength
 * @param {*} fillUnit
 * @returns
 */
const fillToExpectLength = (text, expectLength, fillUnit = "&nbsp;&nbsp;") => {
    let need = expectLength - text.length;
    if (need <= 0) {
        return text;
    }
    let append = "";
    for (let i = 0; i < need; i++) {
        append += fillUnit;
    }
    return text + append;
};

/**
 *
 * @param {*} filePath
 * @param {*} pathname 請求url上的路徑名
 * @param {*} cb
 */
const buildDirectoryViewPage = (filePath, pathname, cb) => {
    let baseHtml =
        '<!DOCTYPE html><html><head><meta charset="UTF-8" />' +
        '<title>文件瀏覽</title><meta name="viewport" content="width=device-width, initial-scale=1.0" />' +
        "<style> * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }" +
        " .container { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } " +
        "@media (min-width: 768px) { .container { width: 750px; } } @media (min-width: 992px) { .container " +
        "{ width: 970px; } } @media (min-width: 1200px) { .container { width: 1170px; } } .h1, h1 { " +
        "font-size: 36px; margin-top: 20px; margin-bottom: 10px; font-family: inherit; font-weight: 500; " +
        "line-height: 1.1; color: inherit; margin: 0.67em 0; } hr { margin-top: 20px; margin-bottom: 20px;" +
        " border: 0; border-top: 1px solid #eee; height: 0; -webkit-box-sizing: content-box; -moz-box-sizing:" +
        " content-box; box-sizing: content-box; } a {text-overflow: ellipsis;overflow:hidden;" +
        "display:inline-block;color: #337ab7; text-decoration: none; background-color:" +
        " transparent; }a:hover{text-decoration: underline;cursor:pointer;color:green;} " +
        ".well { display: block; padding: 9.5px; margin: 0 0 10px; font-size: 13px; line-height:" +
        " 1.42857143; color: #333; word-break: break-all; word-wrap: break-word; background-color: #f5f5f5; border:" +
        " 1px solid #ccc; border-radius: 4px; } #fork { position: fixed; top: 0; right: 0; _position: absolute;" +
        " z-index: 10000; } .bottom { margin: 20px auto; width: 100%; text-align: center; } .container { min-width:" +
        " 800px; margin: 50px auto; } .well a{width:300px;} .date, .size { display: inline-block; min-width: 100px; margin-left: 100px;" +
        ' } .title span,.title a{vertical-align: middle;}</style></head><body class="container">' +
        '<h1 class="title">[title]</h1><hr /><div class="well">[content]</div></body></html>';
    fs.stat(filePath, (err, stats) => {
        if (!stats.isDirectory()) {
            cb(`${filePath} is not a directory`);
            return;
        }
        let content = `<div><a href="/?!dir">根目錄 /</a></div>`;
        if (pathname == "/") {
            content += `<div><a href="/?!dir">返回上一級 ..</a></div>`;
        } else {
            let newPathname = pathname.substring(0, pathname.length - 1);
            let idx = newPathname.lastIndexOf("/");
            newPathname = newPathname.substring(0, idx + 1);
            content += `<div><a href="${newPathname}?!dir">返回上一級 ..</a></div>`;
        }
        fs.readdir(filePath, (err, files) => {
            let sHideDirs = [];
            let sDirs = [];
            let sHideFiles = [];
            let sFiles = [];
            files.forEach((fileName) => {
                let stats = fs.statSync(path.join(filePath, fileName));
                if (stats.isDirectory()) {
                    if (fileName.indexOf(".") == 0) {
                        sHideDirs.push({
                            name: fileName,
                            mtime: fillToExpectLength(
                                stats.mtime.toLocaleString(),
                                19
                            ),
                            size: "-",
                        });
                    } else {
                        sDirs.push({
                            name: fileName,
                            mtime: fillToExpectLength(
                                stats.mtime.toLocaleString(),
                                19
                            ),
                            size: "-",
                        });
                    }
                }
                if (stats.isFile()) {
                    if (fileName.indexOf(".") == 0) {
                        sHideFiles.push({
                            name: fileName,
                            mtime: fillToExpectLength(
                                stats.mtime.toLocaleString(),
                                19
                            ),
                            size: formatBytes(stats.size),
                        });
                    } else {
                        sFiles.push({
                            name: fileName,
                            mtime: fillToExpectLength(
                                stats.mtime.toLocaleString(),
                                19
                            ),
                            size: formatBytes(stats.size),
                        });
                    }
                }
            });
            sHideDirs.sort();
            sDirs.sort();
            sHideDirs = sHideDirs.concat(sDirs);
            sHideFiles.sort();
            sFiles.sort();
            sHideFiles = sHideFiles.concat(sFiles);

            for (let i = 0; i < sHideDirs.length; i++) {
                let f = sHideDirs[i];
                content += `<div><a href="${pathname + f.name}?!dir" title="${
                    f.name
                }">[D]${f.name}</a ><span class="date">${
                    f.mtime
                }</span><span class="size">-</span></div>`;
            }
            for (let i = 0; i < sHideFiles.length; i++) {
                let f = sHideFiles[i];
                content += `<div><a href="${pathname + f.name}"  title="${
                    f.name
                }">${f.name}</a ><span class="date">${
                    f.mtime
                }</span><span class="size">${f.size}</span></div>`;
            }
            let arr = ["<span>Index Of </span>"];
            let pathArr = pathname.split("/");
            let basePath = "/";
            for (let i = 0; i < pathArr.length; i++) {
                const p = pathArr[i];
                if (p.length === 0) {
                    continue;
                }
                basePath += p + "/";
                arr.push(`<a href="${basePath}?!dir">${p}</a>`);
            }
            let html = baseHtml.replace("[title]", arr.join("<span>/</span>"));
            html = html.replace("[content]", content);
            cb(html);
        });
    });
};

/**
 * 獲取默認頁
 * @param {*} filePath
 */
const getDefaultPage = (filePath) => {
    if (!checkPathIsDirectory(filePath)) {
        return;
    }
    //如果當前文件夾是目錄,就訪問默認頁
    let defaultPages = config.defaultPages;
    if (defaultPages.length === 0) {
        defaultPages = ["index.html"];
    }
    for (let i = 0; i < defaultPages.length; i++) {
        const defaultPage = defaultPages[i];
        let newFilePath = path.join(filePath, defaultPage);
        if (checkPathIsFile(newFilePath)) {
            return newFilePath;
        }
    }
    return filePath;
};

/**
 * 獲取擴展名
 * @param {String} filePath
 */
const getExtname = (filePath) => {
    filePath = filePath.toLowerCase();
    if (!checkPathIsFile(filePath)) {
        return;
    }
    let pathObj = path.parse(filePath);
    if (pathObj.ext === "") {
        return pathObj.base;
    }
    return pathObj.ext;
};

/**
 * 處理301永久跳轉
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const process301Redirect = (req, res) => {
    let urlObj = getUrlObj(req);
    let pathname = urlObj.pathname;
    let search = urlObj.search;
    if (search === null) {
        search = "";
    }
    let filePath = path.join(config.home, pathname);
    /**
     * 跳轉情況:
     * 1.請求路徑爲空
     * 2.判斷請求路徑是不是【文件夾】,如果是文件夾,
     * 同時請求末尾不是 / 結尾,跳轉到有 / 的路徑
     */
    let cond1 = pathname.length === 0;
    let cond2 =
        checkPathIsDirectory(filePath) &&
        pathname.lastIndexOf("/") !== pathname.length - 1;
    if (cond1 || cond2) {
        let newPathname = pathname + "/" + search;
        console.log(
            `[info]:redirect url:from ${pathname + search} to ${newPathname}`
        );
        newPathname = encodeURI(newPathname);
        res.writeHead(301, { Location: newPathname });
        res.end();
        return true;
    }
    return false;
};

/**
 * 處理目錄瀏覽 在路徑末尾加 #!dir 即可進行目錄瀏覽
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const processDirectoryView = (req, res) => {
    let urlObj = getUrlObj(req);
    let pathname = urlObj.pathname;
    let filePath = path.join(config.home, pathname);
    if (checkPathIsDirectory(filePath) && urlObj.query === "!dir") {
        console.log(`[info]:open directory:${filePath}`);

        res.writeHead(200, { "Content-Type": "text/html" });
        buildDirectoryViewPage(filePath, pathname, (html) => res.end(html));
        return true;
    }
    return false;
};

/**
 * 處理MarkDown文件,請求url帶有 !skip 跳過,如 /a.md?!skip
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const processMarkDownFile = (req, res) => {
    let urlObj = getUrlObj(req);
    let pathname = urlObj.pathname;
    let filePath = path.join(config.home, pathname);
    if (!checkPathIsFile(filePath) || urlObj.query === "!skip") {
        return false;
    }
    let pathObj = path.parse(filePath);
    if (!(pathObj.ext === ".md" || pathObj.ext === ".markdown")) {
        return false;
    }

    pathname = encodeURI(pathname);
    res.writeHead(301, { Location: `/index.html?p=${pathname}` });
    res.end();
    return true;
};

/**
 * 處理文件定位
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const processFileLocate = (req, res) => {
    let urlObj = getUrlObj(req);
    let pathname = urlObj.pathname;
    pathname = decodeURI(pathname);
    let filePath = path.join(config.home, pathname);
    //判斷是否是個目錄
    if (checkPathIsDirectory(filePath)) {
        filePath = getDefaultPage(filePath);
    }
    if (!checkPathIsFile(filePath)) {
        res.writeHead(404, { "Content-Type": "text/html" });
        res.end(
            `<!DOCTYPE html><html>
            <head><title>404 Page</title><meta charset="UTF-8"/></head><body>
            <p><h1>404 Page Not Found</h1><h2 style="color:#73D661;" >path:${filePath}</h2>
            <h2>try view current <a style="color:#F9AD39;" href="?!dir">directory</a> ? or go 
            <a style="color:#F9AD39;" href="/?!dir">home</a>?</h2></p></body>`
        );
        return false;
    }
    let extname = getExtname(filePath);
    //在返回頭中寫入內容類型
    res.writeHead(200, {
        "Content-Type": getContentType(extname) + ";charset=utf-8",
    });
    //創建只讀流用於返回
    let stream = fs.createReadStream(filePath, {
        flags: "r",
        encoding: null,
        highWaterMark:64 * 1024
    });

    stream.on("error", () => {
        res.writeHead(404, { "Content-Type": "text/html" });
        res.end(`<h1>404 Read File Error</h1><h2>path:${filePath}</h2`);
    });

    //連接文件流和http返回流的管道,用於返回實際Web內容
    stream.pipe(res);
    return true;
};

/**
 * 啓動 web service
 * @param {*} successCallback 啓動成功的回調函數
 */
const startWebService = (successCallback) => {
    const server = http.createServer((req, res) => {
        let reqUrl = decodeURI(req.url);
        console.log(`[info]: request url: ${reqUrl}`);
        if (process301Redirect(req, res)) {
            return;
        }
        if (processDirectoryView(req, res)) {
            return;
        }
        if (processMarkDownFile(req, res)) {
            return;
        }
        processFileLocate(req, res);
    });

    server.on("error", (error) => {
        console.log(`[error]: ${error}`);
    });

    server.listen(config.port, config.hostname, successCallback);
};

startWebService(() => {
    console.log("/******************** web server ********************/\n");
    console.log(`Server running at http://${config.hostname}:${config.port}/`);
    console.log("press [Ctrl+C] to stop server.");
    console.log("\n/****************** web server **********************/");
});

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章