banner
MiniKano

MiniKano

This is MiniKano's blog based on Blockchain technology. 新手,请多关照~
github

React入門

注,此指南可搭配 bilibili 尚硅谷的 react 教程食用~
視頻傳送門

第一章:React 入門#

介紹:#

react 是一個用於構建用戶界面的 JavaScript 庫,由 FaceBook 開源供全球開發者使用

React 特點:

  1. 聲明式編碼
  2. 組件化編碼
  3. 移動端通過 ReactNative 編寫原生應用
  4. 高效 (Diff 算法 + 虛擬 DOM,最小化頁面重繪時間)
  5. etc.

下面是一個最基本的 React 頁面:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 準備好一個容器 -->
    <div id="app"></div>
    <!-- 核心庫 -->
    <script src="../react/react.development.js"></script>
    <!-- dom庫 -->
    <script src="../react/react-dom.development.js"></script>
    <!-- babel -->
    <script src="../react/babel.min.js"></script>

    <!-- code here -->
    <!-- babel -->
    <script type="text/babel">
      // jsx代碼
      //創建虛擬DOM
      const VDOM = (
        <h1>
          <span>hello,React</span>
        </h1>
      ); /*此處不用加引號 jsx特有語法*/
      //渲染虛擬DOM到頁面(已過時)
      ReactDOM.render(VDOM, app);
    </script>
  </body>
</html>

虛擬 DOM 和真實 DOM#

  • 關於虛擬 DOM
    • 1. 本質是 Object 類型的對象
    • 2. 虛擬 DOM 比較輕量化

JSX 語法規則#

在 jsx 文件中編寫 html 元素時,需要遵循以下幾點:

  1. 定義虛擬 DOM 時,無需添加引號。
  2. 標籤中混入 JS表達式時需要使用 { }
  3. 在 jsx 中 DOM 的 class 屬性需要使用 className 替代。
  4. 內聯樣式,要用 style-{{key}} 的形式去寫。
  5. 一個虛擬 DOM 只有一個根標籤
  6. 標籤必須閉合
    1. 若小寫字母開頭,則將該標籤轉為 html 中同名元素,如 html 中無該標籤對應的元素,則報錯
    2. 若大寫字母開頭,則被視為一個 React Component,如找不到則報錯
const myId = "kanoKano";
const myData = "AbCd";

//1.創建虛擬DOM
const VDOM = (
    <div>
        <h2 className="yellow" id={myId.toLowerCase()}>
            <span style={{ color: "red", fontSize: "30px" }}>
                {myData.toLowerCase()}
            </span>
        </h2>
        <h2 className="yellow" id={myId.toLowerCase()}>
            <span style={{ color: "red", fontSize: "30px" }}>
                {myData.toLowerCase()}
            </span>
        </h2>
        <Hello></Hello>
    </div>
);

//2.渲染DOM
ReactDOM.render(VDOM, document.querySelector("#app"));

遍歷數據#

在 React 中,如需渲染的數據是一個數組,這時候就可以使用自動遍歷:

注意:自動遍歷需要一個 key 作為唯一值

const data = [{ name: "Angular" }, { name: "React" }, { name: "Vue" }];
//for
// 自動遍歷需要一個key作為唯一值
const VDOM = (
    <div>
        <h1>前端js框架列表</h1>
        <ul>
            {data.map((item, index) => (
                <li key={index}>{item.name}</li>
            ))}
        </ul>
    </div>
);

//渲染
ReactDOM.render(VDOM, document.querySelector("#app"));

組件與模塊#

和 vue 類似,React 也可以實現組件化編程和模塊提取

函數式組件#

函數式組件適用於簡單組件的定義

// 1.創建函數式組件(函數首字母需要大寫)
function Demo() {
    return <h2>我是用函數定義的組件(適用於簡單組件的定義)</h2>;
}
//渲染組件到頁面
ReactDOM.render(<Demo />, document.querySelector("#app"));

這裡需要注意的是,渲染組件的時候,不能直接寫函數名,需要以組件標籤的形式<Demo /> 填寫
以上代碼經過 babel 編譯後,Demo 下的 this 指向的就是 undefined 了,因為 babel 自動開啟了嚴格模式

執行了 ReactDOM.render 之後,發生了以下事情:

  1. React 解析組件標籤,找到了對應的組件
  2. 發現組件是使用函數定義的,隨後調用該函數,將返回的虛擬 DOM 轉為真實 DOM,隨後呈現在頁面中

類式組件#

創建一個類式組件:

//創建類式組件
class MyComponent extends React.Component {
    render() {
        console.log("render中的this", this);
        return <h2>我是用類定義的組件【適用於複雜組件的定義】</h2>;        
    }
}
//2.渲染組件到頁面
ReactDOM.render(<MyComponent />, document.getElementById("app"));

執行了 ReactDOM.render () 之後,發生了什麼?

  • react 解析組件標籤,找到了 MyComponent 組件
  • 發現組件是使用類定義的,隨後 new 出來該類的實例,並通過該實例調用到原型上的方法
  • 將 render 返回的虛擬 DOM 轉換為真實 DOM,隨後呈現在頁面中。

關於 render 中的 this:

  • render 是放在 MyComponent 的原型對象上供實例使用
  • render 中的 this 是:MyComponent 組件的實例對象

組件實例對象裡面有三個比較常用的屬性:

context
props
refs
state

下面會一一介紹以上屬性

組件實例的三大核心屬性#

state#

state 是組件對象最終要的屬性,值是對象(可以包含多個 key-value 的組合)
組件被稱為 "狀態機",通過更新組件的 state 來更新對應的頁面顯示(重新渲染組件)

不過需要注意:

  • 組件中 render 方法中的 this 為組件實例對象
  • 組件自定義方法中 this 為 undefined 的解決方法:
    • 通過函數對象的 bind () 來指定 this 的指向
    • 使用箭頭函數忽略當前層次的 this
  • 狀態數據不能直接修改或更新

