前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >跨端框架模板解析引擎了解一下

跨端框架模板解析引擎了解一下

作者头像
腾讯云开发TCB
发布2020-07-30 15:56:10
5580
发布2020-07-30 15:56:10
举报
文章被收录于专栏:云开发云开发

Chameleon作为一个优秀的跨多端框架,想要实现"跨多端"这个核心目标,除了工程化配置之外,DSL层面对于各个端的转化的能力也是必不可少的,接下来将为大家介绍CML是如何做DSL层面的转化的。由于这部分只涉及到模板部分,也就是template标签中的内容转化,接下来所有提到的DSL都指的是模板内容。想了解更多有关CML跨端解决方案的内容,请访问 https://cml.didi.cn

与此同时,Chameleon 作为云开发的社区合作伙伴,已经支持云开发啦!有兴趣的小伙伴可以去https://github.com/chameleon-team/cml-cloudbase(点击阅读原文查看详情)查看相关信息,这篇文章也是来自 Chameleon 开发团队的小伙伴的内容。

支持两种模板语法,方便小程序和web开发者零成本学习使用;

一套模板转化出适配多端的代码,一次开发,多端运行;

支持新端的快速扩展。

比如CML的语法,如下

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

要转化成其他端如下语法,比如原生的vue/weex的template模板语法


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

原生的小程序端的语法。

(1) 微信小程序

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

(2) 支付宝小程序

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

(3) 百度小程序

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

基本的目标

1 整体架构

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

核心点是利用 babel 转化为 ast 语法树,在对 ast 语法树解析的过程中,对于每个节点通过 tapable 控制该节点的处理方式,比如标签解析、样式语法解析、循环语句、条件语句、原生组件使用、动态组件解析等,达到适配不同端的需求,各端适配互相独立,互不影响,支持快速适配多端。

2 目录结构

.
├── 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语法

所有源码具体参考:

https://github.com/didi/chameleon/tree/master/packages/chameleon-template-parse/src

1 了解ast语法树

ast语法树就是讲模板层转化为一个js对象之后的树状结构;具体的结构可以在这里看ast-explorer,注意在设置部分勾中jsx选项。

2 ast语法树相关操作

如何转化为ast

如何对每个ast节点进行操作

如何生成转化后的代码

const ast= babylon.parse(source,{ plugins: ['jsx'] })

traverse(ast,{})

generate(ast);

3 节点具体操作

traverse(ast, {
  enter(path) {
    //这里对每一个节点,对于不同的语法,CML/VUE语法以及适配各端语法进行适配
  }
})

这里以 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语法解析,参考:

https://github.com/didi/chameleon/blob/master/packages/chameleon-template-parse/src/compile-template-cml.js

VUE语法解析,参考:

https://github.com/didi/chameleon/blob/master/packages/chameleon-template-parse/src/compile-template-vue.js

这里主要看下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

对于模板解析部分的源码学习到此结束了,欢迎对CML有兴趣的各位开发者可以和我们共建,踊跃提pr。得益于模板解析的灵活性和扩展性,Chameleon在滴滴各个业务线得以快速落地,包括滴滴顺风车、滴滴代驾、滴滴跑腿、青桔单车、滴滴团队版、滴滴企业级、桔研问卷等。

作者:「滴滴顺风车」前端资深研发工程师王梦君,Chameleon框架核心研发成员。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发CloudBase 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档