敏小静

敏小静 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织 segmentfault.com/u/minxiaojing 编辑
编辑

前端工程师

个人动态

敏小静 收藏了文章 · 11月24日

设计方案--浅析观察者模式和发布-订阅模式的区别

有时候面试的时候可能会被问到:

观察者模式和发布订阅模式的区别?

没有区别吧?

好像有区别吧?

我们首先来看一下“观察者模式”和“发布订阅模式”

一、观察者设计模式

理解设计模式在设计之初的是为了解决什么问题就能很好的在写代码中运用不同的设计模式。

所谓的观察者模式,其实为了实现松耦合(loosely coupled),也就是为了解耦。

当数据有变化有更新的时候,某个方法被调用,这时候就可以更新其他地方需要用到这个数据的地方。

举个例子:

以气象站为例,每当气象站测试数据有更新的时候,changed()方法都会被调用,于是我们在changed()方法中,更新气象仪器上的数据,比如温度、气压等等。

这样写没有毛病,但是,如果我们以后再changed()方法调用的时候,更新更多的信息,比如说湿度,这个时候我们需要去修改changed()方法的代码,这个就是紧耦合的坏处。

那怎么解决这个紧耦合的问题呢?

观察者模式中,changed()方法所在的实例对象,就是被观察者(subject),只需要维护一套观察者(observer)的集合,这些observer实现相同的接口,subject只需要知道:通知观察者(observer)时,需要调用哪一个统一方法就行。
imgaes

我们再来看一下简单的例子

function Subject(){
    this.observerData = [];
}
Subject.prototype = {
    add: function(observer) {
        this.observerData.push(observer);
    },
    remove:function(observer) {
        var observerData = this.observerData;
        for(let i=0,length=observerData.length;i<length;i++){
            if(observerData[i] === observer){
                observerData.splice(i,1)
            }
        }
    },
    notify: function() {
        var observerData = this.observerData;
        for(let i=0,length=observerData.length;i<length;i++){
            observerData[i].update();
        }
    }
}

function Observer(name){
    this.name = name;
}

Observer.prototype = {
    update: function() {
        console.log('hello,' + this.name);
    }
}

var sub = new Subject();

var obj1 = new Observer('saucxs');
var obj2 = new Observer('songEagle');

sub.add(obj1);
sub.add(obj2);
sub.notify();

这时候输出的是

hello,saucxs

hello,songEagle

上述代码中,我们创建了Subject对象和两个Observer对象,当前状态发生变更的时候则通过Subject对象的notify方法通知两个Observer队形,这两个Observer对象通过update方法进行更新。

二、发布订阅模式(Publisher && Subscriber)

其实觉得发布订阅模式里的Publisher,就是观察者模式的Subject,而Subscriber就是Observer。Publisher变化的时候,就会主动去通知Subscriber。

其实并不是这样的。

在发布订阅模式里,发布者并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互补相识。

那他们之间如何进行通信的?是通过第三者,就是在消息队列中,我们常说的经纪人Broker。

image

发布者只需要告诉Broker,我要发消息。topic是“saucxs”

订阅者只需要告诉Broker,我要订阅topic是“saucxs”

于是,当Broker接收到发布者发来的消息,并且topic是saucxs的时候,就会把消息推送到订阅了topic的saucxs的订阅者,也可能是订阅者自己过来拉取,具体看代码实现。

也就是说,发布订阅模式中,发布者和订阅者,不是松耦合,是完全的解耦的。

放一张很简单的图,对比一下两个模式的区别:

image

三、总结

表面上看:

1、观察者模式中,两个角色:观察者和被观察者

2、发布订阅模式中,三个角色:发布者,订阅者,经纪人

深层次的看:

1、观察者模式:观察者和被观察者,是松耦合的关系

2、发布订阅模式:发布和订阅者是完全不存在耦合的

从使用层面上看:

1、观察者模式:多用于单个应用内部,维护的是单一事件对应多个依赖的

event -> [obj1,obj2,obj3,....]
2、发布订阅模式:多用于跨应用的模式,比如我们说的消息的中间件,维护的是多个事件以及依赖的

event1 -> [obj1,obj2,...]
event2 -> [obj1,obj3,...]

查看原文

敏小静 赞了文章 · 10月27日

ES6 系列之模拟实现 Symbol 类型

前言

实际上,Symbol 的很多特性都无法模拟实现……所以先让我们回顾下有哪些特性,然后挑点能实现的……当然在看的过程中,你也可以思考这个特性是否能实现,如果可以实现,该如何实现。

回顾

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。

1. Symbol 值通过 Symbol 函数生成,使用 typeof,结果为 "symbol"

var s = Symbol();
console.log(typeof s); // "symbol"

2. Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。

3. instanceof 的结果为 false

var s = Symbol('foo');
console.log(s instanceof Symbol); // false

4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

var s1 = Symbol('foo');
console.log(s1); // Symbol(foo)

5. 如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)

6. Symbol 函数的参数只是表示对当前 Symbol 值的描述,相同参数的 Symbol 函数的返回值是不相等的。

// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();

console.log(s1 === s2); // false

// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');

console.log(s1 === s2); // false

7. Symbol 值不能与其他类型的值进行运算,会报错。

var sym = Symbol('My symbol');

console.log("your symbol is " + sym); // TypeError: can't convert symbol to string

8. Symbol 值可以显式转为字符串。

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'

9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。

var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
console.log(a[mySymbol]); // "Hello!"

10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

var obj = {};
var a = Symbol('a');
var b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);
// [Symbol(a), Symbol(b)]

11. 如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');

console.log(s1 === s2); // true

12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

var s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"

var s2 = Symbol("foo");
console.log(Symbol.keyFor(s2) ); // undefined

分析

看完以上的特性,你觉得哪些特性是可以模拟实现的呢?

如果我们要模拟实现一个 Symbol 的话,基本的思路就是构建一个 Symbol 函数,然后直接返回一个独一无二的值。

不过在此之前,我们先看看规范中调用 Symbol 时到底做了哪些工作:

Symbol ( [ description ] )

When Symbol is called with optional argument description, the following steps are taken:

  1. If NewTarget is not undefined, throw a TypeError exception.
  2. If description is undefined, var descString be undefined.
  3. Else, var descString be ToString(description).
  4. ReturnIfAbrupt(descString).
  5. Return a new unique Symbol value whose [[Description]] value is descString.

当调用 Symbol 的时候,会采用以下步骤:

  1. 如果使用 new ,就报错
  2. 如果 description 是 undefined,让 descString 为 undefined
  3. 否则 让 descString 为 ToString(description)
  4. 如果报错,就返回
  5. 返回一个新的唯一的 Symbol 值,它的内部属性 [[Description]] 值为 descString

考虑到还需要定义一个 [[Description]] 属性,如果直接返回一个基本类型的值,是无法做到这一点的,所以我们最终还是返回一个对象。

第一版

参照着规范,其实我们已经可以开始写起来了:

// 第一版
(function() {
    var root = this;

    var SymbolPolyfill = function Symbol(description) {

        // 实现特性第 2 点:Symbol 函数前不能使用 new 命令
        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        // 实现特性第 5 点:如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create(null)

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        // 实现特性第 6 点,因为调用该方法,返回的是一个新对象,两个对象之间,只要引用不同,就不会相同
        return symbol;
    }

    root.SymbolPolyfill = SymbolPolyfill;
})();

只是参照着规范,我们已经实现了特性的第 2、5、6 点。

第二版

我们来看看其他的特性该如何实现:

1. 使用 typeof,结果为 "symbol"。

利用 ES5,我们并不能修改 typeof 操作符的结果,所以这个无法实现。

3. instanceof 的结果为 false

因为不是通过 new 的方式实现的,所以 instanceof 的结果自然是 false。

4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。主要是为了在控制台显示,或者转为字符串时,比较容易区分。

当我们打印一个原生 Symbol 值的时候:

console.log(Symbol('1')); // Symbol(1)

可是我们模拟实现的时候返回的却是一个对象,所以这个也是无法实现的,当然你修改 console.log 这个方法是另讲。

8. Symbol 值可以显式转为字符串。

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'

当调用 String 方法的时候,如果该对象有 toString 方法,就会调用该 toString 方法,所以我们只要给返回的对象添加一个 toString 方法,即可实现这两个效果。

// 第二版

// 前面面代码相同 ……

var symbol = Object.create({
    toString: function() {
        return 'Symbol(' + this.__Description__ + ')';
    },
});

// 后面代码相同 ……

第三版

9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。

看着好像没什么,这点其实和第 8 点是冲突的,这是因为当我们模拟的所谓 Symbol 值其实是一个有着 toString 方法的 对象,当对象作为对象的属性名的时候,就会进行隐式类型转换,还是会调用我们添加的 toString 方法,对于 Symbol('foo') 和 Symbol('foo')两个 Symbol 值,虽然描述一样,但是因为是两个对象,所以并不相等,但是当作为对象的属性名的时候,都会隐式转换为 Symbol(foo) 字符串,这个时候就会造成同名的属性。举个例子:

var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a ===  b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // {Symbol(foo): 'hi'}

为了防止不会出现同名的属性,毕竟这是一个非常重要的特性,迫不得已,我们需要修改 toString 方法,让它返回一个唯一值,所以第 8 点就无法实现了,而且我们还需要再写一个用来生成 唯一值的方法,就命名为 generateName,我们将该唯一值添加到返回对象的 __Name__ 属性中保存下来。

// 第三版
(function() {
    var root = this;

    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()

    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            }
        })

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            },
            '__Name__': {
                value: generateName(descString),
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        return symbol;
    }


    root.SymbolPolyfill = SymbolPolyfill;

})()

此时再看下这个例子:

var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a ===  b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }

第四版

我们再看看接下来的特性。

7.Symbol 值不能与其他类型的值进行运算,会报错。

+ 操作符为例,当进行隐式类型转换的时候,会先调用对象的 valueOf 方法,如果没有返回基本值,就会再调用 toString 方法,所以我们考虑在 valueOf 方法中进行报错,比如:

var symbol = Object.create({
    valueOf: function() {
        throw new Error('Cannot convert a Symbol value')
    }
})

console.log('1' + symbol); // 报错

看着很简单的解决了这个问题,可是如果我们是显式调用 valueOf 方法呢?对于一个原生的 Symbol 值:

var s1 = Symbol('foo')
console.log(s1.valueOf()); // Symbol(foo)

是的,对于原生 Symbol,显式调用 valueOf 方法,会直接返回该 Symbol 值,而我们又无法判断是显式还是隐式的调用,所以这个我们就只能实现一半,要不然实现隐式调用报错,要不然实现显式调用返回该值,那……我们选择不报错的那个吧,即后者。

我们迫不得已的修改 valueOf 函数:

// 第四版
// 前面面代码相同 ……

