前言书明观念

从第一代码农写下第一行代码开始到上个世纪的80年代的软件危机,码农一直在考虑一个问题,怎么让代码写起来更容易、更简单、更舒适?抛开大牛、大神(大牛、大神哪那么容易找到啊 _…)级别的人员,而且大厂工作,讲究的是一个协同合作开发,代码不是你想怎么写就怎么写的,自然而然就需要形成一套统一的协同方案及规范开发,这样的好处:一是方便管理(如成员变动),二是减少运维成本,三是提高开发质量,四是起到一个学习共勉,五是作为学习开发的参考

正所谓:“新人入团队,什么都不会” 的思想主导着老员工的心态,这种心态是可以理解的,这里没有贬低新人啊,初入团队的成员多少都有几天空白期,多少需要几天去适应,不管是初出 “校庐”,还是进军市场几年的老战士,几乎所有的码农在写代码的时候,都是以自我为思想的,什么都是按着自己的标准,这样的方式故能顺利完成业务的开发,但不利于整个团队开发,其他成员对其设计思想并不了解,在此声明一下,本文并没有贬低任何人员,只是基于个人工作经历,发现的一些开发弊端,给予抒出而已,提供处理这些弊端的一种方式之一,也用于自己在后续开发工作中做个比较

团队协作是一件非常严谨的工作,也是一个技术团队发展壮大的基石,当然也是最难的,可持久发展的,不是一蹴而就能形成的,成员之间的完美协作最起码需要半年到一年的工作沉淀(企业对开发者工作贡献能力的尺量),对于代码组织者就需要对这样的工作量化,提供一套技术方案思想,适用于企业开发前端主管职务人员参考

新人入团队,什么都不会

此处并不是贬低之意,实在是新人的进入多少有个适用团队的周期,进入新公司,融入新团队,就应当要去适应新团队的开发模式,不可能让所有人去适应你吧 ^_^...

市场上,80%~90%的码农写的代码都是 Cow Code(牛仔代码),尤以外包公司出身的码农,其特别的 CV 法则(Ctrl+C、Ctrl+V),从来不讲究代码风格,甚至有的时候,一个月前我写的代码,一个月后回过头来看(脑补:WC,这那个 SB 写的代码),只有上帝知道他写的是什么,这个问题在前端领域最为突出:


  1. 技术团队对新型业务领域没多少开发经验;
  2. html/js/css 发明出来的时候完全是玩玩而已,没有成熟的技术栈(PM:对着文档就能写的东西);
  3. js 是单线程的,css 是全局的,几个人一起搞一块业务(XX 开发者:尼玛,谁改了我的样式);

很早就有人来想办法解决这个问题,在软代时代就已经有解决这个问题的法宝 ——— 组件化,当然那时候不是那么叫的,是通过两个原则来规范这个问题的,这两个原则就是:内聚性和耦合性

意思就是:哥,我想按时回家哄妹子!!!你怎么写代码我不管,你的功能全在这你这儿实现(内聚性),不要让我还帮写你那块功能;另外,哥,求你了,你代码不要 block (影响)我的代码(低耦合性)

既然解决问题的思路在这儿,前端大牛一代代前赴后继的在这条路上狂奔下去,这也是前端组件化开发的前因,至于何为组件化,观字译意,就是将整体划分,按一块一块封装,组件就是一个个独立携带功能单元块,当然,在 react 中组件化就比较宏观啦,因为 react 的思想是万物皆组件,任何事物都可以看成是一个组件,组件化的好处:解耦,平台化,结构单一,复用性,编译集成

高内聚,低耦合

高内聚低耦合是软件工程中的一种概念,是判断软件设计好坏的标准,主要用于程序的面向对象而设计的,观察其内聚性是否过高,耦合度是否过低,目的是使程序模块的可重用性、移植性大大增强

高内聚:尽可能让每个组件内部完成单一事件;低耦合:减少组件内部调用另外一个组件的事件,高内聚低耦合可以使得项目可拓展性、可移植性在技术层面上能够更加的灵活,最大化重用组件,减少开发者 UI 层的开发工作,集中精力完成业务逻辑的设计及实现

前端中的组件

前端的组件是前端页面的一部分,由 HTML、CSS、JavaScript 三种编程语言组合而成,相对于面向对象思想 OOP 中的对象对比,前端组件的语义要素会更丰富一点,前端组件中的要素有:属性 Properties、状态 State、方法 Methods、继承 Inherit、特性 Attribute、配置 Config、事件 Event、生命周期 Leftcycle、子组件 Children

