Git Product home page Git Product logo

blog's Introduction

🏗              👏
  🔥          ❄️
    🤖      👾
      💭  ❤️
        🧑‍💻

blog's People

Contributors

tommy-white avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

你错过的关于 Reactive Programming 介绍

original post

所以你很想知道被叫做 Reactive Programming 的东西, 特别是它的变体包括 Rx, Bacon.js, RAC 和其他。

... ...

学习之旅中最苦难的部分是 以Reactive思考。这就需要抛弃老式的声明式编程和典型编程习惯,强制你的大脑使用不同的范式工作。这方面我没有在互联网上找到任何的指南,并且我认为在世界上需有关于如何以Reactive思考的实用教程,所以你可以开始了。阅读库文档可以点亮你的道路,我希望这也能帮助到你。

什么是 Reactive Programming ?

在网上有太多不好的解释和定义。... ...

Reactive Programming 是结合 异步 数据流 的编程方式

简言之,这些都不是新事物。Event buses(???) 或者你典型的点击事件就是真实的一个异步事件流,你可以观察它执行一些副作用。流十分廉价和普及,任何事物都可以成为流:变量,用户输入,属性,缓存,数据结构,等。例如,想象一下,你的Twitter提醒将是一个与点击事件相同的数据流。你可以监听这个流并做成相应反应。

**在此之上,你被给予了一个惊人的 toolbox of functions to combine, 创建、过滤这些任意的流。**这就是“功能性”魔术引入。流可以被当作另一个的输入。甚至多个流都可以作为其他流的输入。你可以合并2条流。你可以过滤流获得只包含你感兴趣的事件的另一条流。你可以将流中数据映射到另一条流中。

如果流对于Reactive来说是如此重要,那么让我们仔细看看它们,从我们熟悉的“点击按钮”事件流开始。

image

流是一系列随事件推进的事件。它可以发出三种形式:一种值(某种类型<联系Promise.resolve(value)>),一个错误(error),或者“完成的(completed)”信号。 考虑到“完成”发生,例如,当包含该按钮的当前窗口或视图关闭时。

我们仅异步捕获这些发出的事件,通过定义一个方法将在值发出时被调用。有时候剩余的这两种可被省略,你可以仅仅专注定义值发出时对应的方法。监听此流被叫做 subscribing 。这些定义的方法是 observers。 流是被观察的主体(或“可观察的”)。这恰好就是观察者模式

绘制该图的另一种方法是使用ASCII,我们将在本教程的某些部分使用它:

--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline

因为这已经非常熟悉,我不希望你觉得无聊,让我开始些新的:我们将创建一个点击事件流从原始点击事件流中转换而来。

首先,让我们创建一个计数器流,指示单击按钮的次数。通常 Reactive 库,每条流都有许多方法附加其上,例如map,filter,scan,等。当你调用这些方法,例如 clickStream.map(f), 它返回一条基于点击流的 新的流 。它无论如何也不会修改原始的点击流。这个特性叫做不可变性,它和Reactive流相随就像煎饼和糖浆搭配一般。这允许我们链接像clickStream.map(f).scan(g)这样的函数:

clickStream: ---c----c--c----c------c-->
               vvvvv map(c becomes 1) vvvv
               ---1----1--1----1------1-->
               vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->

map(f)方法通过你提供的方法f更替(到新的流中)每一个发出的值。在此例中,我们我们映射点击的值变为数字1. scan(g)方法累积所有流上原始的值,产生新值x = g(accumulated, current), 这里g是一个相加方法。接着counterStream 发出点击时触发的总次数。

为了演示Reactive的强大, 我们假设你想要流上的"双击"事件。为了使它更有意思,我们想得到的新流中的双击和三击,或者通常,多次点击(2此或者更多)。想想我们用原始的方式完成会不会很复杂。

好的,在Reactive中非常的简单。实际上,真真的逻辑就4行代码。但我们先忽略代码。思考以下图例以帮助我们更好的理解和构建流,无论你是菜鸟还是专家。

image

灰色块状表示方法转换流成为新流。首先我们累积流上但点击,whenever 250 milliseconds of "event silence" has happened (that's what buffer(stream.throttle(250ms)) does, in a nutshell. 不必担心不理解这点细节,我们现在只是演示Reactive。这样结果是一流列表,然我们我应用map()同映射每一个列表得到每个列表的长度。最后我们忽略长度为1的值通过filter(x >= 2)方法。就是这样:3个操作生产我们想要的流。接着我们可以定义它,对我们的期望作出反应。

我希望你享受这美妙的方法。这里的例子只是冰山一角:你可以应用同样的操作在不通的流上,例如,在API相应流;另一方面,还有许多其他的方法可以使用。

“为什么我要考虑采纳Reactive Programming(RP)”

Reactive Programming提高了代码的抽象级别,因此您可以专注于定义业务逻辑的事件的相互依赖性,而不必不断地调整大量的实现细节。 使用RP的代码可能更简洁。

现代webapps和移动应用程序中的好处更加明显,这些应用程序与大量与数据事件相关的UI事件具有高度交互性。10年前,与网页的交互基本上是关于向后端提交长格式并对前端执行简单渲染。 应用程序已经发展为更加实时:修改单个表单字段可以自动触发向后端的保存,“喜欢”某些内容可以实时反映给其他连接的用户,等等。

如今的应用程序拥有丰富的各种实时事件,可为用户提供高度互动的体验。 我们需要工具来正确处理它,而Reactive Programming就是一个答案。

通过例子以RP思考

让我们深入了解真实的东西。 一个真实的例子,提供了如何在RP中思考的分步指南。 没有合成的例子,没有半解释的概念。 在本教程结束时,我们将生成真正的功能代码,同时了解我们为什么要做每件事。

我选取了 JavaScriptRxJS作为例子的工具,因为: JavaScript已然成为当今最流行的语言, 并且 Rx* library family 在各种语言和平台上都有实现(.NET, Python, C++, JAVA....)。所以无论你的工具选取是什么,按照本教程,您可以获得具体的益处。

实现“Who to follow” 建议box

在Twitter中有一个UI元素用于建议你可以关注其他账户:
image

我们将专注莫让其核心功能,即:

  • 起始时,从API中加载用户数据显示3条建议
  • 点击“Refresh”, 加载另外3条账户到3行中
  • 点击 “x” 按钮,删除该行用户并显示另一位用户
  • 每行都会显示账户头像和跳转到他们主页的链接

我们可以抛弃其他的功能和按钮因为他们是次要的。实际上Twitter最近意见关闭了未授权访问的API, 让我们构建此UI去关注Github的用户。这里是Github 获取用户的 API

The complete code for this is ready at http://jsfiddle.net/staltz/8jFJH/48/ in case you want to take a peak already.

[译] 图解 Go 并发

注:为减少歧义本文中所有并发或并行均指示有处理多个任务的能力,并不是严格意义上的并发与并行.

你可能已经以一种方式或者其他途径听过Go。它备受欢迎是有一定道理的。Go 快速、间断,背后有一个强大的社区。学习该语言最令人激动的方面是它的并发模型。Go的并发原生支持(原语)(concurrency primitives)使它能简单快乐的创建并发、多线程程序。我将通过插图介绍Go的并发原语,以使这些概念能够为将来学习所用。本文适用于刚接触Go并想开始了解Go并发原语(go例程和通道)的人员。

单线程 vs. 多线程 程序

你或许之前写过许多单线程程序。使用多个方法执行一个特殊的任务是编程中一个常见的模式,但是它们是等到上一部分程序返回数据后才被调用。
image
这就是我们最初设置第一个示例的方式,该程序可开采矿石。此示例中的功能执行:寻找矿石开采矿石冶炼矿石 。在我们的示例中,矿山和矿石被表示为字符串数组,每个函数都接受并返回一个“已处理”字符串数组。对于单线程应用程序,该程序将设计如下。
image
这里有3个主要的方法。 finder、miner、smelter 。在此版本的程序中,我们的方法运行在一个单一线程上,一个接着一个 --- 并且这个单线程(这只地鼠名叫Gary)需要做所有的事情。

func main() {
 theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}
 foundOre := finder(theMine)
 minedOre := miner(foundOre)
 smelter(minedOre)
}

在每个函数的末尾打印出结果数组“ ore”,我们得到以下输出:

From Finder: [ore ore ore]
From Miner: [minedOre minedOre minedOre]
From Smelter: [smeltedOre smeltedOre smeltedOre]

这种方式是的程序设计简洁,但是如果你想利用多线程并独立调用每个发放会发生什么呢?这就是并发编程起作用的地方。
image
这种挖掘效率更高。现在,多个线程(地鼠们)正在独立工作。因此,整个操作并不全在Gary身上。有一个地鼠在寻找矿石,一个在开采矿石,另一个在冶炼矿石–可能也是同时在做。

为了将这种类型的功能引入我们的代码中,我们需要做两件事:一中可以创建独立工作地鼠的方法和一个地鼠们可以相互交流(传递矿石)的方式。这就是Go并发中自建的: go routinechannel

Go routines

Go routines 可以被认为是轻量的线程。 创建 go routines 就是将 go 关键字添加到调用方法前面一样简单。举一个简单的例子,让我们创建两个查找器函数,使用go关键字调用它们,并在每次在矿井中发现“矿石”时将它们打印出来。
image

func main() {
 theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}
 go finder1(theMine)
 go finder2(theMine)
 <-time.After(time.Second * 5) // 目前你可以忽略它
}

这是我们程序的一次输出:

Finder 1 found ore!
Finder 2 found ore!
Finder 1 found ore!
Finder 1 found ore!
Finder 2 found ore!
Finder 2 found ore!

从上面的输出中可以看到,查找正在同时运行。谁先找到矿石没有可靠的顺序,多次运行后顺序并不总是相同。

这是巨大的进步!现在,我们有了一种简单的方法来建立多线程(多线程)程序,但是当我们需要独立的go例程相互通信时会发生什么呢?欢迎来到神奇的 channel 世界。

Channels

image
Channels 使得 go routines可以相互通信。你可以将channels想象为一个通道,每一个routine都可以发送和接受其他routine信息。
image

myFirstChannel := make(chan string)

Go routines 可以发送和接受数据与一个channel之上。这通过使用一个箭头<-表明数据的流向来完成。
image

myFirstChannel <- "hello" // Send
myVariable := <- myFirstChannel // Receive

现在,通过使用渠道,我们可以让寻找矿石的地鼠立即将他们发现的东西发送给破碎矿石的地鼠,而无需等待发现所有事情。

image

我已经更新了示例,因此将查找程序代码和挖掘方法设置为未命名(匿名)的功能。如果您从未见过lambda函数没有过多关注程序的那部分,那就知道每个函数都用go关键字调用了,因此它们是在自己的go例程上运行的。重要的是要注意go例程如何使用通道oreChan在彼此之间传递数据。不用担心,我将在最后解释未命名的功能。

func main() {
 theMine := []string{"ore1", "ore2", "ore3"}
 oreChan := make(chan string)
 // Finder
 go func(mine []string) {
  for _, item := range mine {
   oreChan <- item //send
  }
 }(theMine)
 // Ore Breaker
 go func() {
  for i := 0; i < 3; i++ {
   foundOre := <-oreChan //receive
   fmt.Println("Miner: Received " + foundOre + " from finder")
  }
 }()
<-time.After(time.Second * 5) // 再一次再次忽略
}

你可看到如下输出

Miner: Received ore1 from finder
Miner: Received ore2 from finder
Miner: Received ore3 from finder

太好了,现在我们可以在程序中的不同go例程(地鼠)之间发送数据了。在开始编写带有通道的复杂程序之前,让我们首先介绍一些对理解通道属性至关重要的知识。

Channel Blocking

在各种情况下,通道都会阻止例程执行。这样一来,我们的go例程就可以彼此同步一会儿,然后再继续独立运行。(注:形成异步控制)

Blocking on a Send 阻止发送

image

一旦一个 go routine(地鼠)往 channel 发送数据, 发送的 go routine 将阻塞,直到另一个go例程接收到通道上发送的内容为止。
image

Blocking on a Receive 阻止接受

与阻止发送类似,尚未发送任何东西时, 一个 go routine 可以阻塞直到从通道获取一个值。

阻塞起初可能会有些混乱,但是您可以将其视为两个go例程(密码)之间的事务。无论地鼠是在等待钱还是在汇款,它都会等到交易中的另一位伙伴出现。

现在我们有两种不同的方式可与阻塞 go routine 在 channel 中的传递,让我们来讨论两种不同的 channel 类型: unbufferedbuffered 。选择使用哪种类型的通道可以更改程序的行为。

Unbuffered Channels 无缓冲通道

image

在前面的所有示例中,我们一直在使用无缓冲通道。使它们与众不同的原因是一次只能通过通道容纳一个数据。

Buffered Channels 缓冲通道

image

在并发程序中,时机并不总是完美。在我们的采矿示例中,我们可能会遇到这样一种情况,我们的勘测地鼠在开采地鼠处理一件矿石的时间内可以找到3块矿石。为了不让勘测地鼠花费大部分时间等待向破碎的地鼠发送一些矿石直到完成,我们可以使用缓冲通道。让我们首先创建一个容量为3的缓冲通道。

bufferedChan := make(chan string, 3)

缓冲通道类似无缓冲通道,但是有一点不同,我们可以不必在另一个 go runtine 读取通道的数据前发送多条数据到通道之中。
image

bufferedChan := make(chan string, 3)
go func() {
 bufferedChan <- "first"
 fmt.Println("Sent 1st")
 bufferedChan <- "second"
 fmt.Println("Sent 2nd")
 bufferedChan <- "third"
 fmt.Println("Sent 3rd")
}()
<-time.After(time.Second * 1)
go func() {
 firstRead := <- bufferedChan
 fmt.Println("Receiving..")
 fmt.Println(firstRead)
 secondRead := <- bufferedChan
 fmt.Println(secondRead)
 thirdRead := <- bufferedChan
 fmt.Println(thirdRead)
}()

输出

Sent 1st
Sent 2nd
Sent 3rd
Receiving..
first
second
third

为简单起见,我们不会在最终程序中使用缓冲的通道,但是理解使用哪种 Channel 在你的并发模型中很重要。

全部合在一起

现在,借助go例程和通道的强大功能,我们可以使用Go的并发原语编写一个充分利用多个线程的程序。
image

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
oreChannel := make(chan string)
minedOreChan := make(chan string)
// Finder
go func(mine [5]string) {
 for _, item := range mine {
  if item == "ore" {
   oreChannel <- item //send item on oreChannel
  }
 }
}(theMine)
// Ore Breaker
go func() {
 for i := 0; i < 3; i++ {
  foundOre := <-oreChannel //read from oreChannel
  fmt.Println("From Finder: ", foundOre)
  minedOreChan <- "minedOre" //send to minedOreChan
 }
}()
// Smelter
go func() {
 for i := 0; i < 3; i++ {
  minedOre := <-minedOreChan //read from minedOreChan
  fmt.Println("From Miner: ", minedOre)
  fmt.Println("From Smelter: Ore is smelted")
 }
}()
<-time.After(time.Second * 5) // Again, you can ignore this

输出:

From Finder:  ore
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted
From Miner:  minedOre
From Smelter: Ore is smelted
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted

与原始示例相比,这是一个很大的改进!现在,我们的每个函数都在自己的go例程上独立运行。另外,每当加工一块矿石时,它都会进入我们采矿线的下一个阶段。

