Giter Club home page Giter Club logo

blog's Introduction

zhaoqize

blog's People

Contributors

dependabot[bot] avatar zhaoqize 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

blog's Issues

GitHub新姿势强势解锁

键盘快捷键

在 GitHub 中,很多页面都可以使用键盘快捷键。在各个页面按下 shift + / 都可以打开键盘快捷键一览表。
image

通过部分名称搜索文件

在仓库页面试着按下键盘的 t 键,然后输入要找的目录或文件的部分名称,就可以快速的在仓库进行文件筛选了。
比如在https://github.com/zhaoqize/blog这个页面按下键盘 t
image

查看分支间的差别

在 GitHub 上,通过直接修改 URL 就可以让用户以多种形式查看分支差别。
比如我想查看 master 分支与 dev 分支的差别

https://github.com/Neilpang/acme.sh/compare/master...dev

image

查看指定日期之间的差别
查看master分支 2018-01-01 与现在的差别

https://github.com/Neilpang/acme.sh/compare/master@{2018-01-01}...master

查看几天前的差别
查看master分支最近1天内的差别

  • day
  • week
  • month
  • year

指定期间有上面这几种

https://github.com/Neilpang/acme.sh/compare/master@{1.day.go}...master

添加贡献规则

在Issue时候,经常会看到这个提示
image
只需要在仓库根目录下添加 CONTRIBUTING.md 文件后该链接就会显示出来。

GitHub 中可使用的描述方法并不止“@ 用户名”一种

  • 输入“@ 组织名”可以让属于该 Organization(组织)的所 有成员收到通知。
  • 输入“@ 团队名”可以让该团队的所有成员 收到通知。这就是同时向多人发送通知的方法。
  • 输入“# 编号”,会连接到该仓库所对应的 Issue 编号。
  • 输入 “用户名 / 仓库名 # 编号”则可以连接到指定仓库所对应的 Issue 编号。只要按照这类特定格式书写便会自动创建链接。

自动引用评论

我们可能经常性的在Issue中与别人讨论,会遇到想要引用别人的话语,这时候我们只需要选中想要引用的评论然后按 R 健即可。
image

行号定位

文件内容的左侧会显示该文件的行号。假如我们点击第 10 行的行 号,这一行就会被高亮标记为黄色,同时 URL 末尾会自动添加“#L10”。使用这个 URL,程序员们在交流时,就可以将讨论明确指向 某一行。另外,如果将“ # L10”改成“ # L10-15”,则会标记该文件的第 10~15 行。
image
image

另外推荐一个Git 基本操作的网站:learngitbranching

JavaScript的异步性和队列问题

image

JavaScript线程

浏览器一般会有下面三个线程,多个线程之间的同步操作是通过浏览器 内核控制 实现的。

解说

  • javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
  • GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码)

JavaScript事件队列

解说

  • Macrotasks包含生成dom对象、解析HTML、执行主线程js代码、更改当前URL还有其他的一些事件如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,macrotask代表一些离散的独立的工作。当执行完一个task后,浏览器可以继续其他的工作如页面重渲染和垃圾回收。
  • Microtasks则是完成一些更新应用程序状态的较小任务,如处理promise的回调和DOM的修改,这些任务在浏览器重渲染前执行。Microtask应该以异步的方式尽快执行,其开销比执行一个新的macrotask要小。Microtasks使得我们可以在UI重渲染之前执行某些任务,从而避免了不必要的UI渲染,这些渲染可能导致显示的应用程序状态不一致。

参考

"所谓"的前端算法

算法,这个题目有点大。
其实算法是一个很宽的概念,我们写的所有程序都可称之为算法,因为算法就是一个处理问题的逻辑,将问题进行归类,抽象出一个统一范式,然后为这个范式取个名字,比如:快速排序。
所以这里我们就来看下前端有哪些常用的算法。

递归

default

递归算法 : 英语:recursion algorithm)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。

递归的两个要素:

  • 调用自身
  • 能跳出循环

阶乘
n! = n*(n-1)...21

6! = 6 * 5 * 4 * 3 * 2 *1

规律:n的阶乘就是从n开始依次递减值的积

function factorial(number){
  if(number==1) {
    return number;
  } else{
    return number*factorial(number-1);
  }
}

斐波那契数列
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... 求第n个数是多少

第一个数: 1 = 1
第二个数: 1 = 1
第三个数: 2 = 1 + 1
第四个数: 3 = 2 + 1
第五个数: 5 = 3 + 2
第六个数: 8 = 5 + 3
...

规律:后一项是前两项的和

function fibonacci(number) {
  if (number <= 2) {
    return 1;
  }
  return fibonacci(number-1) + fibonacci(number - 2)
}

排序算法

冒泡排序算法:它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

default

比较流程

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

升顺排列
6 2 7 9 5

第一次冒泡: 2 6 7 9 5
第二次冒泡: 2 5 7 9 6
...

// 升序比较
function bubbleSort(arr) {
  var nArr = arr.slice();
  var temp;
  if (nArr.length > 0) {
     for (var i = 0; i < nArr.length; i++){
        for (var l = i; l < (nArr.length - 1); l++){
            if (nArr[i] > nArr[l+1]) {            
              temp = nArr[i];
              nArr[i] = nArr[l+1];
              nArr[l+1] = temp;
            }
        }
     }
  }

  return nArr;
}

快速排序算法:快速排序是处理大数据集最快的排序算法之一。它是一种分而治之的算法,通过递归的方式将数据依次分解为包含较小元素和较大元素的不同子序列。该算法不断重复这个步骤直到所有数据都是有序的。

default

排序说明

  • 第一步: 基准值选取。基准值可以任意选取
  • 第二步: 进行分区操作。按照顺序将每个元素与基准进行比较,想成两个子集(大于基准值,小于基准值)
  • 第三步,递归操作。对两个子集不断重复第一步和第二步,直到所有子集只剩下一个元素为止
function qSort(arr){ 
	if(arr.length <= 1){
		return arr;
	}

	let midIndex = Math.floor(arr.length/2); //取基准点
	let midVal = arr.splice(midIndex,1); //取基准点的值
	let left = [];//存放比基准点小的数组
	let right = [];//存放比基准点大的数组
	for(let i = 0; i < arr.length; i++){
		if(arr[i] < midVal){
			left.push(arr[i]);//比基准点小的放在左边数组
		}else{
			right.push(arr[i]);//比基准点大的放在右边数组
		}
	}
	// 递归
	return quickSort(left).concat(midVal,quickSort(right));
}

检索算法

二分查找算法:也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

default

查找说明

1.将数组的第一个位置设置为下边界(0)
2.将数组最后一个元素所在的位置设置为上边界(数组的长度减1)。
3.若下边界等于或小于上边界,则做如下操作。

  • 将中点设置为(上边界加上下边界)除以2
  • 如果中点的元素小于查询的值,则将下边界设置为中点元素所在下标加1
  • 如果中点的元素大于查询的值,则将上边界设置为中点元素所在下标减1
  • 否则中点元素即为要查找的数据,可以进行返回。

注意这里的数组必须是有序

function binSearch(arr, data) {
  var upperBound = arr.length - 1;
  var lowerBound = 0;
  while (lowerBound <= upperBound) {
      var mid = Math.floor((upperBound + lowerBound) / 2);
      if (arr[mid] < data) {
          lowerBound = mid + 1;
      }
      else if(arr[mid] > data) {
          upperBound = mid - 1;
      } else {
          return mid;
      }
  }
  return -1;
}

二叉树算法

default

二叉树
二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

二叉树的特性

  • 每个结点最多有两颗子树,结点的度最大为2
  • 左子树和右子树是有顺序的,次序不能颠倒
  • 即使某结点只有一个子树,也要区分左右子树。

二叉树遍历

这里采用递归-先序遍历一个树形结构

var preOrder = function (node) {
  if (node) {
    preOrder(node.left);
    preOrder(node.right);
  }
}

Vue中容易被忽略的知识点

Vue版本2.5.13

不要在选项属性或回调上使用箭头函数

比如

  • created: () => console.log(this.a)
  • vm.$watch('a', newValue => this.myMethod())
    因为箭头函数是和父级上下文绑定在一起的,this 不会是如你所预期的 Vue 实例,经常导致 Uncaught TypeError: Cannot read property of undefined 或 Uncaught TypeError: this.myMethod is not a function 之类的错误

具体见:这里

v-html

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html 指令:

<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

具体见:这里

计算属性缓存 vs 方法

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
这个 计算属性是基于它们的依赖进行缓存的 是什么意思呢?

computed: {
  now: function () {
    return Date.now()
  }
}

这里虽然Date.now()的值一直在变,但是他没有被watch,因为他不是响应式依赖。

具体见:这里

CSS样式自动添加前缀

v-bind:style 使用需要添加浏览器引擎前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

具体见: 这里

<template> 元素上使用 v-if 条件渲染分组

因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

具体见:这里

v-ifv-showv-for

  • v-if 是对页面元素的添加和移除操作
  • v-show 是对页面元素的显示和隐藏操作
  • 当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级。

具体见:这里

数组更改检测注意事项

Vue 不能检测一下方式变动的数组,从而将不会触发视图更新

  • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

具体见:这里

对象更改检测注意事项

Vue 不能检测对象属性的添加或删除

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

但是我们可以通过 Vue.set(object, key, value)方法向嵌套对象添加响应式属性。
另外还有这个常用的方法 Object.assign(),当我们想要为某个对象赋予 多个新属性 的时候,你应该这么玩

this.userProfile = Object.assign({}, this.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

具体见: 这里

v-for 中使用 methods 方法

<li v-for="n in even(numbers)">{{ n }}</li>

具体见: 这里

组件使用 v-for

在自定义组件里,你可以像任何普通元素一样用 v-for

<my-component v-for="item in items" :key="item.id"></my-component>

具体见: 这里

.once 事件修饰符(2.1.4 新增)

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

.once 也可以使用在 自定义组件 上。

具体见:这里

is 特性

由于dom某些元素本身的一些限制,<ul><ol><table><select> 这样的元素里允许包含的元素有限制。

<div id="app">
  <table>
    <money></money>
  </table>
</div>

Vue.component('txt',{
   template: '<div>I like money!</div>'
})

new Vue({
  el:'#app'
})

将会解析成下面的dom

<div id="app">
  <div>I like money!</div>
  <table></table>
</div>

如果想要解析正确,需要使用 is 这个属性。

<div id="app">
  <table>
    <tr is="money"></tr>
  </table>
</div>

这样dom就解析正确了。

<div id="app">
  <table>
     <tbody>
        <div>I like money!</div>
     </tbody>
  </table>
</div>

具体见:这里

将对象的所有属性作为 prop 进行传递

如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一个 todo 对象:

todo: {
  text: 'Learn Vue',
  isComplete: false
}

然后:

<todo-item v-bind="todo"></todo-item>

将等价于:

<todo-item
  v-bind:text="todo.text"
  v-bind:is-complete="todo.isComplete"
></todo-item>

具体见: 这里

非 Prop 特性的替换与合并

  • classstyle这两个特性的值都会做合并 (merge) 操作
  • 其他属性(如: type) 则会进行覆盖

具体见: 这里

Props的一般绑定和动态绑定

我们常用的一般是动态绑定:

// 父组件
<child :my-message="parentMsg"></child>

new Vue({
  data () {
    return {
       parentMsg: '来自父组件的数据'
    }
  }
})

// 子组件
Vue.component('child', {
  // 在 JavaScript 中使用 camelCase
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})

显示:

<span>来自父组件的数据</span>

一般绑定:

// 父组件
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>

子组件获得的是: 字符串 'hello!'

具体见:这里

.sync 修饰符(2.3.0+新增)

之前在 2.0 版本中移除后,在 2.3.0 中又加上了,只是调用的逻辑发生了变化,变成了一种语法糖。
如下代码:

<comp :foo.sync="bar"></comp>

会被扩展为:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

this.$emit('update:foo', newValue)

有点类似与 v-model

另外需要注意的是:在computed中定义的属性需要手动设置 set

computed: {
    // 仅读取
    aDouble: function () {
      return this.a * 2
    },
    // 读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }

具体见:这里

自定义组件的 v-model(2.2.0 新增)

默认情况下,一个组件的 v-model 会使用 value propinput 事件。这也是之前 v-model 默认绑定的元素 和 事件方法。

但是到 2.2.0 时候,我们可以通过 model 配置这个两个属性。

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // 这样就允许拿 `value` 这个 prop 做其它事了
    value: String
  },
  // ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

上述代码等价于:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

具体见:这里

插槽内容分发

我们不总能遇见我们的组件中包含了哪些元素,这时候我们在开发组件的时候,需要让这部分内容自定义。
假定 my-component 组件有如下模板:

<div>
  <h2>我是子组件的标题</h2>
  <slot>
    只有在没有要分发的内容时才会显示。
  </slot>
</div>

父组件模板:

<div>
  <h1>我是父组件的标题</h1>
  <my-component>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </my-component>
</div>

渲染结果:

<div>
  <h1>我是父组件的标题</h1>
  <div>
    <h2>我是子组件的标题</h2>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </div>
</div>

当然还有 具名插槽作用域插槽(2.1.0 新增)、slot-scope(2.5.0新增)

具体见:这里

动态组件

通过使用保留的 <component> 元素,并对其 is 特性进行动态绑定,你可以在同一个挂载点动态切换多个组件:

var vm = new Vue({
  el: '#example',
  data: {
    currentView: 'home'
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
<component v-bind:is="currentView">
  <!-- 组件在 vm.currentview 变化时改变! -->
</component>

注意这里的 is 与 之前说的 v-bind:is 别混淆

具体见:这里

对低开销的静态组件使用 v-once

尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:

Vue.component('terms-of-service', {
  template: '\
    <div v-once>\
      <h1>Terms of Service</h1>\
      ...很多静态内容...\
    </div>\
  '
})

具体见:这里

混合(mixins)的合并策略

周期钩子的合并策略

  • 同名钩子函数将混合为一个数组,因此都将被调用
  • 混合对象的钩子将在组件自身钩子 之前 调用
var mixin = {
  created: function () {
    console.log('混合对象的钩子被调用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})

// => "混合对象的钩子被调用"
// => "组件钩子被调用"

methods, components 和 directives 的合并策略

  • 两个对象键名冲突时,取组件对象的键值对
var mixin = {
  methods: {
    foo: function () {
      console.log('foo')
    },
    conflicting: function () {
      console.log('from mixin')
    }
  }
}

var vm = new Vue({
  mixins: [mixin],
  methods: {
    bar: function () {
      console.log('bar')
    },
    conflicting: function () {
      console.log('from self')
    }
  }
})

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

具体见:这里

Vue2的独立构建与运行时构建的差别

其实这个问题在你使用vue-cli构建项目的时候是不会出现的,因为你在创建项目的构建过程中已经让你勾选了,然后会写入webpack.config.js中。

image

这就在这,会让你选择Vue的构建方式。

image

如果你勾选Runtime + Compiler就会出现如上的配置。

其实这里涉及到一个概念:

  • 独立构建:含义是,拥有完整的模版编译功能运行时调用功能
  • 运行时构建:含义是,只拥有完整的运行时调用功能

image

为什么会有这种区分呢?

  • 第一,因为Vue使用/运行过程分为两个阶段,第一阶段是将模版进行编译(如单个vue文件中的template)为渲染函数(render),第二阶段是将实际函数的调用阶段。
  • 第二,因为自Vue2.x开始,Vue开始支持SSR(服务端渲染),而服务端是没有DOM这些概念的。所以导致了从Vue2.x后会有分包的问题。

下面是官方话术:

  1. 独立构建包括编译和支持 template 选项。 它也依赖于浏览器的接口的存在,所以你不能使用它来为服务器端渲染。
  2. 运行时构建不包括模板编译,不支持 template 选项。运行时构建,可以用 render 选项,但它只在单文件组件中起作用,因为单文件组件的模板是在构建时预编译到 render 函数中,运行时构建只有独立构建大小的 30%,只有 16Kb min+gzip 大小。

所以两者最大的区别也就出来了

  1. 独立构建包括编译和支持 template 选项
  2. 运行时构建不包括模板编译,不支持 template 选项

Jade模板引擎让你飞

现在 jade 改名成 pug

安装

npm install jade

基本使用

简单使用

p hello jade!

渲染后:

<p>hello jade!</p>

jade安装成功后,进入node命令使用。

jade.compile:编译字符窜

> var jade = require('jade')
undefined
> jade.compile('p hello jade!')()
'<p>hello jade!</p>'

jade.compileFile:编译jade文件

> var jade = require('jade')
undefined
> jade.compileFile('hello.jade')()
'<p>hello jade!</p>'
>

jade.render:渲染html

> jade.render('p hello jade!')
'<p>hello jade!</p>'

jade.renderFile:渲染jade文件

> jade.renderFile('hello.jade')
'<p>hello jade!</p>'
>

当jade全局安装后也可以直接使用jade命令。

jade filename

C:\Users\Administrator>jade hello.jade

  rendered hello.html


C:\Users\Administrator>

jade -P filename 使html文件变得可读

修改hello.jade文件为:

doctype html
html
    head
        title hello jade!
    body
        p hello jade

运行:

jade hello.jade

jade.html文件变成这样:

<!DOCTYPE html><html><head><title>hello jade!</title></head><body><p>hello jade</p></body></html>

这样的可读性太差了,不过没事我们有P(pretty)参数

运行:

jade -P hello.jade

hello.html文件变成这样:

<!DOCTYPE html>
<html>
  <head>
    <title>hello jade!</title>
  </head>
  <body>
    <p>hello jade</p>
  </body>
</html>

这样看起来顺眼多了。

jade -w filename 监控文件

执行:

C:\Users\Administrator>jade -w hello.jade

  watching hello.jade
  rendered hello.html

一旦我们修改jade文件,html文件会实时修改。此乃神技也,有点像supervisor。

常规用法

选择器的使用

p.bt.cn#dn

编译后

<p id="dn" class="bt cn"></p>

如果省略标签元素,默认是div

.bt.cn#dn

编译后

<div id="dn" class="bt cn"></div>

属性的使用

一般属性

div(color='red',font-size='1.5rem')

编译后

<div color="red" font-size="1.5rem"></div>

多个属性如果写一行觉得拥挤的话,可以分开写

div(color='red'
    font-size='1.5rem') 

style属性

a(style={color:'red'})

编译后:

<a style="color:red;"></a>

带有杠的CSS属性写法

a(style={'z-index':'11000'})

字符转义

使用 = 赋值会进行转义

div(href="https://www.baidu.com/s?wd=jade&ws=jades")

编译后:

<div href="https://www.baidu.com/s?wd=jade&amp;ws=jades"></div>
& 发生了转义 &amp;

使用 != 不会转义

div(href!="https://www.baidu.com/s?wd=jade&ws=jades")

编译后:

<div href="https://www.baidu.com/s?wd=jade&ws=jades"></div>

数据库中的字符串这样:萱公子&青橙

很明显被转义了。

显示到前端页面如果继续使用 #{} 这样的形式的话,输出的会是萱公子 &amp; 青橙。肯定是不行的。

这时候,我们可以使用:!{} 这样的形式

变量的使用

单个变量

- var code = 1;
p.bt #{code} 

编译后:

<p class="bt">1</p>

对象

- var code = {z:1,q:2};
p.bt #{code.q} 

编译后:

<p class="bt">2 </p>

字符串拼接

- var code = {z:1,q:2};
p(class='bt'+code.z) #{code.q}

编译后:

<p class="bt1">2</p>

流程控制语句

Case

- var i=0;
case i
    when 0
        div 变量为#{i}
    when 1
        div 变量为1
    default
        div 没有匹配项

编译后:

<div>变量为0</div>

For

- for(var i=0;i<2;i++)
   div #{i} //注意缩进

编译后:

<div>0</div>
<div>1</div>

If...else

- var ifv = true;
if(ifv)
    div  为真
else
    div 为假

编译后:

<div>为真</div>

注释

html可见注释

//html可见注释
div.bt

编译后:

 <!--html可见注释-->
 <div class="bt"></div>

html不可见注释

//-html不可见注释
div.bt

编译后:

<div class="bt"></div>

多行注释(注意缩进)

//
  div.bt 

编译后:

<!--div.bt-->

条件注释

<!--[if IE 8]>
<html lang="en" class="ie8">
<![endif]-->
<!--[if IE 8]><!-->
<html lang="en">
<!--<![endif]-->

编译后:

<html lang="en" class="ie8">
<![endif]-->
<!--[if IE 8]><!-->
<html lang="en">
<!--<![endif]-->

include

doctype html
html
  head
    style
      include style.css
  body
    script
      include script.js

编译后:(一定要有这两个文件,不然jade会报错)

<!DOCTYPE html>
<html>
  <head>
    <style>p{
    color:red;
    }
    </style>
  </head>
  <body>
    <script>console.log(1)</script>
  </body>
</html>

extends与block

layout.jade

doctype html
html
    head
        title hello jade!
    body
     block content
        
        block foot     

business.jade

extends ./layout.jade

block content
       h1 content主体部分 

block foot
    h1 foot脚注部分

编译后:

busuness.html

<!DOCTYPE html>
<html>
  <head>
    <title>hello jade!</title>
  </head>
  <body>
    <h1>content主体部分</h1>

    <h1>foot脚注部分</h1>
  </body>
</html>

jade中写行内js或css

doctype html
html
  head
    style.
    p{color:red}
  body
    script.
    console.log(OK)

编译后:

<!DOCTYPE html>
<html>
  <head>
    <style>p{
    color:red;
    }
    </style>
  </head>
  <body>
    <script>console.log(OK)</script>
  </body>
</html>

强大的Mixins

mixin templ_li(value)
    li #{value}
ul
    +templ_li('香蕉')
    +templ_li('橘子')

编译后:

<ul>
   <li>香蕉</li>
   <li>橘子</li>
</ul>

这个特性让我们能自定义一些模板函数。特别是当我们的 html 结构有相似的时候。

其实跟less中的公共类,react中的公共函数也都是共通的。

less中:

.temp_color(@color:red){
  color:@color;  
}

使用

p{
 .temp_color(blank);
}

react中:

var temp_prop = {
  getDefaultProps:function(){
    return {name:'共有属性'};
  }  
}

使用

var ComponentDib = React.createClass({
  mixins:p[temp_prop ],
  render:function(){
    return <h1>{this.props.name}</h1>
  }
})    

我理解的函数柯里化

对于函数柯里化之前就了解过,大概知道是个什么东西。
最近在读Vue源码的时候,看到了 cached 函数的使用,让我觉得这个代码写的挺有意思,于是在sf发问,看了 @sunyongjian 回答,于是又绕到了 柯里化高阶函数上

柯里化定义

维基中有对柯里化的定义:在计算机科学中,柯里化(英语:Currying),又译为卡瑞化加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

用JavaScript表达定义

着重看 多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数

var foo = function(a) {
    return function (b) {
        return function (c) {
            return a+b+c;
        };
    };
};

使用

foo(1) // f (b) {  return function(c) { return a+b+c } }
foo(1)(2) // f (c) {  return a+b+c }
foo(1)(2)(3)  // 6

其实就是逐渐消元的过程。知乎上也有类似的问题,具体见:如何理解functional programming里的currying与partial application

通用的柯里化函数

上面的例子虽然解释了什么是柯里化,但是还不通用,因为 参数个数 理论上是无法估计的。

下面就是一个抽象的柯里化函数

var currying = function(fn) {
  var args = [...arguments].slice(1);

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 没传参数时,调用这个函数
    } else {
      [].push.apply(args, arguments); // 传入了参数,把参数保存下来
      return arguments.callee; // 返回这个函数的引用
    }
  }
}

场景使用

实际场景的运用能更好的加深我们的理解,所以我们用实际的生活场景来看看 柯里化 如何运用。

场景:记账本,每天记录使用多少钱,一个月算一次总花费(或者在我想要知道的时候)

function account(){
   var total = [];
   function money(p) {
     if (arguments.length === 0) {
       // 计算一共花了多少钱
       total = total.reduce((sum, value) => {
          return sum + value;
       }, 0)
       console.log('一共花了', total+' 元');
     } else {
       // 记录每天花了多少钱
       total.push(p)
       console.log('今天花了', p+' 元');
     }
   }
   return money;
}

这个我们就定义好了一个 柯里化 的记账本。
我们来用一下

var spend = account();
spend(15) // 今天花了 15 元
spend(30) // 今天花了 30 元
spend() // 一共花了 45 元 (不传参数的时候就返回真正的结果)

Tip:高阶函数:一个函数接收另一个函数作为其参数。

何时使用柯里化

参考:函数式编程与面向对象编程[1]: Lambda表达式 函数柯里化 高阶函数

如果某些参数在大部分情况下,总是需要同时给出,才具有真实的意义,那么应该把这些参数组合成一个组合型参数来处理,而不应该科里化。
如果要定义的多参函数是一个闭合函数,那么它是很可能需要被多次应用的,这种情况下,应该用组合型参数的方式来处理。
如果先指定某一些参数就有明确的意义,那么就应该用科里化的方式来处理。

我的2019

WechatIMG68

健康

身体是本钱

这是我最近几年体会最深的一个话题。由于17年开始就腰肌劳损,腰突。所以最近几年经常性的会腰酸腰疼。于是今年开始保持每周最低锻炼一次,经过半年时间的锻炼,有了一定好转,没出现过之前的卧床不起的情况了。另外强烈建议程序员们工作再忙,也要保持锻炼,这是为自己的未来做投资。说到投资不得不提的一个点是,趁着年轻一定要给自己和家人买份重疾险(买消费型的,不要买储蓄的)。如果工作强大很大,建议再买一份额外的防猝死险。心里一定要警醒:互联网行业是一个高危行业。

工作

乐观+积极+精益

19 年 7 月份从美团离职来了头条,由于是大小周且加班严重,一开始以为坚持不了一个月,没想到坚持了半年了。头条的氛围正如他的口号一般:永远创业。这里的年轻人非常的多。我所在的部门主要是做安全风险对抗的。业务壁垒比较高,这本身对我来说是就是一份新的尝试和一次巨大的挑战,还好业务比较有意思且较为幸运遇到了一个好领导,开放、包容、思辨等特质在他身上有着完美的体现。这对我有着潜移默化的影响。另外由于领导的认可,也带了一群乐观、积极的小伙伴。

微信很多朋友问我,在头条工作怎么样?我这里简单的用几个词回答下吧。在头条工作你需要真的具有 抗压能力强积极乐观不断精益 的思维。

生活

平安是福

主要做了两件比较有意义的事情。
第一个是第一次出国(自由行),二月份从北京出发去了云南、曼谷、普吉岛和清迈。曼谷给我最大的感受是一个字:香。一到曼谷的机场,那香味,真的很好闻!结果玩了几天发现,曼谷处处都是香味。有点神奇~。由于吃不惯泰国的食物无奈天天吃泰国的 KFC,还好是真的便宜,感觉比国内便宜一半,几乎天天吃(都快吃吐了)。

友情提示:出国一定要买个保险,几百块买个安心。

第二个是在今年的十一阅兵带爸妈逛了 7 天北京城,跑了比较多的地儿。终于去了一次长城,虽然只爬了南段,但看着他们一脸兴奋又激动的样子,特别满足。

还有个小插曲是 11 月份去了次重庆,主要是抢到了比较便宜的飞机票。[你说气人不😄]

成长

心中的火,永不息灭

虽然时间一年一年的过去,但是每年也算有些小成长。2019 年我觉得成长最多的就是心态。因为不断的跳出舒适区,不断的面临各种挑战,这样就自然而然的经历了各种状况,心态和心智成长的也愈发的快。感慨万千道:很多时候不是没有挑战,而是我们习惯刻意回避挑战。因为自我保护是人的本能。但要在如此艰难的社会生存下来和想要生活的更好,不主动出击行吗?毫不谦虚的说,如果 100 是满分,我应该能打 79 分,真的没想到很多错综复杂的事情我也能处理的还不错。比如老板的“有你在我就很放心”,丈母娘第一次夸奖“还是你眼力劲好,勤快”。(最近经常失眠,也是我“作”出来的😂)

新的尝试

去年开始尝试使用“正念法”进行减压和练习专注,想 2020 年深度实践一下。

好了,这就是我简单且充实的 2019 年。欢迎大家在评论中贴出你自己的 2019 年总结。一起分享和交流。

前端开发者应该知道的UI设计的那些事儿

在做移动端开发的时候,可能会遇到类似的对话。

设计师:这个交互的设计跟APP一样
开发:嗯,为什么这么设计呢
设计师:因为APP的设计有它的规范,这样才能保证体验的(接近)一致

可以看见,设计师在设计 WebApp 页面的时候,很多交互来源于原生应用,比如常见且经典的侧滑等交互。所以作为一个前端开发者来说,虽然我们不需要 coding 原生的界面开发,但是还是应该知道一些前端交互,目前看来很多 HTML5页面的UI设计都衍生于原生应用的设计。

跳板式导航

跳板式导航设计指的是一个带有菜单选项的界面,而菜单选项就是进入各个应用的起点。

这个设计在我们常用的APP里面多如牛毛,比如淘宝APP/淘宝M站的首页

这些图片是各个业务的起点,点击后可跳转到对应的业务。

列表菜单式导航

列表菜单式中的每一个列表项都是进入该应用各项功能的入口,这一点与跳板式类似,并且模块之间的切换需要返回到列表主页。

这个在我们的手机邮件中就能看见

每个列表项都是对应信息的入口,且在具体邮件的入口有个返回主列表的功能。

选项卡菜单式导航

选项卡式中标签栏的作用是在应用的主要类别之间进行切换,或是指向特定页面的可执行操作。

还是看手淘APP

下面这四个标签分别用来切换主要类别,应该属于整个应用的 top 1 层级且十分重要。

某些原生应用的设计规范中有对这一层规定,比如选项卡式的导航切换最多不能超过 5 个,但是由于有时候确实会存在多于 5 个的情况,所以会采用更多这种形式去处理。

侧边抽屉式导航

侧边抽屉式有两种风格

  • 浮层(overlay),通过轻滑或点 击的手势打开抽屉,抽屉部分遮挡或覆盖原来页面的内容
  • 嵌入层(inlay),通过轻滑、平移或点 击打开抽屉,把原先的页面内容部分推出屏幕外

比如B站的APP

常见的是左侧抽屉,但也可以是在右侧,侧边抽屉的内容不必限制在导航选项范围内。比如包括个人信息等。

下拉菜单式导航

下拉菜单也有两种风格

  • 嵌入式的,将页面内容推到菜单之下
  • 浮层式的,菜单层浮在内容页之上

下拉菜单式的一个重要规则是:无论什么样的手势,都能打开菜单,比如点击图标、轻滑、平移。同样,隐藏菜单也是这样的。不要让菜单覆盖整个屏幕,要把背景露出来一点。点击背景的任一位置时,同样可以隐藏菜单。

淘宝M站中的分类
wx20180218-095809 2x

你所不知道的H5页面弹窗返回

看到这个题目你可能觉得这是什么鬼?
其实我想说的是这种,看下面的录制:

opp5

这种交互在H5页面中比比皆是,点击城市->弹出城市选择浮层->按返回按钮关闭浮层

这些操作都是不要点击左上角/右上角的关闭按钮就可以进行的,飞猪的H5是前进出现弹层,返回时弹层关闭,其他家都不行(去哪儿网H5飞机票,美团H5酒店)。

为什么要这么设计?

因为H5是在手机上操作的,手机上的手指可操作区域的覆盖范围很小,更别说左上角/右上角这些死角(取消/关闭)区域了。你肯定听过这个操作:轻触返回。这个在用户操作的时候非常方便友好,选择完城市后,不需要点击取消,直接在大拇指可以操作的地方点击返回就关闭了弹层。

如何实现

既然有这种非常好的需求,那作为开发肯定就会想法设法的实现这个功能了。
你甚至都不用google,你就应该会想到类似的history.back(),history.go()这些方法了。
然而想到这些依旧没用,理论上 浏览器/webview 的返回/前进的是要重新加载页面的,因为URL发生了变化。
如果你真的知道单页面应用(SPA),或者使用React/Vue你就应该知道有个东西叫:路由。
这些通过改变hash且无法刷新的url变化是HTML5时加入的history功能

the-history-interface

interface History {
  readonly attribute unsigned long length;
  attribute ScrollRestoration scrollRestoration;
  readonly attribute any state;
  void go(optional long delta = 0);
  void back();
  void forward();
  void pushState(any data, DOMString title, optional DOMString? url = null);
  void replaceState(any data, DOMString title, optional DOMString? url = null);
};
  • pushState
  • replaceState

还有一个事件

  • onpopstate

pushState,replaceState 用来改变histroy堆栈顺序,onpopstate 在返回,前进的时候触发

vue-router中的实现也是如此(第1694行)

具体实现

既然说了这么多,那我们来看下怎么实现这种功能。

来看下 pushState 和 replaceState 的兼容性

image

全绿,用起来放心多了。

思路:

  • 点击弹层时 pushState 添加 hash
  • "轻触返回"的时候触发 onpopstate 事件时候隐藏弹层并修改 hash
<button onclick="city()">
        城市
    </button><br>
    <button onclick="calendar()">
        日历
    </button><br>
    <button onclick="description()">
        说明
    </button>

    <div id="city" class="com" style="display: none;">
      模拟城市弹框层
    </div>
    <div id="calendar" class="com" style="display: none;">
      模拟日历弹框层
    </div>
     <div id="description" class="com" style="display: none;">
      模拟说明弹框层
    </div>
      button {
          border: #0000;
          background-color: #f90;
      }
      .com {
        position: absolute ;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        background-color: #888589;
      }
var cityNode = document.getElementById('city');
    var calendarNode = document.getElementById('calendar');
    var descriptionNode = document.getElementById('description');
      function city() {
        cityNode.style.display = 'block';
        window.history.pushState({'id':'city'},'','#city')
      }
      function calendar() {
        calendarNode.style.display = 'block';
        window.history.pushState({'id':'calendar'},'','#calendar')
      }
      function description() {
        descriptionNode.style.display = 'block';
        window.history.pushState({'id':'description'},'','#description')
      }
      window.addEventListener('popstate', function(e){
        // alert('state:' + e.state + ', historyLength:' + history.length);
        // if (e.state && e.state.id === 'city') {
            // history.replaceState('','','#');
            // cityNode.style.display = 'block';
        // } else if (e.state && e.state.id === 'calendar') {
            // history.replaceState('','','#');
            // calendarNode.style.display = 'block';
        // } else if (e.state && e.state.id === 'description') {
            // history.replaceState('','','#');
            // descriptionNode.style.display = 'block';
        // } else {
            cityNode.style.display = 'none';
            calendarNode.style.display = 'none';
            descriptionNode.style.display = 'none';
        // }
      })

主要看 JS 代码,监听页面的前进和后退事件来控制history。

opp6

也可以扫码下面二维码在手机上查看

源码

换一种方式理解观察者模式

观察者模式

观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

使用观察者模式的好处:

  • 支持简单的广播通信,自动通知所有已经订阅过的对象。
  • 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  • 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

理解的第一阶段

我们先用一个通俗的情景来比喻一下,就拿 航班 与 航站楼 之间的关系:
image

图上显示的是 飞机1在从 北京 飞往 南京, 北京航站楼A 和 南京航站楼B 都需要关注 飞机1 的信息,
而 飞机1 在位置变化 之后也要及时的 通知 北京航站楼A 和 南京航站楼。
(其实可以看出来,这里 航站楼是被动的接受 飞机是主动的发出变化)

所以,我们可以写一个简单的 观察/订阅模式

function Observer () {
    // 数组存放多个观察者
    this.fns = [];
}

Observer.prototype = {
    // 订阅  (飞机信息)
    sub: function (fn) {
        this.fns.push(fn);
    },
    // 退订 (飞机信息)
    unsub: function (fn) {
        this.fns = this.fns.filter(function(el) {
            if (el !== fn) {
                return el;
            }
        })
    },
    // 发布 (飞机信息变化时)
    publish: function (o, thisObj) {
        var scope = thisObj || window;
        this.fns.forEach(function(el) {
            el.call(scope, o);
        });
    }
}
var ob = new Observer;

初始化 飞机1 的地理位置

var location = '北京';

定义 2 个航站楼的行为

// 北京 航站楼A - 观察者
var hangzhanlouBeijing = function (data) {
    console.log('北京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在:' +data);
    // 做一些操作
};

// 南京 航站楼B - 观察者
var hangzhanlouNanjing= function (data) {
    console.log('南京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在:' +data + ',我这就准备接机');
    // 做一些操作
};

南京和北京航站楼开始订阅(注册),将要关注 飞机1 的地址位置

// 订阅  (飞机地理位置的变化)
ob.sub(hangzhanlouBeijing);
ob.sub(hangzhanlouNanjing);

飞机1 经过一段时间的飞行 从 北京 到了 南京 ,这时候地理位置发生了变化

// 经过一些 操作 导致 location 发生了变化
location = '南京';

重要 然后 检查(校验) location(地址位置) 是否发生变化,如果发生变化,就进行通知

// 数据 location 发生变化(现在的值 不等于 原来的值)
if (location!== '北京') {
    // 发布  数据变化了(确认地址位置变了 ),广播 所有订阅(关注) 飞机1 的观察者(航站楼)
    ob.update(location);
}

输出

北京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在: 南京
南京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在: 南京,我这就准备接机

理解的第二阶段

image

  • 情况一:
    因为这里我们只有一架 飞机1 具有了 观察者模式,如果再来一架 飞机2 也是从 北京 飞往 南京 的呢?很明显,我们也需要知道 飞机2 的地理位置信息
    image

  • 情况二:
    还有假如 北京航站楼 只关注 飞机1 的变化,而不关注 飞机2 的变化,同理, 南京航站楼 只关注 _飞机2_的变化, 而不关注 飞机1 的变化

那这样情况就比较复杂了

学术一点的意思就是:要让多个对象都具有观察者模式(让 多个 观察者 可以关注 多个 自己想关注的 观察对象)。

这时候分析下,相比较之前有什么变化

  • 首先,存放 观察者(航站楼) 的数组 的个数不能固定,且类别/附属 应该 与 观察对象 关联挂钩
  • 再者,观察对象被观察属性 也不应该固定(之前只关注了 飞机1 的 location ,比如,我们可以观察 飞机 的 地理位置,同样也可以关注 飞机 的 海拔高度 等信息)

用图表示 应该就是下面这种 数据情景

image

所以这里 我们定义的一个通用函数 ,它应该具备 上述的 一些特性

  • 多个观察者
  • 多个观察者对象的多个属性
  • 给 他们添加 订阅,取消订阅,发布的方法

也就是说:每个观察者对象,都独立维护一套独立的观察者模式。

根据需要,我们抽象出下面 这样一个通用的函数:

//通用代码
var observer = {
    //订阅
    addSubscriber: function (callback) {
        this.subscribers[this.subscribers.length] = callback;
    },
    //退订
    removeSubscriber: function (callback) {
        for (var i = 0; i < this.subscribers.length; i++) {
            if (this.subscribers[i] === callback) {
                delete (this.subscribers[i]);
            }
        }
    },
    //广播
    publish: function (what) {
        for (var i = 0; i < this.subscribers.length; i++) {
            if (typeof this.subscribers[i] === 'function') {
                this.subscribers[i](what);
            }
        }
    },
    // 通过遍历,给 观察者对象 o 添加 观察者容器,订阅,退订,发布
    make: function (o) { 
        for (var i in this) {
            o[i] = this[i];
            // 每个 观察者对象  都维护自身的一个 观察者 数组
            o.subscribers = [];
        }
    }
};

OK,已经抽象出了 具体的 范式,下面就结合具体情景来看下看是否符合

下面 先分别定义 飞机1,飞机2 这两个观察者对象的 多个属性(地理位置 和 海拔)

var plan1 = {
      location: '北京',
      height: '200km'
}

var plan2 = {
      location: '云南',
      height: '100km'
}

再,分别定义 观察者 的行为(航站楼1,航站楼2 分别关注 飞机的运行状态)

var hangzhanlou1 = function hangzhanlou1(cb) {
      console.log('航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...')
      // 订阅的对象发生了变化 , 观察者 做一些自己想做的事...
      cb();
}

var hangzhanlou2 = function hangzhanlou2 (cb) {
      console.log('航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...')
      // 订阅的对象发生了变化 , 观察者 做一些自己想做的事...
     cb();
}

这里,航站楼1 可以 订阅 飞机1,也可以订阅 飞机2 ,也可以都订阅

同样,航站楼1 可以订阅 飞机的 位置,也可以订阅飞机的 海拔, 也可以都订阅

  • 现实层面上:飞机运行,发生状态的变化
  • Web层面上:一系列事件操作导致的状态变化

给 飞机1,飞机2的 地理位置,海拔高度 都 添加 观察者模式

observer.make(plan1);
observer.make(plan2);

image

image

  • 情景1:航站楼1 关注 飞机1
    image

添加 观察者 hangzhanlou1

plan1.addSubscriber(hangzhanlou1)

飞机1 经过飞行,判断 关注的信息是否发生了变化

这里判断是伪代码,但是这个判断的逻辑非常!非常!非常!的重要!如果有时间,这个后续会展开讲解,因为这里是另外一个核心内容,diff变化,从而控制View层的渲染!

 // 这里为了演示方便,默认判断 地理,海拔都变化了(其实也可以判断 观察者某一个 属性)
function checkChange(val, oldVal) {
    if (val !== oldVal) {
         plan1.publish(function(){
                console.log('飞机1 现在位置是上海,海拔是100km');
          })
    }
}

// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是上海,海拔是100km
  • 情景2:航站楼2 关注 飞机1
    image
    添加 观察者 hangzhanlou2
plan1.addSubscriber(hangzhanlou2)

同样判断状态的改变,进行发布

function checkChange(val, oldVal) {
    if (val !== oldVal) {
         plan1.publish(function(){
                console.log('飞机1 现在位置是太原,海拔是120km');
          })
    }
}

// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是太原,海拔是120km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是太原,海拔是120km
  • 情景3:航站楼1 关注 飞机2
    image

添加 观察者 hangzhanlou1

plan2.addSubscriber(hangzhanlou1)

同样判断状态的改变,进行发布

function checkChange1(val, oldVal) {
    if (val !== oldVal) {
         plan1.publish(function(){
                console.log('飞机1 现在位置是青海,海拔是300km');
          })
    }
}

// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km

function checkChange2(val, oldVal) {
    if (val !== oldVal) {
         plan2.publish(function(){
                console.log('飞机2 现在位置是西藏,海拔是500km');
          })
    }
}

// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机2 现在位置是西藏,海拔是500km
  • 情景4:航站楼2 关注 飞机2
    image

添加 观察者 hangzhanlou2

plan2.addSubscriber(hangzhanlou2)

同样判断状态的改变,进行发布

function checkChange1(val, oldVal) {
    if (val !== oldVal) {
         plan1.publish(function(){
                console.log('飞机1 现在位置是青海,海拔是300km');
          })
    }
}

// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km

function checkChange2(val, oldVal) {
    if (val !== oldVal) {
         plan2.publish(function(){
                console.log('飞机2 现在位置是西藏,海拔是500km');
          })
    }
}

// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机2 现在位置是西藏,海拔是500km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机2 现在位置是西藏,海拔是500km
  • 情景5:航站楼2 取消订阅 飞机1(因为飞机1已经 安全降落了)

image

在 飞机1 上移除 观察者 hangzhanlou2

plan1.removeSubscriber(hangzhanlou2)

最后

观察者模式大体就是如此,但是在开发中为了适应各种场景可能会有很多变种,但是万变不离其中。

上面的代码只是用来帮助理解的,针对 航班 与 航站楼 这种情景 还有很多改进的地方。

以上只是个人理解的一些拙见,如有不对之处还请海涵,并希望大家帮忙纠正!

拓展阅读

我的2021 | 我是如何在业务中披荆斩棘的

image

又到一年总结时,去年拖的有点晚,今年卡点。之前总结的是一年的感悟,这次弄点不一样的,可能是个系列。

Part 0: 自我介绍

大家好,我是某互联网企业的一名 Web 前端和技术推广工程师,带一二十人。

从 19 年跳槽后,来这家公司快 3 年了,期间经历了迷茫、自我否定,然后努力爬出来,做成一些事。特别感谢我的 leader 们,他们给予了我非常多的帮助。这种帮助不只是方法论,而是通过他们的言行实实在在的感染了我,让我认识到:如果想要做点不一样的,需要有魄力和担当。只有跳出舒适区,才能获得凤凰涅槃般的成长。

下面进入正题

Part 1: 开场暴击

当我抱着对新公司的美好憧憬入职时,第一天现实就给我来了个当头棒喝。业务 Leader 首先非常欣喜的欢迎了我,接下来就跟我描述了下团队现状,两个词总结:缺人、混乱。

简单、干脆,我喜欢:)

接着,第一周内,就有同学侧面表达了在这里干的不高兴,在考虑新机会。

这是大大的问好

团队中前端 2 人,后端 10 几人,产品 4 人,维护的平台和服务十几个

  • 堆积如山的需求,我来时做的是半年前评审完的需求
  • 前端、后端、PM 之前缺乏信任感
  • 没有研发流程

感受到了风雨欲来。。。

离职警报

果不其然,一周后有同学跟我说:已经在看新机会了,让我有个心理准备

哪来的心里准备

找他了解了下具体情况,想知道为啥态度如此坚决,TA说:自己来的这一年多了,一直在做需求,加班加点的做,但永远做不完,即使做完了也不到业务的认可,整体绩效也一般,呆着没啥意思。深入的问TA为做了这么多事啥得不到好绩效,有分析原因吗?TA的意思是:各方面都把 FE 当工具人。
工具人

沟通对抗

在业务周会中,只要有跟前端相关的需求 dealy,从后端、到 PM 的表述,出奇一致。话里话外都是:前端没开发完,所以 delay,怎么催都没用,感觉不上心。前端在会上也不吭声,只闷头看电脑。会后问同学为啥在会议上不把来龙去脉说清楚,同学说:没用,他们不相信,就这样吧。

已经破罐子破摔了

整个下来,感觉是掉进天坑了啊!导致入职那段时间非常抑郁

原因有二

  • 工作内容:工作和面试时沟通的有较大差距
  • 氛围:团队整个氛围压抑

坦诚的说,当时冒出的第一个想法跟很多人类似:风紧扯呼啊!

Part 2: 厘清问题

然而,很多事不是换个地方就能解决的,如果一个人的能力不提升,去哪都会面临相似的困局,抱着这个心态义无反顾的投入进了工作中

我可真是个工作狂

业务
我所在的业务属于中台性质,大部分场景中我们负责对内的平台建设。把中台的能力平台化,复用到各个业务场景中。这导致我们和B端、C端有着明显的差异。另外由于公司的架构理念,FE 更像是 BP 参与到业务中。再
虽然一再强调不是 BP
者,由于我们负责的业务在业界有一定门槛。所以,综合环境导致了前端在业务的融入、发展上的不利。

工作模式

  • 前端将 100% 的时间投入在需求上了
  • 没有严谨的评审,导致频繁的返工
  • 前端没有技术能力,导致经常会做些不合理的功能

合作情况

  • 需求评审中,前端几乎不说话,产品、后端说啥是啥
  • 执行中没有合作、商量的概念,缺乏沟通

个人成长

  • 不知道谁能真正的给予指导
  • 希望有喘息的时间思考技术

总结:先天不足、前端躺平

Part 3: 分析分析

虽然是综合性问题,但是还是要一个个分析,一件件解决。

归因

关于离职、对抗问题,经过一段时间的熟悉和沟通,进行归因梳理

  • 前端
    • 没成长
    • 业务压力大
    • 付出得不到回报
  • 业务
    • 前端不专业、不理解业务
    • 合作不顺畅
  • 团队
    • 空白

每个问题都是大而难的问题,以我当前的角色定位来看,很难解决。但不尝试解决的话,工作起来又难受,可谓

其实没啥角色定位,主要是我脸皮厚觉得要主动做些什么(才对得起工资?)
骑虎难下。由于问题非常棘手、复杂,决定以不破不立的态度来做。
真的是抱着干不好大不了离职,反正是一次绝佳的磨砺机会,不容错过

为什么搞得跟英勇就义一样呢?

因为在这样一面倒的大情势下,是个人都能发现不对劲。但为啥一直没人尝试解决呢?

好问题,读者可以回答下

拆解

第一步,找到了直属 Leader,向他说明发现的问题,以及可能会采用的一些解决方式。表明我那真切想做事的态度。

主动获取适当的授权,利于事半功倍

第二步,找到虚线 Leader,说明现状和持续恶化的后果,所以我会采取一些行动,并对齐大体的目标和计划。

你痛,我痛,一拍即合

第三步,跟团队内的同学说明当前情况,如果想要改变,希望大家齐心协力背水一战

团结,是克服困难的基础

做这些的目的很简单:把问题摆上台面,直面问题!

拆解看似简单,但里面包含了的团队、业务、个人考量,后面有机会再展开

Part 4: Do it

建立信任

信任,是良好的开端

  • 让前端同学对我有信任感
    • 通过在业务、技术点上给予指导,让他们相信我经验是比他们丰富的
    • 通过和同学们的换位思考,让他们相信我是理解他们的
    • 通过和 Leader 的沟通,把他们的事讲好,让 Leader get 到他们的优秀,得到实实在在的激励
  • 让业务对我有信任感
    • 主动贴近业务,熟悉业务情况,了解他们面临的问题
    • 给业务出谋划策,帮助他们解决问题、提供新的思路
    • 合理安排业务,提升准时交付率,让们可以交差

建立沟通机制

如果有什么误会,那一定是沟通出了问题

  • 在团队打造开放的沟通环境
    • 我相信在一个敌对、闭嘴的内部环境中,沟通是不可能通畅的,所以在内部我建立了定期沟通机制,让大家敢于发言、畅所欲言,针对大家提出的真实存在的问题着手解决
  • 与业务建立良好的沟通机制
    • 跟业务建立从领导层、业务层、前端层的多维度的沟通体系。让每个层面的沟通都能聚焦、高效,从而让大家互相了解和理解
  • 在同学和 Leader 之间做好桥梁的角色
    • 桥梁的角色非常重要。但是要做好桥梁的角色绝不只是信息透传,而是需要从 「Leader 想了解的」、「我想表达的」、「同学想说的」的角度去思考,不然只会增加无意义的噪音

平衡业务和技术

成年人的世界没有选择题,都想要

  • 探索业务和技术的结合点
    • 这个点不好做,但是必须要做。因为这能直接给业务带来实实在在的价值,证明前端技术是有业务价值的
  • 从技术出发,前瞻性的做技术储备
    • 鼓励大家做技术探索,从完善基建的道路上,探索产品级的技术产品,为可能遇到的挑战提供技术上的积累和解决方案

成为大家的依靠

让大家放心把后背交给你,才是最大的成功

  • 身先士卒
    • 在我司,一直提倡要放得下,也要拿得起,这点我很认同。于是我从 A 平台干到 B 平台,从 C 方向干到 D 方向,一直冲在探索的第一线。这也帮助我积累了非常多的合作关系和业务理解。
  • 同理心
    • 说再多的方法论,说再多的保持耐心,如果缺失了同理心,那所有的表达和建议都会显得苍白无力。只有真正的站在对方的角度出发思考问题,才能真正的 get 到问题出在哪,该采取何种措施应对能达到最好的效果。

Part 5: 成长与希望

可以遇见,虽然有计划、有实施、有推进、有合作,但是还是会遇到问题。因为每种改变都是一种巨大的努力。

这里我想重点说下同学们在心态、自信方面的成长。

心态

首先,发生变化的是同学们的心态。大家发现,他们是要跟着我一起冲的,但由于自身和外界等多种综合因素导致他们很难迈出那一步。

其实对这个问题,在我的意料之中。我说:我给你们打样,你们多揣摩学习,一回生二回熟。通过在周会、项目总结会上掷地有声的发声,大家发现把自己想表达的内容说出来并没有那么难。于是我又持续加火,鼓舞大家活跃起来,于是大家越来越真切的感受到,发声真的可以使工作上变得轻松、愉悦。

不是传销

这是非常关键的一步,就是坦诚清晰的直面自己的问题、团队的问题。

自信

其次,遇到了自信心的问题。发现大家在跟合作的研发聊业务、技术的时,总显得不自信,对方说啥事啥,没有反驳。这直接导致了严重的技术债。

对于这个问题,我想的方案是:在做需求的过程中手把手的指导。告诉他们可以将事情拆开看,分为业务和技术。在业务中,前端需要看需求的合理性、自洽性、可行性、价值是否可衡量等。在技术中,前端需要看技术实现的合理性、可行性、拓展性等。只有把每个点都梳理清楚了,才明白该在何时何地采取何种技术。

榜样的力量

随着反复的实操,大家的技术思考越来越全面,估时越来越准。于是同学们渐渐发现,原来我们也是可以主导项目走向的,后端、PM 原来也是想听我们的思考的。

至此,整体的节奏开始往良性循环上发展了。

Part 6: 阶段性总结

其实,整个过程是较为漫长,花了 1 年多时间,才将合作的氛围做了改变。

尤其在 2020 年终总结时,我从研发大群里收集了各方对于前端的服务评价,评分达 3.6 分(5分制)。

当看到这个分数后,大家不约而同的「哽咽」了。这一年我知道大家经历了什么,又得到、失去了什么。

经常跟同学开玩笑说,咱们团队 19 年是石器时代,20 年是青铜时代,21 年是白银时代。

大家对于这种改变更多是还是震惊,没想到成了?有句话让我记忆犹新:没想到跟着你真的把这件事干成了!

其实,我又何尝不幸运呢?在这里的每次失落与成功,都在推演验证着我心中的想法,使我不断成长。不断理解跨部门合作、跨团队合作的奥义。不断理解带好团队的深层含义,也不断优化自己的言行。

这时,对于公司的口号也有了更深的体会,大家不只是说说,是真的在践行准则。也真是因为这种践行,让团队、业务、公司变得越来越强壮,越来越有希望!

团队的成功少不了这种大氛围的支持

Part 7: 新的挑战

随着团队的蒸蒸日上,新的挑战不出所料的如期而至了。领导看我把前端和业务盘的不错,于是将业务领域进行了扩大。从之前 1 个方向,增加到了 4 个方向。这四个方向包括内部平台、B 端、C 端、游戏。

顿时压力山大,不过老话说:有压力,才有动力嘛!干就是了。在这段历程中又增长了对不同业务协同的理解,也更加认识到责任是什么。由于不是本文的主线,以后有机会再展开。

Part 8: 愈加坚韧

经历过这一切后,回首看时,认同很多问题的处理不是 0 和 1 的关系。以前的不成熟观点,随着不断碰撞、交流,变得更加成熟。有些「奇怪」的观点,也变得更容易接受了。

就像经历了洗礼一般,人和事,真诚和虚伪。有些没必要揭穿,有些不可容忍。唯一的方法是,上个台阶想问题,你就知道是该保持沉默还是勇敢发声。

最后,分享一句话:不要玻璃心,从现在开始,放下自己,勇于做对的事吧!

本问正参与掘2021总结年度征文 https://juejin.cn/post/7047790537646014495

北京是个大而空的城市

北京,很大
北京,很空。
钢铁铸就的城市,
它的心是冷的。
拥挤的人充塞的城市,
它的空气是凉的。

广场上,
一瓶酒,
人来人往,
扪心自问,
这丝丝凉意的城市,为何要继续待下去。

你说,
为了生活。
然而,
什么是生活?

你说,
为了钱。
然而,
你赚了多少?

你说,
为了爱情。
呵…
冰冷的爱情。
你说,为了爱情!
然而这城市,容纳不下爱情这事物。

于是你,
自欺欺人。
于是你,
迈不动脚步。
心里想着离去,
身体却依旧沉沦。

我的2022|你好,团队氛围,我们来聊聊

原文发布在「字节技术学院」,github 版本做了脱敏

本文共 2,771 字,预计花费 9 分钟


image


写在前面

作为一名研发,在多家公司待过,能明显的感觉到每个公司在团队氛围的理解与建设上有挺大差异,幸运的是我待过的团队氛围都不错,这在很大程度上帮我建立了好的团队氛围观。另外,随着团队的搭建逐渐深入,我们依旧难能可贵的保持了较好的团队氛围。相信这里面有些内容是值得挖掘的,于是通过分析与抽象,沉淀出我们的经历与思考,供大家交流和参考。


团队氛围是什么?

谈到这个话题,相信每个人都有自己的理解,可能是「待的舒不舒服」、「同事的性格」、「Leader的做事方式」等。在互联网搜寻也较难有确切的定义,因为不同的人立场不同,对团队氛围的定义和要求也不尽相同,但无论它怎么定义,它一定是团队能力的综合体现。

团队成员

  • 我期望的氛围是xx,而现在xx
  • 我希望氛围变得xx,这样就能xx

会发现从团队成员个人的视角做抽象,是较难描述的。这时只有跳脱出来从整体看,才能初窥它的容貌。所以,我更愿意给它这样下定义:团队氛围是团队成员工作与生活的土壤,它依赖团队中的人和事作为动作和要素主体,在这之上通过各种内外部交互产生出的「有机结合物」。通俗点说,团队氛围是通过成员的共同努力,一起打造出的生存环境。这种环境不是一层不变的,它可以不断更新。更新的动力来源于内部与外部的刺激。


团队氛围重要吗?

团队氛围既然是我们工作与生活的土壤,显然易见是非常重要的。但是它到底为什么重要?在团队繁杂的工作中,它的优先级处于怎样的位置?

❓ 为什么重要

如果做过招聘你会发现,十个候选人七个都会说团队氛围不太好,所以想要看新机会。(不要问我还有三个是啥情况,问,就回答你是行业团灭)虽然这些回答比较主观,但确实从另一个侧面证明了团队氛围的重要性,及大家对它是有较强诉求的。
在我们的日常工作中,团队氛围会跟空气一样是围绕着我们的,可能比需求还多 1 度达到 361 无死角。试想下如果连空气都腐坏了、污染了,成员该如何生存呢?轻则重伤(冷漠、对抗)、重则呜呼(转岗、离职),这两种结果对公司都是不可承受之重。

🎯 处于怎样的位置

在工作中,经常会遇到优先级的问题,我想「团队氛围」如果是有意识的,应该会顾影自怜,因为它日常被无视、日常被忽略、日常被冷落。我想读者应该明白我的态度了,我认为团队氛围需提高到战略高度,并处于 P0 级。所以,高优且持续的把团队氛围建设迭代下去,是团队走的更好、更远的另一条腿。对于一个公司亦然。


如何打造团队氛围?

在建设之前需要想清楚下面几件事:

  1. Q:建设中有哪些依赖?
    A:依赖 Leader 的牵引,依赖成员的配合
  2. Q:建设的执行人是谁?
    A:动作主体是团队 Leader,其次是团队成员
  3. Q:谁对建设结果负责?
    A:Leader 是主要责任人,成员是次要责任人,一起对结果负责
  4. Q:谁会享受到建设成果?
    A:团队中的每个人,及周围群体

🏃 保持耐心

英国作家塞缪尔·约翰逊 说过:伟大的工作,并不是用力量而是用耐心去完成的。
从我的实际经历出发,当初加入公司时,业务上的人只有两人,状态处于被业务追着打阶段。这时把氛围建设提高到 P0 显然是不现实且不被接受的。既然是战略,在这里采用润物细无声的方式更为合适。

随着人越来越多,在上个阶段的铺垫下,规划就可徐徐展开了,不断引导大家认知到团队氛围的重要性。我经常说的一句话是:团队不属于某个人,而是属于大家的。如果想让团队变好,需要我们一起努力。但是很显然,计划永远跟不上变化,这时一定不要烦躁、放弃。保持耐心跟它打持久战。不断更新想法和计划,不断试错、不断迭代、不断改进。相信在团队的磨合下,会找到属于它的性格。

对于团队氛围建设来说,耐心是不可或缺的品质。

❣️ 平等、自由、博爱

自法国大革命开始,“平等、自由、博爱”的口号就已深入人心。在团队氛围中它们同样重要,这也是衡量团队价值观的原则之一。如果团队不平等、不自由、不博爱,那就谈不上更多了。

关于平等

在工作中打造平等,同学们第一个可能想到的就是好绩效,大家都拿到一个不错的绩效,这样才是最平等的。其实这个想法有些粗暴且理想主义。平等不代表好绩效,都是好绩效更是不平等

那什么是平等呢?我觉得首先需要弱化职场中的等级概念,尽可能少的强调上下级关系。如果过于强调上下级,难免会给同学造成一种以级压人的感觉。相信这也是字节不提倡「老板」「Boss」之类称呼的起源,以「同学」相称体现「职级」平等。
然后,认可并给予大家追求更优秀自己的权利。这体现在了能做有挑战的事情,能拿到超额的收益,能得到更多的赏识和向上的机会。不因「裙带」、「喜恶」等私欲,人为的限制、打压同学的成长权利。让大家在做好基础工作的同时,做出超预期的产出和超越自身的成长。

关于自由

《论自由》说到:一个人的自由是不应该被干涉的,除非他危害到了别人的安全。
在团队中我提倡以善意假设、互相信任为前提,用底线思维圈定有序,从而创造自由空间。为了创造自由,我邀请大家参与团队方方面面的建设,在这里:你可以直接表达你的独特观点、可以针对任何事做有理有据的争论,共同打造一个活跃开放、坦诚谦虚、包容他人的团队 。我们不因「权威」而折腰、不因「潜规则」而屈服、不因「形式」而附和。提倡大家做一个有独立思考能力的现代人。

  • 比如加班问题,在工作中提倡大家关注效率和产出,而不是只关注工时
    • 白天高效的工作比磨蹭到深夜下班更值得别人尊敬
  • 比如会议问题,在周会上提倡把问题和价值说清楚,不用过于关注会议形式
    • 如果形式不为内容服务,那真的只剩下了形式
  • 比如工作和生活平衡问题,提倡工作的时候认真工作,休息的时候好好休息
    • 两者的平衡需要技术与艺术,如果你能平衡好,相信你就是生活的大师

关于博爱

人真的很难做到《月亮与六便士》里的斯特罗伊夫,能对敌人和自己不利的人保持慈爱。所以我更倾向于“博爱”指向的是法语中的“fraternity”,即友爱、兄弟之爱、手足之爱,因此该词具有强烈的共同体意识,强调的是人与人之间的关系。
回到团队中,虽然团队成员是一个临时的聚集体,具有很强的「欲望」与「功利」属性,但不可否认的是它是由一个个活生生的人组成的。作为团队 Leader 需要具备充沛的正向情感,通过互动与同学们建立感情连接,通过真情实意的感情流露,为成员间干涸的情感河道注入爱的生机,从而达到人人「相爱」,人人自爱的温馨氛围。即使是石头,相信长此以往也会被水滴石穿。


👫 建立友谊

古罗马的马库斯·图留斯·西塞罗 说过:世界上没有比友谊更美好、更令人愉快的东西了;没有友谊,世界仿佛失去了太阳。
团队成员可能是除了家人以外相处时间最长的人了,如果能建立起友谊,那将会身心愉悦、所向披靡。在工作中建立友谊最自然而然的方式莫过于一起 coding、一起 fix bug、一起 「argue」 pm&rd 了。
从入职那天开始,我们就投入到了风控的宏大建设中,一起在 Shark 中遭受历史的毒打、一起在 风险感知 中苦苦摸索、一起在 验证SDK 的 oncall 与事故中惴惴不安。从平台业务到数据业务,从数据业务到验证业务、从验证业务到ToB业务,无时无刻的不一起接受改变、接受挑战,并迎接突破与成长。

一路走来,看着当初的98少年,变成了大家口中的93大叔
一路走来,看着把boe发布到线上的飞翔少年,变成了独当一面的捕鱼达人
...
一路走来,一路的风景

感性的说:因为工作得以养家糊口,但更因这份工作,认识了大家,跟大家建立了信任与友谊,能跟大家并肩作战也感到很荣幸。


最后

可以看见,团队氛围建设是件极具挑战性的工作,它的成形、成长、壮大依赖 Leader 和团队成员的共同努力。作为团队 Leader 有时会遭遇到成员的疑问与不解,是有些孤独的。作为团队成员,需要放下对周围的过分戒备,真诚的融入到团队中。大家手牵手、肩并肩充满勇气的走上这条前期注定荆棘密布的道路,但是,路走着走着就宽了、平坦了,随之而来的是这土壤、这空气对我们无形的滋润与回馈。到这时,相信大家对团队氛围会有更深的理解。

H5页面中尝试调起APP

安卓版本5.0以上
IOS版本10.0以上
采用事件触发的方式处理唤醒APP

市面上常见的功能

这种功能现如今应该非常普遍了,淘宝H5,知乎H5等等。。。

图片名称 图片名称

点击后会调起APP或者打开下载页面或者直接进行下载。

但是我这里发现知乎的这个功能有点不一样

他的逻辑是先提示我是否打开手机中的知乎APP(浏览器的机制询问用户操作许可),然后接着又弹出下载的提示。

图片名称 图片名称

解决方案URL scheme

URL scheme的方式在IOS和安卓都支持,兼容性较好。

优先使用iframe的方式

伪代码如下:

const iframe = document.createElement('iframe');
iframe.src = 'URL scheme'; // URL scheme的方式跳转
iframe.style.display = 'none';
document.body.appendChild(iframe);

这时候如果在一切环境支持的情况下,就会唤醒APP了。

但是这是理想情况下,更多的是要做兼容处理这快逻辑。

有些系统会拦截iframe的src(这只是造成唤醒APP失败的其中一种原因),因为这个src属性是一个法外hacker,很多漏洞都是利用他造成的。

所以这时候就要判断调APP失败的情况了。

伪代码如下:

const timer = 1000;

setTimeout(function() {
        // 执行成功后移除iframe
	document.body.removeChild(iframe);

	//setTimeout小于2000通常认为是唤起APP失败 
	if (Date.now() - last < 2000) {
            
    	    // 执行失败函数
            // 这里需要考虑一下之前知乎遇到的那个问题(浏览器询问导致时间小于2S)

	} else {

           //  执行成功函数

        }
}, timer);

理解

  • 如果唤起成功,H5页面会被切换到后台,计时器就会延迟。即使用户再从app切换到H5页面,这个时间差必然也是大于2S的。

  • 如果唤起失败,定时器会准时执行(即使会有100ms的延迟也是够了),这时候必然是小于2S的。

在iframe被拦截的情况下,我们可以使用window.location.href = URL scheme来做兼容。

对手淘移动开发适配的实例思考

rem

从机型的实例来理解

机型: 小米5
- 物理像素(px): 1920 x 1080
- 独立像素(pt): 640 x 360
- dpr: 3
- 宽高比:16 : 9

机型: 红米note1s
- 物理像素(px): 1280 x 720
- 独立像素(pt): 640 x 360
- dpr: 2
- 宽高比: 16 : 9

问题:实现div的宽度在不同机型上面的兼容
代码:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>h5</title>
	<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport">
	<style>
	    html,body {
	     margin: 0;
	     padding: 0;
	    }
	</style>
</head>
<body>

<div style="width:200px; height: 30px; border: 1px solid red">
    zqz测试数据
</div>

</body>
<script type="text/javascript">
	alert('dpr:' + window.devicePixelRatio);
</script>
</html>                                                                              

我们想要div的宽度与屏幕的宽度相同,我们如果将div的宽度写死肯定有问题,我们先写个width=720px,效果如下:

很明显,两个手机全部都是右边超出了,你很定很好奇,物理像素不是720么?至少红米手机应该显示正确把?

这里就牵扯到 物理像素(px)和 设备像素(dp/pt) 和 dpr 等概念。

物理像素(physical pixel)
物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。正是这些设备像素的微小距离欺骗了我们肉眼看到的图像效果。

设备独立像素(density-independent pixel)
设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。

CSS像素
CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。

屏幕密度
屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。

设备像素比(device pixel ratio)
设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按下面的公式计算得到:

设备像素比 = 物理像素(px) / 设备独立像素(DP/PT)

更多手机信息见Device Metrics

如果想要宽度正常的话,我们只需要将宽度改成width=360px即可。因为这样的话

1个物理像素 = 1个设备独立像素

然而这么改肯定不现实,种类繁多

(其实这个里还有个问题,因为这两个手机的设备独立像素是一样的,所以无论写多少css像素,在这两个机型上的表现是一致的)

当然这也是因为在有限的可见区域内,提高了像素密度,显示就更清晰

  • 小米5的设备的独立像素为 640pt x 360pt ,而其dpr为 3 ,所以我们知道其物理像素为1920px x 1080px
  • 红米note的设备的独立像素为 640pt x 360pt ,但是其dpr为 2 ,所以我们知道其物理像素为1280px x 720px

在不同的屏幕上,CSS像素所呈现的物理尺寸是一致的,而不同的是CSS像素所对应的物理像素具数是不一致的。在普通屏幕下 1 个CSS像素对应 1 个物理像素,而在dpr为2的屏幕下, 1 个CSS像素对应的却是 4 个物理像素。

我们希望的是我们写一个固定数值,而计算出不同的px。

rem的出现,让我们这个想法得以实现。

针对上面红米和小米5的例子,我们可以这么设想:

1.在html中设置一个font-size是多少的情况下,div宽度设置多少rem,能够在 dpr=2 的手机中显示 360px 的宽度?
2.在html中设置一个font-size是多少的情况下,div宽度设置多少rem,能够在 dpr=3 的手机中显示 360px 的宽度?

手淘的适配方案的核心代码:

var dpr = window.devicePixelRatio;
var width = document.documentElement.getBoundingClientRect().width;
if (width / dpr > 540) {
    width = 540 * dpr;
}
var rem = width / 10;

为什么是 540? 见540怎么得来的

这样的话,对于上面的2个自问,已经得出了答案:

1.在html中设置一个font-size是 (360/10)36px 的情况下,div宽度设置 10rem ,能够在 dpr=2 的手机中显示 360px 的宽度
2.在html中设置一个font-size是 (360/10)36px 的情况下,div宽度设置 10rem ,能够在 dpr=3 的手机中显示 360px 的宽度

image
延伸:

  • 在dpr=1的iphone3中呢?
  • 在dpr=2的iphone6中呢?

获取参数:
iphone3

  • dpr : 1
  • dp : 320 x 480
  • px : 320 x 480

iphone6

  • dpr :2
  • dp :375 x 667
  • px : 750 x 1334
    同样:
1.在html中设置一个font-size是 (320/10) 32px 的情况下,div宽度设置 10rem ,能够在 dpr=1 的手机中显示 320px 的宽度
2.在html中设置一个font-size是 (375/10) 37.5px 的情况下,div宽度设置 10rem ,能够在 dpr=2 的手机中显示 360px 的宽度

所以,目的达到,无论我们的设备宽度是多少,只要按照这个规律,我们只需要设置10rem,必定会是屏幕的宽度。

参考:

Puppeteer开发调试的4个Tip

翻译自官方 debugging-tips

1.关闭无头模式 - 有时查看浏览器显示的内容很有用。替换无头模式,使用 headless: false 启动完整版本的浏览器

const browser = await puppeteer.launch({headless: false});

2.慢下来 - slowMo 选项会将 Puppeteer 操作浏览器减慢指定的毫秒数。 这是帮助看看发生了什么的另一种方法

const browser = await puppeteer.launch({
    headless: false,
    slowMo: 250 // 减慢 250 毫秒
});

3.捕获控制台输出 - 你能够监听控制台事件。在 page.evaluate() 中调试代码时这也很方便

page.on('console', msg => console.log('PAGE LOG:', msg.text()));

await page.evaluate(() => console.log(`url is ${location.href}`));

await page.evaluate(() => console.log(`url is ${location.href}`));

4.启用详细的日志 - 所有公共 API 调用和内部协议流量都将通过 puppeteer 命名空间下的调试模块进行记录

# 基础的详细日志
env DEBUG="puppeteer:*" node script.js

# 调试输出可以通过命名空间来启用/禁用
env DEBUG="puppeteer:*,-puppeteer:protocol" node script.js # everything BUT protocol messages
env DEBUG="puppeteer:session" node script.js # protocol session messages (protocol messages to targets)
env DEBUG="puppeteer:mouse,puppeteer:keyboard" node script.js # only Mouse and Keyboard API calls

# 协议通信时消息可能比较杂乱。 此示例就是过滤掉所有网络域的消息
env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'

我的2021 | 关于搭建团队的浅思

image

我的2021 | 作为前端我是如何在业务中披荆斩棘的 发布后有同学私信我是哪个公司哪个业务的,其实很多人都猜到了(我也没匿名😊)。由于整体内容不涉及公司敏感信息,主要为个人的所思所想,所以就明盘了。

发散聊聊

2019 年来字节风控的时候,这个业务有 2 个前端,都较为资浅。业务本身也处于摸索期,也顾不上前端,最大的希望就是前端别总 delay 就行。

好朴实的诉求

这就是他们把我放到风控的背景,至于把我放到风控的原因,猜测可能是我有带小团队的经验,死马当活马医了。不然在上层总会受到业务负责人的 Diss:说前端不重视风控...

领导也不好当

不过这些都是历史了,来的一年半时间里经常整宿整宿的失眠,一部分是因为业务压力非常大,一部分是因为同学眼巴巴的瞅着我,想要激励。还好我的 Leader 当时比较体谅我,没有给我太大的压力,也没给我什么定位,就是作为一个稍资深的同学来这边做业务,可能是想看我能翻起多大浪花吧。

有句话说的好:如果是璞玉自然能经得起雕琢,如果是废铜烂铁,怎么糊也不会变成金子。

其实说到失眠,主要还是我的自律、同理心导致的。在这个大背景下,业务的事只有一步步来,着急也没用。但我对自己要求很高、有各种精神洁癖。比如不对的事、不好的氛围,就想要把它扭转过来。由于白天一直被业务「追着打」,只有躺在那的时候有时间思考,结果一分析、一想大脑就又活跃了。

那时候我治疗失眠比较粗暴,一旦失眠就爬起来写代码,写到凌晨5-6点的时就坚持不住,然后就去睡觉。
事实证明这种方式是不可取的,身体、精神都会遭到摧残,希望读者不要效仿。

你可以说我有点欠儿~,我也觉得我有点欠儿,为啥会这么说?因为一开始大家也没默契、配合啥的,更多的是我热脸贴冷屁股,可能还能被崩着,很多人不能理解,这孩子脸够大的呀。但我乐此不疲,因为每个外部的动作、反应都在不断验证我的推演,让我逼近这难寻的真相。

是有点不自量力,但不重要

其实,我从字节范中受到了很多鼓励,有段时间作为精神支撑,要是没有它,可能就没有我现在的侃侃而谈了。字节范鼓励大家追求极致、务实敢为、开放谦逊、坦诚清晰、始终创业、多元兼容。这些非常对我胃口,翻译过来就是:做事情不要整有的没得,踏实、努力、有目标的去做比什么都重要。

相信做对的事比什么都重要

想喷字节范的别喷我,希望理性讨论 😂

现在回到本文的正题:搭建团队

团队管理前提是要有人,所以这是很多 leader / 预备 leader 重要且高优的事,只有把团队撑起来了,才能把业务撑起来,业务撑起来了,公司才能得到发展。

搭建团队

团队搭建是一个需要长期有耐心的、与业务紧密配合的工作,它是体现 Leader 在业务发展、合作协同、内外诉求理解上的集大成的产物

我个人比较反对在没思考清楚时,为了扩张团队、为了所谓的顶上去而招聘。这种招聘可以归为「垃圾行为」。

所以我在招聘上的原则是:

  • 招技能过硬的人
  • 招能跟团队一起成长的人
  • 能对招来的人负责

招能力过硬的人

这点在业务前期的发展上会起到胜负手的作用。如果招来的人业务、技术、沟通不行那无意义一场灾难。

能力过硬不只看专业技能,还要看软素质等综合实力,因为每个人都有自己的优势、不足。合理搭配人员、合理发挥出同学的优势都是有教无类的典范。

如何组合,这要看团队面临的问题了。下面举几个例子(不一定恰当):

  • 如果团队缺业务、技术攻坚的同学,可以用「专业技能强」+「软素质弱」的组合
  • 如果团队只缺能干活的人,可以用「专业技能还行」+「软素质还行」的组合
  • 如果团队缺有带人经验的同学,可以用「专业技能还行」+「软素质强」的组合

招能跟团队一起成长的人

关于这点,深有体会。有的候选人关注业务是不是核心,有的关注团队加不加班的,有的关注有无坑位,这都可以理解,毕竟靠画大饼是不能长治久安的

但从事物发展的角度看,每个业务都有低谷、高潮,如果只能同甘、不能共苦,那这类人大概率是不受喜欢的。

如果有能跟团队、业务一起成长的同学,请好好珍惜。如果能力到了,该给的激励要给足,不要寒了人心。

其实,这里会遇到一个短期、长期、激励平衡的问题。

  • 如果一个业务短期困难,激励不足。这是体现同学「能否同甘共苦」的最好时机。
    • 这类同学需重点关注,是有耐心的人
  • 如果一个业务长期困难,激励不足。这是体现同学「靠爱发电」的时机
    • 这类同学需重点关注,是有非常好耐心的人,能坚持做自己认准的事,潜力无限!

所以用好激励,在关键时候可以给业务续命。

能对招的人负责

这点比较明确,就是要关心同学的成长,引导同学学会自主成长,关键的节点适当提供助力。而不是招来后直接当工具人,怼在业务上就完事了。

其实,这点非常考验 Leader 的能力,当团队人数一旦超过 4-5 个后 Leader 的精力一般就 Cover 不住了。因为要求同时能跟 4-5 个人产生强关联是件非常折磨人的事。但这不是推诿的借口。从更高维度看,有问题必然有解决方案。比如这时合理的搭建团队梯度,对大家都好,但这里又遇到了新问题,把权力下放这是很多低格局 Leader 做不到的。

有了原则后,如果没有简历,那也是巧妇难为无米之炊。下面就聊聊给招聘提效的事。

给招聘提效

先付出,再寻求帮助

招聘是一件费时、费钱的事。我的策略是勾搭同事+开招聘会员,每天花至少 1 小时跟候选人沟通、保温。

虽然花了大量的精力、金钱。还是会遇到伤心的事,比如:今天勾搭的候选人,明天就投了其他公司。今天接 offer 的人,明天入职了其他公司。所以招聘非常需要耐心。

如果这时,你还是一无所获。建议你寻求 HR 、Leader 的帮助,他们那有大量的资源、门路。可以帮你做提效。

筛选出好简历

这点需要经验,但非常重要,这个会直接关系到面试工作量、入职率。

Review 了几千份简历,发现优秀简历的几个共性(仅供参考):

  • 简历简约而不简单,有重点、不花眼
  • 能 Cover 住复杂业务,并做的有亮点
  • 能平衡好业务、技术的投入
  • 加分项:带人、高绩效、从 0 到 1

面试了上百候选人,发现优秀候选人的几个共性(仅供参考):

  • 有一套自己的思维逻辑
  • 眼光长远
  • 沟通能力强

通过关键词+经验的判断,大概率不会出错。

多主动对齐逻辑

招聘是一件严肃的事,绝不是拿来炫耀的。所以在招聘动作前,思考清楚需要什么样的人负责什么事,是非常重要的。所以多跟 Leader 对齐招聘逻辑是很有必要的,Leader 会从他的视角给我们查漏补缺。提升整体的招聘效率。

最后

过往的经验证明,付出总是有收获的。现在团队人数稳步增长,很好的支撑了业务。晋升、涨薪的同学也越来越多,相对欣慰。

初识createDocumentFragment

最近在看Vue1.x源码的时候,发现一个方法document.createDocumentFragment(),所以花点时间了解一下。
image

createDocumentFragment介绍

createDocumentFragment 用来创建 空的文档片段节点,在插入 dom 的时候 插入的只有其 子孙节点,其本身不插入,也就不占用资源了。

createDocumentFragment使用

let frame = document.createDocumentFragment();

image

let pEle = document.createElement('p');
let textEle = document.createTextNode('这是一段测试数据zqz');

image

createDocumentFragment的性能

关于性能的探索,建议参看这篇文章document.createDocumentFragment的探索之旅(上)(由于原文链接失效,请访问:http://ju.outofmemory.cn/entry/249677)

拓展

当然除了 createDocumentFragment 可以动态创建 dom 节点,还有 createElement 也可以。

这里有篇关于各种动态渲染Element方式 的性能探究 几乎感觉把所有的都列举出来,并做了比较。

最后

其实在Vue2.3.2版本中,createDocumentFragment 已经被 createElement 取代了。

参考

喜爱**古代诗词同学不可错过的小说

这篇文章就是向大家推荐一下我最近看的小说《儒道至圣》
说实话,这是我第一次想要写”小说读后感”。因为看小说的目的是为了放松,如果能额外的学到一点学问,那就更好了。
虽然不能说我看了N多篇小说,但是上百本还是有的。还记得高中第一次看除了古龙、金庸写的,由凤歌写的《昆仑》时的那种激动,感觉现代的武侠小说,很多小说的故事、构思和文笔也是极棒的。
而《儒道至圣》就属于这里面比较精彩的。吸引我的是它的奇妙构思和大量古诗词。

小说介绍:

这是一个读书人掌握天地之力的世界。
才气在身,诗可杀敌,词能灭军,文章安天下。
秀才提笔,纸上谈兵;举人杀敌,出口成章;进士一怒,唇枪舌剑。
圣人驾临,口诛笔伐,可诛人,可判天子无道,以一敌国。
此时,圣院把持文位,国君掌官位,十国相争,蛮族虎视,群妖作乱。
此时,无唐诗大兴,无宋词鼎盛,无创新文章,百年无新圣。
一个默默无闻的寒门子弟,被人砸破头后,挟传世诗词,书惊圣文章,踏上至圣之路。

升级与官名的结合

以往我们看(东方)玄幻/修真类的小说都是“练气金丹元婴大乘飞升度劫”这个升级套路,但是这本小说将升级与**古代的官名结合了起来:

  • 大学士
  • 翰林
  • 进士
  • 秀才
  • 童生

从高到低,依次表明进阶

文位,文胆,文宫的塑造

文中的每个人物都是读书人,读书人通过科举制度进入官场。一旦科举通过并被录取且获得一定的名次,不仅获得官位,还会获得响应的文位。每个文位,都会增强读书人的身体素质,提升读书人的灵敏度。而且一旦获得文位,就会形成文胆和文宫。这里的文胆主要是读书人的浩然之气,刚正不阿,不向强权低头的文人风骨。而文宫有点像读书人的庙堂,里面存储着读书人的才气,随着升级,文宫才气也会增长,才气会成为读书人的主要力量来源。

诗词成为武器

小说嘛,升级打怪是免不了的。与一般的小说中随便取个武功招式并赋予该武功招式相应的能力不同的是,《儒道至圣》中读书人的硬件武器是文房四宝,软武器是诗词,通过书写诗词来获取相应的能力。比如想要阻挡敌人,通过书写《题西林壁》(小说中称《庐山局》)来形成万丈高峰,将敌人困在庐山中,形成的能力与诗词内容高度匹配,所以诗词武器也分成了很多种:

  • 阻敌诗(《题西林壁》)
  • 刺客诗(《送荆轲》)
  • 壮行诗(《常武》)
  • 杀敌诗(《塞下曲·林暗草惊风》)
  • 等等

并且在每句诗词后面都会带上相应的符合小说剧情场景的符合原作的翻译,当真是边看小说边学习诗词。这一点也是最吸引我的地方!

好了,不多说了,我要去看下一章的“诗词”了。😄

基于webpack2.x的vue2.x的多页面站点

vue多页面模版

vue的多页面

依旧使用vue-cli来初始化我们的项目

然后修改主要目录结构如下:

├── build
│   ├── build.js
│   ├── check-versions.js
│   ├── dev-client.js
│   ├── dev-server.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── src
│   ├── pages
│   │   ├── boys
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   └── index.vue
│   │   ├── goods
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   └── index.vue
│   │   ├── index
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   └── index.vue
│   │   └── sotho
│   │       ├── index.html
│   │       ├── index.js
│   │       └── index.vue

编写每个页面

可以看到这里我们有4个单独的页面,分别是boys,goods,index,sotho

首先看boys文件夹中的代码:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vue3</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

这个是我们要单独生成的页面,最后也是生成index.html

index.vue

<style scoped>
  .boys {
    background-color: red;
  }
</style>
<template>

  <div id="app" class="boys">
    boys got many things.
  </div>

</template>
<script>

export default {
  name: 'boys'
}

</script>

这是我们的vue文件,可以看成一个组件,其实.vue文件你可以看成一个语法糖,最终会被vue-loader编译成js,生成对应的css,js,dom

index.js

import Vue from 'vue'
import Index from './index.vue'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  template: '<Index/>',
  components: { Index }
})

这就是主要文件了,这里执行vue的实例化,用法同在浏览器端页面中直接引入vue.js文件一样的含义

其他几个页面一样也是同理,具体可以见:

修改webpack.config.js

由于vue中的配置使用了模块化管理,所以我们需要修改下面两个文件:

  • webpack.base.conf.js

我们需要修改webpack.base.conf.js的入口entry,这是配置多入口文件的重点!
如果不懂多入口含义的化,建议去看下webpack的文档。

webpack.base.conf.js

...

module.exports = {
  entry: {
    'pages/boys/index': './src/pages/boys/index.js', //配置boys页面入口
    'pages/goods/index': './src/pages/goods/index.js', //配置goods页面入口
    'pages/index/index': './src/pages/index/index.js', //配置index页面入口
    'pages/sotho/index': './src/pages/sotho/index.js', //配置sotho页面入口
  },
...
  • webpack.dev.conf.js

这里我们需要修改plugins,它是个强大的即插即用的拓展。

我们使用html-webpack-plugin来生成我们的对于的页面。

...
var HtmlWebpackPlugin = require('html-webpack-plugin')
...

...

module.exports = merge(baseWebpackConfig, {
  ...
  plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),
     new HtmlWebpackPlugin({
      filename:'./pages/boys/index.html', //指定生成的html存放路径
      template:'./src/pages/boys/index.html', //指定html模板路径
      inject: true, //是否将js等注入页面,以及指定注入的位置'head'或'body'
      chunks:['pages/boys/index'] //需要引入的chunk(模块资源),不配置就会引入所有页面的资源(js/css),这是个很重要的属性,你可以不配置试试效果
    }),
    new HtmlWebpackPlugin({
      filename:'./pages/goods/index.html',
      template:'./src/pages/goods/index.html',
      inject: true,
      chunks:['pages/goods/index']
    }),
    new HtmlWebpackPlugin({
      filename:'./pages/index/index.html', 
      template:'./src/pages/index/index.html',
      inject: true,
      chunks:['pages/index/index']
    }),
    new HtmlWebpackPlugin({
      filename:'./pages/sotho/index.html',
      template:'./src/pages/sotho/index.html',
      inject: true,
      chunks:['pages/sotho/index']
    }),
   ...
  ]
})

以上就是我们进行多页开发的主要配置项。

开发环境访问页面

运行npm run dev,我们看下怎么访问我们的多页vue应用。

再来看下我们的dom结构是什么样:

页面里的js就是我们通过插件注入的,并且我们是通过指定chunks完成。

build

运行npm run build

➜  vue2-x-multiple git:(master) ✗ npm run build

> [email protected] build /study/vue2-x-multiple
> node build/build.js

⠋ building for production...
Starting to optimize CSS...
Processing static/css/pages/boys/index.19ebbc80a1c187989dbf02d03192e84e.css...
Processing static/css/pages/goods/index.fe8f1bc39f33dce4c4d610c2326482c6.css...
Processing static/css/pages/index/index.f6340f14071a89cf2b092da280ffaf8c.css...
Processing static/css/pages/sotho/index.7415ffd3ef7b9d1a4398cba49927b12b.css...
Processed static/css/pages/boys/index.19ebbc80a1c187989dbf02d03192e84e.css, before: 114, after: 44, ratio: 38.6%
Processed static/css/pages/goods/index.fe8f1bc39f33dce4c4d610c2326482c6.css, before: 116, after: 46, ratio: 39.66%
Processed static/css/pages/index/index.f6340f14071a89cf2b092da280ffaf8c.css, before: 92, after: 22, ratio: 23.91%
Processed static/css/pages/sotho/index.7415ffd3ef7b9d1a4398cba49927b12b.css, before: 92, after: 22, ratio: 23.91%
Hash: 2467c91090ccf4690865
Version: webpack 2.5.1
Time: 6319ms
                                                                Asset       Size  Chunks             Chunk Names
static/css/pages/sotho/index.7415ffd3ef7b9d1a4398cba49927b12b.css.map  312 bytes       1  [emitted]  pages/sotho/index
                             static/js/vendor.d7548891d04d4f883b29.js    83.2 kB       0  [emitted]  vendor
                  static/js/pages/index/index.b2ce74f4155fb942a064.js  671 bytes       2  [emitted]  pages/index/index
                  static/js/pages/goods/index.7d0dda2791db2d3b1500.js  702 bytes       3  [emitted]  pages/goods/index
                   static/js/pages/boys/index.2c268b75ba9424211d79.js  699 bytes       4  [emitted]  pages/boys/index
                           static/js/manifest.f466ccb58b3271558be5.js    1.57 kB       5  [emitted]  manifest
     static/css/pages/boys/index.19ebbc80a1c187989dbf02d03192e84e.css   44 bytes       4  [emitted]  pages/boys/index
    static/css/pages/goods/index.fe8f1bc39f33dce4c4d610c2326482c6.css   46 bytes       3  [emitted]  pages/goods/index
    static/css/pages/index/index.f6340f14071a89cf2b092da280ffaf8c.css   22 bytes       2  [emitted]  pages/index/index
    static/css/pages/sotho/index.7415ffd3ef7b9d1a4398cba49927b12b.css   22 bytes       1  [emitted]  pages/sotho/index
                         static/js/vendor.d7548891d04d4f883b29.js.map     687 kB       0  [emitted]  vendor
              static/js/pages/sotho/index.e706490d7c42ad8e4f73.js.map    5.55 kB       1  [emitted]  pages/sotho/index
                  static/js/pages/sotho/index.e706490d7c42ad8e4f73.js  674 bytes       1  [emitted]  pages/sotho/index
              static/js/pages/index/index.b2ce74f4155fb942a064.js.map    5.55 kB       2  [emitted]  pages/index/index
static/css/pages/index/index.f6340f14071a89cf2b092da280ffaf8c.css.map  312 bytes       2  [emitted]  pages/index/index
              static/js/pages/goods/index.7d0dda2791db2d3b1500.js.map    5.64 kB       3  [emitted]  pages/goods/index
static/css/pages/goods/index.fe8f1bc39f33dce4c4d610c2326482c6.css.map  338 bytes       3  [emitted]  pages/goods/index
               static/js/pages/boys/index.2c268b75ba9424211d79.js.map    5.62 kB       4  [emitted]  pages/boys/index
 static/css/pages/boys/index.19ebbc80a1c187989dbf02d03192e84e.css.map  333 bytes       4  [emitted]  pages/boys/index
                       static/js/manifest.f466ccb58b3271558be5.js.map    14.6 kB       5  [emitted]  manifest
                                              ./pages/boys/index.html  386 bytes          [emitted]
                                             ./pages/goods/index.html  389 bytes          [emitted]
                                             ./pages/index/index.html  389 bytes          [emitted]
                                             ./pages/sotho/index.html  389 bytes          [emitted]

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

进入dist目录,查看生成的页面

├── pages
│   ├── boys
│   │   └── index.html
│   ├── goods
│   │   └── index.html
│   ├── index
│   │   └── index.html
│   └── sotho
│       └── index.html
└── static
    ├── css
    │   └── pages
    │       ├── boys
    │       │   ├── index.19ebbc80a1c187989dbf02d03192e84e.css
    │       │   └── index.19ebbc80a1c187989dbf02d03192e84e.css.map
    │       ├── goods
    │       │   ├── index.fe8f1bc39f33dce4c4d610c2326482c6.css
    │       │   └── index.fe8f1bc39f33dce4c4d610c2326482c6.css.map
    │       ├── index
    │       │   ├── index.f6340f14071a89cf2b092da280ffaf8c.css
    │       │   └── index.f6340f14071a89cf2b092da280ffaf8c.css.map
    │       └── sotho
    │           ├── index.7415ffd3ef7b9d1a4398cba49927b12b.css
    │           └── index.7415ffd3ef7b9d1a4398cba49927b12b.css.map
    └── js
        ├── manifest.f466ccb58b3271558be5.js
        ├── manifest.f466ccb58b3271558be5.js.map
        ├── pages
        │   ├── boys
        │   │   ├── index.2c268b75ba9424211d79.js
        │   │   └── index.2c268b75ba9424211d79.js.map
        │   ├── goods
        │   │   ├── index.7d0dda2791db2d3b1500.js
        │   │   └── index.7d0dda2791db2d3b1500.js.map
        │   ├── index
        │   │   ├── index.b2ce74f4155fb942a064.js
        │   │   └── index.b2ce74f4155fb942a064.js.map
        │   └── sotho
        │       ├── index.e706490d7c42ad8e4f73.js
        │       └── index.e706490d7c42ad8e4f73.js.map
        ├── vendor.d7548891d04d4f883b29.js
        └── vendor.d7548891d04d4f883b29.js.map

到此为止,一个简单的基于vue2.x的多页应用完成了。

升级

webpack.base.conf.js中的entry入口都是手工写入,如果页面多的话这样肯定有问题。

所以我们通过如下的方式来自动完成这段代码:

var entryJS = glob.sync('./src/pages/**/*.js').reduce(function (prev, curr) {
    prev[curr.slice(6, -3)] = curr;
    return prev;
}, {});

这里的'./src/pages/**/*.js'我们按照一定的规则去匹配我们的入口j s即可。

生成的的是:

{ 'pages/boys/index': './src/pages/boys/index.js',
  'pages/goods/index': './src/pages/goods/index.js',
  'pages/index/index': './src/pages/index/index.js',
  'pages/sotho/index': './src/pages/sotho/index.js' }

与我们想要的行为一致

同样我们也升级下我们的webpack.dev.conf.js中的plugins

var htmls = glob.sync('./src/pages/**/*.html').map(function (item) {
    return new HtmlWebpackPlugin({
        filename: './' + item.slice(6),
        template: item,
        inject: true,
        chunks:[item.slice(2, -5)]
    });
});

module.exports = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: '#cheap-module-eval-source-map',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new FriendlyErrorsPlugin()
  ].concat(htmls)
})

生成的是:

HtmlWebpackPlugin {
  options:
   { template: './src/pages/boys/index.html',
     filename: './pages/boys/index.html',
     hash: false,
     inject: true,
     compile: true,
     favicon: false,
     minify: false,
     cache: true,
     showErrors: true,
     chunks: [ 'pages/boys/index' ],
     excludeChunks: [],
     title: 'Webpack App',
     xhtml: false } }

Open Graph Protocol(开放内容协议)

最近在整理公司hexo博客的时候突然发现在页面 head 里面有一个这个奇怪的 meta
image

Open Graph Protocol(开放内容协议)

开放内容协议一种新的HTTP头部标记,即这种协议可以让网页成为一个“富媒体对象”。

用了Meta Property=og标签,就是你同意了网页内容可以被其他社会化网站引用等。

说白了,这个属性的加入能让用户的页面内容能正确的分享到 SNS 网站,这样 网页内容的传播推广 就更加有力。

如何使用

<!--类型-->
<meta property=”og:type” content=”blog”/>
<!--标题-->
<meta property=”og:title” content=”Open Graph Protocol(开放内容协议)”/>
<!--图片-->
<meta property=”og:image” content=”https://avatars3.githubusercontent.com/u/17929687?s=88&v=4″/>
<!--链接-->
<meta property=”og:url” content=”http://v.youku.com/v_show/id_XMzE3NzY1NTAyOA==.html?spm=a2hww.20027244.m_250379.5~1~3~A”/>
<!--视频链接-->
<meta property=”og:videosrc” content=””/>
<!--宽-->
<meta property=”og:width” content=”300″ />
<!--高-->
<meta property=”og:height” content=”600″ />

