最近,公司招了几个新同学,没有React基础,就整理了一份入门文档,希望能帮助到他们。
前言
目标读者:刚接触React技术栈的新同学。
本文目标:希望读者能知道为什么要使用这些技术栈,我们要借助它解决什么问题。api用法相关请查阅文档。
脚手架
为什么需要脚手架
随着产品线的发展,我们会不断的有新项目需要生成。这时候我们就会有一些麻烦:
- 每一个新项目都需要重新配置各类文件;
- 不利于统一管理升级
脚手架做了什么
我们的脚手架借助了第三方命令行工具YEOMAN
。我们借助它做的事情是:从目标地址拉取预先准备好的代码模版(如webpack.config.js
、src目录
、package.json
...)到我们指定的目录。在执行它的命令后,会进入它的生命周期,依次做的事情是
- 状态初始化;
- 询问用户具体配置,比如脚手架类型、项目名称、作者等;
- 下载模版文件压缩包并解压到本地;
- 安装项目依赖并启动
我们只是继承它的一个基类,在它提供的生命周期里实现了这些事情。
安装依赖
npm install -g yarn
yarn config set registry https://registry.dingxiang-inc.com
yarn global add yo generator-ctu
如果长时间下载不了,换用淘宝源[https://registry.npm.taobao.org](https://registry.npm.taobao.org)
试试。
脚手架初始化
mkdir test
cd test
yo ctu
如果一直卡在“正在下载项目模板”,可以直接到github直接下载解压。这样的话,你需要自己进入到项目中安装依赖、启动项目。
另:如果有兴趣,可以看下脚手架代码,看是在哪一步卡住,可以提交下代码优化下这个问题。
了解项目结构
如果初始化成功,你会看到如下项目结构[图片上传失败...(image-dac1c0-1592968805612)]
备注:
-
data
文件夹在真实项目中已经移入src
内,放的是一些部署后可以被替换的资源,比如logo文件、配置文件 -
.yo-rc
可以不用管,脚手架生成后的产物。
可以进入readme.md查看src
目录下的文件概述。
React
预备知识
JSX语法
为什么需要它
假设我们现在要实现一个功能,可以点击体验下
1. 输入框为空时,tweet按钮不可点
2. 输入框下方显示还可以输入的字符数量
3. 点击add photo按钮,剩余字符数量及add photo按钮状态发生改变(假定图片占用23个字符)
我们看一段jQuery和React代码的对比
React
var TweetBox = React.createClass({
getInitialState: function() {
return {
text: "",
photoAdded: false
};
},
handleChange: function(event) {
this.setState({ text: event.target.value });
},
togglePhoto: function(event) {
this.setState(prevState => {
return {
photoAdded: !prevState.photoAdded
}
});
},
remainingCharacters: function() {
const photoCharacterLength = 23
const maxCharacterLength = 140
if (this.state.photoAdded) {
return maxCharacterLength - photoCharacterLength - this.state.text.length;
}
return maxCharacterLength - this.state.text.length;
},
render: function() {
const { text, photoAdded } = this.state
return (
<div>
<textarea onChange={this.handleChange}></textarea>
<br/>
<span>{ this.remainingCharacters() }</span>
<button disabled={!text.length && !photoAdded}>
Tweet
</button>
<button onClick={this.togglePhoto}>
{photoAdded ? "✓ Photo Added" : "Add Photo" }
</button>
</div>
);
}
});
React.render(
<TweetBox />,
document.body
);
jQuery
<div class="well clearfix">
<textarea></textarea>
<br>
<span>140</span>
<button class="js-tweet-button" disabled>Tweet</button>
<button class="js-add-photo-button">Add Photo</button>
</div>
$("textarea").on("input", function() {
if ($(".js-add-photo-button").hasClass("is-on")) {
// add phtot按钮已经点击,剩余输入文本数量再减23
$("span").text(140 - 23 - $(this).val().length);
} else {
// 计算剩余文本数量
$("span").text(140 - $(this).val().length);
}
if ($(this).val().length > 0 || $(".js-add-photo-button").hasClass("is-on")) {
// 如果文本框里有内容或者add phtot按钮已经点击,tweet button设置为可点击状态
$(".js-tweet-button").prop("disabled", false);
} else {
// tweet button设置为不可点击状态
$(".js-tweet-button").prop("disabled", true);
}
});
// 给添加照片的按钮绑定点击事件监听
$(".js-add-photo-button").on("click", function() {
if ($(this).hasClass("is-on")) {
$(this).removeClass("is-on").text("Add Photo"); // 切换add photo按钮显示状态
$("span").text(140 - $("textarea").val().length);
if ($("textarea").val().length === 0) {
// 切换tweet按钮前需要先判断textarea当前状态
$(".js-tweet-button").prop("disabled", true);
}
} else {
$(this).addClass("is-on").text("✓ Photo Added"); // 切换add photo按钮显示状态
$("span").text(140 - 23 - $("textarea").val().length);
$(".js-tweet-button").prop("disabled", false);
}
});
你会发现
- 在
React
中,state
成为了事件和render()
之间的过渡:每个事件不需要担心哪一部分的DOM
发生变化,他们只需要设置state
就可以了。相应的,当你写render()
的时候,你也只需要担心现在的state
是什么。 -
jQuery
没有中间的过渡层state
,我们需要花费很大的精力来解决它们之间相互的联系,bug
就经常会出现在这里。 -
React
中把各个UI
组件独立出来,有利于提高UI
组件的复用率同时降低各个UI
组件的耦合。 - 新手在直接操作
DOM
时很难写出高效而又优雅的代码,从而使得前端代码变得越来越难以维护。
它是怎么运作的
当我们在代码里写jsx
这个语法时,会被babel编译成浏览器可执行的代码。
比如
// jsx语法
const element = <h1 id='h1' className='h1'><span>你好</span></h1>
// 将声明的元素渲染到节点上
ReactDOM.render(element, document.getElementById('root'));
会被转成
const element = React.createElement(
"h1",
{
id: "h1",
className: "h1"
}, // 节点/组件上的属性
React.createElement("span", null, "你好") // 子元素
);
ReactDOM.render(element, document.getElementById('root'));
执行React.createElement
后,我们会得到一个用来描述这个节点的对象,比如元素是原生元素,或者是一个React组件
,还是说只是一个单纯的文本,有没有子节点等等。
ReactDOM.render
就会根据这个描述,解析出一个节点,如果有子节点就递归往下解析,最终解析出一棵DOM
树,渲染到root
节点里。
当我们通过调用this.stState
改变state
的时候,在this.stState
这个函数内部,最终会调用组件的render
函数,render
函数会重新返回一个描述节点的对象。React
会和之前的对象进行比较,来决定哪些组件需要重新计算渲染,来进行最细粒度的重绘。
前端路由
预备知识
url的#号
history对象
hashchange
popstate
什么是前端路由
客户端浏览器可以不依赖服务端,根据不同的URL渲染不同的视图界面。
为什么需要前端路由
Ajax出现之前,路由工作是由后端处理。在进行页面切换的时候,浏览器发送不同的url请求;服务器接收到浏览器的请求时,通过解析不同的url去拼接需要的html或者模板,然后将结果返回给浏览器端进行渲染。
服务端渲染的优势:
- 安全性更高,更严格得控制页面的展现,如下单支付流程
- 有利于SEO
- 首屏渲染快
服务端渲染的优势:
- 服务器的计算压力,消耗服务器性能
- 不容易维护,如果不使用node中间层,前后端分工不明确,前后端可能同时在一个项目中开发
- 每一次切换页面都需要reload页面,用户体验较差
前端路由渲染的目标
- 在页面不刷新的前提下实现url变化
- 捕捉到url的变化,以便执行页面替换逻辑
它是怎么运作的
hash(IE 8)
打开控制台,执行下面代码
window.addEventListener('hashchange', function() {
console.log('The hash has changed!')
}, false);
window.location.hash = 'testhash'
你会发现控制台执行了回调函数,打印了 The hash has changed!
,在url上也能看到 #testhash
,在url上直接改变 #
后面的内容,同样会执行回调函数。
history(IE 10)
window.onpushstate = function () {
console.log('The hash has changed!')
}
(function (history) {
var pushState = history.pushState;
history.pushState = function (state,title,pathname) {
if (typeof window.onpushstate == "function") {
window.onpushstate(state,pathname);
}
return pushState.apply(history, arguments);
};
})(window.history);
因为pushState
、replaceState
不会触发onpopstate事件事件,所以可以采用 aop
的方法进行监听。现在,我们就可以通过调用 pushState
的方法来改变路径,同时我们也能监听到。
let stateObj = {
foo: "bar",
};
history.pushState(stateObj, "page 2", "/bar.html")
我以hashRouter举例
import RouterContext from './RouterContext'
import React, { Component } from 'react'
const location = window.location
export default class Router extends Component {
state = {
location: {
pathname: location.hash.slice(1),
state: null
},
history: {
push: (to) => {
if (typeof to === 'object') {
window.location.hash = to.pathname
this.locationState = to.state
} else {
window.location.hash = to
}
}
}
}
locationState = undefined
componentDidMount () {
window.addEventListener('hashchange', (HashChangeEvent) => {
this.setState({
location: {
...this.state.location,
pathname: location.hash.slice(1),
state: this.locationState
}
})
})
}
render() {
return (
<RouterContext.Provider value={this.state} >
{this.props.children}
</RouterContext.Provider>
)
}
}
其他组件如 Switch
、 Route
等内部逻辑都比较好理解,感兴趣可以自己继续探究。
MobX
预备知识
为什么需要MobX
四张图解释了为什么需要Redux,MobX同理。
最后
现在,你已经大致了解现有技术栈,可以在脚手架生成的项目中仿造用户管理界面仿造写一个具有增删改查的功能的页面,比如系统中的产品管理。遇到问题先查阅文档或谷歌,还有不清楚及时问师兄。你的目标是熟悉一个简单页面的搭建,加油!