简介
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…