本人技術(shù)棧偏向vue一些,所以之前寫小程序的時候會考慮使用wepy,但是期間發(fā)現(xiàn)用起來有很多問題,然后又沒有什么更好的替代品,直到有mpvue的出現(xiàn),讓我眼前一亮,完全意義上的用vue的語法寫小程序,贊:+1:
根據(jù)官網(wǎng)的文檔,可以很迅速的完成 quick start ,之后很愉快地把自己寫的tabbar組件搬了過來,首先先引入組件...
// script import { LTabbar, LTabbarItem } from '@/components/tabbar' export default { components: { LTabbar, LTabbarItem }, ... // file path components |----tabbar |----tabbar.vue |----tabbar-item.vue |----index.js ...
在vue上很常規(guī)的引入方式,然后使用...然后看效果...結(jié)果沒有任何東西被渲染出來,查看console發(fā)現(xiàn)有一條警告
有問題肯定得去解決是吧,然后就開始作死的mpvue源碼探究之旅
由于是基于實際問題出發(fā)的源碼探究,所以本質(zhì)是為了解決問題,那么就得先定位出該問題可能會產(chǎn)生的原因,并帶著這個問題去閱讀源碼。從warning可以很明確的看出,是vue組件轉(zhuǎn)化為wxml時發(fā)生的問題,而這件事應(yīng)當(dāng)是在loader的時候處理的,所以可以把問題的原因定位到 mpvue-loader ,先看一眼 mpvue-loader 的構(gòu)成
├── component-normalizer.js ├── loader.js // loader入口 ├── mp-compiler // mp script解析相關(guān)文件夾 │ ├── index.js │ ├── parse.js // components & config parse babel插件 │ ├── templates.js // vue script部分轉(zhuǎn)化成wxml的template │ └── util.js // 一些通用方法 ├── parser.js // parseComponent & generateSourceMap ├── selector.js ├── style-compiler // 樣式解析相關(guān)文件夾 ├── template-compiler // 模板解析相關(guān)文件夾 └── utils
首先找到loader.js這個文件,找到關(guān)于script的解析部分,從這里看到調(diào)用了一個 compileMPScript 方法來解析components
// line 259 // <script> output += '/* script */\n' var script = parts.script if (script) { // for mp js // 需要解析組件的 components 給 wxml 生成用 script = compileMPScript.call(this, script, mpOptions, moduleId) ...
接下來看一下mp-compiler目錄下的 compileMPScript 具體做了哪些事情
function compileMPScript (script, optioins, moduleId) { // 獲得babelrc配置 const babelrc = optioins.globalBabelrc ? optioins.globalBabelrc : path.resolve('./.babelrc') // 寫了一個parseComponentsDeps babel插件來遍歷組件從而獲取到組件的依賴(關(guān)鍵) const { metadata } = babel.transform(script.content, { extends: babelrc, plugins: [parseComponentsDeps] }) // metadata: importsMap, components const { importsMap, components: originComponents } = metadata // 處理子組件的信息 const components = {} if (originComponents) { const allP = Object.keys(originComponents).map(k => { return new Promise((resolve, reject) => { // originComponents[k] 為組件依賴的路徑,格式如下: '@/components/xxx' // 通過this.resolve得到realSrc this.resolve(this.context, originComponents[k], (err, realSrc) => { if (err) return reject(err) // 將組件名由駝峰轉(zhuǎn)化成中橫線形式 const com = covertCCVar(k) // 根據(jù)真實路徑獲取到組件名(關(guān)鍵) const comName = getCompNameBySrc(realSrc) components[com] = { src: comName, name: comName } resolve() }) }) }) Promise.all(allP) .then(res => { components.isCompleted = true }) .catch(err => { console.error(err) components.isCompleted = true }) } else { components.isCompleted = true } const fileInfo = resolveTarget(this.resourcePath, optioins.mpInfo) cacheFileInfo(this.resourcePath, fileInfo, { importsMap, components, moduleId }) return script }
這段代碼中有兩處比較關(guān)鍵的部分
首先我在看這份源碼的時候?qū)τ赽abel這塊的知識是零基礎(chǔ),所以著實廢了不少功夫。
在看babel插件之前最好可以先閱覽這些資料
接下來看一下核心的源碼部分,這里聲明了一個components訪問者:
Visitors(訪問者)
當(dāng)我們談及“進(jìn)入”一個節(jié)點,實際上是說我們在訪問它們, 之所以使用這樣的術(shù)語是因為有一個訪問者模式(visitor)的概念。.
訪問者是一個用于 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用于在一個樹狀結(jié)構(gòu)中獲取具體節(jié)點的方法
// components 的遍歷器 const componentsVisitor = { ExportDefaultDeclaration: function (path) { path.traverse(traverseComponentsVisitor) } }
traverseComponentsVisitor里面主要是對結(jié)構(gòu)的一個解析,最后獲取到importsMap,然后組裝成一個components對象并返回
// 解析 components const traverseComponentsVisitor = { Property: function (path) { // 只對類型為components的進(jìn)行操作 if (path.node.key.name !== 'components') { return } path.stop() const { metadata } = path.hub.file const { importsMap } = getImportsMap(metadata) // 找到所有的 imports const { properties } = path.node.value const components = {} properties.forEach(p => { const k = p.key.name || p.key.value const v = p.value.name || p.value.value components[k] = importsMap[v] // Example: components = { Card: '@/components/card' } }) metadata.components = components } }
對于 import Card from '@/components/card'
component就應(yīng)該為 { Card: '@/components/card' }
對于 import { LTabbar, LTabbrItem } from '@/components/tabbar'
則會被解析為 { LTabbar: '@/components/tabbar', LTabbarItem: '@/components/tabbar' }
而我們期望的顯然是 { LTabbar: '@/components/tabbar/tabbar', LTabbarItem: '@/components/tabbar/tabbar-item' }
然后我就得到這樣一個思路:
感覺想法并沒有錯,但是我花費了大量的精力去解析path最后得出一個結(jié)論... 解析不出來!!,期間嘗試了 ImportDeclaration 從中得到過最接近期望的一段path,
然而它是被寫在 LeadingComments 這個字段當(dāng)中的,除非沒有辦法的辦法,否則就不應(yīng)該通過這個字段去進(jìn)行正則匹配
然后看了一部分Rollup的Module部分的 源碼 ,感覺這個源碼寫得是真的好,非常清晰。從中的確收獲了一些啟迪,不過感覺這目前的解析而言沒有什么幫助。
既然從babel插件這條路走不通了,所以想著是否可以從其他路試試,然后就到了第二個關(guān)鍵點部分
既然在babel組件當(dāng)中的importsMap不是我真正想要的依賴文件,那究竟依賴文件怎么獲取到呢?首先我再compileMPScript里面打印了一下 this.resourcePath ,得到了以下輸出
resource: /Users/linyiheng/Code/wechat/my-project/src/App.vue resource: /Users/linyiheng/Code/wechat/my-project/src/pages/counter/index.vue resource: /Users/linyiheng/Code/wechat/my-project/src/pages/index/index.vue resource: /Users/linyiheng/Code/wechat/my-project/src/pages/logs/index.vue resource: /Users/linyiheng/Code/wechat/my-project/src/components/card.vue resource: /Users/linyiheng/Code/wechat/my-project/src/components/tabbar/tabbar.vue resource: /Users/linyiheng/Code/wechat/my-project/src/components/tabbar/tabbar-item.vu
這個其實就是文件的一個加載順序,由于LTabbar、LTabbarItem這兩個組件是在pages/index/index.vue被引入的,所以相應(yīng)的解析操作會被放在這里進(jìn)行,
但是從babel組件無法得到這兩個組件的realSrc,那么是否可以從最后加載進(jìn)來的兩個vue組件著手考慮呢,這個resourcePath顯然就是我們想要的realSrc
簡單的給traverseComponentsVisitor加上這樣的一個代碼段
// traverseComponentsVisitor if (path.node.key.name === 'component') { path.stop() const k = path.node.value.value const components = {} const { metadata } = path.hub.file components[k] = '' metadata.components = components return }
然后稍微改造一下this.resolve的處理
// 如果originComponents[k]不存在的情況下,則使用當(dāng)前的resourcePath this.resolve(this.context, originComponents[k] || this.resourcePath, (err,
感覺一切就緒了,嘗試發(fā)現(xiàn)仍然是不行的,雖然我的確得到了組件的realSrc,但是對于pages/index/index.vue而言,已經(jīng)完成了wxml模板的輸出了,
而后面進(jìn)行的主體是components/tabbar/tabbar.vue和components/tabbar/tabbar-item.vue,顯然這個時候是無法輸出wxml的。看一下生成Wxml的核心代碼
function createWxml (emitWarning, emitError, emitFile, resourcePath, rootComponent, compiled, html) { const { pageType, moduleId, components, src } = getFileInfo(resourcePath) || {} // 這兒一個黑魔法,和 webpack 約定的規(guī)范寫法有點偏差! if (!pageType || (components && !components.isCompleted)) { return setTimeout(createWxml, 20, ...arguments) } let wxmlContent = '' let wxmlSrc = '' if (rootComponent) { const componentName = getCompNameBySrc(rootComponent) wxmlContent = genPageWxml(componentName) wxmlSrc = src } else { // TODO, 這兒傳 options 進(jìn)去 // { // components: { // 'com-a': { src: '../../components/comA$hash', name: 'comA$hash' } // }, // pageType: 'component', // name: 'comA$hash', // moduleId: 'moduleId' // } // 以resourcePath為key值,從cache里面獲取到組件名,組件名+hash形式 const name = getCompNameBySrc(resourcePath) const options = { components, pageType, name, moduleId } // 將所有的配置相關(guān)傳入并生成Wxml Content wxmlContent = genComponentWxml(compiled, options, emitFile, emitError, emitWarning) // wxml的路徑 wxmlSrc = `components/${name}` } // 上拋 emitFile(`${wxmlSrc}.wxml`, wxmlContent) }
這部分代碼主要的工作其實就是根據(jù)之前獲取的組件 & 組件路徑相關(guān)信息,通過genComponentWxml生成對應(yīng)的wxml,但是由于沒辦法一次性拿到realSrc,所以我覺得這里的代碼存在著一些小問題,理想的效果應(yīng)該是完成所有的components解析以后再進(jìn)行wxml的生成,那么這件問題就迎刃而解了。其實作者用嘗試通過components.isCompleted來實現(xiàn)異步加載的問題,但是除非是把所有的compileMPScript給包含在一個Promise里面,否則的話感覺這步操作似乎沒有起到作用。(也有可能是我理解不到位)
雖然這個需求并不是優(yōu)先級很高的一個需求,但是從這個需求出發(fā)看源碼,的確是有發(fā)現(xiàn)源碼中的一些瑕疵(當(dāng)然換我我還寫不出來...所以還是得支持一下大佬的),順帶也了解了一下Babel插件實現(xiàn)的原理,了解了loader大概的一個實現(xiàn)原理,所以還是收獲頗豐的。
經(jīng)過了那么久時間的嘗試我還是沒有解決這個問題,說實話我是心有不甘的,我把這次經(jīng)驗整理出來也希望大家能夠給我提供一些思路,或是如何解析babel插件,或是如何實現(xiàn)wxml的統(tǒng)一解析,或是還有其他的解決方案。最后希望mpvue能夠越來越棒