七天接手react專案-起步
背景
假如七天後必須接手一個 react 專案(spug - 一個開源運維平臺),而筆者只會 vue,之前沒有接觸過 react,此刻能做的就是立刻展開一個“7天 react 掃盲活動”。
react 活動掃盲方針
- 以讀懂 spug 專案為目標
- 無需對每個知識點深究
- 功能優先能實現,程式碼質量無需太苛刻
專案準備
將 spug 克隆到本地:
exercise> git clone https://github.com/openspug/spug spug-dev-demo
Cloning into 'spug-dev-demo'...
fatal: unable to access 'https://github.com/openspug/spug/': OpenSSL SSL_read: Connection was reset, errno 10054
克隆失敗,HTTPS 模式換成 SSH 再次下載:
exercise> git clone git@github.com:openspug/spug.git spug-dev-demo
Cloning into 'spug-dev-demo'...
remote: Enumerating objects: 11675, done.
remote: Counting objects: 100% (4184/4184), done.
remote: Compressing objects: 100% (1161/1161), done.
remote: Total 11675 (delta 3157), reused 3939 (delta 2991), pack-reused 7491
Receiving objects: 100% (11675/11675), 5.09 MiB | 2.32 MiB/s, done.
Resolving deltas: 100% (8460/8460), done.
目錄結構如下:
exercise\spug-dev-demo> dir
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2022/3/12 9:52 .github
d----- 2022/3/12 9:52 docs
d----- 2022/3/12 9:52 spug_api
d----- 2022/3/12 9:52 spug_web
-a---- 2022/3/12 9:52 9 .gitignore
-a---- 2022/3/12 9:52 35184 LICENSE
-a---- 2022/3/12 9:52 3732 README.md
我們前端主要關注 spug_web
這個專案。首先安裝依賴包:
spug_web> cnpm i
/ [7/23] Installing @babel/plugin-transform-function-name@^7.8.3platform unsupported react-scripts@3.4.3 › babel-jest@24.9.0 › @jest/transform@24.9.0 › jest-haste-map@24.9.0 › fsevents@^1.2.7 Package require os(darwin) not compatible with your platform(win32)
- [7/23] Installing @babel/plugin-transform-classes@^7.16.7[fsevents@^1.2.7] optional install error: Package require os(darwin) not compatible with your platform(win32)
....
本地啟動專案:
spug_web> npm run start
> spug_web@3.0.0 start
> react-app-rewired start
注:由於沒有後端 api 的支援,所以不能登入進去。但至少可以從程式碼上分析這個前端專案。
hello-world
直接使用 script 的方式引入 react:
// 新建 hello-world.html
<body>
<div id="root">
<!-- 此元素的內容將替換為您的元件 -->
</div>
<!-- react 庫 -->
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<!-- 用於處理 Dom 的 react 包 -->
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- Babel 能夠轉換 JSX 語法 -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
ReactDOM.render(
// 注:無需新增字串
<h1>Hello, world!</h1>,
document.getElementById('root')
);
</script>
</body>
訪問頁面,瀏覽器顯示“Hello, world!”
這裡我們引入了三個庫,分別是 react 核心庫、處理 dom 的 react以及用於轉換 jsx。
Tip:
- 筆者在 vscode 中安裝 “open in browser” 外掛,直接右鍵選擇 “Open with Live Server” 即可。
- 此示例來自 react 官網 hello-world
- React 和 ReactDOM 的 cdn 來自 react 官網-CDN 連結
- unpkg 是一個快速的全球內容交付網路,適用於 npm 上的所有內容。 使用它可以快速輕鬆地從任何包中載入任何檔案
- react 和 vue 都是 javascript 庫,都能用於構建使用者介面
- React 用於構建使用者介面的 JavaScript 庫 —— 官網
- 漸進式 JavaScript 框架 —— Vue 官網
babel
Babel 是一個 JavaScript 編譯器 —— 官網
babel 之前叫 6to5。意把 es6 轉為 es5,後來目標變成支援 ECMAScript 所有語法,後來還支援將 JSX 轉成 js。2015年2月,改名為 Bable。
Tip:6to5 is now Babel —— not-born-to-die
使用 Babel 最容易上手的是直接在 html 頁面中通過 cdn 引入它。就像這樣:
<body>
<div id="output"></div>
<!-- Load Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Your custom script here -->
<script type="text/babel">
const getMessage = () => "Hello World";
document.getElementById("output").innerHTML = getMessage();
</script>
</body>
瀏覽器頁面顯示:“Hello World”。
當在瀏覽器中載入時,@babel/standalone 將自動編譯並執行所有型別為 text/babel 或 text/jsx 的指令碼標籤。
Tip:@babel/standalone 提供了一個獨立的 Babel 構建,用於瀏覽器和其他非 Node.js 環境
為什麼使用 JSX
我們建議在 React 中配合使用 JSX,JSX 可以很好地描述 UI 應該呈現出它應有互動的本質形式 —— react 官網-JSX 簡介
JSX 僅僅只是 React.createElement(component, props, ...children)
函式的語法糖 —— react 官網-深入 JSX
語法糖,通常用起來更方便,功能或許也更強大。比如類(Class)只是我們自定義類的一個語法糖。jsx 是 React.createElement
的語法糖,意義應該也差不多。
在 hello-world 中我們使用的是語法糖(jsx)。就像這樣:
// jsx
const reactElem = <h1>Hello, world!</h1>
ReactDOM.render(
reactElem,
document.getElementById('root')
);
若不使用語法糖(使用 React.createElement)。就像這樣:
const reactElem = React.createElement(
'h1',
{/* className: 'greeting' */ },
'Hello, world!'
);
ReactDOM.render(reactElem, ...)
對比發現,jsx 更簡潔。
React 元素
react 元素是建立起來開銷極小的普通物件,用於描述你在螢幕上想看到的內容。與瀏覽器的 dom 元素不同。
react 元素是構成 React 應用的最小磚塊 —— react 官網-元素渲染
注:不要混淆元素與元件,react 元件是由 react 元素構成的。
React.createElement 建立並返回指定型別的新的 react 元素。下面我們將 react 元素列印出來:
const reactElem = React.createElement(
'h1',
{},
'Hello, world!'
)
console.log('reactElem: ', reactElem)
react 元素:
reactElem: {$$typeof: Symbol(react.element), type: 'h1', key: null, ref: null, props: {…}, …}
$$typeof: Symbol(react.element)
key: null
props: {children: 'Hello, world!'}
ref: null
type: "h1"
_owner: null
_store: {validated: false}
_self: null
_source: null
[[Prototype]]: Object
Tip:使用 jsx,輸出也是一樣的:
const reactElem = <h1>Hello, world!</h1>
console.log('reactElem: ', reactElem)
react 元素只有極少的幾個屬性。而真實 dom 元素上的屬性卻要多得多。你可以通過瀏覽器執行以下程式碼,將滑鼠移到 root
和 reactElem
對比檢視屬性數量。
const reactElem = <h1>Hello, world!</h1>
let root = document.getElementById('root')
// 打個斷點
debugger
react 元素是不可變物件。一旦被建立,你就無法更改它的子元素或者屬性。
React 只更新它需要更新的部分
React DOM 會將元素和它的子元素與它們之前的狀態進行比較,並只會進行必要的更新來使 DOM 達到預期的狀態。
JSX 語法規則
JSX,是一個 JavaScript 的語法擴充套件。JSX 可能會使人聯想到模板語言,但它具有 JavaScript 的全部功能。
不要寫引號
JSX 標籤語法既不是字串也不是 HTML。
// 正確。頁面顯示“Hello, world!”
const reactElem = <h1>Hello, world!</h1>
// 錯誤。頁面顯示“<h1>Hello, world!</h1>”
const reactElem = '<h1>Hello, world!</h1>'
引入 js 使用 {}
let _class = 'greeting'
let cnt = 'Hello, world!'
const reactElem = (
<h1 className={_class}>
{cnt.toLocaleUpperCase()}
</h1>
)
瀏覽器顯示:”HELLO, WORLD!“。元素內容:
<h1 class="greeting">HELLO, WORLD!</h1>
樣式類名請使用 className
倘若是用 class。就像這樣:
const reactElem = (
<h1 class={_class}>
{cnt.toLocaleUpperCase()}
</h1>
)
元素內容正常:
<h1 class="greeting">HELLO, WORLD!</h1>
但瀏覽器控制檯報錯:Warning: Invalid DOM property 'class'. Did you mean 'className'?
內聯樣式請使用 style={{ color: 'pink' }}
倘若使用 style="color:pink"
:
const reactElem = (
<h1 className={_class} style="color:pink">
{cnt.toLocaleUpperCase()}
</h1>
)
控制檯報錯:
react-dom.development.js:2716 Uncaught Error: The 'style' prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.
react-dom.development.js:2716 未捕獲的錯誤:“樣式”道具需要從樣式屬性到值的對映,而不是字串。 例如,使用 JSX 時 style={{marginRight: spacing + 'em'}}。
改成 {{ color: 'pink' }}
則正常:
const reactElem = (
<h1 className={_class} style={{ color: 'pink' }}>
{cnt.toLocaleUpperCase()}
</h1>
)
只能有一個根標籤
const reactElem = (
<h1 className={_class} style={{ color: 'pink' }}>
{cnt.toLocaleUpperCase()}
</h1>
<p>apple</p>
)
vscode 紅波浪線提示:JSX expressions must have one parent element
(JSX 表示式必須有一個父元素)
包裹一個 div 即可。就像這樣:
const reactElem = (
<div>
<h1 className={_class} style={{ color: 'pink' }}>
{cnt.toLocaleUpperCase()}
</h1>
<p>apple</p>
</div>
)
不要忘記閉合標籤
<div>
<h1 className={_class} style={{ color: 'pink' }}>
{cnt.toLocaleUpperCase()}
</h1>
/* input 標籤未閉合 */
<input type="text">
</div>
vscode 紅波浪線提示:JSX element 'input' has no corresponding closing tag
(JSX 元素“輸入”沒有相應的結束標記)。
瀏覽器執行控制檯報錯:Uncaught SyntaxError: /Inline Babel script: Unterminated JSX contents(未終止的 JSX 內容)。
以下兩種閉合方式都可以:
<input type="text" />
<input type="text"></input>
自定義元件使用大寫字母開頭
const reactElem = (
<div>
<mybutton>18</mybutton>
</div>
)
頁面顯示:“18”。瀏覽器報錯:
react-dom.development.js:61 Warning: The tag <mybutton> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
react-dom.development.js:61 警告:標籤 <mybutton> 在此瀏覽器中無法識別。 如果您打算渲染一個 React 元件,請以大寫字母開頭。
以小寫字母開頭的元素代表一個 HTML 內建元件,比如 <div>
或者 <span>
會生成相應的字串 'div' 或者 'span' 傳遞給 React.createElement
(作為引數)。大寫字母開頭的元素則對應著在 JavaScript 引入或自定義的元件,如 <Foo />
會編譯為 React.createElement(Foo)
。
JSX 括號
官網示例中的 jsx 用括號包圍起來。就像這樣:
// 有括號
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
感覺就是在寫 html,而且還有縮排。
筆者嘗試將括號去除:
const reactElem =
<h1 className="greeting">
Hello, world!
</h1>
ReactDOM.render(
reactElem,
document.getElementById('root')
)
瀏覽器還是正常顯示:“Hello, world!”。
我們建議將內容包裹在括號中,雖然這樣做不是強制要求的,但是這可以避免遇到自動插入分號陷阱 —— 官網-JSX 簡介
JSX 小練習
JavaScript 表示式可以被包裹在 {}
中作為子元素。例如,以下表示式是等價的:
<MyComponent>foo</MyComponent>
<MyComponent>{'foo'}</MyComponent>
例如將下面 ul 列表改為動態:
<ul>
<li>finish doc</li>
<li>submit pr</li>
<li>nag dan to review</li>
</ul>
let todos = ['finish doc', 'submit pr', 'nag dan to review'];
const reactElem = (
<ul>
{
todos.map(item => {
return <li>{item}</li>
})
}
</ul>
)
瀏覽器控制檯告警:
Warning: Each child in a list should have a unique "key" prop.
警告:列表中的每個孩子都應該有一個唯一的“key”屬性。
新增 key
即可:
todos.map((item, index) => {
return <li key={index}>{item}</li>
})
Tip:相同功能,官網(JavaScript 表示式作為子元素)是這樣實現的:
function Item(props) {
return <li>{props.message}</li>;
}
function TodoList() {
const todos = ['finish doc', 'submit pr', 'nag dan to review'];
return (
<ul>
{todos.map((message) => <Item key={message} message={message} />)}
</ul>
);
}
const reactElem = TodoList()
元件
在 vue 中我們會這樣使用元件:
// 註冊元件
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
Tip:僅作示意,通常我們會使用 spa 單頁面應用開發。
於是可以將應用介面抽象成一棵元件樹:
對比 vue 專案和react 專案的入口檔案,其實都是將 App
元件掛載到 dom 元素上:
// vue專案/main.js
import App from './App.vue'
...
new Vue({
router,
store,
// 將 App 掛載到 #app
render: h => h(App)
}).$mount('#app')
// spug_web/src/index.js
import App from './App';
...
// 將 App 掛載到 #root
ReactDOM.render(
<Router history={history}>
<ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
<App/>
</ConfigProvider>
</Router>,
document.getElementById('root')
);
函式元件與 class 元件
定義元件最簡單的方式是使用函式
。請看示例:
<script type="text/babel">
// 函式元件
function MyComponent(props) {
return <h1>Hello, {props.name}</h1>;
}
ReactDOM.render(
<div>
<MyComponent name="peng" />
</div>,
document.getElementById('root')
);
</script>
頁面顯示“Hello, peng”。元件對應的 html 為:<h1>Hello, peng</h1>
。
我們還可以使用 es6 的 class
來定義元件。就像這樣:
// class 元件。
class MyComponent extends React.Component {
// 必須定義 render()。否則會報錯:
// MyComponent(...): No `render` method found on the returned component instance: you may have forgotten to define `render`.
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
// 省略。用法相同
Tip:
- 在
React.Component
的子類中,必須定義render()
函式 - 我們強烈建議你不要建立自己的元件基類。 在 React 元件中,程式碼重用的主要方式是組合而不是繼承。 —— 官網
- class 元件目前提供了更多的功能
元件中的 this
首先看函式元件中的 this:
// 函式元件
function MyComponent(props) {
+ console.log('this', this)
return <h1>Hello, {props.name}</h1>;
}
瀏覽器制臺輸出:this undefined
。說明沒有 this。
上文我們已經在 class 元件的 render()
中使用了 this,我們將其列印出來看一下:
// class 元件
class MyComponent extends React.Component {
render() {
+ console.log('this', this)
return <h1>Hello, {this.props.name}</h1>
}
}
this MyComponent {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternals: FiberNode, …}
context: {}
props: {name: 'peng'}
refs: {}
state: null
updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
_reactInternalInstance: {_processChildContext: ƒ}
_reactInternals: FiberNode {tag: 1, key: null, stateNode: MyComponent, elementType: ƒ, type: ƒ, …}
isMounted: (…)
replaceState: (…)
[[Prototype]]: Component
為什麼函式元件中的 this 是 undifined
將函式元件 MyComponent 放入 bable 的試一試中,會被翻譯成:
"use strict";
function MyComponent(props) {
console.log('this', this);
return /*#__PURE__*/React.createElement("h1", null, "Hello, ", props.name);
}
注:jsx 被 babel 識別了處理啊,因為翻譯成了 React.createElement
。
嚴格模式下,如果沒有指定 this 的話,它值是 undefined
。
我們將 class 元件 MyComponent 也翻譯一下:
"use strict";
class MyComponent extends React.Component {
render() {
console.log('this', this);
return /*#__PURE__*/React.createElement("h1", null, "Hello, ", this.props.name);
}
}
props
我們首先回憶一下 vue 中的 props:
- props 用於接收來自父元件的資料。就像這樣:
<div id='app'>
<button-counter :msg='message'></button-counter>
</div>
<script>
Vue.component('button-counter', {
props: ['msg'],
template: `<div>
來自父元件的資訊: {{msg}}
</div>`
})
var app = new Vue({
el: '#app',
data: {
message: 'hello'
}
})
</script>
- 可以使用.sync 修飾符來實現一個 prop 進行“雙向繫結”。即子元件不要直接更改父元件的這個屬性,而應該通知父元件,讓父元件自己去更改這個屬性。
- prop 驗證。就像這樣:
Vue.component('my-component', {
props: {
// 基礎的型別檢查 (`null` 和 `undefined` 會通過任何型別驗證)
propA: Number,
// 必填的字串
propC: {
type: String,
required: true
},
// 帶有預設值的數字
propD: {
type: Number,
default: 100
},
// 帶有預設值的物件
propE: {
type: Object,
// 物件或陣列預設值必須從一個工廠函式獲取
default: function () {
return { message: 'hello' }
}
},
...
}
})
在 react 中,props 的作用也類似,即用於接收父元件傳來的屬性。
props 基本用法
給元件 MyComponent 定義了兩個 prop(屬性):
name
,字串型別,預設值是 defaultNamesay
,函式型別,必填
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- 使用 PropTypes 進行型別檢查 -->
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
<script type="text/babel">
class MyComponent extends React.Component {
render() {
const { name, say } = this.props
return <h1>Hello, {this.props.name}, {say()}</h1>
}
}
// 屬性型別
MyComponent.propTypes = {
name: PropTypes.string,
// 注:函式不是 function,而是 func
// 函式型別,並且必填
say: PropTypes.func.isRequired
}
// 預設值
MyComponent.defaultProps = {
name: 'defaultName'
}
const props2 = { say: () => 'I know you' }
ReactDOM.render(
<div>
<MyComponent name="peng" say={() => 'I love you'} />
<MyComponent {...props2} />
</div>,
document.getElementById('root')
);
</script>
</body>
頁面顯示:
Hello, peng, I love you
Hello, defaultName, I know you
自 React v15.5 起,React.PropTypes 已移入另一個包中。請使用 prop-types
庫 代替 —— 官網
Tip:有關型別檢測更多介紹請看 使用 PropTypes 進行型別檢查
Props 的只讀性
元件無論是使用函式宣告還是通過 class 宣告,都決不能修改自身的 props。倘若我們嘗試修改 props 屬性,就像這樣:
class MyComponent extends React.Component {
render() {
+ this.props.name = 'aName'
...
}
}
瀏覽器控制檯將報錯如下:
Inline Babel script:10 Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
內聯 Babel 指令碼:10 未捕獲的型別錯誤:無法分配給物件 '#<Object>' 的只讀屬性 'name'
使用 static 優化型別檢測
上文中,型別檢測的相關程式碼,從語法上來說就是給 MyComponent 類增加兩個靜態屬性:
// 型別檢測相關程式碼
MyComponent.propTypes = {
name: PropTypes.string,
say: PropTypes.func.isRequired
}
MyComponent.defaultProps = {
name: 'defaultName'
}
我們可以使用 static 語法來對其優化。就像這樣:
// class 元件
class MyComponent extends React.Component {
// 屬性型別
static propTypes = {
name: PropTypes.string,
say: PropTypes.func.isRequired
}
// 預設值
static defaultProps = {
name: 'defaultName'
}
render() {
...
}
}
函式元件中的 props
上文我們已經使用過了:
function MyComponent(props) {
return <h1>Hello, {props.name}</h1>;
}
雖然函式元件中沒有 this,不能像 class 元件那樣通過 this 去使用 props(this.props.name
),但函式元件可以通過引數接收 props。
super(props)
在 React 元件掛載之前,會呼叫它的建構函式。在為 React.Component 子類實現建構函式時,應在其他語句之前呼叫 super(props)。否則,this.props 在建構函式中可能會出現未定義的 bug —— 官網
這句話什麼意思?請看示例:
class MyComponent extends React.Component {
constructor(props) {
super(props)
// 輸出 {}。表明 this.props 有值
console.log(this.props.name)
}
render() {
...
}
}
將 super(props)
改為 super()
:
class MyComponent extends React.Component {
constructor(props) {
super()
// 輸出 undefined
console.log(this.props)
}
render() {
...
}
}