create-react-app 目录 --template typescript
jsx
语法的文件都需要以tsx
后缀命名Component
泛型参数声明,来代替PropTypes!global.d.ts
中进行声明定义types/
目录下定义好其结构化类型声明RouteComponentProps
RouteComponentProps
RouteChildrenProps
HTMLDivElement
match
React.FC
React.FunctionComponent
React.ReactNode
ComponentType
JSX.Element
Dispatch
AxiosRequestConfig
AxiosPromise
export default class 组件名 extends React.Component{}
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{
state: Readonly = initState;//state不建议通过实例属性修改,作为只读定义
}
initState 可定义到组件外部,也可定义在内部
let initState:IState = {
msg1: 'xx',
msg2: 12,
list: [
{ id: 1, name: 'alex' },
{ id: 2, name: 'alex2' },
{ id: 2, name: 'alex3' },
]
};
类型约束内部确定只读,必传,可选特性,默认值在类属性defaultProps设定
type IProps = { //未export 不通用
readonly id: number;//只读, props理论都应该是只读
title: string;//必传
num?: number;//可选
arr?: string[]
}
export default class 组件名 extends React.Component{
//props默认值
static defaultProps={
num:0
}
}
const initialState = { clicksCount: 0 };//先定义值
//再使用typeof推断类型,并设置只读,限定this.state修改
type TState = Readonly;
class Counter extends Component<{}, TState>{
readonly state: TState = initialState;//因为 React 不推荐直接更新 state 及其属性
render() {
const { clicksCount } = this.state;
return (
事件
{clicksCount}
)
}
// 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 => {
return {props.foo};
};
export {MyComponent}
使用 React.FC
别名定义函数组件
interface IProps {
readonly id?:number;
title?:string;
num?:number;
arr?:string[]
}
type IProps2=Readonly;//类型映射
//函数式组件
const Footer: React.FC = (props) => {
// props.num=2; //error 类型约束props为只读
// props.title='2323';//error
return (
footer
)
}
export default Footer
事件函数通过props传入
type TProps = {
onClick(e: MouseEvent): void//必传
text?: string//可选
}
//props默认值在函数接收参数时设定,handleClick为对象别名
const Button: FC = ({ onClick: handleClick, text = '按钮' }: TProps) => (
<>
无状态组件
>
);
通过props或直接嵌套的方式,向组件注入一些可变的元素
注入的元素有:string,number,boolean,ReactElement
举例:
interface Iprops {
header?: React.ReactNode;//jsx节点类型 string,number,boolean,ReactElement
body: ReactNode;//类型需要导入,来自react包
}
//! 代表排除null
class Comp2 extends React.Component {
render() {
return (
<>
---------接收可渲染的内容start--------
{this.props!.header}
{this.props.body}
---------接收可渲染的内容end--------
>
);
}
}
调用组件时,插入到组件的内容<组件>内容组件>
type AuthProps = {
children?: JSX.Element;//设定children类型,可选
[propName:string]:any//props可以接受其他任何值
}
const authState = { show: false }//先赋值
type AuthState = Readonly//后推断类型
class Auth extends Component{
static readonly defaultProps: AuthProps = {title:'bmw'}
readonly state: AuthState = authState
render() {
const { children } = this.props
return (
<>
组件自身内容
{children && children}
组件自身内容
>
)
}
}
类似于
type AuthProps = {
component?: ComponentType;//设定要接受的组件类型ComponentType
}
const authState = { show: false }
type AuthState = Readonly
class Auth extends Component{
static readonly defaultProps: AuthProps = {title:'bmw'}
readonly state: AuthState = authState
render() {
const { component: InjectedComponent } = this.props //InjectedComponent字面量别名
return (
<>
组件本身
{/* 调用通过props传入的组件 */}
{InjectedComponent && }
>
)
}
}
给调用的组件传递render函数,指定被调用组件的渲染内容
举例:
(
render后的内容
)} />
实现:
type AuthProps = {
render?: () => JSX.Element;
[propName:string]:any
}
const authState = { show: false }
type AuthState = Readonly
class Auth extends Component{
static readonly defaultProps: AuthProps = {title:'bmw'}
readonly state: AuthState = authState
render() {
const { render } = this.props
if (render) {
return render()//调用传入渲染函数,按照外部要求渲染
}
return (
原本内容
)
}
}
引用渲染完成后的元素
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 (
<>
refs
this.input = el}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
/>
>
);
}
componentDidMount(){
this.input != null && this.input.focus() //获取焦点
}
}
import React,{createRef} from 'react';
export default class Comp3 extends React.Component<{ //内联类型注解
value: string;
onChange: (value: string ) => void
}, {}> {
// 使用createRef函数,返回ref对象,并指定类型,作为实例
input = createRef()
render() {
return (
<>
refs
this.props.onChange(e.target.value)}
/>
>
);
}
componentDidMount(){
this.input.current!.focus()
}
}
在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
})
)
};
在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
}={
list:[]
}
render() {
let {list}=this.state
return (
<>
comp4-数据交互
{
list.map((item:IListItem)=>(
{item.title}/{item.time}
))
}
>
);
}
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(
,
// document.getElementById('root') as HTMLElement
document.getElementById('root')! //移除null和undefined
);
根组件.tsx
列表页.tsx
{item.title}/{item.time}
详情页.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;//可选映射
};
//params._id 不存在
//解决方案1
type TProps={
match: match<{_id?: string}>//交叉一个属性到RouteComponentProps类型
} & RouteComponentProps
//解决方案2
type ParamsInfo = {//作为 RouteComponentProps的泛型传入,定义params的内容
_id:string
}
type TProps2=RouteComponentProps;
export default class Detail extends React.Component{
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 ? (
detail
{data.title}/{data.detail?.auth}
) : (
骨架屏
)
}
>
)
}
}
函数式组件,接受路由上下文的类型约束
const Login: React.FC = ({ 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(
,
// 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 {
public render() {
const { count, onIncrement, onDecrement } = this.props;
return (
<>
redux+react-redux+react-thunk
{count}
>
)
}
}
// 将 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);
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(0);//手动指定类型和初始值
const [msg4, setMsg4] = useState- ({id:1});//手动指定类型
const [msg5, setMsg5] = useState
([]);//手动指定类型
// const box = useRef(null)//有时设置引用可能会在稍后的时间点发生
const box = useRef(null)//使用 useRef 时需要更加明确 被引用的类型
useEffect(() => {
console.log('didMount');
box.current!.style.background='red';
return () => {
console.log('unmount');
};
}, []);
return (
<>
hooks
msg:{msg}
msg2:{msg2}
msg3:{msg3+1}
{/* ? 代表对象存在,才去访问子key */}
msg4:{msg4?.id}/{msg4?.title}
msg5:
{
msg5?.map((val,index)=>(
{val}
))
}
box
>
)
}
export default Reg;
官网 项目
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 编译时配置
路由使用约定式,对齐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自动生成,有取当前模板结构
商品 002
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
{
return window.confirm(`确认要去向 ${location.pathname}?`);
}}
/>
自带mock,无需代理,其他数据需要代理,本地数据放在public
在出错时显示个 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 无需重启
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\//,
],
},
}],
],
}
官网
使用react开发,可扩展的企业级前端应用框架,让react开发高度可配置(合并式,无需触碰webpack底层),支持各种功能扩展和业务需求,可替换next完成服务端渲染
把大家常用的技术栈进行整理,收敛到一起,让大家只用 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~@
执向srcsrc/global.css
为全局样式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,但你仍可以在 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'
商品 002
商品 002
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 { props.children };
} else {
redirectTo('/login');
}
}
//同react
{
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' },
],
}
]
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的代理
}
}
// 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提供的插件(内置),基本上无需安装,无需配置,直接使用即可
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使用
import { Button,message } from 'antd';
<Button
type="primary"
onClick={()=>message.info('message')}
>Primary</Button>
网络请求库,基于 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 (
);
}
//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;
}
],
};
文档
一个强大的管理异步数据请求的 Hook.
核心特性
在组件初次加载时, 自动触发该函数执行。同时 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 failed to load
}
if (loading) {
return loading...
}
return (
{JSON.stringify(data)}
);
}
通过 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 (
并行请求:单击所有按钮,每个请求都有自己的状态
{users.map((user => (
-
)))}
);
};
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 (
防抖
run(e.target.value)}
onBlur={cancel}
style={{ width: 300 }}
loading={loading}
/>
{data && JSON.stringify(data)}
);
};
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 (
屏幕聚焦重新请求
{data && JSON.stringify(data)}
)
}
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 (
home: {data && JSON.stringify(data)}
);
};
运行时配置,跑在浏览器端, 如:在出错时显示个 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.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 目录
//配置 里的额外脚本,数组项为字符串或对象
headScripts: [
`alert(1);`,
`http://code.jquery.com/jquery-2.1.1.min.js`,
],
//配置 额外 link 和style
styles: [
`body { color: red; }`,
`https://a.com/b.css`,
],
});
参考
dva = React-Router + Redux + Redux-saga 项目
View:React 组件构成的视图层
Action:一个对象,描述事件
connect 方法:一个函数,绑定 State 到 View
dispatch 方法:一个函数,发送 Action 到 State
model 数据管理模块
antd: true, 开启antd
dva: true, 开启dva数据流
routes: {//路由
exclude: [//排除
/models\//,
/services\//,
/model\.(t|j)sx?$/,
/service\.(t|j)sx?$/,
/components\//,
],
},
export const dva = {
config: {
onError(err) {//监听错误
err.preventDefault();
console.error('dva config',err.message);
},
//初始数据 不给就取根models的state,给了就不取
initialState: {
namespace: {
key:value
},
},
},
plugins: [
// require('dva-logger')(),
],
}
// 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以插件的形式整合 dva 数据流,配置默认开启
文档
// 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)(组件));
文档
一对帮助你处理在拖拽中进行数据转移的 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 (
useDrop 可以单独使用来接收文件、文字和网址的释放
useDrag 允许一个 dom 节点被拖拽,需要配合 useDrop 使用
向节点内触发粘贴时也会被视为拖拽的内容
{isHovering ? '撒手' : '拖到这'}
box
);
};
参数 | 说明 | 类型 |
---|---|---|
getDragProps | 一个接收拖拽的值,并返回需要透传给被拖拽节点 props 的方法 | (content: any) => props |
参数 | 说明 | 类型 |
---|---|---|
props | 需要透传给接受拖拽区域 dom 节点的 props | - |
isHovering | 是否是拖拽中,且光标处于释放区域内 | boolean |
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
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 | - |
解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题
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 (
<>
{list.map((ele, index) => (
Row: {ele.data}
))}
>
);
};
const result:Result = useVirtualList(originalList: any[], Options);
参数 | 说明 | 类型 |
---|---|---|
list | 当前需要展示的列表内容 | {data: T, index: number}[] |
containerProps | 滚动容器的 props | {} |
wrapperProps | children 外层包裹器 props | {} |
scrollTo | 快速滚动到指定 index | (index: number) => void |
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
originalList | 包含大量数据的列表 | T[] | [] |
options | 可选配置项,见 Options | - | - |
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
itemHeight | 行高度,静态高度可以直接写入像素值,动态高度可传入函数 | number | ((index: number) => number) | - |
overscan | 视区上、下额外展示的 dom 节点数量 | number | 10 |
常见表单控件(通过 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 (<>
>
);
};
const [ { value, onChange }, reset ] = useEventTarget<T, U>(initialValue?: T, transformer?: (value: U) => T );
参数 | 说明 | 类型 |
---|---|---|
value | 表单控件的值 | T |
onChange | 表单控件值发生变化时候的回调 | (e: { target: { value: T }}) => void |
reset | 重置函数 | () => void |
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
initialValue? | 可选项, 初始值 | T | - |
transformer? | 可选项,可自定义回调值的转化 | (value: U) => T | - |
开发
部署
项目1
项目2
项目3
项目4
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),官网
即原生开发模式,开发出来的是原生程序,不同平台上,Android和iOS的开发方法不同,开发出来的是一个独立的APP,能发布应用商店,有如下优点和缺点。
优点:
缺点:
即移动端的网站,将页面部署在服务器上,然后用户使用各大浏览器访问,不是独立APP,无法安装和发布Web网站一般分两种,MPA(Multi-page Application)和SPA(Single-page Application)。而Web App一般泛指后面的SPA形式开发出的网站(因为可以模仿一些APP的特性),有如下优点和缺点。
优点:
缺点:
即混合开发,也就是半原生半Web的开发模式,有跨平台效果,实质最终发布的仍然是独立的原生APP(各种的平台有各种的SDK),这是一种 Native App 和 Web App 折中的方案,保留了 Native App 和 Web App 的优点。
优点:
缺点:
Facebook发起的开源的一套新的APP开发方案,Facebook在当初深入研究Hybrid开发后,觉得这种模式有先天的缺陷,所以果断放弃,转而自行研究,后来推出了自己的“React Native”方案,不同于H5,也不同于原生,更像是用JS写出原生应用,有如下优点和缺点
优点:
缺点:
使用expo-cli开发react应用程序需要两种工具:本地开发工具和用于打开应用程序的移动客户端,需要在计算机上安装Node.js(版本10或更新版本)
npm install -g expo-cli
从Play商店下载Android版或从App Store 下载iOS版
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
注意
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:
用法2:
用法3:
const styles = StyleSheet.create({
red: {
color: 'red',
},
});
React Native 中使用 flexbox 规则来指定某个组件的子元素的布局,flexDirection
的默认值是column
而不是row
,而flex
也只能指定一个数字值。布局图解
React Native 中,position 默认值为 relative
,即相对布局。
如下示例代码中,子组件会等比例的占满屏幕
return (
);
如下示例代码中,子组件会等比例的占满屏幕,设置高度并不影响所占的比例
return (
);
如下示例代码中,子组件在界面所占的比例受高度控制,最后一个子组件则自动占满剩余空间
return (
);
样式设置 position: ‘absolute’
正确设置父组件的样式方式
return (
);
如下示例代码中,只有第一个设置了高度的view会显示在界面上,而设置了flex的view不会显示
return (
);
如下示例代码中,父组件与子组件都不会显示
return (
);
如下示例代码中,分别设置了高度和flex的view均会显示在界面上
return (
);
如下示例代码中,子组件均能正常显示,且设置了flex的view会将剩余的屏幕占满显示,不会被绝对布局的同级子view阻挡
return (
如下示例代码中,除相对布局的子组件能正常显示外,绝对布局的子组件会被同级的子view覆盖
return (
);
另外,可以给子组件设置不同的flex值,如flex:2, flex: 0.5,则子组件在界面上所占的比例会随之变化。
注意 react 中的 onChange 对应的是 rn 中的 onChangeText
TouchableHighlight按下时变暗
TouchableOpacity降低按钮的透明度 可选
针对不同平台编写不同代码的需求
Platform
模块.组件规定视图表现,api实现编程式组件操作
推荐React Navigation 提供了简单易用的跨平台导航方案,在 iOS 和 Android 上都可以进行翻页式、tab 选项卡式和抽屉式的导航布局
安装:
yarn add react-navigation --save 其他都不用做