贝塔猫

贝塔猫 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

贝塔猫 收藏了文章 · 3月6日

canvas动画—圆形扩散、运动轨迹

介绍

在ECharts中看到过这种圆形扩散效果,类似css3,刚好项目中想把它用上,but我又不想引入整个echart.js文件,更重要的是想弄明白它的原理,所以自己动手。在这篇文章中我们就来分析实现这种效果的两种方法,先上效果图:
圆形扩散

实现原理

通过不断的改变圆的半径大小,不断重叠达到运动的效果,在运动的过程中,设置当前canvas的透明度context.globalAlpha=0.95,使得canvas上的圆逐渐透明直至为0,从而实现这种扩散、渐变的效果。

实现方法一

1. 关键技术点
context.globalAlpha = 0.95; //设置主canvas的绘制透明度。
创建临时canvas来缓存主canas的历史图像,再叠加到主canvas上。

2. 绘制过程
首先,我们先来写一个绘制圆的方法:

//画圆
var drawCircle = function() {
    context.beginPath();
    context.arc(150, 100, radius, 0, Math.PI * 2);
    context.closePath();
    context.lineWidth = 2; //线条宽度
    context.strokeStyle = 'rgba(250,250,50,1)'; //颜色
    context.stroke();
    radius += 0.5; //每一帧半径增加0.5

    //半径radius大于30时,重置为0
    if (radius > 30) {
        radius = 0;
    }
};

然后,我们创建一个临时canvas用来缓存主canvas上的历史图像,设置主canvas的透明度context.globalAlpha=0.95(关键一步),在每次调用drawCircle方法绘制一个新圆之前都把主canvas上的图像,也就是之前的图像给绘制到临时的canvas中,等到drawCircle方法绘制完新圆后,再把临时canvas的图像绘制回主canvas中。

绘制流程

核心代码如下:

//创建一个临时canvas来缓存主canvas的历史图像
var backCanvas = document.createElement('canvas'),
    backCtx = backCanvas.getContext('2d');
    backCanvas.width = width;
    backCanvas.height = height;

    //设置主canvas的绘制透明度
    context.globalAlpha = 0.95;

    //显示即将绘制的图像,忽略临时canvas中已存在的图像
    backCtx.globalCompositeOperation = 'copy';

var render = function() {
    //1.先将主canvas的图像缓存到临时canvas中
    backCtx.drawImage(canvas, 0, 0, width, height);

    //2.清除主canvas上的图像
    context.clearRect(0, 0, width, height);

    //3.在主canvas上画新圆
    drawCircle();

    //4.等新圆画完后,再把临时canvas的图像绘制回主canvas中
    context.drawImage(backCanvas, 0, 0, width, height);
};

实现方法二

与上一种方法相比,这种方法更加简单,同样是用到了透明度逐渐减小直到为0的原理,不同的是这里并没有创建临时canvas,而是运用了context.globalCompositeOperation属性值source-overdestination-in来配合使用,查看globalCompositeOperation属性介绍

核心代码如下:

var render = function() {
    //默认值为source-over
    var prev = context.globalCompositeOperation;

    //只显示canvas上原图像的重叠部分
    context.globalCompositeOperation = 'destination-in';

    //设置主canvas的绘制透明度
    context.globalAlpha = 0.95;

    //这一步目的是将canvas上的图像变的透明
    context.fillRect(0, 0, width, height);

    //在原图像上重叠新图像
    context.globalCompositeOperation = prev;

    //在主canvas上画新圆
    drawCircle();
};

地图上的应用

这里我采用的是第二种方式,将扩散、渐变效果运用到了百度地图上,感觉还比较炫,查看更多demo

圆形扩散动画
百度地图圆形扩散

运动轨迹动画
百度地图运动轨迹动画

总结

方法一、二都能实现同样的效果,如果动画绘制、操作canvas比较频繁,建议采用第一种方式,用临时canvas来缓存历史图像,效率更高。

查看原文

贝塔猫 收藏了文章 · 3月5日

canvas动画—圆形扩散、运动轨迹

介绍