为了始终专注于了解通道的基本知识和执行例程,上面没有提到一些重要信息-如果您不知道,可能会在开始编程时造成一些麻烦。现在,您已经了解了go例程和通道的工作原理,下面让我们了解一些在开始使用go例程和通道进行编码之前应该了解的信息。

在你开始前,你要知道...

匿名Go Routines

image

与我们使用go关键字将函数设置为在自己的go例程上运行的方式类似,我们可以使用以下格式创建匿名函数以在其go例程上运行:

// Anonymous go routine
go func() {
 fmt.Println("I'm running in my own go routine")
}()

这样,如果我们只需要调用一次函数,就可以将其放在自己的go例程中运行,而不必担心创建正式的函数声明。

main 函数是一个 go routine

image

main 函数实际上在自己的 go routine 中运行!更重要的是要知道,一旦主函数返回,它将关闭当前正在运行的所有其他go例程。这就是为什么我们在主要功能的底部有一个计时器的原因-它创建了一个通道并在5秒钟后发送了一个值。

<-time.After(time.Second * 5) //Receiving from channel after 5 sec

还记得go例程将如何阻止读取,直到发送一些东西?通过在上面添加此代码,这正是主例程正在发生的事情。主例程将阻塞,使我们的其他go例程有5秒钟的额外运行时间。

现在,有更好的方法来处理阻塞主函数的操作,直到完成所有其他go例程。通常的做法是创建一个完成的通道,主要功能在等待读取时会阻塞该通道。完成工作后,写入此通道,程序将结束。

image

func main() {
 doneChan := make(chan string)
 go func() {
  // Do some work…
  doneChan <-Im all done!”
 }()
 
 <-doneChan // block until go routine signals work is done
}

You can range over a channel

In a previous example we had our miner reading from a channel in a for loop that went through 3 iterations. What would happen if we didn’t know exactly how many pieces of ore would come from the finder? Well, similar to doing ranges over collections, you can range over a channel.

Updating our previous miner function, we could write:

// Ore Breaker
 go func() {
  for foundOre := range oreChan {
   fmt.Println(“Miner: Received+ foundOre +from finder”)
  }
 }()

Since the miner needs to read everything that the finder sends him, ranging over the channel here makes sure we receive everything that gets sent.

Note: Ranging over a channel will block until another item is sent on the channel. The only way to stop the go routine from blocking after all sends have occurred is by closing the channel with ‘close(channel)’

You can make a non-blocking read on a channel

But you just told us all about how channels block go routines?! True, but there is a technique where you can make a non-blocking read on a channel, using Go’s select case structure. By using the structure below, your go routine will read from the channel if there’s something there, or run the default case.

 
go func(){
 myChan <-Message!”
}()
 
select {
 case msg := <- myChan:
  fmt.Println(msg)
 default:
  fmt.Println(“No Msg”)
}
<-time.After(time.Second * 1)
select {
 case msg := <- myChan:
  fmt.Println(msg)
 default:
  fmt.Println(“No Msg”)
}

You can also do non-blocking sends on a channel

Non-blocking sends use the same select case structure to perform their non-blocking operations, the only difference is our case would look like a send rather than a receive.

select {
 case myChan <-message”:
  fmt.Println(“sent the message”)
 default:
  fmt.Println(“no message sent”)
}

image

延展阅读

从 babel compiler 理解 async( / await) 函数

异步函数声明定义了一个异步函数。异步函数是一个通过事件循环异步操作的函数,使用隐式Promise返回其结果。但是使用异步函数的代码的语法和结构更像是使用标准同步函数。

image

由 babel preset 也可看出 async 函数属于 es2017 标准提案的功能语法。
在 babel preset 中勾选上 es2017 可得到 转译后的兼容代码如下:

  • img-1:
    image
  • img-2:
    image

async 语法糖

由 img-2 可以看出 async / await 实际上只是 generator fn 语法糖(line: 38-42), 其内部还是依靠 generator fn 返回的迭代器对象。但由于 generator 函数的执行控制时需要使用迭代器对象上next()方法控制,那async如何控制执行流程的呢? 这就依赖于 img-1 上的实现。

基于generator的执行控制流

我们将上述代码添加注释以便能够洞察其基本实现:

// #2.
// asyncGeneratorStep 基于 promise 控制 generator fn 的执行
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    // 内部首先调用 next 或 throw 方法
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  // 若迭代器 finished 则直接fulfilled promise
  if (info.done) {
    resolve(value);
  } else {
    // 否则使用 then 链接再次调用
    Promise.resolve(value).then(_next, _throw);
  }
}

// #1.
// _asyncToGenerator定义一个方法接受一个 <generator fn>
function _asyncToGenerator(fn) {
  // _asyncToGenerator 返回一个 fn
  return function() {
    // 该 fn 记录了调用时参数以及this只想
    var self = this,
      args = arguments;
    // 该 fn 返回一个 Promise 实例
    return new Promise(function(resolve, reject) {
      // executor 中首先调用 generator fn 得到一个迭代器对象 gen
      var gen = fn.apply(self, args);
      // 定义,封装 _next 方法,其接受一个参数(value)[该参数用于2此迭代之间数据传递].
      function _next(value) {
        // 调用 asyncGeneratorStep
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      // 定义,封装 _throw 方法
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      // 首次调用next(类似 gen.next())
      _next(undefined);
    });
  };
}

JavaScript Trap ⎈


1.声明对象时为属性赋值为undefined与不声明此属性有和区别🤔?

in操作符处理逻辑不同。未声明的props返回为false、声明为undefined的props返回true

const test = {
  prop: undefined
}

// 测试如下
'prop' in test  
// -> true
'none' in test
// -> false

ps: 此例中prop为已经声明属性赋值为undefined(调用Object.defineProperty),而none为未声明属性访问未声明属性将返回undefined


2.String.replace第一个参数为正则表达式, 并且其为全局匹配模式, 而第二个参数为字符串或方法时有什么区别?

你可以指定一个函数作为第二个参数。在这种情况下,当匹配执行后, 该函数就会执行。 函数的返回值作为替换字符串。 (注意: 上面提到的特殊替换参数在这里不能被使用。) 另外要注意的是, 如果第一个参数是正则表达式, 并且其为全局匹配模式, 那么这个方法将被多次调用, 每次匹配都会被调用
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace#%E6%8C%87%E5%AE%9A%E4%B8%80%E4%B8%AA%E5%87%BD%E6%95%B0%E4%BD%9C%E4%B8%BA%E5%8F%82%E6%95%B0

let x =0 
let y = 0
const str = '%s%s%s%s' 

str.replace(/%s/g, x++)
// => 0000
x
// => 1

str.replace(/%s/g, ()=>y++)
// => 0123
y
// => 4

3.几种原生/提案支持的对象拷贝方式有何(不详尽)差异 🔍?

1.拷贝类型

  • Deep clone: JSON.parse(JSON.stringify(obj))
  • Shalow clone: { ...obj }Object.assign({}, obj)

2.相互差异与各自issua

  1. JSON.parse(JSON.stringify(obj))
  • issua:1.此方法不能拷贝定义的方法; 2.不适用于循环♻️引用对象; 3.无法很好支持props descriptor(eg: writable). 参见以下:
// #1
const test1 = { fn: ()=>{}, num: 1 }
console.log(JSON.parse(JSON.stringify(test1)))

// #2
const test2 = { }
test2.circle = test2
JSON.parse(JSON.stringify(test1)) // Error

// #3 
const test3 = {}
Object.defineProperties(test3, {
  unWrite: {value: 1, enumerable: true},
  unIteration: { value: 2, writable: true}
})
const copied = JSON.parse(JSON.stringify(test3))

test3.unWrite = 0;
copied.unWrite = 0;
console.log(test3, copied)
  1. { ...obj } vs Object.assign({}, obj)

关于使用深拷贝补充: 1.https://news.ycombinator.com/item?id=16233330


4. 字符转译序列 \x\u 的区别?

字符编码(character code)是特定Unicode字符的数字表示. 在JavaScript中可以使用string.charCodeAt()查看一个字符特定的Unicode编码(直到U+ffff: 0-65535)。

  • '\uxxxx' 为字符以十六进制的Unicode编码,长度为4个字符: '\u0000' -> '\uffff'
  • '\xxx''\uxxxx'但长度为2个字符,即任何字符代码低于256的字符(即扩展ASCII范围内的任何字符)都可以使用前缀为\ x的十六进制编码字符代码进行转义。 且经过测试发现\x对于在低版本兼容性上优于 \u (ie <= 8 支持 \x 但不支持 \u)

5. 带有默认值的参数后还有没带默认值带参数对函数的影响

如果未传递值或未定义,则默认函数参数允许使用默认值初始化命名参数。

  • 建议针对尾部参数使用默认值,且默认值表达式是在实际调用时执行。
  • 目前使用函数默认参数,进过 babel 之类的转译器可见,默认参数值将导致实际函数签名发生变化,形参个数变为从原始函数的第一个参数到第一个带有默认值的参数前一位参数。(这回导致某些异常,eg. 对函数进行柯里化)具体可见下图:

20190927103801


Nginx接触

Nginx 是什么

Nginx是一款Web服务器,具备Web的基本功能:基于REST架构风格,以统一资源描述符或者资源定位符作为沟通一句,通过HTTP为浏览器等客户端程序提供各种网络服务.

正向代理与反向代理

现代互联网应用大多基于CS模式(client-server)。代理服务(proxy server)是一种服务(计算机系统或应用程序),其充当来自从其他客户端的请求寻求服务器资源的的中介。例如:
image

  1. 反向代理(reverse proxy):
    反向代理接收来自Internet的请求并将其转发到内部网络中的服务器。那些发出请求的人连接到代理,可能不知道内部网络。
    在反向代理中(事实上,这种情况基本发生在所有的大型网站的页面请求中),客户端发送的请求,想要访问server服务器上的内容。但将被发送到一个代理服务器proxy,这个代理服务器将把请求代理到和自己属于同一个LAN下的内部服务器上,而用户真正想获得的内容就储存在这些内部服务器上。代理对用户是透明的,即无感知。不论加不加这个反向代理,用户都是通过相同的请求进行的,且不需要任何额外的操作;代理服务器通过代理内部服务器接受域外客户端的请求,并将请求发送到对应的内部服务器上。
    image

  2. 正向代理(forward proxy)
    通俗等讲正向代理(一般又直接叫做代理)是用来做转发代理的。当一个客户端在Internet上对文件传输服务器进行连接尝试时,其请求必须首先通过转发代理。根据转发代理的设置,可以允许或拒绝请求。如果允许,则将请求转发到防火墙(可有可没有),然后转发到文件传输服务器。从文件传输服务器的角度来看,它是发出请求的代理服务器,而不是客户端。因此,当服务器响应时,它会对代理响应。但是当转发代理接收到响应时,它会将其识别为对之前经历的请求的响应。然后它又将响应发送给发出请求的客户端。由此可以看出正向代理对用户来说并非透明,即是你自己配置或者清楚知道请求将被代理服务器代理。
    image

nginx.conf

nginx是高度模块化对,有很多基本对功能都能通过对其设置配置而达到,比如以下几点常用功能:

  • 解决跨域问题
    ...
  • 控制访问地址
    ...
  • 站点重定向
    ...
  • 合并请求
    ...
  • 图片处理
    ...
  • 页面内容修改
    ...

Javascript 101 - Event with Bubble and Capture

我们以一个toy demo 开始.

// dom level
  -> div (onclick)
    -> p
      -> span

问题🤔: 为什么当我们点击<p>, <span> click 事件也触发了?

Bubbling(冒泡)

冒泡是事件的一种传递机制,当事件触发时,事件会以相反的顺序传播从目标元素传递至父级元素,最后以Window结束。

就拿前面的例子🌰来说,当用户点击<span>元素时,事件会从下至上(子元素->父元素)依次触发元素的click事件。

Capture (捕获)

我们知道在事件的执行机制中除了冒泡还有一种就是捕获。

其实这是不完全正确的!!!,依据W3C的定义, 事件的执行分为了3阶段

  1. Capture(捕获阶段): 事件对象通过Window传播到目标的祖先父级再到自身。

  2. Target(目标阶段): 事件对象到达目标自身, 当该事件类型指定不Bubble, 则事件将在此阶段终止(稍后解释)

  3. Bubble(冒泡阶段): 事件对象以相反的顺序传播通过目标的父级祖先,并以Window结束。

image

所以当一个事件发生时标准的传递流为 捕获 -> 目标 -> 冒泡

当我们程序在主流的浏览器运行时,我们在 html 中使用 on[eventType] 或者 javascript 中使用 element. addEventListener(eventType, listener) 这些Web API像是忽略了 捕获阶段 只会运行 目标阶段 和 冒泡阶段。

问题🤔:那么这时候就产生了一个问题? 若我们在一个嵌套的Dom上分别添加事件,我们就不能改变事件的触发顺序(子元素绑定的事件会先于父元素绑定的事件触发),我们如何改变这个顺序?

EventListener Option -【 options ={} | useCapture = boolean 】

感谢 🙏 Web API 的完备性,如果你看过 addEventListener API 的定义,你会发现,声明是有第三个参数选项的,它可传入一个 boolean 或者 一个对象。

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);

所以当你在一个 target 上添加对应的事件监听时, 你可以这样写:

elem.addEventListener(_, _, {capture: true})
// 对象的形式还接受更多的option: {once,...}
// 或者
elem.addEventListener(_, _, true)

他们是等价的,表示监听的回调将会在 捕获阶段 被触发。那么这样我们可以解决上面的问题。
例如:

// dom level
  -> div (onclick= log('div', true))
    -> p (onclick = log('p'))
      -> span
// 此时输出顺序为 div -> p
// 相反
 -> div (onclick= log('div'))
    -> p (onclick = log('p'))
      -> span
// 此时输出顺序为 p -> div

listener callback param - Event

首先对于listener来说,它不仅仅只能传入function还能是一个对象但这个对象必须实现Event接口(包含handleEvent(fn)属性),在此我们不对它过多解释,具体可以去参看文档.

接下来我们具体说说 listener 被调用时传入的参数 Event 或 简写为 e

e.target

// dom level
  -> div (onclick)
    -> p
      -> span

还是使用之前的例子,我们解释几个误区(这里可能存在错误🙅,希望大佬发现后不吝纠正):

⚠️误区 1 : 当<div>添加了 click 事件,是否代表 <p>, <span> 也添加了 click 事件
答: ❌, 事实上在 <div> 添加了 click 与它的子元素并没有任何关系,但是可以通过
Event 对象拿到触发事件的真真对象,这看起来就好像是 <div> 的子元素同样添加了此事件监听,所以回调是发生在目标元素身上的(click 的 listener callback 是发生在 div上的)。

⚠️误区 2: 既然事件的传递分为 捕获 -> 目标 -> 冒泡,那么为什么一次点击事件不会多次触发。
答: 从之前的 addEventListener API 我们知道 事件是可以绑定到 冒泡 或者 捕获 阶段的,当没有设置是默认是在 冒泡阶段 所以只会触发一次,那就是在对应的绑定阶段。 注意: 既然添加事件是区分阶段,那么在移除此事件时也需要明确对应阶段。

解释了上述误区,我们回过来说 e.target
当事件触发时,e.target (read-only)的值为最深嵌套相关元素,而并不一定为添加事件所对应元素。例如上诉例子,当你点击<span>时事件传递冒泡走到<div>元素其 click 事件回调被触发,而e.target 的值是 span 元素对象。

e.currentTarget

