kubesphere console 二次开发源码阅读

前言

建议先了解以下基础

  • 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()中有效参数是titledataIndex,此时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()中有效参数是titledataIndexrender,此时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一样复杂的页面

结束!

kubesphere console是一个基于Kubernetes的开源项目,提供了一个用户友好的Web界面,用于管理和监控Kubernetes集群。关于kubesphere console源码解析,有一些重要的点需要注意。 首先,在分析kubesphere console源码之前,我们需要切换到分析的v1.18.5分支。这可以通过以下命令实现:$ kubernetes [master] git checkout v1.18.5 。这样我们就可以在正确的代码版本上进行源码解析。 其次,kubesphere console采用了声明式API的设计模式,将复杂的逻辑放到controller中进行处理,实现了代码的解耦。这样的设计使得kubesphere console能够方便地与其他系统和服务进行集成。举例来说,可以通过以下API路径进行集成操作: /apis/devops.kubesphere.io/v1alpha2/namespaces/{namespace}/pipelines /apis/devops.kubesphere.io/v1alpha2/namespaces/{namespace}/credentials /apis/openpitrix.io/v1alpha2/namespaces/{namespace}/applications /apis/notification.kubesphere.io/v1alpha2/configs 最后,为了方便测试,我们可以基于v1.18.5分支创建一个名为dev的新分支。这可以通过以下命令实现:$ kubernetes [e6503f8d8f7] git checkout -b dev 。这样我们就可以在新分支上进行测试和调试工作。 综上所述,kubesphere console源码解析涉及到切换到正确的分支、了解声明式API的设计和集成方式,以及创建一个方便测试的新分支。这些操作将帮助我们深入理解和分析kubesphere console的源代码。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Kubernetes二次开发源码分析(环境准备)](https://blog.csdn.net/lcynone/article/details/128681707)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [开源代码:KubeSphere 核心架构解析](https://blog.csdn.net/weixin_50071922/article/details/120150670)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值