一个toB的智能制造项目,分为分析端和管理端。分析端涉及到各类图表展现,经过时间范围来控制显示内容;管理端主要是大量表单&表格。(第一次正经用react进行开发,学习了一个星期就开工咯( ╯□╰ ))javascript
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()
);
复制代码
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
复制代码
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 })))
};
复制代码
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 }),
}
复制代码
/* 定义 */
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;
复制代码
若是整个页面的数据依赖一个id,那么最好把id做为路由的参数。css
缘由:
1.要考虑用户复制当前url在新窗口打开的状况
2.要考虑用户刷新页面的状况
一个新的问题:
路由带参数会有一个极端状况,就是用户在这个导航上再点击一下,参数就会变成id= 'undefined'
(注: 路由参数会转变成字符串型)
解决方法:
if (_routerParam['id'] === 'undefined' || _routerParam['id'] === undefined) {
//从新获取id相关数据
}
复制代码
方法: 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);//改
}
复制代码
这一块处理起来比较麻烦,要保证刷新,新窗口打开都不出问题;另外一方面,还要考虑折叠以后、折叠又展开的状态。vue
思路: 将展开的submenu`s key存在sessionStorage;下次进入再取出;另外注意点击submenu标题时,作去重处理;另外注意折叠以后,子菜单的css。java
selectedKeys: [], //表示当前选中menu-item opendKeys: [], //表示当前展开的submenureact
给这些须要溢出省略的,赋一个class,须要的带上这个class,并将相同的文字内容,赋给title属性
如:
.sampleNameCon{
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
white-space: nowrap;
}
复制代码
在这个项目中,有许多相似这样的控制按钮。webpack
封装一下:
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;
复制代码
将后端返回的数组导出为表(csv),用到一个库叫:saveAsios
var file = new Blob(['\uFEFF' + res.data]); //\uFFEF是为了不发生导出文件乱码现象
saveAs(file, `name.csv`);
复制代码
由于本项目会有多处的折线图表展现,因此考虑将该部分抽象成一个组件。 用到这个库—— import ReactEcharts from "echarts-for-react"; 有几个点:git
习惯在vue中用watch,刚从vue迁移到react很不适应。github
能够用react的static getDerivedStateFromProps(nextProps,nextState) 和 componentDidUpdate(prevProps,prevState)配合,实现watch的功能。
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官网文档我都没读完。从框架搭建到一些详细的业务部分,这只是大概,我会抽空写个更详细的业务版。