我们知道了 e.target(read-only) 并不一定为添加事件所对应元素,那么如何在回调中知道是那个元素添加的此事件监听呢? 这就是 e.currentTarget ,另外在 listener 方法内部也可以直接使用 this,它等同于(this = event.currentTarget

e.path

e.path (read-only)为一个数组eg: [span, p, div, body, html, document, Window]它表示从 e.targetwindow 所经历到元素层级。

e.stopPropagation()

我们在事件传递阶段讲 捕获阶段 的时候提到 可以提前终止不在进行冒泡阶段。 这是怎么做的了,其实可以在捕获阶段添加的监听事件回调被调用时候调用 e.stopPropagation() 来阻止事件在DOM中进一步传播。 由于历史原因也可以调用 e.cancelBubble = true 来阻止事件冒泡(但不建议使用此属性,最好使用 stopPropagation 方法)

e.[other]

Event对象上还有许多属性,在这里不会全部罗列,最常用的基本上就是以上几个,其余属性还可以拿到很多信息,但部分属性可能并不是标准


以上基本是你需要知道EventListener的所有知识,如有不全或错误请指教👆

CSS Craft 💅


1. 使用<img src srcset ... 实现响应式图像

定义不同大小的同一图像,允许浏览器来选择合适的图像源。

// xxx/logo.png & xxx/[email protected]
<Img
  src={`${ICON_BASE}/logo.png`}
  srcset={`${ICON_BASE}/[email protected] 2x`}
  ...
/>

/*** basic usage 
  srcset=" 
      url size,
      url size
  "
***/

Read more: https://html.com/attributes/img-srcset


2. 图片悬浮缩放

悬停缩放(zoom-on-hover)效果是一种吸引注意力到可点击图像的好方法。当用户将鼠标悬停在其上方时,图像会略微放大,但其尺寸保持不变。

  1. 在 img 中设置动画过渡时间,并在 :hover 选择器上设置图片放大效果;
  2. 使用 div 等元素包裹 img 并设置 overflow 属性为 hidden, 并固定其width, height 属性.
.inner-img {
  transition: 0.3s;
}
.inner-img:hover {
  transform: scale(1.1);
}

.img-wrapper {  
  width: 400px;
  height: 400px;
  overflow: hidden; 
}

Demo: https://codepen.io/woo_tommy_l/pen/oNNpGXN


3. 类微信扫一扫渐变条码

通过 radial-gradient() 函数,其由一个从原点辐射开的在两个或者多个颜色之前的渐变组成。

#example {
  background: radial-gradient(150px 15px ellipse at bottom, green 0%, transparent 100%)
  // background: radial-gradient(150px 15px ellipse at 50% 100%, green 0%, transparent 100%)
}

Demo: https://www.w3schools.com/code/tryit.asp?filename=GCVMITBN6HOR


4.多行文本省略...

使用 CSS line-clamp 属性设置截断文本的最大行数.

Demo : https://codepen.io/woo_tommy_l/pen/NWRevmK

⚠️

  • line-clamp 需要多个属性的组合才可以实现效果,并且处于提议阶段需要-webkit-前缀。
  • 可以使用 [clamp.js(https://github.com/josephschmitt/Clamp.js)]来动态设置

[译] A Complete Guide to Flexbox [WIP]

在写一些应用时常常直接使用原始的css flex,其中属性较多经常需要参考CSS_TRICKS上的文章,想想干脆直接将该文章 trans 为中文方便查看。

Flexbox完整指南

我们的CSS flexbox布局综合指南。该完整指南解释有关flexbox的一切,专注父元素(flex容器 |flex container)和子元素(flex元素 |flex items)的所有不同可能属性。它还包括历史,演示,模式和浏览器支持图表(可能未完全搬运)。
翻译中弹性item 可指flex 容器中的item,亦可指期长宽可伸缩变化。

背景

Flexbox布局(flex box)模块(截至2017年10月的W3C候选推荐标准)旨在提供更有效的布局方式,容器中的项目之间对齐和分配空间,即使它们的大小未知和/或动态(因此单词“flex”)。

flex布局背后主要的想法是赋予修改容器子元素宽/高(和顺序)以最好的方式填充可用空间。(主要适用于所有类型的显示设备和屏幕尺寸)。Flex容器拉伸项目以填充可用空间,或缩小它们以防止溢出。

最重要的是,flexbox布局与方向无关,而不是常规布局(基于垂直的块和基于水平的内联块)。虽然这些页面适用于页面,但它们缺乏灵活性(没有双关语)来支持大型或复杂的应用程序(特别是在方向更改,调整大小,拉伸,缩小等方面)。

注意:Flexbox布局最适合应用程序的组件和小规模布局,而Grid布局则适用于更大规模的布局。

基础 & 术语

flexbox是一整套系统而不是一个单一属性,它包含了一系列的属性。其中一些是被用在容器上的(父元素,被叫做“flex container”)其他的则被用来设置子元素(被叫做“flex items”)。

如果“常规”布局基于块和内联流方向,则flex布局基于“flex-flow directions”。请查看规范中的这个图,解释flex布局背后的主要**。

image
注: 图中红绿蓝字是术语而不是真实css属性

子元素将被以此放置在main axis(从main-startmain end)或者corss-axis(从corss-startcorss end) [注:主轴或纵轴]

主轴 main axis:主轴数flex 容器沿主要方向放置item的轴。 ⚠️,它不一定就是水平方向,还取决与flex-direction属性

main-start | main-end: flex item被放置在flex container中起始与main-start终止与main-end

main-size:从main-start到main-end的长度它决定了flex container的width或者height

cross-axis:垂直于主轴的轴称为横轴。其方向取决于主轴方向。

[其余cross-* 类比与 main-*] ......


image

以下介绍真实设置在父元素的 css 属性

display

在元素上声明display属性则定义了一个flex container;内联或块级取决于给定的值。它为所有直接孩子提供了flex上下文。

.container {
  display: flex; /* or inline-flex */
}
// Note that CSS columns have no effect on a flex container.

flex-direction

image

次属性定义了主轴(main-axis),导致flex item在flex contain中的排列顺序。

.container {
  flex-direction: row(default) | row-reverse | column | column-reverse;
}
  • row: (默认): 在left to right从左至右;在right to left从右至左
  • row-reverse: 与row相反
  • column: 与row类似,但是是从上至下
  • column-reverse: 与column相反

flex-wrap

image

默认,flex items将试图排列在一行。您可以更改它并允许item根据需要使用此属性进行换行。

.container{
  flex-wrap: nowrap(default) | wrap | wrap-reverse;
}
  • nowrap: (默认)所有flex items将在一行
  • wrap: 放不下的item 将换行(从上至下)
  • wrap-reverse: 与wrap相反

flex-flow

缩写

flex-flow: <‘flex-direction’> || <‘flex-wrap’>

justify-content:

image

此定义沿主轴的对其方式。它有助于构建item之间的额外空间(当item都没有flex-grow时)或者是弹性的但已经达到最大值。It also exerts some control over the alignment of items when they overflow the line.

.container {
  justify-content: flex-start(default) | flex-end | center | space-between | space-around | space-evenly;
}

align-items

image

此定义了如何沿当前行的纵轴布置弹性item的默认行为。可以将其视为纵轴(垂直于主轴)的对齐版本。

.container {
  align-items: stretch(default) | flex-start | flex-end | center | baseline;
}

align-content

image

This aligns a flex container's lines within when there is extra space in the cross-axis, similar to how justify-content aligns individual items within the main-axis.

当纵轴上有额外的空间时,这会将flex容器的线对齐,类似于在主轴内对齐内容的内容。

**注意:**此属性对应只有一行flex item没有效果
.container {
align-content: flex-start | flex-end | center | space-between | space-around | stretch(default)(仍然受限与max-height/max-width);
}


image

以下介绍真实设置在子元素的 css 属性

order

image

默认情况下,flex item按源顺序排列。但是,order属性控制它们在flex容器中的显示顺序。

.item {
  order: <integer>; /* 默认 0 */
}

flex-grow

image

这定义了flex item在必要时拉伸的能力。这根据每一个item的比例来分配

默认值0代表不会被拉伸, 其他值按照分配比例划分

.item {
  flex-grow: <number>; /* 默认 0 */
  // 负数是非法的
}

flex-shrink

这定义了flex item在必要时缩小的能力。

默认为1代表总items超过了 flex container 的大小会被均匀的收缩(但父元素中flex-wrap必须是 nowrap)

.item {
  flex-shrink: <number>; /* 默认 1 */
  // 负数是非法的
}

flex-basis

This defines the default size of an element before the remaining space is distributed. It can be a length (e.g. 20%, 5rem, etc.) or a keyword. The auto keyword means "look at my width or height property" (which was temporarily done by the main-size keyword until deprecated). The content keyword means "size it based on the item's content" - this keyword isn't well supported yet, so it's hard to test and harder to know what its brethren max-content, min-content, and fit-content do.

.item {
  flex-basis: <length> | auto; /* default auto */
}

If set to 0, the extra space around content isn't factored in. If set to auto, the extra space is distributed based on its flex-grow value. See this graphic.

flex

This is the shorthand for flex-grow, flex-shrink and flex-basis combined. The second and third parameters (flex-shrink and flex-basis) are optional. Default is 0 1 auto.

.item {
  flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

It is recommended that you use this shorthand property rather than set the individual properties. The short hand sets the other values intelligently.

align-self

image

此允许改写某一个item默认但对其方式。
请参阅align-items说明以了解可用值。

.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

请注意,float,clear和vertical-align对flex item没有影响。

浏览器支持

由版本分解"flexbox":
-(new)表示规范中的最新语法(例如display:flex;)

  • (tweener)表示2011年的一种奇怪的非官方语法(例如display:flexbox;)
  • (old)表示2009年的旧语法(例如display:box;)

image
(大致兼容性范围)

JavaScript 101 - Iterable object & Iteration protocols & Generator

Intro

首先关于这些名称可能很多人都很模糊不清,尽管都是有关迭代遍历但并不清楚彼此的定义和区别,那现在我们就慢慢的解释清楚。

terminology

那我们优先从每一个术语称呼开始吧,来看看这些术语对应的中文名:

  • iterable object: [可]迭代对象

  • Iteration protocols: 迭代协议

  • generator:生成器

Iteration protocols - 迭代协议

细心的同学大概已经发现 Iteration protocols 是复数。是的在协议里其实有2种协议分别是:

  • Iteration protocols

    • iterator protocol:迭代器协议

    • iterable protocol:可迭代协议

what are they?

在以往的JavaScripts中已经存在许多对集合类型对迭代方法,例如:for .. in, for 循环, map(), forEach()... 这些语法或者API都是有JavaScript内部实现如何进行迭代集合。例如:

  1. for .. in语法就是遍历所有non-Symbol的可枚举属性
  2. map()API 就是对数组对索引依次遍历得到一个新数组;

那么我们如何对一个变量实现我们自定义的迭代方式呢,这就需要依靠迭代协议。其实在我看来 迭代器协议 与 可迭代协议 是非常类似的以至于初探时后很难弄清,下面我们分别来看看2个协议。

iterator protocol - 迭代器协议

image
😰😰😰

其实 迭代器协议 就是一个对象上定义了一个next属性,而这个next属性的定义有一定的要求,满足的话这就是一个实现了迭代器协议的对象,也被叫做 iterator : 迭代器

那么这个next属性有声明要求呢?

  1. 首先 next 是一个方法,它不接受任何参数传入;
  2. 其次调用next这个方法会返回一个对象,它包含2个属性 value & done,其中 value代表本次迭代得到的数据而 done用来表示迭代是否结束。例如:
    image
    以上 iterator 变量就是一个实现了迭代器协议的迭代器对象。

✌️PS:迭代器协议只是一种协议它指定了一个对象的迭代行为控制(每次迭代返回值、迭代终点),但如何自动迭代运行还需要自己编码实现(不然你得完全编码不停的iterator.next()、iterator.next()、iterator.next()),且每次迭代的上下文你得自己想办法保留。

iterable protocol - 可迭代协议

image
🤬🤬🤬

其实可迭代协议是一个对象是拥有 @@iterator 属性,而这个属性键的定义来自 Symbol.iterator, 同样@@iterator 属性有一定要求,满足要求就实现了可迭代协议。

这些要求分别是:

  1. [Symbol.iterator](key name)属性是一个方法,且不接受任何参数;
  2. 方法返回一个对象,这个对象就是迭代器协议对象。例如:
    image

这就是两种迭代协议对内容与区别,那么说完迭代协议我们可以来谈谈iterable object。

iterable object - [可]迭代对象

可迭代对象是对象上实现了 iterable protocol - 可迭代协议 的对象,且可以使用build-ins语法进行迭代,例如 for (let i in iterable)[...iterable]
⚠️注意: 使用这些build-ins语法必须是对象上实现了可迭代协议不是迭代器协议,否则对对象迭代将会抛出异常:

❌ Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
    at <anonymous>:1:1

目前有很多JavaScript内置对数据集合已经实现了迭代器协议,有:

  1. Array
const iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value); // 10 20 30
}
  1. String
const iterable = 'boo';

for (const value of iterable) {
  console.log(value);  // 'b' 'o' 'o'
}
  1. TypedArray
  2. Map & Set
  3. fn arguments
  4. DOM collections

到此你可能发现了要实现自定义迭代行为在写法上还是很复杂对,并且这里存在两种迭代协议,不同的库或者工具可能选用某一种方法实现迭代对象的行为,那么就会可能造成不兼容。但由于2中协议期本质又是十分类似所有我们可以创造一个同时满足迭代器协议和可迭代协议的对象,它类似:

var myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this }
}

这样看起来还是很复杂,于是有了我们最后要说的 generator

generator - 生成器

generator对象generator函数 返回,它既符合[可]迭代协议,又符合迭代器协议,就像刚刚那种模版写法。

它的写法如下:

// 生成器函数
function* gen() { 
  yield 1;
  yield 2;
}

// 生成器对象
const g = gen();

JavaScript支持了生成器语法我们就可以更快的实现自定义的迭代对象了,例如上面的一个例子我用生成器实现是这样的:
image

具体的 generator 语法再次不再过多解释,这就是 generator 与 itera... 之间的关系。

⚠️注意: 生成器对象不要重复使用
这句话什么意思,我们先来看一个MDN例子🌰:

const gen = (function *(){
  yield 1;
  yield 2;
  yield 3;
})();
for (const o of gen) {
  console.log(o);
  break;  // Closes iterator
}

// The generator should not be re-used, the following does not make sense!
for (const o of gen) {
  console.log(o); // Never called.
}

以上代码我们可以知道 generator对象 就像是一个一次性消费品(一次性筷子🥢)被迭代行为操作一次后将不会再次进行迭代。


基本上所有的东西就说完了,在补充说明最后一点东东

  1. 有了自定义迭代那么如何实现 迭代流 实现 非阻塞代码呢?在很早以前TJ大佬有实现一个库CO就是干这件事情的该库在社区也比较流行。
  2. 迭代器是一个很好去写异步代码的方式,但在 es2017 async/await 语法糖的引入,使得异步代码的编写与阅读更加方便。

ant-design-pro 与 umi 相辅相成的授权实现

在ant-design-pro(antpro)中有一系列的Authrize组件他们是antpro里授权的实现其中包含多种使用场景,而umi框架中使用配置式路由内置提供了特定的属性Routes能在配置路由时集成权限控制。

下面会参数关于antpro和umi细节:

antpro中的权限组件(Authrize)

权限组件,通过比对现有权限与准入权限,决定相关元素(子组件 \ noMatch组件)的展示。

Authrize组件的入口