var symbol = Object.create({
    toString: function() {
        return this.__Name__;
    },
    valueOf: function() {
        return this;
    }
});
// 后面代码相同 ……

第五版

10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

嗯,无法实现。

11. 有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。

这个实现类似于函数记忆,我们建立一个对象,用来储存已经创建的 Symbol 值即可。

12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

遍历 forMap,查找该值对应的键值即可。

// 第五版
// 前面代码相同 ……
var SymbolPolyfill = function() { ... }

var forMap = {};

Object.defineProperties(SymbolPolyfill, {
    'for': {
        value: function(description) {
            var descString = description === undefined ? undefined : String(description)
            return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
        },
        writable: true,
        enumerable: false,
        configurable: true
    },
    'keyFor': {
        value: function(symbol) {
            for (var key in forMap) {
                if (forMap[key] === symbol) return key;
            }
        },
        writable: true,
        enumerable: false,
        configurable: true
    }
});
// 后面代码相同 ……

完整实现

综上所述:

无法实现的特性有:1、4、7、8、10

可以实现的特性有:2、3、5、6、9、11、12

最后的实现如下:

(function() {
    var root = this;

    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()

    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            },
            valueOf: function() {
                return this;
            }
        })

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            },
            '__Name__': {
                value: generateName(descString),
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        return symbol;
    }

    var forMap = {};

    Object.defineProperties(SymbolPolyfill, {
        'for': {
            value: function(description) {
                var descString = description === undefined ? undefined : String(description)
                return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
            },
            writable: true,
            enumerable: false,
            configurable: true
        },
        'keyFor': {
            value: function(symbol) {
                for (var key in forMap) {
                    if (forMap[key] === symbol) return key;
                }
            },
            writable: true,
            enumerable: false,
            configurable: true
        }
    });

    root.SymbolPolyfill = SymbolPolyfill;

})()

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

查看原文

赞 33 收藏 21 评论 1

敏小静 发布了文章 · 10月19日

html之离线缓存

实例 - 完整的 Manifest 文件

  

  

1,什么是应用程序缓存(Application Cache)

  HTML5 引入了应用程序缓存,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问。

  离线缓存:

    离线缓存可以将站点的一些文件缓存到本地,它是浏览器自己的一种机制,

    将需要的文件缓存下来,以便后期即使没有连接网络,被缓存的页面也可以展示。

    即使有网络,优先本地存储的资源

2,应用程序缓存为应用带来三个优势:

  • 离线浏览 - 用户可在应用离线时使用它们

    * 速度 - 已缓存资源加载得更快

    * 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。

  * 【

    在没有网络的时候可以访问到缓存的对应的站点页面,包括html,js,css,img等等文件
    在有网络的时候,浏览器也会优先使用已离线存储的文件,返回一个200(from cache)头。这跟HTTP的缓存使用策略是不同的
    资源的缓存可以带来更好的用户体验,当用户使用自己的流量上网时,本地缓存不仅可以提高用户访问速度,而且大大节约用户的使用流量。
     】

3,如何实现离线缓存:

a)Cache Manifest 基础

如需启用应用程序缓存,请在文档的<html> 标签中包含 manifest 属性:

    * 在需要缓存的html根节点上面添加属性 manifest ,属性值是一个 . appcache 文件;

    Appcache 是一个控制缓存文件

    * 在同目录下创建demo.appcache 文件,幷添加配置项

  b)Manifest 文件

manifest 文件是简单的文本文件,它告知浏览器被缓存的内容(以及不缓存的内容)。

      manifest 文件可分为三个部分:

        CACHE MANIFEST - 在此标题下列出的文件将在首次下载后进行缓存

        NETWORK - 在此标题下列出的文件需要与服务器的连接,且不会被缓存

        FALLBACK - 在此标题下列出的文件规定当页面无法访问时的回退页面(比如 404 页面)

      * CACHE MANIFEST

      第一行,CACHE MANIFEST,是必需的:

      CACHE MANIFEST

      /theme.css

      /logo.gif

      /main.js

      上面manifest 文件列出了三个资源:一个 CSS 文件,一个 GIF 图像,一个 JavaScript 文件

      * NETWORK

      NETWORK:

      login.php

      可以使用星号来指示所有其他资源/文件都需要因特网连接:

      NETWORK:

      *

      * FALLBACK

      FALLBACK:

      /html/   /offline.html

      第一个 URI 是资源,第二个是替补。

4,更新缓存:

一旦应用被缓存,它就会保持缓存直到发生下列情况:

    用户清空浏览器缓存

    manifest 文件被修改

    由程序来更新应用缓存

 5,在服务器端将.appcache文件的mime类型配置成 text/cache-manifest

  下面以phpstudy为例

   打开,mime.types ,在后面添加----

   

6,在网页中打开

    

关于应用程序缓存的注意事项

**-------------

提示:以 "#" 开头的是注释行,(# 2012-02-21 v1.0.0)但也可满足其他用途。应用的缓存只会在其 manifest 文件改变时被更新。如果您编辑了一幅图像,或者修改了一个 JavaScript 函数,这些改变都不会被重新缓存。更新注释行中的日期和版本号是一种使浏览器重新缓存文件的办法。**

浏览器对缓存数据的容量限制可能不太一样(某些浏览器的限制是每个站点 5MB)。

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 10月18日

vue响应式系统(变化检测)

变化侦测分为两种类型,一种是“”推,另一种是“拉”。
Angular和react的变化侦测都属于“拉”,也就是说,当状态发生变化时,它不知道哪个状态发生变化了,只知道状态有可能改变了,然后就会发送一个信号告诉框架,框架收到信号后,就会进行一个暴力对比找到哪些DOM需要重新渲染的。这也是Angular的脏检查(基于zoom.js,利用$digest函数触发)的过程,react用的是虚拟DOM。
Vue的变化侦测属于“推”。当状态发生变化,Vue在一定程度上能马上知道哪些状态发生改变,具有更细粒度的更新;也因为粒度越细,每个状态绑定的依赖就越多,依赖追踪在内存中的开销就越大,因此,Vue也引入了虚拟DOM的概念,将一个状态的细粒度绑定到组件(Vue的另一核心:单文件组件化),这样子当状态发生改变,就会通知到组件,组件内部再使用虚拟DOM进行对比更新。
众所周知,Vue2.x的版本使用Object.defineProperty,Vue3.x使用ES6的Proxy来进行变化侦测的。下面主要讲Object和Array的变化侦测:
(注:以下代码块来自Vue2.6.10源码)

一、Object的变化侦测
1.Object通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化,在getter中收集依赖,在setter中触发依赖。

第一版Object.defineProperty:

function defineReactive$$1 (obj, key, val) {
     Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        return val
      },
      set: function reactiveSetter (newVal) {
         if(val === newVal){
            return
         }
         val = newVal;
      }
    });
  }
    

2.在getter中收集依赖,依赖被存储在Dep中,在Dep中对依赖进行添加,删除以及更新等操作。

Dep的封装:
var uid = 0;
var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
  };

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  };

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

  Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

  // The current target watcher being evaluated.
  // This is globally unique because only one watcher
  // can be evaluated at a time.
  Dep.target = null;
  var targetStack = [];

  function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  }

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }
  function remove (arr, item) {
    if (arr.length) {
      var index = arr.indexOf(item);
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
  }

第二版Object.defineProperty:

function defineReactive$$1 (obj,key,val) {
    var dep = new Dep();//新增
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        if (Dep.target) {//新增
          dep.depend();//新增
        }
        return val
      },
      set: function reactiveSetter (newVal) {
        if (newVal === val) {
          return
        }
        val = newVal;
        dep.notify();//新增
      }
    });
  }

3.所谓的依赖就是wather,只有watcher触发的getter就会去收集依赖到Dep中去,当数据发生变化时,会循环依赖列表,把所有的watcher通知一遍。
watcher原理:先把自己设置到全局唯一的指定位置(pushTarget(this)),然后读取数据( value = this.getter.call(vm, vm);)触发该数据的getter。接着,在getter中就会从全局唯一的那个位置读取正在读取数据的watcher,并把这个watcher收集到Dep中。

  /**
   * A watcher parses an expression, collects dependencies,
   * and fires callback when the expression value changes.
   * This is used for both the $watch() api and directives.
   */
  var Watcher = function Watcher (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);//parsePath读取一个字符串的keypath
    }
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);//this就是当前watcher实例。看上面的pushTarget,把watcher实例赋值给Dep。target
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);//读取值的时候,触发getter,就可以将this主动添加到Dep(依赖收集)
    } catch (e) {
        throw e
    } finally {
      
    }
    return value
  };
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

4.创建Observer方法递归object中的所有数据(包括子数据)都转换成响应式形式。只有Object类型才会调用walk将每个属性转换成getter/setter形式侦测,在defineReactive$$1中新增observe(new Observer(value);)来递归子属性。

var Observer = function Observer (value) {
    this.value = value;
    if (Array.isArray(value)) {
    } else {
      this.walk(value);
    }
  };
    Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  };

5.无法跟踪新增属性和删除属性,可以用Vue提供的vm.$set和vm.$delete解决
6.总结图解:
image

二、Array的变化侦测
1.在Observer中使用拦截器覆盖那些即将转换成响应式的Array类型数据的原型。(value.__proto__=Object.create(Array.prototype);)

function copyAugment (target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i];
    def(target, key, src[key]);
  }
}
 function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    });
  }
  //鉴于有些浏览器不支持__proto__属性,在这个判断对象中是否有__proto__
  var hasProto = '__proto__' in {};
  var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
  var Observer = function Observer (value) {
  this.dep = new Dep();//创建一个Dep实例,存放依赖列表
  def(value, '__ob__', this);//在value上增加一个不可枚举的属性__ob__,这个属性的值是Observer实例。这样就可以通过__ob__访问Observer实例,
  if (Array.isArray(value)) {
      if (hasProto) {
      //__proto__是指向原型的,通过它来覆盖侦测数据的原型;Object.create返回一个带有Array原型对象和属性的新对象。
        value.__proto__=Object.create(Array.prototype);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
    }
  };
2.Array也是在getter中收集依赖,在拦截器中触发依赖,并将依赖保存在Observer实例的Dep中。
function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else {
      ob = new Observer(value);
    }
    
    return ob
  }
function defineReactive$$1 (obj, key, val) {
     var childOb = observe(val);//为val创建Observer实例
     Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
      if (childOb) {
           childOb.dep.depend();//收集依赖
         }
        return val
      },
      set: function reactiveSetter (newVal) {
         if(val === newVal){
            return
         }
         val = newVal;
      }
    });
  }

3.向依赖发送通知(通过this.__ob__访问Observer实例,调用Observer中Dep的notify发送通知依赖)

var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];

  /**
   * Intercept mutating methods and emit events
   */
  methodsToPatch.forEach(function (method) {
    def(arrayMethods, method, function mutator () {
      var ob = this.__ob__;//重点
      var inserted;//获取数组中的新增元素
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      //侦听新增元素的变化
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      ob.dep.notify();//重点
      return result
    });
  });

