敏小静

敏小静 查看完整档案

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

前端工程师

个人动态

敏小静 发布了文章 · 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');
  });
});

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

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

查看原文

赞 15 收藏 10 评论 6

认证与成就

  • 获得 10 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-01
个人主页被 510 人浏览