整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

通过debug搞清楚.vue文件怎么变成.js文件

通过debug搞清楚.vue文件怎么变成.js文件

们每天写的vue代码都是写在vue文件中,但是浏览器却只认识htmlcssjs等文件类型。所以这个时候就需要一个工具将vue文件转换为浏览器能够认识的js文件,想必你第一时间就想到了webpack或者vite。但是webpackvite本身是没有能力处理vue文件的,其实实际背后生效的是vue-loader和@vitejs/plugin-vue。本文以@vitejs/plugin-vue举例,通过debug的方式带你一步一步的搞清楚vue文件是如何编译为js文件的,看不懂你来打我

举个例子

这个是我的源代码App.vue文件:

这个例子很简单,在setup中定义了msg变量,然后在template中将msg渲染出来。

下面这个是我从network中找到的编译后的js文件,已经精简过了:


编译后的js代码中我们可以看到主要有三部分,想必你也猜到了这三部分刚好对应vue文件的那三块。

  • _sfc_main对象的setup方法对应vue文件中的<script setup lang="ts">模块。
  • _sfc_render函数对应vue文件中的<template>模块。
  • import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";对应vue文件中的<style scoped>模块。

debug搞清楚如何将vue文件编译为js文件

大家应该都知道,前端代码运行环境主要有两个,node端和浏览器端,分别对应我们熟悉的编译时和运行时。浏览器明显是不认识vue文件的,所以vue文件编译成js这一过程肯定不是在运行时的浏览器端。很明显这一过程是在编译时的node端。

要在node端打断点,我们需要启动一个debug 终端。这里以vscode举例,首先我们需要打开终端,然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。

假如vue文件编译为js文件是一个毛线团,那么他的线头一定是vite.config.ts文件中使用@vitejs/plugin-vue的地方。通过这个线头开始debug我们就能够梳理清楚完整的工作流程。


vuePlugin函数

我们给上方图片的vue函数打了一个断点,然后在debug终端上面执行yarn dev,我们看到断点已经停留在了vue函数这里。然后点击step into,断点走到了@vitejs/plugin-vue库中的一个vuePlugin函数中。我们看到vuePlugin函数中的内容代码大概是这样的:


@vitejs/plugin-vue是作为一个plugins插件在vite中使用,vuePlugin函数返回的对象中的buildStarttransform方法就是对应的插件钩子函数。vite会在对应的时候调用这些插件的钩子函数,比如当vite服务器启动时就会调用插件里面的buildStart等函数,当vite解析每个模块时就会调用transform等函数。更多vite钩子相关内容查看官网。

我们这里主要看buildStarttransform两个钩子函数,分别是服务器启动时调用和解析每个模块时调用。给这两个钩子函数打上断点。


然后点击Continue(F5),vite服务启动后就会走到buildStart钩子函数中打的断点。我们可以看到buildStart钩子函数的代码是这样的:


将鼠标放到options.value.compiler上面我们看到此时options.value.compiler的值为null,所以代码会走到resolveCompiler函数中,点击Step Into(F11)走到resolveCompiler函数中。看到resolveCompiler函数代码如下:


resolveCompiler函数中调用了tryResolveCompiler函数,在tryResolveCompiler函数中判断当前项目是否是vue3.x版本,然后将vue/compiler-sfc包返回。所以经过初始化后options.value.compiler的值就是vue的底层库vue/compiler-sfc,记住这个后面会用

然后点击Continue(F5)放掉断点,在浏览器中打开对应的页面,比如:http://localhost:5173/ 。此时vite将会编译这个页面要用到的所有文件,就会走到transform钩子函数断点中了。由于解析每个文件都会走到transform钩子函数中,但是我们只关注App.vue文件是如何解析的,所以为了方便我们直接在transform函数中添加了下面这段代码,并且删掉了原来在transform钩子函数中打的断点,这样就只有解析到App.vue文件的时候才会走到断点中去。


经过debug我们发现解析App.vue文件时transform函数实际就是执行了transformMain函数,至于transformStyle函数后面讲解析style的时候会讲:


transformMain函数

继续debug断点走进transformMain函数,发现transformMain函数中代码逻辑很清晰。按照顺序分别是:

  • 根据源代码code字符串调用createDescriptor函数生成一个descriptor对象。
  • 调用genScriptCode函数传入第一步生成的descriptor对象将<script setup>模块编译为浏览器可执行的js代码。
  • 调用genTemplateCode函数传入第一步生成的descriptor对象将<template>模块编译为render函数。
  • 调用genStyleCode函数传入第一步生成的descriptor对象将<style scoped>模块编译为类似这样的import语句,import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";。

createDescriptor函数

我们先来看看createDescriptor函数,将断点走到createDescriptor(filename, code, options)这一行代码,可以看到传入的filename就是App.vue的文件路径,code就是App.vue中我们写的源代码。


debug走进createDescriptor函数,看到createDescriptor函数的代码如下:


这个compiler是不是觉得有点熟悉?compiler是调用createDescriptor函数时传入的第三个参数解构而来,而第三个参数就是options。还记得我们之前在vite启动时调用了buildStart钩子函数,然后将vue底层包vue/compiler-sfc赋值给optionscompiler属性。那这里的compiler.parse其实就是调用的vue/compiler-sfc包暴露出来的parse函数,这是一个vue暴露出来的底层的API,这篇文章我们不会对底层API进行源码解析,通过查看parse函数的输入和输出基本就可以搞清楚parse函数的作用。下面这个是parse函数的类型定义:


从上面我们可以看到parse函数接收两个参数,第一个参数为vue文件的源代码,在我们这里就是App.vue中的code字符串,第二个参数是一些options选项。我们再来看看parse函数的返回值SFCParseResult,主要有类型为SFCDescriptordescriptor属性需要关注。


仔细看看SFCDescriptor类型,其中的template属性就是App.vue文件对应的template标签中的内容,里面包含了由App.vue文件中的template模块编译成的AST抽象语法树和原始的template中的代码。


我们再来看scriptscriptSetup属性,由于vue文件中可以写多个script标签,scriptSetup对应的就是有setupscript标签,script对应的就是没有setup对应的script标签。我们这个场景中只有scriptSetup属性,里面同样包含了App.vue中的script模块中的内容。


我们再来看看styles属性,这里的styles属性是一个数组,是因为我们可以在vue文件中写多个style模块,里面同样包含了App.vue中的style模块中的内容。


所以这一步执行createDescriptor函数生成的descriptor对象中主要有三个属性,template属性包含了App.vue文件中的template模块code字符串和AST抽象语法树scriptSetup属性包含了App.vue文件中的<script setup>模块的code字符串,styles属性包含了App.vue文件中<style>模块中的code字符串。createDescriptor函数的执行流程图如下:


genScriptCode函数

我们再来看genScriptCode函数是如何将<script setup>模块编译成可执行的js代码,同样将断点走到调用genScriptCode函数的地方,genScriptCode函数主要接收我们上一步生成的descriptor对象,调用genScriptCode函数后会将编译后的script模块代码赋值给scriptCode变量。


将断点走到genScriptCode函数内部,在genScriptCode函数中主要就是这行代码: const script=resolveScript(descriptor, options, ssr, customElement);。将第一步生成的descriptor对象作为参数传给resolveScript函数,返回值就是编译后的js代码,genScriptCode函数的代码简化后如下:


我们继续将断点走到resolveScript函数内部,发现resolveScript中的代码其实也很简单,简化后的代码如下:


这里的options.compiler我们前面第一步的时候已经解释过了,options.compiler对象实际就是vue底层包vue/compiler-sfc暴露的对象,这里的options.compiler.compileScript()其实就是调用的vue/compiler-sfc包暴露出来的compileScript函数,同样也是一个vue暴露出来的底层的API,后面我们的分析defineOptions等文章时会去深入分析compileScript函数,这篇文章我们不会去读compileScript函数的源码。通过查看compileScript函数的输入和输出基本就可以搞清楚compileScript函数的作用。下面这个是compileScript函数的类型定义:



这个函数的入参是一个SFCDescriptor对象,就是我们第一步调用生成createDescriptor函数生成的descriptor对象,第二个参数是一些options选项。我们再来看返回值SFCScriptBlock类型:


返回值类型中主要有scriptAstscriptSetupAstcontent这三个属性,scriptAst为编译不带setup属性的script标签生成的AST抽象语法树。scriptSetupAst为编译带setup属性的script标签生成的AST抽象语法树,contentvue文件中的script模块编译后生成的浏览器可执行的js代码。下面这个是执行vue/compiler-sfccompileScript函数返回结果:


继续将断点走回genScriptCode函数,现在逻辑就很清晰了。这里的script对象就是调用vue/compiler-sfccompileScript函数返回对象,scriptCode就是script对象的content属性 ,也就是将vue文件中的script模块经过编译后生成浏览器可直接执行的js代码code字符串。


genScriptCode函数的执行流程图如下:


genTemplateCode函数

我们再来看genTemplateCode函数是如何将template模块编译成render函数的,同样将断点走到调用genTemplateCode函数的地方,genTemplateCode函数主要接收我们上一步生成的descriptor对象,调用genTemplateCode函数后会将编译后的template模块代码赋值给templateCode变量。


同样将断点走到genTemplateCode函数内部,在genTemplateCode函数中主要就是返回transformTemplateInMain函数的返回值,genTemplateCode函数的代码简化后如下:


我们继续将断点走进transformTemplateInMain函数,发现这里也主要是调用compile函数,代码如下:

同理将断点走进到compile函数内部,我们看到compile函数的代码是下面这样的:

同样这里也用到了options.compiler,调用options.compiler.compileTemplate()其实就是调用的vue/compiler-sfc包暴露出来的compileTemplate函数,这也是一个vue暴露出来的底层的API。不过这里和前面不同的是compileTemplate接收的不是descriptor对象,而是一个SFCTemplateCompileOptions类型的对象,所以这里需要调用resolveTemplateCompilerOptions函数将参数转换成SFCTemplateCompileOptions类型的对象。这篇文章我们不会对底层API进行解析。通过查看compileTemplate函数的输入和输出基本就可以搞清楚compileTemplate函数的作用。下面这个是compileTemplate函数的类型定义:

