初始化React腳手架
前期準備
-
1.腳手架: 用來幫助程序員快速創建一個基于xxx庫的模板項目
- 1.包含了所有需要的配置(語法檢查、jsx編譯、devServer…)
- 2.下載好了所有相關的依賴
- 3.可以直接運行一個簡單效果
-
2.react提供了一個用于創建react項目的腳手架庫: create-react-app
-
3.項目的整體技術架構為: react + webpack + es6 + eslint
-
4.使用腳手架開發的項目的特點: 模塊化, 組件化, 工程化
全局安裝工具
首先要在全局安裝create-react-app
,推薦使用yarn來安裝,因為react和yarn都是facebook出的。但是大部分人用的npm,也是完全可以,這里用npm演示
全局安裝react腳手架工具:
npm i -g create-react-app
i代表install
,g代表global全局
,create-react-app代表react腳手架
安裝后查看版本:
D:\WebProject\ReactProject>npm create-react-app -v
6.14.16
我們依靠這個就可以在想要的目錄下面創建react工程
安裝腳手架
切換到想要創建腳手架的目錄
使用命令:create-react-app 工程名字
名字僅限英文,不要用特殊字符
安裝完畢之后,可以看到React提示的四個命令:
npm start 啟動項目Starts the development server.npm run build 打包Bundles the app into static files for production.npm test 測試模式啟動Starts the test runner.npm run eject 將隱藏的webpack配置文件顯示出來Removes this tool and copies build dependencies, configuration filesand scripts into the app directory. If you do this, you can’t go back!
我們可以查看配置文件,package.json,所有啟動短命令已經配置好了
輸入npm start
,啟動項目
腳手架結構
這種項目也叫SPA項目:
- S–single
- P–page
- A–app
public ---- **靜態資源文件夾**favicon.icon ------ 網站頁簽圖標index.html -------- **主頁面,整個項目只有這一個html文件,作為基點**logo192.png ------- logo圖logo512.png ------- logo圖manifest.json ----- 應用加殼的配置文件robots.txt -------- 爬蟲協議文件
src ---- **源碼文件夾**App.css -------- App組件的樣式App.js --------- App組件App.test.js ---- 用于給App做測試index.css ------ 樣式index.js ------- 入口文件logo.svg ------- logo圖reportWebVitals.js--- 頁面性能分析文件(需要web-vitals庫的支持)setupTests.js---- 組件單元測試的文件(需要jest-dom庫的支持)
啟動項目后默認頁面
詳解index.html
作為整個項目里唯一一個html文件,也是整個app的基點,有必要詳細解讀一下
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><!-- %PUBLIC_URL%代表public文件夾的路徑 --><link rel="icon" href="%PUBLIC_URL%/favicon.ico" /><!-- 開啟理想視口,移動端適配 --><meta name="viewport" content="width=device-width, initial-scale=1" /><!-- 配置瀏覽器頁簽顏色(僅支持安卓原生瀏覽器) --><meta name="theme-color" content="#000000" /><!-- 描述網站信息的,搜索引擎會讀取這個 --><metaname="description"content="Web site created using create-react-app"/><!-- 蘋果手機的Safari把網頁添加到主屏幕后,啟動圖標 --><link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /><!-- 將web套殼成為安卓或ios的安裝包用,(應用加殼技術) --><link rel="manifest" href="%PUBLIC_URL%/manifest.json" /><!-- 網頁tab的名字 --><title>React App</title></head><body><!-- 如果客戶端瀏覽器不支持JS的運行 --><noscript>You need to enable JavaScript to run this app.</noscript><!-- 根標簽 --><div id="root"></div></body>
</html>
src目錄解析
App組件相關
webpack入口相關:index.js
如果是用webpack打包,那么默認情況下,index.js就是入口文件
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<React.StrictMode><App /></React.StrictMode>
);**上下這兩種寫法是完全等價的**ReactDOM.render(<App />,document.getElementById('root'))
<React.StrictMode>
這個<React.StrictMode>并不是ES5的嚴格模式,而是React檢查子組件是否有不合理的地方
比如:ref=字符串,或者一些過時的寫法,來做一些檢查<React.StrictMode><App /></React.StrictMode>
reportWebVitals()
作為入口的最底下的方法,是用來記錄網頁的性能檢測
至于public里的index.html是如何被src的index.js找到的,這個我們不需要關心,是React用webpack配置好的。所以index.html的名字是不可以隨便動的,動了就找不到了。
我們寫一個小應用,只需要去關注,index.html 主頁面
,App.js App組件
,index.js 入口文件
,這三個就夠了。
注意:工程里import 進js和jsx都是不需要寫后綴的
寫一個小demo
1.我們保持public中的Index.html不變
2.修改src下面的APP.js以及index.js文件
App.js: 【注意:創建好的組件一定要暴露出去】
//創建外殼組件APP
import React from 'react'class App extends React.Component{render(){return (<div>Hello world</div>)}
}export default App
index.js: 【主要的作用其實就是將App這個組件渲染到頁面上】
//引入核心庫
import React from 'react'
import ReactDOM from 'react-dom'
//引入組件
import App from './App'ReactDOM.render(<App />,document.getElementById("root"))
這樣在重新啟動應用,就成功了。
我們也不建議這樣直接將內容放入App組件中,盡量還是用內部組件。
我們在頂一個Hello組件:
import React,{Componet} from 'react'export default class Hello extends Componet{render() {return (<h1>Hello</h1>)}
}
在App組件中,進行使用
class App extends Component{render(){return (<div><Hello /></div>)}
}
這樣的結果和前面是一樣的。
但是由于普通的Js和組件都是js,所一最好組件使用jsx去展示。
樣式模塊化
前面會有一個問題,當css和組件越來越多的時候,如果css不分層不分級,就會造成混亂的場景,同時帶來樣式沖突難以管理。
當組件逐漸增多起來的時候,我們發現,組件的樣式也是越來越豐富,這樣就很有可能產生兩個組件中樣式名稱有可能會沖突,這樣會根據引入App這個組件的先后順序,后面的會覆蓋前面的。
同名造成的沖突
傳統的引入方式:
修改后的引入方式:
為了避免這樣的樣式沖突,我們采用下面的形式:
1.將css文件名修改: hello.css — >hello.module.css
css內部不用做出修改,也就是以對象形式進行css導入
注意:不要用{}套css對象,這玩意只是個標記,不是解構出來的!!!
會如果套了會導致對象獲取不到!!!
2.引入并使用的時候改變方式:
import React from "react";
//引入css,此時css都以對象形式存在hello中
import hello from "./hello.module.css"export default class Hello extends React.Component{render(){return(// 以對象形式獲取css<div className={hello.title}>Hello world</div>)}
}
逆天導入方法
有些項目為了避免大量導入import時導致后面有一串路徑,就采用這種簡寫方式,即每個文件夾下組件名和css名字都是 index的名字,這種情況下,導入的路徑寫到包就可以了,因為如果不寫最后的文件名,默認就導入index
就會從這種具體到文件的導入
import Hello from './component/Hello/hello.jsx'
import Welcome from './component/Welcome/welcome.jsx'
變成這種,導入截止到包名,后綴默認用index
import Hello from './component/Hello'
import Welcome from './component/Welcome'
這種導入方法肯定是不推薦的,但是有的公司可能就會用到這種,所以要提一下
React插件推薦
可以快速幫助你創建模板
比如輸入rcc,自動根據文件名創建類組件
rcc:react class component
輸入rfc,自動根據文件名創建函數式組件
rfc:react function component
回車就自動生成類組件了,函數式組件也是一樣
等等一些其他的快捷鍵可以自己去查,非常方便
組件化編碼的流程
- 拆分組件: 拆分界面,抽取組件 (頁面拆組件的過程中如果拆完了不知道叫啥名字,多半是沒拆好)
- 實現靜態組件: 使用組件實現靜態頁面效果
- 實現動態組件
3.1 動態顯示初始化數據(數據驅動頁面變化)
3.1.1 數據類型
3.1.2 數據名稱
3.1.2 保存在哪個組件?
3.2 交互(從綁定事件監聽開始)
腳手架插件
nanoId
相比于uuid更加輕量化,生成一個唯一id
UUID:生成唯一時間戳的庫
npm i uuid
(庫比較大)
npm i nanoid
(庫小,很快安裝)/ yarn add nanoid
用法:
import {nanoid} from 'nanoid'
//調用,直接就可以生成一個隨機ID
nanoid()
PropTypes
腳手架本身并不自帶這個,需要自行安裝,完成類型的約束
用npm進行安裝,安裝后即可進行操作,對傳入參數進行類型約束
npm install prop-types
static propTypes = {addTodo: proptypes.func.isRequired,
};
TodoList組件Demo總結
拆分組件、實現靜態組件,注意:className、style的寫法
動態初始化列表,如何確定將數據放在哪個組件的state中?
- 1)某個組件使用:放在其自身的state中
- 2)某些組件使用:放在他們共同的父組件state中(官方稱此操作為:狀態提升)
關于父子之間通信:
- 1)【父組件】給【子組件】傳遞數據:通過props傳遞
- 2)【子組件】給【父組件】傳遞數據:通過props傳遞,要求父提前給子傳遞一個函數
注意defaultChecked 和 checked的區別,類似的還有:defaultValue 和 value
狀態在哪里,操作狀態的方法就在哪里
兄弟之間的組件通信,用Pub-Sub訂閱機制實現,后續會有
腳手架集成Axios
Ajax和Axios是啥就不多說了
React本身并不帶這些插件,所以需要額外去引入這些插件
前期安裝插件
Axios官網
npm安裝Axios npm install axios
npm查看版本 npm list axios
查看package里的依賴
JSX內導入
安裝完成后就可以使用這種方式在JSX內導入
import axios from "axios";
方法里面發起Get請求,其他的用法可以看官網:Axios請求官網例子
startAxios = () => {// 這里會先看自身有沒有這個路徑下的資源,沒有的話才會去轉發給代理服務器axios.get("http://localhost:3000/api/test").then((response) => {console.log("成功了", response.data);},(error) => {console.log("失敗了", error);});};
請求發起后發生跨域問題
這個時候就需要代理來解決跨域問題,跨域為什么要用代理是什么請自行搜索
請求實際上是已經到了server,但是Ajax引擎不許跨域的值返回,所以跨域給攔下來的本質是Ajax引擎把響應給攔住了
代理服務器與server是同一個域名+端口上的
只完成一個轉發功能,代理服務器把請求轉到Server上,Server的內容還給代理,代理上因為沒有Ajax引擎,就把返回來的數據還給Client,由于代理與Client同源,沒有跨域限制,也就解決了跨域問題
代理配置
package.json配置
可以叫做全局代理,因為它直接將代理配置在了配置文件 package.json
中
"proxy":"http://localhost:5000"
// "proxy":"請求的地址"
追加的配置文件
這樣配置代理時,首先會在在原請求地址http://localhost:3000/api/test
上訪問,如果訪問不到資源,就會轉發到這里配置的地址上去請求,也就是http://localhost:5000/api/test
,如果這時候還沒有,就404
說明:
- 優點:配置簡單,前端請求資源時可以不加任何前綴。
- 缺點:不能配置多個代理。
- 工作方式:上述方式配置代理,當請求了3000不存在的資源時,那么該請求會轉發給5000 (優先匹配前端資源)
修改了配置類的操作一定要重啟!!!
setupProxy.js配置
這里不用記,會查會配即可,但是要注意 http-proxy-middleware 與腳手架的版本兼容
一定在src目錄下!!!
-
第一步:創建代理配置文件
在src下創建配置文件:src/setupProxy.js
-
編寫setupProxy.js配置具體代理規則:
const proxy = require('http-proxy-middleware')module.exports = function(app) {app.use(proxy('/api1', { //api1是需要轉發的請求(所有帶有/api1前綴的請求都會轉發給5000)target: 'http://localhost:5000', //配置轉發目標地址(能返回數據的服務器地址)changeOrigin: true, //控制服務器接收到的請求頭中host字段的值/*changeOrigin設置為true時,服務器收到的請求頭中的host為:localhost:5000changeOrigin設置為false時,服務器收到的請求頭中的host為:localhost:3000changeOrigin默認值為false,但我們一般將changeOrigin值設為true*/pathRewrite: {'^/api1': ''} //去除請求前綴,保證交給后臺服務器的是正常請求地址(必須配置)}),proxy('/api2', { target: 'http://localhost:5001',changeOrigin: true,pathRewrite: {'^/api2': ''}})) }
兩個關鍵配置屬性詳解:
changeOrigin:
控制服務器收到的請求頭中Host的值(本次請求從哪來的)
true:Origin是走代理的,false:Origin不走代理,是走代理前的host 。
一般都是true,避免后端做奇奇怪怪的check
changeOrigin設置為true時,服務器收到的請求頭中的host為:localhost:5000
changeOrigin設置為false時,服務器收到的請求頭中的host為:localhost:3000
changeOrigin默認值為false,但我們一般將changeOrigin值設為true
pathRewrite:
重寫請求路徑(必須) { "要被替換的路徑": "替換成啥" },這里的規則是去掉了 /api1
相當于去掉了 /api1 對于后端來說不會有api1的前綴,這只是前端區分請求走哪個代理的標識,真正發起請求給后端要把這種標識干掉當然,這種做法也不絕對,也可以把重寫的路徑換成需要的東西,靈活配置即可
說明:
- 優點:可以配置多個代理,可以靈活的控制請求是否走代理。
- 缺點:配置繁瑣,前端請求資源時必須加前綴。
setupProxy.js配置過程中遇到的坑
參考文檔
因為const proxy = require("http-proxy-middleware");
這個中間件的版本不兼容,導致無法啟動項目,具體現象是:啟動后臺無報錯,但是頁面無內容
要把第一句的中間件獲取換成這個,項目就可以正常啟動了
const {createProxyMiddleware: proxy} =require('http-proxy-middleware');
例子
分別根據 /api1
和 /api2
這兩個前綴,去到不同的server上面
代碼如下
可以看到都是訪問自己的前端端口,如果當前資源沒找到,就會去找上面配置的代理路徑(如果配了多個url前綴
,就會根據url前綴
去不同server)
class App extends React.Component {getData1 = () => {axios.get("http://localhost:3000/api1/search/users2").then((response) => {console.log("成功了", response.data);},(error) => {console.log("失敗了", error);});};getData2 = () => {axios.get("http://localhost:3000/api2/search/users2").then((response) => {console.log("成功了", response.data);},(error) => {console.log("失敗了", error);});};render() {return (<div className="todo-container"><button onClick={this.getData1}>獲取數據1</button><button onClick={this.getData2}>獲取數據2</button></div>);}
}
export default App;
執行結果:
兩個node的Server也都正常執行了
兄弟組件傳值Pub-Sub
之前可以看到不同的組件之間傳值只能通過state的狀態提升來進行傳值,但是很明顯這么做在大量數據交互的時候會變得巨難管理,所以我們要引入Pub-Sub.js來實現兄弟組件傳值。
PubSub組件傳值官網
npm安裝命令
npm install pubsub-js
版本查看
npm pubsub-js -v
快速起步
import PubSub from 'pubsub-js' //引入
PubSub.subscribe('用于識別的key', function(msg, data){ }); //訂閱
PubSub.publish('用于識別的key', data) //發布消息
注意:
適用于任意組件之前的消息溝通
誰用誰接,誰傳誰發
特別注意:
一定要先開啟監聽,再發布,要不然發半天沒人聽發了也沒有用
發布消息(publish)
消息發布,第一個參數訂閱key(后續訂閱會用到),第二個參數是值,下面這兩個都是完全可以的傳值方式
PubSub.publish('MY TOPIC', 'hello world!');
PubSub.publish('MY TOPIC', { info: "info" });還有一種同步發布,但是這種性能略強,但是不推薦,但有可能出現安全問題,慎重。用上面那兩個就夠了
PubSub.publishSync('MY TOPIC', 'hello world!');
比如這種,就可以傳入對象進去,發布給obj-key
的這個消息訂閱方
注意這里傳入對象不要用展開符
訂閱消息(subscribe)
// msg是必須接受的,如果實在不想接受可以傳個占位符
// var mySubscriber = function (_, data)
var mySubscriber = function (msg, data) {console.log(msg, data);
};// 生成一個token是為了后續停止訂閱用的,有專門api來關閉訂閱
var token = PubSub.subscribe('MY TOPIC', mySubscriber);// 關閉訂閱(可以用在組件卸載這種)
PubSub.unsubscribe(token);
也可以用箭頭函數,這兩種寫法是完全等價的
var mySubscriber = function (msg, data) {console.log(msg, data);
};// 生成一個token是為了后續停止訂閱用的,有專門api來關閉訂閱
var token = PubSub.subscribe('MY TOPIC', (msg, data)=>{// 打印出來的msg就是訂閱keyconsole.log(msg, data);
});
在實際代碼中加入,發現token作用域出現問題了,所以要改一下
用this修改一下,再用箭頭函數優化一下
代碼示例
三個組件集成在一個文件里,實現測試
import React from "react";
import PubSub from "pubsub-js";
import "./App.css";// 一定要先開啟監聽,再發布,要不然發半天沒人聽發了也沒有用
class PublishInfo extends React.Component {publishInfo = () => {var obj = { info: "info" };PubSub.publish("publish-key", obj);};render() {return (<div><button onClick={this.publishInfo}>publishInfo(后開啟發布)</button></div>);}
}// 一定要先開啟監聽,再發布,要不然發半天沒人聽發了也沒有用
class SubscriptInfo extends React.Component {subScriptInfo = () => {this.token = PubSub.subscribe("publish-key", (msg, data) => {// 這里只接受一個data也是可以的console.log(msg, data);});};unsubscribeBeforeUnmount = () => {// 模擬組件卸載前函數,暫停監聽。當然放在生命周期里面也可以console.log("tokenID:", this.token, PubSub.unsubscribe(this.token));};render() {return (<div><button onClick={this.subScriptInfo}>subScriptInfo(先開啟監聽)</button><button onClick={this.unsubscribeBeforeUnmount}>模擬卸載組件(最后)</button></div>);}
}
// 創建并暴露App組件
class App extends React.Component {render() {return (<div><SubscriptInfo /><PublishInfo /></div>);}
}
export default App;
極端場景
這種,從F組件給C組件傳值,只要key是一樣得,就可以通信到,所以跨組件傳值非常方便
Fetch發送請求(擴展)
首先 fetch 也是一種發送請求的方式,它是在 xhr 之外的一種,我們平常用的 Jquery 和 axios 都是封裝了 xhr 的第三方庫,而 fetch 是js官方自帶的庫(也是XMLHttpRequest的升級版),同時它也采用的是 Promise 的方式,大大簡化了寫法
由于是js內置,所以甚至可以在控制臺運行
在代碼里如何使用呢?
fetch("https://api.github.com/users/ruanyf").then((response) => response.json()).then((json) => console.log(json)).catch((err) => console.log("Request Failed", err));
搞個按鈕觸發一下fetch就可以用了
它的使用方法和 axios 非常的類似,都是返回 Promise 對象,但是不同的是, fetch 關注分離,它在第一次請求時,不會直接返回數據,會先返回聯系服務器的狀態,在第二步中才能夠獲取到數據
我們需要在第一次 then
中返回 response.json()
因為這個是包含數據的 promise 對象,response.json()
是一個異步操作,取出所有內容,并將其轉為 JSON 對象。再調用一次 then
方法即可獲取到對象,
但是這么多次的調用 then
并不是我們所期望的,相信看過之前生成器的文章的伙伴,已經有了想法。
我們可以利用 async
和 await
配合使用,來簡化代碼
可以將 await
理解成一個自動執行的 then
方法,這樣清晰多了
async function getJSON() {let url = 'https://xxx';try {let response = await fetch(url);return await reasponse.json();} catch (error) {console.log('Request Failed', error);}
}
最后關于錯誤對象的獲取可以采用 try...catch
來實現
關于 fetch 的更多內容
強烈推薦阮一峰老師的博文:fetch