每个标签可以多次重复使用

参考

JavaScript中的加号运算符趣事

加号运算符(+)在JavaScript中无处不再,但是就是因为它太常用以至于我们忽略了它。

加号运算符的两种含义

  • 用在数字上,就是相加
  • 用在字符串上,就是连接

这个应该很简单,我们来试试

数字相加

var a = 3;
var b = 6;
var c = a + b; //=> 9

字符串相加

var a = 3;
var b = '6';
var c = a + b; //=> '36'

这个结果理所应当,但是不是我们想要的。
这里的+号被解释成了连接符号。我们只需要这样即可:c = a + b*1,这样就解释成了运算符加号

连接与运算的优先级

这个问题不可避免,在我们开发中经常遇到。
先来看看下面这个例子:

var money1 = 2000;
var money2 = 3000;
var total = '2个月一共存了' + money1 + money2 + '元' ; 

我想这个结果应该可以预料到: 2个月一共存了20003000元
要是现实中真是这样运算,我们岂不是发了。
出现这种情况的原因是:

  • '2个月一共存了' 遇到 money1 时,处理成了'2个月一共存了2000'
  • '2个月一共存了2000' 遇到 money2时,处理成了 '2个月一共存了20003000'
  • ...

结论是:连接比相加优先级高

我们这个问题解决起来也很简单: var total = '2个月一共存了' + (money1 + money2) + '元' ; 通过括弧改变运算优先级。