入参options主要就是需要编译的template中的源代码和对应的AST抽象语法树。我们来看看返回值SFCTemplateCompileResults,这里面的code就是编译后的render函数字符串。

genTemplateCode函数的执行流程图如下:


genStyleCode函数

我们再来看最后一个genStyleCode函数,同样将断点走到调用genStyleCode的地方。一样的接收descriptor对象。代码如下:

我们将断点走进genStyleCode函数内部,发现和前面genScriptCodegenTemplateCode函数有点不一样,下面这个是我简化后的genStyleCode函数代码:

我们前面讲过因为vue文件中可能会有多个style标签,所以descriptor对象的styles属性是一个数组。遍历descriptor.styles数组,我们发现for循环内全部都是一堆赋值操作,没有调用vue/compiler-sfc包暴露出来的任何API。将断点走到 return stylesCode;,看看stylesCode到底是什么东西?

通过打印我们发现stylesCode竟然变成了一条import语句,并且import的还是当前App.vue文件,只是多了几个query分别是:vuetypeindexscopedlang。再来回忆一下前面讲的@vitejs/plugin-vuetransform钩子函数,当vite解析每个模块时就会调用transform等函数。所以当代码运行到这行import语句的时候会再次走到transform钩子函数中。我们再来看看transform钩子函数的代码:

query中有vue字段,并且querytype字段值为style时就会执行transformStyle函数,我们给transformStyle函数打个断点。当执行上面那条import语句时就会走到断点中,我们进到transformStyle中看看。

transformStyle函数的实现我们看着就很熟悉了,和前面处理templatescript一样都是调用的vue/compiler-sfc包暴露出来的compileStyleAsync函数,这也是一个vue暴露出来的底层的API。同样我们不会对底层API进行解析。通过查看compileStyleAsync函数的输入和输出基本就可以搞清楚compileStyleAsync函数的作用。

我们先来看看SFCAsyncStyleCompileOptions入参:

入参主要关注几个字段,source字段为style标签中的css原始代码。scoped字段为style标签中是否有scoped attribute。id字段为我们在观察 DOM 结构时看到的 data-v-xxxxx。这个是debug时入参截图:

再来看看返回值SFCStyleCompileResults对象,主要就是code属性,这个是经过编译后的css字符串,已经加上了data-v-xxxxx

这个是debugcompileStyleAsync函数返回值的截图:

genStyleCode函数的执行流程图如下:

transformMain函数简化后的代码

现在我们可以来看transformMain函数简化后的代码:

transformMain函数中的代码执行主流程,其实就是对应了一个vue文件编译成js文件的流程。

首先调用createDescriptor函数将一个vue文件解析为一个descriptor对象。

然后以descriptor对象为参数调用genScriptCode函数,将vue文件中的<script>模块代码编译成浏览器可执行的js代码code字符串,赋值给scriptCode变量。

接着以descriptor对象为参数调用genTemplateCode函数,将vue文件中的<template>模块代码编译成render函数code字符串,赋值给templateCode变量。

然后以descriptor对象为参数调用genStyleCode函数,将vue文件中的<style>模块代码编译成了import语句code字符串,比如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";,赋值给stylesCode变量。

然后将scriptCodetemplateCodestylesCode使用换行符\n拼接起来得到resolvedCode,这个resolvedCode就是一个vue文件编译成js文件的代码code字符串。这个是debugresolvedCode变量值的截图:

总结

这篇文章通过debug的方式一步一步的带你了解vue文件编译成js文件的完整流程,下面是一个完整的流程图。如果文字太小看不清,可以将图片保存下来或者放大看:

@vitejs/plugin-vue-jsx库中有个叫transform的钩子函数,每当vite加载模块的时候就会触发这个钩子函数。所以当import一个vue文件的时候,就会走到@vitejs/plugin-vue-jsx中的transform钩子函数中,在transform钩子函数中主要调用了transformMain函数。

第一次解析这个vue文件时,在transform钩子函数中主要调用了transformMain函数。在transformMain函数中主要调用了4个函数,分别是:createDescriptorgenScriptCodegenTemplateCodegenStyleCode

createDescriptor接收的参数为当前vue文件代码code字符串,返回值为一个descriptor对象。对象中主要有四个属性templatescriptSetupscriptstyles

  • descriptor.template.ast就是由vue文件中的template模块生成的AST抽象语法树。
  • descriptor.template.content就是vue文件中的template模块的代码字符串。
  • scriptSetup和script的区别是分别对应的是vue文件中有setup属性的<script>模块和无setup属性的<script>模块。descriptor.scriptSetup.content就是vue文件中的<script setup>模块的代码字符串。

genScriptCode函数为底层调用vue/compiler-sfccompileScript函数,根据第一步的descriptor对象将vue文件的<script setup>模块转换为浏览器可直接执行的js代码。

genTemplateCode函数为底层调用vue/compiler-sfccompileTemplate函数,根据第一步的descriptor对象将vue文件的<template>模块转换为render函数。

genStyleCode函数为将vue文件的style模块转换为import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";样子的import语句。

