大家好,今天给大家带来的干货是vue-i18n(v7.3.0)的源码分析。vue-i18n是用于多语言适配的vue插件,主要用于前端项目的国际化应用
这里是vue-18n的gayhub地址 摸我
首先还是先看看作者给我们的一个简单的例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>getting started</title>
<script src="../../node_modules/vue/dist/vue.min.js"></script>
<script src="../../dist/vue-i18n.min.js"></script>
</head>
<body>
<div id="app">
<p>{{ $t("message.hello") }}</p>
</div>
<script>
var messages = {
en: {
message: {
hello: 'hello world'
}
},
ja: {
message: {
hello: 'こんにちは、世界'
}
}
}
Vue.use(VueI18n)
var i18n = new VueI18n({
locale: 'ja',
messages: messages
})
new Vue({ i18n: i18n }).$mount('#app')
</script>
</body>
</html>
从这个简单的小例子中,我们可以看到vue-i18n的使用非常的简单,我们只需要定义好对应的语言包messages,然后设置一个默认语言类型locale,然后实例化出一个i18n对象并传入我们的vue实例就可以愉快的使用起来
ps:插值表达式中的$t就是vue-i18n暴露给用户的API
接下来,我们就一起看看vue-i18n的源码到底是怎么样的
首先还是先看看目录结构,我们可以看到,源码目录src中有9个文件,而这9个文件便构成了vue-i18n的全部内容
我们先看看入口文件 index.js
这个文件定义了并且导出一个名为VueI18n的类,并在类上定义了availabilities,install,version三个静态属性,以便通过类名直接访问
/* @flow */
// 导入相应的资源
import { install, Vue } from './install'
import {
warn,
isNull,
parseArgs,
fetchChoice,
isPlainObject,
isObject,
looseClone,
remove,
canUseDateTimeFormat,
canUseNumberFormat
} from './util'
import BaseFormatter from './format'
import I18nPath from './path'
import type { PathValue } from './path'
// 定义并且一个VueI18n类
export default class VueI18n {
// 定义静态属性,在后面有赋值操作
static install: () => void
static version: string
static availabilities: IntlAvailability
// 私有变量
_vm: any
_formatter: Formatter
_root: ?I18n
_sync: boolean
_fallbackRoot: boolean
_missing: ?MissingHandler
_exist: Function
_watcher: any
_i18nWatcher: Function
_silentTranslationWarn: boolean
_dateTimeFormatters: Object
_numberFormatters: Object
_path: I18nPath
_dataListeners: Array<any>
// 构造函数,默认参数是一个空对象
constructor (options: I18nOptions = {}) {
// 局部变量,如果默认参数没有传值,则使用默认值
// locale 用于指定页面使用的语言类型,默认是英文
const locale: Locale = options.locale || 'en-US'
// fallbackLocal TODO
const fallbackLocale: Locale = options.fallbackLocale || 'en-US'
// message 就是用户定义的语言包,默认是一个空对像
const messages: LocaleMessages = options.messages || {}
// dateTimeFormats 日期格式
const dateTimeFormats = options.dateTimeFormats || {}
// numberFormats 数字格式
const numberFormats = options.numberFormats || {}
// _vm 默认的vm对象
this._vm = null
// _formatter 可自定义格式化
this._formatter = options.formatter || new BaseFormatter()
// missing TODO
this._missing = options.missing || null
// _root 保存根节点
this._root = options.root || null
this._sync = options.sync === undefined ? true : !!options.sync
// fallbackRoot 保存中fallback语言包的根节点
this._fallbackRoot = options.fallbackRoot === undefined
? true
: !!options.fallbackRoot
this._silentTranslationWarn = options.silentTranslationWarn === undefined
? false
: !!options.silentTranslationWarn
this._dateTimeFormatters = {}
this._numberFormatters = {}
this._path = new I18nPath()
this._dataListeners = []
// _exist方法,用于判断某个key是否存在于这个messages语言包中
// 主要通过path模块的getPathValue方法实现,后面会详细说明,在此只需要知道他的用途
this._exist = (message: Object, key: Path): boolean => {
if (!message || !key) { return false }
return !isNull(this._path.getPathValue(message, key))
}
// 初始化vm
this._initVM({
locale,
fallbackLocale,
messages,
dateTimeFormats,
numberFormats
})
}
_initVM (data: {
locale: Locale,
fallbackLocale: Locale,
messages: LocaleMessages,
dateTimeFormats: DateTimeFormats,
numberFormats: NumberFormats
}): void {
const silent = Vue.config.silent
Vue.config.silent = true
// 实例化一个vue对象,将传入的参数变成vue中的响应式数据,大部分vue的插件都使用这种做法,比如vuex
this._vm = new Vue({ data })
Vue.config.silent = silent
}
// 监听vm数据的变化,将每次的vm数据push到监听队列中
subscribeDataChanging (vm: any): void {
this._dataListeners.push(vm)
}
// 取消监听vm数据的变化
unsubscribeDataChanging (vm: any): void {
remove(this._dataListeners, vm)
}
// 监听i18n中定义的数据的变化,如果数据变化了,就强制更新页面
watchI18nData (): Function {
const self = this
// 利用vue中$watch的api,当this._vm的数据($data)发生改变,就会触发监听队列中所有vm的视图的变化
return this._vm.$watch('$data', () => {
let i = self._dataListeners.length
// 遍历所有的vm,再利用vue中的$forceUpdate的api,实现强制更新
while (i--) {
Vue.nextTick(() => {
self._dataListeners[i] && self._dataListeners[i].$forceUpdate()
})
}
}, { deep: true })
}
// 监听根节点locale的变化
watchLocale (): ?Function {
/* istanbul ignore if */
if (!this._sync || !this._root) { return null }
// 获取当前的vm
const target: any = this._vm
// 注意:这里是根节点的vm
return this._root.vm.$watch('locale', (val) => {
// 设置当前vm的locale,并强制更新
target.$set(target, 'locale', val)
target.$forceUpdate()
}, { immediate: true })
}
// 获取当前的vm
get vm (): any { return this._vm }
// 获取当前vm的messages属性的内容,这里使用了looseClone对js对象进行拷贝,详细会在讲解util.js中分析
get messages (): LocaleMessages { return looseClone(this._getMessages()) }
// 获取当前vm的dateTimeFormats属性的内容
get dateTimeFormats (): DateTimeFormats { return looseClone(this._getDateTimeFormats()) }
// 获取当前vm的numberFormats属性的内容
get numberFormats (): NumberFormats { return looseClone(this._getNumberFormats()) }
// 获取当前vm的locale属性的内容
get locale (): Locale { return this._vm.locale }
// 设置当前vm的locale属性的内容
set locale (locale: Locale): void {
this._vm.$set(this._vm, 'locale', locale)
}
// 获取当前vm的fallbackLocale属性的内容
get fallbackLocale (): Locale { return this._vm.fallbackLocale }
// 设置当前vm的fallbackLocale属性的内容
set fallbackLocale (locale: Locale): void {
this._vm.$set(this._vm, 'fallbackLocale', locale)
}
// 同上
get missing (): ?MissingHandler { return this._missing }
set missing (handler: MissingHandler): void { this._missing = handler }
// 同上
get formatter (): Formatter { return this._formatter }
set formatter (formatter: Formatter): void { this._formatter = formatter }
// 同上
get silentTranslationWarn (): boolean { return this._silentTranslationWarn }
set silentTranslationWarn (silent: boolean): void { this._silentTranslationWarn = silent }
_getMessages (): LocaleMessages { return this._vm.messages }
_getDateTimeFormats (): DateTimeFormats { return this._vm.dateTimeFormats }
_getNumberFormats (): NumberFormats { return this._vm.numberFormats }
// 提示方法
_warnDefault (locale: Locale, key: Path, result: ?any, vm: ?any): ?string {
if (!isNull(result)) { return result }
// 如果missing存在,则执行
if (this.missing) {
this.missing.apply(null, [locale, key, vm])
} else {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(
`Cannot translate the value of keypath '${key}'. ` +
'Use the value of keypath as default.'
)
}
}
return key
}
// 判断是否有回退方案
_isFallbackRoot (val: any): boolean {
return !val && !isNull(this._root) && this._fallbackRoot
}
// 获取message中某个key的值
_interpolate (
locale: Locale,
message: LocaleMessageObject,
key: Path,
host: any,
interpolateMode: string,
values: any
): any {
if (!message) { return null }
// 利用getPathValue方法获取message中key的值
const pathRet: PathValue = this._path.getPathValue(message, key)
// 如果获取的值是一个数组,则返回这个数组
if (Array.isArray(pathRet)) { return pathRet }
let ret: mixed
// 如果获取的值是空
if (isNull(pathRet)) {
/* istanbul ignore else */
if (isPlainObject(message)) {
ret = message[key]
if (typeof ret !== 'string') {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Value of key '${key}' is not a string!`)
}
return null
}
} else {
return null
}
} else {
/* istanbul ignore else */
if (typeof pathRet === 'string') {
// 返回获取的值
ret = pathRet
} else {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Value of key '${key}' is not a string!`)
}
return null
}
}
// Check for the existance of links within the translated string
// 解析message中有@:的情况
if (ret.indexOf('@:') >= 0) {
ret = this._link(locale, message, ret, host, interpolateMode, values)
}
// 如果values为false,则返回ret,否则返回this._render的执行结果
return !values ? ret : this._render(ret, interpolateMode, values)
}
// @:的情况
_link (
locale: Locale,
message: LocaleMessageObject,
str: string,
host: any,
interpolateMode: string,
values: any
): any {
let ret: string = str
// Match all the links within the local
// We are going to replace each of
// them with its translation
// 匹配@:(link)
const matches: any = ret.match(/(@:[\w\-_|.]+)/g)
// 遍历匹配的数组
for (const idx in matches) {
// ie compatible: filter custom array
// prototype method
if (!matches.hasOwnProperty(idx)) {
continue
}
// 获取每个link
const link: string = matches[idx]
// Remove the leading @:
// 除去头部的 @:,将得到的linkPlaceholder作为_interpolate方法的key继续解析
const linkPlaceholder: string = link.substr(2)
// Translate the link
let translated: any = this._interpolate(
locale, message, linkPlaceholder, host,
interpolateMode === 'raw' ? 'string' : interpolateMode,
interpolateMode === 'raw' ? undefined : values
)
if (this._isFallbackRoot(translated)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to translate the link placeholder '${linkPlaceholder}' with root locale.`)
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
const root: any = this._root
translated = root._translate(
root._getMessages(), root.locale, root.fallbackLocale,
linkPlaceholder, host, interpolateMode, values
)
}
// 获取装换的值
translated = this._warnDefault(locale, linkPlaceholder, translated, host)
// Replace the link with the translated
// 替换数据
ret = !translated ? ret : ret.replace(link, translated)
}
return ret
}
// 解析表达式,利用的是this._formatter.interpolate方法
_render (message: string, interpolateMode: string, values: any): any {
const ret = this._formatter.interpolate(message, values)
// if interpolateMode is **not** 'string' ('row'),
// return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
return interpolateMode === 'string' ? ret.join('') : ret
}
// 翻译语言方法
_translate (
messages: LocaleMessages,
locale: Locale,
fallback: Locale,
key: Path,
host: any,
interpolateMode: string,
args: any
): any {
// 通过_interpolate方法获取结果
let res: any =
this._interpolate(locale, messages[locale], key, host, interpolateMode, args)
if (!isNull(res)) { return res }
// 如果获取的结果是null,则使用fallback语言包
res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args)
if (!isNull(res)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to translate the keypath '${key}' with '${fallback}' locale.`)
}
return res
} else {
return null
}
}
_t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
if (!key) { return '' }
// 解析传入的values参数
const parsedArgs = parseArgs(...values)
// 获取locale
const locale: Locale = parsedArgs.locale || _locale
// 调用_translate,设置模式为string,并将parsedArgs.params传入
const ret: any = this._translate(
messages, locale, this.fallbackLocale, key,
host, 'string', parsedArgs.params
)
// 判断是否有回退方案,这个适用于当子组件找不到对应的语言包字段的时候,往根节点查找是否有相应的字段
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to translate the keypath '${key}' with root locale.`)
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
// 调用根节点的t方法
return this._root.t(key, ...values)
} else {
return this._warnDefault(locale, key, ret, host)
}
}
t (key: Path, ...values: any): TranslateResult {
// 调用_t方法实现
return this._t(key, this.locale, this._getMessages(), null, ...values)
}
_i (key: Path, locale: Locale, messages: LocaleMessages, host: any, values: Object): any {
// 设置interpolateMode为raw
const ret: any =
this._translate(messages, locale, this.fallbackLocale, key, host, 'raw', values)
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to interpolate the keypath '${key}' with root locale.`)
}
if (!this._root) { throw Error('unexpected error') }
return this._root.i(key, locale, values)
} else {
return this._warnDefault(locale, key, ret, host)
}
}
// 处理内置i18n组件逻辑
i (key: Path, locale: Locale, values: Object): TranslateResult {
/* istanbul ignore if */
if (!key) { return '' }
if (typeof locale !== 'string') {
locale = this.locale
}
return this._i(key, locale, this._getMessages(), null, values)
}
_tc (
key: Path,
_locale: Locale,
messages: LocaleMessages,
host: any,
choice?: number,
...values: any
): any {
if (!key) { return '' }
if (choice === undefined) {
choice = 1
}
// 先调用this._t,在使用fetchChoice包装,fetchChoice具体在util中会详细分析
return fetchChoice(this._t(key, _locale, messages, host, ...values), choice)
}
tc (key: Path, choice?: number, ...values: any): TranslateResult {
return this._tc(key, this.locale, this._getMessages(), null, choice, ...values)
}
_te (key: Path, locale: Locale, messages: LocaleMessages, ...args: any): boolean {
const _locale: Locale = parseArgs(...args).locale || locale
return this._exist(messages[_locale], key)
}
te (key: Path, locale?: Locale): boolean {
return this._te(key, this.locale, this._getMessages(), locale)
}
// 获取语言包
getLocaleMessage (locale: Locale): LocaleMessageObject {
return looseClone(this._vm.messages[locale] || {})
}
// 设置语言包,或者用于热更新:i18n.setLocaleMessage('en', require('./en').default)
setLocaleMessage (locale: Locale, message: LocaleMessageObject): void {
this._vm.messages[locale] = message
}
mergeLocaleMessage (locale: Locale, message: LocaleMessageObject): void {
this._vm.messages[locale] = Vue.util.extend(this._vm.messages[locale] || {}, message)
}
getDateTimeFormat (locale: Locale): DateTimeFormat {
return looseClone(this._vm.dateTimeFormats[locale] || {})
}
setDateTimeFormat (locale: Locale, format: DateTimeFormat): void {
this._vm.dateTimeFormats[locale] = format
}
mergeDateTimeFormat (locale: Locale, format: DateTimeFormat): void {
this._vm.dateTimeFormats[locale] = Vue.util.extend(this._vm.dateTimeFormats[locale] || {}, format)
}
// 本地格式化时间
_localizeDateTime (
value: number | Date,
locale: Locale,
fallback: Locale,
dateTimeFormats: DateTimeFormats,
key: string
): ?DateTimeFormatResult {
// 获取语言包 & 对应的日期格式对象
let _locale: Locale = locale
let formats: DateTimeFormat = dateTimeFormats[_locale]
// fallback locale
// 判断为空
if (isNull(formats) || isNull(formats[key])) {
if (process.env.NODE_ENV !== 'production') {
warn(`Fall back to '${fallback}' datetime formats from '${locale} datetime formats.`)
}
_locale = fallback
formats = dateTimeFormats[_locale]
}
// 判断为空
if (isNull(formats) || isNull(formats[key])) {
return null
} else {
// 如果不为空,本地获取对应key下的format规则
const format: ?DateTimeFormatOptions = formats[key]
// 生成相应的id
const id = `${_locale}__${key}`
// 获取本地_dateTimeFormatters对象的缓存
let formatter = this._dateTimeFormatters[id]
// 如果没有的缓存规则,则实例化Intl.DateTimeFormat类,获取对应的规则,并缓存在_dateTimeFormatters对象中
if (!formatter) {
formatter = this._dateTimeFormatters[id] = new Intl.DateTimeFormat(_locale, format)
}
// 调用formatter的format方法,得到格式化后的日期
return formatter.format(value)
}
}
_d (value: number | Date, locale: Locale, key: ?string): DateTimeFormatResult {
/* istanbul ignore if */
// 判断是支持Intl.dateTimeFormat方法
if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.dateTimeFormat) {
warn('Cannot format a Date value due to not support Intl.DateTimeFormat.')
return ''
}
// 如果key为空,则直接实例化Intl.DateTimeFormat类,并调用api:format,得到相应的日期格式
if (!key) {
return new Intl.DateTimeFormat(locale).format(value)
}
// 如果key不为空,则调用本地的格式化规则,_localizeDateTime方法
const ret: ?DateTimeFormatResult =
this._localizeDateTime(value, locale, this.fallbackLocale, this._getDateTimeFormats(), key)
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production') {
warn(`Fall back to datetime localization of root: key '${key}' .`)
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
return this._root.d(value, key, locale)
} else {
return ret || ''
}
}
// 日期格式化的入口
d (value: number | Date, ...args: any): DateTimeFormatResult {
let locale: Locale = this.locale
let key: ?string = null
// 如果args的长度唯一,并且值为字符串,则设置为key
if (args.length === 1) {
if (typeof args[0] === 'string') {
key = args[0]
} else if (isObject(args[0])) {
// 如果值为对象,则解构这个对象
if (args[0].locale) {
locale = args[0].locale
}
if (args[0].key) {
key = args[0].key
}
}
} else if (args.length === 2) {
// 如果长度为2,则设置key和locale
if (typeof args[0] === 'string') {
key = args[0]
}
if (typeof args[1] === 'string') {
locale = args[1]
}
}
// 调用_d方法
return this._d(value, locale, key)
}
getNumberFormat (locale: Locale): NumberFormat {
return looseClone(this._vm.numberFormats[locale] || {})
}
setNumberFormat (locale: Locale, format: NumberFormat): void {
this._vm.numberFormats[locale] = format
}
mergeNumberFormat (locale: Locale, format: NumberFormat): void {
this._vm.numberFormats[locale] = Vue.util.extend(this._vm.numberFormats[locale] || {}, format)
}
_localizeNumber (
value: number,
locale: Locale,
fallback: Locale,
numberFormats: NumberFormats,
key: string
): ?NumberFormatResult {
let _locale: Locale = locale
let formats: NumberFormat = numberFormats[_locale]
// fallback locale
// 判断为空
if (isNull(formats) || isNull(formats[key])) {
if (process.env.NODE_ENV !== 'production') {
warn(`Fall back to '${fallback}' number formats from '${locale} number formats.`)
}
_locale = fallback
formats = numberFormats[_locale]
}
// 判断为空
if (isNull(formats) || isNull(formats[key])) {
return null
} else {
// 获取对应的key下的数字格式
const format: ?NumberFormatOptions = formats[key]
// 生成id
const id = `${_locale}__${key}`
// 获取相应Id下的缓存
let formatter = this._numberFormatters[id]
// 如果没有缓存,则实例化Intl.NumberFormat类,并且缓存在_numberFormatters对象中
if (!formatter) {
formatter = this._numberFormatters[id] = new Intl.NumberFormat(_locale, format)
}
// 根据得到的formatter,调用format方法得到相应的数字
return formatter.format(value)
}
}
_n (value: number, locale: Locale, key: ?string): NumberFormatResult {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.numberFormat) {
warn('Cannot format a Date value due to not support Intl.NumberFormat.')
return ''
}
// 如果没有key,则直接利用Intl.NumberFormat获取相应的格式
if (!key) {
return new Intl.NumberFormat(locale).format(value)
}
// 如果有key,则调用本地的格式化规则,_localizeNumber方法
const ret: ?NumberFormatResult =
this._localizeNumber(value, locale, this.fallbackLocale, this._getNumberFormats(), key)
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production') {
warn(`Fall back to number localization of root: key '${key}' .`)
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
return this._root.n(value, key, locale)
} else {
return ret || ''
}
}
// 数字格式化的入口
n (value: number, ...args: any): NumberFormatResult {
let locale: Locale = this.locale
let key: ?string = null
// 解析参数,与上面的方法d类似
if (args.length === 1) {
if (typeof args[0] === 'string') {
key = args[0]
} else if (isObject(args[0])) {
if (args[0].locale) {
locale = args[0].locale
}
if (args[0].key) {
key = args[0].key
}
}
} else if (args.length === 2) {
if (typeof args[0] === 'string') {
key = args[0]
}
if (typeof args[1] === 'string') {
locale = args[1]
}
}
// 调用_n
return this._n(value, locale, key)
}
}
VueI18n.availabilities = {
dateTimeFormat: canUseDateTimeFormat,
numberFormat: canUseNumberFormat
}
VueI18n.install = install
VueI18n.version = '__VERSION__'
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(VueI18n)
}
其实index.js就已经包含了vue-i18n的主要内容了,看明白了这个文件就基本明白了vue-i18n的大体思路了
vue-i18n还有其他优秀的地方,比如它提供了组件和指令,可以供使用者更加方便和灵活的调用
接下来先看看component.js的代码,看看i18n组件是怎么实现的
/* @flow */
// 定义i18n组件
import { warn } from './util'
export default {
name: 'i18n',
functional: true,
props: {
tag: {
type: String,
default: 'span'
},
path: {
type: String,
required: true
},
locale: {
type: String
},
places: {
type: [Array, Object]
}
},
render (h: Function, { props, data, children, parent }: Object) {
// 获取父组件i18n实例
const i18n = parent.$i18n
// 收集子组件
children = (children || []).filter(child => {
return child.tag || (child.text = child.text.trim())
})
if (!i18n) {
if (process.env.NODE_ENV !== 'production') {
warn('Cannot find VueI18n instance!')
}
return children
}
// 获取路径,语言包,其他参数
const path: Path = props.path
const locale: ?Locale = props.locale
const params: Object = {}
const places: Array<any> | Object = props.places || {}
// 判断是否有places占位符
const hasPlaces: boolean = Array.isArray(places)
? places.length > 0
: Object.keys(places).length > 0
const everyPlace: boolean = children.every(child => {
if (child.data && child.data.attrs) {
const place = child.data.attrs.place
return (typeof place !== 'undefined') && place !== ''
}
})
if (hasPlaces && children.length > 0 && !everyPlace) {
warn('If places prop is set, all child elements must have place prop set.')
}
// 提取组件本身的place
if (Array.isArray(places)) {
places.forEach((el, i) => {
params[i] = el
})
} else {
Object.keys(places).forEach(key => {
params[key] = places[key]
})
}
// 提取子组件的place
children.forEach((child, i: number) => {
const key: string = everyPlace
? `${child.data.attrs.place}`
: `${i}`
params[key] = child
})
// 将参数作为createElement方法的参数传入
return h(props.tag, data, i18n.i(path, locale, params))
}
}
i18组件不仅仅提供了vue组件的基本功能,还提供了place占位符,比较灵活
vue-i18n提供了v-t指令,主要就是实现了一个vue组件
/* @flow */
// 指令功能,用户可通过指令v-t来进行多语言操作
import { warn, isPlainObject, looseEqual } from './util'
// 定义了vue指令中的bind方法
export function bind (el: any, binding: Object, vnode: any): void {
t(el, binding, vnode)
}
// 定义了vue指令中的update方法
export function update (el: any, binding: Object, vnode: any, oldVNode: any): void {
if (looseEqual(binding.value, binding.oldValue)) { return }
t(el, binding, vnode)
}
function t (el: any, binding: Object, vnode: any): void {
// 解析参数
const value: any = binding.value
// 获取路径,语言类型以及其他参数
const { path, locale, args } = parseValue(value)
if (!path && !locale && !args) {
warn('not support value type')
return
}
// 获取当前的vm
const vm: any = vnode.context
if (!vm) {
warn('not exist Vue instance in VNode context')
return
}
if (!vm.$i18n) {
warn('not exist VueI18n instance in Vue instance')
return
}
if (!path) {
warn('required `path` in v-t directive')
return
}
// 最后调用vm实例上的t方法,然后进行赋值
el._vt = el.textContent = vm.$i18n.t(path, ...makeParams(locale, args))
}
// 解析参数
function parseValue (value: any): Object {
let path: ?string
let locale: ?Locale
let args: any
// 参数只允许是字符串或是对象
if (typeof value === 'string') {
path = value
} else if (isPlainObject(value)) {
path = value.path
locale = value.locale
args = value.args
}
return { path, locale, args }
}
// 对参数进行调整
function makeParams (locale: Locale, args: any): Array<any> {
const params: Array<any> = []
locale && params.push(locale)
if (args && (Array.isArray(args) || isPlainObject(args))) {
params.push(args)
}
return params
}
看完了组件和指令的实现,其他的文件都是辅助函数,我们一次来看看各个文件的实现
- extend.js
/* @flow */
// 主要是往Vue类的原型链扩展方法,调用的都是i18n的实例方法
export default function extend (Vue: any): void {
Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult {
const i18n = this.$i18n
return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values)
}
Vue.prototype.$tc = function (key: Path, choice?: number, ...values: any): TranslateResult {
const i18n = this.$i18n
return i18n._tc(key, i18n.locale, i18n._getMessages(), this, choice, ...values)
}
Vue.prototype.$te = function (key: Path, locale?: Locale): boolean {
const i18n = this.$i18n
return i18n._te(key, i18n.locale, i18n._getMessages(), locale)
}
Vue.prototype.$d = function (value: number | Date, ...args: any): DateTimeFormatResult {
return this.$i18n.d(value, ...args)
}
Vue.prototype.$n = function (value: number, ...args: any): NumberFormatResult {
return this.$i18n.n(value, ...args)
}
}
- path.js
/* @flow */
import { isObject } from './util'
/**
* Path paerser
* - Inspired:
* Vue.js Path parser
*/
// actions
// 定义了各种常量
const APPEND = 0
const PUSH = 1
const INC_SUB_PATH_DEPTH = 2
const PUSH_SUB_PATH = 3
// states
const BEFORE_PATH = 0
const IN_PATH = 1
const BEFORE_IDENT = 2
const IN_IDENT = 3
const IN_SUB_PATH = 4
const IN_SINGLE_QUOTE = 5
const IN_DOUBLE_QUOTE = 6
const AFTER_PATH = 7
const ERROR = 8
const pathStateMachine: any = []
pathStateMachine[BEFORE_PATH] = {
'ws': [BEFORE_PATH],
'ident': [IN_IDENT, APPEND],
'[': [IN_SUB_PATH],
'eof': [AFTER_PATH]
}
pathStateMachine[IN_PATH] = {
'ws': [IN_PATH],
'.': [BEFORE_IDENT],
'[': [IN_SUB_PATH],
'eof': [AFTER_PATH]
}
pathStateMachine[BEFORE_IDENT] = {
'ws': [BEFORE_IDENT],
'ident': [IN_IDENT, APPEND],
'0': [IN_IDENT, APPEND],
'number': [IN_IDENT, APPEND]
}
pathStateMachine[IN_IDENT] = {
'ident': [IN_IDENT, APPEND],
'0': [IN_IDENT, APPEND],
'number': [IN_IDENT, APPEND],
'ws': [IN_PATH, PUSH],
'.': [BEFORE_IDENT, PUSH],
'[': [IN_SUB_PATH, PUSH],
'eof': [AFTER_PATH, PUSH]
}
pathStateMachine[IN_SUB_PATH] = {
"'": [IN_SINGLE_QUOTE, APPEND],
'"': [IN_DOUBLE_QUOTE, APPEND],
'[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
']': [IN_PATH, PUSH_SUB_PATH],
'eof': ERROR,
'else': [IN_SUB_PATH, APPEND]
}
pathStateMachine[IN_SINGLE_QUOTE] = {
"'": [IN_SUB_PATH, APPEND],
'eof': ERROR,
'else': [IN_SINGLE_QUOTE, APPEND]
}
pathStateMachine[IN_DOUBLE_QUOTE] = {
'"': [IN_SUB_PATH, APPEND],
'eof': ERROR,
'else': [IN_DOUBLE_QUOTE, APPEND]
}
/**
* Check if an expression is a literal value.
*/
const literalValueRE: RegExp = /^\s?(true|false|-?[\d.]+|'[^']*'|"[^"]*")\s?$/
function isLiteral (exp: string): boolean {
return literalValueRE.test(exp)
}
/**
* Strip quotes from a string
*/
function stripQuotes (str: string): string | boolean {
const a: number = str.charCodeAt(0)
const b: number = str.charCodeAt(str.length - 1)
return a === b && (a === 0x22 || a === 0x27)
? str.slice(1, -1)
: str
}
/**
* Determine the type of a character in a keypath.
*/
// 获取path的类型
function getPathCharType (ch: ?string): string {
if (ch === undefined || ch === null) { return 'eof' }
// 获取charCode,判断各种类型,逻辑很简单,不解释
const code: number = ch.charCodeAt(0)
switch (code) {
case 0x5B: // [
case 0x5D: // ]
case 0x2E: // .
case 0x22: // "
case 0x27: // '
case 0x30: // 0
return ch
case 0x5F: // _
case 0x24: // $
case 0x2D: // -
return 'ident'
case 0x20: // Space
case 0x09: // Tab
case 0x0A: // Newline
case 0x0D: // Return
case 0xA0: // No-break space
case 0xFEFF: // Byte Order Mark
case 0x2028: // Line Separator
case 0x2029: // Paragraph Separator
return 'ws'
}
// a-z, A-Z
if ((code >= 0x61 && code <= 0x7A) || (code >= 0x41 && code <= 0x5A)) {
return 'ident'
}
// 1-9
if (code >= 0x31 && code <= 0x39) { return 'number' }
return 'else'
}
/**
* Format a subPath, return its plain form if it is
* a literal string or number. Otherwise prepend the
* dynamic indicator (*).
*/
// 格式化子路径
function formatSubPath (path: string): boolean | string {
const trimmed: string = path.trim()
// invalid leading 0
if (path.charAt(0) === '0' && isNaN(path)) { return false }
return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed
}
/**
* Parse a string path into an array of segments
*/
// 路径解析
function parse (path: Path): ?Array<string> {
// 初始化变量
const keys: Array<string> = []
let index: number = -1
let mode: number = BEFORE_PATH
let subPathDepth: number = 0
let c: ?string
let key: any
let newChar: any
let type: string
let transition: number
let action: Function
let typeMap: any
const actions: Array<Function> = []
// 定义各种actions
// 将key添加到keys数组中
actions[PUSH] = function () {
if (key !== undefined) {
keys.push(key)
key = undefined
}
}
// 设置key,如果key不为空,则追加在key后面
actions[APPEND] = function () {
if (key === undefined) {
key = newChar
} else {
key += newChar
}
}
actions[INC_SUB_PATH_DEPTH] = function () {
actions[APPEND]()
subPathDepth++
}
actions[PUSH_SUB_PATH] = function () {
if (subPathDepth > 0) {
subPathDepth--
mode = IN_SUB_PATH
actions[APPEND]()
} else {
subPathDepth = 0
key = formatSubPath(key)
if (key === false) {
return false
} else {
actions[PUSH]()
}
}
}
// 判断是否为"" 或者 为 ''
function maybeUnescapeQuote (): ?boolean {
const nextChar: string = path[index + 1]
if ((mode === IN_SINGLE_QUOTE && nextChar === "'") ||
(mode === IN_DOUBLE_QUOTE && nextChar === '"')) {
index++
newChar = '\\' + nextChar
actions[APPEND]()
return true
}
}
// 循环遍历路径,然后进行拆分
while (mode !== null) {
index++
c = path[index]
// 判断是否为\\ 并且判断是否为双引号或单引号,如果是,就进入下个循环
if (c === '\\' && maybeUnescapeQuote()) {
continue
}
// 获取当前字符的类型
type = getPathCharType(c)
// 获取mode类型的映射表
typeMap = pathStateMachine[mode]
// 得到相应的transition
transition = typeMap[type] || typeMap['else'] || ERROR
if (transition === ERROR) {
return // parse error
}
// 重新设置mode
mode = transition[0]
// 得到相应的action
action = actions[transition[1]]
if (action) {
newChar = transition[2]
newChar = newChar === undefined
? c
: newChar
if (action() === false) {
return
}
}
if (mode === AFTER_PATH) {
return keys
}
}
}
export type PathValue = PathValueObject | PathValueArray | string | number | boolean | null
export type PathValueObject = { [key: string]: PathValue }
export type PathValueArray = Array<PathValue>
function empty (target: any): boolean {
/* istanbul ignore else */
if (Array.isArray(target)) {
return target.length === 0
} else {
return false
}
}
export default class I18nPath {
_cache: Object
constructor () {
this._cache = Object.create(null)
}
/**
* External parse that check for a cache hit first
*/
// 通过parse解析路径,并缓存在_cache对象上
parsePath (path: Path): Array<string> {
let hit: ?Array<string> = this._cache[path]
if (!hit) {
hit = parse(path)
if (hit) {
this._cache[path] = hit
}
}
return hit || []
}
/**
* Get path value from path string
*/
getPathValue (obj: mixed, path: Path): PathValue {
if (!isObject(obj)) { return null }
// 得到path路径解析后的数组paths
const paths: Array<string> = this.parsePath(path)
if (empty(paths)) {
return null
} else {
const length: number = paths.length
let ret: any = null
let last: any = obj
let i: number = 0
// 遍历查找obj中key对应的值
while (i < length) {
const value: any = last[paths[i]]
if (value === undefined) {
last = null
break
}
last = value
i++
}
ret = last
return ret
}
}
}
- format.js
/* @flow */
import { warn, isObject } from './util'
// 对应key的值进行基础格式化
export default class BaseFormatter {
_caches: { [key: string]: Array<Token> }
constructor () {
// 初始化一个缓存对象
this._caches = Object.create(null)
}
interpolate (message: string, values: any): Array<any> {
// 先查看缓存中是否有token
let tokens: Array<Token> = this._caches[message]
// 如果没有,则进一步解析
if (!tokens) {
tokens = parse(message)
this._caches[message] = tokens
}
// 得到tokens之后进行编译
return compile(tokens, values)
}
}
type Token = {
type: 'text' | 'named' | 'list' | 'unknown',
value: string
}
const RE_TOKEN_LIST_VALUE: RegExp = /^(\d)+/
const RE_TOKEN_NAMED_VALUE: RegExp = /^(\w)+/
// 分析相应的token
export function parse (format: string): Array<Token> {
const tokens: Array<Token> = []
let position: number = 0
let text: string = ''
// 将字符串拆分成字符逐个解析
while (position < format.length) {
// 获取每个字符
let char: string = format[position++]
// 对于符号{,进行特殊处理
if (char === '{') {
if (text) {
tokens.push({ type: 'text', value: text })
}
// 内部循环,直到找到对应的符号}
text = ''
let sub: string = ''
char = format[position++]
while (char !== '}') {
sub += char
char = format[position++]
}
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
: RE_TOKEN_NAMED_VALUE.test(sub)
? 'named'
: 'unknown'
tokens.push({ value: sub, type })
} else if (char === '%') {
// when found rails i18n syntax, skip text capture
if (format[(position)] !== '{') {
text += char
}
} else {
text += char
}
}
// 最后生成对应的tokens
text && tokens.push({ type: 'text', value: text })
return tokens
}
// 编译函数
export function compile (tokens: Array<Token>, values: Object | Array<any>): Array<any> {
const compiled: Array<any> = []
let index: number = 0
// 获取mode
const mode: string = Array.isArray(values)
? 'list'
: isObject(values)
? 'named'
: 'unknown'
if (mode === 'unknown') { return compiled }
// 根据token的各种类型进行编译
while (index < tokens.length) {
const token: Token = tokens[index]
switch (token.type) {
case 'text':
compiled.push(token.value)
break
case 'list':
compiled.push(values[parseInt(token.value, 10)])
break
case 'named':
// 如果是named,则将对应的value值push到complied数组中
if (mode === 'named') {
compiled.push((values: any)[token.value])
} else {
if (process.env.NODE_ENV !== 'production') {
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
}
}
break
case 'unknown':
if (process.env.NODE_ENV !== 'production') {
warn(`Detect 'unknown' type of token!`)
}
break
}
index++
}
return compiled
}
- mixin.js
/* @flow */
import VueI18n from './index'
import { isPlainObject, warn, merge } from './util'
export default {
beforeCreate (): void {
const options: any = this.$options
options.i18n = options.i18n || (options.__i18n ? {} : null)
if (options.i18n) {
if (options.i18n instanceof VueI18n) {
// init locale messages via custom blocks
if (options.__i18n) {
try {
let localeMessages = {}
options.__i18n.forEach(resource => {
localeMessages = merge(localeMessages, JSON.parse(resource))
})
Object.keys(localeMessages).forEach((locale: Locale) => {
options.i18n.mergeLocaleMessage(locale, localeMessages[locale])
})
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
warn(`Cannot parse locale messages via custom blocks.`, e)
}
}
}
this._i18n = options.i18n
this._i18nWatcher = this._i18n.watchI18nData()
this._i18n.subscribeDataChanging(this)
this._subscribing = true
} else if (isPlainObject(options.i18n)) {
// component local i18n
if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) {
options.i18n.root = this.$root.$i18n
options.i18n.fallbackLocale = this.$root.$i18n.fallbackLocale
options.i18n.silentTranslationWarn = this.$root.$i18n.silentTranslationWarn
}
// init locale messages via custom blocks
if (options.__i18n) {
try {
let localeMessages = {}
options.__i18n.forEach(resource => {
localeMessages = merge(localeMessages, JSON.parse(resource))
})
options.i18n.messages = localeMessages
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
warn(`Cannot parse locale messages via custom blocks.`, e)
}
}
}
this._i18n = new VueI18n(options.i18n)
this._i18nWatcher = this._i18n.watchI18nData()
this._i18n.subscribeDataChanging(this)
this._subscribing = true
if (options.i18n.sync === undefined || !!options.i18n.sync) {
this._localeWatcher = this.$i18n.watchLocale()
}
} else {
if (process.env.NODE_ENV !== 'production') {
warn(`Cannot be interpreted 'i18n' option.`)
}
}
} else if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) {
// root i18n
this._i18n = this.$root.$i18n
this._i18n.subscribeDataChanging(this)
this._subscribing = true
} else if (options.parent && options.parent.$i18n && options.parent.$i18n instanceof VueI18n) {
// parent i18n
this._i18n = options.parent.$i18n
this._i18n.subscribeDataChanging(this)
this._subscribing = true
}
},
beforeDestroy (): void {
if (!this._i18n) { return }
if (this._subscribing) {
this._i18n.unsubscribeDataChanging(this)
delete this._subscribing
}
if (this._i18nWatcher) {
this._i18nWatcher()
delete this._i18nWatcher
}
if (this._localeWatcher) {
this._localeWatcher()
delete this._localeWatcher
}
this._i18n = null
}
}
- util.js
/* @flow */
/**
* utilites
*/
export function warn (msg: string, err: ?Error): void {
if (typeof console !== 'undefined') {
console.warn('[vue-i18n] ' + msg)
/* istanbul ignore if */
if (err) {
console.warn(err.stack)
}
}
}
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
const toString: Function = Object.prototype.toString
const OBJECT_STRING: string = '[object Object]'
// 判断是否为一个对象
export function isPlainObject (obj: any): boolean {
return toString.call(obj) === OBJECT_STRING
}
// 判断是否为空
export function isNull (val: mixed): boolean {
return val === null || val === undefined
}
// 解析参数
export function parseArgs (...args: Array<mixed>): Object {
let locale: ?string = null
let params: mixed = null
// 分析参数的长度,如果长度为1
if (args.length === 1) {
// 如果是对象或是数组,则将该参数设置为params
if (isObject(args[0]) || Array.isArray(args[0])) {
params = args[0]
} else if (typeof args[0] === 'string') {
// 如果是字符串,则设置为locale,当做是语言类型
locale = args[0]
}
} else if (args.length === 2) {
// 长度为2时,根据情况设置locale和params
if (typeof args[0] === 'string') {
locale = args[0]
}
/* istanbul ignore if */
if (isObject(args[1]) || Array.isArray(args[1])) {
params = args[1]
}
}
// 最后返回{ locale, params }对象
return { locale, params }
}
// 如果索引值大于1,则返回1
function getOldChoiceIndexFixed (choice: number): number {
return choice
? choice > 1
? 1
: 0
: 1
}
// 获取索引的方法
function getChoiceIndex (choice: number, choicesLength: number): number {
choice = Math.abs(choice)
// 如果长度等于2,则调用getOldChoiceIndexFixed
if (choicesLength === 2) { return getOldChoiceIndexFixed(choice) }
// 确保索引值不大于2,这个很令人费解啊
return choice ? Math.min(choice, 2) : 0
}
export function fetchChoice (message: string, choice: number): ?string {
/* istanbul ignore if */
if (!message && typeof message !== 'string') { return null }
// 将字符串分割为数组
const choices: Array<string> = message.split('|')
// 获取索引
choice = getChoiceIndex(choice, choices.length)
// 得到数组中特定索引的值
if (!choices[choice]) { return message }
// 去掉空格
return choices[choice].trim()
}
// 利用JSON的api实现对象的深拷贝
export function looseClone (obj: Object): Object {
return JSON.parse(JSON.stringify(obj))
}
// 删除数组中的某一项
export function remove (arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
// 递归合并对象
export function merge (target: Object): Object {
const output = Object(target)
for (let i = 1; i < arguments.length; i++) {
const source = arguments[i]
if (source !== undefined && source !== null) {
let key
for (key in source) {
if (hasOwn(source, key)) {
if (isObject(source[key])) {
output[key] = merge(output[key], source[key])
} else {
output[key] = source[key]
}
}
}
}
}
return output
}
// 松散判断是否相等
export function looseEqual (a: any, b: any): boolean {
// 先判断是否全等
if (a === b) { return true }
const isObjectA: boolean = isObject(a)
const isObjectB: boolean = isObject(b)
// 如果两者都是对象
if (isObjectA && isObjectB) {
try {
const isArrayA: boolean = Array.isArray(a)
const isArrayB: boolean = Array.isArray(b)
// 如果两者都是数组
if (isArrayA && isArrayB) {
// 如果长度相等,则递归对比数组中的每一项
return a.length === b.length && a.every((e: any, i: number): boolean => {
// 递归调用looseEqual
return looseEqual(e, b[i])
})
} else if (!isArrayA && !isArrayB) {
// 如果不是数组,则当做对象来对比
const keysA: Array<string> = Object.keys(a)
const keysB: Array<string> = Object.keys(b)
// 如果key的数量相等,则递归对比每个key对应的值
return keysA.length === keysB.length && keysA.every((key: string): boolean => {
// 递归调用looseEqual
return looseEqual(a[key], b[key])
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
// 如果不是对象,则强制转为字符串进行对比
return String(a) === String(b)
} else {
return false
}
}
// 判断是否支持Intl.DateTimeFormat方法
export const canUseDateTimeFormat: boolean =
typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined'
// 判断是否支持Intl.NumberFormat方法
export const canUseNumberFormat: boolean =
typeof Intl !== 'undefined' && typeof Intl.NumberFormat !== 'undefined'
- install.js
/* @flow */
/**
* utilites
*/
export function warn (msg: string, err: ?Error): void {
if (typeof console !== 'undefined') {
console.warn('[vue-i18n] ' + msg)
/* istanbul ignore if */
if (err) {
console.warn(err.stack)
}
}
}
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
const toString: Function = Object.prototype.toString
const OBJECT_STRING: string = '[object Object]'
// 判断是否为一个对象
export function isPlainObject (obj: any): boolean {
return toString.call(obj) === OBJECT_STRING
}
// 判断是否为空
export function isNull (val: mixed): boolean {
return val === null || val === undefined
}
// 解析参数
export function parseArgs (...args: Array<mixed>): Object {
let locale: ?string = null
let params: mixed = null
// 分析参数的长度,如果长度为1
if (args.length === 1) {
// 如果是对象或是数组,则将该参数设置为params
if (isObject(args[0]) || Array.isArray(args[0])) {
params = args[0]
} else if (typeof args[0] === 'string') {
// 如果是字符串,则设置为locale,当做是语言类型
locale = args[0]
}
} else if (args.length === 2) {
// 长度为2时,根据情况设置locale和params
if (typeof args[0] === 'string') {
locale = args[0]
}
/* istanbul ignore if */
if (isObject(args[1]) || Array.isArray(args[1])) {
params = args[1]
}
}
// 最后返回{ locale, params }对象
return { locale, params }
}
// 如果索引值大于1,则返回1
function getOldChoiceIndexFixed (choice: number): number {
return choice
? choice > 1
? 1
: 0
: 1
}
// 获取索引的方法
function getChoiceIndex (choice: number, choicesLength: number): number {
choice = Math.abs(choice)
// 如果长度等于2,则调用getOldChoiceIndexFixed
if (choicesLength === 2) { return getOldChoiceIndexFixed(choice) }
// 确保索引值不大于2,这个很令人费解啊
return choice ? Math.min(choice, 2) : 0
}
export function fetchChoice (message: string, choice: number): ?string {
/* istanbul ignore if */
if (!message && typeof message !== 'string') { return null }
// 将字符串分割为数组
const choices: Array<string> = message.split('|')
// 获取索引
choice = getChoiceIndex(choice, choices.length)
// 得到数组中特定索引的值
if (!choices[choice]) { return message }
// 去掉空格
return choices[choice].trim()
}
// 利用JSON的api实现对象的深拷贝
export function looseClone (obj: Object): Object {
return JSON.parse(JSON.stringify(obj))
}
// 删除数组中的某一项
export function remove (arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
// 递归合并对象
export function merge (target: Object): Object {
const output = Object(target)
for (let i = 1; i < arguments.length; i++) {
const source = arguments[i]
if (source !== undefined && source !== null) {
let key
for (key in source) {
if (hasOwn(source, key)) {
if (isObject(source[key])) {
output[key] = merge(output[key], source[key])
} else {
output[key] = source[key]
}
}
}
}
}
return output
}
// 松散判断是否相等
export function looseEqual (a: any, b: any): boolean {
// 先判断是否全等
if (a === b) { return true }
const isObjectA: boolean = isObject(a)
const isObjectB: boolean = isObject(b)
// 如果两者都是对象
if (isObjectA && isObjectB) {
try {
const isArrayA: boolean = Array.isArray(a)
const isArrayB: boolean = Array.isArray(b)
// 如果两者都是数组
if (isArrayA && isArrayB) {
// 如果长度相等,则递归对比数组中的每一项
return a.length === b.length && a.every((e: any, i: number): boolean => {
// 递归调用looseEqual
return looseEqual(e, b[i])
})
} else if (!isArrayA && !isArrayB) {
// 如果不是数组,则当做对象来对比
const keysA: Array<string> = Object.keys(a)
const keysB: Array<string> = Object.keys(b)
// 如果key的数量相等,则递归对比每个key对应的值
return keysA.length === keysB.length && keysA.every((key: string): boolean => {
// 递归调用looseEqual
return looseEqual(a[key], b[key])
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
// 如果不是对象,则强制转为字符串进行对比
return String(a) === String(b)
} else {
return false
}
}
// 判断是否支持Intl.DateTimeFormat方法
export const canUseDateTimeFormat: boolean =
typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined'
// 判断是否支持Intl.NumberFormat方法
export const canUseNumberFormat: boolean =
typeof Intl !== 'undefined' && typeof Intl.NumberFormat !== 'undefined'
ok~今天就写到这,希望对大家有所帮助,也欢迎拍砖
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。