React 折騰記 - (10) UmiJS 2.x + antd 重寫後臺管理系統記錄的問題及解決姿勢

CRPER發表於2018-12-21

前言

用的是umi 2.x ,寫起來挺舒服;順帶完善了上一版本後臺的一些細節問題,功能等

umijs類似create-react-app, 也是一套方案的集合體,亮點很多.可以具體官網去看

  • 宣告式的路由(nuxtjs既視感)
  • dva(基於redux+redux-saga的封裝方案):寫起來有vuex的感覺;

主要記錄我在過程中遇到的問題及解決的姿勢,技術棧 antd 3.11.x + umi 2.x + react 16.7


問題彙總及解決姿勢

moment的一些用法及antd 日期元件的細節

關於moment

為什麼說另類..就是原生日期API結合moment,因為我們介面需要傳遞時間戳,而是不帶毫秒級的;

而且時間必須為當天的凌晨00:00:00開始,結束時間到操作的此刻(直接new Date().getTime()就是此刻);

// 會直接返回你設定時間的時間戳
new Date().setHours(0, 0, 0, 0)

// 凌晨`00:00:00`
moment(new Date().setHours(0, 0, 0, 0))

// 近七天
moment(new Date().setHours(0, 0, 0, 0) - 7 * 24 * 3600000)

// 月初
moment().startOf('month')

複製程式碼

轉成unix stamp(伺服器常用的時間戳規格),呼叫moment().unix()即可;

若是不控制到凌晨00:00:00這種,

日期可以直接用momentadd方法往後推導,subtract往前推導,支援日/周/月/年

antd的日期元件

置空用null是允許的,其他的話需要轉成moment物件,控制元件獲取的值預設就是moment物件


props.children的改造,新增樣式亦或者事件!

在封裝一些元件的過程,我用了React.Fragment(<></>: 簡寫)來保證元件同級並列

有些必須需要props.children帶上一些屬性或者樣式來保證我想要的效果.

一開始無解, 因為Fragement簡寫的姿勢沒法props,那也就是說沒做寫成高階;

找了下官方文件,發現有這麼兩個API:

  • React.Children : 提供了幾個遍歷子元素(React Element)的方法,與常規陣列用法類似,只是引數不一樣
  • React.cloneElement: 如名字所示,克隆子元素

這是上篇文章用到的部分內容,需要改造傳遞進來的按鈕,給新增樣式


// 構建
// 克隆子元件並且新增自己要新增的特性
const PropsBtn = React.Children.map(this.props.children, child =>
  React.cloneElement(child, {
    style: {
      marginLeft: 8,
    },
  })
);

// 渲染
{PropsBtn ? <>{PropsBtn}</> : null}

複製程式碼

memoize-one來改善效能

可以快取同樣引數的結果集,非常適用於遞迴這類的函式處理,大大減少計算的壓力;

memoize-one;

也能用於React這類,是否有必要重新setState, 第二個引數支援比較,官方推薦用lodash去深度比較


函式式元件內返回一個HOC的元件

最簡單粗暴的方法就是用變數快取,然後直接返回元件,比如我這邊文章就用了;

React 折騰記 - (9) 基於Antd+react-router-breadcrumbs-hoc封裝一個小巧的麵包屑元件


umi 約定式基礎鑑權

layouts裡面分別寫對應的佈局,然後由一個鑑權元件去判定是否允許進入,比如

/src/layout/index.js


import React from 'react';
import withRouter from 'umi/withRouter';

// 鑑權元件, 我寫了webpack alias
import Authorized from 'components/Authorized';

// 佈局元件
import EnranceLayout from './EntranceLayout';
import AdminLayout from './AdminLayout';

// 中文地區時間轉換引入
import moment from 'moment';
import 'moment/locale/zh-cn';

// 路由動效
import { TransitionGroup, CSSTransition } from 'react-transition-group';

