Create by jsliang on 2019-3-18 08:37:10
Recently revised in 2019-3-22 11:46:54
Hello 小伙伴们,如果觉得本文还不错,记得给个 star , 小伙伴们的 star 是我持续更新的动力!GitHub 地址
一 目录
不折腾的前端,和咸鱼有什么区别
目录 |
---|
一 目录 |
二 前言 |
三 正文 |
3.1 新建 React 项目 |
3.2 项目目录解析 |
3.3 精简项目结构 |
3.4 初探组件 |
3.5 JSX |
3.6 事件及双向数据绑定 |
3.7 优化-抽取 CSS |
3.8 优化-抽取 JS |
四 总结 |
五 参考文献 |
二 前言
通过编写一个简单的 TodoList 小 Demo,熟悉 React 的开发流程。
三 正文
Now,开始搞事情。
3.1 新建 React 项目
- 下载 Node.js
- 安装 React 脚手架:
npm i create-react-app -g
- 开启新项目:
create-react-app todolist
cd todolist
npm start
- 打开
localhost:3000
查看页面
3.2 项目目录解析
- todolist
+ node_modules —————————— 项目依赖的第三方的包
- public ———————————————— 共用文件
- favicon.ico —— 网页标签左上角小图标
- index.html —— 网站首页模板
- mainfest.json —— 提供 meta 信息给项目,并与 serviceWorker.js 相呼应,进行离线 APP 定义
- src ——————————————————— 项目主要目录
- App.css —— 主组件样式
- App.js —— 主组件入口
- App.test.js —— 自动化测试文件
- index.css —— 全局 css 文件
- index.js —— 所有代码的入口
- logo.svg —— 页面的动态图
- serviceWorker.js —— PWA。帮助开发手机 APP 应用,具有缓存作用
- .gitignore ——————————— 配置文件。git 上传的时候忽略哪些文件
- package-lock.json ———— 锁定安装包的版本号,保证其他人在 npm i 的时候使用一致的 node 包
- package.json ————————— node 包文件,介绍项目以及说明一些依赖包等
- README.md ———————————— 项目介绍文件
复制代码
3.3 精简项目结构
为了方便开发,下面对 creat-react-app 的初始目录进行精简:
- todolist
+ node_modules —————————— 项目依赖的第三方的包
- public ———————————————— 共用文件
- favicon.ico —— 网页标签左上角小图标
- index.html —— 网站首页模板
- src ——————————————————— 重要的目录
- App.js —— 主组件入口
- index.js —— 所有代码的入口
- .gitignore ——————————— 配置文件。git 上传的时候忽略哪些文件
- package.json ————————— node 包文件,介绍项目以及说明一些依赖包等
- README.md ———————————— 项目介绍文件
复制代码
favicon.ico
、.gitignore
、README.md
是我们无需理会的,但是其他文件,我们需要精简下它们的代码:
代码详情
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>Todolist</title>
</head>
<body>
<noscript>你需要允许在 APP 中运行 JavaScript</noscript>
<div id="root"></div>
</body>
</html>
复制代码
代码详情
- App.js
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div className="App">
Hello React!
</div>
);
}
}
export default App;
复制代码
代码详情
- index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
代码详情
- package.json
{
"name": "todolist",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-scripts": "2.1.8"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
复制代码
3.4 初探组件
类似于上图,在进行页面开发的时候,我们很容易地使用庖丁解牛的技巧,将页面进行划分,然后一部分一部分地将页面搭建出来。
给个比较官方的说法,就叫页面组件化:将页面切成几个部分,从而有利于页面的拼装以及代码的维护。
在 create-react-app 的默认配置中,App.js 就是一个组件,一起来看:
App.js
// 1. 引用 React 及其组件
import React, { Component } from 'react';
// 2. 定义一个叫 App 的组件继承于 Component
class App extends Component {
render() {
return (
<div className="App">
Hello React!
</div>
);
}
}
// 3. 根据 React 实例,在 App 内部编写完毕后,导出这个 App 组件
export default App;
复制代码
在上面,我们引用、定义并导出了这个 App 的组件,然后我们就要使用它:
index.js
// 1. 引入 React、ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';
// 2. 将 App.js 导入进来
import App from './App';
// 3. 通过 ReactDOM 将 App.js 以虚拟 DOM 的形式渲染/挂载到 root 根节点,该节点在 index.html 中
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
index.js
告诉我们,它会通过 ReactDom,将 App.js 这个组件挂载到 root
这个节点上,那么,这个 root
在哪里呢?我们查看下 index.html:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortc ut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>Todolist</title>
</head>
<body>
<noscript>你需要允许在 APP 中运行 JavaScript</noscript>
<div id="root"></div>
</body>
</html>
复制代码
OK,很容易地我们就捋清楚思路了:我们在 index.html 中定义了个 root
根节点,然后我们通过 index.js,将 App.js 以组件形式渲染到了 index.html 中,从而实现了节点的挂载。
思维发散:我们知道 index.js 和 App.js 的最终结合是挂载到
id="root"
节点上的,如果我们再开一个 index2.js 和 App2.js,挂载到id="root2"
节点上,行不行呢?亦或者我们开一个id="root3"
的节点,我们在其中操作 jQuery,是不是也可行?
3.5 JSX
在 create-react-app 的文件中,不管是 index.js 中的:
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
还是 App.js 中的:
class App extends React.Component {
render() {
return (
<div className="App">
Hello React!
</div>
);
}
}
复制代码
等这些有关 DOM 的渲染,都需要用到 JSX,因此需要引入 React:
import React from 'react';
复制代码
- JSX 的定义:
那么,什么是 JSX 呢?
React 的核心机制之一就是可以在内存中创建虚拟的 DOM 元素。React 利用虚拟 DOM 来减少对实际 DOM 的操作从而提升性能。
JSX 就是 JavaScript 和 XML 结合的一种格式。
React 发明了 JSX,利用 HTML 语法来创建虚拟 DOM。当遇到 <
,JSX 就当 HTML 解析,遇到 {
就当 JavaScript 解析。
- JSX 的使用:
在 JSX 语法中,如果我们需要使用自己创建的组件,我们直接使用它的定义名即可,例如:
index.js
// 1. 引入 React、ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';
// 2. 将 App.js 导入进来
import App from './App';
// 3. 通过 ReactDOM 将 App.js 以虚拟 DOM 的形式渲染/挂载到 root 根节点,该节点在 index.html 中
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
其中第三点即是自定义组件渲染到根节点。
提示:在 React 中,如果需要使用自定义组件,那么该组件不能小写开头
app,而是使用App
这样的大写开头形式。
3.6 事件及双向数据绑定
这是我们精简后的目录结构:
我们修改下目录结构,开始编写 TodoList:
首先,我们修改 App.js 为 TodoList.js:
App.jsTodoList.js
import React, { Component } from 'react';
class TodoList extends Component {
render() {
return (
<div className="TodoList">
Hello React!
</div>
);
}
}
export default TodoList;
复制代码
然后,我们修改 index.js 中挂载到 index.html 的组件为 TodoList:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';
// 3. 通过 ReactDOM 将 App.js 以虚拟 DOM 的形式渲染/挂载到 root 根节点,该节点在 index.html 中
ReactDOM.render(<TodoList />, document.getElementById('root'));
复制代码
修改完毕,小伙伴们可以重启下 3000 端口,查看下我们的 React 是否能正常启动。
在此步骤中,我们仅仅修改 App.js 为 TodoList.js,使 index.js 挂载到
root
的是 TodoList.js,除此之外没进行其他操作。
最后,如果没有问题,那么我们进一步编写 TodoList,获取到 input 输入框的值,并渲染到列表中:
代码详情TodoList.js
// Fragment 是一种占位符形式,类似于 Vue 的 Template
import React, { Component, Fragment } from 'react';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
inputValue: '',
list: []
}
}
render() {
return (
<Fragment>
<div>
{/* 单项数据绑定 */}
<input
type="text"
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
<button>提交</button>
</div>
<ul>
<li>
<span>吃饭</span>
<span>X</span>
</li>
<li>
<span>睡觉</span>
<span>X</span>
</li>
<li>
<span>打豆豆</span>
<span>X</span>
</li>
</ul>
</Fragment>
)
}
handleInputChange(e) {
console.log(e.target.value);
this.setState({
inputValue: e.target.value
})
}
}
export default TodoList;
复制代码
我们先查看演示:
OK,这样我们在每输入一个字符的时候,我们就能立刻获取到对应的数据,这时候实现了单向数据流:输入框 -> JS 内存。
其中有 3 点需要讲解下:
Fragment
是 React 提供的一种占位符,它像<input>
、<span>
等标签一样,但是它在实际渲染的时候是不会出现的。因为 React 的 JSX 首层必须要有标签,然后如果使用<div>
等会占用一个层级,所以,类似于 Vue 的Template
,React 使用了Fragment
这种空标签。constructor
表示父类的构造方法,在 ES6 中,构造方法constructor
相当于其构造函数,用来新建父类的this
对象,而super(props)
则是用来修正this
指向的。简而言之,我们可以在这里定义数据,并在整个 js 文件中使用。onChange
这种写法,是 React 指定的写法,例如onClick
等,在原生 JS 中,使用的是onclick
,而在 React 中,为了区别,需要进行半驼峰编写事件名字。同时,绑定的handleInputChange
,可以直接在render
下面进行编写。
这样,我们就对 React 的数据及事件有了初步理解,下面我们加下按钮点击新增列表事件以及点击 X
删除列表事件。
代码详情TodoList.js
// Fragment 是一种占位符形式,类似于 Vue 的 Template
import React, { Component, Fragment } from 'react';
class TodoList extends Component {
// 构造函数
constructor(props) {
super(props);
// 定义数据
this.state = {
inputValue: '',
list: []
}
}
// 渲染页面
render() {
let closeStyle = {
fontSize: '1.2em',
color: 'deepskyblue',
marginLeft: '10px'
}
return (
<Fragment>
<div>
{/* 单项数据绑定 */}
{/* 在 React 中,绑定时间的,一般为半驼峰形式 */}
<input
type="text"
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
<button onClick={this.handleBtnClick.bind(this)}>提交</button>
</div>
<ul>
{
this.state.list.map( (item, index) => {
return <li key={index}>
<span>{index}. {item}</span>
<span style={closeStyle} onClick={this.handleItemDelete.bind(this, index)}>X</span>
</li>
})
}
</ul>
</Fragment>
)
}
// 方法体 - 输入内容
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
// 方法体 - 点击提交
handleBtnClick() {
this.setState({
list: [...this.state.list, this.state.inputValue],
inputValue: ''
})
}
// 方法体 - 删除项目
handleItemDelete(index) {
const list = [...this.state.list];
list.splice(index, 1);
this.setState({
list: list
})
}
}
export default TodoList;
复制代码
在这一部分,我们需要了解 3 个知识点:
- 在
render
中closeStyle
这个变量,我们用来定义 CSS 属性,然后我们通过style={closeStyle}
,直接写了个行内样式(下面我们会抽离出来) - 关于 JSX 遍历输出的形式:
{
this.state.list.map( (item, index) => {
return <li key={index}>
<span>{index}. {item}</span>
<span style={closeStyle} onClick={this.handleItemDelete.bind(this, index)}>X</span>
</li>
})
}
复制代码
我们通过 {}
里面循环输出 DOM 节点。如果你学过 jQuery,那么可以将它当为拼接字符串;如果你学过 Vue,那么可以将它当成 v-for
变了种写法。
在这里我们不用理会为什么这么写,我们先接受这种写法先。
- 关于改变
constructor
中的数据,我们使用:
this.setState({
list: list
})
复制代码
这种形式。其实,这也是有记忆技巧的,要知道我们在定义数据的时候,使用了:
// 定义数据
this.state = {
inputValue: '',
list: []
}
复制代码
即: this.state
,那么我们需要修改数据,那就是 this.setState
了。
至此,我们的简易 TodoList 就实现了,下面我们进一步优化,将 CSS 和 JS 进一步抽取。
3.7 优化-抽取 CSS
在上面中,我们提到 closeStyle
是一种行内的写法,作为一枚 完美编程者,我们肯定不能容忍,下面我们开始抽离:
TodoList.js
import React, { Component, Fragment } from 'react';
import './style.css'
// ... 省略中间代码
<ul>
{
this.state.list.map( (item, index) => {
return <li key={index}>
<span>{index}. {item}</span>
<span className="icon-close" onClick={this.handleItemDelete.bind(this, index)}>X</span>
</li>
})
}
</ul>
复制代码
在这里,我们需要知道:我们可以通过 import
的形式,直接将 CSS 文件直接导入,然后,我们命名 class
的时候,因为 React 怕 JS 的 class
与 HTML 的 class
冲突,所以我们需要使用 className
。
最后我们再编写下 CSS 文件:
.icon-close {
font-size: 1.2em;
color: deepskyblue;
margin-left: 10px;
}
复制代码
如此,我们就实现了 CSS 的抽取。
3.8 优化-抽取 JS
在第 4 章关于组件的介绍中,我们讲到:一些复杂的 JS 是可以抽取出来,并以组件的形式,嵌入到需要放置的位置的。
那么,我们在 JSX 越写越多的情况下,是不是可以将列表渲染那部分抽取出来,从而精简下 JSX 呢?
答案是可以的,下面我们看下实现:
代码详情TodoList.js
// Fragment 是一种占位符形式,类似于 Vue 的 Template
import React, { Component, Fragment } from 'react';
// 引入组件
import TodoItem from './TodoItem';
// 引用样式
import './style.css';
class TodoList extends Component {
// 构造函数
constructor(props) {
super(props);
// 定义数据
this.state = {
inputValue: '',
list: []
}
this.handleInputChange = this.handleInputChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
}
// 渲染页面
render() {
return (
<Fragment>
<div>
<label htmlFor="insertArea">输入内容:</label>
{/* 单项数据绑定 */}
{/* 在 React 中,绑定时间的,一般为半驼峰形式 */}
<input
id="insertArea"
type="text"
value={this.state.inputValue}
onChange={this.handleInputChange}
/>
<button onClick={this.handleBtnClick}>提交</button>
</div>
<ul>
{/* 精简 JSX,将部分抽取出来 */}
{ this.getTodoItem() }
</ul>
</Fragment>
)
}
// 获取单独项
getTodoItem() {
return this.state.list.map( (item, index) => {
return (
<TodoItem
key={index}
item={item}
index={index}
handleItemDelete={this.handleItemDelete}
/>
)
})
}
// 方法体 - 输入内容
handleInputChange(e) {
const value = e.target.value;
this.setState( () => ({
inputValue: value
}))
}
// 方法体 - 点击提交
handleBtnClick() {
const list = this.state.list,
inputValue = this.state.inputValue;
this.setState( () => ({
list: [...list, inputValue],
inputValue: ''
}))
}
// 方法体 - 删除项目
handleItemDelete(index) {
// immutable - state 不允许做任何改变
const list = [...this.state.list];
list.splice(index, 1);
this.setState( () => ({
list: list
}))
}
}
export default TodoList;
复制代码
我们关注下 TodoList.js 的改变:
- 我们在
constructor
中,将方法进行了提前定义:
this.handleInputChange = this.handleInputChange.bind(this);
复制代码
这样,我们在下面就不用写 .bind(this)
形式了。
- 我们修改了下
this.setState()
的形式:
原写法:
this.setState({
list: list
})
复制代码
现写法:
this.setState( () => ({
list: list
}))
复制代码
因为 React 16 版本进行了更新,使用这种写法比之前的好,至于好在哪,咱先不关心,以后就用这种写法了。
- 我们引用了组件:
import TodoItem from './TodoItem';
复制代码
并且将组件放到方法体:this.getTodoItem()
中,而 this.getTodoItem()
的定义是:
// 获取单独项
getTodoItem() {
return this.state.list.map( (item, index) => {
return (
<TodoItem
key={index}
item={item}
index={index}
handleItemDelete={this.handleItemDelete}
/>
)
})
}
复制代码
在这里我们可以看到,我们通过自定义值的形式,将数据 key
、item
、index
传递给了子组件 TodoItem
。同时,通过 handleItemDelete
,将自己的方法传递给了子组件,这样子组件就可以调用父组件的方法了:
代码详情TodoItem.js
import React, { Component } from 'react'
class TodoItem extends Component {
constructor(props) {
super(props);
// 这种写法可以节省性能
this.handleClick = this.handleClick.bind(this);
}
render() {
const { item } = this.props;
return (
<li>
<span>{item}</span>
<span className="icon-close" onClick={this.handleClick}>X</span>
</li>
)
}
handleClick() {
const { handleItemDelete, index } = this.props;
handleItemDelete(index);
}
}
export default TodoItem;
复制代码
这样,我们就完成了组件的抽取,并学会了
- 父组件传递值给子组件
- 子组件调用父组件的方法
由此,我们在接下来就可以编写更丰富健全的项目了。
本文代码地址:React 系列源码地址
四 总结
在我们学习任意一门语言中,大多就是上手 “Hello World!” 编程~
然后做小案例的时候,我们都喜欢来个 TodoList,因为它能讲清楚一些有关基础的知识点。
现在,我们回顾下,我们开发 React 的 TodoList 有啥收获:
- create-react-app 的安装及开发。
- 组件化的思想及在 create-react-app 中关于组件化的应用。
- React 关于数据 data 以及方法 methods 的定义及使用,以及如何进行数据双向绑定。
- 将大的组件拆分成小组件,并实现父子组件通讯(父组件传递参数给子组件,子组件调用父组件的方法)
至此,jsliang 就精通 jQuery、Vue、React 编写 TodoList 了,哈哈!
五 参考文献
- 《React.Component 与 React.PureComponent(React之性能优化)》
- 《visual studio code + react 开发环境搭建》
- 《react 中 constructor() 和 super() 到底是个啥?》
jsliang 广告推送:
也许小伙伴想了解下云服务器
或者小伙伴想买一台云服务器
或者小伙伴需要续费云服务器
欢迎点击 云服务器推广 查看!
jsliang 的文档库 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/LiangJunron…上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。