4.监听数组中元素的变化

var Observer = function Observer (value) {
    if (Array.isArray(value)) {
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };
  //侦听每一项
  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

5.因为Array的变化侦测是通过拦截原型的方式实现的,对于this.lsit[0]=2或this.list.length=0;是无法拦截侦测的。

总结:

  • Array和Object都是在getter触发收集依赖的
  • Object通过Observer(创建实例)递归所有数据的侦测,当从外界读取数据,在watcher读取数据,触发getter,将依赖(watcher)收集到Dep中,当数据发生变化时,会触发setter,从而向Dep中的watcher依赖发送通知,watcher接收到通知之后,通知外界视图或者回调函数
  • Array通过Observer(创建实例)递归所有数据并创建拦截器覆盖原型的方式进行侦测,在Obserer方法中给value(侦测得数组)新增一个不可枚举的__ob__属性,并且该属性的值就是Observer实例;在getter中将依赖收集在Observer实例中的Dep去,当数据发生变化时,通过this.__ob__.dep访问Observer实例的dep去向依赖发送通知。
查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 8月5日

js之防抖和节流

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 8月1日

前端常见跨域解决方案(大方向)

一、iframe标签、JSONP(动态创建script标签)
二、代理:nginx(服务器反向代理)、前端proxy代理(node+webpack+proxy、node+express+proxy)
三、CORS策略(跨域资源共享)

普通跨域请求:只需服务端设置Access-control-Allow-Origin;若要带上Cookie,前后端都需要设置。前端设置xhr.withCredentials = true;
CORS思想是使用自定义的HTTP头部让浏览器和服务器进行沟通,从而请求的成功和失败

四、Web sockets全双工,双向通信(同源策略对web sockets不适用);同时只有支持web sockets协议的服务器才能正常工作。

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 7月28日

下载文件功能

用a标签的download下载,如果是第三方资源的话,就需要请求回来,否则的话,就会被当做链接打开。还要看看后端有没有CORS策略阻止。

//判断是不是ie浏览器
   IEVersion() {
      let userAgent = navigator.userAgent; //取得浏览器的userAgent字符串  
      let isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1; //判断是否IE<11浏览器  
      let isEdge = userAgent.indexOf("Edge") > -1 && !isIE; //判断是否IE的Edge浏览器  
      let isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1;
      if(isIE || isEdge || isIE11) {
        return true
      }
    },
 download(){
      const fileName = 'ITSell.jpeg' // 导出文件名
      // 对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性
      // IE10以上支持blob但是依然不支持download
     
        const blob = new Blob([content]) // 构造一个blob对象来处理数据。content是请求返回的blob数据;请求的时候,要加上responseType = 'blob'让服务器返回blob类型
      if ('download' in document.createElement('a') && !IEVersion()) { // 支持a标签download的浏览器
        const link = document.createElement('a') // 创建a标签
        link.download = fileName // a标签添加属性
        link.style.display = 'none'
        link.href =URL.createObjectURL(blob);
        document.body.appendChild(link)
        link.click() // 执行下载
        URL.revokeObjectURL(link.href) // 释放url
        document.body.removeChild(link) // 释放标签
      } else { // 其他浏览器
        navigator.msSaveBlob(blob, fileName);
      }
    },
查看原文

赞 0 收藏 0 评论 0

敏小静 收藏了文章 · 7月17日

ElementUI select 把整个option(对象)作为值

大部分时候我们使用select,选中选项我们只需要他的ID值,如果同时要在其他地方展示label或者获取选中对象中的其他值怎么办。看图。
关键设置有两处。

1 option中 :value="item"

2 selection中 value-key="id"

clipboard.png

查看原文

敏小静 发布了文章 · 5月28日

object对象和Array数组的变化检测(响应式原理)

变化检测顾名思义就是检测数据发生变化时,响应数据的更新。
它分为两种类型:一种是推,一种是拉;Angular和React的变化检测都属于’拉‘。就是说,当状态发生变化时,它不知道哪个状态发生变化,就发送一个信号给框架,框架使用暴力检测DOM来更新状态。这也是Angular脏检查的原理,React使用虚拟DOM的原理。
而Vue.js是使用’推‘的形式,就是一定程度上知道哪个状态发生了变化,从而进行更新。但是它的依赖相对来说也会比较多。

有两种方式可以检测到变化:Object.definePropertyES6的Proxy

Object.defineProperty(obj, prop, desc)
**Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。**

obj 需要定义属性的当前对象
prop 当前需要定义的属性名
desc 属性描述符

如:let Person = {}
Object.defineProperty(Person, 'name', {
   value: 'jack',
   writable: true // 是否可以改变
   configurable:true
}) 

由于ES6在浏览器的支持度不是很理想,所以Vue.js(2.x.x)是用Object.defineProperty来实现的。

Vue在初始化实例时对属性执行了getter/setter的转化,所以属性必须在data()上才能被Vue转化成响应式。(深入响应式原理)

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 5月21日

使用require.context实现自动导入模块

1.什么是require.context

一个webpack的api,通过执行require.context函数获取一个特定的上下文,主要用来实现自动化导入模块,在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入,使得不需要每次显式的调用import导入模块
2.什么时候需要用到require.context
如果有以下情况,可以考虑使用require.context替换
image.png
image.png
在我的项目中,有n多个品牌,其中每个品牌对应自己条款细则,如果一个个的import导入路由的话,显然有点力不从心,看起来又很杂。这个时候我们只需用require.context遍历template文件夹把这些vue的路由整合到一个文件再导入。

3.分析require.context

require.context函数接受三个参数

  1. directory {String} -读取文件的路径
  2. useSubdirectories {Boolean} -是否遍历文件的子目录
  3. regExp {RegExp} -匹配文件的正则
语法: require.context(directory, useSubdirectories = false, regExp = /^.//);

借用webpakc官网的例子

require.context('./test', false, /.test.js$/);

上面的代码遍历当前目录下的test文件夹的所有.test.js结尾的文件,不遍历子目录
值得注意的是require.context函数执行后返回的是一个函数,并且这个函数有3个属性

  1. resolve {Function} -接受一个参数request,request为test文件夹下面匹配文件的相对路径,返回这个匹配文件相对于整个工程的相对路径
  2. keys {Function} -返回匹配成功模块的名字组成的数组
  3. id {String} -执行环境的id,返回的是一个字符串,主要用在module.hot.accept,应该是热加载?

这三个都是作为函数的属性(注意是作为函数的属性,函数也是对象,有对应的属性)

4.把template目录下的vue文件的路由整合到template.router.js

image.png
其中files(key).default返回的就是我们vue-component的结构;把confinRouter暴露出去,然后再到我们的主体路由引入这个template.router.js

image.png

原文参考:https://www.jianshu.com/p/c894ea00dfec

查看原文

赞 0 收藏 0 评论 0

敏小静 收藏了文章 · 5月19日

辛辛苦苦学会的 webpack dll 配置,可能已经过时了

前段时间写了一篇详解 webpack4 中易混淆知识点的文章,没想到收获了近 600 个赞,在这里对各位老铁抱拳感谢。上篇文章我费了很多时间去构思 demo 和原创作图,就是想把一些概念彻底讲清楚,看评论区的反响我感觉还是做到了自己设定的目标。

如果大家看过一些 webpack4 优化的文章,一定会出现 dll 动态链接库。它以配置之复杂让众多初学者记忆犹新。今天我会以一个学习者的角度去一步一步探讨 webpack dll 的配置,最后得出一个完美的解决方案。

本文的内容和大部分讲解 webpack4 优化文章的观点不一样,如果有不同的见解,欢迎在评论区和我讨论。

<br/>

友情提示:本文章不是入门教程,不会费大量笔墨去描写 webpack 的基础配置,请读者配合教程[源代码](https://github.com/skychx/webpack_learn/tree/master/optimization)食用。

<br/>

1. 基础概念:dll 其实就是缓存

说实话我刚看见这个 dll 动态链接库的时候,我真被镇住了:这是什么玩意?怎么根本没听说过?

好学的我赶紧 Google 一下,在维基百科里找到了标准定义:

所谓动态链接,就是把一些经常会共享的代码制作成 DLL 档,当可执行文件调用到 DLL 档内的函数时,Windows 操作系统才会把 DLL 档加载存储器内,DLL 档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。

唉,你们官方就是不说人话。

我结合 webpack,从前端的角度翻译一下:

具体到 webpack 这块儿,就是事先把常用但又构建时间长的代码提前打包好(例如 react、react-dom),取个名字叫 dll。后面再打包的时候就跳过原来的未打包代码,直接用 dll。这样一来,构建时间就会缩短,提高 webpack 打包速度。

我盯着上面那句话看了三分钟,什么 DLL,什么动态链接库,在前端世界里,不就是个缓存吗!都是拿空间换时间。

注:在这里狭义上可以理解为拿空间换时间,如果真的要探讨 dll 背后的知识:动态链接库静态链接库,就又涉及到编译器的知识了,具体讲下去又是一篇新的文章了,所以暂时按下不表。

我们对比一下 DLL 和前端常接触的网络缓存,一张表就看明白了:

DLL缓存
1.把公共代码打包为 DLL 文件存到硬盘里1.把常用文件存到硬盘/内存里
2.第二次打包时动态链接 DLL 文件,不重新打包2.第二次加载时直接读取缓存,不重新请求
3.打包时间缩短3.加载时间缩短

所以在前端世界里, DLL 就是个另类缓存。

2. DLL 手动配置:这么多步根本记不住

刚开始我们先不搞配置,我们设想一下,如果让你手动创建并管理缓存,你会怎么做?

我想,大家的思路一般都是这样的:

  1. 第一次请求的时候,把请求后的内容存储起来
  2. 建立一个映射表,当后续有请求时,先根据这个映射表到看看要请求的内容有没有被缓存,有的话就加载缓存,没有就走正常请求流程(也就是所谓的缓存命中问题)
  3. 命中缓存后,直接从缓存中拿取内容,交给程序处理

主要流程无非这 3 步,想把事情搞大,可以再加些权重啊,过期时间啊,多级缓存什么的,但主要流程就是上面的 3 步。

一般我们在开发的时候,浏览器,http 协议都帮我们把这些操作封装好了,我们就记几个参数调参就行了;但是 webpack dll 不一样,它需要我们手动实现上面 3 个步骤,所以就非常的无聊 + 繁琐。

下面的代码比较乱,因为我也没打算好好讲这些绕来绕去的配置,具体结构最好看我 github 上放出的示例源代码看不懂也没事,后面有更好的解决方案

看得烦就直接跳过下面的内容

第 1 步,我们先要创建 dll 文件,这个相当于我们对第一次的请求内容进行存储,然后我们还要创建一个映射表,告诉程序我们把啥文件做成 dll 了(这个相当于第 2 步):

首先我们写一个创建 dll 文件的打包脚本,目的是把 reactreact-dom打包成 dll 文件:

// 文件目录:configs/webpack.dll.js
// 代码太长可以不看

'use strict';

const path = require('path');
const webpack = require('webpack');

module.exports = {
    mode: 'production',
    entry: {
        react: ['react', 'react-dom'],
    },
    // 这个是输出 dll 文件
    output: {
        path: path.resolve(__dirname, '../dll'),
        filename: '_dll_[name].js',
        library: '_dll_[name]',
    },
    // 这个是输出映射表
    plugins: [
        new webpack.DllPlugin({ 
            name: '_dll_[name]', // name === output.library
            path: path.resolve(__dirname, '../dll/[name].manifest.json'),
        })
    ]
};

打包脚本写好了,我们总得运行吧?所以我们写个运行脚本放在 package.jsonscripts 标签里,这样我们运行 npm run build:dll 就可以打包 dll 文件了:

// package.json

{
  "scripts": {
    "build:dll": "webpack --config configs/webpack.dll.js",
  },
}

第 3 步,链接 dll 文件,也就是告诉 webpack 可以命中的 dll 文件,配置也是一大坨:

// 文件目录:configs/webpack.common.js
// 代码太长可以不看

const path = require('path');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); // 顾名思义,把资源加到 html 里,那这个插件把 dll 加入到 index.html 里
const webpack = require('webpack');
module.exports = {
  // ......
  plugins: [
    new webpack.DllReferencePlugin({
      // 注意: DllReferencePlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
      context: path.resolve(__dirname, '../'),
      manifest: path.resolve(__dirname, '../dll/react.manifest.json'),
    }),
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, '../dll/_dll_react.js'),
    }),
  ]
}

