背景
還是 之前 那個(gè)用 vue-element-admin 搭建的項(xiàng)目,最近剛遷移到了 Vite 。一個(gè)比較大的問題就是雖然項(xiàng)目是秒啟動(dòng),但首次打開頁(yè)面會(huì)有幾秒的 白屏 ,非常難受。于是就去嘗試了各種緩存方案,測(cè)試效果的時(shí)候就用到了 vite --force 這個(gè)命令來強(qiáng)制 Vite 重新構(gòu)建依賴項(xiàng),接著本文的問題就出現(xiàn)了:
GET http://localhost:5173/node_modules/.vite/deps/element-ui_lib_button.js?v=bc3e4ba5 504 (Outdated Optimize Dep)
用 vite --force 命令啟動(dòng)項(xiàng)目后,打開頁(yè)面有幾率會(huì)顯示白屏,在控制臺(tái)可以看到 504 (Outdated Optimize Dep) 錯(cuò)誤,刷新頁(yè)面是沒用的,不過重新啟動(dòng)項(xiàng)目基本上能解決,這就意味著經(jīng)常需要啟動(dòng)兩遍項(xiàng)目并且時(shí)不時(shí)會(huì)看到這個(gè)錯(cuò)誤,還是比較煩人的。
原因分析
根據(jù)關(guān)鍵字 Outdated Optimize Dep 加斷點(diǎn)定位到了報(bào)錯(cuò)代碼的 位置 :
try {
return await fsp.readFile(file, 'utf-8')
} catch (e) {
// Outdated non-entry points (CHUNK), loaded after a rerun
throwOutdatedRequest(id)
}
去 node_modules/.vite/deps 目錄下查看后發(fā)現(xiàn) element-ui_lib_button.js 這個(gè)文件確實(shí)不存在,但應(yīng)該報(bào) 404 錯(cuò)誤而不是過時(shí)的,這點(diǎn)就很讓人迷惑,先不管了。
根本原因就是: optimizer 實(shí)際上并沒有把第三方庫(kù)預(yù)構(gòu)建為對(duì)應(yīng)的緩存文件,但 resolvePlugin 這個(gè)插件在執(zhí)行 tryOptimizedResolve 的時(shí)候卻把需要預(yù)構(gòu)建的第三方依賴的 id 改成預(yù)構(gòu)建后的路徑,而不管是否預(yù)構(gòu)建成功,二者匹配不上所以就出錯(cuò)了。
關(guān)于這點(diǎn)順帶再提一下,預(yù)構(gòu)建和解析兩個(gè)步驟分別寫在兩個(gè)插件里非常割裂,隨著每個(gè)插件各自不斷迭代就難免會(huì)出現(xiàn)各種不一致的行為,從而導(dǎo)致一些莫名其妙的問題。
比如把項(xiàng)目文件添加到
optimizeDeps.include中會(huì)發(fā)現(xiàn).vite/deps中已經(jīng)預(yù)構(gòu)建了,但實(shí)際訪問的時(shí)候還是讀取的源文件,估計(jì)是故意這么設(shè)計(jì)的,但還是可以通過 preAliasPlugin 中的 alias 來繞過這個(gè)限制。
至于為什么沒預(yù)構(gòu)建上,打下 debug 日志看看:
cross-env DEBUG=vite:deps vite --force
反復(fù)啟動(dòng)項(xiàng)目然后對(duì)比啟動(dòng)日志發(fā)現(xiàn)了端倪,報(bào)錯(cuò)的時(shí)候包含下面的日志:
vite:deps ? using post-scan optimizer result, the scanner found every used dependency +74ms
而正常的時(shí)候包含下面的日志:
vite:deps ? new dependencies were found while crawling that weren't detected by the scanner +1ms
vite:deps ? re-running optimizer +0ms
vite:deps new dependencies found: element-ui/lib/button +1ms
根據(jù)日志內(nèi)容定位到 源碼位置 :
const crawlDeps = Object.keys(metadata.discovered)
// Await for the scan+optimize step running in the background
// It normally should be over by the time crawling of user code ended
await depsOptimizer.scanProcessing
if (!isBuild && optimizationResult && !config.optimizeDeps.noDiscovery) {
const result = await optimizationResult.result
optimizationResult = undefined
currentlyProcessing = false
const scanDeps = Object.keys(result.metadata.optimized)
if (scanDeps.length === 0 && crawlDeps.length === 0) {
debug?.(
colors.green(
`? no dependencies found by the scanner or crawling static imports`,
),
)
result.cancel()
firstRunCalled = true
return
}
const needsInteropMismatch = findInteropMismatches(
metadata.discovered,
result.metadata.optimized,
)
const scannerMissedDeps = crawlDeps.some((dep) => !scanDeps.includes(dep))
const outdatedResult =
needsInteropMismatch.length > 0 || scannerMissedDeps
if (outdatedResult) {
// Drop this scan result, and perform a new optimization to avoid a full reload
result.cancel()
// Add deps found by the scanner to the discovered deps while crawling
for (const dep of scanDeps) {
if (!crawlDeps.includes(dep)) {
addMissingDep(dep, result.metadata.optimized[dep].src!)
}
}
if (scannerMissedDeps) {
debug?.(
colors.yellow(
`? new dependencies were found while crawling that weren't detected by the scanner`,
),
)
}
debug?.(colors.green(`? re-running optimizer`))
debouncedProcessing(0)
} else {
debug?.(
colors.green(
`? using post-scan optimizer result, the scanner found every used dependency`,
),
)
startNextDiscoveredBatch()
runOptimizer(result)
}
給 else 分支打上斷點(diǎn)再?gòu)?fù)現(xiàn)問題,發(fā)現(xiàn) crawlDeps 的值是:
[
"element-ui/packages/theme-chalk/src/index.scss"
]
并且 scanDeps 的值也是:
[
"element-ui/packages/theme-chalk/src/index.scss"
]
而手動(dòng)計(jì)算的 Object.keys(metadata.discovered) 的值是:
[
"element-ui/packages/theme-chalk/src/index.scss",
"element-ui/lib/button"
]
所以原因找到了: crawlDeps 的值錯(cuò)誤導(dǎo)致本該進(jìn)入 if 分支進(jìn)行增量構(gòu)建的,結(jié)果卻走了 else 分支直接結(jié)束了。
粗看這段代碼好像沒什么問題,但注意到中間有兩段 await ,盲猜和這有關(guān)系,再補(bǔ)充點(diǎn)日志看下。
使用 patch-package 把 patches/vite+4.4.9.patch 這個(gè)補(bǔ)丁文件應(yīng)用上
npx patch-packagediff --git a/node_modules/vite/dist/node/chunks/dep-df561101.js b/node_modules/vite/dist/node/chunks/dep-df561101.js index 1bc8674..2603df8 100644 --- a/node_modules/vite/dist/node/chunks/dep-df561101.js +++ b/node_modules/vite/dist/node/chunks/dep-df561101.js @@ -45413,11 +45413,18 @@ async function createDepsOptimizer(config, server) { return; } const crawlDeps = Object.keys(metadata.discovered); + const _debug = () => { + const discovered = Object.keys(metadata.discovered); + console.log(`metadata.discovered ( size: ${discovered.length} ) : ${depsLogString(discovered)}`) + } + _debug() // Await for the scan+optimize step running in the background // It normally should be over by the time crawling of user code ended await depsOptimizer.scanProcessing; + _debug() if (!isBuild && optimizationResult && !config.optimizeDeps.noDiscovery) { const result = await optimizationResult.result; + _debug() optimizationResult = undefined; currentlyProcessing = false; const scanDeps = Object.keys(result.metadata.optimized);
日志也證實(shí)了和 await 確實(shí)有關(guān)系,實(shí)際項(xiàng)目中依賴比較多構(gòu)建比較慢還可以看到三次的值都是不同的。
vite:deps ? static imports crawl ended +2s
+ metadata.discovered ( size: 1 ) : element-ui/packages/theme-chalk/src/index.scss
+ metadata.discovered ( size: 1 ) : element-ui/packages/theme-chalk/src/index.scss
vite:deps Dependencies bundled in 1140.67ms +0ms
+ metadata.discovered ( size: 2 ) : element-ui/packages/theme-chalk/src/index.scss, element-ui/lib/button
vite:deps ? using post-scan optimizer result, the scanner found every used dependency +75ms
vite:deps ? dependencies optimized +1ms
問題解決
問題找到了,解決辦法也很簡(jiǎn)單:調(diào)整定義 crawlDeps 的位置,等兩段 await 都結(jié)束后再讀取最新的 metadata.discovered 。最便捷的方法就是直接改 npm 包代碼再用 patch-package 生成補(bǔ)丁,之后直接應(yīng)用補(bǔ)丁就行了。( 參考 patch-package 小節(jié) )
patches/vite+4.4.9.patch
diff --git a/node_modules/vite/dist/node/chunks/dep-df561101.js b/node_modules/vite/dist/node/chunks/dep-df561101.js
index 1bc8674..092f4e0 100644
--- a/node_modules/vite/dist/node/chunks/dep-df561101.js
+++ b/node_modules/vite/dist/node/chunks/dep-df561101.js
@@ -45412,7 +45412,6 @@ async function createDepsOptimizer(config, server) {
if (closed) {
return;
}
- const crawlDeps = Object.keys(metadata.discovered);
// Await for the scan+optimize step running in the background
// It normally should be over by the time crawling of user code ended
await depsOptimizer.scanProcessing;
@@ -45420,6 +45419,7 @@ async function createDepsOptimizer(config, server) {
const result = await optimizationResult.result;
optimizationResult = undefined;
currentlyProcessing = false;
+ const crawlDeps = Object.keys(metadata.discovered);
const scanDeps = Object.keys(result.metadata.optimized);
if (scanDeps.length === 0 && crawlDeps.length === 0) {
debug$8?.(colors$1.green(`? no dependencies found by the scanner or crawling static imports`));
@@ -45452,6 +45452,7 @@ async function createDepsOptimizer(config, server) {
}
}
else {
+ const crawlDeps = Object.keys(metadata.discovered);
currentlyProcessing = false;
if (crawlDeps.length === 0) {
debug$8?.(colors$1.green(`? no dependencies found while crawling the static imports`));
問題復(fù)現(xiàn)
問題解決了就想著去倉(cāng)庫(kù)提交一個(gè) PR 從根源上進(jìn)行修復(fù),但這就需要提供一個(gè) 最小復(fù)現(xiàn) 來證實(shí)這確實(shí)是個(gè)問題。前面也說了這個(gè)問題是偶現(xiàn)的,甚至可能和項(xiàng)目的復(fù)雜程度有關(guān)系,想要在一個(gè)新建的項(xiàng)目里穩(wěn)定復(fù)現(xiàn)著實(shí)有點(diǎn)困難。花了幾天時(shí)間各種試,終于找到一種特定情況可以穩(wěn)定復(fù)現(xiàn):
-
啟動(dòng)項(xiàng)目的同時(shí)立即訪問任意 url( 除了
/和/favicon.ico以及public下的靜態(tài)資源 ),比如/xxx。這一點(diǎn)非常重要,因?yàn)檫@會(huì)在 transformMiddleware 中觸發(fā)額外的 transformRequest ,會(huì)因此導(dǎo)致 checkIfCrawlEndAfterTimeout 中的定時(shí)器提前啟動(dòng)。const knownIgnoreList = new Set(['/', '/favicon.ico']) ... return async function viteTransformMiddleware(req, res, next) { if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) { return next() } ... const result = await transformRequest(url, server, { html: req.headers.accept?.includes('text/html'), })為了不考驗(yàn)手速,直接寫了一個(gè)插件來模擬請(qǐng)求:
function requestSimulation() { return { name: 'request-simulation', configureServer(server) { const { listen } = server; server.listen = async (...args) => { await listen.apply(server, args); // request as fast as server is ready without manually open browser const url = server.resolvedUrls.local[0] + 'not_root'; axios.get(url, { headers: { Accept: 'text/html' } }).catch((e) => { console.error(e); }); }; }, }; } -
通過插件在解析階段動(dòng)態(tài)增加依賴項(xiàng)( 比如 unplugin-auto-import 插件做的事 ),這類依賴在 自動(dòng)依賴搜尋 階段不會(huì)被識(shí)別到,只有請(qǐng)求源文件時(shí)才會(huì)通過 addMissingDep 追加到預(yù)構(gòu)建依賴中。
function autoImport() { return { name: 'auto-import', transform(code, id) { if (id.includes('/main')) { // trigger addMissingDep return `import ElButton from 'element-ui/lib/button'\n${code}`; } }, }; } -
執(zhí)行緩慢的 transformIndexHtml 用來增加請(qǐng)求完
/xxx到 preTransformRequest 被調(diào)用前的時(shí)間間隔,會(huì)導(dǎo)致 onCrawlEnd 提前執(zhí)行( 因?yàn)?checkIfCrawlEndAfterTimeout 中設(shè)置的定時(shí)器到時(shí)間了而且沒有被延長(zhǎng) )。function slowTransformIndexHtml() { return { name: 'slow-transform-index-html', transformIndexHtml: { order: 'pre', async handler(html) { // manually make it slower await new Promise((resolve) => { // wait time longer than callCrawlEndIfIdleAfterMs setTimeout(() => resolve(), 100); }); return html; }, }, }; }preTransformRequest 就是在請(qǐng)求 html 的時(shí)候就預(yù)先解析依賴文件而無(wú)需等到瀏覽器請(qǐng)求每個(gè)資源文件的時(shí)候請(qǐng)求一個(gè)解析一個(gè),可通過配置關(guān)閉。
function preTransformRequest(server: ViteDevServer, url: string, base: string) { if (!server.config.server.preTransformRequests) return url = unwrapId(stripBase(url, base)) // transform all url as non-ssr as html includes client-side assets only server.transformRequest(url).catch((e) => { if ( e?.code === ERR_OUTDATED_OPTIMIZED_DEP || e?.code === ERR_CLOSED_SERVER ) { // these are expected errors return } // Unexpected error, log the issue but avoid an unhandled exception server.config.logger.error(e.message) }) } -
預(yù)構(gòu)建一個(gè)比較大的 scss 文件 ( 比如 element-ui ),編譯這個(gè) scss 文件可能會(huì)花費(fèi)數(shù)秒的時(shí)間,就會(huì)導(dǎo)致整個(gè)預(yù)構(gòu)建階段變慢,也就是增加前面提到的兩段
await前后的間隔時(shí)間。function slowOptimize() { return { name: 'slow-optimize', config() { return { // pre-build scss file to make the optimize step slower // ref: https://github.com/vitejs/vite/issues/7719#issuecomment-1098683109 optimizeDeps: { extensions: ['.scss', '.sass'], include: ['element-ui/packages/theme-chalk/src/index.scss'], esbuildOptions: { plugins: [ sassPlugin({ type: 'style', logger: { warn() {} }, }), ], }, }, }; }, }; }
復(fù)現(xiàn)步驟

pnpm i
pnpm run dev
啟動(dòng)項(xiàng)目后打開控制臺(tái)就能看到 504 (Outdated Optimize Dep) 錯(cuò)誤了。啟動(dòng)日志如下:
Forced re-optimization of dependencies
vite:deps scanning for dependencies... +0ms
VITE v4.4.9 ready in 986 ms
? Local: http://localhost:5173/
? Network: use --host to expose
? press h to show help
+ call delayDepsOptimizerUntil('/not_root') 0ms after the last
+ call markIdAsDone('/not_root') 0ms after the last
vite:deps Crawling dependencies using entries:
vite:deps /home/projects/vitejs-vite-imqoo8/index.html +0ms
vite:deps ? static imports crawl ended +871ms
+ metadata.discovered ( size: 1 ) : element-ui/packages/theme-chalk/src/index.scss
vite:deps Scan completed in 927.22ms: no dependencies found +95ms
+ metadata.discovered ( size: 1 ) : element-ui/packages/theme-chalk/src/index.scss
+ call delayDepsOptimizerUntil('main.js') 177ms after the last
+ call delayDepsOptimizerUntil('node_modules/.vite/deps/element-ui_lib_button.js?v=1d7c7005') 10ms after the last
vite:deps Dependencies bundled in 1216.69ms +0ms
+ metadata.discovered ( size: 2 ) : element-ui/packages/theme-chalk/src/index.scss, element-ui/lib/button
vite:deps ? using post-scan optimizer result, the scanner found every used dependency +1s
vite:deps ? dependencies optimized +1ms
+ call delayDepsOptimizerUntil('node_modules/.pnpm/vite@4.4.9_sass@1.66.1/node_modules/vite/dist/client/client.mjs') 1175ms after the last
+ call delayDepsOptimizerUntil('node_modules/.pnpm/vite@4.4.9_sass@1.66.1/node_modules/vite/dist/client/env.mjs') 3ms after the last
+ call delayDepsOptimizerUntil('node_modules/.vite/deps/element-ui_lib_button.js?v=1d7c7005') 405ms after the last
多次運(yùn)行項(xiàng)目,你可能會(huì)發(fā)現(xiàn)綠色部分的日志出現(xiàn)在不同的位置。
根據(jù)日志可以看出兩點(diǎn):
-
調(diào)用
delayDepsOptimizerUntil('/not_root')之后 177ms 才調(diào)用的delayDepsOptimizerUntil('main.js')。這個(gè)時(shí)間間隔超出了 callCrawlEndIfIdleAfterMs 定義的 50ms ,所以定時(shí)器沒有被延長(zhǎng)反而早于預(yù)期地執(zhí)行了 onCrawlEnd 。
硬編碼來延長(zhǎng)計(jì)時(shí)器的做法就不太合理,來個(gè)耗時(shí)的異步任務(wù)就打破這個(gè)鏈條導(dǎo)致提前結(jié)束了。
理想的效果:
scan optimize │ ──────────────? ─────────────────────? │ │ ──────────────────────────────────────────────────?│ crawl of static imports │實(shí)際運(yùn)行的效果:
scan optimize │ ──────────────? ─────────────────────? │ │ ──────────────────────────────────────────?│------- crawl of static imports │這時(shí)提前結(jié)束很可能導(dǎo)致一部分依賴沒有預(yù)構(gòu)建上,不太好模擬自行想象吧。
如前面分析過的,
metadata.discovered的數(shù)量從 1 變?yōu)榱?2 。
修改 vite.config.js 中的配置:
- process.env.NO_SLOW || slowTransformIndexHtml(),
+ // process.env.NO_SLOW || slowTransformIndexHtml(),
Vite 會(huì)自動(dòng)重啟,再看下啟動(dòng)日志:
[vite] vite.config.js changed, restarting server...
Forced re-optimization of dependencies
vite:deps scanning for dependencies... +25m
[vite] server restarted.
+ call delayDepsOptimizerUntil('/not_root') 0ms after the last
+ call markIdAsDone('/not_root') 0ms after the last
vite:deps Crawling dependencies using entries:
vite:deps /home/projects/vitejs-vite-imqoo8/index.html +25m
+ call delayDepsOptimizerUntil('main.js') 823ms after the last
vite:deps Scan completed in 928.80ms: no dependencies found +99ms
+ call delayDepsOptimizerUntil('node_modules/.vite/deps/element-ui_lib_button.js?v=e80a6e76') 100ms after the last
+ call markIdAsDone('/home/projects/vitejs-vite-imqoo8/main.js') 922ms after the last
vite:deps ? static imports crawl ended +988ms
+ metadata.discovered ( size: 2 ) : element-ui/packages/theme-chalk/src/index.scss, element-ui/lib/button
+ metadata.discovered ( size: 2 ) : element-ui/packages/theme-chalk/src/index.scss, element-ui/lib/button
vite:deps Dependencies bundled in 1137.07ms +25m
+ metadata.discovered ( size: 2 ) : element-ui/packages/theme-chalk/src/index.scss, element-ui/lib/button
vite:deps ? new dependencies were found while crawling that weren't detected by the scanner +1s
vite:deps ? re-running optimizer +0ms
vite:deps new dependencies found: element-ui/packages/theme-chalk/src/index.scss, element-ui/lib/button +6ms
vite:deps Dependencies bundled in 1032.49ms +1s
vite:deps ? dependencies optimized +1s
+ call delayDepsOptimizerUntil('node_modules/.vite/deps/chunk-76J2PTFD.js?v=b0e72c20') 2196ms after the last
+ call delayDepsOptimizerUntil('node_modules/.pnpm/vite@4.4.9_sass@1.66.1/node_modules/vite/dist/client/client.mjs') 4394ms after the last
+ call delayDepsOptimizerUntil('node_modules/.pnpm/vite@4.4.9_sass@1.66.1/node_modules/vite/dist/client/env.mjs') 2ms after the last
會(huì)發(fā)現(xiàn) metadata.discovered 的數(shù)量不再變化,并且 re-running optimizer 這一步按照預(yù)期所想的被執(zhí)行了。
此時(shí)在頁(yè)面中可以看到 Hello,World! 的內(nèi)容,并且控制臺(tái)也不再報(bào)錯(cuò)了。
另外,這里比較有意思的一點(diǎn)就是 delayDepsOptimizerUntil('main.js') 前的時(shí)間間隔變成了 823ms ,說明在此之前執(zhí)行了 scss 文件的預(yù)構(gòu)建,然而再重啟項(xiàng)目會(huì)發(fā)現(xiàn)這個(gè)時(shí)間可能又變成 10ms 了,也就是說預(yù)構(gòu)建 scss 文件的時(shí)機(jī)忽早忽遲的。
后續(xù)思考
async/await 語(yǔ)法可以看作是 Promise 鏈?zhǔn)交卣{(diào)的語(yǔ)法糖,以類似寫同步代碼的順序來寫異步代碼,更便于理解。加上 JS 本就是單線程執(zhí)行的,所以用多了這種語(yǔ)法以后就會(huì)陷入一種 就是在寫同步代碼 的誤區(qū)。await 表達(dá)式會(huì)跳出當(dāng)前函數(shù)而執(zhí)行 隊(duì)列( event loop ) 中的其他代碼,等待異步操作結(jié)束后再跳回到當(dāng)前函數(shù)繼續(xù)執(zhí)行, 這個(gè)中間過程需要多久,又執(zhí)行了哪些不相關(guān)的其他代碼 是無(wú)法預(yù)料的,所以本文出現(xiàn)的這個(gè)問題就是因?yàn)闆]有考慮到 await 前后局部變量的狀態(tài)會(huì)發(fā)生變化而導(dǎo)致的。
而且異步代碼不只是 Promise ,還有各種 事件監(jiān)聽 、 定時(shí)器 、 微任務(wù) 等,綜合下來整個(gè)代碼的運(yùn)行順序和想象中可能就不太一樣了。思考下面這段代碼的執(zhí)行結(jié)果:
const queue = [];
async function sleep(time) {
await new Promise((resolve) => {
setTimeout(() => resolve(), time);
});
}
async function randomSleep() {
await sleep(Math.round(Math.random() * 100));
}
async function a(n) {
await randomSleep();
queue.push(`a${n}`);
await b(n);
}
async function b(n) {
await randomSleep();
queue.push(`b${n}`);
await c(n);
}
async function c(n) {
await randomSleep();
queue.push(`c${n}`);
}
(async () => {
await Promise.all(
Array(5)
.fill(null)
.map((_, n) => a(n))
);
console.log(queue.join(' > '));
})();
a[n] > b[n] > c[n] 的順序是必然的,但是 a[i] 和 b[j] 的順序就說不準(zhǔn)了。
另外,在寫同步代碼的時(shí)候都喜歡用這種方式來計(jì)算耗時(shí):
const start = Date.now();
foo();
console.log(`use ${Date.now() - start}ms`);
針對(duì)異步代碼還用這種方式就不太準(zhǔn)了,還是以上面的代碼為例,運(yùn)行結(jié)果可能是這樣的:
a0 > a1 > b0 > b1 > c0 > c1
如果只是簡(jiǎn)單的以 a0 開始 c0 結(jié)束來計(jì)算時(shí)間差顯然是錯(cuò)誤的,因?yàn)橹虚g還包括了 a1 和 b1 的耗時(shí)。
Vite 就是如此簡(jiǎn)單粗暴地計(jì)算耗時(shí)的。比如為了 自定義 Element 主題 就需要編譯整個(gè) scss 文件,然而打開 debug 日志后可以發(fā)現(xiàn)不僅是這個(gè) scss 文件的耗時(shí)巨慢,其他代碼的耗時(shí)也跟著增加了,這就對(duì)排查問題造成了干擾( 明明慢的只是一個(gè)文件而已,結(jié)果日志卻顯示很多文件都慢 )。
一個(gè)巨慢的同步任務(wù)導(dǎo)致整個(gè)主線程卡住顯然是不好的體驗(yàn),只能期待后續(xù) Vite 對(duì)多線程的應(yīng)用 。
關(guān)于 StackBlitz
曾經(jīng)在分享 demo 的時(shí)候用到過一些在線編輯器: CodeSandbox 、 CodePen 、 StackBlitz 等,主要原理都是把代碼上傳到云端,然后單獨(dú)啟動(dòng)一個(gè)容器來運(yùn)行項(xiàng)目并提供端口供用戶訪問。限制還是比較多,寫項(xiàng)目不現(xiàn)實(shí),只適合做分享和演示,綜合使用下來還是 CodeSandbox 體驗(yàn)好一點(diǎn)。
然而沒想到幾年過去 StackBlitz 直接彎道超車了, WebContainers 把整個(gè) Node.js 環(huán)境搬到了瀏覽器上,可以像在本地環(huán)境中一樣使用任意的前端框架并且支持各種 Node.js 后端框架,再加上 VSCode 風(fēng)格的編輯器,和本地開發(fā)體驗(yàn)是非常接近的。
這次提 PR 就用到了 StackBlitz Codeflow ( 強(qiáng)烈建議看下官網(wǎng)的演示視頻 ),寫 demo 、提 issue 、寫 PR 整個(gè)流程一條龍服務(wù),不需要 clone 項(xiàng)目到本地,所有操作全在瀏覽器內(nèi)完成。
- .new 域名 支持創(chuàng)建眾多腳手架生成的模板項(xiàng)目。
- 打開任意的 GitHub 倉(cāng)庫(kù),在 url 前面加上
pr.new前綴就能重定向到 Codeflow IDE 一鍵開始項(xiàng)目開發(fā),而無(wú)需專門配置本地開發(fā)環(huán)境。 - 從 issue 頁(yè)面跳轉(zhuǎn)到 pr.new 后會(huì)自動(dòng)拉取 issue 中提供的 reproduction 項(xiàng)目和主體項(xiàng)目進(jìn)行 關(guān)聯(lián) ,這樣寫 PR 的時(shí)候就能直接驗(yàn)證更改操作是否有效。 這個(gè)功能非常有用!
如何評(píng)價(jià) StackBlitz 可在瀏覽器運(yùn)行 Node.js 程序的 WebContainers?
最后
這是我真正意義上的第一次提 issue 和 PR ,很榮幸成為 Vite 項(xiàng)目 contributors 中的一員,為開源貢獻(xiàn)了一點(diǎn)自己的微薄之力。
不過,在本地定位問題并解決也就只需要幾個(gè)小時(shí),然而梳理原因、分析解釋、翻譯、提供最小復(fù)現(xiàn)、測(cè)試卻花費(fèi)了數(shù)天,挺耽誤時(shí)間的。再加上項(xiàng)目成員看到問題、確認(rèn)、合并 PR 、發(fā)包又是不短的時(shí)間,時(shí)效性太低了,這也是我一直以來不想提 issue 的原因。
仔細(xì)想想,還是自己改代碼然后 patch-package 打補(bǔ)丁比較香,這也是 node_modules 這個(gè) 屎山 為數(shù)不多的優(yōu)點(diǎn)了。
