源站链接
继 这篇文章被喷之后,笔者很长一段时候没有上社区逛了。现在 tkvern 又回归了,给大家带来 React 实践的一些经验,一些踩坑的经验。
Rails 嘛,很好用,Laravel 也好用。Phoenix 也好用。都好,哪个方便用哪个。
还有关于 之争,不能单从页面渲染时间去对比,要综合考虑。
Dva 是基于 Redux 做了一层封装,对于 React 的 state 管理,有很多方案,我选择了轻量、简单的 Dva。至于 Mobx,还没应用到项目中来。先等友军踩踩坑,再往里面跳。
顺便贴下 Dva 的特性:
,
- reducers
和
- effects
组织 model
- subscriptions
做为传道士,这么好的 UI 设计语言,肯定不会藏着掖着啦。蚂蚁金服的东西,确实不错,除了 Ant Design 外,还有 Ant Design Mobile、AntV、AntMotion、G2。
太慢,试试 吧。建议用
- npm install
进行安装。
- npm install yarn -g
项目开始了,前端视图写完,要开始数据交互了,后端提供的 API 还没好。
那么问题来了,如何在不依靠后端提供 API 的情况下,实现数据交互?
使用 可以解决这个问题。先对接好 API 数据格式,然后使用 Mockjs 拦截 Ajax 请求,模拟后端真实数据。
在 Mockjs 官方提供的 API 不够用的情况下,还可以使用正则产生模拟数据。
这里给出一个模拟用户数据并持久化的实例实例:
代码摘要:
- '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\"",
完成这些基本工作就做好了
在模拟数据环境,
下的模块这么写就好了,真实 API 则替换为真实 API 的地址。可将地址前缀写到统一配置中去。
- services
- 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 参考实例:
在看 dva 的引导手册时,并没有介绍登录相关的内容。因为不同的项目,对于登录这块的实现会有所不同,并不是唯一的。通常我们会使用 Cookie 的方式保持登录状态,或者 Auth 2.0 的技术。
这里介绍 Cookie 的方式。
登录成功之后服务器会设置一个当前域可以使用的 Cookie,例如
啥的。然后在每次数据请求的时候在
- token
中携带
- Request Headers
,后端会基于这个
- token
进行权限验证。思路清晰了,来看看具体实现吧。(注:在这次项目中使用了统一登录模块,通过 Header 中的
- token
进行验证,将只介绍拿到
- 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 的预处理我放在了 ,这里后端返回的数据都是 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
- 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
单独验证。其实这种基于客户端渲染的应用,如果页面限制有遗漏也关系不太,后端提供的 API 会对数据进行验证,即使前端访问到没有权限的页面,也同样不用担心,做好客户端错误处理即可。
- Route
对于一个 React 应用来说,缓存是很重要的一步。前后端分离后,频繁的 Ajax 请求会消耗大量的服务器资源,如果一些不长变动的持久化数据不做缓存的话,会浪费许多资源。所以,比较常见的方法就是将数据缓存在
中。针对一些敏感信息可适当进行加密混淆处理,我这里就不介绍了。
- LocalStorage
例:用户信息缓存
参见
在
中配置了
- subscriptions
检测
- setup
中的
- LocalStorage
是否存在。不存在时会去
- user
用户信息,然后保存到
- query
中,如果存在就将
- user
中的数据添加到
- user
的
- state
中。当然在进行请求时,已经在
- user: {}
验证用户信息是否正确,同时做了相应的限制
- src/utils/auth.js
- 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,
- });
- }
- });
- },
- },
- .
- .
- .
删除缓存的配置是比较灵活的,这里的业务场景并不复杂所以,我用了比较简单的处理方式。
参见
在执行新增或更新操作成功后,将本地原有的缓存删除。加上数据联动的特性,当再次回到
操作时,缓存已经更新了。
- 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
中,让 state 的数据从
- Localstorage
读取,但不是必要的。而
- Localstorage
数据的更新,是直接操作 state 中的数据的。
- list
如下 (这样就不用更新整个 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
,这样调用时就可随用随取,不会出现延迟了。
- 预缓存
参见
Ant 的 form 组件很完善,需要注意的就是表单的多条件查询。如果单单是一个条件查询的处理比较简单,将查询关键词设成
类型存到相应的 Models 中的 state 即可,多条件的话,稍微麻烦一点,需存成 Hash 对象。灵活处理即可。
- string
官方文档的描述很清楚,我就不充大头了。注意写法规范即可,直接复制粘贴官方例子代码会很难看。
终于说到点子上了,前后端分离遇到跨域问题很正常,而这种基于 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
来源: