本人技术栈偏向vue一些,所以之前写小程序的时候会考虑使用wepy,但是期间发现用起来有很多问题,然后又没有什么更好的替代品,直到有mpvue的出现,让我眼前一亮,完全意义上的用vue的语法写小程序,赞:+1:
根据官网的文档,可以很迅速的完成 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上很常规的引入方式,然后使用...然后看效果...结果没有任何东西被渲染出来,查看console发现有一条警告
有问题肯定得去解决是吧,然后就开始作死的mpvue源码探究之旅
由于是基于实际问题出发的源码探究,所以本质是为了解决问题,那么就得先定位出该问题可能会产生的原因,并带着这个问题去阅读源码。从warning可以很明确的看出,是vue组件转化为wxml时发生的问题,而这件事应当是在loader的时候处理的,所以可以把问题的原因定位到 mpvue-loader ,先看一眼 mpvue-loader 的构成
├── 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 方法来解析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插件来遍历组件从而获取到组件的依赖(关键) 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 }
这段代码中有两处比较关键的部分
首先我在看这份源码的时候对于babel这块的知识是零基础,所以着实废了不少功夫。
在看babel插件之前最好可以先阅览这些资料
接下来看一下核心的源码部分,这里声明了一个components访问者:
Visitors(访问者)
当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。.
访问者是一个用于 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里面,否则的话感觉这步操作似乎没有起到作用。(也有可能是我理解不到位)
虽然这个需求并不是优先级很高的一个需求,但是从这个需求出发看源码,的确是有发现源码中的一些瑕疵(当然换我我还写不出来...所以还是得支持一下大佬的),顺带也了解了一下Babel插件实现的原理,了解了loader大概的一个实现原理,所以还是收获颇丰的。
经过了那么久时间的尝试我还是没有解决这个问题,说实话我是心有不甘的,我把这次经验整理出来也希望大家能够给我提供一些思路,或是如何解析babel插件,或是如何实现wxml的统一解析,或是还有其他的解决方案。最后希望mpvue能够越来越棒