Micro View of Allium

Micro View of Allium

Jeango

Jeango 2/3/2021, 3:44:07 PM

JSX & Babel 入门

JSX 代表 JavaScript + XML,它的意义是实现了 XML 和脚本代码共存,在脚本直接使用 XML 节点来编写程序中的组件,这是非常有趣的技术:

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

通过转译程序,JSX 编写的元素会转译成原生的脚本代码一致实现,JSX 编写了什么样的 Element 元素,转译后就有对应的脚本代码实现,这是解放生产力的一大技术。这样的混合技术使得开发者完全摆脱了旧式字符拼接 DOM 结构的束缚,或者使用 jQuery 来零散地管理 DOM,使用 JSX 可以很方便地直接编写 Elements 供 React 渲染。

JSX 模板语法,可以记住两条规则,花括号 {...} 内的是 JavaScript 代码,在箭括号 <...> 内的就是标签组件定义。要在标签元素中写代码就需要使用花括号,在代码中可以直接写 XML 标。在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式。

除 HTML 标签组件外,其它组件名称必须以大写字母开头。React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签,而 <Welcome /> 则代表一个组件,并且需在其作用域内使用。

因为 JSX 转译后就是输出 JavaScript 代码实现,所以可以省略不用 JSX,直接用 React 提供的 DOM 构建方法来写模板,比如一个 JSX 写的一个标题元素的等价 JSX 和 JS 代码:

// JSX style
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

// JS style - Babel Translated
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

以上,只是经过 Babel 的转译得到的结果,可以使用 Babel 将任意符合语法格式的 JSX 代码进行转译,可以在线尝试 Babel REPL - Read Eval Print Loop 交互式解释器。

如原始 JSX 以下这样:

// JSX - JavaScript + XML
const element = <h1 abc={efg}>Hello, world!</h1>;

const Fc = (props) => {
  return props.children;
}

class Cc extends Any.Component {
    bar = 'hello';
    static foo = "Apple"
    constructor(props){
        this.props = props;
    }
    render(){
        return <div>{this.props.children}</div>;
    }
}

const elements = (props) => {
    let abc = ["A", 1, <Fc name="xyz"/>, <Cc name="xyz"/>];
    return (
        <>
        {abc.length>2 && <b>multiple</b>}
        <div>{abc.map( (it) => <b>{it}</b>) }</div>
        </>
    );
}

React.createElement 方法原型参考:

  export function createElement(tag : any, props ?: any, ...children : any[]) : any

对应 JavaScript - Babel React Preset 的转译结果类似以下,根据不同的配置会有差异:

"use strict";

function _defineProperty(obj, key, value) { 
    if (key in obj) { 
        Object.defineProperty(obj, key, { 
            value: value, 
            enumerable: true, 
            configurable: true, 
            writable: true 
        }); 
    } else { 
        obj[key] = value; 
    } 
    return obj;
}

const element = /*#__PURE__*/React.createElement("h1", {
  abc: efg
}, "Hello, world!");

const Fc = props => {
  return props.children;
};


class Cc extends Any.Component {
  constructor(props) {
    this.props = props;
  }

  render() {
    return /*#__PURE__*/React.createElement("div", null, this.props.children);
  }
}

_defineProperty(Cc, "foo", "Apple");


const elements = props => {
  let abc = ["A", 1, 
    /*#__PURE__*/React.createElement(Fc, {name: "xyz"}), 
    /*#__PURE__*/React.createElement(Cc, {name: "xyz"})];

  return /*#__PURE__*/React.createElement(
    React.Fragment, // Tag
    null,           // Props and children below
    abc.length > 2 && 
    /*#__PURE__*/React.createElement("b", null, "multiple"), 
    /*#__PURE__*/React.createElement("div", null, abc.map(it => /*#__PURE__*/React.createElement("b", null, it))));
};

可以看到,纯粹的尖括号对应的是 React.Fragment 代表的抽象节点,原先的 JavaScript 还是保留不变,只是嵌入的 XML 标签转换成了 React.createElement 方法创建的对象,这些就是 Babel 做的转译工作。而 createElement 方法接收 tag 参数中,可以接收的类型可以分为三类:

  • Plain Text 即字符串或数值这些会原样渲染的内容;
  • Function Component 函数组件,将原函数传入;
  • Class Component 类组件,将原函数传入,组件的属性传递方式不变;

最早,React 提供了 JSX 的转译程序,但是后来 Babel 在转译方法的贡献做得更强,现在前端开发的转译工作基本由 Babel 来做了。

当然,可以有 JavaScript + XML,也可以 TypeScript + XML,只需要搭配相应的开发环境,将 jsx 文件改成 tsx 文件来做开发,并且后者带来的静态类型检查才是强大的。

要搭建一个现代的前端开发环境配套的工具有很多,比如 Grunt / Gulp / Webpack / Broccoli,都是要解决前端工程化问题。这个主题很大,这里关注基于 Webpack 的 React JSX 开发。现在业界领先的 ES6 编译工具 Babel 随着作者被 Facebook 招入麾下,已经内置了对 JSX 的支持,我们只需要配置 Babel 一个编译工具就可以了,配合 webpack 非常方便,现成的可以直接使用 Create React App 脚手架工具。