// 頁面標題
import { Helmet } from 'react-helmet';
import { getDocumentTitle } from 'components/Sidebar/RouterTree';

moment.locale('zh-cn');

export default withRouter(props => {
  const {
    location: { pathname },
    location,
  } = props;

  // 根據路由定址,再結合鑑權來判定是否允許進入,根據您自身的業務進行調整
  if (pathname.indexOf('/entrance') === -1) {
    if (pathname.indexOf('/editor') !== -1) {
      return (
        <Authorized>
          <Helmet>
            <title>{getDocumentTitle(pathname)}</title>
          </Helmet>
          <TransitionGroup>
            <CSSTransition key={location.key} classNames="spread" timeout={1000}>
              {props.children}
            </CSSTransition>
          </TransitionGroup>
        </Authorized>
      );
    }
    return (
      <AdminLayout>
        <Helmet>
          <title>{getDocumentTitle(pathname)}</title>
        </Helmet>
        <TransitionGroup>
          <CSSTransition key={location.key} classNames="spread" timeout={1000}>
            {props.children}
          </CSSTransition>
        </TransitionGroup>
      </AdminLayout>
    );
  }

  return (
    <EnranceLayout>
      <TransitionGroup>
        <CSSTransition key={location.key} classNames="spread" timeout={1000}>
          {props.children}
        </CSSTransition>
      </TransitionGroup>
    </EnranceLayout>
  );
});


複製程式碼

model的規劃

全域性的放在src/models目錄,其他的page級別推薦直接model.js,官方說會自下往上尋找;

是根據namespace來區分的..不允許存在同名的namespace;

若是要開啟umimodel動態引入, page級別不允許呼叫其他pagemodel,不然會報錯,初始化找不到的!!!

所以全域性性放在全域性更為合適,當然你不需要動態引入的話,頁面間跨調是允許的..我目前是這麼做;


pages目錄下的檔案或者目錄不自動生成對應可訪問的page

預設在page目錄下,除了部分特殊的檔案(比如官方自己過濾的models),都會自動產生可訪問的頁面,

也就是說檔案會被當做路由元件;

遮蔽的話, 開啟專案的配置檔案.umirc.js


const path = require('path');
// ref: https://umijs.org/config/
export default {
  plugins: [
    // ref: https://umijs.org/plugin/umi-plugin-react.html
    [
      'umi-plugin-react',
      {
        antd: true,  // 預設引入antd
        dva: {  // 啟用引入dva
          immer: true,
          dynamicImport: false,  // models 動態引入關閉
          hmr: true,
        },
        dynamicImport: false, // 元件切割動態引入
        title: '聲兮後臺管理系統',
        dll: true,
        routes: { // 此處用正則忽略不想產生路徑的檔案或者目錄!!!
          exclude: [/model\.js/, /models\//, /services(\/|\.js)?/, /components\//],
        },
        hardSource: true,
        locale: {},
      },
    ],
  ],
};

複製程式碼

umi配置開發的反向代理及目錄的alias


const path = require('path');
// ref: https://umijs.org/config/
export default {
  plugins: [
  alias: {
    '@': path.resolve(__dirname, './src'),
    models: path.resolve(__dirname, './src/models'),
    components: path.resolve(__dirname, './src/components'),
    utils: path.resolve(__dirname, './src/utils'),
    services: path.resolve(__dirname, './src/services'),
    assets: path.resolve(__dirname, './src/assets'),
  },
  proxy: {
    '/api/web': {
      target: 'http://stagapi.xxxx.com',
      changeOrigin: true,
      secure: false,
      // pathRewrite: { '^/api': '/' },
    },
  },
};

複製程式碼

如果在dvadispatchsetState

// dispatch返回的是一個promise,直接then即可,
// 傳入callback 姿勢無效(對於setState)

this.props.dispatch({
      type: 'appuser/batchItem',
      payload: {
        batchType: 2,
        userIdList: userIdList,
      },
    })
    .then(res => {
      this.setState({
        selectedRowKeys: [],
      });
      message.success(`批量封號,操作成功!`);
    });
}}

