上一篇文章提到了在小程序本地开发过程中使用 live-server 启动本地服务托管图片,接下来就来探究其原理。
在启动 live-server
后,live-server
会启动一个本地服务并且默认会打开浏览器,例如我在 live-server
内通过 node live-server.js
启动,浏览器会打开如下页面:
从源码里,发现 live-server
使用了 serve-index
中间件来提供页面展示能力:
live-server
启动时会给 staticServer
默认传入当前工作目录 process.cwd()
:
然后在 staticServer
内用第三方库 send
把当前文件发送给浏览器:
这里的 reqpath
就是默认是当前执行目录的地址:
也就是说 send
会去读取这个地址的内容,然后发送给访问者(这里指浏览器)。
但是,这个地址很明显是一个文件夹,为什么最后会变成一个文件列表页面呢?
这就是 serve-index
内部实现的功能了。serve-index
在返回的中间件函数里会做如下操作:
- 判断当前 path 是否存在文件夹内或者名称是否过长;
- 如果当前 path 不是文件夹 就执行下一个中间件;
- 如果是文件夹则用
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
:
此时,请求进入到 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
:
serve-index
返回了一个html,这很正常。接下来点击 hello.js 会报错:
因为 hello.js 不是文件夹,serve-index
不处理,而我们的 node 程序也没有处理这个路由,所以访问不到。
接下来就是整活了,改写 serve-index
代码从而实现 可以返回具体的文件内容:
- 读取文件内容
- 利用
mime
获取文件mime
类型 - 设置响应头
- 返回文件内容
- 将控制权交给下一个中间件
具体代码如下:
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 就可以访问到了:
总结
live-server
使用 serve-index
中间件 提供展示文件列表、点击文件、搜索当前层级文件等能力。由于serve-index
只能查看文件夹的局限性, live-server
内部使用 send
库实现 staticServer
中间件来提供访问文件(路由)的能力。