前言
在学习源码之前,要知道我们是为什么要去学习Vite源码,Vite区别于webpack的特点是什么。
webpack原理是把一切需要处理的东西都处理好之后打包在一个文件里运行,这也是无法冷启动导致首次运行速度慢的原因。而vite最大的特点就是no-bundle(不打包),这样就会实现冷启动,提升本地开发速度。
什么是no-bundle
在一个项目里,需要处理的有两类代码:
- 第一类是你自己写的业务代码
- 第二类是node_modules里需要用到的第三方依赖包代码 vite所说的no-bundle仅仅是指第一类业务代码,对于第二类第三方依赖的代码还是采用的bundle(打包) 的方式来引入,为了提升打包的速度使用Esbuild进行秒打包编译。
针对第一类代码,利用浏览器对script
标签的type=module
的(也就是所说的ES Module规范)实现,每一个import语句都是一个网络请求来实现no-boundle。因为ESM
规范是浏览器的通用实现,所以不需要像以前的CommonJS/AMD之类的去制定单独的规范。
关于前端规范角度这篇文章从前端模块化发展史去思考Vite讲得比较详细,可以解释清楚ESM的优点。
生产环境
虽然Vite是no-bundle,开发环境下开发速度很快,但是生产环境下总是要打包的。所以Vite又利用另一个神级打包神器——rollup,很多生产环境下rollup插件是可以和vite兼容的。
总结
所以vite基本是站在巨人肩膀上了,开发环境需要Esbuild,生产环境需要rollup。而Vite自己实现的部分可以看作是一个Dev Server,主要用来处理开发环境下的开发体验。
有了这个认知学习源码会非常清晰地认知到我们到底要学习什么。
准备
在看源码之前,我们先要找到vite的关键函数是什么,也就是vite的运行流程是怎样的。进而明白什么叫作no-bundle。
首先,先使用npm create vite@latest
创建一个项目。然后下面根据vite默认的配置来分析vite的结构,搞清楚vite运行的过程。
分析
vite初始代码
看一下新建项目package.json:
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "node server",
"preview": "vite preview"
},
这里着重看scripts
里的命令,可以看出npm run dev
参数的命令是vite
,我们去node_modules里的.bin里去找到vite
命令。
.bin里的vite
命令来自node_modules里的vite
文件夹里的bin文件夹里的vite.js
:
function start() {
require('../dist/node/cli')
}
总之这一大堆代码,运行到最终就是为了运行start函数,这里其他代码不多说,感兴趣的可以仔细看。
所以此时需要去../dist/node/cli
路径找到cli.js文件:
前面一大堆都不看,找到cli:
cli
.option('-c, --config <file>', `[string] use specified config file`)
.option('--base <path>', `[string] public base path (default: /)`)
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`);
这就是package.json的script里,我们使用npm run dev
会运行的vite
命令,如果我们需要参数去实现功能,参数就是在这里处理的。
然后仔细看这一段代码会发现一些注释在提示:
比如//dev
的注释提示我们,接下来的处理是关于执行vite dev
的处理。dev
的参数有--host/port/https.....
等等。
但是主要的处理还是在action
函数里:从一个压缩打包的./chunks/dep-59dc6e00.js
JS文件里导出一个createServer
函数,然后使用createServer
新建一个Node Server,这个就是用来承载更新的服务器。(Ps:chunk一般表示JS文件的打包)
// dev
cli
.command('[root]', 'start dev server') // default command
.alias('serve') // the command is called 'serve' in Vite's API
.alias('dev') // alias to align with the script name
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`)
.option('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
.action(async (root, options) => {
// output structure is preserved even after bundling so require()
// is ok here
const { createServer } = await Promise.resolve().then(function () { return require('./chunks/dep-59dc6e00.js'); }).then(function (n) { return n.index$1; });
try {
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options)
});
if (!server.httpServer) {
throw new Error('HTTP server not available');
}
await server.listen();
const info = server.config.logger.info;
info(index.colors.cyan(`\n vite v${require('vite/package.json').version}`) +
index.colors.green(` dev server running at:\n`), {
clear: !server.config.logger.hasWarned
});
server.printUrls();
// @ts-ignore
if (global.__vite_start_time) {
// @ts-ignore
const startupDuration = perf_hooks.performance.now() - global.__vite_start_time;
info(`\n ${index.colors.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`);
}
}
catch (e) {
index.createLogger(options.logLevel).error(index.colors.red(`error when starting dev server:\n${e.stack}`), { error: e });
process.exit(1);
}
});
由此可以看出createServer
肯定是vite源码实现本地开发着重要实现的功能。
按着这个思路继续看//build
下的action
,可以看到build
函数也是vite工具实现打包功能需要着重实现的函数。
// build
cli
.command('build [root]', 'build for production')
.option('--target <target>', `[string] transpile target (default: 'modules')`)
.option('--outDir <dir>', `[string] output directory (default: dist)`)
.option('--assetsDir <dir>', `[string] directory under outDir to place assets in (default: assets)`)
.option('--assetsInlineLimit <number>', `[number] static asset base64 inline threshold in bytes (default: 4096)`)
.option('--ssr [entry]', `[string] build specified entry for server-side rendering`)
.option('--sourcemap', `[boolean] output source maps for build (default: false)`)
.option('--minify [minifier]', `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
`or specify minifier to use (default: esbuild)`)
.option('--manifest [name]', `[boolean | string] emit build manifest json`)
.option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
.option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`)
.option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
.action(async (root, options) => {
const { build } = await Promise.resolve().then(function () { return require('./chunks/dep-59dc6e00.js'); }).then(function (n) { return n.build$1; });
const buildOptions = cleanOptions(options);
try {
await build({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
build: buildOptions
});
}
catch (e) {
index.createLogger(options.logLevel).error(index.colors.red(`error during build:\n${e.stack}`), { error: e });
process.exit(1);
}
});
本次源码解析主要是看dev情况下的源码,所以在接下来源码里我们首先从createServer
入手。
网络请求
我们运行成功后打开页面去查看网络请求,会发现每一个import
语句都对应一个网络请求。
比如main.ts文件,返回给浏览器的响应是经过Vite的Dev Server处理后的代码。createApp
来自Esbuild已经打包好的vue源代码,App.vue
可以看到后续继续网络请求了。
这就是Vite的核心:通过网络请求实现按需加载,而不是webpack去打包所有文件。
结束
此时可以去github clone到vite的源码,准备开始真正的源码解析。因为vite采用的是当下比较流行的monorepo的形式,所以vite的源码是放在一个仓库vitejs
下的,vitejs
下有很多别的项目,可以只clone vite,也可以把整个vitejs
clone下来。