查看Authrize的入口index.js

import Authorized from './Authorized';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';

Authorized.Secured = Secured;
Authorized.AuthorizedRoute = AuthorizedRoute;
Authorized.check = check;

export default renderAuthorize(Authorized);

从源码可以看到其导入了5个对象。前面4个都对应着权限组件的不同使用场景,其中Authorized是最常用的其他3个都作为属性挂载在Authorized。最后导入的是renderAuthorize, 然后默认导出renderAuthorize(Authorized)

renderAuthorize

那么renderAuthorize是干什么用的呢?

let CURRENT = 'NULL';
/**
 * use  authority or getAuthority
 * @param {string|()=>String} currentAuthority
 */
const renderAuthorize = Authorized => currentAuthority => {
  if (currentAuthority) {
    if (typeof currentAuthority === 'function') {
      CURRENT = currentAuthority();
    }
    if (
      Object.prototype.toString.call(currentAuthority) === '[object String]' ||
      Array.isArray(currentAuthority)
    ) {
      CURRENT = currentAuthority;
    }
  } else {
    CURRENT = 'NULL';
  }
  return Authorized;
};

export { CURRENT };
export default Authorized => renderAuthorize(Authorized);

由源码可以看出renderAuthorize是一个高阶函数, 在第一次调用时传入Authorized,再次调用时传入currentAuthority此参数代表当前[用户]拥有权限(当currentAuthority为函数此时会执行函数将返回结果赋值到CURRENT, 当currentAuthority为数组或者字符串则直接赋值到CURRENT, 否者CURRENT为字符串NULL,而这个CURRENT就缓冲这当前[用户]拥有权限, 可以从该文件导出使用),最后执行返回第一次调用的Authorized组件。

AuthorizedSecuredAuthorizedRoutecheck是什么?有什么区别?

Authorized

import CheckPermissions from './CheckPermissions';

const Authorized = ({ children, authority, noMatch = null }) => {
  const childrenRender = typeof children === 'undefined' ? null : children;
  return CheckPermissions(authority, childrenRender, noMatch);
};

export default Authorized;

由源码可以看出Authorized就是一个纯函数组件它接受三个属性children, authority, noMatch,然后返回CheckPermissions的执行结果。children:表示权限判断通过时展示什么,authority:表示展示children需要那些权限(之一),noMatch:表示权限判断失败时展示什么

AuthorizedRoute

import React from 'react';
import { Route, Redirect } from 'umi';
import Authorized from './Authorized';

const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => (
  <Authorized
    authority={authority}
    noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
  >
    <Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} />
  </Authorized>
);

export default AuthorizedRoute;

AuthorizedRouteAuthorized类似,区别在于noMatch使用Route render Redirect到其他路由,权限判断通过时也是由Route包裹。

Secured

Secured差不多是Authorized到另一种用法,例如:

const { Secured } = RenderAuthorized('user');

@Secured('admin')
class TestSecuredString extends React.Component {
  render() {
    return (
      <Alert message="user Passed!" type="success" showIcon />
    )
  }
}

Secured将作为类修饰符使用,参入到参数就是Authorized中到authority参数。

check

import React from 'react';
import PromiseRender from './PromiseRender';
import { CURRENT } from './renderAuthorize';

/**
 * 通用权限检查方法
 * Common check permissions method
 * @param { 权限判定 | Permission judgment } authority
 * @param { 你的权限 | Your permission description } currentAuthority
 * @param { 通过的组件 | Passing components } target
 * @param { 未通过的组件 | no pass components } Exception
 */
const checkPermissions = (authority, currentAuthority, target, Exception) => {
  // 没有判定权限.默认查看所有
  // Retirement authority, return target;
  if (!authority) {
    return target;
  }
  // 数组处理
  if (Array.isArray(authority)) {
    if (Array.isArray(currentAuthority)) {
      if (currentAuthority.some(item => authority.includes(item))) {
        return target;
      }
    } else if (authority.includes(currentAuthority)) {
      return target;
    }
    return Exception;
  }
  // string 处理
  if (typeof authority === 'string') {
    if (Array.isArray(currentAuthority)) {
      if (currentAuthority.some(item => authority === item)) {
        return target;
      }
    } else if (authority === currentAuthority) {
      return target;
    }
    return Exception;
  }
  // Promise 处理
  if (authority instanceof Promise) {
    return <PromiseRender ok={target} error={Exception} promise={authority} />;
  }
  // Function 处理
  if (typeof authority === 'function') {
    try {
      const bool = authority(currentAuthority);
      // 函数执行后返回值是 Promise
      if (bool instanceof Promise) {
        return <PromiseRender ok={target} error={Exception} promise={bool} />;
      }
      if (bool) {
        return target;
      }
      return Exception;
    } catch (error) {
      throw error;
    }
  }
  throw new Error('unsupported parameters');
};

export { checkPermissions };

const check = (authority, target, Exception) =>
  checkPermissions(authority, CURRENT, target, Exception);

export default check;

由源码可处导出check方法接受的参数与Authorized接受的属性相同

authority === this.props.authority
target === this.props.children
Exception === this.props.noMatch

check方法直接返回调用checkPermissions的结果。那么checkPermissions是什么,这就是antpro中如何实现准入权限现有权限的判断,最后导出渲染不同的结果。

umi如何辅佐antpro的权限组件

image

上图中在配置路由中传入了2个额外的属性Routesauthority, 他们是干什么用的呢?

看出umi源文件umi/packages/umi/src/renderRoutes.js,由如下代码:

function withRoutes(route) {
  if (RouteInstanceMap.has(route)) {
    return RouteInstanceMap.get(route);
  }

  const { Routes } = route;
  let len = Routes.length - 1;
  let Component = args => {
    const { render, ...props } = args;
    return render(props);
  };
  while (len >= 0) {
    const AuthRoute = Routes[len];
    const OldComponent = Component;
    Component = props => (
      <AuthRoute {...props}>
        <OldComponent {...props} />
      </AuthRoute>
    );
    len -= 1;
  }

由此看出:

  1. const { Routes } = route; 从每个route对象上读取Routes属性
  2. 通过 while 循环嵌套 外部传入Routes权限组件,而传入authority属性通过spread operater 传入到Routes权限组件这就是权限组件的准入权限的列表。

大致就是这样在配置上实现了权限控制。

Go Enlightenment

Interface

深度解密Go语言之关于 interface 的 10 个问题的总结

  1. Duke type in Golang.

Go 中不用显示的声明需要实现某个接口而是在结构上实现某个接口的方法。

// >>> Golang >>>
type Ixxx interface {
  Method()
}

func (x Xxx) Method() {
}
// >>> Java >>>
public interface Ixxx {
    public void method();
 }

public class xxx implements Ixxx {
     public void method() {
     }
 }

[译] GitFlow介绍

什么是GitFlow

GitFlow是Git的一个分支管理模式,由Vincent Driessen创造。它由引起了很多重视由于它是非常适合团队合作和开发团体扩展的。

关键优点

平行开发

GitFlow最突出的一个优点是非常适合平行开发,它通过隔离已完成的工作与新的开发任务。新的开发任务(如新需求和非紧急bug修复)在feature branches上完成,并仅仅当开发准备上线时候merge

虽然中断是BadThing(tm),但如果要求您从一个任务切换到另一个任务,您需要做的就是提交更改,然后为新任务创建新的功能分支。完成该任务后,只需checkout原先功能分支,然后就可以继续中断。

合作

功能分支还使两个或更多开发人员可以更轻松地在同一功能上进行协作,因为每个功能分支都是沙箱,其中唯一的更改是使新功能正常工作所需的更改。这使得十分容易检查各个成员做了一些什么事在新功能点上。

Release Staging Area

随着新开发的完成,它将合并回develop branch,这是所有尚未发布的已完成功能的临时区域。因此,当下一个版本分支开发时,它将自动包含已完成的所有新内容。

对紧急fix的支持

GitFlow支持hoxfix branches -- 由tagged创建的分支。您可以使用这些进行紧急更改,因为您知道此修补程序仅包含您的紧急修复程序。您不会意外地同时合并新开发项目。

如何工作的

新功能(新功能,非紧急错误修复)在feature branches中开发:

image

Feature branches从develop branch分支拉取出来,完成的功能和修复在准备发布时合并回develop branch

image

当什么时候要发布,发布时从** develop创建一个release branch**:

image

release branch上的代码被部署在合适的测试环境,并且任意的问题都直接在release branch上修复。这种** deploy -> test -> fix -> redeploy -> retest**循环指导完全准备正式发布上线给客户受用。

当发布完成,release branch需要同时merge回masterdevelop,以保证任何在release branch上的修改都不会意外的在新开发时丢失。

2018-11-05 4 23 00

master branch追踪最终发布的
代码。commits到master上的代码全部来自与release branches and hotfix branches的merge.

Hotfix branches被用来解决紧急修复:

2018-11-05 4 23 38

他们也是从(master branch)tagged上拉取的分支
,当它们完成后需要同时合并会masterdevelop确保在下一个常规版本发生时不会意外丢失此修补程序。

Ramda 🐏 逐步分析

ramda: A practical functional library for JavaScript programmers.

函数式编程实践 💯


主要项目结构

ramda   # root dir
│   dist	# 输出目录
│   test	# unit test目录  
│	lib		# 性能测试 & 指标
│	scripts
│
└───source
│   │  	F.js	 # 输出方法
│   │   T.js 	# ...
│   │
│   └───internal	# 内部方法
│       │   file111.txt
│       │   file112.txt
│       │   ...
│
│	 # 杂项文件
│	 .eslintrc
│	 .eslintignore
│	 package.json
│ 	 README.md
│ 	 ...
└

真|假 值

  • F.js: 定义一个函数调用它永远返回false
    /**
     * A function that always returns `false`. Any passed in parameters are ignored.
     *
     * @func
     * @memberOf R
     * @since v0.9.0
     * @category Function
     * @sig * -> Boolean
     * @param {*}
     * @return {Boolean}
     * @see R.T
     * @example
     *
     *      R.F(); //=> false
     */
    var F = function() {return false;};
    export default F;

T函数与F函数类似不同的是调用T永远返回true

为什么要有这2个函数? --- ramda作为JS函数式变成的最佳实践,它提倡纯函数式风格一切的逻辑都是在函数的相互配合下完成的这可以使你的工作更加简单,代码更加优雅


Ramda中的函数是自动被curry化的(现目前做法实际是将所有export的方法都通过了_curryN去做📦wrapped);
Ramda函数的参数顺序为了便于curry都是被排列过的。待处理操作的数据通常最后提供。(并有filp方法和placehold解决自定义方法参数顺序问题)

ramda柯里化实现

    1. __.js: 导出一个对象字面量,其中只包含一个特殊的键值对'@@functional/placeholder': true, 这使得调用被curry化的函数在参数传递时候不需要依次传入而可以使用占位符先传递后续参数
    /**
     * A special placeholder value used to specify "gaps" within curried functions,
     * allowing partial application of any combination of arguments, regardless of
     * their positions.
     *
     * If `g` is a curried ternary function and `_` is `R.__`, the following are
     * equivalent:
     *
     *   - `g(1, 2, 3)`
     *   - `g(_, 2, 3)(1)`
     *   - `g(_, _, 3)(1)(2)`
     *   - `g(_, _, 3)(1, 2)`
     *   - `g(_, 2, _)(1, 3)`
     *   - `g(_, 2)(1)(3)`
     *   - `g(_, 2)(1, 3)`
     *   - `g(_, 2)(_, 3)(1)`
     *
     * @name __
     * @constant
     * @memberOf R
     * @since v0.6.0
     * @category Function
     * @example
     *
     *      const greet = R.replace('{name}', R.__, 'Hello, {name}!');
     *      greet('Alice'); //=> 'Hello, Alice!'
     */
    export default {'@@functional/placeholder': true};
    1. _isPlaceholder.js: (tool) 检验是否是一个placehold对象
    export default function _isPlaceholder(a) {
      return a != null &&
             typeof a === 'object' &&
             a['@@functional/placeholder'] === true;
    }

由于JavaScript使用较为灵活,定义函数后参数的传递不必按照函数签名传递,实际参数可多可少。实参可由arguments 获取,它是一个类数组对象。

ramda内部为了使curry化更加高效其预定义了_curry[1 | 2 | 3]他们是针对待curry化函数真实对应实参调用为1 | 2 | 3个时分别对应使用。

这样做为什么会高效? 因为知道了一个curry函数实际调用时参数的个数这样curry化阶段就不再需要判断这个函数是否还有待其它参数传递的逻辑处理。

注: fn’s arity 代表 fn.length

    1. _curry1.js: one-arity 一元函数( = 形参个数为1)柯里化实现
    import _isPlaceholder from './_isPlaceholder';
    
    
    /**
     * Optimized internal one-arity curry function.
     *
     * @private
     * @category Function
     * @param {Function} fn The function to curry.
     * @return {Function} The curried function.
     */
    export default function _curry1(fn) {
      return function f1(a) {
        // 若实际调用时为传递实参或者实参为placehold对象则依旧返回currid fn 否则触发调用
        if (arguments.length === 0 || _isPlaceholder(a)) {
          return f1;
        } else {
          return fn.apply(this, arguments);
        }
      };
    }
    1. _curry2.js: two-arity 二元函数( = 形参个数为2)柯里化实现
    import _curry1 from './_curry1';
    import _isPlaceholder from './_isPlaceholder';
    
    
    /**
     * Optimized internal two-arity curry function.
     *
     * @private
     * @category Function
     * @param {Function} fn The function to curry.
     * @return {Function} The curried function.
     */
    export default function _curry2(fn) {
      return function f2(a, b) {
        switch (arguments.length) {
          case 0:
            return f2;
          case 1:
            return _isPlaceholder(a)
              ? f2
              : _curry1(function(_b) { return fn(a, _b); });
          default:
            return _isPlaceholder(a) && _isPlaceholder(b)
              ? f2
              : _isPlaceholder(a)
                ? _curry1(function(_a) { return fn(_a, b); })
                : _isPlaceholder(b)
                  ? _curry1(function(_b) { return fn(a, _b); })
                  : fn(a, b);
        }
      };
    }

由于有了_curry1.js的实现那么_curry2实际上就只需要根据实参的数量和类型判断是应该直接返回curried fn;还是调用_curry1去curry化一个新函数该函数接受一个参数执行返回原fn调用结果;或者直接触发原fn调用。具体逻辑如上

    1. _curry3.js: three-arity 三元函数( = 形参个数为3)柯里化实现
    import _curry1 from './_curry1';
    import _curry2 from './_curry2';
    import _isPlaceholder from './_isPlaceholder';
    
    
    /**
     * Optimized internal three-arity curry function.
     *
     * @private
     * @category Function
     * @param {Function} fn The function to curry.
     * @return {Function} The curried function.
     */
    export default function _curry3(fn) {
      return function f3(a, b, c) {
        switch (arguments.length) {
          case 0:
            return f3;
          case 1:
            return _isPlaceholder(a)
              ? f3
              : _curry2(function(_b, _c) { return fn(a, _b, _c); });
          case 2:
            return _isPlaceholder(a) && _isPlaceholder(b)
              ? f3
              : _isPlaceholder(a)
                ? _curry2(function(_a, _c) { return fn(_a, b, _c); })
                : _isPlaceholder(b)
                  ? _curry2(function(_b, _c) { return fn(a, _b, _c); })
                  : _curry1(function(_c) { return fn(a, b, _c); });
          default:
            return _isPlaceholder(a) && _isPlaceholder(b) && _isPlaceholder(c)
              ? f3
              : _isPlaceholder(a) && _isPlaceholder(b)
                ? _curry2(function(_a, _b) { return fn(_a, _b, c); })
                : _isPlaceholder(a) && _isPlaceholder(c)
                  ? _curry2(function(_a, _c) { return fn(_a, b, _c); })
                  : _isPlaceholder(b) && _isPlaceholder(c)
                    ? _curry2(function(_b, _c) { return fn(a, _b, _c); })
                    : _isPlaceholder(a)
                      ? _curry1(function(_a) { return fn(_a, b, c); })
                      : _isPlaceholder(b)
                        ? _curry1(function(_b) { return fn(a, _b, c); })
                        : _isPlaceholder(c)
                          ? _curry1(function(_c) { return fn(a, b, _c); })
                          : fn(a, b, c);
        }
      };
    }

同理 _curry3 依赖 _curry[1 | 2] 的实现

看了_curry[1 | 2 | 3]可以发掘ramda柯里化的模式:基础实现_curry1, 然后后续的_curryN都需要依赖_curry[N-1] ... _curry1. 其中使用switch语法去检测实参的个数分别控制,每个分支有去判断各个实参是否为占位符决定使用前一个curry化方法去科里一个新函数还是返回被curried fn

    1. _arity.js: - 在说ramda最终的柯里化实现前置依赖的一个方法
    export default function _arity(n, fn) {
      /* eslint-disable no-unused-vars */
      switch (n) {
        case 0: return function() { return fn.apply(this, arguments); };
        case 1: return function(a0) { return fn.apply(this, arguments); };
        case 2: return function(a0, a1) { return fn.apply(this, arguments); };
        case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
        case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
        case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
        case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
        case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
        case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
        case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
        case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
        default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
      }
    }

由此方法的实现可以看出该方法接受2个参数。第一个为number类型(代表待curry化函数的形参个数),第二个参数为function类型(代表待curry化函数本身)

_arity内部逻辑就是一个简单的switch case语句,根据第一个参数返回不同的函数(其函数签名不同主要是定义形参的个数)。调用返回的实际上就是返回调用fn.apply(this, arguments);的结果。

由此也能看见,Ramda中curry的从实现层面还是由一定缺陷的(当待curry化当函数形参大于10个时会抛出异常)。但从艺术与哲学&实用性方面的思考也没必要覆盖所有场景,在编程最佳实践中大多数人都会告诉你定义的方法形参最多不要超过6个左右,如果你超过了就需要考虑是否有必要对方法进行拆分(基于SOILD原则)。

    1. _curryN.js: ramda.curry内部的最终实现
    import _arity from './_arity';
    import _isPlaceholder from './_isPlaceholder';
    
    
    /**
     * Internal curryN function.
     *
     * @private
     * @category Function
     * @param {Number} The arity of the curried function.
     * @param {Array} An array of arguments received thus far.
     * @param {Function} The function to curry.
     * @return {Function} The curried function.
     */
    export default function _curryN(length, received, fn) {
      return function() {
        var combined = [];
        var argsIdx = 0;
        var left = length;
        var combinedIdx = 0;
        while (combinedIdx < received.length || argsIdx < arguments.length) {
          var result;
          if (combinedIdx < received.length &&
              (!_isPlaceholder(received[combinedIdx]) ||
               argsIdx >= arguments.length)) {
            result = received[combinedIdx];
          } else {
            result = arguments[argsIdx];
            argsIdx += 1;
          }
          combined[combinedIdx] = result;
          if (!_isPlaceholder(result)) {
            left -= 1;
          }
          combinedIdx += 1;
        }
        return left <= 0
          ? fn.apply(this, combined)
          : _arity(left, _curryN(length, combined, fn));
      };
    }

Ramda的柯里化实现基于一个原则以函数签名来确定柯里化(实际上由fn.length可以找到形参的个数),由函数签名可以知道此函数内部逻辑处理了多少参数,当调用传递的实参个数不够时是不会触发真实调用的。

柯里化的实现描述比较复杂但本质就是循环♻️检查已接受到的参数和本次传入的传入是否完整。完整则触发调用,不完整则使用_arity模版继续递归调用_curryN

    1. curry.js & curryN.js: ramda最终暴露出来的curry方法
    import _arity from './internal/_arity';
    import _curry1 from './internal/_curry1';
    import _curry2 from './internal/_curry2';
    import _curryN from './internal/_curryN';
    
    
    /**
     * Returns a curried equivalent of the provided function, with the specified
     * arity. The curried function has two unusual capabilities. First, its
     * arguments needn't be provided one at a time. If `g` is `R.curryN(3, f)`, the
     * following are equivalent:
     *
     *   - `g(1)(2)(3)`
     *   - `g(1)(2, 3)`
     *   - `g(1, 2)(3)`
     *   - `g(1, 2, 3)`
     *
     * Secondly, the special placeholder value [`R.__`](#__) may be used to specify
     * "gaps", allowing partial application of any combination of arguments,
     * regardless of their positions. If `g` is as above and `_` is [`R.__`](#__),
     * the following are equivalent:
     *
     *   - `g(1, 2, 3)`
     *   - `g(_, 2, 3)(1)`
     *   - `g(_, _, 3)(1)(2)`
     *   - `g(_, _, 3)(1, 2)`
     *   - `g(_, 2)(1)(3)`
     *   - `g(_, 2)(1, 3)`
     *   - `g(_, 2)(_, 3)(1)`
     *
     * @func
     * @memberOf R
     * @since v0.5.0
     * @category Function
     * @sig Number -> (* -> a) -> (* -> a)
     * @param {Number} length The arity for the returned function.
     * @param {Function} fn The function to curry.
     * @return {Function} A new, curried function.
     * @see R.curry
     * @example
     *
     *      const sumArgs = (...args) => R.sum(args);
     *
     *      const curriedAddFourNumbers = R.curryN(4, sumArgs);
     *      const f = curriedAddFourNumbers(1, 2);
     *      const g = f(3);
     *      g(4); //=> 10
     */
    var curryN = _curry2(function curryN(length, fn) {
      if (length === 1) {
        return _curry1(fn);
      }
      return _arity(length, _curryN(length, [], fn));
    });
    export default curryN;
    import _curry1 from './internal/_curry1';
    import curryN from './curryN';
    
    
    /**
     * Returns a curried equivalent of the provided function. The curried function
     * has two unusual capabilities. First, its arguments needn't be provided one
     * at a time. If `f` is a ternary function and `g` is `R.curry(f)`, the
     * following are equivalent:
     *
     *   - `g(1)(2)(3)`
     *   - `g(1)(2, 3)`
     *   - `g(1, 2)(3)`
     *   - `g(1, 2, 3)`
     *
     * Secondly, the special placeholder value [`R.__`](#__) may be used to specify
     * "gaps", allowing partial application of any combination of arguments,
     * regardless of their positions. If `g` is as above and `_` is [`R.__`](#__),
     * the following are equivalent:
     *
     *   - `g(1, 2, 3)`
     *   - `g(_, 2, 3)(1)`
     *   - `g(_, _, 3)(1)(2)`
     *   - `g(_, _, 3)(1, 2)`
     *   - `g(_, 2)(1)(3)`
     *   - `g(_, 2)(1, 3)`
     *   - `g(_, 2)(_, 3)(1)`
     *
     * @func
     * @memberOf R
     * @since v0.1.0
     * @category Function
     * @sig (* -> a) -> (* -> a)
     * @param {Function} fn The function to curry.
     * @return {Function} A new, curried function.
     * @see R.curryN, R.partial
     * @example
     *
     *      const addFourNumbers = (a, b, c, d) => a + b + c + d;
     *
     *      const curriedAddFourNumbers = R.curry(addFourNumbers);
     *      const f = curriedAddFourNumbers(1, 2);
     *      const g = f(3);
     *      g(4); //=> 10
     */
    var curry = _curry1(function curry(fn) {
      return curryN(fn.length, fn);
    });
    export default curry;

题外话

  1. Ramda在其柯里化实现中提供了2个feature: 1. 可以使用ramda._占位符; 2.可以使用flip方法反转参数传递顺序

  2. 使用ES2015的 rest params 加上 fn.length可以实现一个类似curry方法:

const curry = fn => (...args) =>
  args.length >= fn.length
    ? fn(...args)
    : (...innerArgs) => fn(...args, ...innerArgs);

上面的方法实现有一定缺陷,它只能满足2步柯里,没有实现任意层次的柯里化,接下来我们进行改造

const curry = fn => (...args) =>
  args.length >= fn.length
    ? fn(...args)
-     : (...innerArgs) => fn(...args, ...innerArgs);
+    : curry((...innerArgs) => fn(...args, ...innerArgs));

上面的方法依然有问题。因为使用Rest parameters的方法是不能由fn.length获取arity(元)[得到的值为0]。所以我们最终改造柯里化实现为:

const curry = fn => {
  const curryN = (n, fn) => (...args) => 
    args.length >= n
      ? fn(...args)
      : curryN(n-args.length, (...innerArgs) => fn(...args, ...innerArgs))
  return curryN(fn.length, fn)
}

Filp翻转函数参数传递顺序

  • flip.js: 返回一个与提供的函数非常相似的新函数,但前两个参数的顺序相反。
    import _curry1 from './internal/_curry1';
    import curryN from './curryN';
    
    
    /**
     * Returns a new function much like the supplied one, except that the first two
     * arguments' order is reversed.
     *
     * @func
     * @memberOf R
     * @since v0.1.0
     * @category Function
     * @sig ((a, b, c, ...) -> z) -> (b -> a -> c -> ... -> z)
     * @param {Function} fn The function to invoke with its first two parameters reversed.
     * @return {*} The result of invoking `fn` with its first two parameters' order reversed.
     * @example
     *
     *      const mergeThree = (a, b, c) => [].concat(a, b, c);
     *
     *      mergeThree(1, 2, 3); //=> [1, 2, 3]
     *
     *      R.flip(mergeThree)(1, 2, 3); //=> [2, 1, 3]
     * @symb R.flip(f)(a, b, c) = f(b, a, c)
     */
    var flip = _curry1(function flip(fn) {
      return curryN(fn.length, function(a, b) {
        var args = Array.prototype.slice.call(arguments, 0);
        args[0] = b;
        args[1] = a;
        return fn.apply(this, args);
      });
    });
    export default flip;

Flip接受一个函数作为参数,并返回一个函数,内部使用Array.prototype.slice方法将arguments转换为数组并置换数组前2个参数的位置,然后将该数组作为参数传递调用原函数

React中基于角色🎭的授权考量

开篇首先提出2个混淆的术语:Authentication & Authorization 。这2个对我感受他们均涉及到会话安全但侧重点有所不用,如下:

  • Authentication:认证,简言之 - 你是谁
    如:登录

  • Authorization:授权,简言之 - 你被允许访问什么
    如:登录系统后可访问级别,第三方应用登录授权

那么我们的系统如何对不用用户做访问授权控制呢?

答:基于角色的授权方式是一种方式,其基本**是传递一个允许查看给定路径的角色列表,并检查当前登录用户所有权限是否是该列表中的角色之一。

如何实现并做的更好?

以下列举有参考ant-design-pro项目对于授权的实现

  1. 是否可重用代码(基于React Component)
  2. 单一职责
  3. 减少不必要的浪费(如属性传递,重复计算...)

开始

首先我们可以改进的是将授权的逻辑封装到一个独立的组建中并使用 function child以防止在未授权用户的情况下mounting组建并在你的路由组建(route handler)渲染它

// Route handler
class YourRoute extends React.Component {
  constructor(props) {
    super(props)
    // Load user from wherever into state.
  }
  render() {
    return <Authorization allowed={this.props.allowed} user={this.state.user}>
      {() => 
        /* the rest of your page */
      }
    <Authorization>
  }
}

export default YourRoute

// Router configuration
<Router history={BrowserHistory}>
  <Route path="/" component={App}>
    <Route
      allowed={['manager', 'admin']}
      path="feature"
      component={YourRoute}
    />
  </Route>
</Router>

以此我们有了可复用的认证逻辑,这朝着正确方向迈出的一步。我们甚至可以使Authorization组建去加载用户角色本身,这样就只需要allowed属性。但是每一个route现在都需要写入授权这看起来不怎么好。

所以有个其他选择,HOC,可以移动授权逻辑完全移出路由组建。假设 Authorization HOC 自己加载当前登录用户角色,他看起来像

// Authorization HOC
const Authorization = (WrappedComponent, allowedRoles) =>
  return class WithAuthorization extends React.Component {
    constructor(props) {
      super(props)

      // In this case the user is hardcoded, but it could be loaded from anywhere.
      // Redux, MobX, RxJS, Backbone...
      this.state = {
        user: {
          name: 'vcarl',
          role: 'admin'
        }
      }
    }
    render() {
      const { role } = this.state.user
      if (allowedRoles.includes(role)) {
        return <WrappedComponent {...this.props} />
      } else {
        return <h1>No page for you!</h1>
      }
    }
  }

// Route handler
class YourRoute extends React.Component {
  render() {
    return <div>
      /* the rest of your page */
    </div>
  }
}

export default Authorization(YourRoute, ['manager', 'admin'])

// Router configuration
<Router history={BrowserHistory}>
  <Route path="/" component={App}>
    <Route
      path="feature"
      component={YourRoute}
    />
  </Route>
</Router>

我几乎没有包含授权HOC的实现,因为它取决于您用于存储数据的方式,是否要显示错误消息或在失败时重定向到其他位置,您正在使用哪个router等有很多未知数,我想关注模式而不是实现细节。重要的是它检查当前登录的用户并呈现他们有足够角色的包装组件。

因为在何时调用HOC是无关紧要的,我们可以将方法调用移至router configuration.

// Route handler
class YourRoute extends React.Component {
  render() {
    return <div>
      /* the rest of your page */
    </div>
  }
}

- export default Authorization(YourRoute, ['manager', 'admin'])
+ export default YourRoute

// Router configuration
<Router history={BrowserHistory}>
  <Route path="/" component={App}>
    <Route
      path="feature"
-     component={YourRoute}
+     component={Authorization(YourRoute, ['manager', 'admin'])}
    />
  </Route>
</Router>

接着,我们现在在路由组建(route handler)中没有一丝授权逻辑,没有什么通过react-router传入component,我们所有允许的角色都在一个文件中定义。但是你知道,如果我们有很多route,这将意味着许多重复的角色定义。

让我们来做2个改动,反转我们传入HOC方法的两个参数顺序并是HOC curry化。我们的HOC成为返回HOC函数。如果使用Redux,这种类型的函数调用看起来应该很熟悉,因为它与connect使用的机制相同。

// Authorization HOC
- const Authorization = (WrappedComponent, allowedRoles) =>
+ const Authorization = (allowedRoles) =>
+  (WrappedComponent) =>
     return class WithAuthorization extends React.Component {
       constructor(props) {
         super(props)
// ...


// Router configuration
<Router history={BrowserHistory}>
  <Route path="/" component={App}>
    <Route
      path="feature"
-     component={Authorization(YourRoute, ['manager', 'admin'])}
+     component={Authorization(['manager', 'admin'])(YourRoute)}
    />
  </Route>
</Router>

所以我们现在可以这样使用(事先定义好各类role HOC)

// Router configuration
+ const Manager = Authorization(['manager', 'admin'])

<Router history={BrowserHistory}>
  <Route path="/" component={App}>
    <Route
      path="feature"
-     component={Authorization(['manager', 'admin'])(YourRoute)}
+     component={Manager(YourRoute)}
    />
  </Route>
</Router>

现在我们可以预先定义一系列的role HOC然后我们可以在router configuration 使用

// Router configuration
const User = Authorization(['user', 'manager', 'admin'])
const Manager = Authorization(['manager', 'admin'])
const Admin = Authorization(['admin'])

<Router history={BrowserHistory}>
  <Route path="/" component={App}>
    <Route path="users" component={User(Users)}>
      <Route path=":id/edit" component={Manager(EditUser)} />
      <Route path="create" component={Admin(CreateUser)} />
    </Route>
  </Route>
</Router>

当然,客户端授权只是其中的一部分。后端应始终强制执行用户角色,因为客户端上的所有数据都可以从devtools更改。

其他: #14 - ant-design-pro 与 umi 相辅相成的授权实现

[译]通过import()来执行JavaScript

Evaluate (eval) - 评估/执行

import()语句允许我们动态的引入ECMAScript 模块. 但是他们也可以当作eval()函数的一种替代品用来执行JavaScript代码(as Andrea Giammarchi recently pointed out to me)。这篇博客文章解释了它是如何工作的。

1. eval()函数不支持exportimport

eval()的一个重要限制是它不支持模块语法,例如export和import。

如果我们使用import()而不是eval(),那么我们实际上可以执行模块代码,这将在本博客文章的后面部分看到。

未来,我们将有Realms, 大致来说, 这是功能更强大的eval 并支持模块。

2. 通过import()执行简单的代码

让我们通过import()来执行一句 console.log

const js = `console.log('Hello everyone!');`;
const encodedJs = encodeURIComponent(js);
const dataUri = 'data:text/javascript;charset=utf-8,' + encodedJs;
import(dataUri);

// > Promise {<pending>}
// Hello everyone!

发生了什么?

  • 首先,我们创建一个所谓的 data URI。这种URI的协议是data:。 URI的其余部分是编码完整的资源,而不是一个资源应用地址。在这种情况下,数据URI包含完整的ECMAScript模块-其内容类型为text / javascript。
  • 然后,我们动态导入此模块并执行它。

警告⚠️:此代码仅在Web浏览器中有效。在Node.js上,import() 不支持data URI

2.1 访问一个导出的执行模块

import()返回的 Promise 的完成值是一个命名模块对象,这使我们可以访问其default值和其他命名导出值。在以下示例中,我们访问默认导出:

const js = `export default 'Returned value'`;
const dataUri = 'data:text/javascript;charset=utf-8,'
  + encodeURIComponent(js);
import(dataUri)
  .then((namespaceObject) => {
    console.log(namespaceObject.default)
    // assert.equal(namespaceObject.default, 'Returned value');
  });

// > Promise {<pending>}
// Returned value

3. 通过 tagged template literals 创建 data URL

使用适当的 esm 方法(稍后将介绍其实现),我们可以重写前面的示例,并通过Tagged templates创建数据URI:

const dataUri = esm`export default 'Returned value'`;
import(dataUri)
  .then((namespaceObject) => {
    assert.equal(namespaceObject.default, 'Returned value');
  });

esm 的实现如下:

function esm(templateStrings, ...substitutions) {
  let js = templateStrings.raw[0];
  for (let i=0; i<substitutions.length; i++) {
    js += substitutions[i] + templateStrings.raw[i+1];
  }
  return 'data:text/javascript;base64,' + btoa(js);
}

注:
image

对于编码,我们已从charset = utf-8切换为base64。相比之下:

  • 源码:a <b
  • Data URI a: data:text/javascript;charset=utf-8,'a'%20%3C%20'b'
  • Data URI b: data:text/javascript;base64,J2EnIDwgJ2In

每种都有其利弊:

  • charset=utf-8 好处(百分比编码(注:这里指字符集设置为utf-8对字符集进行了URL编码并不是utf-8的编码是URL编码)):
    大部分源码仍可读
  • base64好处:
    URI通常较短。
    不包含特殊字符(例如撇号),因此更易于嵌套。我们将在下一部分中看到一个嵌套示例。

btoa()是一个全局工具方法,注意:

  • Node环境下还没有
  • 只适用于ASCII码

4.执行一个导入了其他模块的模块

通过 tagged templates, 我们可以嵌套 data URI 并对导入m1模块m2模块进行编码:

const m1 = esm`export function f() { return 'Hello!' }`;
const m2 = esm`import {f} from '${m1}'; export default f()+f();`;
import(m2)
  .then(ns => assert.equal(ns.default, 'Hello!Hello!'));

5. 进一步阅读

React16.6中的组建懒加载(与预加载)

Origin: Lazy loading (and preloading) components in React 16.6


React 16.6加入了一些新的特征使code splitting(代码分割)更加简单: React.lazy().

让我们以一个小demo来看看如何以及为什么使用这个特征。

我们又一个app可以显示股票列表。当你点击一份股票你可以看到一份图表:
1_HK_529G8O-O6_6fYEKc9VQ

这个应用就是这样的。你可以在github repo 上看到源码(同时通过pull request来查看每次提交的需求和修改)。

本片,我们仅仅关心App.js文件:

我们有一个App组件它接受一系列股票并显示<StockTable>. 当你从表格中选择一份股票,App组件将针对于这份股票显示<StockChart/>

有什么问题?好吧,我们希望我们的应用程序能够快速启动并尽可能快地显示<StockTable />,但是我们要等浏览器下载( 并解压缩,解析,编译和运行)<StockChart/>的代码。

让我们看一下显示<StockTable />所需的时间:
1_amihMyV6mCW5JQogRM6Buw peng
显示StockTable需要2470毫秒(模拟的快速3G网络和4倍速的CPU)。

我们发送至浏览的(被压缩的)125KB文件都包含了什么?
1_QOEav03nGAll-VT_73eE_A

预期所致,里面包含react,react-dom和其他一些react依赖。但是我们还包含moment,lodash和victory,这些仅仅是在<StockChart />显示时的需要,并不是<StockTable />所用到的。

我们可以做些什么来避免依赖关系来减慢的加载速度?我们可以懒惰加载组件(lazy-load the component)。

lazy-load the component(懒加载组件)

使用 dynamic import 我们可以将我们的JavaScript代码一分为二,一份main 文件仅仅是显示<StockTable />所需要的代码另一份是<StackChart>所需要的代码和依赖。

这种技术非常有用,React 16.6添加了一个API,使其更易于与React组件一起使用:React.lazy()

为了使用React.lazy()我们对App.js做了两处改动:
1_DH2ChGy7yiqCfho-gGuP-Q

首先,我们使用React.lazy()传入一个方法它返回一 dynamic import 来代替原来的静态导入(static import)。现在浏览器不会下载./StockChart.js(及其依赖项),直到我们第一次渲染它。
1_s_U-JURSswCDhVDPZgxxNQ

但是当React视图渲染<StockChart />并且它还没有载入代码时会发生什么? 这就是我们为什么引入<React.Suspense/>。它会渲染fallback属性来代替它的children属性,直到子节点全部加载完毕。

现在我们的app将有2个bundled文件:

main.js文件是36KB。另一个文件是89KB,其代码来自./StockChart及其所有依赖项。

让我们再看一下浏览器用这些更改显示需要多少:
1_GkV09FYO5ADOAmw3xvKaYg

浏览器需要760毫秒来下载主js文件(而不是1250毫秒)和61毫秒来评估脚本(而不是487毫秒)。<StockTable />以1546毫秒(而不是2470毫秒)显示。

Preloading a lazy component(预加载惰性组件)

我们使我们的app加载的更快,但是我们有了另一个问题:

1_0Y-FcigqqboxXROFWWFO7A
在显示图表之前注意“正在加载...”(试一试

当用户第一次点击一份股票时, "Loading" fallback 生效。那是因为我们需要等到浏览器加载<StockChart />的代码。

如果我们需要摆脱“Loading” fallback, 我们需要在用户点击股票之前加载那些代码。

预加载代码的简单方法是在调用React.lazy()之前启动动态导入:

Diff

当我们调用动态导入时,组件将开始加载,而不会阻止<StockTable />的呈现。

Take a look at how the trace changed from the original eager-loading app:
1_O-tDkDg9bmDsnvX4vzhHkQ

【注: 本质上是使用promise异步加载其他JS代码而不去阻塞主线程渲染】。

现在,用户将仅仅在显示表格后不到一秒的时间内第一次点击一份股票才能看见“Loading” fallback 试一试

You could also enhance the lazy function to make it easier to preload components whenever you need:

Prerendering a component (预渲染组件)

对于我们的小Demo这些已经够了。 对于大型的apps Lazy component在渲染之前也许有其他的lazy code或者数据待加载。所以用户还需要等待其他的加载。

另一种预加载组件的方法是在需要之前实际渲染它,我们想渲染它但我们不想显示它,所以我们将它隐藏起来:

diff

React将在第一次呈现应用时开始加载<StockChart />, 但是这次他实际上会试图渲染<StockChart />, 所以如果需要加载任何其他依赖项(代码或数据),它将被加载。

我们将惰性组件包装在隐藏的div中,因此在加载后它不会显示任何内容。我们用另一个<React.Suspense />div包装在另一个<React.Suspense />中,因此它在加载时不会显示任何内容。

Note: hidden is the HTML attribute for indicating that the element is not yet relevant. The browser won’t render elements with this attribute. React doesn’t do anything special with that attribute(but it may start giving hidden elements a lower priority in future releases).

少了什么?

最后一种方法在许多情况下很有用,但它有一些问题。

首先,使用隐藏属性隐藏渲染的惰性组件不是防弹的。例如,懒加载组件可以使用portal这将不会被隐藏。(这有只用hack way不需要而外的div并且同样适用于portals,但是这是hack,它会被打破)。

第二,及时将组件显示为隐藏仍然加入了不必要渲染的节点到DOM上,这是一个性能问题。

A better aproach would be to tell react to render the lazy component but without comitting it to the DOM after it’s loaded. But, as far as I know, it isn’t possible with the current version of React.

我们可以做的另一项改进是在预加载图表组件时重用我们渲染的元素,因此当我们想要实际显示图表时,React不需要再次创建它们。如果我们知道用户将点击什么库存,我们甚至可以在用户点击之前使用正确的数据呈现它(就像这样)。

就是这些,谢谢阅读。

[转] 用 GitLab CI 进行持续集成

简介

从 GitLab 8.0 开始,GitLab CI 就已经集成在 GitLab 中,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成。 而且随着 GitLab 的升级,GitLab CI 变得越来越强大,本文将介绍如何使用 GitLab CI 进行持续集成。

一些概念

在介绍 GitLab CI 之前,我们先看看一些持续集成相关的概念。

Pipeline

一次 Pipeline 其实相当于一次构建任务,里面可以包含多个流程,如安装依赖、运行测试、编译、部署测试服务器、部署生产服务器等流程。
任何提交或者 Merge Request 的合并都可以触发 Pipeline,如下图所示:

+------------------+           +----------------+
|                  |  trigger  |                |
|   Commit / MR    +---------->+    Pipeline    |
|                  |           |                |
+------------------+           +----------------+

Stages

Stages 表示构建阶段,说白了就是上面提到的流程。
我们可以在一次 Pipeline 中定义多个 Stages,这些 Stages 会有以下特点:

  • 所有 Stages 会按照顺序运行,即当一个 Stage 完成后,下一个 Stage 才会开始
  • 只有当所有 Stages 完成后,该构建任务 (Pipeline) 才会成功
  • 如果任何一个 Stage 失败,那么后面的 Stages 不会执行,该构建任务 (Pipeline) 失败

因此,Stages 和 Pipeline 的关系就是:

+--------------------------------------------------------+
|                                                        |
|  Pipeline                                              |
|                                                        |
|  +-----------+     +------------+      +------------+  |
|  |  Stage 1  |---->|   Stage 2  |----->|   Stage 3  |  |
|  +-----------+     +------------+      +------------+  |
|                                                        |
+--------------------------------------------------------+

Jobs

Jobs 表示构建工作,表示某个 Stage 里面执行的工作。
我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:

  • 相同 Stage 中的 Jobs 会并行执行
  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 才会成功
  • 如果任何一个 Job 失败,那么该 Stage 失败,即该构建任务 (Pipeline) 失败

所以,Jobs 和 Stage 的关系图就是:

+------------------------------------------+
|                                          |
|  Stage 1                                 |
|                                          |
|  +---------+  +---------+  +---------+   |
|  |  Job 1  |  |  Job 2  |  |  Job 3  |   |
|  +---------+  +---------+  +---------+   |
|                                          |
+------------------------------------------+

GitLab Runner

简介

理解了上面的基本概念之后,有没有觉得少了些什么东西 —— 由谁来执行这些构建任务呢?
答案就是 GitLab Runner 了!

想问为什么不是 GitLab CI 来运行那些构建任务?
一般来说,构建任务都会占用很多的系统资源 (譬如编译代码),而 GitLab CI 又是 GitLab 的一部分,如果由 GitLab CI 来运行构建任务的话,在执行构建任务的时候,GitLab 的性能会大幅下降。

GitLab CI 最大的作用是管理各个项目的构建状态,因此,运行构建任务这种浪费资源的事情就交给 GitLab Runner 来做拉!
因为 GitLab Runner 可以安装到不同的机器上,所以在构建任务运行期间并不会影响到 GitLab 的性能~

安装

安装 GitLab Runner 太简单了,按照着 官方文档 的教程来就好拉!
下面是 Debian/Ubuntu/CentOS 的安装方法,其他系统去参考官方文档:

# For Debian/Ubuntu
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
$ sudo apt-get install gitlab-ci-multi-runner
# For CentOS
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash
$ sudo yum install gitlab-ci-multi-runner

注册 Runner

安装好 GitLab Runner 之后,我们只要启动 Runner 然后和 CI 绑定就可以了:

  • 打开你 GitLab 中的项目页面,在项目设置中找到 runners
  • 运行 [sudo] gitlab-ci-multi-runner register
  • 输入 CI URL
  • 输入 Token(repo中查看)
  • 输入 Runner 的名字
  • 选择 Runner 的类型,简单起见还是选 Shell 吧
  • 完成

当注册好 Runner 之后,可以用 [sudo] gitlab-ci-multi-runner list 命令来查看各个 Runner 的状态:
...

.gitlab-ci.yml

简介

配置好 Runner 之后,我们要做的事情就是在项目根目录中添加.gitlab-ci.yml文件了。
当我们添加了.gitlab-ci.yml文件后,每次提交代码或者合并 MR 都会自动运行构建任务了。

还记得 Pipeline 是怎么触发的吗?Pipeline 也是通过提交代码或者合并 MR 来触发的!
那么 Pipeline 和 .gitlab-ci.yml 有什么关系呢?
其实 .gitlab-ci.yml 就是在定义 Pipeline 而已拉!

基本写法

我们先来看看 .gitlab-ci.yml 是怎么写的:

# 定义 stages
stages:
  - build
  - test
# 定义 job
job1:
  stage: test
  script:
    - echo "I am job1"
    - echo "I am in test stage"
# 定义 job
job2:
  stage: build
  script:
    - echo "I am job2"
    - echo "I am in build stage"

写起来很简单吧!用 stages 关键字来定义 Pipeline 中的各个构建阶段,然后用一些非关键字来定义 jobs。
每个 job 中可以可以再用 stage 关键字来指定该 job 对应哪个 stage
job 里面的 script 关键字是最关键的地方了,也是每个 job 中必须要包含的,它表示每个 job 要执行的命令。

回想一下我们之前提到的 Stages 和 Jobs 的关系,然后猜猜上面例子的运行结果?

I am job2
I am in build stage
I am job1
I am in test stage

根据我们在 stages 中的定义,build 阶段要在 test 阶段之前运行,所以 stage:build 的 jobs 会先运行,之后才会运行 stage:test 的 jobs。

常用的关键字

下面介绍一些常用的关键字,想要更加详尽的内容请前往 官方文档

stages

定义 Stages,默认有三个 Stages,分别是 build, test, deploy

types

stages 的别名。

before_script

定义任何 Jobs 运行前都会执行的命令。

after_script

要求 GitLab 8.7+ 和 GitLab Runner 1.2+

定义任何 Jobs 运行完后都会执行的命令。

variables && Job.variables

要求 GitLab Runner 0.5.0+

定义环境变量。
如果定义了 Job 级别的环境变量的话,该 Job 会优先使用 Job 级别的环境变量。

cache && Job.cache

要求 GitLab Runner 0.7.0+

定义需要缓存的文件。
每个 Job 开始的时候,Runner 都会删掉 .gitignore 里面的文件。
如果有些文件 (如 node_modules/) 需要多个 Jobs 共用的话,我们只能让每个 Job 都先执行一遍 npm install。
这样很不方便,因此我们需要对这些文件进行缓存。缓存了的文件除了可以跨 Jobs 使用外,还可以跨 Pipeline 使用。
...

Job.script

定义 Job 要运行的命令,必填项。

Job.stage

定义 Job 的 stage,默认为 test。

Job.artifacts

定义 Job 中生成的附件。
当该 Job 运行成功后,生成的文件可以作为附件 (如生成的二进制文件) 保留下来,打包发送到 GitLab,之后我们可以在 GitLab 的项目页面下下载该附件。
注意,不要把 artifactscache 混淆了。

实用例子

下面给出一个我自己在用的例子:

stages:
  - install_deps
  - test
  - build
  - deploy_test
  - deploy_production
cache:
  key: ${CI_BUILD_REF_NAME}
  paths:
    - node_modules/
    - dist/
# 安装依赖
install_deps:
  stage: install_deps
  only:
    - develop
    - master
  script:
    - npm install
# 运行测试用例
test:
  stage: test
  only:
    - develop
    - master
  script:
    - npm run test
# 编译
build:
  stage: build
  only:
    - develop
    - master
  script:
    - npm run clean
    - npm run build:client
    - npm run build:server
# 部署测试服务器
deploy_test:
  stage: deploy_test
  only:
    - develop
  script:
    - pm2 delete app || true
    - pm2 start app.js --name app
# 部署生产服务器
deploy_production:
  stage: deploy_production
  only:
    - master
  script:
    - bash scripts/deploy/deploy.sh

上面的配置把一次 Pipeline 分成五个阶段:

安装依赖(install_deps)
运行测试(test)
编译(build)
部署测试服务器(deploy_test)
部署生产服务器(deploy_production)
设置 Job.only 后,只有当 develop 分支和 master 分支有提交的时候才会触发相关的 Jobs。
注意,我这里用 GitLab Runner 所在的服务器作为测试服务器。

从 babel compiler 理解 Class 声明

ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为JavaScript引入新的面向对象的继承模型。

image

那么上图中 Test 类声明被 babel presets + class-properties 转译后是如何的呢:

首先转译的代码运行在严格模式下 "use strict" , 本质上 Babel 会解析出 AST 在根据 AST词法分析将源代码转换成向下兼容的代码,其中主要包含了2部分:

  1. 定义了一系列抽象运算(Abstract Operations)
  2. 由类转换成构造函数,不同的属性和方法再分别使用上面的抽象运算定义。

抽象运算

  1. 用于检查左值是否是右值的实例
function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}
 

Tips: 优先使用右值的Symbol.hasInstance方法检查,若Symbol不可用在直接使用instanceof判断

  1. #1 的封装,当不是实例是抛出TypeError
function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
  1. 在 target 对象上定义多个属性
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

Tips:prop 参数为一个数组,包含要定义的多个属性,每个都是一个属性描述符对象 { enumerable = false, configurable = true, writable, value} 外加 key 属性用于调用 Object.defineProperty时定义属性名。

  1. 在obj上定义一个属性,其访问符全部设置为true
function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

定义最终构造函数

var Test =
  /*#__PURE__*/
  (function() {
    function Test() {
      _classCallCheck(this, Test);

      _defineProperty(this, "C", function() {
        console.log("Instance method");
      });

      _defineProperty(this, "a", undefined);

      this.a = undefined;

      this.A = function() {
        console.log("Instance method");
      };
    }

    _createClass(
      Test,
      [
        {
          key: "A",
          value: function A() {
            console.log("Prototype method");
          }
        },
        {
          key: "getA",
          get: function get() {
            return this.a;
          }
        },
        {
          key: "SetA",
          set: function set(a) {
            this.a = a;
          }
        }
      ],
      [
        {
          key: "B",
          value: function B() {
            console.log("Static method");
          }
        }
      ]
    );

    return Test;
  })();

_defineProperty(Test, "b", undefined);

通过 babel 转译后的代码就可知道类在以原型链为基础的JavaScript语言最终实现方式。

[译] 为什么code review重要(实际上节省时间)

original

敏捷团队都是自我组织的,拥有跨越团队的技能组合。这部分是通过代码审查完成的。code review可以帮助开发人员学习代码库,并帮助他们学习增加技能的新技术和技术。

所以,code review实际是什么?

当开发人员完成某个问题时(或新功能...),另一位开发人员会查看代码并考虑以下问题:

  • 代码中是否有明显的逻辑错误
  • 查看需求,是否所有场景已经满足
  • 新的自动化测试是否足以满足新代码的要求?是否需要重写现有的自动化测试以考虑代码的变化?
  • 新代码是否符合现有的风范指南?

code review应与团队现有流程整合。例如,例如,如果团队正在使用任务分支工作流,则在编写完所有代码并运行并通过自动化测试之后,但在代码合并到上游之前启动代码审查。这确保了代码审查员花费时间检查机器遗漏的事情,并防止糟糕的编码决策污染主要的开发线。

敏捷团队从中的好处?

无论开发方法如何,每个团队都可以从代码审查中受益。然而,敏捷团队可以实现巨大的利益,因为团队中的工作是分散的。没有人是唯一知道代码库特定部分的人。简而言之,代码审查有助于促进整个代码库在整个团队的知识共享。

Code review分享知识

所有敏捷团队的核心是无与伦比的灵活性:具有将积压工作分散出来并在所有团队成员中执行的能力。因此,团队能够更好地围绕新工作,因为没有人是“关键路径”("critical path." )。全栈工程师可以处理前端工作以及服务器端工作。

image

Code review可以提供更好的估算

还记得estimation小节吗?估算是一项团队练习,随着产品知识在整个团队中传播,团队会做出更好的估算。随着新功能添加到现有代码中,原始开发人员可以提供良好的反馈和估计。此外,任何代码审阅者也会接触到代码库中该领域的复杂性,已知问题和关注点。然后,代码审阅者分享代码库中该部分原始开发人员的知识。这种做法创造了多种知情输入,当用于最终估计时,总是使估计更加可靠

Code review可以让你更好的调休

没人想成为一段代码的唯一联系人。同样,没有人愿意深入研究他们没有写过的关键代码 - 特别是在生产紧急情况期间。代码审查在整个团队中分享知识,以便任何团队成员都可以接管并继续操纵船舶。 (We love mixed metaphors at Atlassian!) 但重点在于:没有一个开发人员是关键路径,这也意味着团队成员可以根据需要休假。

Code review 指导新工程师

敏捷的一个特殊方面是,当新成员加入团队时,更多经验丰富的工程师会指导新成员。代码审查有助于促进有关代码库的交流。通常,隐藏在代码中的知识潜藏在团队code review期间。敏锐眼光👀的新成员,需要以新的视角发现代码库中粗糙,耗时的区域。因此,代码审查还有助于确保利用现有知识锻炼新的洞察力。

image

但是code review花时间

当然,他们需要时间。但那段时间并没有浪费 - 远非如此。

image

以下是三种优化方法。

分担负担

在Atlassian,许多团队在检入代码库之前需要对任何代码进行两次审核。听起来👂有些过重?实际上,并非如此。当作者选择reviewers是,他们会使用网络告知团队。任何两位工程师都可以提供意见,这使得流程分散,以便没有人成为瓶颈,并确保整个团队的代码审查覆盖率很高。

merge之前review

在合并上游之前要求进行代码审查可确保没有代码进入未查看状态。这意味着在凌晨2点做出的可疑架构决策以及实习生对工厂模式的不当使用,在他们有机会对您的应用程序产生持久(和令人遗憾的)影响之前就会被捕获。

运用同事压力将会有利

当开发人员知道他们的代码将由同事进行审核时,他们会做出额外的努力来确保所有测试都通过,并且代码设计得很好,因为他们可以使代码顺利进行。这种正念也倾向于使编码过程本身更顺畅,最终更快。
如果在开发周期的早期需要反馈及支持,请不要等待代码审查。早期反馈并经常提供更好的代码,所以不要害羞打扰其他人 - 无论何时出现时。它会让你的工作变得更好,但它也会让你的队友更好地进行代码审查。良性循环继续......!

CSS Framework

BudWise.css

背景

近期参与了一个项目,大致上是提供一个模版网站,在样式的实现和维护上逐渐暴露出系列问题,整理下自己的想法.

前置问题

2021年了,在angular, react, vue等主流前端框架大行其道的时代,在CSS方案层出不穷的年代,像BootStrap,Tailwind还有必要使用吗?

我对此的看法稍后提到,在此之前我们先看看目前大部分CSS框架是什么。

CSS Framework 本质

由于CSS属于声明式语言,不像JavaScript那样的命令式语言构成命令逻辑。这样好处在于用户只需声明对应样式代码,
不需要关心内部的渲染机制等就能得到期望的样式装饰效果,因此CSS的真正魅力在于艺术,是更具创作力的编程语言来丰富页面感官。

既然CSS是充满艺术的声明式语言,那么CSS框架能为我们提供什么?从多个框架的介绍中不难发现许多高频字:responsive, mobile-first, quickly, utility。再从源码中你会发现不管它们各自使用什么方案最终为用户提供的都是一些预定义的样式类,通过这些类能:

  1. 实现一些基本布局,
  2. 可为不同尺寸设备响应媒体查询,
  3. 组合样式提供一些快速解决方案(颜色,居中...),
  4. 提供浏览器兼容性前缀等.

所以用户最终主要还是在class属性上使用这些预定义的类名。但CSS更多的乐趣(动效、艺术感)是样式框架少有满足的。

是否还有必要使用CSS框架

回归上面的问题, 我的看法是:

  • CSS框架有助于快速应用样式和响应布局.
  • CSS框架能减少重复代码: 不同页面、不同开发、不合适的样式构建等,往往会产生多种同样效果的样式类,导致代码冗余难以维护.
  • CSS框架能加强团样式共识,开发者更侧重编写业务差异化样式.
  • 在类后台管理系统,轻动效与交互体验项目中应用较广.

如果你觉得 👆 是团队需要考虑的那么我建议你使用CSS框架。当然用了样式框架也会产生新问题,比较凸显的是:

  • 考虑是否需要去除未使用的预定义类避免文件过大 ( purgecss 是比较好的工具).
  • 熟悉框架中预定义类名, 选择合适的样式框架.

BudWise.css

Pre-defined modular CSS class and utilities with responsive.

为什么重造轮子

经过上面项目的洗礼,我自己也好久没有使用样式框架了。初在项目上使用时有种生疏感,体现在:

  • 使用时要反复查看代码与文档,
  • 部分预定义类使用时有无注意点(坑),
  • 衍生功能过多, 实现上复杂难以阅读,
  • 模块化欠佳等.

所以最近开始造了 budwise 这个轮子(主要还是因为它不复杂👌)。

方案选择

首先说说为什么主要用 sass 来实现它。经过阅读与学习其他框架源码,目前大多数都通过两种方式:一是 sass、less 等富样式语言编译生成;二是通过 js 编写在转换生成样式。我更青睐第一种因为它更接近css易读性好, 性能也不是第一因素(编译构建阶段生成)还有社区中用 sass 编写样式也更受欢迎.

与主流框架的差异

在做BudWise时并没有考虑取代主流样式框架,我觉得也不现实。但是在实现中做出了一些和其他框架的差异的考量:

  1. 原生的模块化支持. eg: 基于sass的项目更易使用
  2. 更丰富的配置. eg: 预定义类生成方式、媒体查询类连接符,前后缀选择等
  3. 使用习惯考虑与精简的实现. eg: 尽力减小生成样式文件大小、Dart-Sass 提供便利性
  4. 易读等.

What I Learn

在有精力的前提下,造轮子对我带来的许多直观的意义,不限于:

  • 有助于跳出个人舒适区.
  • 对主流样式框架的熟悉度有所提升.
  • 巩固了部分CSS知识, 权衡实现方案的优劣与不足,寻找改善方案.

Caveat

  • BudWise 任在开发之中, 基于Dart-Sass版本不兼容Node-Sass.

tr;dl

👀👀👀

Simple RSA note

RSA 示例

  1. 选取2个任意质数 P、Q

P=2, Q =7

  1. 计算N = P*Q

2 * 7 = 14 <== N

  1. 欧拉运算 $(n) = (P-1)(Q-1)

1 * 6 = 6 <== n

  1. 选取 公钥e 1 < e < n && e 与 n 互质

e = 5 | (or other)

  1. 选取 私钥d ,e*d % n = 1

5 * d % 6 = 1 ==> d = 5

  1. 加密 m^e % N => c

c1 = 2(原文) ^ 5 % 14 = 4(密文)

c2 = 3(原文) ^ 5 % 14 = 5(密文)

  1. 解密c^d % N => m

m1 = 4(密文) ^ 5 % 14 = 2(原文)

m2 = 5(密文) ^ 5 % 14 = 3(原文)

Mathematics:

现阶段 大数的质因分解是非常困难与复杂的。

package.json 依赖包版本控制

package.json固定版本和可变版本安装依赖有什么区别(不考虑.lock文件)?

yarn.lock (yarn)package-lock.json (npm) 安装依赖处理上有什么差异 ?

JavaScript Craft ✌︎


1. 使用Proxy模拟实现Lazy Evaluation

In programming language theory, lazy evaluation, or call-by-need is an evaluation strategy which delays the evaluation of an expression until its value is needed (non-strict evaluation) and which also avoids repeated evaluations (sharing).

const lazy = function (option = {}) {
  const { context } = option
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (target, fnName) {
        if (fnName === 'value') {
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        funcStack.push(memoize(context !== undefined ? context[fnName] : window[fnName]));
        return oproxy;
      }
    });

    return oproxy;
  }
};

const shallowEqual = (newV, oldV) => newV === oldV

const fullEqual = (newArgs, lastArgs) => (
  newArgs.length === lastArgs.length &&
  newArgs.every(
    (newArg, index) => shallowEqual(newArg, lastArgs[index]),
  )
)

function memoize(fn, isEqual = fullEqual) {
  let lastThis;
  let lastArgs;
  let lastResult;
  let calledOnce = false;

  return function(...newArgs) {
    if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
      return lastResult;
    }

    lastResult = fn.apply(this, newArgs);
    calledOnce = true;
    lastThis = this;
    lastArgs = newArgs;
    return lastResult;
  }
}

