简介

Chameleon模板解析引擎承载了模板局部的语法转化、款式适配、事件零碎适配、语法能力扩大和加强,反对CMl和类Vue两套语法,反对多端的模板解析,接下来让咱们来理解下其背地的实现原理吧

作者简介

王梦君 滴滴出行资深前端工程师 Chameleon框架外围开发成员

前言

Chameleon作为一个优良的跨多端框架,想要实现"跨多端"这个外围指标,除了工程化配置之外,DSL层面对于各个端的转化的能力也是必不可少的,接下来将为大家介绍CML是如何做DSL层面的转化的。

因为这部分只波及到模板局部,也就是template标签中的内容转化,接下来所有提到的DSL都指的是 模板内容

0 DSL转化的指标是?

  • 反对两种模板语法,不便小程序和web开发者零老本学习应用
  • 一套模板转化出适配多端的代码,一次开发,多端运行
  • 反对新端的疾速扩大

比方CML的语法

<view c-if="{{true}}">{{message}}<view>

要转化成其余端如下语法

比方原生的vue/weex的template模板语法

<div v-if="true">{{message}}<div>

原生的小程序端的语法

微信小程序

<view wx:if="{{true}}">{{message}}</view>

支付宝小程序

<view a:if="{{true}}">{{message}}</view>

百度小程序

<view s:if="true">{{message}}</view>

根本的指标是:

1 如何设计实现?

整体架构

鉴于以上的指标,CML的模板解析的整体架构如下图所示

外围点是利用 babel 转化为 ast 语法树,在对 ast 语法树解析的过程中,对于每个节点通过 tapable 管制该节点的解决形式,比方标签解析、款式语法解析、循环语句、条件语句、原生组件应用、动静组件解析等,达到适配不同端的需要,各端适配相互独立,互不影响,反对疾速适配多端。

目录构造

.├── common│   ├── cml-map.js            //各端标签替换map│   ├── process-template.js   //模板前置解决、后置解决│   └── utils.js              //专用函数├── compile-template-cml.js   //cml语法解析入口├── compile-template-vue.js   //vue语法解析入口├── index.js                  //入口文件,对vue和cml语法进行辨别└── parser    ├── index.js               //所有的parser的对立入口    ├── parse-animation-tag.js //解析动画标签    ├── parse-attribute.js     //解析标签属性    ├── parse-class.js         //解析class,反对动静class    ├── parse-condition.js     //解析条件语句    ├── parse-directive.js     //解析指令语法    ├── parse-event.js         //解析事件代理,事件传参等    ├── parse-interation.js    //解析循环语句    ├── parse-ref.js           //解析ref    ├── parse-style.js         //解析style节点,反对动静style    └── parse-vue2wx.js        //解析vue语法

所有源码具体参考

2 整体实现概览

理解ast语法树

ast语法树就是讲模板层转化为一个js对象之后的树状构造;

具体的构造能够在这里看ast-explorer,留神在设置局部勾中jsx选项

ast语法树相干操作

转化为ast(babylon)解决每个ast节点(babel-traverse)生成代码(babel-generator)
const ast = babylon.parse(source,plugins: ['jsx']})traverse(ast,{})generate(ast);

节点具体操作