Babel 作为一个 JavaScript 转译器,它可以做的工作很多:

  • 语法转换,Babel 能够转换 JSX 语法为相应的 JavaScript,让浏览器能运行!
  • 类型标注 Type Annotations,支持 Flow 和 TypeScript!
  • 因为浏览器进化速度跟不上 JavaScript 速度,可以通过 Browser Polyfill 在目标环境中添加缺失的特性,使用 @babel/polyfill 模块;
  • 源码重构 codemods...

但基本的还编译器原理,Babel 的转译过程分为三个阶段:

  • 对原代码进行解析 parsing 生成抽象语法树等;
  • 根据预置设定进行转译 transforming;
  • 最后生成相应代码 generating;

Babel 作为一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中,这解决了一个很大的问题,使得技术更新慢的浏览器也可以用上功能强大、发展更快的脚本技术。

Babel 对不同的功能集合支持会有相应的一个预置模块 preset,安装相应的预置模块来实现不同的功能,最新版 Babel 7 提供了以下几种预置:

Babel 7 的更新:

  • Babel 7 的 npm 包正式更名为 @babel/core @babel/cli 等。
  • Babel 7 不支持 Node.js 0.10, 0.12, 4 和 5 版本。
  • Babel 7 移除了@babel/polyfill 内的 polyfills 支持,现在 @babel/polyfill 几乎只是 core-js v2 的映射。
  • babylon 现在被重命名为 @babel/parser
  • 去除包名上的年份。
  • 将 react 和 flow 预设分离。

为当前 npm 项目安装 Babel CLI:

npm install --save-dev @babel/core @babel/cli

设置项目脚本命令:

  {
    "name": "my-project",
    "version": "1.0.0",
+   "scripts": {
+     "build": "babel src -d lib"
+   },
    "devDependencies": {
      "@babel/cli": "^7.0.0"
    }
  }

运行 Babel 转译程序:

npm run build

Babel 能够通过 React preset 转换 JSX 语法,和 babel-sublime 插件一起使用还可以把语法高亮的功能提升到一个新的水平。使用 Node + Webpack 开发环境,通过以下命令安装此 preset-react 还有 babel-loader:

npm install --save-dev @babel/preset-react
npm install --save-dev babel-loader

并将 @babel/preset-react 添加到你的 Babel 配置文件中 .babelrc:

{
  "presets": ["env", "stage-0", "react"],
  "plugins": ["transform-runtime"]
}

添加 babel-loader 配置项:

module: {
    rules: [
      { test: /\.js|jsx$/, use: 'babel-loader', exclude: /node_modules/ }
    ]
}

Babel 可以删除类型注释,也就是说支持 Flow 和 TypeScript 这样的类型检查编译工具,查看 Flow preset 或 TypeScript preset 了解如何使用。务必牢记 Babel 不做类型检查,你仍然需要安装 Flow 或 TypeScript 来执行类型检查的工作。

通过以下命令安装 flow preset 或 typescript preset

npm install --save-dev @babel/preset-flow
npm install --save-dev @babel/preset-typescript

这两个工具都有相当简单的方法给逐个文件应用:

  • Flow: 向文件顶部添加注释 // @flow 来激活 Flow 静态类型检查;
  • TypeScript:将文件扩展名 .js/.jsx 更改为 .ts/.tsx 来激活 TypeScript 静态类型检查;

例子:

// @flow
function square(n: number): number {
  return n * n;
}

// typescript
function Greeter(greeting: string) {
    this.greeting = greeting;
}

可以在 HTML 页面中直接引用 Babel CDN 脚本文件支持 JSX 语法,在编写组件的脚本区,只需要使用 <script type="text/babel" /> 脚本标签即可,这种方法很方便,能快速编写示例直接在浏览器中运行,而不用搭建开发环境。

注意,通过 src 引用外部 jsx 脚本文件時,需要使用 AJAX,即 XMLHttpRequest 加載,需要以 Web 服務方式运行,或者修改浏览器的跨站策略 CORS policy 以放行这种跨站文件文件。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title> Demo for React JSX! </title>
        <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"> </script> 
        <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"> </script>
        <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"> </script>
    </head>
    <body>
        <div id="example"> </div>
    </body>
</html>

<script>
    // import React from "react";
    // import {Component} from 'react';
    let Component = React.Component;
    const mountNode = document.getElementById('example')
</script>

<script type="text/babel">

    let pstyle = { "border": "1px solid #cccccc"};
    let dstyle = { 
        background:"#222222", 
        color: "white", 
        padding: "2px 16px 16px", 
        margin: "20%", 
        textAlign: "center",
        borderRadius: "32px",
    };

    function Demo(props){
        return <div style={dstyle}>{props.children} {props.dt}</div>
    }

    function Wrap(props){
        return <div>
            <Demo {...props} >
                <p style={pstyle}>Hello</p>
            </Demo>
        </div>
    }

    function render(){
        setInterval(evt=>{
            let dt = Date.now();
            let wrap = <Wrap key="d001" dt={dt} />;
            ReactDOM.unmountComponentAtNode(mountNode)
            ReactDOM.render(wrap, mountNode);
        }, 1000);
    }
    render();
    
</script>

在线运行示例: