Giter VIP home page Giter VIP logo

weekly's Introduction

logo

薄荷前端周刊

  • 😁 每周至少一篇技术分享在Issues,希望能够给一起走在前端路上的你带来一点小小的帮助 ⸝⸝⸝⸝◟̆◞̆♡
  • ✍️ 文章均为原创,可能来自基础回顾、工作总结、新技术探索等等 ଲଇଉକ
  • ❓ 有问题可以在文章下面留言,我们尽可能解答 ⚆_⚆
  • 📩 Watch 即可自动订阅,新文章发布后第一时间推送到您的邮箱 ʕ•̫͡•ོʔ•̫͡•ཻʕ•̫͡•ʔ•͓͡•ʔ
  • 💖 开源需动力,Star 是最好的赞美 ❛‿˂̵✧

近期weekly

欢迎参与

如果你也恰好热爱技术、喜欢写文章,欢迎给weekly投稿,格式请按照ISSUE_TEMPLATE发布新文章,让我们一起帮助更多人!

weekly's People

Contributors

baifann avatar chenyy0708 avatar lance10030 avatar wieve avatar wusb avatar xpig4432xyx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

weekly's Issues

2019/05/01 - iOS内购填坑之旅

开发前准备

1.付费应用协议

因为内购都是和钱相关的,所以我们在开发之前,需要itunes Connect账号的owner,也就是所有者针对未配置过内购的新项目,签署 Paid Applications agreement(《付费应用程序协议》),这个页面一般我们也进不去。。。
image

  1. 配置内购项目

image

我们需要设置一个app secret(App专用共享秘钥),对于自动续期订阅的内购项目,我们需要这个秘钥去苹果做票据验证,其它模式不需要。

  1. Xcode工程配置

image

开启此选项App Store中APP的介绍界面显示内购的相关项目,关闭则不显示

项目应用

实现步骤主要包括三步:
  1. 首先在项目工程中加入StoreKit.framework
  2. 加入头文件#import <StoreKit/StoreKit.h>
  3. 遵守代理SKPaymentTransactionObserver,SKProductsRequestDelegate
App内请求内购项

在创建购买订单之前要向app store请求内购项,建议在购买之前完成,以减少购买时查询订单的时间

  1. 判断用户是否具备权限
if([SKPaymentQueue canMakePayments]) {
        self.progressHUD.label.text = @"购买中,请等待...";
        [self requestProductData];
    }else{
        self.progressHUD.label.text = @"您没有允许程序内付费...";
        [self.progressHUD hideAnimated:YES afterDelay:1];
    }
  1. 创建商品查询请求
(void)requestProductData{
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:
                                 [NSSet setWithObject:self.itemId]];
    
    request.delegate = self;
    
    [request start];
}

注意:这里的ProductIdentifiers可以查询多个商品,只需传一个product id的集合。

查询的结果我们可以在SKProductsRequestDelegate得到结果

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response

{
    NSArray *myProducts = response.products;
    
    if ([myProducts count] > 0) {
        SKProduct *selectedProduct = [myProducts objectAtIndex:0];
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:selectedProduct];
        
        [[SKPaymentQueue defaultQueue] addPayment:payment];
        
    }else{
        if (self.progressHUD) {
            self.progressHUD.label.text = @"无效的产品";
            [self.progressHUD hideAnimated:YES afterDelay:1];
        }
    }
}

请求完成和请求失败可以调用

- (void)requestDidFinish:(SKRequest *)request NS_AVAILABLE_IOS(3_0);
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error NS_AVAILABLE_IOS(3_0);

项目里面请求产品和支付还是放在一起的,可以在点击购买之前先完成商品信息的查询,然后保存下来,后面用户点击购买的时候可以节省一些请求时间,毕竟苹果的服务器确实是有些慢的。。 也可以缩短加载loading的时间。

构建支付请求
  1. 创建支付
SKPayment * payment = [SKPayment paymentWithProduct:product];

[[SKPaymentQueue defaultQueue] addPayment:payment];
  1. 添加支付交易的回调的SKPaymentTransactionObserver

这个是内购项目的最重要的一个方法,我们创建了支付项之后,剩下的就看苹果服务器的了,我们就靠这个observer来接收购买过程状态。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions NS_AVAILABLE_IOS(3_0);

switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed://交易失败
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored://已经购买过该商品
                [self restoreTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchasing:      //商品添加进列表
                NSLog(@"商品添加进列表");
                break;
            default:
                break;
        }

注意:我们需要在适时的时候调用[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
否则苹果会以为这个事务一直没有完成,每次启动都会重复执行updatedTransactions方法。

对于自动订阅模式,我们需要给后端提供SKPaymentTransaction里面的一些信息,来做唯一标识,对于首次购买的用户SKPaymentTransaction里面的originalTransaction是空的,对于续费的用户才会有值,这样我们就可以区分首次购买和续费。

验证票据

我们app采用的服务器验证,获取票据信息后,通过base64编码以后上传至信任的服务器,由服务器完成与App Store的验证,
验证地址
沙盒环境 https://sandbox.itunes.apple.com/verifyReceipt;
正式环境 https://buy.itunes.apple.com/verifyReceipt;

NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    
    NSString *encodingStr = [receiptData base64EncodedString];

沙盒账号是什么

iOS应用里面用到了苹果应用内付费(IAP)功能,在项目上线前一定要进行功能测试。测试肯定是需要的,何况这个跟money有关。。。不仅仅是要测试,我们还要进行大量的测试,这样才能发现内购存在的一些问题,比如漏单,支付失败等问题。开发完成了之后,如何进行测试呢?难道我测试个内购功能要自己掏钱?就算是公司掏钱,但是苹果要抽掉30%,想想点下购买的时候都会手抖。。。
苹果当然没这么坑了,测试内购,苹果提供了沙盒账号的方式。这个沙盒账号其实是虚拟的AppleID,在开发者账号后台的iTune Connect上配置了之后就能使用沙盒账号测试内购,有了沙盒账号,我们就可以任性挥霍啦。

image

IAP内购的一些坑

对于自动订阅模式,要在每次启动的时候都要添加observer [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
这主要是为了防止漏单的问题,在每次启动的时候会去检测是否有未完成的事务,然后再把相关信息上传到服务器。所以我们需要把orderId本地化,之后无论是首次购买还是续费,还有服务器续费未收到通知的情况,都可以防止漏单的问题。

2018/06/16 - 结合 Vue 源码谈谈发布-订阅模式

结合Vue源码谈谈发布-订阅模式

最近的工作学习中接触到了发布-订阅模式。该**编程中的应用也是很广泛的, 例如在 Vue中也大量使用了该设计模式,所以会结合Vue的源码和大家谈谈自己粗浅的理解.

发布订阅模式主要包含哪些内容呢?

  1. 发布函数,发布的时候执行相应的回调
  2. 订阅函数,添加订阅者,传入发布时要执行的函数,可能会携额外参数
  3. 一个缓存订阅者以及订阅者的回调函数的列表
  4. 取消订阅(需要分情况讨论)

这么看下来,其实就像 JavaScript 中的事件模型,我们在DOM节点上绑定事件函数,触发的时候执行就是应用了发布-订阅模式.

我们先按照上面的内容自己实现一个 Observer 对象如下:

//用于存储订阅的事件名称以及回调函数列表的键值对
function Observer() {
    this.cache = {}  
}

//key:订阅消息的类型的标识(名称),fn收到消息之后执行的回调函数
Observer.prototype.on = function (key,fn) {
    if(!this.cache[key]){
        this.cache[key]=[]
    }
    this.cache[key].push(fn)
}


//arguments 是发布消息时候携带的参数数组
Observer.prototype.emit = function (key) {
    if(this.cache[key]&&this.cache[key].length>0){
        var fns = this.cache[key]
    }
    for(let i=0;i<fns.length;i++){
        Array.prototype.shift.call(arguments)
        fns[i].apply(this,arguments)
    }
}
// remove 的时候需要注意,如果你直接传入一个匿名函数fn,那么你在remove的时候是无法找到这个函数并且把它移除的,变通方式是传入一个
//指向该函数的指针,而 订阅的时候存入的也是这个指针
Observer.prototype.remove = function (key,fn) {
    let fns = this.cache[key]
    if(!fns||fns.length===0){
        return
    }
    //如果没有传入fn,那么就是取消所有该事件的订阅
    if(!fn){
        fns=[]
    }else {
        fns.forEach((item,index)=>{
            if(item===fn){
                fns.splice(index,1)
            }
        })
    }
}


//example


var obj = new Observer()
obj.on('hello',function (a,b) {
    console.log(a,b)
})
obj.emit('hello',1,2)
//取消订阅事件的回调必须是具名函数
obj.on('test',fn1 =function () {
    console.log('fn1')
})
obj.on('test',fn2 = function () {
    console.log('fn2')
})
obj.remove('test',fn1)
obj.emit('test')

为什么会使用发布订阅模式呢? 它的优点在于:

  1. 实现时间上的解耦(组件,模块之间的异步通讯)
  2. 对象之间的解耦,交由发布订阅的对象管理对象之间的耦合关系.

发布-订阅模式在 Vue中的应用

  1. Vue的实例方法中的应用:(当前版本:2.5.16)
// vm.$on
export function eventsMixin (Vue: Class<Component>) {
    const hookRE = /^hook:/
    //参数类型为字符串或者字符串组成的数组
    Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
        const vm: Component = this
        // 传入类型为数组
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                this.$on(event[i], fn)
                //递归并传入相应的回调
            }
        } else {
        //
            (vm._events[event] || (vm._events[event] = [])).push(fn)
            // optimize hook:event cost by using a boolean flag marked at registration
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true
            }
        }
        return vm
    }


// vm.$emit

 Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          cbs[i].apply(vm, args)// 执行之前传入的回调
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
  }

Vue中还实现了vm.$once (监听一次);以及vm.$off (取消订阅) ,大家可以在同一文件中看一下是如何实现的.

  1. Vue数据更新机制中的应用
  • observer每个对象的属性,添加到订阅者容器Dependency(Dep)中,当数据发生变化的时候发出notice通知。
  • Watcher:某个属性数据的监听者/订阅者,一旦数据有变化,它会通知指令(directive)重新编译模板并渲染UI
  • 部分源码如下: 源码传送门-observer
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
   // 属性为对象的时候,observe 对象的属性
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []   //存储订阅者 
  }
  // 添加watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
 // 移除
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
 // 变更通知
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

工作中小应用举例

  1. 场景: 基于wepy的小程序. 由于项目本身不是足够的复杂到要使用提供的 redux进行状态管理.但是在不同的组件(不限于父子组件)之间,存在相关联的异步操作.所以在wepy对象上挂载了一个本文最开始实现的Observer对象.作为部分组件之间通信的总线机制:
wepy.$bus = new Observer()
// 然后就可以在不同的模块和组件中订阅和发布消息了

要注意的点

当然,发布-订阅模式也是有缺点的.

  1. 创建订阅者本身会消耗内存,订阅消息后,也许,永远也不会有发布,而订阅者始终存在内存中.
  2. 对象之间解耦的同时,他们的关系也会被深埋在代码背后,这会造成一定的维护成本.

当然设计模式的存在是帮助我们解决特定场景的问题的,学会在正确的场景中使用才是最重要的.

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/10/23 - 小程序云开发初探

云开发是微信平台新开放的功能,为开发者提供集成了服务器,数据库和资源存储的云服务。本文将基于官方文档,以一个小例子来作为探索云开发的相关功能。

云开发官方文档

一、新建云开发项目

新建项目

将微信开发助手更新之后,选择云开发模板。

enter image description here

项目目录

enter image description here

项目目录分为了2大块内容:cloudfunctions(云函数)和miniprogram。miniprogram存放的是和普通开发相同的业务代码和资源,cloudfunctions中则存放了可以上传至云端的代码,在云开发中被称为云函数。
云开发模板建立之后,会带有一些相关例子可以熟悉api。

二、控制台

微信开发者工具更新之后,在工具栏上会有一个控制台入口,点击可出现以下面板,可查看相关数据情况。

enter image description here

概览

概览界面如上图所示,展示了该 云开发项目下使用云资源的统计数据。

用户管理

凡是访问过云项目的用户,都会在用户管理下留有访问记录。前提是该小程序在app.js中设置traceUser:true,表示允许记录用户信息。

    wx.cloud.init({
        env:'......',
        traceUser: true,
    })

数据库

数据库是控制台中最常用的功能之一,在该界面下,可以快速建立数据集合,可以理解为数据表。可在控制台中建立新的集合,添加记录有三种方式:控制台手动添加、文件导入以及调用api。调用api会在每条记录中自动插入用户_openid。

enter image description here

存储管理

存储管理可保存小程序端上传的文件,可通过调用api进行上传,上传名称和路径需要自己定义。

enter image description here

云函数

云函数对cloudfunctions中上传的函数进行管理,可进行调试,查看调用日志等信息。

enter image description here

enter image description here

云函数添加方式有2种,可视化添加与IDE添加,可视化添加的云函数直接上传至了云端,IDE中添加需要上传部署才可以调用。如果要删除云函数,在控制台删除之后,IDE中同步云函数列表即可。

统计分析

统计分析对云服务的调用情况有针对得给出了数据。

enter image description here

三、环境配置

api会在每条记录中自动插入用户_openid。

enter image description here

项目初始化需要在app.js中进行配置,env中填写的就是自主配置的环境ID。

    wx.cloud.init({
        env:'mina-cloud-test001'
    })

四、实际应用

本例以上传书籍信息为实际应用,实现基本的书籍信息增删改查功能,以及图片的上传删除。

enter image description here

enter image description here

读取数据库数据

先通过调用wx.cloud.database();获取数据库所有集合,然后通过查询具体集合的方式获取数据。

    const db = wx.cloud.database();
    const _ = db.command;
    
    db.collection('bookList').get().then(res => {
        console.log('get', res)
        self.setData({
            bookList: res.data
        });
    })

增加数据

    const db = wx.cloud.database();
    const _ = db.command;
    
    db.collection('bookList').add({
        data: {
          bookMes: self.data.bookMes
        }
    }).then(res => {
        console.log(res)         
    })

删除数据

    db.collection('bookList').doc(id).remove().then(res => {
        console.log(res)
        wx.showToast({
            title: '删除成功!',
        })
        self.getBook();
        }).catch(err => {
            console.log('err', res)
        })
    })

增加数据

    const db = wx.cloud.database();
    const _ = db.command;
	
	db.collection('bookList').doc(id).remove().then(res => {
        console.log(res)
        }).catch(err => {
            console.log('err', res)
        })
    })

改变数据

    const db = wx.cloud.database();
	const _ = db.command

	db.collection('bookList').doc(self.data.currentId).update({
        data: {
          bookMes:self.data.bookMes
        }
    }).then(res=>{
        console.log('update',res)
	    self.getBook();
    }).catch(console.error)

查询数据&调用云函数

查询数据采用云函数为例

先在云函数中定义查询函数,每个需要调用云开发api的云函数都必须使用wx-server-sdk,当新创建一个云函数时,项目会提示是否需要使用依赖,选择是则会自动安装wx-server-sdk
函数中的event参数代表由小程序端传递过来的参数,除此之外默认包含了userInfo,可用来做用户鉴权操作。

	//云函数入口文件
    const cloud = require('wx-server-sdk')
    cloud.init()
    const db = cloud.database()
    const _ = db.command
    
    //云函数函数入口
    exports.main = async (event, context) => {
        return db.collection('bookList').where({
            'bookMes.name': _.eq(event.bookMes.name),
            'bookMes.chooseTags':_.in(event.bookMes.chooseTags)
    }).get({
        success:function(res){
          return res
        }
	  })
	}

小程序端引用云函数,name为云函数文件夹的名称,data中存放的是传递给云函数的参数,云函数通过event获取:

    wx.cloud.callFunction({
        name: 'searchBook',
        // 传给云函数的参数
        data: {
            bookMes: self.data.bookMes
        }
    }).then(res => {
        console.log('search',res.result.data)
        self.setData({
            bookList:res.result.data
        })
    })

本文中的api使用方式仅为示例,实际上服务端的api比小程序端的api丰富,实现功能更多。建议设计文件存储、数据库增删改查的操作都在云函数中进行。

上传图片

上传图片需要先调用wx.chooseImage返回的filePath参数,然后自主定义cloudPath,即上传至云端的地址。

    choose() {
        let self = this
        wx.chooseImage({
            count: 1, // 默认9
            sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
            sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
            success: function (res) {
            // console.log(res.tempFilePaths[0])
            // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片
                self.setData({
                    bookPic: res.tempFilePaths[0]
                })
            }
        })
    }
    upload(){
        let self = this
        const filePath = self.data.bookPic
        let myDate = new Date();
        let time = '' + myDate.getFullYear() + (myDate.getMonth() + 1) + myDate.getDate() + myDate.getHours() + myDate.getMinutes() + myDate.getSeconds();
        const cloudPath = 'book-image' + time + filePath.match(/\.[^.]+?$/)[0];
    
        return wx.cloud.uploadFile({
            cloudPath,
            filePath,
        }).then(res => {
            console.log('upload', res)
            let bookMes = self.data.bookMes;
            bookMes.bookPic = res.fileID;
            return self.setData({
                bookMes
            });
        }).catch(err => {
            console.log('error',err)
        })
    }

删除图片

删除图片或其他文件需要具体的fileId,可通过查询得到,通过该fileID进行删除。

    wx.cloud.deleteFile({
        fileList: [fileId],
        success: res => {
            console.log('delete', res.fileList)
        },
        fail: err => {
            console.log('deleteE', err)
        }
    })

五、发现存在的问题

在实际写例子的过程中,也发现了一些问题,因为云开发的功能开放不久,功能并不是很完善,总结了一些发现的小问题:

  1. 数据库暂不支持模糊查询
  2. 数据库集合之间无法关联
  3. 上传图片如果cloudPath和之前的图片一致的话,返回结果虽然现实成功,但实际替换成了之前的旧图
  4. globalData定义方法发生改变,无法与onLaunch同级进行定义。

六、结语

关于云开发,官方文档给出的说明比较详细,仔细阅读文档可以较快速得实现上手应用。但由于目前其功能的局限性,较为复杂的公司业务不适合采用该模式进行开发,适合个人小型业务采用。
上文中如有不尽不实之处,欢迎指出修改,谢谢!ヾ(=・ω・=)o

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/09/09 - 不可或缺的正则手册

定义

正则表达式regex是用于匹配字符串中字符组合的模式,由参数pattern + 标志flags构成。

参数

普通字符

指所有字母、数字、符号等

非打印字符

指换行、回车、空白等不会实际显示出来的字符

字符 说明
\cx 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\f 匹配一个换页符。等价于 \x0c 和 \cL。
\n 匹配一个换行符。等价于 \x0a 和 \cJ。
\r 匹配一个回车符。等价于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t 匹配一个制表符。等价于 \x09 和 \cI。
\v 匹配一个垂直制表符。等价于 \x0b 和 \cK。

限定符

指定匹配前面的子表达式必须要出现多少次才能满足

字符 说明
* 零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。
+ 一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
? 零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 。? 等价于 {0,1}。
{n} n >= 0。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
{n,} n >= 0。至少匹配 n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
{n,m} m >= n>= 0。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。逗号和两个数之间不能有空格。

定位符

描述字符串定边界

字符 说明
^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。
\b 匹配一个字符边界,也就是指字符和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
\B 匹配非字符边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。

圆括号

组,应用于限制多选结构的范围/分组/捕获文本/环视/特殊模式处理

字符 说明
(abc) 匹配 'abc' 并且记住匹配项,括号被称为捕获括号。模式/(foo) (bar) \1 \2/中的 '(foo)' 和 '(bar)' 匹配并记住字符串 "foo bar foo bar" 中前两个单词。模式中的 \1 和 \2 匹配字符串的后两个单词。注意 \1、\2、\n 是用在正则表达式的匹配环节。在正则表达式的替换环节,则要使用像 $1、$2、$n 这样的语法,例如,'bar foo'.replace( /(...) (...)/, '$2 $1' )。
(?:abc) 匹配 'abc' 这样一组,但不记录,不保存到$变量中,否则可以通过$x取第几个括号所匹配到的项,比如:(aaa)(bbb)(ccc)(?:ddd)(eee),可以用$1获取(aaa)匹配到的内容,而$3则获取到了(ccc)匹配到的内容,而$4则获取的是由(eee)匹配到的内容,因为前一对括号没有保存变量
a(?=bbb) 正向肯定查找,表示a后面必须紧跟3个连续的b。例如,"Windows(?=95|98|NT|2000)"能匹配"Windows2000"中的"Windows",但不能匹配"Windows3.1"中的"Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
a(?!bbb) 正向否定查找,表示a后面不能跟3个连续的b。例如"Windows(?!95|98|NT|2000)"能匹配"Windows3.1"中的"Windows",但不能匹配"Windows2000"中的"Windows"。
(?<=bbb)a 反向肯定查找,表示a前面必须紧跟3个连续的b。例如,"(?<=95|98|NT|2000)Windows"能匹配"2000Windows"中的"Windows",但不能匹配"3.1Windows"中的"Windows"。
(?<!bbb)a 反向否定查找,表示a前面不能跟3个连续的b。例如"(?<!95|98|NT|2000)Windows"能匹配"3.1Windows"中的"Windows",但不能匹配"2000Windows"中的"Windows"。

中括号

单个匹配,字符集/排除字符集/命名字符集

字符 说明
[xyz] 字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 "plain" 中的 'a'。
[^xyz] 负值字符集合。匹配未包含的任意字符。例如, '[^abc]' 可以匹配 "plain" 中的'p'、'l'、'i'、'n'。
[a-z] 字符范围。匹配指定范围内的任意字符。例如,'[a-z]' 可以匹配 'a' 到 'z' 范围内的任意小写字母字符。
[^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^a-z]' 可以匹配任何不在 'a' 到 'z' 范围内的任意字符。

其他特殊字符

字符 说明
\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,'n' 匹配字符 "n"。'\n' 匹配一个换行符,而 "\(" 则匹配 "("。
? 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。
. 匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 '\n' 在内的任何字符,请使用像"(.|\n)"的模式。
x|y 匹配 x 或 y。例如,'z|food' 能匹配 "z" 或 "food"。'(z|f)ood' 则匹配 "zood" 或 "food"。
\d 匹配一个数字字符。等价于 [0-9]。
\D 匹配一个非数字字符。等价于 [^0-9]。
\w 匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。
\W 匹配非字母、数字、下划线。等价于 '[^A-Za-z0-9_]'。
\xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,'\x41' 匹配 "A"。'\x041' 则等价于 '\x04' & "1"。正则表达式中可以使用 ASCII 编码。
\num 匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,'(.)\1' 匹配两个连续的相同字符
\n 标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
\nm 标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
\nml 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
\un 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。

标志

有6个标志,可单独或一起使用

flags 说明
g 全局搜索
i 不区分大小写搜索
m 多行搜索
u 正确处理四个字节的 UTF-16 编码
y 粘连搜索
s dotAll模式,即点(dot)代表一切字符

创建

有以下两种方式构建一个正则表达式:

  1. 字面量:由包含在斜杠之间的模式组成
  • 格式: pattern/flags
const regex = /ab+c/;

const regex = /hello/gi;
  • 优点:加载时编译,性能好
  1. 构造函数
  • 格式:new RegExp(pattern [, flags])
let regex = new RegExp("ab+c");

let regex = new RegExp(/hello/, "gi");

let regex = new RegExp(/hello/gi);

let regex = new RegExp("hello", "gi");
  • 优点:运行时编译,可动态修改

使用

let regex = /nn/;
let str = 'runnobnnnbnn';
方法 说明 示例 返回值
test 测试是否匹配,返回true或false regex.test(str) true
exec 查找匹配的内容,有则返回一个数组,未匹配则返回null regex.exec(str) ["nn", index: 2, input: "runnobnnnbnn", groups: undefined]
match 查找匹配的内容,有则返回一个数组,未匹配则返回null str.match(regex) ["nn", index: 2, input: "runnobnnnbnn", groups: undefined]
search 查找匹配的内容,有则返回位置索引,未匹配则返回-1 str.match(regex) 2
replace 查找匹配的内容,并且使用替换字符str1串替换掉匹配到的子字符串 str.replace(regex, str1) ruccobnnnbnn
split 使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中 str.split(regex) ["ru", "ob", "nb", ""]
  • match:非全局匹配时,跟exec很相似;全局匹配时,就大不同了,上面的例子,在全局匹配时的返回值如下:
regex.exec(str) // ["nn", index: 6, input: "runnobnnnbnn", groups: undefined]

str.match(regex) // ["nn", "nn", "nn"]

参考文档

2018/09/04 - 7分钟理解JS的节流、防抖及使用场景

前言

据说阿里有一道面试题就是谈谈函数节流函数防抖
糟了,这可触碰到我的知识盲区了,好像听也没听过这2个东西,痛定思痛,赶紧学习学习。here we go!

15341407332107

概念和例子

函数防抖(debounce)

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

看一个🌰(栗子):

//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content)
}

let inputa = document.getElementById('unDebounce')

inputa.addEventListener('keyup', function (e) {
    ajax(e.target.value)
})

看一下运行结果:

2018-09-04 09 23 46

可以看到,我们只要按下键盘,就会触发这次ajax请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。下面我们优化一下:

//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content)
}

function debounce(fun, delay) {
    return function (args) {
        let that = this
        let _args = args
        clearTimeout(fun.id)
        fun.id = setTimeout(function () {
            fun.call(that, _args)
        }, delay)
    }
}
    
let inputb = document.getElementById('debounce')

let debounceAjax = debounce(ajax, 500)

inputb.addEventListener('keyup', function (e) {
        debounceAjax(e.target.value)
    })

看一下运行结果:

2018-09-04 09 29 50

可以看到,我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。
再看一个🌰:

    
let biu = function () {
    console.log('biu biu biu',new Date().Format('HH:mm:ss'))
}

let boom = function () {
    console.log('boom boom boom',new Date().Format('HH:mm:ss'))
}


setInterval(debounce(biu,500),1000)
setInterval(debounce(boom,2000),1000)

看一下运行结果:

2018-09-04 09 32 21

这个🌰就很好的解释了,如果在时间间隔内执行函数,会重新触发计时。biu会在第一次1.5s执行后,每隔1s执行一次,而boom一次也不会执行。因为它的时间间隔是2s,而执行时间是1s,所以每次都会重新触发计时

个人理解 函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条。

函数节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

看一个🌰:

  function throttle(fun, delay) {
        let last, deferTimer
        return function (args) {
            let that = this
            let _args = arguments
            let now = +new Date()
            if (last && now < last + delay) {
                clearTimeout(deferTimer)
                deferTimer = setTimeout(function () {
                    last = now
                    fun.apply(that, _args)
                }, delay)
            }else {
                last = now
                fun.apply(that,_args)
            }
        }
    }

    let throttleAjax = throttle(ajax, 1000)

    let inputc = document.getElementById('throttle')
    inputc.addEventListener('keyup', function(e) {
        throttleAjax(e.target.value)
    })

看一下运行结果:

2018-09-04 09 36 49

可以看到,我们在不断输入时,ajax会按照我们设定的时间,每1s执行一次。

结合刚刚biubiubiu的🌰:

    let biubiu = function () {
        console.log('biu biu biu', new Date().Format('HH:mm:ss'))
    }

    setInterval(throttle(biubiu,1000),10)

2018-09-04 09 37 58

不管我们设定的执行时间间隔多小,总是1s内只执行一次。

个人理解 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

总结

  • 函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
  • 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。

结合应用场景

  • debounce
    • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
    • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
  • throttle
    • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
    • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

拓展

参考链接:http://www.cnblogs.com/zichi/p/5331426.html

15343043539670

这是高程中的经典代码:

    function throttle(method, context) {
        clearTimeout(method.tId);
        method.tId = setTimeout(function () {
            method.call(context);
        }, 100)
    }

我们通过上面的例子知道,其实这段函数应该是debounce函数防抖,而不是函数节流,很多文章也都会拿这段代码来做例子,函数本身没错,但是命名错了。

原作者的这段话就写的很好,

就以 throttle 为例,某日,老师给你布置了一个作业,让你深入理解一下 throttle,第二天上课来聊聊。张三心里非常高兴,这个概念在经典书籍《JavaScript高级程序设计》中见过,打开一看,就两页,而且解释地非常清晰,看完就高兴地干别的事情去了。而李四,觉得高程三讲的有点少,而去谷歌了下其他关于 throttle 的知识点,兴奋地看到 throttle 函数的好几种写法,发现高程三只是用了最简单的方式,还有更优雅运用场景更多的写法,或许此时他已经发现和 throttle 同时出现的还有个 debounce,这是什么鬼?反正老师没说,以后再看吧,于是心满意足地玩游戏去了。而王五,和李四一样发现了 debounce,这是什么?一起了解了吧,继而发现 debounce 的用法居然和高程三中的 throttle 一样!继续挖下去,发现高程三中的 throttle 函数其实应该叫 debounce,看到最后,王五已经把 throttle 和 debounce 彻底理解了。

我们要做王五,并且争取早日产出一手知识!加油!

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/07/15 - JavaScript中的垃圾回收和内存泄漏

javaScript 中的垃圾回收和内存泄漏

之前接触的js的内存管理方面的内容一直比较零散,最近在这一块做了一些系统的学习.学习过程中的一些总结在这里分享给大家.欢迎批评指正,共同学习,共同进步.

在一部分语言中是提供了内存管理的接口的,例如C语言中的 molloc()free(); 而在 JavaScript 中会自动进行内存的分配和回收的,因为自动这两个字,就让很多的开发者认为我们是不需要去关心内存方面的问题,当然,这是一种错误的看法.关注内存的管理,避免内存的泄漏也是性能优化重要的一项.

变量的生命周期

Javascript 变量的生命周期要分开来看,对于全局变量,他的生命周期会持续到页面关闭(这就涉及到了后面要总结的内存泄漏的一种方式).而对于局部变量,在所在的函数的代码执行之后,局部变量的生命周期结束,他所占用的内存会通过垃圾回收机制释放(即垃圾回收).

垃圾回收机制

垃圾回收通常有两种方式来实现:

引用计数

这种算法在MDN文档中被称为最"天真"的垃圾回收算法;核心原理是: 判断一个对象是否要被回收就是要看是否还有引用指向它,如果是"零引用",那么就回收.说这种算法天真,是因为它存在着较为严重的缺陷---循环引用:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

首先要注意我们是在函数作用域中讨论的这个问题,而不是全局环境中.老版本的IE中的非JavaScript原生对象如 DOMBOM 对象就采用的这种策略.下面这种情况下就会出现内存泄漏:

var el =document.getElementById("some_element");
var Obj =new Object();
myObj.el = el;
el.someObject = Obj;

当然我们可以在不用的时候手动释放:

myObj.el = null;
el.someObject = null;

标记清除

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”.

这个算法假定有一个根(root)的对象;在 Javascript 里,根是全局对象,对应于浏览器环境的 window,node 环境的 global.垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象.

这个算法相对于引用计数的优势在于,“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”.

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法,都是在此基础上进行优化.所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义.

限制: 那些无法从根对象查询到的对象都将被清除
当然,在我们的开发实践中很少遇到这种情况,这也是我们忽略内存管理的原因之一.

常见的内存泄漏举例

1.忘记声明的局部变量

function a(){
    b=2
    console.log('b没有被声明!')
}

b 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.

2.闭包带来的内存泄漏

var leaks = (function(){
    var leak = 'xxxxxx';// 闭包中引用,不会被回收
    return function(){
        console.log(leak);
    }
})()

当然有时候我们是故意让这个变量保存在内存中的,但是要避免无意的时候造成的内存泄漏.

3.移除 DOM 节点时候忘记移除暂存的值

有时候出于优化性能的目的,我们会用一个变量暂存 节点,接下来使用的时候就不用再从 DOM 中去获取.但是在移除 DOM 节点的时候却忘记了解除暂存的变量对 DOM 节点的引用,也会造成内存泄漏

var element = {
  image: document.getElementById('image'),
  button: document.getElementById('button')
};

document.body.removeChild(document.getElementById('image'));
// 如果element没有被回收,这里移除了 image 节点也是没用的,image 节点依然留存在内存中.

与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的

4. 定时器中的内存泄漏

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 再提一下 setTimeout ,它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.

chrome中查看

老版本的在 Timeline 中查看, 新版本的在 performance 中查看:

image

步骤:

  1. 打开开发者工具 Performance
  2. 勾选 Screenshotsmemory
  3. 左上角小圆点开始录制(record)
  4. 停止录制

图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题.

关于工具的使用暂时在这里浅尝辄止了,后面再深入的学习了开发者工具方方面面的使用再来和大家分享.

参考文档:

  1. MDN文档
  2. 推荐给大家的一个ppt

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/11/05 - React和Vue中,是如何监听变量变化的

React 中

本地调试React代码的方法

  • 先将React代码下载到本地,进入项目文件夹后yarn build
  • 利用create-react-app创建一个自己的项目
  • 把react源码和自己刚刚创建的项目关联起来,之前build源码到build文件夹下面,然后cd到react文件夹下面的build文件夹下。里面有node_modules文件夹,进入此文件夹。发现有react文件夹和react-dom文件夹。分别进入到这两个文件夹。分别运行yarn link。此时创建了两个快捷方式。react和react-dom
  • cd到自己项目的目录下,运行yarn link react react-dom 。此时在你项目里就使用了react源码下的build的相关文件。如果你对react源码有修改,就刷新下项目,就能里面体现在你的项目里。

场景

假设有这样一个场景,父组件传递子组件一个A参数,子组件需要监听A参数的变化转换为state。

16之前

在React以前我们可以使用componentWillReveiveProps来监听props的变换

16之后

