夕水

夕水 查看完整档案

成都编辑四川信息职业技术学院  |  软件技术 编辑某某公司  |  web前端开发 编辑 www.eveningwater.com 编辑
编辑

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。

个人动态

夕水 发布了文章 · 4月1日

深入JavaScript中的this对象

this 对象详解

this关键字是函数当中最重要的一个知识点。它在JavaScript中的表现也会有一些细微的不同,在严格和非严格模式之下也会有一些差别。

绝大多数情况下,this的指向由函数的调用方式决定。它不能被赋值,并且每次函数调用,它也有可能会不同。ES5引入了bind方法来设置函数的this值,而不需要考虑函数的调用方式,ES6的箭头函数不提供自身的this绑定,它的this由当前上下文决定。

    const obj = {
        name:"hello,world!",
        getName(){
            return this.name;
        }
    }
    console.log(obj.getName());//"hello,world!"

语法:

  this

它的值是当前上下文(global,function,eval)中的一个属性,在非严格模式下,它总是指向一个对象,而在严格模式下,它可以被设置成任意值。

描述

全局上下文

全局上下文即全局对象,例如在浏览器环境当中,this始终指的是window对象,不论是否是严格模式。来看如下一个示例:

    //在浏览器环境中,window对象就是全局对象
    console.log(this === window);//true

    //不用标识符定义一个变量,也会自动将该变量添加到window对象中,作为window对象的一个属性
    a = 250;
    console.log(this.a);//250

    this.message = "hello,world!";
    console.log(message);
    console.log(window.message);
    //都是打印的"hello,world!"
笔记:可以始终使用globalThis来获取一个全局对象,无论你的代码是否在当前上下文运行。
    var obj = {
        func:function(){
            console.log(this);
            console.log(globalThis);
        }
    }
    obj.func();//先打印obj对象,再打印window对象,浏览器环境中

函数上下文

在函数内部,this取决于它被调用的方式。例如以下的非严格模式下,没有手动去通过设置调用方式,并且是在全局环境下调用的,所以this指向全局对象。

  function fn(){
      return this;
  }
  //在浏览器环境中
  console.log(fn() === window);//true
  //在node.js环境中
  console.log(fn() === globalThis);//true

然而,在严格模式下,如果没有为this设置值,那么this会保持为undefined。如:

    function fn(){
        'use strict';
        return this;
    }
    console.log(fn() === undefined) //true
tips:上例中,因为fn是直接被调用的,也就是并不是作为对象的属性来调用(window.fn),所以this应是undefined,有些浏览器在最初支持严格模式的时候并没有正确的实现这个功能,所以错误的返回了window对象。

如果想要改变this值,需要使用callapply方法。如例:

    var obj = { value:"this is custom object!"};
    var value = "this is global object!";
    var getThis = function(){
        return this.value;
    }
    console.log(getThis());//"this is global object!"
    console.log(getThis.apply(obj))//"this is custom object!"
    console.log(getThis.call(obj))//"this is custom object!"

类上下文

尽管ES6的类和函数有些相似,this的表现也会类似,但也有一些区别和注意事项。

在类当中,this就是一个常规的类对象,类里面定义的非静态的方法都会被添加到this对象的原型当中。例:

    class Test {
        constructor(){
            const p = Object.getPrototypeOf(this);
            console.log(Object.getOwnPropertyNames(p));
        }
        getName(){}
        getValue(){}
        static getNameAndValue(){}
    }
    new Test();//["constructor","getName","getValue"]
tips:静态方法不是this的属性,它们只是类自身的属性。

比如,我们要调用以上的getNameAndValue方法,我们可以像如下这样调用:

    Test.getNameAndValue();
    //或者
    const test = new Test();
    test.constructor.getNameAndValue();

派生类

在派生类当中,不会像基类那样,有初始的绑定。什么是派生类?也就是继承基类的类。例如:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {}
//这里的test就是一个派生类

在派生类的构造函数当中,如果不使用super绑定this,则在使用this的过程中会报错Must call super constructor in derived class before accessing 'this' or returning from derived constructor。大致意思就是要有一个super绑定。如:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        console.log(this);
    }
}
//ReferenceError

但是如果我们稍微改一下,如下:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        super();//这时候会生成一个this绑定
        console.log(this);
    }
}
//Test,继承了基类的属性和方法,相当于执行this = new Base()

派生类不能在没有super方法的构造函数中返回一个除对象以外的值,或者说是有super方法的前面直接返回一个对象以外的值也是不行的,除非根本就没有构造函数。如:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return 1;
        super();
    }
}
//TypeError

但是下面的示例不会出错:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return {};
        super();
    }
}

下面示例会报错:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return 1;
    }
}
//TypeError

下面示例不会报错:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return {};
    }
}

this和对象之间的转换

在非严格模式下,如果调用call或apply方法,传入的第一个参数,也就是被用作this的值不是一个对象,则会尝试被转换为对象。基本类型值,如null何undefined会被转换成全局对象,而像其他的基本类型值则会使用对应的构造函数来转换成对象。例如number类型数字1就会调用new Number(1),string类型'test'就会调用new String('test')。

例如:

function sum(c,d){
    return this.a + this.b + c + d;
}
var a = 3,b = 4;
var count = {
    a:1,
    b:2
}
//call方法后面的参数直接被用作函数的参数
console.log(sum.call(count,3,4));//10
console.log(sum.call(count,'3',4))//'334'
console.log(sum.call(null,3,4));//14
console.log(sum.call(undefined,'3',4));//'734'
console.log(sum.call(1,3,4));//new Number(1)上没有a和b属性,所以是this.a + this.b就是NaN,即两个undefined相加
console.log(sum.call('',1,'2'))//'NaN2'
//apply方法参数只能传数组参数
//TypeError
// console.log(sum.apply(count,3,4));
// console.log(sum.apply(count,'3',4))
// console.log(sum.apply(null,3,4));
// console.log(sum.apply(undefined,'3',4));
// console.log(sum.apply(1,3,4));
// console.log(sum.apply('',1,'2'))
//必须这样传
console.log(sum.apply(count,[3,4]));//10
console.log(sum.apply(count,['3',4]))//'334'
console.log(sum.apply(null,[3,4]));//14
console.log(sum.apply(undefined,['3',4]));//'734'
console.log(sum.apply(1,[3,4]));//new Number(1)上没有a和b属性,所以是this.a + this.b就是NaN,即两个undefined相加
console.log(sum.apply('',[1,'2']))//'NaN2'

再来看一个示例如下:

function test(){
    console.log(Object.prototype.toString.call(this))
}
console.log(test.call(7));//[object Number]
console.log(test.call(undefined));//[object global],在浏览器环境下指向为[Object window]
console.log(test.apply('123'));//[object String]

根据以上示例,我们就可以知道了利用Object.prototype.toString方法来判断一个对象的类型。如可以封装一个函数如下:

function isObject(value){
    return Object.prototype.toString.call(value) === '[object Object]';
}
//等价于
function isObject(value){
    return Object.prototype.toString.apply(value) === '[object Object]';
}
//等价于
function isObject(value){
    return {}.toString.call(value) === '[object Object]';
}
//等价于
function isObject(value){
    return {}.toString.apply(value) === '[object Object]';
}

bind方法

ES5引入了bind方法,该方法为Function的原型对象上的一个属性,在一个函数fn中调用fn.bind(object)将会创建一个和该函数相同作用域以及相同函数体的函数,但是它的this值将被绑定到bind方法的第一个参数,无论这个新创建的函数以什么方式调用。如:

function fn(){
    var value = "test";
    return this.value;
}
var obj = {
    value:"objName"
}
var newFn = fn.bind(obj);
console.log(fn.bind(obj)());//objName
console.log(newFn());//objName
var bindObj = {
    value:"bind",
    f:fn,
    g:newFn,
    h:fn.bind(bindObj)
}
var newBind = {
    a:fn.bind(bindObj)
}
console.log(bindObj.f());//bind
console.log(bindObj.g());//objName
console.log(bindObj.h());//undefined
console.log(newBind.a());//bind

箭头函数

在箭头函数中,this与封闭环境当中的上下文的this绑定一致,在全局环境中,那它的this就是全局对象。如:

var obj = {
    a:() => {
        return this;
    },
    b:function(){
        var x = () => { return this;};
        return x();
    }
}
console.log(obj.a());//global
console.log(obj.b());//obj
注意:无论使用call,apply还是bind其中的哪一种方法,都不能改变箭头函数的this指向,因为都将被忽略,但是仍然可以传递参数,理论上第一个参数设置为null或者undefined为最佳实践。

如:

    //在浏览器环境下globalObject是window对象
    let globalObject = this;
    let getThis = () => this;
    console.log(getThis() === globalObject);//true
    let obj = {
        getThis:getThis
    }
    console.log(obj.getThis() === globalObject);//true
    console.log(obj.getThis.call(obj) === globalObject);//true
    console.log(obj.getThis.apply(obj) === globalObject);//true
    // 使用bind并未改变this指向
    console.log(obj.getThis.bind(obj)() === globalObject);//true

也就是说,无论如何,箭头函数的this都指向它的封闭环境中的this。如下:

var obj = {
    a:() => {
        return this;
    },
    b:function(){
        var x = () => { return this;};
        return x();
    }
}
console.log(obj.a());//global在浏览器环境下是window对象
console.log(obj.b());//obj

作为某个对象

当调用某个对象中的函数中的方法时,在访问该函数中的this对象,将会指向这个对象。例如:

    var value = "this is a global value!";
    var obj = {
        value:"this is a custom object value!",
        getValue:function(){
            return this.value;
        }
    }
    console.log(obj.getValue());//"this is a custom object value!"

这样的行为方式完全不会受函数定义的方式和位置影响,例如:

    var value = "this is a global value!";
    var obj = {
        value:"this is a custom object value!",
        getValue:getValue
    }
    function getValue(){
        return this.value;
    }
    console.log(obj.getValue());//"this is a custom object value!"

此外,它只受最接近的引用对象的影响。如:

    var value = "this is a global value!";
    var obj = {
        value:"this is a custom object value!",
        getValue:getValue
    }
    obj.b = {
        value:"this is b object value!",
        getValue:getValue
    }
    function getValue(){
        return this.value;
    }
    console.log(obj.b.getValue());//"this is b object value!"

对象原型链中的this

在对象的原型链中,this同样也指向的是调用这个方法的对象,实际上也就相当于该方法在这个对象上一样。如:

   var obj = {
       sum:function(){
           return this.a + this.b;
       }
   }
   var newObj = Object.create(obj);
   newObj.a = 1;
   newObj.b = 2;
   console.log(newObj.sum());//3
   console.log(obj.sum());//NaN

上例中,newObj对象继承了obj的sum方法,并且我们未newObj添加了a和b属性,如果我们调用newObj的sum方法,this实际上指向的就是newObj这个对象,所以我们可以得到结果为3,但是我们调用obj.sum方法的时候,this指向的是obj,obj对象并没有a和b属性,所以也就是两个undefined相加,就会是NaN。obj就作为了newObj的原型对象,这也是原型链当中的一个非常重要的特点。

注意:Object.create()方法表示创建一个新对象,会以第一个参数作为新对象的原型对象,第一个参数只能为null或者新对象,不能为其它基本类型的值,如undefined,1,''等。

getter或setter中的this

在一个对象的setter或者getter中同样的this指向设置或者获取这个属性的对象。如:

   function average(){
       return (this.a + this.b + this.c) / 3;
   }
   var obj = {
       a:1,
       b:2,
       c:3
       get sum:function(){
           return this.a + this.b + this.c;
       }
   }
   Object.defineProperty(obj,'average',{
       get:average,
       enumerable:true,
       configurable:true
   });
   console.log(obj.average,obj.sum);//2,6

构造函数中的this对象

当一个函数被当做构造函数调用时(使用new关键字),this指向的就是实例化的那个对象。

注意:尽管构造函数返回的默认值就是this指向的那个对象,但是也可以手动设置成返回其它的对象,如果手动设置的值不是一个对象,则返回this对象。

如:

    function C(){
        this.a = 1;
    }
    var c1 = new C();
    console.log(c1.a);//1
    function C2(){
        var obj = {
            a:2
        }
        this.a = 3;
        return obj;
    }
    var c2 = new C2();
    console.log(c2.a);//2