在ECharts中看到过这种圆形扩散效果,类似css3,刚好项目中想把它用上,but我又不想引入整个echart.js文件,更重要的是想弄明白它的原理,所以自己动手。在这篇文章中我们就来分析实现这种效果的两种方法,先上效果图:
圆形扩散

实现原理

通过不断的改变圆的半径大小,不断重叠达到运动的效果,在运动的过程中,设置当前canvas的透明度context.globalAlpha=0.95,使得canvas上的圆逐渐透明直至为0,从而实现这种扩散、渐变的效果。

实现方法一

1. 关键技术点
context.globalAlpha = 0.95; //设置主canvas的绘制透明度。
创建临时canvas来缓存主canas的历史图像,再叠加到主canvas上。

2. 绘制过程
首先,我们先来写一个绘制圆的方法:

//画圆
var drawCircle = function() {
    context.beginPath();
    context.arc(150, 100, radius, 0, Math.PI * 2);
    context.closePath();
    context.lineWidth = 2; //线条宽度
    context.strokeStyle = 'rgba(250,250,50,1)'; //颜色
    context.stroke();
    radius += 0.5; //每一帧半径增加0.5

    //半径radius大于30时,重置为0
    if (radius > 30) {
        radius = 0;
    }
};

然后,我们创建一个临时canvas用来缓存主canvas上的历史图像,设置主canvas的透明度context.globalAlpha=0.95(关键一步),在每次调用drawCircle方法绘制一个新圆之前都把主canvas上的图像,也就是之前的图像给绘制到临时的canvas中,等到drawCircle方法绘制完新圆后,再把临时canvas的图像绘制回主canvas中。

绘制流程

核心代码如下:

//创建一个临时canvas来缓存主canvas的历史图像
var backCanvas = document.createElement('canvas'),
    backCtx = backCanvas.getContext('2d');
    backCanvas.width = width;
    backCanvas.height = height;

    //设置主canvas的绘制透明度
    context.globalAlpha = 0.95;

    //显示即将绘制的图像,忽略临时canvas中已存在的图像
    backCtx.globalCompositeOperation = 'copy';

var render = function() {
    //1.先将主canvas的图像缓存到临时canvas中
    backCtx.drawImage(canvas, 0, 0, width, height);

    //2.清除主canvas上的图像
    context.clearRect(0, 0, width, height);

    //3.在主canvas上画新圆
    drawCircle();

    //4.等新圆画完后,再把临时canvas的图像绘制回主canvas中
    context.drawImage(backCanvas, 0, 0, width, height);
};

实现方法二

与上一种方法相比,这种方法更加简单,同样是用到了透明度逐渐减小直到为0的原理,不同的是这里并没有创建临时canvas,而是运用了context.globalCompositeOperation属性值source-overdestination-in来配合使用,查看globalCompositeOperation属性介绍

核心代码如下:

var render = function() {
    //默认值为source-over
    var prev = context.globalCompositeOperation;

    //只显示canvas上原图像的重叠部分
    context.globalCompositeOperation = 'destination-in';

    //设置主canvas的绘制透明度
    context.globalAlpha = 0.95;

    //这一步目的是将canvas上的图像变的透明
    context.fillRect(0, 0, width, height);

    //在原图像上重叠新图像
    context.globalCompositeOperation = prev;

    //在主canvas上画新圆
    drawCircle();
};

地图上的应用

这里我采用的是第二种方式,将扩散、渐变效果运用到了百度地图上,感觉还比较炫,查看更多demo

圆形扩散动画
百度地图圆形扩散

运动轨迹动画
百度地图运动轨迹动画

总结

方法一、二都能实现同样的效果,如果动画绘制、操作canvas比较频繁,建议采用第一种方式,用临时canvas来缓存历史图像,效率更高。

查看原文

贝塔猫 赞了文章 · 3月5日

canvas动画—圆形扩散、运动轨迹

介绍

在ECharts中看到过这种圆形扩散效果,类似css3,刚好项目中想把它用上,but我又不想引入整个echart.js文件,更重要的是想弄明白它的原理,所以自己动手。在这篇文章中我们就来分析实现这种效果的两种方法,先上效果图:
圆形扩散