在最新版本的React中可以使用新出的getDerivedStateFromProps进行props的监听,getDerivedStateFromProps可以返回null或者一个对象,如果是对象,则会更新state

getDerivedStateFromProps触发条件

我们的目标就是找到 getDerivedStateFromProps的 触发条件

我们知道,只要调用setState就会触发getDerivedStateFromProps,并且props的值相同,也会触发getDerivedStateFromProps(16.3版本之后)

setStatereact.development.js当中

Component.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
ReactNoopUpdateQueue {
    //...部分省略
    
    enqueueSetState: function (publicInstance, partialState, callback, callerName) {
    warnNoop(publicInstance, 'setState');
  }
}

执行的是一个警告方法

function warnNoop(publicInstance, callerName) {
  {
    // 实例的构造体
    var _constructor = publicInstance.constructor;
    var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';
    // 组成一个key 组件名称+方法名(列如setState)
    var warningKey = componentName + '.' + callerName;
    // 如果已经输出过警告了就不会再输出
    if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
      return;
    }
    // 在开发者工具的终端里输出警告日志 不能直接使用 component.setState来调用 
    warningWithoutStack$1(false, "Can't call %s on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + 'class property with the desired state in the %s component.', callerName, componentName);
    didWarnStateUpdateForUnmountedComponent[warningKey] = true;
  }
}

看来ReactNoopUpdateQueue是一个抽象类,实际的方法并不是在这里实现的,同时我们看下最初updater赋值的地方,初始化Component时,会传入实际的updater

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

我们在组件的构造方法当中将this进行打印

class App extends Component {
  constructor(props) {
    super(props);
    //..省略

    console.log('constructor', this);
  }
}

-w766

方法指向的是,在react-dom.development.jsclassComponentUpdater

var classComponentUpdater = {
  // 是否渲染
  isMounted: isMounted,
  enqueueSetState: function(inst, payload, callback) {
    // inst 是fiber
    inst = inst._reactInternalFiber;
    // 获取时间
    var currentTime = requestCurrentTime();
    currentTime = computeExpirationForFiber(currentTime, inst);
    // 根据更新时间初始化一个标识对象
    var update = createUpdate(currentTime);
    update.payload = payload;
    void 0 !== callback && null !== callback && (update.callback = callback);
    // 排队更新 将更新任务加入队列当中
    enqueueUpdate(inst, update);
    //
    scheduleWork(inst, currentTime);
  },
  // ..省略
}

enqueueUpdate
就是将更新任务加入队列当中

function enqueueUpdate(fiber, update) {
  var alternate = fiber.alternate;
  // 如果alternat为空并且更新队列为空则创建更新队列
  if (null === alternate) {
    var queue1 = fiber.updateQueue;
    var queue2 = null;
    null === queue1 &&
      (queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState));
  } else

    (queue1 = fiber.updateQueue),
      (queue2 = alternate.updateQueue),
      null === queue1
        ? null === queue2
          ? ((queue1 = fiber.updateQueue = createUpdateQueue(
              fiber.memoizedState
            )),
            (queue2 = alternate.updateQueue = createUpdateQueue(
              alternate.memoizedState
            )))
          : (queue1 = fiber.updateQueue = cloneUpdateQueue(queue2))
        : null === queue2 &&
          (queue2 = alternate.updateQueue = cloneUpdateQueue(queue1));
  null === queue2 || queue1 === queue2
    ? appendUpdateToQueue(queue1, update)
    : null === queue1.lastUpdate || null === queue2.lastUpdate
      ? (appendUpdateToQueue(queue1, update),
        appendUpdateToQueue(queue2, update))
      : (appendUpdateToQueue(queue1, update), (queue2.lastUpdate = update));
}

我们看scheduleWork下

function scheduleWork(fiber, expirationTime) {
  // 获取根 node
  var root = scheduleWorkToRoot(fiber, expirationTime);
  null !== root &&
    (!isWorking &&
      0 !== nextRenderExpirationTime &&
      expirationTime < nextRenderExpirationTime &&
      ((interruptedBy = fiber), resetStack()),
    markPendingPriorityLevel(root, expirationTime),
    (isWorking && !isCommitting$1 && nextRoot === root) ||
      requestWork(root, root.expirationTime),
    nestedUpdateCount > NESTED_UPDATE_LIMIT &&
      ((nestedUpdateCount = 0), reactProdInvariant("185")));
}
function requestWork(root, expirationTime) {
  // 将需要渲染的root进行记录
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    // 执行到这边直接return,此时setState()这个过程已经结束
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

太过复杂,一些方法其实还没有看懂,但是根据断点可以把执行顺序先理一下,在setState之后会执行performSyncWork,随后是如下的一个执行顺序

performSyncWork => performWorkOnRoot => renderRoot => workLoop => performUnitOfWork => beginWork => applyDerivedStateFromProps

最终方法是执行

function applyDerivedStateFromProps(
  workInProgress,
  ctor,
  getDerivedStateFromProps,
  nextProps
) {
  var prevState = workInProgress.memoizedState;
      {
        if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {
          // Invoke the function an extra time to help detect side-effects.
          getDerivedStateFromProps(nextProps, prevState);
        }
      }
      // 获取改变的state
      var partialState = getDerivedStateFromProps(nextProps, prevState);
      {
        // 对一些错误格式进行警告
        warnOnUndefinedDerivedState(ctor, partialState);
      } // Merge the partial state and the previous state.
      // 判断getDerivedStateFromProps返回的格式是否为空,如果不为空则将由原的state和它的返回值合并
      var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
      // 设置state
      // 一旦更新队列为空,将派生状态保留在基础状态当中
      workInProgress.memoizedState = memoizedState; // Once the update queue is empty, persist the derived state onto the
      // base state.
      var updateQueue = workInProgress.updateQueue;

      if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
        updateQueue.baseState = memoizedState;
      }
}

Vue

vue监听变量变化依靠的是watch,因此我们先从源码中看看,watch是在哪里触发的。

Watch触发条件

src/core/instance中有initState()

/core/instance/state.js

在数据初始化时initData(),会将每vue的data注册到objerserver

function initData (vm: Component) {
  // ...省略部分代码
  
  // observe data
  observe(data, true /* asRootData */)
}
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创建observer
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

来看下observer的构造方法,不管是array还是obj,他们最终都会调用的是this.walk()

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      // 遍历array中的每个值,然后调用walk
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

我们再来看下walk方法,walk方法就是将object中的执行defineReactive()方法,而这个方法实际就是改写setget方法

/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
}

/core/observer/index.js
defineReactive方法最为核心,它将set和get方法改写,如果我们重新对变量进行赋值,那么会判断变量的新值是否等于旧值,如果不相等,则会触发dep.notify()从而回调watch中的方法。

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // dep当中存放的是watcher数组 
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) { 
    // 如果第三个值没有传。那么val就直接从obj中根据key的值获取
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
    
    Object.defineProperty(obj, key, {
    enumerable: true,
    // 可设置值
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // dep中生成个watcher
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // 重点看set方法
    set: function reactiveSetter (newVal) {
      // 获取变量原始值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 进行重复值比较 如果相等直接return
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        // dev环境可以直接自定义set
        customSetter()
      }
        
      // 将新的值赋值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 触发watch事件
      // dep当中是一个wacher的数组
      // notify会执行wacher数组的update方法,update方法触发最终的watcher的run方法,触发watch回调
      dep.notify()
    }
  })
}

小程序

自定义Watch

小程序的data本身是不支持watch的,但是我们可以自行添加,我们参照Vue的写法自己写一个。
watcher.js

export function defineReactive (obj, key, callbackObj, val) {
  const property = Object.getOwnPropertyDescriptor(obj, key);
  console.log(property);

  const getter = property && property.get;
  const setter = property && property.set;

  val = obj[key]

  const callback = callbackObj[key];

  Object.defineProperty(obj, key, {
    enumerable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      return value
    },
    set: (newVal) => {
      console.log('start set');
      const value = getter ? getter.call(obj) : val

      if (typeof callback === 'function') {
        callback(newVal, val);
      }

      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      console.log('finish set', newVal);
    }
  });
}

export function watch(cxt, callbackObj) {
  const data = cxt.data
  for (const key in data) {
    console.log(key);
    defineReactive(data, key, callbackObj)
  }
}

使用

我们在执行watch回调前没有对新老赋值进行比较,原因是微信当中对data中的变量赋值,即使给引用变量赋值还是相同的值,也会因为引用地址不同,判断不相等。如果想对新老值进行比较就不能使用===,可以先对obj或者array转换为json字符串再比较。

//index.js
//获取应用实例
const app = getApp()

import {watch} from '../../utils/watcher';

Page({
  data: {
    motto: 'hello world',
    userInfo: {},
    hasUserInfo: false,
    canIUse: wx.canIUse('button.open-type.getUserInfo'),
    tableData: []
  },
    onLoad: function () {
    this.initWatcher();
  },
  initWatcher () {
    watch(this, {
      motto(newVal, oldVal) {
        console.log('newVal', newVal, 'oldVal', oldVal);
      },

      userInfo(newVal, oldVal) {
        console.log('newVal', newVal, 'oldVal', oldVal);
      },

      tableData(newVal, oldVal) {
        console.log('newVal', newVal, 'oldVal', oldVal);
      }
    });    
  },
  onClickChangeStringData() {
    this.setData({
      motto: 'hello'
    });
  },
  onClickChangeObjData() {
    this.setData({
      userInfo: {
        name: 'helo'
      }
    });
  },
  onClickChangeArrayDataA() {
    const tableData = [];
    this.setData({
      tableData
    });
  }
})

参考

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/10/26 -【React源码解读】- 组件的实现

前言

react使用也有一段时间了,大家对这个框架褒奖有加,但是它究竟好在哪里呢?
让我们结合它的源码,探究一二!(当前源码为react16,读者要对react有一定的了解)

15397566862932

回到最初

根据react官网上的例子,快速构建react项目

npx create-react-app my-app

cd my-app

npm start

打开项目并跑起来以后,暂不关心项目结构及语法糖,看到App.js里,这是一个基本的react组件 我们console一下,看看有什么结果。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
        </header>
      </div>
    );
  }
}

export default App;

console.log(<App/>)

15397572879758

可以看到,<App/>组件其实是一个JS对象,并不是一个真实的dom。

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。有兴趣的同学可以去阮一峰老师的ES6入门详细了解一下

上面有我们很熟悉的props,ref,key,我们稍微修改一下console,看看有什么变化。

console.log(<App key={1} abc={2}><div>你好,这里是App组件</div></App>)

15397577334580

可以看到,props,key都发生了变化,值就是我们赋予的值,props中嵌套了children属性。可是为什么我们嵌入的是div,实际上却是一个对象呢?

打开源码

/node_modules/react

15397580720896

首先打开index.js

'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

可以知道目前用上的是./cjs/react.development.js,直接打开文件。
根据最初的代码,我们组件<App/>用到了React.Component。找到React暴露的接口:

15397617558881

接着找到Component: Component方法,

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

Component.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

Component.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

上面就是一些简单的构造函数,也可以看到,我们常用的setState是定义在原型上的2个方法。

至此,一个<App/>组件已经有一个大概的雏形:

15397595217487

到此为止了吗?这看了等于没看啊,究竟组件是怎么变成div的?render吗?
可是全局搜索,也没有一个function是render啊。

原来,我们的jsx语法会被babel编译的。

15397600724075

这下清楚了,还用到了React.createElement

createElement: createElementWithValidation,

通过createElementWithValidation,

function createElementWithValidation(type, props, children) {
······

  var element = createElement.apply(this, arguments);


  return element;
}

可以看到,return了一个element,这个element又是继承自createElement,接着往下找:

function createElement(type, config, children) {
  var propName = void 0;

  // Reserved names are extracted
  var props = {};

  var key = null;
  var ref = null;
  var self = null;
  var source = null;
······
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}

这里又返回了一个ReactElement方法,再顺着往下找:

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner
  };

······
  return element;
};

诶,这里好像返回的就是element对象,再看我们最初的<App/>的结构,是不是很像

15397606651880验证一下我们的探索究竟对不对,再每一个方法上我们都打上console,(注意,将App里的子元素全部删空,利于我们观察)

15397611759810

React.createElement 、 createElementWithValidation 、 createElement 、 ReactElement,通过这些方法,我们用class声明的React组件在变成真实dom之前都是ReactElement类型的js对象

createElementWithValidation:

  • 首先校验type是否是合法的

15397657382603

  • 校验了props是否符合设置的proptypes

15397667118968

  • 校验了子节点的key,确保每个数组中的元素都有唯一的key

15397667422295

createElement

  • type是你要创建的元素的类型,可以是html的div或者span,也可以是其他的react组件,注意大小写
  • config中包含了props、key、ref、self、source等

15397667913454

  • 向props加入children,如果是一个就放一个对象,如果是多个就放入一个数组。

15397668352993

  • 那如果type.defaultProps有默认的props时,并且对应的props里面的值是undefined,把默认值赋值到props中

15397668766904

  • 也会对key和ref进行校验

15397669476655

ReactElement

ReactElement就比较简单了,创建一个element对象,参数里的type、key、ref、props、等放进去,然后return了。最后调用Object.freeze使对象不可再改变。

组件的挂载

我们上面只是简单的探究了<App/>的结构和原理,那它究竟是怎么变成真实dom的呢

15397616989193

ReactDOM.render(<App />, document.getElementById('root'));

我们接着用babel编译一下:

15397619877496

原来ReactDOM.render调用的是render方法,一样,找暴露出来的接口。

var ReactDOM = {
······
  render: function (element, container, callback) {
    return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
  },
······
};

它返回的是一个legacyRenderSubtreeIntoContainer方法,这次我们直接打上console.log

15397629379495

这是打印出来的结果,

15397633591876

legacyRenderSubtreeIntoContainer
这个方法除主要做了两件事:

  • 清除dom容器元素的子元素
while (rootSibling = container.lastChild) {
      {
        if (!warned && rootSibling.nodeType === ELEMENT_NODE && rootSibling.hasAttribute(ROOT_ATTRIBUTE_NAME)) {
          warned = true;
        }
      }
      container.removeChild(rootSibling);
    }
  • 创建ReactRoot对象

15397648731115

源码暂时只读到了这里,关于React16.1~3的新功能,以及新的生命周期的使用和原理、Fiber究竟是什么,我们将在后续文章接着介绍。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/04/23 - Android屏幕适配方案分析

为什么要屏幕适配

Android开发过程中我们常用的尺寸单位有px、dp,还有一种sp一般是用于字体的大小。但是由于px是像素单位,比如我们通常说的手机分辨例如1920*1080都是px的单位。现在Android屏幕分辨率碎片化720x1280、1080x1920、2280x1080,这就造成例如187px会在各个分辨率的机型上都是显示一样大小的,那肯定不是我们想要的效果,所以用px单位我们是难以达到适配效果的,那么为什么用dp可以呢?

使用px单位从左到右依次为 480 * 800、1080 * 1920、1440 * 2560

使用dp单位从左到右依次为 480 * 800、1080 * 1920、1440 * 2560

屏幕总宽度依次为 320dp、415dp、411dp

那么什么是dp?

dp指的是设备独立像素,以dp为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能1dp=1px,而在分辨率较高的手机中,可能1dp=2px,这样的话,一个187dp高度的控件,在不同的手机中就能表现出差不多的大小了。

dp如何计算成px

android中的dp在渲染前会将dp转为px,计算公式:

  • px = density * dp;

  • density = dpi / 160;

  • px = dp * (dpi / 160);

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。

由于density不是固定不变的,所以每个分辨率不同的设备他们的density都肯定不相等,这样就会造成每个设备的宽/高对应的总dp都是不同的,假设480 * 800分辨率的density是1.51080 * 1920分辨率的density是2.61440 * 2560分辨率的density是3.5。那么它们对应的宽度总dp = (宽度px) / density,分别为320dp、415dp、411dp。可以看出单位为dp的时候三个设备之间的差距就不是很大了,但是这样肯定还是不能满足我们对屏幕适配的要求的。下面来看看Android常见的三种比较成熟的屏幕适配方案,并分析这几种方案的优劣。

屏幕适配方案

1.1 宽高限定符适配

设定一个基准的分辨率,也就是设计图对应的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。

比如我们的设计图 375 * 667为基准分辨率

  • 宽度为375,将任何分辨率的宽度整分为375份,取值为x1-x375

  • 高度为667,将任何分辨率的高度整分为667份,取值为y1-y667

那么对于1080*1920的分辨率的dimens文件来说,

  • x1=(1080/375)*1=2.88px

  • x2=(1080/375)*2=5.76px

  • y1=(1920/667)*1=2.87px

  • y2=(1920/667)*2=5.75px

当代码里面引用高度为y_187,在APP运行时会根据当前设备分辨率去找对应xml文件中对应的高度,我们就可以按照设计稿上的尺寸填写相对应的dimens引用了,这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率。

验证方案

简单通过计算验证下这种方案是否能达到适配的效果,例如设计图上有一个宽187dp的View。

480 * 800

  • 设计图占宽比: 187dp / 375dp = 0.498

  • 实际在480 * 800占宽比 = 187 * 1.28px / 480 = 0.498

1080 * 1920

  • 设计图占宽比: 187dp / 375dp = 0.498

  • 实际在1080 * 1920占宽比 = 187 * 2.88px / 1080 = 0.498

  • 计算高同理

但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920x1080的手机就一定要找到1920x1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。

1.2 smallestWidth适配

smallestWidth适配,或者叫sw限定符适配。指的是Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件。

可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的

├── src/main
│   ├── res
│   ├── ├──values
│   ├── ├──values-sw320dp
│   ├── ├──values-sw360dp
│   ├── ├──values-sw400dp
│   ├── ├──values-sw411dp
│   ├── ├──values-sw480dp
│   ├── ├──...
│   ├── ├──values-sw600dp
│   ├── ├──values-sw640dp

验证方案

1920 * 1080分辨率的手机,dpi为420,我们同样设置一个View为187dp宽

  • density = (dpi = 420) / 160 = 2.6
  • 屏幕总宽度dp = 1080 / density = 415
  • 找到文件夹values-sw410dp下的187dp = 204.45dp
  • 通过公式px = density * dp,计算出px = 531.57
  • 算出占屏幕宽度的比例,56.86 / 1080 = 0.492

1440 * 2560分辨率的手机,dpi为560,我们同样设置一个View为187dp宽

  • density = (dpi = 420) / 160 = 3.5
  • 屏幕总宽度dp = 1440 / density = 411
  • 找到文件夹values-sw410dp下的187dp = 204.45dp
  • 通过公式px = density * dp,计算出px = 715.57
  • 算出占屏幕宽度的比例,715.57 / 1440 = 0.496

因为识别的文件夹是values-sw410dp的文件夹,但是屏幕宽度为415dp和411dp,所以最后计算出的占比会有一点点误差,基本可以忽略不计,可以达到相对比较准确的适配效果

优点

  1. 非常稳定,极低概率出现意外
  2. 不会有任何性能的损耗
  3. 适配范围可自由控制,不会影响其他三方库
  4. 在插件的配合下,学习成本低

缺点

  1. 侵入性高,在所有地方都需要引用。
  2. 还是没有办法覆盖所有的机型分辨率,部分机型可能适配效果还是不佳
  3. 不能以高度为基准进行适配
  4. 生成很多文件,增大APP体积1~2M

1.3 今日头条适配方案

今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density

默认px = density * dp,也就是屏幕总宽度dp = 屏幕宽度px / density,这个时候我们假设所有设备上的屏幕总宽度dp会等于我们设计图375dp,那么可以得出一个公式:

density = 屏幕宽度px / 设计图宽度(375dp)

然后我们通过系统api,将density赋值给系统,抛弃掉系统默认计算density的计算公式。

这样可以很巧妙的实现屏幕适配,而且侵入性极低,甚至可以忽略不计。

验证方案

1920 * 1080分辨率的手机,我们同样设置一个View为187dp宽,设计图宽度为375dp

  • density = (屏幕宽度px = 1080) / 375 = 2.88
  • View宽度 = density * 187dp = 538.56
  • 算出占屏幕宽度的比例,57.6 / 1080 = 0.498

1440 * 2560分辨率的手机,我们同样设置一个View为187dp宽,设计图宽度为375dp

  • density = (屏幕宽度px = 1440) / 375 =3.84
  • View宽度 = density * 187dp = 718.08
  • 算出占屏幕宽度的比例,718.08 / 1440 = 0.498

可以看出,这种方案是完全没有误差的,而且侵入性极低,只需要修改系统的density。虽然修改系统的density属性会产生一小部分影响,但是基本都是很好解决的。

优点

  1. 使用成本非常低,操作非常简单
  2. 侵入性非常低
  3. 可适配三方库的控件和系统的控件

缺点

  1. 会全局影响APP的控件大小,例如一些第三方库控件,他们设计的时候可能设计图尺寸并不是像我们一样是375dp,这样就会导致控件大小变形等一些问题。

参考文章

*年你的屏幕适配方式该升级了!-SmallestWidth 限定符适配方案

Android 屏幕适配终结者

Android 目前最稳定和高效的UI适配方案

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/09/18 - 三大图表库:ECharts 、 BizCharts 和 G2,该如何选择?

最近阿里正式开源的BizCharts图表库基于React技术栈,各个图表项皆采用了组件的形式,贴近React的使用特点。同时BizCharts基于G2进行封装,Bizcharts也继承了G2相关特性。公司目前统一使用的是ECharts图表库,下文将对3种图表库进行分析比对。

BizCharts

文档地址:BizCharts

一、安装

通过 npm/yarn 引入

npm install bizcharts --save

yarn add bizcharts  --save

二、引用

成功安装完成之后,即可使用 import 或 require 进行引用。

例子:

import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts';
import chartConfig from './assets/js/chartConfig';

<div className="App">
    <Chart width={600} height={400} data={chartConfig.chartData} scale={chartConfig.cols}>
      <Axis name="genre" title={chartConfig.title}/>
      <Axis name="sold" title={chartConfig.title}/>
      <Legend position="top" dy={-20} />
      <Tooltip />
      <Geom type="interval" position="genre*sold" color="genre" />
    </Chart>
</div>

该示例中,图表的数据配置单独存入了其他js文件中,避免页面太过冗杂

module.exports = {
    chartData : [
	    { genre: 'Sports', sold: 275, income: 2300 },
	    { genre: 'Strategy', sold: 115, income: 667 },
	    { genre: 'Action', sold: 120, income: 982 },
	    { genre: 'Shooter', sold: 350, income: 5271 },
	    { genre: 'Other', sold: 150, income: 3710 }
    ],
    // 定义度量
    cols : {
	    sold: { alias: '销售量' }, // 数据字段别名映射
	    genre: { alias: '游戏种类' }
    },
    title : {
	    autoRotate: true, // 是否需要自动旋转,默认为 true
	    textStyle: {
	      fontSize: '12',
	      textAlign: 'center',
	      fill: '#999',
	      fontWeight: 'bold',
	      rotate: 30
	    }, // 坐标轴文本属性配置
	    position:'center', // 标题的位置,**新增**
    }
}

效果预览:
BizCharts示例

三、DataSet

BizCharts中可以通过dataset(数据处理模块)来对图标数据进行处理,该方法继承自G2,在下文中将对此进行详细分析。

快速跳转

G2

BizCharts基于G2进行开发,在研究BizCharts的过程中也一起对G2进行了实践。

一、安装

和BizCharts一样,可以通过 npm/yarn 引入

npm install @antv/g2 --save

yarn add @antv/g2 --save

与BizCharts不同,G2初始化数据并非以组件的形式引入,而是需要获取需要在某个DOM下初始化图表。获取该DOM的唯一属性id之后,通过chart()进行初始化。

二、引用

示例:

import React from 'react';
import G2 from '@antv/g2';
    class g2 extends React.Component {constructor(props) {
	    super(props);
	    this.state = {
	      data :[
	        { genre: 'Sports', sold: 275 },
	        { genre: 'Strategy', sold: 115 },
	        { genre: 'Action', sold: 120 },
	        { genre: 'Shooter', sold: 350 },
	        { genre: 'Other', sold: 150 }
	      ]
	    };
    }

    componentDidMount() {
	    const chart = new G2.Chart({
	      container: 'c1', // 指定图表容器 ID
	      width: 600, // 指定图表宽度
	      height: 300 // 指定图表高度
	    });
	    chart.source(this.state.data);
	    chart.interval().position('genre*sold').color('genre');
	    chart.render();
    }
    render() {
	    return (
	      <div id="c1" className="charts">
	      </div>
	    );
	}
}
export default g2;

效果图:
G2示例

三、DataSet

DataSet 主要有两方面的功能,解析数据(Connector)&加工数据(Transform)。

官方文档描述得比较详细,可以参考官网的分类:

源数据的解析,将csv, dsv,geojson 转成标准的JSON,查看Connector
加工数据,包括 filter,map,fold(补数据) 等操作,查看Transform
统计函数,汇总统计、百分比、封箱 等统计函数,查看 Transform
特殊数据处理,包括 地理数据、矩形树图、桑基图、文字云 的数据处理,查看 Transform

// step1 创建 dataset 指定状态量
const ds = new DataSet({
 state: {
    year: '2010'
 }
});

// step2 创建 DataView
const dv = ds.createView().source(data);

dv.transform({
 type: 'filter',
 callback(row) {
	return row.year === ds.state.year;
 }
});

// step3 引用 DataView
chart.source(dv);
// step4 更新状态量
ds.setState('year', '2012');

以下采用官网文档给出的示例进行分析

示例一

该表格里面的数据是美国各个州不同年龄段的人口数量,表格数据存放在类型为CVS的文件中
数据链接(该链接中为json类型的数据)

State 小于5岁 5至13岁 14至17岁 18至24岁 25至44岁 45至64岁 65岁及以上
WY 38253 60890 29314 53980 137338 147279 65614
DC 36352 50439 25225 75569 193557 140043 70648
VT 32635 62538 33757 61679 155419 188593 86649
... ... ... ... ... ... ... ...

初始化数据处理模块

import DataSet from '@antv/data-set';

const ds = new DataSet({
//state表示创建dataSet的状态量,可以不进行设置
 state: {
    currentState: 'WY'
    }
});

const dvForAll = ds
// 在 DataSet 实例下创建名为 populationByAge 的数据视图
    .createView('populationByAge') 
// source初始化图表数据,data可为http请求返回的数据结果
    .source(data, {
      type: 'csv', // 使用 CSV 类型的 Connector 装载 data,如果是json类型的数据,可以不进行设置,默认为json类型
});

/**
trnasform对数据进行加工处理,可通过type设置加工类型,具体参考上文api文档
加工过后数据格式为
[
{state:'WY',key:'小于5岁',value:38253},
{state:'WY',key:'5至13岁',value:60890},
]
*/ 
dvForAll.transform({
    type: 'fold',
    fields: [ '小于5岁','5至13岁','14至17岁','18至24岁','25至44岁','45至64岁','65岁及以上' ],
    key: 'age',
     value: 'population'
});

//其余transform操作
const dvForOneState = ds
    .createView('populationOfOneState')
    .source(dvForAll); // 从全量数据继承,写法也可以是.source('populationByAge')
 dvForOneState
     .transform({ // 过滤数据,筛选出state符合的地区数据
    type: 'filter',
    callback(row) {
      return row.state === ds.state.currentState;
    }
})
 .transform({
    type: 'percent',
    field: 'population',
    dimension: 'age',
    as: 'percent'
    });

使用G2绘图
G2-chart Api文档

import G2 from '@antv/g2';

// 初始化图表,id指定了图表要插入的dom,其他属性设置了图表所占的宽高
const c1 = new G2.Chart({
  id: 'c1',
  forceFit: true,
  height: 400,
});

// chart初始化加工过的数据dvForAll
c1.source(dvForAll);

// 配置图表图例
c1.legend({
  position: 'top',
});

// 设置坐标轴配置,该方法返回 chart 对象,以下代码表示将坐标轴属性为人口的数据,转换为M为单位的数据
c1.axis('population', {
  label: {
    formatter: val => {
      return val / 1000000 + 'M';
    }
  }
});

c1.intervalStack()
  .position('state*population')
  .color('age')
  .select(true, {
    mode: 'single',
    style: {
      stroke: 'red',
      strokeWidth: 5
    }
  });
  
//当tooltip发生变化的时候,触发事件,修改ds的state状态量,一旦状态量改变,就会触发图表的更新,所以c2饼图会触发改变
c1.on('tooltip:change', function(evt) {
  const items = evt.items || [];
  if (items[0]) {
  //修改的currentState为鼠标所触及的tooltip的地区
    ds.setState('currentState', items[0].title);
  }
});

// 绘制饼图
const c2 = new G2.Chart({
  id: 'c2',
  forceFit: true,
  height: 300,
  padding: 0,
});
c2.source(dvForOneState);
c2.coord('theta', {
  radius: 0.8 // 设置饼图的大小
});
c2.legend(false);
c2.intervalStack()
  .position('percent')
  .color('age')
  .label('age*percent',function(age, percent) {
    percent = (percent * 100).toFixed(2) + '%';
    return age + ' ' + percent;
  });

c1.render();
c2.render();

ECharts

ECharts是一个成熟的图表库, 使用方便、图表种类多、容易上手。文档资源也比较丰富,在此不做赘述。
ECharts文档

ECharts & BizCharts & G2 对比

对比BizCharts和G2两种图表库,BizCharts主要是进行了一层封装,使得图表可以以组件的形式进行调用,按需加载,使用起来更加方便。
简单对比一下三个图表库的区别:

初始化图表:
ECharts:

// 基于准备好的dom,初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));

BizCharts:

// 以组件的形式,组合调用
import { Chart, Geom, Axis, ... } from 'bizcharts';

<Chart width={600} height={400} data={data}>
	...
</Chart>

G2:

// 基于准备好的dom,配置之后进行初始化
const chart = new G2.Chart({
    container: 'c1', // 指定图表容器 ID
    width: 600, // 指定图表宽度
    height: 300 // 指定图表高度
});
chart.source(data);
chart.render();
 
<div id="c1" className="charts"></div>

配置:

ECharts:

// 集中在options中进行配置
myChart.setOption({
    title: {
        ...
    },
    tooltip: {},
    xAxis: {
        data: [...]
    },
    yAxis: {},
    series: [{
        ...
    }]
});

BizCharts:

// 根据组件需要,配置参数之后进行赋值
const cols = {...};
const data = {...};
<Chart width={600} height={400} data={data} sca`enter code here`le={cols}>
	...
</Chart>

G2:

chart.tooltip({
  triggerOn: '...'
  showTitle: {boolean}, // 是否展示 title,默认为 true
  crosshairs: {
    ...
    style: {
      ...
    }
  }
});

事件:

ECharts:事件 api文档

myChart.on('click', function (params) {
    console.log(params);
});

BizCharts:事件 api文档

<chart
  onEvent={e => {
    //do something
  }}
/>

G2: 事件 api文档

chart.on('mousedown', ev => {});

总结

对比以上3种图表,ECharts和BizCharts相对容易使用,尤其ECharts的配置非常清晰,BizCharts与其也有一定相似之处。BizCharts优势在于组件化的形式使得dom结构相对清晰,按需引用。G2比较适合需要大量图表交互时引用,其丰富的api处理交互逻辑相对更有优势。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/08/13 - 前端可视化构建工具Vue UI&阿里飞冰

Vue Cli3.0可视化构建工具——Vue UI

一、安装环境

安装了最新的Vue CLI。打开Terminal,输入:
npm install -g @vue/cli
or
yarn global add @vue/cli

使用-V来查看刚刚安装的版本:
vue -V 3.0.0-rc.10

更新vue-cli之后,之前的2.0版本的构建方式就不可用了
需要再下载

yarn global add @vue/cli-init

初始化Vue UI,在一个干净的目录下输入:

vue ui

该命令自动打开浏览器,显示如下界面

二、添加项目

添加新项目有两种方式

1.可视化添加

可视化添加

如果保存过自定义项目配置,开始创建时,会在第一个选项显示;配置的选项会同步到vue.config.js这个文件中

点击创建项目之后,可以保存此次配置,在以后创建项目时使用相同配置

跳过此次步骤,开始下载相关插件,此时命令行同步下载,可视化工具通过操控命令行来新建项目

2.命令行添加

vue create <project-name>

? Please pick a preset: (Use arrow keys)
❯ my-test (vue-router, vuex, sass, babel, eslint)
  default (babel, eslint)
  Manually select features
  
? Check the features needed for your project: (Press <space> to select, <a> to t
oggle all, <i> to invert selection)
❯◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
 
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-proce
ssors, Linter
? Use history mode for router? (Requires proper server setup for index fallback
in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
by default): SCSS/SASS
? Pick a linter / formatter config: Prettier
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i
> to invert selection)
❯◉ Lint on save
 ◯ Lint and fix on commit
 ? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In packag
e.json
? Save this as a preset for future projects? (y/N) n

按下空格键进行选取

Vue CLI v3.0.0-rc.10
✨  Creating project in /Users/zjy/ttt.
🗃  Initializing git repository...
⚙  Installing CLI plugins. This might take a while...

yarn install v1.0.1
info No lockfile found.
[1/4] 🔍  Resolving packages...
⠐ @babel/[email protected]

开始构建项目

三、工具分析

1.插件

安装完成之后,可以查看项目下安装的插件,可以添加其他插件

2.依赖

查看项目依赖的资源,可以直接查看相关官网或源码

3.配置

可对项目进行配置,配置的选项会在vue.config.js中

4.任务

可以本地调试,打包,

对项目进行性能分析

四、Vue Cli3项目结构分析

少了很多文件夹,目录结构更加清晰,vue-cli2.0中的build,config统一到了vue.config.js中
移除了static文件夹,添加了public
Src中添加了views文件夹,用来存放视图组件,components中存放公共组件

1.vue.config.js配置

