前言

这篇文章的主要是对taro/taro-tarnsformer-wx进行源码解析,对于想要了解Taro或者了解babel的人希望看了能得到一定的启发。

由于我文笔实在太烂,所以整篇文章都是以阅读笔记的形式展示,希望能对想了解taro编译但是不太了解babel的人提供一个学习途径。 如果有已经充分了解babel编译的的大佬可以直接去看我fork的taro,在里面我写上了全部注释希望能够帮助到你~~

准备

在开始讲解前你需要准备一下事情:

  • 从github上clone下来taro的代码 Taro / Taro-transformer-wx注释版
  • 最起码要知道babel是啥
  • 打开 astexplorer.net/ ,这是个ast在线转换的网站,如果有不理解的地方直接粘贴代码看结构
  • 打开网易云播放一首好听的音乐(文章可能有点枯燥,听歌能缓解心情)

开始

目录

首先我们进入目录结构能看到下面这堆东西

  • taro-tarnsformer-wx/src

    • plugins/
    • adapter.ts
    • class.ts
    • constant.ts
    • create-html-element.ts
    • eslint.ts
    • index.ts
    • interface.d.ts
    • jsx.ts
    • lifecycle.ts
    • loop-component.ts
    • options.ts
    • plugins.ts
    • render.ts
    • utils.ts

然而我们真正主要关注的只有三个文件

  • taro-tarnsformer-wx/src

    • index.ts
    • class.ts
    • render.ts

index.ts

我们先整体来分析下index.ts

export default function transform (options: Options): TransformResult {// ... -> 设置一些参数// 如果是 typescript 代码使用 ts.transpile 转换为 esnext 代码const code = options.isTyped? ts.transpile(options.code, {jsx: ts.JsxEmit.Preserve, // 保留jsx语法target: ts.ScriptTarget.ESNext,importHelpers: true,noEmitHelpers: true}): options.code// babel的第一步,将 js 代码转换成 ast 语法树const ast = parse(code, {parserOpts: {sourceType: 'module',plugins: [ ]},plugins: []}).ast as t.File//... -> 定义一些变量// babel的第二步,遍历语法树,并对语法树做出修改traverse(ast, {//... -> **转换第一步的核心**});//... -> 一些简单的处理/*** **转换第二步的核心*** 对 ast 做了更进一步的处理* 同时生产了模板文件,也就是 wxml*/result = new Transformer(mainClass, options.sourcePath, componentProperies).result// 通过generate将语法树转换成js,这就是最终小程序用的js代码result.code = generate(ast).coderesult.ast = astresult.compressedTemplate = result.templateresult.template = prettyPrint(result.template, {max_char: 0})result.imageSrcs = Array.from(imageSource)return result
}
复制代码

转换第一步核心

先简单了解下用到的配置项的意义,有点多,咱一个一个讲

traverse(ast, {// 模板字符串TemplateLiteral (path) {},// 类的宣言ClassDeclaration (path) {},// 类表达式ClassExpression (path) {},// 类的函数ClassMethod (path) {},// if语句IfStatement (path) {},// 调用表达式CallExpression (path) {},// JSX元素JSXElement (path) {},// JSX开合元素JSXOpeningElement (path) {},// JSX属性JSXAttribute (path) {},// 导入宣言ImportDeclaration (path) {},
})
复制代码

我们从代码由上往下的方式一个一个来看

首先看对导入语句的处理

ImportDeclaration (path) {const source = path.node.source.valueif (importSources.has(source)) {throw codeFrameError(path.node, '无法在同一文件重复 import 相同的包。')} else {importSources.add(source)}const names: string[] = []// TARO_PACKAGE_NAME = '@tarojs/taro'if (source === TARO_PACKAGE_NAME) {/*** 如果文件中有import xx from '@tarojs/taro'* 会自动帮你多导入一些辅助函数* import xx, {*  internal_safe_get,*  internal_get_orignal,*  internal_inline_style,*  getElementById* } from '@tarojs/taro'* */isImportTaro = truepath.node.specifiers.push(t.importSpecifier(t.identifier(INTERNAL_SAFE_GET), t.identifier(INTERNAL_SAFE_GET)),t.importSpecifier(t.identifier(INTERNAL_GET_ORIGNAL), t.identifier(INTERNAL_GET_ORIGNAL)),t.importSpecifier(t.identifier(INTERNAL_INLINE_STYLE), t.identifier(INTERNAL_INLINE_STYLE)),t.importSpecifier(t.identifier(GEL_ELEMENT_BY_ID), t.identifier(GEL_ELEMENT_BY_ID)))}// REDUX_PACKAGE_NAME = '@tarojs/redux'// MOBX_PACKAGE_NAME = '@tarojs/mobx'if (source === REDUX_PACKAGE_NAME || source === MOBX_PACKAGE_NAME) {path.node.specifiers.forEach((s, index, specs) => {if (s.local.name === 'Provider') {/*** 找到 import { Provider } from 'xxx'* 替换成* import { setStore } from 'xxx'*/// 删除引入参数Providerspecs.splice(index, 1)// 添加引入参数setStorespecs.push(t.importSpecifier(t.identifier('setStore'), t.identifier('setStore')))}})}/*** 1.遍历当前import语句收集所有导入的变量名* 2.将 import { Component } from '@tarojs/taro'* 替换成 import { __BaseComponent } from '@tarojs/taro'*/path.traverse({ImportDefaultSpecifier (path) {const name = path.node.local.nameDEFAULT_Component_SET.has(name) || names.push(name)},ImportSpecifier (path) {const name = path.node.imported.nameDEFAULT_Component_SET.has(name) || names.push(name)if (source === TARO_PACKAGE_NAME && name === 'Component') {path.node.local = t.identifier('__BaseComponent')}}})componentSourceMap.set(source, names)
}
复制代码

接着看对类的定义处理

