redux
react-redux
react-router

redux上手


npm install redux --D

redux中的store是管理数据的政委,具有全局唯一性,所有的数据都在这一个数据源里进行管理,但是本身reduxreact并没有直接的联系。只有复杂的项目才需要redux来管理数据,简单项目,state + props + content足矣。

redux特点:

  • 需要一个store来存储数据
  • store里的state是放置数据的地方
  • 通过dispatch一个action来提交对数据的修改
  • 请求提交到reducer函数里,根据传入的actionstate,返回新的state

react-redux


npm install react-redux -D

提供两个api

1、Provider 顶级组件,提供数据

2、connect高阶组件提供数据和方法

在调用组件的地方,增加如下代码

import {Provider} from 'react-redux';
import store from './store';

<Provider store={store}>
    <ReduxText></ReduxText>
</Provider>

3、ReduxText.js做如下修改

import React from 'react'
// import store from '../store.js'
import {connect} from 'react-redux'

const mapStateToProps = state => ({num: state})
const mapDispatchToProps = {
    add: () => ({type: 'add'}),
    minus: () => ({type: 'minus'}),
}

function ReduxText({num,add,minus}){
    return (
        <div>
            <p>{num}</p>
            <div>
                <button onClick={minus}>-</button>
                <button onClick={add}>+</button>
            </div>
        </div>
    )
}

export default connect(mapStateToProps,mapDispatchToProps)(ReduxText)

使用装饰器的写法

// 装饰器写法
@connect(mapStateToProps,mapDispatchToProps)

class ReduxText extends Component {
    render(){
        const {num,add,minus} = this.props;
        return (
            <div>
                <p>{num}</p>
                <div>
                    <button onClick={minus}>-</button>
                    <button onClick={add}>+</button>
                </div>
            </div>
        )
    }
}

export default ReduxText

异步


react默认只支持同步,实现异步任务,比如延迟,网络请求,需要中间件的支持,比如我们使用简单的redux-thunkredux-logger

npm install redux-thunk -D
npm install redux-logger -D
// store.js

import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'

const counterReducer = (state = 0, action) => {
    switch (action.type) {
        case 'add':
            return state + 1
        case 'minus':
            return state - 1
        default:
            return state
    }
}

const store = createStore(counterReducer, applyMiddleware(logger, thunk))

export default store;
// ReduxTest.js

import React, {Component} from 'react'
import {connect} from 'react-redux'

const mapStateToProps = state => ({num: state})
const mapDispatchToProps = {
    add: () => ({type: 'add'}),
    minus: () => ({type: 'minus'}),
    asyncAdd: () => dispatch => {
        // 做异步操作
        setTimeout(() => {
            dispatch({type: 'add'})
        },1500)
    }
}

// 装饰器写法
@connect(mapStateToProps,mapDispatchToProps)

class ReduxText extends Component {
    render(){
        const {num,add,minus,asyncAdd} = this.props;
        return (
            <div>
                <p>{num}</p>
                <div>
                    <button onClick={minus}>-</button>
                    <button onClick={add}>+</button>
                    <button onClick={asyncAdd}>asyncAdd</button>
                </div>
            </div>
        )
    }
}

export default ReduxText

image

由图可以看出,先执行了异步操作,数据回来了后,才执行增加的操作

如何代码拆分优化:

1、新建一个store文件统一管理

2、store中的文件如下

// index.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer } from './count.js'

const store = createStore(counterReducer, applyMiddleware(logger, thunk))

export default store;

// count.js
export const counterReducer = (state = 0, action) => {
    switch (action.type) {
        case 'add':
            return state + 1
        case 'minus':
            return state - 1
        default:
            return state
    }
}

// action creator
export const add = () => ({ type: 'add' });
export const minus = () => ({ type: 'minus' });
export const asyncAdd = () => dispatch => {
    // 做异步操作
    setTimeout(() => {
        dispatch({ type: 'add' })
    }, 1500)
}

3、组件中使用
import {add,minus,asyncAdd} from '../store/count.js'

如何模块化 (combineReducers)

// store index.js

// 引入
import { createStore, applyMiddleware, combineReducers } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer, } from './count.js'