*2. 如何访问不可枚举属性。

前置:目前ECMAScript对JavsScript并未实现对象属性私有化,同时public、private等字段在JavaScript中不是保留关键字。

可使用 Reflect.ownKeys()访问该对象等自身属性(不受enumerable影响,且不会遍历原型属性)。 Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。


3. forEach语句如何跳出循环♻️?

forEach() 为每个数组元素执行callback函数;不像 map() 或者 reduce(),它总是返回 undefined 值,并且不可链式调用。没有办法中止或者跳出 forEach() 循环,除了抛出一个异常。如果你需要这样,这是错误的 forEach 使用场景

若你需要提前终止循环,你可以使用 refer

  • 简单循环
  • for...of 循环
  • Array.prototype.every()
  • Array.prototype.some()
  • Array.prototype.find()
  • Array.prototype.findIndex()
    这些数组方法可以对数组元素判断,以便确定是否需要继续遍历:every(),some(),find(),findIndex()

注:若条件允许,也可以使用 filter() 提前过滤出需要遍历的部分,再用 forEach() 处理。


*4. 类型转换为数字int

使用加法运算符+可以快速实现int类型转换。

let int = "15";
int = +int;
console.log(int); // Result: 15
console.log(typeof int); Result: "number"

这也可以适用于将布尔值转换为数字,true -> 1 false -> 0