参考文档:配置文档

module.exports = {
 baseUrl: '/',
 outputDir: 'dist',
 lintOnSave: true,
 compiler: false,
 // 调整内部的 webpack 配置。
 // 查阅 https://github.com/vuejs/vue-doc-zh-cn/vue-cli/webpack.md
 chainWebpack: () => {},
 configureWebpack: () => {},
 // 配置 webpack-dev-server 行为。
 devServer: {
  open: process.platform === 'darwin',
  host: '0.0.0.0',
  port: 8080,
  https: false,
  hotOnly: false,
  // 查阅 https://github.com/vuejs/vue-doc-zh-cn/vue-cli/cli-service.md#配置代理
  proxy: null, // string | Object
  before: app => {}
 }
 ....
}

阿里飞冰

飞冰:官网

进如官网下载GUI工具,选择模板创建项目,项目页面可选择区块添加组件
enter image description here
enter image description here

选择模板,新建项目

enter image description here
enter image description here
enter image description here
创建好的项目目录
enter image description here

本地调试:

enter image description here

页面中添加组件

点击页面列表右侧对应的+号,即可选择对应框架下的物料源,将在该页面目录下生成一侧Component文件夹,存放下载的组件资源,配置路之后,即可生效。

项目目录

enter image description here

导入已有项目

项目适配设置:文档

已有项目接入 Iceworks

将已有项目接入到 Icewokrs 中,需要增加对应信息的项目描述

  1. 描述项目可被 Iceworks 识别
    package.json 文件 keywords 字段增加 ice-scaffold 表示这是一个适配 ice 规范的模板项目。
{
  "name": "my-project",
  "keywords": ["ice-scaffold"],
  // ...
}
  1. 描述项目使用的框架语言

package.json 文件增加 scaffoldConfig 字段对象,示例如下:

{
  // ...
  "scaffoldConfig": {
    "type": "react",
    "name": "ice-design-pro",
    "title": "ICE Design Pro",
    "screenshot": "https://img.alicdn.com/tfs/TB1_bulmpOWBuNjy0FiXXXFxVXa-1920-1080.png"
  }
}

其中 scaffoldConfig.type 字段描述当前项目所使用的框架名 react vue angular 等,此字段用于与物料源相映射。

  1. package.json 存在可执行命令 npm run start npm run build

这两个命令用于 启动调试服务 构建项目 功能使用,你可以使用自己定义的命令行工具。

{
  "scripts": {
    "start": "custom-cli start",
    "build": "custom-cli build"
  }
}

结语

Vue Cli3.0针对vue项目进行可视化构建,阿里飞冰针对了主流的三大框架,但对react物料支持最多,同时也支持自定义物料进行构建。但该产品目前处于初期阶段,不足之处较多,例如项目下载依赖失败率较大,导入的项目页面目录必须为pages,编译之后的文件目录必须为build,否则软件无法识别并进行展示。随着软件进一步优化,这些问题应该会逐步解决。对于有自己固定框架模板的团队来说,可以考虑使用这样一套成熟的工具来快速搭建项目。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/11/27 - canvas中普通动效与粒子动效的实现

canvas用于在网页上绘制图像、动画,可以将其理解为画布,在这个画布上构建想要的效果。

canvas可以绘制动态效果,除了常用的规则动画之外,还可以采用粒子的概念来实现较复杂的动效,本文分别采用普通动效与粒子特效实现了一个简单的时钟。

普通时钟

普通动效即利用canvas的api,实现有规则的图案、动画。

效果

image

该效果实现比较简单,主要分析一下刻度与指针角度偏移的实现。

绘制刻度

此例为小时刻度的绘制:表盘上共有12个小时,Math.PI为180°,每小时占据30°。
.save()表示保存canvas当前环境的状态,在此基础上进行绘制。绘制完成之后,返回之前保存过的路径状态和属性。

分钟刻度同理,改变角度与样式即可。

  // 小时时间刻度
  offscreenCanvasCtx.save();
  for (var i = 0; i < 12; i++) {
    offscreenCanvasCtx.beginPath();
    // 刻度颜色
    offscreenCanvasCtx.strokeStyle = '#fff';
    // 刻度宽度
    offscreenCanvasCtx.lineWidth = 3;
    // 每小时占据30°
    offscreenCanvasCtx.rotate(Math.PI / 6);
    // 开始绘制的位置
    offscreenCanvasCtx.lineTo(140, 0)
    // 结束绘制的位置;
    offscreenCanvasCtx.lineTo(120, 0);
    // 绘制路径
    offscreenCanvasCtx.stroke();
  }
  offscreenCanvasCtx.restore();

指针指向

以秒针为例:获取当前时间的秒数,并计算对应的偏移角度

  var now = new Date(),
    sec = now.getSeconds(),
    min = now.getMinutes(),
    hr = now.getHours();
  hr = hr > 12 ? hr - 12 : hr;
  
  //秒针
  offscreenCanvasCtx.save();
  offscreenCanvasCtx.rotate(sec * (Math.PI / 30));
  ......
  offscreenCanvasCtx.stroke();

粒子动效

canvas可以用来绘制复杂,不规则的动画。粒子特效可以用来实现复杂、随机的动态效果。

粒子,指图像数据imageData中的每一个像素点,获取到每个像素点之后,添加属性或事件对区域内的粒子进行交互,达到动态效果。

效果

image

粒子获取

以下图的图片转化为例,该效果是先在canvas上渲染图片,然后获取文字所在区域的每个像素点。

  let image = new Image();
  image.src='../image/logo.png';
  let pixels=[]; //存储像素数据
  let imageData;
  image.width = 300;
  image.height = 300
  // 渲染图片,并获取该区域内像素信息
  image.onload=function(){
    ctx.drawImage(image,(canvas.width-image.width)/2,(canvas.height-image.height)/2,image.width,image.height);
    imageData=ctx.getImageData((canvas.width-image.width)/2,(canvas.height-image.height)/2,image.width,image.height); //获取图表像素信息
 //绘制图像
  };

像素信息

图片的大小为300*300,共有90000个像素,每个像素占4位,存放rgba数据。

image

粒子绘制

  function getPixels(){
    var pos=0;
    var data=imageData.data; //RGBA的一维数组数据
    //源图像的高度和宽度为300px
    for(var i=1;i<=image.width;i++){
      for(var j=1;j<=image.height;j++){
        pos=[(i-1)*image.width+(j-1)]*4; //取得像素位置
        if(data[pos]>=0){
          var pixel={
            x:(canvas.width-image.width)/2+j+Math.random()*20, //重新设置每个像素的位置信息
            y:(canvas.height-image.height)/2+i+Math.random()*20, //重新设置每个像素的位置信息
            fillStyle:'rgba('+data[pos]+','+(data[pos+1])+','+(data[pos+2])+','+(data[pos+3])+')'
          }
          pixels.push(pixel);
        }
      }
    }
  }
  function drawPixels() {
    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext("2d");
    ctx.clearRect(0,0,canvas.width,canvas.height);
    var len = pixels.length, curr_pixel = null;
    for (var i = 0; i < len; i++) {
      curr_pixel = pixels[i];
      ctx.fillStyle = curr_pixel.fillStyle;
      ctx.fillRect(curr_pixel.x, curr_pixel.y, 1, 1);
    }
  }

粒子时钟

渲染文字时钟

  function time() {
    ctx.clearRect(0,0,canvas.width,canvas.height)
    ctx.font = "150px 黑体";
    ctx.textBaseline='top';
    ctx.fillStyle = "rgba(245,245,245,0.2)";
    ctx.fillText(new Date().format('hh:mm:ss'),(canvas.width-textWidth)/2,(canvas.height-textHeight)/2,textWidth,textHeight);
  }

效果

image

获取粒子

文字转换粒子概念同上,获取选定区域的像素,根据筛选条件进行选择并存入数组。经过遍历后重新绘制。

  function getPixels(){
    let imgData = ctx.getImageData((canvas.width-textWidth)/2,(canvas.height-textHeight)/2,textWidth,textHeight);
    let data = imgData.data
    pixelsArr = []
    for(let i=1;i<=textHeight;i++){
      for(let j=1;j<=textWidth;j++){
        pos=[(i-1)*textWidth+(j-1)]*4; //取得像素位置
        if(data[pos]>=0){
          var pixel={
            x:j+Math.random()*20, //重新设置每个像素的位置信息
            y:i+Math.random()*20, //重新设置每个像素的位置信息
            fillStyle:'rgba('+data[pos]+','+(data[pos+1])+','+(data[pos+2])+','+(data[pos+3])+')'
          };
          pixelsArr.push(pixel);
        }
      }
    }
  }

imgData保存了所选区域内的像素信息,每个像素点占据4位,保存了RGBA四位信息。筛选每个像素的第四位,这段代码中将所有透明度不为0的像素都保存到了数组pixelsArr中。

xy记载了该粒子的位置信息,为了产生效果图中的运动效果,给每个粒子添加了0-20个像素的偏移位置,每次重绘时,偏移位置随机生成,产生运动效果。

粒子重绘

获取粒子之后,需要清除画布中原有的文字,将获取到的粒子重新绘制到画布上去。

  function drawPixels() {
    // 清除画布内容,进行重绘
    ctx.clearRect(0,0,canvas.width,canvas.height);
    for (let i in pixelsArr) {
      ctx.fillStyle = pixelsArr[i].fillStyle;
      let r = Math.random()*4
      ctx.fillRect(pixelsArr[i].x, pixelsArr[i].y, r, r);
    }
  }

粒子重绘时的样式为筛选像素时原本的颜色与透明度,并且每个在画布上绘制每个粒子时,定义大小参数r,r取值为0-4中随机的数字。最终生成的粒子大小随机。

实时刷新

获取粒子并成功重绘之后,需要页面实时刷新时间。这里采用window.requestAnimationFrame(callback)方法。

  function time() {
    ......
    getPixels(); //获取粒子
    drawPixels(); // 重绘粒子
    requestAnimationFrame(time);
  }

window.requestAnimationFrame(callback) 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。

该方法不需要设置时间间隔,调用频率采用系统时间间隔(1s)。

文档解释戳这里

效果

image

总结

本文主要通过两种不同的方式实现了时钟的动态效果,其中粒子时钟具有更多的可操作性。在以后的canvas系列中会针对粒子系统实现更多的动态效果。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/09/07 -【React 实战教程】从0到1 构建 github star管理工具

前言

在日常使用github中,除了利用git进行项目版本控制之外,最多的用处就是游览各式的项目,在看到一些有趣或者有用的项目之后,我们通常就会顺手star,目的是日后再看。但是当我们star了许多项目之后,回过头想找一个的项目就会发现,很难在短时间内找到它,官方也并没有提供很好的管理我们的star项目的功能,因此在市面上也出现了一些对star进行管理的工具,比如说 astralapp,Star Order等等,其实github的接口api都是开放的,我们完全可以自己构建一个属于自己的项目管理工具。公司的前端技术栈是React,而笔者之前使用的是Vue,因此正好想利用github的open api 自己构建个react的github star管理项目来加深react的使用。而大体功能我们就模仿astralapp。

github open api

官方文档有v3和v4,2个版本,v3是Restful,v4是GraphQL,在这里我们使用的是v3版

v3

使用的是restful 协议

服务器地址

https://api.github.com

在无token情况下使用github的api,每分钟限制是60次请求,考虑到想完整的使用github的api,因此选择构建一个web application,授权OAuth应用程序的流程可以参照官方文档。在这里,就简单的说一下这个流程。

授权OAuth2.0 的流程

github OAuth的授权模式为授权码模式,对OAuth不了解的同学可以具体看阮一峰老师的理解OAuth 2.0

要做的流程主要分为3步

  • 获取code
  • 通过code获取token
  • 在请求时携带token

获取code

首先需要跳转到这个地址

https://github.com/login/oauth/authorize

需要有以下参数

参数名 类型 描述
client_id string 必选 client_id是在注册github application后可以看到
redirect_uri string 可选 授权成功后跳转的地址,这里的这个跳转地址也可以在后台进行设置
scope string 可选 权限范围,具体的权限可以参照,具体传值格式以及需要哪些范围可以参照官方文档
allow_signup string 可选 是否允许为注册的用户注册,默认为true

跳转至目标地址后,会有个授权界面,当用户点击授权之后会重新跳转到我们自己设定的redirect_uri并携带一个code,就像这样

<redirect_url>?code=1928596028123

通过code获取token

在获取code之后,请求用于获取token

POST https://github.com/login/oauth/access_token
参数名 类型 描述
client_id string 必填 client_id是在注册github application后可以看到 必填
client_secret string 必填 该参数是在同client_id一样,也是在注册application后可以看到
code string 必填 通过第一步获取
redirect_uri string 可选
state string 可选 随机数

token的默认返回格式为字符串

access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&token_type=bearer

可以通过更改头部接受格式进行返回格式变更

Accept: application/json
{"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"}

Accept: application/xml
<OAuth>
  <token_type>bearer</token_type>
  <scope>repo,gist</scope>
  <access_token>e72e16c7e42f292c6912e7710c838347ae178b4a</access_token>
</OAuth>

在请求时携带token

携带token有2种方式 一种是永远跟在url的后面作为params

GET https://api.github.com/user?access_token=...

另外一种是放在请求头中

Authorization: token 获取到的token

接口请求

在项目里运用到的github 接口 目前有三个

  • 用户信息接口
  • 当前用户star的项目
  • 获取项目Readme接口

需要注意的是这些接口由于服务端实现了CORS,因此是不存在跨域问题,但是,考虑到本身这个项目的功能情况,之后我们会自己建立服务端进行请求。

用户信息接口

GET https://api.github.com/user

当前用户star的项目

GET https://api.github.com/user/starred

可选的请求参数

参数名 类型 描述
page string
sort string 排序条件 有2种created updated,默认为created
direction string 升序还是倒序 asc desc,默认为``desc

获取仓库Readme接口

GET https://api.github.com/repos/:username/:repo/readme

针对一些文件接口,github提供了头部类型的选择,可以返回不同的文件类型,比如raw等,具体可以参考官方文档中的Custom media types

在这里我们需要的是html格式,因此 我们在头部当中设置

"Accept": "application/vnd.github.v3.html"

这样ReadMe返回的是html代码,我们根据html代码直接显示即可。

目录结构

├── config  // webpack相关文件
├── public  // 公用文件
├── scripts // 脚本文件 build,start,test文件都在里面
├── src
    ├── assets  // 自己放置的资源文件
    ├── components  // 公用组件
    ├── pages   // 页面文件
    ├── utils   // 公用方法文
    App.css
    App.scss
    App.jsx
    index.css
    index.js
    logo.svg    
    reset.css   // 重置样式
    variable.css
    variable.scss   // 公用变量文件
├── package.json
├── .editorconfig   // 编辑器配置
├── .gitignore // git 忽略文件

构建

create-react-app

构建React项目首先第一个想到的是用脚手架工具,Vue当中有Vue-cli,自带webpack,vue-router,vuex,而React对应的是create-react-app

当我们初始化完成项目之后,我们会发现webpack的配置文件找不到,我们需要运行以下命令将wepack配置显示出来

npm run eject

scss

这个方法参照的是create-react-app中的说明adding-a-css-preprocessor-sass-less-etc

npm install --save node-sass-chokidar

还需要装 webpack watch

   "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 start",
     "build": "react-scripts build",
     "test": "react-scripts test --env=jsdom",
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 start",
-    "build": "react-scripts build",
+    "start-js": "react-scripts start",
+    "start": "npm-run-all -p watch-css start-js",
+    "build-js": "react-scripts build",
+    "build": "npm-run-all build-css build-js",
     "test": "react-scripts test --env=jsdom",
     "eject": "react-scripts eject"
   }

安装好这些包之后,新建一个scss文件会自动生成css文件,我们在引用时直接引用css文件即可。

另外一种方法是参照medium的一篇文章CSS Modules & Sass in Create React App

npm i sass-loader node-sass --save or yarn add sass-loader node-sass

随后更改webpack.config.dev.js文件的配置

需要注意的是loadersuse代替,随后在file-loader增加scss文件格式的匹配

跨域问题

跨域问题可以使用webpack自带的proxy进行配置,或者通过ngix进行代理

如果是webpack配置需要在package.json当中进行配置

"proxy": {
    "/user": {
      "target": "https://api.github.com",
      "changeOrigin": true
    },
    "/user/star": {
      "target": "https://api.github.com",
      "changeOrigin": true
    },
    "/login": {
      "target": "https://github.com",
      "changeOrigin": true
    }
}

svg

目前使用了svg-react-loader

 /* eslint-disable */
 // 主要是这里 eslint会报错
import Refresh from '-!svg-react-loader!../../assets/img/refresh.svg';
/* eslint-enable */

class StarFilter extends Component {
  constructor(props) {
    super(props);
require.resolve('svg-react-loader');
    this.state = {
    };
  }

  componentDidMount() {
  }

  render() {
    return (
      <div className="star-filter">
        <div className="title-container">
          <h3 class="title-gray-dark">STARS</h3>
          <!--这样就可以使用了-->
          <Refresh className="icon-refresh text-grey" />
        </div>
      </div>
    );
  }
}

export default StarFilter;

颜色

改变颜色要使用fill属性

.icon-refresh {
  width: 20px;
  height: 20px;
  fill: #606f7b;
}

注意

  • 图片中自带的p-id元素在react中会自动变成pId,随后会被react输出警告日志,建议把pid 属性删除,这个属性不影响显示
  • 我们经常在iconfont上下载svg图片,但是有些svg图片内部默认设置了颜色,如果要让我们样式当中的颜色起作用,建议在下载完svg后,检查下默认的fill属性是否存在,如果有请先删除

引用本地图片

import NoSelectedImg from '../../assets/img/not-selected.svg';

class ResInfo extends Component {
 // ..此处省略
  render() {
    <img
      alt="no-selected"
      src={NoSelectedImg}
      className="img-no-selected"
    />

  }
}

export default ResInfo;

第二种方法是用require

<img src={require('../../assets/img/status-spinner.svg')} alt="fetch" width="16" height="16"/>

需要注意的是如果是要在img标签中使用svg图片,还需要在webpack当中进行配置,在webpack.config.dev.jswebpack.config.prod.js当中大致在133行左右的urlLoader增加svg文件的匹配

{
    test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.svg$/],
    loader: require.resolve('url-loader'),
    options: {
    limit: 10000,
    name: 'static/media/[name].[hash:8].[ext]',
}

路由

使用react-router-dom进行路由的管理,和Vue-router一样,需要对要用到的路由级别组件进行注册。直接将组件写在router内部即可。

render() {
    return (
      <div className="App">
        <BrowserRouter basename="/">
          <div>
            <Route exact path="/" component={Auth} />
            <Route path="/auth" component={Auth} />
            <Route path="/star" component={Star} />
          </div>
        </BrowserRouter>
      </div>
    )
  }

Router中有BrowserRouter,HashRouter等,而这2种类似于Vue-router中的historyhash模式,需要注意的是,在我们这个项目当中必须使用BrowserRouter,如果使用HashRouter在github 授权重定向回我们页面时会出现问题。会出现code不在尾部的问题。

import { Redirect } from 'react-router-dom'

class Auth extends Component {

 //省略...

  render() {
    // 如果isTokenError为true直接跳转至首页
    if (this.state.isTokenError) {
      return (
        <Redirect to="/"/>
      )
    }
    // 如果hasCode有值则跳转至star
    if (this.state.hasCode) {
      return (
        <Redirect to="/star" />
      )
    }
    return (
      <div className="Auth">
        <Button className="btn-auth" onClick={this.onClickAuth}>
          点击授权
        </Button>
      </div>
    )
  }
}

export default Auth

同时它也支持api的跳转,当组件放置在router中,组件props内置会有一个histroy属性,即this.props.history,使用它就可以实现push,replace等跳转了功能了。

  /**
   * 返回首页
   */
  go2home() {
    this.props.history.replace('/auth');
  }

  /**
   * 前往star界面
   */
  go2star() {
    this.props.history.push('/star');
  } 

总结

我们大致了解了项目的概况,在开发项目的过程当中,官方文档是十分重要的,包括githubApi的使用,SCSS的使用,跨域问题等等,都能从官方文档当中得到解答。同时github提供的api也是十分丰富的,基本囊括了所有github的基础功能,在上述文章当中只是展示了它极少的功能,更多的功能大家可以自己来发掘。在接下来的文章当中,会为大家带来服务端开发篇,使用node进行服务端,数据库的一些操作。项目地址可以点我,项目还在初期开发中,就不要来star了=.=。

参考

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/06/08 - 10分钟了解JS堆、栈以及事件循环的概念

前言

其实一开始对栈、堆的概念特别模糊,只知道好像跟内存有关,又好像事件循环也沾一点边。面试薄荷的时候,面试官正好也问到了这个问题,当时只能大方的承认不会。痛定思痛,回去好好的研究一番。
我们将从JS的内存机制以及事件机制大量的🌰(例子)来了解栈、堆究竟是个什么玩意。概念比较多,不用死读,所有的🌰心里想一遍,浏览器console看一遍就很清楚了。
let's go

JS内存机制

因为JavaScript具有自动垃圾回收机制,所以对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。特别是很多不专业的朋友在进入到前端之后,会对内存空间的认知比较模糊。

在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stack)与堆内存(heap)

栈内存一般储存基础数据类型

 Number String Null Undefined Boolean 
 (es6新引入了一种数据类型,Symbol)

最简单的🌰

var a = 1 

我们定义一个变量a,系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问。

数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。

堆内存一般储存引用数据类型

堆内存的🌰

var b = { xi : 20 }

与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。看一下下面的图,加深理解。

比较


wechatimg104

var a1 = 0;   // 栈 
var a2 = 'this is string'; // 栈
var a3 = null; // 栈

var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

测试

var a = 20;
var b = a;
b = 30;
console.log(a)
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
console.log(m.a)

同学们自己在console里打一遍,再结合下面的图例,就很好理解了

wechatimg106

15282536739797

内存机制我们了解了,又引出一个新的问题,栈里只能存基础数据类型吗,我们经常用的function存在哪里呢?

浏览器的事件机制

一个经常被搬上面试题的🌰

console.log(1)
let promise = new Promise(function(resolve,reject){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(100)
})
setTimeout(function(){
    console.log(4);
})
console.log(2)

上面这个demo的结果值是
1
3
2
100
4

wechatimg105

对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可能会调一些Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走**(注意:栈里的代码是先进后出)**,走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。

  • 1.所有同步任务都在主线程上执行,形成一个执行栈
  • 2.主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行
  • 4.主线程从任务队列中读取事件,这个过程是循环不断的

概念又臭又长,没关系,我们先粗略的扫一眼,接着往下看。

举一个🌰说明栈的执行方式

var a = "aa";
function one(){
    let a = 1;
    two();
    function two(){
        let b = 2;
        three();
        function three(){
            console.log(b)
        }
    }
}
console.log(a);
one();

demo的结果是
aa
2

图解

wechatimg107

执行栈里面最先放的是全局作用域(代码执行有一个全局文本的环境),然后再放one, one执行再把two放进来,two执行再把three放进来,一层叠一层。

最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。

那队列又是怎么一回事呢?

再举一个🌰

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3);
})
setTimeout(function(){
    console.log(4);
})
console.log(5);

首先执行了栈里的代码,1 2 5。
前面说到的settimeout会被放在队列里,当栈执行完了之后,从队列里添加到栈里执行(此时是依次执行),得到 3 4

再再举一个🌰

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
})
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
})
console.log(5)

同样,先执行栈里的同步代码 1 2 5.
再同样,最外层的settimeout会放在队列里,当栈里面执行完成以后,放在栈中执行,3 4。
而嵌套的2个settimeout,会放在一个新的队列中,去执行 6 7.

再再再看一个🌰

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
},400)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},100)
console.log(5)

如上:这里的顺序是1,2,5,4,7,3,6。也就是只要两个set时间不一样的时候 ,就set时间短的先走完,包括set里面的回调函数,再走set时间慢的。(因为只有当时间到了的时候,才会把set放到队列里面去)

setTimeout(function(){
    console.log('setTimeout')
},0)
for(var i = 0;i<10;i++){
    console.log(i)
}

这个demo的结果是
0 1 2 3 4 5 6 7 8 9
setTimeout

所以,得出结论,永远都是栈里的代码先行执行,再从队列中依次读事件,加入栈中执行

stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行,这个时候栈中又出现了事件,这个事件又去调用了WebAPIs里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是stack)执行完后又将从任务队列中依次读取事件,这个过程是循环不断的。

再回到我们的第一个🌰

console.log(1)
let promise = new Promise(function(resolve,reject){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(100)
})
setTimeout(function(){
    console.log(4);
})
console.log(2)

上面这个demo的结果值是
1
3
2
100
4

  • 为什么setTimeout要在Promise.then之后执行呢?
  • 为什么new Promise又在console.log(2)之前执行呢?

setTimeout是宏任务,而Promise.then是微任务
这里的new Promise()是同步的,所以是立即执行的。

这就要引入一个新的话题宏任务微任务(面试也会经常提及到)

宏任务和微任务

参考 Tasks, microtasks, queues and schedules(https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly)

概念:微任务和宏任务都是属于队列,而不是放在栈中

一个新的🌰

console.log('1');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('2');

1
2
promise1
promise2
setTimeout

宏任务(task)

浏览器为了能够使得JS内部宏任务与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->…)
鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析HTMl。但是,setTimeout不一样setTimeout的作用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打印‘setTimeout’在‘promise1 , promise2’之后。因为打印‘promise1 , promise2’是第一个宏任务里面的事情,而‘setTimeout’是另一个新的独立的任务里面打印的。

微任务 (Microtasks)

微任务通常来说就是需要在当前 task 执行结束后立即执行的任务
比如对一系列动作做出反馈,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调

一旦一个pormise有了结果,或者早已有了结果(有了结果是指这个promise到了fulfilled或rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即使这个promise早已有了结果。所以对一个已经有了结果的**promise调用.then()**会立即产生一个微任务。这就是为什么‘promise1’,'promise2’会打印在‘script end’之后,因为所有微任务执行的时候,当前执行栈的代码必须已经执行完毕。‘promise1’,'promise2’会打印在‘setTimeout’之前是因为所有微任务总会在下一个宏任务之前全部执行完毕。

还是🌰

<div class="outer">
  <div class="inner"></div>
</div>
//  elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');


//监听element属性变化
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// 
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

click
promise
mutate
click
promise
mutate
(2) timeout

很好的解释了,setTimeout会在微任务(Promise.then、MutationObserver.observe)执行完成之后,加入一个新的宏任务中

多看一些🌰

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
})
setTimeout(function(){
    console.log(3)
    Promise.resolve(1).then(function(){
        console.log('promise2')
    })
})
setTimeout(function(){
    console.log(4)
    Promise.resolve(1).then(function(){
        console.log('promise3')
    })
})

1
2
promise1
3
promise2
4
promise3

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')

        setTimeout(function(){
            console.log(3)
            Promise.resolve(1).then(function(){
                console.log('promise2')
            })
        })

    })
})

1
2
promise1
3
promise2

总结回顾

  • 栈:

    • 存储基础数据类型
    • 按值访问
    • 存储的值大小固定
    • 由系统自动分配内存空间
    • 空间小,运行效率高
    • 先进后出,后进先出
    • 栈中的DOM,ajax,setTimeout会依次进入到队列中,当栈中代码执行完毕后,再将队列中的事件放到执行栈中依次执行。
    • 微任务和宏任务
  • 堆:

    • 存储引用数据类型
    • 按引用访问
    • 存储的值大小不定,可动态调整
    • 主要用来存放对象
    • 空间大,但是运行效率相对较低
    • 无序存储,可根据引用直接获取

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/12/10 - redux 源码分析

redux 源码分析

背景

在之前的文章Redux从入门到实践当中对redux的使用进行了说明,这次就来看下它的源码,从而进一步的熟悉它。

构建

相关git地址

git clone https://github.com/reduxjs/redux.git

构建文档是CONTRBUTING.md

package.json

"main": "lib/redux.js",
// ...
"scripts": {
    "clean": "rimraf lib dist es coverage",
    "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "lint": "eslint src test",
    "pretest": "npm run build",
    "test": "jest",
    "test:watch": "npm test -- --watch",
    "test:cov": "npm test -- --coverage",
    "build": "rollup -c",
    "prepare": "npm run clean && npm run format:check && npm run lint && npm test",
    "examples:lint": "eslint examples",
    "examples:test": "cross-env CI=true babel-node examples/testAll.js"
  }

package.json当中可以看到redux的入口文件是lib/redux.js,这个文件是通过打包出来的。那我们看下打包配置文件rollup.config

{
    input: 'src/index.js',
    output: { file: 'lib/redux.js', format: 'cjs', indent: false },
    external: [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {})
    ],
    plugins: [babel()]
  },
  // ...省略

可以看到入口文件应该是src/index.js

我们来看下src/index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

/*
 * This is a dummy function to check if the function name has been altered by minification.
 * If the function has been minified and NODE_ENV !== 'production', warn the user.
 */
// 是否压缩代码,如果运行环境在非生成环境但是代码被压缩了,警告用户
function isCrushed() {}

// 判断环境是否是生成环境,如果是生成环境使用此代码就给出警告提示
if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === "production". ' +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

src/index.js主要是将方法暴露出来,给使用者使用

  • createStore 用于创建store
  • combineReducers 用于组合成rootReducers,因为在外部初始化store时,只能传入一个reducers
  • bindActionCreators 组装了dispatch方法
  • applyMiddleware 合并多个中间件
  • compose 将中间件(middleware)和增强器(enhancer)合并传入到createStore

combineReducers

src/combineReducers.js

export default function combineReducers(reducers) {
  // 遍历出reducers的对象名称
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍历reducers名称
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      // 如果reducer对应的值是 undefined 输出警告日志
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    // 当这个reducer是函数 则加入到finalReducers对象中
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 读取出过滤后的reducers
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  // 开发环境将unexpectedKeyCache设置为空对象
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    // 检查各个reducers是否考虑过defualt的情况,不能返回undefined
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // combineRducers返回的是一个方法,dispatch最后执行的方法
  return function combination(state = {}, action) {
    // 如果reducer检查出有问题就会抛出异常
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    // 开发者环境下
    if (process.env.NODE_ENV !== 'production') {
      // 对过滤后的reducers和初始化的state进行检查
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      // 如果有问题就会输出
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    // 遍历过滤后的reducers
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      // 根据key取出对应reducer
      const reducer = finalReducers[key]
      // 根据key将state对应的值取出
      const previousStateForKey = state[key]
      // 执行我们reducer的方法,nextStateForKey就是根据actionType返回的state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 检查nextStateForKey是否是undefined
      if (typeof nextStateForKey === 'undefined') {
        // 如果undefined就报错
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 将合并后的state赋值到nextState当中
      nextState[key] = nextStateForKey
      // 如果state的值改变过 则hasChanged置为true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果改变过 则返回新的state不然则是原有的state({})
    // 如果state不传值就是 空的对象{}
    return hasChanged ? nextState : state
  }
}

combineReducers方法会先对传入的reducers进行校验,reducer的类型只能是function,最后返回的是个方法,这个方法很关键,因为在disptach时,最后执行的就是这个方法。这个方法有2个参数stateaction,方法内会根据传入的action返回state,最后会比较新旧的state,如果不相等,则会返回新的state,如果相等会返回新的state。

那么如果我们直接对store的state进行操作而不是通过dispatch会发生呢,比如说我们这样

const state = store.getState();
state.name = 'baifann';

我们看一下combineReducers中的getUnexpectedStateShapeWarningMessage这个方法,它会检查store中初始化的state的key有没有在各个子reducer当中,如果没有就会报错。

/**
 * @inputState 初始化的state
 * @reducers 已经过过滤的reducers 
 * @action 随着combinRecuers传入的action
 * @unexpectedKeyCache 开发者环境是一个空的对象,生成环境是undefined
 */
/**
 * 检查合法的reducers是否存在
 * 
 * 
 */
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // 将过滤的reducers的名取出
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    // 如果这个action的type是预制的ActionTypes.INIT
    // argumentName就是preloadedState argument passed to createStore
    // 不然是previous state received by the reducer
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'
  // 如果过滤后的reducer长度为0
  // 则返回字符串告知没有一个合法的reducer(reducer必须是function类型)
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }
  // 判断输入的state是否是obj对象
  // 如果不是 则返回字符串告知inputState不合法
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  // 传入的state进行遍历
  // 如果state的对象名不包含在reducer中 并且不包含在unexpectedKeyCache对象中
  // unexpectedKeyCache在开发者环境是一个空的对象  因此只要state的对象名不包含在reducer中,这个key就会
  // 保存到 unexpectedKeys 当中
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  // 将inputState的key全部设置为true
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  // 如果这个action的type是定义的定义中的ActionTypes.REPLACE 就返回不执行
  if (action && action.type === ActionTypes.REPLACE) return
  // 如果unexpectedKeys中有值,则发出警告
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}

compose

compose会返回一个方法,这个方法可以将传入的方法依次执行

