JSX语法
JSX其实本质上是一种新的语言,只不过它设计成JavaScript一种扩展,所以,其语法绝大部分都和JavaScript一样。而同时它搭配一个JSX Transform的工具可以将JSX编译为原生的JavaScript。那么这样做好处是什么呢?当然了,首要任务就是让你写代码方便点,否则想想每次都要 React.createElement 也是醉了!其次呢,另一个好处就是它可以让你书写ES6之类的语法,就和CoffeeScript是一个道理,最终它会翻译到浏览器兼容的语法。
const QaQuestion =({props})=>{
return(
hello world
)
}
以上代码就创建一个名为QaQuestion的无状态组件,该组件接收一个props参数,仅仅包含一个div无状态组件的创建形式使代码的可读性更好,并且减少了大量冗余的代码,大大的增强了编写一个组件的便利,除此之外无状态组件还有以下几个显著的特点:
组件不会被实例化,整体渲染性能得到提升
由于是无状态组件,所以无状态组件就不会在有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升。
组件不能访问this对象
无状态组件由于没有实例化过程,所以无法访问组件this中的对象,例如:this.ref、this.state等均不能访问。若想访问就不能使用这种形式来创建组件
组件无法访问生命周期的方法
因为无状态组件是不需要组件生命周期管理和状态管理,所以底层实现这种形式的组件时是不会实现组件的生命周期方法。所以无状态组件是不能参与组件的各个生命周期管理的
无状态组件只能访问输入的props,同样的props会得到同样的渲染结果
无状态组件被鼓励在大型项目中尽可能以简单的写法来分割原本庞大的组件,未来React也会这种面向无状态组件在譬如无意义的检查和内存分配领域进行一系列优化,所以只要有可能,尽量使用无状态组件。
通过React.createClass方式和extends React.Component方式创建的组件都是有状态组件。但是随着React的发展,通过React.createClass这创建组件的这种方式也暴露出一些问题,并且在将来的React版本中,将不在支持这种方式创建组件,因此这种方式并不推荐使用。
class ES6Compant extends React.Component {
constructor(props) {
super(props);
// 设置 initial state
this.state = {
text: props.initialValue || ‘placeholder’
};
}
render() {
return (
React.createClass创建的组件,其状态state是通过getInitialState方法来配置组件相关的状态;React.Component创建的组件,其状态state是在constructor中像初始化组件属性一样声明的。
componentWillMount :此方法会在完成首次渲染之前被调用。这也是在render方法调用前可以修改组件state的最后一次机会。
render :生成页面需要的虚拟DOM结构,用来表示组件的输出。Render需要满足:
(1)只能通过this.props和this.state访问数据;
(2)可以返回null、false或者任何React组件;
(3)只能出现一个顶级组件;
(4)必需纯净,意味着不能改变组件的状态或者修改DOM的输出。
componentDidMount: 该方法发生在render方法成功调用并且真实的DOM已经被渲染之后,在该函数内部可以通过this.getDOMNode()来获取当前组件的节点。然后就可以像Web开发中的那样操作里面的DOM元素了。
componentWillReceiveProps :在任意时刻,组件的props都可以通过父辈组件来更改。当组件接收到新的props(这里不同于state)时,会触发该函数,我们同时也获得更改props对象及更新state的机会。
shouldComponentUpdate: 该方法用来拦截新的props和state,然后开发者可以根据自己设定逻辑,做出要不要更新render的决定,让它更快。
componentWillUpdate: 与componentWillMount方法类似,组件上会接收到新的props或者state渲染之前,调用该方法。但是不可以在该方法中更新state和props。
componentDidUpdate :与componentDidMount类似,更新已经渲染好的DOM。
componentWillUnmount:该方法会在组件被移出之前调被调用。在componentDidMount方法中添加的所有任务都需要在该方法中撤销,比如说创建的定时器或者添加的事件监听等。
Dispatcher
事Dispatcher是Flux应用中管理所有数据流的中心枢纽。它本质上就是一些Store回调函数的注册器,它本身没有其他逻辑 -
只是提供了把Action分发给Store的机制。dispatcher根据action
type调用对应的回调函数。每一个Store都在Dispatcher注册(AppDispatcher.register)并提供回调函数。随着应用的发展,Dispatcher会变得越来越重要。例如Dispatcher可以用来管理Stores之间的依赖关系,通过特定的顺序来调用注册了的回调函数就可以办到。Stores可以等到其他Stores完成更新再进行自己的更新操作。
Store
Store负责封装应用的业务逻辑跟数据的交互,包含应用所有的数据,是应用中唯一的数据发生变更的地方。Store中没有赋值接口,所有数据变更都是由dispatcher发送到store,新的数据随着Store触发的change事件传回view。Store对外只暴露getter,不允许提供setter,禁止在任何地方直接操作Store。
Views和Controller-Views React在View(MVC)层提供了可组合的可自由重新渲染的Views,
在嵌套的views结构顶部, 一个特别的view监听着stores广播的事件,
我们管这种view叫controller-view。在controller-view中我们完成这样的操作::从stores中获取数据并且传递这些数据的到它的子代中.
我们总有一个这样的controller-view控制页面的某一部分。
当controller-view接受到store广播的事件,它首先从store的公共getter方法中获取它需要的新数据,然后调起setState()或者forceUpdate()方法,那么它和它所有子代的render()方法都会运行。
我们常常把整个store的state放在一个对象里面传递到子代中,让子代选择自己需要的东西。这样除了可以在层级结构顶层保持控制(controller)行为因此尽可能保证子代views的单一功能外,还可以减少我们需要管理的属性(props)的数目。
有时候我们可能需要在层级结构的某一层建立另外的一些controller-view使一些组件能简单些。这样可以帮助我们更好地去封装层级上的与特定的数据有关联的一些模块。请注意,在不是顶层建立一个controller-view会破坏单项数据流这个原则,因为有可能会存在数据入口的冲突。在做这样的决定之前,我们可以衡量一下得到一个简单一点的组件和多重数据流多个数据更新入口孰轻孰重。多重数据流会有一些副作用:
React的render()方法会因为不同的controller-view的数据更新而多次被处罚, 会增加debug的难度。
Redux 只有一个 store 。Flux 里面会有多个 store 存储应用数据,并在 store 里面执行更新逻辑,当 store变化的时候再通知 controller-view 更新自己的数据,Redux 将各个 store 整合成一个完整的 store,并且可以根据这个 store 推导出应用完整的 state。同时 Redux 中更新的逻辑也不在 store 中执行而是放在reducer 中。
没有 Dispatcher。
Redux 中没有 Dispatcher 的概念,它使用 reducer 来进行事件的处理,reducer 是一个纯函数,这个函数被表述为 (previousState, action) => newState ,它根据应用的状态和当前的 action 推导出新的 state。Redux 中有多个 reducer,每个 reducer 负责维护应用整体 state 树中的某一部分,多个 reducer 可以通过 combineReducers 方法合成一个根reducer,这个根reducer负责维护完整的 state,当一个 action 被发出,store 会调用 dispatch 方法向某个特定的 reducer 传递该 action,reducer 收到 action 之后执行对应的更新逻辑然后返回一个新的 state,state 的更新最终会传递到根reducer处,返回一个全新的完整的 state,然后传递给 view。
Redux 和 Flux 之间最大的区别就是对 store/reducer 的抽象,Flux 中 store 是各自为战的,每个 store 只对对应的 controller-view 负责,每次更新都只通知对应的 controller-view;而 Redux 中各子 reducer 都是由根reducer统一管理的,每个子reducer的变化都要经过根reducer的整合。用图表示的话可以像这样:
Flux 中的 store :
Redux 中的 store(或者叫 reducer)
前端路由 前端的路由和后端的路由在实现技术上不一样,但是原理都是一样的。在 HTML5 的 history API出现之前,前端的路由都是通过 hash 来实现的,hash 能兼容低版本的浏览器,它的 URI 规则中需要带上 #。例如:http://localhost:8000/#/login Web 服务并不会解析 hash,也就是说 # 后的内容 Web服务都会自动忽略,但是 JavaScript 是可以通过 window.location.hash读取到的,读取到路径加以解析之后就可以响应不同路径的逻辑处理。 history 是 HTML5 才有的新 API,可以用来操作浏览器的session history (会话历史)。基于 history 来实现的路由可以不需要#,例如localhost:8080/login
component={qaBasic}>
Router组件本身只是一个容器,真正的路由要通过Route组件定义,path对应的是访问路径,component是该路径对应的组件。例如:在浏览器中访问/qaBasic的时候,会加载qaBasic这个组件。当然这里还有组件嵌套,也就是在一个Route里面包含另一个子Route,表明在访问子组件的时候,会先加载父组件,然后再父组件里面加载子组件。
Ant Design
Ant design是蚂蚁金服出品的一款前端UI librar,提供了丰富的React组件。
ant design组件库
DVA
dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。( Inspired by elm and choo. )dva 是 框架,不是 library,类似 emberjs,会很明确地告诉你每个部件应该怎么写,这对于团队而言,会更可控。另外,除了 react 和 react-dom 是 peerDependencies 以外,dva 封装了所有其他依赖。dva 实现上尽量不创建新语法,而是用依赖库本身的语法,比如 router 的定义还是用 react-router 的 JSX 语法的方式。
1
2
dva介绍
项目实践
本章节介绍windows系统下react+dva构建项目实现过程,将前面介绍的知识点进行整合,主要包括:项目的搭建、基本配置、目录规划、路由配置、前端通过调用后台restful接口获取数据、react组件间的数据传递等。
1
2
创建项目
这里通过dva来快速新建一个项目,当然在次之前需要提前准备好node环境
下载:https://nodejs.org/en/download/
安装:
检测:命令行下输入 node -v
2) 安装dva-cli
安装: npm install dva-cli -g
检测:dva-v
3) 利用dva创建项目
dva new PORTAL //会创建 PORTAL 目录,并在该目录下生成一些基本配置文件
4) 启动应用
在PORTAL目录下,执行命令 npm start
基本配置
在项目创建成功之后,会在项目下看到一些基础配置文件,这也是一点通过dva来构建项目的方便之处。一般可以看到以下几个配置文件:.eslintrc、.editorconfig、.roadhogrc、.roadhogrc.mock.js、package.json,这里简单了解两个配置文件:roadhogrc和package.json。基于npm模式开发的时候,和以前那样纯粹的写js代码不同,因为这是面向模块化的前端开发。有关于前端模块化开发,这里提供一个介绍文档。
基于npm模式的前端开发
package.json配置文件非常像maven中的pom.xml文件,虽然它还有其它用途,但在很多情况下,都有着管理模块的的作用,例如:
“dependencies”: {
“antd”: “^2.10.0”,
“babel-plugin-import”: “^1.1.1”,
“babel-runtime”: “^6.9.2”,
“dva”: “^1.2.1”,
“echarts”: “^3.5.4”,
“qs”: “^6.4.0”,
“react”: “^15.4.0”,
“react-bootstrap”: “^0.31.0”,
“react-dom”: “^15.4.0”
},
将项目中需要用到的模块添加进package.json文件,然后执行 npm install 就可以将这些模块从npm库下载到本地。
.roadhogrc配置文件里面的内容是一个json对象,是对roadhog模块的配置
“entry”: “src/desktop/index.js”,
“proxy”: {
“/api”: {
“target”: “http://localhost:8080/api/”,
“changeOrigin”: true,
“pathRewrite”: { “^/api” : “” }
},
“/oauth”: {
“target”: “http://localhost:8080/oauth/”,
“changeOrigin”: true,
“pathRewrite”: { “^/oauth” : “” }
}
},
以上是代码中,entry 指定了整个项目的入口文件;proxy设置了代理,上面的意思是会配置所有以api开头的请求
目录规划
前端应用越来越复杂,也越来越规范,在前后端分离的系统中,前端实际上已经控制了MVC模式中的Controller和View层,而后端仅仅是作为M层提供数据。因此,在前端应用开发过程中,特别是基于React这套前端框架的应用中,目录规划显得十分重要。在利用dva开发前端构建react的应用中,主要划分为以下几个目录:components、container、models、routes、services、utils、styles 以下是项目目录截图:
这里写图片描述
components:最基础的组件。这里面存放的只是最基本的UI组件,这些组件接收外部传过来的参数(数据),并将这些数据渲染的到界面。根据传入的参数的不同,界面渲染也不同。
container:contatiner负责将数据的组件进行连接,相当于将compontent组件和store里面的数据进行包装,生成一个新的有数据的组件。然后,在router.js配置文件中引用container中的组件。
routers:router目录其实和container目录基本一样,只是在利用dva开发的应用中叫router,在不是利用dva开发的应用中叫container而已,两者有一个即可。
models:model是dva中的一个重要概念,也可以看作是前端中的数据层。在我的理解里,dva将model以
namespace作为唯一标识进行区分,然后将所有model的数据存储到redux
中的store里面。在引用的时候,通过各个model的namespace进行引用。Model,是一个处理数据的地方,在model里面调用service层获取数据。
services:services负责向后台请求数据,在services里调用后台提供的api获取数据。
utils:工具类目录,比如常见的后台接口请求工具类。
styles:存放css或less样式文件。
constants.js:在里面定义一些通用的常量。
router.js:配置整个应用的路由。
index.js:整个应用的入口文件,dva和其它框架稍有不同。
路由配置
路由配置主要是为了控制在浏览器上界面的跳转。这里引用的是react-router这个框架。在router.js里面对整个应用的路由惊醒配置。主要注意的几点就是:在router.js里面引用的一般都是container组件,通过配置,将路径和对应的要在浏览器上加载的组件对应起来,再通过window.location.hash或者是‘routerRedux’这个组件进行路由之间的转跳。
import React from ‘react’;
import { Router, Route, IndexRoute, history} from ‘dva/router’;
import qaBasic from ‘./container/qa/qaBasic’;
import qaGuide from ‘./container/qa/qaGuide’;
import qaQuestion from ‘./container/qa/qaQuestion’;
import qaAskBasic from ‘./container/qa/qaAskBasic’;
function RouterConfig({ history }) {
return (
)
}
export default RouterConfig;
以上便是一个最基础的路由配置,path对应的是浏览器上地址栏的路径,component是访问该路径时将会在界面上加载的组件。还有这里用到了router嵌套,即在一个router里面嵌套另外一个router,这种情况下,在访问子router对应的路径时,会先加载父router对应的组件,然后再父组件里面加载子router对应的组件。
前后端交互
在前后端分离的项目中,前后端的数据交互式通过在前端应用中调用后端提供的restful接口获取数据。在dva构建的前端应用中,标准的前后端交互大概是这个流程:
model需要在 index.js 里面声明
app.model(require(‘./models/qa));
model里面需要有namesapce这个属性值
外部使用model里面的方法值时需要通过namespace
namespace/方法名
import dva from ‘dva’;
import * as service from ‘…/services/qa’;
export default {
namespace: ‘qa’,
state: {
questionList:[],
},
subscriptions: {
setup ({ dispatch }) {
dispatch({ type: ‘fetchGuide’,payload:{}});
},
},
effects: {
*fetchGuide({ payload:{guidelineId}},{ call, put }) { const {rows} = yield call(service.fetchGuide, {guidelineId});
yield put({
type: ‘guideSave’,
payload: { guideList: rows}
});
},
reducers: {
guideSave(state, { payload: { guideList, breadcrumb} } ) {
return { …state, guideList};
},
},
};
以上新建了一个namespace 为 ‘qa’的model,并在effects里面添加了一个fetchGuide方法和在reducers里面添加了一个guideSave方法。
Subscriptions里面的内容表示在项目启动加载model的时候就会执行,dispatch({ type: ‘fetchGuide’,payload:{}});就相当于在项目启动的时候,就会先调用一次effects里面的fetchGuide方法;
effects里面的put 方法,会调用reducers 里面的方法,根据方法中参数type的值找到reducers中的那个方法并执行。这个过程其原理就是redux中 dispatch 一个action的过程。
reducers里面的 方法负责改变store中的值,其实也只有通过这种方式才能改变store中的值。
import request from ‘…/utils/request’;
import {stringify} from ‘qs’;
const headers={
‘Content-Type’: ‘application/x-www-form-urlencoded;utf-8’,
};
// 查看指引
export function fetchGuide(body={}) {
body.access_token = localStorage.access_token;
return request(/api/qa/guide
,{
method: ‘POST’,
headers: headers,
body: stringify(body)
});
}
可以看到,这个文件比较简单:
首先从utils目录学引入了一个工具类,该工具类主要用来请求后端数据。就是
一个工具类而已,传入两个参数,一个是后台提供的restful API地址,一个是参
数。然后得到后台返回的数据,这就是这个工具类的主要用途。然后再service的
fetchGuide方法里面,传入参数进行调用,并最终返回后台数据。也就是说,在
model里面调用service,可以获取后台的数据,然后保存到store中。
“proxy”: {
“/api”: {
“target”: “http://localhost:8080/api/”,
“changeOrigin”: true,
“pathRewrite”: { “^/api” : “” }
},
“/oauth”: {
“target”: “http://localhost:8080/oauth/”,
“changeOrigin”: true,
“pathRewrite”: { “^/oauth” : “” }
}
},
以上内容表示在前端请求以‘api’为前缀的api的时候,会使用代理:怎么说呢,就相当于在请求/api/qa/guide 这个路径的时候,最后实际上请求的路径会是http://localhost:8080/api/ qa/guide,这样一方面方便了我们配置,在改变ip的时候只需要在配置文件里面改革ip就可以了,很方便。但不是很了解这个到底是怎么一个流程?还有,使用这种方式会自动解决js跨域的问题吗?因为在一般情况下,js跨域问题是需要去解决的,那这种方式呢?还不是很懂。
因此,dva中的前后端交互主要就是以下流程:
这里写图片描述
组件数据流
前一小节讲解了在dva中的前后端交互流程,在获取到数据之后,接下来面临的一个问题就是怎么将数据传递到组件上了。
我们知道,react是自上而下的单向数据流,也就是从父组件传递到子组件,而不能从子组件传递到父组件。那么当我们需要将子组件的数据传递到父组件时,该怎么办呢?一种方法是使用回调函数,当发生某个操作时执行回调函数改变state然后重新渲染界面。还有一种方法是使用第三方框架。Dva中就包含了一个这样的框架:redux
在redux中,通过store管理所有的state,dva只是将几个框架进行整合,根本的东西其实根本没有一丝改变,所以dva中model里面的那些数据其实都是存储在store里面的。Model下的namespace,就相当于是store下的一个个属性。理解清楚了这个,那么给组件传递数据的流程也就清楚了。
import { connect } from ‘dva’;
import QaQuestion from ‘…/…/components/qa/QaQuestion’;
const qaQuestion = ({qa})=>{
return (
);
}
export default connect(({ qa }) => ({ qa }))(qaQuestion);
以上就是一个container组件,当然,上面的写法其实有点多余:const qaQuestion 这里其实生成的还是一个components组件,然后将QaQuestion 这个components组件包装到qaQuestion 组件里面,这里有点多余。但是不影响我们分析问题。Connect是redux提供的一个函数,作用是将数据和组件连接起来,也就是所谓的向components组件传递数据。在这里我们传递了一个qa参数,其实这是一个namespace名为qa的model,当然,数据最终是存储在store下面。也就是说,我们通过connect这个函数,可以直接拿到store里面的数据(model也是在store里面);然后再qaQuestion这个组件上,接收一个参数,也就是connect高阶函数中取出的那个参数,然后我们再将 qa下面的questionList值传递给了QaQuestion组件,参数名为 props,这样我们就可以在QaQuestion组件中直接使用props(它的值就是qa. questionList)这个参数了。
const QaQuestion =({props})=>{
return(
{item.comments}
从上可以看出,已经使用到了container里面传过来的参数。
Dva启动文件
默认情况是index.js,当然这个可以在.roadhogrc配置文件中进行配置。以下是index.js内容
import dva from ‘dva’;
import ‘./styles/common.css’;
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model
app.model(require(’./models/login’));
app.model(require(’./models/qa’));
// 4. Router
app.router(require(’./router’));
// 5. Start
app.start(’#root’);
在dva中,项目启动主要分为以下过程:第一步是实例化一个dva对象;第二步是添加需要使用到的插件;第三步是添加需要使用到的model;第四部是添加路由配置;第五步是调用dva中的start方法,该方法接收一个参数,这个参数是html文件中某个元素的id,作为整个应用的挂载点。
hml文件默认是public目录下的index.html文件,以下是html文件的内容,非常简单,在body标签下面只有一个div标签,这个div就是作为整个应用的挂载点。其中还有个