下面展示了一個簡單的 state 用法的例子:

// jsx代碼
class Weather extends React.Component {
    //傳遞props
    constructor(props) {
        super(props);
        //初始化狀態
        this.state = {
            isHot: false,
            wind: "大風",
        };
        //使用bind也可以更改this指向,直接掛載函數到實例上,但是這樣有點費內存(大量new對象的情況下)
        this.demo = this.demo.bind(this);
    }

    render() {
        return (
            <h1 onClick={this.demo}>
                今天天氣{this.state.isHot ? "很炎熱" : "不炎熱"}
            </h1>
        );
    }

    //使用箭頭函數,這樣this指向才正常,因為作為onclick的回調,加上嚴格模式,所以才會丟失this
    // demo = () => {
    //   console.log(this);
    //   // 這麼改沒有用
    //   this.state.isHot = true;
    //   console.log("我被點擊了");
    // };
    
    demo(){
        console.log(this);
        // 直接賦值是無效的,需要使用setState方法(和Flutter類似,setState掛載在React.Componentd的原型對象上
        // this.state.isHot = true;
        //使用setState更新DOM(對象作為參數,參數會合併到實例上的state上)
        this.setState({
            isHot:!this.state.isHot
        })
        console.log("我被點擊了");
    };
}

ReactDOM.render(<Weather />, document.querySelector("#app"));

以上簡單的實現了一個狀態切換的 demo,我們遇到了 this 指向的問題,this 指向可以使用 bind 方法更換指向,並在實例上掛載上修復過后的方法,但是這樣做的話會有一點問題:

  • 每次添加新方法的時候都需要在構造函數中 bind 一次,非常不方便
  • 使用 bind 也可以更改 this 指向,直接掛載函數到實例上,很消耗內存

所以我們綜合一下上面的錯誤,就有了 state 的標準寫法:

class Weather extends React.Component {
    constructor(props) {
        super(props);
    }

    state = {
        isHot: false,
        wind: "大風",
    };
    
    demo = () => {
        /*
        this.setState({
            isHot: !this.state.isHot,
        });
        */
        //箭頭函數寫法
        this.setState((oldState) => {
            return {
                isHot: !oldState.isHot,
            };
        });
    };

    render() {
        return (
            <h1 onClick={this.demo}>
                今天天氣{this.state.isHot ? "很炎熱" : "不炎熱"}
            </h1>
        );
    }
}

ReactDOM.render(<Weather />, document.querySelector("#app"));

props#

在 Vue 中,props 是定義在組件上的類似參數,用來傳值 / 父子組件通信的一系列屬性,在 react 中也是一樣的,不過稍有區別

props 是每個組件對象都會有的屬性,組件標籤的所有屬性都保存在 props 中

以下是 props 傳值的例子:

class Person extends React.Component {
    render() {
        return (
            <ul>
                <li>姓名:{this.props.name}</li>
                <li>性別:女</li>
                <li>年齡:{this.props.age}</li>
            </ul>
        );
    }
}
//渲染
ReactDOM.render(
    <Person name="kano" age="20" />,
    document.getElementById("app")
);

注意,在 react 中 props 是只讀的,這一點和 vue 是一樣的

此外,props 傳值的方式有很多種,你可以使用{value}的顯示作為 props 參數,也可以直接使用 babel+jsx 支持的對象擴展運算符拆開對象(淺拷貝)作為傳入的 props:

let kano = {
    name: "kanokano",
    age: 18,
};
//注意,這不是解構,也不是展開,這是對象擴展運算,只能在component標籤中使用
ReactDOM.render(<Person {...kano} />, document.getElementById("app"));

對 props 進行限制

有時候我們需要限制 props 的傳入類型,以保證傳入的結果的類型正確

此時我們可以使用 prop-types 包:

npm i prop-types --save

使用方法

class Person extends React.Component {
    render() {
        return (
            <ul>
                <li>姓名:{this.props.name}</li>
                <li>性別:{this.props.sex}</li>
                <li>年齡:{this.props.age}</li>
            </ul>
        );
    }
    //必須是靜態屬性
    static propTypes = {
        // 使用前記得導包
        name: PropTypes.string.isRequired,
        age: PropTypes.number.isRequired,
        speak: PropTypes.func, //函數類型的寫法
    };
    static defaultProps = {
        sex: "男",
    };
}
let kano = {
    name: "kanokano",
    age: 18,
    // sex: "女",
};
ReactDOM.render(<Person {...kano} />, document.getElementById("app"));

在函數式組件中使用 props

React 中的函數式組件其實有一個默認的參數 props,我們可以直接使用

//創建組件
function Person(props) {
    console.log(props);
    return (
        <ul>
            <li>姓名:{props.name}</li>
            <li>性別:{props.sex}</li>
            <li>年齡:{props.age}</li>
        </ul>
    );
}
//給屬性加上限制
Person.propTypes = {
    // 使用前記得導包
    name: PropTypes.string.isRequired,
    age: PropTypes.number.isRequired,
    speak: PropTypes.func, //函數類型的寫法
};
//不傳入參數的默認值
Person.defaultProps = {
    sex: "男",
};

let kano = {
    name: "kanokano",
    age: 18,
    // sex: "女",
};
ReactDOM.render(<Person {...kano} />, document.getElementById("app"));

refs#

React 的 refs 其實和 Vue 的 $refs 是一樣的,不過高版本的 React 好像優化了 refs 這一特性,轉換為和 vue3 差不多的 ref 寫法使用 ref 操作 DOM – React

讓我們一起看看常見的 ref 和 refs 的使用方式:

字符串形式的 ref (已棄用)#
class Demo extends React.Component {
    //展示左側輸入框的數據
    showData = () => {
        console.log(this.refs["input1"]);
        alert(this.refs["input1"].value);
    };
    //展示右側輸入框的數據
    showData1 = () => {
        alert(this.refs["input2"].value);
    };
    render() {
        // ref(之後的版本已經棄用)
        return (
            <div>
                <input ref="input1" type="text" placeholder="點擊按鈕提示數據" />
                <button onClick={this.showData}>點我提示左側的數據</button>
                <p>
                    <input
                        ref="input2"
                        onBlur={this.showData1}
                        type="text"
                        placeholder="失去焦點提示數據"
                        />
                </p>
            </div>
        );
    }
}
ReactDOM.render(<Demo />, document.querySelector("#app"));
內聯函數形式的 ref#
class Demo extends React.Component {
    //展示左側輸入框的數據(ref回調已經將節點掛載在實例上了,無需refs)
    showData = () => {
        console.log(this["input1"]);
        alert(this["input1"].value);
    };
    render() {
        return (
            <div>
                <input
                    ref={ el => this.input1 = el }
                    type="text"
                    placeholder="點擊按鈕提示數據"
                    />
            </div>
        );
    }
}

注意,ref 回調是以內聯函數的方式定義的話,在更新過程中這個函數會被執行兩次,第一次傳入的參數是 null,第二次才會傳入參數 DOM 元素,因為每次渲染的時候都會創建一個新的函數實例(畢竟是匿名函數),React 會清空舊的 ref 設置新的 ref
可以通過把回調放在 class 裡面就可以解決兩次觸發回調的問題
不過這個問題是無關緊要的,強迫症可以試試下面的方法

class Demo extends React.Component {
    state = { isHot: true };
    show = () => {
        const { input1 } = this;
        alert(input1.value);
    };
    changeWeather = () => {
        //更新的時候會觸發兩次ref回調
        // @ null
        // @ <input type=​"text" placeholder=​"輸入數據">​
        const { isHot } = this.state;
        this.setState({ isHot: !isHot });
    };
    saveDOM = (el) => {
        this.input1 = el;
        console.log("@", el);
    };
    render() {
        return (
            <div>
                <p>今天天氣很{this.state.isHot ? "炎熱" : "涼爽"}</p>
                {/*把回調放在class裡面就可以解決兩次觸發回調的問題*/}
                <input ref={this.saveDOM} type="text" placeholder="輸入數據" />
                <button onClick={this.show}>點我提示數據</button>
                <button onClick={this.changeWeather}>點我切換天氣</button>
            </div>
        );
    }
}
使用 createRef (推薦)#

除了前面的方法以外,我們還可以使用 createRef 來綁定和操作 DOM

調用 createRef 後可以返回一個容器,可以存儲被 ref 所標識的節點 (一個屬性只能綁定一個節點)

class Demo extends React.Component {
    // 調用createRef後可以返回一個容器,可以存儲被ref所標識的節點(一個屬性只能存一個節點)
    myRef = React.createRef();
    myRef1 = React.createRef();
    show = () => {
        const { myRef } = this;
        console.log(myRef.current);
        alert(myRef.current.value);
    };
    show1 = () => {
        const { myRef1 } = this;
        console.log(myRef1.current);
        alert(myRef.current.value);
    };
    render() {
        return (
            <div>
                <input ref={this.myRef} type="text" />
                <input ref={this.myRef1} onBlur={this.show1} type="text" />
                <button onClick={this.show}>點我提示數據</button>
            </div>
        );
    }
}

事件處理#

前面我們已經知道可以通過 onXxx 屬性指定事件處理函數 (注意命名規則)
(React 使用的是自定義(合成)事件,而不是使用的原生 DOM 事件(為了更好的兼容性))

React 中的事件綁定和 Vue 一樣,也會自動給事件處理函數傳入一個 event 參數

注意: React 中的事件是通過事件委託的方式處理的(委託給組件最外層的元素

class Demo extends React.Component {
    //自動傳入事件e
    show2 = (e) => {
        e.target.value = "blur了哦"
        console.log(e.target);
    };
    render() {
        return (
            <div>
                <input onBlur={this.show2} type="text" />
            </div>
        );
    }
}

受控組件與非受控組件#

受控組件與非受控組件的區別:

  • 受控組件會維護 state 用於保存 dom 的狀態和數據,使用的時候直接從 state 內取即可,非受控組件則無 state 保存狀態
  • 受控組件可以輕鬆實現類似 Vue 的雙向數據綁定,非受控組件則難以實現

非受控組件:

class Login extends React.Component {
    handleSubmit = (e) => {
        //這裡的e是React重寫的表單事件提交對象
        e.preventDefault()
        console.log(e.currentTarget);
        alert(`用戶名:${this.username.value}密碼:${this.password.value}`);
    };
    render() {
        return (
            <form action="https://kanokano.cn" onSubmit={this.handleSubmit}>
                用戶名:
                <input ref={(e) => (this.username = e)} type="text" />
                密碼:
                <input ref={(e) => (this.password = e)} type="password" />
                <input type="submit" value="提交" />
            </form>
        );
    }
}

受控組件:

class Login extends React.Component {
    state = {
        username: "",
        password: "",
    };
    saveUsername = (e) => {
        this.setState({
            username: e.target.value,
        });
    };
    savePassword = (e) => {
        this.setState({
            password: e.target.value,
        });
    };
    handleSubmit = (e) => {
        //這裡的e是React重寫的表單事件提交對象
        e.preventDefault();
        console.log(e.currentTarget);
        alert(`用戶名:${this.state.username}密碼:${this.state.password}`);
    };
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                用戶名:
                <input type="text" onChange={this.saveUsername} />
                密碼:
                <input type="password" onChange={this.savePassword} />
                <input type="submit" value="提交" />
            </form>
        );
    }
}

當然,上面的受控組件的例子還可以進一步優化(函數柯里化),以應對需要綁定大量節點的情況