export default function compose(...funcs) {
  // 如果函数方法为0 则
  if (funcs.length === 0) {
    // 会将参数直接返回
    return arg => arg
  }

  // 如果只传入一个方法则会返回这个方法
  if (funcs.length === 1) {
    return funcs[0]
  }
  // a为上一次回调函数返回的值 b为当前值
  // 效果就是不断执行数组中的方法最后返回时一个函数
  // 这个方法可以将所有数组中的方法执行
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

createStore

我们接下来看下createStore.js这个文件,它只暴露出了createStore的方法,在createStore中,初始化了一些参数,同时返回了一个store,store中包括了dispatchsubscribegetStatereplaceReducer,[$$observable]: observable

import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'


export default function createStore(reducer, preloadedState, enhancer) {
  // 如果初始化的state是一个方法并且enhancer也是方法就会报错
  // 如果enhancer是方法如果第4个参数是方法就会报错
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  // 如果初始化的state是方法,enhancer的参数为undefined
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer赋值初始话的stae
    enhancer = preloadedState
    // preloadedState赋值为undefined
    preloadedState = undefined
    // 这里是一个兼容2个参数的处理,当参数仅为2个 第二个参数为enhcaner时的处理
  }

  // 如果enhancer 不是undefined
  if (typeof enhancer !== 'undefined') {
    // 如果enhancer不是方法会报错
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 返回enhancer的方法
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 如果reducer不是方法 则报错
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }


  // rootReducer赋值到currentReducer当中 实际是一个函数
  let currentReducer = reducer
  // 当前store中的state 默认是初始化的state
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      // 浅拷贝一个数组 虽然是浅拷贝 但是currentListener不会被nextListener改变
      nextListeners = currentListeners.slice()
    }
  }

  function getState() {
      // 省略代码...
  }

  function subscribe(listener) {
      // 省略代码...
  }

  function dispatch(action) {
      // 省略代码...
  }

  function replaceReducer(nextReducer) {
      // 省略代码...
  }

  function observable() {
      // 省略代码...
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  // 执行dispatch 来初始化store中的state
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

看完之后,我们可能在这个地方有一点疑惑,就是这里

  // 如果enhancer 不是undefined
  if (typeof enhancer !== 'undefined') {
    // 如果enhancer不是方法会报错
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 返回enhancer的方法
    return enhancer(createStore)(reducer, preloadedState)
  }

这个返回的是什么呢,我们知道applyMiddleware返回的其实就是enhancer,那我们结合在一起看一下

applyMiddleware

import compose from './compose'

 /**
  * 创建一个store的增强器,使用中间件来包装dispath方法,这对于各种任务来说都很方便
  * 比如以简洁的方式进行异步操作,或记录每个操作有效负载
  * 
  * 查看`redux-thunk`包,这是一个中间件的例子
  * 
  * 因为中间件可能是异步的,所以应该是对个enhancer传参
  * 
  * 每一个中间件都要提供dispatch和getstate两个方法作参数
  * 
  */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 创建一个store ...args为reducer, preloadedState
    const store = createStore(...args)
    // 默认定义disptach方法,是一个抛出的报错
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    // 中间件的的参数
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 将所有的中间件遍历,将参数传入到中间件函数中,返回一个中间件函数的数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // dispatch做了包装,会在dispatch的同时同时将中间件的方法也返回
    dispatch = compose(...chain)(store.dispatch)
    // 返回store中的属性以及新的dispatch方法
    return {
      ...store,
      dispatch
    }
  }
}

如果直接返回了enhancer那么返回的其实也是store,但是这个store中的dispatch被包装过,当dispatch被执行时,会将所有中间件也依次执行。

接下来分析一下createStore中的方法

  • getState
  • subscribe
  • dispatch
  • replaceReducer
  • observable

getState

很简单,就是返回currentState

/**
   * Reads the state tree managed by the store.
   *
   * @returns {any} The current state tree of your application.
   */
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }
    // 返回state
    return currentState
  }

subscribe

这是将一个回调加入到监听数组当中,同时,它会返回一个注销监听的方法。

  function subscribe(listener) {
    // listener必须是一个方法
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true
  
    ensureCanMutateNextListeners()
    // 把listenr加入到nextListeners的数组当中
    nextListeners.push(listener)

    // 解除观察
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false
      // 这里做了个拷贝 做的所有操作不影响currentListener
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 在nextListener将它去除
      nextListeners.splice(index, 1)
    }
  }

dispatch

dispatch首先会检查参数,随后会执行currentReducer(currentState, action),而这个方法实际就是combineReducers

  function dispatch(action) {
    // 如果dispatch的参数不是action
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    // action必须得有type属性,如果没有会报错
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 执行reducer 遍历过滤后的reducer,随后依次赋值到state当中
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 获取当前的监听器
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 依次执行监听器回调
      listener()
    }

    // dispatch默认返回action
    return action
  }

replaceReducer

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param {Function} nextReducer The reducer for the store to use instead.
   * @returns {void}
   */
  /**
   * 替换reducer
   * 
   * 动态替换原有的reducer
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 将reducer赋值
    currentReducer = nextReducer
    // 发送一个dispatch 随后重置store
    dispatch({ type: ActionTypes.REPLACE })
  }

observable

这里不谈太多observable

这里有个使用例子

const state$ = store[Symbol.observable]();
const subscription = state$.subscribe({
  next: function(x) {
     console.log(x);
   }
 });
 subscription.unsubscribe();
 /**
   * Interoperability point for observable/reactive libraries.
   * @returns {observable} A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       * @param {Object} observer Any object that can be used as an observer.
       * The observer object should have a `next` method.
       * @returns {subscription} An object with an `unsubscribe` method that can
       * be used to unsubscribe the observable from the store, and prevent further
       * emission of values from the observable.
       */
      /**
       * @param {Object} 任何对象都可以当做observer
       * observer应该有一个`next`方法
       * @returns {subscription} 一个对象,它有`unsubscribe`方法能够
       * 用来从store中unsubscribe observable
       */
      subscribe(observer) {
        // observer必须是一个非空的object
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

bindActionCreators

在讲这个方法前,先看下文档对它的使用说明

Example

TodoActionCreators.js

我们在文件中创建了2个普通的action。

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}

SomeComponent.js

import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)
// {
//   addTodo: Function,
//   removeTodo: Function
// }class TodoListContainer extends Component {
  constructor(props) {
    super(props)const { dispatch } = props// Here's a good use case for bindActionCreators:
    // You want a child component to be completely unaware of Redux.
    // We create bound versions of these functions now so we can
    // pass them down to our child later.this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
    console.log(this.boundActionCreators)
    // {
    //   addTodo: Function,
    //   removeTodo: Function
    // }
  }componentDidMount() {
    // Injected by react-redux:
    let { dispatch } = this.props// Note: this won't work:
    // TodoActionCreators.addTodo('Use Redux')// You're just calling a function that creates an action.
    // You must dispatch the action, too!// This will work:
    let action = TodoActionCreators.addTodo('Use Redux')
    dispatch(action)
  }render() {
    // Injected by react-redux:
    let { todos } = this.propsreturn <TodoList todos={todos} {...this.boundActionCreators} />// An alternative to bindActionCreators is to pass
    // just the dispatch function down, but then your child component
    // needs to import action creators and know about them.// return <TodoList todos={todos} dispatch={dispatch} />
  }
}export default connect(state => ({ todos: state.todos }))(TodoListContainer)

bindActionCreators.js

我们接下来来看它的源码

/**
 * 
 * 在看`bindActionCreator`方法之前可以先看`bindActionCreators`方法
 * 
 * @param {Function} actionCreator 实际就是 action
 * 
 * @param {Function}
 * 
 * @returns {Function}
 */
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    // 返回的是dispath
    return dispatch(actionCreator.apply(this, arguments))
  }
}

// actionCreators是一个包含众多actions的对象
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    // actionCreators是函数就代表他是单一的action方法
    return bindActionCreator(actionCreators, dispatch)
  }

  // actionCreator如果不是object 或者它是空的则报错
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }
  // 将action的keys遍历出来
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    // 每个action的key
    const key = keys[i]
    // 将action取出 这是一个方法
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      // bindActionCreator返回的是dispatch的返回值
      // 实际是action 所以boundActionCreators是一个dispatch function的对象
      // 同时如果key相同会被覆盖
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

使用bindActionCreators实际可以创建一个充满dispatch方法的对象。然后可以将这个对象传递子组件来使用。

总结

看完源码后我们大致了解到为什么reducer必须是function,store中的state为什么会创建和reducer相应的对象名的state,为什么只能通过dispatch来对store进行操作。另外redux的一个核心不可变性,redux本身并不能保证。所以我们在自己写的reducer当中必须要保证不能改变store原有的对象,必须得重新创建。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/12/21 - Promise 源码分析

前言

then/promise项目是基于Promises/A+标准实现的Promise库,从这个项目当中,我们来看Promise的原理是什么,它是如何做到的,从而更加熟悉Promise

分析

从index.js当中知道,它是先引出了./core.js,随后各自执行了其他文件的代码,通过requeire的方法。

我们首先先想一下最基础的promise用法

new Promise((resolve, reject) =>  {
    resolve(4);

}).then(res => {
    console.log(res); // export 4
});

Promise中的标准

标准中规定:

  1. Promise对象初始状态为 Pending,在被 resolvereject 时,状态变为 FulfilledRejected
  2. resolve接收成功的数据,reject接收失败或错误的数据
  3. Promise对象必须有一个 then 方法,且只接受两个可函数参数 onFulfilledonRejected

index.js

'use strict';

module.exports = require('./core.js');
require('./done.js');
require('./finally.js');
require('./es6-extensions.js');
require('./node-extensions.js');
require('./synchronous.js');

我们先看src/core.js

function Promise(fn) {
  // 判断 this一定得是object不然就会报错,这个方法一定得要new出来
  if (typeof this !== 'object') {
    throw new TypeError('Promises must be constructed via new');
  }
  // 判断fn 一定得是一个函数
  if (typeof fn !== 'function') {
    throw new TypeError('Promise constructor\'s argument is not a function');
  }
  this._deferredState = 0;
  this._state = 0;
  this._value = null;
  this._deferreds = null;
  if (fn === noop) return;
  // 最终doResolve很关键
  doResolve(fn, this);
}

Promise是一个构造方法,开始时,它进行了校验,确保了fn是一个函数,随后对一些变量进行了初始化,最后执行了doResolve()

我们接着看doResolve这个方法。

/**
 * Take a potentially misbehaving resolver function and make sure
 * onFulfilled and onRejected are only called once.
 *
 * Makes no guarantees about asynchrony.
 */
// 
// 确保`onFulfilled`和`onRejected`方法只调用一次
// 不保证异步
function doResolve(fn, promise) {
  var done = false;
  var res = tryCallTwo(fn, function (value) {
    // 如果done 为true 则return
    if (done) return;
    done = true;
    // 回调执行 resolve()
    resolve(promise, value);
  }, function (reason) {
    // 如果done 为true 则return
    if (done) return;
    done = true;
    reject(promise, reason);
  });
  // res为truCallTwo()的返回值
  // 如果done没有完成 并且 res 是 `IS_ERROR`的情况下
  // 也会执行reject(),同时让done完成
  if (!done && res === IS_ERROR) {
    done = true;
    reject(promise, LAST_ERROR);
  }
}

doResolve最关键的是执行了tryCallTwo方法,这个方法的第二,第三个参数都是回调,当执行回调后,done为true,同时各自会执行resolve()或者reject()方法。最后当tryCallTwo的返回值为IS_ERROR时,也会执行reject()方法。

我们先来看一下tryCallTwo方法

function tryCallTwo(fn, a, b) {
  try {
    fn(a, b);
  } catch (ex) {
    LAST_ERROR = ex;
    return IS_ERROR;
  }
}

fn实际就是Promise初始化时的匿名函数(resolve, reject) => {}ab则代表的是resolve()reject()方法,当我们正常执行完promise函数时,则执行的是resolve则在doResolve中,我们当时执行的第二个参数被回调,如果报错,reject()被执行,则第二个参数被回调。最后捕获了异常,当发生了报错时,会return IS_ERROR,非报错时会return undinfed

再回到刚才的doResolve方法,当执行了第二个参数的回调之后,会执行resolve方法

function resolve(self, newValue) {
  // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
  // 不能吃传递自己
  if (newValue === self) {
    // 报错
    return reject(
      self,
      new TypeError('A promise cannot be resolved with itself.')
    );
  }
  // promise作为参数
  if (
    newValue &&
    (typeof newValue === 'object' || typeof newValue === 'function')
  ) {
    // 获取它的promise方法 读取newValue.then
    var then = getThen(newValue);
    if (then === IS_ERROR) {
      // 如果then IS_ERROR
      return reject(self, LAST_ERROR);
    }
    if (
      // 如果then是self的then
      // 并且Promise
      then === self.then &&
      // newValue 属于Promise
      newValue instanceof Promise
    ) {
      // _state为3
      // 一般then之后走这里
      // 执行then(newValue)返回了promise
      self._state = 3;
      // selft.value为newValue
      self._value = newValue;
      // 当state为3时执行 finale
      finale(self);
      return;
    } else if (typeof then === 'function') {
      doResolve(then.bind(newValue), self);
      return;
    }
  }
  self._state = 1;
  self._value = newValue;
  finale(self);
}

在没有链式调用then的情况下(也就是只要一个then)的情况下,会将内部状态_state设置成3,将传入值赋给内部变量_value最后会执行final()方法,不然则会使用doResolve来调用then

我们再来看下reject

function reject(self, newValue) {
  // _state = 2为reject
  self._state = 2;
  self._value = newValue;
  if (Promise._onReject) {
    Promise._onReject(self, newValue);
  }
  finale(self);
}

reject当中我们的_state变更为了2,同样最后finale被调用。

我们来看下finale函数

// 执行自己的deferreds
function finale(self) {
  if (self._deferredState === 1) {
    handle(self, self._deferreds);
    self._deferreds = null;
  }
  if (self._deferredState === 2) {
    for (var i = 0; i < self._deferreds.length; i++) {
      // 遍历handle
      handle(self, self._deferreds[i]);
    }
    // 将deferred 置空
    self._deferreds = null;
  }
}

在该方法当中根据不同的_deferredState,会执行不同的handle方法。

我们再来看handle方法

function handle(self, deferred) {
  while (self._state === 3) {
    self = self._value;
  }
  // 如果有onHandle方法 则执行该方法
  if (Promise._onHandle) {
    Promise._onHandle(self);
  }
  // (初始 _state 为0)
  if (self._state === 0) {
    // (初始 _deferredState 为0)
    if (self._deferredState === 0) {
      self._deferredState = 1;
      self._deferreds = deferred;
      return;
    }
    // 如果 _deferredState是1 则__deferreds是一个数组
    if (self._deferredState === 1) {
      self._deferredState = 2;
      self._deferreds = [self._deferreds, deferred];
      return;
    }
    // 当走到这里 _deferredState应该是2 将deferred
    // 插入到数组当中
    self._deferreds.push(deferred);
    return;
  }
  handleResolved(self, deferred);
}

这里比较关键的应该就是通过deferredState不同的状态,将deferred放入deferreds当中。另外当我们的_state不为0时,最终会执行handleResolved

继续看handleResolve()方法

function handleResolved(self, deferred) {
  asap(function() {
    // _state为1时,cb = onFulfilled 否则 cb = onRejected
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    if (cb === null) {
      if (self._state === 1) {
        resolve(deferred.promise, self._value);
      } else {
        reject(deferred.promise, self._value);
      }
      return;
    }
    var ret = tryCallOne(cb, self._value);
    if (ret === IS_ERROR) {
      reject(deferred.promise, LAST_ERROR);
    } else {
      resolve(deferred.promise, ret);
    }
  });
}.then((res) => {
}).catch((error) => {
})

在这个方法当中,会根据我们任务(_state)的不同状态,来执行onFulfilled或者onRejected方法。当此方法调用时,也就是我们一个简单的Promise的结束。

回到刚才说的Promise构造方法结束的时候

设置了Promise函数的一些变量

Promise._onHandle = null;
Promise._onReject = null;
Promise._noop = noop;

随后在Promise的原型上设置了then方法。

Promise.prototype.then = function(onFulfilled, onRejected) {
  // 首先看这是谁构造的 如果不是promise
  // 则return 执行safeThen
  if (this.constructor !== Promise) {
    return safeThen(this, onFulfilled, onRejected);
  }
  // 如果是则初始化一个Promise 但是参数 noop 为空对象 {}
  var res = new Promise(noop);
  // 随后执行handle方法
  handle(this, new Handler(onFulfilled, onRejected, res));
  return res;
};

then这个方法中首先判断了它是否由Promise构造的,如果不是,则返回并执行safeThen,不然则执行Promise构造一个res对象,然后执行handle方法,最后将promise变量res返回。handle方法之前有提过,在这里,当初始化时_state_deferred的转改都为0,因此它会将defrred保存到promise当中。

先看一下上面说的safeThen方法

function safeThen(self, onFulfilled, onRejected) {
  return new self.constructor(function (resolve, reject) {
    var res = new Promise(noop);
    res.then(resolve, reject);
    handle(self, new Handler(onFulfilled, onRejected, res));
  });
}

流程

需要有一个Promise的构造方法,这个构造方法最终会执行它的参数(resolve, reject) => {},声明的then方法会通过handle()方法将onFulfilledonRejected方法保存起来。当在外部调用resolve或者onRejected时,最终也会执行handle但是它,会最后根据状态来执行onFulfilled或者onRejected。从而到我们的then回调中。

Promise的扩展

done

done的扩展在src/done.js当中

'use strict';

var Promise = require('./core.js');

module.exports = Promise;
Promise.prototype.done = function (onFulfilled, onRejected) {
  var self = arguments.length ? this.then.apply(this, arguments) : this;
  self.then(null, function (err) {
    setTimeout(function () {
      throw err;
    }, 0);
  });
};

内部执行了then()

finally

finally的扩展在src/finally.js当中

Promise的标准当中,本身是没有finally方法的,但是在ES2018的标准里有,finally的实现如下

'use strict';

var Promise = require('./core.js');

module.exports = Promise;
Promise.prototype.finally = function (callback) {
  return this.then(function (value) {
    return Promise.resolve(callback()).then(function () {
      return value;
    });
  }, function (err) {
    return Promise.resolve(callback()).then(function () {
      throw err;
    });
  });
};

PromiseonFulfilledonRejected 不管回调的哪个,最终都会触发callback 回调。还要注意的一点是finally的返回也是一个Promise

es6-extensions.js

es6-extensions.js文件当中包含了ES6的一些扩展。

Promise.resolve

function valuePromise(value) {
  var p = new Promise(Promise._noop);
  // 将_state赋值为 非0
  // _value进行保存
  p._state = 1;
  p._value = value;
  // 这样做的目的是省略的一些前面的逻辑
  return p;
}

Promise.resolve = function (value) {
  if (value instanceof Promise) return value;

  if (value === null) return NULL;
  if (value === undefined) return UNDEFINED;
  if (value === true) return TRUE;
  if (value === false) return FALSE;
  if (value === 0) return ZERO;
  if (value === '') return EMPTYSTRING;

  // value return new Promise
  if (typeof value === 'object' || typeof value === 'function') {
    try {
      var then = value.then;
      if (typeof then === 'function') {
        // 返回 返回了一个新的Promise对象
        return new Promise(then.bind(value));
      }
    } catch (ex) {
        // 如果报错 则返回一个就只
      return new Promise(function (resolve, reject) {
        reject(ex);
      });
    }
  }

  return valuePromise(value);
};

Promise.reject

Promise.reject = function (value) {
  return new Promise(function (resolve, reject) {
    reject(value);
  });
};

Promise.all

Promise.all = function (arr) {
  // 类似深拷贝了一份给了args
  var args = Array.prototype.slice.call(arr);

  return new Promise(function (resolve, reject) {
    // 判断了all的promise数量
    if (args.length === 0) return resolve([]);
    // remaining则是promise数组的长度
    var remaining = args.length;
    // i为index val 为 promise
    function res(i, val) {
      if (val && (typeof val === 'object' || typeof val === 'function')) {
        if (val instanceof Promise && val.then === Promise.prototype.then) {
          while (val._state === 3) {
            val = val._value;
          }
          if (val._state === 1) return res(i, val._value);
          if (val._state === 2) reject(val._value);
          // val._state 为 0时 走这里
          val.then(function (val) {
            res(i, val);
          }, reject);
          return;
        } else {
          var then = val.then;
          if (typeof then === 'function') {
            var p = new Promise(then.bind(val));
            p.then(function (val) {
              res(i, val);
            }, reject);
            return;
          }
        }
      }
      args[i] = val;
      // 当所有的promise执行完 则是remaining为0
      // 则执行resolve();
      if (--remaining === 0) {
        resolve(args);
      }
    }
    // 遍历所有的promise
    for (var i = 0; i < args.length; i++) {
      res(i, args[i]);
    }
  });
};

Promise.all()返回的也是一个Promise函数。
内部有一个remaining变量每当执行完一个promise函数后就会减一,当所有promise执行完,会执行自己的resolve

Promise.race

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    values.forEach(function(value){
      Promise.resolve(value).then(resolve, reject);
    });
  });
};

遍历传入的promise数组,经过Promise.resolve(value)的源码可以看到,如果value是一个Promise则户直接将这个value返回,最后数组中的promise哪个优先回调即执行。

Promise.property.catch

catch在标准当中也是没有,虽然我们用的比较多

Promise.prototype['catch'] = function (onRejected) {
  return this.then(null, onRejected);
};

catch的回调实际是then(null, onRejected)的回调。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/12/16 - 你该知道的Babel7

Babel是一个广泛使用的转码器,可将任意任意版本语法和API转到当前环境支持的版本。

使用

将配置文件.babelrc,放在项目根目录。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage"
      }
    ],
    ["@babel/preset-react"]
  ],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd-mobile",
        "style": "css"
      }
    ],
    "@babel/plugin-syntax-dynamic-import"
  ]
}

概念介绍

Babel默认只转换新的JS句法(syntax),而不转换新的API,比如Set、Maps等全局对象,以及一些定义在全局对象上的方法,比如Object.assign、Array.from等,具体可以查看这个列表,所以此时就需要包含core-js、regenerator、helpers方法库的@babel/polyfill或@babel/runtime。

@babel/core

Babel编译器,包括了几乎所有核心API,将JS代码抽象成AST,再分析做对应的转换处理。

presets

Babel预设,设定转码规则,包含某一部分plugins,从下往上执行。

  • @babel/preset-env:根据设置的目标运行环境,“自动”决定加载哪些插件和 polyfill 的 preset
  • @babel/preset-react:转换react语法
  • babel-preset-es201X:将es(X+1)代码编译为esX,已经废弃由@babel/preset-env取代

@babel/preset-env

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": { // 目标环境,建议去除,采用在根目录设置.browserslistrc
          "browsers": "> 5%"
        },
        "modules": false, // 设置ES6 模块转译的模块格式 默认是 commonjs
        "useBuiltIns": "usage", // @babel/polyfill加载方式,
        "debug": true, // 调试模式,开启会输出目标环境、transforms、plugins和polyfills
        "include": [], // 总是启用哪些 plugins
        "exclude": [] // 强制不启用哪些 plugins
      }
    ]
  ]
}
useBuiltIns分析

按需是根据目标环境,polyfills影响代码体积

  • false:按需加载transforms、plugins,不加载polyfills
  • usage:按需加载transforms、plugins和polyfills
  • entry:按需加载transforms、plugins,加载所有polyfills

stage-X

对ES一些提案的支持,向下兼容,比如stage-0包含stage-[1-3]。在Babel7已经废弃,换成proposal-x。

  • stage-0:想法
  • stage-1:建议
  • stage-2:草案
  • stage-3:候选
  • stage-4:完成

plugins

Babel插件,从上往下执行,并且在presets之前运行。

@babel/runtime

包含core-js、regenerator、helpers,用来转换新的属性和方法。

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
}

注意:

  • 引入的方法是module级的,会存在重复引用的问题,需要搭配@babel/plugin-transform-runtime来做自动化引用。
  • babel-runtime的引用不是全局生效的,因此实例化的对象方法则不能被 polyfill,比如[1,2,3].includes 这样依赖于全局 Array.prototype.includes的调用依然无法使用,比较适用于库。

@babel/polyfill

功能和@babel/runtime类似,在@babel/preset-env配置useBuiltIns开启,详细可见其说明。

注意:

  • @babel/polyfill引用是全局的,引入可以一劳永逸,但会污染子模块的变量引用,适用于业务项目。

参考文档

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/11/05 - webpack 原理与实践(一):打包流程

webpack 原理与实践(一):打包流程

写在前面的话

在阅读 webpack4.x 源码的过程中,参考了《深入浅出webpack》一书和众多大神的文章,结合自己的一点体会,总结如下。

总述

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 --吴浩麟《深入浅出webpack》

核心的概念

entryloaderpluginmodulechunk 不论文档还是相关的介绍都很多了,不赘述,有疑问的移步文档。

构建流程

webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
    在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 改变 webpack 的运行结果。

webpack 中比较核心的两个对象

  • Compile 对象:负责文件监听和启动编译。Compiler 实例中包含了完整的 webpack 配置,全局只有一个 Compiler 实例。
  • compilation 对象:当 webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
  • 这两个对象都继承自 Tapable。以Compile为例
const {
	Tapable,
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			/** @type {SyncBailHook<Compilation>} */
			//所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<Stats>} */
			//成功完成一次完成的编译和输出流程。
			done: new AsyncSeriesHook(["stats"]),
			/** @type {AsyncSeriesHook<>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<Compiler>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compiler>} */
			//启动一次新的编译
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			// 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			// 输出完毕
			afterEmit: new AsyncSeriesHook(["compilation"]),
                         // 以上几个事件(除了run,beforerun为编译阶段)其余为输出阶段的事件
			/** @type {SyncHook<Compilation, CompilationParams>} */
			// compilation 创建之前挂载插件的过程
			thisCompilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<Compilation, CompilationParams>} */
			// 创建compilation对象
			compilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<NormalModuleFactory>} */
		    // 初始化阶段:初始化compilation参数
			normalModuleFactory: new SyncHook(["normalModuleFactory"]),
			/** @type {SyncHook<ContextModuleFactory>}  */
		    // 初始化阶段:初始化compilation参数
			contextModuleFactory: new SyncHook(["contextModulefactory"]),

			/** @type {AsyncSeriesHook<CompilationParams>} */
			beforeCompile: new AsyncSeriesHook(["params"]),
			/** @type {SyncHook<CompilationParams>} */
			// 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象
			compile: new SyncHook(["params"]),
			/** @type {AsyncParallelHook<Compilation>} */
			//一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
			make: new AsyncParallelHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
		    // 一次Compilation执行完成
			afterCompile: new AsyncSeriesHook(["compilation"]),

			/** @type {AsyncSeriesHook<Compiler>} */
			//监听模式下启动编译(常用于开发阶段)
			watchRun: new AsyncSeriesHook(["compiler"]),
			/** @type {SyncHook<Error>} */
			failed: new SyncHook(["error"]),
			/** @type {SyncHook<string, string>} */
			invalid: new SyncHook(["filename", "changeTime"]),
			/** @type {SyncHook} */
			// 如名字所述
			watchClose: new SyncHook([]),

			// TODO the following hooks are weirdly located here
			// TODO move them for webpack 5
			/** @type {SyncHook} */
			//初始化阶段:开始应用 Node.js 风格的文件系统到compiler 对象,以方便后续的文件寻找和读取。
			environment: new SyncHook([]),
			/** @type {SyncHook} */
			// 参照上文
			afterEnvironment: new SyncHook([]),
			/** @type {SyncHook<Compiler>} */
			// 调用完内置插件以及配置引入插件的apply方法,完成了事件订阅
			afterPlugins: new SyncHook(["compiler"]),
			/** @type {SyncHook<Compiler>} */
			afterResolvers: new SyncHook(["compiler"]),
			/** @type {SyncBailHook<string, EntryOptions>} */
			// 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
			entryOption: new SyncBailHook(["context", "entry"])
		};

webpack 执行的过程中,会按顺序广播一系列事件--this.hooks中的一系列事件(类似于我们常用框架中的生命周期),而这些事件的订阅者该按照怎样的顺序来组织,来执行,来进行参数传递... 这就是 Tapable 要做的事情。
关于 Tapable 给大家推荐一篇比较好(但是阅读量点赞评论都不多2333)的科普文

流程细节

流程细节参照我在引用的Compile对象中的注释,有一点需要注意,作者hooks的书写顺序并不是调用顺序。
有些没注释的有几种情况:

  1. 不那么重要,或参照事件名称和上下文可知
  2. 主要是暂时还不知道(2333,后面有新的理解再补充,逃...)
  3. 当然最重要的事件基本涵盖到了
    这里补充一个大从参考文章里面找来的图

compilation 过程简介

compilation 实际上就是调用相应的 loader 处理文件生成 chunks并对这些 chunks 做优化的过程。几个关键的事件(Compilation对象this.hooks中):

  1. buildModule 使用对应的 Loader 去转换一个模块;
  2. normalModuleLoader 在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 webpack 后面对代码的分析。
  3. seal 所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk

最后从参考文章中摘了一张图片以便于对整个过程有更清晰的认知
image

参考

  1. http://taobaofed.org/blog/2016/09/09/webpack-flow/
  2. http://imweb.io/topic/5baca58079ddc80f36592f1a
  3. 《深入浅出webpack》

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/06/11 - 前端项目性能优化之打包工具篇

现代前端开发,已经进入工程化阶段,但是基于浏览器的前端项目的在工程化后,往往会遇到一些性能问题,例如中大型前端项目,依赖库和项目本身的资源过大,加载慢,首屏渲染时间长等性能问题,严重影响用户体验,这个时候工程化的项目就需要适当的进行优化,本文分享一下,我们前端项目已经或将要在打包工具这一层级的优化方法。

webpack为目前较为流行的前端项目打包工具,它提供了丰富的扩展功能和插件来让我们对项目有更好的控制,我司前端项目也是使用webpack来作为打包工具,接下来的介绍也是围绕在它来进行。

打包性能分析

要做性能优化,首先要知道优化哪些部分,也就是哪些比较慢,通常情况下,最直观的方向就是,项目整体打包文件的大小的问题,如果资源过大,那么网站性能肯定是上不去的。我们这里使用Webpack Bundle Analyzer 这个工具来分析一下,项目打包后各个模块所占用的资源情况。

首先安装:yarn add webpack-bundle-analyzer —dev

然后在webpack 配置文件中,将webpack-bundle-analyzer 加入到插件上:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
......
plugins: [new BundleAnalyzerPlugin()]

之后我们在production模式下构建项目,NODE_ENV=production yarn build BundleAnalyzerPlugin就会对资源进行分析,然后会开起一个网页浏览分析结果:

612D9821-75FB-4C17-AAA1-BB9CF82567F1.png

  13.f4b485a6e42f8d1022b1.js    14.4 kB      13  [emitted]
   0.f4b485a6e42f8d1022b1.js    56.3 kB       0  [emitted]
   2.f4b485a6e42f8d1022b1.js    42.9 kB       2  [emitted]
   3.f4b485a6e42f8d1022b1.js    39.2 kB       3  [emitted]
   4.f4b485a6e42f8d1022b1.js    47.9 kB       4  [emitted]
   5.f4b485a6e42f8d1022b1.js    8.96 kB       5  [emitted]
   6.f4b485a6e42f8d1022b1.js     4.1 kB       6  [emitted]
   7.f4b485a6e42f8d1022b1.js      97 kB       7  [emitted]
   8.f4b485a6e42f8d1022b1.js    40.4 kB       8  [emitted]
   9.f4b485a6e42f8d1022b1.js    14.8 kB       9  [emitted]
  10.f4b485a6e42f8d1022b1.js    15.7 kB      10  [emitted]
  11.f4b485a6e42f8d1022b1.js    12.3 kB      11  [emitted]
  12.f4b485a6e42f8d1022b1.js      23 kB      12  [emitted]
   1.f4b485a6e42f8d1022b1.js    68.4 kB       1  [emitted]
  14.f4b485a6e42f8d1022b1.js    12.4 kB      14  [emitted]
  15.f4b485a6e42f8d1022b1.js    68.7 kB      15  [emitted]
  16.f4b485a6e42f8d1022b1.js    12.4 kB      16  [emitted]
  17.f4b485a6e42f8d1022b1.js    12.3 kB      17  [emitted]
  18.f4b485a6e42f8d1022b1.js    13.7 kB      18  [emitted]
  19.f4b485a6e42f8d1022b1.js    8.46 kB      19  [emitted]
  20.f4b485a6e42f8d1022b1.js     5.3 kB      20  [emitted]
  21.f4b485a6e42f8d1022b1.js    6.08 kB      21  [emitted]
  22.f4b485a6e42f8d1022b1.js    2.13 kB      22  [emitted]
  23.f4b485a6e42f8d1022b1.js    3.03 kB      23  [emitted]
  24.f4b485a6e42f8d1022b1.js  500 bytes      24  [emitted]
main.f4b485a6e42f8d1022b1.js     533 kB      25  [emitted]  [big]  main

第三方库CDN

通过分析结果我们可以看到,第三方库如Lodash 占据了资源文件不小空间,这个我们就可以使用webpack本身提供的external功能,通过CDN加速的lodash。

配置如下:

{
    externals: {
     lodash: '_',
  },
}

然后我们在项目的入口html中收到加入CDN link:

<!doctype html>
<html class="no-js" lang="">
  <head>
	...
  </head>
  <body>
    <div id="container"></div>
    <script src="https://cdn.bootcss.com/lodash.js/4.17.4/lodash.min.js"></script>
    <script src="<%= bundle %>"></script>
    <script src="<%= vender %>"></script>
  </body>
</html>

公共模块提取

优化了上一部分的CDN,其实项目当中有些没有CDN的第三方资源我们也可用通过提取公共模块的方式,把它们单独的打包成一个文件,这样就可以让main.js的体积进一步减小,浏览器也能够并行加载资源。

这里要使用的就是Webpack提供的CommonsChunkPlugin 插件

配置:

{
   entry: {
    main:  './main.js',
    vendor: ['react', 'react-dom', 'react-redux']
  },
  plugins: [new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor' // 公共模块的入口
  })]
}

配置之后,webpack就会将 react一系列的资源,单独打包成一个vendor.js文件来加快网页加载速度。

525F3CBE-F438-4B0E-9682-204D1F5806B9.png

CSS拆分

