snowpack入门 01 snowpack的启动过程


打包工具算是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方法的任务分为下面几个方面:

  1. 设置process.env,赶在plugin工作之前设置好,方便后续处理。
  2. 读取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的作用如下:

  1. 读取配置文件(commandOptions.config)
  2. 调用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}`);
    });
}

summary

summary


Author: Maple
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Maple !
  TOC