源站连接 https://tkvern.comjavascript
继 Rails 从入门到彻底放弃 拥抱 Elixir + Phoenix + React + Redux 这篇文章被喷以后,笔者很长一段时候没有上社区逛了。如今 tkvern 又回归了,给你们带来React实践的一些经验,一些踩坑的经验。php
Rails嘛,很好用,Laravel也好用。Phoenix也好用。都好,哪一个方便用哪一个。前端
还有关于Turbolinks之争,不能单从页面渲染时间去对比,要综合考虑。java
Dva是基于Redux作了一层封装,对于React的state管理,有不少方案,我选择了轻量、简单的Dva。至于Mobx,还没应用到项目中来。先等友军踩踩坑,再往里面跳。react
Why dva and what's dvawebpack
顺便贴下Dva的特性:github
易学易用:仅有 5 个 api,对 redux 用户尤为友好web
elm 概念:经过 reducers
, effects
和 subscriptions
组织 modeltypescript
支持 mobile 和 react-native:跨平台 (react-native 例子)
支持 HMR:目前基于 babel-plugin-dva-hmr 支持 components 和 routes 的 HMR
动态加载 Model 和路由:按需加载加快访问速度 (例子)
插件机制:好比 dva-loading 能够自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
支持 TypeScript:经过 d.ts (例子)
作为传道士,这么好的UI设计语言,确定不会藏着掖着啦。蚂蚁金服的东西,确实不错,除了Ant Design外,还有Ant Design Mobile、AntV、AntMotion、G2。
npm install
太慢,试试yarn吧。建议用npm install yarn -g
进行安装。
项目开始了,前端视图写完,要开始数据交互了,后端提供的API还没好。
那么问题来了,如何在不依靠后端提供API的状况下,实现数据交互?
使用Mock.js能够解决这个问题。先对接好API数据格式,而后使用Mockjs拦截Ajax请求,模拟后端真实数据。
在Mockjs官方提供的API不够用的状况下,还可使用正则产生模拟数据。
这里给出一个模拟用户数据并持久化的实例实例:mock/users.js
代码摘要:
'use strict'; const qs = require('qs'); const mockjs = require('mockjs'); const Random = mockjs.Random; // 数据持久化 let tableListData = {}; if (!global.tableListData) { const data = mockjs.mock({ 'data|100': [{ 'id|+1': 1, 'name': () => { return Random.cname(); }, 'mobile': /1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}/, 'avatar': () => { return Random.image('125x125'); }, 'status|1-2': 1, 'email': () => { return Random.email('visiondk.com'); }, 'isadmin|0-1': 1, 'created_at': () => { return Random.datetime('yyyy-MM-dd HH:mm:ss'); }, 'updated_at': () => { return Random.datetime('yyyy-MM-dd HH:mm:ss'); }, }], page: { total: 100, current: 1, }, }); tableListData = data; global.tableListData = tableListData; } else { tableListData = global.tableListData; }
完成持久化处理后,就能够像操做数据库同样进行增、删、改、查
下面是一个删除用户的API
'DELETE /api/users' (req, res) { setTimeout(() => { const deleteItem = qs.parse(req.body); tableListData.data = tableListData.data.filter((item) => { if (item.id === deleteItem.id) { return false; } return true; }); tableListData.page.total = tableListData.data.length; global.tableListData = tableListData; res.json({ success: true, data: tableListData.data, page: tableListData.page, }); }, 200); },
模拟数据和API写好了,还须要拦截Ajax请求
修改package.json
. . . "scripts": { "start": "dora --plugins \"proxy,webpack,webpack-hmr\"", "build": "atool-build -o ../../../public", "test": "atool-test-mocha ./src/**/*-test.js" } . . .
若是与dora
有端口冲突可修改dora
的端口号
"start": "dora --port 8888 --plugins \"proxy,webpack,webpack-hmr\"",
完成这些基本工做就作好了
在模拟数据环境,services
下的模块这么写就行了,真实API则替换为真实API的地址。可将地址前缀写到统一配置中去。
import request from '../utils/request'; import qs from 'qs'; export async function query(params) { return request(`/api/users?${qs.stringify(params)}`); } export async function create(params) { return request('/api/users', { method: 'post', body: qs.stringify(params), }); } export async function remove(params) { return request('/api/users', { method: 'delete', body: qs.stringify(params), }); } export async function update(params) { return request('/api/users', { method: 'put', body: qs.stringify(params), }); }
真实API参考实例: src/services/users.js
在看dva的引导手册时,并无介绍登陆相关的内容。由于不一样的项目,对于登陆这块的实现会有所不一样,并非惟一的。一般咱们会使用Cookie的方式保持登陆状态,或者 Auth 2.0的技术。
这里介绍Cookie的方式。
登陆成功以后服务器会设置一个当前域可使用的Cookie,例如token
啥的。而后在每次数据请求的时候在Request Headers
中携带token
,后端会基于这个token
进行权限验证。思路清晰了,来看看具体实现吧。(注:在此次项目中使用了统一登陆模块,经过Header中的Authorization
进行验证,将只介绍拿到token
以后的数据处理)
对于操做Cookie的一些操做,建议先封装到工具类模块下。同时我把操做LocalStrage
的一些操做也写进来了。
. . . // Operation Cookie export function getCookie(name) { const reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); const arr = document.cookie.match(reg); if (arr) { return decodeURIComponent(arr[2]); } else { return null; } } export function delCookie({ name, domain, path }) { if (getCookie(name)) { document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=' + path + '; domain=' + domain; } } . . .
Header的预处理我放在了src/utils/auth.js#L5,这里后端返回的数据都是JSON格式,因此在Header里面须要添加application/json
进去,而Authorization
是后端用来验证用户信息的。变量sso_token
为了方便代码阅读就没有按照规范命名了。
export function getAuthHeader(sso_token) { return ({ headers: { 'Accept': 'application/json', 'Authorization': 'Bearer ' + sso_token, 'Content-Type': 'application/json', }, }); }
这里没有使用自带的catch机制来处理请求错误,在开发过程当中,最开始打算使用统一错误处理,可是发现请求失败后,不能在models
层处理components
,因此就换了一种方式处理,后面会讲到。
export default function request(url, options) { const sso_token = getCookie('sso_token'); const authHeader = getAuthHeader(sso_token); return fetch(url, { ...options, ...authHeader }) .then(checkStatus) .then(parseJSON) .then((data) => ({ data })); // .catch((err) => ({ err })); }
完成这些配置以后,每次向服务器发送的请求就都携带了用户token
了。在token
无效时,服务器会抛出401
错误,这时就须要在中间件中处理401
错误。
redirectLogin
是工具类src/utils/auth.js中的重定向登陆方法。
function checkStatus(response) { if (response && response.status === 401) { redirectLogin(); } if (response.status >= 200 && response.status < 500) { return response; } const error = new Error(response.statusText); error.response = response; throw error; }
到此为止,登陆状态的配置基本完成。
咱们的应用中会有多个页面,并且有的须要登陆才可见,那么如何控制呢?
React的路由控制是比较灵活的,来看看下面这个例子:
import React from 'react'; import { Router, Route } from 'dva/router'; import { authenticated } from './utils/auth'; import Dashboard from './routes/Dashboard'; import Users from './routes/Users'; import User from './routes/User'; import Password from './routes/Password'; import Roles from './routes/Roles'; import Permissions from './routes/Permissions'; export default function ({ history }) { return ( <Router history={history}> <Route path="/" component={Dashboard} onEnter={authenticated} /> <Route path="/user" component={User} onEnter={authenticated} /> <Route path="/password" component={Password} onEnter={authenticated} /> <Route path="/users" component={Users} onEnter={authenticated} /> <Route path="/roles" component={Roles} onEnter={authenticated} /> <Route path="/permissions" component={Permissions} onEnter={authenticated} /> </Router> ); }
对于路由的验证配置在onEnter
属性中,authenticated
方法可统一进行路由验证,要注意每个Route
节点的验证都须要配置相应的onEnter
属性。若是权限较为复杂需对每个Route
单独验证。其实这种基于客户端渲染的应用,若是页面限制有遗漏也关系不太,后端提供的API会对数据进行验证,即便前端访问到没有权限的页面,也一样不用担忧,作好客户端错误处理便可。
对于一个React应用来讲,缓存是很重要的一步。先后端分离后,频繁的Ajax请求会消耗大量的服务器资源,若是一些不长变更的持久化数据不作缓存的话,会浪费许多资源。因此,比较常见的方法就是将数据缓存在LocalStorage
中。针对一些敏感信息可适当进行加密混淆处理,我这里就不介绍了。
例:用户信息缓存
在subscriptions
中配置了setup
检测LocalStorage
中的user
是否存在。不存在时会去query
用户信息,而后保存到user
中,若是存在就将user
中的数据添加到state
的user: {}
中。固然在进行请求时,已经在src/utils/auth.js
验证用户信息是否正确,同时作了相应的限制src/utils/auth.js#L20
import { parse } from 'qs'; import { message } from 'antd'; import { query, update, password } from '../services/auth'; import { getLocalStorage, setLocalStorage } from '../utils/helper'; export default { namespace: 'auth', state: { user: {}, isLogined: false, currentMenu: [], }, reducers: { querySuccess(state, action) { return { ...state, ...action.payload, isLogined: true }; }, }, effects: { *query({ payload }, { call, put }) { const { data } = yield call(query, parse(payload)); if (data && data.err_msg === 'SUCCESS') { setLocalStorage('user', data.data); yield put({ type: 'querySuccess', payload: { user: data.data, }, }); } }, } subscriptions: { setup({ dispatch }) { const data = getLocalStorage('user'); if (!data) { dispatch({ type: 'query', payload: {}, }); } else { dispatch({ type: 'querySuccess', payload: { user: data, }, }); } }, }, }
简单来讲,就是没有缓存的时候缓存。
例如,roles
中添加
和修改
功能都须要用到permissions
的数据,哪我怎么拿到最新的permissions
数据呢。首先,我在加载roles
列表页面时就须要将permissions
的数据缓存,这样,在每次点添加
或修改
功能时就不须要再去拉取已缓存的数据了。
在监听路由到roles
时查询permissions
是否缓存,将其更新到缓存中去。
. . . subscriptions: { setup({ dispatch, history }) { history.listen((location) => { const match = pathToRegexp('/roles').exec(location.pathname); if (match) { const data = getLocalStorage('permissions'); if (!data) { dispatch({ type: 'permissions/updateCache', }); } dispatch({ type: 'query', payload: location.query, }); } }); }, }, . . .
删除缓存的配置是比较灵活的,这里的业务场景并不复杂因此,我用了比较简单的处理方式。
参见src/models/permissions.js#L112
在执行新增或更新操做成功后,将本地原有的缓存删除。加上数据联动的特性,当再次回到roles
操做时,缓存已经更新了。
. . . *update({ payload }, { select, call, put }) { yield put({ type: 'hideModal' }); yield put({ type: 'showLoading' }); const id = yield select(({ permissions }) => permissions.currentItem.id); const newRole = { ...payload, id }; const { data } = yield call(update, newRole); if (data && data.err_msg === 'SUCCESS') { yield put({ type: 'updateSuccess', payload: newRole, }); localStorage.removeItem('permissions'); message.success('更新成功!'); } }, . . .
state的中的数据是变化的,刷新页面以后会重置掉,也能够将部分models
中的state
存到Localstorage
中,让state的数据从Localstorage
读取,但不是必要的。而list
数据的更新,是直接操做state中的数据的。
以下(这样就不用更新整个list的数据了)。
. . . grantSuccess(state, action) { const grantUser = action.payload; const newList = state.list.map((user) => { if (user.id === grantUser.id) { user.roles = grantUser.roles; return { ...user }; } return user; }); return { ...state, ...newList, loading: false }; }, . . .
Ant 提供的组件很是多,但用起来仍是须要一些学习成本的,同时多个组件组合使用时也须要有不少地方注意的。
在使用Modal组件时,不免会出现一个页面多个Modal的状况,首先要注意的就是Modal的命名,在多Modal状况下,命名不注意很容易出现分不清用的是哪一个Modal。建议命名时能望名知意。而后就是Modal须要用到别的Models的数据时,若是在弹窗时经过Ajax获取须要的数据再显示Modal,这样就会出现Modal延迟,并且Modal的动画也没法加载出来。因此,个人处理方式是,在进入这一级Route
的时候就将须要的数据预缓存
,这样调用时就可随用随取,不会出现延迟了。
参见src/components/user/UserModalGrant.jsx#L33
Ant的form组件很完善,须要注意的就是表单的多条件查询。若是单单是一个条件查询的处理比较简单,将查询关键词设成string
类型存到相应的Models中的state便可,多条件的话,稍微麻烦一点,需存成Hash对象。灵活处理便可。
官方文档的描述很清楚,我就不充大头了。注意写法规范便可,直接复制粘贴官方例子代码会很难看。
终于说到点子上了,先后端分离遇到跨域问题很正常,而这种基于RESTful API的先后端分离就更好弄了。我这以Fetch + PHP + Laravel为例,这种并非最有解决方案!仅供参考!
在header
中进行以下配置
Access-Control-Allow-Origin
配置容许的域
Access-Control-Allow-Methods
配置容许的请求方式
Access-Control-Allow-Headers
配置容许的请求头
<?php use Illuminate\Http\Request; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- | | Here is where you can register API routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | is assigned the "api" middleware group. Enjoy building your API! | */ Route::group(['middleware'=> ['auth:api']], function() { header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE"); header("Access-Control-Allow-Headers: Access-Control-Allow-Headers, Origin, Accept, Authorization, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers"); require base_path('routes/common.php'); });
基于其余编程语言的处理相似。
了解前端、熟悉前端、精通前端、熟悉前端、不懂前端
了解 X X 、熟悉 X X 、精通 X X 、熟悉 X X 、不懂 X X