<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          尤雨溪寫的100多行的“玩具 vite”,十分有助于理解 vite 原理

          共 9194字,需瀏覽 19分鐘

           ·

          2021-10-28 23:19

          1. 前言

          大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12 參與,已進行兩個多月,大家一起交流學(xué)習(xí),共同進步。

          想學(xué)源碼,極力推薦之前我寫的《學(xué)習(xí)源碼整體架構(gòu)系列》 包含jQueryunderscore、lodashvuex、sentry、axios、reduxkoa、vue-devtools、vuex4、koa-compose、vue-next-release、vue-this、create-vue等10余篇源碼文章。

          最近組織了源碼共讀活動,大家一起學(xué)習(xí)源碼。于是各種搜尋值得我們學(xué)習(xí),且代碼行數(shù)不多的源碼。

          vuejs組織[1] 下,找到了尤雨溪幾年前寫的“玩具 vite”vue-dev-server[2],發(fā)現(xiàn)100來行代碼,很值得學(xué)習(xí)。于是有了這篇文章。

          閱讀本文,你將學(xué)到:

          1.?學(xué)會?vite?簡單原理
          2.?學(xué)會使用?VSCode?調(diào)試源碼
          3.?學(xué)會如何編譯?Vue?單文件組件
          4.?學(xué)會如何使用?recast?生成?ast?轉(zhuǎn)換文件
          5.?如何加載包文件
          6.?等等

          2. vue-dev-server 它的原理是什么

          vue-dev-server#how-it-works[3]README 文檔上有四句英文介紹。

          發(fā)現(xiàn)谷歌翻譯[4]的還比較準(zhǔn)確,我就原封不動的搬運過來。

          • 瀏覽器請求導(dǎo)入作為原生 ES 模塊導(dǎo)入 - 沒有捆綁。
          • 服務(wù)器攔截對 *.vue 文件的請求,即時編譯它們,然后將它們作為 JavaScript 發(fā)回。
          • 對于提供在瀏覽器中工作的 ES 模塊構(gòu)建的庫,只需直接從 CDN 導(dǎo)入它們。
          • 導(dǎo)入到 .js 文件中的 npm 包(僅包名稱)會即時重寫以指向本地安裝的文件。 目前,僅支持 vue 作為特例。 其他包可能需要進行轉(zhuǎn)換才能作為本地瀏覽器目標(biāo) ES 模塊公開。

          也可以看看vitejs 文檔[5],了解下原理,文檔中圖畫得非常好。

          7c7ed8b515eff7030f0ac22d7665dd14.webp

          看完本文后,我相信你會有一個比較深刻的理解。

          3. 準(zhǔn)備工作

          3.1 克隆項目

          本文倉庫 vue-dev-server-analysis,求個star^_^[6]

          #?推薦克隆我的倉庫
          git?clone?https://github.com/lxchuan12/vue-dev-server-analysis.git
          cd?vue-dev-server-analysis/vue-dev-server
          #?npm?i?-g?yarn
          #?安裝依賴
          yarn

          #?或者克隆官方倉庫
          git?clone?https://github.com/vuejs/vue-dev-server.git
          cd?vue-dev-server
          #?npm?i?-g?yarn
          #?安裝依賴
          yarn

          一般來說,我們看源碼先從package.json文件開始:

          //?vue-dev-server/package.json
          {
          ??"name":?"@vue/dev-server",
          ??"version":?"0.1.1",
          ??"description":?"Instant?dev?server?for?Vue?single?file?components",
          ??"main":?"middleware.js",
          ??//?指定可執(zhí)行的命令
          ??"bin":?{
          ????"vue-dev-server":?"./bin/vue-dev-server.js"
          ??},
          ??"scripts":?{
          ????//?先跳轉(zhuǎn)到?test?文件夾,再用?Node?執(zhí)行?vue-dev-server?文件
          ????"test":?"cd?test?&&?node?../bin/vue-dev-server.js"
          ??}
          }

          根據(jù) scripts test 命令。我們來看 test 文件夾。

          3.2 test 文件夾

          vue-dev-server/test 文件夾下有三個文件,代碼不長。

          • index.html
          • main.js
          • text.vue

          如圖下圖所示。

          e9cfc6be85c090d295ebecca42433b60.webptest文件夾三個文件

          接著我們找到 vue-dev-server/bin/vue-dev-server.js 文件,代碼也不長。

          3.3 vue-dev-server.js

          //?vue-dev-server/bin/vue-dev-server.js
          #!/usr/bin/env?node

          const?express?=?require('express')
          const?{?vueMiddleware?}?=?require('../middleware')

          const?app?=?express()
          const?root?=?process.cwd();

          app.use(vueMiddleware())

          app.use(express.static(root))

          app.listen(3000,?()?=>?{
          ??console.log('server?running?at?http://localhost:3000')
          })

          原來就是express啟動了端口3000的服務(wù)。重點在 vueMiddleware 中間件。接著我們來調(diào)試這個中間件。

          鑒于估計很多小伙伴沒有用過VSCode調(diào)試,這里詳細(xì)敘述下如何調(diào)試源碼。學(xué)會調(diào)試源碼后,源碼并沒有想象中的那么難。

          3.4 用 VSCode 調(diào)試項目

          vue-dev-server/bin/vue-dev-server.js 文件中這行 app.use(vueMiddleware()) 打上斷點。

          找到 vue-dev-server/package.jsonscripts,把鼠標(biāo)移動到 test 命令上,會出現(xiàn)運行腳本調(diào)試腳本命令。如下圖所示,選擇調(diào)試腳本。

          f3d5d9c2a759e931ba9f66b77fd0022e.webp調(diào)試0dcd6a204d688c0ccfbff92c19d7156b.webpVSCode 調(diào)試 Node.js 說明

          點擊進入函數(shù)(F11)按鈕可以進入 vueMiddleware 函數(shù)。如果發(fā)現(xiàn)斷點走到不是本項目的文件中,不想看,看不懂的情況,可以退出或者重新來過可以用瀏覽器無痕(隱私)模式(快捷鍵Ctrl + Shift + N,防止插件干擾)打開 http://localhost:3000,可以繼續(xù)調(diào)試 vueMiddleware 函數(shù)返回的函數(shù)。

          如果你的VSCode不是中文(不習(xí)慣英文),可以安裝簡體中文插件[7]。
          如果 VSCode 沒有這個調(diào)試功能。建議更新到最新版的 VSCode(目前最新版本 v1.61.2)。

          接著我們來跟著調(diào)試學(xué)習(xí) vueMiddleware 源碼。可以先看主線,在你覺得重要的地方繼續(xù)斷點調(diào)試。

          4. vueMiddleware 源碼

          4.1 有無 vueMiddleware 中間件對比

          不在調(diào)試情況狀態(tài)下,我們可以在 vue-dev-server/bin/vue-dev-server.js 文件中注釋 app.use(vueMiddleware()),執(zhí)行 npm run test 打開 http://localhost:3000。

          690d5b3b690a8e2a0be58861132584fc.webp沒有執(zhí)行 vueMiddleware 中間件的原始情況

          再啟用中間件后,如下圖。

          40713ade6ef12f0849a90ade87fe51ad.webp執(zhí)行了 vueMiddleware 中間文件變化

          看圖我們大概知道了有哪些區(qū)別。

          4.2 vueMiddleware 中間件概覽

          我們可以找到vue-dev-server/middleware.js,查看這個中間件函數(shù)的概覽。

          //?vue-dev-server/middleware.js

          const?vueMiddleware?=?(options?=?defaultOptions)?=>?{
          ??//?省略
          ??return?async?(req,?res,?next)?=>?{
          ????//?省略
          ????//?對?.vue?結(jié)尾的文件進行處理
          ????if?(req.path.endsWith('.vue'))?{
          ????//?對?.js?結(jié)尾的文件進行處理
          ????}?else?if?(req.path.endsWith('.js'))?{
          ????//?對?/__modules/?開頭的文件進行處理
          ????}?else?if?(req.path.startsWith('/__modules/'))?{
          ????}?else?{
          ??????next()
          ????}
          ??}
          }
          exports.vueMiddleware?=?vueMiddleware

          vueMiddleware 最終返回一個函數(shù)。這個函數(shù)里主要做了四件事:

          • .vue 結(jié)尾的文件進行處理
          • .js 結(jié)尾的文件進行處理
          • /__modules/ 開頭的文件進行處理
          • 如果不是以上三種情況,執(zhí)行 next 方法,把控制權(quán)交給下一個中間件

          接著我們來看下具體是怎么處理的。

          我們也可以斷點這些重要的地方來查看實現(xiàn)。比如:

          d5fc3f92e8735af90eefdee69dce1235.webp重要斷點

          4.3 對 .vue 結(jié)尾的文件進行處理

          if?(req.path.endsWith('.vue'))?{
          ??const?key?=?parseUrl(req).pathname
          ??let?out?=?await?tryCache(key)

          ??if?(!out)?{
          ????//?Bundle?Single-File?Component
          ????const?result?=?await?bundleSFC(req)
          ????out?=?result
          ????cacheData(key,?out,?result.updateTime)
          ??}

          ??send(res,?out.code,?'application/javascript')
          }

          4.3.1 bundleSFC 編譯單文件組件

          這個函數(shù),根據(jù) @vue/component-compiler[8] 轉(zhuǎn)換單文件組件,最終返回瀏覽器能夠識別的文件。

          const?vueCompiler?=?require('@vue/component-compiler')
          async?function?bundleSFC?(req)?{
          ??const?{?filepath,?source,?updateTime?}?=?await?readSource(req)
          ??const?descriptorResult?=?compiler.compileToDescriptor(filepath,?source)
          ??const?assembledResult?=?vueCompiler.assemble(compiler,?filepath,?{
          ????...descriptorResult,
          ????script:?injectSourceMapToScript(descriptorResult.script),
          ????styles:?injectSourceMapsToStyles(descriptorResult.styles)
          ??})
          ??return?{?...assembledResult,?updateTime?}
          }

          接著我們來看 readSource 函數(shù)實現(xiàn)。

          4.3.2 readSource 讀取文件資源

          這個函數(shù)主要作用:根據(jù)請求獲取文件資源。返回文件路徑 filepath、資源 source、和更新時間 updateTime。

          const?path?=?require('path')
          const?fs?=?require('fs')
          const?readFile?=?require('util').promisify(fs.readFile)
          const?stat?=?require('util').promisify(fs.stat)
          const?parseUrl?=?require('parseurl')
          const?root?=?process.cwd()

          async?function?readSource(req)?{
          ??const?{?pathname?}?=?parseUrl(req)
          ??const?filepath?=?path.resolve(root,?pathname.replace(/^\//,?''))
          ??return?{
          ????filepath,
          ????source:?await?readFile(filepath,?'utf-8'),
          ????updateTime:?(await?stat(filepath)).mtime.getTime()
          ??}
          }

          exports.readSource?=?readSource

          接著我們來看對 .js 文件的處理

          4.4 對 .js 結(jié)尾的文件進行處理

          if?(req.path.endsWith('.js'))?{
          ??const?key?=?parseUrl(req).pathname
          ??let?out?=?await?tryCache(key)

          ??if?(!out)?{
          ????//?transform?import?statements
          ????//?轉(zhuǎn)換?import?語句?
          ????//?import?Vue?from?'vue'
          ????//?=>?import?Vue?from?"/__modules/vue"
          ????const?result?=?await?readSource(req)
          ????out?=?transformModuleImports(result.source)
          ????cacheData(key,?out,?result.updateTime)
          ??}

          ??send(res,?out,?'application/javascript')
          }

          針對 vue-dev-server/test/main.js 轉(zhuǎn)換

          import?Vue?from?'vue'
          import?App?from?'./test.vue'

          new?Vue({
          ??render:?h?=>?h(App)
          }).$mount('#app')

          //?公眾號:若川視野
          //?加微信?ruochuan12
          //?參加源碼共讀,一起學(xué)習(xí)源碼
          import?Vue?from?"/__modules/vue"
          import?App?from?'./test.vue'

          new?Vue({
          ??render:?h?=>?h(App)
          }).$mount('#app')

          //?公眾號:若川視野
          //?加微信?ruochuan12
          //?參加源碼共讀,一起學(xué)習(xí)源碼

          4.4.1 transformModuleImports 轉(zhuǎn)換 import 引入

          recast[9]

          validate-npm-package-name[10]

          const?recast?=?require('recast')
          const?isPkg?=?require('validate-npm-package-name')

          function?transformModuleImports(code)?{
          ??const?ast?=?recast.parse(code)
          ??recast.types.visit(ast,?{
          ????visitImportDeclaration(path)?{
          ??????const?source?=?path.node.source.value
          ??????if?(!/^\.\/?/.test(source)?&&?isPkg(source))?{
          ????????path.node.source?=?recast.types.builders.literal(`/__modules/${source}`)
          ??????}
          ??????this.traverse(path)
          ????}
          ??})
          ??return?recast.print(ast).code
          }

          exports.transformModuleImports?=?transformModuleImports

          也就是針對 npm 包轉(zhuǎn)換。 這里就是 "/__modules/vue"

          import?Vue?from?'vue'?=>?import?Vue?from?"/__modules/vue"

          4.5 對 /__modules/ 開頭的文件進行處理

          import?Vue?from?"/__modules/vue"

          這段代碼最終返回的是讀取路徑 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件。

          if?(req.path.startsWith('/__modules/'))?{
          ??//?
          ??const?key?=?parseUrl(req).pathname
          ??const?pkg?=?req.path.replace(/^\/__modules\//,?'')

          ??let?out?=?await?tryCache(key,?false)?//?Do?not?outdate?modules
          ??if?(!out)?{
          ????out?=?(await?loadPkg(pkg)).toString()
          ????cacheData(key,?out,?false)?//?Do?not?outdate?modules
          ??}

          ??send(res,?out,?'application/javascript')
          }

          4.5.1 loadPkg 加載包(這里只支持Vue文件)

          目前只支持 Vue 文件,也就是讀取路徑 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件返回。

          //?vue-dev-server/loadPkg.js
          const?fs?=?require('fs')
          const?path?=?require('path')
          const?readFile?=?require('util').promisify(fs.readFile)

          async?function?loadPkg(pkg)?{
          ??if?(pkg?===?'vue')?{
          ????//?路徑
          ????//?vue-dev-server/node_modules/vue/dist
          ????const?dir?=?path.dirname(require.resolve('vue'))
          ????const?filepath?=?path.join(dir,?'vue.esm.browser.js')
          ????return?readFile(filepath)
          ??}
          ??else?{
          ????//?TODO
          ????//?check?if?the?package?has?a?browser?es?module?that?can?be?used
          ????//?otherwise?bundle?it?with?rollup?on?the?fly?
          ????throw?new?Error('npm?imports?support?are?not?ready?yet.')
          ??}
          }

          exports.loadPkg?=?loadPkg

          至此,我們就基本分析完畢了主文件和一些引入的文件。對主流程有個了解。

          5. 總結(jié)

          最后我們來看上文中有無 vueMiddleware 中間件的兩張圖總結(jié)一下:

          690d5b3b690a8e2a0be58861132584fc.webp沒有執(zhí)行 vueMiddleware 中間件的原始情況

          啟用中間件后,如下圖。

          40713ade6ef12f0849a90ade87fe51ad.webp執(zhí)行了 vueMiddleware 中間文件變化

          瀏覽器支持原生 type=module 模塊請求加載。vue-dev-server 對其攔截處理,返回瀏覽器支持內(nèi)容,因為無需打包構(gòu)建,所以速度很快。

          <script?type="module">
          ????import?'./main.js'
          script>

          5.1 import Vue from 'vue' 轉(zhuǎn)換

          //?vue-dev-server/test/main.js
          import?Vue?from?'vue'
          import?App?from?'./test.vue'

          new?Vue({
          ??render:?h?=>?h(App)
          }).$mount('#app')

          main.js 中的 import 語句 import Vue from 'vue' 通過 recast[11] 生成 ast 轉(zhuǎn)換成 import Vue from "/__modules/vue"而最終返回給瀏覽器的是 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js

          5.2 import App from './test.vue' 轉(zhuǎn)換

          main.js 中的引入 .vue 的文件,import App from './test.vue'則用 @vue/component-compiler[12] 轉(zhuǎn)換成瀏覽器支持的文件。

          5.3 后續(xù)還能做什么?

          鑒于文章篇幅有限,緩存 tryCache 部分目前沒有分析。簡單說就是使用了 node-lru-cache[13]最近最少使用 來做緩存的(這個算法??迹?。后續(xù)應(yīng)該會分析這個倉庫的源碼,歡迎持續(xù)關(guān)注我@若川。

          非常建議讀者朋友按照文中方法使用VSCode調(diào)試 vue-dev-server 源碼。源碼中還有很多細(xì)節(jié)文中由于篇幅有限,未全面展開講述。

          值得一提的是這個倉庫的 `master` 分支[14],是尤雨溪兩年前寫的,相對本文會比較復(fù)雜,有余力的讀者可以學(xué)習(xí)。

          也可以直接去看 `vite`[15] 源碼。

          看完本文,也許你就能發(fā)現(xiàn)其實前端能做的事情越來越多,不由感慨:前端水深不可測,唯有持續(xù)學(xué)習(xí)。

          最后歡迎加我微信 ruochuan12 交流,參與 源碼共讀 活動,大家一起學(xué)習(xí)源碼,共同進步。

          參考資料

          [1]

          vuejs組織: https://github.com/vuejs

          [2]

          vue-dev-server: https://github.com/vuejs/vue-dev-server

          [3]

          更多鏈接可以點擊閱讀原文查看


          往期推薦



          2c971e36a6ca43bd6e505d823508ebfb.webp

          解密初、中、高級程序員的進化之路(前端)


          bc1adcc2e6fb011d6336b2b94ff97b1e.webp

          程序員一定會有35歲危機嗎?


          301d19db13fc9a21804d931afbd8e31b.webp

          近 20k Star的項目說不做就不做了,但總結(jié)的內(nèi)容值得借鑒


          e888b9bba92986015bcb7449a56208fd.webp

          但凡早知道這28個網(wǎng)站,都不至于學(xué)得那么不扎實





          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          1. 點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)

          2. 歡迎加我微信「huab119」拉你進技術(shù)群,長期交流學(xué)習(xí)...

            關(guān)注公眾號「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。



          bdc2995d8d32db8dd9fc89cf33928221.webp點個在看支持我吧,轉(zhuǎn)發(fā)就更好了

          如果覺得這篇文章還不錯,來個【轉(zhuǎn)發(fā)、收藏、在看】三連吧,讓更多的人也看到~

          8630a4b84cee27996b2e89a463c2b515.webp



          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  豆花视频国产区xxx | 午夜三区| 亚洲日韩AV无码专区影院 | 男人天堂99 | 操出白浆视频 |