最后一个我们要说的性能优化是CSS,在我们的前端项目中使用了 css modules和react技术,项目中的cs s就会打包在js文件当中,当网页加载好js代码后,再作为内联样式渲染,这里就带来了一个弊端,样式的渲染不能和js代码的加载同时进行,需要等到js加载完后才可以,解决办法就是将项目中的所有css文件,提取出来作为一个外联的css文件,这样网页在加载时就会首先加载样式表文件,然后进行解析渲染,不需要等js加载完成。

那么我们要做的就是使用extract-text-webpack-plugin 插件来做到样式表提取。

安装:yarn add extract-text-webpack-plugin —dev

配置:

const ExtractTextPlugin = require("extract-text-webpack-plugin");

{
  module: {
    rules: [
            { //提取css文件
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          // use: "css-loader"
          use: [
            {
              loader: 'css-loader',
              options: {
                // sourceMap: isDebug,
                sourceMap: false,
                importLoaders: true,
                // CSS Modules https://github.com/css-modules/css-modules
                modules: true,
                minimize: !isDebug,
              },
            },
          ],
        })
      },
      { // 提取scss文件
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          //resolve-url-loader may be chained before sass-loader if necessary
          // use: ['css-loader', 'sass-loader']
          use: [
            {
              loader: 'css-loader',
              options: {
                sourceMap: false,
                importLoaders: true,
                // CSS Modules https://github.com/css-modules/css-modules
                modules: true,
                localIdentName: isDebug ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]',
                // CSS Nano http://cssnano.co/options/
                minimize: !isDebug,
              },
            },
            {
              loader: 'sass-loader'
            }
          ],

        })
      },
    ]
  }
  
  plugins: [ // 加载插件,并且指定输出css的文件路径名。
    config.plugins.push(new ExtractTextPlugin('[name].css?[hash]'));
  ]
}

最后的效果就是我们从原来输出一个大的主文件,变成了三个文件,main.js的大小533kb减小到了264kb,少了一倍。

  main.40e4cd0b400a3812526a.js     264 kB      25  [emitted]  [big]  main
vendor.40e4cd0b400a3812526a.js     170 kB      26  [emitted]         vendor
 main.css?40e4cd0b400a3812526a    28.1 kB      25  [emitted]         main

结尾

以上介绍的三种打包工具层面的优化,我们就可以看到 路由和webpack本身已经使用了code split技术将我们的页面拆分成了数字编号的小文件,这就是按需加载。前端性能优化中资源大小的优化属于第一步骤,后续更深入的优化,包括运行时优化,渲染优化,数据加载优化等等,之后我们有机会再来介绍。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/09/25 - 你应该知道的相对路径与绝对路径

前言

在最近做项目的时候,项目部署到线上之后,出现了一种明明开发环境就可以,为什么到了生成环境就不可以的问题。出现该问题的根本原因还是对路径的引用上出现了错误。

我们通常会引用本地文件资源包括

  • 图片
  • js文件
  • css文件
  • web页面
  • 等等

在引用文件时,普遍是2种,相对路径和绝对路径

相对路径和绝对路径

相对路径

w3cschools有这样一个表格来描述文件路径。

路径 说明
<img src="picture.jpg"> picture.jpg在与当前文件位于相同目录
<img src="images/picture.jpg"> picture.jpg在相同目录中的images文件夹内
<img src="/images/picture.jpg" picture.jpg在当前站点根目录下的images文件夹
<img src="../picture.jpg"> picture.jpg在上一级目录

在一些网站中会把相对路径分为2类

  • 文档相对路径
  • 根相对路径

文档相对路径

../xxx 当前文件的上一层目录
./xxx 当前文件所在目录

根相对路径

/xxx  当前文件所在的根目录

绝对地址

一些非本地的文件资源,一般都是使用绝对地址的,考虑到环境不同,有可能是通过拼接组成的(前一部分的服务器地址通过配置接口获取)

webpack

本地的资源如果使用../../在不同层级的文件之间引用会比较繁琐,因此可以使用webpack的alias进行设置。我们以Vue-cli的项目为例。

如果我们想引用的图片是在static文件夹中,我们可以在Vue-cli项目中的webpack.base.conf.js之中进行配置。

// ...相关代码省略
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      // 可以这样设置
      '@static': resolve('static')
    }
  }

我们假设还是之前的项目路径,只不过多了一个vue文件。

|___ index.html
|___ src
    |___components
        |___ component-a.vue
|___ static
    |___img
        |___ bg.png
    |___css
        |___ reset.css
    |___js
        |___ app.js

当我们在component-a.vue当中想引用bg.png直接使用@static/img/bg.png

补充

这里再补充一下,如果使用别名引用,VSCode查找是找不到引用的。

-w595

这里需要配置一下,在vetur插件说明当中有提到。
我们以@为例子,要让VSCode也认识这个路径,需要在项目根目录创建一个jsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

同时我们的引用不能写成import DCDialog from '@/components/discount-coupon-dialog';
要写成import DCDialog from '@/components/discount-coupon-dialog.vue';
-w708
这样就可以直接引用文件当中了。

import a from '@/components/a'     // 不能够打开对应文件
import a from '@/components/a.vue' // 能够正常打开对应引用文件

总结

尽量不要使用根相对地址来进行本地资源文件的引用,在公司的项目当中遇到一个这样的问题,本地环境地址为localhost:8080/#/app,线上环境项目地址为https://xxx.com/app/#/app,我所引用的图片资源地址为/static/img/xxx.png,这样,在开发环境是没有问题的,因为根路径就是项目路径,但是部署到线上之后,因为项目映射在了二级目录,导致/static/img/xxx.png最终引用的地址为https://xxx.com/static/img/xxx.png,而实际图片的地址应该为https://xxx.com/app/static/img/xxx.png,这就是使用根相对路径存在的问题,而如果使用文档相对路径就不会存在这个问题。同时如果是使用webpack来构建的项目,推荐使用alias来作为文件引用,来减少繁琐的引用,同时也能够避免出现打包后文件路径异常的问题。

接口路径

这里我们再提一下ajax的相对地址请求。
这里设定了几种不同情况下的请求。

请求路径 所在地址 最终结果
request('/api/app.json') http://xxx.com http://xxx.com/api/app.json
request('api/app.json') http://xxx.com http://xxx.com/api/app.json
request('/api/app.json') http://xxx.com/path http://xxx.com/api/app.json
request('api/app.json') http://xxx.com/path http://xxx.com/path/api/app.json
request('api/app.json') http://xxx.com/app/#/path http://xxx.com/app/api/app.json
  • /xxx/xxx请求的都是根路径下的地址接口
  • xxx/xxx请求的是当前路径下的地址接口

使用场景

相对地址请求,在前后端分离的项目当中比较少见,目前总结了2种场景。

  • 一种场景是如果服务端,和前端部署在同一服务下,我们都是使用根相对地址进行请求的。
  • 另一种场景是前端和服务端部署在不同服务下,前端有个配置文件接口,获取接口服务地址,以及其他资源地址,比如服务器地址等等,而这个配置文件接口(也许就是一个json文件),可以放置在前端服务当中,这个时候,就可以使用第二种方式的路径进行请求了。

相对协议

我们从一些网站引入一些资源文件时可以看到,是不带http或者https前缀的比如淘宝

-w769

如果这样做之后,获取资源会根据当前访问的URL的协议进行变更,当前访问的是https的实际则是https://g.alicdn.com/alilog/mlog/aplus_v2.js,如果是http的,那么就是http://g.alicdn.com/alilog/mlog/aplus_v2.js

参考

12

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/07/08 - 异常处理,"try..catch" [译]

异常处理,"try..catch" [译]

原文地址

不管你多么的精通编程,有时我们的脚本总还是会有一些错误。可能是因为我们的编写出错,或是与预期不同的用户输入,或是错误的的服务端返回或者是其他总总不同的原因。

通常,一段代码会在出错的时候“死掉”(停止执行)并在控制台将异常打印出来。

但是有一种更为合理的语法结构 try..catch,它会在捕捉到异常的同时不会使得代码停止执行而是可以做一些更为合理的操作。

"try..catch" 语法

try..catch 结构由两部分组成:try catch

try {

  // 代码...

} catch (err) {

  // 异常处理

}

它按照以下步骤执行:

  1. 首先,执行 try {...} 里面的代码。
  2. 如果执行过程中没有异常,那么忽略 catch(err) 里面的代码,try 里面的代码执行完之后跳出该代码块。
  3. 如果执行过程中发生异常,控制流就到了 catch(err) 的开头。变量 err(可以取其他任何的名称)是一个包含了异常信息的对象。

所以,发生在 try {…} 代码块的异常不会使代码停止执行:我们可以在 catch 里面处理异常。

让我们来看更多的例子。

  • 没有异常的例子:显示下面(1)和(2)中 alert 的内容:

    try {
    
      alert('Start of try runs');  // *!*(1) <--*/!*
    
      // ...这里没有异常
    
      alert('End of try runs');   // *!*(2) <--*/!*
    
    } catch(err) {
    
      alert('Catch is ignored, because there are no errors'); // (3)
    
    }
    
    alert("...Then the execution continues");
  • 包含异常的例子:显示下面(1)和(3)中 alert 的内容:

    try {
    
      alert('Start of try runs');  // *!*(1) <--*/!*
    
    *!*
      lalala; // 异常, 变量未定义!
    */!*
    
      alert('End of try (never reached)');  // (2)
    
    } catch(err) {
    
      alert(`Error has occured!`); // *!*(3) <--*/!*
    
    }
    
    alert("...Then the execution continues");

````warn header="try..catch only works for runtime errors"
要使得 `try..catch` 能工作,代码必须是可执行的,换句话说,它必须是有效的 JavaScript 代码。

如果代码包含语法错误,那么 try..catch 不能正常工作,例如含有未闭合的花括号:

try {
  {{{{{{{{{{{{
} catch(e) {
  alert("The engine can't understand this code, it's invalid");
}

JavaScript 引擎读取然后执行代码。发生在读取代码阶段的异常被称为 "parse-time" 异常,它们不会被 try..catch 覆盖到(包括那之间的代码)。这是因为引擎读不懂这段代码。

所以,try..catch 只能处理有效代码之中的异常。这类异常被称为 "runtime errors",有时候也称为 "exceptions"。



````warn header="`try..catch` works synchronously"
如果一个异常是发生在计划中将要执行的代码中,例如在 `setTimeout` 中,那么 `try..catch` 不能捕捉到:

```js run
try {
  setTimeout(function() {
    noSuchVariable; // 代码在这里停止执行
  }, 1000);
} catch (e) {
  alert( "won't work" );
}
```

因为 `try..catch` 包裹了计划要执行的 `setTimeout` 函数。但是函数本身要稍后才能执行,这时引擎已经离开了 `try..catch` 结构。

要捕捉到计划中将要执行的函数中的异常,那么 `try..catch` 必须在这个函数之中:
```js run
setTimeout(function() {
  try {
    noSuchVariable; // try..catch 处理异常!
  } catch (e) {
    alert( "error is caught here!" );
  }
}, 1000);
```

Error 对象

当一个异常发生之后,JavaScript 生成一个包含异常细节的对象。这个对象会作为一个参数传递给 catch

try {
  // ...
} catch(err) { // <-- “异常对象”,可以用其他参数名代替 err
  // ...
}

对于所有内置的异常,catch 代码块捕捉到的相应的异常的对象都有两个属性:

name
: 异常名称,对于一个未定义的变量,名称是 "ReferenceError"

message
: 异常详情的文字描述。

还有很多非标准的属性在绝大多数环境中可用。其中使用最广泛并且被广泛支持的是:

stack
: 当前的调用栈:用于调试的,一个包含引发异常的嵌套调用序列的字符串。

例如:

try {
*!*
  lalala; // 异常,变量未定义!
*/!*
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala 未定义
  alert(err.stack); // ReferenceError: lalala 在... 中未定义

  // 可以完整的显示一个异常
  // 可以转化成 "name: message" 形式的字符串
  alert(err); // ReferenceError: lalala 未定义
}

使用 "try..catch"

让我们一起探究一下真实使用场景中 try..catch 的使用。

正如我们所知,JavaScript 支持 JSON.parse(str) 方法来解析 JSON 编码的值。

通常,它被用来解析从网络,从服务器或是从其他来源收到的数据。

我们收到数据后,像下面这样调用 JSON.parse

let json = '{"name":"John", "age": 30}'; // 来自服务器的数据

*!*
let user = JSON.parse(json); // 将文本表示转化成 JS 对象
*/!*

// 现在 user 是一个解析自 json 字符串的有自己属性的对象
alert( user.name ); // John
alert( user.age );  // 30

你可以在 info:json 这章找到更多的关于 JSON 的详细信息。

如果 json 格式错误,JSON.parse 就会报错,代码就会停止执行。

得到报错之后我们就应该满意了吗?当然不!

如果这样做,当拿到的数据出错,用户就不会知道(除非他们打开开发者控制台)。代码执行失败却没有提示信息会导致糟糕的用户体验。

让我们来用 try..catch 来处理这个错误:

let json = "{ bad json }";

try {

*!*
  let user = JSON.parse(json); // <-- 当这里抛出异常...
*/!*
  alert( user.name ); // 不工作

} catch (e) {
*!*
  // ...跳到这里继续执行
  alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  alert( e.name );
  alert( e.message );
*/!*
}

我们用 catch 代码块来展示信息,但是我们可以做的更多:发送一个新的网络请求,给用户提供另外的选择,把异常信息发送给记录日志的工具,... 。所有这些都比让代码直接停止执行好的多。

抛出自定义的异常

如果这个 json 数据语法正确,但是少了我们需要的 name 属性呢?

像这样:

let json = '{ "age": 30 }'; // 不完整的数据

try {

  let user = JSON.parse(json); // <-- 不抛出异常
*!*
  alert( user.name ); // 没有 name!
*/!*

} catch (e) {
  alert( "doesn't execute" );
}

这里 JSON.parse 正常执行,但是缺少 name 属性对我们来说确实是个异常。

为了统一的异常处理,我们会使用 throw 运算符。

"Throw" 运算符

throw 运算符生成异常对象。

语法如下:

throw <error object>

技术上讲,我们可以使用任何东西来作为一个异常对象。甚至可以是基础类型,比如数字或者字符串。但是更好的方式是用对象,尤其是有 namemessage 属性的对象(某种程度上和内置的异常有可比性)。

JavaScript 有很多标准异常的内置的构造器:ErrorSyntaxErrorReferenceErrorTypeError 和其他的。我们也可以用他们来创建异常对象。

他们的语法是:

let error = new Error(message);
// 或者
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

对于内置的异常对象(不是对于其他的对象,而是对于异常对象),name 属性刚好是构造器的名字。message 则来自于参数。

例如:

let error = new Error("Things happen o_O");

alert(error.name); // Error
alert(error.message); // Things happen o_O

让我们来看看 JSON.parse 会生成什么样的错误:

try {
  JSON.parse("{ bad json o_O }");
} catch(e) {
*!*
  alert(e.name); // SyntaxError
*/!*
  alert(e.message); // Unexpected token o in JSON at position 0
}

如我们所见, 那是一个 SyntaxError

假定用户必须有一个 name 属性,在我们看来,该属性的缺失也可以看作语法问题。

所以,让我们抛出这个异常。

let json = '{ "age": 30 }'; // 不完整的数据

try {

  let user = JSON.parse(json); // <-- 没有异常

  if (!user.name) {
*!*
    抛出 new SyntaxError("Incomplete data: no name"); // (*)
*/!*
  }

  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}

(*) 标记的这一行,throw 操作符生成了包含着我们所给的 messageSyntaxError,就如同 JavaScript 自己生成的一样。try 里面的代码执行停止,控制权转交到 catch 代码块。

现在 catch 代码块成为了处理包括 JSON.parse 在内和其他所有异常的地方。

再次抛出异常

上面的例子中,我们用 try..catch 处理没有被正确返回的数据,但是也有可能在 try {...} 代码块内发生另一个预料之外的异常,例如变量未定义或者其他不是返回的数据不正确的异常。

例如:

let json = '{ "age": 30 }'; // 不完整的数据

try {
  user = JSON.parse(json); // <-- 忘了在 user 前加 "let"

  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // ( 实际上并没有 JSON Error)
}

当然,一切皆有可能。程序员也是会犯错的。即使是一些开源的被数百万人用了几十年的项目 —— 一个严重的 bug 因为他引发的严重的黑客事件被发现(比如发生在 ssh 工具上的黑客事件)。

对我们来说,try..catch 是用来捕捉“数据错误”的异常,但是 catch 本身会捕捉到所有来自于 try 的异常。这里,我们遇到了预料之外的错误,但是仍然抛出了 "JSON Error" 的信息,这是不正确的,同时也会让我们的代码变得更难调试。

幸运的是,我们可以通过其他方式找出这个异常,例如通过它的 name 属性:

try {
  user = { /*...*/ };
} catch(e) {
*!*
  alert(e.name); // "ReferenceError" for accessing an undefined variable
*/!*
}

规则很简单:

catch 应该只捕获已知的异常,而重新抛出其他的异常。

"rethrowing" 技术可以被更详细的理解为:

  1. 捕获全部异常。
  2. catch(err) {...} 代码块,我们分析异常对象 err
  3. 如果我们不知道如何处理它,那我们就做 throw err 操作。

在下面的代码中,为了达到只在 catch 代码块处理 SyntaxError 的目的,我们使用重新抛出的方法:

let json = '{ "age": 30 }'; // 不完整的数据
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

*!*
  blabla(); // 预料之外的异常
*/!*

  alert( user.name );

} catch(e) {

*!*
  if (e.name == "SyntaxError") {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // rethrow (*)
  }
*/!*

}

(*) 标记的这行从 catch 代码块抛出的异常,是独立于我们期望捕获的异常之外的,它也能被它外部的 try..catch 捕捉到(如果存在该代码块的话),如果不存在,那么代码会停止执行。

所以,catch 代码块只处理已知如何处理的异常,并且跳过其他的异常。

下面这段代码将演示,这种类型的异常如何被另外一层 try..catch 代码捕获。

function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
*!*
    blabla(); // 异常!
*/!*
  } catch (e) {
    // ...
    if (e.name != 'SyntaxError') {
*!*
      throw e; //  重新抛出(不知道如何处理它)
*/!*
    }
  }
}

try {
  readData();
} catch (e) {
*!*
  alert( "External catch got: " + e ); // 捕获到!
*/!*
}

例子中的 readData 只能处理 SyntaxError,而外层的 try..catch 能够处理所有的异常。

try..catch..finally

然而,这并不是全部。

try..catch 还有另外的语法:finally

如果它有被使用,那么,所有条件下都会执行:

  • try 之后,如果没有异常。
  • catch 之后,如果没有异常。

该扩展语法如下所示:

*!*try*/!* {
   ... 尝试执行的代码 ...
} *!*catch*/!*(e) {
   ... 异常处理 ...
} *!*finally*/!* {
   ... 最终会执行的代码 ...
}

试试运行这段代码:

try {
  alert( 'try' );
  if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

这段代码有两种执行方式:

  1. 如果对于 "Make an error?" 你的回答是 "Yes",那么执行 try -> catch -> finally
  2. 如果你的回答是 "No",那么执行 try -> finally

finally 的语法通常用在:我们在 try..catch 之前开始一个操作,不管在该代码块中执行的结果怎样,我们都想结束的时候执行某个操作。

比如,生成斐波那契数的函数 fib(n) 的执行时间,通常,我们在开始和结束的时候测量。但是,如果该函数在被调用的过程中发生异常,就如执行下面的代码就会返回负数或者非整数的异常。

任何情况下,finally 代码块就是一个很好的结束测量的地方。

这里,不管前面的代码正确执行,或者抛出异常,finally 都保证了正确的时间测量。

let num = +prompt("Enter a positive integer number?", 35)

let diff, result;

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("Must not be negative, and also an integer.");
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

let start = Date.now();

try {
  result = fib(num);
} catch (e) {
  result = 0;
*!*
} finally {
  diff = Date.now() - start;
}
*/!*

alert(result || "error occured");

alert( `execution took ${diff}ms` );

你可以通过后面的不同的输入来检验上面代码的执行:先在 prompt 弹框中先输入 35 —— 它会正常执行,try 代码执行后执行 finally 里面的代码。然后再输入 -1,会立即捕获一个异常,执行时间将会是 0ms。两次的测量结果都是正确的。

换句话说,有两种方式退出这个函数的执行:return 或是 throwfinally 语法都能处理。

```smart header="Variables are local inside try..catch..finally"
请注意:上面代码中的 `result` 和 `diff` 变量,都需要在 `try..catch` 之前声明。

否则,如果用 let{...} 代码块里声明,那么只能在该代码块访问到。


````smart header="`finally` and `return`"
`finally` 语法支持**任何**的结束 `try..catch` 执行的方式,包括明确的 `return`。

下面就是 `try` 代码块包含 `return` 的例子。在代码执行的控制权转移到外部代码之前,`finally` 代码块会被执行。

```js run
function func() {

  try {
*!*
    return 1;
*/!*

  } catch (e) {
    /* ... */
  } finally {
*!*
    alert( 'finally' );
*/!*
  }
}

alert( func() ); // 先 alert "finally" 里面的内容,再执行这里

````smart header="`try..finally`"

`try..finally` 结构也很有用,当我们希望确保代码执行完成不想在这里处理异常时,我们会使用这种结构。

```js
function func() {
  // 开始做需要被完成的操作(比如测量)
  try {
    // ...
  } finally {
    // 完成前面要做的事情,即使 try 里面执行失败
  }
}
```
上面的代码中,由于没有 `catch`,`try` 代码块中的异常会跳出这块代码的执行。但是,在跳出之前 `finally` 里面的代码会被执行。

全局 catch

这个部分的内容并不是 JavaScript 核心的一部分。

设想一下,try..catch 之外出现了一个严重的异常,代码停止执行,可能是因为编程异常或者其他更严重的异常。

那么,有没办法来应对这种情况呢?我们希望记录这个异常,给用户一些提示信息(通常,用户是看不到提示信息的),或者做一些其他操作。

虽然没有这方面的规范,但是代码的执行环境一般会提供这种机制,因为这真的很有用。例如,Node.JS 有 process.on('uncaughtException') 。对于浏览器环境,我们可以绑定一个函数到 window.onerror,当遇到未知异常的时候,它就会执行。

语法如下:

window.onerror = function(message, url, line, col, error) {
  // ...
};

message
: 异常信息。

url
: 发生异常的代码的 URL。

line, col
: 错误发生的代码的行号和列号。

error
: 异常对象。

例如:

<script>
*!*
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };
*/!*

  function readData() {
    badFunc(); // 哦,出问题了!
  }

  readData();
</script>

window.onerror 的目的不是去处理整个代码的执行中的所有异常 —— 这几乎是不可能的,这只是为了给开发者提供异常信息。

也有针对这种情况提供异常日志的 web 服务,比如 https://errorception.com 或者 http://www.muscula.com

它们会这样运行:

  1. 我们注册这个服务,拿到一段 JS 代码(或者代码的 URL),然后插入到页面中。
  2. 这段 JS 代码会有一个客户端的 window.onerror 函数。
  3. 发生异常时,它会发送一个异常相关的网络请求到服务提供方。
  4. 我们只要登录服务方提供方的网络接口就可以看到这些异常。

总结

try..catch 结构允许我们处理执行时的异常,它允许我们尝试执行代码,并且捕获执行过程中可能发生的异常。

语法如下:

try {
  // 执行此处代码
} catch(err) {
  // 如果发生异常,跳到这里
  // err 是一个异常对象
} finally {
  // 不管 try/catch 怎样都会执行
}

可能会没有 catch 代码块,或者没有 finally 代码块。所以 try..catch 或者 try..finally 都是可用的。

异常对象包含下列属性:

  • message —— 我们能阅读的异常提示信息。
  • name —— 异常名称(异常对象的构造函数的名称)。
  • stack (没有标准) —— 异常发生时的调用栈。

我们也可以通过使用 throw 运算符来生成自定义的异常。技术上来讲,throw 的参数没有限制,但是通常它是一个继承自内置的 Error 类的异常对象。更对关于异常的扩展,请看下个章节。

重新抛出异常,是一种异常处理的基本模式:catch 代码块通常处理某种已知的特定类型的异常,所以它应该抛出其他未知类型的异常。

即使我们没有使用 try..catch,绝大多数执行环境允许我们设置全局的异常处理机制来捕获出现的异常。浏览器中,就是 window.onerror

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/08/06 - chrome devtools官方文档阅读笔记(十分钟上手performance面板的基本使用)

chrome devtools 官方文档阅读笔记(十分钟上手performance面板的基本使用)

本文参考自 chrome 的官方文档: 传送门(需要科学上网)

chrome 的开发者工具中提供了很多高效工具方便我们对页面进行性能分析.之前自己只用着一些基本的功能, 最近详细的过了一下官方文档,特别是 performance 面板(大部分都是之前的Timeline面板) 的使用(需要相对新一些的chrome浏览器版本).

Tip: 本文旨在分享给大家使用 chrome 进行性能分析基本方法, 在具体性能问题产生的原因的点上不会太过深入

准备工作

首先,开始分析之前是一些准备工作:

  1. 进入隐身模式,这是为了避免浏览器插件带来的干扰
  2. 打开 performance 选项卡
  3. 点击最右边的设置的小齿轮图标,如果是移动端项目,打开 CPU节流开关,根据电脑性能选择相应的(用于模拟手机的性能)
  4. 打开截图 Screenshots 记录过程中 每一帧的截图
  5. 如果勾选了 memory 还可以看到占用内存的不同组成部分(ex:Heap,node...)在记录过程中的变化,根据变化的情况看到大致的垃圾回收的周期,以及有无明显的内存泄漏的情况.

官方案例分析

官方案例地址(需要科学上网>_>) 按照上述准备打开,如果打不开就暂时先看我这边截取的图片;
image
我们先获取优化前的各种数据分析:先点击左上角 record 圆点记录优化前版本的运行时性能,过一段时间之后点击停止. 圆点旁边的圆形箭头是用于 loading 的性能分析的按钮.
所得图片
所得结果如上图所示.
我们将根据所得的结果一步步分析该 demo 的性能瓶颈是什么?

先介绍图中各部的信息的含义:

  1. 图片的右边标有:FPS;CPU;NET
  • 首先是FPS (Analyze frames per second)帧率;
    FPS 上的红色横条表明,帧率过低已经影响了用户体验,通常情况下绿条越高,帧率越高,体验越好.当帧率不影响使用的时候横条是不会出现的.

  • 接下来是 CPU 相关的分析: 如下图
    CPU分析
    CPU 对应的横条与下面 summary Tab 相一致,这一部分的颜色条也是一一对应. 这一部分占的越高对 CPU 的消耗也就越多.从summary Tab中还可以看出我们最多的时间其实花费在 rendering 中,这也提示了我们demo中的问题极有可能处在渲染相关的代码中
    .

  • 在FPS,CPU,NET上 左右移动鼠标,就可以看到各个时间点的截图,这在分析动画执行的各个阶段,以及了loading的各个阶段的时候尤其有用.

  1. 然后是名称在右边的部分:
  • 如果记录期间包含网络请求那么在 frame 上面还有一栏 Network,会用不同的颜色表示请求不同的资源

  • 然后是 frames 区域: 鼠标移上去可以读取到当时的帧率
    frame

  • 在记录过程中按快捷键cmd + shift + p 然后输入 show rendering (打开实时查看帧率的面板),可以看到实时的帧率变化

  • main 代表主线程, 一段横条代表执行一个事件(函数),长度越长,花费的时间越多; 竖向代表调用栈.如果在这些横条中右上角是红色的就表示在该段代码执行过程中可能存在性能问题.
    image

介绍到这里,我们可以看到上图中很多黄色横条的右上角是红色的,那就让我们来顺便把官方demo中的性能瓶颈排查一下点击展开 main中的 这一部分:
点击 animation frame fired 事件,可以在下面看到相关信息. 并且可以定位到 source 面板中的相关代码.根据定位到的代码段,阅读代码我们可以发现,问题是出在选中的蓝色背景的这句代码中

image

为什么这个句代码有问题?它强制性的触发了layout, 这就涉及到重排和重绘的问题,这里不继续展开了.感兴趣的可以点击下面的参考链接深入了解.

最后再补充介绍一下performance面板最下方与 Summary Tab 同级的几个tab:

  1. Bottom-Up Tab
    image
    Timeline 中选取一段时间,然后点击 Bottom-Up得到上图,图片中展示浏览器执行的各个操作说占用的时间
  2. Call-tree Tab
    image
    同理点击Call Tree 得到上图: 表示浏览器的基本操作(事件执行,绘制...)所占用的时间
  3. Event log Tab
    image
    同理点击 Event Log得到上图: 可以按照选中时间内事件发生的顺序来查看事件执行所占用的时间.

tip: 文中的图片均来自chrome 开发者工具的官方文档.

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/06/07 - 低门槛彻底理解JavaScript中的深拷贝和浅拷贝

在说深拷贝与浅拷贝前,我们先看两个简单的案例:

//案例1
var num1 = 1, num2 = num1;
console.log(num1) //1
console.log(num2) //1

num2 = 2; //修改num2
console.log(num1) //1
console.log(num2) //2

//案例2
var obj1 = {x: 1, y: 2}, obj2 = obj1;
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}

obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 2, y: 2}
console.log(obj2) //{x: 2, y: 2}

按照常规思维,obj1应该和num1一样,不会因为另外一个值的改变而改变,而这里的obj1 却随着obj2的改变而改变了。同样是变量,为什么表现不一样呢?这就要引入JS中基本类型引用类型的概念了。

基本类型和引用类型

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。

打个比方,基本类型和引用类型在赋值上的区别可以按“连锁店”和“单店”来理解:基本类型赋值等于在一个新的地方安装连锁店的规范标准新开一个分店,新开的店与其他旧店互不相关,各自运营;而引用类型赋值相当于一个店有两把钥匙,交给两个老板同时管理,两个老板的行为都有可能对一间店的运营造成影响。

上面清晰明了的介绍了基本类型和引用类型的定义和区别。目前基本类型有:Boolean、Null、Undefined、Number、String、Symbol,引用类型有:Object、Array、Function。之所以说“目前”,因为Symbol就是ES6才出来的,之后也可能会有新的类型出来。

再回到前面的案例,案例1中的值为基本类型,案例2中的值为引用类型。案例2中的赋值就是典型的浅拷贝,并且深拷贝与浅拷贝的概念只存在于引用类型

深拷贝与浅拷贝

既然已经知道了深拷贝与浅拷贝的来由,那么该如何实现深拷贝?我们先分别看看Array和Object自有方法是否支持:

Array

var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]

arr2[0] = 3; //修改arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]

此时,arr2的修改并没有影响到arr1,看来深拷贝的实现并没有那么难嘛。我们把arr1改成二维数组再来看看:

var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]

arr2[2][1] = 5; 
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[1, 2, [3, 5]]

咦,arr2又改变了arr1,看来slice()只能实现一维数组的深拷贝

具备同等特性的还有:concatArray.from()

Object

  1. Object.assign()
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}

obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}

obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}

经测试,Object.assign()也只能实现一维对象的深拷贝

  1. JSON.parse(JSON.stringify(obj))
var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}

obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}

JSON.parse(JSON.stringify(obj)) 看起来很不错,不过MDN文档 的描述有句话写的很清楚:

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

我们再来把obj1改造下:

var obj1 = {
    x: 1,
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}

发现,在将obj1进行JSON.stringify()序列化的过程中,y、z、a都被忽略了,也就验证了MDN文档的描述。既然这样,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简单粗暴,已经满足90%的使用场景了。

经过验证,我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题。只能祭出大杀器:递归

function deepCopy(obj) {
    // 创建一个新对象
    let result = {}
    let keys = Object.keys(obj),
        key = null,
        temp = null;

    for (let i = 0; i < keys.length; i++) {
        key = keys[i];    
        temp = obj[key];
        // 如果字段的值也是一个对象则递归操作
        if (temp && typeof temp === 'object') {
            result[key] = deepCopy(temp);
        } else {
        // 否则直接赋值给新对象
            result[key] = temp;
        }
    }
    return result;
}

var obj1 = {
    x: {
        m: 1
    },
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo")
};

var obj2 = deepCopy(obj1);
obj2.x.m = 2;

console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}

可以看到,递归完美的解决了前面遗留的所有问题,我们也可以用第三方库:jquery的$.extend和lodash的_.cloneDeep来解决深拷贝。上面虽然是用Object验证,但对于Array也同样适用,因为Array也是特殊的Object。

到这里,深拷贝问题基本可以告一段落了。但是,还有一个非常特殊的场景:

循环引用拷贝

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);

此时如果调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而导致爆栈。jquery的$.extend也没有解决。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:

function deepCopy(obj, parent = null) {
    // 创建一个新对象
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp= null,
        _parent = parent;
    // 该字段有父级则需要追溯该字段的父级
    while (_parent) {
        // 如果该字段引用了它的父级则为循环引用
        if (_parent.originalParent === obj) {
            // 循环引用直接返回同级的新对象
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        temp= obj[key];
        // 如果字段的值也是一个对象
        if (temp && typeof temp=== 'object') {
            // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
            result[key] = DeepCopy(temp, {
                originalParent: obj,
                currentParent: result,
                parent: parent
            });

        } else {
            result[key] = temp;
        }
    }
    return result;
}

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);
console.log(obj1); //太长了去浏览器试一下吧~ 
console.log(obj2); //太长了去浏览器试一下吧~ 

至此,已完成一个支持循环引用的深拷贝函数。当然,也可以使用lodash的_.cloneDeep噢~。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/07/06 - 浅谈web前端的发展趋势

web前端的发展趋势

前言