在上例中实例化的c2的构造函数C2中,由于手动的设置了返回的对象obj,所以导致this.a = 3这条语句被忽略,从而得到结果为2,就好像"僵尸"代码。当然也不能算是"僵尸"代码,因为实际上它是被执行了的,只不过对外部没有造成影响,所以可以被忽略。

作为一个DOM事件处理函数

当函数是一个DOM事件处理函数,它的this就指向触发事件的元素(有一些浏览器在使用非addEventListener动态添加函数时不遵守这个约定)。如:

function changeStyle(e){
    console.log(this === e.currentTarget);//true
    console.log(this === e.target);//true
    //将背景色更改为红色
    this.style.setProperty('background',"#f00");
}

// 获取文档中所有的DOM元素
var elements = document.getElementsByTagName('*');

for(let i = 0,len = elements.length;i < len;i++){
    //为每个获取到的元素添加事件
    elements[i].addEventListener('click',changeStyle,false);
}

内联事件中的this

当在内联事件中调用函数时,this指向的就是这个元素。但只有最外层的代码才指向这个元素,如果是内部嵌套函数中没有指定this,则指向全局对象。如:

    <button type="button" onclick="document.writeln(this.tagName.toLowerCase())">clicked me</button> 
    <!-- 点击按钮会在页面中出现button -->
<button type="button" onclick="document.writeln((function(){return this})())">clicked me</button>
<!-- 在浏览器环境下页面会写入[object Window] -->

在类中更改this绑定

类中的this取决于如何调用,但实际上在开发当中,我们手动的去绑定this为该类实例是一个很有用的方式,我们可以在构造函数中去更改this绑定。如:

class Animal {
    constructor(){
        //利用bind方法让this指向实例化的类对象
        this.getAnimalName = this.getAnimalName.bind(this);
    }
    getAnimalName(){
        console.log("The animal name is ",this.animalName);
    }
    getAnimalNameAgain(){
        console.log("The animal name is ",this.animalName);
    }
    get animalName(){
        return "dog";
    }
}
class Bird {
    get animalName(){
        return "bird";
    }
}

let animal = new Animal();
console.log(animal.getAnimalName());//The animal name is dog;
let bird = new Bird();
bird.getAnimalName = animal.getAnimalName;
console.log(bird.getAnimalName());//The animal name is dog;

bird.getAnimalNameAgain = animal.getAnimalNameAgain;
console.log(bird.getAnimalNameAgain());//The animal name is bird;

在这个示例中,我们始终将getAnimalName方法的this绑定到实例化的Animal类对象上,所以尽管在Bird类中定义了一个animalName属性,我们在调用getAnimalName方法的时候,始终得到的就是Animal中的animalName属性。所以第二个打印仍然是dog

注意:在类的内部总是使用的严格模式,所以调用一个this值为undefined的方法会抛出错误。

打个广告,我在思否上线的课程玩转typescript1玩转typescript2适用于有一定基础的前端,还望大家多多支持,谢谢。

查看原文

赞 6 收藏 5 评论 0

夕水 发布了文章 · 3月19日

一个灵活高度自定义的JavaScript颜色选择器

ew-color-picker

这是一个用javascript编写的灵活的,高度自定义的颜色选择器。

使用场景

这个颜色选择器适用于中小型项目,例如主题的切换。不同于组件库中的颜色选择器组件,它的配置自主化,根据用户的需求来自定义。

优点

html5的原生颜色选择器样式不好看,而组件库的颜色选择器不够灵活多变,这样一来,就有了这个颜色选择器的诞生。

我们先来尝尝鲜,看看一个简单的示例:

  <!-引入颜色选择器的css样式-->
  <link rel="stylesheet" href="https://www.unpkg.com/ew-color-picker/dist/ew-color-picker.min.css">
  <!--引入插件JavaScript-->
  <script data-original="https://www.unpkg.com/ew-color-picker/dist/ew-color-picker.min.js"></script>

然后在页面中放一个元素:

  <div></div>

javascript中,我们只需要如下代码:

   const color = new ewColorPicker('div');

如此一来,一个简单的颜色选择器就出现在页面上了。可能大多数人不大喜欢实例化的方式,那么我们也提供了一个方法来创建它:

  const color = ewColorPicker.createColorPicker('div');

这样也可以创建一个颜色选择器实例。

tips:需要注意的就是,这些功能都是1.6.7版本加上的,所以请使用最新版本的js,实际上,以上展示的引入链接会自动帮我们引入最新版本的js,使用最新版本的js,确保我们在使用当中不会出现bug以及使用新功能,只要我在,这个插件就会自动更新,只要能想到的东西,都会加上去。

tips:还需要说明一点的是,为了遵循一个颜色选择器对应一个实例,所以,当传入的dom元素是多个的话,也会取第一个dom元素来实例化。例如传的是div元素,如果页面中有多个div元素,那实际上在颜色选择器内部获取到的div元素就是多个,但始终都会取第一个div元素来实例化。如果想要实例化多个颜色选择器,我们则可以像如下代码那样使用

   const elements = document.querySelectorAll('div');
   elements.forEach(item => new ewColorPicker(item));

我们也提供了一个方法getDefaultConfig来获取颜色选择器实例的默认配置对象。如下:

  ewColorPicker.getDefaultConfig();
tips:还需要注意的就是,传入的dom元素不能是'html','head','body','meta','title','link','style','script'这些特殊的元素,否则插件会在控制台给出一个错误提示。

tips:最新1.7.1版本允许添加到body元素中,当然还是不建议如此做,这个添加有些许bug。

这都是最简单的用法,可能这样不太直观,请看如下一个简单的示例:

demo1

看到这里,也许会有人疑问,这怎么就灵活多变,高度自定义呢?别着急,让我们继续。

自定义配置

我们来看一个配置对象,如下所示:

{
    hue: true,
    alpha: false,
    size: "normal",
    predefineColor: [],
    disabled: false,
    defaultColor: "",
    openPickerAni: "height",
    sure: emptyFun,
    clear: emptyFun,
    openPicker: emptyFun,
    isLog: true,
    changeColor: emptyFun,
    hasBox: true,
    isClickOutside: true,
    hasClear:true,
    hasSure:true,
    hasColorInput:true
}

我们先来分析第一个配置属性hue,或许我们看到一个完整的配置颜色选择器,应该是如下图所示:

1616080898(1).jpg

我们来着重分析一下每一块代表什么:

1.png

根据上图分析,我们也知道了hue的属性就是控制最右边的色阶柱的显隐,显然默认是显示的。

tips:如果是自定义配置,那么传入的元素在配置对象中就是el属性,例如,我们只需要一个颜色面板。那么我们可以编写如下代码:
  const color = new ewColorPicker({
      el:'div',
      hue:false
  })

如此一来,我们就会得到如下所示的颜色选择器:

2.png

正如图中所示,就一个红色的面板可供选择,这确实是一个不好的选择,不过没关系,我们提供了updateColor方法去手动改变颜色值。代码如下:

   //color为实例化的颜色选择器实例
   color.updateColor("#0ff");

当然,使用这个方法的前提是颜色选择器面板必须显示当中,并且传入的参数还要是一个正确格式的颜色,否则会在控制台给出错误提示,也不会生效。

请看如下一个示例:

demo

好,让我们接着来看第二个配置属性alpha,很显然这个属性是为了控制透明度柱子的显隐的,默认是不显示的。例如,我们可以这样修改:

   const color = new ewColorPicker({
      el:'div',
      hue:false,
      alpha:true
  })

以上代码就会得到如下图所示的一个颜色选择器:

3.png

可能很多同学注意到了从前面的一个示例中设置了这样一个hasBox属性,它的默认值是true,很显然这个值是控制色块的显隐的,如果该值为false,那么颜色面板默认就会显示。所以,我们提供了两个方法openPickerclosePicker来手动控制颜色面板的关闭,(PS:点击目标元素区域之外关闭稍后再说)。如下所示:

 //color为颜色选择器实例
 color.openPicker(openPickerAni);//参数为即height或opacity两种字符串值,等同后续的openPickerAni配置属性
 color.closePicker(openPickerAni);

我们来看如下一个手动控制颜色选择器关闭的示例:

demo

在以上示例中,可能有人注意到了isClickOutside这个属性,没错这个属性也是一个布尔值,默认为true,表示点击颜色面板之外的区域,就会关闭颜色面板。来看如下的示例:

<div id="demo"></div>
<button type="button" id="openClickOutsideBtn">开启|关闭目标区域元素点击事件</button>
button {
  float:right;
}
const color = new ewColorPicker({
  el:"#demo",
  isClickOutside:false
})
document.getElementById("openClickOutsideBtn").onclick = function(){
  color1.config.isClickOutside = !color1.config.isClickOutside;
}

demo

让我们继续,我们可以看到size属性,他的值可以是字符串值,也可以是对象。字符串值主要为这四个normal,medium,small,mini中的其中一个,或者也可以自定义一个对象{ width:25;height:25 },默认值是normal。当然设置该值的前提是将hasBox属性设置为true,盒子元素都不显示,设置该值有什么用呢?后面的openPickerAni属性与openPicker方法也是同样的必须要将hasBox设置为true,这也是默认将该值设置为true的原因。让我们来看看如下一个示例:

    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    const colors = document.querySelectorAll('div');
    const colorConfigs = [
        {
            size:"normal"
        },
        {
            size:"medium"
        },
        {
            size:"small"
        },
        {
            size:"mini"
        },
        {
            size:{
                width:25,
                height:25
            }
        }
    ];
    colors.forEach((item,index) => {
        new ewColorPicker({
            el:item,
            ...colorConfigs[index]
        })
    })

可以看到运行效果如下图所示:

4.png

让我们接着看下一个属性predefineColor,顾名思义,这个属性是一个数组,代表预定义颜色,每一个数组项必须是一个合格的颜色值,否则是不会渲染到颜色选择器上的。来看如下一个示例:

  <div></div>
    const color = new ewColorPicker({
        el:"div",
        predefineColor:["#2396ef","#fff","rgba(134,11,12,1)","#666"],
        alpha:true
    })

然后我们可以看到如下图所示:

5.png

让我们接着看下一个属性,disabled,这个属性的作用就是禁止点击盒子块元素打开颜色面板,也就是说如果hasBoxfalse的话,请忽略这个属性。

tips:后续可能会考虑增加颜色面板的禁止点击事件等等。

这个很简单没什么好说的,所以就不举例了。让我们接着来看下一个属性defaultColor,即默认显示的颜色值。如果检测到的颜色值不符合格式,则会在控制台给出错误提示,比如看这样一个示例:

  <div></div>
    const color = new ewColorPicker({
        el:"div",
        predefineColor:["#2396ef","#fff","rgba(134,11,12,1)","#666"],
        alpha:true,
        defaultColor:"#123"
    })

如下图所示:

6.png

tips:或许这个检测颜色值是否合格的机制有些许问题,后续会优化。

让我们接着来看下一个属性openPickerAni,它就只有两个值,和前面手动开启或关闭颜色选择器方法的参数值一样,这里就不必赘述,当然想要该属性生效不能将hasBox设置为false

同样的openPicker也是针对hasBoxtrue而生效的,它是点击色块元素的回调,它有两个回调的参数。即elcontext,也就是元素本身和元素本身的实例对象。

  const color = new ewColorPicker({
     el:"div",
     openPicker:(el,context) => {
        //可以通过context.config.pickerFlag来判断是打开还是关闭
     }
  })

同理clearsure就是清空按钮和确定按钮的回调,要让这两个回调生效,就不能将hasClearhasSure设置为false,因为这两个配置属性分别是空值清空和确定的显隐的。其中hasClear的回调参数为defaultColor,该值为空就为空,以及元素本身实例对象context。而sure的回调参数则是color值和元素本身实例对象。请看如下写法:

   const color = new ewColorPicker({
     el:"div",
     clear:(defaultColor,context) => {
        console.log(defaultColor,context);
     },
     sure:(color,context) => {
       console.log(color,context);
     }
   })

除了这两个回调之外,我们还额外增加了一个回调即changeColor函数,顾名思义,这个函数的作用就是当颜色值改变时触发,比如点击色阶柱改变色彩,点击透明度柱改变透明度等等。请看如下代码:

   const color = new ewColorPicker({
     el:"div",
     changeColor:(color) => {
         //颜色值只要改变时就触发,回调参数为改变后的颜色值
     }
   })

