扩展
Immutable.js
typescript
搭建环境
create-react-app 目录 --template typescript
统一变化
- 所有用到
jsx
语法的文件都需要以tsx
后缀命名 - 使用组件声明时的
Component
泛型参数声明,来代替PropTypes! - 全局变量或者自定义的window对象属性,统一在项目根下的
global.d.ts
中进行声明定义 - 对于项目中常用到的接口数据对象,在
types/
目录下定义好其结构化类型声明
react类型
RouteComponentProps
RouteComponentProps
RouteChildrenProps
HTMLDivElement
match
React.FC
React.FunctionComponent
React.ReactNode
ComponentType
JSX.Element
Dispatch
AxiosRequestConfig
AxiosPromise
类组件
创建组件
export default class 组件名 extends React.Component<IProps, IState>{}
export default class Comp3 extends React.Component<{ //内联类型注解
value: string;
onChange: (value: string ) => void
}, {}> {}
export default class 组件名 extends React.Component<{}, {}>{} //组件没有props和状态时
IProps,IState接口类型需要定义,可以定义在组件文件内部,或者types目录
类型约束
export interface List {//通用,可以丢到外部,也可以在外部定义,推荐`types/`目录下
readonly id: number;
name: string;
}
type IProps = { //未export 不通用
readonly id: number;
title: string;
num?: number;
arr?: string[]
}
type IState = {
msg1: string;
msg2: number;
list: List[]
}
组件状态
export default class 组件名 extends React.Component<IProps, IState>{
state: Readonly<IState> = initState;//state不建议通过实例属性修改,作为只读定义
}
initState 可定义到组件外部,也可定义在内部
let initState:IState = {
msg1: 'xx',
msg2: 12,
list: [
{ id: 1, name: 'alex' },
{ id: 2, name: 'alex2' },
{ id: 2, name: 'alex3' },
]
};
props属性
类型约束内部确定只读,必传,可选特性,默认值在类属性defaultProps设定
type IProps = { //未export 不通用
readonly id: number;//只读, props理论都应该是只读
title: string;//必传
num?: number;//可选
arr?: string[]
}
export default class 组件名 extends React.Component<IProps, IState>{
//props默认值
static defaultProps={
num:0
}
}
事件
const initialState = { clicksCount: 0 };//先定义值
//再使用typeof推断类型,并设置只读,限定this.state修改
type TState = Readonly<typeof initialState>;
class Counter extends Component<{}, TState>{
readonly state: TState = initialState;//因为 React 不推荐直接更新 state 及其属性
render() {
const { clicksCount } = this.state;
return (
<div>
<h3>事件</h3>
<button onClick={this.handlerIncrement}>+</button>
<button onClick={this.handlerDecrement}>-</button>
<div>{clicksCount}</div>
</div>
)
}
// private handlerIncrement=()=>this.setState({clicksCount:this.state.clicksCount+1})
// private handlerDecrement=()=>this.setState({clicksCount:this.state.clicksCount-1})
private handlerIncrement = () => this.setState(increment)
private handlerDecrement = () => this.setState(decrement)
}
//独立纯函数,编译单独测试
const increment = (prevState: State) => ({ clicksCount: prevState.clicksCount + 1 })
const decrement = (prevState: State) => ({ clicksCount: prevState.clicksCount - 1 })
函数式组件
定义组件
使用 React.FunctionComponent
接口定义函数组件
type Props = {
foo: string;
};
const MyComponent: React.FunctionComponent<Props> = props => {
return <span>{props.foo}</span>;
};
export {MyComponent}
使用 React.FC
别名定义函数组件
interface IProps {
readonly id?:number;
title?:string;
num?:number;
arr?:string[]
}
type IProps2=Readonly<IProps>;//类型映射
//函数式组件
const Footer: React.FC<IProps2> = (props) => {
// props.num=2; //error 类型约束props为只读
// props.title='2323';//error
return (
<div className="Footer">
footer
</div>
)
}
export default Footer
事件,props
事件函数通过props传入
type TProps = {
onClick(e: MouseEvent<HTMLElement>): void//必传
text?: string//可选
}
//props默认值在函数接收参数时设定,handleClick为对象别名
const Button: FC<TProps> = ({ onClick: handleClick, text = '按钮' }: TProps) => (
<>
<h3>无状态组件</h3>
<button onClick={handleClick}>按钮</button>
</>
);
组件注入
通过props或直接嵌套的方式,向组件注入一些可变的元素
jsx节点
注入的元素有:string,number,boolean,ReactElement
举例:<Comp2 header={<h4>Header</h4>} body={12} />
interface Iprops {
header?: React.ReactNode;//jsx节点类型 string,number,boolean,ReactElement
body: ReactNode;//类型需要导入,来自react包
}
//! 代表排除null
class Comp2 extends React.Component<Iprops, {}> {
render() {
return (
<>
<div>---------接收可渲染的内容start--------</div>
{this.props!.header}
{this.props.body}
<div>---------接收可渲染的内容end--------</div>
</>
);
}
}
children嵌套
调用组件时,插入到组件的内容<组件>内容</组件>
type AuthProps = {
children?: JSX.Element;//设定children类型,可选
[propName:string]:any//props可以接受其他任何值
}
const authState = { show: false }//先赋值
type AuthState = Readonly<typeof authState>//后推断类型
class Auth extends Component<AuthProps, AuthState>{
static readonly defaultProps: AuthProps = {title:'bmw'}
readonly state: AuthState = authState
render() {
const { children } = this.props
return (
<>
<div>组件自身内容</div>
{children && children}
<div>组件自身内容</div>
</>
)
}
}
注入组件
类似于<Auth path="/foo" component={MyView} />
type AuthProps = {
component?: ComponentType<any>;//设定要接受的组件类型ComponentType
}
const authState = { show: false }
type AuthState = Readonly<typeof authState>
class Auth extends Component<AuthProps, AuthState>{
static readonly defaultProps: AuthProps = {title:'bmw'}
readonly state: AuthState = authState
render() {
const { component: InjectedComponent } = this.props //InjectedComponent字面量别名
return (
<>
<h3>组件本身</h3>
{/* 调用通过props传入的组件 */}
{InjectedComponent && <InjectedComponent/>}
</>
)
}
}
注入render属性
给调用的组件传递render函数,指定被调用组件的渲染内容
举例:
<Auth render={() => (
<div>render后的内容</div>
)} />
实现:
type AuthProps = {
render?: () => JSX.Element;
[propName:string]:any
}
const authState = { show: false }
type AuthState = Readonly<typeof authState>
class Auth extends Component<AuthProps, AuthState>{
static readonly defaultProps: AuthProps = {title:'bmw'}
readonly state: AuthState = authState
render() {
const { render } = this.props
if (render) {
return render()//调用传入渲染函数,按照外部要求渲染
}
return (
<div>原本内容</div>
)
}
}
ref在类组件
引用渲染完成后的元素
方式1
import React from 'react';
export default class Comp3 extends React.Component<{ //内联类型注解
value: string;
onChange: (value: string ) => void
}, {}> {
//使用 ref 和 null 的联合类型,并且在回调函数中初始化他
input: HTMLInputElement | null = null;
render() {
return (
<>
<h3>refs</h3>
<input
ref={el => this.input = el}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
/>
</>
);
}
componentDidMount(){
this.input != null && this.input.focus() //获取焦点
}
}
方式2
import React,{createRef} from 'react';
export default class Comp3 extends React.Component<{ //内联类型注解
value: string;
onChange: (value: string ) => void
}, {}> {
// 使用createRef函数,返回ref对象,并指定类型,作为实例
input = createRef<HTMLInputElement>()
render() {
return (
<>
<h3>refs</h3>
<input
ref={this.input}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
/>
</>
);
}
componentDidMount(){
this.input.current!.focus()
}
}
数据交互 axios
反向代理
在src目录下创建setupProxy.js
const proxy = require('http-proxy-middleware'); //需要安装中间件
module.exports = function(app) {
app.use(
proxy("/api", {
target: 'http://localhost:3001',
changeOrigin: true
})
);
app.use(
proxy("/mock", {
target: 'http://localhost:3333',
changeOrigin: true
})
)
};
axios 二次封装
在plugins目录创建axios.ts
import axios from 'axios';
export interface IUser {//通用,可在外部定义,或外部使用
err:number,
data:any,
token:string
}
type TUser = Partial<IUser> & string | null //映射 交叉 联合
// 添加一个请求的拦截
axios.interceptors.request.use((config) => {
//1抓取本地token,携带在请求头里
let user:TUser = window.localStorage.getItem('user');
user = user ? JSON.parse(user) : '';
config.headers={'token': user!.token}
//显示loading...
return config;//返回请求
}, function(error) {
// 请求错误时做点事
return Promise.reject(error);
});
//添加一个响应拦截
axios.interceptors.response.use(function(response) {
// res.data ~~ {err:1,msg:xx,data:{}} ~~ response.data
//token过期: 返回值2,当前路由不是login时跳转,并传递当前路径,登录后可以有参考原路跳回
if (response.data.err === 2 && !window.location.href.includes('/login')) {
window.location.href='http://localhost:3000/login?path='+window.location.pathname
}
return response;
}, function(error) {
return Promise.reject(error);
});
declare global { //定义到全局 也可以定义到src/global.d.ts
interface Window {//给window接口添加axios方法函数
axios(config: AxiosRequestConfig): AxiosPromise<any>
}
}
window.axios = axios; //希望全局使用axios ,
export default axios;
数据交互
import React from 'react';
// import axios from '../plugins/axios';
export interface IListItem {//推荐定义到src/types目录下
_id: string, title: string, des: string, time: number
}
//数据交互
export default class Comp4 extends React.Component<{},{}> {
readonly state:{
list:Array<IListItem>
}={
list:[]
}
render() {
let {list}=this.state
return (
<>
<h3>comp4-数据交互</h3>
{
list.map((item:IListItem)=>(
<li key={item._id}>
{item.title}/{item.time}
</li>
))
}
</>
);
}
componentDidMount(){
//window.axios({
axios({//需要引入plugins/axios
url:'/api/home'
}).then(
({data:{data:list}})=>this.setState({list})
)
}
}
路由
主入口.tsx
import {BrowserRouter as Router,Route} from 'react-router-dom'
ReactDOM.render(
<Router>
<Route component={App} />
</Router>
,
// document.getElementById('root') as HTMLElement
document.getElementById('root')! //移除null和undefined
);
根组件.tsx
<Header />
<Switch >
<Route path="/home" component={Home} />
<Route path="/goods" component={Goods} />
<Route path="/login" component={Login} />
<Route path="/reg" component={Reg} />
<Route path="/detail/:_id" component={Detail} />
<Redirect exact from="/" to="/home" />
</Switch>
<Footer />
列表页.tsx
<li key={item._id}>
<Link to={`/detail/${item._id}?dataName=home`}>
{item.title}/{item.time}
</Link>
</li>
详情页.tsx,解决params._id 不存在的问题
import React from 'react'
import {RouteComponentProps,match} from 'react-router-dom';
import qs from 'qs';//类似query-string
export interface IDetail {
title: string;
des: string;
time: number;
detail: {
auth: string;
content: string;
auth_icon: string;
}
}
type TDetail = {
err: number;
msg: string;
data: Partial<IDetail>;//可选映射
};
//params._id 不存在
//解决方案1
type TProps={
match: match<{_id?: string}>//交叉一个属性到RouteComponentProps类型
} & RouteComponentProps
//解决方案2
type ParamsInfo = {//作为 RouteComponentProps的泛型传入,定义params的内容
_id:string
}
type TProps2=RouteComponentProps<ParamsInfo>;
export default class Detail extends React.Component<TProps, TDetail>{
readonly state: TDetail = {
err: 1,
msg: '失败',
data: {}
}
componentDidMount() {
let dataName=qs.parse(this.props.location.search,{ignoreQueryPrefix:true}).dataName;
let _id=this.props.match.params._id||null;
window.axios({
url:`/api/${dataName}/${_id}`
}).then(
({data:{err,msg,data}})=>this.setState({err,msg,data})
)
}
render() {
let { err, data } = this.state;
return (
<>
{
err === 0 ? (
<div>
<h3>detail</h3>
{data.title}/{data.detail?.auth}
</div>
) : (
<div>骨架屏</div>
)
}
</>
)
}
}
函数式组件,接受路由上下文的类型约束
const Login: React.FC<RouteChildrenProps> = ({ history, match, location }) => {}
需要规定FC别名的泛型约束RouteChildrenProps, 来自react-router-dom包
状态管理
安装:
yarn add redux react-redux @types/react-redux -S
定义类型:src/types
//public type
export interface IListItem {
_id: string, title: string, des: string, time: number
}
//store type
export interface IStoreState {
bNav: boolean;
bFoot: boolean;
bLoading: false;
home: IListItem[];
follow: Array<IListItem>;
user: {
err: number;
msg: string;
},
count:number;
test:number;
}
export type StoreState = Partial<Readonly<IStoreState>>
//action type
export type TActionCount = {
type: string;
payload?: number
}
定义提交类型:src/store/const.ts
// 定义增加 state 类型常量
export const INCREMENT = "INCREMENT";
// 定义减少 state 类型常量
export const DECREMENT = "DECREMENT";
定义action: src/store/actions
import { DECREMENT, INCREMENT } from '../const'
import { TActionCount } from '../../types'
import { Dispatch } from 'redux';
// 增加 state 次数的方法 同步
export const increment = (): TActionCount => ({
type: INCREMENT,
})
// 减少 state 次数的方法 异步
export const decrement = (arg: any): any => (dispatch: Dispatch): Promise<any> => new Promise((resolve, reject) => {
setTimeout(() => {//axios走起
dispatch({ type: DECREMENT })
resolve('异步actions发回来的回执')
}, 1000)
})
定义reducers: src/store/reducers
import { combineReducers } from 'redux'
import {count} from './count'
import {other} from './other'
// combineReducers 可以吧store变成一个对象来组合reducer
//combineReducers(对象) 对象{key:value} key=state.key value=reducer函数
const rootReducer = combineReducers({
count,//state.count: count的reducer函数
other
})
export default rootReducer;
定义count: src/store/reducers/count
import { DECREMENT, INCREMENT } from '../const';
import { TActionCount } from '../../types';
//参数state 只代表state.count的值 , 一定要有初始值
export function count(state:number = 0, {type,payload}: TActionCount): number {
switch (type) {
case INCREMENT:
return payload ? state + payload : state + 1;
case DECREMENT:
return state - 1
default:
return state
}
}
定义store实例: src/plugins/redux
import { createStore,applyMiddleware} from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'//开启调试工具
import thunk from 'redux-thunk'//改装dispatch接受函数
import reducer from '../store/reducers';
// 1、创建 store initState是可选参数
const store = createStore(reducer,composeWithDevTools(applyMiddleware(thunk)));
export default store;
主入口引入store:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './layouts/App';
import {BrowserRouter as Router,Route} from 'react-router-dom'
import { Provider } from 'react-redux';
import store from './plugins/redux'
ReactDOM.render(
<Provider store={store}>
<Router>
<Route component={App} />
</Router>
</Provider>
,
// document.getElementById('root') as HTMLElement
document.getElementById('root')! //移除null和undefined
);
组件接入redux使用:
import React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { StoreState } from '../types';
import { INCREMENT } from '../store/const';
import { decrement } from '../store/actions';
// 创建类型接口
export interface IProps {
count?: number;
test?: number;
onIncrement: () => void,
onDecrement: () => void
}
// 使用接口代替 PropTypes 进行类型校验
class Counter extends React.PureComponent<IProps> {
public render() {
const { count, onIncrement, onDecrement } = this.props;
return (
<>
<h3>redux+react-redux+react-thunk</h3>
<p>
{count}
<br />
<button onClick={onIncrement}> + </button>
<button onClick={onDecrement}> - </button>
</p>
</>
)
}
}
// 将 reducer 中的状态插入到组件的 props 中
// 下面是单个reducer的时候,多个的时候需要选传入哪个reducer
// const { test, count } = state
// const mapStateToProps = (state: StoreState): StoreState => ({
const mapStateToProps = ({count,other}: StoreState): StoreState => ({
// count:state.count
count
})
// 将 对应action 插入到组件的 props 中
const mapDispatchToProps = (dispatch: Dispatch) => ({
onDecrement: () => dispatch(decrement('组件发出的参数')).then((res:string)=>console.log(res)),
onIncrement: () => dispatch({type:INCREMENT,payload:2})
})
// 使用 connect 高阶组件对 Counter 进行包裹
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
hooks
import React, { useState, useEffect, useRef } from 'react'
interface Item{
id?:number;
title?:string;
}
const Reg: React.FC = () => {
//只有在没有初始值的情况下才需要加入类型限制,因为有初始值时可以推断出实际状态的类型。
const [msg, setMsg] = useState('数据1');//有初始值,会类型推断 √
const [msg2, setMsg2] = useState();//没有初始值,推断为any
const [msg3, setMsg3] = useState<number>(0);//手动指定类型和初始值
const [msg4, setMsg4] = useState<Item|null>({id:1});//手动指定类型
const [msg5, setMsg5] = useState<any[]>([]);//手动指定类型
// const box = useRef(null)//有时设置引用可能会在稍后的时间点发生
const box = useRef<HTMLDivElement>(null)//使用 useRef 时需要更加明确 被引用的类型
useEffect(() => {
console.log('didMount');
box.current!.style.background='red';
return () => {
console.log('unmount');
};
}, []);
return (
<>
<h3>hooks</h3>
<div>msg:{msg}</div>
<div>msg2:{msg2}</div>
<div>msg3:{msg3+1}</div>
{/* ? 代表对象存在,才去访问子key */}
<div>msg4:{msg4?.id}/{msg4?.title}</div>
<div>
msg5:
{
msg5?.map((val,index)=>(
<li key={index}>{val}</li>
))
}
</div>
<div ref={box}>box</div>
<button onClick={
()=>{
setMsg('更新后的msg1')
setMsg2('更新后的msg2')//简单类型直接修改
setMsg3(msg3+1)
// setMsg4({id:2})//符合类型,这样做会丢掉一些东东
setMsg4(msg4=>({...msg4,id:22}))//可以这样
// setMsg5(msg5=>([...msg5,'bmw']))
setMsg5([...msg5,'bmw'])//或者这样
}
}>更新状态</button>
</>
)
}
export default Reg;
umi2
创建项目
mkdir project
cd project
yarn create umi
项目结构
|-public 本地数据资源
|-mock umi支持mock数据,无需代理
|-src
|-assets 开发资源
|-compoennts 通用组件
|-layouts 为根布局,根据不同的路由选择return不同的布局,可以嵌套有一些和布局相关的组件
|-pages 页面 约定式路由的页面
|-plugins 子有的插件配置 如axios
|-routes 授权路由
|-app.js 运行时的配置文件 对等 react的index.js
|- global.css 全局样式
|-umirc 编译时配置
资源指向
- 支持@指向src别名
- 相对指向 src
- 绝对指向 public
- img标签 指向public或者服务器
- 静态图片: import | require |
- 数据图片: 绝对指向public | 服务器
路由
路由使用约定式,对齐nuxt,也可配置umirc后使用配置型路由,路由组件同react-router解构来自umiimport {NavLink} from 'umi'
约定式路由
|-pages
|-index.js "/" 路由 页面
|-index/index.js "/" 路由 页面
|-goods.js // /goods路由
|-goods/index.js // /goods路由页
//goods 路由的默认页 return null 代表没有默认页
|-goods/$id.js // /goods/1 路由页
|-goods/$id$.js // /goods 路由 goods下没有index 时 展示了 /goods 或 /goods/id,代表id可选
|-goods/_layout.js // /goods 路由页 有自己的展示区 {props.children} 不然会在父的展示区展示
|-goods/category.js // /goods/category 路由
|-goods/category/index.js // /goods/category 路由
|-404.js 生产模式下的404页,开发模式默认umi的
//document.ejs 浏览器模板页 没有umi自动生成,有取当前模板结构
路由跳转
声明式
<Link to={{pathname:'/goods/2',query:{a:11,b:22}}}>商品 002</Link>
编程式
import router from 'umi/router';
router.push('/login')
router.push({
pathname:'/goods/1',
query:{a:11,b:22}
})
// search:'a=111&b=222' 如果传递了search 会覆盖query,query只是对search的封装引用了search的值
props.history.xx() //可用
传接参
props.match.params
props.location.query 返回对象
授权路由
前置
//组件内部守卫, 在组件文件最上方
/*
* title: reg Page
* Routes:
* - ./src/routes/Auth.js
* */
后置
//同react
<Prompt
when={true}
message={(location) => {
return window.confirm(`确认要去向 ${location.pathname}?`);
}}
/>
数据交互
自带mock,无需代理,其他数据需要代理,本地数据放在public
app.js 配置
在出错时显示个 message 提示用户,在加载和路由切换时显示个 loading,页面载入完成时请求后端,根据响应动态修改路由,引入一些插件配置模块,在运行时带入这些配置
配置思想
对外导出一堆umi内部可识别的函数 来完成配置
export function render(oldRender) {
渲染应用之前做权限校验,不通过则跳转到登录页
oldRender() 渲染应用的函数
}
export function onRouteChange({ location, routes, action }) {
初始加载和路由切换时的逻辑,用于路由监听, action 是路由切换方式如:push
}
export function rootContainer(container) {
封装 root container 外层有个 Provider 要包裹 的场景 必须要有返回值,无需是可以不写这个函数
// const DvaContainer = require('@tmp/DvaContainer').default;
return React.createElement(DvaContainer, null, container);
}
export function modifyRouteProps(props, { route }) {
修改传给路由组件的 props , 所有组件都中
props,Object,原始 props
route,Object,当前路由配置
return { ...props, 混入后的key: 值 };
}
umirc 配置
配置编译环境 umirc 无需重启
export default {
history:'hash' 路由模式 默认历史记录
publicPath: "/public/" 数据资源在非根目录或cdn时使用, 必须 绝对路径 + /结尾 影响打包后的位置会指向public
disableCSSModules: false 关闭css模块化 默认开启,推荐开启
cssModulesExcludes:['index.css','login.css'] 指定项目目录下的文件不走 css modules 不支持scss
sass: {} 支持scss 需要安装 sass-loader node-sass
mountElementId:'app' 指定 react app 渲染到的 HTML 元素 id。
proxy: {
'/api': {
target: 'http://localhost:3001',
"changeOrigin": true,
// pathRewrite: {'^/api' : ''}
},
'/douban': {
target: 'https://douban.uieee.com',
"changeOrigin": true,
pathRewrite: {'^/douban' : ''},
secure: false //接受https的代理
}
},
routes: [ //使用手动配置路由,约定式路由失效
{
path: '/',
component: '../layouts/index',
routes: [
{ path: '/', component: '../pages/index.js' },
{ path: '/users/', component: '../pages/users/index.js' },
{ path: '/users/list', component: '../pages/users/list.js' },
{ path: '/users/:id', component: '../pages/users/$id.js' },
]
}
],
plugins: [ 插件配置
['umi-plugin-react', {
antd: false, 是否开启antd 需要安装依赖
dva: false, 是否器dva支持 需要安装依赖
dynamicImport: {//按需加载 生产环境下有效果
webpackChunkName: true,//实现有意义的异步文件名
loadingComponent: './components/Loading.js',//指定加载时的loading组件路径
},
title: 'umitest', 开启 title 插件
routes: {
exclude: [ 用于忽略某些路由,比如使用 dva 后,通常需要忽略 models、components、services 等目录
/components\//,
],
},
}],
],
}
umi3
使用react开发,可扩展的企业级前端应用框架,让react开发高度可配置(合并式,无需触碰webpack底层),支持各种功能扩展和业务需求,可替换next完成服务端渲染
Umi 如何工作
把大家常用的技术栈进行整理,收敛到一起,让大家只用 Umi 就可以完整 80% 的日常工作。CRA不支持配置(合并式),不是框架,但umi是,且支持配置,并内置插件的方式整合开发者遇到的一些常规业务问题(如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面)
技术收敛
创建项目
//首先得有 node,并确保 node 版本是 10.13 或以上
mkdir project
cd project
yarn create @umijs/umi-app
yarn 安装依赖
yarn start 启动开发
yarn build 打包构建
项目结构
.
├── package.json //插件和插件集 @umijs/ 开头的依赖会被自动注册为插件或插件集。
├── .umirc.ts //配置文件,包含 umi 内置功能和插件的配置
├── .env //环境变量
├── dist //打包后
├── mock //mock 文件,此目录下所有 js 和 ts 文件会被解析为 mock
├── public //静态资源
└── src
├── .umi //临时文件目录 忽视
├── layouts/index.tsx //布局组件
├── pages // 页面级别路由组件
├── index.less
└── index.tsx
└── app.ts //运行时配置文件 扩展运行时的能力
├── global.css // 全局样式,如果存在此文件,会被自动引入到入口文件最前面 无效
不希望类型检查,可以更名为jsx或者js
样式和资源指向
- 支持
@
别名指向src - css 里面
~@
执向src - 相对 根在 src
- 绝对根在 public
- 静态图片引入: import | require | 指向src
- 数据图片引入: 指向public | 服务器
src/global.css
为全局样式- 当做 CSS Modules 用时,Umi 会自动识别 如:
import styles from './foo.css'
路由
路由使用约定式,对齐nuxt
,也可配置umirc
后使用配置型路由,路由组件同react-router解构来自umi import {NavLink} from 'umi'
约定式路由
也叫文件路由,不需要手写配置,没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages
目录
|-pages
|-index.tsx "/" 路由 页面
|-index/index.tsx "/" 路由 页面
|-goods.tsx // /goods路由
|-goods/index.tsx // /goods路由页
//goods 路由的默认页 return null 代表没有默认页
|-goods/[id].tsx // /goods/1 路由页
|-goods/_layout.tsx // /goods 路由页 有自己的展示区 {props.children} 不然会在全局路由展示
|-goods/category.tsx // /goods/category 路由
|-goods/[uid]/comment.tsx // /goods/23/comment 路由
|-404.js 生产模式下的404页,//目前有问题 https://github.com/umijs/umi/issues/4437
//document.ejs 浏览器模板页 没有umi自动生成,有取当前模板结构 ***
|- layouts
|-index.tsx //全局路由。返回一个 React 组件,并通过 props.children 渲染子组件
不同的全局 layout
你可能需要针对不同路由输出不同的全局 layout,但你仍可以在 src/layouts/index.tsx
中对 location.path
做区分,渲染不同的 layout 。
比如想要针对 `/user输出简单布局,
//layouts/user.jsx
import React from 'react';
export default (props) => {
return (
<div>
<h1>user layouts</h1>
{props.children}
</div>
);
}
//layouts/index.jsx
import user from './user'
export default function(props) {
if (props.location.pathname === '/user') {
return <User>{ props.children }</User>
}
return (
<>
...默认全局路由
{ props.children }
</>
);
}
扩展路由属性
支持在代码层通过导出静态属性的方式扩展路由。
import React from 'react';
import './user.css';
function User(){
return (
<div>
<h1 className={'box'}>Page user</h1>
</div>
);
}
User.title = 'user Page';//修改路由页面标题
export default User;
其中的 title
会附加到路由配置中。
路由跳转
声明式
import {NavLink} from 'umi'
<NavLink activeStyle={{激活样式}} to="/goods/2?a=1&b=2">商品 002</NavLink>
<NavLink activeStyle={{激活样式}} to={{pathname:'/goods/2',query:{a:11,b:22}}}>商品 002</NavLink>
编程式
import {history} from 'umi';
history.push('/login')
history.push('/goods/1?a=1&b=2');
history.push({
pathname:'/goods/1',
query:{a:11,b:22},
search:'a=111&b=222' //如果传递了search 会覆盖query,query只是对search的封装引用了search的值
})
props.history.push({
pathname:'/goods/1',
query:{a:111,b:222},
})
传接参
props.match.params
props.location.query 返回对象
import {useLocation,useParams} from 'umi'
let locationHooks = useLocation();
let params = useParams();
{locationHooks.pathname}|{params.uid}
授权路由
前置
//app.ts 全局
import { history } from 'umi';
export function render(oldRender) {
fetch('/api/auth').then(auth => {
if (auth.isLogin) { oldRender() }
else { history.push('/login'); }
});
}
//路由级别 路由独享
//umirc
routes: [
{ path: '/user', component: 'user',
wrappers: [
'@/wrappers/auth',
],
},
{ path: '/login', component: 'login' },
]
//auth
export default (props) => {
const { isLogin } = useAuth();
if (isLogin) {
return <div>{ props.children }</div>;
} else {
redirectTo('/login');
}
}
后置
//同react
<Prompt
when={true}
message={(location) => {
return window.confirm(`确认要去向 ${location.pathname}?`);
}}
/>
异步路由
组件体积太大,不适合直接计入 bundle 中,以免影响首屏加载速度
//启用按需加载 p__index__index.chunk.css p__index__index.js
// umirc
export default {
dynamicImport: {},
}
配置型路由
umirc 中通过 routes
进行配置,格式为路由信息的数组
routes: [
{ path: '/login', component: 'login' },// 不写路径从 src/pages找组件
{ path: '/reg', component: 'reg' },
//不使用全局layout的配置可以写在 / 的上面
{
path: '/',
component: '@/layouts/index',
routes: [//通常在需要为多个路径增加 layout 组件时使用
{ path: '/index', component: 'index' },
// { exact: true, path: '/goods', component: '@/pages/goods' },
{ path: '/goods', component: '@/pages/goods/index' },
{
path: '/goods/:uid',
component: '@/layouts/goods-detail',//为uid层级页面指定layout
routes: [
{ path: '/goods/:uid', component: '@/pages/goods/[uid]' },
{ path: '/goods/:uid/comment', component: '@/pages/goods/[uid]/comment' },
{ component: '@/pages/404' }//子集的404,每一级都可以设定404
],
},
{
path: '/user',
component: '@/layouts/user',
routes: [
{ path: '/user', component: '@/pages/user/index' },
{ path: '/user/:id', component: '@/pages/user/[id]' },
{ component: '@/pages/404' },
],
},
{ path:'/', redirect: '/index' }, //跳转
{ component: '@/pages/404' },
],
}
]
数据交互
mock
Umi 约定 /mock
文件夹下所有文件为 mock 文件无需代理,其他服务器数据需要代理
import mockjs from 'mockjs'
export default {
// 支持值为 Object 和 Array
'GET /umi/goods': [{id:1,name:'韭菜'},{id:2,name:'西红柿'}],
// 支持自定义函数,API 参考 express@4
'POST /umi/login': (req, res) => {
// 添加跨域请求头
// res.setHeader('Access-Control-Allow-Origin', '*');
console.log('req',req.body);//完成业务
res.send({
err: 404,
msg:'登录失败1'
});
},
//引入mockjs
'GET /umi/goods/home': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
}),
}
//模拟延时
import { delay } from 'roadhog-api-doc'
export default delay({同上},1000)
貌似不支持resFulApi 风格请求,jsonserver支持,值得考虑
代理
//umirc
proxy: {
'/api': {
target: 'http://localhost:9001', //node服务
"changeOrigin": true,
// pathRewrite: {'^/api' : ''}
},
'/mock': {
target: 'http://localhost:3333', //自建的jsonserver服务
"changeOrigin": true,
// pathRewrite: {'^/mock' : ''},
// secure: false //接受https的代理
}
}
axios
// plugins/axios.js
import axios from 'axios';
//添加一个请求的拦截
axios.interceptors.request.use(function (config) {
config.headers={
'token':'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsZXgiLCJfaWQiOiI1ZThhMGQ2MzczNDg2MDIzYTRmZDY4ZGYiLCJpYXQiOjE1ODkwMDIzMDUsImV4cCI6MTU4OTA4ODcwNX0.sStWoKBk2mYwa_1-AJOQobL7LBR82DnOseCeTds5ECs'
};
console.log('axios拦截器');
return config;
}, function (error) {
return Promise.reject(error);
});
// 添加一个响应的拦截
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
return Promise.reject(error);
});
// React.axios = axios;//实例属性 无效
// window.axios = axios; //全局API 无效
export default axios;//需要引入plugins下的axios 才有拦截
插件
umi3提供的插件(内置),基本上无需安装,无需配置,直接使用即可
scss
umi 默认支持 less,要使用scss需要添加插件@umijs/plugin-sass
,默认已安装,支持 Dart Sass 切换到 Node Sass,需安装 node-sass 依赖
yarn add @umijs/plugin-sass -D //无需配置就可以支持dart scss
yarn add node-sass -D // 使用node-sass时 需要的依赖
//使用node-sass时的配置 umirc
sass: {
implementation: require('node-sass'),
}
antd
内置插件,默认开启,直接使用,对齐antd使用
import { Button,message } from 'antd';
<Button
type="primary"
onClick={()=>message.info('message')}
>Primary</Button>
request
介绍
网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的api调用方式, 简化使用
特性 | request | fetch | axios |
---|---|---|---|
实现 | 浏览器原生支持 | 浏览器原生支持 | XMLHttpRequest |
大小 | 9k | 4k (polyfill) | 14k |
query 简化 | ✅ | ❌ | ✅ |
post 简化 | ✅ | ❌ | ❌ |
超时 | ✅ | ❌ | ✅ |
缓存 | ✅ | ❌ | ❌ |
错误检查 | ✅ | ❌ | ❌ |
错误处理 | ✅ | ❌ | ✅ |
拦截器 | ✅ | ❌ | ✅ |
前缀 | ✅ | ❌ | ❌ |
后缀 | ✅ | ❌ | ❌ |
处理 gbk | ✅ | ❌ | ❌ |
中间件 | ✅ | ❌ | ❌ |
取消请求 | ✅ | ❌ | ✅ |
基本用法
request内置插件,默认开启,直接使用,使用对齐 umi-request 和 @umijs/hooks 的 useRequest
import {request} from 'umi'
// useRequest 接收了一个异步函数 getUsername ,在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loading , data , error 等状态
//request 对齐 axios
export default (props) => {
//data ~~ axios的res.data.data
//如果数据里面没有data 返回undefined
//通过配置 umirc request.dataField 可以指定
console.log('data',data)
useEffect(()=>{
//request ~~ axios
request('/api/goods/home',{params:{_limit:1}}).then(
//res ~~ axios的res.data
).catch()
},[]);
return (
<div>
</div>
);
}
拦截器
//app.ts
export const request = {
// timeout: 1000,
// errorConfig: {},
// middlewares: [],
requestInterceptors: [
(url, options)=>{// 请求地址 配置项
options.headers={token:''}
return {url,options}
}
],
responseInterceptors: [
(response, options) => {//响应体 请求时的配置项
console.log(response,options)
return response;
}
],
};
useRequest钩子
介绍
一个强大的管理异步数据请求的 Hook.
核心特性
- 自动请求/手动请求
- SWR(stale-while-revalidate)
- 缓存/预加载
- 屏幕聚焦重新请求
- 轮询
- 防抖
- 节流
- 并行请求
- loading delay
- 分页
- 加载更多,数据恢复 + 滚动位置恢复
- 错误重试
- 请求超时管理
- suspense
- …
用法
在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loading
, data
, error
等状态。
import {useRequest} from 'umi'
export default function RequestHooks(){
// 用法 1
const { data, error, loading } = useRequest('/mock/home');
// 用法 2
const { data, error, loading } = useRequest({
url: '/mock/home',
params:{_limit:1}
});
// 用法 3
const { data, error, loading } = useRequest((id)=> `/api/home/${id}`); //?
// 用法 4
const { data, loading, run } = useRequest((_limit) => ({
url: '/mock/home',
params: { _limit }
}), {
manual: true,//手动通过运行run触发
});
// 轮询
const { data, loading, run } = useRequest((_limit) => ({
url: '/mock/home',
params: { _limit }
}), {
manual: true,//手动通过运行run触发
pollingInterval:1000,//轮询 一秒读一次
pollingWhenHidden:false,//屏幕不可见时,暂停轮询
});
if (error) {
return <div>failed to load</div>
}
if (loading) {
return <div>loading...</div>
}
return (
<div>{JSON.stringify(data)}</div>
<button onClick={()=>run(1)}>手动</button>
);
}
并行请求
通过 options.fetchKey
,可以将请求进行分类,每一类的请求都有独立的状态
import { useRequest } from 'umi';
import { Button } from 'antd';
export default () => {
const { run, fetches } = useRequest((userId)=>({
url: '/mock/home',
params:{_limit:1,_page:userId-0}
}), {
manual: true,
fetchKey: id => id,
onSuccess:(res,params)=>{
console.log(res, params)
}
});
const users = [{ id: '1', username: 'A' }, { id: '2', username: 'B' }, { id: '3', username: 'C' }];
return (
<div>
<h3>并行请求:单击所有按钮,每个请求都有自己的状态</h3>
<ul>
{users.map((user => (
<li key={user.id} style={{ marginTop: 8 }}>
<Button loading={fetches[user.id].loading} onClick={() => { run(user.id) }}>读取</Button>
</li>
)))}
</ul>
</div>
);
};
防抖-解流-缓存
import { useRequest, request } from 'umi';
import { Input } from 'antd';
import React from 'react';
let {Search}=Input;
function getHome(search) {
console.log(1,search)
return request('/mock/home',{params:{_page:search-0, _limit:1}})
}
export default () => {
const { data, loading, run, cancel } = useRequest(getHome, {
// debounceInterval: 500, //频繁调用 run 以防抖策略进行请求
// throttleInterval: 500,//频繁触发 run ,则会以节流策略进行请求
// cacheKey:'homepage', //缓存 回退路由,在进入数据data还在
manual: true
});
return (
<div>
<h3>防抖</h3>
<Search
placeholder="输入页数"
onChange={e=>run(e.target.value)}
onBlur={cancel}
style={{ width: 300 }}
loading={loading}
/>
<br/>
{data && JSON.stringify(data)}
</div>
);
};
激活聚焦重求-loading防闪烁
import { useRequest } from 'umi';
import { Spin } from 'antd';
import React from 'react';
export default () => {
const { data, loading } = useRequest({
url:'/mock/home',
params:{_limit:1,_page:1}
}, {
refreshOnWindowFocus: true,//浏览器窗口 refocus 和 revisible 时,会重新发起请求
focusTimespan: 1000,//请求间隔,默认为 5000ms 。
// loadingDelay:1500 // loading防闪烁 可以延迟 loading 变成 true 的时间,有效防止闪烁
})
return (
<div>
<p>屏幕聚焦重新请求</p>
<Spin spinning={loading}>
<div>{data && JSON.stringify(data)}</div>
</Spin>
</div>
)
}
状态变化重请求
import { useRequest } from '@umijs/hooks';
import { Spin, Select } from 'antd';
import React, { useState } from 'react';
export default () => {
const [pageNum, setPageNum] = useState('1');
const { data, loading } = useRequest(() => ({
url:'/mock/home',
params:{_limit:1,_page:pageNum}
}), {
refreshDeps: [pageNum]//pageNum变化时,会使用之前的 params 重新执行
});
return (
<div>
<Select onChange={setPageNum} value={pageNum} style={{ marginBottom: 16, width: 120 }}>
<Select.Option value="1">page 1</Select.Option>
<Select.Option value="2">page 2</Select.Option>
<Select.Option value="3">page 3</Select.Option>
</Select>
<Spin spinning={loading}>
home: {data && JSON.stringify(data)}
</Spin>
</div>
);
};
app.ts 配置
运行时配置,跑在浏览器端, 如:在出错时显示个 message 提示用户,在加载和路由切换时显示个 loading,页面载入完成时请求后端,根据响应动态修改路由,引入一些插件配置模块,在运行时带入这些配置
配置思想
对外导出一堆umi内部可识别的函数 来完成配置
//修改路由
export function patchRoutes({ routes }) {
//比如在最前面添加一个 /foo 路由
routes.unshift({
path: '/foo',
exact: true,
component: require('@/extraRoutes/foo').default,
});
}
//权限校验
import { history } from 'umi';
export function render(oldRender) {
fetch('/api/auth').then(auth => {
if (auth.isLogin) { oldRender() }
else { history.push('/login'); }
});
}
//动态更新路由
let extraRoutes;
export function patchRoutes({ routes }) {
merge(routes, extraRoutes);
}
export function render() {
//请求服务端根据响应
fetch('/api/routes').then((res) => { extraRoutes = res.routes })
}
export function onRouteChange({matchedRoutes, location, routes, action }) {
//初始加载和路由切换时的逻辑,用于路由监听, action 是路由切换方式如:push
//用于做埋点统计
//动态设置标题
document.title = matchedRoutes[matchedRoutes.length - 1].route.title || ''
}
export function rootContainer(container,args) {
//封装 root container 外层有个 Provider 要包裹 的场景 必须要有返回值,无需是可以不写这个函数
// const DvaContainer = require('@tmp/DvaContainer').default;
return React.createElement(DvaContainer, null, container);
//args 包含:
//routes,全量路由配置
//plugin,运行时插件机制
//history,history 实例
}
export function modifyRouteProps(props, { route }) { // ***
//修改传给路由组件的 props , 所有组件都中
//props,Object,原始 props
//route,Object,当前路由配置
return { ...props, 混入后的key: 值 };
}
umirc 配置
配置编译环境 umirc 无需重启, 推荐在 .umirc.ts
中写配置。如果配置比较复杂需要拆分,可以放到 config/config.ts
中,并把配置的一部分拆出去,比如路由。
两者二选一,.umirc.ts
优先级更高。 文档
import { defineConfig } from 'umi';
export default defineConfig({
//设置 node_modules 目录下依赖文件的编译方式
nodeModulesTransform: {
type: 'none',
},
// 配置型路由 权重高于约定式 且不可合并
// routes: [
// { component: '@/pages/404' },
// ],
history: { type: 'hash' }, //哈希路由模式,解决强刷,或者通过后端解决
//关闭mock
// mock: false,
//多个代理 mock数据无需代理
proxy: {
'/api': {
target: 'http://localhost:9001',
"changeOrigin": true,
// pathRewrite: {'^/api' : ''}
},
'/mock': {
target: 'http://localhost:333',
"changeOrigin": true,
// pathRewrite: {'^/mock' : ''},
// secure: false //接受https的代理
}
},
//按需加载功能默认是关闭的
dynamicImport: {
loading: '@/loading', //定义按需加载时的loading组件
},
title: 'hi',//配置应用统一标题
mountElementId:'app',//指定 react app 渲染到的 HTML 元素 id。
devServer:{
port:8082
},
favicon: '/favicon.ico',//使用本地的图片,图片请放到 public 目录
//配置 <head> 里的额外脚本,数组项为字符串或对象
headScripts: [
`alert(1);`,
`http://code.jquery.com/jquery-2.1.1.min.js`,
],
//配置 <head> 额外 link 和style
styles: [
`body { color: red; }`,
`https://a.com/b.css`,
],
});
SSR
dva
dva = React-Router + Redux + Redux-saga 项目
核心
-
View:React 组件构成的视图层
-
Action:一个对象,描述事件
-
connect 方法:一个函数,绑定 State 到 View
-
dispatch 方法:一个函数,发送 Action 到 State
-
model 数据管理模块
- State:一个对象,保存整个应用状态
- reducers: 一个对象 同步业务
- effects: 一个对象 处理异步
- subscriptions: 订阅数据源
umi2下使用
umirc配置
antd: true, 开启antd
dva: true, 开启dva数据流
routes: {//路由
exclude: [//排除
/models\//,
/services\//,
/model\.(t|j)sx?$/,
/service\.(t|j)sx?$/,
/components\//,
],
},
app配置
export const dva = {
config: {
onError(err) {//监听错误
err.preventDefault();
console.error('dva config',err.message);
},
//初始数据 不给就取根models的state,给了就不取
initialState: {
namespace: {
key:value
},
},
},
plugins: [
// require('dva-logger')(),
],
}
model
// src/models 全局
// src/pages/models 页面
export default {
namespace: 'global',//所有models里面的namespace不能重名
state: { //存放数据
stateName: stateValue,
},
reducers: { //处理同步 左key 等于dispatch({type:key
reducersKey(state,{type,payload}) {
return {
...state,
stateName: newValue,
};
}
},
effects: { //处理异步 左key 等于dispatch({type:key
*login(action, { call, put, select }) {
// call:执行异步函数 如: const result = yield call(fetch, '/todos');
// put:发出一个 Action,类似于 dispatch
// select: 从state里获取数据 如: const todos = yield select(state => state.todos);
const res = yield call(fetch,'/mock/home')
const data = yield res.json();
yield put({
type: 'reducerKey'|'effectsKey',
});
},
},
subscriptions: {
//场景: 时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化
//订阅一个数据源 根据条件 dispatch 需要的 action
//subsription中的方法名是随意定的,每次变化都会一次去调用里面的所有方法
随意的key({ dispatch, history }) {
//路由监听
history.listen(({ pathname, query }) => {});
//需要导入import key from 'keymaster' 监听键盘
key('⌘+i, ctrl+i', () => { dispatch({type:reducersKey|effectsKey}) });
//窗口变化
window.onresize|onscroll = function(){
console.log('onresize')
}
}
}
}
页面
layouts、组件接入 dva
import {connect} from 'dva'
function 组件(props){
props.propname
props.dispatch({type,payload})
//type:'namespace/reducersKey|effectsKey'
//type:'namespace/effectsKey'
}
function mapStateToProps(state) {
return {
propname: state.namespace.stateKey,
propname: state.namespace
};
}
export default connect(mapStateToProps)(组件);
//layouts
import withRouter from 'umi/withRouter';
import {connect} from 'dva'
export default withRouter(connect(mapStateToProps)(组件));
umi3下使用
umi3以插件的形式整合 dva 数据流,配置默认开启
model
// src/models/modelname.js 全局
import { history } from 'umi';
import key from 'keymaster';
export default {
namespace: 'global',//所有models里面的namespace不能重名
state: {
title:'UMI+DVA',
text: '我是全局text',
login: false,
a:'全局models aaaa',
},
reducers: {//处理同步 左key 等于dispatch({type:key
setText(state) {
return {
...state,
text: '全局设置 后的text'+Math.random().toFixed(2),
};
},
setTitle(state,action) {
return {
...state,
text: `全局设置 后的title${action.payload.a}/${action.payload.b}`,
};
},
signin:(state)=>({
...state,
login: true,
}),
},
effects: {
//处理异步 左key 等于dispatch({type:key
//call:执行异步函数
// const result = yield call(fetch, '/todos');
//put:发出一个 Action,类似于 dispatch
//select: 从state里获取数据
//const todos = yield select(state => state.todos);
*login(action, { call, put, select }) {
const res = yield call(fetch,'/umi/goods/home')
const data = yield res.json();
console.log('*login',data);
yield put({
type: 'signin',
});
yield put(history.push('/'));
// yield put(routerRedux.push('/'));
},
*throwError(action, effects) {
console.log(effects);
throw new Error('全局effects 抛出的 error');
},
},
subscriptions: {
//场景: 时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化
//订阅一个数据源 根据条件 dispatch 需要的 action
//subsription中的方法名是随意定的,每次变化都会一次去调用里面的所有方法
listenRoute({ dispatch, history }) {
history.listen(({ pathname, query }) => {
console.log('global subscriptions',pathname,query);//根据不同pathname加载不同数据发actions给reducers组件绑定state就好
});
},
listenKeyboard({dispatch}) {//监听键盘
key('⌘+i, ctrl+i', () => { dispatch({type:'setText'}) });
},
listenResize({dispatch}) {//监听窗口变化
window.onresize = function(){
console.log('onresize')
}
},
listenScroll({dispatch,history,done}){
window.οnscrοll=function () {
console.log('onscroll')
}
}
},
};
// src/pages/pagename/model.js 页面
//model下面如果 没有多个state key 可以不用出现models目录
export default {
namespace: 'count',
state: 0,//count:0
reducers: {
increase(state) {
return state + 1;
},
decrease(state) {
return state - 1;
},
}
};
// src/pages/pagename/models/modelname.js 页面
export default {
namespace: 'a', //组件 通过state.a 得到 'goods page data a'
state: 'goods page data a',
reducers: {},
};
组件
layouts、组件接入 dva
import {connect} from 'umi'
function 组件(props){
props.propname
props.dispatch({type,payload})
//type:'namespace/reducersKey|effectsKey'
//type:'namespace/effectsKey'
}
function mapStateToProps(state) {
return {
propname: state.namespace.stateKey,
propname: state.namespace
};
}
export default connect(mapStateToProps)(组件);
//layouts
import {withRouter, connect} from 'umi'
export default withRouter(connect(mapStateToProps)(组件));
@umijs/hooks
useDrop & useDrag
一对帮助你处理在拖拽中进行数据转移的 hooks
useDrop 可以单独使用来接收文件、文字和网址的拖拽。
useDrag 允许一个 dom 节点被拖拽,需要配合 useDrop 使用。
向节点内触发粘贴时也会被视为拖拽的内容
使用
import React from 'react';
import { useDrop, useDrag } from '@umijs/hooks';
export default () => {
const getDragProps = useDrag();
//props 需要透传给接受拖拽区域 dom 节点的 props
//isHovering 是否是拖拽中,且光标处于释放区域内
const [props, { isHovering }] = useDrop({
onText: (text, e) => {
console.log(text, e);
},
onFiles: (files, e) => {
console.log(e, files);
},
onUri: (uri, e) => {
console.log(uri, e);
},
onDom: (content, e) => {
console.log(content, e);
},
});
return (
<div>
<div>useDrop 可以单独使用来接收文件、文字和网址的释放</div>
<div>useDrag 允许一个 dom 节点被拖拽,需要配合 useDrop 使用</div>
<div>向节点内触发粘贴时也会被视为拖拽的内容</div>
<div style={{ border: '1px dashed #e8e8e8', padding: 16, textAlign: 'center' }} {...props}>
{isHovering ? '撒手' : '拖到这'}
</div>
<div style={{ display: 'flex', marginTop: 8 }}>
<div
{...getDragProps('box')}
style={{ border: '1px solid #e8e8e8', padding: 16, width: 80, textAlign: 'center', marginRight: 16 }}
>
box
</div>
</div>
</div>
);
};
useDrag Result
参数 | 说明 | 类型 |
---|---|---|
getDragProps | 一个接收拖拽的值,并返回需要透传给被拖拽节点 props 的方法 | (content: any) => props |
useDrop Result
参数 | 说明 | 类型 |
---|---|---|
props | 需要透传给接受拖拽区域 dom 节点的 props | - |
isHovering | 是否是拖拽中,且光标处于释放区域内 | boolean |
useDrop Params
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
onText | 拖拽文字的回调 | (text: string, e: Event) => void | - |
onFiles | 拖拽文件的回调 | (files: File[], e: Event) => void | - |
onUri | 拖拽链接的回调 | (text: string, e: Event) => void | - |
onDom | 拖拽自定义 dom 节点的回调 | (content: any, e: Event) => void | - |
useVirtualList
解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题
使用
import React from 'react';
import { useVirtualList } from '@umijs/hooks';
export default () => {
//list 当前需要展示的列表内容 {data: T, index: number}[]
// containerProps 滚动容器的 props {}
// wrapperProps children 外层包裹器 props {}
const { list, containerProps, wrapperProps } = useVirtualList(Array.from(Array(99999).keys()), {
overscan: 30,//视区上、下额外展示的 dom 节点数量
itemHeight: 60,//行高度,静态高度可以直接写入像素值,动态高度可传入函数
});
return (
<>
<div {...containerProps} style={{ height: '300px', overflow: 'auto' }}>
<div {...wrapperProps}>
{list.map((ele, index) => (
<div
style={{
height: 52,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #e8e8e8',
marginBottom: 8,
}}
key={ele.index}
>
Row: {ele.data}
</div>
))}
</div>
</div>
</>
);
};
API
const result:Result = useVirtualList(originalList: any[], Options);
Result
参数 | 说明 | 类型 |
---|---|---|
list | 当前需要展示的列表内容 | {data: T, index: number}[] |
containerProps | 滚动容器的 props | {} |
wrapperProps | children 外层包裹器 props | {} |
scrollTo | 快速滚动到指定 index | (index: number) => void |
Params
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
originalList | 包含大量数据的列表 | T[] | [] |
options | 可选配置项,见 Options | - | - |
Options
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
itemHeight | 行高度,静态高度可以直接写入像素值,动态高度可传入函数 | number | ((index: number) => number) | - |
overscan | 视区上、下额外展示的 dom 节点数量 | number | 10 |
useEventTarget
常见表单控件(通过 e.target.value获取表单值) 的 onChange 跟 value 逻辑封装,支持 自定义值转换 跟 重置 功能
使用
import React from 'react';
import { Input, Button } from 'antd';
import { useEventTarget } from '@umijs/hooks'
export default () => {
//value 表单控件的值 T
// onChange 表单控件值发生变化时候的回调 (e: { target: { value: T }}) => void
// reset 重置函数 () => void
const [valueProps, reset] = useEventTarget('初始值');
return (<>
<Input {...valueProps} style={{ width: 200, marginRight: 20 }}/>
<Button type="primary" onClick={reset}>重置</Button>
</>
);
};
API
const [ { value, onChange }, reset ] = useEventTarget<T, U>(initialValue?: T, transformer?: (value: U) => T );
Result
参数 | 说明 | 类型 |
---|---|---|
value | 表单控件的值 | T |
onChange | 表单控件值发生变化时候的回调 | (e: { target: { value: T }}) => void |
reset | 重置函数 | () => void |
Params
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
initialValue? | 可选项, 初始值 | T | - |
transformer? | 可选项,可自定义回调值的转化 | (value: U) => T | - |
umi+dva项目
开发
部署
umi+dva+ts
React-Native
React-Native介绍
React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的UI框架 React 在原生移动应用平台的衍生产物,目前支持iOS和安卓两大平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。
React Native使你能够在Javascript和React的基础上获得完全一致的开发体验,构建世界一流的原生APP。React Native着力于提高多平台开发的开发效率 —— 仅需学习一次,编写任何平台。(Learn once, write anywhere),官网
Native App
即原生开发模式,开发出来的是原生程序,不同平台上,Android和iOS的开发方法不同,开发出来的是一个独立的APP,能发布应用商店,有如下优点和缺点。
优点:
- 直接依托于操作系统,交互性最强,性能最好
- 功能最为强大,特别是在与系统交互中,几乎所有功能都能实现
缺点:
- 开发成本高,无法跨平台
- 升级困难
- 维护成本高
Web App
即移动端的网站,将页面部署在服务器上,然后用户使用各大浏览器访问,不是独立APP,无法安装和发布Web网站一般分两种,MPA(Multi-page Application)和SPA(Single-page Application)。而Web App一般泛指后面的SPA形式开发出的网站(因为可以模仿一些APP的特性),有如下优点和缺点。
优点:
- 开发成本低,可以跨平台,调试方便
- 版本升级容易
- 维护成本低
- 无需安装 App,不占用手机内存(通过浏览器即可访问)
缺点:
- 性能低,用户体验差
- 依赖于网络,页面访问速度慢,耗费流量
- 功能受限,大量功能无法实现(无法调用原生 API)
- 临时性入口,用户留存率低
Hybrid App
即混合开发,也就是半原生半Web的开发模式,有跨平台效果,实质最终发布的仍然是独立的原生APP(各种的平台有各种的SDK),这是一种 Native App 和 Web App 折中的方案,保留了 Native App 和 Web App 的优点。
优点:
- 开发成本较低,可以跨平台,调试方便
- 维护成本低,功能可复用
- 更新较为自由(只下载资源不更新 apk )
- 学习成本较低(前端开发人员不用学习底层 api)
- 功能更加完善,性能和体验要比起web app 好
缺点:
- 相比原生,性能仍然有较大损耗
- 不适用于交互性较强的app(主要适用于新闻阅读类与信息展示类的 APP)
React Native App
Facebook发起的开源的一套新的APP开发方案,Facebook在当初深入研究Hybrid开发后,觉得这种模式有先天的缺陷,所以果断放弃,转而自行研究,后来推出了自己的“React Native”方案,不同于H5,也不同于原生,更像是用JS写出原生应用,有如下优点和缺点
优点:
- 开发成本在 Hybrid 和 Native 开发之间 ,大部分代码还是可复用的,
- 性能体验高于Hybrid,性能相比原生差别不大
- 技术日益成熟,发展迅猛
缺点:
- 门槛相对 Web App 与 Hybrid App 来说相对高一点(也需要了解 Native 层)
开发模式的对比
环境搭建
使用expo-cli开发react应用程序需要两种工具:本地开发工具和用于打开应用程序的移动客户端,需要在计算机上安装Node.js(版本10或更新版本)
安装Expo CLI 到pc开发机器
npm install -g expo-cli
手机上安装 expo client
从Play商店下载Android版或从App Store 下载iOS版
- 真机测试 更真实
- 打开手机expo client->profile->options->sigin in 注册一个账号 并 登陆
- 安卓真机需要 打开usb调试
pc上装模拟器
- pc机模拟手机来测试 速度快
- for windows: 夜游,逍遥… 推荐: mumu 网易模拟器,百度一下
- 在mumu模拟器上安装 apk -> expo client安装到模拟器 -> 允许expo在其他应用上层显示->登陆expo账号
- for ios: Xcode, 目前只有安卓模拟器,没有好用的苹果模拟器
创建react-native项目
expo init 目录
选择模板: (Use arrow keys)
----- Managed workflow -----
> blank 空模板
tabs 带路由
cd 目录
npm start | yarn start 看你init时用的是什么工具安装,这里就用什么工具
? 回车 查看expo帮助
s 回车 登录expo账号 输入在手机expo注册的账号, 登录只需要做一次
shift + r 重启
npm start | yarn start
注意
- 保持 手机上expo的账号 和项目创建时 s 的账号一致
- 保持pc和手机在同一网段,后期热刷新
- 卡顿时,摇手机 refresh -> pc 机 npm | yarn start + 手机expo client重开
打包apk
npm install -g expo-cli 已安装侧跳过
配置app.json
{
"expo": {
"name": "应用名称",
"slug": "expo-test-win10", //上传到expo时的目录名
"icon": "./assets/icon.png", //桌面图标
"splash": {
"image": "./assets/splash.png",//欢迎图片
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"package": "top.uncle9.expo" //添加安卓的package属性,域名倒放,有无域名无所谓
}
}
}
expo build:android 打包到安装apk,ios需要开发id
1) Let Expo handle the process! √ 让Expo为您生成密钥库
2) I want to upload my own keystore! 上传自己的
Published完成后有个在线地址,或者登陆expo账号去下载apk,之后安卓到模拟器,或真机官网参考
样式
所有的核心组件都接受名为`style`的属性,使用了驼峰命名法,例如将`background-color`改为`backgroundColor`,可以传入一个数组——在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承,尺寸都是无单位的,表示的是与设备像素密度无关的逻辑像素点
用法1: <Text style={{key:value,key:value}}>xx</Text>
用法2: <Text style={[{key:value}],[{key:value}]}>xx</Text>
用法3: <Text style={styles.red}>xx</Text>
const styles = StyleSheet.create({
red: {
color: 'red',
},
});
布局
React Native 中使用 flexbox 规则来指定某个组件的子元素的布局,flexDirection
的默认值是column
而不是row
,而flex
也只能指定一个数字值。布局图解
相对定位
React Native 中,position 默认值为 relative
,即相对布局。
- 如果父组件没有设置 flex 值,则子组件不论是否设置 flex ,子组件都不会显示在界面上
如下示例代码中,子组件会等比例的占满屏幕
return (
<View style={{}}>
<View style={{ flex: 1, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
- 如果父组件设置了 flex 值,子组件设置了 flex 值的同时,也设置了高度值,则高度无效
如下示例代码中,子组件会等比例的占满屏幕,设置高度并不影响所占的比例
return (
<View style={{
flex: 1,
}}>
<View style={{ flex: 1, height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
- 如果父组件设置了 flex 值,子组件没有设置 flex 值,只设置了高度值,则高度有效
如下示例代码中,子组件在界面所占的比例受高度控制,最后一个子组件则自动占满剩余空间
return (
<View style={{
flex: 1,
backgroundColor: 'yellow',
}}>
<View style={{ height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ height: 90, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
绝对布局
样式设置 position: ‘absolute’
- 绝对布局情况下,设置 flex 不能达到页面整屏占满的效果,需要同时设置组件的宽高,否则父组件与子组件都将无法显示
正确设置父组件的样式方式
return (
<View style={{
position: 'absolute',
backgroundColor: 'yellow',
width: Dimensions.get('window').width,
height: Dimensions.get('window').height
}}>
<View style={{ flex: 1, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
- 只设置父组件的宽,设置子组件的高度有效,子组件的flex无效
如下示例代码中,只有第一个设置了高度的view会显示在界面上,而设置了flex的view不会显示
return (
<View style={{
position: 'absolute',
backgroundColor: 'yellow',
width: Dimensions.get('window').width,
}}>
<View style={{ height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
- 只设置父组件的高,则设置子组件的高度和flex均无效,父组件与子组件都无法显示
如下示例代码中,父组件与子组件都不会显示
return (
<View style={{
position: 'absolute',
backgroundColor: 'yellow',
height: Dimensions.get('window').height
}}>
<View style={{ height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
- 同时设置了父组件的宽和高的前提下,再设置子组件的高度和flex均有效
如下示例代码中,分别设置了高度和flex的view均会显示在界面上
return (
<View style={{
position: 'absolute',
backgroundColor: 'yellow',
width: Dimensions.get('window').width,
}}>
<View style={{ height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
</View >
);
- 父组件与子组件同时绝对布局的情况下,且设置绝对布局的子组件写在最后一个时,保持原则一样,则父组件与子组件可正常显示
如下示例代码中,子组件均能正常显示,且设置了flex的view会将剩余的屏幕占满显示,不会被绝对布局的同级子view阻挡
return (
<View style={{
position: 'absolute',
backgroundColor: 'yellow',
width: Dimensions.get('window').width,
height: Dimensions.get('window').height
}}>
<View style={{ height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
<View style={{ position: 'absolute', width: Dimensions.get('window').width, height: 90, bottom: 90, backgroundColor: 'lightgray' }}></View>
</View >
- 父组件与子组件同时绝对布局的情况下,且设置绝对布局的子组件没有写在最后一个时,保持原则一样,则绝对布局的子组件不会显示在父组件中,它会被前一个子组件覆盖
如下示例代码中,除相对布局的子组件能正常显示外,绝对布局的子组件会被同级的子view覆盖
return (
<View style={{
position: 'absolute',
backgroundColor: 'yellow',
width: Dimensions.get('window').width,
height: Dimensions.get('window').height
}}>
<View style={{ height: 90, backgroundColor: 'yellow' }}></View>
<View style={{ position: 'absolute', width: Dimensions.get('window').width, height: 90, bottom: 90, backgroundColor: 'lightgray' }}></View>
<View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
</View >
);
另外,可以给子组件设置不同的flex值,如flex:2, flex: 0.5,则子组件在界面上所占的比例会随之变化。
文本输入
注意 react 中的 onChange 对应的是 rn 中的 onChangeText
触摸事件
TouchableHighlight按下时变暗
TouchableOpacity降低按钮的透明度 可选
滚动视图
- 放置在ScrollView中的所有组件都会被渲染
长列表
- FlatList优先渲染屏幕上可见的元素
- SectionList 带有分组标签
网络请求
- 不存在跨域,安全机制与网页环境有所不同
- 为了安全默认只能访问https,对http做了限制
- 建议你增加HTTPS支持,而不是关闭http限制,会把 苹果提供的安全保障也被关闭了
- 可以使用所有数据交互库,不含jq
平台判断
针对不同平台编写不同代码的需求
- 使用
Platform
模块. - 使用特定平台扩展名.
- 某些属性可能只在特定平台上有效
- ActionSheetIOS 与 DatePickerAndroid
组件与API
组件规定视图表现,api实现编程式组件操作
react native 组件
Picker
图片
- 本地图片,可获取实际宽高,require里面必须是字符
- 网络图片,获取不到宽高,要给设定尺寸
- 背景图片必须指定宽高样式
expo组件
音频 Audio
视频 Video
相机 Camera
路由
推荐React Navigation 提供了简单易用的跨平台导航方案,在 iOS 和 Android 上都可以进行翻页式、tab 选项卡式和抽屉式的导航布局
安装:
yarn add react-navigation --save 其他都不用做
- navigation prop传递给每个在stack navigator中定义的路由
- this.props.navigation.navigate(‘Details’) 无栈,添加栈
- this.props.navigation.goBack() 回退
- this.props.navigation.push() 强制添加栈
- this.props.navigation.popToTop() 后退到 第一个栈
- 跳转到新路由,老路由组件不会卸载,返回时前组件会卸载