笨人学习法 10000个小时策略来学习,因为笨。先照着官方文档敲一遍,写一遍。
准备 先要准备环境。搭建一个基于webpack的react环境:Hello ReactJS .
一些要点 我在想是否应该完整的记录照抄的过程呢。毕竟已经开始一段,前面的要不要补上?回头看以前写过的angularJS的博客,现在完全不会了,太久没用了。所以,还是记录基础以及关注的问题就好。
1.1 基本格式 react的模板文件后缀结尾为.jsx
。
react可以采用html标签拼接的方式定义一个元素。比如:
1 const element = <h1 > Hello, world</h1 > ;
假设页面有个div:
那么,reactJS可以这样渲染页面:
1 2 3 4 5 const element = <h1 > Hello, world</h1 > ;ReactDOM.render( element, document .getElementById('root' ) );
需要引入react-dom
. element
变量就是一个react的元素,一个组件,一个component.通过ReactDOM.render(reactElement, domElement)
来渲染页面 1.1 变量 react可以使用一对大括号来包裹变量,与html拼接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function tick ( ) { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date ().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render( element, document .getElementById('clock' ) ); } setInterval (tick, 1000 );
大括号里的代码是js代码 element是一个react组件:component。可以看成由div
和h1
,h2
拼接的匿名组件。 下面实践以上的代码。首先,由于采用单个元素测试,需要修改上次搭建好的环境。
修改webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 module.exports = { - entry: './app/index.js', + entry: { + app: './app/index.js', + clock: './app/components/step1-element.jsx' + }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'index_bundle.js' + filename: '[name].bundle.js', },
意思是可以渲染多个打包后的js文件。分别定义entry就是需要单独打包的js。在filename就会根据entry的key来生成打包后的文件名。
1.1.1 构建第一个react component 创建app/components/step1-element.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' ; import ReactDOM from 'react-dom' ;function Clock (props ) { return ( <div> <h1>Step1, learn element and variable.</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> ); } function tick ( ) { ReactDOM.render(<Clock date ={new Date ()} /> , document .getElementById('clock' )); } setInterval (tick, 1000 );
function Clock
就是一个react component,和前面的element
一样,都是react组件. react component可以写成html标签的方式,但要求方法名必须大写,也即标签名必须大写。<Clock date={new Date()}/>
就是组件的用法。 组件Clock
接收一个参数对象props
,props
的属性可以通过标签上的变量来赋值。比如date
就通过标签传入到function Clock
里了。由此,像<div>
这种拼接的标签肯定也是有function的,不过react库已经写好了。 react component必须有返回值,返回一段html代码,用圆括号包裹 html标签与js变量可以通过一对大括号的方式拼接起来 修改app/index.html.添加一个我们用来测试div节点。这里主要用于clock
1 2 3 4 5 6 7 8 9 10 <div id="root"> </div> + + +<div id="content"> + <div id="clock"></div> +</div> </body> </html>
然后,运行yarn build
。编译后的dist目录如下:
1 2 3 4 5 |____dist | |____app.bundle.js | |____clock.bundle.js | |____index.html | |____index_bundle.js
可以看到定义的两个js都已经生成。而且index.html中也插入:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>React App</title> </head> <body> <div id="root"> </div> <div id="content"> <div id="clock"></div> </div> <script type="text/javascript" src="app.bundle.js"></script><script type="text/javascript" src="clock.bundle.js"></script></body> </html>
但发现还多了个index_bundle.js
,这是我们上一步生成。在本次构建中并没有自动移除。想要自动移除怎么办?
添加webpack plugin: clean-webpack-plugin
1 yarn add clean-webpack-plugin
修改webpack.config.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); + const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ template: './app/index.html', filename: 'index.html', inject: 'body' }); module.exports = { entry: { app: './app/index.js', clock: './app/components/step1-element.jsx' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/ } ] }, plugins: [ HtmlWebpackPluginConfig, + new CleanWebpackPlugin(['dist']) ] };
重新build.yarn build
。此时,dist目录下当只有所需要的文件了。
启动html查看效果。这时可以采用webstom或者idea里的用浏览器打开功能,会自动创建的静态服务器。方便简单。也可以安装http-server
。不过,既然用webpack,肯定采用webpack的热编译功能。
浏览器访问localhost:8080
就是我们的页面了。
一个值得二级标题的功能。在chrom扩展里搜索React Developer Tools
,添加。然后重新打开我们的页面。看控制台的react节点:
1.3 推荐的react组件写法 除了上文使用function来创建一个react component。推荐采用es6 class的方式。更加清晰。 由于用到lambda语法糖,需要增加一个新的babel插件:
1 yarn add babel-plugin-transform-class-properties --dev
然后在.babelrc文件中新增:
1 "plugins": ["transform-class-properties"]
下面创建app/components/LoginButton.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React from 'react' ;class LoginButton extends React .Component { handleClick = () => { console .log("this is " , this ); }; render ( ) { return ( <button onClick={this .handleClick}> Click me, auto bind this by lambda </button> ); } } export default LoginButton;
这里有几个需要注意的地方。
通过class
来声明一个component,并在结尾处export default
出去。 创建的component需要继承React.Component
必须创建render方法,并返回一个react component组件 通过lambda语法可以指定方法为this的属性,相当于在构造器中绑定放大到this。因此可以在onClick中调用this。否则,普通的方法不会绑定到this上,需要在构造器上绑定。 以上创建了一个组件LoginButton,我们可以像开始一样直接render到一个dom元素里。也可以直接添加到另一个component组件中。比如搭建环境时给的App组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from 'react'; import Clock from './Clock.jsx'; import ActionLink from './ActionLink.jsx'; + import LoginButton from './LoginButton.jsx' class App extends React.Component { render() { return ( <div style={{textAlign: 'center'}}> <h1>Hello World! Hi ReactJS!</h1> <div> <Clock /> <ActionLink /> + <LoginButton/> </div> </div> ); } } export default App;
import引入刚才写的LoginButton,其中地址是相对地址。变量名可以自定义,因为export的时候采用了default。这里仍旧取名为LoginButton。 将引入的变量的标签形式插入拼接即可。 yarn start
可以观察到页面多了按钮。
1.4 使用state控制状态 最开始的demo Clock中,使用一个时间函数,定时render页面。这种需求可以转换为定时更新状态,由react自动根据状态来渲染页面。对于那个Clock组件来说,唯一变化的就是时间,那么这个时间就是动态的状态。react的component的有个state属性,专门用来传递状态,或者说数据的。当我们需要修改数据的时候,直接修改state就可以了。
新建app/components/Clock.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React from 'react' ;function FormattedDate (props ) { return <h2 > It is {props.date.toLocaleTimeString()}. </h2 > } class Clock extends React .Component { constructor (props ) { super (props); this .state = {date : new Date ()}; } componentDidMount ( ) { this .timerID = setInterval ( () => this .tick(), 1000 ); } componentWillUnmount ( ) { clearInterval (this .timerID) } tick ( ) { this .setState({date : new Date ()}); } render ( ) { return ( <div> <h1>This is a clock!</h1> <FormattedDate date={this .state.date} /> </div> ); } } export default Clock;
constructor是一个构造函数,当new Clock()的时候会调用这个方法来创建对象,因此可以把对象的一些初始化操作放在这里。本例中,初始化state.
state是一个对象,内容自定义,本例只增加一个date属性.
componentDidMount()在component挂载的时候触发,这里设置一个定时器,定时调用tick().
this.setState({})是唯一能修改state的方式,通过this.state={}的做法无效。另外,setState是一个merge的异步操作。merge是说,每次set的时候,只会修改指定的变量,不会整体替换。异步是说不能直接this.state.xx来操作属性,因为有可能你调用this.state.xx来获取xx的值的时候,前一次的setState还没执行完。如果想要同步的修改state里的属性,可以采用第二种方式:
1 2 3 4 this .setState((prevState, props ) => ({ counter: prevState.counter + props.increment }));
接受两个参数,第一个是state,第二个是props。这两个变量会在最后一次修改结束后自动注入。所以就可以放心的设置state的属性了。
componentWillUnmount()和componentDidMount()都是react的lifecycle hooks
。是react组件声明周期前后会调用的方法。componentWillUnmount()会在component移除的时候触发。this.timerID可以直接将属性timerID绑定到this上,这个不需要绑定到state,因为这个和渲染(render)页面无关。
FormattedDate是我们抽出来的专门显示时间的组件,date是它的一个props.
组件创建完毕,下面开始使用。使用方式就是转换成标签的方式调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React from 'react'; + import Clock from './Clock.jsx'; import ActionLink from './ActionLink.jsx'; import LoginButton from './LoginButton.jsx' import LoginControl from './LoginControl.jsx' class App extends React.Component { render() { return ( <div style={{textAlign: 'center'}}> <h1>Hello World! Hi ReactJS!</h1> <div> + <Clock /> <ActionLink /> <LoginButton/> </div> <div> <LoginControl /> </div> </div> ); } } export default App;
页面这时候就会自动刷新时间了。
1.5 阻止事件 React里的属性采用驼峰命名规则,在原来的html中,定义onclick属性:
1 2 3 <button onclick ="activateLasers()" > Activate Lasers </button >
但在react里,必须将onclick改成onClick
1 2 3 <button onClick ={activateLasers} > Activate Lasers </button >
在原来的html中,可以通过return false的方式阻止默认事件。比如,a标签有href和onClick属性。在html中,我们想要阻止点击的时候跳转到href,那么可以在onClick中返回false
1 2 3 <a href ="#" onclick ="console.log('The link was clicked.'); return false" > Click me </a >
这样,你点击a标签后,浏览器地址栏不会有#,如果你不return false,浏览器地址栏就会发生跳转。这是a标签的默认行为。在html中可以通过return false来阻止。但在react中这样做无效。必须使用preventDefault
创建app/components/ActionLink.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React from 'react' ;function ActionLink ( ) { function handleClick (e ) { e.preventDefault(); console .log("The link was clicked. PreventDefault event." ); } return ( <a href="#" onClick={handleClick}> Click me </a> ); } export default ActionLink;
然后在App.jsx中引入。刷新页面,点击a标签。观察浏览器地址栏可以发现没有任何变化,证明默认行为被阻止了。如果注释掉e.preventDefault();
,刷新页面,点击a标签,观察地址栏就会发现发生了改变。
1.8 方法绑定到this 接着理解react组件的写法。写一个Toggle按钮,每次点击都切换状态。 创建app/components/Toggle.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import React from 'react' ;class Toggle extends React .Component { constructor (props ) { super (props); this .state = { isToggleOn: true , color: 'red' }; this .handleClick = this .handleClick.bind(this ); } handleClick ( ) { console .log("this=" , this ); this .setState( prevStat => ({ isToggleOn: !prevStat.isToggleOn, color: prevStat.color==='red' ? 'green' :'red' }) ); } render ( ) { return ( <button onClick={this .handleClick} style={{background : this .state.color}}> {this .state.isToggleOn ? 'ON' :'OFF' } </button> ); } } export default Toggle;
首先,构造函数定义了state有两个属性,并初始化 构造函数绑定了handleClick的作用域为Toggle. 关于如何理解这个绑定,参阅如何理解js中的this绑定 . 如果注释掉这一行,触发handleClick的时候,里面的this是null。那么setState当然也就不存在。我们这里setState是希望调用Toggle的方法,希望这个this指向Toggle. 因此需要在构造器中绑定this。 setState的时候,如果和前一个状态相关的话,一定要采用方法传参的方式。这里是一个lambda语法糖。 将Toggle插入到App.jsx中,页面会有个按钮,每次点击都会改变颜色。这是因为,点击的时候触发onClick,调用handleClick,然后setState修改了state,react就会根据state来重新render组件。 另一种方式自动绑定方法成为一个实例,是采用babel-plugin-transform-class-properties
。这个目前还不是es的标准,因为将方法定义为属性这种做法还很有争议。在java8中lambda也是如此,但java8将lambda设定为一等公民,是另一个东西,和成员变量类似。这里,如果使用这个plugin的话,lambda语法糖可以升级为属性,那么就不用绑定this了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class LoggingButton extends React .Component { handleClick = () => { console .log('this is:' , this ); } render ( ) { return ( <button onClick={this .handleClick}> Click me </button> ); } }
这里,handleClick就会升级为属性,就可以直接用this调用,里面的this就是外面的LogginButton. 还有一种方式是lambda语法,但官方不推荐:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class LoggingButton extends React .Component { handleClick ( ) { console .log('this is:' , this ); } render ( ) { return ( <button onClick={(e ) => this .handleClick(e)}> Click me </button> ); } }
The problem with this syntax is that a different callback is created each time the LoggingButton renders. In most cases, this is fine. However, if this callback is passed as a prop to lower components, those components might do an extra re-rendering. We generally recommend binding in the constructor or using the property initializer syntax, to avoid this sort of performance problem. 建议采用前两种方式。
1.7 一个稍微复杂的例子:登录按钮的动态切换 综合以上的demo。编写新需求。当用户没有登录的时候,显示”Please login”,并显示login按钮,当用户登录的时候显示”welcome”和logout按钮。 创建app/components/Greeting.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' ;function UserGreeting (props ) { return <h1 > Welcome back!</h1 > } function GuestGreeting (props ) { return <h1 > Please sign up.</h1 > } function Greeting (props ) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn){ return <UserGreeting /> } return <GuestGreeting /> } export default Greeting;
创建app/components/LoginControl.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import React from 'react' ;import Greeting from "./Greeting.jsx" ;function LoginButton (props ) { return ( <button onClick={props.onClick} style={{color : 'white' , background :"green" }} > Login </button> ); } function LogoutButton (props ) { return ( <button onClick={props.onClick} style={{color : 'white' , background :'red' }}> Logout </button> ); } class LoginControl extends React .Component { constructor (props ) { super (props); this .handleLoginClick = this .handleLoginClick.bind(this ); this .handleLogoutClick = this .handleLogoutClick.bind(this ); this .state = {isLoggedIn : false }; } handleLoginClick ( ) { console .log("Click login" ); this .setState({isLoggedIn : true }); } handleLogoutClick ( ) { console .log("Click logout" ); this .setState({isLoggedIn : false }); } render ( ) { const isLoggedIn = this .state.isLoggedIn; let button = null ; if (isLoggedIn) { button = <LogoutButton onClick ={this.handleLogoutClick} /> ; } else { button = <LoginButton onClick ={this.handleLoginClick} /> ; } return ( <div style={{border : '1px solid #000' }}> <Greeting isLoggedIn={this .state.isLoggedIn}/> {button} </div> ); } } export default LoginControl;
在App.jsx中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React from 'react'; import Clock from './Clock.jsx'; import ActionLink from './ActionLink.jsx'; import LoginButton from './LoginButton.jsx' + import LoginControl from './LoginControl.jsx' import Toggle from './Toggle.jsx' class App extends React.Component { render() { return ( <div style={{textAlign: 'center'}}> <h1>Hello World! Hi ReactJS!</h1> <div> <Clock /> <ActionLink /> <LoginButton/> <Toggle /> </div> + <div> + <LoginControl /> + </div> </div> ); } } export default App;
参考