需要掌握知识:
生成项目那些就不多说了,直接步入正题!
项目目录
├── 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 文件,写入如下配置
在子项目目录下新建 config-overrides.js config-overrides-proxy.js 文件,写入如下配置
1.config-overrides.js
2.config-overrides-proxy.js
在主项目的APP.tsx配置如下:
按照以上配置就可以跑通 qiankun 啦
axios拦截器篇
2. 安装与配置axios
npm i axios
在utils目录下新建http目录,并且分别创建model.ts接口文件以及request.ts配置文件 1.request.ts
主要的响应拦截和请求拦截
请求封装
最后将request抛出就行了
2.model.ts 这是请求封装的接口
现在我们来看看怎么去使用
封装一个请求函数,他的返回值是Promise,并且定义泛型去控制返回值的类型提示
这样我们就可以给调用者传入一个类型,到时候直接可以提示返回类型
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
配置如下:
- index.ts
- activeTypes.ts active 常量
3.reduces 基本配置
步入正题:代码篇
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下项目 细细观看,项目的小星星需要各位客官点亮哦,谢谢!,如需转载,文章,项目属于原创,请根本作者联系