一個React專案總結(toB)

是你的山楂發表於2019-11-13

技術棧

  • 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

一個React專案總結(toB)

自定義配置——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.日期範圍統一處理

在這個專案中,有許多類似這樣的控制按鈕。

一個React專案總結(toB)
其實每一個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"; 有幾個點:

  1. 當資料為null,折線會自然斷開
  2. 會根據圖中資料型別不同(這裡表現會單位),做資料溢位處理。對某一種資料,範圍0-100,溢位顯示0 or 100,但是hover上去的詳情要顯示真實資料。
  3. 資料檢視(靜態顯示資料)是一個很好的功能,但是樣式比較醜,我們可以把它包裝成表格。還要注意資料檢視的top值,不要露出legend。

10.函式

  1. 在react中,我們在render的return中進行函式繫結時,最好不要在jsx中寫箭頭函式。因為這相當於定義了一個匿名函式,每一次render都要去定義一次,開銷較大。
  2. 在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官網文件我都沒讀完。從框架搭建到一些詳細的業務部分,這只是大概,我會抽空寫個更詳細的業務版。

相關文章