本人技术栈偏向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 }这段代码中有两处比较关键的部分
babel插件的转化究竟做了些什么事儿,组件的依赖是怎么样的形式?
组件的realSrc是否真的为我所需要的路径 那么首先先看一下babel插件究竟做了什么
parseComponentsDeps babel插件首先我在看这份源码的时候对于babel这块的知识是零基础,所以着实废了不少功夫。
在看babel插件之前最好可以先阅览这些资料
Babel-handbook - 这份资料里面很详细地描述了如何写一个babel插件。
Babel-types相关 - 这里会涉及到AST节点类型
接下来看一下核心的源码部分,这里声明了一个components访问者:
Visitors(访问者)
当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。.