我的2016

从个人站点迁移过来的2016总结

整体感受

今年的时间感觉过的飞快,转眼间就到了 2016 年的年底了。回想起来今年的经历,那真是可以说一直在挑战者未知,无比充实。

工作与生活

2016 年的职业生涯:离开了养老的地方武汉华为研究所,来到了一家500多人的上市公司做前端开发。
2016 年接触的新技术: Nodejs, NW.js, Electron, React, Webpack, Fis3, Vue, 单元测试, 持续集成
从四月份开始接触 nw.js,并用 nw 做了一个简单的桌面应用 scanDeskImg_boxed.exe,其实做这个也是为了偷懒,因为以前每次写博客都喜欢截图(虽然现在觉得有点lowlow的),所以截的桌面乱七八糟,然后又懒得整理,有了这个后每次直接双击运行,会直接将本地文件按照日期归类到对应的文件夹中。具体的情况我在博客园有相关文章:Node-Webkit打包
其实当时选择 nw.js 的时候还选择了网易的有道云框架(因为一直在使用有道云),后来用了一下感觉不放心,于是转向了 nw.js。随后 Electron 有点小热,就开始投入 V8 的全家桶了。并写了当时属于较早接触 electron 的博客文章。(对着youtube听着英语学真心累)。

六月份因为公司业务需要开发一个后台,因为之前公司所有的后台是用 backbone 来开发的,所以这次想用比较新的技术来开发。于是自发的选择了 React 并配合蚂蚁金服的Ant Design这套UI组件,使用Webpack 来进行构建单页应用来开发,我应该属于公司第二个##单独作战##使用React开发的,第一个人是个PHP高级开发工程师。这个后台的初始代码托管在我的github上(公司项目,涉及到计费名所以只是一个最初版本,后来的版本使用了公司自己的版本管理流程)。

随着项目的开发结束,接着就开始通过使用Fis3(公司建议大家使用 fis3 开发,原因是比国外文档看起来简单)来构建公司大部分计费名代码,并将原来乱糟糟的代码进行了重构,添加了部分构建。个人很赞同他的三个准则的。体现出了前端构建的三个基本功能述求,关于这部分我会随后写出具体项目中的使用文章,现在只有一篇 fis3的前端模块化之路[基础篇]

然后接到上级通知,说需要做一个导航站wb123。出现了各种无法预料的事情。从设计给了我psd图后,就没有然后了。从psd图说起,因为是导航站,所以很多设计自己做的图标还有素材,于是我用ps抠了102个素材。当我做玩前端页面后,等着后台接口给我数据。结果因为之前定的后端,最近太忙没时间做。没办法只有硬着头皮上.于是自己重新进行项目的框架设计,需求调查,数据库处理,后台服务,到调度,边摸索边使用NodeJs完成了一套项目框架。当然这还没完,因为运维没有用过NodeJS,自己又边摸索边学,将项目分了测试,待上线,生成环境三个环境来进行部署。并教运维项目部署与使用NodeJs相关的工具,这个导航站上周的PV是100多万。随后的一个二级域名的mini页也是使用这种方式并上线运行。
最近开发的一个后台管理系统也是这位,跟"煎饼果子"一样打一套。从项目需求设计到前端到后台导运维部署,一个人搞定全部。(其实我的内心是不愿意这样的)。

空闲之余(规定每天晚上还有周末必须学习),很好奇微信公众号是怎么弄得,于是又捣鼓起了微信的公众号开发,具体的代码托管在gitub微信公众号,完成了一些基本的功能,使用了一点WeUI开发界面。项目之前是放在新浪的SAE的,但是由于欠费,被收回了。

也捣鼓了一下 Vue.js,刚好当时室友说怀念大学的时光,于是注册了一个 118boy.top 域名,将网站搭建了起来。代码托管在 github118boys

至于持续集成与单元测试也是因为想学习,所以从开发一个npm包到持续集成到mocha的测试。node-cnblogs-spider 

这里需要特别的感谢 王勇 一个java开发老司机,遇到不懂的后端问题我都会去征求一下他的意见,虽然语言不一样,但是原理是差不多的。

突然发现自己的知识因为在这里的各种挑战而无限膨胀。有时候感觉特别的累,遇到不懂得只有晚上搞到一两点。周围没有人交流,唯一可以交流的只有QQ群,只有自己一个人在奋战有点孤独,当然这样的成就感也是满满的。

这里我越发对新知识有点感触:有时候会用新东西照着教程"抄一遍"写个出个demo,就觉得会了的话,那真是滑稽之谈,一定要经过实战项目,才能搞得清楚里面的细小的点。

感触:其实当你接触的东西越多,你越发现你不懂的东西还有很多。

家庭

因为14年毕业,家里还欠着学费的钱。所以14-15一年都在还贷款。当初快毕业的时候太天真了,想着每年带着父母玩两个地方。结果以失败收场。还好,今年稍微攒了点钱。五一请假带上父母游玩黄山。元旦带着父母游玩武汉,虽然都是穷游,觉得我终于能为父母做了点什么了,我长大了。同时,我也践行了我心里的承诺,觉得很高兴。2017年,同样要带着父母散散心,他们一辈子为我操心了,趁现在还能走得动,吃得香的时候,出来看看祖国的大好山河。

情感

女朋友2017年毕业,工作也早早的找好了。都很好。

结尾

这就是我的2016。平淡,真实。

我的2017

wechatimg54

转瞬已到 2018,趁着还没过年,仔细回想一下 2017 我的收获和成长。

2017 年由于个人感情原因和自己想要出去看看的想法,来了北京 —— 这个让人又爱又恨的城市。说实话,当时我是拒绝的,有下面几个原因:

  • 环境差
  • 压力大
  • 房租贵
  • 离家远

我是南方人,怕去了北方那被雾霾环绕的"尘"世无法适应,之前一直觉得去北京就是无奈的选择,还有北京那令人望而生畏的房价。武汉一室一厅才 1500 左右,北京却要 4000 左右,感觉生活成本涨了好几倍。还有北京那人潮拥挤的地铁,每个人都忙忙碌碌,每天都充满着令人窒息的感觉。

然而,我还是来了。

我记得来北京的时候是过完年,大约是二月十几号。第一个感觉就是北京的空气并不是那么差,有时候天还是很蓝的。首要面临的问题就是如何在北京生活下去,找工作就成了当务之急。还好由于个人专业还可以并且碰上了几个和蔼的面试官,来北京一周拿到了美团 offer。

于是,2017 的新篇章开启了。

工作

纵情向前

变化最明显的应该是前端团队了,从之前的几十人的团队进入了 100 多人的团队。整个接触的面就打开了,从业务到技术的人才比比皆是。充满了年轻活力的气息!业务形式是我没接触过的,技术的深度是我之前没思考到的。在这里,各种技术分享,业务分享,跨公司分享,让那些技术动了起来。跨部门的多业务沟通,跨部门的多问题处理等这些繁杂的问题,使我懂得了如果只是做好自己手头的事其实是不合格的,要勇于肩负起相关责任并完善的处理好相关事务才是优秀的综合素质体现。很感谢美团教会了我很多!

家庭

谁言寸草心,报得三春晖

收入的明显增长,使得我对于实践承诺有了一定的底气。元旦的时候,把父母接到北京逛了逛,虽然妈妈一直唠叨说我又乱花钱,路费这么贵等等。但是看着他们逛***,故宫,鸟巢,水立方的时脸上洋溢着的激动表情,心里感到无比踏实和自豪。虽然有些人觉得这些不算什么,但是对于我来说这是一种具有特殊意义的使命。

读书

当才华撑不起梦想的时候,应该多读书多学习

虽然在美团的这一年由于业务很忙且住的距离远,经常回家比较晚。但是还是告诫自己要多读书多学习。

  • 文学散文:读完《不过一碗人间烟火》(越看越饿)《千年一叹》(叩问生命的思考)《容忍与舒适》(胡适散文集)《素描的诀窍》(为了画二次元萌妹子)
  • 科普:断断续续把《时间简史》看到 [虫洞和时间旅行](真的很烧脑,值得一看)
  • 技术:读完《ppk谈javaScript》《移动web手册》《图解HTTP》《JavaScript函数式编程》,《你不知道的JS》阅读中。

一些新的尝试

  • 积极参与开源库的建设和MDN的文档翻译
  • 喝白酒(建议找北方女孩的男生应该要多练习这个技能 😄)

想法

读研读的是什么?

  • 对于我来说,在考研的过程中,在室友的不理解和恶劣的宿舍环境中,我学到了如何自律,如何主动学习。
  • 当初我虽然调剂上了,但是由于学校和专业的问题,我主动放弃了读研。在这个挣扎的过程中,我学到了一个人一定要清楚的知道自己要什么,而不是为了读而读。

在"大厂"就觉得自己厉害?
我的想法是,这是幻觉。有些小公司出来的人都比你厉害,这是事实。“百舸争流,不进则退”。

最后

我的2017大体就是这样平淡且真实。

2018我有几个愿望:

  • 希望父母平平安安,健健康康
  • 希望能跟女友确定终身并买房
  • 希望大家"百尺竿头,更进一步!"

[翻译] tween.js 中文使用指南

tween.js 英文使用指南
首先来看个例子: hello,tween.js

tween是什么?如何使用?你为什么想用它?

补间(动画)(来自 in-between)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这需要多长时间,补间引擎将负责计算从起始点到结束点的值。

例如,position对象拥有xy两个坐标:

var position = { x: 100, y: 0 }

如果你想将x坐标的值从100变成200,你应该这么做:

// 首先为位置创建一个补间(tween)
var tween = new TWEEN.Tween(position);

// 然后告诉 tween 我们想要在1000毫秒内以动画的形式移动 x 的位置
tween.to({ x: 200 }, 1000);

一般来说这样还不够,tween 已经被创建了,但是它还没被激活(使用),你需要这样启动:

// 启动
tween.start();

最后,想要成功的完成这种效果,你需要在主函数中调用TWEEN.update,如下使用:

animate();

function animate() {
	requestAnimationFrame(animate);
	// [...]
	TWEEN.update();
	// [...]
}

这样在更新每帧的时候都会运行补间动画;经过 1秒后 (1000 毫秒) position.x将会变成 200

除非你在控制台中打印出 x 的值,不然你看不到它的变化。你可能想要使用 onUpdate 回调:

tween.onUpdate(function(object) {
	console.log(object.x);
});

tips:你可能在这里获取不到 object.x ,具体的见我提的这个 issue

这个函数将会在动画每次更新的时候被调用;这种情况发生的频率取决于很多因素 - 例如,计算机或设备的速度有多快(以及如何繁忙)。

到目前为止,我们只使用补间动画向控制台输出值,但是您可以将它与 three.js 对象结合:

var tween = new TWEEN.Tween(cube.position)
		.to({ x: 100, y: 100, z: 100 }, 10000)
		.start();

animate();

function animate() {
	requestAnimationFrame(animate);
	TWEEN.update();

	threeRenderer.render(scene, camera);
}

在这种情况下,因为three.js渲染器将在渲染之前查看对象的位置,所以不需要使用明确的onUpdate回调。

你可能也注意到了一些不同的地方:tween.js 可以链式调用! 每个tween函数都会返回tween实例,所以你可以重写下面的代码:

var tween = new TWEEN.Tween(position);
tween.to({ x: 200 }, 1000);
tween.start();

改成这样:

var tween = new TWEEN.Tween(position)
	.to({ x: 200 }, 1000)
	.start();

在将会看到很多例子,所以熟悉它是很好的!比如 04-simplest 这个例子。

tween.js的动画