你一个写前端的,也敢自称程序员??

15308408976286

相信web前端开发的伙伴们,在职业道路上,十有八九会受到这样的质疑或者嘲讽(大多数其实还是调侃之意)。写几个标签,懂一些HTML CSS 就是程序员?
你们知道CPU、存储、网络、集群吗?
你们了解过并发、业务架构、数据库、性能调优、分布式计算、集群架构、容灾、安全、运维吗

哼 辣鸡👎

今日我们为前端带盐

近年来,Web 应用在整个软件与互联网行业承载的责任越来越重,软件复杂度和维护成本越来越高,Web 技术,尤其是 Web 客户端技术,迎来了爆发式的发展。

  • 1.用Node做中间层的前端工程化方案
  • 2.Webpack、Rollup 这样的打包工具;Babel、PostCSS 这样的转译工具
  • 3.前端三架马车React、Angular、Vue 这样面向现代 web 应用需求的前端框架及其生态
  • 4.与APP结合的混合开发模式,内嵌单页webview,Hybrid App

JavaScript 计算能力、CSS 布局能力、HTTP 缓存与浏览器 API带来了用户体验上质的飞跃

进入主题,我们将从2个方面:

  • 下一代Web应用:PWA
  • WebAssembly

来浅谈一下前端发展的趋势

下一代Web应用:PWA

老生常谈,我们先对比一下生活中WebAPP 和 原生APP的优劣

web APP 对比 原生APP 的优势
开发成本低
适配多种移动设备,不用IOS 安卓多套代码
迭代更新容易,省去了审核、发包、各种渠道发布带来的时间损耗
无需安装成本,拿来即用
web APP 对比 原生APP 的劣势
浏览的体验无法超越原生应用,加载慢,白屏转圈圈
很少有支持离线模式
消息推送及其困难
本地系统功能无法调用

PWA 的一系列关键技术的出现,终于让我们看到了彻底解决这两个平台级别问题的曙光

PWA解决的问题

  • 能够显著提高应用加载速度
  • 甚至让 web 应用可以在离线环境使用 (Service Worker)
  • web 应用能够像原生应用一样被添加到主屏、全屏执行 (Web App Manifest)
  • 进一步提高 web 应用与操作系统集成能力,让 web 应用能在未被激活时发起推送通知 (Push API 与 Notification API) 等等。

一个十分成熟的🌰(例子) 「印度阿里巴巴」 —— Flipkart

FlipKart Lite应该是最为人津津乐道的PWA案例了
当浏览器发现用户需要 Flipkart Lite 时,它就会提示用户“Hello,你可以把它添加至主屏哦”,当然也可以右上角手动添加。
这样,Flipkart Lite 就会像原生应用一样在主屏上留下一个自定义的 icon 作为入口;与一般的添加一个Web书签不同,当用户点击这个 icon 时,Flipkat Lite 将直接全屏打开,不再受困于浏览器的 UI 中,而且有自己的启动屏效果。

15300655325976

而且有一个很大的突破,在无法访问网络时,Flipkart Lite 可以像原生应用一样照常执行,还会很*气的变成黑白色;不但如此,曾经访问过的商品都会被缓存下来得以在离线时继续访问。在商品降价、促销等时刻,Flipkart Lite 会像原生应用一样发起推送通知,吸引用户回到应用。
15300656314905

接下来我们看看PWA的2个重要技术点,Web APP Manifest 和 Service Worker

Web App Manifest

参考链接:https://developers.google.com/web/fundamentals/web-app-manifest/?hl=zh-cn

它其实是一个网络应用清单,一个JSON文件,开发者可以利用它控制在用户想要看到应用的区域(例如移动设备主屏幕)中如何向用户显示网络应用或网站,指示用户可以启动哪些功能,以及定义其在启动时的外观。是PWA技术的必备要素

总结一下Manifest的三个步骤:

  • 创建清单并将其链接到您的页面。
  • 控制用户从主屏幕启动时看到的内容。
  • 启动画面、主题颜色以及打开的网址等。

创建清单demo

15300662214848

short_name:为应用程序提供简短易读的名称。在没有足够空间显示全名时使用。
name:为应用程序提供一个人类可读的名称。
icons:各种环境中用作应用程序图标的图像对象数组
start_url:指定用户从设备启动应用程序时加载的URL。

15300662332376

在创建清单且将清单添加到您的网站之后,将 link 标记添加到包含网络应用的所有页面上

一些可选项

添加启动画面 splash screen

15300666570624

设置启动样式

15300666779957

这里是选择全屏显示,还是保留地址栏

debugger

15300675492998

Chrome 浏览器已经提供给我们一些方法和手段,直接进入 Application 板块,选择 manifest 选项卡,即可,将它添加到 Chrome 应用中。

html5里的manifest是用来缓存网页上的一些资源,跟我们PWA里的WebApp manifest 完全不是一回事

<!DOCTYPE HTML>
<html manifest="demo.appcache">
</html>

Service Worker

我们原有的整个 Web 应用,都是建立在用户能上网的前提之下的,所以一离线就只能看转圈圈了。web社区也做过很多类似的尝试,如APP Cache。但是它,几乎没有路由机制,出了BUG无法监控,现下已经在html5.1中 被干掉了

这个时候,Service workers 横空出世!!

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

service worker将遵守以下生命周期:

  • 下载
  • 安装
  • 激活

15300681075578
15300681336107

看一下实例代码

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {

    if(reg.installing) {
      console.log('Service worker installing');
    } else if(reg.waiting) {
      console.log('Service worker installed');
    } else if(reg.active) {
      console.log('Service worker active');
    }

  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
}

这段代码先做了一个特性检查,在注册之前确保 Service Worker 是支持的,
接着,我们使用 ServiceWorkerContainer.register() 函数来注册 service worker,
这就注册了一个 service worker。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        '/sw-test/star-wars-logo.jpg',
        '/sw-test/gallery/bountyHunters.jpg',
        '/sw-test/gallery/myLittleVader.jpg',
        '/sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});
  • 新增了一个 install 事件监听器,接着在事件上接了一个ExtendableEvent.waitUntil() 方法——这会确保Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成。

  • 在 waitUntil()内,我们使用了 caches.open() 方法来创建了一个叫做 v1 的新的缓存,将会是我们的站点资源缓存的第一个版本。

  • 它返回了一个创建缓存的 promise,当它 resolved的时候,我们接着会调用在创建的缓存示例上的一个方法 addAll(),这个方法的参数是一个由一组相对于 origin 的 URL 组成的数组,这些 URL 就是你想缓存的资源的列表。

  • Service Worker 的 新的标志性的存储 API — cache — 一个 service worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成key。

15300708550158

参考链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers

推送Push Notification

Push API 的出现则让推送服务具备了向 web 应用推送消息的能力,它定义了 web 应用如何向推送服务发起订阅、如何响应推送消息,以及 web 应用、应用服务器与推送服务之间的鉴权与加密机制;由于 Push API 并不依赖 web 应用与浏览器 UI 存活,所以即使是在 web 应用与浏览器未被用户打开的时候,也可以通过后台进程接受推送消息并调用 Notification API 向用户发出通知

self.addEventListener('push', event => {
    event.waitUntil(
        // Process the event and display a notification.
        self.registration.showNotification("Hey!")
    );
});

self.addEventListener('notificationclick', event => {
    // Do something with the event
    event.notification.close();
});

self.addEventListener('notificationclose', event => {
    // Do something with the event
});

PWA任重道远

  • 国内较重视 iOS,而 iOS 对PWA是十分不友好的。

  • 国内的 Android 实为「安卓」,不自带 Chrome ,其次,各厂商喜欢自己瞎加班(JB)订制各种系统,带来兼容性问题

  • Push Notification还处于襁褓阶段(还没有一个标准的协议),未来的变数较大

  • 国内的web应用入口多集中于各类APP,如微信,qq,带来的限制较多

WebAssembly

部分图片和概念来自 参考链接:https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/

15300714659625
15300714727109

布兰登·艾克:你说你们老板10天上线1个app,丧心病狂?大哥10天干了一门语言

正是因为JS的诞生显得没有那么"正式",所以带来了很多的坑点和性能上的限制。它更像一个还在建造当中的楼房,我们web开发人员不断的为它添砖加瓦,总有一天会变成摩天大楼!

什么是WebAssembly

  • WebAssembly 是由主流浏览器厂商组成的 W3C 社区团体 制定的一个新的规范。

  • 它的缩写是".wasm",.wasm 为文件名后缀,是一种新的底层安全的二进制语法。

  • 可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。

  • 能突破前端3D game 、 VR/AR 、 机器视觉、图像处理等运行速度瓶颈

我们来看一个demo:http://webassembly.org.cn/demo/Tanks/

WebAssembly 工作原理

了解WebAssembly之前,我们先大概的了解一下代码的运行机制

在代码的世界中,通常有两种方式来翻译机器语言:解释器和编译器。

如果是通过解释器,翻译是一行行地边解释边执行

15300785911993

解释器启动和执行的更快。你不需要等待整个编译过程完成就可以运行你的代码。从第一行开始翻译,就可以依次继续执行了。

可是当你运行同样的代码一次以上的时候,解释器的弊处就显现出来了。比如你执行一个循环,那解释器就不得不一次又一次的进行翻译,这是一种效率低下的表现。

编译器是把源代码整个编译成目标代码,执行时不再需要编译器,直接在支持目标代码的平台上运行。

15300786239924

它需要花一些时间对整个源代码进行编译,然后生成目标文件才能在机器上执行。对于有循环的代码执行的很快,因为它不需要重复的去翻译每一次循环。

最开始的浏览器是只有解释器的,因为解释器看起来更加适合 JavaScript。对于一个 Web 开发人员来讲,能够快速执行代码并看到结果是非常重要的。后来将编译器也加入进来,形成混合模式。

再添加一个监视器,用来监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。

加这么多东西的好处是什么呢

起初,监视器监视着所有通过解释器的代码。

15300791434221

如果同一行代码运行了几次,这个代码段就被标记成了 “warm”,如果运行了很多次,则被标记成 “hot”。

如果一段代码变成了 “warm”,那么 浏览器 就把它送到编译器去编译,并且把编译结果存储起来。--(基线编译器)

代码段的每一行都会被编译成一个“桩”(stub),同时给这个桩分配一个以“行号 + 变量类型”的索引。如果监视器监视到了执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push 出来给浏览器。

15300797943329

如果一个代码段变得 “very hot”,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。--(优化编译器)

优化编译器会做一些假设。如果某个循环中先前每次迭代的对象都有相同的形状,那么优化编译器就可以认为它以后迭代的对象的形状都是相同的。可是对于 JavaScript 从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性,这个时候,执行过程将会回到解释器或者基线编译器,叫做去优化

15300797943329

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

如果arr 是一个有 100 个整数的数组,类型确定,就很容易的派发到优化编译器中
但是JavaScript 中类型都是动态类型,sum 和 arr[i] 两个数并不保证都是整数,arr[i] 很有可能变成了string 类型,就会去优化,重新分配到解释器或者基线编译器

那是如何编译解释呢?

我们进行机器码的翻译并不是只有一种,不同的机器有不同的机器码,就像我们人类也说各种各样的语言一样,机器也“说”不同的语言。

你想要从任意一个高级语言翻译到众多汇编语言中的一种(依赖机器内部结构),其中一种方式是创建不同的翻译器来完成各种高级语言到汇编的映射。

15300804906133

这种翻译的效率实在太低了。为了解决这个问题,大多数编译器都会在中间多加一层。它会把高级语言翻译到一个低层,而这个低层又没有低到机器码这个层级。这就是中间代码( intermediate representation,IR)。

15300807406283

编译器的前端把高级语言翻译到 IR,编译器的后端把 IR 翻译成目标机器的汇编代码。

重点来了

15300808312348

WebAssembly 在什么位置呢?实际上,你可以把它看成另一种“目标汇编语言”。

每一种目标汇编语言(x86、ARM)都依赖于特定的机器结构。当你想要把你的代码放到用户的机器上执行的时候,你并不知道目标机器结构是什么样的。

而 WebAssembly 与其他的汇编语言不一样,它不依赖于具体的物理机器。可以抽象地理解成它是概念机器的机器语言,而不是实际的物理机器的机器语言。

正因为如此,WebAssembly 指令有时也被称为虚拟指令。它比 JavaScript 代码更直接地映射到机器码,它也代表了“如何能在通用的硬件上更有效地执行代码”的一种理念。所以它并不直接映射成特定硬件的机器码。

那为什么不直接用JS,这么麻烦用WebAssembly

15300826048069

这是JS的性能使用分布情况

  • Parsing——表示把源代码变成解释器可以运行的代码所花的时间;

  • Compiling + optimizing——表示基线编译器和优化编译器花的时间。一些优化编译器的工作并不在主线程运行,不包含在这里。

  • Re-optimizing——包括重优化的时间、抛弃并返回到基线编译器的时间。

  • Execution——执行代码的时间

  • Garbage collection——垃圾回收,清理内存的时间

15300828887458

这是WebAssmbly与JS的对比

wasm的优势是本身就是通过编译器并优化过后的二进制文件,可以直接转换为机器码,省去了Javascript需要解析,优化的工作,所以在加载和执行上本身就具有优势

具体优势点

  • 文件获取

WebAssembly 比 JavaScript 的压缩率更高,所以文件获取也更快。即便通过压缩算法可以显著地减小 JavaScript 的包大小,但是压缩后的 WebAssembly 的二进制代码依然更小。

这就是说在服务器和客户端之间传输文件更快,尤其在网络不好的情况下。

  • 解析

JavaScript 源代码到达浏览器时被解析成了AST (抽象语法树)。
解析过后 AST (抽象语法树)就变成了中间代码(叫做字节码),提供给 JS 引擎编译。

而 WebAssembly 则不需要这种转换,因为它本身就是中间代码。它要做的只是解码并且检查确认代码没有错误就可以了。

  • 优化

浏览器的JIT会反复地进行“抛弃优化代码<->重优化”过程,
比如当循环中发现本次循环所使用的变量类型和上次循环的类型不一样,或者原型链中插入了新的函数,都会使 JIT 抛弃已优化的代码,进行重优化。

在 WebAssembly 中,类型都是确定了的,所以 JIT 不需要根据变量的类型做优化假设。也就是说 WebAssembly 没有重优化阶段。

  • 垃圾回收

在JS中的内存概念是非常模糊的,因为JS并不需要申请内存,所有内存都有JS自动分配,因为它不可控,所以清理垃圾的时候会带来性能开销

WebAssembly不需要垃圾回收,内存操作都是手动控制的(像 C、C++一样)。这对于开发者来讲确实增加了些开发成本,不过这也使代码的执行效率更高。

如何实现一个WebAssembly demo

参考链接:http://webassembly.org.cn/getting-started/developers-guide/

15300836901005

int square (int x) {
  return x * x;
}
emcc math.c -s WASM=1 -o index.html

15300837997256

从0开始完成刚刚坦克大战的例子

15300845024215

15300845134273

4.大功告成

谢谢大家~

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2019/03/30 - 跨平台技术演进

前言

大家好,我是simbawu,关于这篇文章,有问题欢迎来这里讨论。

随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G的快速发展,未来越来越多的“端”也会如雨后春笋般快速兴起。而“快”作为互联网的生存之道,为了占领市场,企业也会积极跟进,快速布局。同一个应用,各个“端”独立开发,不仅开发周期长,而且人员成本高。同时,作为技术人员,也不应该满足于这种重复、低能的工作状态。在这样的形势下,跨平台的技术方案也受到越来越多人和企业的关注。接下来,我将从原理、优缺点等方面为大家分享《跨平台技术演进》。

H5

说到跨平台,没人不知道H5。不管是在Mac、Windows、Linux、iOS、Android还是其他平台,只要给一个浏览器,连“月球”上它都能跑。

浏览器架构

下面,我们来看看让H5如此横行霸道的浏览器的架构:

浏览器架构

  • User Interface 用户界面:提供用户与浏览器交互
  • Browser Engine 浏览器引擎:控制渲染引擎与JS解释器
  • Rendering Engine 渲染引擎:负责页面渲染
  • JavaScript Interpreter JS解释器:执行JS代码,输出结果给渲染引擎
  • Networking 网络工作组:处理网络请求
  • UI Backend UI后端:绘制窗口小部件
  • Data Storage 数据存储:管理用户数据

浏览器由以上7个部分组成,而“渲染引擎”是性能优化的重中之重,一起了解其中的渲染原理。

渲染引擎原理

不同的浏览器内核不同,渲染过程会不太一样,但主要流程还是一致的。

WebKit 主流程

分为下面6步骤:

  1. HTML解析出DOM Tree
  2. CSS解析出CSSOM
  3. DOM Tree与CSSOM关联生成Render Tree
  4. Layout 根据Render Tree计算每个节点的尺寸、位置
  5. Painting 根据计算好的信息绘制整个页面的像素信息
  6. Composite 将多个复合图层发送给GPU,GPU会将各层合成,然后显示在屏幕上。

从以上6步,我们可以总结渲染优化的要点:

  • Layout在浏览器渲染过程中比较耗时,应尽可能避免重排的产生
  • 复合图层占用内存比重非常高,可采用减小复合图层进行优化

以上就是浏览器端的内容。但H5作为跨平台技术的载体,是如何与不同平台的App进行交互的呢?这时候JSBridge就该出场了。

JSBridge原理

JSBridge,顾名思义,是JS和Native之间的桥梁,用来进行JS和Native之间的通信。

JS

通信分为以下两个维度:

  • JavaScript 调用 Native,有两种方式:

    1. 拦截URL Scheme:URL Scheme是一种类似于url的链接(boohee://goods/876898),当web前端发送URL Scheme请求之后,Native 拦截到请求并根据URL Scheme进行相关操作。
    2. 注入API:通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
  • Native 调用 JavaScript
    JavaScript暴露一个对象如JSBridge给window,让Native能直接访问。

那么App内加载H5的过程是什么样的呢?

App打开H5过程

打开H5分为4个阶段:

  1. 交互无反馈
  2. 打开页面 白屏
  3. 请求API,处于loading状态
  4. 出现数据,正常展现

这四步,对应的过程如上图所以,我们可以针对性的做性能优化。

优缺点分析

下面,我们进行H5的优缺点分析:

优点

  • 跨平台:只要有浏览器,任何平台都可以访问
  • 开发成本低:生态成熟,学习成本低,调试方便
  • 迭代速度快:无需审核,及时响应,用户可毫无感知使用最新版

缺点

  • 性能问题:在反应速度、流畅度、动画方面远不及原生
  • 功能问题:对摄像头、陀螺仪、麦克风等硬件支持较差

虽然H5目前还存在不足,但随着PWA、WebAssembly等技术的进步,相信H5在未来能够得到越来也好的发展。

小程序

2018年是微信小程序飞速发展的一年,19年,各大厂商快速跟进,已经有了很大的影响力。下面,我们以微信小程序为例,分析小程序的技术架构。

小程序跟H5一样,也是基于Webview实现。但它包含View视图层、App Service逻辑层两部分,分别独立运行在各自的WebView线程中。

View

可以理解为h5的页面,提供UI渲染。由WAWebview.js来提供底层的功能,具体如下:

  • 消息通信封装为WeixinJSBridge
  • 日志组件Reporter封装
  • wx api(UI相关)
  • 小程序组件实现和注册
  • VirtualDOM,Diff和Render UI实现
  • 页面事件触发

每个窗口都有一个独立的WebView进程,因此微信限制不能打开超过5个层级的页面来保障用户体验。

App Service

提供逻辑处理、数据请求、接口调用。由WAService.js来提供底层的功能,具体如下:

  • 日志组件Reporter封装
  • wx api
  • App,Page,getApp,getCurrentPages等全局方法
  • AMD模块规范的实现

运行环境:

  • iOS:JavaScriptCore
  • Andriod:X5内核,基于Mobile Chrome 53/57
  • DevTool:nwjs Chrome 内核

仅有一个WebView进程

View & App Service通信

视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层将触发的事件通知到逻辑层进行业务处理。

优缺点分析

优点

  • 预加载WebView,准备新页面渲染
  • View层和逻辑层分离,通过数据驱动,不直接操作DOM
  • 使用Virtual DOM,进行局部更新
  • 组件化开发

缺点

  • 仍使用WebView渲染,并非原生渲染,体验不佳
  • 不能运行在非微信环境内
  • 没有window、document对象,不能使用基于浏览器的JS库
  • 不能灵活操作 DOM,无法实现较为复杂的效果
  • 页面大小、打开页面数量都受到限制

既然WebView性能不佳,那有没有更好的方案呢?下面我们看看React Native。

React Native

RN的理念是在不同平台上编写基于React的代码,实现Learn once, write anywhere。

Virtual DOM在内存中,可以通过不同的渲染引擎生成不同平台下的UI,JS和Native之间通过Bridge通信

React Native 工作原理

在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中,而在 React Native 框架中,JSX 源码通过 React Native 框架编译后,与Native原生的UI组件进行映射,用原生代替DOM元素来渲染,在UI渲染上非常接近Native App。

React Native 与Native平台通信

  • React Native用JavaScriptCore作为JS的解析引擎,在Android上,需要应用自己附带JavaScriptCore,iOS上JavaScriptCore属于系统的一部分,不需要应用附带。
  • 用Bridge将JS和原生Native Code连接起来。Native和 JavaScript 两端都保存了一份配置表,里面标记了所有Native暴露给 JavaScript 的模块和方法。交互通过传递 ModuleId、MethodId 和 Arguments 进行。

优缺点分析

优点

  • 垮平台开发:相比原生的ios 和 android app各自维护一套业务逻辑大同小异的代码,React Native 只需要同一套javascript 代码就可以运行于ios 和 android 两个平台,在开发、测试和维护的成本上要低很多。
  • 快速编译:相比Xcode中原生代码需要较长时间的编译,React Native 采用热加载的即时编译方式,使得App UI的开发体验得到改善,几乎做到了和网页开发一样随时更改,随时可见的效果。
  • 快速发布:React Native 可以通过 JSBundle 即时更新 App。相比原来冗长的审核和上传过程,发布和测试新功能的效率大幅提高。
  • 渲染和布局更高效:React Native摆脱了WebView的交互和性能问题,同时可以直接套用网页开发中的css布局机制。脱了 autolayout 和 frame 布局中繁琐的数学计算,更加直接简便。

缺点

  • 动画性能差:React Native 在动画效率和性能的支持还存在一些问题,性能上不如原生Api。
  • 不能完全屏蔽原生平台:就目前的React Native 官方文档中可以发现仍有部分组件和API都区分了Android 和 IOS 版本,即便是共享组件,也会有平台独享的函数。也就是说仍不能真正实现严格意义上的“一套代码,多平台使用”。另外,因为仍对ios 和android的原生细节有所依赖,所以需要开发者若不了解原生平台,可能会遇到一些坑。
  • 生态不完善:缺乏很多基本控件,第三方开源质量良莠不齐

展望未来

虽然RN还存在不足,但RN新版本已经做了如下改进,并且RN团队也在积极准备大版本重构,能否成为开发者们所信赖的跨平台方案,让我们拭目以待。

  1. 改变线程模式。UI 更新不再同时需要在三个不同的线程上触发执行,而是可以在任意线程上同步调用 JavaScript 进行优先更新,同时将低优先级工作推出主线程,以便保持对 UI 的响应。
  2. 引入异步渲染能力。允许多个渲染并简化异步数据处理。
  3. 简化 JSBridge,让它更快、更轻量。

既然React Native在渲染方面还摆脱不了原生,那有没有一种方案是直接操控GPU,自制引擎渲染呢,我们终于迎来了Flutter!

Flutter

Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。

Flutter架构原理

  • Framework:由Dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。此部分的核心代码是:flutter仓库下的flutter package,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。
  • Engine:由C++实现,主要包括:Skia,Dart和Text。
    • Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。其已作为Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS等其他众多产品的图形引擎,支持平台还包括Windows7+,macOS 10.10.5+,iOS8+,Android4.1+,Ubuntu14.04+等。Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体。
    • Dart部分主要包括:Dart Runtime,Garbage Collection(GC),如果是Debug模式的话,还包括JIT(Just In Time)支持。Release和Profile模式下,是AOT(Ahead Of Time)编译成了原生的arm代码,并不存在JIT部分。
    • Text即文本渲染,其渲染层次如下:衍生自minikin的libtxt库(用于字体选择,分隔行)。HartBuzz用于字形选择和成型。
  • Embedder:是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

Dart优势

很多人会好奇,为什么Flutter要用Dart,而不是用JavaScript开发,这里列下Dart的优势

  • Dart 的性能更好。Dart在 JIT模式下,速度与 JavaScript基本持平。但是 Dart支持 AOT,当以 AOT模式运行时,JavaScript便远远追不上了。速度的提升对高帧率下的视图数据计算很有帮助。
  • Native Binding。在 Android上,v8的 Native Binding可以很好地实现,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基础框架的代码模式就很难统一了。而 Dart的 Native Binding可以很好地通过 Dart Lib实现。
  • Fuchsia OS。Fuchsia OS内置的应用浏览器就是使用 Dart语言作为 App的开发语言。

优缺点分析

优点

  • 性能强大:在两个平台上重写了各自的UIKit,对接到平台底层,减少UI层的多层转换,UI性能可以比肩原生
  • 优秀的语言特性:参考上面Dart优势分析
  • 路由设计优秀:Flutter的路由传值非常方便,push一个路由,会返回一个Future对象(也就是Promise对象),使用await或者.then就可以在目标路由pop,回到当前页面时收到返回值。

缺点

  • 优点即缺点,Dart 语言的生态小,精通成本比较高
  • UI控件API设计不佳
  • 与原生融合障碍很多,不利于渐进式升级

总结

移动互联网的普及和快速发展,跨平台技术风起云涌,这也是技术发展过程中的必经之路,等浪潮退去,才知道谁在裸泳。我个人更看好H5或类H5方案,给它一个浏览器,连“月球”都能跑,这才是真正的跨平台,其他都是浮云。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2019/01/14 - 从前端的角度理解缓存

缓存的概念分很多种,本次讨论的主要就是前端缓存中的Http缓存。

缓存是怎么回事

前端发送请求主要经历以下三个过程,请求->处理->响应。
如果有多次请求就需要重复执行这个过程。

重复请求的过程

以下是一个重复请求的流程图:

重复请求

从以上的流程图可以看书,如果用户重复请求同一资源的话,会对服务器资源造成浪费,服务器重复读取资源,发送给浏览器后浏览器重复下载,造成不必要的等待与消耗。

缓存读取的过程

缓存读取就是浏览器在向服务器请求资源之前,先查询一下本地缓存中是否存在需要的资源,如果存在,那便优先从缓存中读取。当缓存不存在或者过期,再向服务器发送请求。

缓存读取

如何开启Http缓存并对缓存进行设置,是本次讨论的关键。

缓存的类型

浏览器有如下常见的几个字段:

  1. expires: 设置缓存过期的时间
  2. private: 客户端可以缓存
  3. public: 客户端和代理服务器都可缓存
  4. max-age=xxx: 缓存的内容将在 xxx 秒后失效
  5. no-cache: 需要使用对比缓存来验证缓存数据
  6. no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发
  7. last-modified: 内容上次被修改的时间
  8. Etag: 文件的特殊标识

强制缓存和协商缓存

缓存方法可以分为强制缓存与协商缓存。

从字面理解,强制缓存的方式简单粗暴,给cache设置了过期时间,超过这个时间之后cache过期需要重新请求。上述字段中的expirescache-control中的max-age都属于强制缓存。

协商缓存根据一系列条件来判断是否可以使用缓存。

强制缓存优先级高于协商缓存

强制缓存

expires

expires给浏览器设置了一个绝对时间,当浏览器时间超过这个绝对时间之后,重新向服务器发送请求。

Expires: Fri, 04 Jan 2019 12:00:00 GMT

这个方法简单直接,直接设定一个绝对的时间 (当前时间+缓存时间)。但是也存在隐患,例如浏览器当前时间是可以进行更改的,更改之后expires设置的绝对时间相对不准确,cache可能会出现长久不过期或者很快就过期的情况。

cache-control: max-age

为了解决expires存在的问题,Http1.1版本中提出了cache-control: max-age,该字段与expires的缓存思路相同,都是设置了一个过期时间,不同的是max-age设置的是相对缓存时间开始往后多久,因此不存在受日期不准确情况的影响。

但是强制缓存存在一个问题,该缓存方式优先级高,如果在过期时间内缓存的资源在服务器上更新了,客服端不能及时获取最新的资源。

协商缓存

协商缓存解决了无法及时获取更新资源的问题。以下两组字段,都可以对资源做标识,由服务器做分析,如果未进行更新,那返回304状态码,从缓存中读取资源,否则重新请求资源。

last-modify

last-modify告知了客户端上次修改该资源的时间,

Last-Modified: Wed, 02 Jan 2019 03:06:03 GMT

浏览器将这个值记录在if-modify-since中(浏览器自动记录了该字段信息),下一次请求相同资源时,与服务器返回的last-modify进行比对,如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

last-modify以秒为单位进行更新,如果小于该单位高频进行更新的话,不适合采用该方法。

ETag

ETag是对资源的特殊标识

Etag: W/"e563df87b65299122770e0a84ada084f"

请求该资源成功之后,将返回的ETag存入if-none-match字段中(浏览器自动记录了该字段信息),同样在请求资源时传递给服务器,服务器查询该编码对应的资源有无更新,无更新返回304状态,更新返回200并重新请求。

以下有个小例子,查询书籍更新:

当书籍信息查询之后,再次查询,服务器根据资源的ETag查询得知该资源没有进行更新,返回304状态码。

书籍信息(旧)

更新返回的数据信息,再次查询,返回200状态码,重新进行请求:

书籍信息(新)

从返回的Request Headers可以看出,再次请求时,浏览器自动发送了If-Modified-SinceIf-None-Match两个字段,浏览器根据这两个字段中(If-None-Match 优先级大于 If-Modified-Since)来判断是否修改了资源。

image

ETag如何计算

ETag是针对某个文件的特殊标识,服务器默认采用SHA256算法生成。也可以采用其他方式,保证编码的唯一性即可。

缓存的优先级

根据上文优缺点的比对,可以得出以下的优先级顺序:

Cache-Control > Expires > ETag > Last-Modified

如果资源需要用到强制缓存,Cache-Control相对更加安全,协商缓存中利用ETag查询更新更加全面。

缓存的判断流程

图片来源:浏览器缓存机制详解

缓存存储在哪

disk cache

disk cache为存储在硬盘中的缓存,存储在硬盘中的资源相对稳定,不会随着tab或浏览器的关闭而消失,可以用来存储大型的,需长久使用的资源。

当硬盘中的资源被加载时,内存中也存储了该资源,当下次改资源被调用时,会优先从memory cache中读取,加快资源的获取。

memory cache

memory cache即存储在内存中的缓存,内存中的内容会随着tab的关闭而释放。

当接口状态返回304时,资源默认存储在memory cache中,当页面关闭后,重新打开需要再次请求。

这两种存储方式的区别可以参考该回答

When you visit a URL in Chrome, the HTML and the other assets(like images) on the page are stored locally in a memory and a disk cache. Chrome will use the memory cache first because it is much faster, but it will also store the page in a disk cache in case you quit your browser or it crashes, because the disk cache is persistent.

当您访问chrome中的URL时,页面上的HTML和其他资产(如图像)将本地存储在内存和磁盘缓存中。Chrome将首先使用内存缓存,因为它的速度快得多,但它也会将页面存储在磁盘缓存中,以防您退出浏览器或它崩溃,因为磁盘缓存是持久的。

为什么有的资源一会from disk cache,一会from memory cache

三级缓存原理

  1. 先去内存看,如果有,直接加载
  2. 如果内存没有,择取硬盘获取,如果有直接加载
  3. 如果硬盘也没有,那么就进行网络请求
  4. 加载到的资源缓存到硬盘和内存,下次请求可以快速从内存中获取到

为什么有的请求状态码返回200,有的返回304

200 from memory cache

不访问服务器,直接读缓存,从内存中读取缓存。此时的数据时缓存到内存中的,当关闭进程后,也就是浏览器关闭以后,数据将不存在。

但是这种方式只能缓存派生资源。

200 from disk cache

不访问服务器,直接读缓存,从磁盘中读取缓存,当关闭进程时,数据还是存在。

这种方式也只能缓存派生资源

304 Not Modified

访问服务器,发现数据没有
更新,服务器返回此状态码。然后从缓存中读取数据。

薄荷应用

举一个简单的小🌰,以薄荷的减肥群页面为讨论对象,查看一下资源加载的情况:

薄荷图片缓存

这些图片都是从硬盘中读取,因为没有在内存中获取到响应的资源,当我们刷新页面时,这个资源因为从硬盘中读取时,也存储到了内存中,再次获取就是从内存中获取了:
薄荷图片缓存2

当我们没有关闭页面时,内存中的资源始终存在,重新打开则内存释放。

CDN缓存

CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的Cache-control: max-age的字段来设置CDN边缘节点数据缓存时间。

当客户端向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。

如何合理应用缓存

强制缓存优先级最高,并且资源的改动在缓存有效期内都不会对缓存产生影响,因此该方法适用于大型且不易修改的的资源文件,例如第三方CSS、JS文件或图片资源,文件后可以加上hash进行版本的区分。建议将此类大型资源存入disk cache,因为存在硬盘中的文件资源不易丢失。

协商缓存灵活性高,适用于数据的缓存,根据上述方法的对比,采用Etag标识进行对比灵活度最高,并考虑将数据存入内存中,因为内存加载速最快,并且数据体积小,不会占用大量内存资源。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/04/18 - 基于socket.io快速实现一个实时通讯应用

随着web技术的发展,使用场景和需求也越来越复杂,客户端不再满足于简单的请求得到状态的需求。实时通讯越来越多应用于各个领域。

HTTP是最常用的客户端与服务端的通信技术,但是HTTP通信只能由客户端发起,无法及时获取服务端的数据改变。只能依靠定期轮询来获取最新的状态。时效性无法保证,同时更多的请求也会增加服务器的负担。

WebSocket技术应运而生。

WebSocket概念

不同于HTTP半双工协议,WebSocket是基于TCP 连接的全双工协议,支持客户端服务端双向通信。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

HTTP与websocket对比

实现

原生实现

WebSocket对象一共支持四个消息 onopen, onmessage, onclose和onerror。

建立连接

通过javascript可以快速的建立一个WebSocket连接:

    var Socket = new WebSocket(url, [protocol] );

以上代码中的第一个参数url, 指定连接的URL。第二个参数 protocol是可选的,指定了可接受的子协议。

同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。

  1. 当Browser和WebSocketServer连接成功后,会触发onopen消息。
    Socket.onopen = function(evt) {};
  1. 如果连接失败,发送、接收数据失败或者处理数据出现错误,browser会触发onerror消息。
    Socket.onerror = function(evt) { };
  1. 当Browser接收到WebSocketServer端发送的关闭连接请求时,就会触发onclose消息。
    Socket.onclose = function(evt) { };

收发消息

  1. 当Browser接收到WebSocketServer发送过来的数据时,就会触发onmessage消息,参数evt中包含server传输过来的数据。
    Socket.onmessage = function(evt) { };
  1. send用于向服务端发送消息。
    Socket.send();

socket

WebSocket是跟随HTML5一同提出的,所以在兼容性上存在问题,这时一个非常好用的库就登场了——Socket.io

socket.io封装了websocket,同时包含了其它的连接方式,你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也必须同样适用。

socket.io是基于 Websocket 的Client-Server 实时通信库。

socket.io底层是基于engine.io这个库。engine.io为 socket.io 提供跨浏览器/跨设备的双向通信的底层库。engine.io使用了 Websocket 和 XHR 方式封装了一套 socket 协议。在低版本的浏览器中,不支持Websocket,为了兼容使用长轮询(polling)替代。

engine.io

API文档

Socket.io允许你触发或响应自定义的事件,除了connect,message,disconnect这些事件的名字不能使用之外,你可以触发任何自定义的事件名称。

建立连接

    const socket = io("ws://0.0.0.0:port"); // port为自己定义的端口号
    let io = require("socket.io")(http);
    io.on("connection", function(socket) {})

消息收发

一、发送数据

    socket.emit(自定义发送的字段, data);

二、接收数据

    socket.on(自定义发送的字段, function(data) {
        console.log(data);
    })

断开连接

一、全部断开连接

    let io = require("socket.io")(http);
    io.close();

二、某个客户端断开与服务端的链接

    // 客户端
    socket.emit("close", {});
    // 服务端
    socket.on("close", data => {
        socket.disconnect(true);
    });

room和namespace

有时候websocket有如下的使用场景:1.服务端发送的消息有分类,不同的客户端需要接收的分类不同;2.服务端并不需要对所有的客户端都发送消息,只需要针对某个特定群体发送消息;

针对这种使用场景,socket中非常实用的namespace和room就上场了。

先来一张图看看namespace与room之间的关系:

namespace与room的关系

namespace

服务端

    io.of("/post").on("connection", function(socket) {
        socket.emit("new message", { mess: `这是post的命名空间` });
    });
    
    io.of("/get").on("connection", function(socket) {
        socket.emit("new message", { mess: `这是get的命名空间` });
    });

客户端

    // index.js
    const socket = io("ws://0.0.0.0:****/post");
    socket.on("new message", function(data) {
        console.log('index',data);
    }
    
    //message.js
    const socket = io("ws://0.0.0.0:****/get");
    socket.on("new message", function(data) {
        console.log('message',data);
    }

room

客户端

    //可用于客户端进入房间;
    socket.join('room one');
    //用于离开房间;
    socket.leave('room one');

服务端

    io.sockets.on('connection',function(socket){
        //提交者会被排除在外(即不会收到消息)
        socket.broadcast.to('room one').emit('new messages', data);
        // 向所有用户发送消息
        io.sockets.to(data).emit("recive message", "hello,房间中的用户");      
    }

用socket.io实现一个实时接收信息的例子

终于来到应用的阶段啦,服务端用node.js模拟了服务端接口。以下的例子都在本地服务器中实现。

服务端

先来看看服务端,先来开启一个服务,安装expresssocket.io

安装依赖

    npm install --Dev express
    npm install --Dev socket.io

构建node服务器

    let app = require("express")();
    let http = require("http").createServer(handler);
    let io = require("socket.io")(http);
    let fs = require("fs");
    
    http.listen(port); //port:输入需要的端口号
    
    function handler(req, res) {
      fs.readFile(__dirname + "/index.html", function(err, data) {
        if (err) {
          res.writeHead(500);
          return res.end("Error loading index.html");
        }
    
        res.writeHead(200);
        res.end(data);
      });
    }
    
    io.on("connection", function(socket) {
        console.log('连接成功');
        //连接成功之后发送消息
        socket.emit("new message", { mess: `初始消息` });
        
    });

客户端

核心代码——index.html(向服务端发送数据)

    <div>发送信息</div>
    <input placeholder="请输入要发送的信息" />
    <button onclick="postMessage()">发送</button>
    // 接收到服务端传来的name匹配的消息
    socket.on("new message", function(data) {
      console.log(data);
    });
    
    function postMessage() {
      socket.emit("recive message", {
        message: content,
        time: new Date()
      });
      messList.push({
        message: content,
        time: new Date()
      });
    }

核心代码——message.html(从服务端接收数据)

    socket.on("new message", function(data) {
      console.log(data);
    });

效果

实时通讯效果
实时通讯效果

客户端全部断开连接
全部断开

某客户端断开连接
某客户端断开连接

namespace应用
namespace

加入房间
加入房间

离开房间
离开房间

框架中的应用

npm install socket.io-client

    const socket = require('socket.io-client')('http://localhost:port');

    componentDidMount() {
        socket.on('login', (data) => {
            console.log(data)
        });
        socket.on('add user', (data) => {
            console.log(data)
        });
        socket.on('new message', (data) => {
            console.log(data)
        });
    }

分析webSocket协议

Headers

Headers

请求包

    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
    Cache-Control: no-cache
    Connection: Upgrade
    Cookie: MEIQIA_VISIT_ID=1IcBRlE1mZhdVi1dEFNtGNAfjyG; token=0b81ffd758ea4a33e7724d9c67efbb26; io=ouI5Vqe7_WnIHlKnAAAG
    Host: 0.0.0.0:2699
    Origin: http://127.0.0.1:5500
    Pragma: no-cache
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Sec-WebSocket-Key: PJS0iPLxrL0ueNPoAFUSiA==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1

请求包说明:

  • 必须是有效的http request 格式;
  • HTTP request method 必须是GET,协议应不小于1.1 如: Get / HTTP/1.1;
  • 必须包括Upgrade头域,并且其值为“websocket”,用于告诉服务器此连接需要升级到websocket;
  • 必须包括”Connection” 头域,并且其值为“Upgrade”;
  • 必须包括”Sec-WebSocket-Key”头域,其值采用base64编码的随机16字节长的字符序列;
  • 如果请求来自浏览器客户端,还必须包括Origin头域 。 该头域用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接;
  • 必须包括“Sec-webSocket-Version”头域,是当前使用协议的版本号,当前值必须是13;
  • 可能包括“Sec-WebSocket-Protocol”,表示client(应用程序)支持的协议列表,server选择一个或者没有可接受的协议响应之;
  • 可能包括“Sec-WebSocket-Extensions”, 协议扩展, 某类协议可能支持多个扩展,通过它可以实现协议增强;
  • 可能包括任意其他域,如cookie.

应答包

应答包说明:

    Connection: Upgrade
    Sec-WebSocket-Accept: I4jyFwm0r1J8lrnD3yN+EvxTABQ=
    Sec-WebSocket-Extensions: permessage-deflate
    Upgrade: websocket
  • 必须包括Upgrade头域,并且其值为“websocket”;
  • 必须包括Connection头域,并且其值为“Upgrade”;
  • 必须包括Sec-WebSocket-Accept头域,其值是将请求包“Sec-WebSocket-Key”的值,与”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″这个字符串进行拼接,然后对拼接后的字符串进行sha-1运算,再进行base64编码,就是“Sec-WebSocket-Accept”的值;
  • 应答包中冒号后面有一个空格;
  • 最后需要两个空行作为应答包结束。

请求数据

    EIO: 3
    transport: websocket
    sid: 8Uehk2UumXoHVJRzAAAA
  • EIO:3 表示使用的是engine.io协议版本3
  • transport 表示传输采用的类型
  • sid: session id (String)

Frames

WebSocket协议使用帧(Frame)收发数据,在控制台->Frames中可以查看发送的帧数据。

其中帧数据前的数字代表什么意思呢?

这是 Engine.io协议,其中的数字是数据包编码:

[]

  • 0 open——在打开新传输时从服务器发送(重新检查)

  • 1 close——请求关闭此传输,但不关闭连接本身。

  • 2 ping——由客户端发送。服务器应该用包含相同数据的乓包应答

    客户端发送:2probe探测帧

  • 3 pong——由服务器发送以响应ping数据包。

    服务器发送:3probe,响应客户端

  • 4 message——实际消息,客户端和服务器应该使用数据调用它们的回调。

  • 5 upgrade——在engine.io切换传输之前,它测试,如果服务器和客户端可以通过这个传输进行通信。如果此测试成功,客户端发送升级数据包,请求服务器刷新其在旧传输上的缓存并切换到新传输。

  • 6 noop——noop数据包。主要用于在接收到传入WebSocket连接时强制轮询周期。

实例

发送数据

接收数据

以上的截图是上述例子中数据传输的实例,分析一下大概过程就是:

  1. connect握手成功
  2. 客户端会发送2 probe探测帧
  3. 服务端发送响应帧3probe
  4. 客户端会发送内容为5的Upgrade帧
  5. 服务端回应内容为6的noop帧
  6. 探测帧检查通过后,客户端停止轮询请求,将传输通道转到websocket连接,转到websocket后,接下来就开始定期(默认是25秒)的 ping/pong
  7. 客户端、服务端收发数据,4表示的是engine.io的message消息,后面跟随收发的消息内容

为了知道Client和Server链接是否正常,项目中使用的ClientSocket和ServerSocket都有一个心跳的线程,这个线程主要是为了检测Client和Server是否正常链接,Client和Server是否正常链接主要是用ping pong流程来保证的。

该心跳定期发送的间隔是socket.io默认设定的25m,在上图中也可观察发现。该间隔可通过配置修改。

socket通信流程

参考engine.io-protocol

参考文章

Web 实时推送技术的总结
engine.io 原理详解

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/11/20 - webpack原理与实践(二):实现一个webpack插件

[TOC]

webpack原理与实践(二):实现一个webpack插件

关于 loaderplugin

  1. webpack 本身只能处理js文件。那如何处理如 css内联图像html 等这些文件了呢。这就需要用 loader 来进行转化。
  2. 通常 loader 功能比较单一,只专注于语言的转化。但是我们会有像压缩,分离文件这样的需求,这就需要通过插件来实现

实现一个插件

插件本身为一个构造函数,除了自己定义的方法外,会有一个 apply 方法 , apply 方法中传入全局唯一的 compiler 对象。

基本结构

class FileFilterPlugin {
    constructor(options){
        this.options = options;
    }
    apply(compiler) {
        
    }
}
  1. 插件 options 是你在使用插件的时候new 一个插件时传入的配置。通常做法是你可以有一些默认的配置,通过 Object.assign 来合并传入的配置和默认的配置,来得到最终的配置项。
  2. 第二个重点就是 apply 方法,该方法会传入一个 compile 对象。compile 对象和 compilation 对象是 webpack 打包过程中最重要的两个对象。他们都继承自 Tapable 关于,通过 Tapable 注入钩子进行流程管理。compile 对象每一次编译全局唯一,并且包含了compilation对象。一个 compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。compilation 对象也提供了很多事件回调供插件做扩展。在开发模式下,每次热更新都会生成一个新的compilation对象。

准备工作

前一篇文章中已经讲过 webpack 执行的一个流程了。我们需要知道插件应该挂载在执行过程中的哪个阶段,各个阶段都做了什么。这里就给大家贴几张总结的比较到位的图(来自《深入浅出webpack》):

image
image
image

然后你需要了解:

  • 你需要做的操作对应挂载在 compile 的哪个钩子以;
  • 这个钩子的触发方式有哪些需要选择哪一种, 注册在这个钩子下的插件时如何执行的
  • 下面这段代码截取compile对象的 hooks ,挂载在这些钩子上的插件如何执行取决于 Tapable, 关于 Tapable 的各种行为和原理, 仍然推荐这篇文章: 传送门
class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			/** @type {SyncBailHook<Compilation>} */
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<Stats>} */
			done: new AsyncSeriesHook(["stats"]),
			/** @type {AsyncSeriesHook<>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<Compiler>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compiler>} */
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			afterEmit: new AsyncSeriesHook(["compilation"]),

			/** @type {SyncHook<Compilation, CompilationParams>} */
			thisCompilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<Compilation, CompilationParams>} */
			compilation: new SyncHook(["compilation", "params"]),
			......
			}
	    
	}
    
}

