首页 > 其他分享 >TypeScript必知三部曲(二)JSX的编译与类型检查

TypeScript必知三部曲(二)JSX的编译与类型检查

时间:2023-05-01 10:33:05浏览次数:49  
标签:React TypeScript 必知 JSX react 编译 babel jsx

在本三部曲系列的第一部中,我们介绍了TypeScript编译的两种方案(tsc编译、babel编译)以及二者的重要差异,同时分析了IDE是如何对TypeScript代码进行类型检查的。该部分基本涵盖了TypeScript代码编译的细节,但主要是关于TS代码本身的编译与类型检查。而本文,我们将着重讨论含有JSX的TypeScript代码(又称TSX)如何进行类型检查与代码编译的。

前言:JSX编译

在介绍如何对JSX代码进行类型检查前,让我们花一点时间认识一下JSX,以及如何对其进行编译。

注意:这块内容很多,如果读者已经熟悉这块的内容,可以直接从JSX(TSX)的类型检查开始阅读。

实际上,JSX并不是合法有效的JS代码或HTML代码。目前为止也没有任何一家浏览器的引擎实现了对JSX的读取和解析。此外,JSX本身没有完全统一的规范,除了一些基本的规则以外,各种利用了JSX的JS库可以根据自身需求来设计JSX额外的特性。譬如,React中的元素会有className属性,而SolidJS中的元素会有classList属性。在FaceBook官方博文中也明确提到了:

JSX是一种类似XML的语法扩展。它不打算由引擎或浏览器实现。它也不会作为某种提案被合并到ECMAScript规范中。它旨在被各种预处理器(转译器)用于将这些标记转换为标准的ECMAScript。—— JSX (facebook.github.io)

当然,只要提到JSX我们就不得不提React,尽管React与JSX是相互独立的东西,但是React将JSX发扬光大,让更多的开发者接触到了JSX。所以我们先从React入手,分析JSX是如何编译为JS代码的。对于JSX的编译方案,已知的有两种:

  1. babel编译方案
  2. tsc编译方案

就像TypeScript编译一样,只要涉及到了编译环节,我们总是离不开编译三要素模型:源代码、编译器以及编译配置:

010-code-compile-flow

接下来将分别详细介绍这两种编译体系的编译过程。

babel编译体系

通过babel可以将结构化的JSX组件,转换为同样结构化的JS代码调用形式。在React中,转换JSX为原生JS代码分为两种形式:

  1. React17以前React.createElment形式;
  2. React17以后'react/jsx-runtime'形式。

先讲第一种:直接转换为React.createElement。假设源代码如下:

import React from 'react';

function App() {
  return <h1>Hello World</h1>;
}

转换过程,会将上述JSX转换为如下的createElement代码:

import React from 'react';

function App() {
  return React.createElement('h1', null, 'Hello world');
}

但官方提到了关于这种转换方式的两个问题:

  • 如果使用 JSX,则需在 React 的环境下,因为 JSX 将被编译成 React.createElement,也就是说强绑定React
  • 有一些 React.createElement 无法做到的性能优化和简化

基于上述的问题,在React17以后,提供了另一种转换方式:引入jsx-runtime层。假设源码如下:

function App() {
  return <h1>Hello World</h1>;
}

下方是新 JSX 被转换编译后的结果:

// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

第二种模式的核心在于:JSX编译出来的代码与React库本身进行了解耦,只将JSX转换为了与React无关的JS形式的调用描述,没有直接使用React.createElement引入了jsx-runtime这一层,屏蔽具体的调用细节,只专注JSX到JS代码最基础的映射。至于这个_jsx的具体实现,就是内部调用的是React.createElement还是另一种createElement,则可以由库内部来进行实现。

020-react-jsx-runtime

PS:可能有小伙伴会说,_jsx不还是从react/jsx-runtime这个React相关库导出的吗?实际上,这个包仅仅是由react团队在维护的原因。

上图描述了一个前端React工程里JSX代码基本的转换思路。当然,Babel在这个转换过程中承担了重要角色。在Babel中,与上述两种转换相关的核心部分是:@babel/preset-react里面引用的插件@babel/plugin-transform-react-jsx

