React + tsx + qiankun + DataV 实现可视化大屏模板

1,720 阅读6分钟

需要掌握知识:

  1. React + tsx 全家桶
  2. qiankun
  3. DataV
  4. plop
  5. scp2
  6. Mock.js 模拟后端请求

生成项目那些就不多说了,直接步入正题!

项目目录

├── mock/                          // mock 服务端数据模拟
├── public/                        
└── src/
    ├── .setting/                  // plop 自动生成文件模板配置
    ├── api/                       // 请求接口存放
    ├── common/                    // 公共接口
    ├── components/                // 公共组件目录
    ├── module/                    // 全局基类以及上下文存放位置
    ├── static/                    // 静态资源管理
    ├── store/                     // 状态管理目录
    ├── utils/                     // 工具函数目录
    ├── pages/                     // 页面组件目录
    ├── App.tsx
    ├── index.tsx
    ├── shims-vue.d.ts
├── plopfile.ts                    // 自动生成模板文件
├── upload.server.ts               // 自动上传指定服务器ftp
├── prettier.config.js             // 保存自动格式化
├── tsconfig.json                  // TypeScript 配置文件
├── config-overrides.js            // 项目配置文件
└── package.json

文章有点长,请耐心观看哦,项目git仓库,欢迎点亮小星星 🌟🌟

搭建底层架构

qiankun篇

1.安装与配置qiankun

npm i qiankun

装包之际,我们可以进行项目的配置啦

在主项目根目录下新建 config-overrides.js 文件,写入如下配置

image.png

在子项目目录下新建 config-overrides.js config-overrides-proxy.js 文件,写入如下配置

1.config-overrides.js

image.png

2.config-overrides-proxy.js

image.png

在主项目的APP.tsx配置如下:

image.png

按照以上配置就可以跑通 qiankun 啦

axios拦截器篇

2. 安装与配置axios

 npm i axios

在utils目录下新建http目录,并且分别创建model.ts接口文件以及request.ts配置文件 1.request.ts

image.png

主要的响应拦截和请求拦截

image.png

请求封装

image.png

最后将request抛出就行了

2.model.ts 这是请求封装的接口

image.png

现在我们来看看怎么去使用

封装一个请求函数,他的返回值是Promise,并且定义泛型去控制返回值的类型提示 image.png

这样我们就可以给调用者传入一个类型,到时候直接可以提示返回类型 image.png

scp2 配置篇

3. 安装与配置scp2

npm i scp2 -D

具体详细配置请看我写的另一篇关scp2的配置文章:前端黑科技篇章之scp2,让你一键打包部署服务器

plop 模板生成篇

4. 安装与配置plop

npm i plop -D

具体详细配置请看我写的另一篇关scp2的配置文章:前端黑科技篇章之plop,让你也拥有自己的脚手架

redux 篇

5. 安装与配置redux需要的相关依赖

npm i react-redux redux-devtools-extension redux-thunk

配置如下:

  1. index.ts

image.png

  1. activeTypes.ts active 常量 image.png

3.reduces 基本配置

image.png

步入正题:代码篇

1. 路由守卫

因为react-router 4.0 版本之后抛弃了 onEnter 之类的监视回调,就利用了高阶函数进行了 路由守卫,代码如下:

// 路由列表
import React from 'react'
import { IMenuItem } from '../common/model/IMenuItem'
import Login from '../pages/login/login'
import PageOne from '../pages/pageOne'
import PageTwo from '../pages/pageTwo'

export const ASSETS_MENUS: IMenuItem[] = [
  { path: '/page-one', title: '页面一', exact: false, isShowTitle: true, render: () => <PageOne /> },
  { path: '/page-two', title: '页面二', exact: false, isShowTitle: true, render: () => <PageTwo /> },
  { path: '/login', title: '登录', exact: false, isShowTitle: false, render: () => <Login /> }
]


// 路由配置

import React from 'react'
import { BrowserRouter, Switch, Route, Redirect, NavLink, RouteComponentProps } from 'react-router-dom'
import { createBrowserHistory } from 'history'

import { ASSETS_MENUS } from '../../module/routerLink'
import { IMenuItem } from '../../common/model/IMenuItem'
import './routeLink.scss'
import { EchartContext, InitEchartContext, InterInitEchartContxt } from '../../module/echarts'
import { connect } from 'react-redux'
import { InterUser, InterUserInfo } from '../../store/model/IUser'
import { ICombinedState } from '../../store/reducers'
import Cookie from 'js-cookie'
import { Dispatch } from 'redux'
import { SET_TOKEN, SET_USERINFO } from '../../store/activeTypes'
import { getUserInfo } from '../../api/userApi'