PropertiesAttribute 在英文翻译都是 “属性” 的意思,在前端组件中,Properties 是组件具备的属性,而 Attribute 通常用于 HTML DOM 的属性;State 可以看成 JavaScript 声明的变量,与 Properties 不同的是,变量是可以进行赋值与计算的,而属性是不变的,可以将 Properties 看成 JavaScript 声明的常量

Config 配置也可以看成 JavaScript 构造函数的参数,是组件中一个一次性生效的数据,不可以被更改

标签设置 代码设置 代码改变 用户输入改变 是否支持
N Y Y property
Y Y Y attribute
N N N Y state
N Y N N config

MethodsEvent 同样是方法动作,在前端组件中,Methods 可以看成 JavaScript 声明的方法,而 EventHTML DOM 节点事件,Leftcycle 是用于观测组件引用过程中的状态:创建 create、挂载 mount、数据更新 update、销毁 destroy 时进行的回调事件

Children 是组件的内容部分,通常也被看成是组件,结合 Inherit 可以继承父组件的属性 Properties 和方法 Methods

  • Content 型 Children:多个 Children 子组件,有几个展示几个
  • Template 型 Children:整个是一个模板,包含多层后代组件

前端框架架构

组合键 Window + R 输入 cmd 回车,选择非 C 盘 下找到一个项目目录,例:E:\wwwroot 目录下执行如下命令,通过 react-cli 脚手架新建项目,并同步加载好相关依赖,最后运行项目:

E:\wwwroot>npx create-react-app react-demo
E:\wwwroot>cd react-demo
E:\wwwroot>react-demo>npm start

当你看到运行如下效果,则表示项目运行成功啦,浏览器自动运行:http://localhost:3000 就可以打开 react-demo

|-- react-demo|-- node_modules                   react 项目所需要的相关依赖包|-- public                         react 项目入口资源文件|-- src                               react 项目入口源码文件|-- .eslintcache                  react 语法规则配置缓存|-- .gitignore                        git 忽略文件配置项|-- package-lock.json                react 所有依赖指引锁定配置|-- package.json                    react 所有依赖指引配置|-- README.md                     react 项目文档

通过编辑器(推荐 VS Code)打开项目,先调整项目 src 目录内容,删除 App.css、index.css、App.js、App.test.js、logo.svg、reportWebVitals.js、setupTests.js,并改造 index.js 内代码如下:

import React from 'react';
import ReactDOM from 'react-dom';ReactDOM.render(<h1>Hello React!</h1>,document.getElementById('root')
);

由于 create-react-app 创建的 react 应用是将 webpack 的配置项隐藏的,可以通过 npm run eject 命令将所有内建的配置暴露出来,选择 y (yes) 继续,如果报出 npm 关联错误,记得到项目的根目录,删除项目中的 .git 隐藏文件夹,配置暴露后的项目结构:

|-- react-demo|-- config                         react 项目 webpack 相关配置文件|-- node_modules                 react 项目所需要的相关依赖包|-- public                         react 项目入口资源文件|-- scripts                           react 项目启动脚本文件|-- src                               react 项目入口源码文件|-- .eslintcache                  react 语法规则配置缓存|-- .gitignore                        git 忽略文件配置项|-- package-lock.json                react 所有依赖指引锁定配置|-- package.json                    react 所有依赖指引配置|-- README.md                     react 项目文档

集成 Antd 组件库

通过指令 npm install antd --save 安装 Antd 组件库,并在根入口引入 Antd 样式表,在 App.js 内引用 Antd 相关组件,并运行

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import 'antd/dist/antd.css';
import App from './App';
import reportWebVitals from './reportWebVitals';ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById('root')
);
reportWebVitals();
import { Button } from 'antd';
import './App.css';function App() {return (<div className="App"><Button type="primary">Primary Button</Button><Button>Default Button</Button><Button type="dashed">Dashed Button</Button><br /><Button type="text">Text Button</Button><Button type="link">Link Button</Button></div>);
}export default App;

PS:在使用 antd 组件库之前请落实产品交互,因为 antd 目前的两个版本:3.x 和 4.x 有些功能迭代并不同步,想好再选择

自定义主题色

