技术栈
- react16
- react-router5.0
- mobx4.0
- antd1.0
- axios(资源请求)
项目介绍
一个toB的智能制造项目,分为分析端和管理端。分析端涉及到各种图表展示,通过时间范围来控制显示内容;管理端主要是大量表单&表格。(第一次正经用react进行开发,学习了一个星期就开工咯( ╯□╰ ))
架构
- create-react-app搭建整体框架
- react-app-rewired进行自定义配置(需要安装customize-cra,并在根目录建一个config_override.js用于修改默认配置)
- import react-router mobx并做好相应配置
- github地址:github.com/fanxueqin/r…
- (可以download到本地,install依赖,然后直接启动。我把业务代码抹掉了,这是一个比较完整的框架,可以直接进行业务开发)
项目目录overview
自定义配置——config-overrides.js
const path = require('path');
const { override, fixBabelImports, addLessLoader, addWebpackAlias, babelInclude ,useBabelRc } = require('customize-cra');
const TerserPlugin = require('terser-webpack-plugin');
function resolve(dir) {
return path.join(__dirname, '.', dir)
}
let addCustom = () => config => { //屏蔽.map.js文件,防止被读到源码
let optimization = {
minimizer: [
new TerserPlugin({
sourceMap: false
})
]
}
config.optimization.minimizer = optimization.minimizer;
return config;
}
module.exports = override(
fixBabelImports('import', { //按需引入antd
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: {
// '@primary-color': '#1DA57A' ,
// '@link-color': '#1DA57A',
},
}),
addWebpackAlias({ //添加别名
'@': resolve('src'),
'components':path.resolve(__dirname,'src/components'),
'views': path.resolve(__dirname,'src/views'),
'layout': path.resolve(__dirname,'src/layout'),
'router': path.resolve(__dirname,'src/router'),
'api':path.resolve(__dirname,'src/api'),
'store': path.resolve(__dirname,'src/store'),
'assets': path.resolve(__dirname,'src/assets,'),
'mock': path.resolve(__dirname,'src/mock'),
'utils': path.resolve(__dirname,'src/utils')
}),
babelInclude([
path.resolve("src"),
]),
useBabelRc(), //配置装饰器(mobx会用到)还需要.babelrc文件配合
addCustom()
);
.babelrc配置装饰器
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
setupProxy.js设置代理——前后端分离
const proxy = require('http-proxy-middleware');
const target = 'http://123.4.5.6:8080';
module.exports = function(app) {
app.use(proxy('/user', { target }))
app.use(proxy('/login', { target }))
app.use(proxy('/show', { target }))
app.use(proxy('/back', { target })))
};
配置axios拦截器--aixos.js
import axios from "axios";
import qs from "qs"; //post请求时序列化
import { notification } from 'antd';
// http请求拦截器
axios.interceptors.request.use(
config => {
if (config.method.toUpperCase() === "GET") {
config.url =
config.url.indexOf("?") > 0
? config.url + "&clearCache=" + new Date().valueOf()
: config.url + "?clearCache=" + new Date().valueOf();
}
if (config.method.toUpperCase() === "POST") {
if (Object.prototype.toString.call(config.data) === "[object FormData]") {
console.log("数据类型", Object.prototype.toString.call(config.data));
} else {
config.data = qs.stringify(config.data); //序列化
config.headers["Content-Type"] = "application/x-www-form-urlencoded";
}
}
config.headers["Authorization"] = window.localStorage.getItem("token") ? window.localStorage.getItem("token") : '';
return config;
},
error => {
return Promise.reject(error);
}
);
// http响应拦截器
let loginTipLock = false;
axios.interceptors.response.use(
data => {
if (data.data["code"] && data.data["code"] === -2) {
window.location.hash = "#/login";
}
window.localStorage.setItem('token', data.headers.authorization) //token刷新机制
return data;
},
err => {
if (err && err.response) {
switch (err.response.status) {
case 400:
err.message = "请求错误";
break;
case 401:
err.message = "登录已过期,请重新登录!";
window.location.hash = "#/login"; //token过期机制
break;
case 403:
err.message = "拒绝访问";
window.location.hash = "#/notAuth";
break;
case 404:
err.message = `请求地址出错: ${err.response.config.url}`;
break;
case 408:
err.message = "请求超时";
break;
case 500:
err.message = "服务器内部错误";
break;
case 501:
err.message = "服务未实现";
break;
case 502:
err.message = "网关错误";
break;
case 503:
err.message = "服务不可用";
break;
case 504:
err.message = "网关超时";
break;
case 505:
err.message = "HTTP版本不受支持";
break;
default:
}
}
if (err.response.status === 401) {
if (!loginTipLock) { //避免同时多个请求都返回401时,弹出多个“未登录”提示框
loginTipLock = true;
notification.info({
message: '提示',
description: err.message
})
setTimeout(function () {
loginTipLock = false;
}, 1000)
}
} else {
notification.error({
message: '出错啦',
description: err.message
});
}
return Promise.reject(err);
}
);
export default axios;
----对请求结果做一个统一处理----
import axios from "./axios";
import { notification } from 'antd';
const $http = (url, method = "GET", data, config = {}) => {
const _config = Object.assign({ url, method, data }, config);
return axios(_config).then(res => {
if (res.status === 200) {
if (res.data.code === -1) {
notification.error({
message: '出错啦',
description: res.data.msg
});
throw new Error("请求出错啦");
}
return res;
}
});
};
export default {
/*注册*/
signIn: ({ username, password }) => $http(`/user/register`, "POST", { username,password }),
/*登录 */
login: ({ username, password }) => $http("/user/login", "POST", { username, password }),
/*上传文件 */
uploadFile: file => $http( "/upload/csv", "POST", {}, {headers: { "Content-Type": "multipart/form-data" }, processData: false, cache: false, data: file }),
}
开发过程中的小知识(包含新get & 需要加强的)
1.用layout文件划分项目区块
- 按这个项目的需求来说,我将项目分割为“分析模块”、“管理模块”、“用户配置模块(包- 括登陆注册)”、“license管理模块”
- 每一个模块的布局都是用共通性的,所以可以把每个模块内公用部分抽离成可复用的组件,比如说header、footer、Nav
- 为每一个模块建立一个layout.js文件,内容包括模块内公共组件引用,和模块内子路由配置
- 然后在App.js里面引入这模块文件的布局文件
2.路由鉴权组件
/* 定义 */
import React, { Component } from 'react'
import { withRouter } from 'react-router'
import { Route, Redirect } from 'react-router-dom'
import { inject, observer } from 'mobx-react';
@inject('appState')
@observer
class AuthorizedRoute extends Component {
render() {
const { component: Component, ...rest } = this.props;
const isLogin = !!JSON.parse(window.localStorage.getItem("userInfo"));
const userRole = JSON.parse(window.localStorage.getItem("userInfo")).role : 'user';
const path = this.props.path.substring(1);
const { authList } = this.props.appState;
return (
<Route {...rest} render={props => {
return isLogin
? (authList[path].includes(userRole) ? <Component {...props} /> : <Redirect to="/notAuth" />)
: <Redirect to="/login" />
}} />
)
}
}
export default withRouter(AuthorizedRoute);
/* 引用 */
const AuthorizedRoute = lazy( () => import('router/auth')); //react懒加载
class App extends Component{
render(){
return (
<div className="App">
<HashRouter>
<Suspense fallback={PageLoading}> //在Suspense组件中渲染lazy组件,我们可以在等待加载lazy组件时做优雅降级(如loading指示器)
<Switch>
<Redirect path='/' exact to="/show" />
<AuthorizedRoute path="/show" component={ShowHomeLayout} />
<AuthorizedRoute path="/back" component={BackHomeLayout} />
<Route path="/login" component={Login}></Route>
<Route path="/notAuth" component={NotAuth}></Route>
<Route component={NotFound} />
</Switch>
</Suspense>
</HashRouter>
</div>
);
}
}
export default App;
3.路由带参数(/:id)
如果整个页面的数据依赖一个id,那么最好把id作为路由的参数。
原因:
1.要考虑用户复制当前url在新窗口打开的情况
2.要考虑用户刷新页面的情况
一个新的问题:
路由带参数会有一个极端情况,就是用户在这个导航上再点击一下,参数就会变成id= 'undefined'
(注: 路由参数会转变成字符串型)
解决方法:
if (_routerParam['id'] === 'undefined' || _routerParam['id'] === undefined) {
//重新获取id相关数据
}
4.canvas导出图片模糊
方法: canvas.toDataURL('image/jepg',1),这种jepg格式可以设置图片质量,将质量设置为1,可以变清晰。(虽然可以选择以jpg or png格式导出,但实际上都是jpeg格式,目前还没找到更好的办法( ╯□╰ ))
引用canvas2image.js,可在github上找到。
function getDataURL(canvas, type, width, height) {
canvas = scaleCanvas(canvas, width, height);
//return canvas.toDataURL(type,1);原
return canvas.toDataURL('image/jpeg', 1);//改
}
5.antd-menu组件要保留“展开”和“选中”状态
-
这一块处理起来比较麻烦,要保证刷新,新窗口打开都不出问题;另一方面,还要考虑折叠之后、折叠又展开的状态。
-
思路: 将展开的submenu`s key存在sessionStorage;下次进入再取出;另外注意点击submenu标题时,做去重处理;另外注意折叠之后,子菜单的css。
selectedKeys: [], //表示当前选中menu-item opendKeys: [], //表示当前展开的submenu
6.对所有可能会超长的文字做溢出省略操作
给这些需要溢出省略的,赋一个class,需要的带上这个class,并将相同的文字内容,赋给title属性
如:
.sampleNameCon{
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
white-space: nowrap;
}
7.日期范围统一处理
在这个项目中,有许多类似这样的控制按钮。
其实每一个button最后都会生成一个时间范围: start:xxx-xxxx end: xxx-xxx封装一下:
import moment from 'moment';
const culateTimeRange = function (val, unit) { //数值(1),单位(day)
let _timeRange = {}
_timeRange['start'] = moment().subtract(unit, val).format('YYYY-MM-DD HH:mm:ss');
_timeRange['end'] = moment().format('YYYY-MM-DD HH:mm:ss');
return _timeRange;
}
export default culateTimeRange;
8.导出文件
将后端返回的数组导出为表(csv),用到一个库叫:saveAs
var file = new Blob(['\uFEFF' + res.data]); //\uFFEF是为了避免发生导出文件乱码现象
saveAs(file, `name.csv`);
9.echarts表单统一处理
因为本项目会有多处的折线图表展示,所以考虑将该部分抽象成一个组件。 用到这个库—— import ReactEcharts from "echarts-for-react"; 有几个点:
- 当数据为null,折线会自然断开
- 会根据图中数据类型不同(这里表现会单位),做数据溢出处理。对某一种数据,范围0-100,溢出显示0 or 100,但是hover上去的详情要显示真实数据。
- 数据视图(静态显示数据)是一个很好的功能,但是样式比较丑,我们可以把它包装成表格。还要注意数据视图的top值,不要露出legend。
10.函数
- 在react中,我们在render的return中进行函数绑定时,最好不要在jsx中写箭头函数。因为这相当于定义了一个匿名函数,每一次render都要去定义一次,开销较大。
- 在render之外,为了避免this指代错误,最好都以箭头函数的方式写方法
11.react中类似于vue的watch
习惯在vue中用watch,刚从vue迁移到react很不适应。
可以用react的static getDerivedStateFromProps(nextProps,nextState) 和 componentDidUpdate(prevProps,prevState)配合,实现watch的功能。
12.性能优化相关
-
代码分割react-loadable
import Loadable from 'react-loadable';
<!--加载中效果-->
const PageLoading = ({ isLoading, error }) => {
if (isLoading) {
return <Spin
className="pageLoading"
size="large"
spinning={true}
/>
} else if (error) {
return <div className="pageLoadingError">资源加载失败!</div>
} else {
return null
}
}
<!--封装一下-->
const loadComponent = (loader,loading = PageLoading) =>{
return Loadable({
loader,
loading
})
}
// 路由
const home = loadComponent(() => import('views/show/home'))
export default{
home
}
-
代码压缩
我们用webpack已经将代码压缩过了,但是如果开启gzip压缩,可以再压缩一半。
开启gzip需要前后端一起配合。
前端RequeshHeaders开启—— Accept-Encoding: gzip, deflate
如果后端开启的gzip,可以在Response-Headers中——Content-Encoding: gzip
-
图片压缩
可以用tinyPNG对图片进行压缩,当然按照业务场景可以进行按需加载和雪碧图
不考虑兼容性的话,推荐用谷歌新出的webp格式的图片,小而美
总结
第一次用react进行项目开发,学习时间很短,甚至react官网文档我都没读完。从框架搭建到一些详细的业务部分,这只是大概,我会抽空写个更详细的业务版。