ClassDeclaration (path) {// 将找到的类的节点存起来,其实这里可以看出,taro默认一个文件只有一个 classmainClass = path/*** 下面这里的目的其实就是当你引用了自定义的组件并且继承了他,这是taro需要把你继承的这个源码也进行编译*/const superClass = path.node.superClass// 先判断这个类必须是有继承的 也就是 class A extends XXX {}if (t.isIdentifier(superClass)) {const binding = path.scope.getBinding(superClass.name)// 再判断这个被继承的XXX在之前已经声明过if (binding && binding.kind === 'module') {const bindingPath = binding.path.parentPath// 第三步判断这个声明语句是导入宣言if (bindingPath.isImportDeclaration()) {/*** 此时匹配到的代码是这样* import XXX from 'xxx';* class A extends XXX {}*/const source = bindingPath.node.sourcetry {// 这里 p = 'xxx.js' || 'xxx.tsx'const p = fs.existsSync(source.value + '.js') ? source.value + '.js' : source.value + '.tsx'const code = fs.readFileSync(p, 'utf8')// 如果xxx.js存在就对它也再进行一次 transform 转换componentProperies = transform({isRoot: false,isApp: false,code,isTyped: true,sourcePath: source.value,outputPath: source.value}).componentProperies} catch (error) {// 文件 xxx.js || xxx.tsx 不存在}}}}
},
ClassExpression (path) {mainClass = path as any
},
ClassMethod (path) {if (t.isIdentifier(path.node.key) && path.node.key.name === 'render') {// 找到render函数节点存起来renderMethod = path}
},
复制代码

再来看看对if语句和函数调用的处理

// 调用表达式
// func() this.func() arr.map(()={}) 只要有函数调用都算
CallExpression (path) {const callee = path.get('callee')// isContainJSXElement 这里是遍历的 path 的所有子节点看里面有没有JSXElement,如果有啥都不处理if (isContainJSXElement(path)) {return}// 被调用者的引用是成员表达式// this.func() arr.map()if (callee.isReferencedMemberExpression()) {/*** 找到被调用者的成员中最靠前的一个标识符* 如:* this.func() => id 就是 this* arr.map() => id 就是 arr*/const id = findFirstIdentifierFromMemberExpression(callee.node)/*** getIdsFromMemberProps就是找到调用者的所有成员的 name* a.b.c.d()  => calleeIds = ['a','b','c','d'];*/const calleeIds = getIdsFromMemberProps(callee.node)if (t.isIdentifier(id) && id.name.startsWith('on') && Adapters.alipay !== Adapter.type) {// 到了这一步被调用者的代码应该是 onXXXX.xxx() || onXXXX.xxx.xxx();/*** 解释下buildFullPathThisPropsRef,大概如下* 如果:* const onXXX = this.props.xxx;* onXXX.call(this, arg1, arg2);* --- 编译后,此时 fullPath 有值* this.props.xxx();* * const onXXX = other;* onXXX.call(this, arg1, arg2);* --- 编译后,此时 fullPath 为空* onXXX();*/const fullPath = buildFullPathThisPropsRef(id, calleeIds, path)if (fullPath) {path.replaceWith(t.callExpression(fullPath,path.node.arguments))}}}// 被调用者的引用是标识符// func()if (callee.isReferencedIdentifier()) {const id = callee.nodeconst ids = [id.name]if (t.isIdentifier(id) && id.name.startsWith('on')) {// 到了这一步被调用者的代码应该是 onXXXX();// 之后的处理和上面一样const fullPath = buildFullPathThisPropsRef(id, ids, path)if (fullPath) {path.replaceWith(t.callExpression(fullPath,path.node.arguments))}}}
},
复制代码

好了,接下来是重头戏,对JSX的处理

JSXElement (path) {/*** 下面这块代码是有bug的,不太重要,可以忽略* 本意可见 => https://github.com/NervJS/taro/issues/550* * 实际结果如下:* let a; a = [1,2,3].map(v => <View>{v}</View>);* --- 编译后* let a = <View>{v}</View>;* --- 期望结果* let a = [1,2,3].map(v => <View>{v}</View>);*/const assignment = path.findParent(p => p.isAssignmentExpression())if (assignment && assignment.isAssignmentExpression()) {const left = assignment.node.leftif (t.isIdentifier(left)) {const binding = assignment.scope.getBinding(left.name)if (binding && binding.scope === assignment.scope) {if (binding.path.isVariableDeclarator()) {// 错误的点其实就是不应该将path.node 直接赋值给 binding.path.node.init// 改成 binding.path.node.init = assignment.node.right 即可binding.path.node.init = path.nodeassignment.remove()} else {throw codeFrameError(path.node, '同一个作用域的JSX 变量延时赋值没有意义。详见:https://github.com/NervJS/taro/issues/550')}}}}/*** 如果是在 switch case 中的JSX会把 switch case切换成 if else* switch (v){ * case 1: {*  any = <View1/>* }* case 2: { *  <View2/>*  break;* }* default: {*  return <View3/>* }* }* --- 编译后* if(v === 1) { any = <View1/> }* else if(v === 2) { <View2/> }* else { return <View3/> }*/const switchStatement = path.findParent(p => p.isSwitchStatement())if (switchStatement && switchStatement.isSwitchStatement()) {const { discriminant, cases } = switchStatement.nodeconst ifStatement = cases.map((Case, index) => {const [ consequent ] = Case.consequent/*** 校验switch case 必须包含 {}* 所以不支持以下写法* case 1:* case 2: *  return <View/>*/if (!t.isBlockStatement(consequent)) {throw codeFrameError(switchStatement.node, '含有 JSX 的 switch case 语句必须每种情况都用花括号 `{}` 包裹结果')}const block = t.blockStatement(consequent.body.filter(b => !t.isBreakStatement(b)))if (index !== cases.length - 1 && t.isNullLiteral(Case.test)) {throw codeFrameError(Case, '含有 JSX 的 switch case 语句只有最后一个 case 才能是 default')}const test = Case.test === null ? t.nullLiteral() : t.binaryExpression('===', discriminant, Case.test)return { block, test }}).reduceRight((ifStatement, item) => {if (t.isNullLiteral(item.test)) {ifStatement.alternate = item.blockreturn ifStatement}const newStatement = t.ifStatement(item.test,item.block,t.isBooleanLiteral(ifStatement.test, { value: false })? ifStatement.alternate: ifStatement)return newStatement}, t.ifStatement(t.booleanLiteral(false), t.blockStatement([])))switchStatement.insertAfter(ifStatement)switchStatement.remove()}// 对for/for in/for of 进行禁用const isForStatement = (p) => p && (p.isForStatement() || p.isForInStatement() || p.isForOfStatement())const forStatement = path.findParent(isForStatement)if (isForStatement(forStatement)) {throw codeFrameError(forStatement.node, '不行使用 for 循环操作 JSX 元素,详情:https://github.com/NervJS/taro/blob/master/packages/eslint-plugin-taro/docs/manipulate-jsx-as-array.md')}/*** 处理 Array.prototype.map* 将 arr.map((v)=> v) 变成 arr.map((v)=> { return v; })*/const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p))if (loopCallExpr && loopCallExpr.isCallExpression()) {const [ func ] = loopCallExpr.node.arguments// 必须是箭头函数 并且没有 {}if (t.isArrowFunctionExpression(func) && !t.isBlockStatement(func.body)) {func.body = t.blockStatement([t.returnStatement(func.body)])}}
},/*** JSX开合元素* <View></View> -> JSXOpeningElement = <View>, JSXClosingElement = </View>* <View/> -> JSXOpeningElement = <View>, JSXClosingElement = null*/
JSXOpeningElement (path) {const { name } = path.node.name as t.JSXIdentifier/*** 找到<Provider />组件和store属性* 将组件改为View, 移除所有属性 * * 这里很尬,taro只修改了 OpeningElement,没有处理CloseElement* 所以转换 <Provider store={store} >xxxx</Provider> => <View>xxxx</Provider>* 但是因为最后会转成wxml所以也没影响*/if (name === 'Provider') {const modules = path.scope.getAllBindings('module')const providerBinding = Object.values(modules).some((m: Binding) => m.identifier.name === 'Provider')if (providerBinding) {path.node.name = t.jSXIdentifier('View')// 从<Provider store={myStore} >上找属性store,并且拿到传给store的值的名字const store = path.node.attributes.find(attr => attr.name.name === 'store')if (store && t.isJSXExpressionContainer(store.value) && t.isIdentifier(store.value.expression)) {// storeName = 'myStore'storeName = store.value.expression.name}path.node.attributes = []}}// IMAGE_COMPONENTS = ['Image', 'CoverImage']// 收集所有图片组件的src值,注意: 只能是字符串if (IMAGE_COMPONENTS.has(name)) {for (const attr of path.node.attributes) {if (attr.name.name === 'src') {if (t.isStringLiteral(attr.value)) {imageSource.add(attr.value.value)} else if (t.isJSXExpressionContainer(attr.value)) {if (t.isStringLiteral(attr.value.expression)) {imageSource.add(attr.value.expression.value)}}}}}
},// 遍历JSX的属性 也就是 <View a={1} b={any} /> 上的 a={1} b={any}
JSXAttribute (path) {const { name, value } = path.node// 过滤 name非 jsx关键字 或者 value 是 null、字符串、JSXElement// 即 any={null} any='123' any={<View />}if (!t.isJSXIdentifier(name) || value === null || t.isStringLiteral(value) || t.isJSXElement(value)) {return}const expr = value.expression as anyconst exprPath = path.get('value.expression')// 这里是向父级找类的名称 class Index {} -> classDeclName = 'Index';// 然后根据classDeclName来判断是否已经转换过const classDecl = path.findParent(p => p.isClassDeclaration())const classDeclName = classDecl && classDecl.isClassDeclaration() && safeGet(classDecl, 'node.id.name', '')let isConverted = falseif (classDeclName) {isConverted = classDeclName === '_C' || classDeclName.endsWith('Tmpl')}/*** 处理内连样式* 将style={{ color: 'red' }} => style={internal_inline_style({ color: 'red' })}* 这里taro在全局上注入了一个函数 internal_inline_style*/// 判断是style属性,且未转换过,正常来说我们写的代码都是未转换的,加这个逻辑应该是给taro内部一写组件使用if (!t.isBinaryExpression(expr, { operator: '+' }) && !t.isLiteral(expr) && name.name === 'style' && !isConverted) {const jsxID = path.findParent(p => p.isJSXOpeningElement()).get('name')if (jsxID && jsxID.isJSXIdentifier() && DEFAULT_Component_SET.has(jsxID.node.name)) {exprPath.replaceWith(t.callExpression(t.identifier(INTERNAL_INLINE_STYLE), [expr]))}}/*** 处理 onXxx 事件属性*/if (name.name.startsWith('on')) {/*** 这里判断 onClick属性 他的值 是[引用表达式]* 即 onClick={myAdd}* * 将 const myAdd = this.props.add; <Button onClick={myAdd} />* 转换成 <Button onClick={this.props.add} />*/if (exprPath.isReferencedIdentifier()) {const ids = [expr.name]const fullPath = buildFullPathThisPropsRef(expr, ids, path)if (fullPath) {exprPath.replaceWith(fullPath)}}/*** 这里判断 onClick属性 他的值 是[引用成员表达式]* 即 onClick={a.add}* * 下面这里的意思应该跟上面差不多* 将 const a = this.props; <Button onClick={a.add} />* 转换成 <Button onClick={this.props.add} />* * 然而 const a = { add: this.props.add }; <Button onClick={a.add} />* 这种他就GG了*/if (exprPath.isReferencedMemberExpression()) {const id = findFirstIdentifierFromMemberExpression(expr)const ids = getIdsFromMemberProps(expr)if (t.isIdentifier(id)) {const fullPath = buildFullPathThisPropsRef(id, ids, path)if (fullPath) {exprPath.replaceWith(fullPath)}}}// @TODO: bind 的处理待定}
},
复制代码

细心的同学肯定发现漏掉了 TemplateLiteral 没讲,其实这里就是对模板语法做处理,可以忽略掉

看到这里Taro编译的第一步就讲解完成了~~

如果你看懂了那你对babel编译已经有了一个初步的了解,接下来的内容可以加快节奏了~

转换第二步核心

还记的是第二步是啥么~帮你回忆一下~~

import { Transformer } from './class'
/*** 分析下参数* mainClass 第一步收集到的类的节点* options.sourcePath 代码文件的根路径(外面传进来的)* componentProperies 不重要,具体看 第一步的 ClassDeclaration */
result = new Transformer(mainClass, options.sourcePath, componentProperies).result
复制代码

然后我们就来到了要将的第二个文件class.ts

惊不惊险,刺不刺激,已经讲完1/3了呢!!!

国际惯例,先看构造函数

非常简单,一堆赋值咱不关心,然后调用了this.compile(),所以玄机应该就在compile中

constructor (path: NodePath<t.ClassDeclaration>,sourcePath: string,componentProperies: string[]
) {this.classPath = paththis.sourcePath = sourcePaththis.moduleNames = Object.keys(path.scope.getAllBindings('module'))this.componentProperies = new Set(componentProperies)this.compile()
}
复制代码

compile长成下面这样,大概描述下各个函数的功能

compile () {// 遍历,各种遍历,在遍历的过程中做了一堆有一堆的修改this.traverse()// 把遍历过程中收集到的自定义组件存到this.result.components,跟编译没啥关系可忽略this.setComponents()// 处理构造函数将constructor改成_constructorthis.resetConstructor()// 收集到更多使用的propsthis.findMoreProps()// 对ref进行处理this.handleRefs()// 大家最关心的一步,将jsx 编译成wxmlthis.parseRender()this.result.componentProperies = [...this.componentProperies]
}
复制代码

关于this.traverse,这里我不是很想讲,因为太多了,有兴趣的可以去看我加上注释的代码,这里我会省略掉很多代码

traverse () {const self = thisself.classPath.traverse({JSXOpeningElement: (path) => {// ...// 是不是在map循环中const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p))const componentName = jsx.name.name// 找到ref属性const refAttr = findJSXAttrByName(attrs, 'ref')if (!refAttr) { return }// 找到id属性const idAttr = findJSXAttrByName(attrs, 'id')// 随机生成idlet id: string = createRandomLetters(5)let idExpr: t.Expressionif (!idAttr) {/*** 这里是处理如果tag上没有 id 属性时自动添加上 id=randomStr* 如果在map循环中 id = randomStr + index*/   if (loopCallExpr && loopCallExpr.isCallExpression()) {// ...} else {// ...}} else {// 有id属性,找到id属性的值或者表达式const idValue = idAttr.valueif (t.isStringLiteral(idValue)) {// ...} else if (t.isJSXExpressionContainer(idValue)) {// ...}}// 如果ref属性是字符串且不在循环中,则添加StringRef// ref="myRef"if (t.isStringLiteral(refAttr.value)) {// ...}// 如果ref属性是jsx表达式 // ref={any}if (t.isJSXExpressionContainer(refAttr.value)) {const expr = refAttr.value.expressionif (t.isStringLiteral(expr)) {// ref={"myRef"}// 将ref收集起来this.createStringRef(componentName, id, expr.value)} else if (t.isArrowFunctionExpression(expr) || t.isMemberExpression(expr)) {// ref={this.xxx} / ref={()=> {}}const type = DEFAULT_Component_SET.has(componentName) ? 'dom' : 'component'// 根据条件收集函数类型的refif (loopCallExpr) {this.loopRefs.set(/*...*/)} else {this.refs.push({/*...*/})}} else {throw codeFrameError(refAttr, 'ref 仅支持传入字符串、匿名箭头函数和 class 中已声明的函数')}}// 删除ref属性for (const [index, attr] of attrs.entries()) {if (attr === refAttr) {attrs.splice(index, 1)}}},ClassMethod (path) {const node = path.nodeif (t.isIdentifier(node.key)) {const name = node.key.nameself.methods.set(name, path)// 处理render函数// 处理吧if(xxx) return; 换成 if(xxx) return null;if (name === 'render') {self.renderMethod = pathpath.traverse({ReturnStatement (returnPath) {const arg = returnPath.node.argumentconst ifStem = returnPath.findParent(p => p.isIfStatement())if (ifStem && ifStem.isIfStatement() && arg === null) {const consequent = ifStem.get('consequent')if (consequent.isBlockStatement() && consequent.node.body.includes(returnPath.node)) {returnPath.get('argument').replaceWith(t.nullLiteral())}}}})}// 处理constructor函数// 收集所有初始化的stateif (name === 'constructor') {path.traverse({AssignmentExpression (p) {if (t.isMemberExpression(p.node.left) &&t.isThisExpression(p.node.left.object) &&t.isIdentifier(p.node.left.property) &&p.node.left.property.name === 'state' &&t.isObjectExpression(p.node.right)) {const properties = p.node.right.propertiesproperties.forEach(p => {if (t.isObjectProperty(p) && t.isIdentifier(p.key)) {self.initState.add(p.key.name)}})}}})}}},IfStatement (path) {// 把if语句中包含jsx语法的复杂判断逻辑用匿名 state 储存// if(func()) { return <View> }const test = path.get('test') as NodePath<t.Expression>const consequent = path.get('consequent')if (isContainJSXElement(consequent) && hasComplexExpression(test)) {const scope = self.renderMethod && self.renderMethod.scope || path.scopegenerateAnonymousState(scope, test, self.jsxReferencedIdentifiers, true)}},ClassProperty (path) {const { key: { name }, value } = path.nodeif (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) {self.methods.set(name, path)}// 收集所有初始化的stateif (name === 'state' && t.isObjectExpression(value)) {value.properties.forEach(p => {if (t.isObjectProperty(p)) {if (t.isIdentifier(p.key)) {self.initState.add(p.key.name)}}})}},JSXExpressionContainer (path) {path.traverse({MemberExpression (path) {// 遍历所有的<JSX attr={any} /> 找到使用的state或者 props 添加到 usedState 中const sibling = path.getSibling('property')if (path.get('object').isThisExpression() &&(path.get('property').isIdentifier({ name: 'props' }) || path.get('property').isIdentifier({ name: 'state' })) &&sibling.isIdentifier()) {const attr = path.findParent(p => p.isJSXAttribute()) as NodePath<t.JSXAttribute>const isFunctionProp = attr && typeof attr.node.name.name === 'string' && attr.node.name.name.startsWith('on')// 判断是不是方法,默认on开头就认为是if (!isFunctionProp) {self.usedState.add(sibling.node.name)}}}})const expression = path.get('expression') as NodePath<t.Expression>const scope = self.renderMethod && self.renderMethod.scope || path.scopeconst calleeExpr = expression.get('callee')const parentPath = path.parentPath// 使用了复杂表达式,并且不是bind函数if (hasComplexExpression(expression) &&!(calleeExpr &&calleeExpr.isMemberExpression() &&calleeExpr.get('object').isMemberExpression() &&calleeExpr.get('property').isIdentifier({ name: 'bind' })) // is not bind) {generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)} else {// 将所有key={any} 生成匿名变量if (parentPath.isJSXAttribute()) {if (!(expression.isMemberExpression() || expression.isIdentifier()) && parentPath.node.name.name === 'key') {generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)}}}const attr = path.findParent(p => p.isJSXAttribute()) as NodePath<t.JSXAttribute>if (!attr) returnconst key = attr.node.nameconst value = attr.node.valueif (!t.isJSXIdentifier(key)) {return}// 处理所有onXxx的事件属性,生成匿名函数if (t.isJSXIdentifier(key) && key.name.startsWith('on') && t.isJSXExpressionContainer(value)) {const expr = value.expressionif (t.isCallExpression(expr) && t.isMemberExpression(expr.callee) && t.isIdentifier(expr.callee.property, { name: 'bind' })) {self.buildAnonymousFunc(attr, expr, true)} else if (t.isMemberExpression(expr)) {self.buildAnonymousFunc(attr, expr as any, false)} else {throw codeFrameError(path.node, '组件事件传参只能在类作用域下的确切引用(this.handleXX || this.props.handleXX),或使用 bind。')}}const jsx = path.findParent(p => p.isJSXOpeningElement()) as NodePath<t.JSXOpeningElement>// 不在jsx语法中if (!jsx) returnconst jsxName = jsx.node.name// 不在jsxName不是标识符if (!t.isJSXIdentifier(jsxName)) return// 是jsx元素if (expression.isJSXElement()) return// 在收集到的组件中 || 关键字 || 成员表达式 || 文本 || 逻辑表达式 || 条件表达式 || on开头 || 调用表达式if (DEFAULT_Component_SET.has(jsxName.name) || expression.isIdentifier() || expression.isMemberExpression() || expression.isLiteral() || expression.isLogicalExpression() || expression.isConditionalExpression() || key.name.startsWith('on') || expression.isCallExpression()) return// 上面加了一堆判断,如果都通过了就抽离生成匿名变量,应该是兜底方案generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)},JSXElement (path) {const id = path.node.openingElement.name// 收集所有导入并且使用过的自定义组件if (t.isJSXIdentifier(id) &&!DEFAULT_Component_SET.has(id.name) &&self.moduleNames.indexOf(id.name) !== -1) {const name = id.nameconst binding = self.classPath.scope.getBinding(name)if (binding && t.isImportDeclaration(binding.path.parent)) {const sourcePath = binding.path.parent.source.value// import Custom from './xxx';if (binding.path.isImportDefaultSpecifier()) {self.customComponents.set(name, {sourcePath,type: 'default'})} else {// import { Custom } from './xxx';self.customComponents.set(name, {sourcePath,type: 'pattern'})}}}},MemberExpression: (path) => {const object = path.get('object')const property = path.get('property')if (!(object.isThisExpression() && property.isIdentifier({ name: 'props' }))) {return}const parentPath = path.parentPath// 处理所有this.props.xxxif (parentPath.isMemberExpression()) {const siblingProp = parentPath.get('property')if (siblingProp.isIdentifier()) {const name = siblingProp.node.nameif (name === 'children') {// 将所有的 <View>{this.props.children}</View> -> <slot />;// 注意只能是{this.props.children} // 不能是 const { children } = this.props; <View>{children}</View>// 不能是 const p = this.props; <View>{p.children}</View>parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [], true), t.jSXClosingElement(t.jSXIdentifier('slot')), [], true))} else if (/^render[A-Z]/.test(name)) {// 将所有的 <View>{this.props.renderAbc}</View> -> <slot name="abc" />;// 其他限制同上const slotName = getSlotName(name)parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [t.jSXAttribute(t.jSXIdentifier('name'), t.stringLiteral(slotName))], true), t.jSXClosingElement(t.jSXIdentifier('slot')), []))// 给class上添加静态属性 static multipleSlots = truethis.setMultipleSlots()} else {// 收集其他使用到的props名称self.componentProperies.add(siblingProp.node.name)}}} else if (parentPath.isVariableDeclarator()) {// 处理对this.props的结构语法, 收集所有用到的props// const { a, b, c, ...rest } = this.props;const siblingId = parentPath.get('id')if (siblingId.isObjectPattern()) {const properties = siblingId.node.propertiesfor (const prop of properties) {if (t.isRestProperty(prop)) {throw codeFrameError(prop.loc, 'this.props 不支持使用 rest property 语法,请把每一个 prop 都单独列出来')} else if (t.isIdentifier(prop.key)) {self.componentProperies.add(prop.key.name)}}}}},CallExpression (path) {const node = path.nodeconst callee = node.callee// 处理所有a.b.c(); 形式调用的函数/*** processThisPropsFnMemberProperties** 将this.props.func(a,b,c); -> this.__triggerPropsFn('func', [a,b,c]);* 将this.props.obj.func(a,b,c); -> this.__triggerPropsFn('obj.func', [a,b,c]);*/if (t.isMemberExpression(callee) && t.isMemberExpression(callee.object)) {const property = callee.propertyif (t.isIdentifier(property)) {if (property.name.startsWith('on')) {self.componentProperies.add(`__fn_${property.name}`)processThisPropsFnMemberProperties(callee, path, node.arguments, false)} else if (property.name === 'call' || property.name === 'apply') {self.componentProperies.add(`__fn_${property.name}`)processThisPropsFnMemberProperties(callee.object, path, node.arguments, true)}}}}})
}
复制代码
resetConstructor () {const body = this.classPath.node.body.body// 如果未定义 constructor 则主动创建一个if (!this.methods.has('constructor')) {const ctor = buildConstructor()body.unshift(ctor)}if (process.env.NODE_ENV === 'test') {return}for (const method of body) {if (t.isClassMethod(method) && method.kind === 'constructor') {// 找到 constructor 改成 _constructor// 找到 super(xxx) 改成 super._constructor(xxx);method.kind = 'method'method.key = t.identifier('_constructor')if (t.isBlockStatement(method.body)) {for (const statement of method.body.body) {if (t.isExpressionStatement(statement)) {const expr = statement.expressionif (t.isCallExpression(expr) && (t.isIdentifier(expr.callee, { name: 'super' }) || t.isSuper(expr.callee))) {expr.callee = t.memberExpression(t.identifier('super'), t.identifier('_constructor'))}}}}}}
}
复制代码
findMoreProps () {// 这个方法的目的是收集到更多使用的props// 因为前面处理了的只有 constructor 和 this.props.xxx const { xxx } = this.props;// // 下面遍历所有的带有使用props的声明周期,找到有使用的props属性并收集/*** 在能生命周期里收集的props如下:* shouldComponentUpdate(props) {*  console.log(props.arg1);*  const { arg2, arg3 } = props;*  const p = props;*  console.log(p.arg4)*  const { arg5 } = p;* }* shouldComponentUpdate({ arg6, arg7 }) {* }* * 最终能收集到的 [arg1,arg2,arg3,arg6,arg7];* [arg4, arg5] 不能收集到*/// 第一个参数是 props 的生命周期const lifeCycles = new Set([// 'constructor','componentDidUpdate','shouldComponentUpdate','getDerivedStateFromProps','getSnapshotBeforeUpdate','componentWillReceiveProps','componentWillUpdate'])const properties = new Set<string>()// 这里的methods是遍历ast的时候收集到的this.methods.forEach((method, name) => {if (!lifeCycles.has(name)) {return}const node = method.nodelet propsName: null | string = nullif (t.isClassMethod(node)) {propsName = this.handleLifecyclePropParam(node.params[0], properties)} else if (t.isArrowFunctionExpression(node.value) || t.isFunctionExpression(node.value)) {propsName = this.handleLifecyclePropParam(node.value.params[0], properties)}if (propsName === null) {return}// 如果找到了propsName说明有类似 shouldComponentUpdate(props) {}// 遍历方法astmethod.traverse({MemberExpression (path) {if (!path.isReferencedMemberExpression()) {return}// 进行成员表达式遍历 a.b.c 找到所有 propsName.xxx并收集const { object, property } = path.nodeif (t.isIdentifier(object, { name: propsName }) && t.isIdentifier(property)) {properties.add(property.name)}},VariableDeclarator (path) {// 进行变量定义遍历 找到所有 const { name, age } = propsName;const { id, init } = path.nodeif (t.isObjectPattern(id) && t.isIdentifier(init, { name: propsName })) {for (const prop of id.properties) {if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {properties.add(prop.key.name)}}}}})properties.forEach((value) => {this.componentProperies.add(value)})})
}
复制代码
handleRefs () {/*** this.refs 是在 this.traverse遍历时收集到的,然后将收集到的refs挂到class的属性上* 变成这样* class Index {*   ...,*   $$refs = [{*    type: "dom",*    id: "随机字符串",*    refName: "",*    fn: this.saveRef*   }, {*    type: "component",*    id: "gMFQv",*    refName: "title",*    fn: null*   }]* }*/const objExpr = this.refs.map(ref => {return t.objectExpression([t.objectProperty(t.identifier('type'),t.stringLiteral(ref.type)),t.objectProperty(t.identifier('id'),t.stringLiteral(ref.id)),t.objectProperty(t.identifier('refName'),t.stringLiteral(ref.refName || '')),t.objectProperty(t.identifier('fn'),ref.fn ? ref.fn : t.nullLiteral())])})this.classPath.node.body.body.push(t.classProperty(t.identifier('$$refs'),t.arrayExpression(objExpr)))
}
复制代码

终于来到了最后一部分,对模板进行生成。这里引入了一个新模块RenderParser

import { RenderParser } from './render'parseRender () {if (this.renderMethod) {this.result.template = this.result.template+ new RenderParser(this.renderMethod,this.methods,this.initState,this.jsxReferencedIdentifiers,this.usedState,this.loopStateName,this.customComponentNames,this.customComponentData,this.componentProperies,this.loopRefs).outputTemplate} else {throw codeFrameError(this.classPath.node.loc, '没有定义 render 方法')}
}
复制代码

老规矩,先看构造函数

constructor (renderPath: NodePath<t.ClassMethod>,methods: ClassMethodsMap,initState: Set<string>,referencedIdentifiers: Set<t.Identifier>,usedState: Set<string>,loopStateName: Map<NodePath<t.CallExpression>, string>,customComponentNames: Set<string>,customComponentData: Array<t.ObjectProperty>,componentProperies: Set<string>,loopRefs: Map<t.JSXElement, LoopRef>
) {this.renderPath = renderPaththis.methods = methodsthis.initState = initStatethis.referencedIdentifiers = referencedIdentifiersthis.loopStateName = loopStateNamethis.usedState = usedStatethis.customComponentNames = customComponentNamesthis.customComponentData = customComponentDatathis.componentProperies = componentProperiesthis.loopRefs = loopRefsconst renderBody = renderPath.get('body')this.renderScope = renderBody.scopeconst [, error] = renderPath.node.body.body.filter(s => t.isReturnStatement(s))if (error) {throw codeFrameError(error.loc, 'render 函数顶级作用域暂时只支持一个 return')}// 上面定义一堆变量// 遍历整个render函数进行一些处理renderBody.traverse(this.loopComponentVisitor)// 遍历整个render函数进行一些处理this.handleLoopComponents()// 再遍历整个render函数进行一些处理renderBody.traverse(this.visitors)// 解析ast生成wxml字符串设置到template上this.setOutputTemplate()// 清除所有jsx语法this.removeJSXStatement()// 生成$usedStatethis.setUsedState()this.setPendingState()// 生成$$eventsthis.setCustomEvent()// 将 render 函数改成 _createDatathis.createData()// 生成propertiesthis.setProperies()
}
复制代码

从结构上可以看出,重点在 this.setOutputTemplate() 之前,之后的几个函数都是在最后阶段为了满足运行时的一些需求给注入一些属性参数

而前三个函数和我们之前所讲的内容基本都在做同样的事,遍历ast、修改ast,因为文章篇幅问题,虽然比较重要但我就不讲了,如果你看懂了前面那这里你直接去看代码吧~比看我讲来会得更快。

有了上面的结果后,我们就能很轻松的处理wxml的生成了

setOutputTemplate () {this.outputTemplate = parseJSXElement(this.finalReturnElement)
}// 根据配置生成 xml字符串 <div attr1="123" >value</div>
export const createHTMLElement = (options: Options) => {
}// 将jsx数组转成成wxml字符串
function parseJSXChildren (children: (t.JSXElement | t.JSXText | t.JSXExpressionContainer)[]
): string {return children.filter(child => {// 过滤掉所有空字符串节点return !(t.isJSXText(child) && child.value.trim() === '')}).reduce((str, child) => {// 如果是字符串,直接拼接if (t.isJSXText(child)) {return str + child.value.trim()}// 如果是JSX,通过parseJSXElement转换成字符串if (t.isJSXElement(child)) {return str + parseJSXElement(child)}// 如果是JSX表达式容器 {xxx}if (t.isJSXExpressionContainer(child)) {// 容器的内容是JSX,通过parseJSXElement转换成字符串if (t.isJSXElement(child.expression)) {return str + parseJSXElement(child.expression)}// 其他情况转换成源代码拼接上return str + `{${decodeUnicode(generate(child, {quotes: 'single',jsonCompatibleStrings: true}).code)// 去除this. this.state 这些,因为在小程序中wxml中不需要从this开始取值.replace(/(this\.props\.)|(this\.state\.)/g, '').replace(/(props\.)|(state\.)/g, '').replace(/this\./g, '')}}`}return str}, '')
}export function parseJSXElement (element: t.JSXElement): string {const children = element.childrenconst { attributes, name } = element.openingElementconst TRIGGER_OBSERER = Adapter.type === Adapters.swan ? 'privateTriggerObserer' : '__triggerObserer'// <View.A /> 即使 JSX 成员表达式if (t.isJSXMemberExpression(name)) {throw codeFrameError(name.loc, '暂不支持 JSX 成员表达式')}const componentName = name.nameconst isDefaultComponent = DEFAULT_Component_SET.has(componentName)const componentSpecialProps = SPECIAL_COMPONENT_PROPS.get(componentName)let hasElseAttr = falseattributes.forEach((a, index) => {if (a.name.name === Adapter.else && !['block', 'Block'].includes(componentName) && !isDefaultComponent) {hasElseAttr = trueattributes.splice(index, 1)}})if (hasElseAttr) {// 如果有 esle 条件且没有用block包裹起来就包上一层<block></block>return createHTMLElement({name: 'block',attributes: {[Adapter.else]: true},value: parseJSXChildren([element])})}let attributesTrans = {}if (attributes.length) {// 处理JSX的属性attributesTrans = attributes.reduce((obj, attr) => {if (t.isJSXSpreadAttribute(attr)) {throw codeFrameError(attr.loc, 'JSX 参数暂不支持 ...spread 表达式')}let name = attr.name.nameif (DEFAULT_Component_SET.has(componentName)) {// 将className改成classif (name === 'className') {name = 'class'}}let value: string | boolean = truelet attrValue = attr.valueif (typeof name === 'string') {const isAlipayEvent = Adapter.type === Adapters.alipay && /(^on[A-Z_])|(^catch[A-Z_])/.test(name)if (t.isStringLiteral(attrValue)) {// 如果值是字符串,直接保留value = attrValue.value} else if (t.isJSXExpressionContainer(attrValue)) {// 如果值是jsx表达式容器let isBindEvent =(name.startsWith('bind') && name !== 'bind') || (name.startsWith('catch') && name !== 'catch')// 将表达式转成代码,然后一堆正则处理let code = decodeUnicode(generate(attrValue.expression, {quotes: 'single',concise: true}).code).replace(/"/g, "'").replace(/(this\.props\.)|(this\.state\.)/g, '').replace(/this\./g, '')if (Adapters.swan === Adapter.type &&code !== 'true' &&code !== 'false' &&swanSpecialAttrs[componentName] &&swanSpecialAttrs[componentName].includes(name)) {value = `{= ${code} =}`} else {if (Adapter.key === name) {const splitCode = code.split('.')if (splitCode.length > 1) {value = splitCode.slice(1).join('.')} else {value = code}} else {// 如果是事件就直接用 `code` 否则当字符串处理 `{{code}}`value = isBindEvent || isAlipayEvent ? code : `{{${code}}}`}}if (Adapter.type === Adapters.swan && name === Adapter.for) {value = code}if (t.isStringLiteral(attrValue.expression)) {// 如果本身就是字符串就直接使用value = attrValue.expression.value}} else if (attrValue === null && name !== Adapter.else) {// 处理隐式写法 <View disabled /> => <View disabled="{{true}}">value = `{{true}}`}if (THIRD_PARTY_COMPONENTS.has(componentName) && /^bind/.test(name) && name.includes('-')) {name = name.replace(/^bind/, 'bind:')}if ((componentName === 'Input' || componentName === 'input') && name === 'maxLength') {// 单独处理input maxLengthobj['maxlength'] = value} else if (componentSpecialProps && componentSpecialProps.has(name) ||name.startsWith('__fn_') ||isAlipayEvent) {obj[name] = value} else {// 将属性名从驼峰改成`-`obj[isDefaultComponent && !name.includes('-') && !name.includes(':') ? kebabCase(name) : name] = value}}if (!isDefaultComponent && !specialComponentName.includes(componentName)) {obj[TRIGGER_OBSERER] = '{{ _triggerObserer }}'}return obj}, {})} else if (!isDefaultComponent && !specialComponentName.includes(componentName)) {attributesTrans[TRIGGER_OBSERER] = '{{ _triggerObserer }}'}return createHTMLElement({// 将驼峰改成 -name: kebabCase(componentName),attributes: attributesTrans,value: parseJSXChildren(children)})
}复制代码

所以其实可以看出来,最终生成wxml没有多么高大上的代码,也是通过递归加字符串拼接将代码一点点拼上,不过之所以最后能这么轻松其实主要是因为在ast语法转换的过程中将太多太多的问题都抹平了,将代码变成了一个比较容易转换的状态。

写在最后的话

第一次写文章,很烂非常烂,比我平时自己在心里喷的那些烂文章还要烂。

无奈。挣扎了很久还是发了,毕竟凡事都要有个开始。

2019~ []~( ̄▽ ̄)~*干杯!

转载于:https://juejin.im/post/5c6cdd2d6fb9a049f154c671

【阅读笔记】Taro转小程序编译源码解析相关推荐

  1. 在线阅读试听视频小程序模板源码

    简介: 这是一款多功能带试听功能的小程序模板源码,可以在线边读边听文章,还可以在线试听音乐,收看视频等等,大家可以看演示图. 网盘下载地址: http://kekewangLuo.net/a3hxdZ ...

  2. 2021最新闪拍竞拍网源码【内有小程序及源码搭建教程】

    2021最新闪拍竞拍网源码[内有小程序及源码搭建教程] 功能比较多,源码完整, 后台测试地址 http://ipai.ym929.cn/web/ admin admin 前段测试地址 http://i ...

  3. 微信小程序实例源码大全demo下载

    怎么本地测试微信小程序实例源码 1.下载源码 2.打开微信开发者工具 3.添加项目->选择本项目目录->编译执行 微信小程序实例源码大全 微信小程序游戏类demo:识色:从相似颜色中挑选不 ...

  4. 微信小程序github源码大全下载

    微信小程序实例源码大全下载 微信小应用示例代码(phodal/weapp-quick) 源码链接:https://github.com/phodal/weapp-quick 微信小应用地图定位demo ...

  5. 小程序源码整理最新的学习小程序的源码清单包含前后台

    微信小程序疯狂猜成语小程序源码UI美观 知识答题类小程序源码 200多套微信小程序源码带后台+教程+源码 校园小情书小程序前端+后端源码 微信版flappybird小游戏源码下载,经典像素鸟小游戏源码 ...

  6. AI美图工具小程序项目源码

    这是AI美图工具小程序项目源码,使用了ColorUI结合了nui-app制作的一款AI美图,修图小程序前端界面,可编译微信小程序/QQ小程序/支付宝小程序/h5等 本文只代表个人观点! 原文链接:ht ...

  7. 微信小程序案例源码-cocos creator跨平台开发小游戏案例

    前言 本人一直想学习游戏开发,以前做过Android游戏开发,但电脑坏了,所有源码都没有了,后面也就没有开发了,近来下班之余又想开发游戏了.开发游戏之前,需要考虑开发平台及语言,以前基于Android ...

  8. java计算机毕业设计社区微服务平台小程序服务器端源码+系统+数据库+lw文档

    java计算机毕业设计社区微服务平台小程序服务器端源码+系统+数据库+lw文档 java计算机毕业设计社区微服务平台小程序服务器端源码+系统+数据库+lw文档 本源码技术栈: 项目架构:B/S架构 开 ...

  9. (转)微信小程序实例源码大全

    https://www.cnblogs.com/tuyile006/p/6268961.html 微信小程序实例源码大全下载 微信小应用示例代码(phodal/weapp-quick) 源码链接:ht ...

最新文章

  1. c#利用宏定义调试代码
  2. springboot2整合mysql5_SpringBoot2整合SSM框架详解
  3. 【C语言重点难点精讲】C语言中的重要符号
  4. 海淘第一单——KindleTouch
  5. 7.卷2(进程间通信)---互斥锁和条件变量
  6. 拓端tecdat:matlab数据可视化交通流量分析天气条件、共享单车时间序列数据
  7. 滚石杂志选出500张最强专辑
  8. lavas自动创建目录及说明
  9. c++自动抢购_软件神器—全网口罩监控抢购
  10. 安卓性能优化全面总结(友盟性能优化大赛获奖作品)
  11. java处理生信数据,生信Java软件安装
  12. Idea中使用maven 与pom文件讲解
  13. roadrunner中文文档(四)app服务器
  14. 虚拟机:centos在主机和虚拟机之间传输文件
  15. html5手机触屏效果,html5手机触屏touch事件的详细介绍
  16. 星之轨迹服务器维护,10月17日例行维护公告
  17. docker容器日常管理(四)
  18. 华为LOGO是什么意思?看完任正非的解释,再看网友的解释,笑出声
  19. 804.唯一摩尔斯密码词
  20. 深海打捞计划——VapourSynth学习笔记(一)

热门文章

  1. SpeechRecognition离线语音识别
  2. Win7/Win10双系统安装方法图文教程
  3. python描述性统计离散型变量_数据的描述性统计(Python3 实现)
  4. chrome清除https状态
  5. Navicat Premium 使用技巧
  6. 全国高校通信与信息系统专业排名
  7. Liunx下Nginx安装配置
  8. Ubuntu18.04添加自启动脚本
  9. 【threeJS】纹理贴图
  10. IDEA中创建启动Vue项目--搭建vue项目