打包工具算是web生态的基础,不管使用什么框架(React,Vue,Ng),项目总得需要打包才能上线。主流的webpack功能十分丰富,但不是特别容易阅读,所以我以snowpack入门进行相关原理的学习,重点看一下hmr原理,如何打包构建,以及snowpack为什么这么快。
下面直接看相关的函数
index.ts/cli
index.ts是snowpack核心文件,整个工作流程又cli启动,直接看代码
export async function cli(args: string[]) {
// parse CLI flags
const cliFlags = yargs(args, {
array: ['install', 'env', 'exclude', 'external'],
}) as CLIFlags;
// some code
const cmd = cliFlags['_'][2];
logger.debug(`run command: ${cmd}`);
if (!cmd) {
printHelp();
process.exit(1);
}
if (cmd === 'build') {
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
}
if (cmd === 'dev') {
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
}
// some code
if (cmd === 'add') {
await addCommand(cliFlags['_'][3], commandOptions);
return process.exit(0);
}
if (cmd === 'rm') {
await rmCommand(cliFlags['_'][3], commandOptions);
return process.exit(0);
}
if (cliFlags['_'].length > 3) {
logger.error(`Unexpected multiple commands`);
process.exit(1);
}
// some code
if (cmd === 'dev') {
await devCommand(commandOptions);
return process.exit(0);
}
logger.error(`Unrecognized command: ${cmd}`);
process.exit(1);
}
cli方法的任务分为下面几个方面:
- 设置process.env,赶在plugin工作之前设置好,方便后续处理。
- 读取cmd参数,比如build dev,调用相关函数(devCommand)
commands/dev.ts
整个启动流程都在dev.ts内,下面以启动过程中函数的调用关系来看一下
command
cli方法调用的devCommand其实就是command
export async function command(commandOptions: CommandOptions) {
try {
//读取config
commandOptions.config.devOptions.output =
commandOptions.config.devOptions.output || 'dashboard';
commandOptions.config.devOptions.open = commandOptions.config.devOptions.open || 'default';
commandOptions.config.devOptions.hmr = commandOptions.config.devOptions.hmr !== false;
// 启动服务器
await startServer(commandOptions, {isWatch: true});
} catch (err) {
// some code
}
return new Promise(() => {});
}
devCommand的作用如下:
- 读取配置文件(commandOptions.config)
- 调用startServer启动服务器
startServer
startServer方法很长,大概有800行,只看关键部分就可以了
首先是获取端口,毕竟启动服务器总得有个port
if (defaultPort !== 0) {
port = await getPort(defaultPort);
if (port !== defaultPort) {
serverStart = performance.now();
}
}
是否需要开启https/读取证书
if (config.devOptions.secure) {
try {
logger.debug(`reading credentials`);
credentials = await readCredentials(config.root);
} catch (e) {
logger.error(`✘ No HTTPS credentials found!`);
logger.info(`some code`);
process.exit(1);
}
}
如果需要读取证书(config.devOptions.secure),调用readCredentials
获取并执行plugin
for (const runPlugin of config.plugins) {
if (runPlugin.run) {
runPlugin
.run({...})
.then(() => {
logger.info('Command completed.', {name: runPlugin.name});
})
.catch((err) => {
logger.error(`Command exited with error code: ${err}`, {name: runPlugin.name});
process.exit(1);
});
}
}
这里读取了配置文件里面的plugin,每个plugin对象都有一个run方法,关于plugin的原理以后仔细学习
下面是启动服务器
server = createServer(async (req, res) => {
res.on('finish', () => {
// some code
});
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);
这里调用的就是node内置方法,createServer是对http.createServer的封装,整个调用过程就是createServer.on.listen,看一下createServer
const createServer = (responseHandler: http.RequestListener | Http2RequestListener) => {
if (credentials) {
return http2.createSecureServer(
{...credentials!, allowHTTP1: true},
responseHandler as Http2RequestListener,
);
}
return http.createServer(responseHandler as http.RequestListener);
};
前面检测过是否需要https,如果需要就调用http2.createSecureServer
接下来检测是否需要hmr,如果需要hmr调用startHmrEngine
const {hmrEngine, handleHmrUpdate} = config.devOptions.hmr
? startHmrEngine(inMemoryBuildCache, server, port, config)
: {hmrEngine: undefined, handleHmrUpdate: undefined};
监听文件变化,监听功能依靠一个叫chokidar的第三方库来完成
if (isWatch) {
const chokidar = await import('chokidar');
watcher = chokidar.watch([], {
// some code
});
watcher.on('add', async () => {});
// some code
// [hmrDelay] - Let users with noisy startups delay HMR (ex: 11ty, tsc builds)
setTimeout(() => {
watcher!.add(Object.keys(config.mount));
if (config.devOptions.output !== 'dashboard' || !process.stdout.isTTY) {
logger.info(colors.cyan('watching for file changes... '));
}
}, config.devOptions.hmrDelay);
}
最后就是启动浏览器了,调用openInBrowser
if (server && port && open && open !== 'none') {
const protocol = config.devOptions.secure ? 'https:' : 'http:';
await openInBrowser(protocol, hostname, port, open, openUrl).catch((err) => {
logger.debug(`Browser open error: ${err}`);
});
}