Python: WEB开发-react

React项目构建

项目依赖安装

将项目开发基础文件 react-mobx-starter-master.zip 解压缩,并用这个目录作为项目根目录。

在项目根目录中,执行下面的命令,就会自动按照package.json的配置安装依赖模块。

npm install #或者 $ npm i

安装完成后,会生成一个目录 node_modules ,里面是安装的所有依赖的模块

项目整体说明

项目目录结构

.
├── .babelrc
├── .gitignore
├── index.html #测试时用的
├── jsconfig.json
├── LICENSE
├── .npmrc
├── package.json
├── README.md
├── src
│ ├── App.js
│ ├── AppState.js
│ ├── index.html #打包时用的
│ └── index.js
├── node_modules
│ ├── ...
├── webpack.config.dev.js
└── webpack.config.prod.js

配置文件详解

package.json

$ npm init 产生的文件,里面记录项目信息,所有项目依赖

版本管理

可指定到对应的git仓库

"repository": { 
    "type": "git", 
    "url": "https://192.168.124.135/react-mobx/react-mobx-starter.git"
}
项目脚本管理
"scripts": {
    "test": "jest",
    "start": "webpack-dev-server --config webpack.config.dev.js --hot --inline",
    "build": "rimraf dist && webpack -p --config webpack.config.prod.js"
}

scripts属性是一个字典描述,key是名字,value是能够让npm运行的脚本内容。

运行时只要输入 npm run key_name 即可。

start 指定启动webpack的dev server开发用WEB Server, 主要提供2个功能:静态文件支持、自动刷新和 热替换HMR(Hot Module replacement) 。对应 npm run start

  • –hot 启动HMR
    • HRM可以在应用程序运行中替换、添加、删除模块,而无需重载页面,只把变化部分替换掉。不使用HMR则自动刷新会导致这个页面刷新
  • –inline 默认模式, 使用HMR的时候建议开启inline模式。热替换时会有消息显示在控制台

build 使用webpack构建打包。对应 npm run build

项目依赖

devDependencies 开发时依赖,不会打包到目标文件中。对应 npm install xxx --save-dev

例如babel的一些依赖,只是为了帮我们转译代码,没有必要发布到生产环境中.

dependencies 运行时依赖,会打包到项目中。对应 npm install xxx --save

开发是依赖
"devDependencies": {
    "babel-core": "^6.24.1",
    "babel-jest": "^19.0.0",
    "babel-loader": "^6.4.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.4.0",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "css-loader": "^0.28.0",
    "html-webpack-plugin": "^2.28.0",
    "jest": "^19.0.2",
    "less": "^2.7.2",
    "less-loader": "^4.0.3",
    "react-hot-loader": "^4.3.12",
    "rimraf": "^2.6.2",
    "source-map": "^0.5.6",
    "source-map-loader": "^0.2.1",
    "style-loader": "^0.16.1",
    "uglify-js": "^2.8.22",
    "webpack": "^2.4.1",
    "webpack-dev-server": "^2.4.2"
}

版本号指定

  • 版本号 ,只安装指定版本号的
  • ~版本号 ,例如 ~1.2.3 表示安装1.2.x中最新版本,不低于1.2.3,但不能安装1.3.x
  • ^版本号 ,例如 ^2.4.1 表示安装2.x.x最新版本不低于2.4.1
  • latest ,安装最新版本
  • 1.2.x或1.x
  • * 任意版本
  • 1.2.7 || >=1.2.9 <2.0.0, 可以安装1.2.7,或者1.2.9本身或者之上的1.2.x版本,但不能是2.x版本。

babel 转译

  • 因为开发用了很多ES6语法。从6.x开始babel拆分成很多插件,需要什么引入什么。
  • babel-core 核心
  • babel-loader webpack的loader,webpack是基于loader装载器的。把HTML,CSS,JS,图片,字体等资源看做模块,就可以像 JS一样加载这些模块。文件类型不同,就需要各种各样的loader。babel-loader加载ES6+代码转换为ES5.
  • babel-preset-xxx 预设的转换插件
  • babel-plugin-transform-decorators-legacy 下面的课程用到了装饰器,这个插件就是转换装饰器用的

css样式相关

  • css-loader、style-loader样式表加载器
  • less、less-loader是对less文件的支持
  • react-hot-loader 是react热加载插件,希望改动保存后,直接在页面上直接反馈出来,不需要手动刷新

source-map文件打包,js会合并或者压缩,没法调试,用它来看js原文件是什么。source-map-loader也需webpack的 loader

webpack 打包工具,2.4.1 发布于2017年4月,当前2.7.0发布于2017年7月。

webpack-dev-server 启动一个开发时用的server