还有一个isLog属性,这个属性的默认值是true,表示会在控制台打印一些信息,请忽略这个属性,啊哈哈,后续考虑将它的默认值设置为false

最后一个就是hasColorInput属性,表示是否显示输入框,这在自定义输入框(比如和element ui的输入框绑定在一起)和颜色选择器绑定中十分有效,如果想要使用它,就不推荐设置为false

我们来看一个示例如下:

  <div></div>
    const color = new ewColorPicker({
        el:"div",
        hasColorInput:false,
        hasSure:false,
        hasBox:false,
        hasClear:false,
        alpha:true
    })

效果如下图所示:

7.png

目前最新版本为1.6.8,后续还会考虑加更多的功能,只要你有需求,跟我提,我觉得合理就会加,如果觉得本颜色选择器可以帮助到你,还望给个star,源码

更多描述可以参见文档官网和码云站点文档官网

tips:如果github访问太慢,可以访问码云站点的官网。

最后,后续有空的话,我会考虑写文章来分析这个颜色选择器的实现原理。

更新日志

  • 1.8.6 颜色选择器配置对象添加了响应式功能,无论是增删改颜色选择器的属性,都会触发颜色选择器的更新,同时添加了pickerAnimationTime属性,表示执行动画的时间,当然不建议将时间设置的过长,优化了一些代码。
  • 1.8.5 取消el属性和不传dom元素的验证,如果不传任何参数,则默认将颜色选择器添加到body中(但会生成一个容器元素来包含)。
  • 1.8.3 ~ 1.8.4 修复了一些问题,优化了一些代码
  • 1.8.2 修复了水平方向透明度改变问题,并调整了一下布局和优化了一些代码。
  • 1.8.1 修复了一些问题,并添加了hueDirectionalphaDirection属性。
  • 1.8.0 修复了添加到body中的问题,以及修改了将disabled设置为true的问题。
  • 1.7.9 修复了隐藏色块的问题。
  • 1.7.8 修复了将input隐藏的问题,优化了代码,将openPickerAni配置属性名更改为pickerAnimation
  • 1.7.7 修复了预定义颜色的一些问题,优化了一些代码。
  • 1.7.6 修复了一些问题,开放了内部工具方法,可通过ewColorPicker.util访问到所有工具方法,并修改了颜色值的验证,支持英文颜色单词的传入,例如设置预定义颜色以及默认颜色。
  • 1.7.5 修复了透明度默认赋值问题,将原openPicker点击色块打开或关闭的回调更名为openOrClosePicker,优化了代码。
  • 1.7.4:修复了颜色模式切换透明度不改变问题,新增了changeBoxByChangeColor配置属性,该属性表示是否在打开颜色面板,颜色值触发的时候,色块的背景色更改,如果不点击确定按钮,关闭颜色面板后会恢复到默认颜色,预定义颜色数组predefineColor新增了可以传数组项为对象或字符串,对象定义为{ color:"#123",disabled:true }(color为合格的颜色值,disabled为true或者false,表示是否禁用该预定义点击更换颜色)
  • 1.7.3:优化了颜色值算法,新增了boxDisabledopenChangeColorMode配置属性。
  • 1.7.2:新增了允许将颜色选择器添加到body中,但会生成一个容器元素来包含,将disabled配置属性更改成了全部禁止点击。
  • 1.3.3 ~ 1.7.1:添加了颜色选择器的基本功能。

打个广告,我在思否上线的课程玩转typescript1玩转typescript2适用于有一定基础的前端,还望大家多多支持,谢谢。

查看原文

赞 16 收藏 7 评论 0

夕水 回答了问题 · 3月11日

解决vuex useStore() 始终返回undefined

你是不是少了从vuex中引入这个方法?

  import { useStore } from 'vuex'

关注 2 回答 2

夕水 发布了文章 · 1月30日

2天用vue3.0实现《掘金 - 2020年度人气创作者榜单》网站

初看到掘金 - 2020年度人气创作者榜单这个网站,感觉整体界面效果给我一种清爽的感觉,于是花了点时间琢磨如何实现。目前实现的功能有:列表展示,搜索,无限加载(与原网站有些区别,加了loading效果),活动介绍,tab切换。通过这些,我对vue3.0的composition api有了一定的认知,下面让我们来看看吧!

ps:个人认为原网站应该是使用react.js写的

直接请求该网站的数据接口,应该是会报跨域问题的。于是我想了一个办法,就是通过node.js来爬取数据。下面来看看代码:

node后端爬取数据

代码如下:

const superagent = require('superagent');
const express = require('express');
const app = express();
const port = 8081;
function isObject(value) {
    return value && typeof value === 'object';
}
function getApi(url, params,method) {
    return new Promise((resolve) => {
        if (!isObject(params)) {
            return resolve(setResponse(400, null, '请传入参数!'));
        } else {
            let paramMethod = method.toLowerCase() === 'post' ? 'send' : 'query';
            superagent(method,url)[paramMethod](params).set('X-Agent', 'Juejin/Web').end((err, supRes) => {
                if (err) {
                    return resolve(setResponse(400, null, err));
                }
                let data = JSON.parse(supRes.text);
                resolve(setResponse(data.err_no === 0 ? 200 : data.err_no, data.data, data.err_msg));
            });
        }
    })
}
app.use(express.json());
app.all("*", function (req, res, next) {
    //设置允许跨域的域名,*代表允许任意域名跨域
    res.header("Access-Control-Allow-Origin", "*");
    //允许的header类型
    res.header("Access-Control-Allow-Headers", "content-type");
    //跨域允许的请求方式 
    res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
    if (req.method.toLowerCase() == 'options') {
        res.send(200);
    } else {
        next();
    }

});
function setResponse(code, data, message) {
    return {
        code: code,
        data: data,
        message: message
    }
}
app.post('/info', (req, res) => {
    const params = req.body;
    getApi('https://api.juejin.cn/list_api/v1/annual/info', params,'post').then(data => {
        res.send(JSON.stringify(data));
    })
})
app.post('/list', (req, res) => {
    const params = req.body;
    getApi('https://api.juejin.cn/list_api/v1/annual/list', params,'post').then(data => {
        res.send(JSON.stringify(data));
    });
})

app.get('/user',(req,res) => {
    const params = req.query;
    getApi('https://api.juejin.cn/user_api/v1/user/get',params,'get').then(data => {
        res.send(JSON.stringify(data));
    })
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

以上只是爬了主要的三个接口,如list接口,info接口以及user接口。当然还有登录功能没有写,掘金应该是通过cookie技术去实现判断用户是否登录的,当从掘金打开,跳往该网站,会向浏览器的cookie存储用户相关登录信息。如下图所示:

这一个功能的实现思路知道即可,源码不会实现。然后在该网站去获取cookie并传递参数给user接口既可以获取登录相关信息。

以上代码思路也很简单,就是通过搭建一个本地服务器,然后爬取该网站的三个主要的接口,主要使用了superagent这个库来进行爬取。相关API可以参考superagent文档。然后就是允许跨域的设置,用了node框架express。没什么技术难点。

web前端

技术点:vue3.0,typescript,vue-cli4.0,axios,less

首先分析一下页面,主要分为首页和活动介绍页。其中HeaderFooter组件作为一个公共组件,这是毋庸置疑的。当然,这两个组件的代码也比较简单,可以不做分析。如下:

Header
<template>
  <div class="header">
      <div class="header-logo"></div>
      <div class="header-screen"></div>
      <div class="header-cascade"></div>
      <div class="header-person"></div>
      <div class="header-python"></div>
      <div class="header-vue"></div>
      <div class="header-react"></div>
      <div class="header-phone"></div>
      <div class="header-phone-wolpe"></div>
      <div class="header-bug"></div>
      <div class="header-coffee"></div>
      <div class="header-year"></div>
      <div class="header-title"></div>
  </div>
</template>

显然,Header组件主要考查CSS布局,好吧,虽然可以说是模仿写了一遍布局(所有布局都是同理,没什么好说的),但也算是抄袭了(PS:希望掘金技术团队不介意吧)。

Footer
<template>
  <div class="footer">
    <ul class="footer-web">
      <li v-for="(web, index) in footerWebNavList" :key="web.text + index">
        <template v-if="web.url">
          <a :href="web.url" target="_blank">{{ web.text }}</a>
        </template>
        <template v-else>{{ web.text }}</template>
      </li>
    </ul>
    <div class="footer-app">
      <ul
        class="footer-app-item"
        v-for="(app, index) in footerAppNavList"
        :key="app + index"
      >
        <li
          v-for="(app_item, app_index) in app"
          :key="app_item.text + app_index"
        >
          <template v-if="app_item.url">
            <a :href="app_item.url" target="_blank">{{ app_item.text }}</a>
          </template>
          <template v-else>{{ app_item.text }}</template>
        </li>
      </ul>
    </div>
  </div>
</template>

<script lang="ts">
import { reactive, toRefs } from "vue";
interface FooterItem {
  text: string;
  url?: string;
}
type FooterList = Array<FooterItem>;
export default {
  setup() {
    const state = reactive({
      footerWebNavList: [
        {
          text: "@2020掘金",
        },
        {
          text: "关于我们",
          url: "https://juejin.cn/about",
        },
        {
          text: "营业执照",
          url: "https://juejin.cn/license",
        },
        {
          text: "用户协议",
          url: "https://juejin.cn/terms",
        },
        {
          text: "京ICP备18012699号-3",
          url: "https://beian.miit.gov.cn/",
        },
        {
          text: "京公网案备11010802026719号",
          url:
            "http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11010802026719",
        },
        {
          text: "北京北比信息技术有限公司版权所有",
        },
      ],
      footerAppNavList: [] as any[],
    });
    const first: FooterList = state.footerWebNavList.slice(0, 4);
    const second: FooterList = state.footerWebNavList.slice(4);
    state.footerAppNavList = [first, second];
    return {
      ...toRefs(state),
    };
  },
};
</script>

这个组件难度也不大,就是把导航数据归纳到一起了而已。

活动介绍页面也比较简单,就一个tab组件,然后其它都是图片布局。

<template>
  <div class="info-container">
    <Header />
    <div class="pc-info"></div>
    <div>
      <div class="home-button-container">
        <router-link to="/">
          <div class="home-button"></div>
        </router-link>
      </div>
      <div class="info-box">
        <div class="info-title"></div>
        <div class="info-box1"></div>
        <div class="info-box2"></div>
        <div class="info-box3"></div>
        <div class="info-box4">
          <div class="info-prizes">
            <div class="info-prizes-tab">
              <div
                class="info-prizes-tab1"
                :style="{ 'z-index': curInfoTab === 0 ? 3 : 1 }"
                @click="onChangeInfoTab(0)"
              ></div>
              <div
                class="info-prizes-tab2"
                :style="{ 'z-index': curInfoTab === 1 ? 3 : 1 }"
                @click="onChangeInfoTab(1)"
              ></div>
            </div>
            <div>
              <img
                :data-original="require('../assets/' + (curInfoTab === 0 ? 'individual' : 'group') + '_prize_web.png')"
                alt="图片加载中"
                style="width: 100%"
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { reactive, toRefs } from "vue";
import Header from "../components/Header.vue";
export default {
  components: {
    Header,
  },
  setup() {
    const state = reactive({
      curInfoTab: 0,
    });
    const onChangeInfoTab = (value: number) => {
      state.curInfoTab = value;
    };
    return {
      ...toRefs(state),
      onChangeInfoTab,
    };
  },
};
</script>

当然后续代码我就不一一展示了,我主要总结一下所用到的技术知识点。

首先是vuexvue2熟练使用的话,其实vue3语法也差别不大。

import { useStore } from "vuex";
 // store.state
 // store.dispath(方法名,数据)

主要如果子组件想通过事件传递给父组件,则需要通过mitt插件,譬如搜索组件的代码实现如下:

import mitt from 'mitt';
export const emitter = mitt();
export default {
  setup() {
    const state = reactive({
        keyword:""
    })
    const refState = toRefs(state);
    const onSearch = () => {
        if(!state.keyword)return alert('请输入你喜欢的作者名!');
        //传递给父组件
        emitter.emit('on-search',state.keyword);
    }
    return {
        ...refState,
        onSearch
    };
  },
};

其它的都是vue3.0的语法了,比如watch监听等等,更多源码在这里

PS:不知道到时间了掘金官方会不会停止相关数据接口的服务,所以下一步,我可能会考虑写静态数据,然后把axios封装一下,当然代码还有些粗糙,因为实现的有些匆忙,后续会做优化。

最后,附上部分效果图:

查看原文

赞 1 收藏 1 评论 0

夕水 回答了问题 · 1月14日

解决element 层级在复选模式下只能选最后的子分支数据要怎么做?

最简单的办法,就是通过css隐藏,比如给该联动选择器,也就是poperclass加一个特别的类名show-last-cascader:

   .show-last-cascader  {
       .el-cascader-menu{
         .el-checkbox {
            display:none;
         }
       }
   }

然后你需要做一次判断expand-change这个事件,去做判断,具体参考这个demo

关注 4 回答 3

夕水 回答了问题 · 2020-09-15

原生JS 我想鼠标划入 添加类名 清空兄弟类名

你应该就是想做类似TAB选项卡的功能吧,按你的需求来看,只需悬浮就可以添加选中类名。


 var lis = document.querySelectorAll(".sci_ban_tit");
 lis.forEach(item => {
     item.onmouseover = () => {
        //先全部清空选中,再给当前添加选中
        lis.forEach(item => item.classList.remove('active'));
        item.classList.add('active');
     }
 })
 

我想你需要的是这样的代码,不过用css也可以实现啊?

关注 6 回答 6

夕水 回答了问题 · 2020-09-07

关于 vue $refs 获取组件 this 指向的疑问

this.$refs.seamlessScroll指向的是vue-seamless-scroll这个组件实例,如果添加到DOM元素上,则指的是该DOM元素,与this是不一样的,详见vm.$refs用法

关注 4 回答 3

夕水 发布了文章 · 2020-09-06

从零开始编写一个时间线组件

一.搭建开发环境

PS:npm速度慢可使用cnpm

第一步,让我们先把项目环境搭建好,首先打开命令窗口,执行如下命令:

npm init

搭建好了package.json文件之后,接下来开始装依赖包,我们需要用到webpack webpack-cli来打包项目,执行如下命令:

npm install webpack webpack-cli --save-dev

在编写代码时,我们需要用到es6的语法,因此我们还需要安装@babel/core @babel/cli @babel/preset-env babel-loader依赖来处理es6兼容语法。继续执行如下命令:

npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-loader

接下来,创建一个babel.config.json文件,然后写入如下代码:

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1",
        },
        "useBuiltIns": "usage",
      }
    ]
  ]
}

