'vue/TypeScript问题两则'

最近尝试为 vue 项目配合 TypeScript 不过发现毕竟 vue 不是原生 TypeScript 开发,在对 TypeScript 支持方面显然还问题多多,这里稍微记录两则

自动注入 h 问题

如果你在 vue 中是用 jsx 你会明显感受到和 react 有明显的一个差异就是 react 中你可以很方便的到处写下 jsx,而 vue 中则限定的比较死,要么写在组建的 render 函数中,要么主动传递 h 函数到你想要写 jsx 的地方比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 找不到 h 函数
const component = <button />
// ok
export default {
render (h) {
return <button />
}
}
// 主动将 h 传递到需要的地方
const component2 = h => <button />
export default {
render (h) {
return component2(h)
}
}

由于 h 函数是在编译后才会被使用到,而每次都要写 h,但实际少主动用到,所以 vue 官方为了进一步简化使用,开发了一个 babel 插件babel-sugar-inject-h, 检测 jsx 自动插入 h 函数,比如这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// With @vue/babel-sugar-inject-h
const component =()=> <button />
export default {
render () {
return <button />
}
}
// 不需要写h,编译后自动注入 h 函数,等价于
const component =(h)=> <button />
export default {
render (h) {
return <button />
}
}

这让看起来让 vue/jsx 更加接近 react/jsx ,还稍微减轻了开发的工作。但我个人认为这样有些自做主张,并且关键是做的并不完美。如果你用了 vue-cli 那么他是自带且默认启动的,如果你没有用 vue-cli 自己配的环境,那么就有可能就忽略了这的插件,就容易导致同一段代码在两个环境下有不同的表现。而这如果不是用户主动去看文档,是不会被感知到的,一但出了问题就特别令人摸不着头脑,

哦,跑题了,这篇文章应该说的是跟 TypeScript 相关的问题,其实也就是它做得不够好的一个体现,假如你要配合 TypeScript 使用,很有可能遇到以下代码

1
2
3
4
5
import { Component, Vue } from "vue-property-decorator";
@Component
export default class App extends Vue {
test = (h: any) => <div>123</div>;
}

其中 test 是你组件的内部变量,理论上这段代码应该等价于

1
2
3
4
5
6
7
export default {
data(){
return {
test: (h)=><div>123</div>
}
}
}

然而假如你实际在 vue-cli 中跑这段代码,是可以跑起来的,但在浏览器中会报出类似以下错误,

1
Error in data(): "TypeError: Cannot read property '$createElement' of undefined"

有了前面的铺垫,不用说问题的关键显然是出在 babel-sugar-inject-h 自动注入 h 的问题上,为了了解为什么,不妨看一下这段代码编译后的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 略去前后无关代码
function (_Vue) {
Object(inherits["a" /* default */])(App, _Vue);
function App() {
var _this;
Object(classCallCheck["a" /* default */])(this, App);
var h = _this.$createElement;
_this = Object(possibleConstructorReturn["a" /* default */])(this, Object(getPrototypeOf["a" /* default */])(App).apply(this, arguments));
_this.test = function (h) {
return h("div", ["123"]);
};
return _this;
}
return App;
}(vue_property_decorator["c" /* Vue */]);

我撇去了无关的代码和不压缩,这里的问题就显而易见了。

尽管在 test 上我主动显式的传入了 h 函数,但是对于构造函数 data() 来说,似乎依然被判断为应当注入 h 的情况,这也其实没有关系,不过它注入 var h = _this.$createElement; 的位置就有些问题了 ,默认情况下,它一般事插入到 this 声明定义之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
function App() {
// 一般情况下编译后 _this 的声明和定义实在同一行的,因此注入后不会报错
var _this = something;
var h = _this.$createElement;
return _this;
}
function App() {
// 在 ts/vue-property-decorato 中的 this 是先声明后定义
var _this;
var h = _this.$createElement;
_this = something;
return _this;
}

然而由于用了ts和vue-property-decorator,_this 的是先声明后定义,导致插入在声明之后的 h 是找不到的 _this.$createElement

解决方法

最好的方法当然是这个插件更新并考虑 ts 这种 edge case ,不过在此之前我其实建议直接关掉自动注入 h 的功能,毕竟 vue/jsx 本身就不是可以到处写 jsx ,本来就需要 h 却隐蔽起来并不会解决太多问题,反而会造成一些认知问题。

