寫給新同學(xué)的基礎(chǔ)入門文檔

最近,公司招了幾個(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ì)有一些麻煩:

  1. 每一個(gè)新項(xiàng)目都需要重新配置各類文件;
  2. 不利于統(tǒng)一管理升級(jí)

腳手架做了什么

我們的腳手架借助了第三方命令行工具YEOMAN。我們借助它做的事情是:從目標(biāo)地址拉取預(yù)先準(zhǔn)備好的代碼模版(如webpack.config.js、src目錄package.json...)到我們指定的目錄。在執(zhí)行它的命令后,會(huì)進(jìn)入它的生命周期,依次做的事情是

  1. 狀態(tài)初始化;
  2. 詢問(wèn)用戶具體配置,比如腳手架類型、項(xiàng)目名稱、作者等;
  3. 下載模版文件壓縮包并解壓到本地;
  4. 安裝項(xiàng)目依賴并啟動(dòng)

我們只是繼承它的一個(gè)基類,在它提供的生命周期里實(shí)現(xiàn)了這些事情。

腳手架項(xiàng)目地址
YEOMAN

安裝依賴

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)]
備注:

  1. data文件夾在真實(shí)項(xiàng)目中已經(jīng)移入src內(nèi),放的是一些部署后可以被替換的資源,比如logo文件、配置文件
  2. .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è)字符)
a548382d-c91e-4a2c-858b-af28ff9b86b0.gif
a548382d-c91e-4a2c-858b-af28ff9b86b0.gif

我們看一段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)
  1. React中,state成為了事件和render()之間的過(guò)渡:每個(gè)事件不需要擔(dān)心哪一部分的DOM發(fā)生變化,他們只需要設(shè)置state就可以了。相應(yīng)的,當(dāng)你寫render()的時(shí)候,你也只需要擔(dān)心現(xiàn)在的state是什么。
  2. jQuery沒有中間的過(guò)渡層state,我們需要花費(fèi)很大的精力來(lái)解決它們之間相互的聯(lián)系,bug就經(jīng)常會(huì)出現(xiàn)在這里。
  3. React中把各個(gè)UI組件獨(dú)立出來(lái),有利于提高UI組件的復(fù)用率同時(shí)降低各個(gè)UI組件的耦合。
  4. 新手在直接操作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ì):

  1. 安全性更高,更嚴(yán)格得控制頁(yè)面的展現(xiàn),如下單支付流程
  2. 有利于SEO
  3. 首屏渲染快

服務(wù)端渲染的優(yōu)勢(shì):

  1. 服務(wù)器的計(jì)算壓力,消耗服務(wù)器性能
  2. 不容易維護(hù),如果不使用node中間層,前后端分工不明確,前后端可能同時(shí)在一個(gè)項(xiàng)目中開發(fā)
  3. 每一次切換頁(yè)面都需要reload頁(yè)面,用戶體驗(yàn)較差

前端路由渲染的目標(biāo)

  1. 在頁(yè)面不刷新的前提下實(shí)現(xiàn)url變化
  2. 捕捉到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文檔,文檔中推薦的入門文章,這里是譯文

為什么需要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è)面的搭建,加油!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容