zoukankan      html  css  js  c++  java
  • 从零开始学前端,React框架背后的核心机制和原理JSX

    什么是React

    React是起源于Facebook的一个前端框架,用于构建用户界面的JavaScript库,Facebook用来探索一种更加高效优雅的Javascript MVC框架来架设Instagram网站用的,后来觉得还不错,于是开源出来。

    React特性

    • 声明式

    React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。

    以声明式编写 UI,可以让你的代码更加可靠,且方便调试。

    • 组件化

    创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。

    组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。

    • 一次学习,随处编写

    无论你现在正在使用什么技术栈,你都可以随时引入 React 来开发新特性,而不需要重写现有代码。

    React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。

    安装最基础的环境NPM

    React依赖NPM(Node.js Package Manager)来安装,所以我们可以先安装Node.Js环境。

    Node.Js会自动带NPM组件和自动安装配套的可选组件,非常简便。

    v14.15.1 LTS长期支持版:https://nodejs.org/dist/v14.15.1/node-v14.15.1-x64.msi

    官网:https://nodejs.org/zh-cn/

    安装推荐的集成编辑器Visual Studio Code

    最新Stable版:https://aka.ms/win32-x64-user-stable

    官网:https://code.visualstudio.com/

    创建项目目录并进行NPM初始化

    创建一个名为zeroreact的文件夹,用Visual Studio Code来进行打开,在终端界面,执行如下命令,进行NPM初始化。

    npm init
    

    一路回车就行了,创建后还能继续编辑的。

    初始化完成之后,会看到当前项目根目录会新建一个叫package.json的文件,其内容如下:

    {
      "name": "zeroreact",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo "Error: no test specified" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    

    其实中main是一个Node项目指向的开始文件,但是没有也可以。

    创建Visual Studio Code的调式配置Launch.json

    切换到Visual Studio Code左侧的运行菜单,点击创建Launch.json文件,选择你中意的可选浏览器平台即可。

    当前项目根目录会自动生成一个Launch.json配置文件,这个配置就是调式配置,它指向了调式项目时启动哪个平台。

    其内容如下(以Edge:Launch为例):

    {
        // 使用 IntelliSense 了解相关属性。 
        // 悬停以查看现有属性的描述。
        // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
        "version": "0.2.0",
        "configurations": [
            {
                "type": "pwa-msedge",
                "request": "launch",
                "name": "Launch Chrome against localhost",
                "url": "http://localhost:8080",
                "webRoot": "${workspaceFolder}"
            }
        ]
    }
    

    配置好了之后,启动调式即可打开对应的浏览器,并且自动打开其中url配置所对应的地址值。

    关于React实际必选项JSX

    虽然React官方说,React可以不依赖JSX去运行,但是实际上JSX可以说是必选项。

    JSX简介

    而JSX是什么呢?

    JSX是一种JavaScript的语法扩展,运用于React架构中,其格式比较像是模版语言,但事实上完全是在JavaScript内部实现的。元素是构成React应用的最小单位,JSX就是用来声明React当中的元素,React使用JSX来描述用户界面。

    注意的是,JSX的特性更接近JavaScript而不是HTML,所以React DOM使用camelCase(小驼峰)命名来定义属性的名称,而不是使用HTML的属性名称。例如:class变成了className,而tableindex则对应着tableIndex

    JSX基本格式:

    • 简单闭合
    const element = <img src={user.avatarUrl} />;
    
    • 嵌套闭合
    const element = (
        <div>
            <h1>Hello!</h1>
            <h2>Good to see you here.</h2>
        </div>
    );
    

    这些有趣的标签语法既不是字符串也不是 HTML。

    它被称为JSX,是一个JavaScript的语法扩展。建议在React中配合使用JSX,JSX可以很好地描述UI应该呈现出它应有交互的本质形式。JSX可能会使人联想到模版语言,但它具有JavaScript的全部功能。

    JSX可以生成React“元素”。

    安装JSX所需的WebPack和Babel组件

    React中的JSX实际上是通过Babel组件,把JSX的代码最终翻译成普通的Javascript代码的。

    Babel转译器会把JSX转换成一个名为React.createElement()的方法调用

    安装WebPack

    什么是WebPack呢?

    WebPack其核心在于让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系。不仅仅是JavaScript文件,我们的CSS、图片、json文件等等在webpack中都可以被当做模块来使用,webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)。在打包的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等等操作,接着我们只要处理最后那个js文件即可。

    为什么要用WebPack呢?

    WebPack其实它是一个Javascript的打包工具,它的输入和产出在正常情况下都是Javascript的文件,它最大的作用就是帮助我们把Javascript里面的import和require,把多文件打包成一个单个的Js文件。所以说呢,webpack往往是由一个文件作为入口,这个文件可能会import一些东西,可能会require一些东西,不管你用哪种写法都是可以的,然后它最终把它变成一个单个的大的文件,这样呢,比较符合我们在Web上的性能还有发布各方面的一些需求,当然WebPack它还同时承载了很多工具,其中就包括接下来会说到的前端重要的工具Babel。

    npm install webpack webpack-cli --save-dev
    

    通过以上命令,同时安装webpackwebpack-cli两个组件,并且--save-dev表示这两个组件会被加到package.json中的devDependencies节点中去。

    注意:这时候,如果你想直接使用webpack指令,是不可行的,因为它没有被全局安装,如果你需要可以通过npm install xxx -g这种形式去安装,但是目前已经不被推荐了。

    我们可以采用被推荐的如下命令来调用WebPack:

    npx webpack
    

    然后你会看到有个报错。

    其实,webpack命令是执行了,但是因为我们没有给webpack做对应的配置及入口文件,所以它最终执行失败。

    这时候,我们可以在根目录新建一个叫webpack.config.js的文件,根据Node的标准,我们可以用module.exports的写法,内如如下:

    module.exports={
        entry:{
            main: './main.js'
        }
    }
    

    其中entry就是入口的意思,然后main是默认入口,main的指向我们可以暂时先给一个main.js文件,同时我们需要在根目录新建一个空的main.js文件。

    完成以上配置之后,我们可以再次执行

    npx webpack
    

    即可看到打包成功的信息了,同时你会发现会新建一个dist目录来输出WebPack最终打包好的Js文件。

    打包后的main.js是一段被压缩的JS代码,可阅读性呢不是很好,如果我们想看到更加可阅读性的JS代码,可以在webpack.config.js增加开发阶段的配置:

    module.exports={
        entry:{
            main: './main.js'
        },
        mode: 'development',
        optimization:{
            minimize: false
        }
    }
    

    新增配置项mode=developmentoptimization.minimize被设置成false之后,再次执行npx webpack会看到main.js中会得到更加可阅读性的JS代码。

    安装Babel

    什么是Babel呢?

    Babel是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

    为什么使用Babel呢?

    Babel这个工具是一个把新版本JS文件翻译成老版本JS文件的这样一种工具。

    Babel在WebPack里面是以Loader的形式去使用的,WebPack允许我们使用Loader去定制各种各样的文件,比如说,原则上WebPack只能打包普通的JS文件,但是我们如果想把CSS文件以某种形式打包成JS文件的话,那么我们就可以写一个CSS-Loader,如果我们想把HTML当作一个JS文件去打包进来,那我们就可以写一个HTML-Loader,而这个Loader也可以是独立的包,我们只要在WebPack的配置里面配一下就可以了。

    接下来我们,安装Babel组件,执行如下命令:

    npm install --save-dev babel-loader @babel/core @babel/preset-env
    

    这里安装三个组件babel-loader@babel/core@babel/preset-env,以空格隔开即可。

    配置并使用Babel

    我们可以通过WebPackModule来配置Babel组件,其中Module中重要的概念就是Rules,它是一个数组,里面可以是一个对象,如下配置新增一个关于babel-loader的rule规则配置。

    module:{
            rules:[
                {
                    test: /.js$/,
                    use: {
                        loader: 'babel-loader',
                        options:{
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        }
    

    如配置所示,test中是一个针对所有JS文件的正则表达式,会匹配所有的JS文件,那么遇到JS文件会执行use中的loader应用,这里指定了loaderBabel-Loader,这样所有的JS文件都会走Babel-Loader来完成一次翻译。同时,我们还需要给Babel-Loader配置一个presets(注意presets也是一个数组值),其值是前面我们安装的@babel/preset-env,这里的presets可以理解为它是一系列Babel的config的一种快捷方式。

    完成上诉Babel-Loader配置之后,我们可以来实现下真正的翻译。

    main.js中,我们可以加入一段JS代码,如下:

    for(let i of [1,2,3])
    {
        console.log(i);
    }
    

    再次执行npx webpack之后,我们将看到打包之后的main.js

    翻译后的JS代码变成了一个eval的for方法,这将是最终被执行的JS代码。

    为了看到这段JS最终执行的效果,我们在dist目录里面新建一个main.html,来引用最终生成main.js,并且F12看下输入效果。

    可以看到,如预期结果,依次输出了1,2,3

    配置并启用用于翻译JSX的Babel插件

    默认Babel组件是没有能力来处理JSX的。

    如果我们此时在main.js里面写一个JSX,那么它会报错。

    let a = <div />
    

    但是有一个Babel插件是可以的,叫babel/plugin-transform-react-jsx,接下来我们安装并配置它。

    npm install @babel/plugin-transform-react-jsx --save-dev 
    

    然后我们还需要在Babel-Loader中配置这个Plugin,在Options配置节点中,新建一个名为plugins的节点,这也是一个数字,里面填入我们刚刚安装的Babel插件@babel/plugin-transform-react-jsx

    module:{
        rules:[
            {
                test: /.js$/,
                use: {
                    loader: 'babel-loader',
                    options:{
                        presets: ['@babel/preset-env'],
                        plugins: ['@babel/plugin-transform-react-jsx']
                    }
                }
            }
        ]
    }
    

    再次执行npx webpack,就会发现,带JSX语法的JS已经可以正常翻译了,得到后的JS如图:

    这里我们看到<div/>被翻译为一个React.createElement("div", null);的方法了。

    备注:

    可以看到,默认它会被一个叫React的函数来调用createElement方法。

    这里我们也可以通过配置来修改调React.createElement这个名字,这里@babel/plugin-transform-react-jsx支持一个叫pragma的参数,这里可以指定你喜欢的名字,比如我这里的zero_react_create

    plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'zero_react_create'}]]
    

    这样翻译后会变成你自定义的名字:

    探索JSX语法糖机制

    带属性的JSX

    let a = <div id="a" class="c"/>
    

    翻译后:

    var a = zero_react_create("div", {
      id: "a",
      "class": "c"
    });
    

    带子节点的JSX

    let a = <div id="a" class="c">
        <div/>
        <div/>
        <div/>
    </div>
    

    翻译后:

    var a = zero_react_create("div", {
      id: "a",
      "class": "c"
    }, zero_react_create("div", null), zero_react_create("div", null), zero_react_create("div", null));
    

    发现子节点都会追加到后面了。

    构造zero_react_create函数,并且输出它

    function zero_react_create(tagName, attributes, ...children){
        return document.createElement(tagName);
    }
    
    window.a = <div id="a" class="c">
        <div/>
        <div/>
        <div/>
    </div>
    

    翻译执行后,可以在浏览器console里面输入a,回车看到a的输入值:

    应用子节点,并且输出它

    function zero_react_create(tagName, attributes, ...children){
    
        let e = document.createElement(tagName);
    
        for(let p in attributes){
            e.setAttribute(p, attributes[p]);
        }
    
        for(let child of children){
            e.appendChild(child);
        }
    
        return e;
    }
    
    window.a = <div id="a" class="c">
        <div/>
        <div/>
        <div/>
    </div>
    

    翻译后执行:

    子节点中包含值,并且输出它

    function zero_react_create(tagName, attributes, ...children){
    
        let e = document.createElement(tagName);
    
        for(let p in attributes){
            e.setAttribute(p, attributes[p]);
        }
    
        for(let child of children){
            if(typeof child === 'string'){
                child = document.createTextNode(child);
            }
            e.appendChild(child);
        }
    
        return e;
    }
    
    window.a = <div id="a" class="c">
        <div>abc</div>
        <div/>
        <div/>
    </div>
    

    这里的变化是,如果子节点类型是个字符串,那我们就把Child变成一个文本节点。

    翻译后:

    将输出挂载到Body里面

    为了挂载到Body,我们现在main.html新建好Body标签:

    <body>
        <script src="main.js"></script>
    </body>
    

    然后修改JSX代码如下:

    document.body.appendChild(<div id="a" class="c">
        <div>abc</div>
        <div/>
        <div/>
    </div>);
    

    翻译后执行,就可以把我们JSX描述的HTML及数据,成功的以DOM形式挂载到Boby里面了

    这样我们就实现了一种完全基于实DOM的zero_react_create方法。

    探索JSX自定义组件机制

    初探自定义标签

    在JSX中有个规定,如果你的Tag是小写,比如div,它就认为这是一种原生的标签,如果是大写开头的,那就认为是自定义组件,比如我们将div改成MyComponent

    document.body.appendChild(<MyComponent id="a" class="c">
        <div>abc</div>
        <div/>
        <div/>
    </MyComponent>);
    

    翻译后运行,会得到一个报错。

    在这里,MyComponent变成了我们自定义的一个对象,或者Class或者函数

    这里我们先做Class处理。

    class MyComponent{
        
    }
    

    翻译后执行,会得到一个报错

    这是因为zero_react_create方法中TagName这时候已经不是一个原始的标签字符串了,在执行let e = document.createElement(tagName);会报错,因为这时候TagName已经变成了一个对象。

    这里,我们修改下zero_react_create方法的判断。

    function zero_react_create(tagName, attributes, ...children){
    
        let e;
        if(typeof tagName === 'string'){
            e = document.createElement(tagName);
        }
        else
        {
            e = new tagName;
        }
    
        for(let p in attributes){
            e.setAttribute(p, attributes[p]);
        }
    
        for(let child of children){
            if(typeof child === 'string'){
                child = document.createTextNode(child);
            }
            e.appendChild(child);
        }
    
        return e;
    }
    

    翻译运行后发现报错如下:

    这里是因为,e这时候已经不是一个原生对象了,那我们自然有很多原始对象可以支持的就运行不了了,所以这里e.setAttribute(p, attributes[p]);自然会报错。

    这时候,我们可以给所有原生的DOM对象,都加一个Wrapper,让它可以正确的执行下去。

    开启自定义组件之路

    我们新建一个名为zero-react.js的文件,把前面main.js那个zero_react_create函数搬过来,并且Export出来。

    export function zero_react_create(tagName, attributes, ...children){
    
        let e;
        if(typeof tagName === 'string'){
            e = document.createElement(tagName);
        }
        else
        {
            e = new tagName;
        }
    
        for(let p in attributes){
            e.setAttribute(p, attributes[p]);
        }
    
        for(let child of children){
            if(typeof child === 'string'){
                child = document.createTextNode(child);
            }
            e.appendChild(child);
        }
    
        return e;
    }
    

    并且在main.jsimport文件zero-react.js

    import { zero_react_create } from './zero-react.js'
    

    重新编写zero-react.js

    对外公开一个Component组件,所有自定义组件继承它

    export class Component{
    
        constructor(){
            this.props = Object.create(null);
            this.children = [];
            this._root = null;
        }
    
        setAttribute(name, value){
            this.props[name] = value;
        }
    
        appendChild(component){
            this.children.push(component);
        }
    
        get root(){
            if(!this._root){
                this._root = this.render().root;
            }
            return this._root;
        }
    }
    

    新建一个TextWrapper来处理纯文本子节点。

    class TextWrapper{
    
        constructor(content){
            this.root = document.createTextNode(content);
        }
    }
    

    新建一个ElementWrapper来处理原生标签节点。

    class ElementWrapper{
    
        constructor(tagName){
            this.root = document.createElement(tagName);
        }
    
        setAttribute(name, value){
            this.root.setAttribute(name, value);
        }
    
        appendChild(component){
            this.root.appendChild(component.root);
        }
    }
    

    替换原来zero_react_create函数中的两个实现

    其中把e = document.createElement(tagName);替换成e = new ElementWrapper(tagName);,把child = document.createTextNode(child);替换成child = new TextWrapper(child);,得到如下新的zero_react_create函数

    export function zero_react_create(tagName, attributes, ...children){
    
        let e;
        if(typeof tagName === 'string'){
            e = new ElementWrapper(tagName);
        }
        else
        {
            e = new tagName;
        }
    
        for(let p in attributes){
            e.setAttribute(p, attributes[p]);
        }
    
        for(let child of children){
            if(typeof child === 'string'){
                child = new TextWrapper(child);
            }
            e.appendChild(child);
        }
    
        return e;
    }
    

    最后对外公开一个render函数

    export function render(component, parentElement){
        parentElement.appendChild(component.root);
    }
    

    修改main.jsMyComponent的实现。

    import { render, Component, zero_react_create } from './zero-react.js'
    
    class MyComponent extends Component{
        render(){
            return <div>zero component</div>
        }
    }
    
    render(<MyComponent id="a" class="c">
        <div>abc</div>
        <div/>
        <div/>
    </MyComponent>, document.body);
    

    翻译后,运行可得:

    但是我们还看不到子节点元素,这时候如果把子节点元素传进来:

    class MyComponent extends Component{
        render(){
            return <div><h1>zero component</h1>
                {this.children}
            </div>
        }
    }
    

    运行会报错:

    因为这时候子节点会把当作一个数组传进去,但是原来child = new TextWrapper(child);处是没有能力处理这种情况的。

    这时候,我们需要改造下这里。

    export function zero_react_create(tagName, attributes, ...children){
    
        let e;
        if(typeof tagName === 'string'){
            e = new ElementWrapper(tagName);
        }
        else
        {
            e = new tagName;
        }
    
        for(let p in attributes){
            e.setAttribute(p, attributes[p]);
        }
    
        let insertChilder = (children) => {
            for(let child of children){
                if(typeof child === 'string'){
                    child = new TextWrapper(child);
                }
                if((typeof child === 'object') && (child instanceof Array)){
                    insertChilder(child);
                }
                else{
                    e.appendChild(child);
                }
            }
        }
        insertChilder(children);
    
        return e;
    }
    

    翻译执行后,就可以看到成功的处理了带多个子节点的情况了。

    于是我们就打造了一个属于自己的自定义组件处理引擎了。

    附件

  • 相关阅读:
    js 修改 title keywords description
    添加第一次进入网站动画
    懒加载
    一些正则
    对图片进行剪切,保留原始比例
    JS判断是否是微信页面,判断手机操作系统(ios或android)并跳转到不同下载页面
    判断网页是否再微信内置浏览器打开
    数字转汉字大写
    java 反射机制 之 getConstructor获取有参数构造函数 然后newInstance执行有参数的构造函数
    web实训项目(快递e栈)-----04项目实现的基本流程
  • 原文地址:https://www.cnblogs.com/craigtaylor/p/14092607.html
Copyright © 2011-2022 走看看