看了会 snowpack 源码

1,647 阅读4分钟

本文参照的 snowpack 是 v0.6.1 版,当然这个时候它的名字其实是 @pika/web

snowpack 和 vite 一样都是利用浏览器原生对 ES modules 的支持提供了一种新的前端打包构建方式。初看也许挺唬人的,实际上实现思路很简单。0.61版的代码一共只有 400 来行。所做的事情用一句话也能说清楚:从项目根目录的 package.json 里收集依赖,抽取 node_modules 中代码利用 rollup 独立打包到 web_modules 文件夹下。当然了,引用方式也是需要对应更改的。

关键函数有三个:cli()install()resolveWebDependency()。首先是 cli() 函数,也是 @pika/web 的主函数,去除掉配置项的解析,它所做的事情实际上就是去项目根目录的 package.json 里收集依赖,对应的是const arrayOfDeps = webDependencies || Object.keys(pkgManifest.dependencies || {}); 这一行,通过Object.keys 把依赖的库名作为数组元素生成的依赖数组再交给install 去处理,最后利用 chalk 这个库将构建过程和打包结果输出的好看一点。

export async function cli(args: string[]) {
  const {
    help,
    sourceMap,
    babel = false,
    optimize = false,
    strict = false,
    clean = false,
    dest = 'web_modules',
    remoteUrl = 'https://cdn.pika.dev',
    remotePackage: remotePackages = [],
  } = yargs(args);
  const destLoc = path.resolve(cwd, dest);

  if (help) {
    printHelp();
    process.exit(0);
  }

  const pkgManifest = require(path.join(cwd, 'package.json'));
  const {namedExports, webDependencies} = pkgManifest['@pika/web'] || {
    namedExports: undefined,
    webDependencies: undefined,
  };
  const doesWhitelistExist = !!webDependencies;
  const arrayOfDeps = webDependencies || Object.keys(pkgManifest.dependencies || {});//收集依赖
  const hasBrowserlistConfig =
    !!pkgManifest.browserslist ||
    !!process.env.BROWSERSLIST ||
    fs.existsSync(path.join(cwd, '.browserslistrc')) ||
    fs.existsSync(path.join(cwd, 'browserslist'));

  spinner.start();
  const startTime = Date.now();
  const result = await install(arrayOfDeps, {//注入
    isCleanInstall: clean,
    destLoc,
    namedExports,
    isExplicit: doesWhitelistExist,
    isStrict: strict,
    isBabel: babel || optimize,
    isOptimized: optimize,
    sourceMap,
    remoteUrl,
    hasBrowserlistConfig,
    remotePackages: remotePackages.map(p => p.split(',')),
  });
  if (result) {
    spinner.succeed(
      chalk.bold(`@pika/web`) +
        ` installed: ` +
        formatDetectionResults(!doesWhitelistExist) +
        '. ' +
        chalk.dim(`[${((Date.now() - startTime) / 1000).toFixed(2)}s]`),
    );
  }
  if (spinnerHasError) {
    // Set the exit code so that programmatic usage of the CLI knows that there were errors.
    spinner.warn(chalk(`Finished with warnings.`));
    process.exitCode = 1;
  }
}

依照官网文档,我们只需要在根目录下执行 npx @pika/web 这条命令,就会自动完成构建打包工作。那么问题来了,何以如此?

我们知道,但模块配置了 bin 定义的时候,就会在安装时,自动软链到 node_modules/.bin 下。而 node_modules/.bin 也会被 npm 添加到 PATH 环境变量中。执行 npx @pika/web ,会到node_modules/.bin路径和环境变量$PATH里查看命令是否存在。于是我们可以在 node-modules/.bin 下看到这样几个文件:pika-webpikapika-web.cmd ... 就是没有 @pika/web。不知道是不是 npm 自动做了转换,我也没分清楚这几个文件各自的含义。但是凭着直觉,我们盲猜是 pika-web 。打开这个文件,它是这样的:

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../@pika/web/dist-node/index.bin.js" "$@"
  ret=$?
else 
  node  "$basedir/../@pika/web/dist-node/index.bin.js" "$@"
  ret=$?
fi
exit $ret

好吧,这是个shell 脚本,执行了 node" $basedir/../@pika/web/dist-node/index.bin.js

再去找这个 index.bin.js 文件:

#!/usr/bin/env node
'use strict';
let hasBundled = true    
try {
  require.resolve('./index.bundled.js');
} catch(err) {
  // We don't have/need this on legacy builds and dev builds
  // If an error happens here, throw it, that means no Node.js distribution exists at all.
  hasBundled = false;
}
const cli = !hasBundled ? require('../') : require('./index.bundled.js');
if (cli.autoRun) {
  return;
}
const run = cli.run || cli.cli || cli.default;
run(process.argv).catch(function (error) {
  console.error(error.stack || error.message || error);
  process.exitCode = 1;
});

终于我们看到它执行 cli 了。

从另外一个角度我们也可以找到它:npx会根据packagejson里定义的bin 找入口。

对应到 @pika/web,入口是这样的:

 "bin": {
    "pika-web": "dist-node/index.bin.js"
  },

以上细节大多是看上去合理的推测,确实没拎清楚 从 npx @pika/webcli() 之间发生了什么。

偏题了一会,我们再来看看 install 函数。算了太长了,不看了。确实也没什么好说的,install 里比较重要的一步是 对 js类型文件执行 第三个重点函数 resolveWebDependency()

const cwd = process.cwd();
console.log(cwd);

function resolveWebDependency(dep: string): string {
  const nodeModulesLoc = path.join(cwd, 'node_modules', dep);//获取 node_moudules 地址
  let dependencyStats: fs.Stats;
  try {
    dependencyStats = fs.statSync(nodeModulesLoc);//返回关于文件的信息
  } catch (err) {
    throw new Error(`"${dep}" not found in your node_modules directory. Did you run npm install?`);
  }
  if (dependencyStats.isFile()) {
    return nodeModulesLoc;//是文件则直接返回文件地址
  }
  if (dependencyStats.isDirectory()) {//是个文件目录
    const dependencyManifestLoc = path.join(nodeModulesLoc, 'package.json');//进入package.json 
    const manifest = require(dependencyManifestLoc);
    if (!manifest.module) {
      throw new ErrorWithHint(
        `dependency "${dep}" has no ES "module" entrypoint.`,
        chalk.italic(
          `Tip: Find modern, web-ready packages at ${chalk.underline(
            'https://pikapkg.com/packages',
          )}`,
        ),
      );
    }
   const resPath = path.join(nodeModulesLoc, manifest.module)
      console.log(resPath)
      return resPath;
  }

  throw new Error(
    `Error loading "${dep}" at "${nodeModulesLoc}". (MODE=${dependencyStats.mode}) `,
  );
}

我们以rollup 为例 resolveWebDependency("rollup"); 看看执行效果.

代码也很好理解,其中有个小细节倒是值得关注:manifest.module。村上春树式提问:当我们 import 一个 npm 包的时候,我们 在import 什么?

package.json 中main 字段指定的路径啦,大部分人如是说到。可实际上,npm 包分为:只允许在客户端使用的和浏览器/服务端都可以使用的(这种描述也不准确。时代在进步。node 以前还不支持 ES modules 呢)。相应的字段有 browsermain、和module。module 对应的当然就是 ESM 规范的入口文件。这才是我们的 snowpack 和 vite 所需要的。如果库本身不支持输出 ES modules 那只能等社区或自己动手优化了。

最后,我们知道 webpack 是web前端应用复杂化发展下的产物,未来是HTTP2 和 5G 普及的时代,webpack的初心会显得多余,但是就目前来看,它的成熟度带来的优势是要盖过给开发者带来的负担的。不管是snowpack 还是 vite而言,相关库对 ES modules 的支持度暂且不说,如果不能j继续丰富插件生态,就还有很长的路要走,况且,说不准webpack下次升级就会支持类 snowpack 的打包方式呢。