在插件apply 方法的回调中我们可以传入 compilation 对象. 如果是异步, 通常还会传入一个回调函数,对资源进行处理.

例子

下面会根据实际开发中的问题举一个完整的例子.

  • 问题: 在开发小程序的 webpack 脚手架过程中我们遇到一个问题: 我们使用了 scss 来编写我们的样式, 但是在输出的文件中我们需要的是 wxss 文件. 首先我们通过 file-loader 已经完成了 css 文件到wxss 文件的转化,但是还有一个问题: 我们输出的文件中包含了 scss 文件, 需要在输出的时候去掉这个文件.

首先是第一中解决方案.我们找到了一个暴露 webpack 钩子的插件: event-hooks-webpack-plugin
该插件通过传入调用的钩子名称和相应的回调函数,在回调函数中执行钩子对应阶段可以执行的操作.插件本身很强大,并且代码很简单,但是需要你了解webpack的原理才能使用,也就是有一定的门槛.我先把我们的用法粘贴出来:

new EventHooksPlugin({
  emit: (compilation) => {
    // compilation.chunks 存放所有代码块,是一个数组
    compilation.chunks.forEach(function(chunk) {
      // chunk 代表一个代码块
      chunk.files.forEach(function(filename) {
        // compilation.assets 存放当前所有即将输出的资源,是一个对象
        let regex = /\.scss$/;
        if (regex.test(filename)) {
          delete compilation.assets[filename];
        }
      });
    });
  }
})

这里我们调用的钩子是 emit 从上文粘贴的对该钩子的介绍我们知道这是最后一个可以改变输出文件的钩子.传入的回调中我们对 scss 文件做了删除处理.到这里我们的目标实现了.但是同时也在思考: 本身这个插件使用时有门槛的,我们能不能自己写一个简单的插件来替代他,
让配置变得简单,不用了解webpack内部的实现就能使用.于是我们自己写了一个插件:

module.exports = class FileFilerPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.emit.tap(' FileFilerPlugin', compilation => {
        // compilation.chunks 存放所有代码块,是一个数组
        compilation.chunks.forEach((chunk) => {
        // chunk 代表一个代码块
          chunk.files.forEach(function(filename) {
            // compilation.assets 存放当前所有即将输出的资源,是一个对象
            // let regex = /\.scss$/;
            let regex = this.options.deleteFileReg
            if (regex.test(filename)) {
              delete compilation.assets[filename];
            }
          });
        });
      })
    }
};

如上,我们只需要在配置中传入需要删除的文件的正则表达式就能实现目标:

 new FileFilterPlugin({
      deleteFileReg: /\.scss$/
    }),

这个插件的实际上就是把我们第一个插件的配置转移到了插件源码中进行实现.但是针对这个场景的使用就变得简单了很多.

参考

  • <<深入浅出webpack>>
  • 官方文档(注意文档目前采用还是老版本插件的写法, 新版钩子都放到了hooks里面,大多数插件也对两种写法做了兼容)

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/10/15- 【从前端到全栈】- koa快速入门指南

前言

随着技术的不断发展,前端工程师也被赋予了越来越多的职责。不再是从前只需要切个图,加个css样式就能完成任务的切图仔了。接下来这篇文章,完成一个简单的登录注册,能让你快速上手,成为一个‘小全栈工程师’,here we go !

15371488705139

koa快速开始

安装

  • 因为node.js v7.6.x已经完全支持async/await语法,所以请保证node的版本在7.6以上
  • 推荐一个node的多版本管理工具:nvm。如何安装这里不再赘述,网上的教程有很多
// 初始化package.json
npm init

// 安装koa2 
npm install koa

一个hello world

新建一个index.js,敲上以下代码

//index.js

const Koa = require('koa')
const app = new Koa()

app.use( async (ctx, next) => {
  ctx.response.body = '你好,我是内地吴彦祖'
})

app.listen(3333, ()=>{
  console.log('server is running at http://localhost:3333')
})

在我们的命令行敲上

node index.js

就可以看到运行结果啦:

15371507388772

几个核心概念

中间件好基友ctx和next

在上面的代码中,我们可以看到app.use后面使用了2个参数,ctxnext,下面我们介绍一个这哥俩到底干嘛的

ctx

ctx作为上下文使用,Koa将 node 的 request, response 对象封装进一个单独对象。即ctx.requestctx.response。Koa 内部又对一些常用的属性或者方法做了代理操作,使得我们可以直接通过 ctx 获取。比如,ctx.request.url 可以写成 ctx.url

next

next 参数的作用是将处理的控制权转交给下一个中间件

15371520197565

经典的洋葱图概念能很好的解释next的执行,请求从最外层进去,又从最里层出来。我们看一个例子

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next)=>{
  let startTime = new Date().getTime()
  await next()
  let endTime = new Date().getTime()
  console.log(`此次的响应时间为:${endTime - startTime}ms`)
})

app.use(async (ctx, next) => {
  console.log('111, 然后doSomething')
  await next()
  console.log('111 end')
})

app.use(async (ctx, next) => {
  console.log('222, 然后doSomething')
  await next()
  console.log('222 end')
})

app.use(async (ctx, next) => {
  console.log('333, 然后doSomething')
  await next()
  console.log('333 end')
})

app.listen(3333, ()=>{
  console.log('server is running at http://localhost:3333')
})

看一下运行结果:

15371528106452

如果将**‘222’**函数的next()去掉的话,会发生什么呢?

15371529369320

可以看到,后面的**‘333’**中间件直接不执行了。所以中间件的顺序对next的执行有很大的影响

路由 koa-router

我们常用koa-router来处理URL

安装

npm i koa-router --save

看一个例子:

const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')

const router = new Router()

router.get('/', async (ctx, next) => {
  ctx.body = '你好,我这里是index页'
})

router.get('/user', async (ctx, next) => {
  ctx.body = '你好,我这里是user页'
})

router.get('/error', async (ctx, next) => {
  ctx.body = '你好,我这里是error页'
})

app.use(router.routes())

app.listen(3333, ()=>{
  console.log('server is running at http://localhost:3333')
})

15371540305250

15371540448439
15371540585094

koa-router也支持嵌套写法,通过一个总路由装载所有子路由,也非常的方便。看一个例子:

const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')

// 子路由1
let oneRouter = new Router()

oneRouter.get('/', async (ctx, next) => {
  ctx.body = '你好,我这里是oneRouter页'
})

// 子路由2
let twoRouter = new Router()

twoRouter.get('/', async (ctx, next) => {
  ctx.body = '你好, 我这里是twoRouter页'
}).get('/home', async (ctx , next) => {
  ctx.body = '你好, 我这里是home页'
})

// 装载所有子路由
let indexRouter = new Router()

indexRouter.use('/one',oneRouter.routes(), oneRouter.allowedMethods())
indexRouter.use('/two',twoRouter.routes(), twoRouter.allowedMethods())

app
  .use(indexRouter.routes())
  .use(indexRouter.allowedMethods())

app.listen(3333, ()=>{
  console.log('server is running at http://localhost:3333')
})

看一下运行结果:

15371560100616
15371560354693
15371560521654

获取请求数据

koa-router提供了常见的 .get .put .post .del 接口来处理各种需求。实际开发中我们用的比较多的是get和post,我们来看看get例子:

const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const router = new Router()

router.get('/data', async (ctx , next)=> {
  let url = ctx.url

  // 从ctx的request中拿到我们想要的数据
  let data = ctx.request.query
  let dataQueryString = ctx.request.querystring

  ctx.body = {
    url,
    data,
    dataQueryString
  }
})

app.use(router.routes())

app.listen(3333, ()=>{
  console.log('server is running at http://localhost:3333')
})

在浏览器里输入http://localhost:3333/data?user=wuyanzu&id=123456 ,可以看到运行结果

15371636443212

可以看到区别,.query返回的结果是对象,而.querystring返回的是字符串,这个很好理解。(chrome插件显示成json格式)

如果遵从 RESTful 规范,比如请求要以 '/user/:id'的方式发出的话,我们可以用下面的例子来获取到想要的数据

添加代码

router.get('/data/:id', async (ctx, next) => {

  // 也从ctx中拿到我们想要的数据,不过使用的是params对象
  let data = ctx.params

  ctx.body = data
})

浏览器运行 http://localhost:3333/data/4396 看到结果

15371643392037

接下来我们看看post的例子

我们常用的请求post,它的数据是放在body当中的。这个时候就推荐一个非常常用且好用的中间件-koa-bodyparser

首先安装

npm i koa-bodyparser --save

然后我们在刚才的代码里添加

router.get('/post', async (ctx, next) => {
    // 模拟一段提交页面
  let html = `    
    <form action="/post/result" method="post">
        <p>你长的最像哪位明星</p>
        <input name="name" type="text" placeholder="请输入名字:"/> 
        <br/>
        <p>输入一段你知道的车牌号</p>
        <input name="num" type="text" placeholder="请输入车牌号:"/>
        <br/> 
        <button>确定不改了哦</button>
     </form> `
  ctx.body = html
})

router.post('/post/result', async (ctx, next) => {
  // 我们可以从ctx的request.body拿到提交上来的数据
  let {name, num} = ctx.request.body

  if (name && num) {
    ctx.body = `hello,你最像的明星是:${name},ch你知道的车牌号是:${num}`
  } else {
    ctx.body = '啊哦~你填写的信息有误'
  }

})

看一下运行结果

2018-09-17 14 26 24

cache

koa操作cookie是非常方便的,也是从上下文ctx中获取。

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

在我们刚才的post请求的代码中加入:

router.post('/post/result', async (ctx, next) => {
  // 我们可以从ctx的request.body拿到提交上来的数据
  let {name, num} = ctx.request.body

  if (name && num) {
    ctx.body = `hello,你最像的明星是:${name},ch你知道的车牌号是:${num}`
    ctx.cookies.set(
      'xunleiCode',num,
      {
        domain: 'localhost',  // 写cookie所在的域名
        path: '/post/result',       // 写cookie所在的路径
        maxAge: 10 * 60 * 1000, // cookie有效时长
        expires: new Date('2018-09-17'),  // cookie失效时间
        httpOnly: false,  // 是否只用于http请求中获取
        overwrite: false  // 是否允许重写
      }
    )
  } else {
    ctx.body = '啊哦~你填写的信息有误'
  }

})

看一下运行结果:
15371681204265
15371681313023

koa操作session的话,需要用到koa-session,🌰:

const session = require('koa-session')

app.keys = ['some secret hurr'];
const CONFIG = {
  key: 'koa:sess',   //cookie key (default is koa:sess)
  maxAge: 86400000,  // cookie的过期时间 maxAge in ms (default is 1 days)
  overwrite: true,  //是否可以overwrite    (默认default true)
  httpOnly: true, //cookie是否只有服务器端可以访问 httpOnly or not (default true)
  signed: true,   //签名默认true
  rolling: false,  //在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)
  renew: false,  //(boolean) renew session when session is nearly expired,
};
app.use(session(CONFIG, app));

小结

在涉及到自己没有接触过的领域时,我一直推崇先看看要怎么玩,等自己会玩了以后,再看看“究竟”怎么玩。我们通过上面的代码和描述,已经对koa及node有一个初步的印象和概念。下篇文章我们会有中间件的拆分,单元测试,记录日志,管理规范等。让我们共同成长!

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2019/03/14 - 如何理解并应用贝塞尔曲线

贝塞尔曲线又叫贝兹曲线,在大学高数中一度让我非常头疼。前阵子练手写动画的时候,发现贝塞尔曲线可以应用于轨迹的绘制以及定义动画曲线。

本文就来探究一下,贝塞尔曲线到底是个什么样的存在。

贝塞尔曲线原理

贝塞尔曲线由n个点来决定,其曲线轨迹可以由一个公式来得出:

贝塞尔公式

其中n就代表了贝塞尔曲线是几阶曲线,该公式描述了曲线运动的路径。

以下我们来讨论一下,贝塞尔公式如何推导。

一阶贝塞尔曲线

一阶

设定图中运动的点为Pt,t为运动时间,t∈(0,1),可得如下公式

公式1

二阶贝塞尔曲线

二阶

二阶坐标

在二阶贝塞尔曲线中,已知三点恒定(P0,P1,P2),设定在P0P1中的点为Pa,在P1P2中的点为Pb,Pt在PaPb上的点,这三点都在相同时间t内做匀速运动。

由公式(1)可知

公式2

将公式(2)(3)代入公式(4)中,可得

公式3

三阶贝塞尔曲线

三阶

三阶坐标

同理,根据以上的推导过程可得

公式4

由此可以推导

公式5

n阶贝塞尔曲线

四阶

五阶

放上一个网址,随意感受一下贝塞尔曲线的绘制过程:

http://myst729.github.io/bezier-curve/

实际应用

贝塞尔曲线在前端中主要有两方面的应用,一方面可以作为动画曲线应用于CSS3动画中;另一方面可以通过canvas来绘制曲线达到需要的效果。

CSS3中贝塞尔曲线的应用

在CSS3中,有两属性经常被用到:transition-timing-functionanimation-timing-function,这两个分别代表了过渡的速度和动画的速度。CSS3为我们提供了一个新的工具——cubic-bezier(x1,y1,x2,y2)。这个工具能够生成一个速度曲线,使我们的元素按照该曲线来调节速度。

在上面的推导中,我们知道在贝塞尔公式中,有两个点的位置恒定——P0和P1,cubic-bezier中定义了两个控制点的位置,所以该曲线为三阶贝塞尔曲线。

有个网站可以方便我们快速建立一个贝塞尔曲线:cubic-bezier

贝塞尔曲线与动画曲线的关联

先来一波动图简单粗暴的感受一下:
例一:

1

例二:

2

例三:

3

左边的是贝塞尔曲线,横轴代表了事件,竖轴代表了进度,无法直观得感受出速度的变化。

右边的曲线是控制面板中的动画曲线,横轴是时间,竖轴是速度,可以方面地看出速度的变化。

上述例子中,以前进反向为速度正方向,后退方向为速度反方向。

如何得知速度的变化

推导

例一中,贝塞尔曲线为一条直线,当时间均匀变化时,进度也在均匀变大,由此可知速度恒定不变,时间和进度之间的关系可以用一个线性方程来表示:

$$y=ax+b (a=1,b=0)$$

其中x为时间,y为进度,a即为速度。

推导案例一

从上面结论中启发,去观察其他贝塞尔曲线,

方程一

图中是一段变化的曲线,我们取其中一小段,将其看作稳定不变的一段直线,通过下面的线性方程来表示,并通过红线标注在图中:

$$y=ax+b$$

根据初中数学的内容,我们知道,当a>1时,与x轴的夹角∈(45°,90°);当a∈(0,1)时,与x轴的夹角在(0,45°)之间。相同的时间内,与x轴的夹角越大,a越大,速度越快。

观察上图的夹角变化趋势,夹角逐渐变小趋向于0,而后逐渐变大,趋向于90°,对应速度应是速度逐渐变慢趋向于0,之后逐渐变快。

放上动画曲线以及动图来验证一下我们的推测:

方程一(1)

5

推导案例二

下图中的曲线部分在第四象限,部分在第一象限,这时对应的动画曲线该如何推导呢。

同样将该曲线视为由n段平滑的直线构成,由线性方程来表示直线的趋势,可知速度a方向一开始为负,之后慢慢向正方靠近,a的速率也在由大变小,当为0时,再向正方慢慢变大。即该曲线表示元素一开始在朝反方向减速运动,当速度为0后,向正方向作加速运动。

方程三

通过动画曲线及动图来验证上述推导:

方程三(1)

6

验证

用两个曲线来验证一下上面的结论:

曲线一:

方程二

方程二(1)

3

曲线二:

方程四

方程四(1)

7

从结果可以判断,用上述推导方法可以正确得出贝塞尔曲线与动画曲线之间的关系。

动画曲线的应用

了解了如何用贝塞尔曲线来指定动画曲线后,很多动画涉及到速度方面的效果就可以实现了,例如小车加速刹车,弹簧动画等速度轨迹都可以根据自己的需要来进行定制。

放上一个缓动函数速查网址,可以让自己的动效更加真实:缓动函数

放一个小例子:

动画案例

该动画模拟了小球落下回弹的过程

代码如下:

    <div class="ground">
      <div class="ball" id="ball"></div>
    </div>
      .ball {
        width: 30px;
        height: 30px;
        background: #000000;
        border-radius: 50%;
        position: absolute;
        top: 0;
        left: 50%;
        animation: move 4s cubic-bezier(0.36, 1.7, 0.26, 0.61) 1s infinite;
      }

      @keyframes move {
        0% {
          top: 0;
        }
        100% {
          top: 90%;
        }
      }

这类动画可以参考网上大大们的案例:

贝塞尔曲线与CSS3动画、SVG和canvas的应用

理解与运用贝塞尔曲线

利用canvas绘制贝塞尔曲线

canvas中提供了api可以快速绘制一条贝塞尔曲线,来达到需要的效果:

二阶贝塞尔曲线

quadraticCurveTo(x1,y1,x2,y2)

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.quadraticCurveTo(40,200,200,20);
ctx.stroke();

canvas-2阶

其中moveTo定义了起始点,quadraticCurveTo(x1,y1,x2,y2)中的(x1,y1)为控制点,(x2,y2)为终点

三阶贝塞尔曲线

bezierCurveTo(x1,y1,x2,y2,x3,y3)

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.bezierCurveTo(40,100,200,150,200,20);
ctx.stroke();

canvas-3阶

其中moveTo定义了起始点,bezierCurveTo(x1,y1,x2,y2)中的(x1,y1),(x2,y2)为控制点,(x3,y3)为终点

总结

为了弄清贝塞尔曲线是个什么东西,和动画曲线、速度又有什么关联,作者跑去复习了一下那些早扔给老师的东西,有说错的请轻拍/(ㄒoㄒ)/~~

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/07/29 - 微信小程序如何使用iconfont?

iconfont给前端带来的便利相信已众所周知,我也一直遵循能用iconfont的情况下,绝不用image。但是这周开始接触小程序,却发现小程序里不能按照之前的方式愉快的使用了。

此前

使用打开iconfont官网,选择需要的icon,加入到个人项目里。

img

然后选择Unicode模式,点击复制代码,将在线资源复制的全局CSS文件里。再用一个专用class(.iconfont)用来定义iconfont。

@font-face {
  font-family: 'iconfont';  /* project id 319212 */
  src: url('//at.alicdn.com/t/font_tdeh59rfkwdhd7vi.eot');
  src: url('//at.alicdn.com/t/font_tdeh59rfkwdhd7vi.eot?#iefix') format('embedded-opentype'),
  url('//at.alicdn.com/t/font_tdeh59rfkwdhd7vi.woff') format('woff'),
  url('//at.alicdn.com/t/font_tdeh59rfkwdhd7vi.ttf') format('truetype'),
  url('//at.alicdn.com/t/font_tdeh59rfkwdhd7vi.svg#iconfont') format('svg');
}

.iconfont {
  font-family: "iconfont";
  font-size: .16rem;
  font-style: normal;
  color: #9e9595;
}

接着鼠标hover到图标上点击复制代码,然后在html里面用一个标签包裹即可愉快的使用了。

<i class="iconfont">&#xe604;</i>

img

但是

在小程序里面,按照这种方式继续用的话,就。。。尴尬了😅

<text class="iconfont">&#xe604;</text>

img

漂亮的图标,变成了让人心慌的代码。这可怎么办???

很多网友推荐将字体图标转化成base64😅😅。虽然这也不乏是一种解决方案,但是,显得很麻烦。特别是在项目初期,不确定需要用哪些图标的时候,加一个图标转一次,特别心累。

美好的事情即将发生

经过一番研究后,我发现还是可以按照以前的方式引用在线资源。不过在html里面使用的时候我们需要用到伪类元素。说到这个大家应该都有底了吧,哈哈。

回到我们第一张图

img

我们需要在原来的CSS基础上,再为每一个图标自定义一个class名。比如这开灯的icon

.icon-kaideng:before { 
  content: "\e604";
  font-size: 36rpx;
  color: #5FB7EB;
}

content里面的内容为字体编码的后4位加一个\,

接着,在wxml里面这么使用就好了

<icon class="iconfont icon-kaideng"/>

img

哈哈,简单吧!!

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/07/22 - Vue-cli原理分析

背景

在平时工作中会有遇到许多以相同模板定制的小程序,因此想自己建立一个生成模板的脚手架工具,以模板为基础构建对应的小程序,而平时的小程序都是用mpvue框架来写的,因此首先先参考一下Vue-cli的原理。知道原理之后,再定制自己的模板脚手架肯定是事半功倍的。


