这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
背景
按照vue3文档使用Vite模板的方式初始化了一个项目(如下图1),但是在配置动态配置项时(如下图2)发现不生效。于是使用vscode调试vite的执行过程,发现是vite版本过低,不支持最新文档中的动态配置的特性。
图1
图2
开始
首先,使用yarn create vite-app xxx
这条命令生成的项目是不带vite默认配置文件的,此时启动vite会使用默认配置。如果想显式配置vite,需要手动在项目根目录创建vite.config.js
,如下:
vite.config.js
export default ({ command, mode }) => {
if (command === 'build') {
return {
base: '/github_cleaner/'
}
}
}
复制代码
看一下此时的package.json
文件,如下
{
"name": "github_cleaner",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"vue": "^3.0.0-beta.14"
},
"devDependencies": {
"vite": "^0.16.6",
"@vue/compiler-sfc": "^3.0.0-beta.14"
}
}
复制代码
然后使用上面的配置执行yarn build
,也就是vite build
,发现编译出来的index.html
文件(如下)并没有按照预期修改部署目录为/github_cleaner/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/_assets/favicon.59ab3a34.ico" />
<title>Vite App</title>
<link rel="stylesheet" href="/_assets/style.9e6be708.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/_assets/index.2f8e9066.js"></script>
</body>
</html>
复制代码
调试
那就调试进vite
的执行过程中看一下为什么我们的动态配置没有生效。先试着在vite.config.js
中我们导出的函数中增加一处断点,然后启动调试,发现断点处并没有停下。
于是找进node_modules
中vite
的入口文件打断点,看一下node_modules
中的vite
包的package.json
,如下
{
"name": "vite",
"version": "0.16.12",
"license": "MIT",
"author": "Evan You",
"bin": {
"vite": "bin/vite.js"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"bin",
"dist/**/*.js",
"dist/**/*.d.ts",
"hmr.d.ts"
],
...
}
复制代码
因为我们在启动vite
执行使用的是vite可执行文件
,所以对应bin
属性中的bin/vite.js
。找到该文件,源码如下
#!/usr/bin/env node
require('../dist/cli')
复制代码
可见对于vite可执行文件
的执行,其实就是在使用当前环境中的node
执行node_modules/vite/dist/cli.js
文件。
其中关键初始化如下
const argv = require('minimist')(process.argv.slice(2));
...
const config_1 = require("./config");
const command = argv._[0];
const defaultMode = command === 'build' ? 'production' : 'development';
复制代码
找到其中第一个立即执行函数源码如下,我们将在这里设置第一个断点
(async () => {
const { help, h, mode, m, version, v } = argv;
if (help || h) {
logHelp();
return;
}
else if (version || v) {
// noop, already logged
return;
}
const envMode = mode || m || defaultMode;
const options = await resolveOptions(envMode);
if (!options.command || options.command === 'serve') {
runServe(options);
}
else if (options.command === 'build') {
runBuild(options);
}
else if (options.command === 'optimize') {
runOptimize(options);
}
else {
console.error(chalk_1.default.red(`unknown command: ${options.command}`));
process.exit(1);
}
})();
复制代码
其中resolveOptions
函数很明显是解析vite
执行配置的,源码也不多,看一下其中的执行逻辑
async function resolveOptions(mode) {
// specify env mode
argv.mode = mode;
// shorthand for serviceWorker option
if (argv['sw']) {
argv.serviceWorker = argv['sw'];
}
// map jsx args
if (argv['jsx-factory']) {
;
(argv.jsx || (argv.jsx = {})).factory = argv['jsx-factory'];
}
if (argv['jsx-fragment']) {
;
(argv.jsx || (argv.jsx = {})).fragment = argv['jsx-fragment'];
}
// cast xxx=true | false into actual booleans
Object.keys(argv).forEach((key) => {
if (argv[key] === 'false') {
argv[key] = false;
}
if (argv[key] === 'true') {
argv[key] = true;
}
});
// command
if (argv._[0]) {
argv.command = argv._[0];
}
// normalize root
// assumes all commands are in the form of `vite [command] [root]`
if (!argv.root && argv._[1]) {
argv.root = argv._[1];
}
if (argv.root) {
argv.root = path_1.default.isAbsolute(argv.root) ? argv.root : path_1.default.resolve(argv.root);
}
const userConfig = await config_1.resolveConfig(mode, argv.config || argv.c);
if (userConfig) {
return {
...userConfig,
...argv // cli options take higher priority
};
}
return argv;
}
复制代码
const userConfig = await config_1.resolveConfig(mode, argv.config || argv.c);
这一行很关键
找到config_1
是文件一开始通过const config_1 = require("./config");
引入的
这里我们理一下进入resolveConfig
之前的逻辑:
- 执行
vite build
build
作为command
参数被vite
可执行文件解析- 内部设置
mode
属性,因为在执行vite
时没有显式声明mode
,因此使用build
命令默认对应的production
- 带着
mode=production
参数执行config.js
中的resolveConfig
函数
由于上面const argv = require('minimist')(process.argv.slice(2));
这一句可知,argv
其实是解析执行vite
时第二个及以后的参数。
由于我们执行vite build
时只带了build
一个参数,所以可知argv
其实是没有任何值的,所以在读取argv.config
和argv.c
时都是undefined
。
这里的设计是为了执行vite
时,可以通过携带参数--config=xxx
或--c=xxx
来传递vite
配置
深入配置解析
首先node_modules/vite/dist/config.js
中的关于resolveConfig
函数的实现如下:
async function resolveConfig(mode, configPath) {
const start = Date.now();
const cwd = process.cwd();
let config;
let resolvedPath;
let isTS = false;
if (configPath) {
resolvedPath = path_1.default.resolve(cwd, configPath);
}
else {
const jsConfigPath = path_1.default.resolve(cwd, 'vite.config.js');
if (fs_extra_1.default.existsSync(jsConfigPath)) {
resolvedPath = jsConfigPath;
}
else {
const tsConfigPath = path_1.default.resolve(cwd, 'vite.config.ts');
if (fs_extra_1.default.existsSync(tsConfigPath)) {
isTS = true;
resolvedPath = tsConfigPath;
}
}
}
if (!resolvedPath) {
return;
}
try {
if (!isTS) {
try {
config = require(resolvedPath);
}
catch (e) {
if (!/Cannot use import statement|Unexpected token 'export'/.test(e.message)) {
throw e;
}
}
}
if (!config) {
// 2. if we reach here, the file is ts or using es import syntax.
// transpile es import syntax to require syntax using rollup.
const rollup = require('rollup');
const esbuilPlugin = await buildPluginEsbuild_1.createEsbuildPlugin(false, {});
const bundle = await rollup.rollup({
external: (id) => (id[0] !== '.' && !path_1.default.isAbsolute(id)) ||
id.slice(-5, id.length) === '.json',
input: resolvedPath,
treeshake: false,
plugins: [esbuilPlugin]
});
const { output: [{ code }] } = await bundle.generate({
exports: 'named',
format: 'cjs'
});
config = await loadConfigFromBundledFile(resolvedPath, code);
}
// normalize config root to absolute
if (config.root && !path_1.default.isAbsolute(config.root)) {
config.root = path_1.default.resolve(path_1.default.dirname(resolvedPath), config.root);
}
// resolve plugins
if (config.plugins) {
for (const plugin of config.plugins) {
config = resolvePlugin(config, plugin);
}
}
// load environment variables
const env = loadEnv(mode, config.root || cwd);
debug(`env: %O`, env);
config.env = env;
debug(`config resolved in ${Date.now() - start}ms`);
config.__path = resolvedPath;
return config;
}
catch (e) {
console.error(chalk_1.default.red(`[vite] failed to load config from ${resolvedPath}:`));
console.error(e);
process.exit(1);
}
}
exports.resolveConfig = resolveConfig;
复制代码
我们从源码实现中可以看出,对于vite
配置的解析,会先从vite
命令行上读取是否有配置文件。如果有,就会使用该配置,否则,会从当前目录读取vite.config.js
文件作为配置信息。
如果是本地声明vite.config.js
的方式,vite
会首先通过commonjs
的规范引入该模块。但是由于我们在vite.config.js
中是以ESM
规范书写的,所以这里会捕获对于该模块引入的报错。
但是vite
会兜底处理非commonjs
规范的情况,内部使用rollup
工具将vite.config.js
配置文件编译为commonjs
可引入的格式。具体实现如下
async function loadConfigFromBundledFile(fileName, bundledCode) {
const extension = path_1.default.extname(fileName);
const defaultLoader = require.extensions[extension];
require.extensions[extension] = (module, filename) => {
if (filename === fileName) {
;
module._compile(bundledCode, filename);
}
else {
defaultLoader(module, filename);
}
};
delete require.cache[fileName];
const raw = require(fileName);
const config = raw.__esModule ? raw.default : raw;
require.extensions[extension] = defaultLoader;
return config;
}
复制代码
对于config
的解析到这里就结束了,我们回到cli.js
中的执行主流程。在拿到解析后的config
,会根据command
执行对应目录下的文件,我们执行的是build
命令,所以会走入以下逻辑
node_modules/vite/dist/cli.js
async function runBuild(options) {
try {
await require('./build').build(options);
process.exit(0);
}
catch (err) {
console.error(chalk_1.default.red(`[vite] Build errored out.`));
console.error(err);
process.exit(1);
}
}
复制代码
build实现
build的核心完整逻辑如下
/**
* Bundles the app for production.
* Returns a Promise containing the build result.
*/
async function build(options) {
const { root = process.cwd(), base = '/', outDir = path_1.default.resolve(root, 'dist'), assetsDir = '_assets', assetsInlineLimit = 4096, cssCodeSplit = true, alias = {}, resolvers = [], rollupInputOptions = {}, rollupOutputOptions = {}, emitIndex = true, emitAssets = true, write = true, minify = true, silent = false, sourcemap = false, shouldPreload = null, env = {}, mode } = options;
const isTest = process.env.NODE_ENV === 'test';
process.env.NODE_ENV = mode;
const start = Date.now();
let spinner;
const msg = 'Building for production...';
if (!silent) {
if (process.env.DEBUG || isTest) {
console.log(msg);
}
else {
spinner = require('ora')(msg + '\n').start();
}
}
const indexPath = path_1.default.resolve(root, 'index.html');
const publicBasePath = base.replace(/([^/])$/, '$1/'); // ensure ending slash
const resolvedAssetsPath = path_1.default.join(outDir, assetsDir);
const resolver = resolver_1.createResolver(root, resolvers, alias);
const { htmlPlugin, renderIndex } = await buildPluginHtml_1.createBuildHtmlPlugin(root, indexPath, publicBasePath, assetsDir, assetsInlineLimit, resolver, shouldPreload);
const basePlugins = await createBaseRollupPlugins(root, resolver, options);
env.NODE_ENV = mode;
const envReplacements = Object.keys(env).reduce((replacements, key) => {
replacements[`process.env.${key}`] = JSON.stringify(env[key]);
return replacements;
}, {});
// lazy require rollup so that we don't load it when only using the dev server
// importing it just for the types
const rollup = require('rollup').rollup;
const bundle = await rollup({
input: path_1.default.resolve(root, 'index.html'),
preserveEntrySignatures: false,
treeshake: { moduleSideEffects: 'no-external' },
onwarn: exports.onRollupWarning,
...rollupInputOptions,
plugins: [
...basePlugins,
...
]
});
const { output } = await bundle.generate({
format: 'es',
sourcemap,
entryFileNames: `[name].[hash].js`,
chunkFileNames: `[name].[hash].js`,
...rollupOutputOptions
});
spinner && spinner.stop();
const cssFileName = output.find((a) => a.type === 'asset' && a.fileName.endsWith('.css')).fileName;
const indexHtml = emitIndex ? renderIndex(output, cssFileName) : '';
if (write) {
const cwd = process.cwd();
const writeFile = async (filepath, content, type) => {
await fs_extra_1.default.ensureDir(path_1.default.dirname(filepath));
await fs_extra_1.default.writeFile(filepath, content);
if (!silent) {
console.log(`${chalk_1.default.gray(`[write]`)} ${writeColors[type](path_1.default.relative(cwd, filepath))} ${(content.length / 1024).toFixed(2)}kb, brotli: ${(require('brotli-size').sync(content) / 1024).toFixed(2)}kb`);
}
};
await fs_extra_1.default.remove(outDir);
await fs_extra_1.default.ensureDir(outDir);
// write js chunks and assets
for (const chunk of output) {
if (chunk.type === 'chunk') {
// write chunk
const filepath = path_1.default.join(resolvedAssetsPath, chunk.fileName);
let code = chunk.code;
if (chunk.map) {
code += `\n//# sourceMappingURL=${path_1.default.basename(filepath)}.map`;
}
await writeFile(filepath, code, 0 /* JS */);
if (chunk.map) {
await writeFile(filepath + '.map', chunk.map.toString(), 4 /* SOURCE_MAP */);
}
}
else if (emitAssets) {
// write asset
const filepath = path_1.default.join(resolvedAssetsPath, chunk.fileName);
await writeFile(filepath, chunk.source, chunk.fileName.endsWith('.css') ? 1 /* CSS */ : 2 /* ASSET */);
}
}
// write html
if (indexHtml && emitIndex) {
await writeFile(path_1.default.join(outDir, 'index.html'), indexHtml, 3 /* HTML */);
}
// copy over /public if it exists
if (emitAssets) {
const publicDir = path_1.default.resolve(root, 'public');
if (fs_extra_1.default.existsSync(publicDir)) {
for (const file of await fs_extra_1.default.readdir(publicDir)) {
await fs_extra_1.default.copy(path_1.default.join(publicDir, file), path_1.default.resolve(outDir, file));
}
}
}
}
if (!silent) {
console.log(`Build completed in ${((Date.now() - start) / 1000).toFixed(2)}s.\n`);
}
// stop the esbuild service after each build
esbuildService_1.stopService();
return {
assets: output,
html: indexHtml
};
}
exports.build = build;
复制代码
可以看出,这里内容仍然是使用rollup
进行编译,编译的配置就是刚才解析过的config
对象,输出的内容则是dist
目录中的静态文件。
整个过程都没有发现有对vite.config.js
文件中的配置做函数执行的逻辑,所以我们可以断定至少在当下所看到的逻辑中,是不支持vite.config.js
通过导出函数动态配置的。
寻求帮助
那么这种时候,我们应该去哪寻求帮助呢。首先想到是通过谷歌搜索一下网上有没有人遇到类似情况。
发现和这个有关的答案只有vite
官方文档和vite
的issues
,还有stackoverflow
中有人问关于如何设置项目根目录
的问题。
先说stackoverflow
中的这个问答,它遇到的情况和我们不太一样,不是动态配置,而是静态配置为什么不生效。
所以再来看vite
的issues
中有没有类似的提问,发现搜索也无果。
于是迟疑了一会。。。
转机
于是我想到,是不是我们用的vite
版本压根就的确不支持动态配置
这一特性呢。所以去到vite
的github
首页看了一眼vite
的最新版本
v2.4.4 !!!
再看我们项目中的vite
版本
"devDependencies": {
"vite": "^0.16.6"
}
复制代码
0.16.6 ~~~
啊,原来如此。
使用npm info vite
查看一下vite
包的版本信息
vite@2.4.4 | MIT | deps: 4 | versions: 215
Native-ESM powered web dev build tool
https://github.com/vitejs/vite/tree/main/#readme
bin: vite
dist
.tarball: https://registry.npmjs.org/vite/-/vite-2.4.4.tgz
.shasum: 8c402a07ad45f168f6eb5428bead38f3e4363e47
.integrity: sha512-m1wK6pFJKmaYA6AeZIUXyiAgUAAJzVXhIMYCdZUpCaFMGps0v0IlNJtbmPvkUhVEyautalajmnW5X6NboUPsnw==
.unpackedSize: 17.9 MB
dependencies:
esbuild: ^0.12.8 postcss: ^8.3.6 resolve: ^1.20.0 rollup: ^2.38.5
maintainers:
- yyx990803 <yyx990803@gmail.com>
- antfu <anthonyfu117@hotmail.com>
- patak <matias.capeletto@gmail.com>
dist-tags:
beta: 2.5.0-beta.1 latest: 2.4.4
复制代码
最新版本的确已经到v2.4.4
了,至此已经基本揭开迷雾了。
更新
使用yarn add vite@2.4.4
安装最新版本vite
包,再重新执行yarn build
你以为到这里就结束了吗?一开始我也这么以为的emmmm
再报错
更新vite
版本后的build
,报出如下错误
yarn run v1.22.10
$ vite build
vite v2.4.4 building for production...
✓ 2 modules transformed.
[rollup-plugin-dynamic-import-variables] Unexpected token (1:0)
file: /Users/wangxin/code/temp/github_cleaner/App.vue:1:0
error during build:
SyntaxError: Unexpected token (1:0)
at Parser.pp$4.raise (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:16965:13)
at Parser.pp.unexpected (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:14473:8)
at Parser.pp$3.parseExprAtom (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:16342:10)
at Parser.pp$3.parseExprSubscripts (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:16137:19)
at Parser.pp$3.parseMaybeUnary (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:16103:17)
at Parser.pp$3.parseExprOps (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:16036:19)
at Parser.pp$3.parseMaybeConditional (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:16019:19)
at Parser.pp$3.parseMaybeAssign (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:15987:19)
at Parser.pp$3.parseExpression (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:15951:19)
at Parser.pp$1.parseStatement (/Users/wangxin/code/temp/github_cleaner/node_modules/rollup/dist/shared/rollup.js:14663:45)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
复制代码
看不出什么问题,执行yarn dev
看一下开发模式下的情况,一开始没什么问题,但当浏览器访问本地页面后,报出如下错误
不得不说,有React
内味了,这种在浏览器报错的方式也许是为了迎合React
用户?
看一下控制台报错信息
yarn run v1.22.10
$ vite
vite v2.4.4 dev server running at:
> Local: http://localhost:3000/
> Network: use `--host` to expose
ready in 310ms.
下午11:39:53 [vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. Install @vitejs/plugin-vue to handle .vue files.
Plugin: vite:import-analysis
File: /Users/wangxin/code/temp/github_cleaner/App.vue
3 | <img alt="Vue logo" src="https://juejin.cn/post/logo.png" />
4 | <HelloWorld msg="Hello Vue 3.0 + Vite" />
5 | </div>
| ^
6 | </template>
7 |
at formatError (/Users/wangxin/code/temp/github_cleaner/node_modules/vite/dist/node/chunks/dep-c1a9de64.js:50738:46)
at TransformContext.error (/Users/wangxin/code/temp/github_cleaner/node_modules/vite/dist/node/chunks/dep-c1a9de64.js:50734:19)
at TransformContext.transform (/Users/wangxin/code/temp/github_cleaner/node_modules/vite/dist/node/chunks/dep-c1a9de64.js:74194:22)
at async Object.transform (/Users/wangxin/code/temp/github_cleaner/node_modules/vite/dist/node/chunks/dep-c1a9de64.js:50939:30)
at async transformRequest (/Users/wangxin/code/temp/github_cleaner/node_modules/vite/dist/node/chunks/dep-c1a9de64.js:66763:29)
at async viteTransformMiddleware (/Users/wangxin/code/temp/github_cleaner/node_modules/vite/dist/node/chunks/dep-c1a9de64.js:66904:32)
复制代码
和浏览器提示的内容当然是一致的,具体错误意思就是vite
在处理模块解析时不认识vue
文件的语法。
控制台也给出了相应的解决方案,就是要安装@vitejs/plugin-vue
这个vite
的插件来解析以.vue
为后缀的文件。
那就手动安装这个插件呗
yarn add @vitejs/plugin-vue
复制代码
然后再重新启动项目yarn dev
,依然报这个错。后来得知,安装这个插件,还需要在vite.config.js
中引入这个插件并初始化,才能让vite
知道以何种规则解析以.vue
为后缀的文件语法。
这里仅仅安装了插件包而没做其他事情属实有点天真了,不过也因此发现了vite
的插件生态。因为vite
不单单是为Vue
设计的,同时也支持React
等其他框架使用,所以这里没有默认支持Vue
语法,而是通过插件的方式来支持每种框架的特殊语法。这种思路从较新版本的@vue/cli
就开始出现了,如果关注Vue
的生态就会发现。
推倒重来
这里我没有选择继续耗下去研究vite
插件生态的配置方式,而是转而看了vite
的官方文档,其内部有着和vue3
不同的项目初始方式yarn create vite xxx --template vue
使用上述命令初始化的项目,相比vue3
文档中提供的示例,合理更多。其package.json
中对于相关包的依赖版本较新
,而且默认提供了支持本地以production
模式预览的运行命令(这个我在使用vue3
方式初始化的项目中执行vite preview
也是提示不存在该命令的,也是因为那个模板初始化的项目中vite
版本过低导致)
package.json
{
"name": "github_cleaner2",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.3.0",
"@vue/compiler-sfc": "^3.0.5",
"vite": "^2.4.4"
}
}
复制代码
另外,该项目默认显式提供了vite.config.js
的配置方式及使用插件支持vue
语法的配置方式,如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})
复制代码
尾声
至此,我们终于可以顺利使用vite
+vue3
按条件配置本地运行和编译构建,输出如下:
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default ({ command, mode }) => {
if (command === 'build') {
return defineConfig({
plugins: [vue()],
base: '/github_cleaner/'
})
}
return defineConfig({
plugins: [vue()]
})
}
复制代码
dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/github_cleaner/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script type="module" crossorigin src="/github_cleaner/assets/index.2f5dd7dc.js"></script>
<link rel="modulepreload" href="/github_cleaner/assets/vendor.e68e32f2.js">
<link rel="stylesheet" href="/github_cleaner/assets/index.f498eb83.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
复制代码
复盘
这一过程中经历了很多探索新技术中的常见问题,所以有必要在此复盘总结一下本次的踩坑经历。
关于Vue
生态
Vue
的作者 Evan You 近两年早就提到过,他近期的重心会主要放在Vite 上,甚至高于Vue3
知道这个信息很重要
,这也是为什么我们使用vite
文档中的示例初始化项目会很顺利,而vue3
中的方式很明显存在版本老旧的问题。
关于vite
在我们搜索condition config
这一条目时,出现的有效搜索结果并不多。可见vite
目前的确还处于非常新
的状态,使用者遇到的问题和抛出的问题还不够多。
所以这就注定了现在使用vite
的人,算是第一批吃螃蟹的人,要具有同vite
一同发展、实践和试错的准备。同时也意味着这不一定适合新手使用,因为你遇到问题不一定能在网上找到有效的解决方法。