Tween.js 不会自行运行。你需要显式的调用 update 方法来告诉它何时运行。推荐的方法是在主动画循环中执行这个操作。使用 requestAnimationFrame 调用此循环以获得最佳的图形性能。

比如之前这个例子:

animate();

function animate() {
	requestAnimationFrame(animate);
	// [...]
	TWEEN.update();
	// [...]
}

如果调用的时候不传入参数,update 将会判断当前时间点以确定自上次运行以来已经有多久。

当然你也可以传递一个明确的时间参数给 update

TWEEN.update(100);

意思是"更新时间 = 100 毫秒"。你可以使用它来确保代码中的所有时间相关函数都使用相同的时间值。例如,假设你有一个播放器,并希望同步运行补间。 你的 animate 函数可能看起来像这样:

var currentTime = player.currentTime;
TWEEN.update(currentTime);

我们使用明确的时间值进行单元测试。你可以看下 tests.js 这个例子,看看我们如何用不同的值调用TWEEN.update() 来模拟时间传递。

控制一个补间

start 和 stop

到目前为止,我们已经了解了Tween.start方法,但是还有更多的方法来控制单个补间。 也许最重要的一个是 star 对应的方法:停止 。 如果你想取消一个补间,只要调用这个方法通过一个单独的补间:

tween.stop();

停止一个从未开始或已经停止的补间没有任何效果。 没有错误被抛出。

start 方法接受一个参数 time。如果你使用它,那么补间不会立即开始,直到特定时刻,否则会尽快启动(i.e 即在下次调用 TWEEN.update)。

update

补间也有一个更新的方法---这实际上是由 TWEEN.update 调用的。 你通常不需要直接调用它,除非你是个 疯狂的hacker。

chain

当你顺序排列不同的补间时,事情会变得有趣,例如在上一个补间结束的时候立即启动另外一个补间。我们称这为链式补间,这使用 chain 方法去做。因此,为了使 tweenBtewwnA 启动:

tweenA.chain(tweenB);

或者,对于一个无限的链式,设置tweenA一旦tweenB完成就开始:

tweenA.chain(tweenB);
tweenB.chain(tweenA);

关于无限的链式查看 Hello world

在其他情况下,您可能需要将多个补间链接到另一个补间,以使它们(链接的补间)同时开始动画:

tweenA.chain(tweenB,tweenC);

警告:调用 tweenA.chain(tweenB) 实际上修改了tweenA,所以tweenA总是在tweenA完成时启动。 chain 的返回值只是tweenA,不是一个新的tween。

repeat

如果你想让一个补间永远重复,你可以链接到自己,但更好的方法是使用 repeat 方法。 它接受一个参数,描述第一个补间完成后需要多少次重复

tween.repeat(10); // 循环10次
tween.repeat(Infinity); // 无限循环

补间的总次数将是重复参数加上一个初始补间。查看 Repeat

yoyo

这个功能只有在独自使用 repeat 时才有效果。 活跃时,补间的行为将像 yoyo 一样,i.e 它会从起始值和结束值之间跳出,而不是从头开始重复相同的顺序。

delay

更复杂的安排可能需要在实际开始运行之前延迟补间。 你可以使用 delay 方法来做到这一点

tween.delay(1000);
tween.start();

将在调用启动方法后的1秒钟后开始执行。

控制所有补间

在 TWEEN 全局对象中可以找到以下方法,除了 update 之外,通常不需要使用其中的大部分对象。

TWEEN.update(time)

我们已经讨论过这种方法。 它用于更新所有活动的补间。
如果 time 不指定,它将使用当前时间。

TWEEN.getAll and TWEEN.removeAll

用于获取对活动 tweens 数组的引用,并分别仅从一个调用中将它们全部从数组中删除

TWEEN.add(tween) and TWEEN.remove(tween)

用于将补间添加到活动补间的列表,或者分别从列表中删除特定的补间。

这些方法通常只在内部使用,但是如果您想要做一些有趣的事情,则会被暴露。

控制补间组

使用 TWEEN 单例来管理补间可能会导致包含许多组件的大型应用程序出现问题。 在这些情况下,您可能希望创建自己的更小的补间组。

示例:交叉组件冲突

如果使用 TWEEN 有多个组件,并且每个组件都想管理自己的一组补间,则可能发生冲突。 如果一个组件调用 TWEEN.update()TWEEN.removeAll(),则其他组件的补间也将被更新或删除。

创建你自己的补间组

为了解决这个问题,每个组件都可以创建自己的 TWEEN.Group 实例(这是全局的 TWEEN 对象在内部使用的)。 实例化新的补间时,可以将这些组作为第二个可选参数传入:

var groupA = new TWEEN.Group();
var groupB = new TWEEN.Group();

var tweenA = new TWEEN.Tween({ x: 1 }, groupA)
	.to({ x: 10 }, 100)
	.start();

var tweenB = new TWEEN.Tween({ x: 1 }, groupB)
	.to({ x: 10 }, 100)
	.start();

var tweenC = new TWEEN.Tween({ x: 1 })
	.to({ x: 10 }, 100)
	.start();

groupA.update(); // 只更新tweenA
groupB.update(); // 只更新tweenB
TWEEN.update(); // 只更新tweenC

groupA.removeAll(); // 只移除tweenA
groupB.removeAll(); // 只移除tweenB
TWEEN.removeAll(); // 只移除tweenC

通过这种方式,每个组件都可以处理创建,更新和销毁自己的一组补间。

改变缓动功能

Tween.js 将以线性方式执行值之间的插值(即缓动),所以变化将与流逝的时间成正比。 这是可以预见的,但在视觉上也是相当无趣的。 不要担心 - 使用缓动方法可以轻松更改此行为。 例如:

tween.easing(TWEEN.Easing.Quadratic.In);

这将导致缓慢地开始向最终值变化,向中间加速,然后迅速达到其最终值,相反,TWEEN.Easing.Quadratic.Out 一开始会加速,但随着值的接近最终放缓。

可用的缓动函数:TWEEN.Easing

tween.js提供了一些现有的缓动功能。它们按照它们表示的方程式进行分组:线性,二次,三次,四次,五次,正弦,指数,圆形,弹性,背部和弹跳,然后是缓动型:In,Out和InOut。

除非您已经熟悉这些概念,否则这些名称可能不会对您说什么,所以您可能需要查看 Graphs 示例,该示例将一个页面中的所有曲线进行图形化,以便比较它们如何看待一瞥。

这些功能是从 Robert Penner 慷慨地提供几年前作为自由软件提供的原始方程派生而来的,但是已经被优化以便与JavaScript很好地发挥作用。

使用自定义缓动功能

您不仅可以使用任何现有的功能,还可以提供您自己的功能,只要遵循一些约定即可:

  • 它必须接受一个参数:
    • k: 缓动过程,或我们的补间所处的时间有多长。允许的值在[0,1]的范围内。
  • 它必须根据输入参数返回一个值。

不管要修改多少个属性,easing函数在每次更新时只调用一次。 然后将结果与初始值以及这个值和最终值之间的差值(delta)一起使用,就像这个伪代码一样:

easedElapsed = easing(k);
for each property:
	newPropertyValue = initialPropertyValue + propertyDelta * easedElapsed;

对于更注重性能表现的人来说:只有在补间上调用 start() 时才会计算增量值。

因此,让我们假设您想使用一个缓解值的自定义缓动函数,但是将 Math.floor 应用于输出,所以只返回整数部分,从而产生一种梯级输出:

function tenStepEasing(k) {
	return Math.floor(k * 10) / 10;
}

你可以通过简单地调用它的缓动方法来使用它,就像我们之前看到的那样:

tween.easing(tenStepEasing);

查看 graphs for custom easing functions 示例,以查看这个动作(还有一些用于生成步进函数的元编程)。

回调函数

另一个强大的特性是能够在每个补间的生命周期的特定时间运行自己的功能。 当更改属性不够时,通常需要这样做。

例如,假设你正在试图给一些不能直接访问属性的对象设置动画,但是需要你调用setter。 您可以使用 update 回调来读取新的更新值,然后手动调用setters。 所有的回调函数都将补间对象作为唯一的参数。

var trickyObjTween = new TWEEN.Tween({
	propertyA: trickyObj.getPropertyA(),
	propertyB: trickyObj.getPropertyB()
})
	.to({ propertyA: 100, propertyB: 200 })
	.onUpdate(function(object) {
		object.setA( object.propertyA );
		object.setB( object.propertyB );
	});

或者想象一下,当一个补间开始时,你想播放声音。你可以使用 start 回调:

var tween = new TWEEN.Tween(obj)
	.to({ x: 100 })
	.onStart(function() {
		sound.play();
	});

每个回调的范围是补间对象--在这种情况下,是 obj

onStart

在补间开始之前执行--i.e. 在计算之前。每个补间只能执行一次,i.e. 当通过 repeat() 重复补间时,它将不会运行。

同步到其他事件或触发您要在补间启动时发生的操作是非常好的。

补间对象作为第一个参数传入。

onStop

当通过 stop() 显式停止补间时执行,但在正常完成时并且在停止任何可能的链补间之前执行补间。

补间对象作为第一个参数传入。

onUpdate

每次补间更新时执行,实际更新后的值。

补间对象作为第一个参数传入。

onComplete

当补间正常完成(即不停止)时执行。

补间对象作为第一个参数传入。

高级补间

相对值

使用 to 方法时,也可以使用相对值。 当tween启动时,Tween.js将读取当前属性值并应用相对值来找出新的最终值。
但是你需要使用引号,否则这些值将被视为绝对的。 我们来看一个例子:

// This will make the `x` property be 100, always
var absoluteTween = new TWEEN.Tween(absoluteObj).to({ x: 100 });

// Suppose absoluteObj.x is 0 now
absoluteTween.start(); // Makes x go to 100

// Suppose absoluteObj.x is -100 now
absoluteTween.start(); // Makes x go to 100

// In contrast...

// This will make the `x` property be 100 units more,
// relative to the actual value when it starts
var relativeTween = new TWEEN.Tween(relativeObj).to({ x: "+100" });

// Suppose relativeObj.x is 0 now
relativeTween.start(); // Makes x go to 0 +100 = 100

// Suppose relativeObj.x is -100 now
relativeTween.start(); // Makes x go to -100 +100 = 0

查看 09_relative_values 示例。

补间值的数组

除了补间为绝对值或相对值之外,还可以让Tween.js跨一系列值更改属性。 要做到这一点,你只需要指定一个数组的值,而不是一个属性的单个值。 例如:

var tween = new TWEEN.Tween(relativeObj).to({ x: [0, -100, 100] });

将使 x 从初始值变为0,-100和100。

这些值的计算方法如下:

  • 首先,补间进度如常计算
  • 进度(从0到1)用作插值函数的输入
  • 基于进度和值的数组,生成内插值

例如,当补间刚刚启动(进度为0)时,插值函数将返回数组中的第一个值。 当补间到一半时,插值函数将返回一个大约在数组中间的值,当补间结束时,插值函数将返回最后一个值。

您可以使用插值方法更改插值函数。 例如:

tween.interpolation( TWEEN.Interpolation.Bezier );

以下值可用:

  • TWEEN.Interpolation.Linear
  • TWEEN.Interpolation.Bezier
  • TWEEN.Interpolation.CatmullRom

默认是 Linear

请注意,插值函数对于与同一补间中的数组进行补间的所有属性是全局的。
您不能使用数组和线性函数进行属性A的更改,也不能使用相同的补间进行数组B的属性B和Bezier函数的更改; 您应该使用运行在同一对象上的两个补间对象,但修改不同的属性并使用不同的插值函数。

查看 06_array_interpolation 示例。

获得最佳性能

虽然Tween.js试图自己执行,但是没有什么能够阻止你以一种反作用的方式使用它。 这里有一些方法可以避免在使用Tween.js时(或者在网页中进行动画制作时)减慢项目速度。

使用高性能的CSS

当您尝试在页面中设置元素的位置时,最简单的解决方案是为 topleft 属性设置动画,如下所示:

var element = document.getElementById('myElement');
var tween = new TWEEN.Tween({ top: 0, left: 0 })
	.to({ top: 100, left: 100 }, 1000)
	.onUpdate(function(object) {
		element.style.top = object.top + 'px';
		element.style.left = object.left + 'px';
	});

但这实际上是效率低下的,因为改变这些属性会迫使浏览器在每次更新时重新计算布局,这是非常昂贵的操作。 相反的,您应该使用 transform,这不会使布局无效,并且在可能的情况下也将被硬件加速,比如:

var element = document.getElementById('myElement');
var tween = new TWEEN.Tween({ top: 0, left: 0 })
	.to({ top: 100, left: 100 }, 1000)
	.onUpdate(function(object) {
		element.style.transform = 'translate(' + object.left + 'px, ' + object.top + 'px);';
	});

如果你想了解更多关于这个,看看这篇文章

但是,如果您的动画需求非常简单,那么在适用的情况下使用CSS动画或转换可能会更好,以便浏览器尽可能优化。
当您的动画需要涉及复杂的布局时,Tween.js是非常有用的,也就是说,您需要将多个补间同步到一起,在完成一些动作之后,循环多次等等。

对垃圾收集器(别名GC)

如果你使用onUpdate回调函数,你需要非常小心的使用它。 因为这个函数每秒钟会被调用很多次,所以如果每次更新都要花费很多的代价,那么你可能会阻塞主线程并导致可怕的结果,或者如果你的操作涉及到内存分配的话, 垃圾收集器运行太频繁,也导致结果。 所以只是不要做些事情中的其中一个。 保持你的onUpdate回调非常轻量级,并确保在开发时也使用内存分析器。

疯狂的补间

这是你可能不经常使用的东西,但是你可以在Tween.js之外使用补间公式。 毕竟,它们只是功能。 所以你可以使用它们来计算平滑曲线作为输入数据。
例如,他们用于在 这个实验 中生成音频数据。

合理的处理函数的默认参数

给函数赋值的类似操作很常见:

function sure(options) {
   let name = options.name || 'qize'
   let age = options.age || 25
  // ...
}

这样的赋值没有什么大问题,但是如果遇到下面这种情况就会有问题了。比如,age 就是 0,且这个值就是合法的,如果按照上面的逻辑,这里还是会显示 25,很显然,这不是我们想要的结果。

所以这里,正确且安全的做法是使用 typeof 进行参数的类型检查

function sure(options) {
   let name = (typeof options.name !== 'undefined') ? options.name : 'qize'
   let age = (typeof options.age !== 'undefined') ? options.age || 25
  // ...
}

其实这里容易出现问题不只是 0,还有 空字符串等 Falsy 值。

Falsy包括:false、undefined、null、正负0、NaN、""

Promise原理与实现探究的一种思路

写在前面

这个文章,展现的是一个实现promise的思路,以及如何发现和处理问题的情境。

从现有的Promise分析

如果我们想要自己实现一个简单的Promise,那现有规范规定的Promise肯定是我们最好的参照。

我们先看下Promise怎么使用:

var promise1 = new Promise(function(resolve, reject){
      // 成功后的TODO
      resolve(value);
      // 失败后的TODO
      reject(err);
})

来看下返回的promise1是什么

image

再进行一些具体操作

var promise1 = new Promise(function(resolve, reject) {
 	resolve('zqz')
})

promise1.then(function(result) {
   console.log(result) 
}).catch(function(err){
   console.log(err) 
})

// => 'zqz'

11

var promise1 = new Promise(function(resolve, reject) {
 	reject('出现异常')
})
promise1.then(function(result) {
   console.log(result) 
}).catch(function(err){
   console.log(err) 
})

// => '出现异常'

22

从Promise的 使用方式上 和 实例 可以看到哪些东西:

  • Promise是一个构造函数
  • Promise包含一个参数,这个参数类型是一个_匿名函数_
  • 匿名函数包括2个形参,分别是 rejectresolve
  • 这两个形参类型是 函数 ,且 rejectresolve 都有一个参数, 参数类型不限定
  • 实例 是个 Promise
  • 实例的 原型 上挂载了 2个方法,分别是 thencatch,同时then可以有多个,所以需要一个回掉函数队列
  • 实例上 有2个属性,分别是 PromiseStatusPromiseValue
  • Promise根据定义 PromiseStatus 需要有 3种状态,分别是 pending , fulfilledrejected

根据上面的分析情况,我们先简单的来构造一个雏形。

function Promise(fn) {
  this.PromiseStatus = 'pending';
  this.PromiseValue = '';

  this.resolvedCb = [];
  this.rejectedCb = [];

  var self = this;

  var resolve = function (result) {
    // 判断状态
     if (self.PromiseStatus === 'pending') {
      self.PromiseStatus = 'resolved';
      self.PromiseValue = result;
       // resolvedCb 队列依次执行
       for (var i = 0;i < self.resolvedCb.length; i++) {
         self.resolvedCb[i](result)
       }
     }
   }
 
   var reject = function (err) {
     // 判断状态
     if (self.PromiseStatus === 'pending') {
        self.PromiseStatus = 'rejected';
        self.PromiseValue = err;
       
        // rejectedCb 队列依次执行
       for (var i = 0;i < self.rejectedCb.length; i++) {
         self.rejectedCb[i](result)
       }
     }
   }
 
 // 错误处理 -> rejected
  try {
    fn(resolve, reject)
  } catch(e) {
    reject(e)
  }
  
}

当然这还不够,因为重要的两个功能thencatch还没有实现。

从现有的 then 分析

分析下then的使用

promise1.then(function(value){
   // todo
   return value;
})
.then(function(value1){
    // todo
    return value1;
})
.then(function(value2){
  // todo
  return value2;
})
  • promise1 返回的值 需要塞到第一个then中函数的value上
  • 链式调用,多次调用
  • then返回的是一个新的Promise
  • then可以接受2个函数作为参数,一个是成功函数,一个是失败函数
  • return 的值 直接作为下个 then 中匿名函数的入参

根据Promise返回的实例,我们可看出来then是挂载在 Promise原型链上。

我们先实现一个大体的框架:

Promise.prototype.then = function (handleSuccess, handleFail) {
    var self = this;
    var PromiseStatus = this.PromiseStatus;

    if(typeof handleSuccess === 'function') {
      handleSuccess = handleSuccess;
    } else {
      handleSuccess = function (result) {}
    }

    if(typeof handleFail === 'function') {
      handleFail = handleFail;
    } else {
      handleFail = function (err) {}
    }

    if(PromiseStatus === 'pending') {
        return new Promise(function(resolve, reject) {
          self.resolvedCb.push(handleSuccess);
          self.rejectedCb.push(handleFail);
        })
    }

    if(PromiseStatus === 'resolved') {
        return new Promise(function(resolve, reject) {
          var result = handleSuccess(self.PromiseValue);
          resolve(result);
        })
    }

    if(PromiseStatus === 'rejected') {
      return new Promise(function(resolve, reject) {
        var result = handleFail(self.PromiseValue);
        reject(result);
      })
    }
    
}

我们先用一下,看下是否符合期望

方式一(无异步操作):

function promise1() {
  return new Promise(function(resolve, reject){
      console.log('执行promise1')
      resolve('zqz');
  })
}

promise1().then(function(result){
  console.log('执行1', 'result:'+result)
  return result + '11';
})
.then(function(result){
    console.log('执行2', 'result:'+result)
  return result + '22';
})
// => 执行promise1
// => 执行1 result:zqz
// => 执行2 result:zqz11
// => Promise {PromiseStatus: "resolved", PromiseValue: "zqz1122", resolvedCb: Array(0), rejectedCb: Array(0)}

这样使用没有问题!

方式二(有异步操作):

function promise1() {
  return new Promise(function(resolve, reject){
    // 异步操作
    setTimeout(function(){
      console.log('执行promise1')
      resolve('zqz');
    },1000)

  })
}

promise1().then(function(result){
  console.log('执行1', 'result:'+result)
  return result + '11';
})
.then(function(result){
    console.log('执行2', 'result:'+result)
  return result + '22';
})
// => 执行promise1
// => 执行1 result:zqz

一旦出现异步操作,就有问题!很明显,Promise的主要作用就是控制异步操作的执行顺序。

肯定是哪里有问题,我们来分析一下,异步的时候 有哪些 不同

  • 当有异步操作(xhr,setTimeout等)的时候,这时候PromiseStatuspending状态

在来看下我们在pending时候的处理

...
// 异步时
if(PromiseStatus === 'pending') {
        return new Promise(function(resolve, reject) {
         // 这里只是将函数塞入队列,然后就没有然后来。。。这是有问题的
          self.resolvedCb.push(handleSuccess);
          self.rejectedCb.push(handleFail);
        })
    }
...

这时候我们的两个数组:resolvedCbrejectedCb就发挥作用了,由于我们不知道异步什么时候结束,但是我们可以根据他们定义的先后顺序注入到 队列 中,然后根据 顺序 依次执行,这样也就保证了异步操作的执行顺序。

if(PromiseStatus === 'pending') {
        return new Promise(function(resolve, reject) {
         // 一个个的塞入队列
          self.resolvedCb.push(function(result) {
              var res = handleSuccess(self.PromiseValue);
              resolve(res);
          })
          self.rejectedCb.push(function(err) {
              var er = handleFail(self.PromiseValue);
              reject(er);
          })
        })
    }

这时候我们用多个异步操作来测试一下

function async1() {
  return new Promise(function(resolve, reject){
    // 异步操作
    setTimeout(function(){
      console.log('执行async1')
      resolve('zqz1');
    },3000)
  })
}
function async2() {
  return new Promise(function(resolve, reject){
    // 异步操作
    setTimeout(function(){
      console.log('执行async2')
      resolve('zqz2');
    },1000)
  })
}
function async3() {
  return new Promise(function(resolve, reject){
    // 异步操作
    setTimeout(function(){
      console.log('执行async3')
      resolve('zqz3');
    },2000)
  })
}

// return 一个新的promise
async1().then(function(result){
   console.log('result = ' + result)
   return async2();
}).then(function(result){
   console.log('result = ' + result)
   return async3();
}).then(function(result){
   console.log('result = ' + result)
   return result;
})