实现原理

通过不断的改变圆的半径大小,不断重叠达到运动的效果,在运动的过程中,设置当前canvas的透明度context.globalAlpha=0.95,使得canvas上的圆逐渐透明直至为0,从而实现这种扩散、渐变的效果。

实现方法一

1. 关键技术点
context.globalAlpha = 0.95; //设置主canvas的绘制透明度。
创建临时canvas来缓存主canas的历史图像,再叠加到主canvas上。

2. 绘制过程
首先,我们先来写一个绘制圆的方法:

//画圆
var drawCircle = function() {
    context.beginPath();
    context.arc(150, 100, radius, 0, Math.PI * 2);
    context.closePath();
    context.lineWidth = 2; //线条宽度
    context.strokeStyle = 'rgba(250,250,50,1)'; //颜色
    context.stroke();
    radius += 0.5; //每一帧半径增加0.5

    //半径radius大于30时,重置为0
    if (radius > 30) {
        radius = 0;
    }
};

然后,我们创建一个临时canvas用来缓存主canvas上的历史图像,设置主canvas的透明度context.globalAlpha=0.95(关键一步),在每次调用drawCircle方法绘制一个新圆之前都把主canvas上的图像,也就是之前的图像给绘制到临时的canvas中,等到drawCircle方法绘制完新圆后,再把临时canvas的图像绘制回主canvas中。

绘制流程

核心代码如下:

//创建一个临时canvas来缓存主canvas的历史图像
var backCanvas = document.createElement('canvas'),
    backCtx = backCanvas.getContext('2d');
    backCanvas.width = width;
    backCanvas.height = height;

    //设置主canvas的绘制透明度
    context.globalAlpha = 0.95;

    //显示即将绘制的图像,忽略临时canvas中已存在的图像
    backCtx.globalCompositeOperation = 'copy';

var render = function() {
    //1.先将主canvas的图像缓存到临时canvas中
    backCtx.drawImage(canvas, 0, 0, width, height);

    //2.清除主canvas上的图像
    context.clearRect(0, 0, width, height);

    //3.在主canvas上画新圆
    drawCircle();

    //4.等新圆画完后,再把临时canvas的图像绘制回主canvas中
    context.drawImage(backCanvas, 0, 0, width, height);
};

实现方法二

与上一种方法相比,这种方法更加简单,同样是用到了透明度逐渐减小直到为0的原理,不同的是这里并没有创建临时canvas,而是运用了context.globalCompositeOperation属性值source-overdestination-in来配合使用,查看globalCompositeOperation属性介绍

核心代码如下:

var render = function() {
    //默认值为source-over
    var prev = context.globalCompositeOperation;

    //只显示canvas上原图像的重叠部分
    context.globalCompositeOperation = 'destination-in';

    //设置主canvas的绘制透明度
    context.globalAlpha = 0.95;

    //这一步目的是将canvas上的图像变的透明
    context.fillRect(0, 0, width, height);

    //在原图像上重叠新图像
    context.globalCompositeOperation = prev;

    //在主canvas上画新圆
    drawCircle();
};

地图上的应用

这里我采用的是第二种方式,将扩散、渐变效果运用到了百度地图上,感觉还比较炫,查看更多demo

圆形扩散动画
百度地图圆形扩散

运动轨迹动画
百度地图运动轨迹动画

总结

方法一、二都能实现同样的效果,如果动画绘制、操作canvas比较频繁,建议采用第一种方式,用临时canvas来缓存历史图像,效率更高。

查看原文

赞 37 收藏 66 评论 39

贝塔猫 提出了问题 · 2019-11-14

iview框架中datepicker根据接口数据修改禁用日期

问题描述

页面内容

一个input框输入编号,一个datepicker框选择时间
根据input框中的编号调用后台接口,接口返回一个日期,如果返回日期为1970-01-01,则今天之前的日期被禁用,否则返回日期前的时间被禁用。

尝试方法

在input框on-blur时调用接口,获取到日期,设置datepicker禁用

存在问题

