mirrorx
1、什么是MirrorX?
MirrorX是基于Redux封装的一种状态机。比如实际使用的时候假设我们要从后台读取值班信息、把接口返回的数据做一些处理(按对象的类型进行分类渲染),然后将处理好的数据显示在界面上。想想就得用很多代码,而且都要放在组件的里
要是有一个框架,组件里只需要调一行代码就能解决,是不是很不错?MirrorX就是用来做这件事的。
2、怎么能做到1行解决?
学过面向对象编程的,应该都知道封装性可以使控制层代码更加简练。但是React里如果要处理刚才说的那件事,既需要state和props控制权,也要知道来来往往的上下文(比如当前用户是谁、VIP等级是多少),最令React程序员难受的是,当前用户信息在UserView组件里,当前的GoodsView组件没有对UserView组件的访问权。因此,需要有一个统一的地方来跨越组件的障碍,存储这些信息,把这个机制封装好了,就可以实现一行解决。
3、具体是怎样的机制?
网上有很多文章讲Redux,看完Redux,再搜索MirroX就可以知道具体的机制。简单的来说,就是系统有若干个状态仓库,我们可以把上下文变量都分门别类放在不同仓库里,比如用户信息、商品信息;放入的时候都是调用动作来实现,比如读取商品信息、更改商品数量。而动作可以选择是否调用服务,如果前台更改商品数量,直接改状态仓库里的数值即可,无需调用服务;如果调用了服务,则一般来说是功能是需要服务器交互的。
mirror.defaults({ historyMode: 'hash' });
一、简单的Model层,包含动作和模型
model.js
import { actions } from "mirrorx"; import * as api from "./HttpUtils"; // 把自定义的请求文件HttpUtils.js引入进来,名称定为api export default { name: "GoodsManager", // 这里写的名字将会成为状态仓库的名称 initialState: { // 这里可以写初始化时状态机里的初始状态 userId: "0001" }, // reducer:状态机处理函数 必填,相当于同步执行的action方法,接受两个参数state和action,合并后返回新的state状态值。 reducers: { // 这个updateState是默认的,它用来主动更新状态机里的各种状态 // state和data都是Object对象 // state是框架传入的,开发者调用的时候,data才是对应的第一个参数 // ...是ES6的对象扩展运算符,后面...data会自动覆盖...state的同名属性 updateState: (state, data) => ({ ...state, ...data }) // 后面还可以写其他的reducer,切记第一个参数一定是state },
// 非必填,相当于异步执行action方法,接受两个参数store和action,store里包括redux自带的getState和dispatch方法,action为用户dispatch时带的参数。 effects: { // 动作处理函数:获取商品 // param是对象,getState是框架传入的函数对象,用来方便获取当前状态机的状态 // 开发者调用的时候,不用给getState形参赋值 async GetGoods(param, getState) { // Promise的同步操作运算,获取Axios返回的data let { data } = await api.GetGoodsApi(param); // 调用当前状态机的updateState方法(也就是上面写的那个函数) // 由调用可见,只放了一个Object类型的参数 actions.GoodsManager.updateState({ goods: data.data }); } } };
实例
import { actions } from "mirrorx"; import * as authService from "../services/auth"; import Toolkit from "../utils/Toolkit"; import set from "lodash/set"; import "antd-mobile/lib/toast/style/css"; import Toast from "antd-mobile/lib/toast"; import 'antd-mobile/lib/modal/style/css'; import Modal from 'antd-mobile/lib/modal'; const alert = Modal.alert; const defaultState = { hddngrTypeList: [], msgTypeList: [], hiddenTrouble: null, isShowNextStep: false, isShowSelectPerson: false, isShowMeetHouse: false, getFlowCnfg: null, NextLink: null, naturalDisastersList: [], }; export default { name: "report", initialState: defaultState, reducers: { change: (state, data) => { return { ...state, ...data }; }, reset: (state, data) => { return defaultState; }, }, effects: { // 获取编号 async getHddngrcodes(params, getState) { let request = { project: "ccep", model: "model:com.chinacreator.flwmgr.base.noRule.noRule", action: "getBusicode", rtype: "e ", data: params, }; return authService .postPeople(request) .then((data) => { if (data.status === "1") { if (data.response.flag) { return data.response.value; } } }) .catch((err) => { return { err }; }); }, // 获取隐患列表 async getNaturalDisastersList2(params, getState) { const loading = Toast.loading("加载中...", 30); let request = { project: "ccep", model: "model:com.chinacreator.xtyjoa.emergency.hddngrInfo.model.hddngrInfo", action: "hddngrInfoQuery", rtype: "e ", data: params, }; let { naturalDisastersList } = getState().report; authService .postPeople(request) .then((data) => { Toast.hide(loading); if (data.status === "1") { let list = []; if (data.response.datas) { list = naturalDisastersList; if (params.pageIndex === 1) { list = data.response.datas; } if (data.response.totalSize <= list.length) { if (params.pageIndex !== 1) { Toolkit.handleError({ message: "滑动到底" }); } } else { if (params.pageIndex !== 1) { data.response.datas.map((item, key) => { list.push(item); }); } } actions.report.change({ naturalDisastersList: list, }); } } }) .catch((err) => { Toolkit.handleError(err); }); }, // 获取自然灾害列表 async getNaturalDisastersList(params, getState) { const loading = Toast.loading("加载中...", 30); let request = { project: "ccep", model: "model:com.chinacreator.xtyjoa.emergency.hddngrInfo.model.hddngrInfo", action: "hddngrInfoQuery", rtype: "e ", data: params, }; let { naturalDisastersList } = getState().report; authService .postPeople(request) .then((data) => { Toast.hide(loading); let totalSize = data.response.totalSize if (data.status === "1") { let list = []; if (data.response.datas) { list = naturalDisastersList; if (params.pageIndex === 1) { list = data.response.datas; } if (data.response.totalSize <= list.length) { if (params.pageIndex !== 1) { Toolkit.handleError({ message: "滑动到底" }); } } else { if (params.pageIndex !== 1) { data.response.datas.map((item, key) => { list.push(item); }); } } actions.report.change({ naturalDisastersList: list, }); }else { if(totalSize === 0 ){ actions.report.change({ naturalDisastersList: [], }); } } } }) .catch((err) => { Toolkit.handleError(err); }); }, /** * 隐患保存 * @returns {Promise.<void>} */ async saveHddngrData2(params, getState) { let savelist = { project: "ccep", model: "model:com.chinacreator.xtyjoa.emergency.hddngrInfo.model.hddngrInfo", action: params.apply_id ? "modify" : "add", rtype: "o", data: params, }; authService .postPeople(savelist) .then((data) => { const { model } = getState().report; if (data.status === "1") { Toolkit.message("保存成功"); set(model, "apply_id", data.response.apply_id); actions.report.change({ hiddenTrouble: data.response, model, }); } }) .catch((err) => { Toolkit.handleError(err); }); }, /** * 自然灾害保存 * @returns {Promise.<void>} */ async saveHddngrData(params, getState) { let savelist = { project: "ccep", model: "model:com.chinacreator.xtyjoa.emergency.hddngrInfo.model.hddngrInfo", action: params.modify ? "modify" : "add", rtype: "e", data: params, }; authService .postPeople(savelist) .then((data) => { const { model } = getState().report; if (data.status === "1") { // Toolkit.message("保存成功"); set(model, "apply_id", data.response.apply_id); actions.report.change({ hiddenTrouble: data.response, model, }); alert('提示', `保存成功!`, [ { text: '确定', onPress: () => { Toolkit.routeBack() } }, ]); } }) .catch((err) => { Toolkit.handleError(err); }); }, /** * 隐患删除 * @returns {Promise.<void>} */ async deleteHddngrData(params, getState) { let savelist = { project: "ccep", model:"model:com.chinacreator.xtyjoa.emergency.hddngrInfo.model.hddngrInfo", action: "remove", rtype: "e", data: params, }; authService .postPeople(savelist) .then((data) => { const { model } = getState().report; if (data.status === "1") { Toolkit.message("删除成功"); } }) .catch((err) => { Toolkit.handleError(err); }); }, /** * 隐患 送下一环节配置数据 * @returns {Promise.<void>} */ async getFlowStartConfig(params, getState) { authService .getFlowStartConfig(params) .then((data) => { if (data.success) { if (data.response != {}) { actions.report.getFlowNextLink({ flowId: data.response.flow_id }); // actions.report.dict() actions.report.getFlowCnfgs(data.response.flow_id); } } }) .catch((err) => { Toolkit.handleError(err); }); }, /** * 查询流程下一环节(下一环节及处理人) * @returns {Promise.<void>} */ async getFlowNextLink(params, getState) { authService .getFlowNextLink(params) .then((data) => { actions.report.change({ NextLink: data.output, }); }) .catch((err) => { Toolkit.handleError(err); }); }, /** * getFlowCnfg * @returns {Promise.<void>} */ async getFlowCnfgs(params, getState) { authService .getFlowCnfg(params) .then((data) => { if (data.status === 1) { actions.report.change({ isShowNextStep: true, getFlowCnfg: data.output, }); } }) .catch((err) => { Toolkit.handleError(err); }); }, /** * 送下一环节提交 * @returns {Promise.<void>} */ async startFlowInst(params, getState) { authService .startFlowInst(params) .then((data) => { if (data.status === 1) { Toolkit.message("送下一环节成功"); actions.report.change({ isShowNextStep: false, hiddenTrouble: null, NextLink: null, }); Toolkit.routeBack(); } }) .catch((err) => { Toolkit.handleError(err); }); }, }, };
二、改造组件,变成由MirrorX托管组件
第一步、在项目入口的地方添加(比如在app.js上添加在内存中创建状态机的代码):
// 引入MirrorX的组件 import mirror from 'mirrorx'; // 引入刚刚写的model,注意路径 import model from './model'; // 调用MirrorX,根据模型创建状态机 mirror.model(model);
实例
import 'antd-mobile/lib/modal/style/css'; import './index.css' import './assets/iconfont/iconfont.css'; import './react-block-ui.css' import React from 'react' import mirror, { render, Router, actions } from 'mirrorx' import EventEmitter from 'eventemitter3' import VConsole from 'vconsole' import App from './App' import registerServiceWorker from './registerServiceWorker' import application from './models/application' import auth from './models/auth' import applyMeeting from './models/applyMeeting' import applyCar from './models/applyCar' import applyBussiTrip from './models/applyBussiTrip' import applyAnnualLeave from './models/applyAnnualLeave' import applyLeave from './models/applyLeave' import capitalSpending from './models/capitalSpending' import report from './models/report' import nextLink from './models/nextlink' // 配置默认的路由方式 mirror.defaults({ historyMode: 'hash' }); // 加载模型 mirror.model(application); mirror.model(auth); mirror.model(applyMeeting); mirror.model(capitalSpending); mirror.model(applyCar); mirror.model(applyBussiTrip); mirror.model(applyAnnualLeave); mirror.model(applyLeave); mirror.model(report); mirror.model(nextLink); // 监听action mirror.hook((action) => { const platformId = (window.cordova && window.cordova.platformId) ? window.cordova.platformId : 'windows'; if (platformId === 'android') { console.log('调用方法:' + action.type); } else { console.log('%c调用方法:%s', 'color: #FF9800;font-family:source code pro;', action.type); } if (action.data) { if (platformId === 'android') { console.log('更新数据:'); } else { console.log('%c更新数据:', 'color: #9C27B0;font-family:source code pro;'); } console.log(action.data); } if (action.type === '@@router/LOCATION_CHANGE' && action.payload.pathname !== '/login') { actions.auth.change({ from: action.payload.pathname }); } }); // 初始化事件总线 const emitter = window.emitter = new EventEmitter(); emitter.on('RENDER', function (params) { if (params && params.vconsole) new VConsole(); // 渲染到DOM节点 render(<Router><App /></Router>, document.getElementById('root')); }); registerServiceWorker();
第二步、在受状态机托管的组件上改一下代码
// 增加对MirrorX的引用 import {connect} from 'mirrorx'; // 这里面GoodsView就是当前受状态机托管组件的class名称,GoodsManager就是第二步里name写的名字 export default connect(state => state.GoodsManager)(GoodsView);
实例
import '../../../common/FormStyle.scss'; import '../ApplyMeal/ApplyMeal.scss'; import React, { Component } from 'react'; import { connect, actions, model } from 'mirrorx' import loadable from 'common/Loadables'; import { Flex, Box } from 'reflexbox'; import set from 'lodash/set'; import get from 'lodash/get'; import map from 'lodash/map'; import 'antd-mobile/lib/button/style/css'; import Toolkit from "../../../utils/Toolkit"; import moment from 'moment'; import 'antd-mobile/lib/modal/style/css'; import Modal from 'antd-mobile/lib/modal'; // title组件 import TitleComp from '../ApplyCommon/TitleComp'; // 输入框 import TextComp from '../ApplyCommon/TextComp'; // 底部按钮 import BottomButton from '../ApplyCommon/BottomButton'; // 弹出窗-下一环节 import NextStep from '../ApplyCommon/NextStep'; const alert = Modal.alert; const NavBars = loadable(() => import('components/NavBar/index')); class ApplyAnnualLeave extends Component { constructor() { super(); const userInfo = Toolkit.userInfo(); this.state = { model: { apply_no: '', creator_account: userInfo.user_account, creator_name: userInfo.user_name, org_code: userInfo.org_code, create_departname: userInfo.oorg_name, apply_date: moment().format('YYYY-MM-DD'), //申请日期 join_date: '', work_years: '', cleave_days: '', aleave_days: 0, nleave_days: '', leave_days: '', start_date: '', end_date: '', is_lateleave: ['0'], apply_reason: '', apply_title:`${userInfo.user_name}同志${moment(new Date()).format('YYYY年MM月份')}年假审批表` }, web: true, node: false, nextStepUser: [] //下一环节人 } } clearData = () => { actions.applyAnnualLeave.change({ isShowNextStep: false }) } getText = (str) => { switch(str){ case '0': return '草稿'; case '1': return '进行'; } } async queryAleaveDays(params) { const {model} = this.state; Toolkit.callAction({ model: 'model:com.chinacreator.xtyjoa.vacation.model.vacationApplication', action: 'queryAleaveDays', data: params, }).then((res) => { if(res.status === '1'){ let forbidArr = res.response.filter(item=>{ return (item.operaState === '0' && item.days > 0) || item.operaState === '1' && item.days > 0 }) if(forbidArr.length > 0){ alert('提示', `您当前存在${this.getText(forbidArr[0].operaState)}中的休假申请,请处理完再重新申请!`, [ { text: '确定', onPress: () => { Toolkit.routeBack() } }, ]); }else { let arr = res.response.filter(item=>{ return item.operaState === '2' }) this.setState({ model:{ ...model, aleave_days:arr[0].days } }) } } }).catch((err) => { return err }); } async componentDidMount() { const userInfo = Toolkit.userInfo() this.queryAleaveDays({ creator_account:userInfo.user_account }) this.clearData() actions.auth.change({ tabTitle: '年假申请' }) // 用车类型 const carTypeList = await Toolkit.requestDict('dataenty:dataitem_root.driveType'); map(carTypeList, item => { return item.label = item.text }) actions.applyAnnualLeave.change({ carTypeList }); } componentWillUnmount() { actions.applyAnnualLeave.reset(); } save = async () => { const { model } = this.state; // 表单验证 if(!model.join_date){ alert('提示', `请选择参加工作时间!`, [{text: '确定', onPress: () => {}}]); return } if(!model.leave_days){ alert('提示', `请填写拟休天数!`, [{text: '确定', onPress: () => {}}]); return } if(!model.start_date){ alert('提示', `请选择拟休开始时间!`, [{text: '确定', onPress: () => {}}]); return } if(!model.end_date){ alert('提示', `请选择拟休结束时间!`, [{text: '确定', onPress: () => {}}]); return } if(!model.apply_reason){ alert('提示', `请填写申请原因!`, [{text: '确定', onPress: () => {}}]); return } if(moment(model.start_date).diff(moment(model.end_date), 'seconds') > 0){ alert('提示', `结束时间不能小于开始时间!`, [{text: '确定', onPress: () => {}}]); return } const {annualLeave} = this.props.applyAnnualLeave const userInfo = Toolkit.userInfo(); let params = { is_lateleave:model.is_lateleave[0], apply_type: '1', creator_name:model.creator_name, create_departname:model.create_departname, work_years:model.work_years, cleave_days:model.cleave_days, aleave_days:model.aleave_days, nleave_days:model.nleave_days, leave_days:model.leave_days, join_date:moment(model.join_date).format('YYYY-MM-DD'), start_date:moment(model.start_date).format('YYYY-MM-DD'), end_date:moment(model.end_date).format('YYYY-MM-DD'), apply_no: model.apply_no, apply_reason:model.apply_reason, apply_date: moment().format('YYYY-MM-DD'), //申请日期 apply_title:`${userInfo.user_name}同志${moment(new Date()).format('YYYY年MM月份')}年假审批表`, apply_id: annualLeave&&annualLeave.apply_id, } if(!annualLeave){ //生成编号 const busiParams = { no_rule_code: 'a_l_apply_no', params: { year: moment().format('YYYY'), month: moment().format('MM'), org_code: userInfo.org_code } } const busicodes = await actions.applyAnnualLeave.getAnnualcodes(busiParams); if (busicodes.err) return Toolkit.handleError(busicodes.err); set(model, 'apply_no', busicodes); actions.applyAnnualLeave.change({ model }) params = { ...params, apply_no: busicodes } } actions.applyAnnualLeave.saveAnnualData(params) } sendNextStep = () => { const userInfo = Toolkit.userInfo(); actions.applyAnnualLeave.getFlowStartConfig({ 'flow_class_code': '0607', 'org_code': `${userInfo.org_code}` }) } componentWillReceiveProps(newProps, newContext) { if (newProps.applyAnnualLeave.NextLink != this.props.applyAnnualLeave.NextLink) { let next_link = [] let next_handler = [] newProps.applyAnnualLeave.NextLink ? newProps.applyAnnualLeave.NextLink.next_link ? newProps.applyAnnualLeave.NextLink.next_link.map((item, key) => { next_link.push({ value: item.link_id, label: item.link_name, id: item.link_code, key: key }) newProps.applyAnnualLeave.NextLink.next_handler[item.link_id].map((item, key) => { next_handler.push({ value: item.user_account, label: item.user_name, id: item.user_account, key: key }) }) }) : '' : '' this.setState({ next_link_value: [next_link[0].value], next_link: next_link, next_handler: next_handler, nextStepUser: [ { value: next_handler[0].value, label: next_handler[0].label } ] }) } } endSave = () => { const { getFlowCnfg, annualLeave } = this.props.applyAnnualLeave; const { nextStepUser, next_link_value, note, web, model } = this.state; let transferChannels = ""; if (note && web) { transferChannels = "note,web" } else if (note && !web) { transferChannels = "note" } else if (!note && web) { transferChannels = "web" } let data = { messageData: { messageContent: JSON.stringify({ content: "接收到一条新的请假审批,请及时处理!", action: "open_menu", params: { text: "请假审批", url: "com.chinacreator.xtyjoa.vacation.forms.vacationAppr.html" } }), transferChannels: transferChannels }, flowData: { flowId: getFlowCnfg.flow_id, flowCode: getFlowCnfg.flow_code, flowClass: getFlowCnfg.flow_class_code.flow_class_code, flowClassSname: getFlowCnfg.flow_class_code.flow_class_shortname, flowInstName: `${annualLeave.creator_name}同志年假申请表`, businessId: `${annualLeave.apply_id}`, businessClass: "ApproveExecuteServiceImpl", businessParam: JSON.stringify({ election_date_time: annualLeave.election_date_time, creator_name: annualLeave.creator_name, apply_reason: annualLeave.apply_reason, apply_id: annualLeave.apply_id, flow_id: getFlowCnfg.flow_id, flow_code: getFlowCnfg.flow_code, flow_name: getFlowCnfg.flow_name, flow_class_code: getFlowCnfg.flow_class_code.flow_class_code, opera_state: '1' }) }, approveData: { nextStepLink: next_link_value[0], nextStepUser: [{ userAccount: nextStepUser[0].value, userName: nextStepUser[0].label }] } } actions.auth.startFlowInst(data) } onChangeCheckboxItem = (v) => { const { web, note } = this.state; switch (v) { case 'web': this.setState({ web: !web }) break; case 'note': this.setState({ note: !note }) break; default: break } } hideModel = () => { actions.applyAnnualLeave.change({ isShowNextStep: false, }) } funCallBack = () => { this.clearData() Toolkit.routeBack() } // 计算休假天数 calculateDays = (year) => { if(year>=1 && year<10){ return 5; }else if(year>=10 && year<20){ return 10; }else if(year>=20){ return 15; }else { return 0 } } // 改变modeldata changeModelData = (v, name) => { const { model } = this.state; set(model, [name], v) this.setState({ model },()=>{ if(moment(model.start_date).diff(moment(model.end_date), 'seconds') > 0){ alert('提示', `结束时间不能小于开始时间!`, [{text: '确定', onPress: () => {}}]); return } }) if (name === "join_date") { //计算工作年限相关 let work_years = moment(new Date()).diff(moment(v), "years"); this.setState({ model: { ...model, work_years, //工作年限 cleave_days:this.calculateDays(work_years) ,//可休 nleave_days:work_years > 0 ? this.calculateDays(work_years) - model.aleave_days : 0,//未休 }, }); } } // 改变state changeState = (name, v) => { this.setState({ [name]: v }) } render() { const { model: { apply_no, creator_name, create_departname, join_date, work_years, cleave_days, aleave_days, nleave_days, leave_days, start_date, end_date, is_lateleave, apply_reason }, next_link, next_link_value, next_handler, note, web,nextStepUser } = this.state; const { isShowNextStep, annualLeave, } = this.props.applyAnnualLeave; const isBuxiuList = [ { value: '0', label: '否' }, { value: '1', label: '是' } ] const renderData = [ { title: '编号', type: 'readOnly', value: apply_no, name: 'apply_no' }, { title: '姓名', type: 'readOnly', value: creator_name, name: 'creator_name' }, { title: '科室(部门)', type: 'readOnly', value: create_departname, name: 'create_departname' }, { title: '参加工作时间', type: 'datetime', value: join_date, name: 'join_date', mode: 'date' , required:true}, { title: '工作年限', type: 'readOnly', value: work_years, name: 'work_years' }, { title: '可休天数', type: 'readOnly', value: cleave_days, name: 'cleave_days' }, { title: '已休天数', type: 'readOnly', value: aleave_days, name: 'aleave_days' }, { title: '未休天数', type: 'readOnly', value: nleave_days, name: 'nleave_days' }, { title: '拟休天数', type: 'input', value: leave_days, name: 'leave_days', required:true }, { title: '拟休开始时间', type: 'datetime', value: start_date, name: 'start_date', mode: 'date' , required:true }, { title: '拟休结束时间', type: 'datetime', value: end_date, name: 'end_date', mode: 'date', required:true }, { title: '是否补休', type: 'select', value: is_lateleave, name: 'is_lateleave', configs: { options: isBuxiuList }, required:true }, { title: '申请原因', type: 'textarea', value: apply_reason, name: 'apply_reason', required:true }, ]; return ( <div className={'pageMain relative FormStyle ApplyMeal'}> <NavBars /> <div className={"ApplyMealBox"}> <Flex> <TitleComp // w={2 / 8} data={renderData} /> <TextComp // w={6 / 8} data={renderData} change={(v, name) => this.changeModelData(v, name)} /> </Flex> </div> <NextStep isShow={isShowNextStep} next_link={next_link} next_link_value={next_link_value} next_handler={next_handler} nextStepUser={nextStepUser} note={note} web={web} onChangeCheckboxItem={this.onChangeCheckboxItem} hideModel={this.hideModel} endSave={this.endSave} changeState={this.changeState} /> <BottomButton isShow={!isShowNextStep} isAdd={!get(annualLeave, 'apply_id')} save={this.save} sendNextStep={this.sendNextStep} funCallBack={this.funCallBack} /> </div> ) } } function dispatch(state) { return { applyAnnualLeave: state.applyAnnualLeave } } export default connect(dispatch)(ApplyAnnualLeave);
三、在需要调用的地方写下如下代码:
// 引入MirrorX的组件 import mirror from 'mirrorx'; // 引入刚刚写的model,注意路径 import model from './model'; // 调用MirrorX,根据模型创建状态机 mirror.model(model);
这里因为GoodsView只要一加载就需要显示商品列表,因此,我们可以把代码写在constructor(props)函数里:
四、补充说明
- 状态机里所有的状态值都会被自动写在托管组件的props里,当发生变化时,也是可以从props里取出来,因此不要尝试去获取或更新组件的state。
- 只要状态机里的值变化,受到托管的组件会重新执行render方法,实现自动刷新。
- 实际开发时由于存在组件嵌套、组件元素属性值与状态机里的状态名称冲突,各种疑难杂症随之而来。介于我对于Antd、UCF等成熟框架的分析和实战,得出一个结论,大型系统的model.js、service.js一般不会超过5个,而且大多都有命名规范。
- 如果是看别人的代码,倒着按顺序找一遍即可,从此前端大神的代码不再难懂