// => Promise {PromiseStatus: "pending", PromiseValue: "", resolvedCb: Array(0), rejectedCb: Array(0)}
// => 执行async1
// => result = zqz1
// => result = [object Object]
// => result = [object Object]
// => 执行async2
// => 执行async3

这里有两个问题:

  • 返回promise的时候,执行顺序有问题
  • 返回promise的时候,无法进行值的传递

我们再来分析下,着重看下下面这块代码

...
if(PromiseStatus === 'pending') {
        return new Promise(function(resolve, reject) {
          self.resolvedCb.push(function(result) {
              // 这里返回的res有可能是promise,但是我们没有做处理
              var res = handleSuccess(self.PromiseValue);
              resolve(res);
          })
          self.rejectedCb.push(function(err) {
              // 这里返回的res有可能是promise,但是我们没有做处理
              var er = handleFail(self.PromiseValue);
              reject(er);
          })
        })
    }
...

因为我们返回的是Promise,由于我们没有做处理,导致无法正确的获取到值。

...
if(PromiseStatus === 'pending') {
        return new Promise(function(resolve, reject) {
          self.resolvedCb.push(function(result) {
              var res = handleSuccess(self.PromiseValue);
              if (res instanceof Promise) {
                   res.then(resolve, reject);
              } else {
                   resolve(res);
              } 
          })
          self.rejectedCb.push(function(err) {
              var er = handleFail(self.PromiseValue);
              if (er instanceof Promise) {
                   er.then(resolve, reject);
              } else {
                   reject(er);
              }
          })
        })
    }
...

如果返回的是一个Promise,就继续塞入到then里面。

在执行一下:

async1().then(function(result){
   console.log('result = ' + result)
   return async2();
}).then(function(result){
   console.log('result = ' + result)
   return async3();
}).then(function(result){
   console.log('result = ' + result)
   return result;
})

// => Promise {PromiseStatus: "pending", PromiseValue: "", resolvedCb: Array(0), rejectedCb: Array(0)}
// => 执行async1
// => result = zqz1
// => 执行async2
// => result = zqz2
// => 执行async3
// => result = zqz3

最后一个简单完整的 then:

Promise.prototype.then = function (handleSuccess, handleFail) {
    var self = this;
    var PromiseStatus = this.PromiseStatus;

    if(typeof handleSuccess === 'function') {
      handleSuccess = handleSuccess;
    } else {
      handleSuccess = function (result) {}
    }

    if(typeof handleFail === 'function') {
      handleFail = handleFail;
    } else {
      handleFail = function (err) {}
    }

    if(PromiseStatus === 'pending') {
        return new Promise(function(resolve, reject) {
          self.resolvedCb.push(function(result) {
              var res = handleSuccess(self.PromiseValue);
              if (res instanceof Promise) {
                   res.then(resolve, reject);
              } else {
                  resolve(er);
              } 
          })
          self.rejectedCb.push(function(err) {
              var er = handleFail(self.PromiseValue);
              if (er instanceof Promise) {
                   er.then(resolve, reject);
              } else {
                  reject(er);
              } 
          })
        })
    }
    if(PromiseStatus === 'resolved') {
        return new Promise(function(resolve, reject) {
          var result = handleSuccess(self.PromiseValue);
          resolve(result);
        })
    }
    if(PromiseStatus === 'rejected') {
      return new Promise(function(resolve, reject) {
        var result = handleFail(self.PromiseValue);
        reject(result);
      })
    }
    
}

参考

JSON Schema 的接口测试实战

“接口返回的数据不对啊!”
我想这句话是前端工程师经常挂在嘴边的
作为前端工程师,与后端工程师对接口是日常工作,每次遇到接口问题,我们都无比头疼。既然这样,那我们能不能通过一种手段来处理这种问题呢?下面的 JSON Schema 就是我的一种尝试。

JSON Schema

JSON Schema is a vocabulary that allows you to annotate and validate JSON documents.

JSON Schema 是用以标注和验证JSON文档的元数据的文档。通俗的说,通过 JSON Schema 我们可以规范和验证 JSON 的格式,符不符合我们的预期。比如某个字段我们通过 JSON Schema 定义为 string,但是返回的 JSON 中该字段是 number ,这种情况通过验证就是不通过。

这里贴一个 JSON Schema 官方的简单例子:

我们来简单分析下这个 Schema 的含义:

  • title 字段:用来描述这个 Schema 是用来做什么的,不具有约束性作用。
  • properties 字段:用来描述这个 Schema 中包含的字段
  • firstName,lastName,age 字段就是我们要描述的 JSON 中要包含的字段
  • type 字段:用来描述字段的类型,这是必须的
  • required 字段:用来描述哪些字段必须的

这时候,对于下面这个 JSON 就是不通过的,因为 Schema 中 age 定义的是 integer

{
   "firstName": "z",
   "lastName": "q",
   "age": "61"
}

check 接口

由于原来项目涉及公司信息,这里使用 demo 进行演示,但是思路是一样的。

jsonschema

其实 JSON Schema 本身描述起来是很复杂的。目前的版本是 draft-07。如果我们的接口是一些简单 JSON 还好,要是很复杂的话,想要写好那还是有些工作量的。所以这里我推荐大家使用 jsonschema,来帮助我们自动生成。然后,我们可以在这个基础上修改。

ajv

既然这里要检验我们的接口是否符合我们的预期,肯定是进行 校验 的。目前校验的库有不少,但是我使用的是 ajv 这个库。用它提供的功能,帮助我们校验,省去不少麻烦。

实战

这里校验 CNODE 的主题,请求接口是 https://cnodejs.org/api/v1/topics?page=1&limit=1

接口格式如下:

{
  "success": true,
  "data": [
    {
      "id": "5ae140407b0e8dc508cca7cc",
      "author_id": "573ab7ba542374db1db0a436",
      "tab": "share",
      "content": "xxx",
      "title": "【NODE PARTY】【上海】【6月9日 13:30】报名&答疑帖",
      "last_reply_at": "2018-06-01T03:01:08.368Z",
      "good": false,
      "top": true,
      "reply_count": 228,
      "visit_count": 8080,
      "create_at": "2018-04-26T02:58:08.067Z",
      "author": {
        "loginname": "aojiaotage",
        "avatar_url": "https://avatars3.githubusercontent.com/u/8339316?v=4&s=120"
      }
   }
  ]
}

通过 jsonschema 生成的 JSON Schema 如下:

{
  "$id": "http://example.com/example.json",
  "type": "object",
  "definitions": {},
  "$schema": "http://json-schema.org/draft-07/schema#",
  "properties": {
    "success": {
      "$id": "/properties/success",
      "type": "boolean",
      "title": "The Success Schema ",
      "default": false,
      "examples": [
        true
      ]
    },
    "data": {
      "$id": "/properties/data",
      "type": "array",
      "items": {
        "$id": "/properties/data/items",
        "type": "object",
        "properties": {
          "id": {
            "$id": "/properties/data/items/properties/id",
            "type": "string",
            "title": "The Id Schema ",
            "default": "",
            "examples": [
              "5ae140407b0e8dc508cca7cc"
            ]
          },
         ....
}

可以看到生成的 JSON Schema 非常的规范,我们可以通过设置,将非必须的字段都去掉。

通过 axios 发送请求,获取返回的报文

const axios = require('axios');
const API = require('../api/index');

/**
 * 描述:获取cnode topic
 */
const getTopic = function () {
  return new Promise((resolve, reject) => {
    axios.get(API.topic)
        .then((response) => {
          resolve(response);
        })
        .catch((error) => {
          reject(error);
        });
  })
}

module.exports = getTopic;

然后对数据进行 JSON Schema 校验

const Ajv = require('ajv');
const ajv = new Ajv(); // options can be passed, e.g. {allErrors: true}
const chalk = require('chalk');

// schema
const topicSchema = require('./schemas/cnode.topic.json');

// json
const getTopic = require('./json/cnode.topic.js');

// check Topic
function checkTopic () {
  getTopic()
    .then((resp) => {
      const validate = ajv.compile(topicSchema);
      const valid = validate(resp.data);
      console.log('---------------------------------------');
      console.log(chalk.green(`请求url: ${resp.request.path}`));
      if (valid) {
        console.log(chalk.green(`校验成功: ${valid}`));
      } else {
        console.log(chalk.red(`校验失败: ${validate.errors}`));
      }
      console.log('---------------------------------------');
    })
    .catch((err) => {
      console.log('---------------------------------------');
      console.log(chalk.red(`请求失败: ${err}`));
      console.log('---------------------------------------');
    })
}

checkTopic();

运行node index.js,返回
image

最后

这个例子很简单,但是到具体的业务中,对于所有(重要)接口的校验,还是需要不少的其他工作。比如

  • 分环境测试
  • 自动生成可配置的 JSON Schema
  • 定时/手动触发校验
  • 校验的上报与统计

其实我觉得这块也应该属于自动化测试的一个环节,目前还在探索和完善中。

JSON Schema 接口测试源码

最后,共勉!💪

我的2020

image

2020 年的整体基调是挑战大、有收获

工作

内心淡定且从容,迎接更大挑战

今年工作依旧还是围绕风控进行建设,不过在业务范围和精细化运作上做了很大提升。从原来的2个方向扩大到5个业务方向,团队人员也出现了爆发式的增长。今年比较热门的直播、电商业务也多有涉猎。随着业务和人员的增长,团队这块也需要多花点功夫,所以今年在团队上做了系统性思考,包括系统性的业务和技术建设、寻找平衡点并长期投入等,在思考、落地上尽力做到了「知行合一」,从年底的满意度调查问卷来看,大家评价还不错,但依旧有提升空间,希望 2021 年有更多进步!

生活

生活是规划出来的,忙碌且充实

买了婚房,找准每个可以利用的大周回去装修房子,这种亲手美缝、挑选家具、电器的感觉真的太奇妙了,查了很多攻略,学会了很多技能,这么做城际来来回回搞了半年,终于在年前住上了,真爽!
迫于腰疼,今年下半年逼迫自己去游泳,发动了团队中的几个感兴趣的同学一起报游泳班,从旱鸭子到会老年蛙大概花了 2 个月时间,时间较长的原因是假期少且经常出差。到现在已经坚持了半年,感觉腰部比以前好了很多,再也没有那种极度酸疼的感觉了,所以游泳还会坚持下去的。
今年跟同事出去休闲了几次,去了秦皇岛、湖南、三亚,一起租个大民宿,各种逛吃,爬山,感觉挺不错,周五红眼航班,周日晚上回来。感觉这种放松模式挺好的,但切忌一定要适度,不然适得其反会很疲惫。
今年也读了很多书,大概二三十本,50% 是业务相关的,40% 是技术相关的,10% 是生活相关的。一直坚信看书是快速学习的不二法门。

总结

最后做个总结,感叹一年竟然可以做这么多事,自豪感油然而生!同时在其中也感悟了些道理,比如面对复杂的事情,首先要做的是梳理清楚,然后一步步的拆解去执行,盲目的逃避或急于执行,都是不可取的。还有不要给自己设定边界,因为人的潜力是需要不断挖掘的,只有勇敢的跳出去才能看到更美的风景。最后也是最重要的就是「坚持」✊!只有坚持才会到达梦想的彼岸!

广告:字节跳动全球风控团队招聘前端开发工程师,实习、校招、社招都需要!base 地点有北京、深圳、新加坡。欢迎投递简历至 [email protected] 或加 wechat:leslie000666 详聊(推荐加V可以时时沟通面试进度等)

我的2018

image

还有三天就要过年了,在这里先祝大家新年快乐!

这个总结拖了有2周了,不知如何下笔。因为有太多的事要说,反而越理越乱。既然这样我就按照既定的顺序,想到啥就写啥了。

2018 年感觉过的超级快,上一句还在跟杨康(我发小)说着我明年怎么怎么,下一句就到这写总结了。

城市

北京,北京

去年我也说了我对于北京的看法,今年我还想多加一句:雪呢?

距离2019年3月还1个月不到,算起来在北京也是有2年光景了。虽然比起大多数人来说这时间不算长,但是我感觉这2年比在武汉的成长快很多。无论是生活的认知,工作的想法,未来的眼界。不得不承认的是我来这,没错。同时,我也建议刚毕业的同学去大城市看看,年轻的时候眼界比什么都重要。

野趣:去年用一首打油的语调在脉脉回了一个话题:在北京生活打拼是什么感受?(回答原文点这里

工作

既往不恋

2018年我的工作可以分为两个部分,一部分在平台,一部分在外卖。在平台的工作与去年变化不是特别大,如果要说多大,只能说从之前的不怎么核心的业务变成了稍微核心的业务。然而我想说的是外卖。外卖的人才确实是多,无论是从个人素质和能力方面。这里不说技术,因为我觉有样东西比技术重要--自我认知。通过一段时间接触,感觉周围的同事对自己都有比较全面的认知,知道自己想要什么,并有相应的规划去达到目标,感觉这点难能可贵。虽然换部门需要顶着巨大的巨大,但是我觉得能与这样一群有理想,有清晰认知的人工作,是我的荣幸。因为工作的另外一个目的是在观察与实践中提升个人的社会属性。

家庭

结婚是最大的喜事

虽然今年因为有些事耽误了带父母出去玩(去天津算吗?😂),但是我觉得这件事比他们出去玩还让他们欣慰。那就是--结婚。没错,我跟我对象谈了快7年,终于领证了。虽然过程非常坎坷,艰辛。但是总算是一起过来了。

这里有个小插曲与大家分享:头天晚上去天津拍照领证之前我俩心里还在打鼓,胡思乱想:明天**知道后会是什么样?要不先斩后奏?第二天我们怀着忐忑不安的心拍完结婚照,在去民政局的路上时,她突然问我:要不打电话跟我妈说下?我立马神经质的说:快打,快打!(抱着必死的决心,屏住呼吸)“嘟..嘟..喂”,“妈,我跟他去领证了,跟你说下”,“...(沉默几秒)...哦,领吧”。呲溜~大石头可算是放下了。

其实,之前是见过父母的,但是,他妈不愿意她太早结婚(可能认为我穷),所以这个需求就一直在delay。当她妈同意后,我们还一脸懵逼的不知所措😳。

买房

房奴,我来了

买房这个事,在现代社会,对于男性来说,是永远也逃不掉的一件事。我记得我从3月份就一直看,一直看到7月份。周末来回的跑,各种攻略的搜。最终确定了两个,但是因为要房票,比较心仪的盘就没买成。另外一个没说要房票,但是要摇号,果不其然,我的号非常靠后(买10W一个车位的排在前面)。还好我跟我对象抱着捡漏的心思,从周六一直等到周一凌晨4点多。终于被我捡到了,选上了房子。(在微信群里面喊号,叫到号的就在群里回选择哪套,果不其然很多人熬不了夜不能坚持一两天的,过期作废),是不是很奇葩的选房方式?😄

傲娇时间:买房只问爸妈借了2W(过年通过红包的方式还回去了),其他的钱是我跟我对象攒的和借(刷)的。😂

苦逼时间:果不其然,后面几个月吃土。买戒指的钱都是问老铁借的。😢

读书&尝试

  • 文学:《君子之道》(推荐大家读一读,对‘君子’的解读比较独到)《浮生六记》(没看完,共6章,主要是夫妻相处的趣事,深受启发)
  • 科普:《时间简史》(纪念霍金)《与社会学同游:人文主义的视角》(没看完,建议大家多了解社会学,有助于我们理解当下社会)
  • 技术:《React深入浅出》(适合初入React的同学)《深入理解ES6》(目前下一代JS大行其道,建议系统的研读一下)
  • 其他:组织了Puppeteer中文文档的翻译

想法

  • 建议有想法的同学出去看看,世界会比你想象的大的多。
  • 试着在工作中少点抱怨,多点思考。

最后

我的2018就是这样平淡且真实。

2019我有几个愿望:

  • 希望父母平平安安,健健康康!
  • 希望两个家庭和睦相处,安安稳稳!
  • 希望大家在2019年这个‘寒冬’中安稳度过!

CommonJS模块化规范和ES6模块化规范

CommonJS模块化规范

关键字:module exports require global

模块的设计**,是尽量的静态化,使得 编译时 就能确定模块的依赖关系,以及输入和输出的变量

ES6模块化规范

关键字:export import

CommonJS 和 AMD 模块,都只能在 运行时 确定这些东西

目前阶段,通过 Babel 转码,CommonJS 模块的require命令ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。
因为import静态解析 阶段执行,所以它是一个模块之中最早执行的。
下面的代码可能不会得到预期结果。

require('core-js/modules/es6.symbol'); // 运行时执行
require('core-js/modules/es6.promise'); // 运行时执行

import React from 'React';  // 静态解析(编译)时执行

两者不是一个概念的东西。

[翻译] SuperAgent 中文使用指南(v3.8.0)

版本: v3.8.0
SuperAgent 英文使用指南

SuperAgent

SuperAgent是轻量级渐进式ajax API,相比于现有的请求API,它更加灵活,可读,且学习成本低。 同样它也适用于Node.js!

 request
   .post('/api/pet')
   .send({ name: 'Manny', species: 'cat' })
   .set('X-API-Key', 'foobar')
   .set('Accept', 'application/json')
   .then(function(res) {
      alert('yay got ' + JSON.stringify(res.body));
   });

测试文档

以下 测试文档 是由Mocha's "doc" 记录生成的,直接反映了测试套件。 这提供了额外的文档来源。

基础请求

请求可以通过调用 request 对象的适当方法来启动,然后调用.then() (or .end() or await) 来发送请求,例如一个简单的GET请求:

 request
   .get('/search')
   .then(function(res) {
      // res.body, res.headers, res.status
   })
   .catch(function(err) {
      // err.message, err.response
   });

HTTP 方法也可以使用字符串:

request('GET', '/search').then(success, failure);

旧的回调依旧可以使用。 将 .then() 替换成 .end()

request('GET', '/search').end(function(err, res){
  if (res.ok) {}
});

可以使用绝对URL。 在Web浏览器中,绝对URL只在服务器实现 CORS 的情况下才起作用。

 request
   .get('http://example.com/search')
   .then(function(res) {

   });

Node 客户端支持向 Unix域套接字 发送请求:

 // pattern: https?+unix://SOCKET_PATH/REQUEST_PATH
 //          Use `%2F` as `/` in SOCKET_PATH
 request
   .get('http+unix://%2Fabsolute%2Fpath%2Fto%2Funix.sock/search')
   .then(res => {

   });

DELETE , HEAD , PATCH , POST , 和 PUT 请求方法依旧可以使用, 只需要修改一下方法名即可:

request
  .head('/favicon.ico')
  .then(function(res) {

  });

DELETE 也可以被称为.del()与旧的IE兼容,其中delete是一个保留字。

HTTP 方法默认是 GET,所以下面的写法也是可以的:

 request('/search', function(err, res){

 });

设置 header 字段

设置标题字段很简单,用字段名和值调用 .set():

 request
   .get('/search')
   .set('API-Key', 'foobar')
   .set('Accept', 'application/json')
   .then(callback);

你也可以传递一个对象来在一次调用中设置几个字段:

 request
   .get('/search')
   .set({ 'API-Key': 'foobar', Accept: 'application/json' })
   .then(callback);

GET 请求

.query() 方法接受对象,当与 GET 方法一起使用时,将形成一个查询字符串。 以下将拼接成/ search?query = Manny&range = 1..5&order = desc这样的一个查询字符串。

 request
   .get('/search')
   .query({ query: 'Manny' })
   .query({ range: '1..5' })
   .query({ order: 'desc' })
   .then(function(res) {

   });

或者用一个对象包裹:

request
  .get('/search')
  .query({ query: 'Manny', range: '1..5', order: 'desc' })
  .then(function(res) {

  });

.query() 方法同样可以接收字符串的形式:

  request
    .get('/querystring')
    .query('search=Manny&range=1..5')
    .then(function(res) {

    });

或者如下这样:

  request
    .get('/querystring')
    .query('search=Manny')
    .query('range=1..5')
    .then(function(res) {

    });

HEAD 请求

对于 HEAD 请求你依旧可以使用 .query() 方法。下面的 .query() 参数将会拼接成 /[email protected]

  request
    .head('/users')
    .query({ email: '[email protected]' })
    .then(function(res) {

    });

POST / PUT 请求

一个典型的JSON POST 请求可能看起来有点像下面这样,我们适当地设置 Content-Type 头字段,并“写”一些数据,在这种情况下,只是一个JSON字符串。

  request.post('/user')
    .set('Content-Type', 'application/json')
    .send('{"name":"tj","pet":"tobi"}')
    .then(callback)

JSON无疑是最常见的,这就是 默认的 ! 下面的例子等同于前面的例子

  request.post('/user')
    .send({ name: 'tj', pet: 'tobi' })
    .then(callback)

或调用多次 .send():

  request.post('/user')
    .send({ name: 'tj' })
    .send({ pet: 'tobi' })
    .then(callback)

默认情况下,发送字符串将把 Content-Type 设置为 application / x-www-form-urlencoded ,多个调用将与连接在一起,这里生成name = tj&pet = tobi

  request.post('/user')
    .send('name=tj')
    .send('pet=tobi')
    .then(callback);

SuperAgent 格式是可扩展的,但默认支持 "json" 和 "form"。 要以 application / x-www-form-urlencoded 方式发送数据,只需使用 "form" 调用 .type(),默认为“json”。 这个请求将 POST 的 body拼接成“名称= tj&pet = tobi"。

  request.post('/user')
    .type('form')
    .send({ name: 'tj' })
    .send({ pet: 'tobi' })
    .then(callback)

发送一个 FormData 格式的数据也是支持的。以下示例将 POST 标识为 id =“myForm” 的HTML表单的内容。

  request.post('/user')
    .send(new FormData(document.getElementById('myForm')))
    .then(callback)

设置 Content-Type

.set() 方法也是经常使用的:

 request.post('/user')
   .set('Content-Type', 'application/json')

作为一个简短方式, .type() 方法也是可用的,它接受规范化的 MIME 类型名称完整的类型/子类型,或简单的扩展名称,如“xml”,“json”,“png”:

 request.post('/user')
   .type('application/json')

 request.post('/user')
   .type('json')

 request.post('/user')
   .type('png')

序列化请求正文

SuperAgent会自动序列化JSON和表单。 如果要以自定义格式发送有效内容,则可以使用 .serialize()方法替换内置序列化。

重试请求

当给定 .retry() 方法时,SuperAgent 会自动重试请求,如果它们以短暂的失败或可能是由于Internet连接造成的。

此方法有两个可选参数:重试次数(默认值3)和回调。 它在每次重试之前调用 callback(err,res)。 回调可能返回 true /false 来控制请求是否被重试(但总是应用最大重试次数)。

 request
   .get('http://example.com/search')
   .retry(2) // or:
   .retry(2, callback)
   .then(finished);

仅对 幂等 的请求使用 .retry()

设置 Accept

类似于 .type() 方法,也可以通过简短的方法 .accept() 来设置 Accept 头。request.types 还允许您指定完整的规范化 MIME 类型名称为type/subtype,或者扩展后缀形式为“xml”,“json”,“png”:

 request.get('/user')
   .accept('application/json')

 request.get('/user')
   .accept('json')

 request.post('/user')
   .accept('png')

Facebook 和 Accept JSON

如果你正在调用 Facebook 的API,请确保在您的请求中发送 "Accept:application/json" 头。 如果你不这样做,Facebook 会用Content-Type:text / javascript; charset = UTF-8,SuperAgent 将不会解析,因此 res.body 将是未定义的。 你可以用 req.accept('json')req.header('Accept','application / json') 来做到这一点。 有关详细信息,请参见 issues1078

查询字符串

req.query(obj) 是一个可以用来建立一个查询字符串的方法。 例如,在 POST 上填充 ?format = json&dest = / login

request
  .post('/')
  .query({ format: 'json' })
  .query({ dest: '/login' })
  .send({ post: 'data', here: 'wahoo' })
  .then(callback);

默认情况下,查询字符串不会以任何特定的顺序组装的。 asciibetically-sorted 排序的查询字符串可以用req.sortQuery() 来启用。 你也可以用 req.sortQuery(myComparisonFn) 提供一个自定义的排序比较函数。 比较函数应该带2个参数并返回一个负/零/正整数。

 // default order
 request.get('/user')
   .query('name=Nick')
   .query('search=Manny')
   .sortQuery()
   .then(callback)

 // customized sort function
 request.get('/user')
   .query('name=Nick')
   .query('search=Manny')
   .sortQuery(function(a, b){
     return a.length - b.length;
   })
   .then(callback)

TLS 选项

在 Node.js 中,SuperAgent 支持配置 HTTPS 请求的方法:

  • .ca(): 将CA证书设置为信任
  • .cert(): 设置客户端证书链
  • .key(): 设置客户端私钥
  • .pfx(): 设置客户端PFX或PKCS12编码的私钥和证书链

更多信息,请看 Node.js https.request docs.

var key = fs.readFileSync('key.pem'),
    cert = fs.readFileSync('cert.pem');

request
  .post('/client-auth')
  .key(key)
  .cert(cert)
  .then(callback);
var ca = fs.readFileSync('ca.cert.pem');

request
  .post('https://localhost/private-ca-server')
  .ca(ca)
  .then(res => {});

解析响应主体

SuperAgent 将为你解析已知的响应主体数据,目前支持 application / x-www-form-urlencodedapplication / jsonmultipart / form-data

您可以使用 .buffer(true).parse(fn) 方法设置自定义解析器(优先于内置解析器)。 如果没有启用响应缓冲 (.buffer(false)),那么 response 事件将不会等待 body 解析完成,所以 response.body 将不可用。

JSON / Urlencoded

例如,如果请求用JSON字符串 “{”user“:{”name“:”tobi“}}” 响应,那么 res.body.user.name 就是解析对象就是“tobi”。同样,“user [name] = tobi”的x-www-form-urlencoded值也会产生相同的结果。 只支持一个嵌套级别。 如果您需要更复杂的数据,请发送JSON。

通过重复key发送数组。 .send({color:['red','blue']}) 发送 color = red&color = blue。 如果你想让数组 key 在它们的名字中包含[],你必须自己添加它,因为SuperAgent不会自动添加它。

Multipart

Node客户端通过 Formidable 模块支持 multipart / form-data 。 当解析多部分响应时,对象 res.files 也可以使用。 假设例如一个请求用下面的多部分主体响应:

--whoop
Content-Disposition: attachment; name="image"; filename="tobi.png"
Content-Type: image/png

... data here ...
--whoop
Content-Disposition: form-data; name="name"
Content-Type: text/plain

Tobi
--whoop--

您可以将 “res.body.name” 作为 “Tobi ”提供,“res.files.image” 作为包含磁盘路径,文件名和其他属性的“File”对象。

二进制

在浏览器中,你可以使用 .responseType('blob') 来请求处理二进制响应主体。 在 node.js 中运行时,此API不是必要的。 这个方法支持的参数值是

  • blob 传递给 XmlHTTPRequest responseType 属性
  • arraybuffer' 传递给 XmlHTTPRequest responseType` 属性
req.get('/binary.data')
  .responseType('blob')
  .end(function (error, res) {
    // res.body will be a browser native Blob type here
  });

更多内容请看 Mozilla Developer Network xhr.responseType docs

响应属性

响应对象设置了许多有用的标志和属性,包括响应文本,解析的响应主体,标题字段,状态标志等等。

响应文本

res.text 属性包含未解析的响应正文字符串。 此属性始终为客户端API提供,并且仅当默认情况下mime类型与节点默认匹配“text / ”,“ / json”或“x-www-form-urlencoded”时。 其原因是为了节省内存,因为诸如多部分文件或图像的大型文本的缓存文本是非常低效的。 要强制缓存请参阅 “缓存响应” 部分。

响应体

与 SuperAgent 可以自动序列化请求数据一样,它也可以自动解析它。 当为 Content-Type 定义一个解析器时,它将被解析,默认包含 “application / json” 和 “application / x-www-form-urlencoded” 。 解析的对象然后可以通过 res.body 获得。

响应头字段

res.header包含一个解析的头部字段的对象,和节点一样压缩字段名称。 例如 res.header ['content-length']

响应类型

Content-Type 响应头是特殊的,提供 res.type,这是没有字符集(如果有的话)。 例如,“text / html; charset = utf8” 的 Content-Type 将提供“text / html”作为“res.type”,然后“res.charset”属性将包含“utf8”。

响应状态

响应状态标志有助于确定请求是否成功以及其他有用信息,从而使 SuperAgent 成为与 RESTful Web 服务交互的理想选择。 这些标志目前定义为:

 var type = status / 100 | 0;

 // status / class
 res.status = status;
 res.statusType = type;

 // basics
 res.info = 1 == type;
 res.ok = 2 == type;
 res.clientError = 4 == type;
 res.serverError = 5 == type;
 res.error = 4 == type || 5 == type;

 // sugar
 res.accepted = 202 == status;
 res.noContent = 204 == status || 1223 == status;
 res.badRequest = 400 == status;
 res.unauthorized = 401 == status;
 res.notAcceptable = 406 == status;
 res.notFound = 404 == status;
 res.forbidden = 403 == status;

中止请求

要放弃请求,只需调用 req.abort() 方法。

超时

有时网络和服务器会“卡住”,接受请求后永远不会回应。 设置超时以避免请求永久等待。

  • req.timeout({deadline:ms})req.timeout(ms)(其中ms是毫秒数> 0)为整个请求(包括所有重定向)设置完成的最后期限.。如果在这段时间内没有完全下载响应,请求将被中止。
  • req.timeout({response:ms}) 设置等待来自服务器的第一个字节的最大时间, 但并不限制整个下载可以花费多长时间。响应超时应比服务器响应所需的时间要长数秒,因为它还包括进行DNS查找,TCP / IP和TLS连接的时间。

你应该使用 deadlineresponse 超时。 通过这种方式,你可以使用短暂的响应超时来快速检测到无响应的网络,并且可以在较慢的但可靠的网络上为下载提供时间。

request
  .get('/big-file?network=slow')
  .timeout({
    response: 5000,  // Wait 5 seconds for the server to start sending,
    deadline: 60000, // but allow 1 minute for the file to finish loading.
  })
  .then(function(res) {
    if (err.timeout) { /* timed out! */ }
  });

超时错误具有 .timeout 属性。

认证

在 Node 和浏览器中,通过 .auth() 方法提供的 auth 可用:

request
  .get('http://local')
  .auth('tobi', 'learnboost')
  .then(callback);

Node 客户端中,基本身份验证可以在URL中作为“user:pass”:

request.get('http://tobi:learnboost@local').then(callback);

默认情况下只使用 Basic auth。 在浏览器中,你可以添加 `{type:'auto'} 来启用浏览器内置的所有方法(Digest,NTLM等):

request.auth('digest', 'secret', {type:'auto'})

重定向 (Following redirects)

默认情况下最多会有5个重定向,但是你可以用 res.redirects(n) 方法指定:

request
  .get('/some.png')
  .redirects(2)
  .then(callback);

代理全局状态

保存cookies

在Node中,SuperAgent 默认不保存 cookie,但是你可以使用 .agent() 方法创建一个保存 cookie 的SuperAgent 副本。 每个副本都有一个单独的 cookie 。

const agent = request.agent();
agent
  .post('/login')
  .then(() => {
    return agent.get('/cookied-page');
  });

在浏览器中,Cookie 由浏览器自动管理,因此 .agent() 不会隔离cookie。

多个请求的默认选项

代理程序上调用的常规请求方法 (.use().set().auth())将用作该代理程序发出的所有请求的默认值。

const agent = request.agent()
  .use(plugin)
  .auth(shared);

await agent.get('/with-plugin-and-auth');
await agent.get('/also-with-plugin-and-auth');

管道数据

Node客户端允许您将数据传入和传出请求。 例如,传递一个文件的内容作为请求:

const request = require('superagent');
const fs = require('fs');

const stream = fs.createReadStream('path/to/my.json');
const req = request.post('/somewhere');
req.type('json');
stream.pipe(req);

请注意,当您管道请求时,superagent 会使用 chunked transfer encoding 发送管道数据,而不是所有服务器都支持(例如,Python WSGI服务器)。

或者将响应传递给一个文件:

const stream = fs.createWriteStream('path/to/my.json');
const req = request.get('/some.json');
req.pipe(stream);

请注意,您应该 尝试管道 .end()Response 对象的结果:

// Don't do either of these:
const stream = getAWritableStream();
const req = request
  .get('/some.json')
  // this pipes garbage to the stream and fails in unexpected ways
  .end((err, response) => response.pipe(stream))
const req = request
  .get('/some.json')
  .end()
  // this is also unsupported, .pipe calls .end for you.
  .pipe(stream);

在superagent的 未来版本 中,不正确地调用pipe()将会失败。

多部分请求

SuperAgent 对于提供 .attach().field() 方法的 building 多部分请求也非常给力。

当你使用 .field() 或者 .attach() 时,你不能使用 .send() 而你 不能 设置 'Content-Type'(正确的类型将会被(自动)设置)。

发送文件

要发送文件,请使用 .attach(name, [file], [options])。 您可以通过多次调用 .attach 来附加多个文件。 参数是:

  • name — 表单字段名称
  • file — 带有文件路径的字符串或者“Blob”/“Buffer”对象
  • options — (可选) 用自定义文件名字符串或者“{filename:string}”对象。 在Node中,还支持{contentType:'mime / type'}。 在浏览器中,用相应的类型创建一个“Blob”。

request
  .post('/upload')
  .attach('image1', 'path/to/felix.jpeg')
  .attach('image2', imageBuffer, 'luna.jpeg')
  .field('caption', 'My cats')
  .then(callback);

字段值

就像HTML中的表单字段一样,你可以用 .field(name,value).field({name:value}) 来设置字段值。 假设你想用你的名字和电子邮件上传几张图片,你的请求可能是这样的:

 request
   .post('/upload')
   .field('user[name]', 'Tobi')
   .field('user[email]', '[email protected]')
   .field('friends[]', ['loki', 'jane'])
   .attach('image', 'path/to/tobi.png')
   .then(callback);

压缩

节点客户端支持压缩响应,最重要的是,你不必做任何事情!它能用。

缓存响应

要强制缓存响应主体为 res.text,你可以调用 req.buffer()。 要撤消文本响应的默认缓存,例如 “text / plain”,“text / html”等,你可以调用 req.buffer(false)

当缓冲提供 res.buffered 标志时,你可以使用它来处理同一个回调中的缓存和无缓存响应。

CORS

出于安全原因,浏览器将阻止跨源请求,除非服务器选择使用CORS标头。 浏览器也会做额外的
OPTIONS 请求来检查服务器允许的HTTP头和方法。 阅读更多关于CORS

The .withCredentials() method enables the ability to send cookies from the origin, however only when Access-Control-Allow-Origin is not a wildcard (""), and Access-Control-Allow-Credentials is "true".
.withCredentials()方法使得能够从源站发送cookie,但是只有当'Access-Control-Allow-Origin'不是通配符(“
”)和Access-Control-Allow-Credentials 是 "true”时。

request
  .get('http://api.example.com:4001/')
  .withCredentials()
  .then(function(res) {
    assert.equal(200, res.status);
    assert.equal('tobi', res.text);
  })

错误处理

你的回调函数将始终传递两个参数:错误和响应。 如果没有错误发生,第一个参数将为null:

request
 .post('/upload')
 .attach('image', 'path/to/tobi.png')
 .then(function(res) {

 });

当你监听的时候,"error" 事件将被触发:

request
  .post('/upload')
  .attach('image', 'path/to/tobi.png')
  .on('error', handle)
  .then(function(res) {

  });

请注意,superagent默认考虑 4xx 和 5xx 响应(以及未处理的 3xx 响应)错误。 例如,如果你得到一个304 Not modified403 Forbidden500 Internal server error的响应,这个状态信息可以通过err.status获得。 来自这样的响应的错误还包含具有在 “响应属性” 中提及的所有属性的“err.response”字段。 库以这种方式处理想要成功响应和将HTTP错误状态码视为错误的常见情况,同时仍允许围绕特定错误条件的定制逻辑。

网络故障,超时以及其他不产生响应的错误将不包含“err.status”或“err.response”字段。

如果你希望处理404或其他HTTP错误响应,则可以查询“err.status”属性。 当发生HTTP错误(4xx或5xx响应)时,res.error属性是一个Error对象,这允许你执行如下检查:

if (err && err.status === 404) {
  alert('oh no ' + res.body.message);
}
else if (err) {
  // all other error types we handle generically
}

或者,您可以使用.ok(callback)方法来决定响应是否是错误的。 如果响应应该被解释为成功,则对“ok”函数的回调将得到一个响应并返回“true”。

request.get('/404')
  .ok(res => res.status < 500)
  .then(response => {
    // reads 404 page as a successful response
  })

进度跟踪

SuperAgent在上传和下载大文件时触发“progress”事件。

request.post(url)
  .attach('field_name', file)
  .on('progress', event => {
    /* the event is:
    {
      direction: "upload" or "download"
      percent: 0 to 100 // may be missing if file size is unknown
      total: // total file size, may be missing
      loaded: // bytes downloaded or uploaded so far
    } */
  })
  .end()

Promise 和 Generator 支持

SuperAgent 的请求是一个“可接受的”对象,它与 JavaScript promise 和 async /await 语法兼容。 如果你使用promises,请勿调用 .end()

co 这样的库或 koa 这样的 web 框架可以在任何 SuperAgent 方法上 "yield" :

const req = request
  .get('http://local')
  .auth('tobi', 'learnboost');
const res = yield req;

请注意,SuperAgent 期望全局 Promise 对象存在。 您需要在 Internet Explorer 或 Node.js 0.10 中使用 polyfill 来使用 promise 。

浏览器和节点版本

SuperAgent 有两个实现:一个用于Web浏览器(使用XHR),一个用于Node.JS(使用核心http模块)。 默认情况下,Browserify和WebPack将选择浏览器版本。

如果要使用WebPack编译Node.JS的代码,您必须在其配置中指定node target

在 electron 中使用浏览器版本

Electron 开发人员报告,如果您希望使用浏览器版本的 SuperAgent 而不是Node版本,则可以使用 require('superagent / superagent')。 您的请求现在将显示在Chrome开发人员工具“网络”标签中。 请注意,此环境不包含在自动化测试套件中,并且不受官方支持。

scrollIntoView引发的思考

起因

同事最近在开发h5页面的时候,遇到了点击输入框,而 输入框输入法 遮挡的问题。

这个问题只会只在 安卓手机 中出现。

想法

一出现这种问题,第一感觉就是认为页面的高度被改变压缩了。
首先想到的是用万能JavaScript去动态改变滚动高度(首先想到的是这两个方法scrollBy和scrollTo
搜索之后发现有这么两个东西

  • scrollIntoView

方法让当前的元素滚动到浏览器窗口的可视区域内

  • scrollIntoViewIfNeeded

是 Element.scrollIntoView() 的变体,如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动

解决方案

@Tao-Quixote 这个大兄弟已经说的很清楚了,具体见scrollIntoViewIfNeeded 与 scrollIntoView

下面摘录一部分:

根据MDN文档,Element.scrollIntoView是一个实验性质的方法,提示截图如下:

Element.scrollIntoViewIfNeeded是一个非标准的特性,建议不要在生产环境中使用,提示截图如下:

其实这两个特性,我个人感觉应用最多的场景应该是在移动端,当表单元素获得焦点的时候滚动到视野中,避免软键盘遮挡元素。鉴于Element.scrollIntoView无论设置什么参数,且不论是否在视野中都会滚动的特点,Element.scrollIntoViewIfNeeded特性应该比Element.scrollIntoView体验好。

虽然MDN文档中说Element.scrollIntoViewIfNeeded是非标准特性,但是在移动端的支持还是非常好的,以下截图来自caniuse:

但是反观Element.scrollIntoView这个实验特性,在移动端的支持并不是特别好,以下截图来自caniuse:

根据上面的比较应该可以得出结论:在移动避免软键盘遮挡表单元素时,应该使用Element.scrollIntoViewIfNeeded特性来实现元素滚动到视野中,不论是体验还是浏览器支持,该特性都好于Element.scrollIntoView特性。

scrollIntoViewIfNeeded不起作用

你以为这就完了?想法太美好!
在实际的使用中,发现scrollIntoViewIfNeeded有时候会不起作用!
这里需要注意一点:就是focus事件和键盘弹起是有交互时间的。
执行scrollIntoViewscrollIntoViewIfNeeded的时机应该是在键盘弹出之后。

// 伪代码
setTimeout(() => {
    element.scrollIntoView()
}, 400);

scrollBy和scrollTo区别

  • scrollBy() 方法可把内容滚动指定的像素数。
  • scrollTo() 方法可把内容滚动到指定的坐标。
    含义的描述很明显就能看出区别了,一个在现有基础上的增/减量,是一种相对的概念。另外一个是绝对量。

滚动的平滑度问题

无论是scrollBy还是scrollTo,他们到达制定的位置都非常生硬(瞬达),没有过渡的效果。
smoothscroll 这个库就很好的处理了这种情况。
具体的效果看这里:smoothscroll演示

当然作为一个非常好奇的人,就很想看看它是怎么在400多行代码中实现的这个效果,因为以前来说这种效果会用setTimeout这种方法来实现,虽然效率不是很好,但是最起码能实现这种功能。
这里我们能找到它使用requestAnimationFrame方法实现的。

参考:

Content-Security-Policy的理解

Content-Security-Policy介绍

Content-Security-Policy 中文的意思是 网页安全政策,主要用来防止XSS攻击。

如何防范XSS攻击的

通过 Content-Security-Policy 网页的开发者可以控制整个页面中 外部资源 的加载和执行。

比如可以控制哪些 域名下的静态资源可以被页面加载,哪些不能被加载。

这样就可以很大程度的防范了 来自 跨站(域名不同) 的脚本攻击。

它还有个非常响亮的称号:XSS攻击的终结者😄

如何使用

对于页面开发者来说,只需要在页面中设置

<meta http-equiv="Content-Security-Policy" content="">

类别

key 作用
default-src 给下面所有的规则设定一个默认值
script-src 外部脚本
style-src 样式表
img-src 图像
media-src 媒体文件(音频和视频)
font-src 字体文件
object-src 插件(比如 Flash)
child-src 框架
frame-ancestors 嵌入的外部资源(比如、<iframe>、和)
connect-src HTTP 连接(通过 XHR、WebSockets、EventSource等)
worker-src worker脚本
manifest-src manifest 文件

script-src

script-src 作用
unsafe-inline 允许执行页面内嵌的<script>标签和事件监听函数
unsafe-eval 允许将字符串当作代码执行,比如使用eval、setTimeout、setInterval和Function等函数
nonce 每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行
hash 列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行

value 作用
主机名 example.org,https://example.com:443...
路径名 example.org/resources/js/
通配符 .example.org,://.example.com:(表示任意协议、任意子域名、任意端口)
协议名 https:、data:
关键字'self' 当前域名,需要加引号
关键字'none' https:、data:
协议名 禁止加载任何外部资源,需要加引号

下面来个具体的例子:

<meta http-equiv="Content-Security-Policy" content="
default-src http: https:  *.meituan.com 'self' 'unsafe-inline' ;
style-src 'self' 'unsafe-inline' *.meituan.com;
script-src 'self' 'unsafe-inline' 'unsafe-eval' ;
">
  • 默认设置(default-src):信任 http ,https协议资源,信任当前域名资源,信任符合*.meituan.com的域名资源
  • CSS设置(style-src):信任当前域名资源,允许内嵌的CSS资源,信任来自*.meituan.com下的CSS资源
  • JS设置(script-src):信任当前域名资源,允许内嵌的JS执行,允许将字符串当作代码执行

参考

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.