第一次输入编号时,datepicker禁用设置正常
若直接删除原来编号,再次触发on-blur时,接口获取日期正常但datepicker禁用依然是上一次的结果。

不知道是不是iview存在options的值只能设置一次的bug?

相关代码

html(省略一些无用的class placeholder等)

<Input v-model="searchForm.no" @on-blur="changeDisabled"></Input>

<DatePicker v-model="searchForm.date" type="date" :options="dateCanChoose"></DatePicker>

js

changeDisabled() {
    if (this.searchForm.blNo != "") {
        // 先把原来的日期清空
        this.isDatePickDisabled = true
        let params = {
            refNum: this.searchForm.blNo
        }
        this.$axios.post(后台接口, params,
            (code,result) => {
                if (code == 200) {   
                    let extendTo = result.data.date
                    if (extendTo == '1970-01-01') {
                        // 今天之后的日期可选
                        this.dateCanChoose = {
                            disabledDate: function (date) 
                                return date && date.valueOf() < Date.now() - 86400000
                            }
                        }
                    } else { 
                        // 此天之后的日期可选
                        this.dateCanChoose = {
                            disabledDate: function (date) {
                                let extendDate = new Date(extendTo).getTime()
                                return date && date.valueOf() < extendDate
                            }
                        }
                    }
                }
            }, (err) => {
                console.log(err)
            })
    }
}

关注 2 回答 2

贝塔猫 收藏了文章 · 2019-11-12

基于Vue.js, iview的全屏图片、视频浏览组件

先上效果图

图片描述

再上源代码

<template>
    <transition name="fade">
        <div class="media-wrapper" v-if="seeMedia">
            <Button type="text" class="media-close" shape="circle" icon="close" @click="close"></Button>
            <div class="media-controller">
                <Button-group shape='circle'>
                    <Button size="large" type="ghost" icon="ios-skipbackward" @click.prevent="prev"></Button>
                    <Button size="large" type="ghost" icon="ios-skipforward" @click.prevent="next"></Button>
                </Button-group>
            </div>
            <div class="media-content">
                <div v-for="(item,index) in data" :key="index" :class="type(index)">
                    <img :data-original='item' v-if="isImg(item)" @click="toggle(index)">
                    <video :data-original="item" v-else controls="controls" @click="toggle(index)">
                    </video>
                </div>
            </div>
        </div>
    </transition>
</template>

<script>
export default {
    name: 'cjMedia',
    data: function () {
        return {
            nowIndex: 0,
            data: [
                '/src/test/media/movie.ogg', '/src/test/media/1.jpg', '/src/test/media/2.jpg'
            ]
        }
    },
    props: {
        // data:{
        //     type:Array
        // }
    },
    methods: {
        next() {
            if (this.nowIndex == this.data.length - 1) {
                this.$Message.warning('已到达最后一张');
            } else {
                this.nowIndex++;
            }
        },
        prev() {
            if (this.nowIndex == 0) {
                this.$Message.warning('已到达第一张');
            } else {
                this.nowIndex--;
            }
        },
        type(index) {
            if (index == this.nowIndex) {
                return 'media-center'
            } else if (index - this.nowIndex == 1) {
                return 'media-right'
            } else if (index - this.nowIndex == -1) {
                return 'media-left'
            } else {
                return 'media-hide'
            }
        },
        isImg(item) {
            var ext = item.substr(item.length - 3, 3);
            var flag = ext == ('jpg' || 'png' || 'gif') ? true : false;
            return flag;
        },
        toggle(index) {
            if (index - this.nowIndex == 1) {
                this.nowIndex++;
            } else if (index - this.nowIndex == -1) {
                this.nowIndex--;
            }
        },
        close() {
            this.$store.commit('SET_MEDIA', false);
            this.nowIndex = 0;
        }
    },
    computed: {
        seeMedia() {
            return this.$store.state.seeMedia;
        }
    }
}
</script>

