lyh2668 / blog

1 stars 0 forks source link

记一次mpvue-loader源码探究 #2

Open lyh2668 opened 6 years ago

lyh2668 commented 6 years ago

本人技术栈偏向vue一些,所以之前写小程序的时候会考虑使用wepy,但是期间发现用起来有很多问题,然后又没有什么更好的替代品,直到有mpvue的出现,让我眼前一亮,完全意义上的用vue的语法写小程序,赞👍

踩坑之旅

起因

根据官网的文档,可以很迅速的完成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

接下来看一下核心的源码部分,这里声明了一个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插件这条路走不通了,所以想着是否可以从其他路试试,然后就到了第二个关键点部分

组件的realSrc

既然在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能够越来越棒👍

EliazTray commented 6 years ago

所以mpvue在组件上,比如这个issue上,index传递的问题.mpvue可以解决吗.repeat循环时时候是否可以和vue一样呢?我还没有弄mpvue https://github.com/Tencent/wepy/issues/531

lyh2668 commented 6 years ago

@EliazTray wepy在做2.0的迭代了,应该在2.0的版本会把这些问题都修复的吧