为了减少一些大型库的二次打包时间,我们在 3 个文件里写了一堆配置代码,小心翼翼,如履薄冰,中间说不定还会因为作用域的问题链接失败(对,说的就是我)。配置 dll 会给人带来巨大的心理阴影,有没有其他方法降低我们的心智负担呢?

3. AutoDllPlugin:解放你的配置负担

在第 2 小节里我疯狂劝退,就是想介绍这个插件:autodll-webpack-plugin,这个插件把上面那 3 坨代码整合到一块儿,让我们摆脱繁琐的配置,让我们看看这么用吧:

// 文件目录:configs/webpack.common.js

const path = require('path');
const AutoDllPlugin = require('autodll-webpack-plugin'); // 第 1 步:引入 DLL 自动链接库插件

module.exports = {
  // ......
  plugins: [
        // 第 2 步:配置要打包为 dll 的文件
        new AutoDllPlugin({
            inject: true, // 设为 true 就把 DLL bundles 插到 index.html 里
            filename: '[name].dll.js',
            context: path.resolve(__dirname, '../'), // AutoDllPlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
            entry: {
                react: [
                    'react',
                    'react-dom'
                ]
            }
        })
  ]
}

autodll-webpack-plugin  的使用方法和 webpack 的其他 plugin 使用方式非常相似,和手动引入 dll 的方法比起来,简单许多,而且这个插件之前是被 vue-cli 使用的,质量也是比较稳定的,大家可以放心使用。

4. 抛弃 DLL:Vue & React 官方的共同选择

第 3 节我说 autodll-webpack-plugin 之前被 vue-cli 使用,那意思是现在不用了?是不是有 bug 啊?这个还真不是。

学习 webpack 的时候,为了借鉴一下业内优秀的框架的 webpack 配置,我专门看了 vue-cli 和 create-react-app 的源码,但是却没有找到任何 dll 的配置痕迹。

这就很奇怪了,我之前翻过一些 nuxt.js 1.0 的源码,里面是有 dll 的配置代码的,按道理来说 vue-cli 也应该有的,我就猜测是在某次升级中,把 dll 去掉了。所以我开始查找 commit 记录,果然被我找到了:

白纸黑字,remove DLL option 3 个大字写的清清楚楚

原因是什么呢?在这个 issue 里尤雨溪解释了去除的原因:

dll option will be removed. Webpack 4 should provide good enough perf and the cost of maintaining DLL mode inside Vue CLI is no longer justified.

dll 配置将会被移除,因为 Webpack 4 的打包性能足够好的,dll 没有在 Vue ClI 里继续维护的必要了。

同样的,在这个 PR 里 create-react-app 里也给出了类似的解释:webpack 4 有着比 dll 更好的打包性能

所以说,如果项目上了 webpack 4,再使用 dll 收益并不大。我拿实际项目的代码试了一下,加入 dll 可能会有 1-2 s 的速度提升,对于整体打包时间可以说可以忽略不计。

Vue 和 React 官方 2018 都不再使用 dll 了,现在 2019 年都快过去了,所以说我上面说的都没用了,都不用学了,是不是感觉松了一口气(疯狂暗示点赞)?

5. 比 DLL 更优秀的插件

dll 加速不明显了,有没有更好的替代品?在 AutoDllPlugin 的 README.md 里,给我们推荐了 HardSourceWebpackPlugin,初始配置更简单,只需要一行代码:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  // ......
  plugins: [
    new HardSourceWebpackPlugin() // <- 直接加入这行代码就行
  ]
}

这个插件加速有多明显呢?我拿本文的试例代码测试了一下,下图是常规的打包时间,大概 900 ms:

加入 dll 优化后,打包时间为 507 ms,缩短了 400 ms 左右:

只使用 HardSourceWebpackPlugin,再次打包时间缩短到 253 ms:

看相关的文档,貌似这个技术直接放到了 webpack 5 里,开箱即用。所以,虽然 dll 的配置你不用学了,但是 webpack 5 is coming......

6. 写在最后

这篇文章很难说它是一篇教程,更多的是记录了我学习 webpack 中的一个探索过程。说实话我把 dll 手动配完觉得我挺 nb 的,这么复杂的配置我都能配好。

当我后续找到 autodll-webpack-plugin,并发现 dll 已经被抛弃时,其实还是有些失望,觉得自己的之前的努力都白费了,不由自主产生 学不动 的想法。但是当我仔细想了一下 dll 的原理,发现也就是那么一会事儿,拿空间换时间,只不过配置复杂了一些。

所以这也提醒我们,学习新知识的时候,不要专注于流程的配置和调参。因为流程终会简化,参数(API)终会升级。要抓大放小,把精力放在最核心的内容上,因为核心的思想是最不容易过时的。

7.参考阅读

面试必备!webpack 中那些最易混淆的 5 个知识点

webpack 官方文档

autodll-webpack-plugin

HardSourceWebpackPlugin


最后打个广告,业余写一些数据可视化科普向的文章,目前一周一篇,内容非技术向。目前在写不需要写代码的爬虫教程,觉得不错的话可以推荐给非技术同事。公众号 ID 是 sky-chx,大家感兴趣的可以关注一波。

查看原文

敏小静 赞了文章 · 5月13日

记一次简单的vue组件单元测试

clipboard.png

记录一些在为项目引入单元测试时的一些困惑,希望可以对社区的小伙伴们有所启迪,少走一些弯路少踩一些坑。

  • jest, mocha, karma, chai, sinon, jsmine, vue-test-utils都是些什么东西?
  • chai,sinon是什么?
  • 为什么以spec.js命名?
  • 如何为聊天的文字消息组件写单元测试?

    • 运行在哪个目录下?
    • karma.conf.js怎么看?
    • 人生中第一次单元测试
  • istanbul是什么?
  • vue-test-utils的常用api?
  • 前端的单元测试,到底该测什么?

jest, mocha, karma, chai, sinon, jsmine, vue-test-utils都是些什么东西?

名词Github描述个人理解
jestDelightful JavaScript Testing. Works out of the box for any React project.Capture snapshots of React treesfacebook家的测试框架,与react打配合会更加得心应手一些。
mochaSimple, flexible, fun JavaScript test framework for Node.js & The Browser强大的测试框架,中文名叫抹茶,常见的describe,beforeEach就来自这里
karmaA simple tool that allows you to execute JavaScript code in multiple real browsers. Karma is not a testing framework, nor an assertion library. Karma just launches an HTTP server, and generates the test runner HTML file you probably already know from your favourite testing framework.不是测试框架,也不是断言库,可以做到抹平浏览器障碍式的生成测试结果
chaiBDD / TDD assertion framework for node.js and the browser that can be paired with any testing framework.BDD/TDD断言库,assert,expect,should比较有趣
sinonStandalone and test framework agnostic JavaScript test spies, stubs and mocksjs mock测试框架,everything is fake,spy比较有趣
jsmineJasmine is a Behavior Driven Development testing framework for JavaScript. It does not rely on browsers, DOM, or any JavaScript framework. Thus it's suited for websites, Node.js projects, or anywhere that JavaScript can run.js BDD测试框架
vue/test-utilsUtilities for testing Vue components专门为测试单文件组件而开发,学会使用vue-test-utils,将会在对vue的理解上更上一层楼

通过上述的表格,作为一个vue项目,引入单元测试,大致思路已经有了:

测试框架:mocha
抹平环境:karma
断言库:chai
BDD库:jsmine

这并不是最终结果,测试vue单文件组件,当然少不了vue-test-utils,但是将它放在什么位置呢。
需要阅读vue-test-utils源码。

chai,sinon是什么?

chai是什么?

  • Chai是一个node和浏览器可用的BDD/TDD断言库。
  • Chai类似于Node内建API的assert。
  • 三种常用风格:assert,expect或者should。
const chai = require('chai');
const assert = chai.assert;
const expect = chai.expect();
const should = chai.should();

sinon是什么?

  • 一个 once函数,该如何测试这个函数?
  • spy是什么?
function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}
Fakes
it('calls the original function', function () {
    var callback = sinon.fake();
    var proxy = once(callback);

    proxy();

    assert(callback.called);
});

只调用一次更重要:

it('calls the original function only once', function () {
    var callback = sinon.fake();
    var proxy = once(callback);

    proxy();
    proxy();

    assert(callback.calledOnce);
    // ...or:
    // assert.equals(callback.callCount, 1);
});

而且我们同样觉得this和参数重要:

it('calls original function with right this and args', function () {
    var callback = sinon.fake();
    var proxy = once(callback);
    var obj = {};

    proxy.call(obj, 1, 2, 3);

    assert(callback.calledOn(obj));
    assert(callback.calledWith(1, 2, 3));
});
行为

once返回的函数需要返回源函数的返回。为了测试这个,我们创建一个假行为:

it("returns the return value from the original function", function () {
    var callback = sinon.fake.returns(42);
    var proxy = once(callback);

    assert.equals(proxy(), 42);
});

同样还有 Testing Ajax,Fake XMLHttpRequest,Fake server,Faking time等等。

sinon.spy()?

