總覽篇:react 實戰之雲書籤
原始碼見最下面
本篇是實戰系列的第一篇,主要是搭建 react 開發環境,在create-react-app
的基礎上加上如下功能:
- antd 元件庫按需引入 ,支援主題定製
- 支援 less 語法,並使用 css-module
- 配置路由
- 支援 http 請求
- 配置 redux
注意:需要 node 版本大於 8.0.
建立 create-react-app
- 安裝
npm install -g create-react-app
- 建立 react 應用
create-react-app bookmark-world
生成的目錄結構如下圖所示:
配置 antd,less
有兩種方法能夠對其配置進行修改:
- 通過
npm run eject
暴露出配置檔案,然後 修改這些配置檔案,相比於下面的方法不太優雅,因此不考慮. - 通過
react-app-rewired
覆蓋配置.
後續需要修改配置的都用第二種--覆蓋配置。
首先安裝依賴
在 2.1.x 版本的 react-app-rewired 需要配合customize-cra
來進行配置覆蓋。所以需要安裝如下依賴:
- react-app-rewired ,配置覆蓋
- customize-cra ,配置覆蓋
- antd ,ui 庫
- babel-plugin-import ,按需引入 antd
- less ,less 支援
- less-loader ,less 支援
程式碼如下:
npm install --save react-app-rewired customize-cra antd babel-plugin-import less less-loader
修改 package.json
用react-app-rewired
替換掉原來的react-scripts
/* package.json */
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
- "test": "react-scripts test",
+ "test": "react-app-rewired test",
}
建立 config-overrides.js
在專案根目錄,也就是package.json
的同級目錄建立config-overrides.js
檔案.內容如下:
const { override, fixBabelImports, addLessLoader } = require("customize-cra");
module.exports = override(
fixBabelImports("import", {
libraryName: "antd",
libraryDirectory: "es",
style: true
}),
addLessLoader({
localIdentName: "[local]--[hash:base64:5]",
javascriptEnabled: true,
modifyVars: { "@primary-color": "#1DA57A" }
})
);
使用 css-module
要使用 css-module 需要將 css 檔案命名為fileName.module.less
,然後就能在元件中引入並正常使用了,如下:
注意預設情況下字尾必須是.module.less 才能用 css-module 的寫法
import React, { Component } from "react";
import { Button } from "antd";
import styles1 from "./index.module.less";
class Hello extends Component {
render() {
return (
<div className={styles1.main}>
hello
<div className={styles1.text}>world</div>
<Button type="primary">你好</Button>
<div className="text1">heihei</div>
</div>
);
}
}
export default Hello;
配置路由
首先修改 src 目錄結構。改成如下所示:
目錄解釋:
- assets: 存放圖示,小圖片等資原始檔
- components:存放公共元件
- layout: 存放樣式元件,用於巢狀路由和子路由中複用程式碼
- pages: 存放頁面元件
- redux:存放 redux 相關
- action: 存放 action
- reducer: 存放 reducer 操作
- util: 工具類
刪除serviceWorker.js
檔案,並在index.js
中刪除和它相關的程式碼。這個是和離線使用相關的。
然後安裝react-router
依賴:
cnpm install --save react-router-dom
從路由開始就能體會到 react 一切都是 js 的精髓,react-router-dom 提供了一些路由元件來進行路由操作。本程式使用history
路由。
首先修改index.js
根元件放到<BrowserRouter>
下,以開啟 history 路由。程式碼如下:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const s = (
<BrowserRouter>
<App />
</BrowserRouter>
);
ReactDOM.render(s, document.getElementById("root"));
然後路由的配置方式有很多種,這裡採用程式碼的方式組織路由,並將將 App.jsx 作為路由配置中心。(也可以基於配置檔案,然後寫一個解析配置檔案的程式碼)
先加入登入和主頁的路由,主要程式碼如下:
render() {
const mainStyle = {
fontSize: "0.16rem"
};
return (
<Provider store={store}>
<div className="fullScreen" style={mainStyle}>
<Switch>
<Route exact path="/" component={Main} />
<Route exact path="/public/login" component={Login} />
<Route exact path="/404" component={NotFound} />
{/* 當前面的路由都匹配不到時就會重定向到/404 */}
<Redirect path="/" to="/404" />
</Switch>
</div>
</Provider>
);
}
名詞解釋:
- Switch: 該元件表示只匹配一個,匹配到後不再繼續往下匹配
- Route:路由元件
- exact:表示完全匹配,如果開啟這個,
/
只匹配/
,否則匹配所有的路徑 - Redirect:重定向元件,當前面的都不匹配就會匹配這個(因為沒有開啟
exact
且 path 為/
),然後重定向到/404
後續用到巢狀路由時會更加深入的講解路由相關。
配置 http 請求工具
http 請求工具這裡選擇的是axios
。
首先安裝依賴:
cnpm install --save axios
然後編寫工具類util/httpUtil.js
,程式碼如下:
// httpUtil.js
import { notification } from "antd";
import axios from "axios";
//定義http例項
const instance = axios.create({
// baseURL: "http://ali.tapme.top:8081/mock/16/chat/api/",
headers: {
token: window.token
}
});
//例項新增攔截器
instance.interceptors.response.use(
function(res) {
return res.data;
},
function(error) {
console.log(error);
let message, description;
if (error.response === undefined) {
message = "出問題啦";
description = "你的網路有問題";
} else {
message = "出問題啦:" + error.response.status;
description = JSON.stringify(error.response.data);
//401跳轉到登入頁面
}
notification.open({
message,
description,
duration: 2
});
setTimeout(() => {
if (error.response && error.response.status === 401) {
let redirect = encodeURIComponent(window.location.pathname + window.location.search);
window.location.replace("/public/login?redirect=" + redirect);
}
}, 1000);
return Promise.reject(error);
}
);
export default instance;
主要實現瞭如下功能:
- 自動新增 token,設計前後端通過 jwt 做認證,因此每個請求都要加上 token
- 響應預處理,如果有錯誤,自動彈窗提示。如果響應碼為 401,重定向到登入頁面。
配置 redux
redux 算是 react 的一大難點。這裡我們可以把 redux 理解成一個記憶體資料庫,用一個物件來儲存所有的資料.
對這個資料的修改有著嚴格的限制,必須通過 reducer 來修改資料,通過 action 定義修改的動作。
這裡以使用者登入資料為例。
定義
- 首先定義 action,建立檔案
redux/action/loginInfoAction.js
,程式碼如下:
// 定義登入資訊在store中的名字
export const DATA_NAME = "loginInfo";
//定義修改loginInfo type
export const CHANGE_LOGIN_INFO = "changeLoginStatus";
export const changeLoginInfo = (token, userInfo) => {
return {
type: CHANGE_LOGIN_INFO,
data: {
token,
userInfo
}
};
};
- CHANGE_LOGIN_INFO :定義操作類別
- changeLoginInfo: 定義一個 action,在元件中呼叫,傳入要修改的資料,在這裡加上 type 上傳遞到 reducer 中處理.
- 定義 reducer,建立檔案
redux/reducer/loginInfo.js
,程式碼如下:
import * as loginAction from "../action/loginInfoAction";
function getInitData() {
let token, userInfo;
try {
token = localStorage.getItem("token");
userInfo = JSON.parse(localStorage.getItem("userInfo"));
} catch (e) {
console.error(e);
token = null;
userInfo = null;
}
window.token = token;
window.userInfo = userInfo;
return {
token,
userInfo
};
}
const LoginStatusReducer = (state = getInitData(), action) => {
switch (action.type) {
case loginAction.CHANGE_LOGIN_INFO:
return { ...action.data };
default:
return state;
}
};
export default LoginStatusReducer;
- getInitData 方法用於初始化 userInfo 資料,這裡寫的比較複雜,會先從 localeStore 中取資料,然後掛載到 window 中,方便
httpUtil
中獲取 token。 - LoginStatusReducer 方法用於處理 action 中的資料,輸出處理後的 loginInfo 資料。
- 編寫 reducer 彙總類(redux/reducer/index.js),所有 reducer 都要彙總到一個方法中,這樣就能生成整個系統的 store 物件。程式碼如下:
import { combineReducers } from "redux";
import { DATA_NAME } from "../action/loginInfoAction";
import loginInfo from "./loginInfo";
const data = {};
data[DATA_NAME] = loginInfo;
const reducer = combineReducers(data);
export default reducer;
- 編寫
redux/index.js
,這裡生成真正的資料物件,程式碼如下:
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;
- 最後將 store 繫結到根節點(App.js)中即可,修改部分如下:
使用
這裡以登入頁為例,學習如何獲取到 loginInfo 和修改 loginInfo.
- 建立登入頁元件,
pages/public/Login/index.js
登入頁程式碼如下:
import React, { Component } from "react";
import queryString from "query-string";
import { Button, Input, message } from "antd";
import IconFont from "../../../components/IconFont";
import styles from "./index.module.less";
import { connect } from "react-redux";
import { changeLoginInfo, DATA_NAME } from "../../../redux/action/loginInfoAction";
import axios from "../../../util/httpUtil";
function mapStateToProps(state) {
return state[DATA_NAME];
}
function mapDispatchToProps(dispatch) {
return {
updateLoginInfo: (token, userInfo) => dispatch(changeLoginInfo(token, userInfo))
};
}
class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: "",
password: ""
};
this.query = queryString.parse(window.location.search);
}
usernameInput = e => {
this.setState({ username: e.target.value });
};
passwordInput = e => {
this.setState({ password: e.target.value });
};
submit = () => {
axios.post("/public/login", this.state).then(res => {
localStorage.setItem("token", res.token);
localStorage.setItem("userInfo", JSON.stringify(res.userInfo));
window.token = res.token;
window.userInfo = res.userInfo;
message.success("登入成功");
this.props.updateLoginInfo(res.token, res.userInfo);
if (this.query.redirect) {
this.props.history.replace(decodeURIComponent(this.query.redirect));
} else {
this.props.history.replace("/");
}
});
};
render() {
return (
<div className="fullScreen flex main-center across-center">
// 省略其他部分
<Button type="primary" onClick={this.submit}>
登入
</Button>
...
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Login);
其中最關鍵的是下面三個部分:
- mapStateToProps:本方法從整個 store 中獲取需要的資料,傳遞到 Login 元件的 props 中。
- mapDispatchToProps:本方法用於修改 store 資料,返回的函式物件也會繫結到 Login 元件的 props 中,其中的 dispath 引數,用於呼叫 reducer 中的處理函式,根據 changeLoginInfo 返回的 action。
- connect 方法用於將上面兩個函式和 Login 元件繫結起來,這樣就能在 props 中獲取到了。如果還有 withRouter,應將 withRouter 放在最外層。
目前登入訪問的介面為 yapi 的 mock 資料,真正的後臺程式碼將會在後面編寫。
結尾
作為一個剛開始學習 react 的菜鳥,歡迎各位大牛批評指正。
原始碼:github,切換到 tag:第一篇:環境搭建
,便可以看到截止到本篇的原始碼。