一、model定义
背景
在传统的开发模式中,没有用到复杂的状态管理,只是应用了组件内部的State以及对外的Props属性,相对于简单的应用开发是可以满足的。当遇到复杂的应用后,组件内部的状态过多,维护起来也就越复杂,组件之间的”信息”传递也变得十分困难,稍不注意就会出现各种令开发者头疼的问题。
Redux、Mobx等状态管理解决方案的出现在技术上解决了开发者的难题,但上手门槛较高、概念多、样板式代码等问题也随之暴露,“当年的屠龙少年,自己变成了一条龙”。
mirrorx – 更简单清晰的解决方案
基于以上实践考虑,我们在ucf-web中选择引入mirrorx模型框架来解决这个令人困扰的问题,它并不是横空出世的新物种,它只是在Redux之上的衍生解决方案,继承了Redux的单一数据源、数据不可变、纯函数执行等三大原则的优势,并解决了概念多、样板式代码、状态树维护难等问题。 使用mirrorx,主要工作在于如何定义model文件。
定义model
一个基本的model的样子如下:
/**
*数据模型类
*/
export default {
name:"app",
initialState:{
},
reducers:{
updateState(state,data) {
return {
...state,
...data
};
}
},
effects:{
}
};
可以看出仅仅只有4个字段(name、initialState、reducers、effects),下面是这4个字段的详细解读及具体使用方法。
name
模型的名称,每一个业务对应一个模型,通过该字段的定义来找到对应的方法。
要创建model,必须指定name,且为一个合法字符串。name很好理解,就是model的名称,这个名称会用于后面创建的Redux store里的命名空间。 假设定义了一个这样的model:
export default {
name:'app'
};
那么后面创建的Redux store会是这样的结构
store.getState(); // { app:null }
可以看到,model的name就是其state在根store下的命名空间(当然,name对全局actions也非常重要,见下文)。 另外,需要注意的是,上面创建的store,其app这个state的值是null。假如你需要一个不同的、更有意义的值,那么你就需要指定一个initialState。
注意:mirror使用了react-router-redux,因此不可以使用routing做为model的name值。
initialState
initialState也很容易理解,表示model的初始state。在创建标准的Redux reducer时,它就表示这个reducer的initialState。 常规组件内部的state应该写在这里,这个值不是必需的,而且可以为任意值。如果没有指定initialState,那么它的值就是null。 创建model:
export default {
name:'app',
initialState:{
num : 0
}
};
得到的store:
Store.getState(); // { app : { num:0 } }
reducers
mirror app所有的Redux reducer都是在reducers中定义的,reducers对象中的方法本身会用于创建reducer,方法的名字会用于创建action type。mirror的原则是,一个reducer只负责一个action,所以你不需要关心你要处理的action具体的type是什么。 reducers里面的方法是同步的,并且是纯函数:
export default {
name : 'app',
initialState : {
num : 0
},
reducers : {
add(state,data) {
return state + data
}
}
};
effects
所谓的effects就是Redux的异步action (async action)。在函数式编程中,effect表示所有会与函数外部发生交互的操作。在Redux的世界里,异步的action显然是effect。 effect不会直接更新Redux state,通常是在完成某些异步操作(例如AJAX请求)之后,再调用其他的“同步action”来更新state。 和reducers对象类似,在effects中定义的所有方法都会以相同名称添加到actions.modelName上,在其他组件中可调用这些方法。
export default {
name : 'app' ,
initialState : {
num : 0
},
reducers : {
add (state,data) {
return state + data
}
},
effects : {
async myEffect(data,getState) {
const res = await Promise.resolve(data)
actions.app.add(res)
}
}
};
执行上述代码,actions.app就会拥有两个方法:actions.app.add和actions.app.myEffect。调用actions.app.myEffect,就会调用effect.myEffect。
在effects中定义的方法接收两个形参: 1.data - 调用actions.modelName上的方法所传递的data,可选; 2.getState - 实际上就是store.getState,返回当前action被dispatch前的store的数据,同样是可选的;
model注册到store
import mirror from 'mirrorx'
import model from './model.js'
mirror.model(model)
model与UI组件双向绑定
import mirror,{ connect } from 'mirrorx'
import model from './model.js'
mirror.model(model)
import App from './container.js';
const ConnectedApp = connect(state => state.app)(App);
export default ConnectedApp;
这里需要注意的是,UI组件与model组件绑定后,UI组件即可通过this.props 访问model中的数据, UI组件被引用时,如需获取store中的数据,需要使用包装后的组件,如上述例子中的ConnectedApp组件。
二、sevice规范化
传统开发是把所有的URL和接口服务写在一起,这样不便于维护归类,通过规范化Service定义来达到一个规范。一个常规的service服务类如下:
/**
* 服务请求类
*/
import request from 'ucf-request';
//定义接口地址
const URL = {
"GET_LIST":`/order/list`
}
/**
* 获取主列表
* @param {*} params
*/
export const getList = (params) => {
return request(URL.GET_LIST,{
method:'get',
params
});
}
然后在model中导入该类拿到调用的方法,大致如下:
/**
* 数据模型类
*/
import {actions} from "mirrorx";
import * as api from "./service";
export default {
// 确定Store中的数据模型作用于
name : 'app',
// 设置当前 Model 所需初始化 state
initialState : {
order: '',
},
reducers: {
/**
* 纯函数,相当于 Redux 中的 Reducer ,只负责对数据的更新
* @param {*} state
* @param {*} data
*/
updateState(state,data) { // 更新state
...state,
...data
};
},
effects: {
/**
* 按钮测试数据
* @param {*} param
* @param {*} getState
*/
async loadData(param,getState) {
let result = await api.getList(params);
return result;
}
}
};
三、路由
目前提供了两种微应用模板【singleApp】、【spaApp】,这里所说的路由就是针对spaApp来讲的,首先切换到开发根目录执行命令:
# 按照提示输入微应用名称、选择spaApp即可
ucf new app
要明确路由对应的组件页面是哪个后,修改ucf-apps/demo/src/routes/index.js路由表
import React from 'react';
import { Route } from 'mirrorx';
import {ConnectedHome } from './home/container';
import {ConnectedContact } from './contact/container';
export default () => { }
可以看出,模板带了三个路由,分别是/
(默认首页)、home
、contact
,它们对应了三个组件,这三个组件实际上是三个页面级别的组件,然后在页面上进行组件拆分到routes文件夹里面,之间的文件归属关系如下:
routes
├── contact
| ├── components
│ │ ├── IndexView
│ │ ├── index.js
│ │ └── index.less
│ ├── container.js
│ ├── model.js
│ └── service.js
├── home
│ ├── components
│ │ └── IndexView
│ │ ├── index.js
│ │ └── index.less
│ ├── container.js
│ ├── model.js
│ └── service.js
└── index.js
6 directories, 11 files
其中的IndexView就是默认首页组件存放的位置,其余是拆分出来的组件,container、model、service和第一部分介绍的开发方式是一样的。合理的拆分组件、布局页面组件,路由之间的关系才能更清晰,也更好维护。
四、关于mirror
mirror是一个开源的应用状态管理解决方案,其主要作用是简化React、Redux开发的步骤。传统的React-Redux开发,需要逐步定义action、reducer、component等相关东西,看起来比较冗长。mirror框架将这些操作进一步封装,使之使用起来更加简单。
1. 为什么要使用mirrorx
一个典型的 React/Redux 应用看起来像下面这样:
- 一个 actions 目录用来手动创建所有的 action type 或者 action creator;
- 一个 reducers 目录以及无数的 switch 来捕获所有的 action type;
- 必须要依赖 middleware 才能处理异步的 action;
- 明确调用 dispatch 方法来 dispatch 所有的 action;
- 手动创建 history 对象关联路由组件,可能还需要与 store 同步;
- 调用 history 上的方法或者 dispatch action 来手动更新路由;
综上所述,存在的问题一目了然,太多的样板文件以及繁琐甚至重复的劳动。实际上,上述大部分操作都是可以简化的。比如,在单个API中创建所有的 action 和 reducer;比如,简单地调用一个函数来 dispatch 所有的同步和异步的 action,且不需要额外引入 middleware;再比如,使用路由的时候只需要关心定义具体的路由,不用去关心 history 对象,等等。 这正是 Mirror 的使命,用极少数的 API 封装所有繁琐甚至重复的工作,提供一种简洁的更高级抽象,同时保持原有的开发模式。
2. Mirror 的安装及使用
2.1 Mirror 的安装
npm i --save mirrorx
2.2 Mirror 的使用
# model.js
import React from 'react';
import mirror, { actions, connect, render } from 'mirrorx';
// 声明 Redux state, reducer 和 action,
// 所有的 action 都会以相同名称赋值到全局的 actions 对象上, actions.[name] 即可取到所有的 action
// mirror.model 抽取到 model.js 中
mirror.model ({
name:'app',// 相当于 reducerName
initialState:0,// 初始化 state
reducers:{ // reducers 事件处理,这里省略了 action 的 type,type 为[name]/[methodName]
increment(state) { return state + 1 },
decrement(state) { return state - 1 }
},
effects:{ // 异步方法声明,异步操作需要在完成后再调用 reducers 定义的同步方法才能进行页面渲染
async incrementAsync() {
await new Promise((resolve,reject) => {
setTimeout(() => {
resolve()
},1000)
})
actions.app.increment() // actions会自动调用 dispatch 对应的 action
}
}
})
# container.js
// 连接组件和状态管理
export default connect ((state) => { return { count: state.app } }) (App)
# 组件App
// 组件中使用,抽取到components文件夹中
const App = (props) => { // 组件定义
return (
{ props.count }
{/* 调用 dispatch 上的方法来 dispatch action */}
actions.app.increment() }> +
actions.app.decrement() }> -
{/* 调用 dispatch 上异步的action */}
actions.app.incrementAsync()}> + Async
)
}
3.Mirror API
Mirror只封装了4个新的api,分别是:
3.1 状态管理
mirror.model({name,initialState,reducers,effects}) : 创建 reducer 和 action , 并作用于 store 。 mirror.hook((action,getState) => {}) : 用于监视 dispatch 出去的 action,相当于store.subscribe(listener)。 设置默认属性
mirror.defaults({
initialState:undefined,// 初始化状态
historyMode:browser,// history对象类型
middleware:[],// 第三方插件
addEffect:(effects) => (name,handler) => { effects[name] = handler } // 自定义异步如何处理
})
3.2 路由管理
actions.routing : 管理Router相关内容,它是一个对象,提供了如下5个方法来手动更新location : push(location) - 往 history 中添加一条记录,并跳转到目标 location; replace(location) - 替换 history 中当前 location; go - 往前或者往后跳转 history中的 location; goForward - 往前跳转一条 location 记录,等价于 go(1); goBack - 往后跳转一条 location 记录,等价于 go(-1);
3.3 渲染启动
connect([mapStateToProps, [mapDispatchToProps], [megeProps], [options]) : 连接 store 和 React render([component], [container], [callback]) : 封装了ReactDOM.render ,它会先创建 store 再渲染页面
五、如何理解Redux
4.1 Store
Store 就是保存数据的地方,可以将其看成一个容器,整个应用只能有一个 Store。Redux 提供 createStore 这个函数,用来生成Store。
import { createStore } from 'redux';
const store = createStore(fn);
上面代码中,createStore 函数接受另一个函数作为参数,返回新生成的 Store 对象。
4.2 State
Store 对象包含所有的数据,如果想要得到某个时点的数据,就要对 Store 生成快照。 这种时点的数据集合,就叫做 State。当前时刻的State,可以通过 store.getState() 拿到。
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
Redux 规定,一个 State 对应一个 View。只要 State 相同,View 就相同。知道 State ,就知道 View 是什么样,反之亦然。
4.3 Action
State 的变化,会导致 View 的变化。但是,用户接触不到 State ,只能接触到 View。所以,State 的变化必须是 View导致的。 Action 就是 View 发出的通知,表示 State 应该要发生变化了。 Action 是一个对象,其中的 type 属性是必须的,表示 Action 的名称。 其他的属性可以自由设置,社区有一个规范可以参考。
const action = {
type:'ADD_TODO',
payloade:'Learn Redux'
};
上面的代码中,Action 的名称是 ADD_TODO,它携带的信息是字符串 ‘Learn Redux’。可以这样理解,Action 描述当前发生的事情。改变 State 的唯一方法,就是使用 Action。 它会运送数据到 Store。
4.4 Actoin Creator
View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。 可以定义一个函数来生成 Action ,这个函数就叫做 Action Creator。如下,addTodo 函数就是一个 Action Creator。
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux')
4.5 store.dispatch()
store.dispatch() 是View 发出 Action 的唯一方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type:'ADD_TODO',
playload:'Learn Redux'
});
上面代码中, store.dispatch() 接受一个 Action 对象作为参数,将它发出去。结合 Action Creator,这段代码可以改写如下:
store.dispatch(addTodo('Learn Redux'));
4.6 Reducer
Store 收到 Action 以后,必须给出一个新的 State , 这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。Reducer 是一个函数,它接收 Action 和当前 State 作为参数,返回一个新的 State。
const reducer = function (state,action) {
// ...
return new_state;
};
整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子。
const defaultState = 0;
cosnt reducer = (state = defaultState,action) => {
switch(action.type) {
case 'ADD':
return state + action.playload;
default:
return state;
}
};
const state = reducer(1,{
type:'ADD',
playload:2
});
上面代码中,reducer函数收到名为 ADD 的 Action 以后,就返回一个新的 State,作为加法的计算结果。其他运算的逻辑(比如减法),也可以根据 Action 的不同来实现。 实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch() 方法会自动出发 Reducer 的自动执行。因此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入 createStore 方法。
import { createStore } from 'redux';
const store = createStore(reducer);
为什么这个函数叫做 Reducer 呢?因为它可以作为数组的 reduce 方法的参数。 请看下面的例子,一系列 Action 对象按照顺序作为一个数组。
const actions = [
{ type:'ADD', playload:0 },
{ type:'ADD', playload:1 },
{ type:'ADD', playload:2 }
];
const total = actions.reduce(reducer,0); // 3
上面代码中,数据 actions 表示依次有三个 Action ,分别是加 0 、加1和加2。数组的 reduce 方法接受 Reducer 函数作为参数,就可以直接得到最终的状态3。
4.7 纯函数
Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是相同的输入,必定得到同样的输出。纯函数是函数式编程的概念,必须遵守以下一些约束。
- 不得改写参数
- 不能调用系统 I/O 的 API
- 不能调用 Date.now() 或者 Math.random() 等不纯的方法,因为每次会得到不一样的结果。
由于 Reducer 是纯函数,就可以保证同样的 State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变State, 必须返回一个全新的对象,参考写法如下:
// State 是一个对象
function reducer(state,action) {
return Object.assign({},state,{ thingToChane });
// 或者
return { ...state, ...newState };
}
// State 是一个数组
function reducer(state,action) {
return [...state,newItem];
}
最好把 State 对象设置成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新的对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象。
4.8 store.subscribe()
Store 允许使用 store.subsrcibe() 方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
显然,只要把 View 的更新函数(对于React项目,就是组件的 render 方法或 setState 方法)放入 listener,就会实现 View 的自动渲染。store.subscribe 方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() => {
console.log(store.getState());
});
unsubscribe();
4.9 Store 的实现
Store提供了三个方法。
store.getState();
store.dispatch();
store.subscribe();
import { createStore } from 'redux';
let { subscribe, dispatch, getState } = createStore(reducer);
createStore 方法还可以接受第二个参数,表示 State 的最初状态。这通常是服务器给出的。
let store = createStore(todoApp, window.STATE_FROM_SERVER);
上面代码中,window.STATE_FROM_SERVER 就是整个应用的状态初始值。注意,如果提供了这个参数,它会覆盖 Reducer 函数的默认初始值。 下面是 createStore 方法的一个简单实现,可以了解一下 Store 是怎么生成的。
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state,action);
listeners.foreEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter( l => l !== listener);
}
};
dispatch({});
return { getState, dispatch, subscribe };
};
4.10 Reducer的拆分
Reducer 函数负责生成 State。 由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大,具体如下面的例子:
const chatReducer = (state = defaultState, action = {}) => {
const { type,playload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign( {},state, {
chatLog : state.chatLog.concat(playload)
});
case CHANGE_STATUS:
return Object.assign({},state,{
statusMessage: playload
});
case CHANGE_USERNAME:
return Object.assign({},state,{
userName:playload
});
default: return state;
}
};
上面的代码中,三种 Action 分别改变 State 的三个属性。
ADD_CHAT : chatLog 属性
CHANGE_STATUS:statusMessage 属性
CHANGE_USERNAME: userName 属性
这三个属性之间没有关系,这提示我们可以把 Reducer 函数拆分。不同函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可。
const chatReducer = (state = defaultState, action ={}) => {
return {
chatLog : chatLog(state.chatLog, action),
statusMessage : statusMessage(state.statusMessage, action),
userName : userName(state.userName,action)
}
}
上面的代码中,Reducer 函数被拆成了三个小函数,每一负责生成对应的属性。这样一拆,Reducer 就易读写多了。而且,这种拆分与 React 应用的结合相吻合: 一个 React 根组件由很多子组件构成。 这就是说,子组件与子 Reducer 完全可以对应。 Redux 提供了一个 combineReducers 方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。
import { combineReducers } from 'redux';
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
});
export default todoApp;
上面的代码通过 combineReducers 方法将三个 Reducer 合并成一个大的函数。这种写法有一个前提,就是 State 的属性名必须与子 Reducer 同名。如果不同名,就要采用下面的写法:
const reducer = combineReducers({
a : doSomethingWithA,
b : processB,
c : c
})
// 等同于
function reducer(state={},action) {
return {
a : doSomethingWith(state.a,action),
b : processB(state.b,action),
c : c(state.c,action)
}
}
总之,combineReducers() 做的就是产生一个整体的 Reducer 函数。 改函数根据 State 的 key 去执行相应的子 Reducer ,并将返回结果合并成一个大的 State 对象, 下面是 combineReducer 的简单实现。
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState,key) => {
nextState[key] = reducers[key](state[key],action);
return nextState;
},
{}
);
};
};
可以把所有子 Reducer 放在一个文件里面,然后统一引入。
import { combineReducers } from 'redux';
import * as reducers from './reducers';
const reducer = combineReducers(reducers);
六、多语言实施
在项目中加入多语的环境和开发规范,主要依赖 react-intl 这个包,外部打包依赖 babel-plugin-react-intl 抽取 id 来实现高效注入和便携开发。
1. 快速开始
1.1 准备多语环境
安装 react-intl 后在项目的主入口注入多语对象,如下编写通用多语组件,该组件可放在 common 目录中。
import React from 'react';
import ReactDOM from 'react-dom';
import mirror,{ connect,withRouter } from 'mirrorx';
import { Locale } from 'tinper-bee';
import { IntlProvider, addLocaleData } from 'react-intl';
const chooseLocale = (locale) => {
let lang;
switch (locale) {
case 'en_US':
// 两个默认的语言包
lang = require('./lang/en_US.js').default;
break;
default:
lang = require('./lang/zh_CN.js').default;
}
return lang;
}
let intlModel = {
name:'intl';
initialState:{
locale:'zh_CN',
localeData:chooseLocale('zh_CN') || {}
},
reducers: {
setLocale(state,locale) {
return {
...state,
locale,
localeData:chooseLocale(locale)
};
}
}
}
// 使用mirror切换语言包,也可采用静态的。
mirror.model(intlModel);
export default connect(state => state.intl)((props) => {
let { children,localeData } = props;
let { tinperLocale, locale, messages } = LocaleData;
return (
{} }
>
{ children }
)
});
1.4 注入多语环境
在页面的主入口使用上面编写的组件包裹,如在路有中使用或者在统一门户组件中使用。
门户组件
import React,{ Component } from ‘react’;
import ReactDOM from ‘react-dom’;
import { actions } from ‘mirrorx’;
import { FormattedMessage } from ‘react-intl’;import LocalePortal from ‘components/LocalePortal’;
import ‘./index.less’;
class MainLayout extends Component {constructor(props) { super(props); this.state = { value:'zh_CN' } } handleChange = value => { this.setState({value}, () => { actions.intl.setLocale(value); }); }; render() { let { children } = this.props; return ( 标题,侧边栏等等 // 业务界面 { children } ); }
}
export default MainLayout;
使用门户组件包裹业务组件,如下包裹路由
import ‘core-js’;
import React from ‘react’;
import mirror, {render, Router, Route
} from ‘mirrorx’;
import MainLayout from ‘components/MainLayout’;
import Routers from ‘./routes’;mirror.defaults({
historyMode:"hash"
});
render (
,
document.getElementById('app')
);
1.3 在业务组件中使用多语
通过上述过程做好准备工作后,业务开发人员就可以在业务界面中加入多语言的代码,且不用操心维护外部如何操作,如下:
import { FormattedMessage,defineMessages,injectIntl } from 'react-intl';
/* 实际使用的时候 有两种方式如下 */
// 1. 标签使用
// 2.API 使用
const locales = defineMessages({
constent : {
id:'Demo.context',
defaultMessage:'我是文本'
}
});
class App extends Component {
render() {
const _this = this;
// 调取 intl 对象
let { intl:{ formatMessage }} = _this.props;
return (
{
formatMessage(locales.content)
}
)
}
}
// 注入 intl 对象
export default injectIntl(App);
2. 项目规约
2.1 项目结构
在自己的模块中增加 lang 目录,这里建议使用 injectIntl API 方式使用 intl,之后在每一个组件中通过高阶方法增加 intl 对象。
├── components
├── lang
│ └── index.js #国际化脚本
├── routes
├── app.js
├── container.js
├── index.html
├── model.js
└── service.js
lang/index.js
import { defineMessages } from ‘react-intl’;
// 不同的组件单独命名
export const orderGridLang = defineMessages({content:{ id:'js.content', defaultMessage:'我是{name}' }, text:{ id:'js.lib.test.text', defaultMessage:'一段字符' },
});
export const searchPanelLang = defineMessages({name:{ id:'js.item.test.name', defaultMessage:'我不是{name}' },
});
注入对象
export default injectIntl(App);
使用定义的语言对象
import { FormattedMessage, defineMessages, injectIntl } from ‘react-intl’;
import { searchPanelLang } from ‘./lang’;class App extends Comonent {
render() { const _this = this; // 调取 intl 对象 let { intl: { formatMessage } } = _this.props; return ( { formatMessage(searchPanelLang.name) } ) }
}
// 注入 intl 对象
export default injectIntl(App);
2.2 命名规范
为避免id重复我们强约束字符的 id 命名规范
- 模块中 :
js.[项目命名].[模块名].[ID名]
- 通用字符命名 : 通用字符如:确认、取消等,常用字符会提炼出来生成通用语言包,命名规范是
js.common.[ID名]
3.自动化构建抽取ID
最开始提到了 babel-plugin-react-intl , 这个插件是用来抽取项目中定义的多语言变量ID的,在ucf.config.js 中添加配置到 babel_plugins 中即可,需要抽取时执行一次即可,不要在开发态下开启此功能。
babel_plugins : [
[require.resolve("babel-plugin-react-intl"),{"messagesDir" : "./intl/"}]
],