test spy是一个函数,它记录所有的参数,返回值,this值和函数调用抛出的异常。
有3类spy:

  • 匿名函数
  • 具名函数
  • 对象的方法

匿名函数

测试函数如何处理一个callback。

"test should call subscribers on publish": function() {
    var callback = sinon.spy();
    PubSub.subscribe("message", callback);
    PubSub.publishSync("message");
    assertTrue(callback.called);
}

对象的方法

用spy包裹一个存在的方法。
sinon.spy(object,"method")创建了一个包裹了已经存在的方法object.method的spy。这个spy会和源方法一样表现(包括用作构造函数时也是如此),但是你可以拥有数据调用的所有权限,用object.method.restore()可以释放出spy。这里有一个人为的例子:

{
    setUp: function () {
        sinon.spy(jQuery, "ajax");
    },
    tearDown: function () {
        jQuery.ajax.restore();// 释放出spy
    },
}

引申问题

BDD/TDD是什么?

What’s the difference between Unit Testing, TDD and BDD?
[[译]单元测试,TDD和BDD之间的区别是什么?](https://github.com/FrankKai/F...

为什么以spec.js命名?

SO上有这样一个问题:What does “spec” mean in Javascript Testing

spec是sepcification的缩写。

就测试而言,Specification指的是给定特性或者必须满足的应用的技术细节。最好的理解这个问题的方式是:让某一部分代码成功通过必须满足的规范。

如何为聊天的文字消息组件写单元测试?

运行在哪个文件夹下?

test文件夹下即可,文件名以.spec.js结尾即可,通过files和preprocessors中的配置可以匹配到。

karma.conf.js怎么看?

看不懂karma.conf.js,到 http://karma-runner.github.io... 学习配置。

const webpackConfig = require('../../build/webpack.test.conf');
module.exports = function karmaConfig(config) {
  config.set({
    browsers: ['PhantomJS'],// Chrome,ChromeCanary,PhantomJS,Firefox,Opera,IE,Safari,Chrome和PhantomJS已经在karma中内置,其余需要插件
    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],// ['jasmine','mocha','qunit']等等,需要额外通过NPM安装
    reporters: ['spec', 'coverage'],//  默认值为progress,也可以是dots;growl,junit,teamcity或者coverage需要插件。spec需要安装karma-spec-reporter插件。
    files: ['./index.js'],// 浏览器加载的文件,  `'test/unit/*.spec.js',`等价于 `{pattern: 'test/unit/*.spec.js', watched: true, served: true, included: true}`。
    preprocessors: {
      './index.js': ['webpack', 'sourcemap'],// 预处理加载的文件
    },
    webpack: webpackConfig,// webpack配置,karma会自动监控test的entry points
    webpackMiddleware: {
      noInfo: true, // webpack-dev-middleware配置
    },
    // 配置reporter 
    coverageReporter: {
      dir: './coverage',
      reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }],
    },
  });
};

结合实际情况,通过https://vue-test-utils.vuejs.... 添加切合vue项目的karma配置。

demo地址:https://github.com/eddyerburg...

人生中第一次单元测试

karma.conf.js

// This is a karma config file. For more details see
//   http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
//   https://github.com/webpack/karma-webpack

const webpackConfig = require('../../build/webpack.test.conf');

module.exports = function karmaConfig(config) {
  config.set({
    // to run in additional browsers:
    // 1. install corresponding karma launcher
    //    http://karma-runner.github.io/0.13/config/browsers.html
    // 2. add it to the `browsers` array below.
    browsers: ['Chrome'],
    frameworks: ['mocha'],
    reporters: ['spec', 'coverage'],
    files: ['./specs/*.spec.js'],
    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap'],
    },
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true,
    },
    coverageReporter: {
      dir: './coverage',
      reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }],
    },
  });
};

test/unit/specs/chat.spec.js

import { mount } from '@vue/test-utils';
import { expect } from 'chai';
import ChatText from '@/pages/chat/chatroom/view/components/text';
describe('ChatText.vue', () => {
  it('人生中第一次单元测试:', () => {
    const wrapper = mount(ChatText);
    console.log(wrapper.html());
    const subfix = '</p> <p>默认文字</p></div>';
    expect(wrapper.html()).contain(subfix);
  });
});

注意,被测试组件必须以index.js暴露出组件。
NODE_ENV=testing karma start test/unit/karma.conf.js --single-run
测试结果:
image

意外收获

1.PhantomJS是什么?

  • 是一个无头的脚本化浏览器。
  • 可以运行在Windows, macOS, Linux, and FreeBSD.
  • QtWebKit,可以做DOM处理,可以CSS选择器,可以JSON,可以Canvas,也可以SVG。

下载好phantomjs后,就可以在终端中模拟浏览器操作了。
foo.js

var page = require('webpage').create();
page.open('http://www.google.com', function() {
    setTimeout(function() {
        page.render('google.png');
        phantom.exit();
    }, 200);
});
phantomjs foo.js

运行上述代码后,会生成一张图片,但是画质感人。
image
2.karma-webpack是什么?
在karma中用webpack预处理文件。

istanbul是什么?

image

vue-test-utils的常用api及其option?

  • mount:propsData,attachToDocument,slots,mocks,stubs?
  • mount和shallowMount的区别是什么?

啥玩意儿???一一搞定。

mount:propsData,attachToDocument,slots,mocks,stubs?

this.vm.$options.propsData // 组件的自定义属性,是因为2.1.x版本中没有$props对象,https://vue-test-utils.vuejs.org/zh/api/wrapper/#setprops-props
const elm = options.attachToDocument ? createElement() : undefined // "<div>" or undefined
slots // 传递一个slots对象到组件,用来测试slot是否生效的,值可以是组件,组件数组或者字符串,key是slot的name
mocks // 模拟全局注入
stubs // 存根子组件

后知后觉,这些都可以在Mounting Options文档查看:https://vue-test-utils.vuejs....

mount和shallowMount的区别是什么?

mount仅仅挂载当前组件实例;而shallowMount挂载当前组件实例以外,还会挂载子组件。

前端的单元测试,到底该测什么?

这是一个一直困扰我的问题。
测试通用业务组件?业务变更快速,单元测试波动较大。❌
测试用户行为?用户行为存在上下文关系,组合起来是一个很恐怖的数字,这个交给测试人员去测就好了。❌
那我到底该测什么呢?要测试功能型组件,vue插件,二次封装的库。✔️

就拿我负责的项目来说:

功能型组件:可复用的上传组件,可编辑单元格组件,时间选择组件。(前两个组件都是老大写的,第三个是我实践中抽离出来的。)
vue插件:mqtt.js,eventbus.js。(这两个组件是我抽象的。)
二次封装库:httpclient.js。(基于axios,老大初始化,我添砖加瓦。)

上述适用于单元测试的内容都有一个共同点:复用性高!

所以我们在纠结要不要写单元测试时,抓住复用性高这个特点去考虑就好了。

单元测试是为了保证什么呢?

  • 按照预期输入,组件或者库有预期输出,告诉开发者all is well。
  • 未按照预期输入,组件或者库给出预期提醒,告诉开发者something is wrong。

所以,其实单元测试是为了帮助开发者的突破自己内心的最后一道心理障碍,建立老子的代码完全ojbk,不可能出问题的自信。

其实最终还是保证用户有无bug的组件可用,有好的软件或平台使用,让自己的生活变得更加美好。

如何为vue插件 eventbus 写单元测试?

/*
  title: vue插件eventbus单测
  author:frankkai
  target: 1.Vue.use(eventBus)是否可以正确注入$bus到prototype
          2.注入的$bus是否可以成功挂载到组件实例
          3.$bus是否可以正常订阅消息($on)和广播消息($emit)
 */
import eventbusPlugin from '@/plugins/bus';
import { createLocalVue, createWrapper } from '@vue/test-utils';
import { expect } from 'chai';

const localVue = createLocalVue();
localVue.use(eventbusPlugin);

const localVueInstance = (() =>
  localVue.component('empty-vue-component', {
    render(createElement) {
      return createElement('div');
    },
  }))();
const Constructor = localVue.extend(localVueInstance);
const vm = new Constructor().$mount();
const wrapper = createWrapper(vm);

describe('/plugins/bus.js', () => {
  it('Vue.use(eventBus)是否可以正确注入$bus到prototype:', () => {
    expect('$bus' in localVue.prototype).to.equal(true);
  });
  it('注入的$bus是否可以成功挂载到组件实例:', () => {
    expect('$bus' in wrapper.vm).to.equal(true);
  });
  it('$bus是否可以正常订阅消息($on)和广播消息($emit):', () => {
    wrapper.vm.$bus.$on('foo', (payload) => {
      expect(payload).to.equal('$bus emitted an foo event');
    });
    wrapper.vm.$bus.$on('bar', (payload) => {
      expect(payload).to.equal('$bus emitted an bar event');
    });
    expect(Object.keys(vm.$bus._events).length).to.equal(2);
    wrapper.vm.$bus.$emit('foo', '$bus emitted an foo event');
    wrapper.vm.$bus.$emit('bar', '$bus emitted an bar event');
  });
});

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!

查看原文

赞 16 收藏 11 评论 6

敏小静 回答了问题 · 2019-10-16

解决axios 设置headers.Authorization都成功了,但是请求头还是没有,这是为什么?

楼主怎么解决了,求分享

关注 12 回答 10

敏小静 回答了问题 · 2019-08-03

gulp如何引入公共文件和创建问文件夹

关注 3 回答 1

敏小静 收藏了文章 · 2019-07-15

如何正确的(?)利用 Vue.mixin() 偷懒

前言

最近开发的页面以及功能大都以表格为主,接口获取来的 JSON 数据大都是需要经过处理,比如时间戳需要转换,或者状态码的转义。对于这样的问题,各大主流框架都提供了类似于过滤的方法,在 Vue 中,一般是在页面上定义 filter 然后在模板文件中使用 | 进行处理。

这种方法和以前的遍历数组洗数据是方便了许多,但是,当我发现在许多的页面都有相同的 filter 的时候,每个页面都要复制一遍就显的很蛋疼,遂决定用 Vue.mixin() 实现一次代码,无限复用

最后,还可以将所有的 filter 包装成一个 vue 的插件,使用的时候调用 Vue.use() 即可,甚至可以上传 npm 包,开发不同的项目的时候可以直接 install 使用。(考虑到最近更新的比较快,遂打包上传这步骤先缓缓,等版本稍微稳定了之后来补全)

正文

闲话说够,开始正题。

Vue.mixin 为何物

学习一个新的框架或者 API 的时候,最好的途径就是上官网,这里附上 Vue.mixin() 官方说明。

一句话解释,Vue.mixin() 可以把你创建的自定义方法混入所有的Vue 实例。

示例代码

Vue.mixin({
  created: function(){
    console.log("success")
  }
})

