Git Product home page Git Product logo

blog's People

Contributors

djlxiaoshi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

原型&原型链深度解读

基本概念

原型链

首先我们看上面的示意图,我们可以看到每一个构造函数都会有一个prototype属性(js引擎自动帮我们加上的),这个属性会指向一个原型对象,这个构造函数通过new 会生成一个新的实例对象,这个实例拥有一个__prto__属性,而这个属性也会指向原型对象。

function A () {} // 构造函数
const a = new A(); // 实例

A.prototye === a.__proro__  // true

也就是说通常情况下(没有手动给A.prototye重新赋值)构造函数的prototype和实例的__proto__指向同一地址。而原型对象会有constructor属性指向这个构造函数。同样原型对象也是一个对象,这个对象也会有他的__proto__属性,这个属性又会指向另一个原型对象,这样一层层链接下去就构成了我们通常所说的原型链。

我们通常会通过instanceof这个操作符来判断某个对象是不是某个构造函数的实例(一般我们认为一个对象的__proto__属性和某个构造函数的prototype属性指向同一地址instanceof就会返回true),a instanceof A返回true,这样看来似乎并没与什么不妥,但是我们发现a isntanceof Object返回的也是true,a 是由Object构造函数的直接实例对象吗?a的__proto__和Object的prototype指向同一地址吗?显然不是,那为什么会返回true。其实instanceof表明的是在a这个对象的原型链上存在一个对象的__proto__属性和某个构造函数的prototype属性指向的是同一地址(翻译过来就是:a的整条[[prototype]]链中是否出现过Object.prototype)。a.__proto__.__proto__ === Object.prototype这里会返回true。其实要知道这样的关系,我们还可以使用isPrototypeOf
Object.prototype.isPrototypeOf(a),当然也可以是b.isPrototypeOf(a)

在ES5中获取对象的原型链标准方法是Object.getPrototypeOf,非标准方式是a.__proto__(大多数现代浏览器都会支持)

但是如果这样我们就不能判断一个对象是不是某个构造函数的直接实例了,这时我们就可以使用constructor这个属性

a.constructor === A //true
a.constructor === Object // false

下面再来看看这张图

我们从左上角说起,f2和f1是Foo构造函数的两个实例,他们的__proto__属性指向Foo.prototype所指向的地址(换句话说在这里f2.__proto__Foo.prototype同一个东西)。而Foo.prototype也是一个对象,也拥有__proto__属性,这个属性和Object.prototype指向同一个地址,而Object.prototype.__porto__指向null(也就是说并不是每个对象都有__proto__这个属性)因为这已经是原型链的顶端了。我们再看构造函数Foo其实也是一个对象(函数也是一个对象)它也拥有__proto__,它的__proto__属性指向Function.prototype所指向的地址(即Foo.__proto__ === Function.prototype),这是因为函数对象都是有Function这个构造函数构造的。
Function.prototype(或者Foo.__proto__.__proto__)指向Object.prototype。这里还有中间的Object这个特殊的构造函数,他是一个函数那么他拥有prototype属性,同时他又是一个函数对象,那么他就是由Function构造出来,所以Object.__proto__ === Function.prototypeFunction构造函数䦹如此。解释起来有点麻烦,大家多看这个图就好。所以就会出现下面这些题目了

Function instanceof Object // true 
Object instanceof Function // true 
Function instanceof Function //true
Object instanceof Object // true
Number instanceof Number //false

上面说一个对象的__proto__属性指向对应构造函数的prototype属性所指向的地址,但是这里如果我们新建的对象是通过Object.create函数创建,那么新创建的这个对象的__proto__会指向crate的参数

const p = {name: 'djlxs'};
const o = Object(p);

o.__proto__ === p

属性屏蔽

当我们读取某个对象的某个属性时,实际上是通过[[Get]]这个操作,在对象本身没有找到时,就会在其原型链上寻找直到找到或者返回undefined,当一个属性既出现在对象本身上,又出现在原型链上,那么就会优先返回对象本身相应的属性值,因此这里就发生了属性屏蔽

当我们向一个对象,添加某个属性时,如果这个属性存在于原型链上,且没有设置成只读,那么会在这个对象本身新建这个属性,从而屏蔽原型链上的相应属性,但是如果原型链上的这个属性设置成了只读,那么在严格模式下,会抛出相应错误,非严格模式下,则会忽略。如果在这种情况下,想要设置这个属性,那么我们就不能直接使用=这个赋值操作符,而是要使用Object.defineProperty()

在我们使用的要注意属性屏蔽,这里还有一种隐式的属性屏蔽尤其要注意

var anotherObject = {
	a: 2
}

var myObject = Object.create(anotherObject);

myObject++;

console.log(anotherObject)  // 2
console.log(myObject) // 3

因为这里myObject++相当于myObject = myObject + 1;

注 以上参考自 《你不知道的JavaScript上卷》(144-146)

最后(欢迎大家关注我)

DJL箫氏个人博客
博客GitHub地址
简书
掘金

设计模式学习之观察者、发布订阅模式

要说起学习设计模式的动机实际上是为了准备面试,惭愧脸,但是也正是因为要面试所以促使我去学习,今天学习的就是观察者模式和发布订阅模式。

说起这两个模式确实很像,为了弄懂其中的不同点确实也花了不少功夫,主要参考了这篇博文观察者模式与发布/订阅模式区别,另外一篇就是曾探的《JavaScript设计模式与开发实践》一书中的《发布订阅模式》。两者结合大有所获。

观察者模式

首先来谈谈观察者模式,在观察者模式中,有两个角色一个是Subject,用来维护一个observer列表,另一个角色就是Observer(观察者),在Observer中定义了一个具体的update方法,用来执行相关操作。整个过程就是当某个值发生变化后,Subject调用notify方法(实际就是循环调用observerList中每个observer的update方法,并把新的值作为update的参数传递进去)。从中我们可以看出在Subject中直接调用了Observer中的方法,也就是说Subject和Observer的联系实际上是非常紧密的。

举个例子,现在有一个房东他要租房子,当有空房子的时候,他就会去通知曾经来询问的租户,那么这个时候房东就是直接知道租客的电话和需求(要住什么样的房子)的,也就是此时房东和租客之间实际上是存在联系的。

大致的流向图就像下面。
观察者模式

代码如下:

//观察者列表
function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

//目标
function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
    this.observers.get(i).update( context );
  }
};

//观察者
function Observer(){
  this.update = function(){
    // ...
  };
}

发布订阅模式

前面说到Subject和Observer联系是非常紧密的,因为我们要在Subject中调用Observer中的方法。那么发布订阅模式就可以解耦合,把调用的任务交给一个调度中心(中介),让调度中心去通知各个订阅者。

接着上面的例子。房东有钱后,自己变懒了,他不想每次有房源后,自己还要亲自打电话通知之前预留电话想要租房的租客,因为自己还要记住那些的租客的电话和需求(有钱了不想干这些活,我要躺着赚钱)。于是他就找到了中介,每次空出房子后,直接告诉中介我这里有什么样的房子,中介这里记录着哪些租客有着什么样的需求,中介再去联系有这样需求的租客。那么这里房东和未来可能的租客之间是没有联系的,房东从此不用自己再去亲自打电话去通知每一个有着这样需求的租客,只需要告诉中介一个人就行,中介去通知。那么整个过程就如下面这样

发布订阅模式

