• DvaJS构建配置React项目与使用


    DvaJS构建配置React项目与使用

    一,介绍与需求分析

     1.1,介绍

    dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以dva是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。是由阿里架构师 sorrycc 带领 team 完成的一套前端框架。

     1.2,需求

    快速搭建基于react的项目(PC端,移动端)。

    二,DvaJS构建项目

    2.1,初始化项目

    第一步:安装node

    第二步:安装最新版本dva-cli

    1 $ npm install dva-cli -g
    2 $ dva -v

    第三步:dva new 创建新应用

    1 $ dva new myapp

    也可以在创建项目目录myapp后,用dva init初始化项目

    1 $ dva init

    第四步:运行项目

    1 $ cd myapp
    2 $ npm start

    浏览器会自动打开一个窗口

    2.2,项目架构介绍

    |-mock             //存放用于 mock 数据的文件 
    |-node_modules             //项目包
    |-public             //一般用于存放静态文件,打包时会被直接复制到输出目录(./dist)
    |-src               //项目源代码
      |  |-asserts         //用于存放静态资源,打包时会经过 webpack 处理
      |  |-caches         //缓存
      |  |-components     //组件 存放 React 组件,一般是该项目公用的无状态组件
      |  |-entries        //入口
      |  |-models         //数据模型 存放模型文件
      |  |-pages          //页面视图
      |  |-routes         //路由 存放需要 connect model 的路由组件
      |  |-services       //服务 存放服务文件,一般是网络请求等
      |  |-test           //测试
      |  |-utils          //辅助工具 工具类库
    |-package.json      //包管理代码
    |-webpackrc.js   //开发配置
    |-tsconfig.json     /// ts配置
    |-webpack.config.js //webpack配置 
    |-.gitignore //Git忽略文件
    在dva项目目录中主要分3层,models,services,components,其中models是最重要概念,这里放的是各种数据,与数据交互的应该都是在这里。services是请求后台接口的方法。components是组件了。

    三,DvaJS的使用

    3.1,DvaJS的五个Api

     1 import dva from 'dva';
     2 import {message} from 'antd';
     3 import './index.css';
     4 
     5 // 1. Initialize 创建 dva 应用实例
     6 const app = dva();
     7 
     8 // 2. Plugins 装载插件(可选)
     9 app.use({
    10   onError: function (error, action) {
    11     message.error(error.message || '失败', 5);
    12   }
    13 });
    14 
    15 // 3. Model 注册model
    16  app.model(require('../models/example').default);
    17 
    18 // 4. Router 配置路由
    19 app.router(require('../routes/router').default);
    20 
    21 // 5. Start 启动应用
    22 app.start('#root');
    23 
    24 export default app._store; // eslint-disable-line 抛出

    1,app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)​

    opts可以配置所有的hooks 

    1 const app = dva({
    2      history,
    3      initialState,
    4      onError,
    5      onHmr,
    6 });

    这里比较常用的是,history的配置,一般默认的是hashHistory,如果要配置 history 为 browserHistory,可以这样:

    1 import dva from 'dva';
    2 import createHistory from 'history/createBrowserHistory';
    3 const app = dva({
    4   history: createHistory(),
    5 });
      • initialState:指定初始数据,优先级高于 model 中的 state,默认是 {},但是基本上都在modal里面设置相应的state。

    2,app.use(Hooks):配置 hooks 或者注册插件。

    1 app.use({
    2   onError: function (error, action) {
    3     message.error(error.message || '失败', 5);
    4   }
    5 });

    可以根据自己的需要来选择注册相应的插件

    3,app.model(ModelObject):这里是数据逻辑处理,数据流动的地方。

     1 export default {
     2 
     3   namespace: 'example',//model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace
     4 
     5   state: {},//表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值)
     6 
     7   subscriptions: {//语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action
     8     setup({ dispatch, history }) {  // eslint-disable-line
     9     },
    10   },
    11 
    12   effects: {//Effect 被称为副作用,最常见的就是异步操作
    13     *fetch({ payload }, { call, put }) {  // eslint-disable-line
    14       yield put({ type: 'save' });
    15     },
    16   },
    17 
    18   reducers: {//reducers 聚合积累的结果是当前 model 的 state 对象
    19     save(state, action) {
    20       return { ...state, ...action.payload };
    21     },
    22   },
    23 
    24 };

    4,app.router(Function):注册路由表,我们做路由跳转的地方

     1 import React from 'react';
     2 import { routerRedux, Route ,Switch} from 'dva/router';
     3 import { LocaleProvider } from 'antd';
     4 import App from '../components/App/App';
     5 import Flex from '../components/Header/index';
     6 import Login from '../pages/Login/Login';
     7 import Home from '../pages/Home/Home';
     8 import zhCN from 'antd/lib/locale-provider/zh_CN';
     9 const {ConnectedRouter} = routerRedux;
    10 
    11 function RouterConfig({history}) {
    12   return (
    13     <ConnectedRouter history={history}>
    14       <Switch>
    15         <Route path="/login"  component={Login} />
    16         <LocaleProvider locale={zhCN}>
    17         <App>
    18           <Flex>
    19             <Switch>
    20             <Route path="/"  exact component={Home} />
    21             </Switch>
    22           </Flex>
    23         </App>
    24         </LocaleProvider>
    25       </Switch>
    26     </ConnectedRouter>
    27   );
    28 }
    29 
    30 export default RouterConfig;

    5,app.start([HTMLElement], opts)

    启动我们自己的应用

    3.2,DvaJS的十个概念

    1,Model

    model 是 dva 中最重要的概念,Model 非 MVC 中的 M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发

     1 import Model from 'dva-model';
     2 // import effect from 'dva-model/effect';
     3 import queryString from 'query-string';
     4 import pathToRegexp from 'path-to-regexp';
     5 import {ManagementPage as namespace} from '../../utils/namespace';
     6 import {
     7   getPages,
     8 } from '../../services/page';
     9 
    10 export default Model({
    11   namespace,
    12   subscriptions: {
    13     setup({dispatch, history}) {  // eslint-disable-line
    14       history.listen(location => {
    15         const {pathname, search} = location;
    16         const query = queryString.parse(search);
    17         const match = pathToRegexp(namespace + '/:action').exec(pathname);
    18         if (match) {
    19            dispatch({
    20              type:'getPages',
    21             payload:{
    22                s:query.s || 10,
    23                p:query.p || 1,
    24                j_code:parseInt(query.j,10) || 1,
    25              }
    26            });
    27         }
    28 
    29       })
    30     }
    31   },
    32   reducers: {
    33     getPagesSuccess(state, action) {
    34       const {list, total} = action.result;
    35       return {...state, list, loading: false, total};
    36     },
    37   }
    38 }, {
    39   getPages,
    40 })

    2,namespace

    model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace

    3,State(状态)

    初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中 modal 中的优先级低于传给 dva() 的 opts.initialState

     1 // dva()初始化
     2 const app = dva({
     3   initialState: { count: 1 },
     4 });
     5 
     6 // modal()定义事件
     7 app.model({
     8   namespace: 'count',
     9   state: 0,
    10 });
    Model中state的优先级比初始化的低,但是基本上项目中的 state 都是在这里定义的

    4,Subscription

    Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等

     1 subscriptions: { //触发器。setup表示初始化即调用。
     2     setup({dispatch, history}) {
     3       history.listen(location => {//listen监听路由变化 调用不同的方法
     4         if (location.pathname === '/login') {
     5          //清除缓存
     6         } else {
     7           dispatch({
     8             type: 'fetch'
     9           });
    10         }
    11       });
    12     },
    13   },

    5,Effects

    用于处理异步操作和业务逻辑,不直接修改 state,简单的来说,就是获取从服务端获取数据,并且发起一个 action 交给 reducer 的地方。其中它用到了redux-saga里面有几个常用的函数。

    1. put  用来发起一条action
    2. call 以异步的方式调用函数
    3. select 从state中获取相关的数据
    4. take 获取发送的数据
     1 effects: {
     2     *login(action, saga){
     3       const data = yield saga.call(effect(login, 'loginSuccess', authCache), action, saga);//call 用户调用异步逻辑 支持Promise
     4       if (data && data.token) {
     5         yield saga.put(routerRedux.replace('/home'));//put 用于触发action 什么是action下面会讲到
     6       }
     7     },
     8     *logout(action, saga){
     9       const state = yield saga.select(state => state);//select 从state里获取数据
    10     },
    11  
    12   },
     1 reducers: {
     2     add1(state) {
     3       const newCurrent = state.current + 1;
     4       return { ...state,
     5         record: newCurrent > state.record ? newCurrent : state.record,
     6         current: newCurrent,
     7       };
     8     },
     9     minus(state) {
    10       return { ...state, current: state.current - 1};
    11     },
    12   },
    13   effects: {
    14     *add(action, { call, put }) {
    15       yield put({ type: 'add1' });
    16       yield call(delayDeal, 1000);
    17       yield put({ type: 'minus' });
    18     },
    19   },

    如果effectreducers中的add方法重合了,这里会陷入一个死循环,因为当组件发送一个dispatch的时候,model会首先去找effect里面的方法,当又找到add的时候,就又会去请求effect里面的方法。

    这里的 delayDeal,是我这边写的一个延时的函数,我们在 utils 里面编写一个 utils.js

     1 /**
     2  *超时函数处理
     3  * @param timeout  :timeout超时的时间参数
     4  * @returns {*} :返回样式值
     5  */
     6 export function delayDeal(timeout) {
     7   return new Promise((resolve) => {
     8     setTimeout(resolve, timeout);
     9   });
    10 }

    接着我们在 models/example.js 导入这个 utils.js

    1 import { delayDeal} from '../utils/utils';

    6,Reducer

    key/value 格式定义 reducer,用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。其实一个纯函数。

    1  reducers: {
    2     loginSuccess(state, action){
    3       return {...state, auth: action.result, loading: false};
    4     },
    5   }

    7,Router

    Router 表示路由配置信息,项目中的 router.js

    8,RouteComponent

    RouteComponent 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据

    9,Action:表示操作事件,可以是同步,也可以是异步

    action 的格式如下,它需要有一个 type ,表示这个 action 要触发什么操作;payload则表示这个 action 将要传递的数据

    1 {
    2      type: namespace + '/login',
    3      payload: {
    4           userName: payload.userName,
    5           password: payload.password
    6         }
    7  }

    构建一个Action 创建函数,如下:

     1 function goLogin(payload) {
     2 let loginInfo = {
     3             type: namespace + '/login',
     4             payload: {
     5               userName: payload.userName,
     6               password: payload.password
     7             }
     8           }
     9   return loginInfo 
    10 }
    11 
    12 //我们直接dispatch(goLogin()),就发送了一个action。
    13 dispatch(goLogin())

    10,dispatch 

    type dispatch = (a: Action) => Action

    dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。

    在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:

    1 dispatch({
    2    type: namespace + '/login', // 如果在 model 外调用,需要添加 namespace,如果在model内调用 无需添加 namespace
    3   payload: {}, // 需要传递的信息
    4 });
    1. reducers 处理数据
    2. effects   接收数据
    3. subscriptions 监听数据

    3.3,使用antd

    先安装 antd 和 babel-plugin-import

    1 npm install antd babel-plugin-import --save
    2 # 或
    3 yarn add antd babel-plugin-import

    babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:

    1 {
    2   "extraBabelPlugins": [
    3     ["import", {
    4       "libraryName": "antd",
    5       "libraryDirectory": "es",
    6       "style": true
    7     }]
    8   ]
    9 }

    现在就可以按需引入 antd 的组件了,如 import { Button } from 'antd',Button 组件的样式文件也会自动帮你引入。

    3.4,配置.webpackrc

    1,entry是入口文件配置

    单页类型:

    1 entry: './src/entries/index.js',

    多页类型:

    1 "entry": "src/entries/*.js"

    2,extraBabelPlugins 定义额外的 babel plugin 列表,格式为数组。

    3,env针对特定的环境进行配置。dev 的环境变量是?development,build 的环境变量是?production

     1 "extraBabelPlugins": ["transform-runtime"],
     2 "env": {
     3   development: {
     4       extraBabelPlugins: ['dva-hmr'],
     5     },
     6     production: {
     7       define: {
     8         __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }
     9     }
    10 }

    开发环境下的 extraBabelPlugins 是?["transform-runtime", "dva-hmr"],而生产环境下是?["transform-runtime"]

    4,配置 webpack 的?externals?属性

    1 // 配置 @antv/data-set和 rollbar 不打入代码
    2 "externals": {
    3     '@antv/data-set': 'DataSet',
    4     rollbar: 'rollbar',
    5 }

    5,配置 webpack-dev-server 的 proxy 属性。 如果要代理请求到其他服务器,可以这样配:

    1   proxy: {
    2     "/api": {
    3       // "target": "http://127.0.0.1/",
    4       // "target": "http://127.0.0.1:9090/",
    5       "target": "http://localhost:8080/",
    6       "changeOrigin": true,
    7      "pathRewrite": { "^/api" : "" }
    8     }
    9   },

    6,disableDynamicImport

    禁用 import() 按需加载,全部打包在一个文件里,通过 babel-plugin-dynamic-import-node-sync 实现。

    7,publicPath

    配置 webpack 的 output.publicPath 属性。

    8,extraBabelIncludes

    定义额外需要做 babel 转换的文件匹配列表,格式为数组

    9,outputPath

    配置 webpack 的 output.path 属性。

    打包输出的文件

    1 config["outputPath"] = path.join(process.cwd(), './build/')

    10,根据需求完整配置如下:

    文件名称是:.webpackrc.js,可根据实际情况添加如下代码:

     1 const path = require('path');
     2 
     3 const config = {
     4   entry: './src/entries/index.js',
     5   extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]],
     6   env: {
     7     development: {
     8       extraBabelPlugins: ['dva-hmr'],
     9     },
    10     production: {
    11       define: {
    12         __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }
    13     }
    14   },
    15   externals: {
    16     '@antv/data-set': 'DataSet',
    17     rollbar: 'rollbar',
    18   },
    19   lessLoaderOptions: {
    20     javascriptEnabled: true,
    21   },
    22   proxy: {
    23     "/api": {
    24       // "target": "http://127.0.0.1/",
    25       // "target": "http://127.0.0.1:9090/",
    26       "target": "http://localhost:8080/",
    27       "changeOrigin": true,
    28     }
    29   },
    30   es5ImcompatibleVersions:true,
    31   disableDynamicImport: true,
    32   publicPath: '/',
    33   hash: false,
    34   extraBabelIncludes:[
    35     "node_modules"
    36   ]
    37 };
    38 if (module.exports.env !== 'development') {
    39   config["outputPath"] = path.join(process.cwd(), './build/')
    40 }
    41 export default config

    更多 .webpackrc 的配置请参考 roadhog 配置

    3.5,使用antd-mobile

    先安装 antd-mobile 和 babel-plugin-import

    1 npm install antd-mobile babel-plugin-import --save #
    2 yarn add antd-mobile babel-plugin-import

    babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:

    1 {
    2   "plugins": [
    3     ["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 会加载 less 文件
    4   ]
    5 }

    现在就可以按需引入antd-mobile 的组件了,如 import { DatePicker} from 'antd-mobile',DatePicker 组件的样式文件也会自动帮你引入。

    四,整体架构

    1. 我们根据 url 访问相关的 Route-Component,在组件中我们通过 dispatch 发送 actionmodel 里面的 effect 或者直接 Reducer
    2. 当我们将action发送给Effect,基本上是取服务器上面请求数据的,服务器返回数据之后,effect 会发送相应的 actionreducer,由唯一能改变 statereducer 改变 state ,然后通过connect重新渲染组件。
    3. 当我们将action发送给reducer,那直接由 reducer 改变 state,然后通过 connect 重新渲染组件。如下图所示:

    数据流向

    数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State

    重置models里的数据:

    1 dispatch({type:namespace+'/set',payload:{mdata:[]}});
       set是内置的方法

    Dva官方文档              nginx代理部署Vue与React项目

    五,问题记录

    5.1,路由相关的问题

    1,使用match后的路由跳转问题,版本routerV4

    match是一个匹配路径参数的对象,它有一个属性params,里面的内容就是路径参数,除常用的params属性外,它还有url、path、isExact属性。

    问题描述:不能跳转新页面或匹配跳转后,刷新时url所传的值会被重置掉

    不能跳转的情况

     1 const {ConnectedRouter} = routerRedux;
     2 
     3 function RouterConfig({history}) {
     4 const tests =({match}) =>(
     5     <div>
     6       <Route exact path={`${match.url}/:tab`} component={Test}/>
     7       <Route exact path={match.url} component={Test}/>
     8     </div>
     9 
    10   );
    11   return (
    12     <ConnectedRouter history={history}>
    13       <Switch>
    14         <Route path="/login" component={Login}/>
    15         <LocaleProvider locale={zhCN}>
    16           <App>
    17             <Flex>
    18               <Switch>
    19                 <Route path="/test" component={tests }/>
    20                <Route exact path="/test/bindTest" component={BindTest}/>
    21             
    22               </Switch>
    23             </Flex>
    24           </App>
    25         </LocaleProvider>
    26       </Switch>
    27     </ConnectedRouter>
    28   );
    29 }

    路由如上写法,使用下面方式不能跳转,但是地址栏路径变了

     1 import { routerRedux} from 'dva/router';
     2 ...
     3  
     4 this.props.dispatch(routerRedux.push({
     5       pathname: '/test/bindTest',
     6       search:queryString.stringify({
     7         // ...query,
     8         Code: code,
     9         Name: name
    10       })
    11     }));
    12 
    13 ...

    能跳转,但是刷新所传的参数被重置

     1 const {ConnectedRouter} = routerRedux;
     2 
     3 function RouterConfig({history}) {
     4 const tests =({match}) =>(
     5     <div>
     6       <Route exact path={`${match.url}/bindTest`} component={BindTest}/>
     7       <Route exact path={`${match.url}/:tab`} component={Test}/>
     8       <Route exact path={match.url} component={Test}/>
     9     </div>
    10 
    11   );
    12   return (
    13     <ConnectedRouter history={history}>
    14       <Switch>
    15         <Route path="/login" component={Login}/>
    16         <LocaleProvider locale={zhCN}>
    17           <App>
    18             <Flex>
    19               <Switch>
    20                 <Route path="/test" component={tests }/>
    21               </Switch>
    22             </Flex>
    23           </App>
    24         </LocaleProvider>
    25       </Switch>
    26     </ConnectedRouter>
    27   );
    28 }

    路由如上写法,使用下面方式可以跳转,但是刷新时所传的参数会被test里所传的参数重置

     1 ...
     2 
     3 this.props.dispatch(routerRedux.push({
     4         pathname: '/test/bindTest',
     5         search:queryString.stringify({
     6           // ...query,
     7           Code: code,
     8           Name: name
     9        })
    10 }));
    11 
    12 ...

     解决办法如下:地址多加一级,跳出以前的界面

    路由配置

     1 const {ConnectedRouter} = routerRedux;
     2 
     3 function RouterConfig({history}) {
     4 const tests =({match}) =>(
     5     <div>
     6       <Route exact path={`${match.url}/bind/test`} component={BindTest}/>
     7       <Route exact path={`${match.url}/:tab`} component={Test}/>
     8       <Route exact path={match.url} component={Test}/>
     9     </div>
    10 
    11   );
    12   return (
    13     <ConnectedRouter history={history}>
    14               <Switch>
    15                 <Route path="/test" component={tests }/>
    16               </Switch>
    17     </ConnectedRouter>
    18   );
    19 }

    调用

     1 ...
     2  
     3 this.props.dispatch(routerRedux.push({
     4       pathname: '/test/bind/test1',
     5       search:queryString.stringify({
     6         // ...query,
     7         Code: code,
     8         Name: name
     9       })
    10     }));
    11 
    12 ...

     5.2,箭头函数this指向问题

    箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。

  • 相关阅读:
    OEA框架学习:缓存
    2012年 博文整理
    技术支持经验总结
    OEA框架学习:元数据设计
    安装后新建Android出现“AndroidManifest.xml 系统找不到指定的文件”解决方案
    Android控件学习笔记之 GridView(实现九宫格)
    获取url地址中主机的域名
    C# 语音读取字符串
    JSON省市联动
    MOTO Droid手机自定义本地铃声设置方法
  • 原文地址:https://www.cnblogs.com/jackson-yqj/p/10108648.html
Copyright © 2020-2023  润新知