最近,公司招了幾個(gè)新同學(xué),沒有React基礎(chǔ),就整理了一份入門文檔,希望能幫助到他們。
前言
目標(biāo)讀者:剛接觸React技術(shù)棧的新同學(xué)。
本文目標(biāo):希望讀者能知道為什么要使用這些技術(shù)棧,我們要借助它解決什么問(wèn)題。api用法相關(guān)請(qǐng)查閱文檔。
腳手架
為什么需要腳手架
隨著產(chǎn)品線的發(fā)展,我們會(huì)不斷的有新項(xiàng)目需要生成。這時(shí)候我們就會(huì)有一些麻煩:
- 每一個(gè)新項(xiàng)目都需要重新配置各類文件;
- 不利于統(tǒng)一管理升級(jí)
腳手架做了什么
我們的腳手架借助了第三方命令行工具YEOMAN。我們借助它做的事情是:從目標(biāo)地址拉取預(yù)先準(zhǔn)備好的代碼模版(如webpack.config.js、src目錄、package.json...)到我們指定的目錄。在執(zhí)行它的命令后,會(huì)進(jìn)入它的生命周期,依次做的事情是
- 狀態(tài)初始化;
- 詢問(wèn)用戶具體配置,比如腳手架類型、項(xiàng)目名稱、作者等;
- 下載模版文件壓縮包并解壓到本地;
- 安裝項(xiàng)目依賴并啟動(dòng)
我們只是繼承它的一個(gè)基類,在它提供的生命周期里實(shí)現(xiàn)了這些事情。
安裝依賴
npm install -g yarn
yarn config set registry https://registry.dingxiang-inc.com
yarn global add yo generator-ctu
如果長(zhǎng)時(shí)間下載不了,換用淘寶源[https://registry.npm.taobao.org](https://registry.npm.taobao.org)試試。
腳手架初始化
mkdir test
cd test
yo ctu
如果一直卡在“正在下載項(xiàng)目模板”,可以直接到github直接下載解壓。這樣的話,你需要自己進(jìn)入到項(xiàng)目中安裝依賴、啟動(dòng)項(xiàng)目。
另:如果有興趣,可以看下腳手架代碼,看是在哪一步卡住,可以提交下代碼優(yōu)化下這個(gè)問(wèn)題。
了解項(xiàng)目結(jié)構(gòu)
如果初始化成功,你會(huì)看到如下項(xiàng)目結(jié)構(gòu)[圖片上傳失敗...(image-dac1c0-1592968805612)]
備注:
-
data文件夾在真實(shí)項(xiàng)目中已經(jīng)移入src內(nèi),放的是一些部署后可以被替換的資源,比如logo文件、配置文件 -
.yo-rc可以不用管,腳手架生成后的產(chǎn)物。
可以進(jìn)入readme.md查看src目錄下的文件概述。
React
預(yù)備知識(shí)
JSX語(yǔ)法
為什么需要它
假設(shè)我們現(xiàn)在要實(shí)現(xiàn)一個(gè)功能,可以點(diǎn)擊體驗(yàn)下
1. 輸入框?yàn)榭諘r(shí),tweet按鈕不可點(diǎn)
2. 輸入框下方顯示還可以輸入的字符數(shù)量
3. 點(diǎn)擊add photo按鈕,剩余字符數(shù)量及add photo按鈕狀態(tài)發(fā)生改變(假定圖片占用23個(gè)字符)