注意:可能存在这样的上下文,其中+将被解释为连接运算符而不是加法运算符。当发生这种情况时(你想要返回一个整数而不是浮点数),你可以使用两个代数:~~

对数值n 进行按位NOT运算符(~)得到对结果是: -n - 1, 例如:

~15 = -15 - 1 = -16 | ~-16 = -(-16) - 1 = 15

使用2次按位NOT运算符其实会抵消掉之前对运算,所以也可用于int类型转换(额外性能损失)。


*5. 如何将数值从小数位截断✂️[非四舍五入]?

按位OR运算符(|)是将浮点数截断为整数的快捷方法, 如:

console.log(16.6 | 0, -16.6 | 0) -> 16, -16

准确的说任何按位运算符会强制浮点数为整数,导致数值直接发生截断后再进行位运算。
按位OR运算符(n | i)的实际效果是将n(n != ±1)从小数处截断得到整数s再➕i, 例如:

console.log(1.2 | 1) -> 1  // ⚠️
console.log(-1 | 1) -> -1  // ⚠️
console.log(16 | 1) -> 17
console.log(16.4 | 1) -> 17
console.log(16.5 | 1) -> 17
// 
console.log(-16 | 1) -> 15
console.log(-16.4 | 1) -> 15
console.log(-16.5 | 1) -> 15