<style lang="scss">
.media-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.52);
    z-index: 1010;
    i {
        color: #fff
    }
    .media-controller {
        position: absolute;
        left: 50%;
        bottom: 30px;
        transform: translateX(-50%)
    }
    .media-close {
        position: absolute;
        right: 5px;
        top: 5px;
        i {
            font-size: 30px;
        }
    }
    .media-content {
        div {
            position: absolute;
            top: 50%; // background: green;
            color: #fff;
            text-align: center;
            font-size: 30px;
            transition: all .56s ease;
            img {
                max-width: 100%;
                max-height: 100%
            }
            video {
                width: 100%;
            }
        }
        .media-center {
            height: 50%;
            width: 40%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 1011;
        }
        .media-left,
        .media-right {
            width: 25%;
            height: 35%;
            filter: grayscale(90%);
        }
        .media-right {
            left: 100%;
            transform: translate(-105%, -50%);
        }
        .media-left {
            left: 0;
            transform: translate(5%, -50%);
        }
        .media-hide {
            height: 0;
            width: 0;
            left: 50%;
            z-index: 1010;
            opacity: 0;
        }
    }
}
</style>

data传入媒体路径即可。纯原创,转载请标注来源。

查看原文

贝塔猫 回答了问题 · 2019-11-04

IE中sessionStorage越权?

自己找到原因了,是IE的缓存问题。
解决方案有两种

  1. 在请求后面增加随机参数,例如?t=123
  2. 在ajax请求中增加cache: false

参考

关注 1 回答 1

贝塔猫 提出了问题 · 2019-11-04

IE中sessionStorage越权?

问题描述

背景:
在一个网站中有两个权限admin和user,两者会共享一个接口getInfoList(),但由于权限不同,获取的数据也不同。获取到数据后,利用sessionstorage进行存储。

Chrome中运行正常,当切换到IE时,在不同的选项卡中同时登录admin和user两个账号,getInfoList得到的数据就会出现问题。例如首先登录admin,新开选项卡登录user后调用getInfoList接口,得到的却是admin的数据。

问题出现的环境背景及自己尝试过哪些方法

两者sessionStorage的名字不同,没有效果

相关代码

// 请把代码文本粘贴到下方(请勿用图片代替代码)

var TOOLS = {
setLocVal: function (name, val)  {
    sessionStorage.setItem(name , JSON.stringify(val))
},
getLocVal: function (name)  {
   return JSON.parse(sessionStorage.getItem(name))
},

}

关注 1 回答 1

贝塔猫 提出了问题 · 2019-08-18

解决vue-cli3新建项目之后运行,端口号不是8080而是一个五位数?

问题描述

使用vue-cli3新建一个项目,直接npm run serve, 给出的地址理论上应当是http://localhost:8080/,而实际上是一个类似http://localhost:16150/的地址,即端口号是一个五位数。且每次npm run serve之后,给出的端口号都不一样。卸载重装vue-cli之后都没有区别?

第一次npm run serve的给出的地址
图片描述

重新npm run serve之后的地址
图片描述

关注 12 回答 11

贝塔猫 收藏了文章 · 2019-07-30

浅谈Vue.use

先举个?

我们先来看一个简单的事例
首先我使用官方脚手架新建一个项目vue init webpack vue-demo
然后我创建两个文件index.js plugins.js.
我将这两个文件放置在src/classes/vue-use目录下

接下来对这两个文件进行编写

// 文件:  src/classes/vue-use/plugins.js

const Plugin1 = {
    install(a, b, c) {
        console.log('Plugin1 第一个参数:', a);
        console.log('Plugin1 第二个参数:', b);
        console.log('Plugin1 第三个参数:', c);
    },
};

function Plugin2(a, b, c) {
    console.log('Plugin2 第一个参数:', a);
    console.log('Plugin2 第二个参数:', b);
    console.log('Plugin2 第三个参数:', c);
}

export { Plugin1, Plugin2 };
// 文件: src/classes/vue-use/index.js

import Vue from 'vue';

import { Plugin1, Plugin2 } from './plugins';

Vue.use(Plugin1, '参数1', '参数2');
Vue.use(Plugin2, '参数A', '参数B');

然后我们在入口文件main.js引用这段代码

// 文件: src/main.js

import Vue from 'vue';

import '@/classes/vue-use';
import App from './App';
import router from './router';

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
    el: '#app',
    router,
    render: h => h(App),
});

