从零开始一个前端项目
安装部分
- (如果未安装create-react-app)npm install -g create-react-app
- 在对应文件夹下create-react-app 文件名创建对应的项目
- 失败大多因为powershell权限问题,百度搜索,易于解决
脚手架文件夹浅析
一个App.js文件其实就是一个组件(HTML+CSS+JS),在App.js中
(注意,jsx语法中,组件首字母必须大写)
这个App.js也可以重命名,比如Todolist.js,如果修改,要记得修改本文件中的class Todolist。还得修改index.js中的import Todolist from “./Todolist”、ReactDom.render()
import React,{component} from "react";
class App extends Component{
render(){
return(
<div>你好</div>//在js文件中使用HTML标签,是JSX语法使其生效
//另外jsx要求return的必须全部包裹在一个<div>中
//或者import React,{component,Fragment} from "react"
//然后将所有东西包裹在<Fragment>标签中
);
}
}
ES6的解构赋值,其实等价于:
import React from "react";
const Component = React.componet;
上面这是旧版的写法,新版18.0已经不支持ReactDom.render,但旧版仍能生效,新版是下面样子
function App() {
return (
<div>
hello world
</div>
);
}
export default App;
基础JSX语法使用
- 在render中的标签中如果想使用js语句,需要加{ }
- 调用函数的话,只写函数名,不要带上括号,{this.functionName}
- React中标签绑定事件,如onclick、onchange等,第二个字母要大写
- 改变组件的状态,要用setState({ … }),不允许直接修改state内的数据:this.state.inputvalue=“改变”,这种就不行,不能直接修改,可以拷贝一个副本
- JSX中首字母大写的一般都是组件
- JSX的注释:{/*这是注释*/ },就相当于在里面使用js,需要加{ }
import React, { Component } from "react";
class Todolist extends Component {
constructor(props) {
super(props);//用于继承Component类的一些属性
this.state = {
inputValue: "请输入内容"
}
}
render() {
return (
<div>
<div>
<input
value={this.state.inputValue}
onChange={this.handle.bind(this)}>//这里bind返回的是一个函数,并未调用
</input>
<button>提交</button>
</div>
<ul>
<li>学前端</li>
<li>应付科研</li>
<li>好好休息</li>
</ul>
</div>
);
}
handle(e) {
console.log(this);//显示是Todolist
// console.log(e.target.value);
this.setState({
inputValue: e.target.value
})
}
}
export default Todolist;
实现一个基本的todolist,可以增删
这是Todolist.js文件(App.js)
import React, { Component } from "react";
class Todolist extends Component {
constructor(props) {
super(props);//用于继承Component类的一些属性
this.state = {
inputValue: "请输入内容",
list: ["学前端", "搞科研","运动锻炼"]
}
}
render() {
return (
<div>
<div>
<input
value={this.state.inputValue}
onChange={this.handle.bind(this)}>
</input>
<button onClick={ this.handleBtnClick.bind(this)}>提交</button>
</div>
<ul>
{this.state.list.map((item,index) => {
return <li key={index} onClick={ this.handleItemDelete.bind(this,index)}>{item}</li>
}) }
</ul>
</div>
);
}
handle(e) {
// console.log(this);
// console.log(e.target.value);
this.setState({
inputValue: e.target.value
})
}
handleBtnClick() {
this.setState({
list: [...this.state.list, this.state.inputValue],
inputValue:""
})
}
handleItemDelete(k) {
let newlist = this.state.list;
newlist.splice(k, 1);//删除一个元素
this.setState({
list:newlist
})
}
}
export default Todolist;
jsx语法补充
- label的for要改为htmlFor(以防与循环冲突)
- 标签样式名不能用class,要用className,以免与类冲突
- 如果想要对输入框内的文字,取消自动转义,可以用dangerouslySetHTML(有遭受XSS攻击的风险)
组件拆分
拆分上面的Todolist.js文件,具体是把它里面的<li>标签剥离出来,形成一个TodoItem.js的文件,而后再进行父子组件通信。
新建一个TodoItem.js,并且在Todolist.js中import引入
将Todolist.js中的下面这一部分
<ul>
{this.state.list.map((item,index) => {
return <li
key={index}
onClick={ this.handleItemDelete.bind(this,index)}
>{item}</li>
})
}
</ul>
改成下面的样子:就能获取到父组件的数据了
<ul>
{this.state.list.map((item,index) => {
return <TodoItem content={ item }/>
}) }
</ul>
对应的TodoItem.js内的初步写法如下
class TodoItem extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<li>{ this.props.content}</li>
);
}
}
- 父传子:通过属性props,父组件不仅可以传值还可以传方法给子组件
接下来想实现点击删除的操作,可以将父组件的 handleItemDelete(k)传给子组件,让子组件来调用
<ul>
{this.state.list.map((item,index) => {
return <TodoItem content={item} in={index}
dadDelete={ this.handleItemDelete.bind(this)}
{/*如果这里dadDelete={ this.handleItemDelete},就会报错,原因是
在TodoItem.jsz中无法找到this.handleItemDelete这个函数,因此只有绑定了
this值,子组件在编译到这句话时,才能正确调用父组件的函数*/}
/>
})
}
</ul>
对应的TodoItem.js内的写法如下
class TodoItem extends React.Component {
constructor(props) {
super(props);
this.deleteFu = this.deleteFu.bind(this);//放在这里会节约性能
}
render() {
return (
<li onClick={this.deleteFu}>{ this.props.content}</li>
);
}
deleteFu() {
// console.log(this.props.in);
this.props.dadDelete(this.props.in);
}
}
单向数据流
react只允许单向的。比如子组件只能去使用来自父组件的值,但不能去改变它
代码优化
- 使用解构赋值
- 将标签内的函数this.deleteFu.bind(this)全部放在constructor内:this.deleteFu = this.deleteFu.bind(this)
- setState()括号内由返回对象改为返回箭头函数(箭头函数内返回对象)
- setState()内凡是用到this.state的地方都可以通过箭头函数传入prevState来代替
优化后总体的TodoItem.js如下:
import React from "react";
class TodoItem extends React.Component {
constructor(props) {
super(props);
this.deleteFu = this.deleteFu.bind(this);//放在这里会节约性能
}
render() {
const { content } = this.props;{/*解构赋值*/}
return (
<li onClick={this.deleteFu}>{content}</li>{/新/}
<li onClick={this.deleteFu}>{ this.props.content}</li> {/旧,弃用/}
);
}
deleteFu() {
// console.log(this.props.in);
const { dadDelete,index } = this.props;{/*注意这里函数也解构赋值了*/}
dadDelete( index );
}
}
export default TodoItem;
setState()的优化步骤如下面所示,由新到旧
handleBtnClick() {
/*版本3.0最终版 */ /*()=>{return{}}改进为()=>({}) */
this.setState((prevState) => ({
list: [...prevState.list, prevState.inputValue],
inputValue: ""
}
))
/*版本2.0 */ /*由对象改为箭头函数 */
// this.setState(() => {
// return {
// list: [...this.state.list, this.state.inputValue],
// inputValue: ""
// }
// })
/*版本1.0*/
// this.setState({
// list: [...this.state.list, this.state.inputValue],
// inputValue:""
// })
}
handleItemDelete(k) {
/*版本2.0 */
this.setState((prevState) => {
const newlist = prevState.list;
newlist.splice(k, 1);
return {
list: newlist
}
})
/*版本1.0 */
// let newlist = this.state.list;
// newlist.splice(k, 1);//删除一个元素
// this.setState({
// list:newlist
// })
}
每当数据或者props发生改变的时候,就会重新render
父组件render,子组件也会重新render,因为子组件就嵌在父组件内
React虚拟DOM
- state数据
- JSX模板
- 数据+模板 生成虚拟DOM(用JS实现),比如
[”div“, {id:“name”},[”span“,{ },“hello world”] ]
React提供了一些API来创建一般JS对象:
const vDOM=React.createElement(‘xx’,{id:‘xx’},‘xx’) - 用虚拟DOM生成真实DOM
<div id=“name”><span>hello world</span></div> - state数据改变
- 数据+模板 生成新的虚拟DOM
[”div“, {id:“name”},[”span“,{ },“you will get what you want” ] ] - 比较新的虚拟DOM与原始虚拟DOM的区别
Diff算法:二叉树同层比对,带key和不带key的比对,key要唯一才好 - 根据刚才找到的差异,直接操作DOM,改变span的内容
优点:
- 性能得到了提升,不需要数据一改变就重新渲染整个DOM
- 使得跨端应用得以实现,React Native
- React高效的原因就是Diff算法!
setState()是异步操作
- setState(()=>{},()=>{})其实里面是可以放两个函数的,第一个是异步更改状态,第二个是更改完成之后的callback函数,大多数时候并不需要使用callback
- setState()底层有其优化机制,如果短时间内多次更改状态,他会把几次的状态改变集中在一起观察比较,这样避免了多次生成VDOM
ref提供了一种直接获取DOM的方法,但最好别用
<label htmlFor="content">请输入内容
<input
id="content"
value={this.state.inputValue}
onChange={this.handle}
ref={(input) => {this.input=input }}>{/*这里就提供了可以直接拿到input节点的方式*/}
</input>
</label>
在之后的方法中,就可以不用事件e.target
handle(e) {
// const value = e.target.value;不用ref,最好采用这种方式
const value = this.input.value;//用ref可以直接获取input节点,最好别用
this.setState(() => {
return {
inputValue: value
}
})
}
组件的生命周期函数(旧)
初始化阶段: 由ReactDOM.render()触发—初次渲染
- constructor()
- componentWillMount()
- render()
- componentDidMount()
更新阶段: 由组件内部this.setSate()或父组件重新render触发
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
卸载组件: 由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
组件的生命周期函数(新)
初始化阶段: 由ReactDOM.render()触发—初次渲染
- constructor()
- getDerivedStateFromProps
- render()
- componentDidMount()
更新阶段: 由组件内部this.setSate()或父组件重新render触发
- getDerivedStateFromProps
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate
- componentDidUpdate()
卸载组件: 由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
弃用了三个will函数
组件的生命周期演示:
class TodoItem extends React.Component {
constructor(props) {
super(props);
this.deleteFu = this.deleteFu.bind(this);//放在这里会节约性能
}
componentWillMount() {/*已经不鼓励用了,最好别用 */
console.log("componentWillMount");
}
shouldComponentUpdate() {
console.log("shouldComponentUpdate?");
return true;
}
render() {
console.log("render");
const { content } = this.props;
return (
<li onClick={this.deleteFu}>
{content}</li>
);
}
deleteFu() {
// console.log(this.props.in);
const { dadDelete,index } = this.props;
dadDelete( index );
}
componentDidMount() {/*适合发送AJAX请求 */
console.log("componentDidMount");
}
componentWillUnmount() {/*适合收尾工作,比如清理定时器,这个函数并未弃用,而且还是比较重要的勾子函数 */
console.log("componentWillUnmount");
}
}
生命周期函数的应用
- 比如shouldComponentUpdate()来看是否该更新,以提升性能
shouldComponentUpdate(nextProps,nextState) {
console.log("shouldComponentUpdate?");
if (nextProps.content !== this.props.content) {
return true;
} else {
return false;
}
}
效果如上,可见子组件TodoItem.js在输入框输入的同时,一直在打印shouldComponentUpdate?可在”提交“按钮未被点击之前并没有child render,这样就优化了性能
- componentDidMount()函数中发送AJAX请求
需要先引入依赖axios:终端输入npm install axios --save
就可以在node_modules里面看到axios了
在组件头部import axios from “axios”;
开始敲
componentDidMount() {/*适合发送AJAX请求 */
axios.get(".api/somedata")//这个文件不存在,所以一定会报错
.then((res) => { console.log(res) })
.catch((err) => { alert(err) });
}
axios实现服务器客户端通信
两个不同的端口不能进行通信的根源在于AJAX引擎的阻拦,他不允许协议、端口、主机(域名)不同的两台设备进行通信,可以返回请求数据,但拿不到。中间的代理服务器之所以能作为服务器和客户端的中间人,是因为他没有AJAX引擎,因此不受协议、端口、主机约束。
如果想使用代理,可以在package.json里面添加”proxy“:"http://…地址"但这种方式只能转发给一台固定的服务器。不能转发给多个地址。
更有效的方法,是在src里面新增一个setupProxy.js的文件,它是用commonJS写的,这个文件是固定的名称,不能改名,React脚手架会找到这个文件,把它加到webpack的配置里面
创建一个文件夹,编写自创的服务器脚本myCreateServer.js
const express = require('express')
const app = express()
//这个是用express语言写的
app.use((request, response, next) => {
console.log('有人请求服务器');
console.log('请求来自于', request.get('Host'));
console.log('请求的地址', request.url);
next()
})
app.get('/todos', (request, response) => {
const todos = [
"学前端","爱惜身体","搞科研"
]
response.send(todos)
})
app.listen(5000, (err) => {
if (!err) console.log('服务器启动成功,服务器地址为:http://localhost:5000/todos');
})
需要在这个文件中npm install require、npm install express这两个包才能启用服务器:node myCreateServer.js,服务器启动
在Todolist.jsx文件夹中编写setupProxy.js文件,这个文件是用commonJs写的,用到了http-proxy-middleware中间件,这是create-react-app自动下载的,旧版的写法已经不支持了,改成下面的写法即可
const { createProxyMiddleware: proxy } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
proxy('/api', { //遇见/api1前缀的请求,就会触发该代理配置
target: 'http://localhost:5000', //请求转发给谁
changeOrigin: true,//控制服务器收到的请求头中Host的值,欺骗服务器让它以为请求一样来自5000端口
pathRewrite: { '^/api': '' } //重写请求路径(必须)
})
);
}
在componentDidMount()函数中发送AJAX请求,并将请求返回得到的数据加以利用
componentDidMount=()=> {/*适合发送AJAX请求 */
console.log("componentDidMount");
axios.get("http://localhost:3000/api/todos")
.then((res) => {
console.log(res.data);
this.setState(() => {
return {
list: res.data
}
})
})
.catch((err) => { alert(err); })
}