traverse(ast, {  enter(path) {    // 对每一个节点,不同的语法(CML/VUE语法)以及各端(微信支付宝小程序等)语法进行适配  }})

3 具体实现细节

这里以 CML 语法为例

入口文件

const { compileTemplateForCml } = require('./compile-template-cml');//cml语法转换const { compileTemplateForVue } = require('./compile-template-vue');//vue语法转化module.exports = function(source, type, options = {lang: 'cml'}) {  /*  source:模板内容<view>xxx</view>  type:对应端,web/weex/wx/alipay/baidu 等  */  if (!source) {    return {source, usedBuildInTagMap: {}};  }  //这里对两套语法进行分流  let compileTemplateMap = {    'cml': compileTemplateForCml,    'vue': compileTemplateForVue  };  let result = compileTemplateMap[options.lang](source, type, options);  if (/;$/.test(result.source)) {     result.source = result.source.slice(0, -1);  }  return result;}

CML语法解析

VUE语法解析

这里次要看下CML语法相干源码

外围逻辑如下:

const babylon = require('babylon');const traverse = require('@babel/traverse')["default"];const generate = require('@babel/generator')["default"];const parseTemplate = require('./parser/index.js');const processTemplate = require('./common/process-template.js')const cliUtils = require('chameleon-tool-utils');exports.compileTemplateForCml = function (source, type, options) {//===>source  --> 前置解决开始  // 预处理html模板中的正文,jsx不反对,这个须要优先解决,避免解析 < > 的时候呈现问题;  source = processTemplate.preDisappearAnnotation(source);    source = processTemplate.preParseGtLt(source);  source = processTemplate.preParseDiffPlatformTag(source, type);    source = processTemplate.preParseBindAttr(source);    source = processTemplate.preParseVueEvent(source);    source = processTemplate.preParseMustache(source);  source = processTemplate.postParseLtGt(source);    source = processTemplate.preParseAnimation(source, type);  source = processTemplate.preParseAliComponent(source, type, options);//====>前置处理完毕//====>这里模板对于不同端的解决进行辨别  if (type === 'web') {    source = compileWebTemplate(source, type, options).code;  }  if (type === 'weex') {    source = compileWeexTemplate(source, type, options).code;  }  if (type === 'wx') {    source = compileWxTemplate(source, type, options).code;  }  if (type === 'qq') {    source = compileQqTemplate(source, type, options).code;  }  if (type === 'alipay') {    source = compileAliPayTemplate(source, type, options).code;  }  if (type === 'baidu') {    source = compileBaiduTemplate(source, type, options).code;  }  //====> 后置解决,解析origin-tag ==> tag  source = processTemplate.postParseOriginTag(source,type)    source = processTemplate.postParseMustache(source)    source = processTemplate.postParseUnicode(source);    source = processTemplate.transformNativeEvent(source)  return {    source,    usedBuildInTagMap: options.usedBuildInTagMap  }}

接着咱们看下 compileWxTemplate的具体实现

function compileWxTemplate(source, type, options) {  const ast = babylon.parse(source, {    plugins: ['jsx']  })  traverse(ast, {    enter(path) {      parseTemplate.parseClassStatement(path, type, options);      parseTemplate.parseTagForSlider(path, type, options);      parseTemplate.parseRefStatement(path, type, options)      parseTemplate.parseBuildTag(path, type, options) // 解析内置标签;      parseTemplate.parseTag(path, type, options);// 替换标签;      parseTemplate.parseAnimationStatement(path, type, options);      parseTemplate.afterParseTag(path, type, options);      parseTemplate.parseConditionalStatement(path, type, options);// 替换c-if c-else      parseTemplate.parseEventListener(path, type, options);      parseTemplate.parseDirectiveStatement(path, type, options);      parseTemplate.parseIterationStatement(path, type, options);      parseTemplate.parseStyleStatement(path, type, options);      // <component is="{{currentComp}}"></component>      parseTemplate.parseVue2WxStatement(path, type, options);    }  })  return generate(ast);}

在 traverse 中是对每个节点不同端进行了辨别

这里次要看下parseClass 这个实现,其余的都是一样的思路,这里贴出来的代码我只保留了小程序端的解决,其余的大家能够看源码

const { SyncHook } = require("tapable");const utils = require('../common/utils');const t = require('@babel/types');const weexMixins = require('chameleon-mixins/weex-mixins.js')let parseClass = new SyncHook(['args']);const hash = require('hash-sum');parseClass.tap('web-cml', (args) => {  let { node, type, options: {lang, isInjectBaseStyle} } = args;  if (lang === 'cml' && type === 'web') {   //这里辨别语法(cml,vue)和端(web/weex/wx/alipay/baidu)    } else {      throw new Error(`Only allow one class node in element's attribute with cml syntax`);    }  }})parseClass.tap('weex-cml', (args) => {  let { node, type, options: {lang, isInjectBaseStyle} } = args;  if (lang === 'cml' && type === 'weex') {   //这里辨别语法(cml,vue)和端(web/weex/wx/alipay/baidu)    } else {      throw new Error(`Only allow one class node in element's attribute with cml syntax`);    }  }})parseClass.tap('wx-alipay-baidu-cml', (args) => {  let { node, type, options: {lang, filePath, usingComponents, isInjectBaseStyle} } = args;  // type === 'wx' || type === 'alipay' || type === 'baidu'  if (lang === 'cml' && (['wx', 'qq', 'baidu', 'alipay'].includes(type))) {    let tagName = node.openingElement.name.name;    let attributes = node.openingElement.attributes;    let classNodes = attributes.filter((attr) => // 如果没有符合条件的classNodes则返回一个空数组      attr.name.name === 'class'    );    let isUsingComponents = (usingComponents || []).find((comp) => comp.tagName === tagName);    let extraClass = '';    if (['wx', 'qq', 'baidu'].includes(type)) {      if (isInjectBaseStyle) {        extraClass = ` cml-base cml-${tagName}`;        if (isUsingComponents) {          extraClass = ` cml-view cml-${tagName}`;        }      }    }    if (type === 'alipay') {      let randomClassName = hash(filePath);      if (isInjectBaseStyle) {        extraClass = ` cml-base cml-${tagName}`;        extraClass = `${extraClass} cml-${randomClassName}`      } else {        extraClass = `${extraClass} cml-${randomClassName}` // 不插入全局款式的时候也要插入款式隔离      }    }    if (classNodes.length === 0) {      extraClass && attributes.push(t.jsxAttribute(t.jsxIdentifier('class'), t.stringLiteral(extraClass)))    } else if (classNodes.length === 1) {      classNodes.forEach((itemNode) => {        const dealedClassNodeValue = `${itemNode.value.value} ${extraClass}`        itemNode.value.value = dealedClassNodeValue;      })    } else {      throw new Error(`Only allow one class node in element's attribute with cml syntax`);    }  }})// vue语法:class='cls1 cls2' :class="true ? 'cls1 cls2 cls3' : 'cls4 cls5 cls6'"parseClass.tap('web-vue', (args) => {  let { node, type, options: {lang, isInjectBaseStyle} } = args;  if (lang === 'vue' && type === 'web') {    //这里辨别语法(cml,vue)和端(web/weex/wx/alipay/baidu)  }})parseClass.tap('weex-vue', (args) => {  let { node, type, options: {lang, isInjectBaseStyle} } = args;  if (lang === 'vue' && type === 'weex') {    //这里辨别语法(cml,vue)和端(web/weex/wx/alipay/baidu)  }})parseClass.tap('wx-alipay-baidu-vue', (args) => {  let { node, type, options: {lang, filePath, usingComponents, isInjectBaseStyle} } = args;  // (type === 'wx' || type === 'alipay' || type === 'baidu')  if (lang === 'vue' && (['wx', 'qq', 'baidu', 'alipay'].includes(type))) {    let tagName = node.openingElement.name.name;    let attributes = node.openingElement.attributes;    let classNodes = attributes.filter((attr) => // 如果没有符合条件的classNodes则返回一个空数组      attr.name.name === 'class' || attr.name.name.name === 'class'    );    let isUsingComponents = (usingComponents || []).find((comp) => comp.tagName === tagName);    let extraClass = '';    if (['wx', 'qq', 'baidu'].includes(type)) {      if (isInjectBaseStyle) {        extraClass = ` cml-base cml-${tagName}`;        if (isUsingComponents) {          extraClass = ` cml-view cml-${tagName}`;        }      }    }    if (type === 'alipay') {      let randomClassName = hash(filePath);      if (isInjectBaseStyle) {        extraClass = ` cml-base cml-${tagName}`;        extraClass = `${extraClass} cml-${randomClassName}`      } else {        extraClass = `${extraClass} cml-${randomClassName}` // 不插入全局款式的时候也要插入款式隔离      }    }    utils.handleVUEClassNodes({classNodes, attributes, extraClass, lang, type: 'miniapp'})  }})module.exports.parseClass = parseClass;

4 结语

得益于模板解析的灵活性和扩展性,Chameleon在滴滴各个业务线得以疾速落地,包含滴滴逆风车、滴滴代驾、滴滴跑腿、青桔单车、滴滴团队版、滴滴企业级、桔研问卷等。

最初,咱们也欢送社区的开发者和咱们共建Chameleon,为开源社区继续注入生机。

github地址:https://github.com/didi/chame...