...
//保存表單數據到狀態中
saveFromData = (prop) => {
    //函數柯里化
    return (e) => {
        console.log(e.target.value);
        this.setState({
            // 用中括號表示字符串屬性名
            [prop]: e.target.value,
        });
    };
};
render() {
    return (
        <form onSubmit={this.handleSubmit}>
            用戶名:
            <input type="text" onChange={this.saveFromData("username")} />
            密碼:
            <input type="password" onChange={this.saveFromData("password")} />
            <input type="submit" value="提交" />
        </form>
    );
}
...

組件的生命週期 (舊)#

和 vue 一樣,React 中組件也有生命週期的概念,其生命週期大概分下面三個階段

  1. 初始化階段:由ReactDOM.render()觸發 -- 初次渲染

    • constructor()
    • componentWillMount()
    • render()
    • componentDidMount()
  2. 更新階段:由組件內部this.setState()或父組件 render 觸發

    • shouldComponentUpdate()
    • componentWillUpdate()
    • render()
    • componentDidUpdate()
  3. 卸載組件:由ReactDOM.unmountComponentAtNode()觸發

    • componentWillUnmount()

image

代碼演示:

下面的componentWillReceiveProps需要特別注意:頁面打開時組件第一次接收到的 props 不會觸發此鉤子

//父組件
class B extends React.Component {
    state = { name: "kano" };
    change = () => {
        this.setState({
            name: "kanokano",
        });
    };
    render() {
        return (
            <div>
                <div>B</div>
                <button onClick={this.change}>換車</button>
                <Count Bname={this.state.name} />
            </div>
        );
    }
}

//創建組件(子組件
class Count extends React.Component {
    constructor(props) {
        console.log("Count-constructor");
        super(props);
        // 初始化狀態
        this.state = { count: 0 };
    }

    //按鈕回調
    add = () => {
        const { count } = this.state;
        this.setState({ count: count + 1 });
    };

    //卸載按鈕的回調
    unmount = () => {
        ReactDOM.unmountComponentAtNode(document.getElementById("app"));
    };

    //B將要接收到props(第一次接收到的props不會調用此鉤子)
    componentWillReceiveProps() {
        console.log("B--componentWillReceiveProps");
    }

    //強制更新按鈕的回調(不更改狀態也能更新組件)
    force = () => {
        //強制更新不管shouldComponentUpdate的返回值,都可以重新強制更新
        this.forceUpdate();
    };

    //組件將要掛載的鉤子
    componentWillMount() {
        console.log("Count-componentWillMount");
    }

    //掛載完畢
    componentDidMount() {
        console.log("Count-componentDidMount");
    }

    //組件將要卸載的鉤子
    componentWillUnmount() {
        console.log("Count-componentWillUnmount");
    }

    //組件是否需要更新,會返回true(執行更新)或者false(不執行更新)
    shouldComponentUpdate() {
        console.log("Count-shouldComponentUpdate");
        return true;
    }

    //組件將要更新的鉤子
    componentWillUpdate() {
        console.log("Count-componentWillUpdate");
    }

    //組件更新完畢的鉤子,接受兩個參數,一個是更新之前的props,一個是更新之前的state
    componentDidUpdate(prevProps, prevState) {
        console.log(prevProps, prevState);
    }

    render() {
        console.log("Count-render");
        const { count } = this.state;
        return (
            <div>
                <h2>當前求和為{count}</h2>
                <h2>父組件的值:{this.props.Bname}</h2>
                <button onClick={this.unmount}>BOOM</button>
                <button onClick={this.force}>
                    強制更新,不更改狀態中的任何數據
                </button>
                <button onClick={this.add}>點我+1S</button>
            </div>
        );
    }
}

//渲染組件
ReactDOM.render(<B />, document.getElementById("app"));

組件的生命週期 (新)#

  1. 初始化階段:由ReactDOM.render()觸發 -- 初次渲染
    • constructor()
    • getDerivedStateFromProps()
    • render()
    • componentDidMount()
  2. 更新階段:由組件內部this.setState()或父組件 render 觸發
    • getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()
  3. 卸載組件:由ReactDOM.unmountComponentAtNode()觸發
    • componentWillUnmount()

新版本的 React 組件中增加了getDerivedStateFromPropsgetSnapshotBeforeUpdate兩個新鉤子,
邊緣化了componentWillReceivePropscomponentWillMountcomponentWillUpdate 三個鉤子,原因是這三個鉤子意義不大,但卻經常被開發人員濫用,所以 React 開發團隊準備邊緣化並在未來棄用這三個鉤子。
如果實在是想再新版本使用這三個鉤子話,請在前面加上UNSAFE_前綴。

image

getDerivedStateFromProps#

這是新版本 React 增加的一個預處理鉤子,可以將組件傳入的 props 進行返回,用作為 state 屬性
這個鉤子還有一個 state 參數,表示當前實例組件中現有的 state

getDerivedStateFromProps 可以返回 null 或者一個對象

  • 當返回值為 null 的時候代表鉤子什麼也不做,繼續執行生命週期
  • 當返回值為一個對象的時候,返回的對象將作為之後的生命週期中的 state 屬性使用

下面的代碼可以將組件傳入的 props 用作為 state
表示 state 的值在任何時候都取決於 props (可以使用,但沒有必要,因為這個操作在構造器裡就可以實現)

static getDerivedStateFromProps(props) {
    return props;
}
...
ReactDOM.render(<B />, document.getElementById("app"));

下面的代碼可以控制 count 的值在 0-5 以內

static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps");
    if (state.count > 5 && state.count < 0) {
        return { count: 0 };
    }
    return null;
}

getSnapshotBeforeUpdate#

這個鉤子在組件更新(render)之前執行,通常我們可以在裡面做一些操作,比如獲取更新之前的元素的滾動位置等,且這個鉤子必須返回一個值(只要不是 undefined)

