学习过java语言,才发现类型标注的重要性,在前端,js最让人诟病的便是无类型规范,可以随意赋值和改变类型等,这样项目出现bug的几率大大增加,而js的超集typescript完美解决了这个问题。(别犹豫是否该学ts了,说白了,ts才是前端的脸面,何况主流框架和库早就推出了ts环境下的开发方式和类型标注)
1. 声明函数组件或类组件
函数组件的类型 React.FC 或 FC 接受一个泛型作为父级传递过来的值的约束!
import Reeact ,{FC} from "react";
interface ExtendedProps{
title:string,
value:number
}
const WrapComponent: React.FC<ExtendedProps> = (props) => {
return (
<div> {title} </div>
)
};
or
const WrapComponent: FC<ExtendedProps> = (props) => {
return (
<div> {value} </div>
)
};
export default WrapComponent;
函数组件接受父级“插槽”内容时children的类型声明:
1.类型断言
const App: React.FC<{}> = props => (props.children as unknown) as JSX.Element
const App: React.FC<{}> = () => ([1, 2, 3] as unknown) as JSX.Element
const App: React.FC<{}> = () => ('hello' as unknown) as JSX.Element
2.使用 PropsWithChildren
type AppProps = React.PropsWithChildren<{ message: string }>
const App = ({ message, children }: AppProps) => (
<div>
{message}
{children}
</div>
)
3.直接声明:
//子组件
import React from 'react'
type AppProps = {
message: string
children?: React.ReactNode
}
const App :React.FC = ({ message, children }: AppProps) => (
<div>
{message}
{children}
</div>
)
//父组件
cosnt Parent:React.FC=()=>{
return (
<App message="阿尼亚我的宝~"> <span>上坂堇我的宝!</span> <App />
)
}
类组件在继承React.Compinent时需要传递两个泛型变量
分别是类中的state和父级传递props的类型约束
type IEProps {
Cp?: React.ComponentClass<{ id?: number }>;
}
type IEState { id: number; }
class ClassCpWithModifier extends React.Component<IEProps, IEState> {
private gid: number = 1;
public state: Readonly<IEState> = { id: 1 };
state = { //无需构造器,状态直接定义在类中即可
transferData: {
optimizationOrderFn: false,
allocationFn: false,
releaseFn: false,
revocationFn: false,
},
};
render() {
const { transferData } = this.state || {};
return (
<span> {id} </span>
<div> {transferData} </div>
)}
}
2.事件
- onClick and onChange
-
export default let eventComp:FC<Iprops> = (props)=>{ const onClick = (e: React.MouseEvent<HTMLInputElement>) => { console.log("点击事件"); }; const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { console.log("值改变了~") }; return ( <> <ProForm<DataType> /> <input onClick={onClick} onChange={onChange} /> </> ); }
- Forms and onSubmit
-
import * as React from 'react' type changeFn = (e: React.FormEvent<HTMLInputElement>) => void const App: React.FC = () => { const [state, setState] = React.useState('') const onChange: changeFn = e => { setState(e.currentTarget.value) } ✅ better const onSubmit = (e: React.SyntheticEvent) => { e.preventDefault() const target = e.target as typeof e.target & { password: { value: string } } // 类型扩展 const password = target.password.value } return ( <form onSubmit={onSubmit}> <input type="text" value={state} onChange={onChange} /> </form> ) }
3.react-hooks
const [val, toggle] = useState<boolean>(false);
const [val, toggle] = useState<number>(0);
interface msg{
sex:string,
age:number,
name:string
}
interface props{
num:number,
users: msg[]
}
const [val, changeVal] = useState<props>(
{... props });
4.对象类型的声明
type ObjectTypes = {
objBetter: Record<string, unknown>; // ✅ better,代替 obj: object
// 对于 obj2: {}; 有三种情况:
obj2Better1: Record<string, unknown>; // ✅ better 同上
obj2Better2: unknown; // ✅ any value
obj2Better3: Record<string, never>; // ✅ 空对象
/** Record 更多用法 */
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // 等价于 dict1
};
//好处:
//1.当你书写 home 值时,键入 h 常用的编辑器有智能补全提示;
//2.home 拼写错误成 hoem,会有错误提示,往往这类错误很隐蔽;
//3.收窄接收的边界。
5.参数传递
组件之间传递参数需要用接口来约束类型,父级传递参数严格遵守接口定义的规则
interface trans {
optimizationOrderFn: boolean,
allocationFn: boolean,
releaseFn: boolean,
revocationFn: boolean
}
interface Iprops {
val:trans
}
默认参数和可选参数
type GreetProps = { age?: number }
const Greet = ({ age = 21 }: GreetProps) => {
/* ... */
}
子组件无法决定传递参数的类型时:
type Props<T> = {
name: T
name2?: T
}
const TestC: <T>(props: Props<T>) => React.ReactElement = ({ name, name2 }) => {
return (
<div className="test-b">
TestB--{name}
{name2}
</div>
)
}
const TestD = () => {
return (
<div>
<TestC <string> name="123" />
</div>
)
}
6.自定义hooks的返回值类型
需要注意,自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 Union 类型,而我们实际需要的是数组里里每一项的具体类型,
需要手动添加 const 断言 进行处理:
function useLoading() {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
// 实际需要: [boolean, typeof load] 类型
// 而不是自动推导的:(boolean | typeof load)[]
return [isLoading, load] as const
}
或者直接声明返回值类型
export function useLoading(): [
boolean,
(aPromise: Promise<any>) => Promise<any>
] {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
return [isLoading, load]
}
3.封装一个工具函数
export default function tuplify<T extends any[]>(...elements: T) {
return elements
}
function useTupleLoading() {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
// [boolean, typeof load]
return tuplify(isLoading, load)
}
7.ts环境下类组件路由操作
官方已经想让你抛弃类组件了,react-Router新版本只支持 hooks,而在类组件中使用路由相关操作,需要利用hooks来封装
封装:
import React from "react";
import { NavigateFunction, useLocation, useNavigate, useParams } from "react-router";
export interface RoutedProps<Params = any, State = any> {
location: State;
navigate: NavigateFunction;
params: Params;
}
export function withRouter<P extends RoutedProps>( Child: React.ComponentClass<P> ) {
return ( props: Omit<P, keyof RoutedProps> ) => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return <Child { ...props as P } navigate={ navigate } location={ location } params={ params }/>;
}
}
调用:
import React from "react";
import FrameLoader from "./game";
import { NavBar, Space, Toast } from 'antd-mobile';
import { withRouter, RoutedProps } from '../../../hooks/withRouter';
interface trans {
optimizationOrderFn: boolean,
allocationFn: boolean,
releaseFn: boolean,
revocationFn: boolean
}
interface Istate {
transferData: trans
}
interface Iprops {
val: any
}
class FactoryInfo extends React.Component<RoutedProps, Istate> {
state = {
transferData: {
optimizationOrderFn: false,
allocationFn: false,
releaseFn: false,
revocationFn: false,
},
};
componentDidMount() {
console.log(this.props);
// 接收Iframe 传递的数据
window.addEventListener("message", (e) => {
const { allocationFn, releaseFn } = e.data || {}; //传递的数据
if (allocationFn || releaseFn) {
this.setState({
transferData: e.data,
});
}
});
}
back = () => {
Toast.show({
content: '马上就给你返回,别着急!',
duration: 2000,
})
//console.log(this.props.location.state.url);
this.props.navigate("/home");
}
render() {
const { transferData } = this.state || {};
const url = this.props.location.state.url;
console.log(transferData, "数据");
return (
<div style={{ height: "100%" }}>
<NavBar onBack={this.back}>标题</NavBar>
<div style={{ minHeight: "90vh", position: "relative" }}>
<FrameLoader height={1000} url={"./game/" + url + ".html"} />
</div>
</div>
);
}
}
export default withRouter(FactoryInfo)
生命周期调用两次请求的解决方案
将 componentDidMount() 改写为 componentWillMount()
hooks中的解决
let getNews = () => {
axios({
method: 'get',
url: "https://api.apiopen.top/api/getImages?type=scenery&page=0&size=10"
}).then((res) => {
console.log(res);
changeList(res.data.result.list)
})
}
useEffect(() => {
getNews();
}, [])
//注意方法单独封装,在hooks里调用,直接将请求写入hooks会发送两次...
react中的防抖实现
防抖的句柄timer在组件重新渲染时丢失,通过useCallback缓存该方法即可
const Search = (props: IProps) => {
let [val, changeVal] = useState('');
let changeInner = () => {
let timer: any = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
console.log("值改变了!");
timer = null;
}, 1000);
}
}
}
const change= useCallback(changeInner(), []);
return (
<div className="App">
<input type="text" value={val} onChange={(e) => {
changeVal(e.target.value),
change()
}} />
</div>
);
}
关于redux的一些思考
redux核心文件 createStore 封装了三个方法 ,dispatch,getState,subScribe
接受reducer, preloadedState, enhancer作为createStore实例的参数,
有中间件的情况下createStore需要通过enhancer中的compose函数进行中间件处理,之后执行dispatch,触发reducer从而初始化state
store创建好之后,立即发出一个初始化action,是为了让reducer返回store的初始化状态,否则,创建store之后,调用getState方法得到的就是undefined。
import { createStore } from 'redux'
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 }
default:
return state
}
}
let store = createStore(counterReducer)
store.dispatch({ type: 'counter/incremented' })
dispatch :根据传递的旧状态和新action 触发reducer来改变state,之后触发所有监听器
subScribe : 返回一个unsubScribe 方法,负责添加订阅事件和取消事件
全局的数据有 : state reducer currentListeners nextListeners isDispatching(全局锁)
派发监听器的时候有可能在监听的方法内存在再次订阅或取消监听的方法,这样会导致 currentListeners 长度改变从而报错,所以需要在派发前浅拷贝一下,订阅和取消操作nextListeners,在派发之前进行拷贝 currentListeners = nextListeners
全局锁的作用:为了防止用户在时事件派发的同时再获取state,在派发时 isDispatching为true,完成后为false
中间件类似于洋葱模型,利用compose函数实现redux实例和中间件的嵌套
combineReducers
reducer函数负责生成state,整个应用只有一个state对象,包含所有的数据,对于一个大型应用来说,state必然比较庞大,会导致reducer函数也十分庞大,所以需要对reducer函数进行拆分。并且几个人开发的项目中,我们需要编写各自的reucer函数并合并在一起,因为createStore只能接收一个reducer函数。这就是combineReducers函数做的事情。
下面来举例说明:
假设我们项目当中有两个reducer函数,reducerI 和 reducerII 函数,将两个reducer函数以对象的形式传入combinereducers函数,函数执行返回的finalReducer作为reducer函数传给createStore,实现了原本reducer函数相同的功能。
combineReducers接收reducers对象为入参,返回一个合成的reducer函数,利用闭包,实现了reducer合成的功能。实质是通过遍历的形式,将action传入每一个reducer函数,获取初始值或者改变state,返回新的state对象
// 参数reducers是一个对象,key值和state的key值一样,value是一个reducer函数
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 遍历key值,过滤掉value不是reducer函数的值
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 过滤掉value不是reducer函数的值
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 返回一个合成的reducer函数(接收state和action作为参数)
return function combination(state = {}, action) {
let hasChanged = false // 记录state对象是否改变的变量
const nextState = {} // 下一个状态
// 遍历key值
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key] // 当前key值对应的状态
const nextStateForKey = reducer(previousStateForKey, action) //调用reducer后新生成的状态
nextState[key] = nextStateForKey //将新生成的状态赋值到新状态的对应key值上
// 通过对比previousStateForKey 和 nextStateForKey是否相等来表明状态是否改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 如果状态改变就返回新的状态,没有,就返回原来的state对象
return hasChanged ? nextState : state
}
}
// combineReducers函数实际上也是一个reducer函数,接收state和action作为参数,返回一个新的state
使用react实现 通讯录功能
思路:
得到地区数据,改变其数据结构,提取地区首字母作为key,地区为value组成新的数组对象,注意地区按字母进行排序。
let transdata = (arr) => {
let map = {};
arr.forEach((ele) => {
let str = ele.short[0].toUpperCase();
if (map[str]) {
map[str] = [...map[str], ele.label];
} else {
map[str] = [ele.label];
}
});
objToArr(map);
};
let objToArr=(obj)=>{
let arr = [];
for (const key in obj) {
arr.push({key:key,value:obj[key],smallKey:key.toLowerCase()})
}
arr.sort((a,b)=>{
if(a.key<b.key){
return -1
}else{
return 1
}
return 0;
})
setCitys(arr);
}
再之后就是渲染
这里点击右侧字母实现相应跳转利用了a标签的锚点属性~
citys.map((item,index)=>(
<div className="citys" key={index}>
<div id={item.key}>{item.key}</div>
<ul>
{item.value.map(
i=>(
<li key={i}>
{i}
</li>
)
)}
</ul>
</div>
))
<ul className="aside">
{
citys.map(item=>(
<a key={item.key} href={"#"+item.key}>{item.smallKey}</a>
))
}
</ul>