interface IRouterProps extends RouteComponentProps {
  token: string
  setToken: (token: string) => void
  setUserInfo: (userInfo: InterUserInfo) => void
  [key: string]: any
}

interface IRouterState {
  menus?: IMenuItem[]
}

class RouterLink extends React.Component<IRouterProps, IRouterState> {
  state: IRouterState = {}

  componentDidMount() {
    this.setState({
      menus: ASSETS_MENUS
    })
  }

  routerList = () => {
    const menus = this.state.menus
    return (
      <div className='nav dispaly-content-center mt-1'>
        {
          menus?.map(i =>
            i.isShowTitle &&
            <NavLink key={ i.path } to={ i.path } className='nav-item' activeClassName='nav-item-active'>{ i.title }</NavLink>
          )
        }
      </div>
    )
  }

  render() {
    const echartContext: InterInitEchartContxt = new EchartContext()
    return (
      <div>
        <InitEchartContext.Provider value={ echartContext } >
          <BrowserRouter>
            { this.props.token && this.routerList() }
            <Switch>
              {/* {
                this.state.menus?.map(i => (
                  // <Route path={ i.path } exact={ i.exact } render={i.render} key={ i.path } />

                ))
              } */}

              <RouterGuard
                menus={ this.state.menus }
                token={ this.props.token }
                setToken={ this.props.setToken }
                setUserInfo={ this.props.setUserInfo }
                location={this.props.location}
                match={this.props.match}
                history={this.props.history}
              />
              {/* <Route
                path='*'
                exact
                render={props =>
                  <RouterGuard
                    routeInfo={ props }
                    menus={ this.state.menus }
                    token={ this.props.token }
                    setToken={ this.props.setToken }
                    setUserInfo={ this.props.setUserInfo }
                  />}
              /> */}
            </Switch>
          </BrowserRouter>
        </InitEchartContext.Provider>
      </div>
    )
  }
}

interface RouterGuardProps extends IRouterProps {
  menus?: IMenuItem[]
  [key: string]: any
}

// 路由守卫高阶组件( 只适配一级路由 )
class RouterGuard extends React.Component<RouterGuardProps, {}> {
  async componentDidMount() {
    const { token: reduxToken } = this.props
    const token = Cookie.get('USER_TOKEN')
    if (
      token &&
      !reduxToken
    ) {
      this.props.setToken(token as string)
      await getUserInfo<InterUserInfo>().then(res => {
        this.props.setUserInfo(res)
      })
    }
  }

  render() {
    const { menus, token: reduxToken, location } = this.props

    const history = createBrowserHistory()

    let toPath = location.pathname

    if (!reduxToken && toPath !== '/login') { // 如果没登陆
      toPath = '/login'
      history.replace(toPath)
    }

    // 获取当前路由的路由对象
    const component: IMenuItem = menus?.find(i => i.path === toPath) as IMenuItem

    if (!reduxToken && component) { // 没有登录 并且存在路由对象
      return <Route path={ toPath } exact={ component.exact } render={component.render} key={ component.path } />
    }

    // 登录了 并且跳往登录页,则返回上一级
    if (
      reduxToken &&
      toPath === '/login'
    ) {
      history.goBack()
      return null
    }

    // 重定向首页
    if (
      toPath === '/' ||
      !menus?.some(c => toPath !== c.path)
    ) {
      return <Redirect to='/page-one' />
    }

    // return this.props.token ? <Redirect to={ props.match.url } /> : <Redirect to='/login' />

    if (reduxToken) { // 渲染加载路由
      return <Route path={ component.path } exact={ component.exact } render={component.render} key={ component.path } />
    } else {
      return <Redirect to='/login' />
    }
  }
}

const mapStateToProps = (state: ICombinedState): InterUser => state.user

