react 学习
先决条件
- 对HTML和CSS的基本熟悉程度。
- JavaScript和编程的基础知识。
- 对DOM的基本了解。
- 熟悉ES6的语法和功能。
- 全局安装的 Node.js 和 npm。
组件
- 组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思
- 组件类型
- 类组件
- 函数组件(简单组件)
- props
- state
一、官方例子
描述
- Square 组件渲染了一个单独的
- Board 组件渲染了 9 个方块
- Game 组件渲染了含有默认值的一个棋盘
- 胜出逻辑分离
当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。
官方提出可改进
- 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
- 在历史记录列表中加粗显示当前选择的项目。
- 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
- 添加一个可以升序或降序显示历史记录的按钮。
- 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
- 当无人获胜时,显示一个平局的消息。
个人想法
- 如果组件逻辑不复杂的话,可以改为函数组件,比如Board
- 能不用hardcode就不用hardcode,使用循环优化Board
- Game 组件优化
- 状态组件
- 步数组件
二、统一认证jumpserver例子
一、目录概要
二、AdminUsersDetail细节解说
1. AdminUsersDetail
- AdminUsersDetail
- 特点
- 定义了tabs
- 定义了tab props
-
代码
import React, { Component } from "react"; import {getJp_assets_admin_users} from 'api/certification/assetsList' import CSSTransitions from 'components/CSSTransitions' import {Button} from 'antd' import BaseInfo from './BaseInfo' import AssetUsers from "./AssetUsers"; import TabComponents, {renderComponent} from "components/jp/TabComponents"; const tabs = [ { label: '资产详情', key: 'base', Component: BaseInfo, }, { label: '资产用户列表', key: 'asset-users', Component: AssetUsers, }, ] class AssetsDetail extends Component { constructor(props) { super(props) this.id = props.match.params.id; this.name = props.match.params.name || '_' this.state = { asset: null, } } componentDidMount() { this.fetchUserInfo() } fetchUserInfo() { getJp_assets_admin_users(this.id).then(rep => { this.setState({ asset: rep.data }) }) } render() { const asset = this.state.asset const tabProps = { id: this.id, asset, renewAsset: () => this.fetchAssetInfo(), } return ( <CSSTransitions> <div style= > <div style=> <span style= > 管理用户: {asset ? asset.name : '加载中...'} </span> <Button type="primary" style= onClick={() => { this.props.history.push('/assets/admin-user/detail/' + this.id +'/upstate'); }} > 更新 </Button> <TabComponents loading={!asset}> {renderComponent(tabs, tabProps)} </TabComponents> </div> </div> </CSSTransitions> ) } } export default AssetsDetail
2. TabComponents 二次封装tab组件
- 特点
- 固定一些项目自有的属性
- 改进
- 将renderChildren提升
-
代码
import React from 'react' import PropTypes from 'prop-types' import Loading from 'components/Loading' import {Tabs} from 'antd' import CSSTransitions from 'components/CSSTransitions' import tabReplaceHoc from './TabReplaceHoc' const {TabPane} = Tabs const TabComponents = props => { if(props.loading) { return <Loading loading={props.loading}></Loading> } const renderChildren = children => { return children.map(child => ( <TabPane tab={child.props.label} key={child.key} > <CSSTransitions> {child} </CSSTransitions> </TabPane> )) } return ( <Tabs activeKey={props.activeKey} type="card" onChange={props.tabChange} style= destroyInactiveTabPane={true} > { props.children ? renderChildren(props.children) : '' } </Tabs> ) } TabComponents.propTypes = { loading: PropTypes.bool, } TabComponents.defaultProps = { loading: false } export const renderComponent = (tabs, props) => { return tabs.map(tab => { return <tab.Component {...props} key={tab.key} label={tab.label} ></tab.Component> }) } export default tabReplaceHoc(React.memo(TabComponents))
3. TabReplaceHoc
- 特点
- 使用高阶组件,管理tabkey字段
- 使TabComponents职责更单一
- 如果有其他组件需要,可以复用
- 改进
- 支持多个key
- 指定key
-
代码
import React, { useState } from "react" import { useHistory } from "react-router" import { encodeQuery, parseQuery, query } from "utils/common" /** * 处理tab 转换时的key 以及url query中的tab */ const tabReplaceHoc = (WrappedComponent, defaultActiveKey) => { return props => { const history = useHistory() const [activeKey, setActiveKey] = useState(query(history.location, 'tab') || defaultActiveKey) const tabChange = activeKey => { setActiveKey(activeKey) const query = parseQuery(history.location) query['tab'] = activeKey history.replace( `${history.location.pathname}?`+ encodeQuery(query) ) } return <WrappedComponent {...props} activeKey={activeKey} tabChange={tabChange}> </WrappedComponent> } } export default tabReplaceHoc
4. BaseInfo
- 特点
- 状态控制
- 调用公共封装组件
- render内容结构比较分明
- 改进
- 可以将结构封装成更具名的组件
-
代码
import React, { Component } from "react"; import Loading from 'components/Loading' import {Row, Col, message} from 'antd' import SelectCard from 'components/jp/SelectCard' import BaseInfoCard from 'components/jp/BaseInfoCard' import {patchJp_assets_admin_users_nodes,} from 'api/certification/assetsList' import {asset_notes_fetch} from 'utils/jp-assets' const cardData = [ { title: "ID", key: "id" }, { title: "名称", key: "name" }, { title: "用户名", key: "username" }, { title: "创建日期", key: "date_created", render: record => record.date_created.split(' +')[0] }, { title: "创建者", key: "created_by" }, ] class BaseInfo extends Component { constructor(props) { super(props) this.id = props.id this.state = { asset: props.asset, } } componentDidMount() { } onBindNodes(nodes) { if(!nodes || nodes.length <= 0) { message.warning("请选择节点") return } return patchJp_assets_admin_users_nodes(this.id, {nodes}).then(rep => { message.success("替换节点成功") }) } render() { if(!this.id ||!this.state.asset) { return <Loading loading={true}></Loading> } const asset = this.state.asset return ( <> <Row> <Col span={14}> <BaseInfoCard cardData={cardData} data={asset} ></BaseInfoCard> </Col> <Col span={10} style=> <SelectCard headerStyle= title=" 替换资产的管理员" onLoadData={asset_notes_fetch} relates={[]} render={record => `${record.full_value}`} onCreate={keys => this.onBindNodes(keys)} > </SelectCard> </Col> </Row> </> ) } } export default BaseInfo
三、SystemUserCreate细节解说
1. SystemUserCreate
- 特点
- 针对表单数据、提交做处理
- 默认表单数据
- 分离表单具体实例
- 初始数据判断,fixed ant form 初始化数据
- onSubmit 返回promise状态,方便调用方进行后续处理
- 改进
- 表单提交后处理方式
- Loading 可以加入虚拟骨架
-
代码
import React, { Component } from 'react'; import { Divider } from 'antd'; import { getJp_assets_system_users, patchJp_assets_system_users, postJp_assets_system_users, } from 'api/certification/assetsList'; import RebLoading from 'components/RebLoading'; import SystemUserForm from './SystemUserForm' import Loading from 'components/Loading'; const defaultInitialValues = { login_mode: "auto", username_same_with_user: false, priority: 20, protocol: "ssh", auto_push: false, sudo: "/bin/whoami", shell: "/bin/bash", auto_generate_key: false, sftp_root: "tmp", name: "", username: "", home: "", password: "", cmd_filters: [], private_key_file: [], } class SystemUserCreate extends Component { constructor(props) { super(props) this.id = props.match.params.id; this.state = { initialValues: null } } componentDidMount() { if (!this.id) { this.setState({ initialValues: {...defaultInitialValues} }) } else { getJp_assets_system_users(this.id).then(rep => { this.setState({initialValues: {...rep.data}}) }) } } onSubmit(data) { this.props.setLoading(true) const promise = !this.id ? postJp_assets_system_users(data) : patchJp_assets_system_users(this.id, data) return promise.finally(() => { this.props.setLoading(false) }) } render() { return ( <> <div style=> <Divider orientation="left">基本</Divider> { !this.state.initialValues ? <Loading loading={true}></Loading> : <SystemUserForm initialValues={this.state.initialValues} onFinish={data => this.onSubmit(data)} > </SystemUserForm> } </div> </> ) } } export default RebLoading(SystemUserCreate);
2. SystemUserForm
- 特点
- 添加了几个中间状态控制表单展示
- 用合理的状态控制表单展示
- 使用了统一封装的一些组件
- 改进
- 如果有其他地方也用protocolOptions,可以再提升到公共的静态变量中
- 保存并继续添加
- 能否有一个有效的方式,将重点展示出来
-
代码
import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types' import { Form, Divider, Select, Button, Input, Switch, Radio, Upload, Checkbox, message, Space, } from 'antd' import { UploadOutlined } from '@ant-design/icons' import {filter_commands_init, filter_commands_fetch, jp_lazy_search} from 'utils/jp-assets' import ScrollSelect from 'components/ScrollSelect'; import {commonFormLayout, commonTailLayout} from 'utils/common' import { useHistory } from 'react-router'; const { Option } = Select const { TextArea } = Input const protocolOptions = [ {label: 'ssh', value: 'ssh'}, {label: 'rdp', value: 'rdp'}, {label: 'telnet', value: 'telnet'}, {label: 'vnc', value: 'vnc'}, {label: 'mysql', value: 'mysql'}, {label: 'oracle', value: 'oracle'}, {label: 'mariadb', value: 'mariadb'}, {label: 'postgresql', value: 'postgresql'}, {label: 'k8s', value: 'k8s'}, ] const SystemUserForm = props => { const [form] = Form.useForm() const history = useHistory() // 是否同名 控制用户名是否可以编辑 const [usernameSameWithUser, setUsernameSameWithUser] = useState(props.initialValues.username_same_with_user) // 自动推送 控制自动推送下Sudo、Shell、家目录、用户附属组等展示 const [autoPush, setAutoPush] = useState(props.initialValues.auto_push) // 自动生成密钥 控制密码、密钥展示 const [autoGenerateKey, setAutoGenerateKey] = useState(props.initialValues.auto_generate_key) // 更新密码 控制密码展示 const [renewPassword, setRenewPassword] = useState(!props.initialValues.id) const [saveAndNext, setSaveAndNext] = useState(false) const onFieldsChange = (changedFields, allFields) => { changedFields.forEach(field => { const name = field.name[0] if (name === "username_same_with_user") { setUsernameSameWithUser(field.value) } else if (name === "auto_push") { setAutoPush(field.value) } else if (name === "auto_generate_key") { setAutoGenerateKey(field.value) } else if (name === "renew_password") { setRenewPassword(field.value) } }) } // 上传私钥 const onUploadPrivateKey = (e) => { if (!e.file) { return } if (!!e.file.type && e.file.type.indexOf('text') === -1) { const msg = '请上传纯文本格式或者txt格式' message.error(msg) e.onError(new Error(msg)) return } // console.log(e) e.file.text().then(text => { form.setFieldsValue({private_key: text}) e.onSuccess() }) } const onFinish = data => { // 根据选择过滤一些数据 // console.log(data) if (!data['renew_password']) { delete data['password'] } if (!data['private_key']) { delete data['private_key'] } if (props.initialValues.id) { delete data['auto_generate_key'] } delete data['private_key_file'] delete data['renew_password'] return props.onFinish(data).then(rep => { const system_user = rep.data saveAndNext && form.resetFields() message.success(!props.initialValues.id ? '添加成功': '更新成功') history.push(`/jumpserver/assets/system-user/details/${system_user.id}`) return rep }).finally(() => { saveAndNext && setSaveAndNext(false) }) } useEffect(() => { if (saveAndNext) { form.submit() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [saveAndNext]) return ( <Form {...commonFormLayout} form={form} onFinish={onFinish} initialValues={props.initialValues} onFieldsChange={onFieldsChange} > <Form.Item label='名称' name="name" required={true} > <Input placeholder="请输入名称" /> </Form.Item> <Form.Item label='登录模式' extra="如果选择手动登录模式,用户名和密码可以不填写" name="login_mode" required={true} > <Radio.Group> <Radio value="auto">自动登录</Radio> <Radio value="manual">手动登录</Radio> </Radio.Group> </Form.Item> <Form.Item label='用户名' name="username" required={true} > <Input placeholder="请输入用户名" disabled={usernameSameWithUser}/> </Form.Item> <Form.Item label='用户名与用户相同' extra="用户名是动态的,登录资产时使用当前用户的用户名登录" name="username_same_with_user" valuePropName="checked" > <Switch /> </Form.Item> <Form.Item label='优先级' extra="1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为默认登录用户" name="priority" > <Input placeholder="请输入优先级" /> </Form.Item> <Form.Item label='协议' name="protocol" > <Select placeholder="请选择协议"> { protocolOptions.map( option => <Option key={option.value} value={option.value}>{option.label}</Option> ) } </Select> </Form.Item> <Divider orientation="left">自动推送</Divider> <Form.Item label='自动推送' name="auto_push" valuePropName="checked" > <Switch /> </Form.Item> <Form.Item label='Sudo' extra="使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" name="sudo" hidden={!autoPush} required={autoPush} > <Input placeholder="请输入Sudo" disabled={!autoPush}/> </Form.Item> <Form.Item label='Shell' name="shell" hidden={!autoPush} required={autoPush} > <Input placeholder="请输入Shell" disabled={!autoPush}/> </Form.Item> <Form.Item label='家目录' extra="默认家目录 /home/系统用户名: /home/username" name="home" hidden={!autoPush} > <Input placeholder="请输入家目录" disabled={!autoPush} /> </Form.Item> <Form.Item label='用户附属组' extra="请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)" name="system_groups" hidden={!autoPush} > <Input placeholder="请输入用户附属组" disabled={!autoPush} /> </Form.Item> <Divider orientation="left">认证</Divider> <Form.Item label='自动生成密钥' name="auto_generate_key" valuePropName="checked" hidden={!!props.initialValues.id} > <Switch disabled={!!props.initialValues.id}/> </Form.Item> <Form.Item label='更新密码' name="renew_password" valuePropName="checked" hidden={renewPassword} > <Checkbox></Checkbox> </Form.Item> <Form.Item label='密码' extra="密码或密钥密码" name="password" hidden={autoGenerateKey || !renewPassword} > <Input.Password placeholder="请输入密码" disabled={autoGenerateKey || !renewPassword} /> </Form.Item> <Form.Item label='ssh私钥' name="private_key" hidden > <Input disabled={autoGenerateKey} /> </Form.Item> <Form.Item label='ssh私钥' name="private_key_file" valuePropName="fileList" hidden={autoGenerateKey} getValueFromEvent={e => Array.isArray(e) ? e : e ? e.fileList : false} > <Upload customRequest={onUploadPrivateKey} name="private_key_upload" disabled={autoGenerateKey} listType="text" maxCount={1} // showUploadList={false} > <Button icon={<UploadOutlined />}>点击上传</Button> </Upload> </Form.Item> <Divider orientation="left">命令过滤器</Divider> <Form.Item label='命令过滤器' name="cmd_filters" > <ScrollSelect defaultProps= renderOption={command => <Option key={command.id} value={command.id}>{command.name}</Option>} onInitData={filter_commands_init(props.initialValues.nodes)} onLoadData={filter_commands_fetch} onSearch= {jp_lazy_search(filter_commands_fetch)} /> </Form.Item> <Divider orientation="left">其它</Divider> <Form.Item label='SFTP根路径' name="sftp_root" required > <Input placeholder="请输入公网SFTP根路径" /> </Form.Item> <Form.Item label='备注' name="comment" > <TextArea /> </Form.Item> <Form.Item {...commonTailLayout}> <Space> { props.initialValues.id ? ( <Button htmlType="button" onClick={() => form.resetFields()} style=> 重置 </Button> ) : <Button htmlType="button" onClick={() => setSaveAndNext(true)}>保存并继续添加</Button> } <Button type="primary" htmlType="submit"> 提交 </Button> </Space> </Form.Item> </Form> ) } SystemUserForm.propTypes = { onFinish: PropTypes.func.isRequired, initialValues: PropTypes.object } export default SystemUserForm