由以上示例可使用( n | 0)将数值从小数位截断:

console.log(16.1 | 0) -> 16
console.log(-16.1 | 0) -> 16

附加:使用 /| 运算符可以实现去除整数最后几位数字 :

console.log(1553 / 10   | 0)  // Result: 155
console.log(1553 / 100  | 0)  // Result: 15
console.log(1553 / 1000 | 0)  // Result: 1

6. 如何使用正则进行千分位分组(Digital Grouping)?

https://stackoverflow.com/questions/6784894/add-commas-or-spaces-to-group-every-three-digits/57140750#57140750


7. 定义 init(iate) 方法处理 class 异步构造。

异步构造是一种不好的设计(ES也不提倡async constructor提案)。构造函数应该是一个单一函数,它设置实例的初始状态,而不会去其他设置。

最近在RNcoding中遇到类似Platform.select()功能涉及原生实现用于选择不同情况下所对应到值,eg:

type BiometricType = "FACE" | "FINGERPRINT" | "NONE"
class Biometic {
  bioType: BiometricType

  async init () {
    this.bioType = await nativeM.getBiometicType()
  }
  select<T>(specifics: { [type in BiometricType ]?: T }): T {
    return specifics[this.bioType]
  }
}

export default Biometic

MobX - observable data change in callback (Mainly use after fetch data form the remote)

mobx中对可观察数据修改改进。

在现有业务中,通常我们会从远端fetch data然后将它赋值到mobx的可观察的数据中,所以往往赋值发上在callback上.

mobx4中可以通过配置强制启用严格模式configure({enforceAction: true})

mobx4下直接的赋值会抛出一个error,如下📍:
image

所以我们大多数现有写法雷同于用action.bound封装一个赋值方法,如下:

  @action.bound
  fetchDataSuccess(str, { data }) {
    this[str] = data;
  }

// invoke

  @action
  fetchSettleYears() {
    this.settleYears = []
    fetchSettleYears().then(
      rep => this.fetchDataSuccess('settleYears', rep),  // <-
      error => {}
    )
  }  

这样写有很多弊端,比如:

  • 仅仅是对可观察对数据赋值必须wrap在一个action中显得太过麻烦;
  • 上述🌰中使用了[]运算符来形成一个属性赋值模式,但在使用中可能传入错误参数名字符串(由于拼写等)。
  • ...

How to improve 👏🏽

1.Using runInAction

runInAction将自动把被修饰方法绑定this并wrap在Action中
例如:

  import { observable, action, runInAction } from 'mobx';
  //....

  fetchSettleYears() {
    this.settleYears = []
    fetchSettleYears().then(
      ({data}) => runInAction(() => this.settleYears = data),
      error => {}
    )
  }

注意到一点在fetchSettleYears中我们并没有使用action修饰方法,因为他并不是真实修改可观察数据源的触法点

en ~ 感觉好一点

1.1. Using async/await 替换 promise

  fetchSettleYears = async () => {
    const {data} = await fetchSettleYears()
    runInAction(() => this.settleYears = data)
  }

这样是不是更简洁。

1.2. think more using plugin

The babel-plugin-mobx-deep-action plugin scans for all functions, marked as actions, and then marks all nested functions, which created inside actions as actions too.

The babel-plugin-mobx-async-action Converts all async actions to mobx's 4 flow utility call.

  • yarn add / npm install -D babel-plugin-mobx-deep-action babel-plugin-mobx-async-action

  • 在babel中添加此plugin

//.babelrc
"plugins": [
  "mobx-deep-action",
  "mobx-async-action"
]
  • 使用
  @action
  fetchSettleYears() {
    this.settleYears = []
    fetchSettleYears().then(
      ({data}) => this.settleYears = data,  // <-
      error => {}
    )
  }

  @action
  fetchSettleYears = async () => {
    const {data} = await fetchSettleYears()
    this.settleYears = await data  // <-
  }

2. Using flow and generator function

直接参考实例,现主流推荐方式(代码简洁,逻辑清晰)

import { flow } from 'mobx'

fetchSettleYears = flow(function*() {
    const { data } = yield fetchSettleYears()
    this.settleYears = data
  })

缺点: [mobx] Flow expects one 1 argument and cannot be used as decorator.

Javascript中 防抖 & 节流

节流[throttle]与防抖[debounce]在前端领域经常涉及,下面我们会尝试解释其中的原理与差异与实现以及一些应用场景

common sense

  • 随着应用与需求复杂度不断上升,节流与防抖也出现了一些相同的参数设置其中一点就是可以选择触发的时机leadingtrailing(前置或后置)或both。
  • 一些库中的immediate option与上面所谈到的设置leading: true类似。

debounce

debounce: Debounce technique allows us to "group" multiple sequential calls in a single one.
防抖: 防抖技术允许我们捆绑多个连续调用成为单一的一次调用。

可简单的理解防抖是将一次调用发生时的前后时间(TIMING)断内不允许再次触发,若多次触发则方法的真实调用根据设置可以在:

  • 第一次触发时调用(leading)--- 这将导致后续每每相隔一个TIMING内的连续触发不再发生调用
  • 上一次触发后间隔至少一个TIMING内没有被再次触发时调用(trailing)
  • Both

例如当设置leading: true且 TIME = 400ms
image

防抖的实现:

/**
   * 返回一个函数,只要它一直被触发将不会被调用
   * 函数将在其不再被触发的N毫秒后调用,如果immediate被传入那么
   * 函数将在第一次触发是立即调用
   * 
   */

// es6 syntax import & export
export function deBounce(func, delay, immediate) {
    let timeout;

    return function executedFunction() {
      const context = this;
      const args = arguments;

      var later = function() {
        timeout = null;
        if(!immediate) func.apply(context, args);
      }

      const callNow = immediate && !timeout;

      clearTimeout(timeout);

      timeout = setTimeout(later, delay);

      if (callNow) func.apply(context, args);
    }
  }

// 这是其中的一种实现关于leading与trailing可自行调整immediate。

防抖的应用

这个简单的举个🌰: 在autocomplete中keypress事件与ajax配合使用可减少不必要的请求,可以参考Corbacho所作的demo.

throttle

throttle: Throttle technique don't allow us to execute our function more than once every X milliseconds.
节流: 节流技术是我们不能在X毫秒内触发第二次函数调用。

简单的理解节流就像控制水龙头单位时间内的出水量一样,在一个设定时间段内只能触发一次调用。若在一个时间段内连续触发多次函数真实调用根据设置可以在:

  • 这个时间段的开头(leading)
  • 这个时间段的结尾(trailing)
  • Both

例如当设置leading: true且 TIME = 400ms
image
可见第一段中我一直在触发函数但正式但函数调用是在调用后但400ms后再次调用,在看第二段在首次触发后我在接着但300和400ms均匀触发函数但是后面不再触发导致函数没有方式第二次调用...

节流的实现

/**
  * 简单做法,leading
  */
  export function throttle(fn, limit) {  
    let delay = false
    return (...args) => {
      if (!delay) {
        fn.apply(this, args)
        delay = true
        setTimeout(() => {
          delay = false
        }, limit)
      }
    }
  }

节流的应用

个人看过一个比较有趣的例子是使用节流实现无限下拉,使用节流控制是保证用户在获取新内容可以即使但又不会过于频繁, demo在此。

[译] 精通JavaScript面试:Function Composition是什么?

image

Function composition是结合两个或更多函数产生一个新函数的过程。将函数组合在一起就像是将一系列管道拼凑在一起,以便我们的数据流通。

简单的说,函数`f`和`g`的组合可以被定义为`f(g(x))`,它是从里到外 -- 右至左去执行的。换句话说,执行的顺序为:

  1. `x`
  2. `g`
  3. `f`

让我们在代码中更仔细地看一下.想象你想要转换用户全名到URL slugs,以便为每个用户提供一个配置文件页面。为此,你需要经历一下步骤:

  1. 由空格拆分名字到数组中
  2. map名字成小写
  3. 加入破折号
  4. encode the URI component

这里是一个简单的实现:

const toSlug = input => encodeURIComponent(
  input.split(' ')
    .map(str => str.toLowerCase())
    .join('-')
);

不算太差...但是如果我告诉你可以更具可读性呢?

想象一下,这些操作都是对应可组合函数。这就可以写成:

const toSlug = input => encodeURIComponent(
  join('-')(
    map(toLowerCase)(
      split(' ')(
        input
      )
    )
  )
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

这比我们第一次尝试更难阅读,但至此我们已经走了一步。

为了实现这一目标, 我们为这些常用方法( split(), join() and map())使用了可组合模式 ,这里是其实现:

const curry = fn => (...args) => fn.bind(null, ...args);

const map = curry((fn, arr) => arr.map(fn));

const join = curry((str, arr) => arr.join(str));

const toLowerCase = str => str.toLowerCase();

const split = curry((splitOn, str) => str.split(splitOn));

除了toLowerCase()之外,所有这些功能的生产测试版本都可以从Lodash / fp获得。您可以像这样导入它们:

import { curry, map, join, split } from 'lodash/fp';

或者 --- (cherry pick)

const curry = require('lodash/fp/curry');
const map = require('lodash/fp/map');
//...

我在这里有点懒。请注意,这种curry在技术上不是真正的curry,它总能产生一元函数。相反,这是一个简单的partial application. 见“What’s the Difference Between Curry and Partial Application?”,但是为了本演示的目的,它将与真正的curry函数互换。

看看我们toSlug()实现,有一些东西真的困扰我:

const toSlug = input => encodeURIComponent(
  join('-')(
    map(toLowerCase)(
      split(' ')(
        input
      )
    )
  )
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

这看对我来说看起来嵌套太多,还有一点难读。我们可以通过一个函数来展平嵌套,该函数将自动为我们组合这些函数,这意味着它将从一个函数获取输出并自动将其传入到下一个函数的输入,直到它吐出最终值。

想象,我们有一些通用方法它看起来想做下面这些事,它接受一些值并将函数应用于每个值,累积计算出单一结果。这些值本省可以是方法。这个函数被叫做reduce(),但是为了满足以上compose函数行为,我们需要从右至左累计计算。

好事是,reduceRight()方法正式我们寻找的:

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

类似reduce(),reduceRight()方法接受reducer函数和一个初始值(x)。我们在这个函数数组上(从右至左),依次轮流累计计算。

使用compose,我们可以在不嵌套的情况下重写上面的组合:

const toSlug = compose(
  encodeURIComponent,
  join('-'),
  map(toLowerCase),
  split(' ')
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

同样,compose()一样 在lodash/fp中:

import { compose } from 'lodash/fp';
// or
const compose = require('lodash/fp/compose');

当你从数学形式思🤔的组合时,组合是很棒的......但是如果你想从左到右的顺序思考怎么办呢?

这又另一种形式通常叫做pipe(). Lodash中叫做flow():

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'

const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!

注意它的实现和compos()十分相似只是它的调用顺序刚好相反。

来看一下toSlug()函数的pipe()实现:

const toSlug = pipe(
  split(' '),
  map(toLowerCase),
  join('-'),
  encodeURIComponent
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

对我来说,这更可读一点。

original

核心的函数式程序员在整个应用中定义功能函数。我常使用它来消除中间过程的临时变量。仔细查看toSlug()pipe()版本,您可能会注意到一些特殊的东西。

在命令式变成中,当你对一些变量做转换,你需要知道这个转换过程每一步对应中间变量对引用,上面的pipe()实现是以无点的方式编写的,这意味着它根本不识别它运行的参数。

我经常使用pipes在像单元测试和Redux状态管理的reducer里来消除了对中间变量的需求,这些中间变量仅存在于一个操作和下一个操作之间的瞬态值。

这听起来可能有些诡异,但是你练习使用它,你会发现在函数式编程中,你正在使用非常抽象的通用函数,其中事物的名称并不重要。名字只是妨碍了。您可能会开始将变量视为不必要的样板。

这就是说,我认为无点的风格可一走的很远。它可能变得太密集,难以理解,但如果你感到困惑,这里有一点小建议... you can tap into the flow to trace what’s going on::

const trace = curry((label, x) => {
  console.log(`== ${ label }:  ${ x }`);
  return x;
});

这是你如何使用:

const toSlug = pipe(
  trace('input'),
  split(' '),
  map(toLowerCase),
  trace('after map'),
  join('-'),
  encodeURIComponent
);

console.log(toSlug('JS Cheerleader'));
// '== input:  JS Cheerleader'
// '== after map:  js,cheerleader'
// 'js-cheerleader'

trace()只是更通用的tap()的一种特殊形式,它允许你为pipe流中的每一个值(初始值,中间值,最终值)执行一些行为。明白了?pipe?tap?你可以这样写tap()

const tap = curry((fn, x) => {
  fn(x);
  return x;
});

现在你可以看见trace()只是tap()的一个特例:

const trace = label => {
  return tap(x => console.log(`== ${ label }:  ${ x }`));
};

您应该开始了解函数式编程是什么样的,以及部分应用程序和currying如何与函数组合协作以帮助您编写更易读且更少样板的程序。

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.