const mapDispatchToProps = (dispatch: Dispatch) => ({
  setToken: (token: string) => dispatch({ type: SET_TOKEN, token }),
  setUserInfo: (userInfo: InterUserInfo) => dispatch({ type: SET_USERINFO, userInfo })
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(RouterLink)

2.全局基类

用于写一些全局公用方法

import React from 'react'

import Cookie from 'js-cookie'
import Message, { InterMessagesProps } from '../components/messages/messages'

export interface IOptions {
  msg: any,
  isShowLoading: boolean
}

export interface InterAbstractComponent {
  closeLoadingShow?: () => void
  setLoadingState?: (loadingInfo: IOptions) => void
  message?: (options: InterMessagesProps) => void

  setToken?: (token: string) => void
  getToken?: () => string
}

export class AbstractComponent<
  P extends InterAbstractComponent,
  S,
  SS = any
> extends React.PureComponent<P, S, SS> {
  private USER_TOKEN = 'USER_TOKEN'

  message(options: InterMessagesProps) {
    // eslint-disable-next-line no-new
    new Message(options)
  }

  closeLoadingShow() {
    this.props.setLoadingState?.({ msg: '', isShowLoading: false })
  }

  setToken(token: string) {
    Cookie.set(this.USER_TOKEN, token)
  }

  getToken() {
    return Cookie.get(this.USER_TOKEN)
  }

  removeToken() {
    return Cookie.remove(this.USER_TOKEN)
  }
}

3.全局echart 上下文

统一管理echart 配置以及结构逻辑,把相同的图表进行封装复用

import React from 'react'
import echarts from 'echarts/lib/echarts'
// import * as gexf from 'echarts/extension-src/dataTool/gexf'

import 'echarts/lib/chart/line'
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/scatter'
import 'echarts/lib/chart/graph'

import 'echarts/lib/component/legend'
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/toolbox'
import 'echarts/lib/component/graphic'
import 'echarts/lib/component/legendScroll'

import { EChartsFullOption } from 'echarts/lib/option'
import { ScatterSeriesOption } from 'echarts/lib/chart/scatter/ScatterSeries'
import { IEcharts, ILineSeries, IScatterSeries } from '../pages/pageOne/model'
import { ILinks, ICategories, INodes } from '../common/model/IGrah'

export interface InterInitEchartContxt {
  initEchart: (id: string, type: EChartsFullOption) => void
  getDocumentElementId: (id: string) => HTMLElement

  lineDataFormat: (echartData: IEcharts) => EChartsFullOption
  lineSeriesDataFormat: (ser: ILineSeries[]) => ILineSeries[]

  scatterDataFormat: (echartData: IEcharts) => EChartsFullOption
  scatterSeriesObj: () => ScatterSeriesOption
  scatterSeriesTitleFormat: (srcData: IScatterSeries[]) => string[]
  scatterSeriesDataFormat: (srcData: IScatterSeries[]) => ScatterSeriesOption[]

  graphOptionsFormat: (echartData: any) => EChartsFullOption
  graphDataFormat: (echartData: any) => { links: any[], nodes: any[], categories: ICategories[] }
  grahNodeDataFormat: (nodes: any) => INodes[]
  grahLinksDataFormat: (links: any) => ILinks[]
}

export class EchartContext implements InterInitEchartContxt {
  getDocumentElementId(id: string): HTMLElement {
    return document.getElementById(id) as HTMLElement
  }

  initEchart(id: string, data: EChartsFullOption) {
    echarts.init(this.getDocumentElementId(id) as HTMLElement).setOption(data)
  }

  lineDataFormat(echartData: IEcharts): EChartsFullOption {
    return {
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'cross',
          crossStyle: {
            color: '#616E80'
          }
        }
      },
      legend: {
        data: echartData.titleArr,
        textStyle: {
          color: '#6D7988'
        },
        right: 'left'
      },
      grid: {
        top: '20%',
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
      },
      xAxis: [
        {
          type: 'category',
          data: echartData.xAxisArr,
          axisPointer: {
            type: 'shadow'
          }
        }
      ],
      yAxis: [
        {
          type: 'value',
          min: 0,
          max: 80,
          interval: 20,
          axisLabel: {
            formatter: '{value}',
            color: '#728FAA'
          },
          splitLine: {
            lineStyle: {
              type: 'dashed',
              color: '#6D7988'
            }
          }
        },
        {
          type: 'value',
          min: 0,
          max: 100,
          interval: 25,
          axisLabel: {
            formatter: '{value}%',
            color: '#728FAA'
          },
          splitLine: {
            lineStyle: {
              type: 'dashed',
              color: '#6D7988'
            }
          }
        }
      ],
      series: this.lineSeriesDataFormat(echartData.seriesArr as ILineSeries[])
    }
  }

  lineSeriesDataFormat(ser: ILineSeries[]): ILineSeries[] {
    ser.map(i => {
      if (i.type === 'line') {
        i.yAxisIndex = 1
        i.lineStyle = {
          color: '#1EBCA1'
        }
        i.itemStyle = {
          color: '#1EBCA1'
        }
      }

      return i
    })
    return ser
  }

  scatterSeriesTitleFormat(srcData: IScatterSeries[]): string[] {
    let titleArr: string[] = []
    srcData.forEach(i => {
      titleArr = [...titleArr, i.d1 as string]
    })
    return titleArr
  }

  scatterSeriesObj(): ScatterSeriesOption {
    return {
      name: '',
      data: [],
      type: 'scatter',
      symbolSize: function(data) {
        return Math.sqrt(data[0]) / 5 // 球球大小
      },
      emphasis: {
        label: {
          show: true,
          formatter: function(param: { data: any[]; }) {
            return param.data[2]
          },
          position: 'top'
        }
      },
      itemStyle: {
        shadowBlur: 10,
        shadowColor: 'rgba(120, 36, 50, 0.5)',
        shadowOffsetY: 5
      }
    }
  }

  scatterSeriesDataFormat(srcData: IScatterSeries[]): ScatterSeriesOption[] {
    let seriesArr: ScatterSeriesOption[] = []
    let curObj = {}
    srcData.forEach(i => {
      curObj = {
        data: [[i.d3, i.d2, i.d1]],
        name: i.d1
      }
      seriesArr = [        ...seriesArr,        {          ...this.scatterSeriesObj(),          ...curObj        }      ]
    })
    return seriesArr
  }

  scatterDataFormat(echartData: IEcharts): EChartsFullOption {
    return {
      legend: {
        type: 'scroll',
        top: 10,
        data: echartData.data && this.scatterSeriesTitleFormat(echartData.data),
        textStyle: {
          color: '#6D7988'
        },
        pageButtonPosition: 'end'
      },
      xAxis: {
        axisLabel: {
          color: '#728FAA'
        },
        splitLine: {
          lineStyle: {
            type: 'solid',
            color: 'transparent'
          }
        }
      },
      yAxis: {
        axisLabel: {
          color: '#728FAA'
        },
        splitLine: {
          lineStyle: {
            type: 'dashed',
            color: '#6D7988'
          }
        },
        scale: true
      },
      grid: {
        top: '20%',
        left: '3%',
        right: '12%',
        bottom: '3%',
        containLabel: true
      },
      series: echartData.data && this.scatterSeriesDataFormat(echartData.data)
    }
  }

  graphOptionsFormat(echartData: any): EChartsFullOption {
    const { links, nodes, categories } = this.graphDataFormat(echartData)
    return {
      animationDurationUpdate: 1500,
      animationEasingUpdate: 'quinticInOut',
      series: [
        {
          name: 'Les Miserables',
          type: 'graph',
          layout: 'circular',
          circular: {
            rotateLabel: true
          },
          data: nodes,
          links: links,
          categories: categories,
          roam: true,
          label: {
            position: 'right'
          },
          lineStyle: {
            color: 'source',
            curveness: 0.3
          }
        }
      ]
    }
  }

  graphDataFormat(echartData: any): { links: any[], nodes: any[], categories: ICategories[] } {
    const nodes = echartData.gexf.graph[0].nodes[0].node
    const categories = []
    for (let i = 0; i < 9; i++) {
      categories[i] = {
        name: '类目' + i
      }
    }
    return {
      links: this.grahLinksDataFormat(echartData.gexf.graph[0].edges[0].edge),
      nodes: this.grahNodeDataFormat(nodes),
      categories
    }
  }

  grahNodeDataFormat(nodes: any): INodes[] {
    let newNodes: any[] = []
    nodes.forEach((c:any, i: number) => {
      newNodes = [...newNodes, {        attributes: { [c.attvalues[0].attvalue[0].$.for]: Number(c.attvalues[0].attvalue[0].$.value) },
        id: c.$.id,
        name: c.$.label,
        category: Number(c.attvalues[0].attvalue[0].$.value),
        itemStyle: null,
        symbolSize: Number(c['viz:size'][0].$.value) / 3,
        label: { normal: { show: (Number(c['viz:size'][0].$.value) / 3) > 8, textStyle: {
          color: '#5FEBF2',
          fontWeight: 700
        }}},
        value: Number(c['viz:size'][0].$.value),
        x: Number(c['viz:position'][0].$.x),
        y: Number(c['viz:position'][0].$.y)
      }]
    })

    return newNodes
  }

  grahLinksDataFormat(links: any): ILinks[] {
    let newLinks: any[] = []
    links.forEach((c:any) => {
      newLinks = [...newLinks, {        ...c.$,        lineStyle: { normal: {}},        name: ''      }]
    })
    return newLinks
  }
}

export const InitEchartContext = React.createContext<InterInitEchartContxt>(null as any)

4. 项目Layout

<div className='App'>
        <FullScreenContainer style={{ background: ' radial-gradient(ellipse closest-side, #125886, #000e25)' }}>
          { isShowLoading ? <Loading>{ msg }</Loading> : null }
          {
            this.props.user.token && (
              <HeaderTop
                headerTitle='大屏公用模板'
                currentTime={ new Date().getTime() }
                userInfo={this.props.user.userInfo}
                logout={() => this.props.logout()}
              />
            )
          }
          <RouterLink />
        </FullScreenContainer>
      </div>


项目配置以及架构就是以上,具体详情还是要 clone下项目 细细观看,项目的小星星需要各位客官点亮哦,谢谢!,如需转载,文章,项目属于原创,请根本作者联系