const store = createStore(
    // 使用
    combineReducers({ counter: counterReducer }),
    applyMiddleware(logger, thunk)
)

export default store;

// 调用时也有部分修改,要使用命名空间的方式获取
const mapStateToProps = state => ({num: state.counter})

react-router


npm install react-router-dom -D

路由基本用法

// RouterSample.js

import React from 'react'
import {BrowserRouter,Link,Route} from 'react-router-dom'

function Home(){
    return (
        <div>
            Home
        </div>
    )
}

function About(){
    return (
        <div>
            About
        </div>
    )
}

export default function RouterSample(){
    return (
        <div>
            <BrowserRouter>
                <div>
                    {/* 导航链接 */}
                    <ul>
                        <li>
                            <Link to="/">首页</Link>
                        </li>
                        <li>
                            <Link to="/about">关于</Link>
                        </li>
                    </ul>
                    {/* 路由配置:路由即组件 */}
                    {/* 注意exact 路由匹配是包容性质 */}
                    <Route exact path="/" component={Home}></Route>
                    <Route path="/about" component={About}></Route>
                </div>
            </BrowserRouter>
        </div>
    )
}

路由传参(动态路由)

import React from 'react'
import {BrowserRouter,Link,Route} from 'react-router-dom'

function Home(){
    return (
        <div>
            <h3>课程列表</h3>
            <ul>
                <li>
                    <Link to="/detail/web">web架构师</Link>
                </li>
                <li>
                    <Link to="/detail/python">python架构师</Link>
                </li>
            </ul>
        </div>
    )
}

function About(){
    return (
        <div>
            About
        </div>
    )
}

// 传递进来路由器对象
function Detail(props){
    // 1、history: 导航指令 后退等
    // 2、match: 获取参数信息
    // 3、location: 当前url信息
    console.log(props)
    return (
        <div>
            {/* 动态参:params, 查询参:query*/}
            当前课程是:{props.match.params.course}
            <button onClick={props.history.goBack}>后退</button>
        </div>
    )
}

export default function RouterSample(){
    return (
        <div>
            <BrowserRouter>
                <div>
                    {/* 导航链接 */}
                    <ul>
                        <li>
                            <Link to="/">首页</Link>
                        </li>
                        <li>
                            <Link to="/about">关于</Link>
                        </li>
                    </ul>
                    {/* 路由配置:路由即组件 */}
                    {/* 注意exact 路由匹配是包容性质 */}
                    <Route exact path="/" component={Home}></Route>
                    <Route path="/detail/:course" component={Detail}></Route>
                    <Route path="/about" component={About}></Route>
                </div>
            </BrowserRouter>
        </div>
    )
}

404问题,路由匹配不到

import {BrowserRouter,Link,Route,Switch} from 'react-router-dom'

function NoMatch({location}){
    return (
        <div>
            404,{location.pathname}不存在
        </div>
    )
}

{/* 路由匹配是包容性质 */}
{/* Switch组件只会展示一个 */}
<Switch>
    <Route exact path="/" component={Home}></Route>
    <Route path="/detail/:course" component={Detail}></Route>
    <Route path="/about" component={About}></Route>
    {/* 404 没有path */}
    <Route component={NoMatch}></Route>
</Switch>

路由嵌套

// 当前用户信息
function About(params){
    return (
        <div>
            <h3>个人中心</h3>
            <div>
                <Link to="/about/me">个人信息</Link>
                <Link to="/about/order">订单查询</Link>
            </div>
            <Switch>
                <Route path="/about/me" component={() => (<div>Me</div>)}></Route>
                <Route path="/about/order" component={() => (<div>order</div>)}></Route>
            </Switch>
        </div>
    )
}

设置页面进来的默认页面

import {BrowserRouter,Link,Route,Switch,Redirect} from 'react-router-dom'

<Switch>
    <Route path="/about/me" component={() => (<div>Me</div>)}></Route>
    <Route path="/about/order" component={() => (<div>order</div>)}></Route>
    {/* 访问/about时会重定向到/about/me */}
    <Redirect path="/about/me"></Redirect>
</Switch>

路由的守卫

