update formatters

This commit is contained in:
wxzhang 2022-08-11 22:06:31 +08:00
parent ab9e150470
commit 3c31d3fb67
3 changed files with 388 additions and 308 deletions

View File

@ -250,48 +250,39 @@ function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){
}
}
/**
* 获取指定名称的格式化器函数
*
* 查找逻辑
* - 在当前作用域中查找
*
* - 在当前作用域中查找
* - 在全局作用域中查找
*
* 全局作用域的格式化器优先
*
* @param {*} scope
* @param {*} activeLanguage 当前激活语言名称
* @param {*} name 格式化器名称
* @returns {Function} 格式化函数
*/
function getFormatter(scope,activeLanguage,name){
// 缓存格式化器引用,避免重复检索
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
if(!scope.$cache) resetScopeCache(scope)
if(scope.$cache.activeLanguage === activeLanguage) {
if(name in scope.$cache.formatters) return scope.$cache.formatters[name]
}else{// 当语言切换时清空缓存
resetScopeCache(scope,activeLanguage)
}
// 先在当前作用域中查找,再在全局查找
const targets = [scope.global.formatters,scope.formatters]
for(const target of targets){
// 1. 优先在当前语言查找
if(activeLanguage in target){
let formatters = target[activeLanguage] || {}
if((name in formatters) && isFunction(formatters[name])) {
return scope.$cache.formatters[name] = formatters[name]
}else{ // 如果语言指定了fallback,则在其回退语言中查找
let fallbackLangName = scope.getFallbackLanguage(activeLanguage)
if((fallbackLangName in formatters) && isFunction(formatters[fallbackLangName])) {
return scope.$cache.formatters[name] = formatters[fallbackLangName]
}
}
}
// 2. 全局作用域中查找
let formatters = target["*"] || {}
if((name in formatters) && isFunction(formatters[name])) return scope.$cache.formatters[name] = formatters[name]
const fallbackLanguage = scope.getLanguage(activeLanguage).fallback
// 2. 先在当前作用域中查找,再在全局查找 formatters={$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}
const range = [
scope.activeFormatters,
scope.formatters[fallbackLanguage], // 如果指定了回退语言时,也在该回退语言中查找
scope.global.formatters[activeLanguage], // 适用于activeLanguage全局格式化器
scope.global.formatters["*"], // 适用于所有语言的格式化器
]
for(const formatters of range){
if(!formatters) continue
if(isFunction(formatters[name])) {
return scope.$cache.formatters[name] = formatters[name]
}
}
}
@ -317,8 +308,6 @@ function executeFormatter(value,formatters,scope){
return result
}
/**
*
*
*
* [[格式化器名称,[参数,参数,...]][格式化器名称,[参数,参数,...]]]格式化器转化为
* 格式化器的调用函数链
@ -328,7 +317,6 @@ function executeFormatter(value,formatters,scope){
* @param {*} formatters
* @returns {Array} [(v)=>{...},(v)=>{...},(v)=>{...}]
*
*
*/
function buildFormatters(scope,activeLanguage,formatters){
let results = []
@ -356,7 +344,7 @@ function buildFormatters(scope,activeLanguage,formatters){
}
/**
* 将value经过格式化器处理后返回
* 将value经过格式化器处理后返回的结果
* @param {*} scope
* @param {*} activeLanguage
* @param {*} formatters
@ -578,9 +566,8 @@ function translate(message) {
}
I18nManager.instance = this;
this._settings = deepMerge(defaultLanguageSettings,settings)
this._scopes=[]
this._defaultMessageLoader = null // 默认文本加载器
return I18nManager.instance;
this._scopes=[] // 保存i18nScope实例
this._defaultMessageLoader = null // 默认文本加载器
}
get settings(){ return this._settings }
get scopes(){ return this._scopes }
@ -692,7 +679,9 @@ function translate(message) {
}else{
await Promise.all(requests)
}
}catch{}
}catch(e){
if(this._debug) console.error(`Error while refresh voerkai18n scopes:${e.message}`)
}
}
}

View File

@ -1,78 +1,114 @@
const { isPlainObject,isFunction } = require("./utils")
const { isPlainObject, isFunction, getByPath, deepMixin } = require("./utils");
const DataTypes = ["String","Number","Boolean","Object","Array","Function","Null","Undefined","Symbol","Date","RegExp","Error"];
const DataTypes = [
"String",
"Number",
"Boolean",
"Object",
"Array",
"Function",
"Null",
"Undefined",
"Symbol",
"Date",
"RegExp",
"Error",
];
module.exports = class i18nScope {
constructor(options={},callback){
this._id = options.id || (new Date().getTime().toString()+parseInt(Math.random()*1000))
this._debug = options.debug // 当出错时是否在控制台台输出错误信息
this._languages = options.languages // 当前作用域的语言列表
this._defaultLanguage = options.defaultLanguage || "zh" // 默认语言名称
this._activeLanguage = options.activeLanguage // 当前语言名称
this._default = options.default // 默认语言包
this._messages = options.messages // 当前语言包
this._idMap = options.idMap // 消息id映射列表
this._formatters = options.formatters // 当前作用域的格式化函数列表{<lang>:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}}
this._loaders = options.loaders // 异步加载语言文件的函数列表
this._global = null // 引用全局VoerkaI18n配置注册后自动引用
this._activeFormatters= options.formatters[options.activeLanguage] // 激活使用的格式化器,查找格式化器时在此查找
this._patchMessages = {} // 语言包补丁信息 {<language>:{....},<language>:{....}}
// 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历
this.$cache={
activeLanguage : null,
typedFormatters: {},
formatters : {},
}
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域则自动创建全局VoerkaI18n实例
if(!globalThis.VoerkaI18n){
const { I18nManager } = require("./")
globalThis.VoerkaI18n = new I18nManager({
debug : this._debug,
defaultLanguage: this.defaultLanguage,
activeLanguage : this.activeLanguage,
languages : options.languages,
})
}
this._global = globalThis.VoerkaI18n
// 合并补丁语言包
this._mergePatchedMessages()
this._patch(this._messages,this.activeLanguage)
// 正在加载语言包标识
this._refreshing=false
// 在全局注册作用域
this.register(callback)
}
// 作用域
get id(){return this._id}
// 调试开关
get debug(){return this._debug}
// 默认语言名称
get defaultLanguage(){return this._defaultLanguage}
// 默认语言名称
get activeLanguage(){return this._activeLanguage}
// 默认语言包
get default(){return this._default}
// 当前语言包
get messages(){return this._messages}
// 消息id映射列表
get idMap(){return this._idMap}
// 当前作用域的格式化器 {<lang>:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}}
get formatters(){return this._formatters}
// 当前作用域支持的语言列表[{name,title,fallback}]
get languages(){return this._languages}
// 异步加载语言文件的函数列表
get loaders(){return this._loaders}
// 引用全局VoerkaI18n配置注册后自动引用
get global(){return this._global}
/**
* 在全局注册作用域
* @param {*} callback 注册成功后的回调
*/
register(callback){
if(!isFunction(callback)) callback = ()=>{}
this.global.register(this).then(callback).catch(callback)
}
/**
constructor(options = {}, callback) {
this._id = options.id || Date.now().toString() + parseInt(Math.random() * 1000);
// 当出错时是否在控制台台输出错误信息
this._debug = options.debug == undefined ? process && process.env && process.env.NODE_ENV === "development" : options.debug;
this._languages = options.languages; // 当前作用域的语言列表
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
this._activeLanguage = options.activeLanguage; // 当前语言名称
this._default = options.default; // 默认语言包
this._messages = options.messages; // 当前语言包
this._idMap = options.idMap; // 消息id映射列表
this._formatters = options.formatters; // 当前作用域的格式化函数列表{<lang>:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}}
this._loaders = options.loaders; // 异步加载语言文件的函数列表
this._global = null; // 引用全局VoerkaI18n配置注册后自动引用
this._activeFormatters = options.formatters[options.activeLanguage]; // 激活使用的格式化器,查找格式化器时在此查找
this._patchMessages = {}; // 语言包补丁信息 {<language>:{....},<language>:{....}}
// 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历
this.$cache = {
activeLanguage: null,
typedFormatters: {},
formatters: {},
};
// 如果不存在全局VoerkaI18n实例说明当前Scope是唯一或第一个加载的作用域则自动创建全局VoerkaI18n实例
if (!globalThis.VoerkaI18n) {
const { I18nManager } = require("./");
globalThis.VoerkaI18n = new I18nManager({
debug: this._debug,
defaultLanguage: this.defaultLanguage,
activeLanguage: this.activeLanguage,
languages: options.languages,
});
}
this._global = globalThis.VoerkaI18n;
// 合并补丁语言包
this._mergePatchedMessages();
this._patch(this._messages, this.activeLanguage);
// 正在加载语言包标识
this._refreshing = false;
// 在全局注册作用域
this.register(callback);
}
// 作用域
get id() {
return this._id;
}
// 调试开关
get debug() {
return this._debug;
}
// 默认语言名称
get defaultLanguage() {
return this._defaultLanguage;
}
// 默认语言名称
get activeLanguage() {
return this._activeLanguage;
}
// 默认语言包
get default() {
return this._default;
}
// 当前语言包
get messages() {
return this._messages;
}
// 消息id映射列表
get idMap() {
return this._idMap;
}
// 当前作用域的格式化器 {<lang>:{$types,$options,[格式化器名称]:()=>{},[格式化器名称]:()=>{}}}
get formatters() {
return this._formatters;
}
// 当前作用域支持的语言列表[{name,title,fallback}]
get languages() {
return this._languages;
}
// 异步加载语言文件的函数列表
get loaders() {
return this._loaders;
}
// 引用全局VoerkaI18n配置注册后自动引用
get global() {
return this._global;
}
/**
* 在全局注册作用域
* @param {*} callback 注册成功后的回调
*/
register(callback) {
if (!isFunction(callback)) callback = () => {};
this.global.register(this).then(callback).catch(callback);
}
/**
* 注册格式化器
*
* 格式化器是一个简单的同步函数value=>{...}用来对输入进行格式化后返回结果
@ -87,213 +123,242 @@ module.exports = class i18nScope {
language : 声明该格式化器适用语言
asGlobal : 注册到全局
*/
registerFormatter(name,formatter,{language="*",asGlobal}={}){
if(!isFunction(formatter) || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
language = Array.isArray(language) ? language : (language ? language.split(",") : [])
if(asGlobal){
this.global.registerFormatter(name,formatter,{language})
}else{
language.forEach(lng=>{
if(DataTypes.includes(name)){
this._formatters[lng].$types[name] = formatter
}else{
this._formatters[lng][name] = formatter
}
})
}
}
/**
* 注册默认文本信息加载器
* @param {Function} 必须是异步函数或者是返回Promise
*/
registerDefaultLoader(fn){
this.global.registerDefaultLoader(fn)
}
/**
* 获取指定语言信息
* @param {*} language
* @returns
*/
_getLanguage(language){
let index = this._languages.findIndex(lng=>lng.name==language)
if(index!==-1) return this._languages[index]
}
/**
* 返回是否存在指定的语言
* @param {*} language 语言名称
* @returns
*/
hasLanguage(language){
return this._languages.indexOf(lang=>lang.name==language)!==-1
}
/**
* 获取指定语言的回退语言名称
registerFormatter(name, formatter, { language = "*", asGlobal } = {}) {
if (!isFunction(formatter) || typeof name !== "string") {
throw new TypeError("Formatter must be a function");
}
language = Array.isArray(language)
? language
: language
? language.split(",")
: [];
if (asGlobal) {
this.global.registerFormatter(name, formatter, { language });
} else {
language.forEach((lng) => {
if (DataTypes.includes(name)) {
this._formatters[lng].$types[name] = formatter;
} else {
this._formatters[lng][name] = formatter;
}
});
}
}
/**
* 注册默认文本信息加载器
* @param {Function} 必须是异步函数或者是返回Promise
*/
registerDefaultLoader(fn) {
this.global.registerDefaultLoader(fn);
}
/**
* 获取指定语言信息
* @param {*} language
* @returns
*/
getLanguage(language) {
let index = this._languages.findIndex((lng) => lng.name == language);
if (index !== -1) return this._languages[index];
}
/**
* 返回是否存在指定的语言
* @param {*} language 语言名称
* @returns
*/
hasLanguage(language) {
return this._languages.indexOf((lang) => lang.name == language) !== -1;
}
/**
* 回退到默认语言
*/
_fallback() {
this._messages = this._default;
this._activeLanguage = this.defaultLanguage;
}
/**
* 当切换语言时格式化器应该切换到对应语言的格式化器
*
* 如果没有指定则总是回退到到默认语言
* 重要需要处理
* $options参数采用合并继承机制
*
* 但是不支持回退链
*
* 可以在配置中指定回退语言
* // settings.json
* {
* languages:[
* {name:"zh"},
* {name:"cht",fallback:"zh"}, //繁体中文可以回退到简体中文,
* {name:"en"},
* ]
* }
*
* @param {*} language
* @returns {Object} 语言信息数据 {name,title,fallback,...}
*/
getFallbackLanguage(language){
let lang = this._getLanguage(language)
if(lang){
return this.hasLanguage(lang.fallback) ? lang.fallback : this.defaultLanguage
}
}
* @param {*} language
*/
async _changeFormatters(newLanguage) {
try {
if (newLanguage in this._formatters) {
let loader = this._formatters[newLanguage];
if (isPlainObject(loader)) {
this._activeFormatters = loader;
} else if (isFunction(loader)) {
this._activeFormatters = (await loader()).default;
}
// 合并生成格式化器的配置参数,当执行格式化器时该参数将被传递给格式化器
this._generateFormatterOptions(newLanguage)
} else {
if (this._debug) console.warn(`Not configured <${newLanguage}> formatters.`);
}
} catch (e) {
if (this._debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`);
}
}
/**
* 回退到默认语言
* 生成格式化器的配置参数该参数由以下合并而成
* - global.formatters[*].$options
* - global.formatters[language].$options
* - scope.activeFormattes.$options 优先
*/
_fallback(){
this._messages = this._default
this._activeLanguage = this.defaultLanguage
_generateFormatterOptions(language){
let options = Object.assign({},getByPath(global.formatters,`*.$options`,{}))
deepMixin(options,getByPath(global.formatters,`${language}.$options`,{}))
deepMixin(options,getByPath(scope.activeFormattes,"$options",{}))
return this._activeFormatterOptions = options
}
/**
* 当切换语言时格式化器应该切换到对应语言的格式化器
* @param {*} language
*/
async _loadFormatters(newLanguage){
try{
if(newLanguage in this._formatters){
let loader = this._formatters[newLanguage]
if(isPlainObject(loader)){
this._activeFormatters = loader
}else if(isFunction(loader)){
this._activeFormatters = (await loader()).default
}
}else{
if(this._debug) console.warn(`Not configured <${newLanguage}> formatters.`)
}
}catch(e){
if(this._debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`)
}
}
/**
* 刷新当前语言包
* @param {*} newLanguage
*/
async refresh(newLanguage){
this._refreshing = true
if(!newLanguage) newLanguage = this.activeLanguage
// 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可
if(newLanguage === this.defaultLanguage){
this._messages = this._default
await this._patch(this._messages,newLanguage) // 异步补丁
this._loadFormatters(newLanguage)
return
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[newLanguage]
try{
if(isPlainObject(loader)){
this._messages = loader
await this._patch(this._messages,newLanguage)
}else if(isFunction(loader)){
this._messages = (await loader()).default
this._activeLanguage = newLanguage
await this._patch(this._messages,newLanguage)
}else if(isFunction(this.global.defaultMessageLoader)){// 如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this)
this._messages = Object.assign({},this._default,loadedMessages)
this._activeLanguage = newLanguage
}else{
this._fallback()
}
// 应该切换到对应语言的格式化器
this._loadFormatters(newLanguage)
}catch(e){
if(this._debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`)
this._fallback()
}finally{
this._refreshing = false
}
}
/**
* 当指定了默认语言包加载器后会从服务加载语言补丁包来更新本地的语言包
*
* 补丁包会自动存储到本地的LocalStorage中
*
* @param {*} messages
* @param {*} newLanguage
* @returns
*/
async _patch(messages,newLanguage){
if(!isFunction(this.global.loadMessagesFromDefaultLoader)) return
try{
let pachedMessages = await this.global.loadMessagesFromDefaultLoader(newLanguage,this)
if(isPlainObject(pachedMessages)){
Object.assign(messages,pachedMessages)
this._savePatchedMessages(pachedMessages,newLanguage)
}
}catch(e){
if(this._debug) console.error(`Error while loading <${newLanguage}> messages from remote:${error.message}`)
}
}
/**
* 从本地存储中读取语言包补丁合并到当前语言包中
*/
_mergePatchedMessages(){
let patchedMessages= this._getPatchedMessages(this.activeLanguage)
if(isPlainObject(patchedMessages)){
Object.assign(this._messages,patchedMessages)
}
}
/**
* 将读取的补丁包保存到本地的LocalStorage中
*
* 为什么要保存到本地的LocalStorage中
*
* 因为默认语言是静态嵌入到源码中的而加载语言包补丁是延后异步的
* 当应用启动第一次就会渲染出来的是没有打过补丁的内容
*
* - 如果还需要等待从服务器加载语言补丁合并后再渲染会影响速度
* - 如果不等待从服务器加载语言补丁就渲染则会先显示未打补丁的内容然后在打完补丁后再对应用进行重新渲染生效
* 这明显不是个好的方式
*
* 因此采用的方式是
* - 加载语言包补丁后将之保存到到本地的LocalStorage中
* - 当应用加载时会查询是否存在补丁如果存在就会合并渲染
*
* @param {*} messages
*/
_savePatchedMessages(messages,language){
try{
if(globalThis.localStorage){
globalThis.localStorage.setItem(`voerkai18n_${this.id}_${language}_patched_messages`, JSON.stringify(messages));
}
}catch(e){
if(this.$cache._debug) console.error("Error while save voerkai18n patched messages:",e.message)
}
}
/**
* 从本地缓存中读取补丁语言包
* @param {*} language
* @returns
*/
_getPatchedMessages(language){
try{
return JSON.parse(localStorage.getItem(`voerkai18n_${this.id}_${language}_patched_messages`))
}catch(e){
return {}
}
}
// 以下方法引用全局VoerkaI18n实例的方法
get on(){return this._global.on.bind(this._global)}
get off(){return this._global.off.bind(this._global)}
get offAll(){return this._global.offAll.bind(this._global)}
get change(){return this._global.change.bind(this._global)}
}
/**
* 刷新当前语言包
* @param {*} newLanguage
*/
async refresh(newLanguage) {
this._refreshing = true;
if (!newLanguage) newLanguage = this.activeLanguage;
// 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可
if (newLanguage === this.defaultLanguage) {
this._messages = this._default;
await this._patch(this._messages, newLanguage); // 异步补丁
this._changeFormatters(newLanguage);
return;
}
// 非默认语言需要异步加载语言包文件,加载器是一个异步函数
// 如果没有加载器,则无法加载语言包,因此回退到默认语言
let loader = this.loaders[newLanguage];
try {
if (isPlainObject(loader)) {
this._messages = loader;
await this._patch(this._messages, newLanguage);
} else if (isFunction(loader)) {
this._messages = (await loader()).default;
this._activeLanguage = newLanguage;
await this._patch(this._messages, newLanguage);
} else if (isFunction(this.global.defaultMessageLoader)) {
// 如果该语言没有指定加载器,则使用全局配置的默认加载器
const loadedMessages =
await this.global.loadMessagesFromDefaultLoader(
newLanguage,
this
);
this._messages = Object.assign(
{},
this._default,
loadedMessages
);
this._activeLanguage = newLanguage;
} else {
this._fallback();
}
// 应该切换到对应语言的格式化器
this._changeFormatters(newLanguage);
} catch (e) {
if (this._debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
} finally {
this._refreshing = false;
}
}
/**
* 当指定了默认语言包加载器后会从服务加载语言补丁包来更新本地的语言包
*
* 补丁包会自动存储到本地的LocalStorage中
*
* @param {*} messages
* @param {*} newLanguage
* @returns
*/
async _patch(messages, newLanguage) {
if (!isFunction(this.global.loadMessagesFromDefaultLoader)) return;
try {
let pachedMessages =
await this.global.loadMessagesFromDefaultLoader(
newLanguage,
this
);
if (isPlainObject(pachedMessages)) {
Object.assign(messages, pachedMessages);
this._savePatchedMessages(pachedMessages, newLanguage);
}
} catch (e) {
if (this._debug) console.error(`Error while loading <${newLanguage}> messages from remote:${error.message}`);
}
}
/**
* 从本地存储中读取语言包补丁合并到当前语言包中
*/
_mergePatchedMessages() {
let patchedMessages = this._getPatchedMessages(this.activeLanguage);
if (isPlainObject(patchedMessages)) {
Object.assign(this._messages, patchedMessages);
}
}
/**
* 将读取的补丁包保存到本地的LocalStorage中
*
* 为什么要保存到本地的LocalStorage中
*
* 因为默认语言是静态嵌入到源码中的而加载语言包补丁是延后异步的
* 当应用启动第一次就会渲染出来的是没有打过补丁的内容
*
* - 如果还需要等待从服务器加载语言补丁合并后再渲染会影响速度
* - 如果不等待从服务器加载语言补丁就渲染则会先显示未打补丁的内容然后在打完补丁后再对应用进行重新渲染生效
* 这明显不是个好的方式
*
* 因此采用的方式是
* - 加载语言包补丁后将之保存到到本地的LocalStorage中
* - 当应用加载时会查询是否存在补丁如果存在就会合并渲染
*
* @param {*} messages
*/
_savePatchedMessages(messages, language) {
try {
if (globalThis.localStorage) {
globalThis.localStorage.setItem(
`voerkai18n_${this.id}_${language}_patched_messages`,
JSON.stringify(messages)
);
}
} catch (e) {
if (this.$cache._debug)
console.error(
"Error while save voerkai18n patched messages:",
e.message
);
}
}
/**
* 从本地缓存中读取补丁语言包
* @param {*} language
* @returns
*/
_getPatchedMessages(language) {
try {
return JSON.parse(
localStorage.getItem(
`voerkai18n_${this.id}_${language}_patched_messages`
)
);
} catch (e) {
return {};
}
}
// 以下方法引用全局VoerkaI18n实例的方法
get on() {
return this._global.on.bind(this._global);
}
get off() {
return this._global.off.bind(this._global);
}
get offAll() {
return this._global.offAll.bind(this._global);
}
get change() {
return this._global.change.bind(this._global);
}
};

View File

@ -172,6 +172,31 @@ function toNumber(value,defualt=0) {
return prefix + result.join("") + suffix
}
/**
* 根据路径获取指定值
*
* getByPath({a:{b:1}},"a.b") == 1
* getByPath({a:{b:1}},"a.c",2) == 2
*
* @param {*} obj
* @param {*} path 使用.分割的路径
* @param {*} defaultValue 默认值
* @returns
*/
function getByPath(obj,path,defaultValue){
if(typeof(obj)!="object") return defaultValue
let paths = path.split(".")
let cur = obj
for(let key of paths){
if(typeof(cur)=="object" && key in cur ){
cur = cur[key]
}else{
return defaultValue
}
}
return cur
}
/**
* 返回value相对rel的相对时间
*
@ -191,6 +216,7 @@ module.exports ={
isNothing,
deepMerge,
deepMixin,
getByPath,
getDataTypeName,
toDate,
toNumber,