根据文档说明,用户自定义 antd 主题色需要引用 cracocraco-less 两个插件,基于 less-loadermodifyVars 来进行主题配置,变量和其他配置方式可以参考如下:

@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
@border-radius-base: 2px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影

虽然 antd 提供了一套非常优秀的主题色,但往往不敬人意的时候,企业往往想的是自个独有的色调,这就需要开发者们对项目框架进行主题配置,前提还需要将引用 antd.css 方式调整为 antd.less,调整项目 src 目录:

E:\wwwroot>react-demo>npm install @craco/craco --save-dev
E:\wwwroot>react-demo>npm install craco-less --save-dev
|-- src|-- App.js                        // 调整引用相对应的 Less 文件|-- App.less|-- index.js                 // 调整引用相对应的 Less 文件|-- index.less|-- reportWebVitals.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.less';
import Root from './root';
import reportWebVitals from './reportWebVitals';
import './index.less';ReactDOM.render(<Root />, document.getElementById('root'));
reportWebVitals();
import { Button } from 'antd';
import './App.less';function App() {return (<div className="App"><Button type="primary">Primary Button</Button><Button>Default Button</Button><Button type="dashed">Dashed Button</Button><br /><Button type="text">Text Button</Button><Button type="link">Link Button</Button></div>);
}export default App;

在项目根目录下新建 craco.config.js 配置文件,修改项目配置文件 package.json

