大家好,我是若川。最近組織了源碼共讀活動《1個月,200+人,一起讀了4周源碼》,已經有超50+人提交了筆記,群里已經有超1500人,感興趣的可以點此鏈接掃碼加我微信?ruochuan12
create-vue
公開了,可以使用npm init vue@next
替代vue-cli
快速初始化vue3
項目。我粗看了源碼,發現只有300行左右,打算加入到源碼共讀計劃大家一起學習。
沒想到源碼共讀群里的小伙伴
upupming
就很迅速的學習了源碼并且輸出了文章。本文是upupming
投稿。
原標題:create-vue 發布并成為官方推薦,未來將替換 vue cli,看看 Vue Contributor Days 說了哪些內容(附 create-vue 源碼解析)
美國時間 2021 年 10 月 7 日早晨,This Dot Media 邀請了 Vue 的核心成員和 Vue Community (例如 Quasar, Ionic 開發者等)的一些主要貢獻者舉辦了一個 Vue Contributor Days 在線會議,長達兩個半小時,會上 vue-cli 的核心貢獻者胖茶也在同一天公開了全新的腳手架工具 create-vue[1],我也是看到 antfu 發推就關注了一下,看完直播回放[2]之后收獲很大,這里做一些總結并且分析一下最新發布的 create-vue 的源碼。
主要關注了尤大的 talk,PPT 鏈接[3]在這里,我轉載到了我的 GitHub 上[4],大家可以下載來看一下最新的進展,另外還有胖茶現場演示了如何使用 create-vue。
1. 主要內容
Vue 3.2 在 2021.08.09 就已經發布了,最重要的就是 <script setup>
不再是實驗階段了,可以穩定使用。之前用過 composition API 的朋友都會覺得比較麻煩,<script setup>
主要是為了作為語法糖簡化其寫法,可以參考文檔[5]和 RFC[6]。Vue 3.2 新增了一些新功能,如 defineCustomElement
和 v-memo
等等。
尤大還提到了新的 Ref Transform 提案,雖然現在 TS 環境下多一個 .value
沒有什么太大的問題,類型提示能夠自動補全做的很好,但是 Ref Transform 提案可以進一步簡化省去 .value
這一步,而且 TS 支持也做好了:

使用 cout = $ref(0)
定義之后,使用 count
的時候會被編譯成 count.value
,同時還有一個反向操作 $$
,重新把 reactive value 變成 ref。感覺這個簡化確實很有前途,可能會被廣泛使用。
另外 Vue 團隊正在積極準備 3.3 版本,主要集中精力在優化 SSR 相關的功能。可以期待一下。
尤大給出了最新的官方推薦:

推薦使用 create-vue 替換 vue cli,注意如果你的項目如果使用 vue cli 創建的,能夠穩定使用的話,暫時沒有提供轉換成 create-vue 項目的方案,而且也不建議修改大型項目的基礎配置。以后的新項目大家可以使用 create-vue 來創建更加快的應用,因為 create-vue 的模板項目都是基于 vite 來進行構建的了。
推薦使用 VSCode 的 Volar 插件而不是 Vetur 來獲取更好的 TypeScript 支持(script setup 支持地很好,
vue-tsc
表現和 volar 一致,因為都是用的一個 language service (@johnsoncodehk 開發的vscode-vue-languageservice
)),卸載 Vetur 安裝 Volar 即可。狀態管理的話,正在考慮在 vuex next / Pinia (也是新出來的一個狀態管理工具) / vue core 三者中考慮一個新的主推
另外 Vue 3 的官方文檔正在快速更新,新版的文檔可以在 https://github.com/vuejs/docs/tree/next 看到源碼,部署在 https://vue-docs-preview.netlify.app/ 。里面新增了許多 example,可以看 Options API 和 Composition API 兩種格式,Composition API 已經都是用 script setup
來寫的了,另外也有 HTML 和 SFC 兩種不同的版本,感覺用 example 學起來方便很多。同時也提供了 tutorial 和 guide 兩種學習方案,非常體貼。
胖茶介紹了 create-vue 的使用,令人興奮的是,所有的模板現在的構建工具全部都是基于 vite 而不是 vue cli (Webpack) 的了,開發效率大大提升,同時使用 cypress
來作為自動測試的工具。之前 Vue 2 單元測試用的是 Jest,但是 Jest 對 Vue 3 的編譯支持的不是很好,所以選擇了 cypress
同時做單元測試和 E2E 測試。整個 create-vue
包的依賴數量非常少,很多沒有必要的依賴都沒放,而且胖茶自己做了一個預先打包導致下載速度變快了許多。同時創建的模板項目也足夠輕量。
后面 antfu 還介紹了最近他開發的 unplugin[7],支持一個插件寫完,Rollup、Vite、Webpack 4、Webpack 5 都能使用,這個還沒有詳細使用過,下次有機會細看。
2. create-vue 源碼解析
首先看目錄結構:
index.js
是整個 CLI 的打包入口,所有邏輯都是從這里開始的utils
包含了用到的一些工具函數template
Vue 項目模板,例如默認的 default 模板、帶 router 的模板、帶 ts 支持的模板等等。playground
利用 create-vue 生成的項目的快照結果,在運行pnpm test
時會用到,測試生成的模板項目代碼的正確性
非常簡潔明了,好在項目處于剛開始的階段,index.js
只有 300 多行,可以很容易了解其中的細節。
2.1 整體流程
使用
prompt
詢問用戶一系列 Yes/No 的問題,看用戶需要哪些 feature,包括 TS, JSX, router, vuex, cypress。同時也會詢問包名和是否覆蓋已經存在的文件夾(如果之前已經創建過內容的話)。驗證包名是否合法,將不合法包名轉換成合法的。
寫入帶包名和版本號的 package.json
調用
render
函數,首先使用render('base')
創建一個基礎的模板,接下來按照用戶需要哪些 feature,往已經創建的項目中添加對應的模板,例如render('config/jsx')
就是對基礎模板添加了 JSX 支持。如果需要 TS 支持的話,后面有個特殊操作把所有 JS 重命名為 TS。將
jsconfig.json
重命名為tsconfig.json
。默認是所有模板都包含測試的,如果用戶不需要,最后需要刪除一下
判斷當前使用的包管理器是
npm/yarn/pnpm
,方便后續輸出xxx install
提示生成 README.md
最后輸出提示,提示用戶生成成功并展示綠色(
kolorist
這個包用來處理顏色)的提示消息,提示后續操作cd xxx
,xxx install
,xxx dev
可以先運行一遍 npm init vue@next
感受一下具體的效果。
2.2 具體分析
可以看到,這里面最重要的還是 render
函數的實現,可以把一個相對目錄下的文件給復制到最終生成的項目里面,同時還需要考慮文件相同的時候需要如何進行合并操作。這里主要看一下 render
函數的實現和一些我們以后可能用到的工具函數。
2.2.1 支持 feature flag
支持類似 npm init vue@next --vuex --ts
這種命令行參數,省去 prompt
提問環節直接開始生成項目。
//?index.jsconst?isFeatureFlagsUsed?=typeof?(argv.default?||?argv.ts?||?argv.jsx?||?argv.router?||?argv.vuex?||?argv.tests)?==='boolean'prompt(
//?...
{name:?'needsTypeScript',//?如果使用了?feature?flag,直接?type?函數返回?null,就不會提問了type:?()?=>?(isFeatureFlagsUsed???null?:?'toggle'),message:?'Add?TypeScript?',initial:?false,active:?'Yes',inactive:?'No'
},
)
//?...
2.2.2 render
函數
//?index.js//?所有模板根目錄位于?template?之下
const?templateRoot?=?path.resolve(__dirname,?'template')
//?傳給一個模板名稱,例如?`base`,對應于?template/base?這個模板
const?render?=?function?render(templateName)?{//?拿到真正的模板路徑?templateDir?之后使用?renderTemplate?將?templateDir?下的內容嘗試生成到?root?中,這里?root?就是之前用戶輸入指定的目標路徑const?templateDir?=?path.resolve(templateRoot,?templateName)renderTemplate(templateDir,?root)
}
template/base
是一個最簡單的所有結果都需要模板,它包括了 .vscode
、index.html
、vite.config.js
等這些基礎性的東西。注意 vite 的理念和 Webpack 不一樣,Webpack 和 esbuild 這些都是以 JS 為入口,但是 vite 是以 index.html 為入口的,使用的時候需要轉換一下思維。這個模板的目錄結構如下:
.
├── _gitignore
├── index.html
├── package.json
├── public
│ └── favicon.ico
└── vite.config.js
注意里面有個 _gitignore
文件,使用 _
開頭是個慣例,因為以 .
開頭的都是配置文件,會影響一些 CLI 工具和編輯器的行為,所以為了避免影響而使用 _
,真正 render 的過程中需要重命名成 .
開頭
我們主要看 renderTemplate
這個函數,位于 util/renderTemplate.js
中。
函數簽名注釋可以看,就是一個復制過程,但是又不完全是直接的復制,需要有一些特殊操作要考慮:
//?utils/renderTemplate.js/***?Renders?a?template?folder/file?to?the?file?system,*?by?recursively?copying?all?files?under?the?`src`?directory,*?with?the?following?exception:*???-?`_filename`?should?be?renamed?to?`.filename`*???-?Fields?in?`package.json`?should?be?recursively?merged*?@param?{string}?src?source?filename?to?copy*?@param?{string}?dest?destination?filename?of?the?copy?operation*/
function?renderTemplate(src,?dest)?{
如果發現傳入的是 src
文件夾的話,遞歸調用 renderTemplate
處理文件夾下的每一個文件或者文件夾:
//?utils/renderTemplate.jsconst?stats?=?fs.statSync(src)if?(stats.isDirectory())?{//?if?it's?a?directory,?render?its?subdirectories?and?files?recusivelyfs.mkdirSync(dest,?{?recursive:?true?})for?(const?file?of?fs.readdirSync(src))?{renderTemplate(path.resolve(src,?file),?path.resolve(dest,?file))}return}
遞歸調用寫好,下面就只需要考慮 src
是文件的情況了。
如果是 package.json
文件,并且目標路徑已經存在,需要先 merge 兩個 JSON 對象,然后將 dependencies, devDependencies, peerDependencies, optionalDependencies 這 4 個字段按照字母序從上到下排列好。
if?(filename?===?'package.json'?&&?fs.existsSync(dest))?{//?merge?instead?of?overwritingconst?existing?=?JSON.parse(fs.readFileSync(dest))const?newPackage?=?JSON.parse(fs.readFileSync(src))const?pkg?=?sortDependencies(deepMerge(existing,?newPackage))fs.writeFileSync(dest,?JSON.stringify(pkg,?null,?2)?+?'\n')return}
如果文件以 _
開頭,需要重命名成以 .
開頭:
if?(filename.startsWith('_'))?{//?rename?`_file`?to?`.file`dest?=?path.resolve(path.dirname(dest),?filename.replace(/^_/,?'.'))}
2.2.3 deepMerge
和 sortDependencies
這里有兩個比較有用的函數,deepMerge
用來 merge 兩個 object,相信這個也是面試的時候常考的一個題目,具體的思路就是如果都是對象的話就繼續遞歸,遞歸到原始類型的時候就可以直接賦值來實現賦值了,而數組的話直接用解構賦值來一個淺拷貝就行了。
const?isObject?=?(val)?=>?val?&&?typeof?val?===?'object'
const?mergeArrayWithDedupe?=?(a,?b)?=>?Array.from(new?Set([...a,?...b]))/***?Recursively?merge?the?content?of?the?new?object?to?the?existing?one*?@param?{Object}?target?the?existing?object*?@param?{Object}?obj?the?new?object*/
function?deepMerge(target,?obj)?{for?(const?key?of?Object.keys(obj))?{const?oldVal?=?target[key]const?newVal?=?obj[key]if?(Array.isArray(oldVal)?&&?Array.isArray(newVal))?{//?key?字段對應的值都是?array,那么使用?destructuring?來?mergetarget[key]?=?mergeArrayWithDedupe(oldVal,?newVal)}?else?if?(isObject(oldVal)?&&?isObject(newVal))?{//?key?字段對應的值都是對象,那么遞歸調用target[key]?=?deepMerge(oldVal,?newVal)}?else?{target[key]?=?newVal}}return?target
}
sortDependencies
是將對象按照 key 進行排序,ES6 標準要求 object 對字符串類型的 key 按照插入序排列,對整數類型的 key 按照升序排列[8],因為依賴項都是 npm 包名,必然以字母開頭,可以按照插入序保證其迭代的時候的順序,從而使得解構賦值能夠拿到正確的順序。
export?default?function?sortDependencies(packageJson)?{const?sorted?=?{}const?depTypes?=?['dependencies',?'devDependencies',?'peerDependencies',?'optionalDependencies']for?(const?depType?of?depTypes)?{if?(packageJson[depType])?{sorted[depType]?=?{}Object.keys(packageJson[depType]).sort().forEach((name)?=>?{sorted[depType][name]?=?packageJson[depType][name]})}}return?{...packageJson,...sorted}
}
2.2.4 清除舊項目 rm -rf
之前經常遇到一個問題是 fs.rmdirSync
這個函數只能刪除空文件夾,非空文件夾會報錯,搜索 Stack Overflow 給的最高票答案是用 rimraf[9],但是這里為了少引入包可以直接實現了遞歸刪除文件的功能。用的是多叉樹深搜中的后序遍歷,因為需要先刪除子文件和子文件夾,才能保證當前文件夾為空。實現如下:
//?utils/directoryTraverse.jsexport?function?postOrderDirectoryTraverse(dir,?dirCallback,?fileCallback)?{for?(const?filename?of?fs.readdirSync(dir))?{const?fullpath?=?path.resolve(dir,?filename)//?如果是文件夾,遞歸if?(fs.lstatSync(fullpath).isDirectory())?{postOrderDirectoryTraverse(fullpath,?dirCallback,?fileCallback)//?子文件和子文件夾都處理好了再來用?dirCallback?處理這個文件夾dirCallback(fullpath)continue}//?如果是文件,直接用?fileCallback?處理fileCallback(fullpath)}
}function?emptyDir(dir)?{postOrderDirectoryTraverse(dir,(dir)?=>?fs.rmdirSync(dir),(file)?=>?fs.unlinkSync(file))
}
這個工具函數也非常有用,又省去了一個 npm install
。
3. 測試
寫完代碼需要進行測試保證正確性。package.json
中的測試腳本如下所示:
"build":?"esbuild?--bundle?index.js?--format=cjs?--platform=node?--outfile=outfile.cjs",
"snapshot":?"node?snapshot.js",
"pretest":?"run-s?build?snapshot",
"test":?"node?test.js",
可以看到,首先是 pretest
運行 npm run build
進行打包,然后運行 npm run snapshot
生成 snapshot,生成快照過程就是各個 feature flag 排列組合一下,調用 create-vue 生成所有的可能的 feature flag 組合的模板結果,結果存放在 playground
文件夾下。排列組合可以使用二進制枚舉實現。代碼如下:
const?featureFlags?=?['typescript',?'jsx',?'router',?'vuex',?'with-tests']//?The?following?code?&?comments?are?generated?by?GitHub?CoPilot.
function?fullCombination(arr)?{const?combinations?=?[]//?for?an?array?of?5?elements,?there?are?2^5?-?1=?31?combinations//?(excluding?the?empty?combination)//?equivalent?to?the?following://?[0,?0,?0,?0,?1]?...?[1,?1,?1,?1,?1]//?We?can?represent?the?combinations?as?a?binary?number//?where?each?digit?represents?a?flag//?and?the?number?is?the?index?of?the?flag//?e.g.//?[0,?0,?0,?0,?1]?=?0b0001//?[1,?1,?1,?1,?1]?=?0b1111//?Note?we?need?to?exclude?the?empty?comination?in?our?casefor?(let?i?=?1;?i?<?1?<<?arr.length;?i++)?{const?combination?=?[]for?(let?j?=?0;?j?<?arr.length;?j++)?{if?(i?&?(1?<<?j))?{combination.push(arr[j])}}combinations.push(combination)}return?combinations
}const?flagCombinations?=?fullCombination(featureFlags)
flagCombinations.push(['default'])for?(const?flags?of?flagCombinations)?{createProjectWithFeatureFlags(flags)
}
之后再運行 test.js
,就是對 playground
里面所有的項目依次運行 test:unit:ci
(組件單元測試) 和 test:e2e:ci
(E2E測試)了:
const?playgroundDir?=?path.resolve(__dirname,?'./playground/')for?(const?projectName?of?fs.readdirSync(playgroundDir))?{if?(projectName.endsWith('with-tests'))?{console.log(`Running?unit?tests?in?${projectName}`)const?unitTestResult?=?spawnSync('pnpm',?['test:unit:ci'])console.log(`Running?e2e?tests?in?${projectName}`)const?e2eTestResult?=?spawnSync('pnpm',?['test:e2e:ci'])}
}
4. 總結
看尤大的 talk 每次都有比較大的收獲,有很多細小的問題都會解釋的比較清晰,同時對 Vue 的未來規劃能了解一些。
create-vue 代碼簡潔,依賴少,啟動快,同時這次全面擁抱 vite 也將是非常好的,拋棄掉 Webpack 之后輕松了許多,開發體驗提升了不少。create-vue 中有不少工具函數可以先記下來,下次需要用到的時候就不愁沒處 copy 啦~
不過 create-vue 現在還沒有給模板添加 eslint 配置,后續可能會加上
參考資料
[1]
create-vue: https://github.com/vuejs/create-vue
[2]直播回放: https://www.youtube.com/watch?v=gpTbH469Qog&ab_channel=ThisDotMedia
[3]PPT 鏈接: https://docs.google.com/presentation/d/137pQTDQI8O1FHzn2AtjL5tjeTnr8wbON7IonkcNyBVs/edit#slide=id.p
[4]我的 GitHub 上: https://github.com/upupming/frontend-learning-map/tree/main/slides/State_of_Vue_ThisDot_Meetup_Oct_2021.pdf
[5]文檔: https://v3.vuejs.org/api/sfc-script-setup.html#basic-syntax
[6]RFC: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
[7]unplugin: https://github.com/unjs/unplugin
[8]ES6 標準要求 object 對字符串類型的 key 按照插入序排列,對整數類型的 key 按照升序排列: https://stackoverflow.com/a/23202095/8242705
[9]rimraf: https://stackoverflow.com/a/16605300/8242705
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信?江西?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~