複製程式碼

如何在umi這種加入preloading

就是react程式碼沒載入之前,顯示的區域塊,

目前的做法就是自定義模板檔案,放在react渲染塊內部,在解析程式碼渲染完畢會被替換掉

效果如下

React 折騰記 - (10) UmiJS 2.x + antd 重寫後臺管理系統記錄的問題及解決姿勢

src/pages/document.ejs

<!doctype html>
<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>xx管理後臺</title>
  <style>
    .preloadLoading{
        position:fixed;
        left:0;
        top:0;
        width:100%;
        height:100%;
        display:flex;
        justify-content:center;
        align-items:center;
    }
    @-webkit-keyframes square-animation {
    0% {
        left: 0;
        top: 0;
    }
    10.5% {
        left: 0;
        top: 0;
    }
    12.5% {
        left: 32px;
        top: 0;
    }
    23% {
        left: 32px;
        top: 0;
    }
    25% {
        left: 64px;
        top: 0;
    }
    35.5% {
        left: 64px;
        top: 0;
    }
    37.5% {
        left: 64px;
        top: 32px;
    }
    48% {
        left: 64px;
        top: 32px;
    }
    50% {
        left: 32px;
        top: 32px;
    }
    60.5% {
        left: 32px;
        top: 32px;
    }
    62.5% {
        left: 32px;
        top: 64px;
    }
    73% {
        left: 32px;
        top: 64px;
    }
    75% {
        left: 0;
        top: 64px;
    }
    85.5% {
        left: 0;
        top: 64px;
    }
    87.5% {
        left: 0;
        top: 32px;
    }
    98% {
        left: 0;
        top: 32px;
    }
    100% {
        left: 0;
        top: 0;
    }
}
@keyframes square-animation {
    0% {
        left: 0;
        top: 0;
    }
    10.5% {
        left: 0;
        top: 0;
    }
    12.5% {
        left: 32px;
        top: 0;
    }
    23% {
        left: 32px;
        top: 0;
    }
    25% {
        left: 64px;
        top: 0;
    }
    35.5% {
        left: 64px;
        top: 0;
    }
    37.5% {
        left: 64px;
        top: 32px;
    }
    48% {
        left: 64px;
        top: 32px;
    }
    50% {
        left: 32px;
        top: 32px;
    }
    60.5% {
        left: 32px;
        top: 32px;
    }
    62.5% {
        left: 32px;
        top: 64px;
    }
    73% {
        left: 32px;
        top: 64px;
    }
    75% {
        left: 0;
        top: 64px;
    }
    85.5% {
        left: 0;
        top: 64px;
    }
    87.5% {
        left: 0;
        top: 32px;
    }
    98% {
        left: 0;
        top: 32px;
    }
    100% {
        left: 0;
        top: 0;
    }
}
@-webkit-keyframes hue-rotate {
    0% {
        -webkit-filter: hue-rotate(0deg);
        filter: hue-rotate(0deg);
    }
    100% {
        -webkit-filter: hue-rotate(360deg);
        filter: hue-rotate(360deg);
    }
}
@keyframes hue-rotate {
    0% {
        -webkit-filter: hue-rotate(0deg);
        filter: hue-rotate(0deg);
    }
    100% {
        -webkit-filter: hue-rotate(360deg);
        filter: hue-rotate(360deg);
    }
}
.loading {
    position: relative;
    width: 96px;
    height: 96px;
    -webkit-transform: rotate(45deg);
    transform: rotate(45deg);
    -webkit-animation: hue-rotate 10s linear infinite both;
    animation: hue-rotate 10s linear infinite both;
}
.loading__square {
    position: absolute;
    top: 0;
    left: 0;
    width: 28px;
    height: 28px;
    margin: 2px;
    border-radius: 2px;
    background: #07a;
    background-image: -webkit-linear-gradient(45deg, #fa0 40%, #0c9 60%);
    background-image: linear-gradient(45deg, #fa0 40%, #0c9 60%);
    background-image: -moz-linear-gradient(#fa0, #fa0);
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
    -webkit-animation: square-animation 10s ease-in-out infinite both;
    animation: square-animation 10s ease-in-out infinite both;
}
.loading__square:nth-of-type(0) {
    -webkit-animation-delay: 0s;
    animation-delay: 0s;
}
.loading__square:nth-of-type(1) {
    -webkit-animation-delay: -1.42857s;
    animation-delay: -1.42857s;
}
.loading__square:nth-of-type(2) {
    -webkit-animation-delay: -2.85714s;
    animation-delay: -2.85714s;
}
.loading__square:nth-of-type(3) {
    -webkit-animation-delay: -4.28571s;
    animation-delay: -4.28571s;
}
.loading__square:nth-of-type(4) {
    -webkit-animation-delay: -5.71429s;
    animation-delay: -5.71429s;
}
.loading__square:nth-of-type(5) {
    -webkit-animation-delay: -7.14286s;
    animation-delay: -7.14286s;
}
.loading__square:nth-of-type(6) {
    -webkit-animation-delay: -8.57143s;
    animation-delay: -8.57143s;
}
.loading__square:nth-of-type(7) {
    -webkit-animation-delay: -10s;
    animation-delay: -10s;
}
  </style>
</head>

<body>
  <div id="root">
    <div class="preloadLoading">
        <div class='loading'>
            <div class='loading__square'></div>
            <div class='loading__square'></div>
            <div class='loading__square'></div>
            <div class='loading__square'></div>
            <div class='loading__square'></div>
            <div class='loading__square'></div>
            <div class='loading__square'></div>
        </div>
    </div>
  </div>
</body>

</html>

複製程式碼

標題如何自動隨著路由表資訊改變

首先得自己維護一份靜態路由表,類似vue或者react-router@3那種,

結合@withRouter拿到pathname 傳入到靜態路由表遍歷

(這裡就可以用到上面說的memoize-one來提高效能),

效果如下

React 折騰記 - (10) UmiJS 2.x + antd 重寫後臺管理系統記錄的問題及解決姿勢

姿勢如下

react-helmet來實現title的替換,這貨不僅僅可以替換title還能替換meta這些

參考上面的問題 ==> umi 約定式基礎鑑權 ,這裡就有用到


antd 選單欄隨著寬度自適應及風格變化

就是縮小的時候隱藏部分子選單,這個問題在我做側邊欄變水平的時候遇到.我縮小到ipad的尺寸

會溢位,用了常規的法子,就正常了,就是style那裡設定一個最大寬度或者寬度

至於風格變化是因為antd內建了兩套風格


<Menu
          style={{ maxWidth: '100%', flex: 1 }}
          subMenuOpenDelay={0.3}
          theme={theme ? 'dark' : 'light'}
          mode={mode ? 'horizontal' : 'inline'}
          openKeys={openKeys}
          selectedKeys={selectedKeys}
          onOpenChange={this.onOpenChange}
        >

複製程式碼

當然Logo元件這些肯定是你自己拿了狀態去變化的,還有包裹的父級區域的樣式

目前不做配置儲存,想做儲存的,寫在localStorage不失為一個好法子,沒必要寫到資料庫,都是自己人用

效果如下

React 折騰記 - (10) UmiJS 2.x + antd 重寫後臺管理系統記錄的問題及解決姿勢

專案沒有用到antd pro這個模板(太臃腫),自己寫比較實在


總結

有新的且覺得有點意義的問題我會陸續更新...繼續寫小程式的需求去..

有不對之處請留言,會及時修正..謝謝閱讀

相關文章