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
$ npm install dva-cli -g $ dva -v
第三步:
dva new创建新应用
$ dva new myapp
也可以在创建项目目录myapp后,用
dva init初始化项目
$ dva init
第四步:运行项目
$ cd myapp $ 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
import dva from 'dva'; import {message} from 'antd'; import './index.css'; // 1. Initialize 创建 dva 应用实例 const app = dva(); // 2. Plugins 装载插件(可选) app.use({ onError: function (error, action) { message.error(error.message || '失败', 5); } }); // 3. Model 注册model app.model(require('../models/example').default); // 4. Router 配置路由 app.router(require('../routes/router').default); // 5. Start 启动应用 app.start('#root'); export default app._store; // eslint-disable-line 抛出
1,app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)
在
opts可以配置所有的
hooks
const app = dva({ history, initialState, onError, onHmr, });
这里比较常用的是,history的配置,一般默认的是
hashHistory,如果要配置 history 为
browserHistory,可以这样:
import dva from 'dva'; import createHistory from 'history/createBrowserHistory'; const app = dva({ history: createHistory(), });
-
[li]
initialState
:指定初始数据,优先级高于 model 中的 state,默认是{}
,但是基本上都在modal里面设置相应的state。
2,app.use(Hooks):配置 hooks 或者注册插件。
app.use({ onError: function (error, action) { message.error(error.message || '失败', 5); } });
可以根据自己的需要来选择注册相应的插件
3,app.model(ModelObject):这里是数据逻辑处理,数据流动的地方。
export default { namespace: 'example',//[code]model的命名空间,同时也是他在全局
state上的属性,只能用字符串,我们发送在发送
action到相应的
reducer时,就会需要用到
namespacestate: {},//表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值) subscriptions: {//语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action setup({ dispatch, history }) { // eslint-disable-line }, }, effects: {//Effect 被称为副作用,最常见的就是异步操作 *fetch({ payload }, { call, put }) { // eslint-disable-line yield put({ type: 'save' }); }, }, reducers: {//reducers 聚合积累的结果是当前 model 的 state 对象 save(state, action) { return { ...state, ...action.payload }; }, }, };[/code]
4,app.router(Function):注册路由表,我们做路由跳转的地方
import React from 'react'; import { routerRedux, Route ,Switch} from 'dva/router'; import { LocaleProvider } from 'antd'; import App from '../components/App/App'; import Flex from '../components/Header/index'; import Login from '../pages/Login/Login'; import Home from '../pages/Home/Home'; import zhCN from 'antd/lib/locale-provider/zh_CN'; const {ConnectedRouter} = routerRedux; function RouterConfig({history}) { return ( <ConnectedRouter history={history}> <Switch> <Route path="/login" component={Login} /> <LocaleProvider locale={zhCN}> <App> <Flex> <Switch> <Route path="/" exact component={Home} /> </Switch> </Flex> </App> </LocaleProvider> </Switch> </ConnectedRouter> ); } export default RouterConfig;
5,app.start([HTMLElement], opts)
启动我们自己的应用
3.2,DvaJS的十个概念
1,Model
model是
dva中最重要的概念,
Model非
MVC中的
M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发
import Model from 'dva-model'; // import effect from 'dva-model/effect'; import queryString from 'query-string'; import pathToRegexp from 'path-to-regexp'; import {ManagementPage as namespace} from '../../utils/namespace'; import { getPages, } from '../../services/page'; export default Model({ namespace, subscriptions: { setup({dispatch, history}) { // eslint-disable-line history.listen(location => { const {pathname, search} = location; const query = queryString.parse(search); const match = pathToRegexp(namespace + '/:action').exec(pathname); if (match) { dispatch({ type:'getPages', payload:{ s:query.s || 10, p:query.p || 1, j_code:parseInt(query.j,10) || 1, } }); } }) } }, reducers: { getPagesSuccess(state, action) { const {list, total} = action.result; return {...state, list, loading: false, total}; }, } }, { getPages, })
2,namespace
model的命名空间,同时也是他在全局
state上的属性,只能用字符串,我们发送在发送
action到相应的
reducer时,就会需要用到
namespace
3,State(状态)
初始值,我们在
dva()初始化的时候和在
modal里面的
state对其两处进行定义,其中
modal中的优先级低于传给
dva()的
opts.initialState
// dva()初始化 const app = dva({ initialState: { count: 1 }, }); // modal()定义事件 app.model({ namespace: 'count', state: 0, });
Model中state的优先级比初始化的低,但是基本上项目中的 [code]state都是在这里定义的[/code]
4,Subscription
Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等
subscriptions: { //触发器。setup表示初始化即调用。 setup({dispatch, history}) { history.listen(location => {//listen监听路由变化 调用不同的方法 if (location.pathname === '/login') { //清除缓存 } else { dispatch({ type: 'fetch' }); } }); }, },
5,Effects
用于处理异步操作和业务逻辑,不直接修改
state,简单的来说,就是获取从服务端获取数据,并且发起一个
action交给
reducer的地方。其中它用到了redux-saga里面有几个常用的函数。
- put 用来发起一条action
- call 以异步的方式调用函数
- select 从state中获取相关的数据
- take 获取发送的数据
effects: { *login(action, saga){ const data = yield saga.call(effect(login, 'loginSuccess', authCache), action, saga);//call 用户调用异步逻辑 支持Promise if (data && data.token) { yield saga.put(routerRedux.replace('/home'));//put 用于触发action 什么是action下面会讲到 } }, *logout(action, saga){ const state = yield saga.select(state => state);//select 从state里获取数据 }, },
reducers: { add1(state) { const newCurrent = state.current + 1; return { ...state, record: newCurrent > state.record ? newCurrent : state.record, current: newCurrent, }; }, minus(state) { return { ...state, current: state.current - 1}; }, }, effects: { *add(action, { call, put }) { yield put({ type: 'add1' }); yield call(delayDeal, 1000); yield put({ type: 'minus' }); }, },
如果
effect与
reducers中的
add方法重合了,这里会陷入一个死循环,因为当组件发送一个
dispatch的时候,
model会首先去找
effect里面的方法,当又找到
add的时候,就又会去请求
effect里面的方法。
这里的 delayDeal,是我这边写的一个延时的函数,我们在
utils里面编写一个
utils.js
/** *超时函数处理 * @param timeout :timeout超时的时间参数 * @returns {*} :返回样式值 */ export function delayDeal(timeout) { return new Promise((resolve) => { setTimeout(resolve, timeout); }); }
接着我们在 models/example.js
导入这个 utils.js
import { delayDeal} from '../utils/utils';
6,Reducer
以
key/value格式定义
reducer,用于处理同步操作,唯一可以修改
state的地方。由
action触发。其实一个纯函数。
reducers: { loginSuccess(state, action){ return {...state, auth: action.result, loading: false}; }, }
7,Router
Router表示路由配置信息,项目中的
router.js
8,RouteComponent
RouteComponent表示
Router里匹配路径的
Component,通常会绑定
model的数据
9,Action:表示操作事件,可以是同步,也可以是异步
action的格式如下,它需要有一个
type,表示这个
action要触发什么操作;
payload则表示这个
action将要传递的数据
{ type: namespace + '/login', payload: { userName: payload.userName, password: payload.password } }
构建一个
Action创建函数,如下:
function goLogin(payload) { let loginInfo ={ type: namespace + '/login', payload: { userName: payload.userName, password: payload.password } } return loginInfo } //我们直接dispatch(goLogin()),就发送了一个action。 dispatch(goLogin())
10,dispatch
type dispatch = (a: Action) => Action
dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:
dispatch({ type: namespace + '/login', // 如果在 model 外调用,需要添加 namespace,如果在model内调用 无需添加 namespace payload: {}, // 需要传递的信息 });
- reducers 处理数据
- effects 接收数据
- subscriptions 监听数据
3.3,使用antd
先安装
antd和
babel-plugin-import
npm install antd babel-plugin-import --save # 或 yarn add antd babel-plugin-import
babel-plugin-import也可以通过
-D参数安装到
devDependencies中,它用于实现按需加载。然后在
.webpackrc中添加如下配置:
{ "extraBabelPlugins": [ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }] ] }
现在就可以按需引入 antd 的组件了,如
import { Button } from 'antd',Button 组件的样式文件也会自动帮你引入。
3.4,配置.webpackrc
1,entry是入口文件配置
单页类型:
entry: './src/entries/index.js',
多页类型:
"entry": "src/entries/*.js"
2,extraBabelPlugins 定义额外的 babel plugin 列表,格式为数组。
3,env针对特定的环境进行配置。dev 的环境变量是?
development,build 的环境变量是?
production。
"extraBabelPlugins": ["transform-runtime"], "env": { development: { extraBabelPlugins: ['dva-hmr'], }, production: { define: { __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' } } }
开发环境下的 extraBabelPlugins 是?
["transform-runtime", "dva-hmr"],而生产环境下是?
["transform-runtime"]
4,配置 webpack 的?externals?属性
// 配置 @antv/data-set和 rollbar 不打入代码 "externals": { '@antv/data-set': 'DataSet', rollbar: 'rollbar', }
5,配置 webpack-dev-server 的 proxy 属性。 如果要代理请求到其他服务器,可以这样配:
proxy: { "/api": { // "target": "http://127.0.0.1/", // "target": "http://127.0.0.1:9090/", "target": "http://localhost:8080/", "changeOrigin": true, "pathRewrite": { "^/api" : "" } } },
6,disableDynamicImport
禁用
import()按需加载,全部打包在一个文件里,通过 babel-plugin-dynamic-import-node-sync 实现。
7,publicPath
配置 webpack 的 output.publicPath 属性。
8,extraBabelIncludes
定义额外需要做 babel 转换的文件匹配列表,格式为数组
9,outputPath
配置 webpack 的 output.path 属性。
打包输出的文件
config["outputPath"] = path.join(process.cwd(), './build/')
10,根据需求完整配置如下:
文件名称是:.webpackrc.js,可根据实际情况添加如下代码:
const path = require('path'); const config = { entry: './src/entries/index.js', extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]], env: { development: { extraBabelPlugins: ['dva-hmr'], }, production: { define: { __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' } } }, externals: { '@antv/data-set': 'DataSet', rollbar: 'rollbar', }, lessLoaderOptions: { javascriptEnabled: true, }, proxy: { "/api": { // "target": "http://127.0.0.1/", // "target": "http://127.0.0.1:9090/", "target": "http://localhost:8080/", "changeOrigin": true, } }, es5ImcompatibleVersions:true, disableDynamicImport: true, publicPath: '/', hash: false, extraBabelIncludes:[ "node_modules" ] }; if (module.exports.env !== 'development') { config["outputPath"] = path.join(process.cwd(), './build/') } export default config
更多 .webpackrc
的配置请参考 roadhog 配置。
3.5,使用antd-mobile
先安装 antd-mobile 和
babel-plugin-import
npm install antd-mobile babel-plugin-import --save # 或 yarn add antd-mobile babel-plugin-import
babel-plugin-import也可以通过
-D参数安装到
devDependencies中,它用于实现按需加载。然后在
.webpackrc中添加如下配置:
{ "plugins": [ ["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 会加载 less 文件 ] }
现在就可以按需引入antd-mobile 的组件了,如
import { DatePicker} from 'antd-mobile
',DatePicker 组件的样式文件也会自动帮你引入。
四,整体架构
- 我们根据
url
访问相关的Route-Component
,在组件中我们通过dispatch
发送action
到model
里面的effect
或者直接Reducer
- 当我们将
action
发送给Effect
,基本上是取服务器上面请求数据的,服务器返回数据之后,effect
会发送相应的action
给reducer
,由唯一能改变state
的reducer
改变state
,然后通过connect
重新渲染组件。 - 当我们将
action
发送给reducer
,那直接由reducer
改变state
,然后通过connect
重新渲染组件。如下图所示:
数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过
dispatch发起一个 action,如果是同步行为会直接通过
Reducers改变
State,如果是异步行为(副作用)会先触发
Effects然后流向
Reducers最终改变
State
重置models里的数据:
dispatch({type:namespace+'/set',payload:{mdata:[]}});
set是内置的方法
Dva官方文档 nginx代理部署Vue与React项目
五,问题记录
5.1,路由相关的问题
1,使用match后的路由跳转问题,版本routerV4
match是一个匹配路径参数的对象,它有一个属性params,里面的内容就是路径参数,除常用的params属性外,它还有url、path、isExact属性。
问题描述:不能跳转新页面或匹配跳转后,刷新时url所传的值会被重置掉
不能跳转的情况
const {ConnectedRouter} = routerRedux; function RouterConfig({history}) { const tests =({match}) =>( <div> <Route exact path={`${match.url}/:tab`} component={Test}/> <Route exact path={match.url} component={Test}/> </div> ); return ( <ConnectedRouter history={history}> <Switch> <Route path="/login" component={Login}/> <LocaleProvider locale={zhCN}> <App> <Flex> <Switch> <Route path="/test" component={tests }/> <Route exact path="/test/bindTest" component={BindTest}/> </Switch> </Flex> </App> </LocaleProvider> </Switch> </ConnectedRouter> ); }
路由如上写法,使用下面方式不能跳转,但是地址栏路径变了
import { routerRedux} from 'dva/router'; ... this.props.dispatch(routerRedux.push({ pathname: '/test/bindTest', search:queryString.stringify({ // ...query, Code: code, Name: name }) })); ...
能跳转,但是刷新所传的参数被重置
const {ConnectedRouter} = routerRedux; function RouterConfig({history}) { const tests =({match}) =>( <div> <Route exact path={`${match.url}/bindTest`} component={BindTest}/> <Route exact path={`${match.url}/:tab`} component={Test}/> <Route exact path={match.url} component={Test}/> </div> ); return ( <ConnectedRouter history={history}> <Switch> <Route path="/login" component={Login}/> <LocaleProvider locale={zhCN}> <App> <Flex> <Switch> <Route path="/test" component={tests }/> </Switch> </Flex> </App> </LocaleProvider> </Switch> </ConnectedRouter> ); }
路由如上写法,使用下面方式可以跳转,但是刷新时所传的参数会被test里所传的参数重置
... this.props.dispatch(routerRedux.push({ pathname: '/test/bindTest', search:queryString.stringify({ // ...query, Code: code, Name: name }) })); ...
解决办法如下:地址多加一级,跳出以前的界面
路由配置
const {ConnectedRouter} = routerRedux; function RouterConfig({history}) { const tests =({match}) =>( <div> <Route exact path={`${match.url}/bind/test`} component={BindTest}/> <Route exact path={`${match.url}/:tab`} component={Test}/> <Route exact path={match.url} component={Test}/> </div> ); return ( <ConnectedRouter history={history}> <Switch> <Route path="/test" component={tests }/> </Switch> </ConnectedRouter> ); }
调用
... this.props.dispatch(routerRedux.push({ pathname: '/test/bind/test1', search:queryString.stringify({ // ...query, Code: code, Name: name }) })); ...
5.2,箭头函数this指向问题
箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
- Android Studio第一次使用配置gradle项目构建
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- Android studio第一次使用配置(三)gradle项目构建
- webpack 1.x构建react项目简单配置
- 使用Maven构建Web项目+Spring+Mybatis配置
- [转]第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 使用java配置来构建spring项目
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 使用Maven构建web app开发项目,并配置tomcat
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 用vue构建项目笔记4(在vue中使用sass的配置)
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 在使用spring构建项目中,将db配置与程序jar包分离的一种方式
- Maven配置和打包以及使用Eclipse构建Maven项目
- 第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- [项目构建 十三]babasport Nginx负载均衡的详细配置及使用案例详解.
- 【转】第一次使用Android Studio时你应该知道的一切配置(三):gradle项目构建
- 使用Jenkins配置自动化构建maven项目