const CracoLessPlugin = require('craco-less');module.exports = {plugins: [{plugin: CracoLessPlugin,options: {lessLoaderOptions: {lessOptions: {modifyVars: {'@primary-color': '#1DA57A'     // 这里修改了主题色},javascriptEnabled: true,},},},},],
};
{"name": "react-demo",......"scripts": {"start": "craco start","build": "craco build","test": "craco test","eject": "craco eject"},......"devDependencies": {"@craco/craco": "^6.4.3","craco-less": "^2.0.0"}
}

PS:craco-less 非常方便的集成 less,当然也可以通过 npm run eject 暴露的 webpack 配置修改

集成路由、状态管理

E:\wwwroot>npm install react-router-dom@5.x --save
E:\wwwroot>npm install redux --save
E:\wwwroot>npm install --save react-redux
E:\wwwroot>npm install redux-saga --save

PS:需要说明的是 react-router-dom 从 5.x 升级到 6.x+ 后,Switch 重命名为 Routes,component/render 被 element 替代

调整项目 src 结构,优化文件管理,规范化命名,便于项目管理:

|-- src|-- assets                            # 存放项目相关静态资源,如:图片|-- components                       # 存放项目通用组件|-- mock                          # 暴露出项目所需的模拟数据|-- pages                         # 项目的主页面:布局、404、登陆|-- IndexPage                  # 项目的布局页面,通常引用 antd 的布局组件|-- InvalPage                   # 项目的意外页面,不匹配相关路由的展示页|-- LoginPage                   # 项目的登陆页面,有些项目登陆放在布局头部|-- panes                          # 存放项目布局用到的版面组件|-- redux                            # 项目状态管理目录:管理项目的整体状态|-- utils                            # 项目工具集|-- index.js                     # 项目主要入口文件|-- index.less                        # 项目全局样式表|-- reportWebVitals.js             # web-vitals的库|-- root.js                           # 项目容器组件文件|-- route.js                      # 项目的路由配置

分别在 IndexPageInvalPageLoginPage 下创建对应的 index.jsindex.less 文件,编辑内容如下:

import './index.less';
function IndexPage() {return (<div>布局组件</div>)
}
export default IndexPage;
import './index.less';
function InvalPage() {return (<div>404组件</div>)
}
export default InvalPage;
import './index.less';
function LoginPage() {return (<div>登录组件</div>)
}
export default LoginPage;

编辑 root.js 路由根文件,引入相关页面,基于 react-router-dom 路由对象进行路由配置

import { BrowserRouter as Router, Route } from 'react-router-dom';
import IndexPage from './pages/IndexPage';
import InvalPage from './pages/InvalPage';
import LoginPage from './pages/LoginPage';
function Root() {return (<Router><Route exact path="/login" component={LoginPage} /><Route exact path="/" component={IndexPage} /><Route exact path='*' component={InvalPage} /></Router>);
}
export default Root;

这个时候,可以通过 http://localhost:3000/ 和 http://localhost:3000/login 分别访问到首页和登录页面啦,匹配不到路由的展示 404

在项目 src/redux 下创建:store.jsreducer.jssaga.js 编辑如下,并调整路由根组件 root.js

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer';export default function configureStore(state) {const sagaMiddleware = createSagaMiddleware();const createStoreWithMiddleware = applyMiddleware(sagaMiddleware)(createStore);return {...createStoreWithMiddleware(reducer, state),runSaga: sagaMiddleware.run};
}
import { combineReducers } from 'redux';export default combineReducers({});
import { all } from 'redux-saga/effects';export default function* rootSaga() {yield all([]);
}
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import Store from './redux/store';
import Sagas from './redux/saga';
import IndexPage from './pages/IndexPage';
import InvalPage from './pages/InvalPage';
import LoginPage from './pages/LoginPage';const store = Store();
store.runSaga(Sagas);function Root() {return (<Provider store={store}><Router><Switch><Route exact path="/login" component={LoginPage} /><Route exact path="/" component={IndexPage} /><Route exact path='*' component={InvalPage} /></Switch></Router></Provider>);
}export default Root;

完善项目的登陆页面 src/pages/LoginPage,创建:index.less 样式、state.js 状态、action.js 执行、reducer.js 迭代、saga.js 监听、server.js 服务、handle.js 交互

引用 antd 表单组件创建一个登陆表单,编辑项目 src/pages/LoginPage/index.js 如下:

import { Form, Input, Button } from 'antd';
import { connect } from 'react-redux';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import './index.less';
import * as handle from './handle';function LoginPage(props) {const [formData] = Form.useForm();const handPush = values => {handle.handPush(props, values, result => {console.log(result);   });};return (<ConfigProvider locale={zhCN}><FormlabelCol={{ span: 6, }} wrapperCol={{ span: 10 }} autoComplete="off"form={formData} onFinish={handPush} style={{ width: '800px', marginTop: '40px' }}><Form.Item label="Username" name="username" rules={[ { required: true, message: 'Please input your username!' } ]}><Input placeholder='Please input your username!' /></Form.Item><Form.Item label="Password" name="password" rules={[ { required: true, message: 'Please input your password!' } ]}><Input.Password placeholder='Please input your password!' /></Form.Item><Form.Item wrapperCol={{ offset: 8, span: 16 }}><Button type="primary" htmlType="submit">Submit</Button></Form.Item></Form></ConfigProvider>)
}
export default connect(({ login }) => ({}))(LoginPage);;

表单提交的时候,需要执行一个事件 handPush,所以需要在 handle.js 声明这个事件,集中处理,逻辑清晰

import { FETCH_LOGIN_PUSH } from './action';// 监听页面点击事件,去找到需要执行的方法,返回执行结果
export function handPush(props, data, callback) { props.dispatch({ type: FETCH_LOGIN_PUSH, payload: data, callback }) }

在调用事件执行方法时,声明执行体 action.js,声明 ATION_ALL_TYPE 用于执行更新状态

export const FETCH_LOGIN_PUSH = 'FETCH_LOGIN_PUSH';     // 监听初始化用户登陆请求行为export const ATION_ALL_TYPE = {};

通过迭代来更新状态,声明迭代体 reducer.js,映射到 saga.js 监听,在状态管理 src/redux 中需要引用

import { ATION_ALL_TYPE } from './action';
import initState from './state';export default (state = initState, action) => {if (Object.prototype.toString.call(ATION_ALL_TYPE[action.type]) === '[object Function]') {return ATION_ALL_TYPE[action.type](state, action.payload);} return state;
};
import { combineReducers } from 'redux';
import login from '../pages/LoginPage/reducer';export default combineReducers({login,
});

声明监听器 saga.js,调用后端服务接口,回调返回登陆结果,在状态管理 src/redux 中需要引用

import { take, fork, call, put } from 'redux-saga/effects';
import { FETCH_LOGIN_PUSH } from './action';
import { sendLogin } from './server';
import { message } from 'antd';// 监听页面用户表单提交,执行用户登陆接口,返回登陆结果
function* fetchLoginPush() {while (true) {const { payload, callback } = yield take(FETCH_LOGIN_PUSH);const response = yield call(sendLogin, payload);response.code === 200 && callback && callback(response);response.code !== 200 && message.error(response.msg);}
}export default [fork(fetchLoginPush),
];
import { all } from 'redux-saga/effects';
import WatchLoginModal from '../pages/LoginPage/saga';export default function* rootSaga() {yield all([...WatchLoginModal, ]);
}

声明用户登陆接口服务 server.js,引用集成封装请求方法和配置常量

import { request } from '../../utils/request';
import { API } from '../../utils/constant';/*** 异步调用后端声明接口:表单提交执行用户登陆* @returns */
export async function sendLogin(params) {return await request(`${API}/user/login`, { method: 'POST', dataType: 'json', params });
}

分别在项目的工具集 src/utils 下创建 request.jsconstant.jslodash.jsstorage.js

import { notification } from 'antd';
import { queryStringify } from './lodash';
import { ReadUseToken } from './storage';// 声明一个请求错误机制,用于请求异常通知提醒
const codeMessage = {200: '服务器成功返回请求的数据。',201: '新建或修改数据成功。',202: '一个请求已经进入后台排队(异步任务)。',204: '删除数据成功。',400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',401: '用户没有权限(令牌、用户名、密码错误)。',403: '用户得到授权,但是访问是被禁止的。',404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',406: '请求的格式不可得。',410: '请求的资源被永久删除,且不会再得到的。',422: '当创建一个对象时,发生一个验证错误。',500: '服务器发生错误,请检查服务器。',502: '网关错误。',503: '服务不可用,服务器暂时过载或维护。',504: '网关超时。',
};/*** 请求异常,服务器或网关异常导致的异常,进行异常通知* @param {*} error           fetch 请求异常错误*/
function errorHandler(error) {const { response } = error;// 根据异常错误码匹配异常错误信息,并在客户端进行通知操作if (response && response.status) {const errorText = codeMessage[response.status] || response.statusText;const { status, url } = response;notification.error({message: `请求错误 ${status}: ${url}`,description: errorText,});} else if (!response) {notification.error({description: '您的网络发生异常,无法连接服务器',message: '网络异常',});}return response;
}/*** 请求成功,返回请求结果,否则抛出请求错误异常* @param {*} response            fetch 请求返回结果* @return {*}                  返回 fetch 请求结果*/
function checkStatus(response) {if (response?.status >= 200 && response?.status < 300) {return response;}const error = new Error(response.statusText);error.response = response;throw error;
}// 用于处理 fetch 请求返回结果进行 json 格式化操作
async function parseJSON(response) {const data = await response.json();return { data, headers: response.headers };
}
// 用于处理 fetch 请求返回结果进行 blob 格式化操作
async function parseBlob(response) {const data = await response.blob();return { data, headers: response.headers };
}/*** 通过请求接口 API,进行数据请求操作,通过 Promise 异步方式返回请求结果* @param {String} url              fetch 请求接口 API 地址* @param {Object} options          fetch 请求参数:请求方式 method、请求头 headers 等* @returns {Object}                返回 fetch 请求结果:data | err*/
export function request(url, options) {url = options.method.toLocaleLowerCase() === 'get' && options.params ? url + `?${queryStringify(options.params)}` : url;let headers = !options.body ? { 'Content-type': `application/${options.dataType || 'x-www-form-urlencoded'}; charset=UTF-8`, } : {};options.headers = headers;if (!options.isVire) {let { token, validtime } = ReadUseToken(), nowtime = new Date().getTime();// 如果 token 本地存在,进行时间对比,若效果当前时间表示 token 已经过期啦if (validtime && validtime < nowtime) {}headers['token'] = token || 'Basic dGVzdF9jbGllbnQ6dGVzdF9zZWNyZXQ=';}if (!options.body) {// POST 请求,需要判断传入类型是否为 JSON 格式,否侧通过 querystring 将请求 body 数据转化为字符串格式if (options.method.toLocaleLowerCase() === 'post' || options.method.toLocaleLowerCase() === 'put' || options.method.toLocaleLowerCase() === 'delete' || options.method.toLocaleLowerCase() === 'patch') {let body = options.params ? options.params : {};body = options.dataType === 'json' ? JSON.stringify(body) : queryStringify(body);options.body = body;}}return fetch(url, options).then(checkStatus).then(parseJSON).then(({ data }) => data).catch(err => errorHandler(err));
}/*** 通过请求接口 API,进行数据请求操作,通过 Promise 异步方式返回请求结果* @param {String} url              fetch 请求接口 API 地址* @param {Object} options          fetch 请求参数:请求方式 method、请求头 headers 等* @returns {Object}                返回 fetch 请求结果:data | err*/
export function download(url, options) {url = options.method.toLocaleLowerCase() === 'get' && options.params ? url + `?${queryStringify(options.params)}` : url;let headers = {};options.headers = headers;if (!options.isVire) {let { token, validtime } = ReadUseToken(), nowtime = new Date().getTime();// 如果 token 本地存在,进行时间对比,若效果当前时间表示 token 已经过期啦if (validtime && validtime < nowtime) {}headers['token'] = token || 'Basic dGVzdF9jbGllbnQ6dGVzdF9zZWNyZXQ=';}if (!options.body) {// POST 请求,需要判断传入类型是否为 JSON 格式,否侧通过 querystring 将请求 body 数据转化为字符串格式if (options.method.toLocaleLowerCase() === 'post' || options.method.toLocaleLowerCase() === 'put' || options.method.toLocaleLowerCase() === 'delete' || options.method.toLocaleLowerCase() === 'patch') {let body = options.params ? options.params : {};body = options.dataType === 'json' ? JSON.stringify(body) : queryStringify(body);options.body = body;}}return fetch(url, options).then(checkStatus).then(parseBlob).then(data => data).catch(err => errorHandler(err));
}
export const APP_LINE = 0;         // 1 表示线上环境,0 表示测试环境export const API = APP_LINE === 0 ? '/api-dev' : '/api-pod';
/*** 通过跌倒对象 KEY 值,将 JSON 对象参数转化为地址来 GET 请求参数方式* @param {Object} data * @returns {String}                返回请求参数格式的字符串*/
export function queryStringify(data) {return Object.keys(data).map(function (key) {return ''.concat(encodeURIComponent(key), '=').concat(encodeURIComponent(data[key]));}).join('&');
}/*** 对日期进行计算,精确到秒* @param {Date} datetime           被计算日期* @param {Number} second           计算值:正数表示之后,负数表示之前* @returns {Date}                  返回日期计算后的日期对象*/
export function datatimeCount(datetime, second) {return new Date(datetime.getTime() + second * 1000);
}
import { datatimeCount } from './lodash';/*** 登录成功后,将用户登录 Token 存储到本地缓存中* @param {String} token */export function SaveUseToken(token) {let datetime = new Date(), second = 24 * 60 * 60;let validtime = datatimeCount(datetime, second).getTime();sessionStorage.setItem('use_token', JSON.stringify({ token, validtime }));
}/*** 获取登录成功后的 Token 信息* @returns */
export function ReadUseToken() {let itemdata = sessionStorage.getItem('use_token');if (!itemdata) return { token: '', validtime: '' };let { token, validtime } = JSON.parse(itemdata);;return { token, validtime };
}

至此,还缺少一个后台登录接口,这里通过 node.js 编写一个用户登录接口,组合键 Window + R 输入 cmd 回车,就在上述项目同一个磁盘目录下 E:\wwwroot,执行如下,创建一个 Node 服务:

E:\wwwroot>mkdir interface                # 创建一个接口目录
E:\wwwroot>cd interface                  # 进入到这个目录
E:\wwwroot\interface>npm init -y     # 初始化一个服务
E:\wwwroot\interface>cd.>index.js     # 创建服务入口文件
# 一下这一步,可以通过任意编辑器打开我的电脑,进入 E:\wwwroot 盘下的接口目录的 index.js 文件
E:\wwwroot\interface>code .              # 使用 VS Code 打开当前项目
E:\wwwroot\interface>npm install express
E:\wwwroot\interface>npm install body-parser
E:\wwwroot\interface>npm install jsonwebtoken
E:\wwwroot\interface>node index.js       # 编辑如下后执行运行后端

并编辑后端接口主程序文件 index.js,内容如下,这里创建两个接口,第一个用于查看服务是否启动成功

const express = require('express');
const bodyparser = require('body-parser');
const app = express();
const jwt = require('jsonwebtoken');app.use(bodyparser.urlencoded({ extended: false }));app.use(function(req, res, next) {res.header('Access-Control-Allow-Origin', '*');res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');res.header('Access-Control-Allow-Headers', 'X-Requested-With');res.header('Access-Control-Allow-Headers', 'Content-Type');next();
});app.get('/', (req, res) => { res.send({ code: 0, msg: '登录成功' }); });app.post('/user/login', (req, res) => {let { username, password } = req.body;try {let token = jwt.sign({ username, password }, 'WOAINI', { expiresIn: 60 * 60 * 2 });res.send({ code: 200, msg: '登录成功', token: token });} catch (e) {console.log(e)}
});app.listen(3300, () => { console.log('server starting sucess, address: http://127.0.0.1:3300'); });

执行成功后,在浏览器中输入:http://127.0.0.1:3300,如果打印 {"code":0,"msg":"登录成功"} 表示成功

回到前台 http://localhost:3000,此时的前台是无法调用后端接口的,那是因为存在跨域问题

通过插件配置跨域问题,项目执行 npm install http-proxy-middleware --save,并在项目的 src 根目录下创建 setupProxy.js,配置信息如下:

const { createProxyMiddleware } = require('http-proxy-middleware');module.exports = function (app) {app.use(createProxyMiddleware('/api-dev', {target: 'http://127.0.0.1:3300',changeOrigin: true,pathRewrite: {'^/api-dev': ''}}))
}

回到前端 http://localhost:3000/login,按 F12 键,输入任意账号密码点击登录,查看浏览器控制台打印信息如下,表示请求成功:

{code: 200, msg: '登录成功', token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2N…cwNH0.dkKIAdGiQuEDaxBnIPHuCFyG5bCqAJRBN4ARI7MuV00'}

前端架构总结

通常将 src/pages/IndexPage 组件作为一个容器组件,也是一个布局 layout 组件,布局采用中台布局,调整路由配置,左侧菜单路由就是布局组件的子路由,右侧内容映射子路由匹配组件,调整布局组件路由配置 root.js


import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import Store from './redux/store';
import Sagas from './redux/saga';
import IndexPage from './pages/IndexPage';
import InvalPage from './pages/InvalPage';
import LoginPage from './pages/LoginPage';
import menuRoute from './route';const store = Store();
store.runSaga(Sagas);function Root() {return (<Provider store={store}><Router><Switch><Route exact path="/login" component={LoginPage} /><IndexPage><Switch>{menuRoute.length > 0 ? menuRoute.map(item => (<Route key={item.href} exact path={item.href} component={item.component} />)) : null}<Route exact path='*' component={InvalPage} /></Switch></IndexPage></Switch></Router></Provider>);
}export default Root;

通过 route.js 来管理中台路由映射相应模板文件,如下例内容:

import WhomePane from './panes/WhomePane';
import CasesPane from './panes/CasesPane';
import CasdbPane from './panes/CasdbPane';
import EnshePane from './panes/EnshePane';
import HisrcPane from './panes/HisrcPane';
import QuickPane from './panes/QuickPane';
......
export default [{ href: '/', name: '首页', component: WhomePane },{ href: '/cases', name: '合作案例列表', component: CasesPane },{ href: '/cases/:id', name: '合作案例详情', component: CasdbPane },{ href: '/workbench/collect', name: '我的收藏', component: EnshePane },{ href: '/workbench/chronicle', name: '选号历史记录', component: HisrcPane },{ href: '/redbook/numerical', name: '快捷选号中心', component: QuickPane },......
];

细细分化,分发处理,对于前期开发,充分的利用高内聚、低耦合思想,简化前端开发工作,提高开发效率;于后期运维,也能更好的定位代码,快速进行产品需求迭代,减少运维难点和成本

对于人事变动,也能够很好的进行工作交接,确保公司的开发进程不受影响

最后声明

以上内容完全是个人在工作中通常遇到的一些问题,在捆绑这些问题的时候,通过集成 react 全家桶系列,指定一定的规范和统一性,能更好的减少新人入手的时间,确保企业项目的开发进度,当然,前端实际开发中远远不至于这些问题,做事情往往是要求精益求精的,此文仅仅是提供一种思想,并非强制使用,作为一个企业的前端管理者,有必要协调组员开发工作,以及管控企业产品开发周期

十一:以理论结合实践方式梳理前端 React 框架 ———框架架构相关推荐

  1. 十:以理论结合实践方式梳理前端 React 框架 ———集成框架

    dva 框架简介 dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个 ...

  2. 九:以理论结合实践方式梳理前端 React 框架 ——— 简述中间件

    redux-saga 基本简介 中间件是一种独立运行于各个框架之间的代码,以函数的形式存在,连接在一起,形成一个异步队列,可以访问请求对象和响应对象,可以对请求进行拦截处理,再将处理后的控制权向下传递 ...

  3. 四:以理论结合实践方式梳理前端 React 框架 ——— React 高级语法

    事件处理 react 内置组件的事件处理 react 内置组件是指 react 中已经定义好的,可以直接使用的如 div.button.input 等与原生 HTML 标签对应的组件 <!DOC ...

  4. 二:以理论结合实践方式梳理前端 ES 6+ ——— ES 6+ 基础语法

    ES 6+ 基础语法 虽说 JavaScript 是一门非编程式语言,但它又具备编程思想,这也是因为 JavaScript 在设计之初参考了 Java 的设计思路所带来的捆绑思想,也间接的导致了前期的 ...

  5. 一:以理论结合实践方式梳理前端 CSS 3 ———真正了解样式表

    真的了解 CSS 3 吗 在学习 CSS 3 之前,首先要了解的是,什么是 CSS 3.CSS 3 能做什么?学习 HTML 的都知道,HTML 是网页的结构,那么 CSS 就是网页的表现,就像美女和 ...

  6. 八:以理论结合实践方式梳理前端 CSS 3 ——— 弹性布局特性

    基本概念 设置 display: flex; 的元素称为 Flex 容器,其中所有的子元素称为 Flex 项目 容器存在两根用于定位的轴,分别是水平的 主轴 和垂直的 交叉轴,项目默认沿主轴排列 容器 ...

  7. 七:以理论结合实践方式梳理前端 CSS 3 ——— 字体颜色独特性

    样式单位 相对长度单位指定了一个长度相对于另一个长度的属性,对于不同的设备相对长度更适用 绝对长度单位是一个固定的值,它反应一个真实的物理尺寸,绝对长度单位视输出介质而定,不依赖于环境(显示器.分辨率 ...

  8. 五:以理论结合实践方式梳理前端 ES 6+ ——— ES 6+ 全局对象

    全局对象 全局属性 属性描述 Infinity 代表正的无穷大的数值. NaN 指示某个值是不是数字值. undefined 指示未定义的值 函数 描述 函数 描述 decodeURI() 解码某个编 ...

  9. 最佳实践系列:前端代码标准和最佳实践

    最佳实践系列:前端代码标准 @窝窝商城前端(刘轶/李晨/徐利/穆尚)翻译于2012年 版本0.55 @郑昀校对 isobar的这个前端代码标准和最佳实践文档,涵盖了Web应用开发的方方面面,我们翻译了 ...

最新文章

  1. python读取nii文件_python实现批量nii文件转换为png图像
  2. 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码
  3. android textview import,android – textview中的镜像文本?
  4. Struts2的Stack Context和ValueStack
  5. 什么?在 VSCode 里也能用 Postman了?
  6. 牛客网笔记之JAVA运算符
  7. adb echo shell 覆盖_一次写shell脚本的经历记录
  8. 信息权限管理(RMS)
  9. 关于调试模块BC26-移远NB模块-过程所遇问题-记录
  10. linux系统富士通打印机驱动,PRIMERGY:驱动下载 - 富士通中国
  11. 36.42. schemata
  12. 新加坡国立大学计算机系访学,从实践中来,到实践中去——记新加坡国立大学访学项目...
  13. ios是什么,ios是什么意思
  14. box2d的角色邹形
  15. OSChina 周六乱弹 —— 生命诚可贵,啤酒价更高
  16. VSCode配置文件“.vscode/c_cpp_properties.json”不断被覆盖的原因及解决方法
  17. 计算机操作系统之进程
  18. 操作系统面试基础知识点
  19. Hadoop3.2.1 【 YARN 】源码分析 : ContainerManager浅析
  20. Mysql 时间格式化 DATE_FORMAT使用

热门文章

  1. 如何将电脑和手机连起来,实现同步打字?
  2. 考研计算机难度排名2015,全国考研难度排行榜--前100名
  3. Python中10个常用的内置函数
  4. echarts绘制完整的中国地图
  5. 基于聚类分析和协同过滤算法的营养膳食分析系统的设计与实现
  6. linux设备驱动程序 中文第三版,Linux 设备驱动 Edition 3
  7. 在虚拟机VMware上安装XP系统
  8. psp记忆棒测试软件,识别4GB PSP记忆棒真伪的方法
  9. python可以做网页开发么_Python可以开发网页吗?Python学习班
  10. Redhat Linux 上安装输入法