本文参照的 snowpack 是 v0.6.1 版,固然这个时候它的名字实际上是 @pika/web前端
snowpack 和 vite 同样都是利用浏览器原生对 ES modules 的支持提供了一种新的前端打包构建方式。初看也许挺唬人的,实际上实现思路很简单。0.61版的代码一共只有 400 来行。所作的事情用一句话也能说清楚:从项目根目录的 package.json 里收集依赖,抽取 node_modules 中代码利用 rollup 独立打包到 web_modules 文件夹下。固然了,引用方式也是须要对应更改的。node
关键函数有三个:cli()
、install()
和 resolveWebDependency()
。首先是 cli() 函数,也是 @pika/web 的主函数,去除掉配置项的解析,它所作的事情实际上就是去项目根目录的 package.json 里收集依赖,对应的是const arrayOfDeps = webDependencies || Object.keys(pkgManifest.dependencies || {});
这一行,经过Object.keys 把依赖的库名做为数组元素生成的依赖数组再交给install 去处理,最后利用 chalk
这个库将构建过程和打包结果输出的好看一点。webpack
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
这条命令,就会自动完成构建打包工做。那么问题来了,何以如此?git
咱们知道,但模块配置了 bin
定义的时候,就会在安装时,自动软链到 node_modules/.bin 下。而 node_modules/.bin 也会被 npm 添加到 PATH 环境变量中。执行 npx @pika/web
,会到node_modules/.bin
路径和环境变量$PATH
里查看命令是否存在。因而咱们能够在 node-modules/.bin 下看到这样几个文件:pika-web
、pika
、pika-web.cmd
... 就是没有 @pika/web
。不知道是否是 npm 自动作了转换,我也没分清楚这几个文件各自的含义。可是凭着直觉,咱们盲猜是 pika-web
。打开这个文件,它是这样的:github
#!/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
web
再去找这个 index.bin.js
文件:shell
#!/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 了。npm
从另一个角度咱们也能够找到它:npx会根据packagejson里定义的bin 找入口。json
对应到 @pika/web,入口是这样的:数组
"bin": {
"pika-web": "dist-node/index.bin.js"
},
复制代码
以上细节大可能是看上去合理的推测,确实没拎清楚 从 npx @pika/web
到 cli()
之间发生了什么。
偏题了一会,咱们再来看看 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 呢)。相应的字段有 browser
、main
、和module
。module 对应的固然就是 ESM 规范的入口文件。这才是咱们的 snowpack 和 vite 所须要的。若是库自己不支持输出 ES modules 那只能等社区或本身动手优化了。
最后,咱们知道 webpack 是web前端应用复杂化发展下的产物,将来是HTTP2 和 5G 普及的时代,webpack的初心会显得多余,可是就目前来看,它的成熟度带来的优点是要盖过给开发者带来的负担的。无论是snowpack 仍是 vite而言,相关库对 ES modules 的支持度暂且不说,若是不能j继续丰富插件生态,就还有很长的路要走,何况,说不许webpack下次升级就会支持类 snowpack 的打包方式呢。