// store 新建 user.redux.js
const initial = {
    isLogin: false,
    loading: false
}
export const user = (state = initial, action) => {
    switch (action.type) {
        case 'requestLogin':
            return {
                isLogin: false,
                loading: true
            }
        case 'login':
            return {
                isLogin: true,
                loading: false
            }
        default:
            return state
    }
}

export const login = () => dispatch => {
    dispatch({ type: 'requestLogin' })
        // 做异步操作
    setTimeout(() => {
        dispatch({ type: 'login' })
    }, 2000)
}
// store index.js 注册下
import { user } from './user.redux'

const store = createStore(
    combineReducers({ counter: counterReducer, user })
)
// RouterSample.js

import React from 'react'
import {BrowserRouter,Link,Route,Switch,Redirect} from 'react-router-dom'
import {connect} from 'react-redux'
import {login} from '../store/user.redux'

function Home(params){
    return (
        <div>
            <h3>课程列表</h3>
            <ul>
                <li>
                    <Link to="/detail/web">web架构师</Link>
                </li>
                <li>
                    <Link to="/detail/python">python架构师</Link>
                </li>
            </ul>
        </div>
    )
}

// 当前用户信息
function About(params){
    return (
        <div>
            <h3>个人中心</h3>
            <div>
                <Link to="/about/me">个人信息</Link>
                <Link to="/about/order">订单查询</Link>
            </div>
            <Switch>
                <Route path="/about/me" component={() => (<div>Me</div>)}></Route>
                <Route path="/about/order" component={() => (<div>order</div>)}></Route>
                {/* 访问/about时会重定向到/about/me */}
                <Redirect to="/about/me"></Redirect>
            </Switch>
        </div>
    )
}

// 传递进来路由器对象
function Detail(props){
    // 1、history: 导航指令 后退等
    // 2、match: 获取参数信息
    // 3、location: 当前url信息
    console.log(props)
    return (
        <div>
            {/* 动态参:params, 查询参:query*/}
            当前课程是:{props.match.params.course}
            <button onClick={props.history.goBack}>后退</button>
        </div>
    )
}

function NoMatch({location}){
    return (
        <div>
            404,{location.pathname}不存在
        </div>
    )
}

// 路由守卫
// 希望用法:<PrivateRoute path="/xxx" component={} ... />
const PrivateRoute = connect( state => ({
    isLogin: state.user.isLogin
})
)(
    ({component: Comp, isLogin, ...rest}) => {
        // 做认证
        // render 根据条件动态渲染组件
        return (
            <Route {...rest} render={
                props => isLogin ? <Comp></Comp> : <Redirect to={{
                    pathname: '/login', 
                    state: {
                        redirect: props.location.pathname
                    }
                }}></Redirect>
            }></Route>
        )
    }
)


// 登录组件的设置
const Login = connect(
    state => ({
        isLogin: state.user.isLogin,
        loading: state.user.loading,
    }),{login}
)(
    function Login({location, isLogin, login, loading}){
        const redirect = location.state.redirect || '/';
        if(isLogin){
            return <Redirect to={redirect}></Redirect>
        }
    
        return (
            <div>
                <p>用户登录</p>
                <hr></hr>
                <button onClick={login} disabled={loading}>{loading ? '登录中' : '登录'}</button>
            </div>
        )
    }
)

export default function RouterSample(){
    return (
        <div>
            <BrowserRouter>
                <div>
                    {/* 导航链接 */}
                    <ul>
                        <li>
                            <Link to="/">首页</Link>
                        </li>
                        <li>
                            <Link to="/about">关于</Link>
                        </li>
                    </ul>
                    {/* 路由配置:路由即组件 */}
                    {/* 注意exact 路由匹配是包容性质 */}
                    {/* Switch组件只会展示一个 */}
                    <Switch>
                        <Route exact path="/" component={Home}></Route>
                        <Route path="/detail/:course" component={Detail}></Route>
                        <PrivateRoute path="/about" component={About}></PrivateRoute>
                        <Route path="/login" component={Login}></Route>
                        {/* 404 没有path */}
                        <Route component={NoMatch}></Route>
                    </Switch>
                </div>
            </BrowserRouter>
        </div>
    )
}

redux原理


import { compose } from "redux"

