是 jsv 而不是 jsx:这可能是你想要的魔改vue3代码编写方式

12,594

vue官方推荐的是单文件 .vue 编写组件,简单易学但没有jsx灵活,很多知名的UI库最终选择了使用jsx,例如:ant-desing-vue, vant。但是在 vue 中使用 jsx 也存在一些问题。比如:大部分 vue 指令无法使用或者很难使用。在 vue3 中专门开了个 issue 讨论解决方案,至今没有优雅的解决方案。 jsv 而不是 jsx 可能是更好的解决方案。

一、先看看 .vue 与 .jsx 的比较

1. 变量作用域比较

  • 1.1 .vue 中无法使用当前作用域变量,必须return后才能使用
// Scope.vue
<template>
	<div>{{ state.count }}</div>
	<button @click="handleClick">点击加1</button>
</template>

<script>
import { reactive } from "vue";
export default {
	setup() {
		let state = reactive({ count: 0 });
		function handleClick() {
			state.count++;
		}
		// 在模板中无法直接使用setup中的变量,必须return
		return { state, handleClick };
	},
};
</script>
  • 1.2 .jsx 中可以直接使用当前作用域的变量
// Scope.jsx
import { reactive } from "vue";

export default {
	setup() {
		let state = reactive({ 
			count: 0 
		});

		function handleClick() {
			state.count++;
		}
		
		// 可以在渲染函数中直接使用当前作用域的变量
		return ()=>(
			<div>
				<div >{ state.count }</div>
				<button onClick={handleClick}>点击加1</button>
			</div>
			)
	},
};

2. 灵活性比较

  • 2.1 .vue 中一个文件只能写一个组件
// NoMulti.vue
<template>
	<Title :title="state.title" />
</template>

<script>
import { reactive } from "vue";
// 不能在一个.vue中写多个组件,必须将 Title 写在另外一个文件中
import Title from "./Title.vue";
export default {
	components: { Title },
	setup() {
		let state = reactive({ title: "jsv-compiler" });

		return { state };
	},
};
</script>
// Title.vue
<template>
  <h1>hello {{title}}</h1>
</template>

<script>
export default {
  props: {
    title: String
  }
}
</script>
  • 2.2. .jsx 中可以写多个组件
// Multi.jsx
import { reactive } from "vue";

export default {
	setup() {
		let state = reactive({ 
			title: 'jsv-compiler'
		});

		// 一个文件中可以写多个组件
		let Title = ()=><h1>hello {state.title}</h1>
		
		return ()=>(
			<div>
				<Title />
			</div>
			)
	},
};

3. 指令比较

  • 3.1. .vue 原生支持优雅的指令写法
<A v-model:argument.modifer="val" />
  • 3.2. .jsx 本身不支持指令 社区有多种指令的书写方式,各种千奇百怪,为此还开了issue讨论,至今仍然没有友好的解决方式。因此可以暂时认为 .jsx 不支持指令或支持的不好. github.com/vuejs/jsx/i…
<A v-model={[val, 'argument', ['modifier']]} />

4. 运行时性能比较

// 编译前
<div>
  <div>静态节点</div>
  <div >{{state.count }}</div>
</div>
// 编译后
const { createVNode: _createVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createBlock: _createBlock } = Vue

const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "静态节点", -1 /* HOISTED */)
// render 函数 上面的代码只会执行一次
// 每次重新渲染都再次执行 render 函数
// 1. 关于 _hoisted_1 静态节点变量提升,作用是再次执行render函数时,不用重新创建节点,直接从内存中读取;
// 2. 关于 _openBlock, _createBlock,作用是在 dom-diff 时,不比较静态节点,只比较可变节点;
// 3. 关于 patchProps,作用是在 dom-diff 时,只比较 text 的变化,不比较其他任何属性的变化;
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1, // 静态节点变量提升
    _createVNode("div", null, _toDisplayString(_ctx.state.count), 1 /* TEXT */) // 此处的 1 用于 patchProps 的标识
  ]))
}
// Check the console for the AST
// 编译前
<div>
	<div >static node</div>	
  	<div >{ state.count }</div>
</div>
// 编译后 (使用 vite 工具编译)
jsx(
  "div",
  null /* @__PURE__ */,
  jsx("div", null, "static node") /* @__PURE__ */,
  jsx("div", null, state.count)
)

5. 生态比较

  • 5.1. 当前知名的UI库比如 ant-desing-vue,vant 内部采用了 jsx
  • 5.2. 但对外提供的组件仍然是 vue的模板语法。对于使用者如果要用 jsx,还得自己把 vue 改成 jsx,代价高昂呀!

二、这才是正文:是 jsv 不是 jsx !

经过比较我们发现 .jsx 灵活但对指令支持不友好也没有运行时性能优化。 .vue很好的支持指令和运行时性能优化,但不灵活。

那有没有一种解决方案,既有 jsx 的灵活性,又有原生支持vue指令和运行时高性能,在 js代码 中直接使用 vue 模板语法?就像这样: 文件后缀为 .jsv

// 文件命名为 App.jsv
import { reactive } from "vue";