跑起你的项目,你会发现在控制台输出了一坨 success

效果出来了意思也就出来了,所有的 Vue 实例的 created 方法都被改成了我们自定义的方法。

使用 Vue.mixin()

接下来的思路很简单,我们整合所有的 filter 函数到一个文件,在 main.js 中引入即可。

在上代码之前打断一下,代码很简单,但是我们可以写的更加规范化,关于如何做到规范,在 Vue 的官网有比较详细的 风格指南 可以参考。

因为我们的自定义方法会在所有的实例中混入,如果按照以前的方法,难免会有覆盖原先的方法的危险,按照官方的建议,混入的自定义方法名增加前缀 $_ 用作区分。

创建一个 config.js 文件,用于保存状态码对应的含义,将其暴露出去

export const typeConfig = {
  1: "type one",
  2: "type two",
  3: "type three"
}

再创建一个 filters.js 文件,用于保存所有的自定义函数

import { typeConfig } from "./config"
export default {
  filters: {
    $_filterType: (value) => {
      return typeConfig[value] || "type undefined"
    }
  }
}

最后,在 main.js 中引入我们的 filters 方法集

import filter from "./filters"
Vue.mixin(filter)

接下来,我们就可以在 .vue 的模板文件中随意使用自定义函数了

<template>
  <div>{{typeStatus | $_filterType}}<div>
</template>

包装插件

接下来简单应用一下 Vue 中插件的制作方法。创建插件之后,就可以 Vue.use(myPlugin) 来使用了。

首先附上插件的 官方文档

一句话解释,包装的插件需要一个 install 的方法将插件装载到 Vue 上。

关于 Vue.use() 的源码

function initUse (Vue) {
  Vue.use = function (plugin) {
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    var 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
  };
}

很直观的就看到他在最后调用了 plugin.install 的方法,我们要做的就是处理好这个 install 函数即可。

上代码

config.js 文件依旧需要,这里保存了所有状态码对应的转义文字

创建一个 myPlugin.js 文件,这个就是我们编写的插件

import { typeConfig } from "./config"

myPlugin.install = (Vue) => {
  Vue.mixin({
    filters: {
      $_filterType: (value) => {
        return typeConfig[value] || "type undefined"
      }
    }
  })
}
export default myPlugin

插件的 install 函数的第一个参数为 Vue 的实例,后面还可以传入一些自定义参数。

main.js 文件中,我们不用 Vue.mixin() 转而使用 Vue.use() 来完成插件的装载。

import myPlugin from "./myPlugin"
Vue.use(myPlugin)

至此,我们已经完成了一个小小的插件,并将我们的状态码转义过滤器放入了所有的 Vue 实例中,在 .vue 的模板文件中,我们可以使用 {{ typeStatus | $_filterType }} 来进行状态码转义了。

结语

Vue.mixin() 可以将自定义的方法混入所有的Vue 实例中。

本着快速开发的目的,一排脑门想到了这个方法,但是这是否是一个好方法有待考证,虽然不是说担心会对原先的代码造成影响,但是所有的Vue 实例也包括了第三方模板

本文可以随意转载,只要附上原文地址即可。

如果您认为我的博文对您有所帮助,请不吝赞赏,点赞也是让我动力满满的手段 =3=。

待完善

发布 npm 包

新增项

在 v-html 中骚骚的使用 filter (2018年05月28日)

最近碰到一个问题,也是关于状态码过滤的,但是实现的效果是希望通过不同的状态码显示不同的 icon 图标,这个就不同于上面的文本过滤了,上文使用的 {{ styleStatus | $_filterStyleStatus }} 是利用 v-text 进行渲染,若碰到需要渲染 html 标签就头疼了。

按照前人的做法,是这样的,我随意上一段代码。

...
<span v-if="item.iconType === 1" class="icon icon-up"></span>
<span v-if="item.iconType === 2" class="icon icon-down"></span>
<span v-if="item.iconType === 3" class="icon icon-left"></span>
<span v-if="item.iconType === 4" class="icon icon-right"></span>
...

不!这太不 fashion 太不 cool,我本能的拒绝这样的写法,但是,问题还是要解决,我转而寻找他法。

在看 Vue 的文档的时候,其中一个 API$options官方文档 就引起了我的注意,我正好需要一个可以访问到当前的 Vue 实例的 API,一拍脑袋,方案就成了。

首先,还是在 config.js 文件中定义一个状态码对应对象,这里我们将其对应的内容设为 html 段落。

export const iconStatus = {
  1: "<span class='icon icon-up'></span>",
  2: "<span class='icon icon-down'></span>",
  3: "<span class='icon icon-left'></span>",
  4: "<span class='icon icon-right'></span>"
}

接着,我们在 filters.js 文件中引入他,写法还是和以前的 filters 一样。

import { iconStatus } from "./config"
export default {
  $_filterIcon: (value) => {
      return iconStatus[value] || "icon undefined"
  }
}

重头戏在这里,我们在模板文件中需要渲染的地方,使用 v-html 来进行渲染。

<span v-html="$options.filters.$_filterIcon(item.iconType)"></span>

大功告成,以后需要根据状态码来渲染 icon 的地方都可以通过这个办法来完成了。

事实证明,懒并不一定是错误的,关键看懒的方向,虽然本篇博客写的标题是 偷懒 ,但其实我只是对于重复性的无意义的搬运代码而感到厌烦,在寻找对应解决办法的时候可是一点都没偷懒,相反的,在寻求更快更好更方便的方法的时候,我逐渐找回了当初敲代码的乐趣。

查看原文

敏小静 赞了文章 · 2019-07-02

Vue面试中,经常会被问到的面试题/Vue知识点整理

看看面试题,只是为了查漏补缺,看看自己那些方面还不懂。切记不要以为背了面试题,就万事大吉了,最好是理解背后的原理,这样面试的时候才能侃侃而谈。不然,稍微有水平的面试官一看就能看出,是否有真才实学还是刚好背中了这道面试题。
(都是一些基础的vue面试题,大神不用浪费时间往下看)

一、对于MVVM的理解?

MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
bg2015020110.png

二、Vue的生命周期

beforeCreate(创建前) 在数据观测和初始化事件还未开始
created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
1.什么是vue生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

2.vue生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

3.vue生命周期总共有几个阶段?
答:它可以总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

4.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

5.DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。

三、 Vue实现数据双向绑定的原理:Object.defineProperty()

vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

js实现简单的双向绑定

<body>
    <div id="app">
    <input type="text" id="txt">
    <p id="show"></p>
</div>
</body>
<script type="text/javascript">
    var obj = {}
    Object.defineProperty(obj, 'txt', {
        get: function () {
            return obj
        },
        set: function (newValue) {
            document.getElementById('txt').value = newValue
            document.getElementById('show').innerHTML = newValue
        }
    })
    document.addEventListener('keyup', function (e) {
        obj.txt = e.target.value
    })
</script>

四、Vue组件间的参数传递

1.父组件与子组件传值
父组件传给子组件:子组件通过props方法接受数据;
子组件传给父组件:$emit方法传递参数
2.非父子组件间的数据传递,兄弟组件传值
eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适。(虽然也有不少人推荐直接用VUEX,具体来说看需求咯。技术只是手段,目的达到才是王道。)

五、Vue的路由实现:hash模式 和 history模式

hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

六、Vue与Angular以及React的区别?

(版本在不断更新,以下的区别有可能不是很正确。我工作中只用到vue,对angular和react不怎么熟)
1.与AngularJS的区别
相同点:
都支持指令:内置指令和自定义指令;都支持过滤器:内置过滤器和自定义过滤器;都支持双向数据绑定;都不支持低端浏览器。

不同点:
AngularJS的学习成本高,比如增加了Dependency Injection特性,而Vue.js本身提供的API都比较简单、直观;在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。

2.与React的区别
相同点:
React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用;中心思想相同:一切都是组件,组件实例之间可以嵌套;都提供合理的钩子函数,可以让开发者定制化地去处理需求;都不内置列数AJAX,Route等功能到核心包,而是以插件的方式加载;在组件开发中都支持mixins的特性。
不同点:
React采用的Virtual DOM会对渲染出来的结果做脏检查;Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作Virtual DOM。

七、vue路由的钩子函数

首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

beforeEach主要有3个参数to,from,next:

to:route即将进入的目标路由对象,

from:route当前导航正要离开的路由

next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

八、vuex是什么?怎么使用?哪种功能场景使用它?

只用来读取的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。
在main.js引入store,注入。新建了一个目录store,….. export 。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车
图片描述

state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
getters
类似vue的计算属性,主要用来过滤一些数据。
action
actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。

const store = new Vuex.Store({ //store实例
      state: {
         count: 0
             },
      mutations: {                
         increment (state) {
          state.count++
         }
          },
      actions: { 
         increment (context) {
          context.commit('increment')
   }
 }
})

modules
项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
 }
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
 }

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
})

九、vue-cli如何新增自定义指令?

1.创建局部指令

var app = new Vue({
    el: '#app',
    data: {    
    },
    // 创建指令(可以多个)
    directives: {
        // 指令名称
        dir1: {
            inserted(el) {
                // 指令中第一个参数是当前使用指令的DOM
                console.log(el);
                console.log(arguments);
                // 对DOM进行操作
                el.style.width = '200px';
                el.style.height = '200px';
                el.style.background = '#000';
            }
        }
    }
})

2.全局指令

Vue.directive('dir2', {
    inserted(el) {
        console.log(el);
    }
})

3.指令的使用

<div id="app">
    <div v-dir1></div>
    <div v-dir2></div>
</div>

十、vue如何自定义一个过滤器?

html代码:

<div id="app">
     <input type="text" v-model="msg" />
     {{msg| capitalize }}
</div>

JS代码:

var vm=new Vue({
    el:"#app",
    data:{
        msg:''
    },
    filters: {
      capitalize: function (value) {
        if (!value) return ''
        value = value.toString()
        return value.charAt(0).toUpperCase() + value.slice(1)
      }
    }
})

全局定义过滤器

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

过滤器接收表达式的值 (msg) 作为第一个参数。capitalize 过滤器将会收到 msg的值作为第一个参数。

十一、对keep-alive 的了解?

keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include) 。

使用方法

<keep-alive include='include_components' exclude='exclude_components'>
  <component>
    <!-- 该组件是否缓存取决于include和exclude属性 -->
  </component>
</keep-alive>

参数解释
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存
include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。

使用示例

<!-- 逗号分隔字符串,只有组件a与b被缓存。 -->
<keep-alive include="a,b">
  <component></component>
</keep-alive>

<!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) -->
<keep-alive :include="/a|b/">
  <component></component>
</keep-alive>

<!-- Array (需要使用 v-bind,被包含的都会被缓存) -->
<keep-alive :include="['a', 'b']">
  <component></component>
</keep-alive>

十二、一句话就能回答的面试题