运行时依赖
"dependencies": {
    "antd": "^3.10.9",
    "axios": "^0.16.1",
    "babel-polyfill": "^6.23.0",
    "babel-runtime": "^6.23.0",
    "mobx": "^4.6.0",
    "mobx-react": "^5.4.2",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "react-router": "^4.3.1",
    "react-router-dom": "^4.3.1"
}
  • antd 即 ant design,基于react实现,蚂蚁金服开源的react的UI库。做中后台管理非常方便.
  • axios 异步请求支持。
  • polyfill 解决浏览器api不支持的问题。写好polyfill就让你的浏览器支持,帮你抹平差异化
  • react开发的主框架
  • react-dom 支持DOM
  • react-router 支持路由
  • react-router-dom DOM绑定路由
  • mobx 状态管理库,透明化。
  • mobx-react mobx和react结合的模块。react和mobx是一个强强联合

babel配置

.babelrc babel转译的配置文件

{
  "presets": [
    "react",
    "env",
    "stage-0"
  ],
  "plugins": [
    "transform-decorators-legacy", 
    "transform-runtime",
    "react-hot-loader/babel"
  ]
}

webpack配置

webpack.config.dev.js

这是一个符合Commonjs的模块

module.exports 导出

  • devtool:'source-map'

    生成及如何生成source-map文件。

    source-map适合生成环境使用,会生成完成Sourcemap独立文件。

    由于在Bundle中添加了引用注释,所以开发工具知道如何找到Sourcemap。

  • entry入口

    描述入口。webpack会从入口开始,找出直接或间接的模块和库构建依赖树,最后输出为bundles文件中。

    entry如果是一个字符串,定义就是入口文件

    如果是一个数组,数组中最后一项是入口文件,其他项先加载,可以配置的热加载插件来自己刷新页面。

    如果是一个对象,可以支持多入口。也可是函数,返回以上的字符串或数组或对象,好处是动态生成。

  • output输出

    告诉webpack输出bundles到哪里去,如何命名。

    filename定义输出的bundle的名称

    path 输出目录是 __dirname+'dist' ,名字叫做 bundle.js 。 __dirname 是nodejs中获取当前js文件所在的目录名

    publicPath 可以是绝对路径或者相对路径,一般会以/结尾。/assets/表示网站根目录下assets目录下

  • resolve解析

    设置模块如何被解析。

    extensions 自动解析扩展名。'.js'的意思是,如果用户导入模块的时候不带扩展名,它尝试补全

  • module 模块

    如何处理不同的模块

    rules 匹配请求的规则,以应用不同的加载器、解析器等

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
                { loader: 'babel-loader' }
            ]
        }, {
            test: /\.css$/,
            use: [
                { loader: "style-loader" },
                { loader: "css-loader" },
            ]
         }, 
         {
            test: /\.less$/,
            use: [
                 { loader: "style-loader" },
                 { loader: "css-loader" },
                 { loader: "less-loader" }
            ]
        }
    ]
},

test 匹配条件的

exclude 排除的, /node_modules/打包排除目录。这一句一定要有,否则,编
译就把这个目录下所有文件也拿进来了,巨大无比。

use 使用模块的UseEntries列表中的loader

rules中对.js结尾的但不在node_modules目录的文件使用转译babel-loader

加载器:倒着写,先应用数组中的最后一个

style-loader通过 <style> 标签把css添加到DOM中
css-loader 加载css
less-loader 对less的支持

LESS

CSS好处简单易学,但是坏处是没有模块化、复用的概念,因为它不是语言。

LESS是一门CSS的预处理语言,扩展了CSS,增加了变量、Mixin、函数等开发语言的特性,从而简化了CSS的编写。

LESS本身是一套语法规则及解析器,将写好的LESS解析成CSS。LESS可以使用在浏览器端和服务器端。

范例:

/*test.less*/
@color: #4D926F; 

#header { 
     color: @color; 
} 
h2 { 
    color: @color; 
}

可以使用解析成如下的CSS

PS D:\project\pyproj\trae\react-mobx-starter-master> ./node_modules/.bin/lessc test.less       
#header {
  color: #4D926F;
}
h2 {
  color: #4D926F;
}

LESS在服务器端使用,需要使用LESS编译器, $ npm install less ,本项目目录已经安装过了

#编译输出到控制台
node_modules/.bin/lessc test.less
npx lessc test.less

#编译输出到文件
node_modules/.bin/lessc test.less test.css
npx lessc test.less test.css

plugins:webpack的插件

