小程序模板网

记一次mpvue-loader源码探究

本人技术栈偏向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

  • script参数即为vue单文件的 <script></script> 包含部分
  • mpOptions mp相关配置参数
  • moduleId 用于模块唯一标识 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 具体做了哪些事情

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
}

这段代码中有两处比较关键的部分

  1. babel插件的转化究竟做了些什么事儿,组件的依赖是怎么样的形式?
  2. 组件的realSrc是否真的为我所需要的路径 那么首先先看一下babel插件究竟做了什么

parseComponentsDeps babel插件

首先我在看这份源码的时候对于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中解析出LTabbar和LTabbarItem真实的路径,或者关联的部分
  • 找到以后替换这里的importsMap

感觉想法并没有错,但是我花费了大量的精力去解析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里面,否则的话感觉这步操作似乎没有起到作用。(也有可能是我理解不到位)

总结

虽然这个需求并不是优先级很高的一个需求,但是从这个需求出发看源码,的确是有发现源码中的一些瑕疵(当然换我我还写不出来...所以还是得支持一下大佬的),顺带也了解了一下Babel插件实现的原理,了解了loader大概的一个实现原理,所以还是收获颇丰的。

经过了那么久时间的尝试我还是没有解决这个问题,说实话我是心有不甘的,我把这次经验整理出来也希望大家能够给我提供一些思路,或是如何解析babel插件,或是如何实现wxml的统一解析,或是还有其他的解决方案。最后希望mpvue能够越来越棒


本文地址:https://www.eyoucms.com/wxmini/doc/course/24294.html 复制链接 如需定制请联系易优客服咨询:800182392 点击咨询
QQ在线咨询