技術棧
- react16
- react-router5.0
- mobx4.0
- antd1.0
- axios(資源請求)
專案介紹
一個toB的智慧製造專案,分為分析端和管理端。分析端涉及到各種圖表展示,通過時間範圍來控制顯示內容;管理端主要是大量表單&表格。(第一次正經用react進行開發,學習了一個星期就開工咯( ╯□╰ ))
架構
- create-react-app搭建整體框架
- react-app-rewired進行自定義配置(需要安裝customize-cra,並在根目錄建一個config_override.js用於修改預設配置)
- import react-router mobx並做好相應配置
- github地址:github.com/fanxueqin/r…
- (可以download到本地,install依賴,然後直接啟動。我把業務程式碼抹掉了,這是一個比較完整的框架,可以直接進行業務開發)
專案目錄overview
自定義配置——config-overrides.js
const path = require('path');
const { override, fixBabelImports, addLessLoader, addWebpackAlias, babelInclude ,useBabelRc } = require('customize-cra');
const TerserPlugin = require('terser-webpack-plugin');
function resolve(dir) {
return path.join(__dirname, '.', dir)
}
let addCustom = () => config => { //遮蔽.map.js檔案,防止被讀到原始碼
let optimization = {
minimizer: [
new TerserPlugin({
sourceMap: false
})
]
}
config.optimization.minimizer = optimization.minimizer;
return config;
}
module.exports = override(
fixBabelImports('import', { //按需引入antd
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: {
// '@primary-color': '#1DA57A' ,
// '@link-color': '#1DA57A',
},
}),
addWebpackAlias({ //新增別名
'@': resolve('src'),
'components':path.resolve(__dirname,'src/components'),
'views': path.resolve(__dirname,'src/views'),
'layout': path.resolve(__dirname,'src/layout'),
'router': path.resolve(__dirname,'src/router'),
'api':path.resolve(__dirname,'src/api'),
'store': path.resolve(__dirname,'src/store'),
'assets': path.resolve(__dirname,'src/assets,'),
'mock': path.resolve(__dirname,'src/mock'),
'utils': path.resolve(__dirname,'src/utils')
}),
babelInclude([
path.resolve("src"),
]),
useBabelRc(), //配置裝飾器(mobx會用到)還需要.babelrc檔案配合
addCustom()
);
複製程式碼
.babelrc配置裝飾器
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
複製程式碼
setupProxy.js設定代理——前後端分離
const proxy = require('http-proxy-middleware');
const target = 'http://123.4.5.6:8080';
module.exports = function(app) {
app.use(proxy('/user', { target }))
app.use(proxy('/login', { target }))
app.use(proxy('/show', { target }))
app.use(proxy('/back', { target })))
};
複製程式碼
配置axios攔截器--aixos.js
import axios from "axios";
import qs from "qs"; //post請求時序列化
import { notification } from 'antd';
// http請求攔截器
axios.interceptors.request.use(
config => {
if (config.method.toUpperCase() === "GET") {
config.url =
config.url.indexOf("?") > 0
? config.url + "&clearCache=" + new Date().valueOf()
: config.url + "?clearCache=" + new Date().valueOf();
}
if (config.method.toUpperCase() === "POST") {
if (Object.prototype.toString.call(config.data) === "[object FormData]") {
console.log("資料型別", Object.prototype.toString.call(config.data));
} else {
config.data = qs.stringify(config.data); //序列化
config.headers["Content-Type"] = "application/x-www-form-urlencoded";
}
}
config.headers["Authorization"] = window.localStorage.getItem("token") ? window.localStorage.getItem("token") : '';
return config;
},
error => {
return Promise.reject(error);
}
);
// http響應攔截器
let loginTipLock = false;
axios.interceptors.response.use(
data => {
if (data.data["code"] && data.data["code"] === -2) {
window.location.hash = "#/login";
}
window.localStorage.setItem('token', data.headers.authorization) //token重新整理機制
return data;
},
err => {
if (err && err.response) {
switch (err.response.status) {
case 400:
err.message = "請求錯誤";
break;
case 401:
err.message = "登入已過期,請重新登入!";
window.location.hash = "#/login"; //token過期機制
break;
case 403:
err.message = "拒絕訪問";
window.location.hash = "#/notAuth";
break;
case 404:
err.message = `請求地址出錯: ${err.response.config.url}`;
break;
case 408:
err.message = "請求超時";
break;
case 500:
err.message = "伺服器內部錯誤";
break;
case 501:
err.message = "服務未實現";
break;
case 502:
err.message = "閘道器錯誤";
break;
case 503:
err.message = "服務不可用";
break;
case 504:
err.message = "閘道器超時";
break;
case 505:
err.message = "HTTP版本不受支援";
break;
default:
}
}
if (err.response.status === 401) {
if (!loginTipLock) { //避免同時多個請求都返回401時,彈出多個“未登入”提示框
loginTipLock = true;
notification.info({
message: '提示',
description: err.message
})
setTimeout(function () {
loginTipLock = false;
}, 1000)
}
} else {
notification.error({
message: '出錯啦',
description: err.message
});
}
return Promise.reject(err);
}
);
export default axios;
----對請求結果做一個統一處理----
import axios from "./axios";
import { notification } from 'antd';
const $http = (url, method = "GET", data, config = {}) => {
const _config = Object.assign({ url, method, data }, config);
return axios(_config).then(res => {
if (res.status === 200) {
if (res.data.code === -1) {
notification.error({
message: '出錯啦',
description: res.data.msg
});
throw new Error("請求出錯啦");
}
return res;
}
});
};
export default {
/*註冊*/
signIn: ({ username, password }) => $http(`/user/register`, "POST", { username,password }),
/*登入 */
login: ({ username, password }) => $http("/user/login", "POST", { username, password }),
/*上傳檔案 */
uploadFile: file => $http( "/upload/csv", "POST", {}, {headers: { "Content-Type": "multipart/form-data" }, processData: false, cache: false, data: file }),
}
複製程式碼
開發過程中的小知識(包含新get & 需要加強的)
1.用layout檔案劃分專案區塊
- 按這個專案的需求來說,我將專案分割為“分析模組”、“管理模組”、“使用者配置模組(包- 括登陸註冊)”、“license管理模組”
- 每一個模組的佈局都是用共通性的,所以可以把每個模組內公用部分抽離成可複用的元件,比如說header、footer、Nav
- 為每一個模組建立一個layout.js檔案,內容包括模組內公共元件引用,和模組內子路由配置
- 然後在App.js裡面引入這模組檔案的佈局檔案
2.路由鑑權元件
/* 定義 */
import React, { Component } from 'react'
import { withRouter } from 'react-router'
import { Route, Redirect } from 'react-router-dom'
import { inject, observer } from 'mobx-react';
@inject('appState')
@observer
class AuthorizedRoute extends Component {
render() {
const { component: Component, ...rest } = this.props;
const isLogin = !!JSON.parse(window.localStorage.getItem("userInfo"));
const userRole = JSON.parse(window.localStorage.getItem("userInfo")).role : 'user';
const path = this.props.path.substring(1);
const { authList } = this.props.appState;
return (
<Route {...rest} render={props => {
return isLogin
? (authList[path].includes(userRole) ? <Component {...props} /> : <Redirect to="/notAuth" />)
: <Redirect to="/login" />
}} />
)
}
}
export default withRouter(AuthorizedRoute);
/* 引用 */
const AuthorizedRoute = lazy( () => import('router/auth')); //react懶載入
class App extends Component{
render(){
return (
<div className="App">
<HashRouter>
<Suspense fallback={PageLoading}> //在Suspense元件中渲染lazy元件,我們可以在等待載入lazy元件時做優雅降級(如loading指示器)
<Switch>
<Redirect path='/' exact to="/show" />
<AuthorizedRoute path="/show" component={ShowHomeLayout} />
<AuthorizedRoute path="/back" component={BackHomeLayout} />
<Route path="/login" component={Login}></Route>
<Route path="/notAuth" component={NotAuth}></Route>
<Route component={NotFound} />
</Switch>
</Suspense>
</HashRouter>
</div>
);
}
}
export default App;
複製程式碼
3.路由帶引數(/:id)
如果整個頁面的資料依賴一個id,那麼最好把id作為路由的引數。
原因:
1.要考慮使用者複製當前url在新視窗開啟的情況
2.要考慮使用者重新整理頁面的情況
一個新的問題:
路由帶引數會有一個極端情況,就是使用者在這個導航上再點選一下,引數就會變成id= 'undefined'
(注: 路由引數會轉變成字串型)
解決方法:
if (_routerParam['id'] === 'undefined' || _routerParam['id'] === undefined) {
//重新獲取id相關資料
}
複製程式碼
4.canvas匯出圖片模糊
方法: canvas.toDataURL('image/jepg',1),這種jepg格式可以設定圖片質量,將質量設定為1,可以變清晰。(雖然可以選擇以jpg or png格式匯出,但實際上都是jpeg格式,目前還沒找到更好的辦法( ╯□╰ ))
引用canvas2image.js,可在github上找到。
function getDataURL(canvas, type, width, height) {
canvas = scaleCanvas(canvas, width, height);
//return canvas.toDataURL(type,1);原
return canvas.toDataURL('image/jpeg', 1);//改
}
複製程式碼
5.antd-menu元件要保留“展開”和“選中”狀態
-
這一塊處理起來比較麻煩,要保證重新整理,新視窗開啟都不出問題;另一方面,還要考慮摺疊之後、摺疊又展開的狀態。
-
思路: 將展開的submenu`s key存在sessionStorage;下次進入再取出;另外注意點選submenu標題時,做去重處理;另外注意摺疊之後,子選單的css。
selectedKeys: [], //表示當前選中menu-item opendKeys: [], //表示當前展開的submenu
6.對所有可能會超長的文字做溢位省略操作
給這些需要溢位省略的,賦一個class,需要的帶上這個class,並將相同的文字內容,賦給title屬性
如:
.sampleNameCon{
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
white-space: nowrap;
}
複製程式碼
7.日期範圍統一處理
在這個專案中,有許多類似這樣的控制按鈕。
其實每一個button最後都會生成一個時間範圍: start:xxx-xxxx end: xxx-xxx封裝一下:
import moment from 'moment';
const culateTimeRange = function (val, unit) { //數值(1),單位(day)
let _timeRange = {}
_timeRange['start'] = moment().subtract(unit, val).format('YYYY-MM-DD HH:mm:ss');
_timeRange['end'] = moment().format('YYYY-MM-DD HH:mm:ss');
return _timeRange;
}
export default culateTimeRange;
複製程式碼
8.匯出檔案
將後端返回的陣列匯出為表(csv),用到一個庫叫:saveAs
var file = new Blob(['\uFEFF' + res.data]); //\uFFEF是為了避免發生匯出檔案亂碼現象
saveAs(file, `name.csv`);
複製程式碼
9.echarts表單統一處理
因為本專案會有多處的折線圖表展示,所以考慮將該部分抽象成一個元件。 用到這個庫—— import ReactEcharts from "echarts-for-react"; 有幾個點:
- 當資料為null,折線會自然斷開
- 會根據圖中資料型別不同(這裡表現會單位),做資料溢位處理。對某一種資料,範圍0-100,溢位顯示0 or 100,但是hover上去的詳情要顯示真實資料。
- 資料檢視(靜態顯示資料)是一個很好的功能,但是樣式比較醜,我們可以把它包裝成表格。還要注意資料檢視的top值,不要露出legend。
10.函式
- 在react中,我們在render的return中進行函式繫結時,最好不要在jsx中寫箭頭函式。因為這相當於定義了一個匿名函式,每一次render都要去定義一次,開銷較大。
- 在render之外,為了避免this指代錯誤,最好都以箭頭函式的方式寫方法
11.react中類似於vue的watch
習慣在vue中用watch,剛從vue遷移到react很不適應。
可以用react的static getDerivedStateFromProps(nextProps,nextState) 和 componentDidUpdate(prevProps,prevState)配合,實現watch的功能。
12.效能優化相關
-
程式碼分割react-loadable
import Loadable from 'react-loadable';
<!--載入中效果-->
const PageLoading = ({ isLoading, error }) => {
if (isLoading) {
return <Spin
className="pageLoading"
size="large"
spinning={true}
/>
} else if (error) {
return <div className="pageLoadingError">資源載入失敗!</div>
} else {
return null
}
}
<!--封裝一下-->
const loadComponent = (loader,loading = PageLoading) =>{
return Loadable({
loader,
loading
})
}
// 路由
const home = loadComponent(() => import('views/show/home'))
export default{
home
}
複製程式碼
-
程式碼壓縮
我們用webpack已經將程式碼壓縮過了,但是如果開啟gzip壓縮,可以再壓縮一半。
開啟gzip需要前後端一起配合。
前端RequeshHeaders開啟—— Accept-Encoding: gzip, deflate
如果後端開啟的gzip,可以在Response-Headers中——Content-Encoding: gzip
-
圖片壓縮
可以用tinyPNG對圖片進行壓縮,當然按照業務場景可以進行按需載入和雪碧圖
不考慮相容性的話,推薦用谷歌新出的webp格式的圖片,小而美
總結
第一次用react進行專案開發,學習時間很短,甚至react官網文件我都沒讀完。從框架搭建到一些詳細的業務部分,這只是大概,我會抽空寫個更詳細的業務版。