export function createStore(reducer, enhancer) {
    // reducer 更新状态的函数
    // enhancer强化器 又被称为高阶函数
    if (enhancer) {
        // 返回一个强化后的新函数
        return enhancer(createStore)(reducer)
    }

    let currentState = {} // 当前状态
    let currentListeners = [] // 当前监听器

    // 暴露3个接口
    function getState() {
        return currentState
    }

    // 状态变更后,要执行的回调函数放到currentListeners数组中
    function subscribe(listener) {
        currentListeners.push(listener)
    }

    // 执行真正的更新操作
    function dispatch(action) {
        // 根据action指令做更新,返回全新的状态
        currentState = reducer(currentState, action)
            // 通知所有的listener做更新
        currentListeners.forEach(v => v())
        return action
    }

    dispatch({ type: '@IMOOC/WONIU-REDUX' })
    return { getState, subscribe, dispatch }
}

export function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args)
        let dispatch = store.dispatch

        // 让中间件可以获取当前的状态和能够派发
        const midApi = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }

        // middlewareChain 称为中间件链
        const middlewareChain = middlewares.map(middleware => middleware(midApi))
            // 把一个数组按照一个顺序进行包裹复合,最终生成一个函数,把store.dispatch传进去进行强化返回
        dispatch = compose(...middlewareChain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    }
}

export function compose(...funcs) {
    if (funcs.length == 0) {
        return arg => arg
    }

    if (funcs.length == 1) {
        return funcs[0]
    }

    // 进行聚合操作 变成一层函数套另一层函数 从洋葱的心开始执行
    return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}

react-redux原理


import React,{Component} from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from 'redux'

export const connect = (mapStateToProps = state => state,mapDispatchToProps = {}) => 
(wrapComponent) => {
    return class ConnectComponent extends Component{
        // 上下文
        static contextType = {
            store: PropTypes.object
        }

        constructor(props,context){
            super(props,context)
            this.state = {
                props: {}
            }
        }

        componentDidMount(){
            const {store} = this.context
            // 订阅更新 如果有状态发生变化,执行更新函数
            store.subscribe(() => this.update())
            this.update()
        }

        update(){
            const {store} = this.context
            // 映射到组件的属性中
            const stateProps = mapStateToProps(store.getState())
            // 组合成一个新的对象
            const dispatchProps = bindActionCreators(mapDispatchToProps,store.dispatch)
            this.setState({
                props: {
                    ...this.state.props,
                    ...stateProps,
                    ...dispatchProps
                }
            })
        }

        render(){
            return <wrapComponent {...this.state.props}></wrapComponent>
        }
    }
}

redux-thunk原理


const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action == 'function') {
        return action(dispatch, getState)
    }
    // 如果是普通对象,进入洋葱的下一层
    return next(action)
}

export default thunk

redux-saga使用


redux-saga使(数据获取、浏览器缓存获取)易于管理、执行、测试和失败处理。redux-saga是基于generater实现的。
npm install redux-saga -D
  • 1、在store文件夹下创建sagas.js
// call 用来调用异步函数
// put 通过异步函数拿到结果,通过put去通知状态更新
// takeEvery 负责全局监听action,拦截action,然后去执行异步的事情
import { call, put, takeEvery } from 'redux-saga/effects'

// 模拟登陆
const UserService = {
    login(uname) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (uname === 'panjie') {
                    resolve({
                        id: 1,
                        name: 'panjie',
                        age: 20
                    })
                } else {
                    reject('用户名或密码错误')
                }
            }, 1000)
        })
    }
}

function* login(action) {
    try {
        yield put({ type: 'requestLogin' })
        const result = yield call(UserService.login, action.uname)
        yield put({ type: 'loginSuccess', result })
    } catch (error) {
        yield put({ type: 'loginFailure', error })
    }
}

// 作为redux的中间件,提前拦截action,不直接走reducer,而是走我们自己的逻辑先去处理
// 当事情处理完了再继续往下执行
function* mySaga() {
    yield takeEvery('login', login)
}

export default mySaga
  • 2、在store文件夹中的index.js中注册上述步骤中导出的mySaga
import { createStore, applyMiddleware, combineReducers } from 'redux'
import logger from 'redux-logger'
import { user } from './user.redux'
import createSagaMiddleware from 'redux-saga'
import mySaga from './sagas'