plugins: [
        new webpack.optimize.OccurrenceOrderPlugin(true),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.DefinePlugin({'process.env': {NODE_ENV: JSON.stringify('development')}})
  • HotModuleReplacementPlugin 开启HMR
  • DefinePlugin 全局常量配置

devServer,开发用server

devServer: {
     compress: true,
     port: 3000,
     publicPath: '/assets/',
     hot: true,
     inline: true,
     historyApiFallback: true,
     stats: {
        chunks: false
     },
     proxy: {
         '/api': {
             target: 'http://127.0.0.1:8000',
             changeOrigin: true,
             pathRewrite: {'^/api':''}
        }
    }
}
compress 启动gzip
port 启动端口3000
hot 启用HMR
proxy指定访问/api 开头URL都代理到 http://127.0.0.1:8000 去

vscode配置

jsconfig.json是vscode的配置文件,覆盖当前配置

以上是所有配置文件的解释。拿到这个文件夹后,需要修改name项目名称、 version版本、description描述,需要修改repository仓库地址,需要修改 author作者、license许可证信息。这些信息修改好之后,就可以开始开发了。

启动项目

在项目根目录,使用

$ npm run start 或 npm start

#package.json  start热加载选项--hot   --open 自动打开浏览器
  "scripts": {
    "test": "jest",
    "start": "webpack-dev-server --config webpack.config.dev.js --hot --inline --open",
    "build": "webpack -p --config webpack.config.prod.js"
  },

启动成功应该就可以访问了.

webpack使用babel转译、打包,较为耗时,需要等一会儿

React技术

简介

React是Facebook开发并开源的前端框架。

当时他们的团队在市面上没有找到合适的MVC框架,就自己写了一个Js框架,用来架设Instagram(图片分享社交网络)。2013年React开源。

React解决的是前端MVC框架中的View视图层的问题。

Virtual DOM

DOM(文档对象模型Document Object Model)

img_20250513_212850.png

将网页内所有内容映射到一棵树型结构的层级对象模型上,浏览器提供对DOM的 支持,用户可以是用脚本调用DOM API来动态的修改DOM结点,从而达到修改网页 的目的,这种修改是浏览器中完成,浏览器会根据DOM的改变重绘改变的DOM结点 部分。

修改DOM重新渲染代价太高,前端框架为了提高效率,尽量减少DOM的重绘,提出 了Virtual DOM。Virtual DOM是一个JavaScript对象,性能更高。所有的修改都 是先生成一个新的Virtual DOM,通过比较算法比较新旧Virtual DOM,得到差异 化VirtualDOM,将这部分差异更新到浏览器DOM,浏览器只需要渲染这部分变化 就行了。

React实现了DOM Diff算法可以高效比对Virtual DOM

支持JSX语法

JSX是一种JavaScript和XML混写的语法,是JavaScript的扩展

React.render(
    <div> 
        <div> 
            <div>content</div> 
        </div> 
    </div>, 
    document.getElementById('example')
);

测试程序

替换 /src/index.js 为下面的代码

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

class Root extends React.Component { 
    render() {
        return <div>Hello World</div>;
    }
}

ReactDom.render(<Root/>, document.getElementById('root'));

保存文件后,会自动编译,并重新装载刷新浏览器端页面

程序解释

import React from 'react'; 导入react模块。

import ReactDOM from 'react-dom'; 导入react的DOM模块。

class Root extends React.Component 组件类定义,从React.Component类上继承。这个类生成JSXElement对象即React元素 render() 渲染函数。返回组件中渲染的内容。注意,只能返回唯一 一个顶级元素回去。

ReactDom.render(<Root/>, document.getElementById('root')); 第一个参数是JSXElement对象,第二个是DOM的Element元素。将React元素添加到DOM的Element元素中并渲染。

还可以使用React.createElement创建react元素,第一参数是React组件或者一个HTML的标签名称(例如div、span)。

改写后代码为

import React from 'react';
import ReactDom from 'react-dom';
class Root extends React.Component {
  render() {
    //return <div>Hello World</div>;
    return React.createElement('div', null, 'Hello World');
  }
}
//ReactDom.render(<Root/>, document.getElementById('root'));
ReactDom.render(React.createElement(Root), document.getElementById('root'));

很明显JSX更简洁易懂,推荐使用JSX语法

增加一个子元素

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

class SubEle extends React.Component {
  render() {
    return <div>Sub content</div>;
  }
}

class Root extends React.Component {
  render() {
    return (
      <div>
      <h2>Hello World</h2>
      <br />
      <SubEle />
    </div >);
  }
}

ReactDom.render(<Root />, document.getElementById('root'));

注意:

  • React组件的render函数return,只能是一个顶级元素
  • JSX语法是XML,要求所有元素必须闭合,注意 <br /> 不能写成 <br>

JSX规范

React快速入门:https://zh-hans.react.dev/learn

  • 约定标签中首字母小写就是html标记,首字母大写就是组件
  • 要求严格的HTML标记,要求所有标签都必须闭合
  • br也应该写成 <br /> ,/前留一个空格
  • 单行省略小括号,多行请使用小括号
  • 元素有嵌套,建议多行,注意缩进
  • JSX表达式:表达式使用{}括起来,如果大括号内使用了引号,会当做字符串处理,例如 <div>{'2>1?true:false'}</div> 里面的表达式成了字符串

组件状态state **

每一个React组件都有一个状态属性state,它是一个JavaScript对象,可以为它定义属性来保存值。

如果状态变化了,会触发UI的重新渲染。使用setState()方法可以修改state值。

注意:state是每个组件自己内部使用的,是组件自己的属性。必须是state,不能是其它名字。

依然修改/src/index.js

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

class Root extends React.Component {
  // 定义一个state对象
  state = {domain: 'cici.com', host: 'www'} // 构造函数中定义state 
  render() { // render 用来呈现state或props值
    // this.state.host = 'python'; // 可以修改属性值
    // this.setState({host:'python'}); //不可对还在更新中的state使用setSate. state改变会继续引起render, 递归了
    //Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor).
    setTimeout(()=> this.setState({host:this.state.host + '*'}), 3000)
    console.log('Root render ~~~~')
    return <div>
      Welcome to {this.state.host}.{this.state.domain}
      <hr />
      </div>
  }
}

ReactDom.render(<Root />, document.getElementById('root'));

如果将 ·this.state.host = ‘python’· 改为 ·this.setState({host:'python'});· 就会出警告。

可以使用延时函数 setTimeout(() => this.setState({ host: 'python' }), 5000); 即可。

使用延时函数好吗? 不好,一般不推荐这么做,但是它确实不递归。

复杂的状态例子

先看一个网页

<html>
<head>
    <script type="text/javascript">
        function getEventTrigger(e) {
            x = e.target; // 从事件中获取元素
            alert("触发的元素的id是:" + x.id);
        }
    </script>
</head>
<body>
    <div id="t1" onmousedown="getEventTrigger(event)">
        点击这句话,会触发一个事件,并弹出一个警示框
    </div>
</body>
</html>

div的id是t1,鼠标按下事件捆绑了一个函数,只要鼠标在对象上按下就会触发调用getEventTrigger函数,浏览器会送给它一个参数event。event是事件对象,当事件触发时,event包含触发这个事件的对象

HTML DOM的JavaScript事件

属性 此事件发生在何时
onabort 图像的加载被中断
onblur 元素失去焦点
onchange 域的内容被改变
onclick(常用) 当用户点击某个对象时调用的事件句柄
ondblclick 当用户双击某个对象时调用的事件句柄
onerror 在加载文档或图像时发生错误
onfocus 元素获得焦点. 例如文本框中光标在里面时聚焦了
onkeydown 某个键盘按键被按下
onkeypress 某个键盘按键被按下并松开
onkeyup 某个键盘按键被松开
onload 一张页面或一幅图像完成加载
onmousedown 鼠标按钮被按下
onmousemove 鼠标被移动
onmouseout 鼠标从某元素移开
onmouseover 鼠标移到某元素之上
onmouseup 鼠标按键被松开
onreset 重置按钮被点击
onresize 窗口或框架被重新调整大小
onselect 文本被选中
onsubmit 确认按钮被点击
onunload 用户退出页面

使用React实现上面的传统的HTML

import React from 'react';
import ReactDom from 'react-dom';
class Toggle extends React.Component {

  state = { flag: true }; // 类中定义state 
  handleClick(event) {
    console.log(event.target.id);
    console.log(event.target === this);
    console.log(this);
    console.log(this.state);
    this.setState({ flag: !this.state.flag });
  }

  render() {/* 注意一定要绑定this onClick写成小驼峰 */
    return <div id="t1" onClick={this.handleClick.bind(this)}>
      点击这句话,会触发一个事件。{this.state.flag.toString()}
    </div>;
  }
}
class Root extends React.Component {
  // 定义一个对象
  state = { p1: 'www.python', p2: '.org' }; // 构造函数中定义state 
  render() {
    return (
      <div>
        <div>Welcome to {this.state.p1}{this.state.p2}</div>
        <br />
        <Toggle />
      </div>);
  }
}
ReactDom.render(<Root />, document.getElementById('root'));

Toggle类

  • 它有自己的state属性,就给组件自身增加属性的。
  • 当render完成后,网页上有一个div标签,div标签对象捆绑了一个click事件的处理函数
  • 如果通过点击左键,就触发了click方法关联的handleClick函数,在这个函数里将状态值改变
    • 使用this.state.flag = !this.state.flag 方式改变,不会触发render函数执行
    • 使用this.setState({flag: !this.state.flag}) 方式,会触发该组件和其子组件render函数执行。

注意

  • {this.handleClick.bind(this)},不能外加引号
  • this.handleClick.bind(this) 一定要绑定this,否则当触发捆绑的函数时,this是函数执行的上下文决定的,this已经不是触发事件的对象。 或将handleClick定义为箭头函数也能解决this问题。
  • console.log(event.target.id),取回的产生事件的对象的id,但是这不是我 们封装的组件对象。所以,console.log(event.target===this)是false。所 以这里一定要用this,而这个this是通过绑定来的
  • this写在类中,始终指的是React组件实例本身

React中的事件

  • 使用小驼峰命名
  • 使用JSX表达式,表达式中指定事件处理函数
  • 不能使用return false,如果要阻止事件默认行为,使用event.preventDefault()

属性props

props就是组件的属性properties。

把React组件当做标签使用,可以为其增加属性,如下

<Toggle name="school" parent={this} />

为上面的Toggle元素增加属性:

  • name = "school" ,这个属性会作为一个单一的对象传递给组件,加入到组件的props属性中
  • parent = {this} ,注意这个this是在Root元素中,指的是Root组件本身。parent属性是我们自己添加的。
  • 使用JSX语法为Toggle增加子元素,这些子元素也会被加入Toggle组件的props.children中。 如果要显示这些内容,就必须在Toggle组件中从props中提取出来。
import React from 'react';
import ReactDom from 'react-dom';

class Toggle extends React.Component {
  state = { flag: true }; // 类中定义state 
  handleClick(event) {
    console.log(event.target.id);
    console.log(event.target === this);
    console.log(this);
    console.log(this.state);
    this.setState({ flag: !this.state.flag });
  }

  render() {/* 注意一定要绑定this onClick写成小驼峰 */
    return <div id="t1" onClick={this.handleClick.bind(this)}
      style={{backgroundColor:'white', margin: '10' + 'px'}}
    >
      点击这句话,会触发一个事件。{this.state.flag.toString()}<br />
      显示props<br />
      {this.props.name} : {this.props.parent.state.p1}{this.props.parent.state.p2}
      {this.props.children}
    </div>;
  }
}

class Root extends React.Component {
  // 定义一个对象 
  state = { p1: 'www.python', p2: '.org' }; // 构造函数中定义state
  render() {
    return (
      <div  style={{backgroundColor: '#f0f0f0'}}>
        <div>Welcome to {this.state.p1}{this.state.p2}</div>
        <br />
        <Toggle name="school" parent={this}>{/*自定义2个属性通过props传给Toggle组件对象*/}
          <hr />{/*子元素通过props.children访问*/}
          <span>我是Toggle元素的子元素</span>{/*子元素通过props.children访问*/}
        </Toggle>
      </div>);
  }
}

ReactDom.render(<Root />, document.getElementById('root'));

尝试修改props中的属性值,会抛出 TypeError: Cannot assign to read only property 'name' of object '#<Object>'异常。也就是说props在组件内部不能修改,只读。

范例:复杂点的

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

class Sub extends React.Component {
  render() {// render 用来呈现state或props值
    return <div>sub content ++++</div>
  }
}

class Toggle extends React.Component {
  state = { flag: true }; // 类中定义state 

  handleClick(e) {
    console.log('toggle handle click ~~~~')
    // e.preventDefault(); // 阻止默认事件
    // console.log(e.target);
    // console.log(e.target.id);
    // console.log(this) //this 的坑
    this.setState({ flag: !this.state.flag });
    this.props.parent.setState({host: this.props.parent.state.host + '@'})
  }

  render() {// render 用来呈现state或props值
    console.log('Toggle render ~~~~')
    console.log(this.state, this.props)
    return <div id="toggle" onClick={this.handleClick.bind(this)}
      style={{backgroundColor:'white', margin: '10' + 'px'}}
    >
      我是子组件Toggle. Flag = {this.state.flag.toString()}
      <hr />
      子组件区域显示props = {this.props.school} - {this.props.parent.state.host}
      <div style={{width:'80%', border:'1px solid red'}}>
        Toggle的子元素显示区域: {this.props.children}
      </div>
    </div>
  }
}

class Root extends React.Component {
  state = { domain: 'cici.com', host: 'www' }; // 构造函数中定义state

  handleClick(e) {
    console.log('root click')
    this.setState({host:this.state.host + '*'})
  }
  render() {
    console.log('Root render ++++')
    return (
      <div  style={{backgroundColor: '#f0f0f0'}} 
      onDoubleClick={this.handleClick.bind(this)}>
        Welcome to {this.state.host}.{this.state.domain}
        <hr />
        <Toggle school='python' parent={this}>
          <div>toggle的子元素
            <div>cell</div>
          </div>
          <div>第二个子元素
            <Sub />
          </div>
        </Toggle>
      </div>);
  }
}

ReactDom.render(<Root />, document.getElementById('root'));

单向数据流

  • state是私有private的属于组件自己的属性,组件外无法直接访问。可以修改state,但是建议使用setState方法。
  • props是公有public属性,组件外也可以访问,但组件内只读。
  • props是一种组件外部传入向组件内部传入数据的一种方式,只不过采用标签属性的方式。
  • 在React中,数据只能由外层通过props传递给内层。这种称为单向数据流Single Data Flow。

构造器constructor

使用ES6的构造器,要提供一个参数props,并把这个参数使用super传递给父类

import React from 'react';
import ReactDom from 'react-dom';
class Toggle extends React.Component {
  //state = { flag: true }; // 类中定义state
  constructor(props) {
    super(props); // 一定要调用super父类构造器,否则报错
    this.state = { flag: true }; // 类中定义state
  }

  handleClick(event) {
    console.log(event.target.id);
    console.log(event.target === this);
    console.log(this);
    console.log(this.state);
    this.setState({ flag: !this.state.flag });
  }

  render() {/* 注意一定要绑定this onClick写成小驼峰 */
    return <div id="t1" onClick={this.handleClick.bind(this)}>
      点击这句话,会触发一个事件。{this.state.flag.toString()}<br />
      显示props<br />
      {this.props.name} : {this.props.parent.state.p1}{this.props.parent.state.p2}
      {this.props.children}
    </div>;
  }
}

class Root extends React.Component {
  // 定义一个对象 
  state = { p1: 'www.python', p2: '.org' }; // 构造函数中定义state 

  render() {
    return (
      <div>
        <div>Welcome to {this.state.p1}{this.state.p2}</div>
        <br />
        <Toggle name="school" parent={this}>{/*自定义2个属性通过props传给Toggle组件对象*/}
          <hr />{/*子元素通过props.children访问*/}
          <span>我是Toggle元素的子元素</span>{/*子元素通过props.children访问*/}
        </Toggle>
      </div>);
  }
}
ReactDom.render(<Root />, document.getElementById('root'));

组件的生命周期

组件的生命周期可分成三个状态:

  • Mounting:已插入真实 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真实 DOM

组件的生命周期状态,说明在不同时机访问组件,组件正处在生命周期的不同状态上。

在不同的生命周期状态访问,就产生不同的方法。

生命周期的方法如下:

  • 装载组件触发
    • componentWillMount 在渲染前调用,只会在装载之前调用一次。此方法在V17即将过期。建议在constructor中初始化state。
    • componentDidMount : 在第一次渲染后调用,只在客户端。之后组件已经生 成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想 和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送A JAX请求等操作(防止异部操作阻塞UI)。只在装载完 成后调用一次,在render之后。
  • 更新组件触发。这些方法不会在首次render组件的周期调用
    • componentWillReceiveProps(nextProps) 在组件接收到一个新的prop时被调用。这个方法在初始化render时不会被调用。在V17中也即将过期。
    • shouldComponentUpdate(nextProps,nextState)返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。
      • 可以在你确认不需要更新组件时使用
      • 如果设置为false,就是不允许更新组件,那么componentWillUpdate、componentDidUpdate不会执行
    • componentWillUpdate(nextProps, nextState) 在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。在V17中也即将过期。
    • componentDidUpdate(prevProps, prevState) 在组件完成更新后立即调用。在初始化时不会被调用。
  • 卸载组件触发
    • componentWillUnmount在组件从 DOM 中移除的时候立刻被调用
img_20250515_160508.png

由图可知 constructor构造器是最早执行的函数

组件构建好之后,如果更新组件的state或props(注意在组件内props是只读的),就会在render渲染前触发一系列的 更新生命周期函数

因此,重新编写/src/index.js 构造两个组件,在子组件Sub中,加入所有生命周期函数

下面的例子添加是装载、卸载组件的生命周期函数

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

class Sub extends React.Component {
  constructor(props) {
    console.log('Sub constructor')
    super(props); // 调用父类构造器   
    this.state = { count: 0 };
  }

  handleClick(event) {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    console.log('Sub render');
    return (<div id="sub" onClick={this.handleClick.bind(this)}>
      Sub's count = {this.state.count}
    </div>);
  }

  componentWillMount() {
    // constructor之后,第一次render之前  
    console.log('Sub componentWillMount');
  }

  componentDidMount() {
    // 第一次render之后  
    console.log('Sub componentDidMount');
  }

  componentWillUnmount() {
    // 清理工作  
    console.log('Sub componentWillUnmount');
  }
}

class Root extends React.Component {
  constructor(props) {
    console.log('Root Constructor')
    super(props); // 调用父类构造器
    // 定义一个对象
    this.state = {};
  }

  render() {
    return (
      <div>
        <Sub />
      </div>);
  }
}

ReactDom.render(<Root />, document.getElementById('root'));  

上面可以看到顺序是

constructor -> componentWillMount -> render -> componentDidMount -—state或props改变-—> render

增加 更新组件函数

为了演示props的改变,为Root元素增加一个click事件处理函数

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

class Sub extends React.Component {
  constructor(props) {
    console.log('Sub constructor')
    super(props); // 调用父类构造器
    this.state = { count: 0 };
  }

  handleClick(event) {
    this.setState({ count: this.state.count + 1 });// 不要用++
  }
  render() {
    console.log('Sub render');
    return (<div style={{
      height: 200 + 'px', color: 'red', backgroundColor: '#f0f0f0',
      padding: '20px'
    }}>
      <a id="sub" onClick={this.handleClick.bind(this)} style={{ backgroundColor: 'white' }}>
        Sub's count = {this.state.count}
      </a>
    </div>);
  }

  componentWillMount() {
    // constructor之后,第一次render之前
    console.log('Sub componentWillMount');
  }

  componentDidMount() {
    // 第一次render之后
    console.log('Sub componentDidMount');
  }

  componentWillUnmount() {
    // 清理工作
    console.log('Sub componentWillUnmount');
  }

  componentWillReceiveProps(nextProps) {
    // props变更时,接到新props了,交给shouldComponentUpdate。
    // props组件内只读,只能从外部改变
    console.log(this.props);
    console.log(nextProps);
    console.log('Sub componentWillReceiveProps', this.state.count);
  }

  shouldComponentUpdate(nextProps, nextState) {
    // 是否组件更新,props或state方式改变时,返回布尔值,true才会更新
    console.log('Sub shouldComponentUpdate', this.state.count, nextState);
    return true; // return false将拦截更新
  }

  componentWillUpdate(nextProps, nextState) {
    // 同意更新后,真正更新前,之后调用render 
    console.log('Sub componentWillUpdate', this.state.count, nextState);
  }

  componentDidUpdate(prevProps, prevState) {
    // 同意更新后,真正更新后,在render之后调用
    console.log('Sub componentDidUpdate', this.state.count, prevState);
  }
}

class Root extends React.Component {
  constructor(props) {
    console.log('Root Constructor')
    super(props); // 调用父类构造器  
    // 定义一个对象 
    this.state = { flag: true, name: 'root' };
  }

  handleClick(event) {
    this.setState({
      flag: !this.state.flag,
      name: this.state.flag ? this.state.name.toLowerCase() : this.state.name.toUpperCase()
    });
  }

  render() {
    return (
      <div id="app" onClick={this.handleClick.bind(this)}>
        My Name is {this.state.name}
        <hr />
        <Sub /> {/*父组件的render,会引起下一级组件的更新流程,导致props重新发送,即使子组件props没有
  改变过*/}
      </div>);
  }
}

ReactDom.render(<Root />, document.getElementById('root'));

componentWillMount 第一次装载,在首次render之前。例如控制state、props。

componentDidMount 第一次装载结束,在首次render之后。例如控制state、props。

componentWillReceiveProps 在组件内部,props是只读不可变的,但是这个函 数可以接收到新的props,可以对props做一些处理,this.props = {name:‘roooooot’};这就是偷梁换柱。componentWillReceiveProps触发,也 会走shouldComponentUpdate。

shouldComponentUpdate 判断是否需要组件更新,就是是否render,精确的控制渲染,提高性能。

componentWillUpdate 在除了首次render外,每次render前执行,componentDidUpdate在render之后调用。

不过,大多数时候,用不上这些函数,这些钩子函数是为了精确的控制。

修改Root组件render中的这一句为 <div id="root" onDoubleClick={this.handleClick.bind(this)}> ,可以看到点击Sub中红色的文字,Root不会重绘。

如果子组件和父组件使用了相同的事件,可以认为点击子组件也是点击了父组件, 父组件重绘,就会把子组件props更新,引起子组件组件更新流程,就会从 componentWillReceiveProps开始执行。如果子组件自己修改自己的state,不会 执行componentWillReceiveProps。

范例:

import React from 'react';
import ReactDOM, {render} from 'react-dom';

// bundle.js <script src="bundle.js"></script>
// 未来bundler.js下载并相当于运行在index.html中
class Sub extends React.Component{ // 只要出现组件,就必须导入react
  constructor(props) { // 构造函数
    super(props); // 调用父类的构造函数,必须写
    console.log('组件实例化了')
    this.state = {count: 0}// 定义state状态
  }
  componentWillMount() {
   console.log('组件将要挂载到DOM上') 
  }

  // 第一次调用render

  componentDidMount() {
    console.log('组件已经挂载到DOM上')
  }
  componentWillUnmount() {
    console.log('组件已经从DOM上卸载了') 
  }
  /////////////////////////
  componentWillReceiveProps(nextProps) {
    // props变更时,接到新props了,交给shouldComponentUpdate。
    // props组件内只读,只能从外部改变
    console.log('当前值', this.props);
    console.log('未来要更新值', nextProps); // 待更新的props
    console.log('componentWillReceiveProps,props发生了变化,注意pops的改变来自外部,传入')
  }
  shouldComponentUpdate(nextProps, nextState) {
    // 是否组件更新,props或state方式改变时,返回布尔值,true才会更新
    console.log('当前值', this.state, this.props);
    console.log('未来要更新值', nextState, nextProps);
    console.log('shouldComponentUpdate, 由props或state发生变化引起执行')
    // return false; // 就算state和props有更新,到底结束,不更新组件
    return true;
  }
  componentWillUpdate(nextProps, nextState) {
    // 同意更新后,真正更新前,之后调用render 
    console.log('当前值', this.state, this.props);
    console.log('未来要更新值', nextState, nextProps);
    // nextProps = {'name': 'componentWillUpdate nextProps'}
    // nextProps.name = 'python.org' // 改了没有用. 因为props只读
    nextState.count = nextState.count + 100; // 改了有效果
    console.log('未来要更新值', nextState, nextProps);
    console.log('componentWillUpdate, 由props或state发生变化引起执行')
  }

  render() {
    console.log('render, 组件渲染')
    return <div id='sub' 
      onClick={e => {
        this.setState({count: this.state.count + 10})
      }}
      style={{backgroundColor:'#FFF', width:'80%',
      margin: '5px'
    }}>
      Sub content. props = {this.props.name} <br />
      state = {this.state.count}
    </div>
  }

  componentDidUpdate(prevProps, prevState) {
    // 同意更新后,真正更新后,在render之后调用
    console.log('当前值', this.state, this.props);
    console.log('上次值', prevState, prevProps);
    console.log('componentDidUpdate')
  }
}

class App extends React.Component{ 
  constructor(props) {
    super(props);
    this.state = {name: 'APP'}
  }
  handleClick(e) {
    this.setState({ 
      name: this.state.name + '*'
      })
  }
  render() {
    return <div 
    onDoubleClick={this.handleClick.bind(this)} 
      style={{backgroundColor: '#f0f0f0'}}>
      Root state = {this.state.name} <br />
      <Sub name={this.state.name} />
    </div>
  }
}
render(<App />, document.getElementById('root'))

函数式组件

React从15.0开始支持函数式组件,定义如下

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

function Root(props) {
  return <div>{props.schoolName}</div>;
}
ReactDom.render(<Root schoolName="Hello World" />, document.getElementById('root'));

开发中,很多情况下,组件其实很简单,不需要state状态,也不需要使用生命周期函数。

函数式组件的函数应该提供一个参数props,返回一个React元素。

函数式组件的函数本身就是render函数。

改写上面代码

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

let Root = props => <div>{props.schoolName}</div>;

ReactDom.render(<Root schoolName="Hello World" />, document.getElementById('root'));

以前函数式组件还有个名字叫stateless components无状态组件。当前React发布了16.8,已经可以在函数式组件中使用state了,所以官方建议叫函数式组件。

高阶组件

let Root = props => <div>{props.schoolName}</div>;

如果要在上例的Root组件进行增强怎么办?例如将Root组件的div外部再加入其它div。

柯里化这个Wrapper函数

let Wrapper = function (Component, props) {
  return (  
    <div>
      {props.schoolName}  
       <hr />   
       <Component />  
   </div>
  );
};

let Root = props => <div>{props.schoolName}</div>; 

柯里化这个Wrapper函数

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

let Wrapper = function (Component) {
  function _wrapper(props) {
   return (
    <div>
      {props.schoolName}
      <hr />
      <Component />
    </div>
  );
  }
  return _wrapper;
};

let Root = props => <div>Root</div>;

let NewComp = Wrapper(Root) // 返回一个包装后的元素

ReactDom.render(<NewComp schoolName="Hello World"/>, document.getElementById('root'));

下面代码本身就是一个无状态组件,内部包裹这一个传入的组件,可以看做增强了传入的组件。

传入的组件作为返回的新组件的子组件

function _wrapper(props) {
  return (  
   <div>
    {props.schoolName}   
    <hr />  
    <Component />  
  </div>
  );
}

那么上面代码的Wrapper函数可以看做参数是一个组件,返回一个新组件,即高阶组件。

简化Wrapper,箭头函数

let Wrapper = function (Component) { 
    return (props) => { 
        return ( 
            <div>
                {props.schoolName}
                <hr /> 
                <Component /> 
            </div>
        );
    };
};

再次变化

let Wrapper = function (Component) { 
    return props =>
        ( 
        <div>
            {props.schoolName} 
            <hr /> 
            <Component /> 
        </div>
    );
};

再次变化

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

let Wrapper = Component => props =>
    (<div>
        {props.schoolName}
        <hr />
        <Component />
    </div>);

let Root = props => <div>Root</div>;

let NewComp = Wrapper(Root)

ReactDom.render(<NewComp schoolName="Hello World" />, document.getElementById('root'));

装饰器

新版ES 2016中增加了装饰器的支持,因此可以使用装饰器来改造上面的代码

@Wrapper // 这是不对的,装饰器装饰函数或类
let Root = props => <div>Root</div>;

ES 2016的装饰器只能装饰类,所以,将Root改写成类

import React from 'react';
import ReactDom from 'react-dom';
let Wrapper = Component => props =>
    (<div>
        {props.schoolName}
        <hr />
        <Component />
    </div>);

@Wrapper
class Root extends React.Component {
    render() {
        return <div>Root</div>;
    }
}

ReactDom.render(<Root schoolName="Hello World" />, document.getElementById('root'));

如何让Root也显示出schoolName?

import React from 'react';
import ReactDom from 'react-dom';
let Wrapper = Component => props =>
    (<div>
        {props.schoolName}
        <hr />
        {/* <Component schoolName={props.schoolName} /> */}
        <Component {...props} />
    </div>);

@Wrapper
class Root extends React.Component {
    render() {
        return <div>{this.props.schoolName}</div>;
    }
}

ReactDom.render(<Root schoolName="Hello World" />, document.getElementById('root'));

使用 <Component {…props} /> 相当于给组件增加了属性。

带参装饰器

想给装饰器函数增加一个id参数

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

// 带参装饰器函数
let Wrapper =
    id => Component => props =>
        (<div id={id}>
            {props.schoolName}
            <hr />
            <Component {...props} />
        </div>);

@Wrapper('wrapper') // 带参
class Root extends React.Component {
    render() {
        return <div>{this.props.schoolName}</div>;
    }
}

ReactDom.render(<Root schoolName="Hello World" />, document.getElementById('root'));
import React from 'react';
import ReactDom from 'react-dom';

// 带参装饰器函数
let Wrapper =
    (id, id2) => Component => props =>
        (<div id={id}>
            {props.schoolName}
            <hr />
            <Component {...props} id={id2}/>
        </div>);

@Wrapper('wrapper', 'wrapp_app') // 带参
class Root extends React.Component {
    render() {
        return <div id={this.props.id}>{this.props.schoolName}</div>;
    }
}

ReactDom.render(<Root schoolName="Hello World" />, document.getElementById('root'));

通过上面的改写,就得到带参装饰器。

如果觉得不好接受,可以先写成原始的形式,熟悉了箭头函数语法后,再改成精简的形式。

emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike