vuelovesvelte
v0.0.5
Published
VueLoveSvelte 是一个基于AST的编译器,支持把 Vue DSL组件无痛编译为 Svelte 组件。
Downloads
3
Readme
VueLoveSvelte编译器
VueLoveSvelte 是一个基于AST的编译器,支持把 Vue DSL组件无痛编译为 Svelte 组件。
关键路经
6月8日
[x] vueLoveSvelte 0.0.2 版本完成
[x] 可以编译出来 ngcc-im-core 中的所有 Vue 组件不报错 6月9日
[x] 所有模板的最外层需要加 main
[x] Vue 组件转化后,模板代码和 js 代码会存在缩进问题
[x] Vue 组件中 style 标签,转变为 Svelte 组件, 'lang = less ' 的属性不会丢失,剔除 scoped 属性
[x] Vue 组件中 style 标签可能存在多个,需要合并为一个 style 6月10日
[x] Vue 组件中 DOM 节点的 Class 属性可能存在多个,需要合并为一个 。
[x] 同时支持了各种 class 表达式的写法
[x] 目前是,手动递归遍历 ast 树进行转化,感觉不太合适,还是要换成 babel 的 translate 方法
[x] Vue 组件中引用的 'import from xx.vue' 需要替换为 ' import from xx.svelte ' 6月11日
[x] Vue 组件中 DOM 节点的 :Style 、style 属性可能存在多个,需要合并为一个 。
[x] 支持各种 :style 表达式的写法
[x] 处理 :style 、 style 的样式优先级问题 6月12日 [ ] Vue 组件内是可以使用 filters的,但是 Svelte 不支持 filter
[x] 支持 写法 this.$set('obj', newValue, 'xx') ,Vue.$set('obj', newValue, 'xx'), 甚至还支持 this.$set(this, obj, 'xx') 这种写法
[x] 目前在 vue 中的 this.arr.push(xx) 的语法,转化到 svelte 后 变成了 arr.push(xx) 的语法,并不能触发 Svelte 的更新,需要在后面再加一句 arr = arr
[x] 支持 Vue 组件中的事件修饰符, @click.prevent => on:click|preventDefault Todo
[ ] 在转化中,对 less 变量的替换,比如说移动端的 @base 变为 px (或者也可以放到 webpack less loader 里面搞) [ ] 支持 vue.$bus.$emit('sss' [ ]
为什么要做 VueLoveSvelte 编译器? 背景是 IM 项目需求变化非常迅速,一开始我们做了pc端的SDK,考虑到加载的行性能和平台兼容性,采用了 Svelte 框架。 后来,需求方反馈暂时不考虑 PC 端的场景,要优先推动移动端H5落地,于是 pc端的SDK就暂时不维护了,全力迭代移动端的需求。 等到移动端IM已经平稳线上运行后,需求迭代再次聚焦在 PC 端SDK时,整个用户侧的UI 的设计语言、后端的接口逻辑、前端的交互行为都发生了翻天覆地的变化。移动端的核心 message 组件完全可以复用在 pc 端上,但是两端采用的技术栈不一致。 存在问题 如果是接着之前旧的PC端代码改下去,就面临下面的问题:
- 修改预估耗时双周,而且是硬着头皮重写一遍相同的逻辑。
- 考虑后续新增的迭代需求,要移动端、pc端两套技术栈耗费double的人力
- 后端API 版本不好做隔离 上面的问题严重制约了敏捷开发的效率,只能靠堆人力来解决。那么,有没有一个更好的办法去解决这个问题呢? 解决思路 考虑到IM移动端和PC端创建会话、收发会话、渲染消息、问题卡片等等等……大部分逻辑和样式基本相同,这块如果抽成共用的组件,通过我们的 VueLoveSvelte 编译器分别编译为 Vue 代码和 Svelte 代码的核心npm包,频繁的需求迭代只需要开发一次,各端同时升级npm包,极大的释放了生产效率。 方案的好处
- 不熟悉 Svelte 语法的同学,只要会写 VUE 组件,就支持编译为 Svelte 组件,降低了学习成本
- 减少移动端和PC端写同样的逻辑,降低维护成本和开发成本
- PC端还享受了 Svelte 无运行时框架的优势
- 为团队沉淀AST编译器的探索实践 可行性分析 Vue 和 Svelte 都是基于 Temlate 模板语法的框架,支持的语法特性基本对齐。通过 AST 解析后可以实现相互转化。 通俗来讲,Vue的语法特性更多,而 Svelte 的特性比较少,如果一个特性在 Svelte 没有对应的实现,就会在编译的时候报错提示。
| 这个轮子好用嘛?会不会翻车? 目标
- 落地 vueLoveSvelte 编辑器,领先业界标准,一套标准Vue的组件,可 100% 编译为Svelte
- 产物高可读性,方便进行二次开发 原理分析 针对下图中的 Parser、Traversal、Transform、Generator,下面会分别介绍: | Parser const babylon = require('babylon');
const code = function plus(a, b) {
return a + b;
}
;
const ast = babylon.parse(code, {
sourceType: 'module'
});
traversal
遍历节点,筛选遍历
const traversal = require('babel-traverse').default;
traversal(ast, { Identifier: function (path) { console.log(path.node.name); } });
进出节点: traversal(ast, { Identifier: { enter: function (path) { console.log(path.node.name, 'enter'); }, exit: function (path) { console.log(path.node.name, 'exit\n'); } } });
局部遍历: traversal(ast, { FunctionDeclaration: function (path) { if (path.node.id.name !== 'plus') return; path.traverse({ Identifier: { enter: function (path) { console.log(path.node.name, 'enter'); }, exit: function (path) { console.log(path.node.name, 'exit\n'); } } }); } });
transform traversal(ast, { FunctionDeclaration: function (path) { path.traverse({ Identifier: { enter: function (path) { if (types.isIdentifier(path.node, { name: "a" })) { // 节点替换 path.replaceWith(types.Identifier('x'), path.node); } }, exit: function (path) { console.log(path.node.name); } } }); } });
其实原理不是特别高深莫测,就是精细活,需要考虑到各种各样的情况。总之要做一个完整的语法解释器需要的是十分的细心与耐心 (如果是只匹配到 80% 其实还好)
Props Vue 官网里面介绍的 props 支持哪些东西呢?
- type:可以是下列原生构造函数中的一种:String、Number、Boolean、Array、Object、Date、Function、Symbol、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。
- default:any 为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。
- required:Boolean定义该 prop 是否是必填项。在非生产环境中,如果这个值为 truthy 且该 prop 没有被传入的,则一个控制台警告将会被抛出。
- validator:Function自定义验证函数会将该 prop 的值作为唯一的参数代入。 举一个例子: props: { // 检测类型 height: Number, // 检测类型 + 其他验证 age: { type: Number, default: 0, required: true, validator: function (value) { return value >= 0 } } }
Svelte 的 props 相对比较简单 export let baz = ''; // 最常见的props export let baz = 'default' // 默认的 props
监听数组变更 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化,所以会改写数组的原生的方法来触发更新,比如说 push 方法。 Svelte 没有改写数组的原生的方法,触发更新的逻辑,就是看数组有没有被重新赋值(也就是 = 号) 所以,在 vue 中的写法是: this.arr.push(1) 转换到 Svelte 中的写法是: arr.push(1) arr = arr // 这一行代码,对 arr 重新赋值触发了更新
this.$set 或者 Vue.$set Vue 中支持下面的写法: this.$set(this.obj, 'c', 'ccc') 转化为 Svelte 为: obj.c = 'ccc' 要小心,Vue 也支持这种写法: this.$set(this, 'obj', 'c') 转为为 Svelte 为: obj = 'c' 所以,这块的逻辑很简单,直接无脑匹配+ 转换一波带走: |
ngccIMcoreMessage组件 和 ngccIMCore 覆盖的范围 ngccIMcore组件就是跨技术栈,平台无关的组件,是一个纯粹的和 IM 建立会话、消息相关的组件 将会被 vueLoveSvelte 编译核心编译成 不包括
eventBus Svelte 中没有对应的 eventBus 的特性,解决思路是自己实现一个 eventBus
移动端 h5 和 pc 端的兼容行 根据运行环境做判断
一些组件库 比如说,跑马灯组件用的是bytedesign的mobile-vue Svelte 也有对应的跑马灯组件
Style 样式转变
@base
的 rem 的布局需要变为 1px ,目前的想法是,
vue-template-compiler Vue 2.0 底层的语法解析引擎,npm 包的地址是: https://www.npmjs.com/package/vue-template-compiler 有两个核心的API:
- compiler.parseComponent() 抽出三部分代码: template, js,style
- compiler.compile(template) 把vue 的template 抽成 ast 语法树
在线的 vue template 编译为 ast 树的的网站: https://magiccwl.github.io/vue-compiler-online/
Javascript 语法对应关系 Compute
Template 语法对应关系 并不是修改 template 生成的 ast 树,而是深度遍历 ast 树的过程中,生成一个 output 数组,每遍历一个 节点,就从节点上摘取一些 vue 指令的信息,转变为 Svelte 语法的字符串 push 到 output 数组中 v-html Vue 更新元素的 innerHTML。注意:内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译
对应的 Svelte 的模板语法应该是: {@html expression}
不管是 v-html 还是 {@html } 都不会对 xss 做过滤,因此在使用的过程中需要小心
v-for
对应的 Svelte 的模板语法应该是: {#each list as item, index}
Class 绑定 vue 中的 class 的绑定写法比较多,分为下面三种情况去转变
- 变量写法 :class="classObject" => class="{ classObject }"
- 对象写法 :class={ active: isActive } => class="{ isActive ? 'active' : '' }" :class={ active: isActive, 'text-danger': hasError } => class="{ isActive ? 'active' : '' } {hasError? 'text-danger' : ''}"
- 数组写法 :class=[activeClass, errorClass] => class="{ activeClass } { errorClass }"
:class=[isActive ? activeClass : '', errorClass] => class="{isActive ? activeClass : ''} {errorClass}"
:class=[{ active: isActive }, errorClass] => class="{isActive ? "active" : ''} {errorClass}"
这里有一个不太好处理的地方。 就是:class="[activeClass, errorClass]"` 这样的字符串,是可以被 ast 解析的
| 只要可以解析成 ast,我们就可以愉快的替换节点。 但是 :class = "{ activeClass: isActive, errorClass: isError }" 竟然没有办法被 ast 解析的
| 所以,暂时这种情况,就先使用正则匹配来解决吧。 反正不管怎么样,目前不管多么复杂 class 表达式,都可以正常的转化了: ||
Style 绑定 Style 绑定比上面 Class 绑定更难处理😭,因为 Svelte 对于 style 绑定的语法更不灵活,只支持这种类型的动态绑定,即动态的地方是放到花括号里面 style="color: {setColor}; font-size: {setFontSize + 'px'}"
而 vue style 的语法则非常的灵活,把一个灵活的东西,转变为不灵活的东西,难度是很大的(因为要匹配灵活的各种各样的情况)。可以在 vue 中分别使用下面的语法:
变量写法 :style="styleObject"
对象写法 :style="{ color: activeColor, 'font-size': fontSize + 'px' }"
数组写法 :style="[baseStyles, overridingStyles]"
同时,vue里面还有一个潜规则,允许 :style 和 style 同时存在同一个标签上面,如果 :style 和 style 存在同名的属性名,:style 的优先级是更高的,如下图所示: | 所以合并到 svelte style 中时,可以把 :style 中对应的样式插到 style 的后面,因为在 css 的规则中,越靠后的样式,优先级更高 |
事件修饰符 Vue 中的事件修饰符有
- .stop
- .prevent
- .capture
- .self
- .once
- .passive
Svelte 中的事件修饰符有:
- stopPropagation
- preventDefault
- capture
- self
- once
- passvie 嘻嘻嘻,这个就比较好转化了,只需要在 template 中匹配到对应的情况,然后替换成 svelte 中对应的用法即可
过滤器 Filters Vue 中有一个叫过滤器的功能,但是 Svelte 中并没有对应的过滤器 filter 的能力。 目前的处理方式是,把 Vue 中的 filters 转化为 Svelte 中的函数(其实我感觉 vue 中的 filters 其实就是一种特殊的函数)
{{ textContent | filter1 | filter2 | filter3}}
上面 filter 的调用的顺序是 filter1 => filter2 => filter3, 那么转化为 Svelte 语法之后,应该是 : { filter3(filter2(fitler1(textContent))) }
@babel/parser babelParser 把js解析为 ast tree,然后我们对 tree 进行修改 const script = babelParser.parse(sfc.script.content, { sourceType: 'module', }); 可以在线 ast 转换 javascript 的网站 https://astexplorer.net/
遍历 javascript 节点并不太容易,并没有一个 children 的属性让我们一路遍历下来,还好babel 给我们提供了一个映射表,可以根据不同的节点类型找到不同的子节点 babel/types/visitor-keys.json https://github.com/babel/babel/blob/v5.1.11/src/babel/types/visitor-keys.json 映射表大概长这个样子: |
QA 写的 Vue 的 template 语法不规范怎么办?vueLoveSvelte编译会怎么处理? 在vueLoveSvelte 开始转换之前,会先通过 vue compiler 编译看是否是标准的vue语法,如果vue语法上有报错,则抛出报错 |
个人写的编辑器不太放心啊,怎么保证能否覆盖100%的语法? 编译器本质很简单,就是匹配到的规则足够覆盖所有的语法,不同的开发者会有不同习惯,要匹配到所有的语法情况需要大量的测试组件,这里就有二八效应,绝大多数的 Vue 语法是可以被覆盖的,如果想覆盖100%的情况,需要不断的反馈和完善,大家感兴趣也可以共建 打算用 vue real world 来做测试用例
中长期 plan
- 在线转化 Vue组件 为 Svelte 组件在线网站 😊
- 完美编译饿了么 Element vue 的组件库为 Svelte 组件库 😄
- 完美编译公司的 bytedesign vue 的组件库为 Svelte 组件库 😆