var pubsub = {};
(function(myObject) {
    // Storage for topics that can be broadcast
    // or listened to
    var topics = {};
    // An topic identifier
    var subUid = -1;
    // Publish or broadcast events of interest
    // with a specific topic name and arguments
    // such as the data to pass along
    myObject.publish = function( topic, args ) {
        if ( !topics[topic] ) {
            return false;
        }
        var subscribers = topics[topic],
            len = subscribers ? subscribers.length : 0;
        while (len--) {
            subscribers[len].func( topic, args );
        }
        return this;
    };
    // Subscribe to events of interest
    // with a specific topic name and a
    // callback function, to be executed
    // when the topic/event is observed
    myObject.subscribe = function( topic, func ) {
        if (!topics[topic]) {
            topics[topic] = [];
        }
        var token = ( ++subUid ).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    // Unsubscribe from a specific
    // topic, based on a tokenized reference
    // to the subscription
    myObject.unsubscribe = function( token ) {
        for ( var m in topics ) {
            if ( topics[m] ) {
                for ( var i = 0, j = topics[m].length; i < j; i++ ) {
                    if ( topics[m][i].token === token ) {
                        topics[m].splice( i, 1 );
                        return token;
                    }
                }
            }
        }
        return this;
    };
}( pubsub ));

这里为什么要用一个立即执行函数传递一个对象进去,因为我们可能有多个中介,每多一个中介,我们都会动态的去添加一些方法(即告诉中介如何去运作)。subUid就是用于方便取消订阅操作的,假如有一天你租到了自己满意的房子,你就要打电话告诉中介,不要再给我这个号码打电话了,我已经租到房子了。(不然天天都要被中介*扰了)

总结:观察者模式和发布订阅模式的区别应该就是当有房源消息的时候,到底是谁来通知租客,观察者是房东自己本人,而发布订阅则是中介。

最后(欢迎大家关注我)

DJL箫氏个人博客
博客GitHub地址
简书
掘金

如果你觉得有所收获的话,欢迎点赞,欢迎到我的github上面star一下。

前端性能优化

性能优化最核心的部分就是减少请求数量和资源下载量

性能判断指标

DnsTime:DNS解析时间 (选择合适的DNS服务商 DNS预解析)

ConnectTime:tcp连接时间 (忽略)

SSLTime:SSL安全连接时间 (忽略)

RequestTime:发送请求所用时间

FirstByteTime(TTFB):“最初的网络请求被发起”到“从服务器接收到第一个字节前”所花费的毫秒数(优化后端性能,购买更好的服务器内存、CPU、带宽等等)

ReponseTime 资源下载时间

性能测试工具

在这里推荐几款性能测试工具

pagespeed

webpagetest

pingdom

Google develop

性能优化手段

文本文件优化

首先从减少资源大小方面入手,首先就是我们的代码层面竟可能的精简

  • 比如公共代码的提取(通常在项目中我们会把代码分成业务代码,业务代码中的公共代码和第三方公共库)当然这些都是Webpack的code slipt功能来做的
  • 只加载我们需要的代码(treeshaking),例如我们在使用lodash等等库的时候,我们往往只用到了部分功能,那么我们可以只需要加载我们需要使用的代码片段。首先是从代码源上面较少代码的体积。
  • 压缩代码(去掉注释,空格,使用更短的变量名等等),当然这些webpack插件已经帮我们实现好了
  • 服务端开启gzip压缩,我们可以通过判断Content-Encoding这个响应头,当然我们也可以直接通过这个在线工具

到了现在我们的代码在大小方面或许没有多少优化空间了,但是最后还讲一点,就是有时候在引入库的时候,我们首先要思考是不是非得要引入这个库,因为有可能我们只用到了一点点,比如jQuery,在这里我推荐一个网站也许你并不需要jQuery

接下来就是从减少请求数量开始,我们常见的方式就是代码的合并,例如我们可能在webpack中会有一个bundle专门用来存放第三方库的代码,而不是每个库我都给他打包成一个bundle,实际也是为了减少请求数。

图片优化

图片优化我们可以主要从如下四个方面:

  • 1 选择正确的格式
  • 2 选择合适的大小
  • 3 去除元数据适当压缩
  • 4 图片懒加载

首先我们从图片格式说起,我们最常见的格式就是PNG,JPG和Gif。对于gif我们都知道是在有动画效果的时候我们去使用,但是对于png和jpg我们该什么场景下去使用。首先png是一种无损格式,支持半透明,一般图片占用体积非常大,可能动不动就上M了。jpg是一种有损格式,不支持半透明效果。所以在一般情况下我们建议在没有透明效果的要求下,我们都应该使用jpg格式。因为在同等视觉效果下,jpg可能是png的一半体积。当然现在还有一些其他更现代化的格式例如webp等等,视觉效果更好,支持动画,透明而且占用体积小,这基本就是我们理想中的图片格式,但是遗憾的就是目前的支持情况不太好,可以期待一下。

选择合适的大小,这个很容易明白,就是根据图片在网站中展示的真是大小来进行图片的尺寸更改,因为在一般情况下,设计给我们切得图都是1000 甚至2000像素的,但是在网页中我们可能只展示四五百左右的像素,所以图片尺寸调整也是非常重要的

图片压缩,在照片中会存在一些照片基本信息(例如地理位置,曝光度等等),实际上这些数据对于我们来说是无用的,这一部分数据通常可能有20~30k左右,因此我们可以采用一些工具将这些元数据给去掉。一些图片压缩工具:
http://www.verexif.com/en/
https://tinypng.com/

最后就是图片懒加载,这个是非常有用的手段,然后实施起来也很简单,甚至不用我们自己去写js代码。这里主要推荐下面两款图片按需加载的库。
https://github.com/aFarkas/lazysizes
https://github.com/tuupola/jquery_lazyload
我们可以先加载一张低分辨率的,然后等图片真正到了可视区域再去加载真实图片。

当然还有一些其他图片优化相关的比如响应式图片,但是由于我还没有真正实践过,所以暂且不谈。

更多关于图片优化内容可以参考这篇文章:
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/image-optimization
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/automating-image-optimization/

缓存

其实缓存才是性能优化里面最重要的一环,而一般缓存都是需要后端来进行配合。
关于缓存相关的请求头
expires: 绝对时间 (客户端的时间可能不准确)
cache-control: 相对时间
last-modify:服务器端绝对时间 (只能精确到1s,如果是在1s之内放生变化,还是会用缓存的)
Etag:根据内容生成的一个hash值

优先级 cache-control > expires > etag > last-modify
强缓存 200 from memory from disk
协商缓存 304

离线缓存

这一块应该是PWA相关的知识,离线缓存主要用来提升应用处于离线下的用户体验,但是由于现在还没有实践过,所以就到这了。

性能监控

我们在做性能测试的时候其实我们都是在自己的机器上测试的,但是我们其实并不知道用户所处的真正的环境,那么我们为了获取用户真实的数据我们可以做呢?我们可以通过performance API 写一个性能监控脚本 然后将获取到的真实数据上报到我们的服务器上面。

var pageNav = performance.getEntriesByType("navigation")[0];
var dnsTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;
// Request plus response time (network only)
var totalTime = pageNav.responseEnd - pageNav.requestStart;

// Response time only (download)
var downloadTime = pageNav.responseEnd - pageNav.responseStart;

// Time to First Byte (TTFB)
var ttfb = pageNav.responseStart - pageNav.requestStart;

参考链接;https://developers.google.com/web/fundamentals/performance/navigation-and-resource-timing/

https://developers.google.com/web/tools/chrome-devtools/network-performance/understanding-resource-timing?hl=zh-cn

在这里由于我也没有真正做过,所以不详细讲,大家有兴趣可以参考这篇文章

参考文章

谷歌开发文档-性能优化

Angular4学习之依赖注入

Angular4学习之依赖注入

在一个项目中,组件和服务之间存在错综复杂的关系,为了最小程度的耦合,我们需要来管理组织这种关系,依赖注入就是管理这种关系的一种方式。

为什么要使用依赖注入

在学习一个概念之前,我们必须要知道我们为什么要学习这个东西,这个东西究竟解决了什么问题。就好比这里讲到的,依赖注入究竟解决了什么问题。要解决这个问题,我们先来看看示例代码:

export class Car {

  public engine: Engine;
  public tires: Tires;
  public description = 'No DI';

  constructor() {
    this.engine = new Engine();
    this.tires = new Tires();
  }

  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

以上是来自angular官网的一段代码,我们可以看到一个Car类依赖于EngineTires这两个类,我们在Car的构造函数中去实例这两个依赖类。这有什么问题?如果有一天我们的Tires构造函数需要一个参数,那么我们必须要在Car的构造函数中去更改代码。

// ...
constructor() {
   this.engine = new Engine();
   this.tires = new Tires(params);
 }
]
// ...

这种代码是非常不灵活的。虽然我们可以进行如下结构调整

export class Car {

  public engine: Engine;
  public tires: Tires;
  public description = 'No DI';

  constructor(engine, tires) {
    this.engine = engine;
    this.tires = tires;
  }

  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

const car = new Car(new Engine(), new Tires())

这样似乎解决了不灵活的问题,但是如果依赖项很多的话,我们都要去手动创建这些实例,也不太方便。其实创建依赖实例的过程完全可以交给一个专门的'工厂'来做,这就是angular里面的Injector。

基本使用

  • 在组件中使用
@(HexoBlog)Component({
  selector: 'app-heroes',
  providers: [Engine, Tires],
  template: `
    <h2>Heroes</h2>
    <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent {
  construtor(private engine: Engine) {
	this.engine.start();
  }
}

在Angular中,一般我们将这些公共的依赖都会一些一个服务里面。在上面的用法我们可以看到多了一个providers,另外就是在类的构造函数中增加了private engine: Engine我们就可以去使用engine这个实例了,在这个过程中,我们并没有去手动去创建依赖项的实例。这是因为angular的Injector帮我们自动创建了。在这里有一个比较形象的比喻就是,一个厨子(Injector)根据菜谱(providers)去做菜(依赖的实例),但是究竟做哪些菜呢,客人说了算(private engine: Engine也就是构造函数中的)

  • 在服务中使用
import { Injectable } from '@angular/core';

@Injectable()
export class HeroService {
  constructor(private engine: Engine) { }
}

如果我们的一个服务本身就依赖于其他依赖项,那么我们使用@Injectable()装饰器(即使一个服务并没有依赖于其他服务,我们也推荐加上@Injectable()装饰器),我们依然要提供providers。这里由于服务通常跟视图是没有具体的关系,所以这里我们不会引入@component装饰器,那么我们在哪里确定这个providers呢?我们可以在一个module中的providers属性中去定义,那么这个module中的所有组件都会去共用这一个实例,但是我们有时候我们不希望共用一个实例,而是一个新的实例,那么我们可以在这个组件中的providers中重新定义,这样我们就会得到一个新的实例。实际上这就是层级注入。利用层级注入我们既可以共用实例,也可以不共用实例非常方便。一般全局使用的服务,我们会注册在app.module模块之下,这样在整个应用中都可以使用。

在上面我们说过通过依赖注入创建的实例是可以实现共享的,我们证明一下。

import { Component, OnInit, ReflectiveInjector } from '@angular/core';
import {DependenceComponent} from './dependence.component';

@Component({
  selector: 'app-service',
  templateUrl: './service.component.html',
  styleUrls: ['./service.component.scss'],
})


@Injectable()
export class ServiceComponent implements OnInit {
  
  constructor() {
	let injector = ReflectiveInjector.resolveAndCreate([Dependence]);
    let dependence1 = injector.get(Dependence);
    let dependence2 = injector.get(Dependence);
    console.log('dependence1 === dependence2', dependence1 === dependence2); // true
  }
  
  ngOnInit() {}
}

在这里我们可以看见打印出来的是true,这里我们采用的是手动创建实例,所以我们并不需要在providers中提供“菜谱”,实际上resolveAndCreate的参数就是一个providers

Providers

我们有四种配置注入过程,即使用类、使用工厂、使用值、使用别名

  • 使用类
{provide: MyService, useClass: MyService}

这是我们最常见的情形在angular中,通常如果provide的值和useclass的值一样,我们可以简化为[MyService]

  • 使用值
    显然并不是每种情况,我们都需要注入一个类,有时候可以仅仅是一个值
{provide: MyValue, useValue: 12345}
  • 使用别名
{provide: OldService, useClass: NewService}

如果我们有两个服务OldServiceNewService接口都一致,出于某种原因,我们不得不使用OldService作为Token,但是我们又想使用NewService中的接口,那么我们就可以使用别名。

  • 使用存在的值
[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]

这种情况下会创建两个NewLogger的实例,这显然不是我们想要的结果,这时我们就可以使用存在的

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]
  • 使用工厂
    如果我们的服务需要根据不同的输入值,做出不同的响应,那么就必须要接受一个参数,那么我们就必须使用工厂
{provide: MyService, useFactory: (user: User) => {
	user.isAdmin ? new adminService : customService,
	deps: [User]
}}

当使用工厂时,我们可以通过变量的不同值,去实例不同的类。也就是说我们需要根据不同的值返回不同的依赖实例的时候,那么我们就需要使用工厂。

@options@host

目前为止我们的依赖都是存在的,但是实际情况并不是总是这样。那么我们可以通过@optional装饰器来解决这个问题。

import { Optional } from '@angular/core';
// ....
constructor(
    @Optional() private dependenceService: DependenceService
) {}

但是这里DependenceService这个服务类的定义还是存在的,只是没有准备好,例如没有在providers中使用

依赖查找的规则是按照注入器从当前组件向父级组件查找,直到找到这个依赖为止,但是如果限定查找路径截止在宿主组件,那么如果宿主组件中没有就会报错,我们可以通过@host修饰器达到这一功能。

如果一个组件注入了依赖项,那么这个组件就是这个依赖项的宿主组件,但是如果这个组件通过ng-content被嵌入到宿主组件,那么这个宿主组件就是该依赖项的宿主组件。

Token

当我们在构造函数中使用private dependenceService: DependenceService,injector就可以正确的知道我们要实例哪一个类,这是因为在这里DependenceService充当了Token的角色(也就是说类名是可以充当Token的),我们只需要在providers中去寻找具有相同Token的值就行,但是往往我们注入不是一个类,而是一个字符串,function或者对象。而这里string、方法名和对象是不能够充当Token的,那么这时我们就需要来手动创建一个Token:

import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

Inject 装饰器显示的声明所依赖对象的类型

@Injectable()
class A {
	constructor(private buffer: Buffer) {}
}

等同于

class A {
	constructor(@Inject(Buffer) private buffer: Buffer) {}
}

Javascript 异步编程

所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,当第一段有了执行结果之后,再回过头执行第二段。JavaScript采用异步编程原因有两点,一是JavaScript是单线程,二是为了提高CPU的利用率。在提高CPU的利用率的同时也提高了开发难度,尤其是在代码的可读性上。

console.log(1);

setTimeout(function () {
  console.log(2);
});

console.log(3);

JavaScript异步执行示意图

callback

最开始我们在处理异步的时候,采用的是callback回调函数的方式

asyncFunction(function(value){
	// todo
})

在一般简单的情况下,这种方式是完全够用的,但是如果碰到稍微复杂的场景,就有些力不从心,例如当异步嵌套过多的时候。

回调金字塔

但是当我们的异步操作比较多,而且都依赖于上一步的异步的执行结果,那么我们就会产生回调金字塔,难于阅读

step1(function (value1) {
    step2(function(value2) {
        step3(function(value3) {
            step4(function(value4) {
                // Do something with value4
            });
        });
    });
});

当然为了改进这种层层嵌套的写法,我们有几种方式
1 命名函数

function fun1 (params) {
  // todo
  asyncFunction(fun2);
}

function fun2 (params) {
  // todo
  asyncFunction(fun3)
}

function fun3 (params) {
  // todo
  asyncFunction(fun4)
}

function fun4 (params) {
  // todo
}

asyncFunction(fun1)

2 基于事件消息机制的写法

eventbus.on("init", function(){
    operationA(function(err,result){
        eventbus.dispatch("ACompleted");
    });
});
 
eventbus.on("ACompleted", function(){
    operationB(function(err,result){
        eventbus.dispatch("BCompleted");
    });
});
 
eventbus.on("BCompleted", function(){
    operationC(function(err,result){
        eventbus.dispatch("CCompleted");
    });
});
 
eventbus.on("CCompleted", function(){
    // do something when all operation completed
});

当然也可以利用模块化来处理,使得代码易于阅读。以上这三种方式都只是在代码的可读性上面做了改进,但是并没有解决另外一个问题就是异常捕获。

错误栈

function a () {
    b();
}

function b () {
    c();
}

function c () {
    d();
}

function d () {
    throw new Error('出错啦');
}

a();

Node错误打印

从上面的图我们可以看到有一个比较清晰的错误栈信息,a调用b - b调用c - c调用d ,在d中抛出了一个异常。也就是说在JavaScript中在执行一个函数的时候首先会压入执行栈中,执行完毕后会移除执行栈,FILO的结构。我们可以很方便的从错误信息中定位到出错的地方。

function a() {
    b();
}

function b() {
    c(cb);
}

function c(callback) {
    setTimeout(callback, 0)
}

function cb() {
    throw new Error('出错啦');
}

a();

包含异步的错误栈

从上图我们可以看到只打印出了是在一个setTimeout中的回调函数中出现了异常,执行顺序是跟踪不到的。

异常捕获

回调函数中的异常是不能够捕捉到的,因为是异步的,我们只能在回调函数中使用try catch捕获,也就是我注释的部分。

function a() {
    setTimeout(function () {
        // try{
            throw new Error('出错啦');
        // } catch (e) {
        
        // }
        
    }, 0);
}

try {
    a();
} catch (e) {
    console.log('捕捉到异常啦,好高兴哦');
}

但是try catch只能捕捉到同步的错误,不过在回调中也有一些比较好的错误处理模式,例如error-first的代码风格约定,这种风格在node.js中广泛被使用 。

function foo(cb) {
  setTimeout(() => {
    try {
      func();
      cb(null, params);
    } catch (error) {
      cb(error);
    }
    
  }, 0);
}

foo(function(error, value){
	if(error){
		// todo
	}
	// todo
});

但是这么做也很容易陷入恶魔金字塔中。

Promise

规范简述

  • promise 是一个拥有 then 方法的对象或函数。
  • 一个promise有三种状态 pending, rejected, resolved 状态一旦确定就不能改变,且只能够由pending状态变成rejected或者resolved状态,reject和resolved状态不能相互转换。
  • 当promise执行成功时,调用then方法的第一个回调函数,失败时调用第二个回调函数。
  • promise实例会有一个then方法,这个then方法必须返回一个新的promise。

基本用法

// 异步操作放在Promise构造器中
const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
});

// 得到异步结果之后的操作
promise1.then(value => {
  console.log(value, 'world');
}, error =>{
  console.log(error, 'unhappy')
});

异步代码,同步写法

asyncFun()
	.then(cb)
	.then(cb)
	.then(cb)

promise以这种链式写法,解决了回调函数处理多重异步嵌套带来的回调地狱问题,使代码更加利于阅读,当然本质还是使用回调函数。

异常捕获

前面说过如果在异步的callback函数中也有一个异常,那么是捕获不到的,原因就是回调函数是异步执行的。我们看看promise是怎么解决这个问题的。

asyncFun(1).then(function (value) {
    throw new Error('出错啦');
}, function (value) {
    console.error(value);
}).then(function (value) {

}, function (result) {
  console.log('有错误', result);
});

其实是promise的then方法中,已经自动帮我们try catch了这个回调函数,实现大致如下。

Promise.prototype.then = function(cb) {
	try {
		cb()
	} catch (e) {
       // todo
       reject(e)
	}
}

then方法中抛出的异常会被下一个级联的then方法的第二个参数捕获到(前提是有),那么如果最后一个then中也有异常怎么办。

Promise.prototype.done = function (resolve, reject) {
    this.then(resolve, reject).catch(function (reason) {
        setTimeout(() => {
           throw reason;
        }, 0);
    });
};
 asyncFun(1).then(function (value) {
     throw new Error('then resolve回调出错啦');
 }).catch(function (error) {
     console.error(error);
     throw new Error('catch回调出错啦');
 }).done((reslove, reject) => {});

我们可以加一个done方法,这个方法并不会返回promise对象,所以在此之后并不能级联,done方法最后会把异常抛到全局,这样就可以被全局的异常处理函数捕获或者中断线程。这也是promise的一种最佳实践策略,当然这个done方法并没有被ES6实现,所以我们在不适用第三方Promise开源库的情况下就只能自己来实现了。为什么需要这个done方法。

const asyncFun = function (value) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(value);
    }, 0);
  })
};


asyncFun(1).then(function (value) {
  throw new Error('then resolve回调出错啦');
});

(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: then resolve回调出错啦
(node:6312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code
我们可以看到JavaScript线程只是报了一个警告,并没有中止线程,如果是一个严重错误如果不及时中止线程,可能会造成损失。

局限

promise有一个局限就是不能够中止promise链,例如当promise链中某一个环节出现错误之后,已经没有了继续往下执行的必要性,但是promise并没有提供原生的取消的方式,我们可以看到即使在前面已经抛出异常,但是promise链并不会停止。虽然我们可以利用返回一个处于pending状态的promise来中止promise链。

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
});

promise1.then((value) => {
    throw new Error('出错啦!');
}).then(value => {
    console.log(value);
}, error=> {
    console.log(error.message);
    return result;
}).then(function () {
    console.log('DJL箫氏');
});

特殊场景

  • 当我们的一个任务依赖于多个异步任务,那么我们可以使用Promise.all
  • 当我们的任务依赖于多个异步任务中的任意一个,至于是谁无所谓,Promise.race

上面所说的都是ES6的promise实现,实际上功能是比较少,而且还有一些不足的,所以还有很多开源promise的实现库,像q.js等等,它们提供了更多的语法糖,也有了更多的适应场景。

核心代码

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

当调用then的时候,把所有的回调函数存在一个队列中,当调用resolve方法后,依次将队列中的回调函数取出来执行

var ref = function (value) {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            return ref(callback(value));
        }
    };
};

这一段代码实现的级联的功能,采用了递归。如果传递的是一个promise那么就会直接返回这个promise,但是如果传递的是一个值,那么会将这个值包装成一个promise。

generator

基本用法

function * gen (x) {
    const y = yield x + 2;
    // console.log(y);  // 猜猜会打印出什么值
}

const g = gen(1);
console.log('first', g.next());  //first { value: 3, done: false }
console.log('second', g.next()); // second { value: undefined, done: true }

通俗的理解一下就是yield关键字会交出函数的执行权,next方法会交回执行权,yield会把generator中yield后面的执行结果,带到函数外面,而next方法会把外面的数据返回给generator中yield左边的变量。这样就实现了数据的双向流动。

generator实现异步编程

我们来看generator如何是如何来实现一个异步编程(*)

const fs = require('fs');

function * gen() {
    try {
        const file = yield fs.readFile;
        console.log(file.toString());
    } catch(e) {
        console.log('捕获到异常', e);
    }
}

// 执行器
const g = gen();

g.next().value('./config1.json', function (error, value) {
  if (error) {
    g.throw('文件不存在');
  }
  g.next(value);
});

那么我们next中的参数就会是上一个yield函数的返回结果,可以看到在generator函数中的代码感觉是同步的,但是要想执行这个看似同步的代码,过程却很复杂,也就是流程管理很复杂。那么我们可以借用TJ大神写的co。

generator 配合 co

下面来看看如何使用:

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
const co = require('co');

function * gen(path) {
    try {
        const file = yield readFile('./basic.use1.js');
        console.log(file.toString());
    } catch(e) {
        console.log('出错啦');
    }
}

co(gen());

我们看到使用co这个执行器配合generator和promise会非常方便,非常类似同步写法,而且异步中的错误也能很容易被try catch到。这里之所以要使用utils.promisify这个工具函数将普通的异步函数转换成一个promise,是因为co may only yield a chunk, promise, generator, array, or object。使用co 配合generator最大的一个好处就是错误可以try catch 到。

async/await

先来看一段async/await的异步写法

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
    try {
        const file = await readFile('../generator/config.json');
        console.log(file.toString());
    } catch (e) {
        console.log('出错啦');
    }

}

readJsonFile();

我们可以看到async/await的写法十分类似于generator,实际上async/await就是generator的一个语法糖,只不过内置了一个执行器。并且当在执行过程中出现异常,就会停止继续执行。当然await后面必须接一个promise,而且node版本必须要>=7.6.0才可以使用,当然低版本也可以采用babel。

补充

在开发过程中我们常常手头会同时有几个项目,那么node的版本要求很有可能是不同的,那么我们就需要安装不同版本的node,并且管理这些不同的版本,这里推荐使用nvm下载好nvm,安装,使用nvm list 查看node版本列表。使用nvm use 版本号 进行版本切换。

在Node.js中捕获漏网之鱼

process.on('uncaughtException', (error: any) => {
    logger.error('uncaughtException', error)
})

在浏览器环境中捕获漏网之鱼

window.addEventListener('onrejectionhandled', (event: any) => {
    console.error('onrejectionhandled', event)
})

参考文章

Promise中文迷你书
剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类
深入理解Promise实现细节
DJL箫氏的个人博客

Markdown 样式测试

标题1

标题2

标题3

标题4

标题5

sdgfsdgdsgds

  • dsvsd
  • dgfsdgs
指标 描述 计算方式 意义
unload 前一个页面卸载耗时 unloadEventEnd - unloadEventStart
redirect 重定向耗时 redirectEnd - redirectStart 重定向的时间
appCache 缓存耗时 domainLookupStart - fetchStart 读取缓存的时间
DNS时间 DNS时间 domainLookupEnd - domainLookupStart 可观察域名解析服务是否正常
TCP握手时间 TCP握手时间 connectEnd - connectStart 建立连接的耗时
ssl SSL 安全连接耗时 connectEnd - secureConnectionStart 反映数据安全连接建立耗时
TTFB(首字节时间) TTFB是发出页面请求到接收到应 答数据第一个字节所花费的毫秒数 responseStart – requestStart
// FMP
// 前提设置某个DOM元素的elementtiming属性值为meaningful
new PerformanceObserver((entryList, observer) => {
    let perfEntries = entryList.getEntries();
    FMP = perfEntries[0];//startTime 2000以后
    observer.disconnect();//不再观察了
}).observe({ entryTypes: ['element'] });//观察页面中的意义的元素

observer.disconnect(); 行内代码块

5 分钟撸一个前端性能监控工具

图片测试

Webpack优化

webpack 优化主要分为两部分,一是优化构建速度,二是优化输出质量。所谓优化构建速度,那就是要打包快,优化输出质量就是要打的包尽可能的小。

缩小文件匹配范围(include/exclude)

顾明思议,exclude就是用来告诉loader哪些目录下的资源是不用构建,而include就是告诉loader哪些目录下的文件是需要被构建的。

module: {
    rules: [
        {
            test: /\.js$/,
            use: 'babel-loader',
            exclude: /node_modules/, // 排除不处理的目录
            include: path.resolve(__dirname, 'src') // 精确指定要处理的目录
        }
    ]
}

缓存loader的执行结果(cacheDirectory)

cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程(recompilation process)。如果设置了一个空值 (loader: 'babel-loader?cacheDirectory') 或者 true (loader: babel-loader?cacheDirectory=true),loader 将使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。

以上来自这里

module: {
    rules: [
        {
            test: /\.js$/,
            use: 'babel-loader?cacheDirectory', // 缓存loader执行结果
            exclude: /node_modules/,
            include: path.resolve(__dirname, 'src')
        }
    ]
}

优化 resolve modules 配置

resolve modules:它用于配置 Webpack 去哪些目录下寻找第方模块。默认值是['node_modules'],这代表的含义先去当前目录的./node_modules目录下去找我们想找的模块,如果没找到,就去上一级目录的../node_modules中找,如果还没有找到去../../node_modules中去找,以此类推,这和Node.js的模块寻找机制是相似的(向上递归搜索的方式),在现在的应用中我们的第三方模块都放在项目的根目录的node_modules目录下,所以就没有必要用默认向上的方式一层层去找,在这一层如果没有找到,就直接报错就行。配置如下:

const path = require('path');

function resolve(dir) { // 转换为绝对路径
   return path.join(__dirname, dir);
}

resolve: {
    modules: [ // 优化模块查找路径
        path.resolve('src'),
        path.resolve('node_modules') // 指定node_modules所在位置 当你import 第三方模块时 直接从这个路径下搜索寻找
    ]
}

优化 resolve.alias 配置

resolve.alias 配置项通过别名来将原导入路径映射成一个新的导入路径。
在实 战项目中经常会依赖一些庞大的第三方模块,以 React 库为例,安装到 node_module 目录下的 React 库的目录结构如下:

可以看到在发布出去的 React 库中包含两套代码:

  • 一套是采用 CornmonJS 规范的模块化代码,这些文件都放在 ib 录下,package.json 中指定的入口文件 react.js 为模块的入口。
  • 一套是将 React 的所有相关代码打包好的完整代码放到 个单独的文件中, 这些代码没有采用模块化,可以直接执行。其中 dist/react.j s用于开发环境,里包含检查和警告的代码。 dist/react.min.j 用于线上环境,被最小化了。

在默认情况下, Webpack 会从入口文件./ node_modules/react/react.js 开始递
归解析和处理依赖的几十个文件,这会是一个很耗时的操作。通过配置 resolve. alias,
可以让 Webpack 在处理 React 库时,直接使用单独、完整的 react.min.js 文件 ,从而跳
过耗时的递归解析操作。

module.exports = {
	resolve: {
	// 使用 alias 将导入 react的语句换成直接使用单独、 完整的 react.min.js 文件,
	// 减少耗时的递归解析操作
		alias: {
			'react': path.resolve(__dirname, './ node_modules/react/dist/react.min.js')
		}
	}
};

对某些库使用本优化方法后,会影响到后面要讲的使用 Tree-Sharking 去除无效
代码的优化,因为打包好的完整文件中有部分代码在我们的项目中可能永远用不上。一般对整体性比较强的库采用本方法优化,因为完整文件中的代码是个整体,每行都是不可或缺的但是对于些工具类的库如 odash ( https:/ github.com/lodash/lodash),我们的项目中可能只用到了其中几个工具函数,就不能使用本方法去优化了,因为这会导致在我们 的输出代码中包含很多永远不会被执行的代码。

优化 resolve.extensions 配置

resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:extensions:['.js', '.json']

也就是说,当遇到 require ( '. /data ’) 这样的导入语句 webpack 会先去寻找./data .js 文件,如果该文件不存在,就去寻找 data.json 文件,如果还是找不到就报错。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以
resolve .extensions 的配置也会影响到构建的性能 在配置 resolve.extensions时需要遵守 以下几点,以做到尽可能地优化构建性能。

  • 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程。
  • 在源码中写导入语句时,要尽可能带上后缀从而可以避免寻找过程。例如在确定
    的情况下将 require('./data')写成 require('. data.json')

优化 module. noParse 配置

module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库如 jQuery、ChartJS 庞大又没有采用模块化标准,让 Webpack 解析这些文件既耗时又没有意义。

module.exports = { 
	module: { 
		noParse: [/react\.min\.js$/],  //单独、完整的 react.min.js 文件没有采用模块归解析处理
	}
};

注意,被忽略掉的文件里不应该包含 import require define 等模块化语句,不
然会导致在构建出的代码中包含无法在浏览器环境下执行的模块化语句。

使用 DllPlugin

dll(动态链接库),在一个动态链接库中可以包含为其他模块调用的函数和数据。
为什么需要动态链接库:
在通常情况下我们使用一个第三方包,我们希望最后加载的是min.js,现在基本都会去踢动这样一个打包后的min.js。但是我们同在使用第三方包是通过import React from 'react'来进行引用,但是这样引用Webpack就对React做了一次构建,但其实React官方已经提供了构建好的react.min.js,我们其实不必要去再构建一遍,于是我们可能这样去配置Webpack:

module.exports = {
    externals: {
        'react': 'window.React'
    }
    //其它配置忽略...... 
};

这样Webpack在js文件中发现了import React from 'react'就不会再去构建一遍(当然要在html文件中用script标签引入react.min.js),但是这么做还有两个问题,第一如果另外一个第三方内部引用了react,那么在webpack构建这个第三方库时,又会把react构建一遍,第二个问题未必是所有的库都提前已经给你了一个构建好的版本(也就是没有提供min.js),所以我们在引用后,每次build时都会去构建一遍这个第三方库,这样就会造成构建变慢。所以在使用了这种方式后我们只需要在我们所依赖的第三方库发生变化的时候,去执行一遍npm run dll

所以这个时候我们就可以使用动态链接库。具体配置请查看https://webpack.docschina.org/plugins/dll-plugin/

使用HappyPack

由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作, 特别是当文件数量变多后, Webpack 构建慢的问题会显得更为严重。运行在 Node. 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要一个一个地处理任务,不能同时处理多个任务。
Happy Pack 将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。由于 JavaScript 是单线程模型,所以要想发挥多核 CPU 的功能,就只能通过多进程实现,而无法通过多线程实现。

const HappyPack =require('happypack');
module: {
        rules: [
            {
                test: /\.js$/,
                exclude: path.resolve(__dirname, '../node_modules'),
                use: ['happypack/loader?id=babel']
            }
         ]
},
plugins: [
        new HappyPack({
            id: 'babel',
            loaders: [ 'babel-loader?cacheDirectory' ]
        })
]

使用 ParallelUglifyPlugin

在使用 Webpack 构建出用于发布到线上的代码时,都会有压缩代码这 流程 。最常见
JavaScript 代码压缩工具是 [UglifyJS](https: //g ithub .c om/mishoo/UglifyJS2),并且 Webpack也内置了它若用过 UglifyJS ,则我们 定会发现能很快通过它构建用于开发环境的代码,但在构建用于线上的代码时会卡在一个时间点迟迟没有反应,其实在这个卡住的时间 点正在进行的就是代码压缩。

由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,
再去应用各种规则分析和处理 ST ,所以导致这个过程的计算量巨大 耗时非常多。

当Webpack 有多个 JavaScript 文件需要输出和压缩时 原本会使用 UglifyJS 去一个一个压缩再输出,但是 Paralle!Uglify Plugin 会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。所以Paralle!Uglify Plugin 能更快地完成对多个文件的压缩工作。

当然也可以使用UglifyjsWebpackPlugin

使用Tree Shaking

Tree Shaking 可以用来剔除 JavaScript 中用 不上的死代码。它依赖静态的 ES6 模块化
语法,例如通过 import和 export 导入、导出。首先在使用Tree Shaking 的时候,需要将ES6 模块化的代码提交给 Webpack,而不是babel(因为Tree Shaking依赖于ES6的模块化语法)配置如下:

{
	"presets": [
		[
			"env",
			{
				"modules": false
			}
		]
		
	]
}

其中,"modules": false 的含义是关闭 babel 的模块转换功能,保留原本的 ES6模块化语法。然后接入UglifyJS。webpack -- display-used-exports --optimize-minimize

在项目中使用大量的第 方库时,我们会发现 Tree Shaking 似乎不生效了,原因是大部分 Npm 中的代码都采用了 CommonJS 语法,这导致 Tree Shaking 无法正常工作而降级处理。

但幸运的是,有些库考虑到了这一点,这些库在发布到 Npm 上时会同时提供两份代码,一份采用CommonJs 模块化语法,一份采用 ES6 模块化语法。并且在 package.json 文件中分别指出这两份代码的入口。以Redux 库为例,其发布到 Npm 上的目录结构为

然后我们可以通过 mainFields 字段配置优先使用哪个作为入口文件

module.exports = { 
	resolve: { 
		// 针对 Npm 中的第 方模块优先采用 snext main 中指向的 ES6 模块化语法的文件
		mainFields: ['jsnext:main', 'browser', 'main'] 
	}
};

以上配置的含义是优先使用 jsnext:main 作为入口,如果不存在, jsnext:main 就会采用browser或者main作为入口文件。

提取公共代码

在一个多页应用中,可能使用过一些相同的代码,最常见的就是utils.js。如果每个页面都去包含这一段代码,必定会增加资源大小,我们可以将这一部分公共代码提取出来,成为一个单独的bundle,而且由于用户在访问一个页面后很可能会访问另外一个页面,那么在访问前一个页面的时候就会将那个公共的bundle进行缓存,那么在访问下一个页面直接从缓存里面读取就可以。

一般我们的应用中js代码分为三个部分,业务代码、业务公共代码、第三方库(为了长期缓存)。由于一般情况我们采用的第三库是不会改变的那么打的bundle的hash也只也不会改变,这样就可以将这个bundle长期保存。

Webpack3提取公共代码

const ComrnonsChunkPlugin = 
require ('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({ 
// 从哪些 Chunk 中提取
chunks : [ 'a', 'b'], 
// 提取出的公共部分形成 个新的 Chunk
name: 'common'
});

Webpack4提取公共代码

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

区别

它们的区别就在于,CommonChunksPlugin 会找到多数模块中都共有的东西,并且把它提取出来(common.js),也就意味着如果你加载了 common.js,那么里面可能会存在一些当前模块不需要的东西。

而 SplitChunksPlugin 采用了完全不同的方法,它会根据模块之间的依赖关系,自动打包出很多很多(而不是单个)通用模块,可以保证加载进来的代码一定是会被依赖到的。

按需加载

在为单页应用做按需加载优化时, 一般采用以下原则。

  • 将整个网站划分成 个个小功能,再按照每个功能的相关程度将它们分成几类
  • 将每 类合并为一个 Chunk ,按需加载对应的 Chunk
  • 不要按需加载用户首次打开网站时需要看到的画面所对应的功能,将其放到执行入口所在的 Chunk 中,以减少用户能感知的网页加载时间。
  • 对于不依赖大量代码的功能点,例如依赖 Chart.js 去画图表、依赖 flv.js 去播放视
    频的功能点,可再对其进行按需加载。

被分割出去的代码的加载需要一定的时机去触发,即当用户操作到了或者即将操作到对应的功能时再去加载对应的代码。被分割出去的代码的加载时机需要开发者根据网页的需求去衡量和确定。

如果要实现点击某一个按钮之后,去加载一段js,webpack可以这样配置

window.document.getElementByid('btn')
.addEventListener ('click', function () {
	// 在按钮被单击后才去加载 show js 文件,文件加载成功后执行文件导出的函数
	import(/* webpackChunkName : "show" */'./show').then ((show) => { 
		show ();
	})
});
module.exports = {
// JavaScript 执行入口文件
	entry: {
		main:'./main.js', 
	},
	output: {
		// 为从 entry 中配直生成的 Chunk 配置输出文件的名称
		filename:'[name].js', 
		// 为动态加载 Chunk 配置输出文件的名称
		chunkFilename: '[name].js'
	}
};

其中最关键的一句是:import(/* webpackChunkName : ""show" */'./ show')
Webpack 内置了对 import (*)语句的支持,当 Webpack遇到了类似的语句时会这样处理:

  • 以./show.js 为入口重新生成一个 Chunk;
  • 当代码执行到 import 所在的语句时才去加载由 Chunk 对应生成的文件;
  • import 返回一个 Promise ,当文件加载成功时可以在 Promise then 方法中获取
    show.js 导出的内容。

/* webpackChunkName: "show" */的含义是为动态生成的 Chunk 赋予一个名称,
以方便我们追踪和调试代码 。如果不指定动态生成的 Chunk 的名称,则其默认的名称将会是[id] .js。/* webpackChunkName: "show" */,是在 Webpack 中引入的新特性,在Webpack3 之前是无法为动态生成的 Chunk 赋予名称的。

参考文章

深入浅出Webpack

https://jeffjade.com/2017/08/12/125-webpack-package-optimization-for-speed/

http://www.cnblogs.com/imwtr/p/7801973.html

https://zhuanlan.zhihu.com/p/37148975?utm_source=wechat_session&utm_medium=social&utm_oi=32383348768768&from=timeline&isappinstalled=0

PWA学习总结

PWA学习笔记

简单介绍

PWA(Progressive Web App)渐进式Web APP,它并不是单只某一项技术,而是一系列技术综合应用的结果,其中主要包含的相关技术就是Service Worker、Cache Api、Fetch Api、Push API、Notification API 和 postMessage API。使用PWA可以给我们带来什么好处呢?主要体现在如下几方面

1 离线缓存
2 web页面添加桌面快速入口
3 消息推送

相关知识

Service Worker

简单来说,Service Worker 是一个可编程的 Web Worker,它就像一个位于浏览器与网络之间的客户端代理,可以拦截、处理、响应流经的 HTTP 请求。它没有调用 DOM 和其他页面 api 的能力,但他可以拦截网络请求,包括页面切换,静态资源下载,ajax请求所引起的网络请求。Service Worker 是一个独立于JavaScript主线程的浏览器线程。Service Worker有如下特性:

  • 必须在 HTTPS 环境下才能工作(在开发模式下http://localhost也可以工作)
  • 不能直接操作 DOM,(但是可以通过postMessage发送某些信号,主进程根据信号类型,进行不同的操作)
  • 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
  • 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求
  • Service Worker 必须要在主线中进行注册
  • 一旦被 install,就永远存在,除非被手动 unregister
  • 用到的时候可以直接唤醒,不用的时候自动睡眠

注册Service Work

我们需要在主线程中注册Service Worker,并且一般是在页面触发load事件之后进行注册。当Service Worker注册成功后便会进入其生命周期。scope代表Service Worker控制该路径下的所有请求,如果请求路径不是在该路径之下,则请求不会被拦截。

// 注册service worker
window.addEventListener('load', function () {
  navigator.serviceWorker.register('/sw.js', {scope: '/'})
    .then(function (registration) {

      // 注册成功
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    })
    .catch(function (err) {

      // 注册失败:(
      console.log('ServiceWorker registration failed: ', err);
    });
});

Service Worker生命周期

Service Worker生命周期大致如下

install -> installed -> actvating -> Active -> Activated -> Redundant

Service Worker生命周期图

在Service Worker注册成功之后就会触发install事件,在触发install事件后,我们就可以开始缓存一些静态资。waitUntil方法确保所有代码执行完毕后,Service Worker 才会完成Service Worker的安装。需要注意的是只有CACHE_LIST中的资源全部安装成功后,才会完成安装,否则失败,进入redundant状态,所以这里的静态资源最好不要太多。如果 sw.js 文件的内容有改动,当访问网站页面时浏览器获取了新的文件,它会认为有更新,于是会安装新的文件并触发 install 事件。但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来打开的页面里生效。为了能够让新的Service Worker及时生效,我们使用skipWaiting直接使Service Worker跳过等待时期,从而直接进入下一个阶段。

const CACHE_NAME = 'cache_v' + 2;
const CACGE_LIST = [
  '/',
  '/index.html',
  '/main.css',
  '/app.js',
  '/icon.png'
];

function preCache() {
  // 安装成功后操作 CacheStorage 缓存,使用之前需要先通过 caches.open() 打开对应缓存空间。
  return caches.open(CACHE_NAME).then(cache => {
    // 通过 cache 缓存对象的 addAll 方法添加 precache 缓存
    return cache.addAll(CACGE_LIST);
  })
}

// 安装
self.addEventListener('install', function (event) {
  // 等待promise执行完
  event.waitUntil(
    // 如果上一个serviceWorker不销毁 需要手动skipWaiting()
    preCache().then(skipWaiting)
  );
});

在安装成功后,便会触发activate事件,在进入这个生命周期后,我们一般会删除掉之前已经过期的版本(因为默认情况下浏览器是不会自动删除过期的版本的),并更新客户端Service Worker(使用当前处于激活状态的Service Worker)。

// 删除过期缓存
function clearCache() {
  return caches.keys().then(keys => {
    return Promise.all(keys.map(key => {
      if (key !== CACHE_NAME) {
        return caches.delete(key);
      }
    }))
  })
}

// 激活 activate 事件中通常做一些过期资源释放的工作
self.addEventListener('activate', function (e) {
  e.waitUntil(
    Promise.all([
      clearCache(),
      self.clients.claim()
    ])
  );
});

在这里还有一个问题就是sw.js文件有可能会被浏览器缓存,所以我们一般需要设置sw.js不缓存或者较短的缓存时间
更多详细参考 如何优雅的为 PWA 注册 Service Worker

Service Worker 拦截请求

之前说过,Service Worker 是可以拦截请求的,那么一定就会存在一个拦截请求的事件fetch。我们需要在sw.js去监听这个事件。

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache => {
      return cache.match(event.request).then(function (response) {

        // 如果 Service Worker 有自己的返回,就直接返回,减少一次 http 请求
        if (response) {
          console.log('cache 缓存', event.request.url, response);
          return response;
        } else {
            
            if (navigator.online) {
            
                return fetch(event.request).then(function(response) {
                    console.log('network', event.request.url, response);
            // 由于响应是一个JavaScript或者HTML,会认为这个响应为一个流,而流是只能被消费一次的,所以只能被读一次
            // 第二次就会报错 参考文章https://jakearchibald.com/2014/reading-responses/
            cache.put(event.request, response.clone());
            return response;
          }).catch(function(error) {
            console.error('请求失败', error);
            throw error;
          });
          
            } else {
                // 断网处理
                offlineRequest(fetchRequest);
            }
          
        }
      });
    })
  );
});

这里我们在fetch事件中监听请求事件,我们通过cache.match来进行请求的比较,如果存再这个请求的响应我们就直接返回缓存结果,否则就去请求。在这里我们通过cache.add来添加新的缓存,他实际上内部是包含了fetch请求过程的(注意:Cache.put, Cache.add和Cache.addAll只能在GET请求下使用)。在match的时候,需要请求的url和header都一致才是相同的资源,可以设定第二个参数ignoreVary:true。caches.match(event.request, {ignoreVary: true})
表示只要请求url相同就认为是同一个资源。另外需要提到一点,Fetch 请求默认是不附带 Cookies 等信息的,在请求静态资源上这没有问题,而且节省了网络请求大小。但对于动态页面,则可能会因为请求缺失 Cookies 而存在问题。此时可以给 Fetch 请求设置第二个参数。示例:fetch(fetchRequest, { credentials: 'include' } );

Cache API

Cache API 不仅在Service Worker中可以使用,在主页面中也可以使用。我们通过 caches.open(cacheName)来打开一个缓存空间,在,默认情况下,如果我们不手动去清除这个缓存空间,这个缓存会一直存在,不会过期。在使用Cache API之前,我们都需要通过caches.open先去打开这个缓存空间,然后在使用相应的Cache方法。这里有几个注意点:

  • Cache.put, Cache.add和Cache.addAll只能在GET请求下使用
  • 自Chrome 46版本起,Cache API只保存安全来源的请求,即那些通过HTTPS服务的请求。
  • Cache API不支持HTTP缓存头

在使用cache.add和cache.addAll的时候,是先根据url获取到相应的response,然后再添加到缓存中。过程类似于调用 fetch(), 然后使用 Cache.put() 将response添加到cache中

详细MDN文档

Fetch API

Fetch API不仅可以在主线程中进行使用,也可以在Service Worker中进行使用。fetch 和 XMLHttpRequest有两种方式不同:

  • 当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。

  • 默认情况下,fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)

// Example POST method implementation:

postData('http://example.com/answer', {answer: 42})
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  // Default options are marked with *
  return fetch(url, {
    body: JSON.stringify(data), // must match 'Content-Type' header
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include(始终携带), same-origin(同源携带cookie), omit(始终不携带)
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, cors, *same-origin
    redirect: 'follow', // manual, *follow, error
    referrer: 'no-referrer', // *client, no-referrer
  })
  .then(response => response.json()) // parses response to JSON
}

更多信息请查阅:使用 Fetch

Notification

Notification API 用来进行浏览器通知,当用户允许时,浏览器就可以弹出通知。这个API在主页面和Service Worker中都可以使用,MDN文档

  • 在主页面中使用
// 先检查浏览器是否支持
  if (!("Notification" in window)) {
    alert("This browser does not support desktop notification");
  }

  // 检查用户是否同意接受通知
  else if (Notification.permission === "granted") {
    // If it's okay let's create a notification
    new Notification(title, {
      body: desc,
      icon: '/icon.png',
      requireInteraction: true
    });
  }

  // 否则我们需要向用户获取权限
  else if (Notification.permission !== 'denied') {
    Notification.requestPermission(function (permission) {
      // 如果用户同意,就可以向他们发送通知
      if (permission === "granted") {
        new Notification(title, {
          body: desc,
          icon: '/icon.png',
          requireInteraction: true
        });
      } else {
        console.warn('用户拒绝通知');
      }
    });
  }
  • 在Service Worker中使用
// 发送 Notification 通知
function sendNotify(title, options={}, event) {

  if (Notification.permission !== 'granted') {
    console.log('Not granted Notification permission.');

    // 通过post一个message信号量,来在主页面中询问用户获取页面通知权限
    postMessage({
      type: 'applyNotify'
    })
  } else {

    // 在Service Worker 中 触发一条通知
    self.registration.showNotification(title || 'Hi:', Object.assign({
      body: '这是一个通知示例',
      icon: '/icon.png',
      requireInteraction: true
    }, options));
  }
  
}

我们可以看见当我们在Service Worker中进行消息提示时,用户可能关闭了消息提示的功能,所以我们首先要再次询问用户是否开启消息提示的功能,但是在Service Worker中是不能够直接询问用户的,我们必须要在主页面中去询问,这个时候我们可以通过postMessage去发送一个信号量,根据这个信号量的类型,来做响应的处理(例如:询问消息提示的权限,DOM操作等等)

function postMessage(data) {
  self.clients.matchAll().then(clientList => {
    clientList.forEach(client => {
      // 当前打开的标签页发送消息
      if (client.visibilityState === 'visible') {
        client.postMessage(data);
      }
    })
  })
}

在这里我们只向打开的标签页发送该信号量,避免重复询问

message 事件

由于Service Worker是一个单独的浏览器线程,与JavaScript主线程互不干扰,但是我们还是可以通过postMessage实现通信,而且可以通过post特定的消息,从而让主线程去进行相应的DOM操作,实现间接操作DOM的方式。

  • 页面发送消息给Service Worker
    在页面上通过 navigator.serviceWorker.controller 获得 ServiceWorker 的句柄。但只有 ServiceWorker 注册成功后该句柄才会存在。
function sendMsg(msg) {
    const controller = navigator.serviceWorker.controller;

    if (!controller) {
        return;
    }

    controller.postMessage(msg, []);
}

// 在 serviceWorker 注册成功后,页面上即可通过 navigator.serviceWorker.controller 发送消息给它
navigator.serviceWorker
    .register('/test/sw.js', {scope: '/test/'})
    .then(registration => console.log('ServiceWorker 注册成功!作用域为: ', registration.scope))
    .then(() => sendMsg('hello sw!'))
    .catch(err => console.log('ServiceWorker 注册失败: ', err));
    

在 ServiceWorker 内部,可以通过监听 message 事件即可获得消息:

self.addEventListener('message', function(ev) {
    console.log(ev.data);
});
  • Service Worker发送消息给页面
// self.clients.matchAll方法获取当前serviceWorker实例所接管的所有标签页,注意是当前实例 已经接管的
self.clients.matchAll().then(clientList => {
    clientList.forEach(client => {
        client.postMessage('Hi, I am send from Service worker!');
    })
});

在主页面中监听

navigator.serviceWorker.addEventListener('message', event => {
  console.log(event.data);
}); 

Client.postMessage

manifest

3 manifest.json 作用
PWA 添加至桌面的功能实现依赖于 manifest.json,也就是说如果要实现添加至主屏幕这个功能,就必须要有这个文件

{
  "short_name": "短名称",
  "name": "这是一个完整名称",
  "icons": [
  {
    "src": "icon.png",
    "type": "image/png",
    "sizes": "144x144"
  }
],
  "start_url": "index.html"
}

<link rel="manifest" href="path-to-manifest/manifest.json">

name —— 网页显示给用户的完整名称

short_name —— 当空间不足以显示全名时的网站缩写名称

description —— 关于网站的详细描述

start_url —— 网页的初始 相对 URL(比如 /)

scope —— 导航范围。比如,/app/的scope就限制 app 在这个文件夹里。

background-color —— 启动屏和浏览器的背景颜色

theme_color —— 网站的主题颜色,一般都与背景颜色相同,它可以影响网站的显示

orientation —— 首选的显示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary。

display —— 首选的显示方式:fullscreen, standalone(看起来像是native app),minimal-ui(有简化的浏览器控制选项) 和 browser(常规的浏览器 tab)

icons —— 定义了 src URL, sizes和type的图片对象数组。

详细配置

MDN详细配置

manifest验证

相关问题

  • 对于不同的资源,我们可能有不同的缓存策略,怎么方便的去实现这些复杂的场景

使用workbox,如果使用webpack进行项目打包,我们可以使用workbox-webpack-plugin插件

  • 为什么不适用其他的本地缓存方案

Web Storage(例如 LocalStorage 和 SessionStorage)是同步的,不支持网页工作线程,并对大小和类型(仅限字符串)进行限制。 Cookie 具有自身的用途,但它们是同步的,缺少网页工作线程支持,同时对大小进行限制。WebSQL 不具有广泛的浏览器支持,因此不建议使用它。File System API 在 Chrome 以外的任意浏览器上都不受支持。目前正在 File and Directory Entries API 和 File API 规范中改进 File API,但该 API 还不够成熟也未完全标准化,因此无法被广泛采用。

同步的问题 就是负担大,如果有大量请求缓存在本地缓存中,如果是同步,可能负担重

  • 在将相应存在cache中并返回给浏览器报错

resulted in a network error response: a Response whose "body" is locked cannot be used to respond to a request

这是因为在使用put的时候,是流的一个pipe操作,流是只能被消费一次的。我们可以clone这个response或者reques参考文章 

  • 在经过webpack打包后,所有的静态资源都会带有hash值,怎么办

使用某些webpack插件,例如offline-plugin或者webpack-workbox-plugin

代码示例

pwa-study

pwa-webpack-study

参考资料

最后(欢迎大家关注我)

DJL箫氏个人博客

博客GitHub地址(欢迎star)

简书

掘金

个人公众号
个人公众号

You can

Fighting,you are the best!

This解读

在很长的一段时间之内,我一直以为作用域就是上下文,这也就对JavaScript中的this理解增加了很多麻烦,所以这篇文章开篇第一个要陈诉的概念就是作用域和上下文不是一个概念。作用域(scope) 是指变量的可访问性,上下文是来决定this。(注意执行期上下文指的是作用域,这是JavaScipt规范,所以得遵守)

在JavaScript中只有两种作用域,一种是全局作用域,另一个就是函数作用域。上下文则会this息息相关,而this是在运行的时候进行绑定的,它的上下文取决于函数在哪里被调用,this的绑定和函数声明的位置没有任何关系。

当一个函数被调用时,会创建一个活动记录(即执行上下文)。这个活动会包含函数在哪里被调用,函数的调用方法,传入的参数信息等信息。this就是记录的其中一个属性,会在函数的执行过程中用到。

当然这句话出自《你不知道的JavaScript(上卷)》,在这里强烈推荐这本书,字字珠玑。

再次强调:this实际上是在函数被调用的时候发生绑定,它指向什么完全取决于函数在哪里调用。

调用位置

接下来我们看看函数调用包括哪几种情况,只有正确的知道函数调用的位置,才能正确的明白this的指向问题。

默认绑定(全局调用)

var a = 2;
function foo() {
   console.log(this.a)
}
foo();

以上就是默认绑定,foo函数是直接调用的。

隐式绑定

b = 2;
var obj = {
	b: 3,
	foo: foo
}

function foo () {
	console.log(this.b);
}
obj.foo(); // 3

这里为什么叫做隐式绑定,因为这个foo函数无论是在obj里面声明还是在obj外面声明,他实际上都是不属于obj这个对象的(obj只是记录了foo这个属性的引用值),但是最后在执行的时候this却被绑定到了obj这个对象上下文中。当然如果有多个对象链式调用,this只会绑定到最后一层。obj2.obj1.foo(),this是绑定到obj1这个对象上下文中。

当然这里有一个注意点

var obj = {
	b: 3,
	foo: foo
}
function foo () {
	console.log(this.b);
}
var bar = obj.foo;

bar(); // 2	

这里实际上bar直接是foo的引用,就相当于var bar = obj.foo = foo,我们打印一下可以发现

console.log(bar === foo && foo === obj.foo && bar === obj.foo) // true

所以此时就和第一种默认绑定一样,bar函数是直接在全局上下文中被调用的,所以this会指向全局。

还有一种就是嵌套函数了

b = 2;
var obj = {
	b: 3,
	foo: foo
}
function foo () {
	console.log('foo', this.b);// 3
	foo2();
}
function foo2() {
	console.log('foo2', this.b); // 2
}
obj.foo();
	

实际上foo2也是直接被(window)调用了。

显示绑定call,apply,bind

通过call,apply,bind函数可以强制某个函数在哪个对象(或者上下文)中被调用

b = 2;

var obj = {
	b: 3,
	foo: foo
}

function foo () {
	console.log('foo', this.b);
}

foo.call(obj); // 3

当然如果你传入的是一个基本类型的值,那么JavaScript会把它转换成它的对象形式。

new绑定

说到new操作符,就不得不说它的内部工作原理了,我们在执行new操作的时候究竟执行了什么。

1 创建一个全新的对象 var obj = {}
2 这个新对象的原型会被执行[[原型]]连接 obj[[prototype]] = Fun.prototye
3 这个新对象会绑定到函数调用的this Fun.bind(obj)
4 如果函数没有返回其他对象,那么会返回这个新创建的对象 return obj;

所以new绑定实质还是显式绑定。

总结一下我们可以按照下面的顺序进行判断

1 函数是否在new中调用(new 绑定),如果是this绑定的就是返回的新对象
2 函数是否通过call、apply(显式绑定)如果是this绑定的是那个指定的对象
3 函数是否在某个上下文对象中调用(隐式绑定),如果是,this绑定的是那个上下文无关文法对象
4 如果都不是那么就是默认绑定,this绑定的就是全局对象或者undefined(严格模式)

例外

凡事总有例外,如果你把null、undefined作为this的绑定对象传入call、apply或者bind那么实际上,这些值在执行的时候会被忽略,实际使用的是默认绑定。那么什么情况下我们会去绑定一个null或者undefined的呢?一种就是用apply来展开一个数组,当然这种方法的确很实用(不过在ES6中出现了...操作符来展开数组)。

function foo(a, b) {return a + b}
foo.apply(null, [2, 3]);

箭头函数,箭头函数中的this是根据外层作用域来决定this的,也就是说箭头函数中的this就和箭头函数在哪里声明有关系了。

a = 2;

var obj = {
	a: 3,
	foo: foo
}

function foo () {
	return () => {
		console.log(this.a);
	};	
}


var fun = foo.call(obj);

fun(); // 3 此时箭头函数的外层作用域为foo,foo函数的this被绑定在了obj对象上
a = 2;

var obj = {
	a: 3,
	foo: foo
}

var arrowFun = () => {
	console.log(this.a);
}

function foo () {
	return arrowFun;	
}


var fun = foo.call(obj);

fun(); //2 箭头函数的外层作用域为全局作用域,全局作用域中的this指向全局上下文

JavaScript 继承

继承是JS中非常内容,原因就是JS没有地道的继承方式,我们只能通过各种方式来模拟面向对象中的继承。下面介绍几种常见的继承方式及其不足。

构造函数继承

function Parent1 (){
	this.name = 'parent'
}

function Child1() {
	Parent1.call(this);
	this.type = 'child1';
}

缺点:Parent1 原型链上的东西并不会继承,这种方式,所以只实现了部分继承,如果父类的属性都在构造函数中,没问题,但是如果有一部分在原型链上,那么就继承不了,为了解决这个不足我们就要使用原型链来进行原型继承。

原型继承

function Parent2() {
this.name = 'Parent2';
this.res = [1, 2, 3]
}

function Child2 () {
this.type = 'Child2'
}

Child2.prototype = new Parent2()

var child2 = new Child2();
var child3 = new Child3();

child2.res.push(4);
console.log(child3.res) // 1,2,3,4

缺点 : 这种方式缺点也很明显,实例的两个对象,如果一个对象改变res的值,那么另一个对象 的res属性也会被改变(这两个对象共享一个原型),这违背了独立性。

组合

function Parent3() {
	this.name = 'parent3';
	this.res = [1, 2, 3];
}

function Child3() {
	Parent3.call(this);
	this.type = 'child3';
}

Child3.prototype = Parent3.prototype;
var child = new Child3();

缺点:此时child.constructor并不是Child3;而是Parent3,这是因为当我们想获取child.constructor实际上是访问Child3.prototype.constructor(也就是说constructor这个属性是存在于原型上,并不是直接在child这个对象上),而Child3.prototype此时等于Parent3.prototype,所以最后constructor的属性值为Parent3

组合优化

function Parent4() {
this.name = 'parent4';
this.res = [1, 2, 3];
}

function Child4() {
 Parent4.call(this);
 this.type = 'child4';
}

Child4.prototype = Object.create(Parent4.prototype);
// Child4.prototype = Parent4.prototype;
Child4.prototype.constructor = Child4;	

在这里我们加上这句Child4.prototype = Object.create(Parent4.prototype);的目的是为了隔离子类原型和父类原型,现在就是Child4.prototype.__proto__ === Parent4.prototype,如果我们不加,直接修正子类的构造函数(Child4.prototype.constructor = Child4;)那么也会把父类的Parent4.prototype.constructor更改成Child4,因为此时Child4.prototypeParent4.prototype指向同一地址。

注: 如果这里不支持Object.create,我们可以采用下面的plolly
function F(){} F.prototype = Parent4.prototype Child4.prototype = new F()

缺点:貌似还没有实现静态属性的继承

实现静态属性的继承

function Parent() {
  this.age = 20
}

Parent.sex = 'male';
Parent.habby = 'badminton';

function Child() {
  Parent.call(this);
  this.type = 'Child';

 if (Object.setPrototypeOf) {
   Object.setPrototypeOf(Child, Parent)
 } else if (Child.__proto__) {
   Child.__proto__ = Parent
 } else {
   for (var attr in Parent) {
     if (Parent.hasOwnProperty(attr) && !(attr in Child)) {
       Child[attr] = Parent[attr]
     }
   }
 }   
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

var child = new Child();
var parent = new Parent();
console.log(child)
for (var key in Child) {
  console.log(key)
}

console.log(child.constructor)
console.log(parent.constructor)
console.log(child instanceof Child)
console.log(child instanceof Parent)

看似完美了,但是还有一个问题,就是如果后续父类继续添加一些静态的方法,是不会自动同步到子的静态方法上面去的。

最后(欢迎大家关注我)

DJL箫氏个人博客
博客GitHub地址
简书
掘金

基于React的脚手架搭建

基于React的脚手架搭建

目的

为了能够在工作中更加快速的搭建起开发环境,所以在create-react-app的基础之上结合自身公司业务和个人习惯,搭建了个更加方便的脚手架。GitHub地址

技术栈

react  // mvm框架
typescipt // 开发语言
sass // 预编译语言
anti-mobile // UI框架
axios // http请求
react-router-dom // 路由

目录结构

core // 存放一些工具代码和一些服务
environments  // 存放环境配置文件
pages // app页面存放文件
routing // 路由
components // 公共组件

搭建过程

1 配置typescript使用环境

首先我打算使用typescript作为开发语言,首先我们是用create-react-app来作为基础的脚手架工具,按照github上面的操作方法,进行如下操作:

npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start

这样当我们就可以通过npm start启动应用了

配置scss 参考链接

npm install --save node-sass-chokidar

"scripts": {
+  "build-css": "node-sass-chokidar src/ -o src/",
+  "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/--watch --recursive",
   "start": "react-scripts-ts start",
   "build": "react-scripts-ts build",
   "test": "react-scripts-ts test --env=jsdom",

按照如上操作即可,然后再添加src/**/*.css.gitignore文件中。接下来我们希望项目能够监测scss的变化,从而自动去编译scss文件,显然我们要用到watch-cssbuild-css这两个任务,我们可以使用&&操作符,但是这种跨平台性并不是很好,所以我们采用npm-run-all这个工具。

npm install --save npm-run-all

然后进行如下修改

   "scripts": {
     "build-css": "node-sass-chokidar src/ -o src/",
     "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
-    "start": "react-scripts-ts start",
-    "build": "react-scripts-ts build",
+    "start-js": "react-scripts-ts start",
+    "start": "npm-run-all -p watch-css start-js",
+    "build-js": "react-scripts-ts build",
+    "build": "npm-run-all build-css build-js",
     "test": "react-scripts-ts test --env=jsdom",
     "eject": "react-scripts-ts eject"
   }

ok,以上我们就搭建好了scss环境

安装anti-mobile

npm install antd-mobile --save
npm install react-app-rewired --save-dev
"scripts": {
	"build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/",
	"watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive",
+	"start-js": "react-app-rewired start --scripts-version react-scripts-ts",
    "start": "npm-run-all -p watch-css start-js",
+    "build-js": "react-app-rewired build --scripts-version react-scripts-ts",
    "build": "npm-run-all build-css build-js",
+    "test": "react-app-rewired test --env=jsdom",
    "eject": "react-scripts-ts eject"
  },

这里由于我们使用的是typescript,所以"start-js": "react-app-rewired start --scripts-version react-scripts-ts","build-js": "react-app-rewired build --scripts-version react-scripts-ts",后面要加上react-scripts-ts。接下来我们实现按需加载参考链接

安装npm install babel-plugin-import --save-dev

更改config-overrides.js文件

/* config-overrides.js */
const tsImportPluginFactory = require('ts-import-plugin')
const { getLoader } = require("react-app-rewired");

module.exports = function override(config, env) {
    const tsLoader = getLoader(
        config.module.rules,
        rule =>
            rule.loader &&
            typeof rule.loader === 'string' &&
            rule.loader.includes('ts-loader')
    );

    tsLoader.options = {
        getCustomTransformers: () => ({
            before: [ tsImportPluginFactory({
                libraryName: 'antd-mobile',
                libraryDirectory: 'es',
                style: 'css',
            }) ]
        })
    };

    return config;
};

这样我们就可以直接引入import { Button } from 'antd-mobile';不需要在前面引入import 'antd-mobile/dist/antd-mobile.css';

当我们引入antd-mobile后可能会出现下列错误

E:/MyProjects/frame-work-cli/my-app/node_modules/antd-mobile/lib/picker/PropsType.d.ts
(7,15): Parameter 'values' implicitly has an 'any' type.

找到node_modules/antd-mobile/lib/picker/PropsType.d.ts把format?: (values) => void;改成 format?: (values: any) => void;即可当然为了直接省事,直接在tsconfig.json设置"noImplicitAny": false

4 开发环境切换配置

现在公司一般都会有开发环境,预发布环境和正式环境,这些环境所对应的后端地址并不一样,所以我们要来进行一些配置方便切换。实现的思路就是读取命令行参数,根据参数把不同环境的配置文件的内容复制替换掉environment.js这个文件,这里我们用到了shelljscross-env(读取命令行参数)这两个js库。
创建cp-environment.js

const shell = require('shelljs');

const env = process.env.NODE_ENV;
const src = `src/environments/environment.${env}.ts`;
const to = `src/environments/environment.ts`;

shell.cp('-R', src, to);

更改package.json

"scripts": {
    "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/",
    "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive",
    "start-js": "react-app-rewired start --scripts-version react-scripts-ts",
    "start": "cross-env NODE_ENV=pro npm-run-all cp-environment -p watch-css start-js",
    "startDev": "cross-env NODE_ENV=dev npm-run-all cp-environment -p watch-css start-js",
    "startLocal": "cross-env NODE_ENV=local npm-run-all cp-environment -p watch-css start-js",
    "build-js": "react-app-rewired build --scripts-version react-scripts-ts",
    "build": "cross-env NODE_ENV=pro npm-run-all cp-environment build-css build-js",
    "buildLocal": "cross-env NODE_ENV=Local npm-run-all cp-environment build-css build-js",
    "buildDev": "cross-env NODE_ENV=dev npm-run-all cp-environment build-css build-js",
    "test": "react-app-rewired test --env=jsdom",
    "eject": "react-scripts-ts eject",
    "cp-environment": "node cp-environment.js"
  }

当然我们开发环境上传的代码应该是buildDev后的代码,但是有可能有时进行了误操作,把build后的代码上传上去,我们还可以写一个检测的脚本。
原理就是读取当前分支名,根据分知名和当前的环境变量进行对比。

const branchEnvMap = {
  dev: 'dev',
  test: 'test',
  release: 'prod',
};

const shell = require('shelljs');
const branch = shell.exec('git rev-parse --symbolic-full-name --abbrev-ref HEAD', { silent: true }).toString().trim();
const env = process.argv.slice(2).toString().split('=')[1].toString();

if (branchEnvMap[branch] !== env) {
  shell.echo('该分支对应的编译任务不是这个,请检查执行的命令!', branch, env);
  shell.exit(1);
}

Http

用到就是axios,至于怎么用直接看我的GitHub地址就行,这里进行了一层封装,主要是对返回的结果进行统一的处理。

路由

我用到的是react-router-dom,至于用法还是推荐看github,

1 E:/MyProjects/frame-work-cli/my-app/node_modules/@types/react-dom/node_modules/@types/react/index.d.ts
(3631,13): Subsequent property declarations must have the same type.
Property 'a' must be of type 'DetailedHTMLProps<AnchorHTMLAttributes, HTMLAnchorElement>',
but here has type 'DetailedHTMLProps<AnchorHTMLAttributes, HTMLAnchorElement>'.

当我们启动应用的时候可能会出现如上错误,这是由于我们安装的@types/react@types/react-dom版本并不怎么一致,参考链接, 例如下面的版本

 "@types/react": "^15.6.7",
 "@types/react-dom": "^16.0.3",

所以我们可以换成

"@types/react": "^16.0.36", 
"@types/react-dom: "^16.0.3"

2 E:/MyProjects/frame-work-cli/my-app/node_modules/antd-mobile/lib/picker/PropsType.d.ts(7,15): Parameter 'values' implicitly has an 'any' type.

在tsconfig.json设置"noImplicitAny": false

3 error TS1192: Module '"react"' has no default export.

设置 tsconfig.json "allowSyntheticDefaultImports": true,

4 expected parameter: 'props' to have a typedef

在tslint.json设置typedef: false,这个属于tslint相关配置的问题,具体可以看文档

最后(欢迎大家关注我)

DJL箫氏个人博客
博客GitHub地址
简书
掘金

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.