这只是一个默认的配置,也可以自行根据需求来进行配置,更多信息详见babel文档

这还没有结束,我们还需要搭建vue的开发环境,我们需要编译.vue,所以我们需要安装vue-loader vue-template-compiler vue等依赖包,继续执行如下命令:

npm install vue vue-loader vue-template-compiler --save-dev

我们目前所需要的依赖就暂时搭建完成,接下来在页面根目录创建一个index.html文件,写入如下代码:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue-cli</title>
</head>
<body>
    <div id="app"></div>
</body>
<script data-original="/build.js"></script>
</html>

在这里,我们注意到了我们打包最后引入的文件为build.js文件,接下来我们开始编写webpack的配置,在根目录下继续创建一个webpack.config.js文件,代码如下:


const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    mode:"development",
    entry:'./main.js',
    output:{
        path:__dirname,
        filename:'build.js'
    },
    module:{
        rules:[
            {
                test: /\.vue$/,
                loader: "vue-loader"
            },
            {
                test: /\.js$/,
                loader: "babel-loader",
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}

在导出后面还需要加上这一段js代码,如下:

    resolve: {
        alias: {
            'vue': 'vue/dist/vue.js'
        }
    }

为什么要加上这一个配置,这个后面会说明原因,这里暂时先放置,继续在根目录下分别创建一个App.vuemain.js文件。代码分别如下:

import Vue from 'vue';
import App from './App.vue'

Vue.config.productionTip = false;

var vm = new Vue({
    el: "#app",
    // render:(h) => { return h(App)},
    components: {
        App
    },
    template: "<App />"
})
<template>
  <div id="app">
    <p>{{ msg }}</p>
  </div>
</template>
<script>
    export default {
        data() {
            return {
                msg: "hello,vue.js!"
            };
        },
        mounted() {

        },
        methods: {

        }
    };
</script>

接下来,执行命令webpack,然后我们就可以看到页面中会生成一个build.js文件,然后运行index.html文件,我们就可以在浏览器页面上看到hello,vue.js!的字符串,稍等,我们似乎忘记了什么,一般在开发中,谁会给你运行webpack命令来打包,不都是执行npm run build嘛,让我们在package.json中加上这一行代码

{
  "name": "timeline-project",
  "version": "1.0.0",
  "description": "a component with vue.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack" //这里是添加的代码
  },
  "keywords": [
    "timeline"
  ],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.11.5",
    "@babel/core": "^7.11.5",
    "@babel/preset-env": "^7.11.5",
    "babel-loader": "^8.1.0",
    "vue": "^2.6.12",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.12",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  }
}

等等,我们还忘了一件事,别人都可以使用npm run dev命令来在本地运行项目,我们为什么不可以呢?我们需要安装webpack-dev-server依赖,执行如下命令安装:

npm install webpack-dev-server --save-dev

安装完成,让我们继续在package.json中添加这样一行代码,如下所示:

{
  "name": "timeline-project",
  "version": "1.0.0",
  "description": "a component with vue.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server --open --hot --port 8081" //这里是添加的代码
  },
  "keywords": [
    "timeline"
  ],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.11.5",
    "@babel/core": "^7.11.5",
    "@babel/preset-env": "^7.11.5",
    "babel-loader": "^8.1.0",
    "vue": "^2.6.12",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.12",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  }
}

添加的代码很好理解,就是启动服务热更新,并且端口是8081。接下来让我们尝试运行npm run dev,看看发生了什么!

真的很棒,我们已经成功运行了vue-cli项目,搭建环境这一步到目前为止也算是成功了。

前面还提到一个问题,那就是在webpack.config.js中为什么要加上resolve配置,这是因为,如果我们需要在.vue文件中使用components选项来注册一个组件的话,就必须要引入完整的vue.js,也就是编译模板代码,如果我们只用render来创建一个组件,那么就不需要添加这个配置,这就是官网所说的运行时 + 编译器 vs. 只包含运行时

在这里我们还要注意一个问题,那就是我们需要处理单文件组件中的css样式,所以我们需要安装css-loader与style-loader依赖。执行如下命令:

npm install style-loader css-loader --save-dev

webpack.config.js中添加如下代码:


const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    mode:"development",
    entry:'./main.js',
    output:{
        path:__dirname,
        filename:'build.js'
    },
    resolve: {
        // if you don't write this,you can't use components to register component
        //only use render to register component
        alias: {
            'vue': 'vue/dist/vue.js'
        }
    },
    module:{
        rules:[
            {
                test: /\.vue$/,
                loader: "vue-loader"
            },
            {
                test: /\.css$/,
                loader: ["style-loader","css-loader"]
            },
            {
                test: /\.js$/,
                loader: "babel-loader",
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}

如果是用less或者stylus或者scss,我们还需要格外安装依赖,例如less需要安装less-loader,这里我们就只用style-loader与css-loader即可,如对less等感兴趣可自行研究。

特别说明:由于我们所编写的时间线组件并没有用到图标,所以无需添加图标以及图片的处理。

二.分析时间线组件结构以及搭建基本架构

时间线组件可以分成三部分组成,第一部分即时间线,第二部分即时间戳,第三部分则是内容。我们先来看时间线组件的一个结构:

<timeline>
  <timeline-item></timeline-item>
</timeline>

从上图我们可以看到时间线组件包含两个组件,即timelinetimeline-item组件,接下来我们来分析一下组件的属性有哪些。首先是父组件timeline组件,根据element ui官方文档。我们可以看到父组件仅仅只提供了一个reverse属性,即指定节点排序方向,默认为正序,但实际上我们还可以添加一个属性,那就是direction属性,因为element ui默认给的时间线组件只有垂直方向,而并没有水平方向,因此我们提供这个属性来确保时间线组件分为水平时间线和垂直时间线。

根据以上分析,我们总结如下:

  direction:'vertical' //或'horizontal'
  reverse:true //或false

接下来,我们来看子组件的属性,它包含时间戳,是否显示时间戳,时间戳位置,节点的类型,节点的图标,节点的颜色以及节点的尺寸。这里我们暂时忽略图标这个选项。因此我们可以将属性定义如下:

 timestamp:'2020/9/1' //时间戳内容
 showTimestamp:true //或false,表示是否显示时间戳
 timestampPlacement:'top' //或'bottom',即时间戳的显示位置
 nodeColor:'#fff'//节点的颜色值
 nodeSize:'size' //节点的尺寸
 nodeIcon:'el-icon-more' //节点的图标,在这里我们没有引入element ui组件,因此不添加这个属性,如果要添加这个属性,需要先编写图标组件

确定了以上属性之后,我们就可以先来编写一个静态的组件元素结构,如下图所示:

  <!-- 父组件结构 -->
  <div class="timeline">
    <!-- 子组件结构 -->
    <div class="timeline-item">
      <!-- 时间线 -->
      <div class="timeline-item-tail"></div>
      <!-- 时间线上的节点 -->
      <div class="timeline-item-node"></div>
      <!-- 时间线的时间戳与内容 -->
      <div class="timeline-item-wrapper">
        <!-- 时间戳,位置在top-->
        <div class="timeline-item-timestamp is-top"></div>
        <!-- 每一个时间戳对应的内容 -->
        <div class="timeline-item-content"></div>
        <!-- 时间戳,位置在bottom-->
        <div class="timeline-item-timestamp is-bottom"></div>
      </div>
    </div>
  </div>

根据以上代码,我们可以清晰的看到一个时间线的元素构成,为了确保布局方便,我们多写几个子元素,即time-line-item以及它的所有子元素。接下来,我们开始编写静态的样式。如下:

  .timeline {
        font-size: 14px;
        margin: 0;
        background-color: #ffffff;
    }

    .timeline-item {
        position: relative;
        padding-bottom: 20px;
    }

    .timeline-item-tail {
        position: absolute;
    }

    .is-vertical .timeline-item-tail {
        border-left: 3px solid #bdbbbb;
        height: 100%;
        left: 3px;
    }

    .is-horizontal .timeline-item .timeline-item-tail {
        width: 100%;
        border-top: 3px solid #bdbbbb;
        top: 5px;
    }

    .is-horizontal:after {
        content: " ";
        display: block;
        height: 0;
        visibility: hidden;
        clear: both;
    }

    .timeline-item.timeline-item-info .timeline-item-tail {
        border-color: #44444f;
    }

    .timeline-item.timeline-item-info .timeline-item-node {
        background-color: #44444f;
    }

    .timeline-item.timeline-item-info .timeline-item-content {
        color: #44444f;
    }

    .timeline-item.timeline-item-primary .timeline-item-tail {
        border-color: #2396ef;
    }

    .timeline-item.timeline-item-primary .timeline-item-node {
        background-color: #2396ef;
    }

    .timeline-item.timeline-item-primary .timeline-item-content {
        color: #2396ef;
    }

    .timeline-item.timeline-item-success .timeline-item-tail {
        border-color: #23ef3e;
    }

    .timeline-item.timeline-item-success .timeline-item-node {
        background-color: #23ef3e;
    }

    .timeline-item.timeline-item-success .timeline-item-content {
        color: #23ef3e;
    }

    .timeline-item.timeline-item-warning .timeline-item-tail {
        border-color: #efae23;
    }

    .timeline-item.timeline-item-warning .timeline-item-node {
        background-color: #efae23;
    }

    .timeline-item.timeline-item-warning .timeline-item-content {
        color: #efae23;
    }

    .timeline-item.timeline-item-error .timeline-item-tail {
        border-color: #ef5223;
    }

    .timeline-item.timeline-item-error .timeline-item-node {
        background-color: #ef5223;
    }

    .timeline-item.timeline-item-error .timeline-item-content {
        color: #ef5223;
    }

    .is-horizontal .timeline-item {
        float: left;
    }

    .is-horizontal .timeline-item-wrapper {
        padding-top: 18px;
        left: -28px;
    }

    .timeline-item-node {
        background-color: #e1e6e6;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
    }

    .timeline-item-node-normal {
        width: 12px;
        height: 12px;
        left: -2px;
    }

    .timeline-item-node-large {
        width: 14px;
        height: 14px;
        left: -4px;
    }

    .timeline-item-wrapper {
        position: relative;
        top: -3px;
        padding-left: 28px;
    }

    .timeline-item-content {
        font-size: 12px;
        color: #dddde0;
        line-height: 1;
    }

    .timeline-item-timestamp {
        color: #666;
    }

    .timeline-item-timestamp.is-top {
        margin-bottom: 8px;
        padding-top: 6px;
    }

    .timeline-item-timestamp.is-bottom {
        margin-top: 8px;
    }

    .timeline-item:last-child .timeline-item-tail {
        display: none;
    }

接下来我们就要开始实现组件的逻辑封装了,首先我们需要封装timeline组件,为了将该组件归纳到一个目录下,我们先新建一个目录,叫timeline,然后新建一个index.vue文件,并且将我们编写好的css代码给移到该文件下,现在,你看到该文件的代码应该如下所示:

<script>
    export default {
        name: "timeline"
    }
</script>
<style>
    .timeline {
        font-size: 14px;
        margin: 0;
        background-color: #ffffff;
    }

    .timeline-item {
        position: relative;
        padding-bottom: 20px;
    }

    .timeline-item-tail {
        position: absolute;
    }

    .is-vertical .timeline-item-tail {
        border-left: 3px solid #bdbbbb;
        height: 100%;
        left: 3px;
    }

    .is-horizontal .timeline-item .timeline-item-tail {
        width: 100%;
        border-top: 3px solid #bdbbbb;
        top: 5px;
    }

    .is-horizontal:after {
        content: " ";
        display: block;
        height: 0;
        visibility: hidden;
        clear: both;
    }

    .timeline-item.timeline-item-info .timeline-item-tail {
        border-color: #44444f;
    }

    .timeline-item.timeline-item-info .timeline-item-node {
        background-color: #44444f;
    }

    .timeline-item.timeline-item-info .timeline-item-content {
        color: #44444f;
    }

    .timeline-item.timeline-item-primary .timeline-item-tail {
        border-color: #2396ef;
    }

    .timeline-item.timeline-item-primary .timeline-item-node {
        background-color: #2396ef;
    }

    .timeline-item.timeline-item-primary .timeline-item-content {
        color: #2396ef;
    }

    .timeline-item.timeline-item-success .timeline-item-tail {
        border-color: #23ef3e;
    }

    .timeline-item.timeline-item-success .timeline-item-node {
        background-color: #23ef3e;
    }

    .timeline-item.timeline-item-success .timeline-item-content {
        color: #23ef3e;
    }

    .timeline-item.timeline-item-warning .timeline-item-tail {
        border-color: #efae23;
    }

    .timeline-item.timeline-item-warning .timeline-item-node {
        background-color: #efae23;
    }

    .timeline-item.timeline-item-warning .timeline-item-content {
        color: #efae23;
    }

    .timeline-item.timeline-item-error .timeline-item-tail {
        border-color: #ef5223;
    }

    .timeline-item.timeline-item-error .timeline-item-node {
        background-color: #ef5223;
    }

    .timeline-item.timeline-item-error .timeline-item-content {
        color: #ef5223;
    }

    .is-horizontal .timeline-item {
        float: left;
    }

    .is-horizontal .timeline-item-wrapper {
        padding-top: 18px;
        left: -28px;
    }

    .timeline-item-node {
        background-color: #e1e6e6;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
    }

    .timeline-item-node-normal {
        width: 12px;
        height: 12px;
        left: -2px;
    }

    .timeline-item-node-large {
        width: 14px;
        height: 14px;
        left: -4px;
    }

    .timeline-item-wrapper {
        position: relative;
        top: -3px;
        padding-left: 28px;
    }

    .timeline-item-content {
        font-size: 12px;
        color: #dddde0;
        line-height: 1;
    }

    .timeline-item-timestamp {
        color: #666;
    }

    .timeline-item-timestamp.is-top {
        margin-bottom: 8px;
        padding-top: 6px;
    }

    .timeline-item-timestamp.is-bottom {
        margin-top: 8px;
    }

    .timeline-item:last-child .timeline-item-tail {
        display: none;
    }
</style>

三.时间线组件逻辑

PS:加载sourceMap还需要这样一个配置devtool: 'inline-source-map'

为了确保父子组件共享状态,我们利用provide/inject API来传递this对象,如下所示:

  export default {
    name: "timeline",
    provide(){
        return {
            timeline:this
        }
    }
  }

然后我们开始定义父组件的属性,根据前面所述,它包含两个属性,因此我们定义好在props中,如下所示:

    import { oneOf } from '../util'
    export default {
        name: "timeline",
        provide(){
            return {
                timeline:this
            }
        },
        props:{
            reverse:{
                type:Boolean,
                default:false
            },
            direction:{
                type:String,
                default:'vertical',
                validator:(value) => {
                    return oneOf(['vertical','horizontal'],value,'vertical');
                }
            }
        }
    }

上面代码需要用到一个工具函数oneOf,顾名思义,就是必须是其中的一项,它有三个参数,第一个参数是匹配的数组,第二个参数是匹配的项,第三个是提供的默认项,该工具函数代码如下:

export const oneOf = (arr,value,defaultValue) => {
    return arr.reduce((r,i) => i === value ? i : r,defaultValue);
}

其实也不难理解,就是我们想要的值必须是数组的一项,如果不是就返回默认项,工具函数内部代码,我们可以写得更清晰明了一点,如下所示:

export const oneOf = (arr,value,defaultValue) => {
    return arr.reduce((result,item) => {
      return item === value ? item : value;
    },defaultValue);
}

以上的代码经过简洁处理就得到了前面的一行代码,如果理解不了,可以采用后者的代码,至于validator验证选项,可参考vue-prop-validator-自定义验证函数

接下来,我们在render方法中去渲染这个父组件,代码如下:

import { oneOf } from '../util'
    export default {
        name: "timeline",
        provide(){
            return {
                timeline:this
            }
        },
        props:{
            reverse:{
                type:Boolean,
                default:false
            },
            direction:{
                type:String,
                default:'vertical',
                validator:(value) => {
                    return oneOf(['vertical','horizontal'],value,'vertical');
                }
            }
        },
        //新添加的内容
        render(){
            const reverse = this.reverse;
            const direction = this.direction;
            const classes = {
                'timeline':true,
                'is-reverse':reverse,
                ['is' + direction]:true
            }
            const slots = this.$slots.default || [];
            if(reverse)slots = slots.reverse();
            return (<div class={classes}>{slots}</div>)
        }
    }

以上代码似乎不是很好理解,其实也不难理解,首先是获取reversedirection属性,接着就是设置类名对象,类型对象包含三个,然后就是获取该组件的默认插槽内容,判断如果提供了reverse属性,则调用数组的reverse方法来让slots倒序,在这里我们应该很清晰的明白 slots如果存在,那么则一定是一个vNode节点组成的数组,如果没有,默认就是一个空数组。然后最后返回一个父元素包含该插槽的jsx元素。

在这里,我们使用了jsx语法,而我们的项目环境当中还并没有添加处理jsx的依赖,所以我们需要再次安装处理jsx语法的依赖babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx,并添加相应的配置。继续在终端输入以下命令安装依赖:

npm install babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx --save-dev

然后在babel.config.json中添加一行配置代码如下:

 "plugins": ["transform-vue-jsx"]

接着在webpack.config.js中添加一行配置处理如下:

{
    test: /\.(jsx?|babel|es6|js)$/,
    loader: 'babel-loader',
    exclude: /node_modules/
}

现在,我们可以看到页面中不会报错,未处理jsx了。

到此为止,父组件的逻辑代码也就完成了,接下来,让我们在全局里面使用一下该组件,看看是否生效。

main.js里面添加如下一行代码:

import Timeline from './components/timeline/timeline.vue'
Vue.component(Timeline.name,Timeline)

然后在App.vue里面,我们使用这个组件,代码如下:

<template>
    <div id="app">
        <timeline></timeline>
    </div>
</template>

ok,似乎看起来没什么问题,让我们继续编写子组件的逻辑代码。

接下来,新建一个item.vue文件,在该文件中编写如下代码:

<template>
    <div class="timeline-item">
        <div class="timeline-item-tail"></div>
        <div class="timeline-item-node" 
            :class="[`timeline-item-node-${ size || ''}`,`timeline-item-node-${type || ''}`]" 
            :style="{ backgroundColor:color }"
            v-if="!$slots.dot"
        ></div>
        <div class="timeline-item-node" v-if="$slots.dot">
            <slot name="dot"></slot>
        </div>
        <div class="timeline-item-wrapper">
            <div class="timeline-item-timestamp" 
                :class="[`is-`+ timestampPlacement ]" 
                v-if="item.placement === 'top' && showTimestamp"
            >{{ timestamp }}</div>
            <div class="timeline-item-content"><slot></slot></div>
            <div class="timeline-item-timestamp" 
                :class="[`is-`+ timestampPlacement ]" 
                v-if="item.placement === 'bottom' && showTimestamp"
            >{{ timestamp }}</div>
        </div>
    </div>
</template>
<script>
export default {
    name:"timeline-item",
    inject:['timeline'],
    props:{
        timestamp:String,
        showTimestamp:{
            type:Boolean,
            default:false
        },
        timestampPlacement:{
            type:String,
            default:'top',
            validator:(value) => {
                return oneOf(['top','bottom'],value,'bottom')
            }
        },
        type:{
            type:String,
            default:'default',
            validator:(value) => {
                return oneOf(['default','info','primary','success','warning','error'],value,'default');
            }
        },
        size:{
            type:String,
            default:'normal',
            validator:(value) => {
                return oneOf(['normal','large'],value,'normal')
            }
        },
        color:String
    }
}
</script>

编写完成之后,让我们继续在main.js中引用它,然后在App.vue中使用它。代码分别如下:

 //main.js
 import TimelineItem from './components/timeline/timeline-item.vue'
 Vue.component(TimelineItem.name,TimelineItem)
<!--App.vue-->
<timeline>
    <timeline-item type="default" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">待审核</timeline-item>
    <timeline-item type="info" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">审核中</timeline-item>
    <timeline-item type="error" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">审核失败</timeline-item>
    <timeline-item type="success" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">审核完成</timeline-item>
</timeline>

接下来我们就可以看到一个时间线已经完美的展示了,时间线组件算是大功告成了。

本文档已经录制为视频,地址如下:

搭建开发环境
分析与创建时间线基本架构
编写时间线逻辑

源码和文档已经上传到github

查看原文

赞 5 收藏 4 评论 0

夕水 回答了问题 · 2020-08-28

解决一个按钮调用不同的接口

这里提供一个在线demo给你看看。

关注 4 回答 4

夕水 回答了问题 · 2020-08-13

vue组件内更新DOM绑定方法,this的指向竟然不是VueComponent对象?

function() {
  console.log(this);
}
//函数会有作用域,可以使用箭头函数
() => { console.log(this); }

关注 4 回答 3

夕水 回答了问题 · 2020-08-11

js怎么把对象合并起来?

   let cn = json.reduce((r,i) => (r[i.permission] = i['title'],r),{});

关注 4 回答 3

夕水 回答了问题 · 2020-08-07

js监听iframe的点击事件

可以使用postMessage api将事件发送,然后另一边接收。

关注 3 回答 3

夕水 发布了文章 · 2020-07-14

20个无库无框架的小型web项目(中文版)

前言

前一阵子看到了大火的github项目vanillawebprojects,对其中的实现有些好奇,于是研究了写法,但是对英文有些不太熟悉的我,无法忍受这样的纯英文项目,于是花了二周时间,潜心研究了项目的实现思路,然后自己实现了这20web项目的中文版。虽然思路来源于原项目,但是很多功能我做了扩展,相信细心的人一定会发现有哪些不同之处,让我们来一一看一下吧。

表单验证

效果如下图所示:

在线示例
github源码地址

挑选电影座位

效果如下图所示:

在线示例
github源码地址

视频播放器

效果如下图所示:

在线示例
github源码地址

汇率计算器

效果如下图所示:

在线示例
github源码地址

DOM数组方法

效果如下图所示:

在线示例
github源码地址

弹窗与滑动菜单

效果如下图所示:

在线示例
github源码地址

猜单词的双人小游戏

效果如下图所示:

在线示例
github源码地址

查找膳食

效果如下图所示:

在线示例
github源码地址

费用追踪器

效果如下图所示:

在线示例
github源码地址

音乐播放器

效果如下图所示:

在线示例
github源码地址

无限滚动

效果如下图所示:

在线示例
github源码地址

打字游戏

效果如下图所示:

在线示例
github源码地址

文本阅读器

效果如下图所示:

在线示例
github源码地址

记忆卡片

效果如下图所示:

在线示例
github源码地址

歌词搜索器

效果如下图所示:

在线示例
github源码地址

呼吸调节器

效果如下图所示:

在线示例
github源码地址

打砖块小游戏

效果如下图所示:

在线示例
github源码地址

新年倒计时

效果如下图所示:

在线示例
github源码地址

拖拽排序列表

效果如下图所示:

在线示例
github源码地址

猜数字小游戏

效果如下图所示:

在线示例
github源码地址

如果有人能发现bug,欢迎提issue,如果这些项目能够帮助到您,希望您能点个star

查看原文

赞 32 收藏 23 评论 0

夕水 发布了文章 · 2020-07-10

用JavaScript无canvas来完成一个柱状图表

前言

提起数据可视化技术,都不免会让人想到echarts,而最简单的入门图表就是柱状图了。它是基于canvas来实现的,而我在想,如果不用canvas,我是否能不用canvas实现柱状图,经过我的探索,终于实现了一款柱状图表。

让我们查看一个已经完成的在线示例,如下图所示:

分析实现思路

首先,我们需要确定柱状图表有哪些部分,第一右上角头部有legend部分,第二有xy轴部分,第三就是柱状图部分了。好了确定了有哪些部分,我们就可以很好的实现了,好了,让我们进入正题吧。

实现静态页面结构

编写html

目前我们完成的成品是已经封装好的,然后页面就只有一个容器元素。但我们最开始肯定不能这样写,我们先写一个写死的结构如下所示:

<div id="weekCost" class="ew-charts">
        <ew-charts-body>
            <ew-charts-legend>
                <i class="leg-1"></i>
                <span>直接访问</span>
                <i class="leg-2"></i>
                <span>邮件营销</span>
                <i class="leg-3"></i>
                <span>联盟广告</span>
                <i class="leg-4"></i>
                <span>视频广告</span>
                <i class="leg-5"></i>
                <span>搜索引擎</span>
            </ew-charts-legend>
            <ew-charts-x>
                <div class="x-1" style="letter-spacing:2px;">一月</div>
                <div class="x-2" style="letter-spacing:2px;">二月</div>
                <div class="x-3" style="letter-spacing:2px;">三月</div>
                <div class="x-4" style="letter-spacing:2px;">四月</div>
                <div class="x-5" style="letter-spacing:2px;">五月</div>
                <div class="x-6" style="letter-spacing:2px;">六月</div>
                <div class="x-7" style="letter-spacing:2px;">七月</div>
            </ew-charts-x>
            <ew-charts-y>
                <div class="y-1">500</div>
                <div class="y-2">1000</div>
                <div class="y-3">1500</div>
                <div class="y-4">2000</div>
            </ew-charts-y>
            <ew-charts-zone>
                <div class="zone-1">
                    <bar class="bar-1 dataId-1-1" data-value="320"></bar>
                    <bar class="bar-2 dataId-1-2" data-value="120"></bar>
                    <bar class="bar-3 dataId-1-3" data-value="220"></bar>
                    <bar class="bar-4 dataId-1-4" data-value="150"></bar>
                    <bar class="bar-5 dataId-1-5" data-value="862"></bar>
                </div>
                <div class="zone-2">
                    <bar class="bar-1 dataId-2-1" data-value="332"></bar>
                    <bar class="bar-2 dataId-2-2" data-value="132"></bar>
                    <bar class="bar-3 dataId-2-3" data-value="182"></bar>
                    <bar class="bar-4 dataId-2-4" data-value="232"></bar>
                    <bar class="bar-5 dataId-2-5" data-value="1018"></bar>
                </div>
                <div class="zone-3">
                    <bar class="bar-1 dataId-3-1" data-value="301"></bar>
                    <bar class="bar-2 dataId-3-2" data-value="101"></bar>
                    <bar class="bar-3 dataId-3-3" data-value="191"></bar>
                    <bar class="bar-4 dataId-3-4" data-value="201"></bar>
                    <bar class="bar-5 dataId-3-5" data-value="964"></bar>
                </div>
                <div class="zone-4">
                    <bar class="bar-1 dataId-4-1" data-value="334"></bar>
                    <bar class="bar-2 dataId-4-2" data-value="134"></bar>
                    <bar class="bar-3 dataId-4-3" data-value="234"></bar>
                    <bar class="bar-4 dataId-4-4" data-value="154"></bar>
                    <bar class="bar-5 dataId-4-5" data-value="1026"></bar>
                </div>
                <div class="zone-5">
                    <bar class="bar-1 dataId-5-1" data-value="390"></bar>
                    <bar class="bar-2 dataId-5-2" data-value="90"></bar>
                    <bar class="bar-3 dataId-5-3" data-value="290"></bar>
                    <bar class="bar-4 dataId-5-4" data-value="190"></bar>
                    <bar class="bar-5 dataId-5-5" data-value="1679"></bar>
                </div>
                <div class="zone-6">
                    <bar class="bar-1 dataId-6-1" data-value="330"></bar>
                    <bar class="bar-2 dataId-6-2" data-value="230"></bar>
                    <bar class="bar-3 dataId-6-3" data-value="330"></bar>
                    <bar class="bar-4 dataId-6-4" data-value="330"></bar>
                    <bar class="bar-5 dataId-6-5" data-value="1600"></bar>
                </div>
                <div class="zone-7">
                    <bar class="bar-1 dataId-7-1" data-value="320"></bar>
                    <bar class="bar-2 dataId-7-2" data-value="210"></bar>
                    <bar class="bar-3 dataId-7-3" data-value="310"></bar>
                    <bar class="bar-4 dataId-7-4" data-value="410"></bar>
                    <bar class="bar-5 dataId-7-5" data-value="1570"></bar>
                </div>
            </ew-charts-zone>
        </ew-charts-body>
    </div>

编写css

接下来就需要根据页面元素,一个一个的添加样式了,这是一个慢工细活的过程,需要慢慢来。

/**
* 功能:普通页面样式设置
**/
/*********************************************/
/* 样式初始化部分 */
/*********************************************/
* {
    margin: 0;
    padding: 0;
}
body,html {
    height: 100%;
    font: 20px "微软雅黑";
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    overflow: hidden;
}
/* 转换为IE盒子模型 */
*,*::before,*::after {
    box-sizing: border-box;
}

/* 手型按钮 */
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="radio"],
input[type="checkbox"],
a {
    cursor: pointer;
}

button,
input,
textarea,
select {
    outline: none;
}
@charset "utf-8";

/**
* 功能:统计图表样式
**/
/**** 图表自定义标签初始化部分 ****/
.ew-charts,
ew-charts-body,
ew-charts-x,
ew-charts-y,
ew-charts-zone,
ew-charts-legend {
    display: block;
}

ew-charts-x,
ew-charts-x>div,
ew-charts-y,
ew-charts-y>div {
    box-sizing: border-box;
    position: absolute;
    overflow: hidden;
}

ew-charts-zone,
ew-charts-zone>div,
ew-charts-zone>div bar {
    box-sizing: border-box;
}

ew-charts-body,
ew-charts-zone>div,
ew-charts-zone>div bar {
    position: relative;
}

ew-charts-zone,
ew-charts-zone>div bar,
ew-charts-legend,
ew-charts-zone>div bar>span {
    position: absolute;
}

/* 图表容器 */
.ew-charts {
    width: 100%;
    height: 100%;
    color: #f8f5fa;
    background: linear-gradient(to right, #234, #789);
    margin: auto;
    color: #b3b3b3;
}

/*表体*/
ew-charts-body {
    width: 100%;
    height: 100%;
    font-size: 16px;
}

/*X轴*/
ew-charts-x {
    width: 90%;
    height: 8%;
    border-top: 1px solid #fefefe;
    left: 6%;
    bottom: 0;
}

ew-charts-x>div {
    height: 100%;
    text-align: center;
    line-height: 30px;
    top: 0;
}

/*Y轴*/
ew-charts-y {
    width: 6%;
    height: 80%;
    border-right: 1px solid #fefefe;
    overflow: visible;
    left: 0;
    top: 12%;
}

ew-charts-y>div {
    width: 100%;
    height: 24px;
    text-align: right;
    padding-right: 6px;
    left: 0;
}

/*表格数据区间*/
ew-charts-zone {
    width: 90%;
    height: 80%;
    left: 6%;
    top: 12%;
}

ew-charts-zone>div {
    height: 100%;
    float: right;
}

ew-charts-zone>div bar {
    height: 0;
    bottom: 0;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
    text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
    transition: 0.6s cubic-bezier(.19, .55, .58, 1.3);
    /*默认值设置*/
    background-color: #606060;
    border: 1px solid #cdcdcd;
    box-shadow: 0 0 5px #606060;
}

ew-charts-zone>div bar:hover {
    z-index: 10;
}

ew-charts-zone>div bar>span {
    left: 50%;
    top: -40px;
    transform: translateX(-50%);
    font: 32px "方正姚体", "arial";
    opacity: 0;
}

ew-charts-zone>div bar>span.animation {
    animation: data-value-show 0.6s forwards;
}

/*图注*/
ew-charts-legend {
    top: 10px;
    right: 4%;
}

ew-charts-legend i,
ew-charts-legend span {
    display: inline-block;
    vertical-align: middle;
}

ew-charts-legend i {
    width: 34px;
    height: 20px;
    border-radius: 3px;
    margin-left: 12px;
    margin-right: 6px;
    background-color: #606060;
    border: 1px solid #cdcdcd;
}
ew-charts-legend span {
    letter-spacing: 2px;
}
/*图表动画部分*/
@keyframes data-value-show {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

编写js

首先,我们需要定义一个函数,用于封装。

function ewCharts(options) {
    //这里是判断传入的参数中是否含有color属性,从而给予color属性值
    if (!Array.isArray(options.color) || options.color.length !== options.data.Y.length) {
        let len = options.data.Y.length - options.color.length;
        for (let i = 0; i < len; i++) {
            options.color.push('#ffffff');
        }
    }
    //为后期的扩展做准备,type类型为bar就是默认的柱状图
    options.type = options.type === "bar" ? options.type : "bar";
    //将参数赋值到实例上
    this.options = options;
    //开始初始化
    this.init(options);
}

接着,我们可以看到页面效果颜色有点高亮,接下来就是完成颜色的高亮效果工具函数,如下所示:

/**
 * 颜色高亮
 */
ewCharts.prototype.lightColor = function (color) {
    // 传入的颜色为16进制颜色模式,如:#ffffff
    let everyColorLight = function (lightColor) {
        // 将传入的颜色转换成16进制数字,然后再乘以1.6相当于将颜色变亮1.6倍
        const value = Math.round(parseInt(lightColor, 16) * 1.6);
        // 值有一个最小值与最大值,当超过255则等于255,最小值不能小于16
        return (value >= 255 ? 255 : value <= 16 ? 16 : value).toString(16);
    }
    // 相当于处理每一区间的颜色代码,除了#之外的,每2位代表一种颜色,如#fef2f3,则f2代表红色区间,f2代表绿色区间,f3代表蓝色区间
    return '#' + everyColorLight(color.slice(1, 3)) + everyColorLight(color.slice(3, 5)) + everyColorLight(color.slice(5, 7));
}

然后,我们需要创建一个设置样式的函数,如下所示:

/**
 * 样式规则设置
 */
ewCharts.prototype.setStyle = function () {
    //这里的操作无非就是判断页面中是否含有link标签,如果含有,就将样式规则插入到该标签所包含的样式表中
    let link = this.$('link', false), linkIndex = 0;
    for (let i = 0, len = link.length; i < len; i++) {
        if (/\w+\.css/.test(link[i].getAttribute('href'))) {
            linkIndex = i;
        }
    }
    //api文档https://www.w3school.com.cn/xmldom/met_cssstylesheet_insertrule.asp
    return link[linkIndex].sheet.insertRule.bind(link[linkIndex].sheet);
}

然后,我们再来完成一个获取DOM元素的函数封装,如下所示:

/**,
 * 获取DOM元素
 */
ewCharts.prototype.$ = function (selector, isSingle) {
    // 如果传入的包含#,则是唯一的元素执行querySelector方法,否则根据传入的布尔值来判断执行哪个方法查询DOM
    isSingle = selector.indexOf('#') > -1 ? true : typeof isSingle === 'boolean' ? isSingle : true;
    return isSingle ? document.querySelector(selector) : document.querySelectorAll(selector);
}

然后,我们就来完成初始化函数,如下所示:

/**
 * 初始化
 */
ewCharts.prototype.init = function (options) {
    // 设置样式规则
    let setStyle = this.setStyle();
    //图表类型判断,为后期做扩展
    switch (options.type) {
        case "bar":
            //初始化页面图表所有部分
            this.resetAllCharts(this.$(options.el));
            //初始化X轴部分
            this.resetChartsX(options.data.X, setStyle);
            //初始化Y轴部分
            this.resetChartsY(options.data.Y, setStyle);
            //初始化图注部分
            this.resetChartsLegend(options.data, setStyle);
            break;
    }
}

然后,完成初始化页面图表的结构,前面页面结构和css写好只有,页面应该只保留一个容器元素,如下所示:

<div id="weekCost"></div>

接下来,我们就往该元素添加结构,如下所示:

/**
 * 初始化图表结构
 */
ewCharts.prototype.resetAllCharts = function (el) {
    el.innerHTML = "<ew-charts-body>" +
        "<ew-charts-legend></ew-charts-legend>" +
        "<ew-charts-x></ew-charts-x>" +
        "<ew-charts-y></ew-charts-y>" +
        "<ew-charts-zone></ew-charts-zone>" +
        "</ew-charts-body>";
    //为容器元素添加一个类名
    el.classList.add('ew-charts');
    return el;
}

继续初始化X轴,如下所示:

/**
 * 设置X轴
 * x轴的数据
 * 设置样式的方法
 */
ewCharts.prototype.resetChartsX = function (dataX, setStyle) {
    let chartsX = this.$('ew-charts-x'), chartsXHTML = '';
    let dataXLen = dataX.length;
    // 添加x轴的文本元素
    for (let i = 0; i < dataXLen; i++) {
        chartsXHTML += "<div class=x-" + (i + 1) + " style='letter-spacing:2px;'>" + dataX[i] + "</div>";
    }
    chartsX.innerHTML = chartsXHTML;
    let chartsXContent = this.$('ew-charts-x > div', false), chartsXContentWidthArr = [];
    // 获取元素的宽度数组,并找到最大宽度,从而设置每个元素的宽度为最大宽度
    for (let j = 0; j < dataXLen; j++) {
        chartsXContentWidthArr.push(chartsXContent[j].offsetWidth);
    }
    //最大宽度与单位宽度以及单位宽度的一半
    let maxWidth = Math.max.apply(null, chartsXContentWidthArr), unitWidth = parseInt(100 / dataXLen), half = unitWidth / 2;
    for (let k = 0; k < dataXLen; k++) {
        //循环分别设置x轴上的坐标数据的元素宽度与left偏移量
        setStyle('ew-charts-x > div.x-' + (k + 1) + '{width:' + maxWidth + 'px;' + 'left:calc(' + (unitWidth * (k + 1) - half) + '% - ' + half + 'px)}', k);
    }
}

x轴部分已经完成,继续完成y轴部分:

/**
 * 设置Y轴
 */
ewCharts.prototype.resetChartsY = function (dataY, setStyle) {
    let newDataValue = [], chartsY = this.$('ew-charts-y'), chartsYHTML = '';
    let keyNameArr = this.options.data.keyName;
    let keyValue = Array.isArray(keyNameArr) && keyNameArr.length === 2 ? keyNameArr[1] : 'value';
    for (let i = 0, len = dataY.length; i < len; i++) {
        // 将多个value值数组合并成一个数组
        newDataValue = newDataValue.concat(dataY[i][keyValue]);
    }
    // 求value数组的最大值
    let maxValue = Math.max.apply(null, newDataValue);
    if (/\./.test(String(maxValue))) {
        // 如果最大值有小数,则向上取整
        maxValue = Math.ceil(maxValue);
    }
    // 定义分段数与当前Y轴的最大值
    let subSections = null, currentMaxValue = null;
    // 按照每段为1,5,50,500,5000,50000基准值来分段的
    // 当前作为基准值判断的依据数组
    let judgeMaxArr = [1000000, 100000, 10000, 1000, 100, 10];
    let currentJudgeValue = null;
    for (let l = 0, length = judgeMaxArr.length; l < length; l++) {
        // 如果满足条件就跳出循环
        if (maxValue >= judgeMaxArr[l]) {
            currentJudgeValue = judgeMaxArr[l];
            break;
        }
    }
    // 如果currentValue的值为null,则默认分段值设为1
    if (!currentJudgeValue) currentJudgeValue = 1;
    // 计算分段数
    subSections = currentJudgeValue > 1 ? Math.ceil(maxValue / (currentJudgeValue / 2)) : Math.ceil(maxValue / currentJudgeValue);
    // 计算当前Y轴最大值
    currentMaxValue = currentJudgeValue > 1 ? subSections * (currentJudgeValue / 2) : subSections * currentJudgeValue;
    // 根据分段数来生成Y轴元素
    for (let j = 0; j < subSections; j++) {
        chartsYHTML += "<div class='y-" + (j + 1) + "'>" + (currentMaxValue / subSections) * (j + 1) + "</div>";
    }
    chartsY.innerHTML = chartsYHTML;
    // 设置CSS规则
    for (let k = 0; k < subSections; k++) {
        setStyle('ew-charts-y > div.y-' + (k + 1) + '{ bottom:calc(' + parseInt((100 / subSections) * (k + 1)) + '% - 16px);}');
    }
    // 设置区域
    this.resetChartsZone(subSections, keyValue, currentMaxValue, setStyle);
}

y轴部分也已经完成,接下来是完成柱状图部分,也就是区域部分,如下:

/**
 * 设置区域
 */
ewCharts.prototype.resetChartsZone = function (subSections, keyValue, currentMaxValue, setStyle) {
    // 区域整体背景
    setStyle("ew-charts-zone { background:repeating-linear-gradient(180deg,#535456 0%,#724109 " + 100 / subSections + "%,#334455 calc(" + 100 / subSections + "% + 1px),#e0e1e5 " + 100 / subSections * 2 + "%)}", subSections + 1);
    let zoneLen = this.options.data.X.length;
    let chartsZone = this.$('ew-charts-zone'), chartsZoneHTML = '';
    // 因为设置了margin-left与margin-right各1%,所以要减去2
    let series_unit = parseInt(100 / zoneLen) - 2;
    // 设置剩余空间
    let freeSpace = 0;
    // 系列数
    let series_count = this.options.data.Y.length;
    // 每一条数据的宽度
    let series_width = 0;
    // 每一条数据的left值
    let series_left = null;
    // 根据系列数来调整样式
    if (series_count < 3) {
        series_width = 28;
        freeSpace = (100 - (series_count * 30)) / 2;
        series_left = 30;
    } else if (series_count >= 3 && series_count < 6) {
        series_width = 18;
        freeSpace = (100 - (series_count * 20)) / 2;
        series_left = 20;
    } else {
        series_width = 100 / (series_count - 1);
        freeSpace = 100 / series_count;
        series_left = 0;
    }
    let seriesHTML = '';
    for (let j = 0; j < series_count; j++) {
        // 边框颜色高亮
        let borderColor = this.lightColor(this.options.color[j]);
        let left = null;
        if (series_left > 0) {
            left = series_left * j + freeSpace;
        } else {
            left = freeSpace * j;
        }
        // 设置初始样式
        setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + "{width:" + series_width + '%;background-color:' + this.options.color[j] + ';border-color:' + borderColor + ';left:' + left + '%;box-shadow:0 0 5px ' + this.options.color[j] + ';}', j);
        // 设置悬浮样式
        setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + ':hover{box-shadow:0 0 15px ' + this.options.color[j] + ';}');
        seriesHTML += '<bar class="bar-' + (j + 1) + '"></bar>'
    }
    setStyle("ew-charts-zone > div[class*='zone-']{ width:" + series_unit + "%;margin-left:1%;margin-right:1%;}");
    for (let i = 0; i < zoneLen; i++) {
        chartsZoneHTML += "<div class='zone-" + (i + 1) + "'>" + seriesHTML + "</div>";
    }
    chartsZone.innerHTML = chartsZoneHTML;
    let dataY = this.options.data.Y;
    // 延迟设置高度
    setTimeout(() => {
        for (let k = 0; k < zoneLen; k++) {
            for (let l = 0; l < series_count; l++) {
                // 获取bar元素
                const bar = chartsZone.children[k].children[l];
                // 设置class类名,方便设置样式规则
                bar.classList.add('dataId-' + (k + 1) + '-' + (l + 1));
                // 设置值,方便后续的悬浮操作显示值
                bar.setAttribute('data-value', dataY[l][keyValue][k]);
                // 设置高度
                setStyle('ew-charts-zone > div bar.dataId-' + (k + 1) + '-' + (l + 1) + '{height:' + (dataY[l][keyValue][k]) / currentMaxValue * 100 + '%;}', l);
            }
        }
        // 绑定悬浮事件
        let bar = this.$('ew-charts-zone div bar', false);
        [].slice.call(bar).forEach((item) => {
            item.onmouseenter = function () {
                let value = this.getAttribute('data-value');
                this.innerHTML = "<span class='animation'>" + value + '</span>';
            }
            item.onmouseleave = function () {
                this.innerHTML = '';
            }
        })
    }, 0);

}

最后就是完成图注部分了,如下所示:

/**
 * 设置图注
 */
ewCharts.prototype.resetChartsLegend = function (dataLegend, setStyle) {
    let legendHTML = "";
    //图注数据的属性名
    let keyName = Array.isArray(dataLegend.keyName) && dataLegend.keyName.length === 2 ? dataLegend.keyName[0] : 'label';
    for (let i = 0, len = dataLegend.Y.length; i < len; i++) {
        let borderColor = this.lightColor(this.options.color[i]);
        setStyle("ew-charts-legend > i.leg-" + (i + 1) + "{ background:" + this.options.color[i] + ";border-color:" + borderColor + ";}", i);
        legendHTML += "<i class='leg-" + (i + 1) + "'></i><span>" + dataLegend.Y[i][keyName] + "</span>";
    }
    this.$('ew-charts-legend').innerHTML = legendHTML;
}

接下来,调用这个封装好的函数,如下所示:

/**
* 功能:调用统计图表功能
**/
/************************************************/
/* DOM加载完毕后执行(多媒体资源尚未开始加载) */
/************************************************/
document.onreadystatechange = function(){
    if(document.readyState == "interactive"){
        let ewChart = new ewCharts({
            el:"#weekCost",
            color:["#07bc85","dd2345","#346578","#ff8654","#998213"],
            data:{
                X:['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
                Y:[
                    {
                        name: '直接访问',
                        data: [320, 332, 301, 334, 390, 330, 320]
                    },
                    {
                        name: '邮件营销',
                        data: [120, 132, 101, 134, 90, 230, 210]
                    },
                    {
                        name: '联盟广告',
                        data: [220, 182, 191, 234, 290, 330, 310]
                    },
                    {
                        name: '视频广告',
                        data: [150, 232, 201, 154, 190, 330, 410]
                    },
                    {
                        name: '搜索引擎',
                        data: [862, 1018, 964, 1026, 1679, 1600, 1570]
                    },
                ],
                keyName:['name','data']
            }
        });
        console.log(ewChart);
    }
}

嗯,一款柱状图表就大功告成了,由于每一部分的功能我都做了注释,所以不需要做详解,如有问题欢迎联系我,如果发现bug,也欢迎提issue,项目地址为:my-web-projects,如有帮助,望不吝啬star

查看原文

赞 1 收藏 1 评论 0

夕水 回答了问题 · 2020-06-19

解决js数组对象的处理

转换成时间戳的思路很好,以下是我的写法:

/**
* 将时间转换成时间戳,仅小时与分钟的转换,如7:03,21:00之类的时间
* @param {*} value 
*/
let filterTime = function (value) {
    const hour = Number(value.slice(0, value.indexOf(':')).replace(/^0/, '')) * 60 * 60 * 1000;
    const minute = Number(value.slice(value.indexOf(':') + 1, value.length).replace(/^0/, '')) * 60 * 1000;
    return hour + minute;
}
/**
* 判断时间段是否交叉
*/
let judgeDate = function (timeArr,startTime,endTime) {
    for (let i = 0, len = timeArr.length; i < len; i++) {
        let start = timeArr[i], end = timeArr[i];
        //满足可以修改的时间段只有两种情况,即开始时间和结束时间都小于等于已知时间段与开始时间大于已知时间段的结束时间,但结束时间需要小于等于
        //已知时间段的开始时间
        if (filterTime(startTime) <= filterTime(start) && filterTime(endTime) <= filterTime(start)) {
            return true;
        } else if (filterTime(startTime) >= filterTime(end)) {
            return true;
        }
    }
    return false;
}
if(!judgeDate()){
    console.log('时间交叉')
}

关注 4 回答 4

夕水 回答了问题 · 2020-06-19

ajax怎么用url拼接的方式传参

url拼接的方式为何不用get请求?

关注 5 回答 4

夕水 回答了问题 · 2020-06-17

解决Rust 函数如何返回一个不定长的字符串数组呢

文档中已经说明,Rust中的数组是固定长度的,不允许改变长度,因此,你只能使用vector来替代。中文站点你可以参考rust中文文档
这里给你找到了一个优雅的写法:

fn parse_core_args(args: Vec<String>) -> (Vec<String>, Vec<String>) {
   let mut rest = vec![];
   // Filter out args that shouldn't be passed to V8
   let mut args: Vec<String> = args
       .into_iter()
       .filter(|arg| {
           if arg.as_str() == "--help" {
               rest.push(arg.clone());
               return false;
           }
           true
       })
       .collect();
   // Replace args being sent to V8
   for idx in 0..args.len() {
       if args[idx] == "--v8-options" {
           mem::swap(args.get_mut(idx).unwrap(), &mut String::from("--help"));
       }
   }
   (args, rest)
}

源码地址

关注 2 回答 1

夕水 回答了问题 · 2020-06-15

react 百度地图中信息框infoWindow再插入echart视图

猜测你的传入echarts.initdom元素参数有问题,从这方面上去调试,可以找到问题所在。
1.png

关注 3 回答 2

夕水 回答了问题 · 2020-05-12

js如何获取第一出现的个元素后代?

   document.querySelector('.a').firstElementChild

关注 2 回答 1

夕水 发布了文章 · 2020-05-10

从零开始实现一个消息提示框

引言

消息提示框在实际应用场景当中比较常见,最常用的就是element ui的消息提示框,我们通常都是直接使用它们,但是我们有没有尝试过去探究其实现原理,并自己动手实现呢?为了提升我们的个人能力和竞争力,我们可以尝试来实现这样一个消息提示框。

实现效果

我们来查看一下最终实现效果,如下图所示:

准备工作

搭建基本的项目结构

我们创建一个message文件夹,然后创建一个index.html文件,以及message.js和message.css文件,如下所示:

对消息提示框进行布局

在html文件中,我们可以先来实现一个静态的消息提示框,代码如下:

<div class="message">
    <p>这是一个提示框</p>
    <i class="message-close-btn">&times;</i>
</div>

然后再message.css我们写上基本的css代码:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
/* 消息提示框容器样式 */
.message {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    min-width: 300px;
    background-color: #edf2fc;
    border: 1px solid #edf2fc;
    padding: 16px 17px;
    top: 25px;
    border-radius: 6px;
    overflow: hidden;
    z-index: 1000;
}
/* 关闭按钮样式 */
.message > .message-close-btn {
    position: absolute;
    right: 15px;
    top: 50%;
    transform: translateY(-50%);
    color: #c0c0c4;
    font-size: 17px;
    cursor: pointer;
}
.message > .message-close-btn:hover,.message > .message-close-btn:active {
    color: #909399;
}
/* 消息提示框内容样式 */
.message p {
    line-height: 1;
    font-size:14px;
    color: #909399;
}

有四种提示框,以及让内容居中,我们不外乎是多加一个类名来写css样式,比如内容居中,我们只需要在html消息提示框容器元素加上一个类名,代码如下:

<div class="message message-center">
    <p>这是一个提示框</p>
    <i class="message-close-btn">&times;</i>
</div> 

然后再css文件中加一段如下的css代码即可:

/* 内容居中 */
.message.message-center {
    justify-content: center;
}

四种类型的提示框不外乎也是同样的原理,增加一个类名,然后改变的是背景色和字体色,所以html代码如下:

<div class="message message-success">
    <p>这是一个成功提示框</p>
    <i class="message-close-btn">&times;</i>
</div>
<div class="message message-warning">
    <p>这是一个警告提示框</p>
    <i class="message-close-btn">&times;</i>
</div>
<div class="message message-error">
    <p>这是一个错误提示框</p>
    <i class="message-close-btn">&times;</i>
</div>

css代码如下:

/* 成功提示框样式 */
.message.message-success {
    background-color: #e1f3d8;
    border-color:#e1f3d8;
}
.message.message-success p {
    color: #67c23a;
}
/* 警告提示框样式 */
.message.message-warning{
    background-color: #fdfce6;
    border-color: #fdfce6;
}
.message.message-warning p{
    color: #e6a23c;
}
/* 错误提示框样式 */
.message.message-error {
    background-color: #fef0f0;
    border-color: #fef0f0;
}
.message.message-error p {
    color: #f56c6c;
}

这样一来,准备工作就完成了,接下来就是我们的重头戏,JavaScript代码,尝试将如上代码注释掉。

动态实现创建提示框

定义四种类型的消息提示框

我们通过定义一个对象来表示消息提示框的类型,如下所示:

// 消息提示框的四种类型
let typeMap = {
    info: "info",
    warning: "warning",
    success: "success",
    error: "error"
}

添加默认配置项

我们分析一下需要传入的配置项有内容(content),关闭提示框时间(closeTime),是否显示关闭提示框按钮(showClose),内容居中(center)以及消息提示框类型(type)。所以定义配置项如下:

// 提示框的默认配置项
let messageOption = {
    type: "info",
    closeTime: 600,
    center: false,
    showClose: false,
    content: "默认内容"
}

创建一个消息提示框类

我们通过面向对象的编程思维将消息提示框当做是一个类对象,所以我们只需要创建一个类。虽然可以使用es6的class语法来创建,但是为了方便,我们使用构造函数来实现。创建一个构造函数Message,如下所示:

function Message(option) {
    //这里做了一次初始化
    this.init(option);
}

初始化

创建了消息提示框构造函数之后,我们需要传入配置项,并且我们在函数里做了初始化操作,接下来我们来实现初始化的操作。

Message.prototype.init = function (option) {
    //这里创建了提示框元素,并将整个提示框容器元素添加到页面中
    document.body.appendChild(this.create(option));
    //这里设置提示框的top
    this.setTop(document.querySelectorAll('.message'));
    //判断如果传入的closeTime大于0,则默认关闭提示框
    if (option.closeTime > 0) {
        this.close(option.container, option.closeTime);
    }
    //点击关闭按钮关闭提示框
    if (option.close) {
        option.close.onclick = (e) => {
            this.close(e.currentTarget.parentElement, 0);
        }
    }
}

创建提示框

在前面的初始化操作中,我们做了几个功能,首先创建提示框容器元素,并将提示框容器元素添加到页面bod中。我们还做了动态计算提示框的top以及判断传入的默认关闭时间来关闭提示框,点击关闭按钮关闭提示框。我们来看创建提示框的方法,即create方法的编写操作。如下:

Message.prototype.create = function (option) {
    //这里做了一个判断,表示如果设置showClose为false即不显示关闭按钮并且closeTime也为0,即无自动关闭提示框,我们就显示关闭按钮
    if(!option.showClose && option.closeTime <=0)option.showClose = true;
    //创建容器元素
    let element = document.createElement('div');
    //设置类名
    element.className = `message message-${option.type}`;
    if (option.center) element.classList.add('message-center');
    //创建关闭按钮元素以及设置类名和内容
    let closeBtn = document.createElement('i');
    closeBtn.className = 'message-close-btn';
    closeBtn.innerHTML = '&times;';
    //创建内容元素
    let contentElement = document.createElement('p');
    contentElement.innerHTML = option.content;
    //判断如果显示关闭按钮,则将关闭按钮元素添加到提示框容器元素中
    if (closeBtn && option.showClose) element.appendChild(closeBtn);
    //将内容元素添加到提示框容器中
    element.appendChild(contentElement);
    //在配置项对象中存储提示框容器元素以及关闭按钮元素
    option.container = element;
    option.close = closeBtn;
    //返回提示框容器元素
    return element;
}

实现提示框的关闭

我们可以看到,我们创建了一个close方法,并传入提示框容器元素,来实现关闭一个提示框,接下来我们来实现这个关闭方法。如下所示:

Message.prototype.close = function (messageElement, time) {
    //根据传入的时间来延迟关闭,实际上也就是移除元素
    setTimeout(() => {
        //判断如果传入了提示框容器元素,并且分两种情况,如果是多个提示框容器元素则循环遍历删除,如果是单个提示框容器元素,则直接删除
        if (messageElement && messageElement.length) {
            for (let i = 0; i < messageElement.length; i++) {
                if (messageElement[i].parentElement) {
                    messageElement[i].parentElement.removeChild(messageElement[i]);
                }
            }
        } else if (messageElement) {
            if (messageElement.parentElement) {
                messageElement.parentElement.removeChild(messageElement);
            }
        }
        //关闭了提示框容器元素之后,我们重新设置提示框的top值
        this.setTop(document.querySelectorAll('.message'));
    }, time * 10);
}

实现提示框的动态top

最后我们需要实现的是动态计算消息提示框的top,然后不让消息提示框重叠在一起。代码如下:

Message.prototype.setTop = function (messageElement) {
    //这里做一个判断的原因就是当点击页面中最后一个提示框的时候,会重新调用一次,这时获取不到提示框容器元素,所以就不执行后续的设置top
    if (!messageElement || !messageElement.length) return;
    //由于每个提示框的高度一样,所以我们只需获取第一个提示框元素的高度即可
    const height = messageElement[0].offsetHeight;
    for (let i = 0; i < messageElement.length; i++) {
        //每个提示框的top由一个固定值加上它的高度,并且我们要乘以它的一个索引值
        messageElement[i].style.top = (25 * (i + 1) + height * i) + 'px';
    }
}

最后,实现封装,让我们可以如同调用element ui那样来调用

我们想要这样调用$message()或者$message.info(),那么我们可以实现如下:

let $message = {};
window['$message'] = $message = function (option) {
    let newMessageOption = null;
    if (typeof option === 'string') {
        newMessageOption = Object.assign(messageOption, { content: option });
    } else if (typeof option === 'object' && !!option) {
        newMessageOption = Object.assign(messageOption, option);
    }
    return new Message(newMessageOption);
}
for (let key in typeMap) {
    window['$message'][key] = function (option) {
        let newMessageOption = null;
        if (typeof option === 'string') {
            newMessageOption = Object.assign(messageOption, { content: option,type:typeMap[key] });
        } else if (typeof option === 'object' && !!option) {
            newMessageOption = Object.assign(JSON.parse(JSON.stringify(messageOption)),option,{ type:typeMap[key] });
        }
        return new Message(newMessageOption);
    }
}

整个逻辑也十分简单,无非就是判断传入的配置项,然后进行合并,并传入实例化的Message中。

如此一来,我们就完成了一个消息提示框。

录制视频

如果以上分析还不懂的话,可以查看我录制的一个视频:

从零开始实现一个消息提示框

查看原文

赞 10 收藏 8 评论 0