本人技術棧偏向vue一些,因此以前寫小程序的時候會考慮使用wepy,可是期間發現用起來有不少問題,而後又沒有什麼更好的替代品,直到有mpvue的出現,讓我眼前一亮,徹底意義上的用vue的語法寫小程序,贊👍html
根據官網的文檔,能夠很迅速的完成quick start
,以後很愉快地把本身寫的tabbar組件搬了過來,首先先引入組件...vue
// script import { LTabbar, LTabbarItem } from '@/components/tabbar' export default { components: { LTabbar, LTabbarItem }, ... // file path components |----tabbar |----tabbar.vue |----tabbar-item.vue |----index.js ... 複製代碼
在vue上很常規的引入方式,而後使用...而後看效果...結果沒有任何東西被渲染出來,查看console發現有一條警告 node
有問題確定得去解決是吧,而後就開始做死的mpvue源碼探究之旅因爲是基於實際問題出發的源碼探究,因此本質是爲了解決問題,那麼就得先定位出該問題可能會產生的緣由,並帶着這個問題去閱讀源碼。從warning能夠很明確的看出,是vue組件轉化爲wxml時發生的問題,而這件事應當是在loader的時候處理的,因此能夠把問題的緣由定位到mpvue-loader
,先看一眼mpvue-loader
的構成webpack
├── component-normalizer.js
├── loader.js // loader入口
├── mp-compiler // mp script解析相關文件夾
│ ├── index.js
│ ├── parse.js // components & config parse babel插件
│ ├── templates.js // vue script部分轉化成wxml的template
│ └── util.js // 一些通用方法
├── parser.js // parseComponent & generateSourceMap
├── selector.js
├── style-compiler // 樣式解析相關文件夾
├── template-compiler // 模板解析相關文件夾
└── utils
複製代碼
首先找到loader.js這個文件,找到關於script的解析部分,從這裏看到調用了一個compileMPScript
方法來解析componentsgit
<script></script>
包含部分moduleId = 'data-v-' + genId(filePath, context, options.hashKey)
// 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
具體作了哪些事情github
function compileMPScript (script, optioins, moduleId) { // 得到babelrc配置 const babelrc = optioins.globalBabelrc ? optioins.globalBabelrc : path.resolve('./.babelrc') // 寫了一個parseComponentsDeps babel插件來遍歷組件從而獲取到組件的依賴(關鍵) 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) // 將組件名由駝峯轉化成中橫線形式 const com = covertCCVar(k) // 根據真實路徑獲取到組件名(關鍵) 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 } 複製代碼
這段代碼中有兩處比較關鍵的部分web
首先我在看這份源碼的時候對於babel這塊的知識是零基礎,因此着實廢了很多功夫。
在看babel插件以前最好能夠先閱覽這些資料小程序
接下來看一下核心的源碼部分,這裏聲明瞭一個components訪問者:bash
Visitors(訪問者)
當咱們談及「進入」一個節點,其實是說咱們在訪問它們, 之因此使用這樣的術語是由於有一個訪問者模式(visitor)的概念。.babel訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法
// components 的遍歷器 const componentsVisitor = { ExportDefaultDeclaration: function (path) { path.traverse(traverseComponentsVisitor) } } 複製代碼
traverseComponentsVisitor裏面主要是對結構的一個解析,最後獲取到importsMap,而後組裝成一個components對象並返回
// 解析 components const traverseComponentsVisitor = { Property: function (path) { // 只對類型爲components的進行操做 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就應該爲{ 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最後得出一個結論...解析不出來!!,期間嘗試了ImportDeclaration
從中獲得過最接近指望的一段path,然而它是被寫在LeadingComments
這個字段當中的,除非沒有辦法的辦法,不然就不該該經過這個字段去進行正則匹配
而後看了一部分Rollup的Module部分的源碼,感受這個源碼寫得是真的好,很是清晰。從中的確收穫了一些啓迪,不過感受這目前的解析而言沒有什麼幫助。
既然從babel插件這條路走不通了,因此想着是否能夠從其餘路試試,而後就到了第二個關鍵點部分
既然在babel組件當中的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被引入的,因此相應的解析操做會被放在這裏進行,可是從babel組件沒法獲得這兩個組件的realSrc,那麼是否能夠從最後加載進來的兩個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]不存在的狀況下,則使用當前的resourcePath this.resolve(this.context, originComponents[k] || this.resourcePath, (err, 複製代碼
感受一切就緒了,嘗試發現仍然是不行的,雖然個人確獲得了組件的realSrc,可是對於pages/index/index.vue而言,已經完成了wxml模板的輸出了,然後面進行的主體是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 約定的規範寫法有點誤差! 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 進去 // { // 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 } // 將全部的配置相關傳入並生成Wxml Content wxmlContent = genComponentWxml(compiled, options, emitFile, emitError, emitWarning) // wxml的路徑 wxmlSrc = `components/${name}` } // 上拋 emitFile(`${wxmlSrc}.wxml`, wxmlContent) } 複製代碼
這部分代碼主要的工做其實就是根據以前獲取的組件 & 組件路徑相關信息,經過genComponentWxml生成對應的wxml,可是因爲沒辦法一次性拿到realSrc,因此我以爲這裏的代碼存在着一些小問題,理想的效果應該是完成全部的components解析之後再進行wxml的生成,那麼這件問題就迎刃而解了。其實做者用嘗試經過components.isCompleted來實現異步加載的問題,可是除非是把全部的compileMPScript給包含在一個Promise裏面,不然的話感受這步操做彷佛沒有起到做用。(也有多是我理解不到位)
雖然這個需求並非優先級很高的一個需求
// 其實只要把 import { LTabbar, LTabbarItem } from '@/components/tabbar' 拆分爲如下兩段就能夠了 import LTabbar from '@/components/tabbar' import LTabbarItem from '@/components/tabbar-item' 複製代碼
可是從這個需求出發看源碼,的確是有發現源碼中的一些瑕疵(固然換我我還寫不出來...因此仍是得支持一下大佬的),順帶也瞭解了一下Babel插件實現的原理,瞭解了loader大概的一個實現原理,因此仍是收穫頗豐的。 通過了那麼久時間的嘗試我仍是沒有解決這個問題,說實話我是心有不甘的,我把此次經驗整理出來也但願你們可以給我提供一些思路,或是如何解析babel插件,或是如何實現wxml的統一解析,或是還有其餘的解決方案。最後但願mpvue可以愈來愈棒👍