不过这里牵扯到另一个问题,如何关闭这个插件。

vue/jsx 的文档中写明的修改配置方法是在 babel config 中修改 @vue/babel-preset-jsx 的配置,假如你是自己配置环境那么直接如此就可以了。

1
2
3
4
5
6
7
8
9
10
{
"presets": [
[
"@vue/babel-preset-jsx",
{
"injectH": false
}
]
]
}

但是,假如你用 vue-cli 构建的项目的话,你会发现这样写并没有用,这是因为 vue-cli 非常“聪明”的封装了另一个 @vue/babel-preset-app ,这 preset 内部引入了 @vue/babel-preset-jsx自己包裹了一层配置的传递, 源码在这里

1
2
3
4
5
6
7
if (options.jsx !== false) {
presets
.push([
require('@vue/babel-preset-jsx'),
typeof options.jsx === 'object' ? options.jsx : {}
])
}

可以看到它根本不会去读取 babel config 中 @vue/babel-preset-jsx 的配置参数,因此正确的配置方法是

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"presets": [
[
"@vue/babel-preset-app",
{
// 要扔到 @vue/babel-preset-app 的 jsx 字段上
jsx:{
"injectH": false
}
}
]
]
}

而关于 jsx 可配置 @vue/babel-preset-jsx 参数这一点,官方一开始并没有任何说明(现在有了),在文档中也只提到,jsx 是一个布尔值用于开启关闭 jsx 支持,如果不去看源码谁又知道,它还可以传入 object 来做进一步的配置?

所以说,为了搞清 babel-sugar-inject-h 这一个小问题,不得不跑去看 vue-cli/babel-preset-jsx/babel-preset-app 等一长串的相关源码配置方法才摸索清楚,这说不定就是封装之美?

真有你的啊 vue-cli

tsx 中组件属性类型推断问题

tsx 中无法正确给出自定义 component 的属性类型定义,甚至会报错。相比之下这个问题更加简单也更加恼人,假如你定义了一个组件,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from "vue";
const MyComponent = Vue.extend({
props: {
text: { type: String, required: true },
important: Boolean,
},
computed: {
className() {
return this.important ? "label-important" : "label-normal";
}
},
methods: {
onClick(event) { this.$emit("ok", event); }
},
template: "<span :class='className' @click='onClick'>{{ text }}</span>"
});
// 在别的组件中调用,则会报错
// Compilation error(TS2339): Property `text` does not exist on type '...'
<MyComponent text="foo" />;

这个问题如果是从官方角度来说,几乎除了等待 vue3 重构以后得到 TypeScript 更好的支持以外,看来别无其他折中的办法,详情可参见这个 issue

所幸这个 issue 中也给出了一个比较成熟的第三方解决方案 vue-tsx-support,这个库的文档已经非常详细了,这里没有必要赘述。总而言之这个库可以相当好的解决目前的 tsx component prop 缺少类型推导的问题,不过,代价是引入了他自己的一套定义组件的方式。

兼容?升级?

首先这个库虽然得到推荐,但官方对其的态度也停留在可以有的暧昧程度,不如 **vue-property-decorator** 那样得到官方认可并且整合到官方工具链中这个级别的对待。在这个问题上 vue 官方寄希望于 vue3 重构能解决,而没有给出现在的过度方案,显然你不可能寄希望于他们能去兼容现在的第三方过度方案……

因此另外一个不甚优雅的解决方案,那就是通过 d.ts 文件让 component 的参数允许任何属性

1
2
3
4
5
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
[propName: string]: any
}
}

这样 component 实际上就可以接受任何参数而不报错,然而无法正确推导出对应的 component 有什么属性的问题并没有解决,所以依然的不到任何提示,这某种程度上就损失了 TypeScript 一个大优势。

而好处是,没有引入任何新的东西,也不需要魔改任何旧的代码,在兼容性上和其他的 vue2 代码处于一个级别。

总结

目前来说,ts + vue 当然可用,但是依然相当多的问题,没法如丝般顺滑的相互配合。对于他们完全打算通过 vue3 去彻底(?)解决于 ts 共用的的问题,而不打算优化一下 2.x 当前存在问题的决定……我是比较不赞同的