//在更新之前獲取之前的快照(需配合componentDidUpdate一起使用)
getSnapshotBeforeUpdate() {
    console.log("getSnapshotBeforeUpdate");
    return "snapshotkanokano";
}

//組件更新完畢的鉤子,接受兩個參數,一個是更新之前的props,一個是更新之前的state
//還有一個是snapshot的值
componentDidUpdate(prevProps, prevState,snapshotValue) {
    console.log(prevProps, prevState,snapshotValue);
}

下面的例子模擬了一個新聞列表,並且在列表的最上層不斷地更新元素
實現的功能:更新列表的同時不影響用戶的滾動和預覽體驗。
原理:滾動的高度隨著項目的個數增加而變大
公式:滾動的位置 = 現在的 list 高度減去之前的高度

class NewList extends React.Component {
    state = { newsArr: [] };

    componentDidMount() {
        setInterval(() => {
            //獲取原狀態
            const { newsArr } = this.state;
            //模擬一條news
            const news = "新聞" + (newsArr.length + 1);
            //update
            this.setState({ newsArr: [news, ...newsArr] });
        }, 500);
    }

    getSnapshotBeforeUpdate() {
        //拿一下內容區的高度
        return this.refs.list.scrollHeight;
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        //滾動的高度隨著項目個數增加而變高,這樣就可以讓滾動頁面“固定”起來了
        //滾動的位置=現在的list高度減去之前的高度
        this.refs.list.scrollTop += this.refs.list.scrollHeight - snapshot;
        console.log(snapshot);
    }

    render() {
        return (
            <div className="list" ref="list">
                {this.state.newsArr.map((item, index) => (
                    <div className="news" key={index}>
                        {item}
                    </div>
                ))}
            </div>
        );
    }
}

效果:

image

第二章 React 工程化開發#

React 腳手架#

腳手架顧名思義,就是用來幫助程序員快速創建一個基於 xx 庫的模板項目的工具

一個腳手架包含了:

  • 所有需要的配置(語法檢查、jsx 編譯、devServer 等)
  • 所需的依賴
  • 一個示例 DEMO

React 和 Vue 一樣,也有相關的腳手架工具,叫做 create-react-app

安裝方法:

npm i create-react-app -g && create-react-app myapp

以上的安裝方法不太推薦,這裡推薦使用 npx 快速創建 react 實例的方法:

npx create-react-app myapp

啟動項目:

npm start

初始化後的目錄樹如下:

  .gitignore   -- git的忽略文件
  package-lock.json -- 固定版本號後的npm包描述文件
  package.json -- npm包描述文件
  README.md