在说代码之前我们首先回顾一下Vue-cli的使用,我们通常使用的是webpack模板包,输入的是以下代码。

vue init webpack [project-name]

在执行这段代码之后,系统会自动下载模板包,随后会询问我们一些问题,比如模板名称,作者,是否需要使用eslint,使用npm或者yarn进行构建等等,当所有问题我们回答之后,就开始生成脚手架项目。

我们将源码下载下来,源码仓库点击这里,平时用的脚手架还是2.0版本,要注意,默认的分支是在dev上,dev上是3.0版本。

我们首先看一下package.json,在文件当中有这么一段话

{
  "bin": {
    "vue": "bin/vue",
    "vue-init": "bin/vue-init",
    "vue-list": "bin/vue-list"
  }
}

由此可见,我们使用的命令 vue init,应该是来自bin/vue-init这个文件,我们接下来看一下这个文件中的内容


bin/vue-init

const download = require('download-git-repo')
const program = require('commander')
const exists = require('fs').existsSync
const path = require('path')
const ora = require('ora')
const home = require('user-home')
const tildify = require('tildify')
const chalk = require('chalk')
const inquirer = require('inquirer')
const rm = require('rimraf').sync
const logger = require('../lib/logger')
const generate = require('../lib/generate')
const checkVersion = require('../lib/check-version')
const warnings = require('../lib/warnings')
const localPath = require('../lib/local-path')

download-git-repo 一个用于下载git仓库的项目的模块
commander 可以将文字输出到终端当中
fs 是node的文件读写的模块
path 模块提供了一些工具函数,用于处理文件与目录的路径
ora 这个模块用于在终端里有显示载入动画
user-home 获取用户主目录的路径
tildify 将绝对路径转换为波形路径 比如/Users/sindresorhus/dev → ~/dev
inquirer 是一个命令行的回答的模块,你可以自己设定终端的问题,然后对这些回答给出相应的处理
rimraf 是一个可以使用 UNIX 命令 rm -rf的模块
剩下的本地路径的模块其实都是一些工具类,等用到的时候我们再来讲


// 是否为本地路径的方法 主要是判断模板路径当中是否存在 `./`
const isLocalPath = localPath.isLocalPath
// 获取模板路径的方法 如果路径参数是绝对路径 则直接返回 如果是相对的 则根据当前路径拼接
const getTemplatePath = localPath.getTemplatePath
/**
 * Usage.
 */

program
  .usage('<template-name> [project-name]')
  .option('-c, --clone', 'use git clone')
  .option('--offline', 'use cached template')

/**
 * Help.
 */

program.on('--help', () => {
  console.log('  Examples:')
  console.log()
  console.log(chalk.gray('    # create a new project with an official template'))
  console.log('    $ vue init webpack my-project')
  console.log()
  console.log(chalk.gray('    # create a new project straight from a github template'))
  console.log('    $ vue init username/repo my-project')
  console.log()
})

/**
 * Help.
 */
function help () {
  program.parse(process.argv)
  if (program.args.length < 1) return program.help()
}
help()

这部分代码声明了vue init用法,如果在终端当中 输入 vue init --help或者跟在vue init 后面的参数长度小于1,也会输出下面的描述

  Usage: vue-init <template-name> [project-name]

  Options:

    -c, --clone  use git clone
    --offline    use cached template
    -h, --help   output usage information
  Examples:

    # create a new project with an official template
    $ vue init webpack my-project

    # create a new project straight from a github template
    $ vue init username/repo my-project

接下来是一些变量的获取

/**
 * Settings.
 */
// 模板路径
let template = program.args[0]
const hasSlash = template.indexOf('/') > -1
// 项目名称
const rawName = program.args[1]
const inPlace = !rawName || rawName === '.'
// 如果不存在项目名称或项目名称输入的'.' 则name取的是 当前文件夹的名称
const name = inPlace ? path.relative('../', process.cwd()) : rawName
// 输出路径
const to = path.resolve(rawName || '.')
// 是否需要用到 git clone
const clone = program.clone || false

// tmp为本地模板路径 如果 是离线状态 那么模板路径取本地的
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  template = tmp
}

接下来主要是根据模板名称,来下载并生产模板,如果是本地的模板路径,就直接生成。

/**
 * Check, download and generate the project.
 */

function run () {
  // 判断是否是本地模板路径
  if (isLocalPath(template)) {
    // 获取模板地址
    const templatePath = getTemplatePath(template)
    // 如果本地模板路径存在 则开始生成模板
    if (exists(templatePath)) {
      generate(name, templatePath, to, err => {
        if (err) logger.fatal(err)
        console.log()
        logger.success('Generated "%s".', name)
      })
    } else {
      logger.fatal('Local template "%s" not found.', template)
    }
  } else {
    // 非本地模板路径 则先检查版本
    checkVersion(() => {
      // 路径中是否 包含'/'
      // 如果没有 则进入这个逻辑
      if (!hasSlash) {
        // 拼接路径 'vuejs-tempalte'下的都是官方的模板包
        const officialTemplate = 'vuejs-templates/' + template
        // 如果路径当中存在 '#'则直接下载
        if (template.indexOf('#') !== -1) {
          downloadAndGenerate(officialTemplate)
        } else {
          // 如果不存在 -2.0的字符串 则会输出 模板废弃的相关提示
          if (template.indexOf('-2.0') !== -1) {
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
            return
          }

          // 下载并生产模板
          downloadAndGenerate(officialTemplate)
        }
      } else {
        // 下载并生生成模板
        downloadAndGenerate(template)
      }
    })
  }
}

我们来看下 downloadAndGenerate这个方法

/**
 * Download a generate from a template repo.
 *
 * @param {String} template
 */

function downloadAndGenerate (template) {
  // 执行加载动画
  const spinner = ora('downloading template')
  spinner.start()
  // Remove if local template exists
  // 删除本地存在的模板
  if (exists(tmp)) rm(tmp)
  // template参数为目标地址 tmp为下载地址 clone参数代表是否需要clone
  download(template, tmp, { clone }, err => {
    // 结束加载动画
    spinner.stop()
    // 如果下载出错 输出日志
    if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
    // 模板下载成功之后进入生产模板的方法中 这里我们再进一步讲
    generate(name, tmp, to, err => {
      if (err) logger.fatal(err)
      console.log()
      logger.success('Generated "%s".', name)
    })
  })
}

到这里为止,bin/vue-init就讲完了,该文件做的最主要的一件事情,就是根据模板名称,来下载生成模板,但是具体下载和生成的模板的方法并不在里面。

下载模板

下载模板用的download方法是属于download-git-repo模块的。

最基础的用法为如下用法,这里的参数很好理解,第一个参数为仓库地址,第二个为输出地址,第三个是否需要 git clone,带四个为回调参数

download('flipxfx/download-git-repo-fixture', 'test/tmp',{ clone: true }, function (err) {
  console.log(err ? 'Error' : 'Success')
})

在上面的run方法中有提到一个#的字符串实际就是这个模块下载分支模块的用法

download('bitbucket:flipxfx/download-git-repo-fixture#my-branch', 'test/tmp', { clone: true }, function (err) {
  console.log(err ? 'Error' : 'Success')
})

生成模板

模板生成generate方法在generate.js当中,我们继续来看一下


generate.js

const chalk = require('chalk')
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const async = require('async')
const render = require('consolidate').handlebars.render
const path = require('path')
const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')

chalk 是一个可以让终端输出内容变色的模块
Metalsmith是一个静态网站(博客,项目)的生成库
handlerbars 是一个模板编译器,通过templatejson,输出一个html
async 异步处理模块,有点类似让方法变成一个线程
consolidate 模板引擎整合库
multimatch 一个字符串数组匹配的库
options 是一个自己定义的配置项文件

随后注册了2个渲染器,类似于vue中的 vif velse的条件渲染

// register handlebars helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
  return a === b
    ? opts.fn(this)
    : opts.inverse(this)
})

Handlebars.registerHelper('unless_eq', function (a, b, opts) {
  return a === b
    ? opts.inverse(this)
    : opts.fn(this)
})

接下来看关键的generate方法

module.exports = function generate (name, src, dest, done) {
  // 读取了src目录下的 配置文件信息, 同时将 name auther(当前git用户) 赋值到了 opts 当中
  const opts = getOptions(name, src)
  // 拼接了目录 src/{template} 要在这个目录下生产静态文件
  const metalsmith = Metalsmith(path.join(src, 'template'))
  // 将metalsmitch中的meta 与 三个属性合并起来 形成 data
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  })
  // 遍历 meta.js元数据中的helpers对象,注册渲染模板数据
  // 分别指定了 if_or 和   template_version内容
  opts.helpers && Object.keys(opts.helpers).map(key => {
    Handlebars.registerHelper(key, opts.helpers[key])
  })

  const helpers = { chalk, logger }

  // 将metalsmith metadata 数据 和 { isNotTest, isTest 合并 }
  if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    opts.metalsmith.before(metalsmith, opts, helpers)
  }

  // askQuestions是会在终端里询问一些问题
  // 名称 描述 作者 是要什么构建 在meta.js 的opts.prompts当中
  // filterFiles 是用来过滤文件
  // renderTemplateFiles 是一个渲染插件
  metalsmith.use(askQuestions(opts.prompts))
    .use(filterFiles(opts.filters))
    .use(renderTemplateFiles(opts.skipInterpolation))

  if (typeof opts.metalsmith === 'function') {
    opts.metalsmith(metalsmith, opts, helpers)
  } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
    opts.metalsmith.after(metalsmith, opts, helpers)
  }

  // clean方法是设置在写入之前是否删除原先目标目录 默认为true
  // source方法是设置原路径
  // destination方法就是设置输出的目录
  // build方法执行构建
  metalsmith.clean(false)
    .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
    .destination(dest)
    .build((err, files) => {
      done(err)
      if (typeof opts.complete === 'function') {
        // 当生成完毕之后执行 meta.js当中的 opts.complete方法
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
        logMessage(opts.completeMessage, data)
      }
    })

  return data
}

meta.js

接下来看以下complete方法

complete: function(data, { chalk }) {
    const green = chalk.green
    // 会将已有的packagejoson 依赖声明重新排序
    sortDependencies(data, green)

    const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
    // 是否需要自动安装 这个在之前构建前的询问当中 是我们自己选择的
    if (data.autoInstall) {
      // 在终端中执行 install 命令
      installDependencies(cwd, data.autoInstall, green)
        .then(() => {
          return runLintFix(cwd, data, green)
        })
        .then(() => {
          printMessage(data, green)
        })
        .catch(e => {
          console.log(chalk.red('Error:'), e)
        })
    } else {
      printMessage(data, chalk)
    }
  }

构建自定义模板

在看完vue-init命令的原理之后,其实定制自定义的模板是很简单的事情,我们只要做2件事

  • 首先我们需要有一个自己模板项目
  • 如果需要自定义一些变量,就需要在模板的meta.js当中定制

由于下载模块使用的是download-git-repo模块,它本身是支持在github,gitlab,bitucket上下载的,到时候我们只需要将定制好的模板项目放到git远程仓库上即可。

由于我需要定义的是小程序的开发模板,mpvue本身也有一个quickstart的模板,那么我们就在它的基础上进行定制,首先我们将它fork下来,新建一个custom分支,在这个分支上进行定制。

我们需要定制的地方有用到的依赖库,需要额外用到less以及wxparse
因此我们在 template/package.json当中进行添加

{
  // ... 部分省略
  "dependencies": {
    "mpvue": "^1.0.11"{{#vuex}},
    "vuex": "^3.0.1"{{/vuex}}
  },
  "devDependencies": {
    // ... 省略
    // 这是添加的包
    "less": "^3.0.4",
    "less-loader": "^4.1.0",
    "mpvue-wxparse": "^0.6.5"
  }
}

除此之外,我们还需要定制一下eslint规则,由于只用到standard,因此我们在meta.js当中 可以将 airbnb风格的提问删除

"lintConfig": {
  "when": "lint",
  "type": "list",
  "message": "Pick an ESLint preset",
  "choices": [
    {
      "name": "Standard (https://github.com/feross/standard)",
      "value": "standard",
      "short": "Standard"
    },
    {
      "name": "none (configure it yourself)",
      "value": "none",
      "short": "none"
    }
  ]
}

.eslinttrc.js

'rules': {
    {{#if_eq lintConfig "standard"}}
    "camelcase": 0,
    // allow paren-less arrow functions
    "arrow-parens": 0,
    "space-before-function-paren": 0,
    // allow async-await
    "generator-star-spacing": 0,
    {{/if_eq}}
    {{#if_eq lintConfig "airbnb"}}
    // don't require .vue extension when importing
    'import/extensions': ['error', 'always', {
      'js': 'never',
      'vue': 'never'
    }],
    // allow optionalDependencies
    'import/no-extraneous-dependencies': ['error', {
      'optionalDependencies': ['test/unit/index.js']
    }],
    {{/if_eq}}
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }

最后我们在构建时的提问当中,再设置一个小程序名称的提问,而这个名称会设置到导航的标题当中。
提问是在meta.js当中添加

"prompts": {
    "name": {
      "type": "string",
      "required": true,
      "message": "Project name"
    },
    // 新增提问
    "appName": {
      "type": "string",
      "required": true,
      "message": "App name"
    }
}

main.json

{
  "pages": [
    "pages/index/main",
    "pages/counter/main",
    "pages/logs/main"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    // 根据提问设置标题
    "navigationBarTitleText": "{{appName}}",
    "navigationBarTextStyle": "black"
  }
}

最后我们来尝试一下我们自己的模板

vue init Baifann/mpvue-quickstart#custom min-app-project

image_1cj0ikq141je51ii31eek25t18il19.png-31.4kB

image_1cj0m986t1qdu1j97lruh4kjgo9.png-36.2kB

总结

以上模板的定制是十分简单的,在实际项目上肯定更为复杂,但是按照这个思路应该都是可行的。比如说将一些自行封装的组件也放置到项目当中等等,这里就不再细说。原理解析都是基于vue-cli 2.0的,但实际上 3.0也已经整装待发,如果后续有机会,深入了解之后,再和大家分享,谢谢大家。


参考文章

  • vue-cli是如何工作的]6

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/10/15- 前端骨架屏方案小结

骨架屏

最近在项目不时有用到骨架屏的需求,所以抽时间对骨架屏的方案作了一下调研,骨架屏的实践已经有很多了,也有很多人对自己的方案作了介绍.在这里按照个人的理解做了一个汇总和分类,分享给大家.

关于骨架屏(简介)

骨架屏就是在页面数据尚未加载前先给用户展示出页面的大致结构,直到请求数据返回后再渲染页面,补充进需要显示的数据内容。常用于文章列表、动态列表页等相对比较规则的列表页面。
很多项目中都有应用:ex:饿了么h5版本,知乎,facebook等网站中都有应用。
借个图举例如下:
image

两类用途

简介中作了关于用途的说明,但是仍然可以继续细分:

  1. 作为spa中路由切换的loading,结合组件的生命周期和ajax请求返回的时机来使用.
  2. 作为首屏渲染的优化.

第一类用途

第一类用途需要自己编写骨架屏,推荐两个成熟方便定制的svg组件定制为骨架屏的方案

作为首屏渲染(自动化方案)

该方案是饿了么在骨架屏的实践中总结出的一套方案:

  1. cssUnit的配置: 需要使用自适应的单位,按照文档给出的选择范围选,直接用 px 生成的比例会不合适
  2. puppeteer有大概80M, 安装的时候有可能不能一次下载成功.
  • 原理:
通过 puppeteer 在服务端操控 headless Chrome 打开开发中的需要生成骨架屏的页面,在等待页面加载
渲染完成之后,在保留页面布局样式的前提下,通过对页面中元素进行删减或增添,对已有元素通过层叠样
式进行覆盖,这样达到在不改变页面布局下,隐藏图片和文字,通过样式覆盖,使得其展示为灰色块。然后
将修改后的 HTML 和 CSS 样式提取出来,这样就是骨架屏了.

自动生成骨架屏

其他方案

结合ssr render/prerender来使用:

  1. 事先编写好骨架屏组件通过ssr render 解析注入html文件中(除了需要自己编写外其实过程类似于上面的自动化方案)参考文章

  2. 1中事先编写好的骨架屏组件可以用图片代替 (svg) ;或者设计师设计好.

小程序的骨架屏

  1. 不存在预渲染的概念,但是还是可以通过自己预先编写骨架屏组件放在页面中,等到异步请求的数据回来后更新页面.

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/06/24 - 从0开始发布一个无依赖、高质量的npm

写在前面

没有发布过npm包的同学,可能会对NPM对开发有一种蜜汁敬畏,觉得这是一个很高大上的东西。甚至有次面试,面试官问我有没有发过npm包,当时只用过还没写过,我想应该挺难的,就小声说了没有,然后就让我回去了o(╯□╰)o。

其实,在现在的我看来,npm包就是一个我们平时经常写的一个export出来的模块而已,只不过跟其它业务代码耦合性低,具有较高的独立性。

当然,要发布一个npm包,除了写的模块组件外,还需要做一些基础的包装工作。下面我就以最近开发的「DigitalKeyboard 数字键盘 NPM」 为例,一一列出具体步骤:

  1. 写基础模块代码;
  2. 注册npm账号;
  3. 配置package.json;
  4. 配置webpack;
  5. 添加单元测试;
  6. 完善README.md;
  7. 发布

1、2、3足可以完成一个npm,4、5、6是为了开发一个高质量的npm。

开始

具体代码移步github,请反手 给个 ★ Star ^_~。完整目录结构如下:

├── LICENSE
├── README.md
├── build
│   └── Keyboard.js
├── config
│   └── webpack
│       ├── webpack.base.config.js
│       ├── webpack.config.js
│       ├── webpack.dev.config.js
│       └── webpack.prod.config.js
├── index.html
├── package.json
├── src
│   ├── Keyboard.js
│   ├── Keyboard.scss
│   └── main.js
├── test
│   └── Keyboard.test.js
└── yarn.lock

基础模块代码

现在只需要看src目录下的三个文件。其中,main.js 主要是对将要开发模块的引用,只需存在于开发阶段,同时作为此阶段webpack的入口文件,核心代码在Keyboard.js。

这里,主要用的是ES6的classexport default,Keyboard的核心**就是点击哪个键就对外输出什么内容,实现也比较简单,大家都能看得懂,这里就不展开讲了,具体可以看github 源码。

注册npm账号

这一步也不用说,大家直接去官网注册就好了。

配置package.json

{
  "name": "digital-keyboard",
  "version": "1.0.0",
  "main": "build/Keyboard.js",
  "repository": "https://github.com/simbawus/DigitalKeyboard.git",
  "author": "simbawu <[email protected]>",
  "description": "DigitalKeyboard Component",
  "keywords": [
    "DigitalKeyboard",
    "Digital",
    "Keyboard",
  ]
}

此时的配置文件也比较简单,只需配置npm包名,准备用的名字现在npm搜索一下,已经存在的就不能用了;版本号version,每次发布版本号都需要更新,不然发布不成功;对外export的文件路径,这里我用的是webpack打包后的文件,如果不用webpack,直接引用src/Keyboard.js也可以,只不过要做模块化方式兼容,这个后面说。也可以放上项目所在github地址及作者名,description和keywords比较利于SEO,不过这些都不是必需项。

到这里,一个npm包就开发完成了,直接发布即可使用。但是,略显粗糙:代码压缩、单元测试、readme都没写,别人不知道怎么用也不敢用。下面一步步完善。

配置webpack

这里用的是最新版的webpack4,官方提供production和development两种开发模式,并分别做了默认压缩处理,非常适合这里。有两点要特别说明下:

  • libraryTarget: 'umd'

    umd有的同学可能不是太熟悉,但是cmd、amd大家应该都知道,分别应用于服务端和浏览器端的模块方案。umd就是前面提到的模块化方式兼容。感兴趣可以参考我的另一篇文章JavaScript Module 设计解析及总结

  • production和development的entry不一样:

    development的entry是main.js,而production的entry是Keyboard.js。前面说过,开发阶段需要有对模块的引用,但是正式发布就不需要了,所以要分别配置。

其他就不展开讲了,我的webpack配置结构很清晰,欢迎大家直接copy。

├── webpack.base.config.js
├── webpack.config.js
├── webpack.dev.config.js
└── webpack.prod.config.js

添加单元测试

大家经常看到很多不错的项目都有Build Status,这就像一个证明可用性的证书,给人安全感和信任感,所以添加单元测试,还是很有必要的,同时也可以提高代码质量。先介绍需要用到的几个概念:

mocha:测试框架;

chai:断言库,断言通俗来讲就是判断代码结果对不对;

jsdom:node端是没有js dom对象的,比如window、document等等,所以需要这个库提供;

istanbul:代码覆盖率计算工具;

coveralls:统计上面的代码测试覆盖率工具;

travis-ci:自动集成,比如master代码push到github上之后,travis-ci就会自动进行自动化测试。

这里介绍下jsdom的用法,当时按照几个文档来都跑不通:

const {JSDOM} = require('jsdom');
const {window} = new JSDOM(`<!DOCTYPE html>
  <html>
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0,user-scalable=no">
      <meta name="author" content="吴胜斌,simbawu">
      <title>数字键盘</title>
  </head>
  <body>
  <div id="values"></div>
  <div id="app"></div>
  </body>
  </html>`);

propagateToGlobal(window);

function propagateToGlobal(window) {
  for (let key in window) {
    if (!window.hasOwnProperty(key)) continue;
    if (key in global) continue;
    global[key] = window[key];
  }
}

首先引入jsdom,然后构造一个document,并引入其中的window对象然后一一赋值给node的global对象。其实也很简单,只不过第一次接触,而且找的文档写的也不清楚,所以花了点时间。其他几个文档都还不错,可以看看文档再看看我是怎么用的。此时的package.json就很很丰富了,可以执行yarn testyarn cover看看测试是否通过及测试覆盖率。

完善README.md

一个好的readme是决定用户用不用你项目的关键因素,所以要多花点心思,千万不能忽略。

  • 标题:直观的描述这个项目是干什么的。
  • 徽章:
    Build Status
    Coverage Status
    npm
    npm
    GitHub license
    分别表示是否构建成功、代码测试覆盖率、npm版本号、下载量、开源证书,看起来逼格满满有木有。推荐去shields io 添加,生成一次,之后会自动更新,不过需要等npm发布后才能搜到。
  • 配图:要让用户直观的看到这个组件长什么样,是否满足他的需求。
  • API介绍:不能让用户猜。
  • 使用示例:尽量降低使用门槛。

发布

#先登录NPM账号:
npm login

#会依次让你输入用户名、密码、和邮箱
Username: simbawu        
Password:
Email: (this IS public) [email protected]

#登录成功会出现以下提示信息:
Logged in as simbawu on https://registry.npmjs.org/.

#执行发布命令:
npm publish

#发布成功后会出现以下提示信息:
+ [email protected]
#这里digital-keyboard是我的NPM包名,1.0.0是包的版本号

接下来,我们可以在npm官网,通过搜索包名或者在个人中心看到刚刚发布的包。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

2018/11/09 - 两种方案开发小程序动画

在普通的网页开发中,动画效果可以通过css3来实现大部分需求,在小程序开发中同样可以使用css3,同时也可以通过api方式来实现。

指路:小程序animatiom动画API

API解读

小程序中,通过调用api来创建动画,需要先创建一个实例对象。这个对象通过wx.createAnimation返回,animation的一系列属性都基于这个实例对象。

创建这个对象

    let animation = wx.createAnimation({
        duration: 2000,
        delay: 0,
        timingFunction: "linear",
    });

这个animation就是通过wx.createAnimation之后返回的实例。在创建过程中,可以给这个实例添加一些属性,如以上代码所示,等同于css3animation:$name 2s linear的写法。

添加动效

实例创建完成之后,基于该实例,添加需要的动态效果,动态类型可以查阅文档得知,以最常见的移动,旋转为例:

    animation.translate($width, 0).rotate($deg);

结束动画

.step()表示一组动画的结束

    animation.step();

导出动画

动画效果添加完成了,如何给想要的dom添加动效呢。这里需要用到.export()导出动画队列,赋值给某个dom对象。

    this.setData({ moveOne: animation.export() })
    <view  animation="{{moveOne}}"></view>

例子

以下将通过2组动画,来对比一下css3api实现方式的不同。

一、模块移动动画

动画效果:

下图有两组动画,分别为api方式(上)与css3方式(下)完成的效果,点击move按钮,动画启动。

image

代码实现

以下分别为css3api的核心代码:

css3:
    <!-- wxml -->
    <view class='border'>
        <view class='css-block {{isMove && "one"}}'></view>
        <view class='css-block {{isMove && "two"}}'></view>
        <view class='css-block {{isMove && "three"}}'></view>
        <view class='css-block {{isMove && "four"}}'></view>
    </view>
    // scss
    @mixin movePublic($oldLeft,$oldTop,$left,$top) {
        from {
          transform:translate($oldLeft,$oldTop);
        }
        to {
          transform:translate($left,$top);
        }
    }
    
    @mixin blockStyle($color,$name) {
        background: $color;
        animation:$name 2s linear infinite alternate;
    }
    .one {
        @include blockStyle(lightsalmon,onemove);
    }
    
    @keyframes onemove {
        @include movePublic(50rpx,-25rpx,-150rpx,0rpx);
    }
    
    .two {
        @include blockStyle(lightblue,twomove);
    }
    
    @keyframes twomove {
        @include movePublic(0rpx,25rpx,-50rpx,0rpx);
    }
    
    .three {
        @include blockStyle(lightgray,threemove);
    }
    
    @keyframes threemove {
        @include movePublic(0rpx,25rpx,50rpx,0rpx);
    }
    
    .four {
        @include blockStyle(grey,fourmove);
    }
    
    @keyframes fourmove {
        @include movePublic(-50rpx,-25rpx,150rpx,0rpx);
    }
    // js
    moveFunction(){
        this.setData({
            isMove: true
        })
    }

css3中通过动态改变class类名来达到动画的效果,如上代码通过onetwothreefour来分别控制移动的距离,通过sass可以避免代码过于冗余的问题。(纠结如何在小程序中使用sass的童鞋请看这里哦:wechat-mina-template

api:
    moveClick(){
        this.move(-75,-12.5,25,'moveOne');
        this.move(-25,12.5, 0,'moveTwo');
        this.move(25, 12.5,0,'moveThree');
        this.move(75, -12.5,-25,'moveFour');
        this.moveFunction(); // 该事件触发css3模块进行移动
    },

    // 模块移动方法
    move: function (w,h,m,ele) {
        let self = this;
        let moveFunc = function () {
        let animation = wx.createAnimation({
            duration: 2000,
            delay: 0,
            timingFunction: "linear",
        });
    
        animation.translate(w, 0).step()
        self.setData({ [ele]: animation.export() })
        let timeout = setTimeout(function () {
            animation.translate(m, h).step();
            self.setData({
                // [ele] 代表需要绑定动画的数组对象
                [ele]: animation.export()
            })
          }.bind(this), 2000)
        }
        moveFunc();
        let interval = setInterval(moveFunc,4000)
    }

效果图可见,模块之间都是简单的移动,可以将他们的运动变化写成一个公共的事件,通过向事件传值,来移动到不同的位置。其中的参数w,h,m,ele分别表示发散水平方向移动的距离、聚拢时垂直方向、水平方向的距离以及需要修改animationData的对象。

通过这种方法产生的动画,无法按照原有轨迹收回,所以在事件之后设置了定时器,定义在执行动画2s之后,执行另一个动画。同时动画只能执行一次,如果需要循环的动效,要在外层包裹一个重复执行的定时器到。

查看源码,发现api方式是通过js插入并改变内联样式来达到动画效果,下面这张动图可以清晰地看出样式变化。

代码变化

打印出赋值的animationDataanimates中存放了动画事件的类型及参数;options中存放的是此次动画的配置选项,transition中存放的是wx.createAnimation调用时的配置,transformOrigin是默认配置,意为以对象的中心为起点开始执行动画,也可在wx.createAnimation时进行配置。

animationData

二、音乐播放动画

上面的模块移动动画不涉及逻辑交互,因此新尝试了一个音乐播放动画,该动画需要实现暂停、继续的效果。

动画效果:

播放音乐

两组不同的动画效果对比,分别为api(上)实现与css3实现(下):

旋转对比

代码实现

以下分别是css3实现与api实现的核心代码:

css3:
    <!-- wxml -->
    <view class='music musicTwo musicRotate {{playTwo ? " ": "musicPaused"}} ' bindtap='playTwo'>
        <text class="iconfont has-music" wx:if="{{playTwo}}"></text>
        <text class="iconfont no-music" wx:if="{{!playTwo}}"></text>
    </view>
    // scss
    .musicRotate{
        animation: rotate 3s linear infinite;
    }
    
    @keyframes rotate{
        from{
            transform: rotate(0deg)
        }
        to{
            transform: rotate(359deg)
        }
    }
    
    .musicPaused{
        animation-play-state: paused;
    }
    // js
    playTwo(){
        this.setData({
            playTwo: !this.data.playTwo
        },()=>{
            let back = this.data.backgroundAudioManager;
            if(this.data.playTwo){
                back.play();
            } else {
                back.pause();
            }
        })
    }

通过playTwo这个属性来判断是否暂停,并控制css类的添加与删除。当为false时,添加.musicPaused类,动画暂停。

api:
    <!-- wxml -->
    <view class='music' bindtap='play'  animation="{{play && musicRotate}}">
        <text class="iconfont has-music" wx:if="{{play}}"></text>
        <text class="iconfont no-music" wx:if="{{!play}}"></text>
    </view>
    // js
    play(){
        this.setData({
            play: !this.data.play
        },()=>{
            let back = this.data.backgroundAudioManager;
            if (!this.data.play) {
                back.pause();
               // 跨事件清除定时器
               clearInterval(this.data.rotateInterval);
            } else {
                back.play();
                // 继续旋转,this.data.i记录了旋转的程度
                this.musicRotate(this.data.i);
            }
        })
    },
    musicRotate(i){
        let self = this;
        let rotateFuc = function(){
            i++;
            self.setData({
                i:i++
            });
            let animation = wx.createAnimation({
                duration: 1000,
                delay: 0,
                timingFunction: "linear",
            });
            animation.rotate(30*(i++)).step()
            self.setData({ musicRotate: animation.export() });
        }
        rotateFuc();
        let rotateInterval = setInterval(
            rotateFuc,1000
        );
        // 全局定时事件
        this.setData({
            rotateInterval: rotateInterval
        })
    }

通过api实现的方式是通过移除animationData来控制动画,同时暂停动画也需要清除定时器,由于清除定时器需要跨事件进行操作,所以定了一个全局方法rotateInterval

api方式定义了旋转的角度,但旋转到该角度之后便会停止,如果需要实现重复旋转效果,需要通过定时器来完成。因此定义了变量i,定时器每执行一次便加1,相当于每1s旋转30°,对animation.rotate()中的度数动态赋值。暂停之后继续动画,需要从原有角度继续旋转,因此变量i需要为全局变量。

代码变化

下图可以看出,api方式旋转是通过不断累加角度来完成,而非css3中循环执行。

代码对比

对比

通过上述两个小例子对比,无论是便捷度还是代码量,通过css3来实现动画效果相对来说是更好的选择。api方式存在较多局限性:

  1. 动画只能执行一次,循环效果需要通过定时器完成。
  2. 无法按照原有轨迹返回,需要返回必须定义定时器。
  3. 频繁借助定时器在性能上有硬伤。

综合以上,推荐通过css3来完成动画效果。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

薄荷前端招聘,快来!

🎉 前端小伙伴们,我们又有坑位开放啦,欢迎来撩!!

开放职位

Web 前端工程师(13k~18k)

职位描述

负责薄荷健康 Web 应用的设计、开发和维护

基本要求

  1. 1 年以上 Web 前端开发经验,有成熟作品演示
  2. 编程基础扎实,追求优雅的代码,有极客精神
  3. 擅长解决问题,善于沟通协作
  4. 计算机相关专科及以上学历

加分项

  1. 熟悉后端开发
  2. 坚持写技术博客
  3. 参与开源项目(PS:有 GitHub 账号请告知)

高级 Web 前端工程师(18k~25k)

职位描述

负责薄荷健康 Web 应用的架构设计与功能实现,具备独当一面的能力

基本要求

  1. 3 年以上 Web 前端开发经验,有成熟作品演示
  2. 有 H5 和 Native 本地混合开发经验
  3. 编程基础扎实,追求优雅的代码,有极客精神
  4. 擅长解决问题,善于沟通协作
  5. 计算机相关专科及以上学历

(PS:该职位暂不考虑实习生和应届生)

加分项

  1. 熟悉后端开发
  2. 熟悉 Android 或 iOS 开发
  3. 坚持写技术博客
  4. 参与开源项目(PS:有 GitHub 账号请告知)

团队介绍

  1. 薄荷非常重视技术文化、极客精神,每周都会有技术分享,并且建设了《薄荷前端周刊》;
  2. 我们目前主要开发H5及小程序,会逐步接入混合开发;
  3. 以react技术栈为主,积极拥抱新技术,面试不做框架方面要求,只问你熟悉的,但基础要好;
  4. 全员Mac,自带设备有补贴;
  5. 开发话语权比较高,工作自由;
  6. 薄荷是一家“打造科学健康的生活方式”的公司,所以很少加班,还会安排教练和健康顾问帮助你减肥、健身,强烈抵制996
  7. 公司位于【上海 世纪大道】,2、4、6、9号线直达,交通非常便捷。

简历投递

  1. 感兴趣简历请投递:[email protected]
  2. 邮件标题为:简历-姓名-职位-工作年限(例如:简历-阮一峰-高级前端工程师-3年)
  3. 简历请尽量提供PDF格式

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.