react study


react 学习

先决条件

  1. 对HTML和CSS的基本熟悉程度。
  2. JavaScript和编程的基础知识。
  3. 对DOM的基本了解。
  4. 熟悉ES6的语法和功能。
  5. 全局安装的 Node.js 和 npm。

组件

  1. 组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思
  2. 组件类型
    • 类组件
    • 函数组件(简单组件)
  3. props
  4. state

一、官方例子

描述

  1. Square 组件渲染了一个单独的
  2. Board 组件渲染了 9 个方块
  3. Game 组件渲染了含有默认值的一个棋盘
  4. 胜出逻辑分离

当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。

官方提出可改进

  1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
  2. 在历史记录列表中加粗显示当前选择的项目。
  3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
  4. 添加一个可以升序或降序显示历史记录的按钮。
  5. 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
  6. 当无人获胜时,显示一个平局的消息。

个人想法

  1. 如果组件逻辑不复杂的话,可以改为函数组件,比如Board
  2. 能不用hardcode就不用hardcode,使用循环优化Board
  3. Game 组件优化
    • 状态组件
    • 步数组件
  4. avatar

二、统一认证jumpserver例子

一、目录概要

二、AdminUsersDetail细节解说

1. AdminUsersDetail

  1. AdminUsersDetail
  2. 特点
    1. 定义了tabs
    2. 定义了tab props
  3. 代码

       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组件

  1. 特点
    1. 固定一些项目自有的属性
  2. 改进
    1. 将renderChildren提升
  3. 代码

       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

  1. 特点
    1. 使用高阶组件,管理tabkey字段
    2. 使TabComponents职责更单一
    3. 如果有其他组件需要,可以复用
  2. 改进
    1. 支持多个key
    2. 指定key
  3. 代码

       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

  1. 特点
    1. 状态控制
    2. 调用公共封装组件
    3. render内容结构比较分明
  2. 改进
    1. 可以将结构封装成更具名的组件
  3. 代码

         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

  1. 特点
    1. 针对表单数据、提交做处理
    2. 默认表单数据
    3. 分离表单具体实例
    4. 初始数据判断,fixed ant form 初始化数据
    5. onSubmit 返回promise状态,方便调用方进行后续处理
  2. 改进
    1. 表单提交后处理方式
    2. Loading 可以加入虚拟骨架
  3. 代码

         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

  1. 特点
    1. 添加了几个中间状态控制表单展示
    2. 用合理的状态控制表单展示
    3. 使用了统一封装的一些组件
  2. 改进
    1. 如果有其他地方也用protocolOptions,可以再提升到公共的静态变量中
    2. 保存并继续添加
    3. 能否有一个有效的方式,将重点展示出来
  3. 代码

         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