├─public --存放靜態資源的目錄
      favicon.ico --站點圖標
      index.html --首頁
      logo192.png
      logo512.png
      manifest.json --app的配置文件(通過瀏覽器添加到桌面上的圖標的信息 https://developers.google.com/web/fundamentals/web-app-manifest/)
      robots.txt --控制搜索引擎爬蟲規則

└─src
        App.css --App組件樣式
        App.js  --App組件的js文件
        App.test.js --單元測試用
        index.css --全局樣式
        index.js --入口文件
        logo.svg
        reportWebVitals.js --頁面性能測試用
        setupTests.js --應用整體測試用

以上是 React 默認生成的 Demo,但在初期學習中,為了簡化,我們使用的目錄結構通常是下面這樣的:

  .gitignore
  package-lock.json
  package.json
  README.md

├─public --存放靜態資源的目錄
      favicon.ico --站點圖標
      index.html --首頁

└─src
  App.jsx
  index.js

    └─components
        └─Hello
                index.jsx
                index.module.css

在 React 裡,組件通常是用 jsx 為擴展名命名,位於components/組件名/index.jsx
樣式文件和組件文件放在同一個目錄,但是以 module.css 為擴展名,這樣可以在 import 的時候模塊化 css (less),就可以變量名.css屬性的形式來使用 css (less) 類

下面展示了一個 Hello 組件的寫法:

import React, { Component } from "react";
//把css文件改成module.css文件就可以避免樣式衝突的問題
import hello from "./index.module.css";
export class Hello extends Component {
  render() {
    return (
      <div>
        <h4 className={hello.title}>Hello React</h4>
      </div>
    );
  }
}

App 組件的寫法

import { Hello } from "./components/Hello";
//App組件一般用函數式就可以了
function App() {
  return (
    <div>
      <p>我是App組件</p>
      <Hello />
    </div>
  );
}
export default App;

入口文件 (index.js) 的寫法

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

index.html 的寫法

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

注:%PUBLIC_URL%process.env中的屬性,react 腳手架可以讀到當前項目的 publicdir 路徑,之後由react-scripts讀取到並替換為 publicdir

組件化編碼流程#

一個完整的組件化編碼大致分為這幾點:

  1. 拆分組件:拆分界面,抽取組件
  2. 實現靜態組件:使用組件實現靜態頁面效果
  3. 實現動態組件
    1. 動態顯示初始化數據
      1. 數據類型
      2. 數據名稱
      3. 數據保存位置
    2. 交互 (從偵聽數據開始)

組件化編碼之 ToDoList 案例#

案例下載:https://kanokano.cn/wp-content/uploads/2023/05/ToDoList.zip

image

第三章 使用 React 發送 AJAX 請求#

React 和 Vue 一樣,不會內置發送 ajax 請求的代碼,需要自行構建或者使用第三方庫 (fetch 或者 axios)

使用 axios#

在 react 中使用 axios 和在 Vue 中沒有太大的區別,首先都需要安裝 axios:

npm i axios --save

示例代碼:

import React, { Component } from "react";
import axios from "axios";
class App extends Component {
  getStudentData = () => {
    axios.get("http://localhost:3000/v1/students").then(
      (response) => {
        console.log("data:", response.data);
      },
      (error) => {
        console.log("err:", error);
      }
    );
  };
  getCarData = () => {
    axios.get("http://localhost:3000/v2/cars").then(
      (response) => {
        console.log("data:", response.data);
      },
      (error) => {
        console.log("err:", error);
      }
    );
  };
  render() {
    return (
      <div>
        <button onClick={this.getStudentData}>點我獲取學生數據</button>
        <button onClick={this.getCarData}>點我獲取汽車數據</button>
      </div>
    );
  }
}
export default App;

配置代理服務器#

配置代理服務器的目的是為了解決瀏覽器的跨域請求問題

配置方法:** 直接在 src 目錄下新建setupProxy.js** 文件:

const { createProxyMiddleware } = require("http-proxy-middleware");
//創建代理中間件
module.exports = (app) => {
    //可以配置多個代理服務器
    app.use(
        createProxyMiddleware("/v1", {
            target: "http://localhost:5000",
            changeOrigin: true,
            pathRewrite: { "^/v1": "" },
        })
    );
    app.use(
        createProxyMiddleware("/v2", {
            target: "http://localhost:5001",
            changeOrigin: true,
            pathRewrite: { "^/v2": "" },
        })
    );
};

還有一個簡單的方法,適用於配置單個代理服務器:

在 package.json 中追加以下配置

"proxy":"http://xxxxx:xxx"

這個方法有點就是配置簡單,前端沒有的請求統統走 proxy
缺點就是不能配置多個代理,而且如果前端有的請求,不能手動指定請求到後端,不便於管理和控制

使用 Pubsub 優化現有代碼#

通過前面的實例我們可以發現,每當組件之間需要傳遞參數,特別是兄弟組件之間需要傳遞參數的時候,通常只能借助父組件作為傳值的中間節點,這樣其實不是很優雅,也會增加不必要的代碼量,所以我們可以利用設計模式中的發布訂閱模式來解決這個問題。

PubSub 這個插件剛好就是利用了發布訂閱模式,我們可以直接安裝使用:

npm i pubsub-js --save

使用 Pubsub#

訂閱與取消訂閱:

import PubSub from "pubsub-js";
...
componentDidMount() {
    this.token = PubSub.subscribe("onSearch", (msg, data) => {
        this.setState({
            list: data,
        });
    });
    this.token1 = PubSub.subscribe("toggleLoading", (msg, flag) => {
        this.setState({ loading: flag });
    });
}
//銷毀組件之前記得取消訂閱
componentWillUnmount(){
    PubSub.unsubscribe(this.token)
    PubSub.unsubscribe(this.token1)
}
...

發布:

import PubSub from "pubsub-js";
search = async () => {
    //連續解構
    const {
        current: { value },
    } = this.keyWord;
    //發送請求
    try {
        PubSub.publish("toggleLoading", true);
        const res = await axios.get(
            `https://api.github.com/search/users?q=${value}`
        );
        const list = res.data.items || [];
        //更新數據
        PubSub.publish("toggleLoading", false);
        PubSub.publish("onSearch", list);
    } catch (err) {
        console.log(err.message);
        PubSub.publish("toggleLoading", false);
    }
};

使用 fetch#

fetch 作為一個新 ajax 的解決方案,自然也有他自己的優點,比如原生支持 promise,符合關注分離的原則

直接上最簡單的案例:

try {
    const res = await fetch(`https://api.github.com/search/users?q=${value}`);
    if (res.status === 200) {
        const list = (await res.json()).items || [];
    }
} catch (err) {
    console.log(err.message);
}

可以看到,使用原生的 fetch 也可以和 axios 之類的二次封裝 xhr 一樣優雅,我的評價是建議多用,都 3202 年了,兼容性都不是問題(

第四章 React 路由#

由於現在前端開發的大多都是單頁面應用(SPA),自然就會用到路由。
路由分為 hash 實現和 history 實現,hash 的兼容性更高,但不優雅,history 更現代友好...
關於路由的概念這裡就不過多闡述了,直接上正題

使用 react-router5#

注意,這裡使用的是老版本的 react-router (v5)

安裝:

npm i react-router-dom@5

使用 router:

index.js

....
//引入router
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
//直接對整個App包裹BroserRouter或者HashRouter
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
....

App.jsx

import React, { Component } from "react";
import { Link, Route } from "react-router-dom";
import Home from "./components/Home";
import About from "./components/About";
class App extends Component {
    render() {
        return (
            ...
            {/* React中的切換鏈接方式 */}
            <Link to="/about" className="list-group-item active">
                About
            </Link>
            <Link to="/home" className="list-group-item ">
                Home
            </Link>
            <div>
                {/*當然,component內可以單獨引入一個組件,這裡為了方便,就用函數組件代替了*/}
                <Route path="/about" component={()=><h2>About</h2>} />
                <Route path="/home" component={()=><h2>Home</h2>} />
            </div>
            ...
        );
    }
}
export default App;

上面的 link 在切換的時候不會切換高亮狀態,所以我們可以換成 NavLink (別忘記 import):

{/* 使用NavLink 點擊誰就會加上activeClassName內指定的類名*/}
<NavLink activeClassName="active" to="/about" className="list-group-item">
    About
</NavLink>
<NavLink activeClassName="active" to="/home" className="list-group-item ">
    Home
</NavLink>

看完了上面的例子,可以分析出路由的基本使用大致可以包括這四步:

  1. 先進行界面分區佈局
  2. a 標籤改為 Link 標籤
  3. 在指定區域使用 Route 標籤,並進行路由匹配
  4. 在根組件外側包裹<BrowserRouter />或者<HashRouter />

當然,上面的例子並沒有進行組件細分,一般情況下,我們需要將靜態組件拆分到 src/components 文件夾下,路由組件放在 src/pages 文件夾下

注意,路由組件在渲染的時候,內部會得到幾個 props 傳遞的參數,分別如下:

{
    history: {
        action: "PUSH"
        block: ƒ block(prompt),
        createHref: ƒ createHref(location),
        go: ƒ go(n),
        goBack: ƒ goBack(),
        goForward: ƒ goForward(),
        length: 45
        listen: ƒ listen(listener), 
        location: {
            pathname: '/home', 
            search: '',
            hash: '', 
            state: undefined, 
            key: 'faavc8'
        }
        push: ƒ push(path, state)
        replace: ƒ replace(path, state)
    }
	location: {
        hash: ""
        key: "faavc8"
        pathname: "/home"
        search: ""
        state: undefined
    }
    match: {
        path: '/home', 
        url: '/home', 
        isExact: true, 
        params: {…}
    }
	staticContext: undefined
}

先別急,這裡面的參數含義將會在後面通過例子逐一分析。

上面我們使用了 NavLink 組件,我們會發現每次傳遞的參數個數還是比較多的,這該怎麼優化呢?

<NavLink activeClassName="active" to="/home" className="list-group-item ">
    Home
</NavLink>

答案是二次封裝它!

MyNavLink.jsx

import React, { Component } from "react";
import { NavLink } from "react-router-dom";
class MyNavLink extends Component {
  render() {
    //props的children可以拿到傳過來的標籤體內容
    const { to, children } = this.props;
    console.log(this.props);
    return (
      <NavLink
        activeClassName="active"
        to={to}
        // 也可以直接展開(children也可以寫在標籤屬性內)
        //{...this.props}
        className="list-group-item "
      >
        {children}
      </NavLink>
    );
  }
}
export default MyNavLink;

App.jsx

{/* 封裝NavLink */}
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>

上面使用到了一个之前沒有提到的 props 屬性:children

children 就是組件在標籤之間傳入的值,上面傳入的是字符 About 和 Home,所以 children 裡就是 About、Home
如果傳入的是組件,那麼 children 內就是一個組件。
總之就是標籤之間傳入的是什麼,children 內裝的就是什么

children 屬性可以寫在標籤體內作為屬性,也可以放在標籤之間

Switch 組件#

正常情況下,一個路徑對應一個組件。
有時候我們會寫很多路由組件,組件解析的時候會從上往下逐次解析,如果遇到同路徑路由會怎麼樣呢

下面的三個路由參雜了同路徑的路由,進入/home路徑時候,兩個組件都會被解析到:

<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Route path="/home" component={()=><p>FAKE</p>} />

這時候我們可以引入 Switch 組件。import {Switch} from 'react-router-dom'
加上了 Switch 組件之後,同路徑路由只會匹配從上往下最近的那個(Home),多餘的不會進行匹配:

<Switch>
    <Route path="/about" component={About} />
    <Route path="/home" component={Home} />
    <Route path="/home" component={()=><p>FAKE</p>} />
</Switch>

Switch 可以保證路由的組件單一性

路由的模糊匹配#

默認情況下路由默認會進行前綴模糊匹配:

{/* 封裝NavLink */}
<MyNavLink to="/about">About</MyNavLink>
{/*可以跳轉到home路由*/}
<MyNavLink to="/home/home1">Home</MyNavLink>
<Switch>
    <Route path="/about" component={About} />
    <Route path="/home" component={Home} />
</Switch>

開啟了嚴格模式之後的路由將會進行嚴格匹配:

{/* 封裝NavLink */}
<MyNavLink to="/about">About</MyNavLink>
{/*開啟了嚴格模式後無法跳轉到home路由了*/}
<MyNavLink to="/home/home1">Home</MyNavLink>
<Switch>
    {/*開啟了嚴格模式*/}
    <Route exact path="/about" component={About} />
    <Route exact path="/home" component={Home} />
</Switch>

注意:一般情況下不需要開啟路由的嚴格匹配,尤其是路徑後面還跟隨 query 參數的時候
而且嚴格匹配開啟了還會導致無法繼續匹配二級路由

Redirect 的使用#

一般情況下,如果用戶請求了不存在的路由,React 將什麼也不會匹配,顯示空白頁面,這樣並不優雅,這時可以使用 Redirect 組件

redirect 組件一般寫在所有路由註冊的最下方,當所有路由都無法匹配的時候,就會跳轉到 Redirect 指定的路由
具體寫法 (記得 import):

<Switch>
    <Route exact path="/about" component={About} />
    <Route exact path="/home" component={Home} />
    {/* 啥也沒有默認去home */}
    <Redirect to="/home" />
</Switch>

嵌套路由#

在 React 中,嵌套路由做法是比較簡單的,只需要區分好父子組件的關係,然後注意以下兩點:

  1. 註冊子路由時寫上父路由的 path 值
  2. 路由的匹配是按照註冊路由的順序進行的
  3. 以上的路由路徑寫法確實比較冗餘,所以在後面的 v6 版本之後進行了改進,詳細後面的章節會講到

話不多說,直接上案例:

父路由如下設置:

/src/pages/Home/index.jsx

import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import MyNavLink from "../../components/MyNavLink";
import Message from "./Message";
import News from "./News";
class Home extends Component {
  render() {
    console.log("Home渲染的props:", this.props);
    return (
      <div>
        <h3>我是Home內容</h3>
        <ul className="nav nav-tabs">
          <li>
            {/* 二級路由的寫法 */}
            <MyNavLink to="/home/news" className="list-group-item active">
              News
            </MyNavLink>
          </li>
          <li>
            <MyNavLink to="/home/message" className="list-group-item">
              Message
            </MyNavLink>
          </li>
        </ul>
        {/* 註冊路由 */}
        <Switch>
          <Route path="/home/news" component={News}></Route>
          <Route path="/home/message" component={Message}></Route>
        </Switch>
      </div>
    );
  }
}
export default Home;

子路由的設置 (這裡只展示 Message 組件,News 組件同理):

/src/pages/Home/Message/index.jsx

import React, { Component } from "react";
class Message extends Component {
  render() {
    return (
      <div>
        <ul>
          <li>
            <a href="/">message001</a>
          </li>
          <li>
            <a href="/">message002</a>
          </li>
          <li>
            <a href="/">message003</a>
          </li>
        </ul>
      </div>
    );
  }
}
export default Message;

向路由組件傳遞參數#

向路由組件傳遞 params 參數#

路由傳遞 params 參數其實很簡單,只需要在註冊路由時加上佔位符:xxx即可,下面是詳細步驟

  1. 首先在路由鏈接中攜帶參數 (/home/message/detail/${obj.id}/${obj.title})
  2. 註冊路由的時候需要聲明參數 (xxx/:id/:title)
  3. 在目標路由中需要接收參數 (props.match.params)

示例:

/src/pages/Home/Message/index.jsx

import React, { Component } from "react";
import Detail from "./Detail";
import { Link, Route } from "react-router-dom";
const data = [
  { id: "01", title: "消息1" },
  { id: "02", title: "消息2" },
];
class Message extends Component {
  render() {
    return (
      <div>
        <ul>
          {data.map((obj) => {
            return (
              <li key={obj.id}>
                {/* 像路由組件傳遞params參數 */}
                <Link to={`/home/message/detail/${obj.id}/${obj.title}`}>
                  {obj.title}
                </Link>
              </li>
            );
          })}
        </ul>
        <hr />
        {/* 聲明接受params參數 */}
        <Route path="/home/message/detail/:id/:title" component={Detail} />
      </div>
    );
  }
}
export default Message;

/src/pages/Home/Message/Detail/index.jsx

import React, { Component } from "react";
const data = [
  { id: "01", content: "hahaha" },
  { id: "02", content: "kanokano.cn" },
];
class Detail extends Component {
  render() {
    // 傳過來的params
    console.log(this.props.match.params);
    const { params } = this.props.match;
    const content = data.find((item) => {
      return item.id === params.id;
    });
    return (
      <ul>
        <li>ID:{params.id}</li>
        <li>title:{params.title}</li>
        <li>content:{content.content}</li>
      </ul>
    );
  }
}
export default Detail;

向路由組件傳遞 search 參數#

傳遞 seatch 也就是 query 參數,通常長這樣:

http://xxx.com?id=02&title=%E6%B6%88%E6%81%AF2

?後面的字符串就是 search(query)參數

在路由組件之間傳遞 search 參數很簡單,只需要如下操作:

  1. 在路由鏈接中攜帶 search 參數:to={/home/message/detail/?id=${obj.id}&title=${obj.title}}
  2. 在目標路由組件中使用props.location.search即可查看傳入的 search 字符串
  3. 在目標路由組件中使用 query-string 插件中的 parse()方法即可轉換urlencoded形式的 search 字符串為實體對象

注意,query-string 插件需要手動安裝:npm i query-string

具體代碼:

源路由組件:

//...
const data = [
  { id: "01", title: "消息1" },
  { id: "02", title: "消息2" },
];
//...
{/* 像路由組件傳遞search(query)參數 */}
<Link to={`/home/message/detail/?id=${obj.id}&title=${obj.title}`}>
    {obj.title}
</Link>
{/* 無需聲明search(query)參數 */}
<Route path="/home/message/detail" component={Detail} />
//...

目標路由組件:

//....
//npm i query-string
import qs from "query-string";
render() {
    // 傳過來的query
    console.log("query", this.props.location);
    const { search } = this.props.location;
    //querystring需要轉換成對象
    let out = qs.parse(search);
    console.log(out);
    const content = data.find((item) => {
      return item.id === out.id;
    });
    return (
      <ul>
        <li>ID:{out.id}</li>
        <li>title:{out.title}</li>
        <li>content:{content.content}</li>
      </ul>
    );
  }
//.....

向路由組件傳遞 state 參數#

這種方法不會因為地址欄的改動而變化,信息不容易被隨意篡改

源路由組件:

{/* 像路由組件傳遞state參數 */}
<Link
    to={{
        pathname: "/home/message/detail/",
            state: { id: obj.id, title: obj.title },
    }}
    >
    {obj.title}
</Link>
{/* 無需聲明state參數 */}
<Route path="/home/message/detail" component={Detail} />

使用useLocation接受 state 參數

import React from "react";
import { useLocation } from "react-router-dom";
export default function Index() {
    //接受state參數
    const a = useLocation();
    console.log(a.state);
    const { id, title } = a.state;
    return (
        <ul>
            <li>{id}</li>
            <li>{title}</li>
        </ul>
    );
}

useMatch()#

1. 作用:返回當前匹配信息,對標 5.x 中的路由組件的match屬性

示例代碼:

....
const match = useMatch('/home/:x/:y')
console.log(match.params);
/*
params: {x:'1',y:'3'}
pathname: "/home"
pathnameBase: "/home"
pattern: {path: '/home', caseSensitive: false, end: true}
*/
....

useInRouterContext()#

useInRouterContext可以判斷當前代碼環境是否出於路由上下文中 (被路由包裹)

console.log(useInRouterContext(), "useInRouterContext");

useNavigationType()#

可以判斷當前頁面路由是以什麼方式導航的(pop、push、replace)
备注:POP 是指瀏覽器中直接打開了這個路由組件(刷新頁面)

console.log("useNavigationType:", useNavigationType());

useOutlet()#

用來呈現當前組件中渲染的嵌套路由的信息
如果嵌套路由沒有掛載,則返回 null
如果嵌套路由已經掛載,則顯示嵌套路由對象

console.log(useOutlet(), "useOutlet");

useResolvedPath()#

作用:給定一個 URL 值,解析其中的 path、search、hash 值(貌似有點用)

console.log("useResolvedPath", useResolvedPath("/user?id=1&name=kano"));

結束語#

以上就是 React 的基本用法,當然還有很多高級用法和最佳實踐,建議在實際開發中多加練習,並參考官方文檔進行深入學習。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。