export default {
	name: "App",
	setup() {
		let state = reactive({ 
			title: 'jsv-compiler',
			count: 0 
		});

		function handleClick() {
			state.count++;
		}
        // 一个文件中写多个组件。此处的 Title 是 组件。
		let Title = <template><h1>hello {{state.title}}</h1></template>

		// 直接使用当前作用域的变量,比如 state.count
		return (
			<template>
				<Title />
				<div>{{ state.count }}</div>
				<button @click="handleClick">点击加1</button>
			</template>
		)
	},
};

目标有了那就开始魔改吧。我写了个编译器集成了vite插件实现:jsv-compiler

1. 创建vue3项目

npx create-vite-app <your projectname>

2. 安装依赖包

npm i -D jsv-compiler

3. 配置插件

文件: vite.config.js

import {jsvPlugin} from 'jsv-compiler'

export default {
  configureServer: [jsvPlugin]
}

4. 使用示例

新建文件后缀为 .jsv

// 文件命名为 **App.jsv**
import { reactive } from "vue";

export default {
	name: "App",
	setup() {
		let state = reactive({ 
			title: 'jsv-compiler',
			count: 0 
		});

		function handleClick() {
			state.count++;
		}
        // 一个文件中写多个组件。此处的 Title 是 组件。
		let Title = <template><h1>hello {{state.title}}</h1></template>
		
		// 直接使用当前作用域的变量,比如 state.count
		return (
			<template>
				<Title />
				<div>{{ state.count }}</div>
				<button @click="handleClick">点击加1</button>
			</template>
			)
	},
};

启动项目 npm run dev,很神奇的魔改成功了: 还是有点小激动的!

5. 语法高亮

  • 5.1 在vscode的插件市场中下载插件: jsv
  • 5.2 因为是第一个版本,在 App.jsv 中暂时无法语法高亮,需将文件名改为 App.js ,并在 <template></template> 旁边加上反引号 才能在vscode中实现语法高亮,实际在编译层面是不需要加反引号的。

6. 复盘下魔改的主要过程

主要看编译结果,还是看这个简单的例子

// 编译前
<div>
  <div>静态节点</div>
  <div >{{state.count }}</div>
</div>
  • 6.1. 我是如何做到在一个文件中写多个template ?

在原来的 .vue 中, template 编译后会被写到单独的文件中,并return 或者 export 出去。

为了达到在一个文件中写多个组件,我将其编译成一个自执行函数并返回render函数。同时利用闭包特性,仍然能使用静态节点提升后的变量 _hoisted_1

// jsv的编译结果
(() => {
	const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "静态节点", -1 /* HOISTED */)

	return function render(_ctx, _cache) {
		return (_openBlock(), _createBlock("div", null, [
			_hoisted_1,
			_createVNode("div", null, _toDisplayString(
				(() => {
					try {
						return state
					} catch {
						return _ctx.state
					}
				})().count), 1 /* TEXT */)
		]))
	}
})()
  • 6.2. 如何做到使用当前作用域的变量 state?

我将_ctx.state编译成自执行函数:

(() => {
  try {
    return state
  } catch {
    return _ctx.state
  }
})()
  • 6.3. 如何使用当前作用域的组件?

我们知道在 .vue 中必须要将组件注册后才能在 template 中使用,比如

// NoMulti.vue
<template>
	<Title :title="state.title" />
</template>

<script>
import { reactive } from "vue";
import Title from "./Title.vue";
export default {
	components: { Title },  // 必须要在此处注册组件
	setup() {
		let state = reactive({ title: "jsv-compiler" });

		return { state };
	},
};
</script>

为了实现不注册也能直接使用,我将其编译成这样:

const _component_Title = (()=>{
  try {
    return Title;
  } catch (error) {
    return _resolveComponent("Title");
  }
}) () 

也是一个自执行函数,哈哈。如果当前作用域有 Title,就使用当前作用域的 Title。如果当前作用域没有 Title, 就使用注册的组件_resolveComponent("Title")。

还有很多魔性修改来不及写了。。。大家发现了吧,大量用到了自执行函数。。。

三、总结

在 .js 中使用 vue 模板语法 (简称 jsv

vue3 模板jsvjsx
灵活性不灵活灵活灵活
文件中组件数量1个文件1个组件1个文件多个组件1个文件多个组件
能否使用当前作用域变量不能,需在setup()方法中return后才能使用可以使用可以使用
是否很好支持指令原生支持原生支持不支持或支持不友好
运行时性能支持hoist,createBlock,patchProps支持hoist,createBlock,patchProps不支持
自动热更新支持不支持不支持

值得一提的是 jsv 保持了原本 vue 的高性能,支持hoist,createBlock,patchProps等在 jsx 中难以实现的特性,比 jsx快了近3倍。 可能有人会说为什么要折腾这些?我想说的是 人类对效率的追求总是无止境的,正是像这样的折腾铸就了人类宏伟的科技蓝图。也希望我的一点贡献能抛砖引玉激发起大家的兴趣尝试一下

备注: jsv-compiler 在线编译器: ruige24601.github.io/jsv-compile…