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


    前言书明观念

    从第一代码农写下第一行代码开始到上个世纪的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 构造函数的参数,是组件中一个一次性生效的数据,不可以被更改

    标签设置代码设置代码改变用户输入改变是否支持
    NYYproperty
    YYYattribute
    NNNYstate
    NYNNconfig

    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

    2323

    |-- 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(
        

    Hello React!

    , 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(
    	, document.getElementById('root')
    );
    reportWebVitals();
    
    import { Button } from 'antd';
    import './App.css';
    
    function App() {
    	return (
    		

    ); } 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(, document.getElementById('root'));
    reportWebVitals();
    
    import { Button } from 'antd';
    import './App.less';
    
    function App() {
    	return (
    		

    ); } 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 (
    布局组件
    ) } export default IndexPage;
    import './index.less';
    function InvalPage() {
        return (
    404组件
    ) } export default InvalPage;
    import './index.less';
    function LoginPage() {
        return (
    登录组件
    ) } 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 (
            
    			
    			
                
    		
        );
    }
    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 (
            
                
                    
                        
                        
                        
                    
                
            
        );
    }
    
    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 (
            
                
    { span: 6, }} wrapperCol={{ span: 10 }} autoComplete="off" form={formData} onFinish={handPush} style={{ width: '800px', marginTop: '40px' }} > { offset: 8, span: 16 }}>
    ) } 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 (
            
                
                    
                        
                        
                            
                                {
                                    menuRoute.length > 0 ? menuRoute.map(item => (
                                    	
                                    )) : null
                                }
                                
                            
                        
                    
                
            
        );
    }
    
    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 全家桶系列,指定一定的规范和统一性,能更好的减少新人入手的时间,确保企业项目的开发进度,当然,前端实际开发中远远不至于这些问题,做事情往往是要求精益求精的,此文仅仅是提供一种思想,并非强制使用,作为一个企业的前端管理者,有必要协调组员开发工作,以及管控企业产品开发周期

  • 相关阅读:
    〖Python 数据库开发实战 - MySQL篇⑪〗- 修改数据表结构
    基于神经网络和遗传算法的unity开发框架
    麻雀优化算法SSA及其改进策略
    均匀光源积分球的应用领域有哪些
    (第一天:)1.字典赋值默认值、字典解压赋值
    独孤九剑第一式-岭回归和Lasso回归
    CAS:1192802-98-4_UV 裂解的生物素-PEG2-叠氮
    进程替换与复制
    基于python的C语言学习笔记(1)
    六、Zabbix — Proxy分布式监控
  • 原文地址:https://blog.csdn.net/weijun20180101/article/details/123979829