// 1、创建saga中间件并注册
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
    combineReducers({ user }),
    applyMiddleware(logger, sagaMiddleware)
)

// 2、中间件运行saga
sagaMiddleware.run(mySaga)

export default store
  • 3、修改store文件中的user-redux.js
const initial = {
    isLogin: false,
    loading: false,
    error: ''
}
export const user = (state = initial, action) => {
    switch (action.type) {
        case 'requestLogin':
            return {
                isLogin: false,
                loading: true,
                error: ''
            }
        case 'loginSuccess':
            return {
                isLogin: true,
                loading: false,
                error: ''
            }
        case 'loginFailure':
            return {
                isLogin: false,
                loading: false,
                error: action.error
            }
        default:
            return state
    }
}

export function login(uname) {
    return {
        type: 'login',
        uname
    }
}

// export const login = () => dispatch => {
//     dispatch({ type: 'requestLogin' })
//         // 做异步操作
//     setTimeout(() => {
//         dispatch({ type: 'login' })
//     }, 2000)
// }
  • 4、完善页面
// RouterSample.js

// 登录组件的设置
const Login = connect(
    state => ({
        isLogin: state.user.isLogin,
        loading: state.user.loading,
        error: state.user.error  // 登录错误信息
    }),{login}
)(
    function Login({location, isLogin, login, loading, error}){  // 登录错误信息
        const redirect = location.state.redirect || '/';
        const [uname, setUname] = useState('')  // 用户名输入状态
        if(isLogin){
            return <Redirect to={redirect}></Redirect>
        }
    
        return (
            <div>
                <p>用户登录</p>
                <hr></hr>
                {/* 登录错误信息展示 */}
                {error && <p>{error}</p>}
                {/* 输入用户姓名 */}
                <input type="text" onChange={e => setUname(e.target.value)} value={uname}></input>
                <button onClick={() => login(uname)} disabled={loading}>{loading ? '登录中' : '登录'}</button>
            </div>
        )
    }
)

企业级框架


umi
redux解决方案dva

umi


  • 开箱即用,内置reatc,react-router
  • next.js且功能完备的路由约定,同时支持配置的路由方式
  • 完善的插件系统,覆盖从源码到构建产物的每个生命周期
  • 高性能,通过插件支持PWA,以路由为单元的code splitting
  • 支持静态页面导出,适配各种环境,比如中台业务,无线业务,egg,支付宝钱包,云凤蝶等
  • 开发启动快,支持一键开启dllhard-source-webpack-plugin
  • 一键兼容到IE9,基于umi-plugin-polyfills
  • 完善的TypeScript支持,包括d.ts定义和umi test
  • dva数据流的深入融合,支持duck directory、model的自动加载等等

dva+umi的约定

1、src源码

  • pages页面
  • components组件
  • layout布局
  • model

2、config配置
3、mock数据模拟
4、test测试等

项目骨架

// 创建一个文件 umi-test
npm init
yarn global add umi

新建页面

umi g page index
umi g page users

起服务

umi dev

动态路由

$开头的文件或目录

// 创建users/$id.js

import React from 'react';

export default function (props) {
  return (
    <div>
      {props.match.params.id}
    </div>
  );
}

嵌套路由

umi g page ./users/_layout

import styles from './_layout.css';

export default function(props) {
  return (
    <div className={styles.normal}>
      <h1>Page _layout</h1>
      <div>{props.children}</div>
    </div>
  );
}

页面跳转

// users/index.js
import styles from './index.css';
import Link from 'umi/link';
// router是个实例,全局只有一个
import router from 'umi/router'