我們看一段jQuery和React代碼的對(duì)比
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按鈕已經(jīng)點(diǎn)擊,剩余輸入文本數(shù)量再減23
$("span").text(140 - 23 - $(this).val().length);
} else {
// 計(jì)算剩余文本數(shù)量
$("span").text(140 - $(this).val().length);
}
if ($(this).val().length > 0 || $(".js-add-photo-button").hasClass("is-on")) {
// 如果文本框里有內(nèi)容或者add phtot按鈕已經(jīng)點(diǎn)擊,tweet button設(shè)置為可點(diǎn)擊狀態(tài)
$(".js-tweet-button").prop("disabled", false);
} else {
// tweet button設(shè)置為不可點(diǎn)擊狀態(tài)
$(".js-tweet-button").prop("disabled", true);
}
});
// 給添加照片的按鈕綁定點(diǎn)擊事件監(jiān)聽
$(".js-add-photo-button").on("click", function() {
if ($(this).hasClass("is-on")) {
$(this).removeClass("is-on").text("Add Photo"); // 切換add photo按鈕顯示狀態(tài)
$("span").text(140 - $("textarea").val().length);
if ($("textarea").val().length === 0) {
// 切換tweet按鈕前需要先判斷textarea當(dāng)前狀態(tài)
$(".js-tweet-button").prop("disabled", true);
}
} else {
$(this).addClass("is-on").text("? Photo Added"); // 切換add photo按鈕顯示狀態(tài)
$("span").text(140 - 23 - $("textarea").val().length);
$(".js-tweet-button").prop("disabled", false);
}
});
你會(huì)發(fā)現(xiàn)
- 在
React中,state成為了事件和render()之間的過(guò)渡:每個(gè)事件不需要擔(dān)心哪一部分的DOM發(fā)生變化,他們只需要設(shè)置state就可以了。相應(yīng)的,當(dāng)你寫render()的時(shí)候,你也只需要擔(dān)心現(xiàn)在的state是什么。 -
jQuery沒有中間的過(guò)渡層state,我們需要花費(fèi)很大的精力來(lái)解決它們之間相互的聯(lián)系,bug就經(jīng)常會(huì)出現(xiàn)在這里。 -
React中把各個(gè)UI組件獨(dú)立出來(lái),有利于提高UI組件的復(fù)用率同時(shí)降低各個(gè)UI組件的耦合。 - 新手在直接操作
DOM時(shí)很難寫出高效而又優(yōu)雅的代碼,從而使得前端代碼變得越來(lái)越難以維護(hù)。
它是怎么運(yùn)作的
當(dāng)我們?cè)诖a里寫jsx這個(gè)語(yǔ)法時(shí),會(huì)被babel編譯成瀏覽器可執(zhí)行的代碼。
比如
// jsx語(yǔ)法
const element = <h1 id='h1' className='h1'><span>你好</span></h1>
// 將聲明的元素渲染到節(jié)點(diǎn)上
ReactDOM.render(element, document.getElementById('root'));
會(huì)被轉(zhuǎn)成
const element = React.createElement(
"h1",
{
id: "h1",
className: "h1"
}, // 節(jié)點(diǎn)/組件上的屬性
React.createElement("span", null, "你好") // 子元素
);
ReactDOM.render(element, document.getElementById('root'));
執(zhí)行React.createElement后,我們會(huì)得到一個(gè)用來(lái)描述這個(gè)節(jié)點(diǎn)的對(duì)象,比如元素是原生元素,或者是一個(gè)React組件,還是說(shuō)只是一個(gè)單純的文本,有沒有子節(jié)點(diǎn)等等。
ReactDOM.render就會(huì)根據(jù)這個(gè)描述,解析出一個(gè)節(jié)點(diǎn),如果有子節(jié)點(diǎn)就遞歸往下解析,最終解析出一棵DOM樹,渲染到root節(jié)點(diǎn)里。
當(dāng)我們通過(guò)調(diào)用this.stState改變state的時(shí)候,在this.stState這個(gè)函數(shù)內(nèi)部,最終會(huì)調(diào)用組件的render函數(shù),render函數(shù)會(huì)重新返回一個(gè)描述節(jié)點(diǎn)的對(duì)象。React會(huì)和之前的對(duì)象進(jìn)行比較,來(lái)決定哪些組件需要重新計(jì)算渲染,來(lái)進(jìn)行最細(xì)粒度的重繪。
前端路由
預(yù)備知識(shí)
url的#號(hào)
history對(duì)象
hashchange
popstate
什么是前端路由
客戶端瀏覽器可以不依賴服務(wù)端,根據(jù)不同的URL渲染不同的視圖界面。
為什么需要前端路由
Ajax出現(xiàn)之前,路由工作是由后端處理。在進(jìn)行頁(yè)面切換的時(shí)候,瀏覽器發(fā)送不同的url請(qǐng)求;服務(wù)器接收到瀏覽器的請(qǐng)求時(shí),通過(guò)解析不同的url去拼接需要的html或者模板,然后將結(jié)果返回給瀏覽器端進(jìn)行渲染。
服務(wù)端渲染的優(yōu)勢(shì):
- 安全性更高,更嚴(yán)格得控制頁(yè)面的展現(xiàn),如下單支付流程
- 有利于SEO
- 首屏渲染快
服務(wù)端渲染的優(yōu)勢(shì):
- 服務(wù)器的計(jì)算壓力,消耗服務(wù)器性能
- 不容易維護(hù),如果不使用node中間層,前后端分工不明確,前后端可能同時(shí)在一個(gè)項(xiàng)目中開發(fā)
- 每一次切換頁(yè)面都需要reload頁(yè)面,用戶體驗(yàn)較差
前端路由渲染的目標(biāo)
- 在頁(yè)面不刷新的前提下實(shí)現(xiàn)url變化
- 捕捉到url的變化,以便執(zhí)行頁(yè)面替換邏輯
它是怎么運(yùn)作的
hash(IE 8)
打開控制臺(tái),執(zhí)行下面代碼
window.addEventListener('hashchange', function() {
console.log('The hash has changed!')
}, false);
window.location.hash = 'testhash'
你會(huì)發(fā)現(xiàn)控制臺(tái)執(zhí)行了回調(diào)函數(shù),打印了 The hash has changed! ,在url上也能看到 #testhash ,在url上直接改變 # 后面的內(nèi)容,同樣會(huì)執(zhí)行回調(diào)函數(shù)。
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);
因?yàn)?code>pushState、replaceState不會(huì)觸發(fā)onpopstate事件事件,所以可以采用 aop 的方法進(jìn)行監(jiān)聽。現(xiàn)在,我們就可以通過(guò)調(diào)用 pushState 的方法來(lái)改變路徑,同時(shí)我們也能監(jiān)聽到。
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 等內(nèi)部邏輯都比較好理解,感興趣可以自己繼續(xù)探究。
MobX
預(yù)備知識(shí)
為什么需要MobX
四張圖解釋了為什么需要Redux,MobX同理。
最后
現(xiàn)在,你已經(jīng)大致了解現(xiàn)有技術(shù)棧,可以在腳手架生成的項(xiàng)目中仿造用戶管理界面仿造寫一個(gè)具有增刪改查的功能的頁(yè)面,比如系統(tǒng)中的產(chǎn)品管理。遇到問(wèn)題先查閱文檔或谷歌,還有不清楚及時(shí)問(wèn)師兄。你的目標(biāo)是熟悉一個(gè)簡(jiǎn)單頁(yè)面的搭建,加油!