1.css只在当前组件起作用
答:在style标签中写入scoped即可 例如:<style scoped></style>

2.v-if 和 v-show 区别
答:v-if按照条件是否渲染,v-show是display的block或none;

3.$route$router的区别
答:$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。而$router是“路由实例”对象包括了路由的跳转方法,钩子函数等。

4.vue.js的两个核心是什么?
答:数据驱动、组件系统

5.vue几种常用的指令
答:v-for 、 v-if 、v-bind、v-on、v-show、v-else

6.vue常用的修饰符?
答:.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;.capture: 事件侦听,事件发生的时候会调用

7.v-on 可以绑定多个方法吗?
答:可以

8.vue中 key 值的作用?
答:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。

9.什么是vue的计算属性?
答:在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。好处:①使得数据处理结构清晰;②依赖于数据,数据更新,处理结果自动更新;③计算属性内部this指向vm实例;④在template调用时,直接写计算属性名即可;⑤常用的是getter方法,获取数据,也可以使用set方法改变数据;⑥相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候computed从缓存中获取,不会重新计算。

10.vue等单页面应用及其优缺点
答:优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

11.怎么定义 vue-router 的动态路由? 怎么获取传过来的值
答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。

Vue面试中,经常会被问到的面试题/Vue知识点整理
532道前端真实大厂面试题
学习ES6笔记──工作中常用到的ES6语法

查看原文

赞 1123 收藏 869 评论 80

敏小静 收藏了文章 · 2019-06-13

VueJS中学习使用Vuex详解

在SPA单页面组件的开发中 Vue的vuex和React的Redux 都统称为同一状态管理,个人的理解是全局状态管理更合适;简单的理解就是你在state中定义了一个数据之后,你可以在所在项目中的任何一个组件里进行获取、进行修改,并且你的修改可以得到全局的响应变更。下面咱们一步一步地剖析下vuex的使用:
首先要安装、使用 vuex
首先在 vue 2.0+ 你的vue-cli项目中安装 vuex :

npm install vuex --save

然后 在src文件目录下新建一个名为store的文件夹,为方便引入并在store文件夹里新建一个index.js,里面的内容如下:

 
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store();
 
export default store;

接下来,在 main.js里面引入store,然后再全局注入一下,这样一来就可以在任何一个组件里面使用this.$store了:

import store from './store'//引入store
 
new Vue({
  el: '#app',
  router,
  store,//使用store
  template: '<App/>',
  components: { App }
})

说了上面的前奏之后,接下来就是纳入正题了,就是开篇说的state的玩法。回到store文件的index.js里面,我们先声明一个state变量,并赋值一个空对象给它,里面随便定义两个初始属性值;然后再在实例化的Vuex.Store里面传入一个空对象,并把刚声明的变量state仍里面:

 
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
 const state={//要设置的全局访问的state对象
     showFooter: true,
     changableNum:0
     //要设置的初始属性值
   };
 const store = new Vuex.Store({
       state
    });
 
export default store;

实际上做完上面的三个步骤后,你已经可以用this.$store.state.showFooter或this.$store.state.changebleNum在任何一个组件里面获取showfooter和changebleNum定义的值了,但这不是理想的获取方式;vuex官方API提供了一个getters,和vue计算属性computed一样,来实时监听state值的变化(最新状态),并把它也仍进Vuex.Store里面,具体看下面代码:

 
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
 const state={   //要设置的全局访问的state对象
     showFooter: true,
     changableNum:0
     //要设置的初始属性值
   };
const getters = {   //实时监听state值的变化(最新状态)
    isShow(state) {  //方法名随意,主要是来承载变化的showFooter的值
       return state.showFooter
    },
    getChangedNum(){  //方法名随意,主要是用来承载变化的changableNum的值
       return state.changebleNum
    }
};
const store = new Vuex.Store({
       state,
       getters
});
export default store;

光有定义的state的初始值,不改变它不是我们想要的需求,接下来要说的就是mutations了,mutattions也是一个对象,这个对象里面可以放改变state的初始值的方法,具体的用法就是给里面的方法传入参数state或额外的参数,然后利用vue的双向数据驱动进行值的改变,同样的定义好之后也把这个mutations扔进Vuex.Store里面,如下:

 
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
 const state={   //要设置的全局访问的state对象
     showFooter: true,
     changableNum:0
     //要设置的初始属性值
   };
const getters = {   //实时监听state值的变化(最新状态)
    isShow(state) {  //承载变化的showFooter的值
       return state.showFooter
    },
    getChangedNum(){  //承载变化的changebleNum的值
       return state.changableNum
    }
};
const mutations = {
    show(state) {   //自定义改变state初始值的方法,这里面的参数除了state之外还可以再传额外的参数(变量或对象);
        state.showFooter = true;
    },
    hide(state) {  //同上
        state.showFooter = false;
    },
    newNum(state,sum){ //同上,这里面的参数除了state之外还传了需要增加的值sum
       state.changableNum+=sum;
    }
};
 const store = new Vuex.Store({
       state,
       getters,
       mutations
});
export default store;

这时候你完全可以用 this.$store.commit('show') 或 this.$store.commit('hide') 以及 this.$store.commit('newNum',6) 在别的组件里面进行改变showfooter和changebleNum的值了,但这不是理想的改变值的方式;因为在 Vuex 中,mutations里面的方法 都是同步事务,意思就是说:比如这里的一个this.$store.commit('newNum',sum)方法,两个组件里用执行得到的值,每次都是一样的,这样肯定不是理想的需求

好在vuex官方API还提供了一个actions,这个actions也是个对象变量,最大的作用就是里面的Action方法 可以包含任意异步操作,这里面的方法是用来异步触发mutations里面的方法,actions里面自定义的函数接收一个context参数和要变化的形参,context与store实例具有相同的方法和属性,所以它可以执行context.commit(' '),然后也不要忘了把它也扔进Vuex.Store里面:

 
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
 const state={   //要设置的全局访问的state对象
     showFooter: true,
     changableNum:0
     //要设置的初始属性值
   };
const getters = {   //实时监听state值的变化(最新状态)
    isShow(state) {  //承载变化的showFooter的值
       return state.showFooter
    },
    getChangedNum(){  //承载变化的changebleNum的值
       return state.changableNum
    }
};
const mutations = {
    show(state) {   //自定义改变state初始值的方法,这里面的参数除了state之外还可以再传额外的参数(变量或对象);
        state.showFooter = true;
    },
    hide(state) {  //同上
        state.showFooter = false;
    },
    newNum(state,sum){ //同上,这里面的参数除了state之外还传了需要增加的值sum
       state.changableNum+=sum;
    }
};
 const actions = {
    hideFooter(context) {  //自定义触发mutations里函数的方法,context与store 实例具有相同方法和属性
        context.commit('hide');
    },
    showFooter(context) {  //同上注释
        context.commit('show');
    },
    getNewNum(context,num){   //同上注释,num为要变化的形参
        context.commit('newNum',num)
     }
};
  const store = new Vuex.Store({
       state,
       getters,
       mutations,
       actions
});
export default store;

而在外部组件里进行全局执行actions里面方法的时候,你只需要用执行

this.$store.dispatch('hideFooter')

或this.$store.dispatch('showFooter')

以及this.$store.dispatch('getNewNum',6) //6要变化的实参

这样就可以全局改变改变showfooter或changebleNum的值了,如下面的组件中,需求是跳转组件页面后,根据当前所在的路由页面进行隐藏或显示页面底部的tabs选项卡

<template>
  <div id="app">
    <router-view/>
    <FooterBar v-if="isShow" />
  </div>
</template>

<script>
import FooterBar from '@/components/common/FooterBar'
import config from './config/index'
export default {
  name: 'App',
  components:{
    FooterBar:FooterBar
  },
  data(){
    return {
    }
  },
  computed:{
     isShow(){
       return this.$store.getters.isShow;
     }
  },
  watch:{
      $route(to,from){ //跳转组件页面后,监听路由参数中对应的当前页面以及上一个页面
          console.log(to)
        if(to.name=='book'||to.name=='my'){ // to.name来获取当前所显示的页面,从而控制该显示或隐藏footerBar组件
           this.$store.dispatch('showFooter') // 利用派发全局state.showFooter的值来控制        }else{
           this.$store.dispatch('hideFooter')
        }
      }
  }
}
</script>
        }else{
           this.$store.dispatch('hideFooter')
        }
      }
  }
}
</script>

至此就可以做到一呼百应的全局响应状态改变了!

modules 模块化 以及 组件中引入 mapGetters、mapActions 和 mapStates的使用

因为在大多数的项目中,我们对于全局状态的管理并不仅仅一种情况的需求,有时有多方面的需求,比如写一个商城项目,你所用到的全局state可能是关于购物车这一块儿的也有可能是关于商品价格这一块儿的;像这样的情况我们就要考虑使用vuex中的 modules 模块化了,具体怎么使用modules呢?咱们继续一步一步的走:

首先,在store文件夹下面新建一个modules文件夹,然后在modules文件里面建立需要管理状态的js文件,既然要把不同部分的状态分开管理,那就要把它们给分成独立的状态文件了,如下图:
图片描述

而对应的store文件夹下面的index.js 里面的内容就直接改写成:

import Vue from 'vue';
import Vuex from 'vuex';
import footerStatus from './modules/footerStatus'
import collection from './modules/collection'
Vue.use(Vuex);

export default new Vuex.Store({
    modules:{
         footerStatus,
         collection
    }
});

相应的js,其中的 namespaced:true 表示当你需要在别的文件里面使用( mapGetters、mapActions 接下来会说 )时,里面的方法需要注明来自哪一个模块的方法:

//collection.js

const state={
    collects:[],  //初始化一个colects数组
};
const getters={
  renderCollects(state){ //承载变化的collects
    return state.collects;
  }
};
const mutations={
     pushCollects(state,items){ //如何变化collects,插入items
        state.collects.push(items)
     }
 };
const actions={
    invokePushItems(context,item){ //触发mutations里面的pushCollects ,传入数据形参item 对应到items
        context.commit('pushCollects',item);
    }
};
export default {
     namespaced:true,//用于在全局引用此文件里的方法时标识这一个的文件名
     state,
     getters,
     mutations,
     actions
}
 
//footerStatus.js
 
const state={   //要设置的全局访问的state对象
     showFooter: true,
     changableNum:0
     //要设置的初始属性值
   };
