引言
如有错误,欢迎大家留言指出来
我的项目地址:https://github.com/jdbi336534/weather
技术栈:react + dva + redux + react-router + redux-sage + antd-mobile
效果图
1.安装dva-cli
通过 npm 安装 dva-cli 并确保版本是 0.7.0 或以上。
$ npm install dva-cli -g
$ dva -v
0.7.0
2.创建应用
安装完 dva-cli 之后,就可以在命令行里访问到 dva
命令。现在,你可以通过 dva new
创建新应用。
$ dva new dva-quickstart
这会创建 dva-quickstart
目录,包含项目初始化目录和文件,并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。 然后我们 cd
进入 dva-quickstart
目录,并启动开发服务器:
$ cd dva-quickstart
$ npm start
几秒钟后,你会看到以下输出:
Compiled successfully!
The app is running at:
http://localhost:8000/
Note that the development build is not optimized.
To create a production build, use npm run build.
在浏览器里打开 http://localhost:8989 ,你会看到 dva 的欢迎界面。
3.引入antd-mobile
安装依赖
npm install antd-mobile babel-plugin-import --save
npm install babel-plugin-import --save
babel-plugin-import 是用来按需加载脚本和样式,编辑 webpack.config.js,使 babel-plugin-import 插件生效。同时使用antd的高清方案。
// 引入 babel-plugin-import
webpackConfig.babel.plugins.push(['import', { libraryName: 'antd-mobile', style: 'css' }]);
// 引入高清方案
webpackConfig.postcss.push(pxtorem({
rootValue: 100,
propWhiteList: [],
}));
下面是完整的webpack.config.js
const webpack = require('atool-build/lib/webpack');
const pxtorem = require('postcss-pxtorem');
const path = require('path');
module.exports = function(webpackConfig, env) {
webpackConfig.babel.plugins.push('transform-runtime');
// Support hmr
if (env === 'development') {
webpackConfig.devtool = '#eval';
webpackConfig.babel.plugins.push('dva-hmr');
} else {
webpackConfig.babel.plugins.push('dev-expression');
webpackConfig.externals = {
// Use external version of React
"react": "React",
"react-dom": "ReactDOM"
};
}
// Don't extract common.js and common.css
webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
return !(plugin instanceof webpack.optimize.CommonsChunkPlugin);
});
// Support CSS Modules
// Parse all less files as css module.
webpackConfig.module.loaders.forEach(function(loader, index) {
if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) {
loader.include = /node_modules/;
loader.test = /\.less$/;
}
if (loader.test.toString() === '/\\.module\\.less$/') {
loader.exclude = /node_modules/;
loader.test = /\.less$/;
}
if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.css$') > -1) {
loader.include = /node_modules/;
loader.test = /\.css$/;
}
if (loader.test.toString() === '/\\.module\\.css$/') {
loader.exclude = /node_modules/;
loader.test = /\.css$/;
}
});
// 引入 babel-plugin-import
webpackConfig.babel.plugins.push(['import', { libraryName: 'antd-mobile', style: 'css' }]);
// 引入高清方案
webpackConfig.postcss.push(pxtorem({
rootValue: 100,
propWhiteList: [],
}));
const svgDirs = [
require.resolve('antd-mobile').replace(/warn\.js$/, ''), // 1. 属于 antd-mobile 内置 svg 文件
path.resolve(__dirname, './src/assets/'), // 2. 自己私人的 svg 存放目录
];
// 因为一个 SVG 文件不能被处理两遍. 在 atool-build 默认为 svg配置的svg-url-loade 里 exclude 掉需要 svg-sprite-loader处理的目录
// https://github.com/ant-tool/atool-build/blob/master/src/getWebpackCommonConfig.js#L162
// https://github.com/kisenka/svg-sprite-loader/issues/4
webpackConfig.module.loaders.forEach(loader => {
if (loader.test && typeof loader.test.test === 'function' && loader.test.test('.svg')) {
loader.exclude = svgDirs;
}
});
// 4. 配置 webpack loader
webpackConfig.module.loaders.unshift({
test: /\.(svg)$/i,
loader: 'svg-sprite',
include: svgDirs, // 把 svgDirs 路径下的所有 svg 文件交给 svg-sprite-loader 插件处理
});
return webpackConfig;
};
4.目录结构
5.初始化dva
import './index.html';
import './index.css';
import dva from 'dva';
import FastClick from 'fastclick'
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
// 1. Initialize
const app = dva();
// 2. Plugins
//app.use({});
// 3. Model
app.model(require('./models/weatherReport'));
// 4. Router
app.router(require('./router'));
// 5. Start
app.start('#root');
初始化很简单,接下来详细的说明下
5.1.const app = dva()
app = dva(opts)
是用来创建应用,返回dva实例。 opts
包含:
history
:指定给路由用的 history,默认是hashHistory
initialState
:指定初始数据,优先级高于 model 中的 state,默认是{}
如果要配置 history 为 browserHistory
,可以这样:
import { browserHistory } from 'dva/router';
const app = dva({
history: browserHistory,
});
常见的history有三种形式 另外,出于易用性的考虑,opts
里也可以配所有的 hooks ,下面包含全部的可配属性:
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
5.2.app.use(hooks)
这个是配置 hooks 或者注册插件的,如全局的loading,因为此项目目前还没有用到,所以此句代码被注释
比如注册 dva-loading 插件的例子:
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
5.3.app.model(model)
modal 是dva中的概念,我的项目所有的数据都存放在此modal中,这样的好处是可以统一管理,它主要是用来接收action的。 简单的说:
state 定义初始化的值
reducers 处理数据(这里可以进行数据的修改)
effects 接收数据(这里可以使用fetch获取数据或者提交数据)
subscriptions 监听数据(监听路由变化等)
如下,一个典型的案例:
app.model({
namespace: 'todo',
state: [],
reducers: {
add(state, { payload: todo }) {
// 保存数据到 state
return [...state, todo];
},
},
effects: {
*save({ payload: todo }, { put, call }) {
// 调用 saveTodoToServer,成功后触发 `add` action 保存到 state
yield call(saveTodoToServer, todo);
yield put({ type: 'add', payload: todo });
},
},
subscriptions: {
setup({ history, dispatch }) {
// 监听 history 变化,当进入 `/` 时触发 `load` action
return history.listen(({ pathname }) => {
if (pathname === '/') {
dispatch({ type: 'load' });
}
});
},
},
});
官方说明如下:
model 包含 5 个属性:
namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 .
的方式创建多层命名空间。
state
初始值,优先级低于传给 dva()
的 opts.initialState
。
比如:
const app = dva({
initialState: { count: 1 },
});
app.model({
namespace: 'count',
state: 0,
});
此时,在 app.start()
后 state.count 为 1 。
reducers
以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state
的地方。由 action
触发。
格式为 (state, action) => newState
或 [(state, action) => newState, enhancer]
。
详见: https://github.com/dvajs/dva/blob/master/test/reducers-test.js
effects
以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state
。由 action
触发,可以触发 action
,可以和服务器交互,可以获取全局 state
的数据等等。
格式为 *(action, effects) => void
或 [*(action, effects) => void, { type }]
。
type 类型有:
takeEvery
takeLatest
throttle
watcher
详见:https://github.com/dvajs/dva/blob/master/test/effects-test.js
subscriptions
以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start()
时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
格式为 ({ dispatch, history }, done) => unlistenFunction
。
注意:如果要使用 app.unmodel()
,subscription 必须返回 unlisten 方法,用于取消数据订阅。
5.3.app.router
这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。
dva 实例提供了 router 方法来控制路由,使用的是react-router。
import { Router, Route } from 'dva/router';
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
当然如果项目非常大页面很多的话,使用上面这种路由的方法,在打包后文件是非常大的,这回导致首次加载页面的时候非常缓慢,大项目可以使用下面这种按需加载的方式:
按需加载,可以以下面的方式进行
const Basicplantform = (location, callback) => {
require.ensure([], require => {callback(null,
require('./routes/basicplantform'))}, 'Basicplantform')
}
const Login = (location, callback) => {
require.ensure([], require => {callback(null,
require('./routes/login/login'))}, 'Login')
};
export default function({ history }) {
return (
<Router history={history}>
<Route path="/" component={System}>
<Route path="Login" getComponent={Login} />
<Route path="Basicplantform" getComponent={Basicplantform} />
</Route>
</Router>
);
}
6.代码/components
在components文件加下面写的组件,主要是ui层面的,事件的处理一般会传递到父组件中统一处理,需要用到的数据也是从父组件中通过props传递过来的。
其实我这里是使用函数式
定义了一个组件,React定义组件的方式有三种:
- 函数式定义的无状态组件(会直接省略生命周期的部分 从而 可以大大的加快加载速度)
- es5原生方式React.createClass定义的组件
- es6形式的extends React.Component定义的组件
在大部分React代码中,大多数组件被写成无状态的组件,通过简单组合可以构建成其他的组件等;这种通过多个简单然后合并成一个大应用的设计模式被提倡。
详细区别请点击:React定义组件的三种方式
7.代码/routes
将components/Weather/index.js
引入到此文件中。
在定义Model和Component之后,我们需要将它们连接在一起。连接后,Component可以使用Model中的数据,而Model可以接收从Component dispatch的操作。而connect主要作用就是将model中的数据当做props传递组件。 注意: connect 是来自于 react-redux。
通过这样的方式传递给Weather组件,mapStateToProps可以获取到我们想要获取到的值,我这里全部获取到了,传递给组件。
function mapStateToProps({ weather }) {
return { weather };
}
export default connect(mapStateToProps)(Weather);
8.代码/moudels
import {
briefforecast3days,
briefcondition,
briefaqi
} from '../services/weather';
export default {
namespace: 'weather',
state: {
briefdata: {
city:{
name:''
},
condition: {
condition:'',
updatetime: '',
humidity:'',
icon:'0',
temp:'',
windDir:'',
windLevel:''
}
},
brief3day: {
forecast: [{
conditionDay: '',
conditionIdDay: '0',
conditionIdNight: '0',
conditionNight: '',
predictDate: '',
tempDay: '',
tempNight: '',
updatetime: '',
windDirDay: '',
windDirNight: '',
windLevelDay: '',
windLevelNight: ''
}, {
conditionDay: '',
conditionIdDay: '0',
conditionIdNight: '0',
conditionNight: '',
predictDate: '',
tempDay: '',
tempNight: '',
updatetime: '',
windDirDay: '',
windDirNight: '',
windLevelDay: '',
windLevelNight: ''
}, {
conditionDay: '',
conditionIdDay: '0',
conditionIdNight: '0',
conditionNight: '',
predictDate: '',
tempDay: '',
tempNight: '',
updatetime: '',
windDirDay: '',
windDirNight: '',
windLevelDay: '',
windLevelNight: ''
}]
},
Drawerstatus:false
},
subscriptions: {
// 订阅监听
setup({
dispatch,
history
}) {
history.listen(location => {
switch (location.pathname) {
case '/weather':
dispatch({
type: 'fetchbrief',
payload: {
lat: "32.812036",
lon: "106.969741"
}
});
dispatch({
type: 'fetch3days',
payload: {
lat: "32.812036",
lon: "106.969741"
}
});
break;
}
});
},
},
effects: {
// 请求提交数据
* fetchbrief({
payload
}, {
call,
put
}) {
const {
data
} = yield call(briefcondition, payload);
console.log('fetchbrief:', data);
if (data.code === 0) {
yield put({
type: 'commonData',
payload: {
briefdata: data.data
}
})
}
},
* fetch3days({
payload
}, {
call,
put
}) {
const {
data
} = yield call(briefforecast3days, payload);
console.log('brief3day:', data);
if (data.code === 0) {
yield put({
type: 'commonData',
payload: {
brief3day: data.data
}
})
}
}
},
reducers: {
// 修改数据
commonData(state, action) {
return { ...state,
...action.payload
};
},
},
}
再次强调:
state 定义初始化的值
reducers 处理数据(这里可以进行数据的修改)
effects 接收数据(这里可以使用fetch获取数据或者提交数据)
subscriptions 监听数据(监听路由变化等)
1.首先在subscriptions
中监听路由,如果路由为/weather
则会去执行effects
中的fetchbrief
和fetch3days
,fetchbrief
和fetch3days
主要用来请求数据,请求一天的天气和未来三天的预报。 大家不要奇怪 function *(next){...............}
这样的写法,这是就是ES6的新特性Generator Function(生成器函数)。详细请看:生成器函数 生成器函数这样写的好处是,通过yeild关键字,可以用同步的写法去写异步的代码,没有了回调函数。
2.请求到数据后通过reducers
中的commonData
方法去改变state
中定义的briefdata
和brief3day
,简单的说当我们使用put发送一条action的时候 与之对于的reducers就会接收到这个消息 然后在里面返回state等数据。
流程是这样的一个流程,但是call,put,select又是什么鬼? 一般常用的有put call select take 。
- put 用来发起一条action
- call 以异步的方式调用函数
- select 从state中获取相关的数据
- take 获取发送的数据
3.如果需要在生成器函数中做路由的跳转的话可以这样
import { routerRedux } from 'dva/router';
*example({payload},{call,put}){
yeild put(routerRedux.push('/weather'))
}
9.如何通过dispatch修改state中的值?
那么看图,图上一共有5步,过程是这样的:
- 1.子组件将ontap事件传递到父组件。
- 2.父组件执行dsipatch方法,type是又model的namespace和reducers中的方法名组成,根据你里面设置的type内容 然后转发到指定的model的设置正确后model那边才能接收到你发送的这条action。
- 3.执行reducer中的方法,改变state中的值。
10.如何通过dispatch跳转路由?
在父组件中的function中,通过如下方式写就能进行跳转:
import { routerRedux } from 'dva/router';
onTap(){
dispatch(routerRedux.push('/audit'));
}
11.路由的写法(注意:页面较多一定要按需加载)
import React, { PropTypes } from 'react';
import { Router, Route, IndexRoute, Link } from 'dva/router';
import IndexPage from './routes/IndexPage';
import info from './routes/Information';
import Weather from './routes/WeatherRoute';
export default function({ history }) {
return (
<Router history={history}>
<Route path="/" component={info} />
<Route path="/device" component={IndexPage} />
<Route path="/weather" component={Weather} />
</Router>
);
};
下一篇将持续更新