Snowpack入门02 Server如何工作
Snowpack启动后会在本地启动一个node服务器,用于处理浏览器发起的请求,下面看一下本地server的启动过程。
createServer
该函数位于snowpack/command/dev.ts,是server启动的核心函数
const createServer = (responseHandler: http.RequestListener | Http2RequestListener) => {
if (credentials) {
//https
}
return http.createServer(responseHandler as http.RequestListener);
};
根据配置文件是否写入secure分为两种情况,如果写入secure则需要启动https服务,反之则启动http服务(server直接调用http包的createServer)。
下面看一下createServer的调用过程
if (port) {
server = createServer(async (req, res) => {
// Attach a request logger.
res.on('finish', () => {
const {method, url} = req;
const {statusCode} = res;
logger.debug(`[${statusCode}] ${method} ${url}`);
});
// Otherwise, pass requests directly to Snowpack's request handler.
handleRequest(req, res);
})
.on('upgrade', (req, socket, head) => {
handleUpgrade(req, socket, head);
})
.on('error', (err: Error) => {
logger.error(colors.red(` ✘ Failed to start server at port ${colors.bold(port!)}.`), err);
server!.close();
process.exit(1);
})
.listen(port);
}
createServer接受一个函数用来处理请求,在该函数内调用handleRequest处理浏览器发送的请求
这里面有几个要注意的点:
- finish事件是指server已经向客户端发送响应,但是并不能确定客户端是否已经收到。贴一下node文档原文:Emitted when the response has been sent. More specifically, this event is emitted when the last segment of the response headers and body have been handed off to the operating system for transmission over the network. It does not imply that the client has received anything yet.
- upgrade事件是指server支持升级机制,比如从http升级到websocket。
handleRequest
handlerRequest是snoapack处理请求的核心function,下面是完整源码
async function handleRequest(
req: http.IncomingMessage, res: http.ServerResponse, {handleError}: {handleError?: boolean} = {},) {
let reqUrl = req.url!;
const matchedRouteHandler = matchRouteHandler(reqUrl, 'dest');
if (matchedRouteHandler) {
if (typeof matchedRouteHandler === 'string') {
reqUrl = matchedRouteHandler;
} else {
return matchedRouteHandler(req, res);
}
}
const quickETagCheck = req.headers['if-none-match'];
const quickETagCheckUrl = reqUrl.replace(/\/$/, '/index.html');
if (quickETagCheck && quickETagCheck === knownETags.get(quickETagCheckUrl)) {
logger.debug(`optimized etag! sending 304...`);
res.writeHead(304, {'Access-Control-Allow-Origin': '*'});
res.end();
return;
}
if (
reqUrl.startsWith(PACKAGE_PATH_PREFIX) &&
reqUrl.split('.').length <= 2 &&
config.packageOptions.source !== 'remote'
) {
if (!warnedDeprecatedPackageImport.has(reqUrl)) {
logger.warn(
`(${reqUrl}) Deprecated manual package import. Please use snowpack.getUrlForPackage() to create package URLs instead.`,
);
warnedDeprecatedPackageImport.add(reqUrl);
}
const redirectUrl = await pkgSource.resolvePackageImport(
reqUrl.replace(PACKAGE_PATH_PREFIX, '').replace(/\.js/, ''),
);
res.writeHead(301, {Location: redirectUrl});
res.end();
return;
}
try {
const result = await loadUrl(reqUrl, {allowStale: true, encoding: null});
if (!result) {
throw new NotFoundError(reqUrl);
}
sendResponseFile(req, res, result);
if (result.checkStale) {
await result.checkStale();
}
if (result.contents) {
const tag = etag(result.contents, {weak: true});
const reqPath = decodeURI(url.parse(reqUrl).pathname!);
knownETags.set(reqPath, tag);
}
return;
} catch (err) {
if (handleError === false) {
throw err;
}
handleResponseError(req, res, err);
}
}
处理routes
看一下比较重要的几个部分,首先是matchRoutehandler
function matchRouteHandler(
reqUrl: string,
expectHandler: 'dest' | 'upgrade',
): RouteConfigObject['dest'] | RouteConfigObject['upgrade'] | null {
if (reqUrl.startsWith(config.buildOptions.metaUrlPath)) {
return null;
}
const reqPath = decodeURI(url.parse(reqUrl).pathname!);
const reqExt = matchOutputExt(reqPath);
const isRoute = !reqExt || reqExt.toLowerCase() === '.html';
for (const route of config.routes) {
if (route.match === 'routes' && !isRoute) {
continue;
}
if (!route[expectHandler]) {
continue;
}
if (route._srcRegex.test(reqPath)) {
return route[expectHandler];
}
}
return null;
}
这里的routes是指配置文件的一部分,像下面这样就是一个SPA应用的标准配置,对于所有不包含extension的请求都返回index.html
export default {
routes: [
{
match: 'routes',
src: '.*',
dest: '/index.html',
},
],
};
尝试返回缓存文件
const quickETagCheck = req.headers['if-none-match'];
const quickETagCheckUrl = reqUrl.replace(/\/$/, '/index.html');
if (quickETagCheck && quickETagCheck === knownETags.get(quickETagCheckUrl)) {
logger.debug(`optimized etag! sending 304...`);
res.writeHead(304, {'Access-Control-Allow-Origin': '*'});
res.end();
return;
}
snowpack使用协商缓存来提高速度,当前版本使用etag + if-none-match的缓存策略。snowpack在内存中维护一个map(knownEtags是以url为key,etag为value的map),map将资源的url与etag对应,这样通过缓存提高用户体验,如果etag不匹配则通过loadUrl读取文件并写入缓存。
createServer剩余代码
const remoteIps = Object.values(os.networkInterfaces())
.reduce((every: os.NetworkInterfaceInfo[], i) => [...every, ...(i || [])], [])
.filter((i) => i.family === 'IPv4' && i.internal === false)
.map((i) => i.address);
const protocol = config.devOptions.secure ? 'https:' : 'http:';
// Log the successful server start.
const startTimeMs = Math.round(performance.now() - serverStart);
logger.info(colors.green(`Server started in ${startTimeMs}ms.`));
logger.info(`${colors.green('Local:')} ${`${protocol}//${hostname}:${port}`}`);
if (remoteIps.length > 0) {
logger.info(`${colors.green('Network:')} ${`${protocol}//${remoteIps[0]}:${port}`}`);
}
这里主要是输出一些提示信息,比如ip protocol。
os.netWorkInterface 返回包含已分配网络地址的网络接口的对象。 返回对象上的每个键都标识一个网络接口。 关联的值是每个对象描述一个分配的网络地址的对象数组。