Babelv7.9.0版本之前的该插件,只能将JSX代码转换为React.createElement调用形式。而在v7.9.0版本以后,支持我们配置转换行为。默认选项为 {"runtime": "classic"},也就是说默认还是React.createElement

如需启用新的转换,你可以使用 {"runtime": "automatic"} 作为 @babel/plugin-transform-react-jsx@babel/preset-react 的选项:

// 如果你使用的是 @babel/preset-react(内部引用了@babel/plugin-transform-react-jsx)
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"
    }]
  ]
}
// 如果你使用的是 @babel/plugin-transform-react-jsx
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
      "runtime": "automatic"
    }]
  ]
}

让我们创建一个样例jsx-babel-example,来实践上述过程。对应编译模型三要素,我们定义好如下的内容:

(1)源代码:src/index.jsx

const MyButton = (props) => <button>{props.children}</button>

function App() {
    return <h1><MyButton>Hello World</MyButton></h1>;
}

(2)babel编译器以及相关插件:

yarn add -D @babel/core @babel/cli
yarn add -D @babel/plugin-transform-react-jsx

(3)编译配置.babelrc:

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx"
    ]
  ]
}

补充一个脚本:

{
  ...
+ "scripts": {
+ 	 "build": "babel src -x .jsx --config-file ./.babelrc -d dist"
+ },
}

搭建完成以后,整体如下:

030-jsx-babel-compile-project

项目结构并不复杂,编译的过程我们也不再赘述。当我们运行yarn build的时候可以看到在dist目录下能够生成对应js代码:

040-jsx-babel-compile-result

从上图可以看到,我们的代码直接转换为了React.createElement。但是注意的是,编译结果中,babel是没有替我们插入import React from 'react'这一句代码的!如果你的代码本身没有添加import React from 'react',那么最终编译到了js代码(无论是commonjs还是esmodule),也不会引入React,然而代码却调用的是:React.createElement。正是因为如此,所以才会有我们日常小伙伴会发现,项目能够编译通过,但是运行起来的时候,会提示:

ReferenceError: React is not defined

对于上面问题的解决办法,有两种方式解决:

方式一:在你的代码中手动写上:import React from 'react'。编译后,自然而然就有了import React from 'react'

方式二:使用编译参数:"runtime": "automatic"

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
+     { "runtime": "automatic" } 
    ]
  ]
}

新增上述配置以后,重新编译代码,能够看到生产的js代码:

050-jsx-babel-compile-with-automatic

对于重新编译好的代码,此时可以看到React.createElement调用变为了来源于"react/jsx-runtime"中的jsx方法。同时,由于这一段引入是由编译器自动加入的,因此代码进行后续的babel编译的时候,由于有react/jsx-runtime的引入,所以就不再会有所谓的React is not defined的问题。

tsc编译体系

介绍完babel编译jsx体系以后,我们再讲一下关于tsc编译jsx代码的方式。当然,基于编译要素模型,我们依然准备一个项目来解释这个过程。

(1)源代码同上src/index.jsx。

(2)typescript包:

yarn add -D typescript

(3)编译配置tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react",
    "outDir": "dist",
    "rootDir": "src",
    "allowJs": true
  }
}

补充一个脚本:

{
  ...
  "scripts": {
+ 	"build": "tsc -p tsconfig.json"
  },
  ...
}

搭建完成后,整体如下:

060-jsx-tsc-compile-project

当我们运行yarn build的时候可以看到在dist目录下能够生成对应js代码:

070-jsx-tsc-compile-result-by-jsxreact

读者应该能够看到由tsc编译出来的js代码,JSX相关的代码编译为了React.createElement形式的调用。对于这个编译结果,tsconfig.json里面的配置起到了决定性作用。现在,我们着重讲一下两个配置项:jsxallowJs

"allowJs"

由于本example中我们没有编写tsx代码,还是用的jsx代码,如果不配置"allowJs": true,那么tsc编译器默认将不会处理js以及jsx文件,又因为example中src目录下只有jsx文件,于是会出现报错:

error TS18003: No inputs were found in config file '/Users/w4ngzhen/projects/web-projects/jsx-tsc-example/tsconfig.json'. Specified 'include' paths were '["**/*"]' and 'exclude' paths were '["dist"]'.

后续如果是TSX的文件,将不会出现这个问题,也不用显式配置该选项。

"jsx"

对于"jsx"这个配置,主要有以下几个值:

  • react: 将 JSX 改为等价的对 React.createElement 的调用并生成 .js 文件。
  • react-jsx: 改为 __jsx 调用并生成 .js 文件。
  • preserve: 不对 JSX 进行改变并生成 .jsx 文件。
  • react-jsxdev: 改为 __jsx 调用并生成 .js 文件。
  • react-native: 不对 JSX 进行改变并生成 .js 文件。

下图展示了当"jsx"的配置分别为:"react""react-jsx"的结果:

080-tsconfig-jsx-result

不难发现,"react""react-jsx"配置的编译结果,与前面babel编译中插件@babel/plugin-transform-react-jsx"runtime"的配置分别为"classic"(或默认不配置)、"automatic"的编译结果能够相对应。

tsconfig默认使用commonjs作为模块化方案,所以,"jsx": "react-jsx"配置的编译结果中引用react/jsx-runtime时,使用commonjs规范的require。如果给tsconfig.json添加配置"module": "ES6",则会看到import {jsx as __jsx} from 'react/jsx-runtime'的引用方式。

正文:JSX(TSX)的类型检查

在《2023-04-08-TypeScript必知三部曲(一)TypeScript编译方案以及IDE对TS的类型检查》中,我们已经了解了,babel不会参与TS代码的类型检查,TS代码本身的类型检查、IDE上的类型检查提示,都是经过tsc配合tsconfig配置完成。所以,接下来我们所谈的关于JSX(TSX)的类型检查,将会围绕tsc+tsconfig来进行讨论。

准备工作

在进行讨论之前,我们依然准备一个样例,这个样例与前面关于tsc编译体系的样例差别不大,重点在于index.jsx改为了index.tsx

(1)源代码src/index.tsx:

const MyButton = (props) => <button>{props.children}</button>

export function App() {
    return <h1><MyButton>Hello World</MyButton></h1>;
}

(2)tsconfig.json:

{
  "compilerOptions": {
    "module": "ES6",
    "jsx": "react",
    "outDir": "dist",
    "rootDir": "src",
    "allowJs": true
  }
}

注意"jsx"的配置我们使用"react"

(3)安装typescript并添加编译脚本:

{
  "name": "jsx-tsc-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "tsc -p tsconfig.json"
  },
  "devDependencies": {
    "typescript": "^5.0.4"
  }
}

准备后整体如下:

090-tsx-tsc-example-project

类型问题

在上述的项目搭建完成后,我们会发现一个问题:在index.tsx代码中,我们用到了两个jsx基础标签:<button><h1>以及我们自己编写的React组件<MyButton>,但IDE替我们显示了红色,鼠标悬浮以后,会看到报错提示:

100-tsx-jsxreact-error-result

  • Cannot find name 'React'.

为什么会这样呢?针对该问题,需要两个知识点来解释。

第一,tsconfig.json的"jsx": "react"配置的编译结果是将 JSX 改为等价的对 React.createElement 的调用并生成 .js 文件;

第二,IDE进行TS的类型检查流程如下:

110-IDE-ts-check-flow

基于上述两点,我们可以解释这个出错的过程为:IDE识别到了tsconfig.json中的"jsx": "react"配置,调用了形如tsc --noEmit的指令,又因为我们的项目没有添加对react的依赖(类型文件也没有),因此出现了这个地方的IDE报错提示。不难想到,我们实际运行脚本进行编译的时候,会出现同样的错误:

120-tsx-jsxreact-error-result-in-cli

细心的小伙伴会看到dist目录下依然生成了index.js代码,因为类型检查结果实际上不妨碍实际js代码的生成。

上面的配置,我们使用了"jsx": "react",当我们修改为"jsx": "react-jsx"又会有什么效果呢?

修改配置以后,报错如下:

130-tsx-jsxreactjsx-error-result

有两方面:

  • Cannot find module 'react/jsx-runtime'。

  • Did you mean to set the 'moduleResolution' option...。

当我们将"moduleResolution": "node"添加以后可以解决问题2,剩下的问题就是:

140-tsx-jsxreactjsx-error-result-with-moduleResolution-Node

  • Cannot find module 'react/jsx-runtime' or its corresponding type declarations.

无法找到模块react/jsx-rutnime或它对应的类型声明。

对照前面的"jsx": "react",当我们的配置改为了"jsx": "react-jsx"以后,JSX标签都将编译为_jsx("div", ..., ...)的调用形式,而这个_jsx来源于:import {jsx as _jsx} from 'react/jsx--runtime',而我们的项目并没有安装这个包。所以,IDE根据react-jsx"配置的结果,识别到了问题,并帮助我们提示了对应的问题。

无论是Cannot find name 'React'.还是Cannot find module 'react/jsx-runtime' or its corresponding type declarations.问题,要处理起来也很简单,安装react以及其类型定义:

yarn add react
yarn add -D @types/react

@types/react中包含了Reactreact/jsx-runtime类型定义。

150-tsx-jsxreactjsx-error-result-import-react

终于,我们不再有类型问题了。此时,我们看看这些h1、button标签到底是什么类型以及ts是如何对这些JSX标签进行类型定义的。

在安装了@types/react后,IDEA里面,通过CTRL+鼠标左键点击相关的标签就能进入到对应的定义里面,比如我们查看<a>标签的具体定义:

160-jsx-tag-dts

通过查看类型定义dts文件,可以很容易的看到该类型为:JSX.IntrinsicElements.a。这个JSX.IntrinsicElements接口是什么呢?让我们探索。

类型检查的源头:JSX.IntrinsicElements

Intrinsic:固有的,本质的,根本的。

内在元素(IntrinsicElements)在特殊接口(既JSX.IntrinsicElements接口)上查找。 默认情况下,如果未指定此接口,则在TypeScript进行类型检查的时候,会直接忽略这些类型JSX标签具体的类型定义,任何JSX都不会对内部元素进行类型检查。 但是,如果存在此接口定义,则内部元素的名称将作为接口上的属性进行查找。

举一个简单的例子,我们可以尝试修改上图中react的dts代码,添加一个新的接口字段abc,该字段还有一个必填的name属性:

        interface IntrinsicElements {
+           abc: { name: string; children?: Element };
        }

170-jsx-tag-dts-add-abc-example

于是,在代码中,我们就能使用这个<abc>标签,同时,如果不填写name字段的值,TS还会有类型检查异常,只有正确填写name属性才能通过类型检查:

180-jsx-use-abc-tag

同时,当我们检查编译后的代码,会发现无论是"jsx": "react"还是"jsx": "react-jsx",关于我们使用的<abc>标签的部分,都变成了字符串"abc"的处理(这里只用tsc编译演示,babel是同样的结果,不再赘述):

190-jsx-use-abc-tag-compile-result

当然,我们还能编写自己的自定义组件,譬如上面示例里面的<MyButton>MyButton是一个函数组件,满足React DTS文件里面的类型定义关于使用函数组件类型进行createElement的类型定义:

200-func-comp-dts

总结来讲,JSX(TSX)中关于内置标签的类型检查流程如下:

210-intrinsic-elements-type-check-flow

在前面,我们在react的官方dts中的JSX.IntrinsicElements添加了abc字段,所以我们才能编写<abc>标签并通过类型检查。但这种方式目前来讲,有个问题:非常不优雅,居然去修改react类型定义代码。那么,还有什么方式扩展JSX的内置标签元素呢?编写声明文件扩充即可:

220-extend-intrinsic-elements

上图中,我们主动声明了JSX.IntrinsicElements接口,并且向里面添加了a-custom-tag,于是,后面的tsx代码中我们就能使用<a-custom-tag>这个标签了。

但要注意的是,我们声明的种种类型,只针对类型检查。它仅仅保证了tsc在进行类型检查的正确性。而实际编译后的代码,因为会生成诸如:React.createElement("a-custom-tag", ...)_jsx('a-cutoms-tag', ...)等调用的js代码。不难想到在实际运行过程中,React内部是无法处理这个所谓的a-custom-tag的“内置标签”的,它就不明白这个"a-custom-tag"是什么,所以在运行时一定会有错误。

