React技术点入坑
React
官网
中文网
用于构建用户界面(UI)的 JavaScript 库
版本:16.x / 17.x / 18.x
创建 React 项目
利用脚手架创建react项目框架 cra5.x
react直接是18.x版本,近两月出来的
之前一直用的是16.x 17.x react项目
create-react-app ( cra )
官方定义的脚手架,用于快速搭建项目结构, 创建一个新的单页应用
目前 create-react-app 升级到 5.x 版本,要求 node.js 的版本至少为 14.x 以后的版本,所以对于 win7 不支持。
创建项目
$ npx create-react-app <project-name> # 或 $ npm install yarn -g $ yarn create react-app <project-name>
安装依赖包
yarn 回车
react:核心包
react-dom:浏览器渲染 (react-native:原生渲染,了解)
react-scripts:封装了 webpack 各项任务的脚本
npm scripts
可以在命令行中使用 npm run 的任务:
start:开发任务
build:生产任务,打包构建生成打包后的代码
test:测试
eject:弹出(将封装好的 webpack 任务还原到项目本地,以便于在项目目录下找到 webpack 的配置文件并进行修改扩展。**注意:**执行 eject 弹出任务后,是不能撤销的)
以下是react16.x | 17.x 项目
入口文件
src/index.js 文件( 16.x 17.x):
只能复制黏贴以前的,18很多问题…
import React from 'react' // 在 16.x 中,要使用 JSX 表达式,则必须引入 react import ReactDOM from 'react-dom' ReactDOM.render( <h1>Hello React</h1>, document.getElementById('root') )
18.x的写法
const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<h1>Hello, world!</h1>);
JSX
JavaScript 的语法扩展。
建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。
本质语法糖,在vue中需要借助babel插件才能使用,react天生支持
class语法糖 函数+原型链…
JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖
在JSX中嵌入 JS 表达式
const message = '<input type="text"/>' const element = ( <h1 title={message}>hello,{message}</h1> )
使用 { expression } 来嵌入 JS 表达式内容,注意:在 {} 内的 HTML 表达式内容会被转义处理,以避免出现 XSS 攻击。 XSS(cross-site-scripting, 跨站脚本)
属性值避免引号,如果有引号就是‘’字符串
需要html文本正常渲染的话:
<div>{ this.state.html }</div> // 源码 <div dangerouslySetInnerHTML={ {__html:this.state.html }}></div> //渲染html
为了便于阅读,我们会将 JSX 拆分为多行。同时,我们建议将内容包裹在括号中,虽然这样做不是强制要求的,但是这可以避免遇到自动插入分号陷阱。
const element = ( <h1>hello,{message}</h1> )
jsx实战
{/* 展开 收起的图标组件渲染(实际为两个图标组件) */} {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => setCollapsed(!collapsed), })} {/* 转化成jsx语法 */} { collapsed ? <MenuUnfoldOutlined className='trigger' onClick={() => setCollapsed(!collapsed)}/> : <MenuFoldOutlined className='trigger' onClick={() => setCollapsed(!collapsed)}/> }
JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖
组件
1.函数组件
src/function.js 【rfc】
function MyComponent(props){ // 通过参数props接受到的属性 return ( <div> JSX表示的 React 元素 </div> ) }
return 一个JSX 表达式 (定义组件)
单个根节点包裹布局结构
哪里用到 直接import 引入 (渲染组件)
组件名字 作为 标签名字去渲染
组件名字 每个单词首字母大写 (帕斯卡命名规范)。JSX中通过标签的首字母是否大写来判定是 html标签 还是自定义的组件
2.类组件
src/class.jsx 【 rcc 】
class MyComponent extends React.Component { // constructor() {} render() { // this.props 获取组件接收到的属性 return ( <div>JSX 表示的 React 元素</div> ) } } //自定义类,继承自 React.component 父类(基类) //类主体中,必须写 render(){ return () }
import 引入
组件名字作为 标签名字引入
class组件必须使用render() 方法
渲染组件 { }
State 与 Props
state 是组件内私有使用到的数据,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
props 是组件接收到的属性数据,应该是只读的,绝不能修改组件中的 属性 数据。
正确使用 state
不能直接修改 state 的值,而是调用 setState() 方法更新状态数据。直接修改 state 数据,视图不会响应式渲染
组件管理自身的数据
setState 的更新可能是异步的
解释:setState 本质上还是在一个事件循环中,并没有切换到另外宏任务或者微任务中,在运行上是基于同步代码实现,只是行为上看起来像异步
初始化:class state={ } constructor( this.state = {} ) this.setState({ inputValue:'' }) console.log('input',this.state.inputValue); //undefined setTimeout(()=>{console.log(this.state.inputValue);},1000) // 正常显示输入框的值
state 的更新会被合并
属性 Props 类型检查
vue组件中props传递数据,可以定义数据类型;
react用类型检查
TypeScript || Flow :静态类型检查器 【写的时候就会说明】
prop-types 运行时类型检查 【运行时才会发现】
prop-types:运行时类型检查 【三方包】
$ npm i prop-types # 或 $ yarn add prop-types
import PropTypes from 'prop-types'; // ES6 var PropTypes = require('prop-types'); // ES5 with npm class /** * 运行时类型检查 * 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保 * 这个 prop 没有被提供时,会打印警告信息。 */ static propTypes = { todos:PropTypes.array.isRequired, }
props 默认值
/** * 默认值 不需要安装包 * */ class static defaultProps = { todos:[] }
ref
vue中获取焦点 this.$refs == react中有 this.refs 【弃用】
createRef this.refs == this.inputRef.current.focus( )
创建一个ref对象 inputRef = createRef()
ref = { this.inputRef }
/* 获取焦点 */ // 拿到createRef import React, { Component,createRef } from 'react' // 创建一个 Ref 对象 inputRef = createRef() // React.createRef() // 点击事件 handleButton= (event)=>{ // 获取焦点 refs 已经弃用 // this.refs.inputRef.focus() this.inputRef.current.focus() } <input ref={this.inputRef}/>
组件通信
父子组件
父传子:利用props
类组件
App.jsx: constructor(props){ super(props) // 调用父类的构造函数 this.state = { } } 不显示定义 constructor 来书写 state的结构,简写以下: /* 父传子 */ App.jsx => 传子 todos数组 state = { todos:new Array(5).fill(null).map((_,index) => { return { id:Math.random(), completed:false, title:'什么是闭包'+ index, } }) } <TodoList todos={ this.state.todos }></TodoList> -------------------------------------------------- Todolist.jsx: => 传子 todo 当前项 {/* this.props 是在 class 组件的成员方法中获取属性值的调用写法 */} render() { return ( <div> {/* <TodoItem></TodoItem> */} { this.props.todos.map(todo => { return ( <TodoItem todo={ todo }></TodoItem> ) }) } </div> ) } ------------------------------------------------------ TodoItem.jsx: render() { const {id,title,completed} = this.props.todo return ( <div> {title}--{completed?'已':'未'}完成---{id} </a> </div> ) }
子传父:利用props 传递函数的引用
利用 props 来传递函数的引用。父组件中渲染子组件实例时,传递一个函数的引用作为属性值到子组件中,当子组件需要传递数据时,调用在属性中接收到的函数并传递参数即可。要注意函数中 this 指向的问题(可以调用 bind 修改 this 指向,或使用箭头函数定义)
类组件
添加待办事件 方法全部用箭头函数写,不会有this困扰
// 修改state状态数据,是调用this.setState() 方法【继承自父类,react.component】来修改 /* 添加待办事件 方法全部用箭头函数写,不会有this困扰*/ App.jsx: addTodo = (title)=>{ this.setState({ todos:[ ...this.state.todos, { id:Math.random(), title, // 子传父 completed:false, } ] }) } <TodoInput add={this.addTodo}></TodoInput> ---------------------------------------------------- TodoInput.jsx: /* 处理添加待办事件 * onKeyUp={this.handleAdd} * 获取输入框的值 - 点击enter 加入todos数组 */ handleAdd=(event)=>{ let title = event.target.value if(event.keyCode===13){ console.log('key13'); this.props.add(title) event.target.value = '' } } <input onKeyUp={this.handleAdd} /> -------------------------------------------------- /* 添加待办事件 方法全部用普通函数写,有this困扰*/ TodoInput.jsx: //事件函数,不绑定this, this默认为undefined => 改变this指向 // bind() 改变this指向 this.bind(null) => 全局对象 严格模式下为undefined handleAdd(event){ event.target.value if(event.keyCode === 13){ // 回车 // 调用从属性中传递的 add 函数 this.props.add(event.target.value) } } <input onKeyDown={ this.handleAdd.bind(this) }></input> ------------------------------------------------------- App.jsx: addTodo(title){ console.log('addTodo',this); // addTodo() this.setState({ todos:[ ...this.state.todos, { id:Math.random(), title, // 子传父 completed:false, } ] }) } <TodoInput add={this.addTodo.bind(this)}></TodoInput>
跨组件层级
Context
redux
课堂案例
随机点名
src/class.jsx
import React, { Component } from 'react' // 所有人姓名 const _names = ['石雷', '文凯伦', '王君辰', '包旭扬', '侯安康', '王海建', '王万', '王杰', '陈陶', '银登梦', '吕溜洋', '刘茂', '陈鑫荣', '李燕丹', '邓蔚', '何萧', '方炎园', '陈茜', '谢玲星'] export default class ClassComponent extends Component { render() { return ( <div> <button onClick={this.handleClick}>点名开始</button> <button onClick={this.handleEnd}>点名截止</button> <div>{this.state.currName}</div> </div> ) } state = { names:_names, currName:'', } /* 开启定时器 */ handleClick = ()=>{ this.timer = setInterval(()=>{ const index = Math.floor(Math.random()*(this.state.names.length)) this.setState({ currName : this.state.names[index] }) },100) } /* 停止定时器 */ handleEnd =()=>{ clearInterval(this.timer) } }
待办事项
todolist-app/cra4项目模板copy
yarn 直接回车安装依赖
vscode 安装 react 插件 ( ES7+ React/Redux/React-Native snip ) 自动补全代码
src / components/各组件创建
todoHeader.jsx todoInput.jsx ....
src / index.js
import App from './App' reactDOM.render( <App></App>, ) import 'bulma/css/min.css'
src / App.jsx ( 最外面的app )
/* 书写组件渲染 */ import React, { Component } from 'react' import TodoHeader from './components/TodoHeader' import TodoInput from './components/TodoInput' import TodoList from './components/TodoList' import TodoFooter from './components/TodoFooter' export default class App extends Component { render() { return ( <div> <TodoHeader></TodoHeader> <TodoInput></TodoInput> <TodoList></TodoList> <TodoFooter></TodoFooter> </div> ) } }
rcc (react class component) class组件快速编写
rfc (react class component) 函数组件
bulma 样式库
npm i bulma
class 是JSX的关键字 最好改成 className
style={ { width:‘100px’ } }
多余父标签不进行渲染,无需向Dom添加额外节点
<React.Fragment></React.Fragment> ===短语法 <></> // 不支持属性添加
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。 (谨慎使用,会使得组件复用性变差)
利用 Context 可在全局范围内实现组件树中各组件间数据的共享,通常可以 Context 中共享的数据有如:用户登录的状态、地区偏好设置、UI主题设置、语言设置等。
作用:(每个组件拿到都可以改,和vuex不一样)
API
React.createContext() 创建context对象
Provider 生产组件
Consumer 消费组件
class.contextType 这是类的静态属性,设置后,可以成员方法中使用 this.context 获取到最近保存在 Provider 组件 value 属性中的值。
todoItem 删除 App 组件内的 todos 数组
utils> with-contenxt.js
import { createContext } from 'react' //创建Context对象 const TodoContext = createContext() const { Provider,// 生产组件,允许消费组件订阅Context的变化 Consumer,// 消费组件,可以订阅 context 的变更 } = TodoContext export { Provider Consumer TodoContext }
App.jsx>生产组件
import { Provider } from '' render(){ return (// 相当于共享数据 <Provider value={ { todos:this.state.todos, add:this.addTodoItem, 删除方法, 改变状态, } }> ...把以前的用标签包起来 </Provider> ) } ----------------------------------------------------------------------------- render() { return ( <Provider value={{ todos:this.state.todos, add:this.addTodo, remove:this.removeTodo, toogle:this.toggleTodoItem, }}> <article className="panel is-primary" style={{width:'500px',height:'500px'}}> <TodoHeader></TodoHeader> <TodoInput add={this.addTodo.bind(this)}></TodoInput> <TodoList todos={ this.state.todos }></TodoList> <TodoFooter></TodoFooter> </article> </Provider> ) }
todoItem.jsx:> 消费组件
删除按钮,调到全局共享的删除方法 import { Consumer } from '' render(){ return ( <Consumer> ...包起来 { (value)=> { const {删除,切换状态方法} = value return(...之前的...) } } </Consumer> ) } -------------------------------------------------------------- render() { const {id,title,completed} = this.props.todo return ( <Consumer> {/* Consumer 组件的孩子节点应该是一个函数,函数的 */} {/* 参数是在 Provider 组件 value 属性中保存的数据 */} {/* Consumer 组件会订阅 context 的变化,即 Provider */} {/* 组件 value 属性中保存的值发生变化时,Consumer 组件 */} {/* 会自动响应式重新渲染 */} { // 此value为provider定义的属性 (value) => { const { toogle, remove } = value return ( <div> <a className="panel-block is-active" > <input type="checkbox" onChange={()=>{ toogle(id)} } checked={completed} ></input> <span className="panel-icon"> <i className="fas fa-book" aria-hidden="true"></i> </span> {title}--{completed?'已':'未'}完成---{id} 注意:这里的{ 里面是个方法调用 } <button onClick={()=>{ remove(id)} }>删除</button> </a> </div> ) } } </Consumer> ) }
需要一个函数作为consumer子元素,
函数的参数是在Provider 组件 value 属性中保存的数据,
consum组件会订阅context的变化,即Provider组件 value 属性中保存的值发生变化时,Consumer组件会自动响应式重新渲染
todoFooter:> 类组件 的 contextType 简便的consumer写法
import {TodoContext} from '' /*class类组件*/ { // 定义静态的 contextType,则在成员方法中就可以使用this.context 获取到Provider 组件 value 属性值 static contextType = TodoContext render(){ const { todos } = this.context // 拿到共享的todos数组 } }
条件渲染
jsx内嵌js表达式: 只能写 { 表达式 } 不可用流程语句 { // 相比 vue 条件渲染 可读性差 三目运算符 this.props.todos.length? 满足的:不满足的渲染 } ------------------------------------------------------ { this.props.todos.length >0 ? this.props.todos.map(todo => { return ( <TodoItem todo={ todo } key={ todo.id }></TodoItem> ) }):<div>需要先添加待办事件</div> }
事件处理
React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
无修饰符 .enter 键值需要自己去判断
handleAdd(event){ this.setState({ inputValue:event.target.value }) if(event.keyCode===13){ this.props.add(this.state.inputValue) event.target.value = '' } }
传参数
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
表单
vue中的双向绑定 v-model == react实现
表单中内容由 state 控制,不能手动输入 要么设置只读或者绑定onchange事件
区分按键事件还是按钮点击行为,单独定义两个函数
双向绑定实现
<input value={this.state.inputValue}> </input> state = { inputValue:'', } 控制台报错: /*表单中内容由 state 控制,不能手动输入 要么设置只读或者绑定onchange事件*/ ------------------------------------------------------- <input value={this.state.inputValue} onChange={this.handleChange.bind(this)} ref='inputRef' /> /* 双向绑定输入框 */ handleChange=(event)=>{ this.setState({ inputValue:event.target.value }) } /** * 点击事件 */ handleButton= (event)=>{ this.props.add(this.state.inputValue) // 清空输入框 this.setState({ inputValue:'' }) }
高阶组件(HOC)复用
Higher Order Component
实现复用组件
是一种基于 React 的组合特性而形成的设计模式。---- 装饰器模式:允许向一个现有的对象添加新的功能,同时又不改变其结构。
高阶组件本质上是一个函数,函数以组件作为参数,返回新组件。
利用高阶组件,可以在现有组件功能基础上增加新的功能,也可以对现有组件进行功能增强。
高阶组件(HOC)复用 Higher Order Component 实现复用组件 是一种基于 React 的组合特性而形成的设计模式。---- 装饰器模式:允许向一个现有的对象添加新的功能,同时又不改变其结构。 高阶组件本质上是一个函数,函数以组件作为参数,返回新组件。 利用高阶组件,可以在现有组件功能基础上增加新的功能,也可以对现有组件进行功能增强。
vueRouter实例 vuex中store 可以看作是单例模式
原型链–原型模式
类组件
方法和初始数据states
/* 类组件-父传子 */ state = { todos:new Array(5).fill(null).map(todo=>{ return { title:'宝宝', id:Math.random(), completed:false, } }) }
生命周期
组件从创建开始,到销毁结束,所经历的整个过程,就是生命周期。
生命周期钩子:在正常的生命周期过程中,有机会执行到用户额外的代码逻辑。
只在 class 组件中有生命周期钩子函数,函数组件中不存在这些生命周期钩子(函数组件中 this 为 undefined)。
挂载阶段
当组件实例被创建并插入 DOM 中时
**constructor():**构造器,初始化,如:state初始化,方法中 this 指向的绑定
static getDerivedStateFromProps():
**render():**渲染
**componentDidMount():**会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。
了解:
componentWillMount():会在 render() 前调用,已废弃
更新阶段
当组件的 props 或 state 发生变化时会触发更新
static getDerivedStateFromProps():
shouldComponentUpdate():通常作为性能优化的方式存在。这个方法会返回 boolean 值,当返回 true 时,会继续向后执行流程,返回 false 时,中断当前阶段渲染。默认行为是 state 每次发生变化组件都会重新渲染。
render():
**componentDidUpdate():**会在更新后会被立即调用 (一般避免在这里进行修改状态数据,可能会进入更新循环 ===== updated)
卸载阶段(销毁)
**componentWillUnmount():**销毁阶段常销毁的资源:打开的连接、启动的定时器、未完成的网络请求、订阅的监听等
函数组件:
方法和初始数据Hooks
生命周期 组件从创建开始,到销毁结束,所经历的整个过程,就是生命周期。 生命周期钩子:在正常的生命周期过程中,有机会执行到用户额外的代码逻辑。 只在 class 组件中有生命周期钩子函数,函数组件中不存在这些生命周期钩子(函数组件中 this 为 undefined)。 挂载阶段 当组件实例被创建并插入 DOM 中时 **constructor():**构造器,初始化,如:state初始化,方法中 this 指向的绑定 static getDerivedStateFromProps(): **render():**渲染 **componentDidMount():**会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。 了解: componentWillMount():会在 render() 前调用,已废弃 更新阶段 当组件的 props 或 state 发生变化时会触发更新 static getDerivedStateFromProps(): shouldComponentUpdate():通常作为性能优化的方式存在。这个方法会返回 boolean 值,当返回 true 时,会继续向后执行流程,返回 false 时,中断当前阶段渲染。默认行为是 state 每次发生变化组件都会重新渲染。 render(): **componentDidUpdate():**会在更新后会被立即调用 (一般避免在这里进行修改状态数据,可能会进入更新循环 ===== updated) 卸载阶段(销毁) **componentWillUnmount():**销毁阶段常销毁的资源:打开的连接、启动的定时器、未完成的网络请求、订阅的监听等 函数组件: 方法和初始数据Hooks
Props 通过函数参数传递
什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。
父传子
<TodoList todos={todos}></TodoList> export default function TodoList(props) { const todos = props.todos console.log('todolist:todos',todos); return ( todos.map(todo=>{ return (<TodoItem todo={todo} key={todo.id}></TodoItem>) }) ) }
Hooks 作用:【面试】
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。闭包处理
函数组件 | 自定义的hook中用
Hook是在函数组件中使用的,不能在 class 组件中使用,可以在函数组件中使用到 class 类组件中的特性。
hooks
State Hook
在函数组件中使用类似 class 组件的 state 特性:
函数组件的 this 为 undefined
import React,{useState} from 'react' /* useState初始值 */ const todosInit= new Array(5).fill(null).map(todo=>{ return { title:'宝宝', id:Math.random(), completed:false, } }) export default function FunctionalApp(){ const [todos,setTodos] = useState(todosInit) const addTodo = title=>{ setTodos([ ...todos, { title, id:Math.random(), completed:false, } ]) } }
useState() 传递状态初始值作为参数,返回一个数组。数组第一个元素为状态数据,第二个元素是修改状态的方法。
可能是异步的 与 class 中setState特性类似
const [todos,setTodos] = useState(todosInit)
Effect Hook 【面试】
模拟三个 class 组件的钩子
Effect Hook 可以让你在函数组件中执行副作用操作。
网络请求…
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
React.useEffect(回调函数,可选的依赖项)
/** * Effect Hook */ useEffect(() => { console.log('未传递第二个参数...') // 每次组件渲染,那会执行回调函数 }) // componentDidMount() useEffect(() => { console.log('传递第二个参数为空数组...') // 组件挂载执行一次回调函数 // componentWillUnmount() 面试重点 return () => { console.log('销毁') } }, []) // componentDidMount() + componentDidUpdate() useEffect(() => { // 组件挂载 及 依赖项 更新时执行回调函数 // 怎么让回调函数中初次不渲染? =》 添加开关 ifelse 判断是否为初次渲染 console.log('传递第二个参数为数组,添加 todos 依赖项...') }, [todos])
useCallback & useMemo
缓存函数 实现优化
函数组件内部用到局部函数的时候 可以使用useCallback实现函数缓存优化
不缓存 内存总是重新开辟存取数据,重复开辟浪费内存空间,需要缓存优化
useCallback(callback, deps) – 对 callback 回调函数进行缓存,当依赖项变化时,重新缓存回调函数
/** * 生成验证码 */ const getCode = useCallback( async ()=>{ /* 发起网络请求获得验证码 */ // getCaptcha().then(data=>{console.log('captcha:',data)}) let resault = await getCaptcha() const {captcha} = resault // 修改验证码 setCaptcha(captcha) },[])
useMemo(callback, deps) – 对 callback 回调函数的返回值进行缓存,当依赖项发生变化时,才重新计算需要缓存的数据。
函数组件里定义:
/** * 利用 useMemo() 来缓存头像下拉菜单。 * useMemo() 缓存的是回调函数的返回值。 */ const userMenu = useMemo(()=> ( // 4.20.0以后版本写法 items属性 <Menu items={[ { key: '/mine', label: ( <a target="_blank" rel="noopener noreferrer" href="/mine"> 个人中心 </a> ), }, ]} ></Menu> ),[])
useReducer
useState的替代方案
可以替代redux,很灵活,容易出现问题
React Router
官网
V5.x
包资源:
react-router:核心包
react-router-dom:用于DOM处理的包资源 文档对象模型(下载这个)
react-router-native:用于原生应用路由切换
react-router-config:路由配置相关的包(renderRoutes()、matchRoutes())
SPA
reactRouter实现单页面应用导航
/前端路由模式
【面试? 有无自己做过项目部署?了解路由模式】
hash:利用URL中 hash 值变化不会重新请求服务器html资源的特点,在 JS 中监测 hash 值的变化并动态切换对应组件的渲染。
history:利用 H5 中新增的 history API 方法(pushState() / replaceState()),当修改 history 记录时也不会向服务器发送新的网络请求。
安装
$ npm i react-router-dom@5.2.0 # 或 $ yarn add react-router-dom@5.2.0
实例
创建三个页面 16.X版本react
src>views>home.jsx src>views>about.jsx src>App.jsx 【最外层盒子】
index.js
/* 引入hashrouter */ import { HashRouter as Router} from 'react-router-dom' import App from './App' ReactDOM.render( <Router> <App/> </Router>, document.getElementById('root') ) -------------------------------------------------------------- 运行之后 地址栏会有 # http://localhost:3000/#/
渲染页面
相比 vue 更灵活
import React, { Component } from 'react' // 引入 link route import { Link, Route } from 'react-router-dom' // 页面组件 import Home from './views/home' import About from './views/about' import User from './views/user' // 复用头部导航 import Navbar from './components/nav-bar' export default class App extends Component { render() { return ( <> <div> <Link to="/home">首页</Link> | <Link to="/about">关于</Link> | <Link to="/users">用户管理</Link> | </div> // 三种方法渲染组件------------- <Route path="/home" component={Home}/> <Route path="/about" render={(props)=>{ // render方法渲染组件 return ( <> <About {...props}></About> <div>尾部</div> </> ) }}> </Route> <Route path="/users"> {/* 内嵌孩子节点渲染组件 */} <Navbar></Navbar> <User></User> </Route> </> ) } }
API
HashRouter
hash 路由模式
index.js: import React from 'react' import ReactDOM from 'react-dom' /* 引入hashrouter */ import { HashRouter as Router} from 'react-router-dom' import App from './App' ReactDOM.render( <Router> <App/> </Router>, document.getElementById('root') ) App.jsx: export default class App extends Component { render() { return ( <div>App <Link to="/home">首页</Link> <Link to="/about">关于</Link> <Link to="/users">用户管理</Link> </div> ) } } ---------------------------------------------------- 根据采用的 hash 路由模式来渲染link标签 地址:#/home
BrowserRouter
history 路由模式
index.js: import React from 'react' import ReactDOM from 'react-dom' /* 引入browserrouter */ import { BrowserRouter as Router} from 'react-router-dom' import App from './App' ReactDOM.render( <Router> <App/> </Router>, document.getElementById('root') ) App.jsx: export default class App extends Component { render() { return ( <div>App <Link to="/home">首页</Link> <Link to="/about">关于</Link> <Link to="/users">用户管理</Link> </div> ) } } ---------------------------------------------------- 根据采用的 history 路由模式来渲染link标签 地址:没有多余的 #
Link 链接
App.jsx: import { Link, Route,Redirect } from 'react-router-dom' render() { return ( <div>App <Link to="/home">首页</Link> <Link to="/about">关于</Link> <Link to="/users">用户管理</Link> </div> ) }
Route 路由配置
import { Link, Route,Redirect } from 'react-router-dom' render() { return ( <> <Route path="" component={} /> <Route path="" render={() => {return <></>}} /> <Route path=""> {/* 内嵌孩子节点渲染组件 类似vue 中的路由组件children:[]*/} <></> </Route> </> ) }
Redirect 重定向
import { Link, Route,Redirect } from 'react-router-dom' {/* 重定向 */} render() { return ( <Redirect to="/users"></Redirect> ) }
Switch
解决问题:当前页面 /user 刷新当前页之后 跳回重定向设置的 home 页面了
解决以上问题:switch 组件直接包裹路由 | 重定向组件 : 当前页面刷新还是当前页面 选择匹配
return ( <Switch> {/* 路由配置 */} <Route path="/home" component={Home}/> <Route path="/about" render={()=>{ // render方法渲染组件 return ( <> <About></About> <div>尾部</div> </> ) }}> </Route> <Route path="/users"> {/* 内嵌孩子节点渲染组件 */} <Navbar></Navbar> <User></User> </Route> {/* 重定向 */} <Redirect to="/users"></Redirect> </Switch> )
exact
地址栏和定义的 route 路径完全匹配 才会渲染页面
<route exact path='/home'></route>
嵌套路由
App.js: <Route path="/users"> {/* 内嵌孩子节点渲染组件 */} <Navbar></Navbar> <User></User> </Route> -------------------------------------------------- user.jsx: import { Link, Route, Switch } from 'react-router-dom' import UserEdit from './user-edit' // 子路由组件 import UserList from './user-list' // 子路由组件 export default class User extends Component { render() { return ( <> <div>用户管理</div> <div className='left'> <Link to="/users/list">查看用户列表</Link> <Link to="/users/edit">添加用户信息</Link> </div> <div className="right"> 右侧内容显示区域 {/* 嵌套路由 */} <Switch> <Route path="/users/list" component={UserList}></Route> <Route path="/users/edit" component={UserEdit}></Route> </Switch> </div> </> ) } }
属性 - 拿到history location match
history 做编程式导航 push goback
props.history.push(path)
location 拿到 ? 参数 location.search()
<route component={home}> 用props拿到 <route render={}> 用props拿不到 // 方法1: 可以调用 withRouter 这个 hoc 为login组件的属性注入history/location/match index.jsx组件 引入 withRouter from 'react-dom' export default withRouter(Login) // 方法2:推荐使用 hooks useHistory useLocation useParams // 方法3:参数props也可以传递参数 <Route path="/about" render={(props)=>{ // render方法渲染组件 return ( <> <About {...props}></About> </> ) }}>
match 动态路径参数 params
获取参数1:
export default function App() { return ( <> <div>App <Button type="primary">按钮</Button> </div> <Route path="/login" component={Login}></Route> </> ) } login.jsx ----- props拿到 export default function Login(props) { console.log('参数',props); return ( <div>Login登陆页面</div> ) }
获取参数2:
export default function App() { return ( <> <div>App <Button type="primary">按钮</Button> </div> {/* <Route path="/login" component={Login}></Route> */} <Route path="/login" render={ (props)=>{ // 可以拿到props参数 获取路由 return( <> <Login {...props}></Login> </> ) } }></Route> </> ) } login.jsx -------- export default function Login(props) { console.log('参数',props); return ( <div>Login登陆页面</div> ) }
获取参数3:推荐
<Route path='/login' render={ ()=>{ return ( <Login></Login> ) } }></Route> login.jsx---------------- 官网查看钩子函数如何使用 useHistory useLocation.... import { useHistory } from 'react-router-dom' export default function Login(props) { // console.log('参数',props); let history = useHistory() history.push(path) // 编程式导航 history.push(path?action=add) // 编程式导航 =?传参 console.log('useHistory',history); return ( <div>Login登陆页面</div> ) }
路由传参方式:
querystring : 利用 ? 传递查询字符串参数
path : 路径参数,利用 /:id 来定义参数,传递参数时直接以 / 分隔
动态路径参数获取 传参是:url:'/doctor/:id?' 获取是: Match.
路由封装
src/routes/index.js
/* 引入路由对应渲染组件 */ import Login from '../views/login' // 登陆页面 import FrameLayout from '../views/frame-layout' // 首页 import Dashboard from '../views/dashboard' // 指示板 import Doctor from '../views/hospital/doctor' // 医生管理 const routes = [ { path:'/login', component:Login }, { path:'/', component:FrameLayout, children:[ { path:'/dashboard', // component:Dashboard, render(){ return ( <> <Dashboard></Dashboard> <h2>指示板内容</h2> </> ) } }, { path:'/doctor', // component:Doctor, render(){ return ( <> <Doctor></Doctor> <h2>医生管理内容</h2> </> ) } } ] } ] export default routes
src/utils/render-routes.js
import { switch ,Route} from '-dom' function renderRoutes(routes){ return ( <switch> { routes.map(route=>( <Route path={route.path} // props传参用的 render={props=>{ if(route.render){ return route.render(props) } retrun <route.component {...props} }} )) } <switch> ) } export default renderRoutes
使用:index.jsx
import React from 'react' /** * 引入封装的路由表 */ import renderRoutes from './utils/render-routes'; import routes from './routes'; export default function App() { return ( <> {/* 一级路由渲染 */} { renderRoutes(routes) } </> ) }
src/layout/index.jsx
import React from 'react' import renderRoutes from '../utils/render-routes' export default function FrameLayout(props) { console.log('framelayout:',props); return ( <div> FrameLayout, 登录成功后进入的页面 左边是指示板 右边是对应路由渲染的子路由 {/* 二级路由在调用函数进行渲染 */} { renderRoutes(props.route.children) } </div> ) }
Redux :
类似于 vue 中的 vuex
状态管理器
集中式管理应用中的状态数据,以一种可预测的方式更新状态数据。
官网
中文文档
react-redux 绑定库官网
redux 与 react 没有任何直接关系,所以在 react 中要使用 redux 进行状态管理,通常还需要额外的绑定库:react-redux(https://cn.redux.js.org/)
概念
store:仓库,是一个容器,主要用于集中管理应用中的状态
state:状态,是各组件需要共享的状态数据
action:是一个普通对象,通常用于描述发生了什么。这个对象中一般有两个属性:type 和 payload。
action creator:action 创建函数,是一个函数结构函数体内部返回一个action对象,主要用于创建 action 对象,以实现 action 的复用。
reducer:是用于同步更新状态数据的纯函数。该函数以 state 和 action 作为参数,返回新的 state 状态数据。函数签名是:(state, action) => newState。
纯函数:
返回值依赖于参数
函数主体内部不产生任何副作用
类组件的render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互
Reducer 必需符合以下规则:
仅使用 state 和 action 参数计算新的状态值
禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)。(深克隆操作)
禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码
reducer 函数内部的逻辑通常遵循以下步骤:
检查 reducer 是否关心这个 action
如果是,则复制 state,使用新值更新 state 副本,然后返回新 state
否则,返回原来的 state 不变
dispatch:是一个函数,传递 action 作为参数,以调用到 reducer 进行状态更新(在组件中不能直接调用 reducer() 进行状态更新,注意:reducer() 函数的调用是封装在 dispatch() 函数体内部实现的。)
selector:是一个函数,用于从 store 的 state 状态树中提取片段。
三大原则
单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
State 是只读的
唯一改变 state 的方法就是触发 action(即调用 dispatch(action) 方法),action 是一个用于描述已发生事件的普通对象。
使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
深克隆
json.parse|| json.string (数组,对象)
递归 (数组,对象)
Immutable (facebook)与redux对接良好 (redux 先前技术)
lodash 三方库 考虑的数据非常丰富
lodash文档
yarn add lodash /** * 深克隆-三方库 */ import _ from 'lodash' /** 对 state 进行深克隆 */ const copyState = _.cloneDeep(state)
_. 调方法的是 lodash
$. 调方法是jquery
使用:
安装
$ npm i redux@4.1.2 react-redux@7.2.6 # 或 $ yarn add redux@4.1.2 react-redux@7.2.6
创建
创建reducers
创建 ./src/reducers 目录,定义 reducer 函数,如:
购物车状态数据管理:
src/reducers/cart.js
实现购物车状态数据管理
rx re 快代码提示快捷输入
/* 实现购物车状态数据管理 * rx 代码提示 */ /** * 引入actions type变量 */ import { ADD_TO_CARTS,REMOVE_FROM_CARTS} from '../actions/constants' /** * 深克隆-三方库 */ import _ from 'lodash' /* * 初始状态数据 * cart购物车数组 */ const initialState = { cart:[], } /** * reducer 函数,是一个纯函数,用于同步更新状态数据 * state 只能通过复制处理更新数据 * @param {*} state 旧状态数据 * @param {*} action action对象,有 type 和 payload 属性 * @returns 返回新的状态数据 */ const cartReducer = (state = initialState,{type,payload})=>{ /* 纯函数-避免副作用,所以需要深克隆 */ const copyState = _.cloneDeep(state) /** * 使用 switch 多分支选择,来判断当前 action 的动作类型 * 检查 reducer 是否关心这个 action,否则,返回原来的 state 不变 * 解决:由于匹配字符串 type 可能会不太方便,用变量来保存 */ switch (type) { // 添加商品操作'': case 'add_to_cart': copyState.cart.push(payload) return copyState; // 添加商品操作: case ADD_TO_CARTS: copyState.cart.push(payload) return copyState; // 删除商品操作: case REMOVE_FROM_CARTS: copyState.cart = copyState.cart.filter(prod=>prod.id!==payload.id) return copyState default: return state } } export default cartReducer
多个reducer 合并成一个 reducer:
src/reducers/index.js
类似 modules 将多个 reducer 合并成一个 reducer
将各独立的 reducer 合并为根 reducer,
redux : conbineReducers方法实现合并
如:
/** * 将多个独立的 reducer 合并为一个根 reducer */ import { combineReducers } from 'redux' import cartReducer from './cart' const rootReducer = combineReducers({ shoppingCart: cartReducer,// 类似命名空间 }) export default rootReducer
创建actions
同步action: 定义 action creator (action创建函数),用于创建 action 对象(同步action)on :
异步action ( 同步action 和网络请求结合起来 ) :通过特定的中间件, action creator 返回 action对象 + 函数
创建 ./src/actions 目录,定义 action creator(action创建函数),如:
src/actions
action creator
src/actions/constants.js
action type 取值 :由于匹配字符串可能出现差错,可以用常量储存
export const ADD = Symbol('add_to_cart')
src/actions/cart.js
用于创建添加到购物车时使用的 action 对象
/** * 定义 action creator (action创建函数),用于创建 action 对象(同步action),以便于复用 action 对象 */ import { ADD_TO_CART } from "./constants" /** * 用于创建添加到购物车时使用的 action 对象 */ export const addToCartAction = (product) => { return { type: ADD_TO_CART, payload: product, } }
异步action => 使用 **redux-thunk **中间件
npm install redux-thunk yarn add redux-thunk
redux-saga 结合异步任务实现状态更新
用到 es6 生成器函数 generator()
避免在组件中请求-发送异步-逻辑不紧密
直接在状态更新的时候。请求网络-状态更新-逻辑更紧密-中间件的面试
actions/user.js export const loginAsyncAction =(user)=>{ // 闭包 // 返回的函数,自动被redux-thunk中间件调用执行。 return (dispatch)=>{ // 异步逻辑 login(user).then() } } --------------------------------------------------- /**异步 action * 结合网络请求的API与同步的action创建函数。 * 这仍然是一个 action creator 创建函数。 */ export const loginAsyncAction = user => { /** * 返回的这个函数,会自动被 redux-thunk 中间件调用执行。 * 在调用执行这个函数时,会自动传递 dispatch 作为参数。 * 不要求返回的这个函数保持纯净(即可以包含异步逻辑) */ return async dispatch => { const resault = await login(user) // 登录成功或失败后,要更新状态数据 if (resault.status === 200) { // 登录成功 dispatch(loginAction(resault)) return true } else { // 登录失败 dispatch({ type: LOGIN_FAILED }) return false } } } usage:npmjs
store.js: import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import rootReducer from './reducers/index' const store = createStore(rootReducer, applyMiddleware(thunk))
store.js: import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import rootReducer from './reducers/index' const store = createStore(rootReducer, applyMiddleware(thunk))
创建 Store
创建 ./src/store 目录,定义 Store:
getState()
返回当前store当中最新的状态数据
store.getState().user.token
createStore 方法
src/store/index.js
/** * 创建 Store 仓库 */ import { createStore } from 'redux' import rootReducer from '../reducers' /** * 基于根 reducer 来创建 store */ const store = createStore(rootReducer) export default store
--------------------------将store用到react中----------------------
react组件连接 Redux 的 Store
安装绑定库
yarn add react-redux@7.2.6
Provider ===二次封装的context provider
src/index.js
import React from 'react' import ReactDOM from 'react-dom' /* 引入App */ import App from './App' /** * 引入 react-redux中的provider :二次封装的context provider * 让每个react组件都可以使用到store */ import { Provider} from 'react-redux' /* 引入store */ import store from '../src/store' /** * Since any React component in a React Redux app can be connected to the store, most applications will render a <Provider> at the top level, with the entire app’s component tree inside of it. */ ReactDOM.render( <Provider store={store}><App/></Provider>, document.getElementById('root') )
componnets/list.jsx
component/cart.jsx
创建组件
使用Redux 的 store
1.connect方法 连接store
import { connect } from ‘react-redux’
connect() 函数的主要作用是:在 React 组件中连接 redux 的 store。
connect() 调用后,返回的是 HOC。
connect(mapStateToProps,mapDispatchToprops)
mapStateToProps 是一个函数,可将 store 中的 state 状态数据映射到组件的 props 属性中。
该函数传递 state 作为参数,返回一个普通对象。返回的对象中各属性会被合并到组件的 props 中
mapDispatchToProps 也是一个函数,将 store 的 dispatch API方法映射到组件属性中使用。
该函数传递 dispatch 作为参数,返回一个普通对象,返回的对象中应该是一个个的方法。
/** * action */ import { removeFromCartAction } from '../actions/cart' import React, { Component } from 'react' /** * connect方法 */ import { connect } from 'react-redux' /** * mapStateToProps 是一个函数,可将 store 中的 state 状态数据映射到组件的 props 属性中。 * 该函数传递 state 作为参数,返回一个普通对象。返回的对象中各属性会被合并到组件的 props 中。 * mapStateToProps() 会订阅 Store 的更新,即当 store 有更新时,mapStateToProps() 就会 * 被调用,即组件会监听到这种更新,然后实现视图的响应式渲染。 */ const mapStateToProps = state=>{ return { cart:state.shoppCart.cart } } /** * mapDispatchToProps 也是一个函数,将 store 的 dispatch API方法映射到组件属性中使用。 * 该函数传递 dispatch 作为参数,返回一个普通对象,返回的对象中应该是一个个的方法。 * 这些方法内部应该是 dispatch(action) 实现 store 中状态数据的更新, * 返回对象中的方法也会被合并到组件的 props 属性中。 */ const mapDispatchToProps = dispatch =>{ return { remove:id=>{ const action = removeFromCartAction(id) dispatch(action) } } } const hoc = connect(mapStateToProps, mapDispatchToProps) class CartClass extends Component { render() { console.log('connect连接的方法:cart',this.props); const {remove,cart} = this.props return ( <div> <h2>小兰的购物车:</h2> <ul> { cart.map(prod=>{ return ( <li key={prod.id}> 小兰喜爱的:{prod.title}--{prod.price}--{prod.num} <button onClick={()=>{remove(prod.id)}}>不喜欢</button> </li> ) }) } </ul> </div> ) } } export default hoc(CartClass)
2.函数组件的hooks方法 连接store
useSelector() 【推荐使用函数hooks】
useDispatch()
import { useSelector,useDispatch } from ‘react-redux’
// useSelector => state 获取数组数据
// useDispatch => dispatch 获取方法
src/componrnts/cart.jsx
import React from 'react' /** * useSelector 获取state * useDispatch 获取action */ import { useSelector,useDispatch } from 'react-redux' /** * 引入dispatch用的 action */ import { removeFromCartAction } from '../actions/cart' export default function Cart() { /** * 调用useSelector 的hook函数 * 从store的state中获取购物车数组数据 */ const cart = useSelector(state=>state.shoppCart.cart) /** * 调用 useDispatch() 的 hook函数, * 从 store 中获取dispatch()函数的引用 */ const dispatch = useDispatch() /** * 移除当前商品 */ const handleRemove = (id)=>{ dispatch(removeFromCartAction(id)) } return ( <div> <h2>小兰的购物车:</h2> <ul> { cart.map(prod=>{ return ( <li key={prod.id}> 小兰喜爱的:{prod.title}--{prod.price}--{prod.num} <button onClick={handleRemove.bind(this,prod.id)}>不喜欢</button> </li> ) }) } </ul> </div> ) }
Redux-logger 【中间件】
查看是否调用reducer成功?
记录状态更新前后的信息 - console.log
redux-logger 三方包 控制台操控日志
react devtools
store.index.js import { applyMiddleeare,createStore} from 'redux' /* 使用中间件redux-logger 实现调试redux状态变更状态追踪 */ import logger from 'redux-logger' /** * 基于根 reducer 来创建 store */ const store = createStore(rootReducer,applyMiddleware(logger,thunk)) export default store
Ant Design 组件库使用
介绍
Ant design charts 图表
Ant Design Pro 后台
Ant Design Pro Component 文档说明
Ant Design Mobile 移动端
ant design 组件库4.x版本
3/4年前来项目 3.x版本
医疗管理系统
技术栈
create-react-app
react.js
react-router
redux react-redux
antd
axios
echarts
wang-editor 富文本编辑器
xlsx 导出表格
es6
搭建项目结构
创建结构
利用脚手架创建基本结构
npx create-react-app 项目名称 yarn create react-app 项目名称 yarn # 安装依赖包
引入 UI 组件库
官方文档
yarn add antd
自定义的组件中引入 antd 组件测试
src/App.jsx
rfc /* * 引入组件测试 */ import { Button } from 'antd'; export default function App() { return ( <div>App <Button type="primary">按钮</Button> </div> ) }
src/index.js
引入antd css样式 /* 引入样式 */ import App from './App' import 'antd/dist/antd.css' ReactDOM.render( <App/>, document.getElementById('root') )
医疗管理系统
技术栈:create-react-app + react.js + react-router + redux + react-redux + antd + axios + echarts + wang-editor + xlsx + ES6 Modules + …
后端接口文档:https://www.quanzhan.site:8889/swagger/
1. 搭建项目结构
创建项目
利用脚手架来创建项目的基本结构
$ npx create-react-app 项目名称 # 或 $ yarn create react-app 项目名称 yarn add
引入 UI 组件库
安装
$ yarn add antd
自定义的组件中引入 antd 组件测试
import { Button, } from 'antd' import 'antd/dist/antd.min.css' export default function App() { return ( <div> App <div> <Button>antd按钮</Button>高级配置 </div> </div> ) }
高级配置
$ yarn add @craco/craco --dev
修改 package.json 中的 scripts 脚本:
{ "scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" }, }
在项目根目录下创建 craco.config.js 文件:
module.exports = { devServer: { // 开发服务器配置 webpack-dev-server port: 10086, // 修改默认监听的端口 proxy:{ // 后端的开发人员多个接口,需要在这里做配置!整理以下 } }, }
反向代理
module.exports = { publicPath: '', devServer: { port: 3000, host: 'localhost', open: true, proxy: { // 反向代理 '/zs': { // 当访问类似 /zs/api/tabs 接口资源时,就会进入这个代理 target: 'http://192.168.100.37:5000', // 目标服务器 http://192.168.100.37:5000/zs/api/tabs changeOrigin: true, // 跨域 pathRewrite: { '^/zs': '', // http://192.168.100.37:5000/api/tabs }, }, '/ls': { target: 'http://192.168.100.92:3000', changeOrigin: true, pathRewrite: { '^/ls': '', }, }, '/mobile': { target: 'https://shopapi.smartisan.com', // 'https://shopapi.smartisan.com/mobile/waterfall' changeOrigin: true, }, }, }, }
定制主题
需要修改 antd less 文件中的变量,则先安装包:
const CracoLessPlugin = require('craco-less') module.exports = { plugins: [ { plugin: CracoLessPlugin, options: { lessLoaderOptions: { lessOptions: { modifyVars: { // 修改 less 变量 '@primary-color': '#1DA57A', }, javascriptEnabled: true, }, }, }, }, ], devServer: { // 开发服务器配置 webpack-dev-server port: 10086, // 修改默认监听的端口 }, }
将antd 样式引入修改为
将antd 样式引入修改为
冷知识
首页仪表盘 ‘dashboard’ / ‘home’ / '/ ’
一般三方包放在顶部引入 自定义模块后面引入
antd4.x版本的图标库自动下载好了,之前版本需要下载包
2. 路由封装
3. redux引入
4. axios配置
5. less样式
layout/index.jsx
<Layout className='frame'> // 命名空间 <Sider trigger={null} collapsible collapsed={collapsed}> <div className="logo" /> <Menu> <Layout >
layout/index.less
对应文件夹写各自的less样式 让其只在当前组件中 样式生效 .frame{ // 类似命名空间 height: 100%; padding: 0 24px; .trigger { font-size: 18px; line-height: 64px; cursor: pointer; transition: color 0.3s; }
6. 动态路由加载
根据路由动态更新仪表盘组件
点击路由菜单实现编程式导航
编程式导航,每次点击重复调用函数,性能差 => useCallback
对回调函数设置缓存 ( 优化 )
7. 登录
为什么要用redux,项目只需要token存储,可以直接本地保存
当前项目还需要 动态路由 | 用户角色 … 等状态数据需要用到 redux
login.jsx
引入图片资源问题:
<img src='../../assets/female.svg'/> // 渲染的时候http://localhost:10086/assets/female.svg 找不到
src写相对路径出不来
引入模块 设置 Es6 或者 require(‘nodejs需要额外处理’)
/* 引入图片资源 */ import female from '../../assets/femal.svg' <div className='female'> <img src={female} alt="祝你身体健康"/> </div> // 模块化开发
栅格系统 flex 布局 一行分成24列
验证码渲染
类型很多:拖拽验证码 图片拼图 旋转 字符 认证事物…
//生成验证码接口 <Input placeholder="请输入验证码" addonAfter={( <div // 网络请求返回标签 - 渲染原生html标签 dangerouslySetInnerHTML={ {__html:captcha}} onClick={getCode} //点击发起新的请求,更换验证码图片 ></div> )} onClick={getCode} />
/* 引入网络请求 */ import {getCaptcha } from '../../api/use' /*** 生成验证码*/ const getCode = async ()=>{ /* 发起网络请求获得验证码 */ // getCaptcha().then(data=>{console.log('captcha:',data)}) let resault = await getCaptcha() const {captcha} = resault console.log(captcha); // 修改验证码 setCaptcha(captcha) } /** * 加载验证码componentDidMount() * 组件挂载执行一次 - 进入页面就得渲染出来 需要useEffect钩子 */ useEffect(()=>{ getCode() },[])
验证码失效问题:
验证码校验 withCredentials
/* 引入网络请求 */ import { verifyCaptcha } from '../../api/use' /** * 输入的验证码是否正确 * 输入框做失焦事件 */ const verifyCode = async (event)=>{ const code = event.target.value console.log('code验证码:',code); const resault = await verifyCaptcha(code) console.log(resault); } -------------resault[控制台日志]-------------- {status: 4005, message: '验证码已失效'}
服务器需要判断哪个用户用的哪个验证码?
获取验证码的时候,服务器发送 cookie到响应头中给客户端,跟踪用户信息,校验时候,拿到cookie携带发送回去?【withCredentials】
服务器知道 对应用户的对应验证码 【cookie来区分】
发送网络请求-跨域情况下-cookie数据不会自动携带在请求头
跨域请求情况下需要将cookie携带在请求头中发送过去用到【withCredentials】
验证码 通过session 会话机制实现的
发送网络请求的时候,不管是fetch or axios 中都可以使用【withCredentials】
axios – withCredentilas= true 跨域站点请求,默认值是false
**XMLHttpRequest.withCredentials** 属性是一个 Boolean 类型,它指示了是否该使用类似 Cookies、Authorization Headers (头部授权) 或者 TLS 客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。在同一个站点下使用 withCredentials 属性是无效的。
此外,这个指示也会被用做响应中 Cookies 被忽视的标示。默认值是 false。
如果在发送来自其他域的 XMLHttpRequest 请求之前,未设置withCredentials 为 true,那么就不能为它自己的域设置 Cookie 值。而通过设置 withCredentials 为 true 获得的第三方 Cookies,将会依旧享受同源策略,因此不能被通过 document.cookie 或者从头部相应请求的脚本等访问。
后端接受cookie需要做配置,因为cookie传数据很危险容易受到攻击
request.js /** * 创建 axios 实例 */ const service = axios.create({ baseURL, timeout:10000, // 超时时间 withCredentials:true, // 允许跨域时向后端传递cookie }) 设置此项,验证码失效问题就解决了
用户登录:
前端给后端发送密码需要MD5加密后在发送
yarn add crypto-js
crypto-js包-实现Md5加密
cryptoJs.MD5().toString()
use.js: /* 引入加密工具 */ import CryptoJS from 'crypto-js' /** * 用户登录,像后端发送用户密码时,需要经过MD5加密 */ export const login = values=>{ const {username,password,captcha} = values // 对密码进行加密处理 const hash = CryptoJS.MD5(password).toString() return request({ method:'post', url:API.LOGIN_API, data:{ username, password:hash, captcha } }) }
账号密码 admin1-200
123456
异步action实现-逻辑紧密处理
登录成功失败提示
dispatch 拿到返回的值 登陆成功返回true |登陆失败返回 false
/**异步 action * 结合网络请求的API与同步的action创建函数。 * 这仍然是一个 action creator 创建函数。 */ export const loginAsyncAction = user => { /** * 返回的这个函数,会自动被 redux-thunk 中间件调用执行。 * 在调用执行这个函数时,会自动传递 dispatch 作为参数。 * 不要求返回的这个函数保持纯净(即可以包含异步逻辑) */ return async dispatch => { const resault = await login(user) // 登录成功或失败后,要更新状态数据 if (resault.status === 200) { // 登录成功 dispatch(loginAction(resault)) return true } else { // 登录失败 dispatch({ type: LOGIN_FAILED }) return false } } } ---------- const onFinish =async (values) => { // console.log('Onfilnish:登录', values); /* 异步action-将请求放进action然后实现数据更新 */ const resault = await dispatch(loginAsyncAction(values)) if(!resault){ //登陆失败 message.error('用户名或密码错误') } }
首页实现权限拦截:
用户未登录,跳转登陆页面
/* 获取用户token - 如没有跳回登陆页面 */ const token = useSelector(state=>state.user.token) /* 布局容器 */ !token?<Redirect to='/login'/>: <Layout className='frame'/>
优化
函数组件内部用到局部函数的时候 可以使用useCallback实现函数缓存优化
useMemo 实现函数返回值缓存
不缓存 内存总是重新开辟存取数据,重复开辟浪费内存空间,需要缓存优化
/** * 生成验证码 - useCallback优化 */ const getCode = useCallback( async ()=>{ /* 发起网络请求获得验证码 */ // getCaptcha().then(data=>{console.log('captcha:',data)}) let resault = await getCaptcha() const {captcha} = resault // 修改验证码 setCaptcha(captcha) },[])
axios二次封装
请求拦截
/** * 请求拦截 */ service.interceptors.request.use(config=>{ // 请求头中携带token数据 - 需要注意后端配置 config.headers['Authorization'] = 'Bearer '+ store.getState().user.token return config },error=>{ return Promise.reject(error) })
响应拦截
token异常清空数据
request.js import { RESET_USER } from "../constants/actions" import store from '../store' //拿到token调取action /** * 响应拦截 */ service.interceptors.response.use(resData=>{ /** * response.status 为 HTTP 状态码 */ if(resData.status>=200 && resData.status<300){ /** * 获取响应数据,处理前后端接口规范内容 */ const { code, data} = resData.data if(code === 200){ return data } // 当token 有异常的时候,应该清空状态store 中的数据,跳转到登录页面重新登录 if(code>=5000){ store.dispatch({type:RESET_USER}) window.location.reload() //刷新当前页面 } }else{ const err = new Error('请求接口数据有误...') return Promise.reject(err) } },error=>{ return Promise.reject(error) })
请求拦截 store.getState().user.token :始终是最新的token
响应拦截:如果code>=5000清空store保存的用户状态数据 store.dispatch()
window.location.reload() 刷新页面 -代码自动刷新
拿路由拿不进来 hook只能在自定义hook和函数组件能用
8. 页面书写
页面标题获取
props数组遍历===location.pathname 寻找当前路由,渲染对于标题
表格书写
dataindex:支持数组查询
嵌套查询–[ ‘district’,‘name’ ]
render 复杂渲染函数 render:(job)=>{ {job.name} }
可选链运算符:?. 空值合并:?? department.name??‘无’
表格选中数据获得 -(批量删除)
获得选中的所有数据 rowSelection -选中奇数行-偶数行-反选
onChange( function(selectedRowKeys, selectedRows, info: { type }){})
…args ES6使用剩余运算符 箭头函数没有自己的arguments
要想使用可以 ...args
删除按钮-绑定点击事件 rowSelection={{ type:'checkbox', /* ...args=selectedRowKeys, selectedRows, info: { type } 1:当前选中项的key值(绑定id) 2:当前选中项对象 3:点击的框是全选框|还是每项的单选框 */ onChange:(...args)=>{ console.log('选中项的roeSelection...',args); } }} ------------------------------------------------ rowSelection={{ type:'checkbox', /* ...args=selectedRowKeys, selectedRows, info: { type } 1:当前选中项的key值(绑定id) 2:当前选中项对象 3:点击的框是全选框|还是每项的单选框 */ onChange:(selectedRowKeys,selectedRows)=>{ // 选中的id setSelectedRowKeys(selectedRowKeys) // 选中的对象 setSelectedRows(selectedRows) console.log('选中项',selectedRows); } }}
导出功能 Excel 【三方库-xlsx】
原来-后端处理 -react使用–sheetjs.js
前端-excel识别的格式,三方包-xlsx
yarn add xlsx
// 全部引入xlsx import * as XLSX from 'xlsx' // 按需引入需要的方法 import { read, utils, writeFileXLSX } from 'xlsx'; react/sheetjs.js: handleFile实现导入 const handleFile = (file) => { const reader = new FileReader(); reader.onload = (e) => { /* Parse data */ const ab = e.target.result; const wb = XLSX.read(ab, {type:'array'}); /* Get first worksheet */ const wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; /* Convert array of arrays */ const data = XLSX.utils.sheet_to_json(ws, {header:1}); /* Update state */ setData(data); setCols(make_cols(ws['!ref'])) }; reader.readAsArrayBuffer(file); } exportFile实现导出: const exportFile = () => { // 构建待导出的数据,数据是一个二维数组结构 const data = [ [’编号‘,’姓名‘,’科室],//第一行中表头的数据, //[1,'xialan','皮肤科'],//对应数据值 ...doctors.map(doc=>{ const {id,name,keshi}=doctor return [id,name,keshi] }) ] /* convert state to workbook */ // 将待导出的数据添加到一张工作表中 //ws workSheet const ws = XLSX.utils.aoa_to_sheet(data); //wb 创建新工作簿 const wb = XLSX.utils.book_new(); //在工作簿中追加工作表 apend内部末尾追加, ’工作表名‘ XLSX.utils.book_append_sheet(wb, ws, "SheetJS"); /* generate XLSX file and send to client */ // 将内存中工作簿的内容写入文件并发送到客户端(下载) XLSX.writeFile(wb, "doctors文件名.xlsx") //医生信息$(new Data().toISOString()) };
一个excel就是工作簿
默认三个 sheet 工作表
行和列的交叉点叫做单元格
表单书写
添加功能
表单onFinish事件
<Form onFinish={saveDoctor}> {/* 提交按钮 */} <Form.Item <Button type="primary" htmlType='submit'>保存</Button> </Form.Item> </Form>
提交 表单且数据验证成功后回调事件 onFinish() 参数为表单所有信息
所以按钮 需要设置为 submit 提交类型 才会触发 onFinish 事件htmlType=‘submit’
表单初次渲染
需要初次渲染获取多项网络请求的信息 - 如下花费很长时间,异步
useEffect( ()=>{ async function getInitialData () { let data = await getDistricts() let data1 = await getJobs() let data2 = await getDepartments() console.log(data,data1,data2); } getInitialData() },[])
promise.all方法
/**
* Promise.all() 会(并行)同时发送四个网络请求,
* 当四个请求都成功时,返回的 Promise 对象才是成功状态(fulfilled),
* 当任意一个请求失败时,返回的 Promise 对象都是失败状态(rejected)。
*
useEffect( ()=>{ async function getInitialData () { let data = Promise.all([getDistricts(),getJobs(),getDepartments()]) console.log(data); } getInitialData()
prommise.allSettled()
* Promise.allSettled() 与 all() 类似,但返回的结果数组中,每个
* 对象有 status 属性表示对应的 Promise 对象状态是 fulfilled 还是 rejected。
* allSettled() 并不会因为任意一个 Promise 失败而导致整个结果失败。
*/
let data = Promise.allSettled([getDistricts(),getJobs(),getDepartments()])
calendar 组件
日期默认时间语言为 en-US,所以如果需要使用其他语言,
推荐在入口文件全局设置 locale
//日期处理时间 moment.js库 //moment.js 库的语言包,在使用如 DatePicker 组件时会用到 import 'moment/locale/zh-cn';
axios发送请求的时候,自动将数据(根据传的数据是formData还是普通文本)进行转化
upload组件
当上传图片有单独的接口的时候-有action属性
上传的地址-有action属性自动封装好 formData 格式数据,
上传-单独接口,服务器会直接把图片地址返回
<Upload name="avatar" listType="picture-card" //上传列表的内建样式,支持三种基本样式 className="avatar-uploader" showUploadList={false} //上传图片可以有多涨,关闭显示 action="https://www.mocky.io/v2/5cc8019d300000980a055e76" //上传的地址 beforeUpload={beforeUpload} // 上传前判断是否符合格式和转化操作 onChange={handleChange} //上传中、完成、失败都会调用这个函数。 > >
没有单独的上传接口的时候,去掉action属性 - 手动上传实例
去掉action,会有404报错
// 不自动提交到 action 的路径资源进行处理,
需要手动提交时,return false
<Upload beforeUpload={beforeUpload}> /** * 上传文件前处理 */ const beforeUpload = (file) => { // 不自动提交到 action 的路径资源进行处理, // 需要手动提交时,return false return false; };
上传文件数量
normFile 中的 e 会返回两个 file | fileList 单文件还是文件列表
只需要上传一张图片 就返回 file
maxCount
const normFile = (e) => { console.log('Upload event:', e); if (Array.isArray(e)) { return e; } // return e?.fileList;//fileList 是文件列表,当允许多文件上传时可使用 return e?.file }; ------------上传图片组件 <Form.Item name="upload" label="Upload" // valuePropName="fileList" //单文件 valuePropName="file" //单文件 getValueFromEvent={normFile} extra="longgggggggggggggggggggggggggggggggggg" > <Upload maxCount={1} //上传文件数量限制 name="logo" action="/upload.do" listType="picture"> <Button icon={<UploadOutlined />}>Click to upload</Button> </Upload> </Form.Item>
const normFile = (e) => { console.log('Upload event:', e); if (Array.isArray(e)) { return e; } /** 手动调用 base64 这样 图片才会显示出来-------- * 将上传文件转换为 base64 格式。 * getBase64() 传递上传的图像文件与回调函数作为参数。 * 转换为 base64 之后的字符串会作为回调函数的参数传递. */ getBase64(e?.file,url=>{ setImageUrl(url) // console.log('base64',url); }) // return e?.fileList;//fileList 是文件列表,当允许多文件上传时可使用 return e?.file };
参数为formData格式
// 将表单中的数据保存到 FormData 中 const formData = new FormData() //fromData无法直接打印,需要遍历迭代的方法 // 将doctor中的每个属性添加到 //object.keys 取得对象每一个遍历迭代的属性,属性名 Object.keys(doctor).forEach(key => { if (doctor[key]) { formData.append(key, doctor[key]) } }) //添加医生--网络请求 const data = await addDoctor(formData) console.log('dic...',data);
表单信息转化成接口需要的类型数据
日期DatePicker组件‘ moment文档
》日期(返回的是个moment对象)转化成字符串
表单信息转化成接口需要的类型数据 日期DatePicker组件‘ moment文档 》日期(返回的是个moment对象)转化成字符串
表单中日期选择对象得到的是 moment 对象 birthday: Moment
moment对象=》moment.format(‘YYYY-MM-DD’) doctor.birthday = values.birthday?.format('YYYY-MM-DD')
随机颜色生成:
新增|修改信息 (公用一个页面)
弹出新页面 | 跳到新页面
doctor/edit.jsx 配置路由
文件上传信息
base64一种基于64个可打印字符来表示二进制数据的方法(编码格式)4
图片转化成base64的方法函数:页面中显示出来
后端要求参数数据是 formData格式【key value】: 【还有?query,params】
//keys返回一个包含所有键的iterator对象。
multipart/form-data ??
上传文件(二进制流数据)的时候,需要请求头设置multipart/form-data
const formData = new FormData() // formData.append()方法 // 对象遍历迭代 Object,keys(object).forEach(key=>{ ... })
formData无法直接打印 | 可以用遍历的方法 formData.keys()
表单提交信息
进入表单页面,需要初次渲染获取多项网络请求的信息 - Promise.all
手动实现图片上传 - 【复杂】
一般上传图片:有对应的接口,上传成功后交给组件的action方法直接
upload组件 – 有action属性自动封装好 formData 格式数据,显示给用户----返回网络请求的地址,然后再整合其他所有选项,网络请求添加信息
1
2
如果后台没有对应的图片接口,需要手动处理 -
moment.js库 日期处理类库
表单中日期选择对象得到的是 moment 对象 birthday: Moment
日期(返回的是个moment对象)转化成字符串
moment对象=》moment.format(‘YYYY-MM-DD’)
doctor.birthday = values.birthday?.format('YYYY-MM-DD')
随机颜色生成:
string.padStart () // 填充
string.padEnd(6,‘0’) //1参数:字符串的总长度为6,不够的话填充0修改信息
修改方式:与添加医生公用同一个页面
1.确定=》点击的按钮是添加还是修改-传参
是新增医生:action=create
<Button onClick={()=>{ /*编程式导航实现路由传参是添加还是修改*/ props.history.push('/hospital/doctor/edit?action=create') }}>新增医生信息</Button>
路由 | localStorage | …
?传参history.push(‘/adimin?action=add’)
拿到参数=》location.search
{/* 修改button:公用编辑页面-路由传参-action:update id:修改的医生是哪个*/} <Button type="primary" onClick={()=>{ props.history.push('/hospital/doctor/edit?action=update&id'+record.id) }} >修改</Button>
传参 医生id 根据医生id查询详情网络请求
三方包:qs(queryString)
import qs from 'qs'
1
自己传参的时候是字符串,拿到对应值需要自己封装转化为对象结构
qs包提供方法-转成字符串内容Stringfy,转成对象 qs.parse(‘字符串’,{ignoreQueryPrefix:‘true’})
useEffect
/* 挂载获取location参数 */ useEffect(()=>{ var prefixed = qs.parse(location.search, { ignoreQueryPrefix: true }); setAction(prefixed) //可能是异步的 // 修改医生:获取当前医生id const getDoctorData= async function(){ if(prefixed.action==='update'){ console.log('获取医生id',prefixed.id); // 根据id获取医生信息 let data = await getDoctor(prefixed.id) console.log('根据id获取医生信息:',data); } } getDoctorData() },[])
.回填:【数据处理逻辑匹配】
1-formInstance (from表单的实例) 方法 setFieldValue() 整个表单实现回填
2-如何拿到表单实例 useForm 创建实例,管理所有数据状态
//经 Form.useForm() 创建的 form 控制实例,不提供时会自动创建 const [editForm] = Form.useForm(); //解构数组 <Form from={editForm}> // 将查询到的数据回填到表单中 editForm.setFieldsValue(data)
拿到医生数据对象转换为能够回填表单数据的结构
出生日期:拿到的是字符传,回填需要moment对象,转换需要moment方法
科室:拿到对象数据-转换成级联中需要的数组结构处理
头像渲染:
import moment from 'moment' // react已经有moment不需要yarn add moment('字符串') // 返回的就是moment对象 //data.birthday 为字符串=》moment对象 data.birthday = moment(data.birthday) ....
3-修改信息
修改医生信息接口(修改的formData,id)
点击保存按钮判断调用更新还是添加接口
添加医生id –
表单中添加表单项:formItem (不显示idhidden隐藏 )- 回填-保存时回拿到
formData.get(‘id’) //调用get方法,拿到对应的key值
-优化-修改的时候用户名禁用
/** * 修改医生 * *@param doctor 是 FormData 格式的数据,在请求主体中发送给后端 */ export const updateDoctor =(doctor)=>{ return request({ // eslint-disable-next-line no-useless-concat // formData.get() 获取对应key的value url:API.DOCTOR_ADD_API + '/' + doctor.get('id'), method:'post', data:doctor }) }
富文本编辑器
summernote 先来说一个我绝对不推荐的富文本。这是一个韩国人开源的富文本(当然不推荐的理由不是因为这个),它对很多富文本业界公认的默认行为理解是反起到而行的,而且只为用了一个dialog的功能,引入了boostrap,一堆人抗议就是不改。格式化也是差劲。。反正不要用!不要用!不要用!
ckeditor ckeditor也是一家老牌做富文本的公司,楼主旧版后台用的就是这个,今年也出了5.0版本,ui也变美观了不少,相当的不错,而且它号称是插件最丰富的富文本了。推荐大家也可以试用一下。
quill 也是一个非常火的富文本,长相很不错。基于它写插件也很简单,api设计也很简单。楼主不选择它的原因是它对图片的各种操作不友善,而且很难改。如果对图片没什么操作的用户,推荐使用。
medium-*editor* 大名鼎鼎的medium的富文本(非官方出品),但完成度还是不很不错,拓展性也不错。不过我觉得大部分用户还是会不习惯medium这种写作方式的。
Squire 一个比较轻量的富文本,压缩完才11.5kb,相对于其它的富文本来说是非常的小了,推荐功能不复杂的建议使用。
wangEditor 一个国人写的富文本,用过感觉还是不错的。不过毕竟是个人的,不像专门公司做富文本的,配置型和丰富性不足。前端几大禁忌就有富文本 为什么都说富文本编辑器是天坑?,不过个人能做成这样子很不容易了。
百度UEditor 没有深入使用过,只在一个angular1X的项目简单用过,不过说着的ui真的不好看,不符合当今审美了,官方也已经很久没跟新过了。
作者:花裤衩链接:https://juejin.cn/post/6844903481224986638来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
使用实例
wangEditor很简单–
1-安装
react
@wangeditor/editor 和 @wangeditor/editor-for-react
1
2
实例copy
图表
1-useRef:定义 ref 对象,拿到dom节点【react.crateRef】
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
// 拿到ref 节点 - 挂号 const registrationRef = useRef([]) <div className="guahao" ref={registrationRef}></div> => registrationRef.current 拿到当前 div 节点 //初始渲染 -- useEffect(()=>{ async function getStatic(){ let data = await getStatistics() console.log('首页数据:::',data); setOverview(data.overview) setRegistration(data.registration) //挂号数组 if(!data.registration){ return } /* 挂号图 */ // x-y 轴对应的值 const x = data.registration.map(reg=>reg.date) const y = data.registration.map(reg=>reg.value) //渲染挂号的节点dom const registrationChart = echarts.init(registrationRef.current) const option = { title:{ text:'近7日挂号预约情况', textStyle:{ fontWeight:'bolder', } }, tooltip:{ formatter:'{a}{b}-{c}' // {a} --series.name // {b} -- x轴数值 // {c} -- y轴数值 } , xAxis: { type: 'category', data: x, // x轴旋转 axisLabel: { rotate: 45, // 旋转30度 show:true, interval:0, //强制显示所有标签 textStyle: { fontSize: '12' // x轴字体大小 }, }, }, yAxis: { type: 'value', }, series: [ { data:y, name:'guahao', type: 'bar',// 树状图 showBackground: true, backgroundStyle: { color: 'rgba(180, 180, 180, 0.2)' }, itemStyle: { // 树状图显示数值 normal: { label: { show: true, //开启显示 position: 'top', //在上方显示 textStyle: { //数值样式 color: 'black', } } } }, } ] } registrationChart.setOption(option) } getStatic() },[])
canvas绘制学习
写一个原生的拖拽效果+裁剪
x轴标签倾斜处理
2-实时数据显示渲染:
a.如果服务端时用的原生写的 — 前端需要用: webSocket
send() 发数据
b.服务端用的socket.io — 前端用socket – 三方库-socket.io - 负责客户端
》通过事件机制,发送数据 emit() 触发事件- on() 接收数据注册事件
即时聊天技术 不得行 做不了-
yarn add socket.io-client 客户端安装
调 io参数为地址服务器 url https http => socket.io()
注册事件监听 调用 on 方法 socket.io() 返回的是JSON字符串
组件销毁-跳转其他页面时候-socket也需要将实时数据停掉
socket.close() // 组件销毁时关闭cocket连接
闭包会延长引用变量的使用时间
socket = null // 垃圾回收机制:
echarts return ()=>{
echarts.dispose() // 销毁资源
echarts变量 = null
}
yarn add socket.io-client@版本号 //soket.io import { io } from "socket.io-client"; // 实时挂号 useEffect(()=>{ // io(‘服务器url’) // 创建客户端套接字对象 let socket = io('wss://www.quanzhan.site:8889'); // 注册message事件--接口文档 客户端可注册 message 事件接收服务器发送的实时数据 socket.on("message", (message) => { // 获取到的是 JSON 字符串表示的对象数据,将患者数据保存到 state 中 console.log('message',message); setTimedata(JSON.parse(message).patients) console.log(timeData); }); // 卸载,销毁资源,在组件销毁时,应该关闭 socket 连接 return () => { socket.close() socket = null //闭包会延长引用变量的使用时间 } },[])
》销毁那些:定时器,连接的网络请求,打开的连接啊,echarts啊,…
// 卸载,销毁资源,在组件销毁时,应该关闭 socket 连接 return () => { socket.close() //销毁 socket = null //闭包会延长引用变量的使用时间 } //图表销毁 return () => { depositsChart.dispose() //销毁 depositsChart = null }
9. i18n 国际化
index.js
import { ConfigProvider } from 'antd' import zhCN from 'antd/lib/locale/zh_CN'; return ( <ConfigProvider locale={zhCN}> <App /> </ConfigProvider> );
moment.js 库(比较大)=>使用比较多
用于处理js日期处理类库,解析-检验-操作-显示日期
替换成 day.js…
ctrl+f
ctrl+d
10.防抖节流
》 闭包使用 -
lodash 包 API方法很多 - 全部引入 - 按需引入
_.throttle(节流,一定时间必须执行一次 )
获取验证码
-lodash三方包实现
import _ from 'lodash' // 全部引入 // 获取验证码 - 节流方法 _throttle(func,waitTime) getCode = _.throttle(函数,2000) /* 三方包实现 */ const getCode = useCallback(_.throttle( async ()=>{ /* 发起网络请求获得验证码 */ // getCaptcha().then(data=>{console.log('captcha:',data)}) let resault = await getCaptcha() const {captcha} = resault // 修改验证码 setCaptcha(captcha) } ,5000),[])
-封装实现节流
const getCode = throttled(async function(){ /* 发起网络请求获得验证码 */ // getCaptcha().then(data=>{console.log('captcha:',data)}) let resault = await getCaptcha() const {captcha} = resault setCaptcha(captcha) },5000) /* 节流实现 - 封装实现*/ function throttled(fn, delay) { let timer = null let starttime = Date.now() return function () { let curTime = Date.now() // 当前时间 let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间 let context = this let args = arguments clearTimeout(timer) if (remaining <= 0) { fn.apply(context, args) starttime = Date.now() } else { timer = setTimeout(fn, remaining); } } }
登录-防抖
三方包-
_.throttle() /* 三方包实现debounce */ const onFinish = _.debounce( async (values) => { /* 异步action-将请求放进action然后实现数据更新 */ const resault = await dispatch(loginAsyncAction(values)) if(!resault){ //登陆失败 message.error('用户名或密码错误') } },5000)
封装实现防抖
/* 登录-防抖 最后一次登录重新计时*/ const onFinish =debounce(async (values) => { /* 异步action-将请求放进action然后实现数据更新 */ const resault = await dispatch(loginAsyncAction(values)) if(!resault){ //登陆失败 message.error('用户名或密码错误') } },5000) /* 防抖实现 */ function debounce(func, wait) { let timeout; return function () { let context = this; // 保存this指向 let args = arguments; // 拿到event对象 clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context, args) }, wait); } }
上传upload组件
formdata格式
11.路由懒加载
》应用复杂程度,代码提价越大,带宽一定的情况下,js包会非常大,把不同的路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应的组件-
react-lodable 三方包 => 高阶组件实现动态加载
A higher order component for loading components with dynamic imports.
借助了webpack代码分割
yarn add react-lodable
utils/with-loadable.js: import Loadable from 'react-loadable' /** * 异步加载组件时的中转组件 * @param {*} props * @returns */ function Loading(props) { if (props.error) { return <div>Error! <button onClick={ props.retry }>Retry</button></div>; } else { return <div>Loading...</div>; } } // HOC const withLoadable = loader => { return Loadable({ loader, // 待异步加载组件 loading: Loading, // 异步加载时的“加载中”提示组件 }) } export default withLoadable
npm run build 打包构建后-生成build文件
react开发:灵活-实现一个项目可能会有很多方法
import withLoadable from '../utils/with-loadable'; /* 路由懒加载 */ const Login = withLoadable(()=>import('../views/login'))
12.redux状态数据实现持久化
页面刷新,redux数据会清空,怎么办?持久化–
token过时,异常
本地数据缓存 -localStorage
三方包-redux-persist
yarn add redux-persist store/index.js /** * 创建 store 仓库 */ /* 实现持久化存储 rdux-perist */ import { persistStore,persistReducer } from 'redux-persist'; // 默认存储在localStorage - 可改变 import storage from 'redux-persist/lib/storage'; import rootReducer from "../reducers"; /** * 基于根 reducer 来创建 store */ // 持久化的配置 const persistConfig = { key: 'root', storage, } //处理持久化reducer const persistedReducer = persistReducer(persistConfig, rootReducer) // 创建store const store = createStore(persistedReducer,applyMiddleware(logger,thunk)) //持久化处理store中所有信息-注意可以选择性的区储存 let persistor = persistStore(store) export default store export {store,persistor}
src/index.js 入口文件设置:
import { PersistGate } from 'redux-persist/integration/react' // ... normal setup, create store and persistor, import components etc. const App = () => { return ( <Provider store={store}> <PersistGate loading={null} persistor={persistor}> <RootComponent /> </PersistGate> </Provider> ); };
》可能本地存储只存储token–用户修改信息-如果缓存本地用户原来的数据信息-逻辑错误-对于用户信息可能不会缓存
13-侧边导航菜单渲染
不同角色有不同菜单的渲染-权限还是比较复杂的东西-
面试比较多-用户角色权限处理-登录流程?成功后导航渲染的流程?
-按钮级的权限-前端没做-只是还是保留再页面中-公司做的粗糙交给后端做的?后端接口反馈为无权限直接跳到无权限的页面;我们只做了页面级的权限处理
最简单的页面级的菜单渲染
角色权限处理的过程:参考vue
react 利用-来实现权限拦截处理
从本地读取保存的用户 token 数据
存在 token 时:
a.如果访问的是登陆页面,则跳转至主页面中;(已有token,用户无需再次登录 )
b.如果访问的是其他页面,则继续:
1.从 redux 的 store 中获取用户的角色信息
2.如果已有保存用户的角色信息,则继续正常执行下一步流程(store 中有保存用户角色的信息,则说明用户角色权限相关的路由已经处理好了)
3.如果在 store 中还没有用户角色信息,则:根据token 发送网络请求获取用户基本信息后保存到store 中,从响应结果中获取到
4.有了用户角色信息后,根据用户角色信息,动态生成可访问的路由表(这些动态生成的路由表也会保存到store 中)
5.将动态生成的路由表 与前端路由表进行筛选,生成可访问路由表
6.进入正常的下一步导航流程
不存在 token 时:
继续判断:
如果是访问白名单页面,则继续进入下一步正常访问
如果不是白名单页面,则跳转登陆页面进行登录处理(白名单:不需要权限就能访问的页面)
管理员-医生-护士
路由-layout页面根据用户动态生成的路由表动态渲染 - actions方法 - use
dispatch方法的调用 - 递归实现角色权限筛选
-获取登录用户的角色与
接口获取数据
1-layout页面根据用户动态生成的路由表动态渲染 -
2-后端接口数据获得用户角色和路由表【后端在接口数据中告诉当前角色能够访问的菜单-前端再进行渲染】 – 异步action
接口获取数据 角色权限接口 - 登录奇数医生-偶数管理员角色-保存roles
/** * 异步action * 实现动态加载路由 */ export const genDynamicRoutesAction = (routes)=>{ console.log('props.routes',routes); return async (dispatch)=>{ // 发起网络请求-获取用户角色和路由 let {roles} = await genPermissions() let {permissions} = roles[0] console.log('获取用户权限:',permissions); // 将获取的允许访问的路由和前端路由表进行对比,筛选得出动态路由表 /** * 定义递归函数,用于计算动态路由表 * routes:前端路由表 * permissions:网络请求得到的路由 */ function genDynamicRoutes(permissions,routes){ const result = [] //保存最后可访问的路由 // 根据角色信息得到路由表 permissions.forEach(permission=>{ const route = routes.find(route=>route.path===permission.menu.url) console.log('匹配的路由',route); if(route){ // 存在可访问的路由 const copyRoute = {...route} // 拷贝 // 判断是否有子路由 - 递归 if(copyRoute.children && permission.children){ const children = genDynamicRoutes(permission.children,copyRoute.children) copyRoute.children = children } result.push(copyRoute) } }) return result } const dynamicRoutes = genDynamicRoutes(permissions,routes) console.log('生成的可访问路由表',dynamicRoutes); //更新状态数据: dispatch({ type:PERMISSION_ROUTES, payload:dynamicRoutes, }) } } ----------------- reducers case PERMISSION_ROUTES: //匹配路由表 copy.routes = payload return copy
前端路由表和反馈的路由权限筛选–
完整的路由表routes
修改bug:知道路径也可以访问其他地址
右侧content渲染拿动态生成的路由表-
数据持久化【了解一下】:可以实现自定义数据持久化的各项规则 transform.js
第一个回调函数时要保存的状态数据 return返回的就是持久保存的
第二个:本地存储还原到
表格的crud 增加检索更新删除
对接接口
前后端接口的链条:设计稿-口头-原型
相关的文档?-- 有无接口文档?-- 沟通 –
无swagger可用 postman测试接口-看到响应数据
TypeScript
js弱类型语言,ts是其语法的一个超集,强类型语言。
js变量未声明类型,运行时类型检查,来定义类型。
ts开发大型应用-合作需要-静态类型检查
安装
npm install -g typescript
使用
建立文件.ts
后缀名 .ts
正常按照js代码写
ts编译成 js文件
转化成匿名自执行函数解决变量全局污染问题
tsc 文件名 #编译成同名后缀为js文件 tsc 文件名 --watc h #在监视模式下运行编译器。会监视输出文件,在它们改变时重新自动编译。
类型注解
const hello : string = "Hello World!" //定义变量的时候声明该变量的数据类型
保留字关键字
有特殊含义的关键字
目前没有含义,将来语言以后使用的字
ts基础类型
string
number
boolean
数组类型
元组:元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
enum:枚举: 枚举类型用于定义数值集合
redux/reducer中的action参数,switch常量值
symbol唯一值–ts中可以用枚举使用
//string let name: string = "Runoob"; //number let years: number = 5; //bolean let bol: boolean = true; /* 数组 */ // 在元素类型后面加上[] 数组中元素为数字 let arr: number[] = [1, 2]; // 或者使用数组泛型 let arr: Array<number> = [1, 2]; let array: Array<string> = ['1','2']; /* 元组 */ let x: [string, number]; x = ['Runoob', 1]; // 运行正常 x = [1, 'Runoob']; // 报错 console.log(x[0]); // 输出 Runoob /* enum -语义化 */ enum Color {Red, Green, Blue}; //自定义类型 let c: Color = Color.Blue; console.log(c); // 输出 2 // 看一下转译后的数组
any: 声明为 any 的变量可以赋予任意类型的值。
let v: any = 1; v = 'true'; v = true;
void: 用于标识方法返回值的类型,表示该方法没有返回值。
function hello(): void { alert("Hello Runoob"); }
类型断言:
<类型>值 值 as 类型
类型推断 - 自动判断第一次赋值时的数据类型
var str = '1' var str2:number = <number> <any> str //str、str2 是 string | number?? console.log(str2) !!!
函数
语法:
function fnName( param1: dataType, param2: dataType = defaultValue, param3?: dataType //可选 ): reutrnType { //函数体 }
剩余参数:任意个数参数
…剩余运算 (数组结构 - 函数内部)
…展开运算
Lambda函数 (箭头函数)
联合类型
语法
Type1|Type2|Type3 ------------------------------------------------------------ var val:string|number val = 12 console.log("数字为 "+ val) val = "Runoob" console.log("字符串为 " + val)
ts接口:interface
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法 : 类的模板
面向对象三大特性:封装|继承|多态
面向接口编程:模板
interface IPerson { firstName:string, lastName:string, sayHi: ()=>string } var customer:IPerson = { firstName:"Tom", lastName:"Hanks", sayHi: ():string =>{return "Hi there"} } console.log("Customer 对象 ") console.log(customer.firstName) console.log(customer.lastName) console.log(customer.sayHi()) class Student implements Iperson { firstName:string, lastName:string, sayHi: ()=>string , constructor(firstName,lastName){ this.firstName = firstName, } }
implement 实现接口
ts命名空间:namespace
标识符|标识符 - 变量名字 就是标识符
声明文件
.d.ts文件后缀 - 是ts的声明文件
/// <reference path = " runoob.d.ts" />
泛型
实现重用 复用
定义一个函数,要求函数类型与返回值类型一致
的定义就是一个泛型结构
T 可看作是一个类型变量
console.log('匹配的路由',route); if(route){ // 存在可访问的路由 const copyRoute = {...route} // 拷贝 // 判断是否有子路由 - 递归 if(copyRoute.children && permission.children){ const children = genDynamicRoutes(permission.children,copyRoute.children) copyRoute.children = children } result.push(copyRoute) } }) return result } const dynamicRoutes = genDynamicRoutes(permissions,routes) console.log('生成的可访问路由表',dynamicRoutes); //更新状态数据: dispatch({ type:PERMISSION_ROUTES, payload:dynamicRoutes, })
}
}
reducers
case PERMISSION_ROUTES: //匹配路由表
copy.routes = payload
return copy
前端路由表和反馈的路由权限筛选-- 完整的路由表routes 修改bug:知道路径也可以访问其他地址 右侧content渲染拿动态生成的路由表- 数据持久化【了解一下】:可以实现自定义数据持久化的各项规则 transform.js > 第一个回调函数时要保存的状态数据 return返回的就是持久保存的 > > 第二个:本地存储还原到 表格的crud 增加检索更新删除 # 对接接口 前后端接口的链条:设计稿-口头-原型 相关的文档?-- 有无接口文档?-- 沟通 -- 无swagger可用 postman测试接口-看到响应数据 # [TypeScript](https://www.runoob.com/w3cnote/getting-started-with-typescript.html) js弱类型语言,ts是其语法的一个超集,强类型语言。 js变量未声明类型,运行时类型检查,来定义类型。 ts开发大型应用-合作需要-静态类型检查 ## 安装 ```bash npm install -g typescript
安装完毕之后,可在命令行中使用tsc命令测试是否安装成功 tsc -v
使用
建立文件.ts
后缀名 .ts
正常按照js代码写
ts编译成 js文件
转化成匿名自执行函数解决变量全局污染问题
tsc 文件名 #编译成同名后缀为js文件 tsc 文件名 --watc h #在监视模式下运行编译器。会监视输出文件,在它们改变时重新自动编译。
类型注解
const hello : string = "Hello World!" //定义变量的时候声明该变量的数据类型
保留字关键字
有特殊含义的关键字
目前没有含义,将来语言以后使用的字
ts基础类型
string
number
boolean
数组类型
元组:元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
enum:枚举: 枚举类型用于定义数值集合
redux/reducer中的action参数,switch常量值
symbol唯一值–ts中可以用枚举使用
//string let name: string = "Runoob"; //number let years: number = 5; //bolean let bol: boolean = true; /* 数组 */ // 在元素类型后面加上[] 数组中元素为数字 let arr: number[] = [1, 2]; // 或者使用数组泛型 let arr: Array<number> = [1, 2]; let array: Array<string> = ['1','2']; /* 元组 */ let x: [string, number]; x = ['Runoob', 1]; // 运行正常 x = [1, 'Runoob']; // 报错 console.log(x[0]); // 输出 Runoob /* enum -语义化 */ enum Color {Red, Green, Blue}; //自定义类型 let c: Color = Color.Blue; console.log(c); // 输出 2 // 看一下转译后的数组
any: 声明为 any 的变量可以赋予任意类型的值。
let v: any = 1; v = 'true'; v = true;
void: 用于标识方法返回值的类型,表示该方法没有返回值。
function hello(): void { alert("Hello Runoob"); }
类型断言:
<类型>值
值 as 类型
类型推断 - 自动判断第一次赋值时的数据类型
var str = '1' var str2:number = <number> <any> str //str、str2 是 string | number?? console.log(str2) !!!
函数
语法:
function fnName( param1: dataType, param2: dataType = defaultValue, param3?: dataType //可选 ): reutrnType { //函数体 }
剩余参数:任意个数参数
…剩余运算 (数组结构 - 函数内部)
…展开运算
Lambda函数 (箭头函数)
联合类型
语法
Type1|Type2|Type3 ------------------------------------------------------------ var val:string|number val = 12 console.log("数字为 "+ val) val = "Runoob" console.log("字符串为 " + val)
ts接口:interface
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法 : 类的模板
面向对象三大特性:封装|继承|多态
面向接口编程:模板
interface IPerson { firstName:string, lastName:string, sayHi: ()=>string } var customer:IPerson = { firstName:"Tom", lastName:"Hanks", sayHi: ():string =>{return "Hi there"} } console.log("Customer 对象 ") console.log(customer.firstName) console.log(customer.lastName) console.log(customer.sayHi()) class Student implements Iperson { firstName:string, lastName:string, sayHi: ()=>string , constructor(firstName,lastName){ this.firstName = firstName, } }
implement 实现接口
ts命名空间:namespace
标识符|标识符 - 变量名字 就是标识符
声明文件
.d.ts文件后缀 - 是ts的声明文件
/// <reference path = " runoob.d.ts" />
泛型
实现重用 复用
定义一个函数,要求函数类型与返回值类型一致
的定义就是一个泛型结构
T 可看作是一个类型变量