const getters = {   //实时监听state值的变化(最新状态)
    isShow(state) {  //承载变化的showFooter的值
       return state.showFooter
    },
    getChangedNum(){  //承载变化的changebleNum的值
       return state.changableNum
    }
};
const mutations = {
    show(state) {   //自定义改变state初始值的方法,这里面的参数除了state之外还可以再传额外的参数(变量或对象);
        state.showFooter = true;
    },
    hide(state) {  //同上
        state.showFooter = false;
    },
    newNum(state,sum){ //同上,这里面的参数除了state之外还传了需要增加的值sum
       state.changableNum+=sum;
    }
};
 const actions = {
    hideFooter(context) {  //自定义触发mutations里函数的方法,context与store 实例具有相同方法和属性
        context.commit('hide');
    },
    showFooter(context) {  //同上注释
        context.commit('show');
    },
    getNewNum(context,num){   //同上注释,num为要变化的形参
        context.commit('newNum',num)
     }
};
export default {
    namespaced: true, //用于在全局引用此文里的方法时标识这一个的文件名
    state,
    getters,
    mutations,
    actions
}

这样一改就有了关于两个模块的state管理文件了 footerStatus.js和collection.js,现在你要运行当前的代码话,项目会报错!因为我们把上面的代码模块化分开了,引用的地方还没有改。接下来咱们一起来看看 mapState,mapGetters,mapActions的使用,首先 在需要用的 组件里面先导入 import {mapState,mapGetters,mapActions} from 'vuex';咱们先修正一下隐藏或显示页面底部的tabs选项卡(就是上面举的临时例子)的组件代码

 
<template>
  <div id="app">
    <router-view/>
    <FooterBar v-if="isShow" />
  </div>
</template>
 
<script>
import {mapState,mapGetters,mapActions} from 'vuex'; //先要引入
import FooterBar from '@/components/common/FooterBar'
import config from './config/index'
export default {
  name: 'App',
  components:{
    FooterBar:FooterBar
  },
  data(){
    return {
    }
  },
  computed:{
    ...mapState({  //这里的...是超引用,ES6的语法,意思是state里有多少属性值我可以在这里放多少属性值
         isShow:state=>state.footerStatus.showFooter //注意这些与上面的区别就是state.footerStatus,
                                                      //里面定义的showFooter是指footerStatus.js里state的showFooter
      }),
     //你也可以用下面的mapGetters来获取isShow的值,貌似下面的更简洁
    /*...mapGetters('footerStatus',{ //footerStatus指的是modules文件夹下的footerStatus.js模块
         isShow:'isShow' //第一个isShow是我自定义的只要对应template里v-if="isShow"就行,
                         //第二个isShow是对应的footerStatus.js里的getters里的isShow
      })*/
  },
  watch:{
      $route(to,from){
        if(to.name=='book'||to.name=='my'){
           this.$store.dispatch('footerStatus/showFooter') //这里改为'footerStatus/showFooter',
                                                           //意思是指footerStatus.js里actions里的showFooter方法
        }else{
           this.$store.dispatch('footerStatus/hideFooter') //同上注释
        }
      }
  }
}
</script>

现在项目代码应该就不会报错了,好,最后咱们再来看一下mapActions的用法,实际上上面的this.$store.dispatch('footerStatus/showFooter')已经算是一种执行相应模块的action里的方法了,但有时会牵扯的事件的触发及传值,那就会有下面的mapActions用法了,还记得上面的另一个模块collection.js吗?来看一下里面的actions中的方法结构:

const state={
    collects:[],  //初始化一个colects数组
};
const getters={
  renderCollects(state){ //承载变化的collects
    return state.collects;
  }
};
const mutations={
     pushCollects(state,items){ //如何变化collects,插入items
        state.collects.push(items)
     }
 };
const actions={
    invokePushItems(context,item){ //触发mutations里面的pushCollects ,传入数据形参item 对应到items
        context.commit('pushCollects',item);
    }
};

需要传值来实时变动state.collects里的数据,那肯定要在执行它的地方进行传值了,所以下面用到它的地方我们用了个@click来执行这个invokePushItems方法了,并且传入相应的对象数据item,如下:

<template>
  <div >
      <section class="joinState">
           <div class="joinStateHead">
                <span class="h3">全国改性料通讯录</span>
                <span class="joinStatus" @click="invokePushItems(item)">加入收藏列</span>
           </div>
      </section>
  </div>
</template>

<script>
import { mapActions } from 'vuex'
export default {
  components:{
     conditionFilter
  },
  name: 'bookDetail',
  data () {
    return {
      msg: '',
      item:{
         id:'01',
         productName: '苹果',
         price:'1.6元/斤'
       }
    }
  },
  mounted() {
    this.$store.dispatch('footerStatus/hideFooter')
  },
  methods:{
      ...mapActions('collection',[ //collection是指modules文件夹下的collection.js
          'invokePushItems'  //collection.js文件中的actions里的方法,在上面的@click中执行并传入实参
      ])
  }

}
</script>

这样一来,在这个组件里面操作的 collecttion.js 中的state的数据,在其他的任何的一个组件里面都会得到相应的更新变化了,获取状态的页面代码如下:

<template>
  </div>
    <div>
        <ul>
            <li v-for="(val,index) in arrList" :key="index">
                <h5>{{val.productName}}</h5>
                 <p>{{val.price}}</p>
            </li>
        </ul>
    </div>
</template>

<script>
import {mapState,mapGetters,mapActions} from 'vuex';
    export default {
        name: 'book',
        data() {
            return {
            }
        },
    computed:{
        // ...mapState({  //用mapState来获取collection.js里面的state的属性值
        //    arrList:state=>state.collection.collects
        // }),
        ...mapGetters('collection',{ //用mapGetters来获取collection.js里面的getters
            arrList:'renderCollects'
        })

    }
    }
</script>

至此,vuex中的常用的一些知识点使用算是简单的分享完了,当然了,相信这些只是一些皮毛!只能说是给予刚接触vuex的初学者一个参考与了解吧!有哪里不明白的或不对的,留言下,咱们可以一起讨论、共同学习!

查看原文

敏小静 发布了文章 · 2019-06-05

vue-cli配置全局sass、less变量

一、全局配置less

1.下载插件

**vue add style-resources-loader**

vue add pluginName 是vue-cli3提供的。vue add 是用yarn安装插件的, yarn源的问题有可能导致失败。如果上面安装失败的话,就分别安装 style-resources-loader 和 vue-cli-plugin-style-resources-loader(前提是已经安装过 less less-loader)




// 没有出错的话,可以无视这里
 npm i style-resources-loader vue-cli-plugin-style-resources-loader -D 
 或 
 yarn add  style-resources-loader vue-cli-plugin-style-resources-loader -D

**

第二步配置vue.config.js

  const path = require("path");
   module.exports = {
     ...
     pluginOptions: {
        "style-resources-loader": {
            preProcessor: "less",
            patterns: [
              //这个是加上自己的路径,
              //注意:试过不能使用别名路径
              path.resolve(__dirname, "./src/assets/variable.less")
             ]
         }
     }
     ...
    }

或者使用官网的自动导入在chainWebpack中引入
https://cli.vuejs.org/zh/guid...

二、全局配置sass(直接配置vue.config.js)

注意:官网独爱sass,这种loader的形式只有sass才合适用,其他的(less、stylus)都会报错。

  module.exports = {
    ...
    css: {
        loaderOptions: {
            sass: {
              // @是src的别名
              data: `
                @import "@/assets/variable.scss";
              `
            }
        }
    }
    ...
  }

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 2019-06-05

vue scoped在组件中使用深度选择器修改样式

当你在style中使用了scoped就相当本地作用域,这时无法通过类名去修改组件内部的样式,需要用到深度选择器才可以。
clipboard.png

图片描述

查看原文

赞 1 收藏 1 评论 0

敏小静 发布了文章 · 2019-06-05

ionic3+angular4+cordova混合开发app总结

1.更新热更新:先添加平台--ionic build --prod压缩编译一次 cordova-hcp build ionic cordova prepare(将www的文件复制到platform里面的www)

安卓检测更新-----config.xml与update.xml(版本代号)的版本号要一致,update.xml的url是apk的地址,放在oss上,不建议放服务器上,,带宽小

安卓版本发布需要android:versionCode版本代号;设置版本号之后就会生成版本代号,相对应;版本代号要比应用市场的大

2.添加插件或者代码不生效的时候可以rm安卓平台 ionic cordova platform rm android 然后把plugins platform www文件夹删掉。 再添加回来 ionic cordova platform add android 安卓打包:ionic cordova build android --prod

3.ionic cordova platform查看已安装平台,ionic info查看ionic配置,
ionic info查看信息,若显示全局cordova没有安装,则安装npm install -g cordova@4特定版本,,再来 安装npm install -g cordova@7.0.1或其他版本就可以;卸载npm uninstall cordova -g

4.devApp调试时,无法触发键盘的,以及图片上传

5.ios打包需要在mac电脑上的Xcode 先ionic cordova build ios

6.热更新需要ionic cordova platform add android,添加安卓平台报错的话,加android studio配置sdk,重新安装报错的插件ionic cordova plugin add ionic-plugin-keyboard
利用java的keytool生成一个秘钥文件(android.keystore )

7.热更新控制版本号的不同而进行更新 :
http://blog.csdn.net/ljw12421...
https://www.jianshu.com/p/2f3...

8.添加平台报错的时候需降低平台的版本 : (在android@7.0.0平台以后项目结构发生改变,构建会报错)
https://stackoverflow.com/que...

9.使用Chrome 浏览器调试移动端网页 chrome://inspect/#devices (debug版本才可以调试,签名包无法调试,第一次使用需要翻墙,不使用--prod压缩还可以调试main.js) 可以检测真机上的报错
清缓存 :chrome://appcache-internals
连上真机之后 ionic cordova run android -lc 可以动态刷新页面,实时调试。

10.应用图标和启动页画面--》根目录下的resources文件夹 会在platforms的android的res文件夹生成相应的icon和splash:
https://blog.csdn.net/qq_2043...
https://blog.csdn.net/zapzqc/...

11.tappable属性解决点击延迟 https://blog.csdn.net/bangren...

12.极光推送jpush,解决推送获取不到设备id : (降低版本)
https://blog.csdn.net/li11_/a...

安卓关闭进程,收不到消息通知,ios的通知系统是苹果的服务器发送给ios设备的,如果你第一次打开应用同意接收通知,那么苹果就会在服务器上注册这台设备的通知ID,并且应用的服务器也会收到通知ID,当应用要发通知时,是应用的服务器发消息给苹果服务器,然后苹果服务器发消息给设备,跟你的应用是否打开没有关系。

13.添加平台报node-sass错误时重装:(安装nodesass的方法)
注意:sass的使用依赖ruby环境,但是mac机都是不自带ruby环境。但是也不需要去大费周章的装ruby环境,直接 npm install node-sass --save-dev 把node-sass作为开发依赖安装即可。

sudo SASS_BINARY_SITE=https://npm.taobao.org/mirror... npm install node-sass@4.7.2 --save --ignore-scripts
sudo npm install
然后在node_modules下的node-sass创建vendor空文件夹
sudo SASS_BINARY_SITE=https://npm.taobao.org/mirror... npm rebuild node-sass --save

查看原文

赞 4 收藏 3 评论 0