从源码分析live-server 是如何启动本地服务并且托管文件的?

上一篇文章提到了在小程序本地开发过程中使用 live-server 启动本地服务托管图片,接下来就来探究其原理。

在启动 live-server 后,live-server 会启动一个本地服务并且默认会打开浏览器,例如我在 live-server 内通过 node live-server.js 启动,浏览器会打开如下页面:

image.png

从源码里,发现 live-server 使用了 serve-index 中间件来提供页面展示能力:

image.png

live-server 启动时会给 staticServer 默认传入当前工作目录 process.cwd():

image.png

然后在 staticServer 内用第三方库 send 把当前文件发送给浏览器:

image.png

这里的 reqpath 就是默认是当前执行目录的地址:

image.png

也就是说 send 会去读取这个地址的内容,然后发送给访问者(这里指浏览器)。
但是,这个地址很明显是一个文件夹,为什么最后会变成一个文件列表页面呢?

这就是 serve-index 内部实现的功能了。serve-index 在返回的中间件函数里会做如下操作:

  1. 判断当前 path 是否存在文件夹内或者名称是否过长;
  2. 如果当前 path 不是文件夹 就执行下一个中间件;
  3. 如果是文件夹则用 fs.readdir 读取文件夹,并在回调内判断当前 mediaType 是否是
    'text/html'、 'text/plain'、 'application/json'这三种,如果是就执行对应的 Response 方法。
function serveIndex(root, options) {
 
     return function (req, res, next) {
          fs.stat(path, function(err, stat){
              if (err && err.code === 'ENOENT') {
                return next();
              }
        
              if (err) {
                err.status = err.code === 'ENAMETOOLONG'
                  ? 414
                  : 500;
                return next(err);
              }
        
              if (!stat.isDirectory()) return next();
            
              fs.readdir(path, function(err, files){
                // ......
        
                // content-negotiation
                var accept = accepts(req);
                var type = accept.type(mediaTypes);
        
                // not acceptable
                if (!type) return next(createError(406));
                serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet);
              });
            });
          };
        };
    }
}

例如,mediaType === 'text/html' ,则会执行 serveIndex.html

serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) {
    var render = typeof template !== 'function'
        ? createHtmlRender(template)
        : template
    
    //.....
    
    // stat all files
    stat(path, files, function (err, stats) {
        if (err) return next(err);

        // ......
        
        // read stylesheet
        fs.readFile(stylesheet, 'utf8', function (err, style) {
            if (err) return next(err);

            // create locals for rendering
            var locals = {
                directory: dir,
                displayIcons: Boolean(icons),
                fileList: fileList,
                path: path,
                style: style,
                viewName: view
            };

            // render html
            render(locals, function (err, body) {
                if (err) return next(err);
                send(res, 'text/html', body)
            });
        });
    });
};

serve-index 提供了默认的页面模板,在返回页面前对页面内容进行替换、生成文件列表等:

/**
 * Create function to render html.
 */

function createHtmlRender(template) {
    return function render(locals, callback) {
        // read template
        fs.readFile(template, 'utf8', function (err, str) {
            if (err) return callback(err);

            var body = str
                .replace(/\{style\}/g, locals.style.concat(iconStyle(locals.fileList, locals.displayIcons)))
                .replace(/\{files\}/g, createHtmlFileList(locals.fileList, locals.directory, locals.displayIcons, locals.viewName))
                .replace(/\{directory\}/g, escapeHtml(locals.directory))
                .replace(/\{linked-path\}/g, htmlPath(locals.directory));

            callback(null, body);
        });
    };
}


function createHtmlFileList(files, dir, useIcons, view) {
    var html = '<ul id="files" class="view-' + escapeHtml(view) + '">'
        + (view == 'details' ? (
            '<li class="header">'
            + '<span class="name">Name</span>'
            + '<span class="size">Size</span>'
            + '<span class="date">Modified</span>'
            + '</li>') : '');

    html += files.map(function (file) {
        var classes = [];
        var isDir = file.stat && file.stat.isDirectory();
        var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });

        if (useIcons) {
            classes.push('icon');

            if (isDir) {
                classes.push('icon-directory');
            } else {
                var ext = extname(file.name);
                var icon = iconLookup(file.name);

                classes.push('icon');
                classes.push('icon-' + ext.substring(1));

                if (classes.indexOf(icon.className) === -1) {
                    classes.push(icon.className);
                }
            }
        }

        path.push(encodeURIComponent(file.name));

        var date = file.stat && file.name !== '..'
            ? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
            : '';
        var size = file.stat && !isDir
            ? file.stat.size
            : '';

        return '<li><a href="'
            + escapeHtml(normalizeSlashes(normalize(path.join('/'))))
            + '" class="' + escapeHtml(classes.join(' ')) + '"'
            + ' title="' + escapeHtml(file.name) + '">'
            + '<span class="name">' + escapeHtml(file.name) + '</span>'
            + '<span class="size">' + escapeHtml(size) + '</span>'
            + '<span class="date">' + escapeHtml(date) + '</span>'
            + '</a></li>';
    }).join('\n');

    html += '</ul>';

    return html;
}

回到 live-server, live-server 启动时默认会执行 open(openURL + openPath) 打开浏览器,页面地址为 http://127.0.0.1:8080

image.png

此时,请求进入到 serve-index 中间件会生成并发送一个html文件到浏览器,也就是文章的第一张图。

从上文我们知道 live-server 是利用 send 库来返回文件内容的,而 serve-index 本身无法访问具体某个文件。

例如当我写了如下node程序 :

// hello.js
const express = require('express')
const app = express()
const serveIndex = require('serve-index');

app.use('/api',
  serveIndex(process.cwd(), { icons: true })
)

app.listen(3000);
console.log('Express started on port 3000');

用了 serve-index 作为中间件,打开浏览器访问 localhost:3000/api

image.png

serve-index 返回了一个html,这很正常。接下来点击 hello.js 会报错:

image.png

因为 hello.js 不是文件夹,serve-index 不处理,而我们的 node 程序也没有处理这个路由,所以访问不到。

接下来就是整活了,改写 serve-index 代码从而实现 可以返回具体的文件内容:

  1. 读取文件内容
  2. 利用 mime 获取文件 mime 类型
  3. 设置响应头
  4. 返回文件内容
  5. 将控制权交给下一个中间件

具体代码如下:

  if (!stat.isDirectory()) {
        // return next();
        var body = fs.readFileSync(path);
        var type = mime.lookup(extname(path));
        res.setHeader('Content-Type', type)
        res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
        res.send(body);
        return next();
     }

改写后,再去点击 hello.js 就可以访问到了:


image.png

总结

live-server 使用 serve-index中间件 提供展示文件列表、点击文件、搜索当前层级文件等能力。由于serve-index只能查看文件夹的局限性, live-server 内部使用 send 库实现 staticServer 中间件来提供访问文件(路由)的能力。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容