React是FaceBook(脸书)公司研发的一款JS框架(MVC)
React的脚手架
React是一款框架 : 具备自己开发的独立思想(MVC : Model View Controller)
- 划分组件开发
- 基于路由的SPA单页面开发
- 基于ES6来编写代码(最后部署上线的时候,我们需要把ES6编译成ES5 =>基于Babel来完成编译)
- 可能用到Less/Sass等,我们也需要使用对应的插件把他们进行预编译
- 为了优化性能(减少http请求次数),我们需要把JS或者CSS进行合并压缩
- ………….
- webpack来完成以上页面组件合并、JS / CSS编译加合并等工作
前端工程化开发
- 基于框架的组件化开发 / 模块化开发
- 基于webpack的自动部署
但是配置webpack是一个相对复杂的工作,我们需要自己安装许多的包,还需要自己写相对复杂的配置文件… 如果我们有一个插件,基于它可以快速构建一套完整的自动化工程项目结构,那么有助于提高开发的效率=>脚手架
VUE : vue-cli
REACT : create-react-app
create-react-app 的使用
1.把模块安装在全局(目的:可以使用命令),mac电脑安装的时候,前面需要加sudo,否则没有权限
npm install creact-react-app -g
2.基于脚手架命令,创建出一个基于react的自动化 / 工程化项目目录,和npm发包时候的命名规范一样,项目名称中不能出现:大写字母 / 中文 / 特殊符号(-或者_是可以的)等
create-react-app [项目名称]
脚手架生成目录中的一些内容
node_modules : 当前项目中依赖的包都安装在这里
- .bin : 本地项目中可执行命令,在package.json的scripts中配置对应的脚本即可(react-scripts命令)
public : 存放的是当前项目的HTML页面(单页面应用放一个index.html即可,多页面根据自己需求放置需要的页面)
<!-- 在react框架中,所有的逻辑都是在JS中完成的(包括页面结构的创建)如果想给当前的页面导入一些css样式或者image图片等内容,我们有两种方式: 1.在JS中基于ES6 Module模块规范使用import导入,这样webpack在编译合并页面的时候,会把导入的资源文件等插入到页面的结构中(绝对不能再JS管控的结构中通过相对目录./或者../导入资源,因为在webpack编译的时候,地址就不再是之前的相对地址了) 2.如果不想在JS中导入(JS中导入的资源都会基于webpack编译),我们也可以把资源手动的在HTML中导入,但是HTML最后也要基于webpack编译,带入的地址也不建议写相对地址,而是使用%PUBLIC_URL%/ 写成绝对路径 <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> -->
src : 项目结构中最主要的目录,因为后期所有的JS、路由、组件等都是放到这里面(包括需要编写的CSS或者图片等)
.gitignore : git提交时候忽略文件的目录配置项
package.json : 当前项目的配置清单
```javascript
“dependencies”: {
“react”: “^16.4.1”,
“react-dom”: “^16.4.1”,
“react-scripts”: “1.1.4”
},
基于脚手架生成工程目录,自动帮我们安装了三个模块,react/react-dom/react-scripts
react-scripts集成了webpack需要的内容:babel
css
eslink
webpack
其他
没有less/sass的处理内容(项目中使用less,需要自己额外安装)“scripts”: {
“start”: “react-scripts start”,
“build”: “react-scripts build”,
“test”: “react-scripts test –env=jsdom”,
“eject”: “react-scripts eject”
}
可执行的脚本”npm run start / yarn start”
start:开发环境下,基于webpack编译处理,最后可以预览当前开发的项目成果(在webpack中,安装了webpack-dev-server插件,基于这个插件会自动创建一个web服务[端口号默认是3000],webpack会帮我们自动打开浏览器并且展示我们的页面,并且能够监听我们代码的改变,如果代码改变了,webpack会自动重新编译并且刷新浏览器来完成重新渲染)
build:项目需要部署到服务器上,我们先执行yarn build,把项目整体编译打包(完成后会在项目中生成一个build文件夹,这个文件夹中包含了所有编译后的内容,我们把它上传到服务器即可);而且在服务器上进行部署的时候,不需要安装任何模块,因为webpack已经把需要的内容都打包到一个JS中了
React脚手架的深入剖析
creact-react-app脚手架为了让结构目录清晰,把安装的webpack及配置文件都集成在了react-scripts模块中,放到了node_modules中
但是真实项目中,我们需要在脚手架默认安装的基础上,额外安装一些我们需要的模块,例如 : react-router-dom / axios 及 less / less-loader….
情况一 : 如果我们安装其他的组件,但是安装成功后不需要修改webpack配置项,此时我们直接的安装并且调取使用即可
情况二 : 我们安装的插件是基于webpack处理的,也就是需要把安装的模块配置到webpack当中(重新修改webpack配置项)
- 首先需要把隐藏到node_modules中的配置项暴露到项目
yarn eject:暴露到项目中
首先会提示确认是否执行eject操作,这个操作是不可逆转的,一旦暴露出来配置项,就无法再隐藏回去了
如果当前的项目基于git管理,在执行eject的时候,如果还有没有提交到历史区的内容,需要先提交到历史区,然后再eject才可以,否则报错
一旦暴露后,项目中多了两个文件夹
config:存放到的是webpack的配置文件
webpack.config.dev.js:开发环境下的配置项(yarn start)
webpack.config.prod.js:生产环境下的配置项(yarn build)
scripts:存放的是可执行脚本的JS文件
start.js:yarn start执行的JS
bulid.js:yarn build执行的JS
package.json中的内容也改了
例如:配置less
- yarn add less less-loader
- less是开发和生产环境下都需要配置的
{loader: require.resolve(‘less-loader’) }
- 再去修改对应的配置项即可
我们预览项目的时候,也是先基于webpack编译,把编译后的内容放到浏览器中运行,所以如果项目中使用了less,我们需要修改webpack配置项,在配置项中加入less的编译工作,这样后期预览项目,首先基于webpack把less编译为css,然后再呈现在页面中.
### 设置环境变量
set HTTPS=true&&npm start 开启HTTPS协议模式
set PORT=6666&&npm start 修改端口号
### react&&react-dom
[渐进式框架]
- 一种最流行的框架设计思想,一般框架中包含很多内容,这样导致框架的体积过于臃肿,会拖慢加载速度,真实项目中,我们使用一个框架,不一定用到所有的功能,此时我们应该把框架的功能进行拆分,用户想用什么,让其自由组合即可
- 全家桶 : 渐进式框架N多部分的组合
- Vue全家桶 : vue-cli / vue / vue-router / vuex / axios(fatch) / vue element(vant)
- React全家桶 : create-react-app / react / react-dom / react-router(-dom) / redux / react-redux / axios(fatch) / ant / dva / mobx...
> 1.react : react框架的核心部分,提供了Component类可以供我们进行组件开发,提供了钩子函数(生命周期函数 : 所有的生命周期函数都是基于回调函数完成的)
>
> 2.react-dom : 把JSX语法(React独有的语法)渲染为真实DOM(能够放到页面中展示的结构都叫做真实的DOM)的组装件
### 把JSX(虚拟DOM)变为真实的DOM
**`JSX语法`**
> ReactDOM.render([jsx],[container],[callback]); 把JSX渲染到页面中
> - jsx:react虚拟元素
> - container:容器,我们想把元素放到页面中的哪个容器中
> - callback:当把内容放到页面中呈现触发的回调函数
JSX(javascript+XML(HTML))
- 和之前拼接的HTML字符串类似,都是把HTML结构代码和JS代码或者数据混合在一起了,但是它不是字符串
- 1.不建议我们把JSX直接渲染到body中,而是放在自己创建的一个容器中,一般我们都放在一个id为root的div中即可
- 2.在JSX中出现的{}是存放JS的,但是要求JS代码执行完成需要有返回结果(JS表达式)
- 不能直接放一个对象数据类型的值(对象(除了给style赋值)、函数都不行,数组里面如果没有对象,都是基本值是可以的或者是JSX元素)
- 可以是基本数据类型的值(布尔类型什么都不显示,null、undefined也是JSX元素,代表的是空)
- 循环判断的语句都不支持,但是支持三元运算符
- 3.循环数组创建JSX元素(一般都基于数组的map方法完成迭代),需要给创建的元素设置唯一的key值(当前本次循环内唯一即可)
- 4.只能出现一个根元素
- 5.给元素设置样式类用的是className而不是class
- 6.style中不能直接的写样式字符串,需要基于一个样式对象来遍历赋值
```javascript
let data = [
{ name:"张三",age:18},
{name:"李四",age:20}
],
root = document.querySelector("#root");
ReactDOM.render(<div id="box">Hello world!
<ul>
{data.map((item,index)=>{
let {name, age} = item;
return <li key={index}>
<span>{name}</span>
<span>{age}</span>
</li>
})}
</ul>
</div>, root);
JSX渲染机制
- 1.基于babel中的语法解析模块(babel-preset-react)把JSX语法编译为react.createElement(…)结构
- 2.执行createElement
React.createElement(type, props, children); 创建一个对象(虚拟DOM) 属性: type:"h1" : 标签名 props:{ id:"titleBox", className:"title", style:{...}, children:"jiatengda" =>存放的是元素中的内容 } ref:null key:null ... __proto__:Object.prototype
- 3.reactDOM.render(JSX语法最后生成的对象,容器),基于render方法把生成的对象对台创建DOM元素,插入到执行的容器中
自己写createElement&&render
function createElement(type, props, children) {
props = props || {};
//创建一个对象,设置一些默认属性值
let obj = {
type: null,
props: {
children: ""
},
ref: null,
key: null
};
//用传递的type和props覆盖原有的默认值
// obj = {...obj, type, props};//=>type:type,props:props
obj = {...obj, type, props: {...props, children}};
//把ref和key提取出来(并且删除props中的属性)
"key" in obj.props ? (obj.key = obj.props.key, delete obj.props.key) : null;
"ref" in obj.props ? (obj.ref = obj.props.ref, delete obj.props.ref) : null;
return obj;
}
function render(obj, container, callback) {
let {type, props} = obj || {},
newElement = document.createElement(type);
for (let attr in props) {
if (props.hasOwnProperty(attr)) break;//不是私有的直接结束遍历
if (!props[attr]) continue;//如果当前属性没有值,直接不处理即可
let value = props[attr];
//className的处理
if (attr === "className") {
newElement.setAttribute("class", value);
continue;
}
//style
if (attr === "style") {
if (value === "") continue;
for (let styKey in value) {
if (!value.hasOwnProperty(styKey)) break;
newElement['style'][styKey] = value[styKey];
}
continue;
}
//children
if (attr === "children") {
if (typeof value === "string") {
let text = document.createTextNode(value);
newElement.appendChild(text);
}
continue;
}
newElement.setAttribute(attr, value);//基于setAttribute可以让设置的属性表现在HTML的结构上
}
container.appendChild(newElement);
callback && callback();
}
React组件
不管是VUE还是REACT框架,设计之初都是期望我们按照组件/模块管理
的方式来构建程序的,也就是把程序划分为一个个的组件来单独处理
- 有助于多人协作开发
- 我们开发的组件可以被复用
- ……
React中创建组件有两种方式:
- 函数声明式组件
- 基于继承component类来创建组件
src->components : 这个文件夹中存放的就是开发的组件
知识点 : createElement在处理的时候,遇到一个组件,返回的对象中,type就不再是字符串标签名了,而是一个函数(类),但是属性还是存在props中
{
type:Dialog
props:{
lx:1,
con:"xxx",
children:一个值||一个数组
}
}
render渲染的时候,我们需要做处理,首先判断type的类型,如果是字符串,就创建一个元素标签,如果是函数或者类,就把函数执行,把props中的每一项(包含children) 传递给函数
在执行函数的时候,把函数中return的JSX转换为新对象(createElement),把这个对象返回;紧接着render按照以往的渲染方式,创建DOM元素,插入到指定的容器中即可
基于继承component类来创建组件
基于createElement把JSX转化为一个对象,当render渲染这个对象的时候,遇到type是一个函数或者类,不是直接创建元素,而是先把方法执行
如果就是函数式声明组件,就把它当做普通方法执行(方法中的this是undefined),把函数返回的JSX元素(也是解析后的对象)进行渲染
如果是类声明式组件,会把当前类new它执行,创建类的一个实例(当前本次调取的组件就是它的实例),执行contructor之后,会执行this.render(),把render返回的JSX拿过来渲染,所以类声明式组件
,必须有一个render的方法,方法中需要返回一个JSX元素
但是不管是哪一种方式,最后都会把解析出来的props属性对象作为实参传递给对应的函数或者类
总结
创建组件有两种方式,函数式
和创建类式
[函数式]
- 1.操作很简单
- 2.功能也简单,简单的调取,简单的返回
[创建类式]
- 1.操作相对复杂一点,但是也可以实现更为复杂的业务功能
- 2.能够使用声明周期函数操作业务
- 3.函数式可以理解为静态组件(组件中的内容调取的时候就已经固定了,很难再修改),而类这种方式,可以基于组件内容的状态来动态更新渲染的内容
组件的属性和状态
React中有两个非常重要的概念
- 1.组件的属性 : [只读] 调取组件的时候传递进来的信息
- 2.组件的状态 : [读写] 自己在组件中设定和规划的(只有类声明式组件才有状态的管控,函数式声明组件不存在状态的管理)
function Clock() {
return <section>
<h3>当前北京时间为:</h3>
<div style={{color:"lightblue",fontWeight:"bold"}}>{new Date().toLocaleString()}</div>
</section>
}
window.setInterval(()=>{
//每隔一秒钟重新调取组件,然后渲染到页面中
ReactDOM.render(<Clock/>, root)
}, 1000);
所谓函数式组件是静态组件 : 和执行普通方法一样,调取一次组件,就把组件的内容获取到,插入到页面当中,如果不重新调取组件,显示的内容是不会发生任何改变的
真实项目中只有调取组件,组件中的内容不会再次改变的情况下,我们才可能使用函数式组件
组件的状态类似于Vue中的数据驱动 : 我们数据绑定的时候是基于状态值绑定,当修改组件内部状态后,对应的JSX元素也会跟着重新渲染(差异渲染 : 值把数据改变的部分重新渲染,基于dom-diff算法完成的)
当代框架最重要的核心思想就是
数据操控视图(视图改变数据)
,告别jQuery手动操作DOM的时代,我们以后只需要改变数据,框架会帮我们重新渲染改变视图,从而减少直接操作DOM,提高性能,也有助于开发效率
class Clock extends React.Component {
constructor() {
super();
//初始化组件的状态(都是对象类型的):要求我们在constructor中需要把后期使用的状态信息全部初始化一下(约定俗成)
this.state = {
time: new Date().toLocaleString()
};
}
componentDidMount() {
//React声明周期函数之一:第一次组件渲染完成后触发(我们在这里只需要间隔1秒把state状态中的time数据改变,这样React会自动帮我们把组件中部分内容重新渲染)
//React中虽然下面方式可以修改状态,但是并不会通知React重新渲染页面,所以不能这么操作和修改状态
//this.state.time = new Date().toLocaleString();
setInterval(() => {
//修改组件的状态
//1.修改部分状态:会用我们传递的对象和初始化的state进行匹配,只把我们传递的属性进行修改,没有传递的依然保留原始的状态信息(部分无法修改)
//2.当状态修改完成,会通知React,把组件JSX中的部分元素重新进行渲染
this.setState({
time: new Date().toLocaleString()
},()=>{
//当通知React把需要重新渲染的JSX元素渲染完成后,执行的回调操作(类似于声明周期函数中的componentDidUpdate,项目中一般使用钩子函数,而不是这个回调)
//=>设置回调函数是因为setState是一个异步操作,基于async和await可以把异步的变为类似于同步
});
}, 1000);
}
render() {
return <section>
<h3>当前北京时间为:</h3>
<div style={{color: "lightblue", fontWeight: "bold"}}>
{/*获取组装件的状态信息*/}
{this.state.time}
</div>
</section>
}
}
ReactDOM.render(<Clock/>, root);
JSX中的事件绑定
render(){
//this:当前类的实例 this.support.bind(this)
<button className="btn btn-success" onClick={this.support}>支持</button>
}
support(){
//this:undefined
//ev.target:通过事件源可以获取当前操作的元素(一般很少操作,一般框架是数据驱动所有DOM的改变)
}
如果能让方法中的this变成当前类的实例就好了,这样可以操作属性和状态等信息
support=ev=>{
//this:继承上下文中的this(实例),真实项目中,给JSX元素绑定的事件方法一般都是箭头函数,目的是为了保证函数中的this还是实例
}
受控组件 : 数据管控的组件
非受控组件 : 不受数据管控的组件
在React中
1.基于数据驱动(修改状态数据,React帮助我们重新渲染视图)完成的组件叫做
受控组件
2.通过ref操作DOM实现视图更新的,叫做
非受控组件
真实项目中尽量多使用受控组件
VUE : [MVVM] 数据更改视图也跟着更改,视图更改数据也跟着更改(双向数据绑定)
REACT : [MVC] 数据更改视图跟着更改(原本是单向数据绑定,但是我们可以自己构建出双向的效果)
React中的生命周期函数
所谓生命周期函数(钩子函数) : 描述一个组件或者程序从创建到销毁的过程,我们可以在过程中间基于钩子函数来完成一些自己的操作,例如 : 在第一次渲染完成做什么,或者第二次即将重新渲染之前做什么等
[基本流程]
constructor 创建一个组件
|
componentWillMount 第一次渲染之前
|
render 第一次渲染
|
componentDidMount 第一次渲染之后
[修改流程]
当组件的状态数据发生改变(setState)或者传递给组件的属性发生改变(重新调用组件传递不通的属性都会引发render重新渲染(差异渲染)
shouldComponentUpdate : 是否允许组件重新渲染(允许则执行后面函数,不允许直接结束即可)
|
componentWillUpdate : 重新渲染之前
|
render 第二次及以后重新渲染
|
componentDidUpdate : 重新渲染之后
[componentWillReceiveProps]父组件把传递给子组件的属性 发生改变后触发的钩子函数
[卸载]
原有渲染的是不消失的,只不过以后不能基于数据改变视图了
componentWillUnmount : 卸载组件之前(一般不用)
function queryData() {
return new Promise(resolve => {
setTimeout(()=>{
resolve(200);
},3000)
});
}
class A extends React.Component{
// static default = {};//这个是第一个执行的,执行完成(给属性设置默认值后)才向下执行
constructor(){
super();
console.log("1-constructor");
this.state={
n: 1
};
}
/*componentWillMount(){
console.log("2-willMount: 第一次渲染之前",this.refs.div);//undefined
//在willMount中,如果直接的setState修改数据,会把状态信息改变后,然后render和didMount;但是如果setState是放到一个异步操作中完成(例如:定时器或者从服务器获取数据),也是先执行render和did,然后在执行这个异步操作修改状态,紧接着走修改的流程(这样和放到didMount中没有区别),所以我们一般吧数据请求放到did中处理
//=>真实项目中的数据绑定,一般第一次组件渲染,我们都是绑定的默认数据,第二次才是绑定的从服务器获取的数据(有些需求我们需要根据数据是否存在判断显示隐藏)
this.setState({n: 2})
}*/
async componentWillMount(){
console.log("2-willMount");
let res = await queryData();
this.setState({n: res})
}
componentDidMount(){
console.log("4-didMount: 第一次渲染之后", this.refs.div);//div
/*
* 真实项目中在这个阶段一般做如下处理:
* 1.控制状态信息更改的操作
* 2.从服务器获取数据,然后修改状态信息,完成数据绑定
*/
setInterval(() => {
this.setState({n: this.state.n + 1})
}, 5000);
}
shouldComponentUpdate(nextProps,nextState) {
//在这个钩子函数中,我们获取的state不是最新修改的,而是上一次的state
//例如:第一次加载完成,五秒后,我们基于setState把b修改为2,但是此处获取的还是1
//但是这个方法有两个参数
//nextProps:最新修改的属性
//nextState:最新修改的状态信息
console.log("5-是否允许更新,函数返回true允许,反之false不允许");
return nextState.n > 3 ? false : true;
}
componentWillUpdate(nextProps,nextState) {
// console.log(this.state.n);//这里获取的是更新之前的(和should相同,也有两个参数存储最新的信息-)
console.log("6-组件更新之前")
}
componentDidUpdate(){
// console.log(this.state.n);这里获取的状态是更新之后的
console.log("8-组件更新之后,因为7是render")
}
componentWillReceiveProps(nextProps, nextState) {
console.log("组件属性改变 ", this.props.n, nextProps.n);
//属性改变也会触发子组件重新渲染,继续完成修改这套流程
}
render() {
console.log("render");
return <div ref="div" style={{textAlign:"center",fontSize:"30px"}}>{this.state.n}</div>
}
}
ReactDOM.render(<A/>, root);