然后使用换行符\ngenScriptCode函数、genTemplateCode函数、genStyleCode函数的返回值拼接起来赋值给变量resolvedCode,这个resolvedCode就是vue文件编译成js文件的code字符串。

当浏览器执行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";语句时,触发了加载模块操作,再次触发了@vitejs/plugin-vue-jsx中的transform钩子函数。此时由于有了type=stylequery,所以在transform函数中会执行transformStyle函数,在transformStyle函数中同样也是调用vue/compiler-sfccompileStyleAsync函数,根据第一步的descriptor对象将vue文件的<style>模块转换为编译后的css代码code字符串,至此编译style部分也讲完了。

avaScript 是语言,而 React 是工具。

力争用最简洁的方式让你入门 React,前提是你已了解 JavaScript 和 HTML。

0x01 从 JavaScript 到 React

一些必须要懂的 JavaScript 概念

如果你在学习 React 的同时,也在学习 JavaScript,那么这里罗列了一些必须要懂的 JavaScript 概念,来帮助你更好地学习 React。

  • 函数 和 箭头函数
  • 对象
  • 数组及其常用方法
  • 解构赋值
  • 模版字符串
  • 条件运算符
  • JS 模块化和导入导出方法

本文将不会深入讲解关于 JavaScript 方面的知识,你无需非常精通 JavaScript 才能学习 React,但以上的一些概念是最适合初学者掌握的 JavaScript 的重要知识。

当然,你也可以跳过这些基本概念直接进入下面章节的学习,当遇到不理解的问题再回过头来翻阅这里的概念。

渲染 UI

要理解 React 如何工作,首先要搞清楚浏览器是如何解释你的代码并转化成 UI 的。

当用户访问一个网页时,服务器将返回一段 html 代码到浏览器,它可能看起来是这样的:



浏览器阅读了 html 代码,并将它结构化为 DOM。

什么是 DOM

DOM 是一个 html 元素的对象化表达,它是衔接你的代码和 UI 之间的桥梁,它表现为一个父子关系的树形结构。



你可以使用 JavaScript 或 DOM 的内置方法来监听用户事件,操作 DOM 包括,查询、插入、更新、删除界面上特定的元素,DOM 操作不仅允许你定位到特定的元素上,并且允许你修改它的内容及样式。

小问答:你可以通过操作 DOM 来修改页面内容吗?

使用 JavaScript 及 DOM 方法来更新 UI

让我们一起来尝试如何使用 JavaScript 及 DOM 方法来添加一个 h1 标签到你的项目中去。

打开我们的代码编辑软件,然后创建一个新的 index.html 的文件,在文件中加入以下代码:

<!-- index.html -->
<html>
  <body>
    <div></div>
  </body>
</html>

然后给定 div 标签一个特定的 id ,便于后续我们可以定位它。

<!-- index.html -->
<html>
  <body>
    <div id="app"></div>
  </body>
</html>

要在 html 文件中编写 JavaScript 代码,我们需要添加 script 标签

<!-- index.html -->
<html>
  <body>
    <div id="app"></div>
    <script type="text/javascript"></script>
  </body>
</html>

现在,我们可以使用 DOM 提供的 getElementById 方法来通过标签的 ID 定位到指定的元素。

<!-- index.html -->
<html>
  <body>
    <div id="app"></div>

    <script type="text/javascript">
      const app=document.getElementById('app');
    </script>
  </body>
</html>

你可以继续使用 DOM 的一系列方法来创建一个 h1 标签元素,h1 元素中可以包含任何你希望展示的文本。

<!-- index.html -->
<html>
  <body>
    <div id="app"></div>

    <script type="text/javascript">
      // 定位到 id 为 app 的元素
      const app=document.getElementById('app');

      // 创建一个 h1 元素
      const header=document.createElement('h1');

      // 创建一个文本节点
      const headerContent=document.createTextNode(
        'Develop. Preview. Ship.  ',
      );

      // 将文本节点添加到 h1 元素中去
      header.appendChild(headerContent);

      // 将 h1 元素添加到 id 为 app 的元素中去
      app.appendChild(header);
    </script>
  </body>
</html>

至此,你可以打开浏览器来预览一下目前的成果,不出意外的话,你应该可以看到一行使用 h1 标签的大字,写道:Develop. Preview. Ship.

HTML vs DOM

此时,如果你打开浏览器的代码审查功能,你会注意到在 DOM 中已经包含了刚才创建的 h1 标签,但源代码的 html 中却并没有。换言之,你所创建的 html 代码中与实际展示的内容是不同的。



这是因为 HTML 代码中展示的是初始化的页面内容,而 DOM 展示的是更新后的页面内容,这里尤指你通过 JavaScript 代码对 HTML 所改变后的内容。

使用 JavaScript 来更新 DOM,是非常有用的,但也往往比较繁琐。你写了如下那么多内容,仅仅用来添加一行 h1 标签。如果要编写一个大一些的项目,或者团队开发,就感觉有些杯水车薪了。

<!-- index.html -->
<script type="text/javascript">
  const app=document.getElementById('app');
  const header=document.createElement('h1');
  const headerContent=document.createTextNode('Develop. Preview. Ship.  ');
  header.appendChild(headerContent);
  app.appendChild(header);