此时我们执行npm run dev打开8080端口开启开发调试工具可以看到控制台输出以下信息
clipboard.png...]

从中可以发现我们在plugin1中的install方法编写的三个console都打印出来,第一个打印出来的是Vue对象,第二个跟第三个是我们传入的两个参数。
plugin2没有install方法,它本身就是一个方法,也能打印三个参数,第一个是Vue对象,第二个跟第三个也是我们传入的两个参数。

那么现在我们是不是大概对Vue.use有一个模糊的猜想~

分析源码

好我们还是不要猜想,直接上源码

// Vue源码文件路径:src/core/global-api/use.js

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

从源码中我们可以发现vue首先判断这个插件是否被注册过,不允许重复注册。
并且接收的plugin参数的限制是Function | Object两种类型。
对于这两种类型有不同的处理。
首先将我们传入的参数整理成数组 => const args = toArray(arguments, 1)
(toArray源码)

// Vue源码文件路径:src/core/shared/util.js

export function toArray (list: any, start?: number): Array<any> {
  start = start || 0
  let i = list.length - start
  const ret: Array<any> = new Array(i)
  while (i--) {
    ret[i] = list[i + start]
  }
  return ret
}

再将Vue对象添加到这个数组的起始位置args.unshift(this),这里的this 指向Vue对象
如果我们传入的plugin(Vue.use的第一个参数)的install是一个方法。也就是说如果我们传入一个对象,对象中包含install方法,那么我们就调用这个plugininstall方法并将整理好的数组当成参数传入install方法中。 => plugin.install.apply(plugin, args)
如果我们传入的plugin就是一个函数,那么我们就直接调用这个函数并将整理好的数组当成参数传入。 => plugin.apply(null, args)
之后给这个插件添加至已经添加过的插件数组中,标示已经注册过 => installedPlugins.push(plugin)
最后返回Vue对象。

小结

通过以上分析我们可以知道,在我们以后编写插件的时候可以有两种方式。
一种是将这个插件的逻辑封装成一个对象最后将最后在install编写业务代码暴露给Vue对象。这样做的好处是可以添加任意参数在这个对象上方便将install函数封装得更加精简,可拓展性也比较高。
还有一种则是将所有逻辑都编写成一个函数暴露给Vue。
其实两种方法原理都一样,无非第二种就是将这个插件直接当成install函数来处理。
个人觉得第一种方式比较合理。
举个?

export const Plugin = {
    install(Vue) {
        Vue.component...
        Vue.mixins...
        Vue...
        // 我们也可以在install里面执行其他函数,Vue会将this指向我们的插件
        console.log(this)  // {install: ...,utils: ...}
        this.utils(Vue)    // 执行utils函数
        console.log(this.COUNT) // 0
    },
    utils(Vue) {
        Vue...
        console.log(Vue)  // Vue
    },
    COUNT: 0    
}
// 我们可以在这个对象上添加参数,最终Vue只会执行install方法,而其他方法可以作为封装install方法的辅助函数

const test = 'test'
export function Plugin2(Vue) {
    Vue...
    console.log(test)  // 'test'
    // 注意如果插件编写成函数形式,那么Vue只会把this指向null,并不会指向这个函数
    console.log(this)  // null
}
// 这种方式我们只能在一个函数中编写插件逻辑,可封装性就不是那么强了

小弟不才,对vue源码的理解暂且到这。欢迎大佬们多指教~

查看原文

贝塔猫 收藏了文章 · 2019-07-29

你想要的——vue-i18n源码分析

大家好,今天给大家带来的干货是vue-i18n(v7.3.0)的源码分析。vue-i18n是用于多语言适配的vue插件,主要用于前端项目的国际化应用

这里是vue-18n的gayhub地址   摸我

首先还是先看看作者给我们的一个简单的例子:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>getting started</title>
    <script data-original="../../node_modules/vue/dist/vue.min.js"></script>
    <script data-original="../../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~今天就写到这,希望对大家有所帮助,也欢迎拍砖

查看原文

认证与成就

  • 获得 3 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-07-10
个人主页被 126 人浏览