export default function() {
  // 模拟数据
  const users = [
    {
      id: 1,
      name: 'tom'
    },
    {
      id: 2,
      name: 'jerry'
    }
  ]
  return (
    <div className={styles.normal}>
      <h1>Page users</h1>
      <ul>
        {users.map(u => (
          // 声明式
//           <li key={u.id}>
// <Link to={`/users/${u.id}`}>{u.name}</Link>
//           </li>
        // 命令式
          <li key={u.id} onClick={() => router.push(`/users/${u.id}`)}>
            {u.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

配置式路由

默认路由为声明式,根据pages下面内容自动生成路由,业务复杂后仍需配置路由

// 根目录下新建config/config.js
export default {
    routes: [{
            path: '/about',
            component: './about',
        },
        {
            path: '/',
            component: './index',
        },
        {
            path: '/users',
            component: './users/_layout',
            routes: [{
                    path: '/users/',
                    component: './users/index',
                },
                {
                    path: '/users/:id',
                    component: './users/$id',
                },
            ],
        },
    ],
};

404页面

umi g page NotFound
// config.js中添加不带path的路由配置
{
    component: './NotFound'
}

权限路由

  • 通过配置路由的 Routes 属性来实现
{
    path: '/about',
    component: './about',
    // 这里相对根目录,文件名后缀不能少
    Routes: ['./routes/PrivateRoute.js']
}
  • 创建./routes/PrivateRoute.js
import Redirect from 'umi/redirect'

export default props => {
    // 50%概率需要去登录页面
    if(Math.random() > 0.5){
        return <Redirect to='/login'></Redirect>
    }

    return (
        <div>
            <div>PrivateRoute (routes/PrivateRoute.js)</div>
            {props.children}
        </div>
    )
}
  • 创建登录页面
umi g page login

引入antd

npm install antd -D
npm isntall umi-plugin-react -D
修改config/config.js
plugins: [
    ['umi-plugin-react', {
        antd: true
    }],
],
使用
import {Button} from 'antd'
export default () => {
    return <div> <Button>登录</Button></div>
}

引入dva


配置dva

export default {
  plugins: [
    ['umi-plugin-react', {
      antd: true,
      dva: true,
}], ],
// ...
}

创建model: 维护页面数据状态

src下新建models/goods.js
export default {
    namespace: 'goods', // model的命名空间,区分多个model
    state: [{ title: "web全栈" },{ title: "java架构师" }], // 初始状态 effects:{}, // 异步操作
    reducers: { // 更新状态 }
}

使用状态

创建页面goods.js
umi g page goods
// pages/goods.js

import styles from './goods.css';
import { connect } from 'dva'
import { Card, Button } from 'antd'
import { useEffect } from 'react'

export default connect(state => ({
  loading: state.loading,  // dva可以通过loading空间获取加载状态
  goodsList: state.goods  // 获取指定命名空间的模型状态
}),{
  addGood: title => ({
    type: "goods/addGood",  // action的type需要以命名空间为前缀+reducer名称
    title
  }),
  getList: () => ({
    type: "goods/getList"
  })
})(function({goodsList,addGood,getList,loading}) {

  useEffect(() => {
    getList();
  },[])

  console.log(loading)

  return (
    <div className={styles.normal}>
      <h1>Page goods</h1>
      {/* 商品列表 */}
      <div>
        {/* 加载状态 */}
        {loading.models.goods && <p>loading...</p> }
        {goodsList.map(good => {
          return (
            <Card key={good.title}>
              <div>{good.title}</div>
            </Card>
          )
        })}
        <div>
          <Button onClick={() => addGood('商品' + new Date().getTime())}>添加商品</Button>
        </div>
      </div>
    </div>
  );
})
更新模型
// src/models/goods.js

import axios from 'axios'

// api 项目中写到service层
function getGoods() {
    return axios.get('/api/goods')
}

export default {
    namespace: 'goods', // model的命名空间,区分多个model
    state: [], // 初始状态 
    effects: {
        // 异步操作
        * getList(action, { call, put }) {
            const res = yield call(getGoods)
            yield put({
                type: 'initGoods',
                payload: res.data.result
            })
        }
    },
    reducers: { // 同步操作
        // 更新状态 
        addGood(state, action) {
            return [...state, { title: action.title }]
        },
        initGoods(state, action) {
            return action.payload
        }
    }
}
数据mock: 模拟数据接口
// 根目录 新建mock/goods.js

const data = [{ title: "web全栈" }, { title: "java架构师" }];

export default {
    'get /api/goods': function(req, res) {
        setTimeout(() => {
            res.json({ result: data })
        }, 1250)
    },
}

洁本佳人
86 声望3 粉丝

test