1 KubeSphere console功能导图
模块:
-
命令行工具 (kubectl)
-
日志(Logging)
-
平台设置(Platform Settings)
-
服务组件(Service Components)
-
监控和警报(Monitoring & Alerting)
-
基础架构(Infrastructure)
-
集群角色(Cluster Roles)
-
账户(Accounts)
-
应用商店(OpenPitrix App)
-
工作区(Workspaces)
-
项目(Projects)
-
(开发运维)Devops
2 KubeSphere console整体结构
2.1 概述
ks-console主要是作为用户和kubereshpere交互的入口,主要为用户提供页面的交互方式,以及少量API接口。 如图所示,ks-console第一层级主要包含会话管理,其他API,页面。
-
会话管理主要是登录授权后维持用户token等的权限cache
-
其他API主要是直接提供了部分和dockerhub或者下载的部分API
-
页面主要提供用户的交互入口
从页面功能来看,又分为管理页面和终端页面,终端页面是提供在页面上使用命令行直接与kubernetes的交互界面,管理页面则是集成化的对整个kubesphere的管理
管理业面又主要分为集群管理,权限管理和系统设置
-
集群管理 管理集群的整体资源
-
权限管理 管理用户或用户组的权限
-
系统设置 对系统邮箱,webhook(消息通知)等全局配置进行管理
通知配置页面
2.2 整体结构分析
-
路由层是整个前端系统的入口,主要使用koa(Node.js的服务端开发框架)提供了最外层的服务框架,其中嵌入了配置管理config和部分交互数据压缩等的中间件工具utils。
-
会话层主要是提供了用户的登录,以及登录后的session数据维持管理; 主要提供页面的访问入口。此外还有dockerhub等API接口。
-
路由分发层则从业面上做了功能分发,提供管理页面(kubesphere pages)以及终端页面(terminal)两个访问入口。
-
页面逻辑层中才是管理页面的真正实现,使用react框架,完成了页面的支持。
-
管理页面或者终端页面都将最终在后台API层通过ks-apiserver 与后台交互。
-
路由层 如下所示,可以看到在路由层中,根据访问路径对业务进行分发,包括基本工具API,登录管理API,终端页面,和最后的用户管理页面。
router
.use(proxy('/devops_webhook/(.*)', devopsWebhookProxy))
.use(proxy('/b2i_download/(.*)', b2iFileProxy))
.post('/dockerhub/(.*)', parseBody, handleDockerhubProxy)
.post('/harbor/(.*)', parseBody, handleHarborProxy)
.get('/blank_md', renderMarkdown)
.all('/(k)?api(s)?/(.*)', checkToken, checkIfExist)
.use(proxy('/(k)?api(s)?/(.*)', k8sResourceProxy))
.get('/sample/:app', parseBody, handleSampleData)
// session
.post('/login', parseBody, handleLogin)
.get('/login', renderLogin)
.post('/login/confirm', parseBody, handleLoginConfirm)
.get('/login/confirm', renderLoginConfirm)
.post('/logout', handleLogout)
// oauth
.get('/oauth/redirect/:name', handleOAuthLogin)
// terminal
.get('/terminal*', renderTerminal)
// page entry
.all('*', renderView)
module.exports = router
-
路由分发层 注意前面最后路由分发的 renderTerminal 和 renderView其实现如下,该层是根据路由的路径不同,去查询对应打包文件中的页面入口,从而真正让用户进入终端页面和管理业面。
const renderTerminal = async ctx => {
try {
const manifest = getManifest('terminalEntry')
const [user, ksConfig, runtime] = await Promise.all([
getCurrentUser(ctx),
getKSConfig(),
getK8sRuntime(ctx),
])
const localeManifest = getLocaleManifest()
await ctx.render('terminal', {
manifest,
isDev: global.MODE_DEV,
title: clientConfig.title,
hostname: ctx.hostname,
globals: JSON.stringify({
localeManifest,
user,
ksConfig,
runtime,
}),
})
} catch (err) {
renderViewErr(ctx, err)
}
}
const renderView = async ctx => {
try {
const clusterRole = await getClusterRole(ctx)
const [user, ksConfig, runtime, supportGpuType] = await Promise.all([
getCurrentUser(ctx, clusterRole),
getKSConfig(),
getK8sRuntime(ctx),
getSupportGpuList(ctx),
])
await renderIndex(ctx, {
ksConfig,
user,
runtime,
clusterRole,
config: { ...clientConfig, supportGpuType },
})
} catch (err) {
renderViewErr(ctx, err)
}
}
-
页面逻辑层 因为终端页面直接使用的第三方库,因此基本没有开发逻辑,而管理页面则是使用react实现后打包完成.
2.3 项目的目录结构
2.3.1 build
里面只有一个Dockerfile 。在Linux环境下打包
2.3.2 cypress和jest 都是测试框架。可以模拟用户与KubeSphere Console进行交互
(1)Cypress是一个用于进行端到端测试的JavaScript测试框架,它允许开发人员编写和运行自动化测试来模拟用户与应用程序的交互。
在cypress
目录下,通常包含以下文件和子目录:
-
integration
目录:该目录用于存放Cypress的集成测试文件。集成测试是指测试应用程序的不同组件之间的交互和协调是否正常。 -
plugins
目录:该目录包含Cypress的插件文件。插件文件允许您在运行测试时对Cypress进行自定义配置或执行其他操作。 -
support
目录:该目录用于存放支持测试的辅助文件。这些文件包括自定义命令、工具函数或测试配置。 -
fixtures
目录:该目录用于存放测试所需的静态资源或测试数据。例如,您可以将一些模拟的JSON文件或图像文件放在这个目录下,供测试使用。 -
screenshots
目录:该目录用于存放测试运行过程中自动生成的截图。这些截图可用于调试和分析测试失败的原因。 -
videos
目录:该目录用于存放测试运行过程中生成的视频录制。这些视频可用于回放测试的执行过程。
通过使用Cypress框架和编写测试脚本,可以模拟用户与KubeSphere Console进行交互,并验证应用程序的行为和功能是否符合预期。cypress
目录提供了组织和管理这些测试文件所需的结构和资源。
(2)Jest是一个流行的JavaScript测试框架,主要用于编写单元测试和集成测试。
在jest
目录下,通常包含以下文件和子目录:
-
setup.js
文件:该文件包含在执行测试之前需要进行的全局设置和配置。您可以在此文件中编写代码来配置测试环境、导入所需的测试库或设置全局变量。 -
test-utils.js
文件:该文件通常包含一些测试工具函数,用于辅助编写测试代码。这些工具函数可以包括模拟数据、创建测试环境或执行常见的测试操作。 -
__mocks__
目录:该目录用于存放模拟(mock)文件或模块。模拟文件可以用于模拟外部依赖或模块,以便在测试中进行替代或模拟。 -
__tests__
目录:该目录用于存放Jest测试文件。在这个目录中,您可以编写单元测试或集成测试,以验证KubeSphere Console中的各个功能和模块的行为是否符合预期。
通过使用Jest测试框架和编写测试脚本,可以对KubeSphere Console的不同部分进行测试,包括组件、函数、API等。jest
目录提供了组织和管理这些测试文件所需的结构和资源,并允许您进行测试配置和定制。
2.3.3 hack
通常用于存放一些用于辅助开发、构建和部署的脚本、配置和工具。
具体而言,hack
目录的作用可以包括以下内容:
-
构建和部署脚本:该目录可能包含一些用于自动化构建和部署KubeSphere Console的脚本。这些脚本可以包括构建Docker镜像、部署到Kubernetes集群或其他云平台的脚本等。
-
代码生成器和模板:一些项目可能会使用代码生成器或模板引擎来自动生成一些代码文件或模板文件。这些文件可以位于
hack
目录中,并用于生成特定的代码结构或文件。 -
工具脚本:
hack
目录可能包含一些用于辅助开发和调试的工具脚本。这些脚本可以执行一些特定的任务,例如数据转换、格式化代码、静态分析、检查依赖等。 -
环境配置和样例文件:
hack
目录可能包含一些用于配置开发环境或提供样例配置文件的文件。这些文件可以包括本地开发环境的配置示例、测试环境的配置文件模板等。
总体而言,hack
目录是一个用于存放各种辅助开发、构建和部署的脚本、配置和工具的目录。它提供了一个组织和管理这些辅助文件的位置,使得开发者能够更方便地进行开发、构建和部署相关的操作。
2.3.4 locales
存储语言文件,国际化
2.3.5 scripts
存储语言文件,国际化
2.3.6 server
用于存放与服务器端相关的代码和配置文件。
2.3.7 src
2.3.7.1 actions
通常存放着与应用程序的动作(Actions)相关的代码。在前端应用中,动作是指触发状态变化或触发其他操作的行为。Actions可以被组件、用户交互或其他触发机制调用,它们描述了应用程序中发生的事件或操作。
2.7.3.2 assets
存放一些静态资源,如图片
2.7.3.3 components
存放封装的通用组件
2.7.3.4 configs
配置管理。通过集中定义规则配置项,可以方便地管理和维护规则的相关信息,并在需要时进行扩展和修改。
2.7.3.5 core
核心组件的二次封装,整体入口
2.7.3.6 pages
页面封装
1)access 访问控制页面
2) app 应用商店
3) clusters 集群管理
4) console 工作台
5)devops
6)fedprojects 多集群
7)Projects 项目
8)settings 平台设置
9)terminal 终端页面
10)workspaces 工作区
2.7.3.7 scss
样式文件
2.7.3.8 stores
页面的数据管理
2.7.3.9 utils
一些工具函数
3 管理页面整体结构分析
-
首先index是整个页面的入口。
-
index中包含的route则是路由的入口。
-
路由注册了两种页面,一种是导航页面view1, 一种是逻辑页面view2。 逻辑页面会交互完成集群查询管理,节点管理等具体逻辑功能。而导航页面则只负责展示导航列并提供点击做页面跳转。
-
导航页面支持动态呈现,其通过global组件从config里面获取页面元素和布局,动态展现支持的资源提供跳转链接。
-
逻辑页面则是导航页面跳转的。
-
逻辑页面通过controller调用action中的模块和后台交互,管理获取后台的实际资源。
-
而store则是在前端存取的后台资源的cache。
-
view展示数据时对应的后台资源则从store获取。
4 React核心概念
4.1 生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
React组件也有自己的生命周期,生命周期可以让我们在最合适的地方完成自己想要的功能
生命周期和生命周期函数的关系:
生命周期是一个 抽象的概念 ,在生命周期的整个过程,分成了很多个阶段
比如装载阶段(Mount ),组件第一次在 DOM 树中被渲染的过程;
比如更新过程(Update ),组件状态发生变化,重新更新渲染的过程
比如卸载过程(Unmount ),组件从 DOM 树中被移除的过程;
React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的 某些函数进行回调 ,这些函数就是 生命周期函数
比如实现componentDidMount 函数:组件已经挂载到 DOM 上时,就会回调;
比如实现componentDidUpdate 函数:组件已经发生了更新时,就会回调;
比如实现componentWillUnmount 函数:组件即将被移除时,就会回调;
我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
我们谈React 生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的
最基础、最常用的生命周期函数
4.1.1 生命周期函数
Constructor
如果不初始化state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
constructor中通常只做两件事情:
(1)通过给this.state 赋值对象来初始化内部的 state
(2)为事件绑定实例(this)
componentDidMount
componentDidMount()会在组件挂载后(插入 DOM 树中)立即调用。
componentDidMount中通常进行哪里操作呢?
(1)依赖于DOM 的操作可以在这里进行;
(2)在此处发送网络请求就最好的地方;(官方建议)
(3)可以在此处添加一些订阅(会在componentWillUnmount 取消订阅);
componentDidUpdate
componentDidUpdate()会在更新后会被立即调用,首次渲染不会执行此方法。
当组件更新后,可以在此处对DOM 进行操作;
如果你对更新前后的props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
componentWillUnmount
componentWillUnmount()会在组件卸载及销毁之前直接调用。
(1)在此方法中执行必要的清理操作;
(2)例如,清除timer ,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
4.1.2 不常用生命周期函数
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
getDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象
来更新 state
getSnapshotBeforeUpdate:在 React 更新 DOM之前回调的一个函数,可以获取 DOM 更新前的一
些信息(比如说滚动位置);
shouldComponentUpdate:该生命周期函数很常用(很多时候,我们简称为 SCU ),作为性能优化的一种方式。这个方法接受参数,并且需要有返回值:
该方法有两个参数:
参数一:
nextProps 修改之后,最新的 props 属性
参数二:
nextState 修改之后,最新的 state 属性
该方法返回值是一个boolean 类型:
(1)返回值为true ,那么就需要调用 render 方法;
(2)返回值为false ,那么久不需要调用 render 方法;
(3)默认返回的是true ,也就是只要 state 发生改变,就会调用 render 方法;
4.2 Router路由
react-router 最主要的 API 是给我们提供的一些组件:
BrowserRouter或 HashRouter
(1)Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
(2)BrowserRouter使用 history 模式;
(3)HashRouter使用 hash 模式;
4.2.1 路由映射配置
Routes:包裹所有的 Route ,在其中匹配一个路由
Router5.x使用的是 Switch 组件
Route:Route 用于路径的匹配;
(1)path属性:用于设置匹配到的路径;
(2)element属性:设置匹配到路径后,渲染的组件;
Router5.x使用的是 component 属性
(3)exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
Router6.x不再支持该属性
export default [
{ path: '/404', component: NotFound, exact: true },
{ path: '/dashboard', component: Dashboard, exact: true },
{ path: `/logquery`, exact: true, component: LogQuery },
{ path: '/eventsearch', exact: true, component: EventSearch },
{ path: '/auditingsearch', exact: true, component: AuditingSearch },
{ path: '/bill', exact: true, component: Bill },
{
path: '/',
redirect: { from: '/', to: '/dashboard', exact: true },
},
{
path: '*',
redirect: { from: '*', to: '/404', exact: true },
},
]
4.2.2 路由配置和跳转
Link和 NavLink
(1)通常路径的跳转是使用Link 组件,最终会被渲染成 a 元素;
(2)NavLink是在 Link 基础之上增加了一些样式属性;
(3)to属性: Link 中最重要的属性,用于设置跳转到的路径;
import { Link } from 'react-router-dom'
<Link to={`/clusters/${cluster}/components?type=${item.type}`}>
<Icon
name={COMPONENT_ICON_MAP[item.type]}
size={44}
clickable
/>
</Link>
<NavLink
key={name}
className={styles.item}
activeClassName={styles.active}
to={`${match.url}/${name}`}
>
{t(title)}
</NavLink>
默认的activeClassName
事实上在默认匹配成功时,NavLink 就会添加上一个动态的 active class
所以我们也可以直接编写样式
当然,如果你担心这个class 在其他地方被使用了,出现样式的层叠,也可以自定义 class
4.2.3 路由参数传递
传递参数有二种方式:
(1)动态路由的方式;
(2)search传递参数;
动态路由的概念指的是路由中的路径并不会固定:
比如/detail 的 path 对应一个组件 Detail
如果我们将path 在 Route 匹配时写成 /detail/:id ,那么 / abc 、 /detail/123 都可以匹配到该 Route ,并且进行显示
这个匹配规则,我们就称之为动态路由
通常情况下,使用动态路由可以为路由传递参数。
export default [
{
path: `${PATH}/members/:name`,
component: MemberDetail,
},
{
path: `${PATH}/roles/:name`,
component: RoleDetail,
},
{
path: `${PATH}/apps/:appId`,
component: AppDetail,
},
{
path: `${PATH}/repos/:repo_id`,
component: RepoDetail,
},
]
search传递参数
import { Link } from 'react-router-dom'
<Link to={`/clusters/${cluster}/components?type=${item.type}`}>
<Icon
name={COMPONENT_ICON_MAP[item.type]}
size={44}
clickable
/>
</Link>
4.3 状态管理
参见文章的第六部分:KubeSphere console中React的状态管理工具Mobx
5 KubeSphere console中React组件写法
ks-console中组件采用class组件的写法
export default class UploadInput extends React.Component {
static propTypes = {
className: PropTypes.string,
defaultLogo: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
}
static defaultProps = {
className: '',
value: '',
onChange() {},
}
constructor(props) {
super(props)
this.uploaderProps = {
name: 'file',
action: '/images/upload',
accept: 'image/*',
beforeUpload: file => {
if (file.size > 1024 * 1024 * 2) {
Notify.error(t('FILE_OVERSIZED_TIP'))
return false
}
return true
},
onSuccess: res => {
if (res) {
props.onChange(res.path)
}
},
}
}
render() {
const { className, value, placeholder, defaultLogo } = this.props
return (
<Columns className={classNames('is-variable is-2', className)}>
<Column className="is-narrow">
<img
className={classNames(styles.image, 'upload-preview')}
src={value || defaultLogo}
/>
</Column>
<Column>
<Upload {...this.uploaderProps}>
<div className={styles.upload}>
<Icon size={32} name="upload" />
<p>{placeholder}</p>
</div>
</Upload>
</Column>
</Columns>
)
}
}
React中还可以采用函数式组件的写法
const FileUploader = ({ onFileUpload }) => {
const [fileName, setFileName] = useState('');
const [fileContent, setFileContent] = useState('');
const handleUpload = useCallback((files) => {
const reader = new FileReader();
const file = files[0];
reader.onload = (e) => {
const content = e.target.result;
setFileContent(content);
onFileUpload(file.name, content);
};
reader.readAsText(file);
setFileName(file.name);
}, [onFileUpload]);
return (
<ReactFileReader
fileTypes={['.yaml', '.txt']}
handleFiles={handleUpload}
>
<Icon
name="upload"
size={20}
clickable
changeable
/>
</ReactFileReader>
);
};
export default FileUploader;
5.1 React 的 class 组件和函数组件的区别
相同:都可以接收 props 并返回 react 对象
不同:
-
编程思想和内存:类组件需要创建实例面向对象编程,它会保存实例,需要一定的内存开销,而函数组件面向函数式编程,可节约内存
-
可测试性:函数式更利用编写单元测试
-
捕获特性:函数组件具有值捕获特性(只能得到渲染前的值)
-
状态:class 组件定义定义状态,函数式需要使用 useState
-
生命周期:class 组件有完整的生命周期,而函数式组件没有,可以用useEffect 实现类生命周期功能
-
逻辑复用:类组件通过继承或者高阶组件实现逻辑复用,函数组件通过自定义组件实现复用
-
跳过更新:类组件可以通过shouldComponents 和 PureComponents(浅比较,深比较可以用immer) 来跳过更新,函数组件react.memo 跳过更新
-
发展前景:函数组件将成为主流,因为他更好屏蔽this问题,和复用逻辑,更好的适合时间分片和并发渲染
内存开销对比
(1)当使用类组件时,内存开销会具体取决于以下因素:
-
组件数量:每个类组件的实例都会占用内存。因此,如果你有大量的类组件实例,它们的内存开销将随之增加。
-
组件状态:如果类组件具有较大的状态对象,这些状态数据会占用内存。例如,如果你的类组件包含大型的数据结构或缓存,那么这些数据也会增加内存使用量。
-
生命周期方法:每个类组件都具有一组生命周期方法,这些方法本身也会占用一些内存。虽然这一开销通常较小,但仍然需要考虑。
-
事件处理函数:如果你在类组件中定义了多个事件处理函数,它们也会占用内存。这包括事件处理函数的引用以及与它们相关的任何闭包变量。
-
虚拟DOM:React内部使用虚拟DOM来管理组件的渲染和协调。虚拟DOM的数据结构也需要一定的内存。
综合考虑这些因素,类组件的内存开销可能会因项目的规模、组件数量和每个组件的具体实现而异。对于大型项目,特别是在移动设备上,内存开销可能需要更加谨慎地管理,可以考虑使用函数组件或其他性能优化方法来降低内存开销。
(2)函数式组件相对于类组件通常具有更低的内存开销,这是因为它们不需要创建实例和不涉及类的实例变量。以下是关于函数式组件的内存开销的一些相关方面:
-
无实例:函数式组件本质上是纯函数,它们不需要创建类实例,因此不会占用与类实例相关的内存。这意味着你可以拥有大量的函数式组件而不会导致大量的内存开销。
-
无生命周期方法:函数式组件通常不包含生命周期方法,因为它们没有类的实例,这减少了内存开销。在函数式组件中,你可以使用React的钩子函数(Hooks)来模拟生命周期行为,但Hooks的实现方式更轻量。
-
无实例变量:在函数式组件中,不会有实例变量(例如
this.state
、this.props
)来存储状态或属性,这减少了内存使用。 -
适合短期生命周期:函数式组件适用于具有较短生命周期的场景,因为它们在每次渲染时重新执行,不会保留旧的状态。这有助于避免内存泄漏问题。
总之,函数式组件通常在内存效率方面具有优势,因为它们更轻量,不需要创建实例和类的实例变量。这使得它们成为React的首选编程风格,尤其是在需要大量组件并保持较低内存占用的情况下。
5.2 class和Hook的一些对比
Hook是React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
class组件比较常见的是下面的优势:
1 class组件可以定义自己的state,用来保存组件自己内部的状态;
函数式组件不可以,因为函数每次调用都会产生新的临时变量;
2 class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
3 class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
Class组件存在的问题
1 复杂组件变得难以理解:
我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);
而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
2 难以理解的class:
很多人发现学习ES6的class是学习React的一个障碍。
比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
3 组件复用状态很难:
在前面为了一些状态的复用我们需要通过高阶组件;
像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
这些代码让我们不管是编写和设计上来说,都变得非常困难;
Hook的出现,可以解决上面提到的这些问题;
1 简单总结一下hooks:
它可以让我们在不编写class的情况下使用state以及其他的React特性;
但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
2 Hook的使用场景:
Hook的出现基本可以代替我们之前所有使用class组件的地方;
但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
3 在我们继续之前,请记住Hook 是:
完全可选的:你无需重写任何已有代码就可以在一些组件中尝试Hook。但是如果你不想,你不必现在就去学习或使用Hook。
100% 向后兼容的:Hook 不包含任何破坏性改动。
6 KubeSphere console中React的状态管理工具Mobx
MobX是一个基于响应式编程的状态管理库,React和MobX是一对强力组合,React提供机制把应用状态转为可渲染组件树并对其进行渲染,而MobX提供机制来存储和更新应用的状态供React使用。MobX背后的哲学很简单:任何源自应用状态的东西都应该自动地获得, 包括UI 数据序列化, 服务器通讯等等
6.1 核心概念
MobX的核心概念有三个:State(状态)、Actions(动作)、Derivations(派生)
6.1.1.定义可观察的State
MobX通过observable
标记一个可以被观察的状态并跟踪它们,只需要直接给它们赋值即可实现状态的修改
-
方法一:显示地标记observable和action
import { makeObservable, observable, action, computed } from "mobx";
export class Store {
count: number = 0;
price = 0;
amount = 1;
constructor() {
makeObservable(this, {
count: observable, // 标记observable
price: observable,
amount: observable,
add: action, // 标记action
});
}
add() {
this.count += 1;
}
}
-
方法二:通过
makeAutoObservale
自动地给类中的每个属性和方法标记上observale
和action
import { makeAutoObservable, observable, action, computed } from "mobx";
export class Store {
count: number = 0;
price = 0;
amount = 1;
constructor() {
makeAutoObservable(this);
}
add() {
this.count += 1;
}
}
6.1.2.使用Action更新State
Action
可以理解为任何可以改变State
的代码,比如用户事件处理,后端推送数据处理等等 在上面的例子中,add
方法改变了count
的属性值,而count
是被标记为observale
的,MobX推荐我们将所有修改observale
的值的代码标记为action
6.1.3.创建Derivations以便自动对State变化进行响应
任何来源是State且不需要进一步交互的东西都是Derivations
MobX区分了两种Derivations:
-
Computed:计算属性,可以用纯函数的形式从当前可观测的
State
中派生 -
Reactions:当State改变时需要运行的副作用
注:副作用可以看成是响应式编程和命令式编程之间的桥梁
-
通过computed对派生值进行建模
import { makeAutoObservable } from "mobx";
export class Store {
count: number = 0;
price = 0;
amount = 1;
constructor() {
makeAutoObservable(this);
}
add() {
this.count += 1;
}
get total() {
console.log("computed render");
return this.price + this.amount;
}
// computed可以有setter方法
set total(value: number) {
this.price = value;
}
}
6.2.MobX配合MobX-React创建状态管理
6.2.1.创建Store
1、@observable 定义变量; 2、@action 定义方法;
export default class RootStore {
@observable
navs = globals.config.navs
@observable
showGlobalNav = false
@observable
actions = {}
@observable
oauthServers = []
constructor() {
this.websocket = new WebSocketStore()
this.user = new UserStore()
this.routing = new RouterStore()
this.routing.query = this.query
global.navigateTo = this.routing.push
}
register(name, store) {
extendObservable(this, { [name]: store })
}
query = (params = {}, refresh = false) => {
const { pathname, search } = this.routing.location
const currentParams = parse(search.slice(1))
const newParams = refresh ? params : { ...currentParams, ...params }
this.routing.push(`${pathname}?${getQueryString(newParams)}`)
}
@action
toggleGlobalNav = () => {
this.showGlobalNav = !this.showGlobalNav
}
@action
hideGlobalNav = () => {
this.showGlobalNav = false
}
@action
registerActions = actions => {
extendObservable(this.actions, actions)
}
@action
triggerAction(id, ...rest) {
this.actions[id] && this.actions[id].on(...rest)
}
login(params) {
return request.post('login', params)
}
@action
async logout() {
const res = await request.post('logout')
const url = get(res, 'data.url')
if (url) {
window.location.href = url
}
}
@action
getRules(params) {
return this.user.fetchRules({ ...params, name: globals.user.username })
}
}
6.2.2 使用store
方式一
1 在入口文件引入
// mobx
import { Provider } from 'mobx-react';
import store from 'store/index';
ReactDom.render(
<AppContainer>
<Provider {...store}>
<RootElement />
</Provider>
</AppContainer>, document.getElementById('app') );
2 通过@inject注入,通过 this.props.store名称.方法/变量 的方式使用
import React from 'react';
import { observer, inject } from 'mobx-react';
@inject('UI') 和redux的connect作用一样,将数据注册到组件。
@observer 将你的组件变成响应式组件。就是数据改变时候可以出发重新的渲染。
class LeftMenu extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
toggleCollapsed = () => {
this.props.UI.toggleCollapsed();
};
render() {
const { collapsed } = this.props.UI;
return (
<div className={cx({ 'left-menu': true, 'left-menu-collapsed': collapsed === true })}>
<div className="open-menu" onClick={this.toggleCollapsed}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined)}
</div>
<Menu mode="inline" theme="dark" inlineCollapsed={collapsed} inlineIndent={15}></Menu>
</div>
);
}
}
export default LeftMenu;
方式二
在组件中导入store,构造一个store实例
import RootStore from 'stores/root'
import { lazy } from 'utils'
const getActions = lazy(() =>
import(/* webpackChunkName: "actions" */ 'actions')
)
export default class Environments extends React.Component {
rootStore = new RootStore()
envError = ''
static defaultProps = {
prefix: '',
checkable: true,
}
get prefix() {
const { prefix } = this.props
return prefix ? `${prefix}.` : ''
}
componentDidMount() {
getActions().then(actions =>
this.rootStore.registerActions(actions.default)
)
}
handleErrorStatus = (err = '') => {
this.envError = err
}
envValidator = (rule, value, callback) => {
if (this.envError === '') {
callback()
}
}
render() {
const {
checkable,
namespace,
isFederated,
cluster,
projectDetail,
} = this.props
return (
<Form.Group
label={t('ENVIRONMENT_VARIABLE_PL')}
desc={t('CONTAINER_ENVIRONMENT_DESC')}
checkable={checkable}
>
<Form.Item rules={[{ validator: this.envValidator }]}>
<EnvironmentInput
rootStore={this.rootStore}
name={`${this.prefix}env`}
namespace={namespace}
isFederated={isFederated}
cluster={cluster}
projectDetail={projectDetail}
handleInputError={this.handleErrorStatus}
/>
</Form.Item>
</Form.Group>
)
}
}
7 kubeSphere的官方API接口文档
https://kubesphere.io/api/kubesphere#tag/DevOps-Pipeline/operation/CheckCron
8 实际开发中的一些问题
(1)ks-console中使用class组件写法,许多方法并不是在当前class组件中定义的,而是通过不断继承父组件。导出时,使用的默认导出(意味着在某个组件中使用时,可以重命名),重命名之后相关的方法就比较难以找到(多个组件中可能使用同一个方法名,全局搜索有时会搜索出很多同名方法),这对于二次开发有点困难。
(2)class组件都是继承自 React.Component,而不是继承自PureComponent,这样可能导致一些性能问题。
例如,初始组件中定义一个名为counter的state,初始值为1;当在某处使用setState将counter重置为1,这时该组件就会重新执行render函数,这是我们不希望的。PureComponent可以解决这一问题。
(3)连接后端服务器时,建议在console\server 目录下创建一个local_config.yaml文件,项目启动后,就会从该配置文件读取要连接的后端服务地址和端口号。
local_config.yaml文件内容如下
server:
apiServer:
url: http://x.x.x.x:port // 后端服务器地址
wsUrl: ws://x.x.x.x:port // 后端服务器地址
资料来源
-
Kubesphere 源码分析1 整体结构
-
容器化部署方案_半只青年的博客-CSDN博客
-
kubesphere console 二次开发源码阅读_kube-design_tina_sprunt的博客-CSDN博客
-
blog.csdn.net
-
https://cloud.tencent.com/developer/beta/article/1865745
-
Documentation
-
词汇表
-
KubeSphere简介,功能介绍,优势,架构说明及应用场景_kybesphere_爱是与世界平行的博客-CSDN博客
-
一文看懂 Webhook 是什么?怎么使用?
-
什么是 Webhook?
-
kubesphere console 二次开发源码阅读_kube-design_tina_sprunt的博客-CSDN博客
-
React 18中MobX的使用 - 掘金
-
Redux 核心概念
-
Redux的三大核心 - 掘金
-
mobx、mobx-react和mobx-react-lite新手入门 - 掘金
-
初探mobx-react-lite
-
https://mp.weixin.qq.com/s/AsH0nRYr3hDF2Zr4_KQOCA
-
mobx 的原理以及在 React 中应用 - 掘金
-
mobx在react如何使用?3分钟学会!
-
Mobx-React : 当前最适合React的状态管理工具| 青训营笔记 - 掘金
-
blog.csdn.net
-
React 核心总结 - 掘金
-
react调度源码-切片原理 - 掘金
-
【React 18.2 源码学习】React render 原来你是这样的 - 掘金
-
走进React Fiber的世界 - 掘金