在前言中,我们已经解释了如何将JSX编译为react、react/runtime的相关调用。那么,我们可以自定义处理JSX代码吗?当然可以,如果使用的是babel编译体系,则需要自己编写babel插件;如果是tsc编译体系,则需要自定义jsxFactory,像是solidjs,就有自己的babel插件(babel-preset-solid - npm (npmjs.com))。本文不再赘述关于自定义JSX的编译过程了,网上有很多优秀的文章可以阅读。

写在最后

本文的内容其实还是有点杂乱,后续可能会重构这篇文章。

标签:React,TypeScript,必知,JSX,react,编译,babel,jsx
From: https://www.cnblogs.com/w4ngzhen/p/17366237.html

相关文章

  • typeScript声明文件的一个注意点:不能使用导入导出语法
    一、起因使用vue3+ts在写一个demo的时候,用到路由模块的时候,觉得需要自定义一个类型声明,所以写了一个.d.ts声明文件,而这个文件写完的时候,发现vscode老是提示找不到类型声明。 起初,我以为是ts配置文件写错了,没有在include里面写入这个文件,ts察觉不到。但是后来改来改去发现还是......
  • 【TypeScript】document.body.style TS 报错 Cannot assign to 'style' because it is
    报错信息解决方法style对象提供了一个cssText属性,支持设置多种CSS样式:document.body.style.cssText=`width:${targetX}px;height:${targetY}px;transform:scale(${scaleRatio})translateX(-50%);left:50%`;还有其他方法也可以,参考下面的文章参考文章七爪源码:使用......
  • Vue3+typescript如何给元素添加一个Ctrl+s的事件,用于保存文件?
    如下代码,建议用这个,e.keyCode已经过时,后面都是用e.key:string.onMounted(()=>{window.addEventListener('keydown',(e)=>{if(e.ctrlKey&&e.key==='s'){//检查是否按下了Ctrl+Se.preventDefault();//阻止默认行为(保存网页)con......
  • [转]typeScript interface和type区别
    原文地址:https://www.jianshu.com/p/555e6998af36以下为截取的总结,详细请点击查看原文:总结interface和type很像,很多场景,两者都能使用。但也有细微的差别:不同点:扩展语法:interface使用extends,type使用‘&’同名合并:interface支持,type不支持。描述类型:对象、函数......
  • TypeScript:接口
    介绍TypeScript的核心原则之一是对值所有的结构类型进行类型检查。在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义约束。接口的基本使用interfaceLabelledValue{label:string;}functionprintLabel(labelledObj:LabelledValue){consol......
  • jsx中使用js表达式
    //在jsx中使用js表达式///通过一个{}展示变量即可vue中使用{{}}展示js表达式//什么是js表达式有结果的importreactDomfrom"react-dom"//函数也是表达式//syntaxError语法错误constsayHi=()=>{return"你好"}constspan=<span>我是一......
  • 使用typescript实现Promise
    /***@nameMyPromise*@description简单实现Promise*@author*****/classMyPromise<T>{//存放成功的回调函数privateresolveFn:Function=()=>{};//存放失败的回调函数privaterejectFn:Function=()=>{};//当前的状态/......
  • 《JSON 必知必会》阅读摘要记录
    [《JSON必知必会》阅读摘要记录|国光](https://www.sqlsec.com/2020/04/jsonbook.html#10-2-%E7%BB%93%E8%AF%AD)JSON必知必会书籍学习记录笔记,想深入一下JSONHijacking漏洞,所以就打算找一本JSON的书籍来读一遍,打捞自己的基础,于是就选了这本书来学习,以后这种读书笔记的......
  • TypeScript 学习笔记 — 数组常见的类型转换操作记录(十四)
    获取长度lengthtypeLengthOfTuple<Textendsany[]>=T["length"];typeA=LengthOfTuple<["B","F","E"]>;//3typeB=LengthOfTuple<[]>;//0取第一项FirstItemtypeFirstItem<Textendsany[]>......
  • ai问答:使使用 Vue3 组合式API 和 TypeScript 父子组件demo
    这是一个使用Vue3组合式API和TypeScript的简单父子组件示例父组件Parent.vue:<template><div><p>{{msg}}</p><Child/></div></template><scriptlang="ts">import{ref}from'vue'import......