前言
建议先了解以下基础
- ES6语法
- React基础
console代码结构
怎么尽快的上手
作为一个开源前端项目,代码量很大的情况下,除了借助官方文档了解代码结构,还可以从浏览界面入手先理顺一条完整的调用链,这样其他的照葫芦画瓢就会比较容易
一个例子
举一个具体的简单例子,比如首页的蜘蛛图(集群资源使用情况)数据获取,根据一个具体的比较好理解console用到的组件、路由、请求封装
对于我刚开始接触concole的时候,最困惑的事情是三样
- 不知道怎样在代码里找到它
- 不知道搜索出来的一堆里面哪一个是需要的
- 不知道这个数据从哪里来的
所以我们从最基础的捞代码开始,打开浏览器,在选择目标附近的文本进行搜索
结合浏览器检查元素,可知搜索的文本是一个pannel
所以确定是这个不是上面两个,最上面四个js是国际化
进入文件,查看render里面,是那个组件渲染了数据
找到getResourceOptions函数
getResourceOptions = () => {
const data = getLastMonitoringData(this.metrics)
return [
{
name: 'CPU',
unitType: 'cpu',
used: this.getValue(data[MetricTypes.cpu_usage]),
total: this.getValue(data[MetricTypes.cpu_total]),
},
{
name: 'Memory',
unitType: 'memory',
used: this.getValue(data[MetricTypes.memory_usage]),
total: this.getValue(data[MetricTypes.memory_total]),
},
{
name: 'Pod',
unitType: '',
used: this.getValue(data[MetricTypes.pod_count]),
total: this.getValue(data[MetricTypes.pod_capacity]),
},
{
name: 'Local Storage',
unitType: 'disk',
used: this.getValue(data[MetricTypes.disk_size_usage]),
total: this.getValue(data[MetricTypes.disk_size_capacity]),
},
]
}
可知getResourceOptions的数据来源于getLastMonitoringData(this.metrics),这里解释一下为什么集群资源首页的数据会名字与Monitoring相关,实际上是因为这里用的就是监控的数据,互通的。
export const getLastMonitoringData = data => {
const result = {}
Object.entries(data).forEach(([key, value]) => {
const values = get(value, 'data.result[0].values', []) || []
const _value = isEmpty(values)
? get(value, 'data.result[0].value', []) || []
: last(values)
set(result, `[${key}].value`, _value)
})
return result
}
由此可知,getLastMonitoringData只是做了一下数据转换处理,并不是真正的数据来源,那么数据来源就是被getLastMonitoringData处理的this.metrics,回到数据渲染文件,
monitorStore = new ClusterMonitorStore({ cluster: this.props.cluster })
componentDidMount() {
this.fetchData()
}
get metrics() {
return this.monitorStore.data
}
可知this.metrics来源于ClusterMonitorStore,此处也可以进行本地调试打印辅助确认
依照路径找到ClusterMonitorStore类(react项目,引用路径默认是src,所以有很多stores也不要疑惑,指的就是src下的那个)
import { action, observable } from 'mobx'
import { get } from 'lodash'
import { to } from 'utils'
import Base from './base'
export default class ClusterMonitoring extends Base {
@observable
statistics = {
data: {},
isLoading: false,
}
@observable
resourceMetrics = {
originData: {},
data: [],
isLoading: false,
}
@action
async fetchStatistics() {
this.statistics.isLoading = true
const params = {
type: 'statistics',
}
const result = await to(request.get(this.getApi(), params))
const data = this.getResult(result)
this.statistics = {
data,
isLoading: false,
}
return data
}
@action
async fetchApplicationResourceMetrics({
workspace,
namespace,
autoRefresh = false,
...filters
}) {
if (autoRefresh) {
filters.last = true
this.resourceMetrics.isRefreshing = true
} else {
this.resourceMetrics.isLoading = true
}
if (filters.cluster) {
this.cluster = filters.cluster
}
const params = this.getParams(filters)
// set correct path
const paramsReg = /^[a-zA-Z]+_/g
const metricType = get(filters.metrics, '[0]', '').replace(
paramsReg,
'cluster_'
)
let path = 'cluster'
if (workspace) {
path = `workspaces/${workspace}`
params.metrics_filter = `${metricType.replace(paramsReg, 'workspace_')}$`
}
if (namespace && namespace !== 'all') {
path = `namespaces/${namespace}`
params.metrics_filter = `${metricType.replace(paramsReg, 'namespace_')}$`
}
const result = await to(request.get(`${this.apiVersion}/${path}`, params))
let data = this.getResult(result)
if (autoRefresh) {
data = this.getRefreshResult(data, this.resourceMetrics.originData)
}
this.resourceMetrics = {
originData: data,
data: get(Object.values(data), '[0].data.result') || [],
isLoading: false,
isRefreshing: false,
}
return data
}
fetchClusterDevopsCount = async () => {
const result = await request.get(
'kapis/tenant.kubesphere.io/v1alpha2/devopscount/'
)
return get(result, 'count', 0)
}
}
有意思的事情来了,遍寻ClusterMonitoring类也找不到数据来源,经过一番调试,发现ClusterMonitoring只是一个桥,数据来源在他继承的Base类
@action
async fetchMetrics({
autoRefresh = false,
more = false,
fillZero = true,
...filters
}) {
if (autoRefresh) {
filters.last = true
this.isRefreshing = true
} else {
this.isLoading = true
}
if (filters.cluster) {
this.cluster = filters.cluster
}
const params = this.getParams(filters)
const api = this.getApi(filters)
const response = await to(request.get(api, params))
let result = this.getResult(response)
if (autoRefresh) {
result = this.getRefreshResult(result, this.data)
}
if (more) {
result = this.getMoreResult(result, this.data)
}
this.data = fillZero ? fillEmptyMetrics(params, result) : result
this.isLoading = false
this.isRefreshing = false
return result
}
打印const response = await to(request.get(api, params))中的params以及api,对照浏览器的请求头及参数
去掉编码后的字符以及分页限制等等暂时和主线不相关的,这是完整的数据化获取请求头
拆解开
1.请求地址: /kapis/monitoring.kubesphere.io/v1alpha3/cluster?
2.请求参数: metrics_filter=cluster_cpu_usage|cluster_cpu_total|cluster_memory_usage_wo_cache|cluster_memory_total|cluster_disk_size_usage|cluster_disk_size_capacity|cluster_pod_running_count|cluster_pod_quota$
以上,就是一个简短的数据获取,以此类推,其他的也只是调用链长一点,顺序大致如此,数据怎么来的一般就可以这么找,下面我们来看具体的组件化
console的组件
以上面的来讲
对应界面
然后我们来看ResourceItem的实现,对于react来说、函数、类、html标签等等都是组件,都可以以<xxx/>
的形式呈现,然后经render渲染到界面
import React from 'react'
import { Text } from 'components/Base'
import { PieChart } from 'components/Charts'
import { getSuitableUnit, getValueByUnit } from 'utils/monitoring'
import styles from './image/index.scss'
export default function ResourceItem(props) {
const title = t(props.name)
const unit = getSuitableUnit(props.total, props.unitType) || unit
const used = getValueByUnit(props.used, unit)
const total = getValueByUnit(props.total, unit) || used
return (
<div className={styles.item}>
<div className={styles.pie}>
//这是左边那个小环形图
<PieChart
width={48}
height={48}
data={[
{
name: 'Used',
itemStyle: {
fill: '#329dce',
},
value: used,
},
{
name: 'Left',
itemStyle: {
fill: '#c7deef',
},
value: total - used,
},
]}
/>
</div>
//文本
<Text
title={`${Math.round((used * 100) / total)}%`}
description={title}
/>
<Text title={unit ? `${used} ${unit}` : used} description={t('Used')} />
<Text
title={unit ? `${total} ${unit}` : total}
description={t('Total')}
/>
</div>
)
}
<PieChart>
的实现
import React from 'react'
import PropTypes from 'prop-types'
import { PieChart, Pie, Cell } from 'recharts'
export default class Chart extends React.Component {
static propTypes = {
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}
static defaultProps = {
width: 100,
height: 100,
dataKey: 'value',
}
render() {
const { width, height, data, dataKey } = this.props
return (
<PieChart width={width} height={height}>
<Pie
data={data}
dataKey={dataKey}
innerRadius="60%"
outerRadius="100%"
animationDuration={1000}
>
{data.map(entry => (
<Cell
key={`cell-${entry.name}`}
{...entry.itemStyle}
strokeWidth={0}
/>
))}
</Pie>
</PieChart>
)
}
}
根据调用处导入的Text可知,用的是最顶层的base下的Text组件,<Text>
的实现
import React from 'react'
import { Icon } from '@kube-design/components'
import { isUndefined } from 'lodash'
import classNames from 'classnames'
import styles from './image/index.scss'
export default class Text extends React.PureComponent {
render() {
const {
icon,
title,
description,
className,
ellipsis,
extra,
onClick,
} = this.props
return (
<div
className={classNames(
styles.wrapper,
{ [styles.clickable]: !!onClick, [styles.ellipsis]: ellipsis },
className
)}
onClick={onClick}
>
{icon && <Icon className={styles.icon} name={icon} size={40} />}
<div className={styles.text}>
<div>{isUndefined(title) || title === '' ? '-' : title}</div>
<p>{description}</p>
</div>
{extra}
</div>
)
}
}
对于一些复杂的组件,可能里面还会嵌套其他组件,但是原理都是一样的,根据导入路径一级一级找就行了,如果我们对于UI有修改的需求,比如要另一个样子的<Text>
,那么可以复写一个<Text>
组件,引用新写的<Text>
组件
未完待续……
console的请求封装
就上面讲到的,数据真正来自于metrics
fetchData = () => {
this.monitorStore.fetchMetrics({
metrics: Object.values(MetricTypes),
last: true,
})
}
fetchMetrics函数cluster.js同级的base.js,我们去详细看一下这个base文件
@action
async fetchMetrics({
autoRefresh = false,
more = false,
fillZero = true,
...filters
}) {
if (autoRefresh) {
filters.last = true
this.isRefreshing = true
} else {
this.isLoading = true
}
if (filters.cluster) {
this.cluster = filters.cluster
}
//获取参数列表
const params = this.getParams(filters)
//获取请求头的API
const api = this.getApi(filters)
//发起请求获取response
const response = await to(request.get(api, params))
let result = this.getResult(response)
if (autoRefresh) {
result = this.getRefreshResult(result, this.data)
}
if (more) {
result = this.getMoreResult(result, this.data)
}
this.data = fillZero ? fillEmptyMetrics(params, result) : result
this.isLoading = false
this.isRefreshing = false
return result
}
我们在浏览器打印一下看看
console直接用的就是nodejs的request.get
请求,同时支持http和https,这个例子里面就是缺省回调的写法,完整的长这样
request.get(url, (error, response, body) => {
//需要xxx回调执行的代码放这里
});
我们再看一眼请求头
kapis/monitoring.kubesphere.io/v1alpha3/cluster
其中monitoring.kubesphere.io
是代表这个请求组属于monitoring,所以如果这个请求出现了服务器错误,就应该去看监控的pod是不是出问题了
对照官方的api文档可以看到还有很多这样的apiGroup
//集群相关的cluster.kubesphere.io
/kapis/cluster.kubesphere.io/v1alpha1/clusters/{cluster}/agent/deployment
//devops相关的devops.kubesphere.io
/kapis/devops.kubesphere.io/v1alpha2/crumbissuer
//资源相关的resources.kubesphere.io
/kapis/resources.kubesphere.io/v1alpha3/{resources}
普通用法一般就这样,咱们再看一个比较特别的
import { withProjectList, ListPage } from 'components/HOCs/withList'
import Table from 'components/Tables/List'
import OpAppStore from 'stores/openpitrix/application'
@withProjectList({
store: new OpAppStore(),
module: 'applications',
name: 'Application',
})
export default class OPApps extends React.Component {
// 多余的代码省略……
getColumns = () => {
const { getSortOrder } = this.props
//getColumns 的数据来自于 record,表面上并没有任何地方传入了 record (OPApps已经是根组件没有调用他的组件了)
//record由浏览器打印可以看到是一条完整的table数据
return [
{
title: t('Name'),
dataIndex: 'name',
render: (name, record) => (
<Avatar
isApp
to={`${this.prefix}/${record.cluster_id}`}
avatar={get(record, 'app.icon')}
iconLetter={name}
iconSize={40}
title={name}
desc={record.description}
/>
),
},
{
title: t('Status'),
dataIndex: 'status',
isHideable: true,
width: '16%',
render: (status, record) => (
<Status
name={t(record.transition_status || status)}
type={record.transition_status || status}
/>
),
},
{
title: t('Application'),
dataIndex: 'app.name',
isHideable: true,
width: '16%',
render: (name, record) => (
<Link to={`/apps/${get(record, 'version.app_id')}`}>{name}</Link>
),
},
{
title: t('Version'),
dataIndex: 'version.name',
isHideable: true,
width: '16%',
},
{
title: t('Last Updated Time'),
dataIndex: 'status_time',
sorter: true,
sortOrder: getSortOrder('status_time'),
isHideable: true,
width: 180,
render: (time, record) =>
getLocalTime(record.update_time || record.status_time).format(
'YYYY-MM-DD HH:mm:ss'
),
},
]
}
render() {
const { bannerProps, tableProps, match } = this.props
// table的Columns数据来自于getColumns
return (
<ListPage {...this.props}>
<Banner {...bannerProps} match={match} type={this.type} />
<Table
//真正的数据传入是tableProps
{...tableProps}
{...this.getTableProps()}
itemActions={this.itemActions}
columns={this.getColumns()}
/>
</ListPage>
)
}
}
record打印
实际上这里的参数只是起到占位及展示的作用,叫什么都可以,即使改变名字也是获取到同样的数据,因为实际向Table
传递参数的是tableProps
,
this.getColumns()
的结果也作为columns
参数传递过去,为了便于理解拆解开的,可以人为拆解开为三个回合,实际上反映到执行过程中只是参数的变化过程
第一回合(即render之前):参数是tableProps
以及this.getColumns()
,此时this.getColumns()
中有效参数是title
和dataIndex
,此时render
是一个正在准备的回调方法
{
title: t('Name'),
dataIndex: 'name',
render: (name, record) => (
<Avatar
isApp
to={`${this.prefix}/${record.cluster_id}`}
avatar={get(record, 'app.icon')}
iconLetter={name}
iconSize={40}
title={name}
desc={record.description}
/>
),
},
第二回合(render):参数是tableProps
以及this.getColumns()
此时this.getColumns()
中有效参数是title
和dataIndex
和render
,此时render
是根据dataIndex识别出的要装载那些参数的已经完成回调的一个对象
第三回合(render结束):this.getColumns()
装载内部渲染完成,以columns
形式作为Table
的一个参数进行Table
渲染(此时render
已经是dom
元素了)
console的路由
继续回归到我们这个简单的例子,路由我们也以此为例先讲普遍简单的
路由的找法不推荐由界面==>代码,因为有多级路由逆向并不方便,还是老老实实从项目根路由找起,一般react项目的根路由都在src下名字与route有关(这里仅仅指单页面应用,多页面应用路由后面讲)
由此可以找到/src/core/routes.js
import { lazy } from 'react'
//懒加载代码省略……
export default [
{
component: BaseLayout,
routes: [
{
path: '/clusters',
component: Clusters,
},
{
path: '/access',
component: AccessControl,
},
{
path: '/:workspace/clusters/:cluster/projects/:namespace',
component: Projects,
},
{
path: '/:workspace/clusters/:cluster/devops/:devops',
component: DevOps,
},
{
path: '/:workspace/federatedprojects/:namespace',
component: FederatedProjects,
},
{
path: '/workspaces/:workspace',
component: Workspaces,
},
{
path: '/apps',
component: AppStore,
},
{
path: '/apps-manage',
component: ManageApp,
},
{
path: '/settings',
component: Settings,
},
{
path: '*',
component: Console,
},
],
},
]
怎么确定是不是根路由?
- 1.看路由的目录层级
- 2.与开头介绍的项目代码功能结构进行对比
然后根据浏览器的访问路径/clusters/default/overview
可知,我们的这个例子是第一个clusters
的路由,所以如果要添加一个与集群管理同级页面路由在那里添就不言而喻了吧
然后直接去找overview
文件夹,我们本例的代码也确实在overview下(这里就和上面看到的代码无缝对接了,经过一串组件的封装之后渲染到了界面)
ps:这里解释一下default
,这是集群的名字叫default, kubesphere 3.0是支持多集群的,一级路由是clusters
要进入二级路由当然需要指定是那个集群,选择你要进入哪一个集群的概览
组件封装调用链
// 红框1
export default class Overview extends React.Component {
get cluster() {
return this.props.clusterStore
}
render() {
const { isReady } = this.cluster.detail
if (!isReady) {
return <Initializing store={this.cluster} />
}
//加载<Dashboard/>
return <Dashboard match={this.props.match} />
}
}
//黄框2
<div>
<NewClusterTitle
className="margin-b12"
cluster={detail}
size="large"
noStatus
/>
<Columns>
<Column>
{globals.app.isMultiCluster && (
<ClusterInfo cluster={detail} version={this.cluster.version} />
)}
<ServiceComponents cluster={match.params.cluster} />
//加载<ResourcesUsage/>
<ResourcesUsage cluster={match.params.cluster} />
{globals.app.isPlatformAdmin && (
<Tools cluster={match.params.cluster} />
)}
</Column>
<Column className="is-narrow is-4">
<KubernetesStatus cluster={match.params.cluster} />
<ClusterNodes cluster={match.params.cluster} />
</Column>
</Columns>
</div>
//篮筐3
<Panel title={t('Cluster Resources Usage')}>
<Loading spinning={this.monitorStore.isLoading}>
<div className={styles.wrapper}>
<div className={styles.chart}>
//加载资源使用情况,具体渲染在RadarChart组件中,上面讲过了
<RadarChart
cx={180}
cy={158}
width={360}
height={316}
data={radarOptions}
>
<PolarGrid gridType="circle" />
<PolarAngleAxis dataKey="name" />
<PolarRadiusAxis domain={[0, 100]} />
<Radar dataKey="usage" stroke="#345681" fill="#1c2d4267" />
</RadarChart>
</div>
<div className={styles.list}>
{options.map(option => (
<ResourceItem key={option.name} {...option} />
))}
</div>
</div>
</Loading>
</Panel>
最简单的路由就这样就完了,再说一个复杂一点的多页面应用的路由
先解释一下为什么需要多页面应用,因为作为单页面应用所有的页面都受同一个跟路由管控,当我们需要不受跟路由管控的页面时,就需要多页面应用
比如登录,和帮助手册
多页面应用是需要在webpack下配置的,通常在webpack.config.js中,但是一搜会找到很多webpack.config.js文件,分不出来是哪一个?没关系我们可以去抄登录,这是现成的多页面引用的例子
于是我们找到登录的注册路由server/routes.js
const Router = require('koa-router')
//多余代码省略……
const {
handleLogin,
handleLogout,
handleOAuthLogin,
} = require('./controllers/session')
const {
renderView,
renderLogin,
renderDocument,//渲染同级目录/controllers/view下的document
renderMarkdown,
renderCaptcha,
} = require('./controllers/view')
const parseBody = convert(
bodyParser({
formLimit: '200kb',
jsonLimit: '200kb',
bufferLimit: '4mb',
})
)
const router = new Router()
router
//多余代码省略……
// session
.post('/login', parseBody, handleLogin)
.post('/logout', handleLogout)
.get('/login', renderLogin)
.get('/oauth/redirect', handleOAuthLogin)
// markdown template
.get('/blank_md', renderMarkdown)
// 注册一个document
.get('/document', renderDocument)
// page entry
.all('*', renderView)
module.exports = router
document是一个简单的静态页面,但他的路由级别与整个console内部是同级的互不干扰的,也就是说我们也可以基于它构建和console一样复杂的页面
结束!