</script>

以上这个例子中,开发者花了大力气来“指导”计算机该如何做事,但这似乎并不太友好,或者有没有更友好的方式让计算机迅速理解我们希望达到的样子呢?

命令式 vs 声明式编程

以上就是一个很典型的命令式编程,你一步一步的告诉计算机该如何更新用户界面。但对于创建用户界面,更好的方式是使用声明式,因为那样可以大大加快开发效率。相较于编写 DOM 方法,最好有种方法能声明开发者想要展示的内容(本例中就是那个 h1 标签以及它包含的文本内容)。

换句话说就是,命令式编程就像你要吃一个披萨,但你得告诉厨师该如何一步一步做出那个披萨;而声明式编程就是你告诉厨师你要吃什么样的披萨,而无需考虑怎么做。

React 正是那个“懂你”的厨师!

React:一个声明式的 UI 库

作为一个 React 开发者,你只需告诉 React 你希望展示什么样的页面,而它会自己找到方法来处理 DOM 并指导它正确地展示出你所要的效果。

小问答:你觉得以下哪句话更像声明式?
A:我要吃一盘菜,它要先放花生,然后放点鸡肉丁,接着炒一下...
B:来份宫保鸡丁


0x02 快速入门 React

要想在项目中使用 React,最简单的方法就是从外部 CDN(如:http://unpkg.com)引入两个 React 的包:

  • react:React 核心库
  • react-dom:提供用于操作 DOM 特定方法的库
<!-- index.html -->
<html>
  <body>
    <div id="app"></div>

    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <script type="text/javascript">
      const app=document.getElementById('app');
    </script>
  </body>
</html>

这样就无需使用纯 JavaScript 来直接操作 DOM 了,而是使用来自 react-dom 中的 ReactDOM.render() 方法来告诉 React 在 app 标签中直接渲染 h1 标签及其文本内容。

<!-- index.html -->
<html>
  <body>
    <div id="app"></div>

    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <script type="text/javascript">
      const app=document.getElementById('app');
      ReactDOM.render(<h1>Develop. Preview. Ship.  </h1>, app);
    </script>
  </body>
</html>

但当你在浏览器中运行的时候,它会报一个语法错误:

因为代码中的 <h1>Develop. Preview. Ship. </h1&gt; 并不是 JavaScript 代码,而是 JSX。

什么是 JSX

JSX 是一种 JS 的语法扩展,它使得你可以用类 HTML 的方式来描述界面。你无需学习 HTML 和 JavaScript 之外的新的符号和语法等,只需要遵守以下三条规则即可:

  • 返回一个单根节点的元素,如:
<!-- 你可以使用 div 标签 -->
<div>
  <h1>Hedy Lamarr's Todos</h1>
  <img 
    src="https://i.imgur.com/yXOvdOSs.jpg" 
    alt="Hedy Lamarr" 
    className="photo"
  />
  <ul>
    ...
  </ul>
</div>

<!-- 你也可以使用空标签 -->
<>
  <h1>Hedy Lamarr's Todos</h1>
  <img 
    src="https://i.imgur.com/yXOvdOSs.jpg" 
    alt="Hedy Lamarr" 
    className="photo"
  />
  <ul>
    ...
  </ul>
</>
  • 关闭所有标签
<!-- 诸如 img 必须自关闭 <img />,而包围类标签必须成对出现 <li></li> -->
<>
  <img 
    src="https://i.imgur.com/yXOvdOSs.jpg" 
    alt="Hedy Lamarr" 
    className="photo"
   />
  <ul>
    <li>Invent new traffic lights</li>
    <li>Rehearse a movie scene</li>
    <li>Improve the spectrum technology</li>
  </ul>
</>
  • 使用驼峰命名(camalCase)方式
<!-- 如 stroke-width 必须写成 strokeWidth,而 class 由于是 react 的关键字,因此替换为 className
<img 
  src="https://i.imgur.com/yXOvdOSs.jpg" 
  alt="Hedy Lamarr" 
  className="photo"
/>

JSX 并不是开箱即用的,浏览器默认情况下是无法解释 JSX 的,所以你需要一个编译器(compiler),诸如 Babel,来将 JSX 代码转换为普通的浏览器能理解的 JavaScript。

在项目中添加 Babel

复制粘贴以下脚本到 index.html 文件中:

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

另外,你还需要告诉 Babel 需要转换哪些代码,为需要转换的代码添加类型 type="text/jsx"

<html>
  <body>
    <div id="app"></div>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <!-- Babel Script -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
      const app=document.getElementById('app');
      ReactDOM.render(<h1>Develop. Preview. Ship.  </h1>, app);
    </script>
  </body>
</html>

现在可以再次回到浏览器中刷新页面来确认是否能成功展示了。



使用声明式的 React,你只编写了以下代码:

<script type="text/jsx">
  const app=document.getElementById("app")
  ReactDOM.render(<h1>Develop. Preview. Ship.  </h1>, app)
</script>

而命令式代码如此前编写的:

<script type="text/javascript">
  const app=document.getElementById('app');
  const header=document.createElement('h1');
  const headerContent=document.createTextNode('Develop. Preview. Ship.  ');
  header.appendChild(headerContent);
  app.appendChild(header);
</script>

相比较后不难发现,你节省了很多重复冗余的工作。

这就是 React,一款富含可重用代码,为你节省时间和提高效率的工具。

目前,你还无需过多关注究竟 React 用了什么神奇的魔法实现这样的功能。当然如果你感兴趣的话,可以参考 React 的官方文档中的 UI Tree 和 render 两个章节。

React 核心概念

在真正上手 React 项目之前,还有三个最重要的 React 核心概念需要理解

  • 组件(Components)
  • 参数(Props)
  • 状态(State)

在后续的章节中我们将逐一学习以上三个核心概念。


0x03 使用组件(Components)来建立界面

一个用户界面可以被分割成更小的部分,我们称之为“组件”。它是自包含、可重用的代码块,你可以把它想象成乐高玩具,独立的砖块可以拼装成更大的组合结构。如果你要更新界面的某一部分,你可以仅更新特定的组件或“砖块”。



模块化让你的代码更具有可维护性,因为你可以轻易地添加、修改、删除特定的组件而无需改动程序的其他部分。React 组件其实就是用 JavaScript 编写的,接下来我们将学习如何编写一个从 JavaScript 原生到 React 的组件。

创建组件

在 React 中,组件就是函数,我们在 script 中插入一个 header 方法:

<script type="text/jsx">
  const app=document.getElementById("app")
  function header() {}
  ReactDOM.render(<h1>Develop. Preview. Ship.  </h1>, app)
</script>

组件函数返回一个界面元素(即我们前面所提到过的单根节点的元素),可以使用 JSX 语法,如:

<script type="text/jsx">
  const app=document.getElementById("app")

  function header() {
     return (<h1>Develop. Preview. Ship.  </h1>)
   }

  ReactDOM.render(, app)
</script>

然后将 header 传入 ReactDOM.render 的第一个参数中去:

ReactDOM.render(header, app)

但如果你现在刷新浏览器预览效果的话,将会报错,因为还需要做两件事。

首先,React 组件必须以大写字母开头:

// 首字母大写
function Header() {
  return <h1>Develop. Preview. Ship.  </h1>;
}

ReactDOM.render(Header, app);

其次,你在使用 React 组件时,也需要使用 JSX 的语法格式,将组件名称用 <> 扩起来:

function Header() {
  return <h1>Develop. Preview. Ship.  </h1>;
}
<br/>ReactDOM.render(<Header />, app);

嵌套组件

一个应用程序通常包含多个组件,有的甚至是组件嵌套的组件。例如我们来创建一个 HomePage 组件:

function Header() {
  return <h1>Develop. Preview. Ship.  </h1>;
}

function HomePage() {
  return <div></div>;
}

ReactDOM.render(<Header />, app);

然后将 Header 组件放入 HomePage 组件中:

function Header() {
  return <h1>Develop. Preview. Ship.  </h1>;
}

function HomePage() {
  return (
    <div>
      {/* 嵌套的 Header 组件 */}
      <Header />
    </div>
  );
}

ReactDOM.render(<HomePage />, app);

组件树

你可以继续以这种方式嵌套 React 组件,以形成一个更大的组件。



比如上图中,你的顶层组件是 HomePage,它下面包含了一个 Header,一个 ARTICLE 和一个 FOOTER。然后 HEADER 组件下又包含了它的子组件等等。

这样的模块化使得你可以在项目的许多其他地方重用组件。


0x04 参数(Props)与数据展示

如果你重用 Header 组件,你将显示相同的内容两次。

function Header() {
  return <h1>Develop. Preview. Ship.  </h1>;
}

function HomePage() {
  return (
    <div><br/>      <Header />
      <Header />
    </div>
  );
}

但如果你希望在标题中传入不同的文本,或者你需要从外部源获取数据再进行文本的设置时,该怎么办呢?

普通的 HTML 元素允许你通过设置对应标签的某些重要属性来修改其实际的展示内容,比如修改 <img>src 属性就能修改图片展示,修改 <a>href 就能改变超文本链接的目标地址。

同样的,你可以通过传入某些属性值来改变 React 的组件,这被称为参数(Props)



与 JavaScript 函数类似,你可以设计一个组件接收一些自定义的参数或者属性来改变组件的行为或展示效果,并且还允许通过父组件传递给子组件。

?? 注意:React 中,数据流是顺着组件数传递的。这被称为单向数据流

使用参数

HomePage 组件中,你可以传入一个自定义的 title 属性给 Header 组件,就如同你传入了一个 HTML 属性一样。

// function Header() {
//   return <h1>Develop. Preview. Ship.  </h1>
// }

function HomePage() {
  return (
    <div>
      <Header title="Hello React" />
    </div>
  );
}

// ReactDOM.render(<HomePage />, app)

然后,Header 作为子组件可以接收这些传入的参数,可在组件函数的第一个入参中获得。

function Header(props) {
  return <h1>Develop. Preview. Ship.  </h1>
}

你可以尝试打印 props 来查看它具体是什么东西。

function Header(props) {
    console.log(props) // { title: "Hello React" }
    return <h1>Hello React</h1>
}

由于 props 是一个 JS 对象,因此你可以使用对象解构来展开获得对象中的具体键值。

function Header({ title }) {
    console.log(title) // "Hello React"
    return <h1>Hello React</h1>
}

现在你就能使用 title 变量来替换 h1 标题中的文本了。

function Header({ title }) {
    console.log(title) // "Hello React"
    return <h1>title</h1>
}

但当你打开浏览器刷新页面时,你会发现页面上展示的是标题文本是 title,而不是 title 变量的值。这是因为 React 不能对纯文本进行解析,这就需要你额外地对文本展示做一些处理。

在 JSX 中使用变量

要在 JSX 中使用你定义的变量,你需要使用花括号 {} ,它允许你在其中编写 JavaScript 表达式

function Header({ title }) {
    console.log(title) // "Hello React"
    return <h1>{ title }</h1>
}

通常,它支持如下几种方式:

  • 输出对象属性 { props.title }
  • 模板字符串 {`Hello ${title}`}
  • 函数返回值 { getTitle() }
  • 三元表达式 { title ? title : "Hello" }

这样你就能根据参数输出不同的标题文本了:

function Header({ title }) {
  return <h1>{title ? title : 'Hello React!'}</h1>;
}

function Page() {
  return (
    <div>
      <Header title="Hello JavaScript!" />
      <Header title="Hello World!" />
    </div>
  );
}

通过列表进行迭代

通常我们会有一组数据需要展示,它以列表形式呈现,你可以使用数组方法来操作数据,并生成在样式上统一的不同内容。

例如,在 HomePage 中添加一组名字,然后依次展示它们。

function HomePage() {
  const names=['Mike', 'Grace', 'Margaret'];

  return (
    <div>
      <Header title="Develop. Preview. Ship.  " />
    </div>
  );
}

然后你可以使用 Arraymap 方法对数据进行迭代输出,并使用箭头函数来将数据映射到每个迭代项目上。

function HomePage() {
  const names=['Mike', 'Grace', 'Margaret'];

  return (
    <div>
      <Header title="Develop. Preview. Ship.  " />
      <ul>
        {names.map((name)=> (
          <li>{name}</li>
        ))}
      </ul>
    </div>
  );
}

现在如果你打开浏览器查看,会看到一个关于缺少 key 属性的警告。这是因为 React 需要通过 key 属性来唯一识别数组上的元素来确定最终需要在 DOM 上更新的项目。通常我们会使用 id,但本例子中你可以直接使用 name,因为它们的值也是唯一不同的。

function HomePage() {
  const names=['Mike', 'Grace', 'Margaret'];

  return (
    <div>
      <Header title="Develop. Preview. Ship.  " />
      <ul>
        {names.map((name)=> (
          <li key={name}>{name}</li>
        ))}
      </ul>
    </div>
  );
}

0x05 使用状态(State)来增加交互性

首先,我们看下 React 是如何通过状态和事件处理来帮助我们增加交互性的。

我们在 HomePage 组件中添加一个“喜欢”按钮:

function HomePage() {
  const names=['Mike', 'Grace', 'Margaret'];

  return (
    <div>
      <Header title="Develop. Preview. Ship.  " />
      <ul>
        {names.map((name)=> (
          <li key={name}>{name}</li>
        ))}
      </ul>

      <button>Like</button>
    </div>
  );
}

监听事件

要让按钮在被点击的时候做些什么时,你可以在按钮上添加 onClick 事件属性:

function HomePage() {
  // ...
  return (
    <div>
      {/* ... */}
      <button onClick={}>Like</button>
    </div>
  );
}

在 React 中,属性名称都是驼峰命名式的,onClick 是许多事件属性中的一种,还有一些其他的事件属性,如:输入框会有 onChange ,表单会有 onSubmit 等。

处理事件

你可以定义一个函数来处理以上一些事件,当它被触发的时候。事件处理函数可以在返回语句之前定义,如:

function HomePage() {
  // ...

  function handleClick() {
    console.log("I like it.")
  }

  return (
    <div>
      {/* ... */}
      <button onClick={}>Like</button>
    </div>
  )
}

接着你就可以在 onClick 中调用 handleClick 方法了。

function HomePage() {
  //    ...
  function handleClick() {
    console.log('I like it.');
  }

  return (
    <div>
      {/* ... */}
      <button onClick={handleClick}>Like</button>
    </div>
  );
}

状态和钩子

React 里有一系列钩子函数(Hooks),你可以利用钩子函数在组件中创建状态,你可以把状态理解为在界面上随时间或者行为变化的一些逻辑信息,通常情况下是由用户触发的。



你可以通过状态来存储和增加用户点击喜欢按钮的次数,在这里我们可以使用 React 的 useState 钩子函数。

function HomePage() {
  React.useState();
}

useState 返回一个数组,你可以使用数组解构来使用它。

function HomePage() {
  const []=React.useState();

  // ...
}

该数组的第一个值是状态值,你可以定义为任何变量名称:

function HomePage() {
  const [likes]=React.useState();

  // ...
}

该数组的第二个值是状态修改函数,你可以定义为以 set 为前缀的函数名,如 setLikes

function HomePage() {
  const [likes, setLikes]=React.useState(); 
  // likes 存储了喜欢被点击的次数;setLikes 则是用来修改该次数的函数

  // ...
}

同时,你可以在定义的时候给出 likes 的初始值

function HomePage() {
  const [likes, setLikes]=React.useState(0);
}

然后你可以尝试查看你设置的初始值是否生效

function HomePage() {
  // ...
  const [likes, setLikes]=React.useState(0);

  return (
    // ...
    <button onClick={handleClick}>Like({likes})</button>
  );
}

最后,你可以在每次按钮被点击后调用 setLikes 方法来更新 likes 变量的值。

function HomePage() {
  // ...
  const [likes, setLikes]=React.useState(0);

  function handleClick() {
    setLikes(likes + 1);
  }

  return (
    <div>
      {/* ... */}
      <button onClick={handleClick}>Likes ({likes})</button>
    </div>
  );
}

点击喜欢按钮将会调用 handleClick 方法, 然后调用 setLikes 方法将更新后的新值传入该函数的第一个入参中。这样 likes 变量的值就变成了新值

状态管理

本章节仅对状态做了简单的介绍,举例了 useState 的用法,你可以在后续的学习中了解到更多的状态管理和数据流处理的方法,更多的内容可以参考官网的 添加交互性 和 状态管理 两个章节进行更深入的学习。

小问答:请说出参数(Props)和状态(State)的区别?

0x06 长路漫漫,继续前行

到此为止,你已了解了 React 的三大核心概念:组件、参数和状态。对这些概念的理解越深刻,对今后开发 React 应用就越有帮助。学习的旅程还很漫长,途中若有困惑可以随时回看本文,或阅读以下主题文章进行更深入的学习:

  • Render and Commit (reactjs.org)
  • Referencing Values with Refs (reactjs.org)
  • Managing State (reactjs.org)
  • Passing Data Deeply with Context (reactjs.org)
  • React APIs (reactjs.org)

React 学习资源

React 的学习资源层出不穷,你可以在互联网上搜索 React 来获取无穷无尽的资源,但在我看来最好的仍是官方提供的《React 文档》,它涵盖了所有你需要学习的主题。

最好的学习方法就是实践。

eact 作为前端开发的明星框架,其灵魂之一就是 JSX。今天就来详细分析一下什么是 JSX,以及如何在开发中高效使用它。作为一名程序员,这些技巧你不可不知!

1. 什么是 JSX?

JSX 是 JavaScript XML 的缩写,它是 React 独有的一种语法扩展,让你在 JavaScript 代码中写类似 HTML 的标记。这不仅让代码可读性更强,还能直观地描述 UI 结构。

示例代码:

const element=<h1>Hello, world!</h1>;

2. 渲染虚拟 DOM(元素)

React 通过将 JSX 转换为虚拟 DOM,再通过对比虚拟 DOM 和实际 DOM 的差异,来高效地更新 UI。

示例代码:

import React from 'react';
import ReactDOM from 'react-dom';

const element=<h1>Hello, world!</h1>;
ReactDOM.render(element, document.getElementById('root'));

代码解析:

  • React 和 ReactDOM 是 React 核心库,用于创建组件和渲染 DOM。
  • element 是一个简单的 JSX 元素。
  • ReactDOM.render 将 element 渲染到实际 DOM 中的 id 为 root 的节点上。

3. JSX 的使用

JSX 可以嵌套、包含表达式,还能直接用于条件渲染和数组渲染,灵活又强大。

嵌套示例:

const element=(
  <div>
    <h1>Hello, world!</h1>
    <p>This is a paragraph.</p>
  </div>
);

包含表达式示例:

const user={
  firstName: 'Harper',
  lastName: 'Perez'
};

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const element=<h1>Hello, {formatName(user)}!</h1>;

4. JSX 的语法规则

大小写区分:

  • 小写字母开头的 JSX 标签默认为 HTML 元素,如 <div>。
  • 大写字母开头的 JSX 标签默认为 React 组件,如 <MyComponent>。

示例代码:

const MyComponent=()=> {
  return <h1>Hello, Component!</h1>;
};

ReactDOM.render(<MyComponent />, document.getElementById('root'));

必须被包裹:

  • 所有 JSX 元素必须被一个根元素包裹。
  • 可以使用空标签 <> </> 来包裹不想增加额外节点的元素。

示例代码:

const element=(
  <>
    <h1>Hello, world!</h1>
    <p>This is a paragraph.</p>
  </>
);

表达式用{}包裹:

  • 在 JSX 中使用 JavaScript 表达式必须用 {} 包裹。

示例代码:

const name='Josh Perez';
const element=<h1>Hello, {name}!</h1>;

总结

掌握 JSX 是深入学习 React 的起点,它不仅提升代码可读性,还能大大提高开发效率。从简单的标签嵌套到复杂的表达式嵌套,JSX 让你在编写 UI 组件时如鱼得水。大家赶紧动手试一试吧!

#如何自学IT#