React是一个渐进式MVC框架:具备自己开发的独立思想(MVC:model,view,controller)
React全家桶:react / react-dom / react-router / redux / react-redux / axios / ant / dva / saga / mobx ...
- 划分组件开发
- 基于路由的SPA单页面开发
- 基于ES6编写代码
- 使用webpack来完成编译和打包
- 基于框架的组件化/模块化开发
- 基于webpack的自动部署
可以快速构建一套完整的自动化工程项目结构,有助于提高开发效率
Vue : VUE-CLI
React : CREATE-REACT-APP
$ npm install create-react-app -g // 安装在全局环境下,可以使用命令操作
// MAC电脑安装的时候需要加sudo,否则没有权限
$ create-react-app [项目名称] // 基于脚手架命令,创建出一个基于react的自动化/工程化项目目录
// 项目名称中不能出现大写字母、中文汉字、特殊符号等
1. node_modules //当前项目中依赖的包都在这里
//.bin文件夹:本地项目中可执行命令,在package.json的scripts配置对应的脚本即可
//react-scripts命令:
2. public //存放的是当前项目的HTML页面,如果是SPA应用,那么只有一个index.html
//react框架中,使用import方式导入,绝对不能用相对路径(./或../)方式导入资源,因为webpack会把相对地址改变
//如果不用import,可以用 %PUBLIC_URL% 方式导入,代表的是public文件夹
3. src //项目结构中最重要的目录,因为后期所有的js、路由、组件等都是在放到这里面(包括css、图片)
4. package.json
"dependencies": {
"react": "^16.8.6", //react核心模块
"react-dom": "^16.8.6", //react核心模块
"react-scripts": "2.1.8" //集成了webpack需要的内容(babel,css,eslint,webpack...),没有less/sass处理,自己安装
},
"scripts": {
"start": "react-scripts start", //开发环境
"build": "react-scripts build", //打包,生成build文件夹
"test": "react-scripts test",
"eject": "react-scripts eject" //把隐藏在node_modules中的webpack配置项暴露到项目中
},
create-react-app 脚手架为了让结构目录清晰,把安装的webpack以及配置文件都集成在了react-scripts模块中,放到了node_modules中
真实项目中,我们需要在脚手架默认安装的基础上,额外安装一些我们需要的模块,例如react-router-dom/axios... 以及 less/less-loader...
-
如果我们安装其他的组件,但是安装成功后不需要修改webpack配置项,此时我们直接安装并且调用即可
-
我们安装的插件是基于webpack处理的,那么就需要重新修改webpack配置项
-
首先需要把隐藏在node_modules中的webpack配置项暴露到项目中
-
再去修改对应的配置项即可
-
$ npm run eject
该操作不可逆转,并且使用前需要先提交git
一旦暴露,项目目录中会多出两个文件夹:
config 存放的是webpack的配置文件
scripts 存放的是可执行脚本的js文件
start.js : npm run start 执行的代码
build.js : npm run build 执行的代码
举例:需要安装less
1. npm install less less-loader --save //less是开发跟生产环境下都需要配置的
2. 修改 const cssRegex = /\.(css|less)$/;
const cssModuleRegex = /\.module\.(css|less)$/;
3. 添加 在css-loader最后添加
{
loader: require.resolve('less-loader'),
},
set HTTPS=true&&npm start //开启https模式
set PORT=63341&&npm start //更换端口号
-
提供了Component类可以供我们进行组件开发
-
提供了钩子函数(生命周期函数) ---- 所有的生命周期函数 都是基于回调函数完成
ReactDOM.render([JSX],[container],[callback])
//把JSX元素渲染到页面中,callback一般不用
//JSX:虚拟dom container:容器 callback:当把内容放到页面中时触发的回调函数,一般不用
let data = 'mbw';
ReactDOM.render(<div id="box">hello world! {data}</div>, document.getElementById('root'), ()=>{
let oBox = document.getElementById('box');
console.log(oBox.innerHTML);
});
react独有的语法 :JAVASCRIPT + XML(HTML)
-
不建议把JSX直接渲染到body当中,而是放在自己创建的容器中。一般我们都放在一个id为root的div当中即可
-
在JSX中出现的{}是存放JS的,但是其中的JS必须要有返回结果 {}中不能直接放一个对象类型的值(对象,含有对象的数组,函数都不行) {}中判断语句基本都不支持,但是支持map与三元运算符,map遍历的时候需要一个唯一KEY值
-
给元素设置样式用的是className className = "box"
-
style中不能直接的写样式字符串,需要基于一个样式对象来遍历赋值 style={{color:'red'}}
//参考项目中的JSX个人手写源码
import {render,createElement} from './JSX_A+'
let obj = createElement(
'h1', //=> type
{ id: 'titleBox', className: 'title', style: { color: 'red' }, ref: 'AA', key: '12' }, //=> props
'系统提示', //=> children
createElement(
'h2',
{ id: 'titleBox1', className: 'title1', style: { color: 'green' } },
'系统提示',
),
createElement(
'h3',
{ id: 'titleBox3', className: 'title3', style: { color: 'black' } },
'系统提示',
),
);
console.log(obj);
render(obj, document.getElementById('root'), () => {
console.log('ok!');
})
不管是vue还是react框架,设计之初都是期望我们按照'组件/模块管理'的方式来构建程序的。
-
有助于多人协作开发
-
开发的组件可以被复用
-
函数声明式组件(查看Dialog.js)
- 操作简单,但是功能较少,只是简单的调取和返回而已
- 静态组件,组件中的内容调取的时候就已经固定了,很难再修改
-
基于继承Component类来创建组件(查看DialogComponent.js)
- 操作复杂,但是可以实现复杂功能
- 能够使用生命周期函数操作业务
- 可以基于组件内部的状态来动态更新渲染的内容
- ...
- createElement在处理的时候,遇到一个组件,返回的对象中type不再是字符串标签名,而是一个函数或者是一个类,但是属性还是存在props中
//函数声明式组件demo
//index.js
ReactDOM.render(<div>
<Dialog con='哈哈哈' lx={2}>
<span>1</span>
</Dialog>
</div>, document.getElementById('root'));
//Dialog.js
import React from 'react';
/***
* 函数声明式组件
* 1. 函数返回结果是一个JSX
* 2. PROPS变量存储的是一个对象,包含了调取组件时候传递的属性值(不传递也是个空对象)
*/
export default function Dialog(props) {
let { con, lx = 0 ,children} = props,
title = lx === 0 ? 'MBW' : 'mbw';
//children 可能有可能没有,可能是值也可能是数组,但是都代表双闭合组件中的子元素
return <section>
<h2>{title}</h2>
<div>{con}</div>
{/**把属性中的子元素放到组件中的指定位置 */}
{children}
{/**也可以基于REACT中提供的专门遍历children的方法来完成遍历操作,一般用的不多 */}
{/* {React.Children.map(children,item=>item)} */}
</section>
}
//这是 Dialog.js return返回的对象
{
type:Dialog,
props:{
lx:2,
con:'哈哈哈',
children:可能是一个值或者是一个数组
}
}
-
type如果是字符串,就创建一个标签;
-
type如果是函数,就把函数执行(方法中的this是undefined),把props中的每一项(包括children)传递给函数;在执行函数的时候,把函数中return的JSX转换为新的对象(通过createElement),把这个对象返回,并按照以往Render的渲染方式,创建dom,并插入到指定容器即可;
-
type如果是类,会把当前类 new执行,创建类的一个实例(当前调取的就是他的实例)。new的时候会执行constructor,执行constructor后会执行this.render(),把render返回的JSX拿过来渲染。所以类声明式组件中必须有一个render方法,方法中返回一个JSX元素
-
不管是哪种方式,最后都会把解析出来的props属性对象作为实参传递给对应的函数或者类
属性是只读的,是调取组件的时候传递过来的信息
var props = { foo: x, bar: y };
var component = <Component { ...props } />;
以上代码和下面代码完全相同
var component = <Component foo={x} bar={y} />
状态是读写的,是自己在组件中设定跟规划的(只有 类声明式 才有状态的管控)
support(event) {
console.log(this);
//this是undefined;event.target也可以获取当前的元素,但是我们不会去操作dom,所以也不用这个方法
//所以要想办法让方法中的this变成当前类的实例,这样就可以操作属性和状态等信息
//1.修改JSX:<button onClick={this.support.bind(this)}>支持</button>
//2.修改support方法为箭头函数 => 最常用的方式
}
support = event => {
console.log(this);
}
react中专门提供 通过操作dom来实现需求的方式
refs是一个对象,存储了当前组件中所有设置ref属性的元素
//JSX中:
<span ref='spanleft'></span>
//获取span节点:
this.refs.spanleft
-
基于REF操作DOM实现试图更新的,叫做'非受控组件'
-
基于数据驱动(修改状态数据,react帮助我们重新渲染视图)完成的组件叫做'受控组件(受数据控制的组件)'
//class:
changeVal=event=>{
this.setState({
test : event.target.value
})
}
//JSX:
<input type="text" value={this.state.test} onChange={this.changeVal}/>
描述一个组件或者程序从创建到销毁的过程。我们可以在过程中间基于钩子函数完成一些自己的操作
- constructor 创建一个组件
- componentWillMount 第一次渲染之前
- render 正在第一次渲染
- componentDidMount 第一次渲染之后
-
shouldComponentUpdate 是否允许组件重新渲染
-
componentWillUpdate 重新渲染之前
-
render 重新渲染
-
componentDidUpdate 重新渲染之后
-
componentWillReceiveProps 父组件把传递给子组件的属性发生改变后
- componentWillUnmount 卸载组件之前(一般不用)
-
父组件把信息传递给子组件 ---1.通过props传递(参考 trans-props.js ) 2.基于上下文传递(参考 trans-context.js )
-
子组件把信息传递给父组件 ---把父组件中的方法作为属性传递给子组件,子组件执行方法(相当于执行父组件方法)
基于REDUX进行状态管理,实现组件之间的信息传输(最常用的方案)
可以应用在任何项目中,react、vue、Jq项目都可以使用。react-redux才是专门给react项目提供的方案
使用Redux:
- 安装redux
npm install redux react-redux --save
使用方式:参考redux-index.js
|-- store文件夹
|
|-- reducer文件夹
| |
| |-- index.js //把每一个模块的reducer最后合并成为一个reducer
| |
| |-- vote.js
| |-- personal.js
| |-- ...
|
|-- action文件夹 //存放每一个模块需要进行的派发任务(ActionCreator)
| |
| |-- index.js //把每一个模块的action最后合并成为一个action
| |
| |-- vote.js
| |-- personal.js
| |-- ...
|
|-- action-types.js //所有派发任务的行为标识都在这里进行宏观管理
|
|-- index.js //创建store
|
//使用store代码
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
import action from './store/action';
/******************BOX******************************/
class Box extends React.Component {
constructor() {
super();
}
render() {
return <div>
<h1>this is a Box</h1>
<Header store={store}/> //把属性传递给子组件
<Body store={store}/> //把属性传递给子组件
</div>
}
}
/******************Header******************************/
class Header extends React.Component {
constructor(props) {
super(props);
let {store} = this.props, //获取store并解构
{n,m} = store.getState().vote;
this.state = {
n,
m
}
}
componentDidMount(){
this.props.store.subscribe(()=>{ //状态改变的时候通知该方法执行
let {n,m} = store.getState().vote;
this.setState({
n,
m
})
})
}
render() {
return <div>
<h5>this is header</h5>
<h2>支持人数:{this.state.n}</h2>
<h2>反对人数:{this.state.m}</h2>
<hr />
</div>
}
}
/******************Body******************************/
class Body extends React.Component {
constructor(props) {
super(props);
console.log(this.props);
}
support = () => {
let {store:{dispatch}} = this.props; //dispatch 用来调用store中的方法改变属性
dispatch(action.vote.support())
}
against = () => {
let {store:{dispatch}} = this.props;
dispatch(action.vote.against())
}
render() {
// let {store:{dispatch}} = this.props;
return <div>
<h5>this is a Body</h5>
<button onClick={this.support}>支持</button>
<button onClick={this.against}>反对</button>
</div>
}
}
ReactDOM.render(<Box></Box>, document.getElementById('root'))
/***
* react-redux 是把redux进一步封装,适配react项目,让redux操作更简洁
*
* -- store文件夹的内容与 redux 一模一样
* -- 在组件调用的时候可以优化一些步骤:
* 1.导出的不再是我们创建的组件,而是基于connect构造后的高阶组件
*
* 1. Provider 跟组件
* 当前整个项目都在Provider组件下,作用就是把创建的store可以供内部任何后代组件使用,不再需要传属性了(NICE!)
*
* 1. Provider组件中只允许出现一个子元素
* 2. 把创建的store基于属性传递给Provider(后代组件中都可以使用这个store了)
*
* 2. connect 高阶组件
* 1. 导出的不再是我们创建的组件,而是基于connect构造后的组件export default connect(mapStateToProps,mapDispatchToProps)(自己创建的组件);
* 2. 以前我们需要自己基于subscribe向事件池追加方法,已达到容器状态信息改变,执行我们追加的方法,重新渲染组件的目的。而使用react-redux,所有用到redux状态信息的组件,都会向事件池中追加一个方法,当状态改变,通知方法执行,把最新的状态信息作为属性传递给组件,组件重新渲染
*/
/***************** index.js ******************/
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import store from '../src/store';
import VoteBody from './component/VoteBody';
import VoteFooter from './component/VoteFooter';
ReactDOM.render(<Provider store={store}>
<VoteBody/>
<VoteFooter/>
</Provider>, document.getElementById('root'));
/***************** 组件内的正常用法 ******************/
import { connect } from 'react-redux';
import action from '../store/action';
let mapStateToProps = (state) => { //把redux容器中的状态信息遍历,赋值给当前组件的属性 state:redux容器中的状态信息
return { //返回的是什么,就把什么挂载到当前组件的属性上
...state.vote
}
}
let mapDispatchToProps = (dispatch) => { //把redux中的dispatch遍历,赋值给当前组件的属性 dispatch:store中存储的dispatch方法
return { //返回的是什么方法,就把什么方法挂载到当前组件的属性上:一般我们挂载一些方法,用于完成dispatch的派发操作
support(){
dispatch(action.vote.support());
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(VoteBody);
/***************** 组件内的简化用法 ******************/
import { connect } from 'react-redux';
import action from '../store/action';
export default connect(state => ({ ...state.vote }), action.vote)(VoteBody);
//react-redux 帮我们把 action-creator中编写的方法(返回action对象的方法),自动构建成dispatch派发任务的方法,也就是mapDispatchToProps这种格式
--- redux-A+文件夹内
--- Todo文件夹内
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter,HashRouter,Route,Switch,Redirect} from 'react-router-dom';
import A from './component/A';
import B from './component/B';
import C from './component/C';
/***
* 单页面应用(SPA)
* - 使用REACT路由实现SPA
*
* 1. 使用react-router-dom 最新版本react路由 https://reacttraining.com/react-router/web/api/Redirect/to-object
*
* 2. BrowserRouter[浏览器路由] VS HashRouter[哈希路由]
*
* BrowserRouter:
* 他是基于H5中的history API (pushState,replaceState,popState)来保持UI和URL同步。真实项目中应用的不多,因为history API不稳定,并且当前项目基于服务器渲染才会选择浏览器路由
* http://www.demo.com/
* http://www.demo.com/personal
* http://www.demo.com/personal/login
*
*
* -----------------------
* HashRouter:
* 真实项目中(前后端分离的项目,客户端渲染),我们经常使用哈希路由来完成,他依据相同的页面地址,不同的哈希值,来规划当前页面中的哪一个组件呈现渲染,它基于原生js构造了一套类似于histpry API
* 的机智,每一次路由切换都是基于 history stack 完成的。
* http://www.demo.com/#/
* http://www.demo.com/#/personal
* http://www.demo.com/#/personal/login
*
* 1. 当前项目一旦使用hashRouter,则默认在页面地址后面加上#
* 2. HashRouter中只能出现一个子元素
* 3. HashRouter机制中,我们需要根据哈希地址不同,展示不同的组件内容。此时我们需要使用Route
*
*
* -----------------------
* Route:
* Route有如下几个属性:
* path:匹配哈希后面的值(地址)
* component:一旦哈希值和当前route的path相同了,则渲染component指定的组件
* exact:严格匹配
*
* 如果不加严格匹配:
* #/user 能同时匹配A和B
* #/user2 能匹配A不能匹配B
*
* strict:严格匹配并且最后必须有斜杠 #/user/
* render:当页面的哈希地址和path匹配,会把render规定的方法执行。方法一般用来做权限校验
*
* -----------------------
* Switch:
*
* 默认情况下,会和每一个Route都做校验(哪怕之前已经有校验成功的)
* > 使用Switch组件可以解决这个问题。只要有一种情况校验成功,就不再向后校验了
* -----------------------
* Redirect:
*
* 1. <Redirect to='/user'/>
* 2. <Redirect to={
* pathname:, 定向地址
* search: 给定向地址问号传参=>根据问号参数值来统计是正常进入首页还是非正常跳转;也可以做其他事情
* state: 给定向后的组件传递一些信息
* }/>
*
* 3. push:如果设置了这个属性,当前跳转的地址会加入到history stack中一条记录 ,方便返回等操作
*
* 4. from:设定当前来源的页面地址 <Redirect from='/user' to='/user/list'/> : 如果请求的地址是user,重定向到user/list
* -----------------------
*/
ReactDOM.render(<HashRouter>
<Switch>
<Route path='/' exact component={A}/>
<Route path='/user' exact component={B}/>
<Route path='/pay' exact render={()=>{
//一般在render中处理的是权限校验
let flag = localStorage.getItem('FLAG');
if(flag && flag=='safe') {
return <C/>;
}else{
return <A/>;
}
}}/>
{/* 上述都设置完成后,会在末尾设置一个匹配:以上都不符合的情况下,路由地址是非法地址,做一些特殊处理 */}
{/* <Redirect to='/?lx=404'/> 与下面写法一样*/}
<Redirect
to={{
pathname: "/",
search: "?lx=404",
}}
/>
</Switch>
</HashRouter>, document.getElementById('root'));