一個React項目總結(toB)

技術棧

  • react16
  • react-router5.0
  • mobx4.0
  • antd1.0
  • axios(資源請求)

項目介紹

一個toB的智能製造項目,分爲分析端和管理端。分析端涉及到各類圖表展現,經過時間範圍來控制顯示內容;管理端主要是大量表單&表格。(第一次正經用react進行開發,學習了一個星期就開工咯( ╯□╰ ))javascript

架構

  • 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做爲路由的參數。css

緣由:
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組件要保留「展開」和「選中」狀態

  • 這一塊處理起來比較麻煩,要保證刷新,新窗口打開都不出問題;另外一方面,還要考慮摺疊以後、摺疊又展開的狀態。vue

  • 思路: 將展開的submenu`s key存在sessionStorage;下次進入再取出;另外注意點擊submenu標題時,作去重處理;另外注意摺疊以後,子菜單的css。java

    selectedKeys: [], //表示當前選中menu-item opendKeys: [], //表示當前展開的submenureact

6.對全部可能會超長的文字作溢出省略操做

給這些須要溢出省略的,賦一個class,須要的帶上這個class,並將相同的文字內容,賦給title屬性
如:
.sampleNameCon{
    text-overflow: ellipsis;
    overflow: hidden;
    word-break: break-all;
    white-space: nowrap;
  }
複製代碼

7.日期範圍統一處理

在這個項目中,有許多相似這樣的控制按鈕。webpack

其實每個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),用到一個庫叫:saveAsios

var file = new Blob(['\uFEFF' + res.data]);  //\uFFEF是爲了不發生導出文件亂碼現象
 saveAs(file, `name.csv`);
複製代碼

9.echarts表單統一處理

由於本項目會有多處的折線圖表展現,因此考慮將該部分抽象成一個組件。 用到這個庫—— import ReactEcharts from "echarts-for-react"; 有幾個點:git

  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很不適應。github

能夠用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官網文檔我都沒讀完。從框架搭建到一些詳細的業務部分,這只是大概,我會抽空寫個更詳細的業務版。

相關文章
相關標籤/搜索