先来回答一下下面这个问题:对于 setTimeout(function() { console.log('timeout') }, 1000) 这一行代码,你从哪里可以找到 setTimeout 的源代码(同样的问题还会是你从哪里可以看到 setInterval 的源代码)?

很多时候,可以我们脑子里面闪过的第一个答案肯定是 V8 引擎或者其它 VM们,但是要知道的一点是,所有我们所见过的 Javascript 计时函数,都没有出现在 ECMAScript 标准中,也没有被任何 Javascript 引擎实现,计时函数,其实都是由浏览器(或者其它运行时,比如 Node.js)实现的,并且,在不同的运行时下,其表现形式有可能都不一致

在浏览器中,主计时器函数是 Window 接口的一部分,这保证了包括如 setTimeoutsetInterval 等计时器函数以及其它函数和对象能被全局访问,这才是你可以随时随地使用 setTimeout 的原因。同样的,在 Node.js 中,setTimeoutglobal 对象的一部分,这拿得你也可以像在浏览器里面一样,随时随地的使用它。

到现在可能会有一些人感觉这个问题其实并没有实际的价值,但是作为一个 Javascript 开发者,如果不知道本质,那么就有可能不能完全的理解 V8 (或者其它VM)是到底是如何与浏览器或者 Node.js 相互作用的。

暂缓一个函数的执行

计时器函数都是更高阶的函数,它们可以用于暂缓一个函数的执行,或者让一个函数重复执行(由他们的第一个参数执行需要执行的函数)。

下面这是一个暂缓执行的示例:

setTimeout(() => {
  console.log('距离函数的调用,已经过去 4 秒了')
}, 4 * 1000)

在上面的示例中, setTimeoutconsole.log 的执行暂缓了 4 * 1000 毫秒,也就是 4 秒钟, setTimeout 的第一个函数,就是需要暂缓执行的函数,它是一个函数的引用,下面这个示例是我们更加常见到的写法:

const fn = () => {
  console.log('距离函数的调用,已经过去 4 秒了')
}

setTimeout(fn, 4 * 1000)

传递参数

如果被 setTimeout 暂缓的函数需要接收参数,我们可以从第三个参数开始添加需要传递给被暂缓函数的参数:

const fn = (name, gender) => {
  console.log(`I'm ${name}, I'm a ${gender}`)
}

setTimeout(fn, 4 * 1000, 'Tao Pan', 'male')

上面的 setTimeout 调用,其结果与下面这样调用类似:

setTimeout(() => {
  fn('Tao Pan', 'male')
}, 4 * 1000)

但是记住,只是结果类似,本质上是不一样的,我们可以用伪代码来表示 setTimeout 的函数实现:

const setTimeout = (fn, delay, ...args) => {
  wait(delay) // 这里表示等待 delay 指定的毫秒数
  fn(...args)
}

挑战一下

编写一个函数:

  • delay 为 4 秒的时候,打印出:距离函数的调用,已经过去 4 秒了
  • delay 为 8 秒的时候,打印出:距离函数的调用,已经过去 8 秒了
  • delay 为 N 秒的时候,打印出:距离函数的调用,已经过去 N 秒了

下面这个是我的一个实现:

const delayLog = delay => {
  setTimeout(console.log, delay * 1000, `距离函数的调用,已经过去 ${delay} 秒了`)
}

delayLog(4) // 输出:距离函数的调用,已经过去 4 秒了
delayLog(8) // 输出:距离函数的调用,已经过去 8 秒了

我们来理一下 delayLog(4) 的整个执行过程:

  1. delay = 4
  2. setTimeout 执行
  3. 4 * 1000 毫秒后, setTimeout 调用 console.log 方法
  4. setTimeout 计算其第三个参数 距离函数的调用,已经过去 ${delay} 秒了 得到 距离函数的调用,已经过去 4 秒了
  5. setTimeout 将计算得到的字符串当作 console.log 的第一个参数
  6. console.log('距离函数的调用,已经过去 4 秒了') 执行,输出结果

规律性重复一个函数的执行以及停止重复调用

如果我们现在要每 4 秒第印一次呢?这里面就有很多种实现方式了,假如我们还是使用 setTimeout 来实现,我们可以这样做:

const loopMessage = delay => {
  setTimeout(() => {
    console.log('这里是由 loopMessage 打印出来的消息')
    loopMessage(delay)
  }, delay * 1000)
}

loopMessage(1) // 此时,每过 1 秒钟,就会打印出一段消息:*这里是由 loopMessage 打印出来的消息*

但是这样有一个问题,就是开始之后,我们就没有办法停止,怎么办?可以稍稍改改实现:

let loopMessageTimer

const loopMessage = delay => {
  loopMessageTimer = setTimeout(() => {
    console.log('这里是由 loopMessage 打印出来的消息')
    loopMessage(delay)
  }, delay * 1000)
}

loopMessage(1)

clearTimeout(loopMessageTimer) // 我们随时都可以使用 `clearTimeout` 清除这个循环

但是这样还是有问题的,如果 loopMessage 被调用多次,那么他们将共用一个 loopMessageTimer,清除一个,将清除所有,这是肯定不行的,所以,还得再改造一下:

const loopMessage = delay => {
  let timer
  
  const log = () => {
    timer = setTimeout(() => {
      console.log(`每 ${delay} 秒打印一次`)
      log()
    }, delay * 1000)
  }

  log()

  return () => clearTimeout(timer)
}

const clearLoopMessage = loopMessage(1)
const clearLoopMessage2 = loopMessage(1.5)

clearLoopMessage() // 我们在任何时候都可以取消任何一个重复调用,而不影响其它的

这…… 实现是实现了,但是其它有更好的解决办法:

const timer = setInterval(console.log, 1000, '每 1 秒钟打印一次')

clearInterval(timer) // 随时可以 `clearInterval` 清除

更加深入了认识取消计时器(Cancel Timers)

上面的示例只是简单的给我们展现了 setTimeout 以及 setInterval,也看到了,我们可以通过 clearTimeout 或者 clearInterval 取消计时器,但是关于计时器,远远不止这点知识,请看下面的代码(请):

const cancelImmediate = () => {
  const timerId = setTimeout(console.log, 0, '暂缓了 0 秒执行')
  clearTimeout(timerId)
}

cancelImmediate() // 这里并不会有任何输出

或者看下面这样的代码:

const cancelImmediate2 = () => setTimeout(console.log, 0, '暂缓了 0 秒执行')

const timerId = cancelImmediate2()

clearTimeout(timerId)

请将上面的的任一代码片段同时复制到浏览器的控制台中(有多行复制多行)执行,你会发现,两个代码片段都没有任何输出,这是为什么?

这是因为,Javascript 的运行机制导致,任何时刻都只能存在一个任务在进行,虽然我们调用的是暂缓 0 秒,但是,由于当前的任务还没有执行完成,所以,setTimeout 中被暂缓的函数即使时间到了也不会被执行,必须等到当前的任务完全执行完成,那么,再试着,上面的代码分行复制到控制台,看看结果是不是会打印出 暂缓了 0 秒执行 了?答案是肯定的。

当你一行一行复制执行的时候, cancelImmediate2 执行完成之后,当前任务就已经全部执行完成了,所以开始执行下一个任务(console.log 开始执行)。

从上面的示例中,我们可以看出,setTimeout 其实是将一个任务安排进一个 Javascript 的任务队列里面去,当前面的所有任务都执行完成之后,如果这个任务时间到了,那么就立即执行,否则,继续等待计时结束。

此时,你应该发现,只要是 setTimeout 所暂缓的函数没有被执行(任务还没有完成),那么,我们就可以随时使用 clearTimeout 清除掉这个暂缓(将这条任务从队列里面移除)

计时器是没有任何保证的

通过前面的例子,我们知道了 setTimeoutdelay0 时,并不表示立马就会执行了,它必须等到所有的当前任务(对于一个 JS 文件来讲,就是需要执行完当前脚本中的所有调用)执行完成之后都会执行,而这里面就包括我们调用的 clearTimeout

下面用一个示例来更清楚了说明这个问题:

setTimeout(console.log, 1000, '1 秒后执行的')

// 开始时间
const startTime = new Date()
// 距离开始时间已经过去几秒
let secondsPassed = 0
while (true) {
  // 距离开始时间的毫秒数
  const duration = new Date() - startTime
  // 如果距离开始时间超过 5000 毫秒了, 则终止循环
  if (duration > 5000) {
    break
  } else {
    // 如果距离开始时间增长一秒,更新 secondsPassed
    if (Math.floor(duration / 1000) > secondsPassed) {
      secondsPassed = Math.floor(duration / 1000)
      console.log(`已经过去 ${secondsPassed} 秒了。`)
    }
  }
}

你们猜上面这段代码会有什么样的输出?是下面这样的吗?

1 秒后执行的
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。

并不是这样的,而是下面这样的:

已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
1 秒后执行的

怎么会这样?这是因为 while(true) 这个循环必须要执行超过 5 秒钟的时间之后,才算当前所有任务完成,在它 break 之前,其它所有的操作都是没有用的,当然,我们不会在开发的过程中去写这样的代码,但是并不表示就不存在这样的情况,想象以下下面这样的场景:

setTimeout(somethingMustDoAfter1Seconds, 1000)

openFileSync('file more then 1gb')

这里面的 openFileSync 只是一个伪代码,它表示我们需要同步进行一个特别费时的操作,这个操作很有可能会超过 1 秒,甚至更长的时间,但是上面那个 somethingMustDoAfter1Seconds 将一直处于挂起状态,只要这个操作完成,它才有可能执行,为什么叫有可能?那是因为,有可能还有别的任务又会占用资源。所以,我们可以将 setTimeout 理解为:计时结束是执行任务的必要条件,但是不是任务是否执行的决定性因素

setTimeout(somethingMustDoAfter1Seconds, 1000) 的意思是,必须超过 1000 毫秒后,somethingMustDoAfter1Seconds 才允许执行。

再来一个小挑战

那如果我需要每一秒钟都打印一句话怎么办?从上面的示例中,已经很明显的看到了,setTimeout 是肯定解决不了这个问题了,不信我们可以试试下面这个代码片段:

const log = (delay) => {
  timer = setTimeout(() => {
    console.log(`每 ${delay} 秒打印一次`)
    log(delay)
  }, delay * 1000)
}

log(1)

上面的代码是没有任何问题的,在浏览器的控制台观察,你会发现确实每一秒钟都打印了一行,但是再试试下面这样的代码:

const log = (delay) => {
  timer = setTimeout(() => {
    console.log(`每 ${delay} 秒打印一次`)
    log(delay)
  }, delay * 1000)
}

const readLargeFileSync = () => {
  // 开始时间
  const startTime = new Date()
  // 距离开始时间已经过去几秒
  let secondsPassed = 0
  while (true) {
    // 距离开始时间的毫秒数
    const duration = new Date() - startTime
    // 如果距离开始时间超过 5000 毫秒了, 则终止循环
    if (duration > 5000) {
      break
    } else {
      // 如果距离开始时间增长一秒,更新 secondsPassed
      if (Math.floor(duration / 1000) > secondsPassed) {
        secondsPassed = Math.floor(duration / 1000)
        console.log(`已经过去 ${secondsPassed} 秒了。`)
      }
    }
  }
}

log(1)

setTimeout(readLargeFileSync, 1300)

输出结果是:

每 1 秒打印一次
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
每 1 秒打印一次
  1. 第一秒的时候, log 执行
  2. 第 1300 毫秒时,开始执行 readLargeFileSync 这会需要整整 5 秒钟的时间
  3. 第 2 秒的时候,log 执行时间到了,但是当前任务并没有完成,所以,它不会打印
  4. 第 5 秒的时候, readLargeFileSync 执行完成了,所以 log 继续执行
关于这个具体怎么实现,就不在本文讨论了

最终,到底是谁在调用那个被暂缓的函数?

当我们在一个 function 中调用 this 时,this 关键字会指向当前函数的 caller

function whoCallsMe() {
  console.log('My caller is: ', this)
}

当我们在浏览器的控制台中调用 whoCallsMe 时,会打印出 Window,当在 Node.js 的 REPL 中执行时,会执行出 global,如果我们将 whoCallsMe 设置为一个对象的属性:

function whoCallsMe() {
  console.log('My caller is: ', this)
}

const person = {
  name: 'Tao Pan',
  whoCallsMe
}

person.whoCallsMe()

这会打印出:My caller is: Object { name: "Tao Pan", whoCallsMe: whoCallsMe() }

那么?

function whoCallsMe() {
  console.log('My caller is: ', this)
}

const person = {
  name: 'Tao Pan',
  whoCallsMe
}

setTimeout(person.whoCallsMe, 0)

这会打印出什么?这个很容易被忽视的问题,其实真的值得我们去思考。

请直接将上面这个代码片段复制进浏览器的控制台,看执行的结果:

My caller is:  Window https://pub.ofcrab.com/admin/write-post.php?cid=2952

再打开系统终端,进入 Node.js REPL 中,执行同样的代码,看执行结果:

My caller is:  Timeout {
  _idleTimeout: 1,
  _idlePrev: null,
  _idleNext: null,
  _idleStart: 7052,
  _onTimeout: [Function: whoCallsMe],
  _timerArgs: undefined,
  _repeat: null,
  _destroyed: false,
  [Symbol(refed)]: true,
  [Symbol(asyncId)]: 221,
  [Symbol(triggerId)]: 5
}

回到这句话:当我们在一个 function 中调用 this 时,this 关键字会指向当前函数的 caller,当我们使用 setTimeout 时,这个 caller 是跟当前的运行时有关系的,如果我想 this 总是指向 person 对象呢?

function whoCallsMe() {
  console.log('My caller is: ', this)
}

const person = {
  name: 'Tao Pan'
}
person.whoCallsMe = whoCallsMe.bind(person)

setTimeout(person.whoCallsMe, 0)

结语

标题是写上了 你需要知道的一切都在这里,但是如果有什么没有考虑到了,欢迎大家指出。

本文列举了一些日常会使用到的 Javascript技巧,可以明显提升代码的表现力。

解构赋值

首先,我们来看一下下面这段代码:

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

console.log(animal.type.mammal.bear) // 输出:{ age: 12 }
console.log(animal.type.mammal.deer) // 输出:{ age: 4 }

对象解构赋值

其实我们可以利用解构赋值做得更好:

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

const { bear, deer } = animal.type.mammal
console.log(bear) // 输出:{ age: 12 }
console.log(deer) // 输出:{ age: 4 }

不管上面哪种实现方式,我们都使用的 const,这表示这些被定义的变量不允许再被赋值,我们推荐 在编写 Javascript 代码时,尽可能的使用 const,除非这个变量确实需要被多次赋值,比如,年龄是可以增长的:

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

const { age } = animal.type.mammal.bear

age += 1 // 这里会报错,因为 age 是一个 const 变量

在这种情况下,我们可以将 const 改为 let

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

let { age } = animal.type.mammal.bear

age += 1
console.log(age) // 输出:13

接下来,我们给每一个 animal 增加一个姓名字段 name,然后,同时使用 letconst,任何动物年龄是会增长的,但是姓名不允许修改:

const animal = {
  type: {
    mammal: {
      bear: {
        name: 'Bug',
        age: 12
      },
      deer: {
        name: 'Debug',
        age: 4
      }
    }
  }
}

const { name } = animal.type.mammal.bear
let { age } = animal.type.mammal.bear

age += 1
console.log(age) // 输出:13
console.log(name) // 输出:Bug

数组解构赋值

我们现在有三只动物,有一个数组保存了它们的名字:

const animalNames = ['Bug', 'Debug', 'Bugfix']
const [bug, debug, bugfix] = animalNames
console.log(debug) // 输出:Debug

解构赋值时重命名

我们还有可能有这样的需求,我想同时拿到上面示例中 animal 那个对象中,两只动物的姓名,这个时候我们可以完全按照对象的结构去解构它:

const animal = {
  type: {
    mammal: {
      bear: {
        name: 'Bug',
        age: 12
      },
      deer: {
        name: 'Debug',
        age: 4
      }
    }
  }
}

const { bear: { name }, deer: { name }} = animal.type.mammal

上面的意思是:从 animal.type.mammal 对象中,访问 bear,并拿到它的 name 的值,并赋值给一个 const 变量,变量名为 name,同时再从 deer 中拿到 name 的值,赋值给一个名为 nameconst 变量。

看出问题来了吧? name 被声明了两次,这是不允许的,此时,我们可以在声明时,为两个 name 指定不同的名称:

const animal = {
  type: {
    mammal: {
      bear: {
        name: 'Bug',
        age: 12
      },
      deer: {
        name: 'Debug',
        age: 4
      }
    }
  }
}

const { bear: { name: bearName }, deer: { name: deerName }} = animal.type.mammal
console.log(bearName) // 输出:Bug
console.log(deerName) // 输出:Debug
数组的解构中同样支持重命名。

箭头函数

箭头函数可以大大减少编码工作量,但是它们并非普通函数的完全替代者,先来看看下面的代码:

const animals = ['Bug', 'Debug', 'Bugfix']

animals.forEach(function (animal) {
  console.log(animal)
})

我们使用箭头函数改写上面的代码:

const animals = ['Bug', 'Debug', 'Bugfix']

animals.forEach(animal => {
  console.log(animal)
})

这是一个简单的示例,只是为了证明箭头函数能让我们的代码更清晰可读,编码量也能大大减少,有一个不成文的经验是,一个项目的代码量越少,维护的成本一般情况下,都会越低,那为了证明箭头函数确实有用,我们再来看一个更复杂点的例子:

function multiplyAndAdd(multiply) {
  const pow = multiply ** multiply
  return function (number) {
    return pow + number
  }
}

const result = multipleAndAdd(3)(5) // 等于:3 ** 3 + 5 = 27 + 5 = 32

console.log(result) // 输出:32

用箭头函数再来改写一次:

const multiplyAndAdd = multiply => {
  const pow = multiply ** multiply
  return number => pow + number
}

如果熟练的话,我一般会这么写:

const multiplyAndAdd = multiply => number => multiply ** multiply + number

这里面可以这么阅读:

  • 声明一个 const 值:multiplyAndAdd,它的值为 multiply => number => multiply ** multiply + number,这个都很好理解
  • 这个值是一个箭头函数,该函数接受一个名为 multiply 的参数,返回 number => multiply ** multiply + number
  • 它的返回值还是一个箭头函数,这个箭头函数接受一个 number 参数,返回 multiply ** multiply + number
这么写可能会提升阅读难度,但是确实能节省代码量,但是个人还是不太推荐在多人协作的项目里面大量使用这样的写法。

原文:https://pub.ofcrab.com/press/instant-post-message-between-wechat-mini-program-webview-h5.html

在做 React Native 应用时,如果需要在 App 里面内嵌 H5 页面,那么 H5 与 App 之间可以通过 Webview 的 PostMessage 功能实现实时的通讯,但是在小程序里面,虽然也提供了一个 webview 组件,但是,在进行 postMessage 通讯时,官方文档里面给出了一条很变态的说明:

网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail = { data }data 是多次 postMessage 的参数组成的数组

这里面已经说的很明白了,不管我们从 H5 页面里面 postMessage 多少次,小程序都是收不到的,除非:

  1. 用户做了回退到上一页的操作
  2. 组件销毁
  3. 用户点击了分享

这里面其实我没有完全说对,官方其实说的是 小程序后退,并没有说是用户做回退操作,经过我的实测,确实人家表达得很清楚了,我们通过微信官方的SDK调起的回退也是完全可行的:

wx.miniProgram.navigateBack()

大体思路

从上面的分析和实测中我们可以知道,要实现无需要用户操作即可完成的通讯,第三种情况我们是完全不需要考虑了的,那么来仔细考虑第 1 和第 2 种场景。

第 1 种方式:回退

当我们想通过网页向小程序发送数据,同时还可以回退到上一个页面时,我们可以在 wx.miniProgram.postMessage 之后,立马调用一次 wx.miniProgram.navigateBack(),此时小程序的操作是:

  1. 处理 postMessage 信息
  2. 回退到上一页

我们在处理 postMessage 的时候做一些特殊操作,可以将这些数据保存下来

第 2 种方式:组件销毁

这是我感觉最合适的一种方式,可以让小程序拿到数据,同时还保留在当前页面,只需要销毁一次 webview 即可,大概的流程就是:

  1. 小程序 postMessage
  2. 小程序 navigateTo 将小程序页面导向一个特殊的页面
  3. 小程序的那个特殊页面立马回退到 webview 所在的页面
  4. webview 所在的页面的 onShow 里面,做一次处理,将 webview 销毁,然后再次打开
  5. 触发 onMessage 拿到数据
  6. H5 页面再次被打开

这种方式虽然变态,但是至少可以做到实时拿到数据,同时还保留在当前 H5 页面,唯一需要解决的是,在做这整套操作前,H5 页面需要做好状态的缓存,要不然,再次打开之后,H5 的数据就清空了。

第 1 种方式:通过回退,将数据提交给小程序之后传递给 webview 的上一页面

这种方式实现起来其实是很简单的,我们现在新建两个页面:

sandbox/canvas-by-webapp/index.js

const app = getApp();

Page({
  data: {
    url: '',
    dimension: null,
    mime: '',
  },
  handleSaveTap: function() {
    wx.navigateTo({
      url: '/apps/browser/index',
      events: {
        receiveData: data => {
          console.log('receiveData from web browser: ', data);
          if (typeof data === 'object') {
            const { url, mime, dimension } = data;
            if (url && mime && dimension) {
              this.setData({
                url,
                dimension,
                mime,
              });

              this.save(data);
            }
          }
        }
      }
    })
  },

  save: async function({ url, mime, dimension }) {
    try {
      await app.saveImages([url]);
      app.toast('保存成功!');
    } catch (error) {
      console.log(error);
      app.toast(error.message || error);
    }
  },
});

上面的代码中,核心点,就在于 wx.navigateTo 调用时,里面的 events 参数,这是用来进行与 /apps/browser/index 页面通讯,接收数据用的。

apps/browser/index.js

我省略了绝大多数与本文无关的代码,保存最主要的三个:

Page({
  onLoad() {
    if (this.getOpenerEventChannel) {
      this.eventChannel = this.getOpenerEventChannel();
    }
  },
  handleMessage: function(message) {
    const { action, data } = message;
    if (action === 'postData') {
      if (this.eventChannel) {
        this.eventChannel.emit('receiveData', data);
      }
    }
  },

  handlePostMessage: function(e) {
    const { data } = e.detail;
    if (Array.isArray(data)) {
      const messages = data.map(item => {
        try {
          const object = JSON.parse(item);
          this.handleMessage(object);
          return object;
        } catch (error) {
          return item;
        }
      });

      this.setData({
        messages: [...messages],
      });
    }
  },
})

其实,onLoad 方法中,我们使用了自微信 SDK 2.7.3 版本开始提供的 getOpenerEventChannel 方法,它可以创建一个与上一个页面的事件通讯通道,这个我们会在 handleMessage 中使用。

handlePostMessage 就是被 bindmessagewebview 上面的方法,它用于处理从 H5 页面中 postMessage 过来的消息,由于小程序是将多次 postMessage 的消息放在一起发送过来的,所以,与其它的Webview不同点在于,我们拿到的是一个数组: e.detail.datahandlePostMessage 的作用就是遍历这个数组,取出每一条消息,然后交由 handleMessage 处理。

handleMessage 在拿到 message 对象之后,将 message.actionmessage.data 取出来(*这里需要注意,这是我们在 H5 里面的设计的一种数据结构,你完全可以在自己的项目中设计自己的结构),根据 action 作不同的操作,我在这里面的处理是,当 action === 'postData' 时,就通过 getOpenerEventChannel 得到的消息通道 this.eventChannel 将数据推送给上一级页面,也就是 /sandbox/canvas-by-webapp,但是不需要自己执行 navigateBack ,因为这个需要交由 H5 页面去执行。

H5 页面的实现

我的 H5 主要就是使用 html2canvas 库生成 Canvas 图(没办法,自己在小程序里面画太麻烦了),但是这个不在本文讨论过程中,我们就当是已经生成了 canvas 图片了,将其转为 base64 文本了,然后像下面这样做:

wx.miniProgram.postMessage({
  data: JSON.stringify({
    action: 'postData',
    data: 'BASE 64 IMAGE STRING'
  })
});

wx.miniProgram.navigateBack()

将数据 postMessage 之后,立即 navigateBack() ,来触发一次回退,也就触发了 bindmessage 事件。

使用销毁 webview 实现实时通讯

接下来,咱就开始本文的重点了,比较变态的方式,但是也没想到更好的办法,所以,大家将就着交流吧。

H5 页面的改变

wx.miniProgram.postMessage({
  data: JSON.stringify({
    action: 'postData',
    data: 'BASE 64 IMAGE STRING'
  })
});

wx.miniProgram.navigateTo('/apps/browser/placeholder');

H5 页面只是将 wx.miniProgram.navigateBack() 改成了 wx.miniProgram.navigateTo('/apps/browser/placeholder') ,其它的事情就先都交由小程序处理了。

/apps/browser/placeholder

这个页面的功能其实很简单,当打开它了之后,做一点点小操作,立马回退到上一个页面(就是 webview 所在的页面。

Page({
  data: { loading: true },
  onLoad(options) {

    const pages = getCurrentPages();

    const webviewPage = pages[pages.length - 2];

    webviewPage.setData(
      {
        shouldReattachWebview: true
      },
      () => {
        app.wechat.navigateBack();
      }
    );
  },
});

我们一行一行来看:

const pages = getCurrentPages();

这个可以拿到当前整个小程序的页面栈,由于这个页面我们只允许从小程序的 Webview 页面过来,所以,它的上一个页面一定是 webview 所在的页面:

const webviewPage = pages[pages.length - 2];

拿到 webviewPage 这个页面对象之后,调用它的方法 setData 更新一个值:

    webviewPage.setData(
      {
        shouldReattachWebview: true
      },
      () => {
        app.wechat.navigateBack();
      }
    );

shouldReattachWebview 这个值为 true 的时候,表示需要重新 attach 一次 webview,这个页面的事件现在已经做完了,回到 webview 所在的页面

apps/browser/index.js 页面

我同样只保留最核心的代码,具体的逻辑,我就直接写进代码里面了。

Page({
  data: {
    shouldReattachWebview: false, // 是否需要重新 attach 一次 webview 组件
    webviewReattached: false,     // 是否已经 attach 过一次 webview 了
    hideWebview: false            // 是否隐藏 webview 组件
  },
  onShow() {
    // 如果 webview 需要重新 attach 
    if (this.data.shouldReattachWebview) {
      this.setData(
        {
          // 隐藏 webview
          hideWebview: true,
        },
        () => {
          this.setData(
            {
              // 隐藏之后立马显示它,此时完成一次 webview 的销毁,拿到了 postMessage 中的数据
              hideWebview: false,
              webviewReattached: true,
            },
            () => {
              // 拿到数据之后,处理 canvasData
              this.handleCanvasData();
            }
          );
        }
      );
    }
  },
  // 当 webview 被销毁时,该方法被触发
  handlePostMessage: function(e) {
    const { data } = e.detail;
    if (Array.isArray(data)) {
      const messages = data.map(item => {
        try {
          const object = JSON.parse(item);
          this.handleMessage(object);
          return object;
        } catch (error) {
          return item;
        }
      });

      this.setData({
        messages: [...messages],
      });
    }
  },
  // 处理每一条消息
  handleMessage: function(message) {
    const {action, data} = message
    // 如果 saveCanvas action
    if (action === 'saveCanvas') {
      // 将数据先缓存进 Snap 中
      const { canvasData } = this.data;
      // app.checksum 是我自己封装的方法,计算任何数据的 checksum,我拿它来当作 key
      // 这可以保证同一条数据只会被处理一次
      const snapKey = app.checksum(data);
      // 只要未处理过的数据,才需要再次数据
      if (canvasData[snapKey] !== true) {
        if (canvasData[snapKey] === undefined) {
          // 将数据从缓存进 `snap` 中
          // 这也是我自己封装的一个方法,可以将数据缓存起来,并且只能被读取一次
          app.snap(snapKey, data);
          // 设置 canvasData 中 snapKey 字段为 `false`
          canvasData[snapKey] = false;
          this.setData({
            canvasData,
          });
        }
      }
    }
  },
  // 当 webview 被重新 attach 之后,canvas 数据已经被保存进 snap 中了,
  handleCanvasData: async function handleCanvasData() {
    const { canvasData } = this.data;
    // 从 canvasData 中拿到所有的 key,并过滤到已经处理过的数据
    const keys = Object.keys(canvasData).filter(key => canvasData[key] === false);

    if (keys.length === 0) {
      return;
    }

    for (let i = 0; i < keys.length; i += 1) {
      try {
        const key = keys[i];
        const { url } = app.snap(key);
        // 通过自己封装的方法,将 url(也就是Base64字符)保存至相册
        const saved = await app.saveImages(url);
        // 更新 canvasData 对象
        canvasData[key] = true
        this.setData({
          canvasData
        })
        console.log('saved: ', saved);
      } catch (error) {
        app.toast(error.message);
        return;
      }
    }
  },
})

对应的 index.wxml 文件内容如下:

<web-view src="{{src}}" wx:if="{{src}}" bindmessage="handlePostMessage" wx:if="{{!hideWebview}}" />

流程回顾与总结

  1. 打开 webview 页面,打开 h5
  2. h5 页面生成 canvas 图,并转为 base64 字符
  3. 通过 wx.miniProgram.postMessagebase64 发送给小程序
  4. 调用 wx.miniProgram.navigateTo 将页面导向一个特殊页面
  5. 在特殊页面中,将 webview 所在页面的 shouldReattachWebview 设置为 true
  6. 在特殊页面中回退至 webview 所在页面
  7. webview 所在页面的 onShow 事件被触发
  8. onShow 事件检测 shouldReattachWebview 是否为 true,若为 true
  9. hideWebview 设置为 true,引起 web-view 组件的销毁
  10. handlePostMessage 被触发,解析所有的 message 之后交给 handleMessage 逐条处理
  11. handleMessage 发现 action === 'saveCanvas' 的事件,拿到 data
  12. 根据 data 计算 checksum ,以 checksumkey 缓存下来数据,并将这个 checksum 保存到 canvasData 对象中
  13. 此时 hideWebviewonShow 里面 setData 的回调中的 setData 重新置为 falseweb-view 重新加 attach,H5页面重新加载
  14. webview 重新 attach 之后, this.handleCanvasData 被触发,
  15. handleCanvasData 检测是否有需要保存的 canvas 数据,如果有,保存,修改 canvasData 状态

整个流程看旧去很繁琐,但是写起来其实还好,这里面最主要的是需要注意,数据去重,微信的 postMessage 里面拿到的永远 都是 H5 页面从被打开到关闭的所有数据。

原文:https://pub.ofcrab.com/press/react-native-deep-linking-for-ios-android.html
代码:https://github.com/pantao/react-native-deep-linking-example

我们生活在一个万物兼可分享的年代,而分享的过程,几乎最终都会分享某一个链接,那么,作为开发者,最常遇到的问题中应该包括如何通过一个URL地址快速的打开App,并导航至特定的页面。

什么是深度链接(Deep Link)

深度链接是一项可以让一个App通过一个URL地址打开,之后导航至特定页面或者资源,或者展示特定UI的技术,Deep 的意思是指被打开的页面或者资源并不是App的首页,最常使用到的地方包括但远远不限于 Push Notification、邮件、网页链接等。

其实这个技术在很久很久以前就已经存在了,鼠标点击一下 mailto:pantao@parcmg.com 这样的链接,系统会打开默认的邮件软件,然后将 pantao@parcmg.com 这个邮箱填写至收件人输入栏里,这就是深度链接。

本文将从零开始创建一个应用,让它支持通过一个如 deep-linking://articles/{ID} 这样的 URL 打开 文章详情 页面,同时加载 {ID} 指定的文章,比如:deep-linking://articles/4 将打开 ID4 的文章详情页面。

深度链接解决了什么问题?

网页链接是无法打开原生应用的,如果一个用户访问你的网页中的某一个资源,他的手机上面也已经安装了你的应用,那么,我们要如何让系统自动的打开应用,然后在应用中展示用户所访问的那一个页面中的资源?这就是深度链接需要解决的问题。

实现深度链接的不同方式

有两种方式可以实现深度链接:

  • URL scheme
  • Universal links

前端是最常见的方式,后者是 iOS 新提供的方式,可以一个普通的网页地址链接至App的特定资源。

本文将创建一个名为 DeepLinkingExample 的应用,使得用户可以通过打开 deep-linking://home 以及 deep-linking://articles/4 分别打开 App 的首页以及 App 中 ID 为 4 的文章详情页面。

react-native init DeepLinkingExample
cd DeepLinkingExample

安装必要的库

紧跟 TypeScript 大潮流,我们的 App 写将使用 TypeScript 开发。

yarn add react-navigation react-native-gesture-handler
react-native link react-native-gesture-handler

我们将使用 react-navigation 模块作为 App 的导航库。

添加 TypeScript 相关的开发依赖:

yarn add typescript tslint tslint-react tslint-config-airbnb tslint-config-prettier ts-jest react-native-typescript-transformer -D
yarn add @types/jest @types/node @types/react @types/react-native @types/react-navigation @types/react-test-renderer

添加 tsconfig.json

{
  "compilerOptions": {
    "target": "es2017",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
    "module": "es2015",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [                                  /* Specify library files to be included in the compilation:  */
      "es2017",
      "dom"
    ],
    "resolveJsonModule": true,
    "allowJs": false,                         /* Allow javascript files to be compiled. */
    "skipLibCheck": true,                     /* Skip type checking of all declaration files. */
    "jsx": "react-native",                    /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true,                      /* Generates corresponding '.d.ts' file. */
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    "outDir": "./lib",                        /* Redirect output structure to the directory. */
    "removeComments": true,                   /* Do not emit comments to output. */
    "noEmit": true,                           /* Do not emit outputs. */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    "noImplicitAny": true,                    /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true,                 /* Enable strict null checks. */
    "strictFunctionTypes": true,              /* Enable strict checking of function types. */
    "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
    "alwaysStrict": true,                     /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,                   /* Report errors on unused locals. */
    "noUnusedParameters": true,               /* Report errors on unused parameters. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    "noFallthroughCasesInSwitch": true,       /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",                          /* Base directory to resolve non-absolute module names. */
    "paths": {                                /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
      "*": [
        "*.android",
        "*.ios",
        "*.native",
        "*.web",
        "*"
      ]
    },
    "typeRoots": [                            /* List of folders to include type definitions from. */
      "@types",
      "../../@types"
    ],
    // "types": [],                           /* Type declaration files to be included in compilation. */
    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Experimental Options */
    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true             /* Enables experimental support for emitting type metadata for decorators. */
  },
  "exclude": [
    "node_modules",
    "web"
  ]
}

添加 tslint.json 文件

{
  "defaultSeverity": "warning",
  "extends": [
    "tslint:recommended", 
    "tslint-react",
    "tslint-config-airbnb",
    "tslint-config-prettier"
  ],
  "jsRules": {},
  "rules": {
    "curly": false,
    "function-name": false,
    "import-name": false,
    "interface-name": false,
    "jsx-boolean-value": false,
    "jsx-no-multiline-js": false,
    "member-access": false,
    "no-console": [true, "debug", "dir", "log", "trace", "warn"],
    "no-empty-interface": false,
    "object-literal-sort-keys": false,
    "object-shorthand-properties-first": false,
    "semicolon": false,
    "strict-boolean-expressions": false,
    "ter-arrow-parens": false,
    "ter-indent": false,
    "variable-name": [
      true,
      "allow-leading-underscore",
      "allow-pascal-case",
      "ban-keywords",
      "check-format"
    ],
    "quotemark": false
  },
  "rulesDirectory": []
}

添加 .prettierrc 文件:

{
  "parser": "typescript",
  "printWidth": 100,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all"
}

编写我们的应用

在项目根目录下创建一个 src 目录,这个将是项目原代码的目录。

添加 src/App.tsx 文件

import React from 'react'

import { createAppContainer, createStackNavigator } from 'react-navigation'

import About from './screens/About'
import Article from './screens/Article'
import Home from './screens/Home'

const AppNavigator = createStackNavigator(
  {
    Home: { screen: Home },
    About: { screen: About, path: 'about' },
    Article: { screen: Article, path: 'article/:id' },
  },
  {
    initialRouteName: 'Home',
  },
)

const prefix = 'deep-linking://'

const App = createAppContainer(AppNavigator)

const MainApp = () => <App uriPrefix={prefix} />

export default MainApp

添加 src/screens/Home.tsx 文件

import React from 'react';

添加 src/screens/About.tsx

import React from 'react'

import { StyleSheet, Text, View } from 'react-native'

import { NavigationScreenComponent } from 'react-navigation'

interface IProps {}

interface IState {}

const AboutScreen: NavigationScreenComponent<IProps, IState> = props => {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>About Page</Text>
    </View>
  )
}

AboutScreen.navigationOptions = {
  title: 'About',
}

export default AboutScreen

const styles = StyleSheet.create({
  container: {},
  title: {},
})

添加 src/screens/Article.tsx

import React from 'react'

import { StyleSheet, Text, View } from 'react-native'

import { NavigationScreenComponent } from 'react-navigation'

interface NavigationParams {
  id: string
}

const ArticleScreen: NavigationScreenComponent<NavigationParams> = ({ navigation }) => {
  const { params } = navigation.state

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Article {params ? params.id : 'No ID'}</Text>
    </View>
  )
}

ArticleScreen.navigationOptions = {
  title: 'Article',
}

export default ArticleScreen

const styles = StyleSheet.create({
  container: {},
  title: {},
})

配置 iOS

打开 ios/DeepLinkingExample.xcodeproj

open ios/DeepLinkingExample.xcodeproj

点击 Info Tab 页,找到 URL Types 配置,添加一项:

  • identifier:deep-linking
  • URL Schemes:deep-linking
  • 其它两项留空

打开项目跟目录下的 AppDelegate.m 文件,添加一个新的 import

#import "React/RCTLinkingManager.h"

然后在 @end 前面,添加以下代码:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
  return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
}

至此,我们已经完成了 iOS 的配置,运行并测试是否成功。

react-native run-ios

打开 simulator 之后,打开 Safari 浏览器,在地址栏中输入:deep-linking://article/4 ,你的应用将会自动打开,并同时进入到 Article 页面。

同样的,你还可以在命令行工具中执行以下命令:

xcrun simctl openurl booted deep-linking://article/4

配置 Android

要为Android应用也创建 External Linking,需要创建一个新的 intent,打开 android/app/src/main/AndroidManifest.xml,然后在 MainActivity 节点添加一个新的 intent-filter

<application ...>
  <activity android:name=".MainActivity" ...>
    ...
    <intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data android:scheme="deep-linking" />
    </intent-filter>
    ...
  </activity>
</application>

Android 只需要完成上面的配置即可。

执行:

react-native run-android

打开系统浏览器,输入:

deep-linking://article/4

系统会自动打开你的应用,并进入 Article 页面

也可以在命令行工具中使用以下命令打开:

adb shell am start -W -a android.intent.action.VIEW -d "deep-linking://article/3" com.deeplinkingexample;

附录

点击以下链接即可:

Javascript 总是以超自然的方式执行我们的代码,这是一件很神奇的事情,如果不信的话,思考一下 ['1', '7', '11'].map(parseInt) 的结果是什么?你以为会是 [1, 7, 11] 吗?我都这么问了,那肯定不是:

['1', '7', '11'].map(parseInt)
// 输出:(3) [1, NaN, 3]

要理解为什么为会这里,首先需要了解一些 Javascript 概念,如果你是一个不太喜欢阅读长文的人,那直接跳到最后看结论吧

真值与假值

请看下面示例:

if (true) {
  // 这里将启动都会被执行
} else {
  // 这里永远都不会被执行
}

在上面的示例中, if-else 声明为 true,所以 if-block 会一直都执行,而 else-block 永远都会被忽略掉,这是个很容易理解的示例,下面我们来看看另一个例子:

if ("hello world") {
  // 这里会被执行吗?
  console.log("条件判断为真");
} else {
  // 或者会执行这里吗?
  console.log("条件判断为假");
}

打开你的开发者工具 console 面板,执行上面的代码,你会得到看到会打印出 条件判断为真,也就是说 "hello world" 这一个字符串被认为是真值

在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除 false、0、""、null、undefined 和 NaN 以外皆为真值)。

引用自:Mozilla Developer: Truthy(真值)

这里一定要划重点:在布尔上下文中,除 false0""(空字符串)、nullundefined 以及 NaN 外,其它所有值都为真值

基数

0 1 2 3 4 5 6 7 8 9 10

当我们从 0 数到 9,每一个数字的表示符号都是不一样的(0-9),但是当我们数到 10 的时候,我们就需要两个不同的符号 10 来表示这个值,这是因为,我们的数学计数系统是一个十进制的。

基数,是一个进制系统下,能使用仅仅超过一个符号表示的数字的最小值,不同的计数进制有不同的基数,所以,同一个数字在不同的计数体系下,表示的真实数据可能并不一样,我们来看一下下面这张在十进制、二进制以及十六进制不同值在具体表示方法:

十进制(基数:10)二进制(基数:2)十六进制(基数:16)
000
111
2102
3113
41004
51015
61106
71117
810008
910019
101010A
111011B
121100C
131101D
141110E
151111F
161000010
171000111
181000212

你应该已经注意到了, 11 在上表中,总共出现了三次。

函数的参数

在 Javascript 中,一个函数可以传递任何多个数量的参数,即使调用时传递的数量与定义时的数量不一致。缺失的参数会以 undefined 作为实际值传递给函数体,然后多余的参数会直接被忽略掉(但是你还是可以在函数体内通过一个类数组对象 arguments 访问到)。

function sum(a, b) {
  console.log(a);
  console.log(b);
}

sum(1, 2);    // 输出:1, 2
sum(1);       // 输出:1, undefined
sum(1, 2, 3); // 输出:1, 2

map()

快要有结果了。

map() 是一个存在于数组原型链上的方法,它将其数组实例中的每一个元素,传递给它的第一个参数(在本文最开始的例子中就是 parseInt),然后将每一个返回值都保存到同一个新的数组中,遍历完所有元素之后,将新的包含了所有结果的数组返回。

function multiplyBy3 (number) {
  return number * 3;
}

const result = [1, 2, 3, 4, 5].map(multiplyBy3);

console.log(result); // 输出:[3, 6, 9, 12, 15]

现在,假想一下,我们想使用 map() 将一个数组中的每一个元素都打印到控制台,我可以直接将 console.log 函数传递给 map() 方法:

[1, 2, 3, 4, 5].map(console.log);

输出:

1 0 (5) [1, 2, 3, 4, 5]
2 1 (5) [1, 2, 3, 4, 5]
3 2 (5) [1, 2, 3, 4, 5]
4 3 (5) [1, 2, 3, 4, 5]
5 4 (5) [1, 2, 3, 4, 5]

是不是感觉有点神奇了?为什么会这样?来分析一下上面输出的内容都有些什么?

  • 第一列:这个看上去跟我们想要的输出是一致的
  • 第二列:这个是一个从 0 开始递增的数字
  • 第三列:总是 (5) [1, 2, 3, 4, 5]

再来看看下面这段代码:

[1, 2, 3, 4, 5].map((value, index, array) => console.log(value, index, array));

在控制台上试试执行上面这行代码,你会发现,它与 [1, 2, 3, 4, 5].map(console.log); 输出的结果是完全一致的,**总是会被我们忽略的一点是,map() 方法会将三个参数传递传递给它的函数,分别是 currentValue(当前值)、currentIndex(当前索引)以及 array 本身,这就是,上面为什么结果有三列的原因,如果我们只是想将每一个值打印,那么需要像下面这样写:

[1, 2, 3, 4, 5].map((value) => console.log(value));

回到最开始的问题

现在让我们来回顾一下本文最开始的问题:

为什么 ['1', '7', '11'].map(parseInt) 的结果是 [1, NaN, 3]

来看看 parseInt() 是一个什么样的函数:

parseInt(string, radix)将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。

详情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt

这里还有一条需要注意的是,虽然我们一般都是使用 parseInt('11') 这样的调用方式,认为是将 11 按十进制转换成数字,但是,请在使用的时候,永远都添加 radix 参数,因为, radix10 并不保证永远有效(这并不是规范的一部分)。

那么看看下面这些的输出:

parseInt('11');             // 输出:11
parseInt('11', 2);          // 输出:3
parseInt('11', 16);         // 输出:17

parseInt('11', undefined);  // 输出:11 (radix 是假值)
parseInt('11', 0);          // 输出:11 (radix 是假值)

现在,让我们一步一步来看 ['1', '7', '11'].map(parseInt) 的整个执行过程,首先,我们可以将这一行代码,转换为完整的版本:

['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))

根据前面的知识,我们应该知道, parseInt(value, index, array) 中的 array 会被忽略(并放入 arguments 对象中。

那么,完整的代码就是下面这样的:

['1', '7', '11'].map((value, index, array) => parseInt(value, index))
  • 遍历到第一个元素时:

    parseInt('1', 0);

    radix0,是一个假值,在我们的控制台中,一般将使用 10 进制,所有,得到结果 1

  • 遍历到第二个元素时:

    parseInt('7', 1)

    在一个 1 进制系统中,7 是不存在的,所以得到结果 NaN(不是一个数字)

  • 遍历到第三个元素时:

    parseInt('11', 2)

    在一个 2 进制系统中,11 就是进十制的 3

总结

['1', '7', '11'].map(parseInt) 的结果是 [1, NaN, 3] 的原因是因为,map() 方法是向传递给他的函数中传递三个参数,分别为当前值,当前索引以及整个数组,而 parseInt 函数接收两个参数:需要转换的字符串,以及进制基数,所以,整个语句可以写作:['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))arrayparseInt 舍弃之后,得到的结果分别是:parseInt('1', 0)parseInt('7', 1) 以及 parseInt('11', 2),也就是上面看到的 [1, NaN, 3]

正确的写法应该是:

['1', '7', '11'].map(numStr => parseInt(numStr, 10));

自己的一句话理解:JSON Web Token 并不是一种认证方式,他只是认证信息的载体。

基于自己的理解,谈谈 JSON Web Token 的一些事儿。

Session 认证

  1. 用户向服务器A发送用户名和密码
  2. 服务器验证通过后,在当前会话里保存相关数据,比如登录时间、过期时间,用户角色等,并生成一个 session_id
  3. 服务器响应用户登录时,将 session_id 写入 cookies
  4. 用户随后发送的每一次请求都会将 session_id 传回服务器
  5. 服务器在处理请求之前,先拿到 sesion_id ,然后找到前面创建的会话信息

这种模式最简单,但是它的扩展性(Scaling)不好,如果是多台服务器,还需要考虑到所有服务器都能读取会话,那么会话就需要在多台服务器之间共享了,然后,如果有跨域的话,还需要保证 Cookie 在多个域下都是可访问的,这种方式看起来,确实就很麻烦了。

客户端保存会话数据

这种方式比 Session 实现起来就简单得多了:

  1. 用户还是发起登录请求
  2. 服务器验证通过之后生成会话数据
  3. 服务器直接把会话数据发送给请求方
  4. 请求方之后去请求任何服务的时候,自己带上就可以了
  5. 服务器接收到请求之后,自己解析会话数据,然后接下步处理即可

那么,这种方法,需要关注的问题并不是会话数据如何共享的问题了,要关注的点是:

  1. 我如何解析会话数据?
  2. 我如何认定客户端发送的会话数据是合法的?

JSON Web Token(JWT)的原理

JWT 就是为解决上面这个问题的,它首先能保存下很多会话数据,其次,它还提供了数据校验机制,下面我们来看看它的原理。

用户在登录时,服务器校验成功之后,生成一个 JSON 对象,比如:

{
  "id": 1,
  "name": "pantao",
  "role": "manager",
  "loginTime": "2019-08-01"
}

上面的这个 JSON 对象保存下了当前登录用户的ID、姓名、角色以及登录时间,以后的客户端在请求任何服务时,都应该要带上这些信息。

当然,真正的 JWT 肯定不止是保存这些数据就足够了的,上面这样的信息,只是载体(Payload),也就是认证的数据本身,但是我们还需要提供一些别的信息,来帮助服务器确定这条数据是合法的(你不能随便造一条这样的信息我就认为你是真实的吧?),在完整的 JWT 中,还会带有另外两种数据:

  • Header:头部信息,也是一个 JSON 对象,用于描述当前这个 JWT 数据是什么的元数据,比如通常是下面这样的:

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    在上面的代码中, algalgorithm 的缩写,表示了当前这个JWT使用了什么签名算法,默认就是 HMAC SHA256,缩写就是 HS256typ 属性表示了,这个是一个 JWT 类型的 token

  • Signature:这部分是对 Header 跟 Payload 两部分的签名字符串,在生成这个字符串的时候,服务器端会有一个 Secret 密钥,这使得,除了服务器自己,别人是没有办法生成正确的签名的,所以,即使前面两个内容可以随意的造,别人也没有黑涩会生成正确的签名,那么数据是否合法,最主要就是通过这个内容了。

整个认证流程就是:

  1. 客户端拿到 JWT 数据之后,在以后的请求里面带上 JWT 信息
  2. 服务器先校验这个JWT是不是自己生成的(根据 Header, Payload 以及自己保存的 Secret 再计算一次 Signature 看是不是跟用户传进来的一致就成了)
  3. 如果是自己生成的,则解析 Payload 部分,拿到上面所设计的载体JSON对象,这里面就保存了我们的会话信息
  4. 拿到会话信息后进行进一步

Base64URL

上面说了 Header.Payload.Signature 这样三段式的格式,客户端收到这样的一个 TOKEN 之后,可能是通过 COOKIE 发送服务器,也可能是通过 Header,还可能是通过 POST 请求的Payload中的一个字段,也可能是 URL 里面的查询参数,为了保证在各种不同的一方传输的过程中都通用,所以,我们肯定不能直接把两个JSON字符串以及一个签名字符串用两个 . 连起来就用,这里面就用到了 Base64URL 的算法。

在 Base64 算法里面,有三个字符 += 以及 / 是有特殊含义的,Base64URL 算法就是,将字符串按 Base64 处理之后,再将 = 省略,将 + 换成 - ,将 / 替换成 _,看个示例:

const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjpcIjExNTE4NjA4NjM5NTA0NTg4ODFcIixcInBob25lXCI6XCIxODM3NDUwMDk5OVwiLFwiYXBwSURcIjpcInd4YTJhY2IwZGVhODgyYmNmN1wiLFwib3BlbklkXCI6XCJvckpxcDVRMlo3U3V4Y3Jxa3dmelJNZ2RpVGRJXCIsXCJ1bmlvbklkXCI6XCJvZ0FtLTFBbm9mMU1rVzdtZEY3bGVsalZDcURvXCIsXCJjaXR5XCI6XCJcIixcImNvdW50cnlcIjpcIkNoaW5hXCIsXCJnZW5kZXJcIjpcIlwiLFwibmlja05hbWVcIjpcIuWkp-iDoeWtkOWGnOawkeW3pea9mOWNiuS7mVwiLFwicHJvdmluY2VcIjpcIlwiLFwiYXZhdGFyVXJsXCI6XCJodHRwczovL3d4LnFsb2dvLmNuL21tb3Blbi92aV8zMi9EWUFJT2dxODNlckhLVTNPdEk3WUliazB1NmliQlA2eTdZeDZpY2dwbXpUdWRPbEVQeHUydldpYmhudlhwWmlhSndpYjhjcEpOVjRaUFRtbERjb09vMnR5Q2ljQS8xMzJcIn0iLCJpc3MiOiJkZXZlbG9wIiwiZXhwIjoxNTY4MDc2ODcxLCJpYXQiOjE1NjU0ODQ4NzF9.kta-7LP7dIEbWYILDfw93aiKg1FRC4IOAajsUzSXeUY';

上面这个就是一个完整的 JWT 字符串,如何能解析出里面的数据呢?很简单:

  1. . 号分割字符串
  2. 将第0与第1项里面的 - 号换成 + 号,_ 号换成 / 号,
  3. 再使用 atob 将字符串从 base64 转成正常的字符
const [header, payload] = jwt.split('.').slice(0,2).map(s => s.replace(/-/gi, '+').replace(/_/gi, '/')).map(s => atob(s));

// header = {typ: "JWT", alg: "HS256"}
// payload = {"sub":"{\"userId\":\"1151860863950458881\",\"phone\":\"18374500999\",\"appID\":\"wxa2acb0dea882bcf7\",\"openId\":\"orJqp5Q2Z7SuxcrqkwfzRMgdiTdI\",\"unionId\":\"ogAm-1Anof1MkW7mdF7leljVCqDo\",\"city\":\"\",\"country\":\"China\",\"gender\":\"\",\"nickName\":\"大胡子农民工潘半仙\",\"province\":\"\",\"avatarUrl\":\"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132\"}","iss":"develop","exp":1568076871,"iat":1565484871}

要转成对象的话,再 JSON.parse() 一下就可以了:

const [header, payload] = jwt.split('.').slice(0,2).map(s => s.replace(/-/gi, '+').replace(/_/gi, '/')).map(s => atob(s)).map(s => JSON.parse(s));

可以得到下面这样结构的对象:

{
  "typ": "JWT",
  "alg": "HS256"
}

{
  "sub": "{\"userId\":\"1151860863950458881\",\"phone\":\"18374500999\",\"appID\":\"wxa2acb0dea882bcf7\",\"openId\":\"orJqp5Q2Z7SuxcrqkwfzRMgdiTdI\",\"unionId\":\"ogAm-1Anof1MkW7mdF7leljVCqDo\",\"city\":\"\",\"country\":\"China\",\"gender\":\"\",\"nickName\":\"大胡子农民工潘半仙\",\"province\":\"\",\"avatarUrl\":\"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132\"}",
  "iss": "develop",
  "exp": 1568076871,
  "iat": 1565484871
}

可以看到, Payload 段中的 sub ,应该也是一个 JSON 字符串,再解析一下即可:

{
  "userId": "1151860863950458881",
  "phone": "18374500999",
  "appID": "wxa2acb0dea882bcf7",
  "openId": "orJqp5Q2Z7SuxcrqkwfzRMgdiTdI",
  "unionId": "ogAm-1Anof1MkW7mdF7leljVCqDo",
  "city": "",
  "country": "China",
  "gender": "",
  "nickName": "大胡子农民工潘半仙",
  "province": "",
  "avatarUrl": "https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132"
}

虽然我们可以解析出 Header 跟 Payload,但是只要我们对其修改之后再发送回服务器,内容一改,签名就会改,所以,服务器直接就认定这是假的 TOKEN了

安全性

  • JWT 默认是不加密的,但是,我们也可以对生成之后的 Header 与 Payload 字符串进行一次加密,这样客户端就无法直接解析出明文了,只是在接收时,在进行 JWT校验之前,先对 Header 与 Payload 密文进行一次解密即可
  • JWT 不加密的情况下,不能将私密数据写入 JWT
  • JWT 除了认证外,还可以用于数据交换,可以大大减少查询数据库的次数
  • 如果服务器端不做特殊的其它逻辑,那么一个JWT签发之后,除非它过期,否则将永远有效,如果权限记录在 JWT 中,那么,就算修改了某个用户的权限,在签发给他的TOKEN过期前,他的权限还将是上一次签发的,由于这条特性,对于重要的操作,比如转帐等,应该进行二次确认。
  • 应该使用 HTTPS 协议传输数据

定义 Packages

应该在文件的最顶部定义包名

package my.demo

import java.util.*

//...
文件名与包名并不一定必须一致。

定义函数

定义一个接口两个数字,返回它们和的函数

fun sum(a: Int, b: Int): Int {
  return a + b
}

只有一个表达式作为主体的函数,Kotlin 会自动推断出它的返回类型

fun sum(a: Int, b: Int) = a + b

函数默认会返回无具体意义的值

fun printSum(a: Int, b: Int): Unit {
  println("Sum of $a and $b is ${a + b}")
}

Unit 类型是可以被省略的

fun printSum(a: Int, b: Int) {
  println("Sum of $a and $b is ${a + b}")
}

定义变量

使用 val 定义一个本地只读的只能被赋值一次的变量

val a: Int = 1 // 立即赋值
val b = 2 // `Int` 类型是被推断出来的
val c: Int // 先定义一个 `Int` 类型的变量
c = 3 // 然后再赋值

也可以使用 var 定义一个可以被多次赋值的变量

var x = 5
x += 1

上层变量

val PI = 3.14
var x = 0

fun incrementX() {
  x += 1
}

注释

就如同 Java、JavaScript,Kotlin 同时支持文件行末注释以及块注释:

// 这是一个行末注释

/* 这是一个
  块级注释 */
与 Java 不同点在于,Kotlin 的块注释支持嵌套

使用字符模板

var a = 1
// 模板中和简单名称
val s1 = "a 的值为 $a"

a += 1

// 在模板中的任意格式
val s2 = "${s1.replace("为", "以前为")},但是现在为 $a"

使用条件表达式

fun maxOf(a: Int, b: Int): Int {
  if (a > b) {
    return a
  } else {
    return b
  }
}

使用 if 表达示

fun maxOf(a: Int, b: Int) = if (a > b) a else b

使用可为 null 的值以及对 null 的检查

对于一个可为 null 的值,必须显示的标记它可能为 nullnullable

fun parseInt(str: String): Int? {
  // ...
}

在函数中返回可为 null 的值

fun printProduct(arg1: String, arg2: String) {
  val x = parseInt(arg1)
  var y = parseInt(arg2)

  // 使用 `x * y` 的话,会报错误,因为它们有可能为 `null`
  if (x != null & y != null) {
    // 在 `null` 检查完成之后,`x` 与 `y` 会自动的被转为不会为 `null`(`non-nullable`) 的值
    println(x * y)
  }
  else {
    println("'$arg1' 与 '$arg2' 不是都为数字")
  }
}

或者

if (x == null) {
  println("$arg1 的类型错误")
  return
}

if (y == null) {
  println("$arg2 的类型错误")
  return
}
println(x * y)

使用类型检查以及自动类型转换

is 关键字会检测一个值是否是某个类型的,只要完成类型检查,那么被检查的值就可以直接当作该类型去使用。

fun getStringLength(obj: Any): Int? {
  if (obj is String) {
    // `obj` 已经被自动转换为 `String` 类型了
    return obj.length
  }
  return null
}

或者

fun getStringLength(obj: Any): Int? {
  if (obj !is String) return null
 
  return obj.length
}

也可以

fun getStringLength(obj: Any): Int? {
  if (obj is String && obj.length > 0) {
    return obj.length
  }
  return null
}

使用 for 循环

val items = listOf("apple", "banana", "kiwifrut")
for (item in items) {
  println(item)
}

或者

val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
  println("item at $index is ${items[index]}")
}

使用 while 循环

val items = listOf("apple", "banaba", "kiwifruit")
var index = 0
while (index < items.size) {
  println("item at $index is ${items[index]}")
  index += 1
}

使用 when 表达式

fun describe(obj: Any): String = 
  when (obj) {
    1 -> "One"
    "Hello" -> "Greeting"
    is Long -> "Long"
    !is String -> "Not a string"
    else -> "Unknown"
  }

使用 ranges

检查一个数字是否在一个范围内

val x = 10
val y = 9
if (x in 1..y+1) {
  println("fits in range")
}

检查一个值是否不在一个范围内

val list = listOf("a", "b", "c")
if (-1 !in 0..list.lastIndex) {
  println("-1 不在范围内")
}
if (list.size in !in list.indices) {
  println("list.size 同样的也不在 list.indices 范围内")
}

遍历一个范围

for (x in 1..5) {
  print(x)
}

或者可以指定一个步进值

for (x in 1..10 step 2) {
    print(x)
}
println()
for (x in 9 downTo 0 step 3) {
    print(x)
}

使用集合

遍历一个集合

for (item in items) {
  println(item)
}

使用 in 确定一个集合是否包含某个对象

when {
  "orange" in items -> println("juicy")
  "apple" in items -> println("apple is fine too")
}

在集合上使用 filtermap

val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
  .filter { it.startsWith("a") }
  .sortedBy { it }
  .map { it.toUpperCase() }
  .forEach { println(it) }

创建基础类实例

val rectangle = Rectangle(5.0, 2.0) // 不需要 `new` 关键字
val triangle = Triangle(3.0, 4.0, 5.0)

开发计算机软件是一个复杂的过程,在过去很长的一段时间时间里,计算机软件的开发被开发者们规整为以下这些常见的不同活动(Activity):

  1. 定义问题(Problem Definition)
  2. 需求分析(Requirements Development)
  3. 规划构建(Construction Planning)
  4. 软件架构(Software Architecture)或者高层设计(High-level Design)
  5. 详细设计(Detailed Design)
  6. 编码调试(Coding and Debugging)
  7. 单元测试(Unit Testing)
  8. 集成测试(Integration Testing)
  9. 集成(Integration)
  10. 系统测试(System Testing)
  11. 保障维护(Corrective Maintenance)

构建有时候被简单的理解为编码(Coding)或者编程(Programming),但是编码并不算是最贴切的,因为它有一种“把已经存在的设计机械化地翻译成计算机语言”的意味,而构建并不都是这么机械化的,需要可观的创造力与判断力。下面列出了一些软件构建活动中的具体任务(Task):

  • 验证有关的基础工作都已经完成,因此构建活动可以顺利的进行下去
  • 确定如何测试所写的代码
  • 设计并编写类(Class)和子程序(Routine)
  • 创建并命名变量(Variable)和具名常量(Named Constant)
  • 选择控制结构(Control Structure),组织语句块
  • 对你的代码进行单元测试和集成测试,并排除其中的错误
  • 评审开发团队其他成员的底层设计和代码,并让他们评审你的工作
  • 润饰代码,仔细进行代码的格式化和注释
  • 将单独开发的多个软件组件集成为一体
  • 调整代码(Tuning Code),让它更快,更省资源

为什么构建活动很重要?

  • 构建活动是软件开的主要组成部分
  • 构建活动是软件开发中的核心活动
  • 把主要精力集中于构建活动,可以大大提高程序员的生产率
  • 构建活动的产中物 —— 源代码 —— 往旆是对软件的唯一精确描述
  • 构建活动是唯一一项确保会完成的工作

用隐喻更充分的理解软件开发

你走进一间安全严密、温度精确控制在 20 摄氏度的房间,并在里面发现了病毒(Virus)、特洛伊木马(Trojan horse)、蠕虫(Worm)、臭虫(Bug)、逻辑炸弹(Logic bomb)、崩溃(Crash)、论坛口水战(Flame)、双绞线转换头(Twisted sex changer),还有致使错误(Fatal Error)……

重要的研发成果常常产自类比(Analogy),通过把不太理解的东西和一些你较为理解、且十分类似的东西做比较,可以对不理解的东西产生更深刻的理解,这种使用隐喻的方法,叫作“建模(Modeling)”。建模有其不好的一面,但是总的来说,模型是有好处的,它的威力在于其生动性,让你能够把握整个概念,能隐隐地暗示各种属性(Properties)、关系(Relationships)以及需要补充查证的部分(Additional areas of inquiry)。

隐喻的价值绝不应实低估,隐喻的优点在于其可预期的效果,能被所有人理解。不必要的沟通和误解也因此大为减少,学习与教授更为快速。实际上,隐喻是对概念进行内在化(Internalizing)和抽象(Abstracting)的一种途径,它让人们在更高的层面上思考问题,从而避免低层次的错误

-- Fernando J. Corbato

人地心说到日心说,从以计算机为中心到以数据为中心

托勒密的地心说模型持续了1400年,直到1543年哥白尼提出了以太阳为中心的理论,最终帮助人们找到了更多的行星,并将月亮重新界定为地球的卫星而不是一个独立的行星,它使人们对人类在宇宙中的地位有了一个完全不同的理解。

20世纪70年代早期计算机编程方面的变化就像从日心说到地心说一样,以前的编程观点是以计算机为中心(Computer-centered),数据处理是把所有的数据看作是流经计算机(Flowing through a computer)的连续卡片流(stream of crads),而后转变为以数据为中心(Database-centered),把焦点放到的数据池(Pool of data),而计算机偶尔涉足其中。

与其说是隐喻是一张路线图,不如说它是探路明灯,它不会告诉你该去哪里寻找答案,而是告诉你该如何去寻找答案,隐喻的作用更像是启示(Heuristic),而不是算法(Algorithm)。算法是一套定义明确的指令,使你能完成某个特定的任务,算法是可预测的(Predictable)、确定性的(Deterministic)不易变化的(Not subject to chance)。

启发式方式是一种帮你寻求答案的技术,但是它给出的答案是具有偶然性的(Subject to chance)

一直用着 Microsoft 的 AppCenter.ms 服务都不错,功能强大,但是最近总是抽风,没办法,只能自己部署私有 Code Push Server了,如果直接搜索 Code Push Server,一般得到的结果都是 https://github.com/lisong/code-push-server 这个,我安装过,不过并没有实现去测试,因为发现它并没有完美的实现 Code Push 的逻辑,在各种坛里面找了好几天之后,终于发现了 http://Electrode.io,Walmart Labs 的东西总是这么难发现, Hapijs 也是。

什么是 Electrode ,大家可以直接上官方去了解,我们只使用 Electrode OTA Server 功能,我本身就是一个长期的 HapiJS 用户,所以一看到这货,还是很亲切的。

安装运行环境

安装 Node

安装 nvm

nvm 是一个很不错的 Node 版本管理工具,使用下面任何一个命令安装即可,如果在安装过程中有任何疑问,请直接自行解决 https://github.com/nvm-sh/nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

或者

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

安装最新版本 Node

nvm install node

安装 Docker

这个不是必须的,但是如果只是在本地测试的话,建议安装,Electrode OTA Server 默认使用的是 Apache Cassandra 数据库,有了 Docker 之后,数据库的问题更好解决,否则需要在本机安装个 Cassandra 也是很烦人的一件事情,当然,如果不使用 Cassandra 的话,也可以直接使用 MariaDB 数据,这个下面都会说,因为我的机器配置不高,所以,最终还是选择了 MariaDB 数据库。

如果你已经安装了 Docker 了,那么直接跳过这一步,如果感觉没有安装过,那么继续,使用下面的命令删除所有过往的 docker 版本。

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

安装安装 yum-utils 以提供 yum-config-manager 工具,同时,device-mapper-persistent-data 以及 lvm2devicemapper 所必须的库:

sudo yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2

使用下面的命令设置 stable 版本 docker

sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

使用下面命令安装 docker

sudo yum install docker-ce docker-ce-cli containerd.io

安装数据库

基于 docker 之后,我们就直接安装 MariaDB 以及 Cassandra 数据库了。

安装 Cassandra

docker pull cassandra

或者有一个增强版本的选择:

docker pull datastax/dse-server

两个任选一个即可

docker run --name parcmg-cassandra -p 9042:9042 -d cassandra
docker container start parcmg-cassandra

安装 MariaDB

docker pull mariadb
docker run --name parcmg-mariadb -p 3306:3306 -d mariadb
docker container start parcmg-mariadb

现在我们已经有了可用的数据库服务了,接下来,部署 Electrode OTA Server

创建 Github App

这一步就不多说了,直接在 Github 后台创建一个 App,拿到 ClientId 以及 ClientSecret 两个值,在下面会用。

部署 Electrode OTA Server

创建 parcmg-ota-server 项目

mkdir parcmg-ota-server
yarn init
yarn add electrode-ota-server electrode-ota-server-dao-mariadb

添加 config/default.json 配置文件

在项目目录下面创建一个名为 config 的目录,在目录中,添加一个 default.json 的配置文件(这个是我最喜欢 HapiJS 的一点,所有东西都是配置优先。

{
  "connection": {
    "host": "localhost",
    "port": 9001,
    "compression": false
  },
  "server": {
    "app": {
      "electrode": true
    }
  },
  "plugins": {
    "electrode-ota-server-dao-plugin": {
      "module": "electrode-ota-server-dao-mariadb",
      "priority": 4,
      "options": {
        "keyspace": "parcmg_ota",
        "contactPoints": ["localhost"],
        "clusterConfig": {
          "canRetry": true,
          "defaultSelector": "ORDER",
          "removeNodeErrorCount": 5,
          "restoreNodeTimeout": 0
        },
        "poolConfigs": [
          {
            "host": "<%Database Host%>",
            "port": 3306,
            "dialect": "mysql",
            "database": "<%Database Name%>",
            "user": "<%Database Username%>",
            "password": "<%Database Password%>"
          }
        ]
      }
    },
    "electrode-ota-server-fileservice-upload": {
      "options": {
        "downloadUrl": "https://<%ota.domain.com%>/storagev2/"
      }
    },
    "electrode-ota-server-auth": {
      "options": {
        "strategy": {
          "github-oauth": {
            "options": {
              "password": "<%RandomKey%>",
              "isSecure": true,
              "location": "https://<%ota.domain.com%>",
              "clientId": "<%GithubClientId%>",
              "clientSecret": "<%GithubClientSecret%>"
            }
          },
          "session": {
            "options": {
              "password": "LYG2AqpUK3L4rKQERbuyJWxCqMYh5nlF",
              "isSecure": true
            }
          }
        }
      }
    }
  }
}

然后给 package.json 添加下面两个 script

{
  "scripts": {
    "start": "NODE_ENV=production node node_modules/electrode-ota-server",
    "development": "NODE_ENV=development node node_modules/electrode-ota-server"
  }
}

此时,可以直接使用 yarn development 或者 yarn start 运行了。

这里需要注意一点,如果使用 MariaDB,需要自己先建立好数据库以及数据表, schema 保存在 https://github.com/electrode-io/electrode-ota-server/tree/master/electrode-ota-mariadb-schema/electrode-ota-db/tables 这里面,一个一个创建即可。

安装 pm2

安装 pm2 工具

npm install -g pm2

添加 ecosystem.config.js 文件

内容如下:

module.exports = {
  apps: [
    {
      name: "parcmg-ota",
      script: "node_modules/electrode-ota-server/index.js",
      env: {
        NODE_ENV: "production"
      }
    }
  ]
};

package.json 中添加 serve 命令:

{
  "scripts": {
    "serve": "yarn install && pm2 startOrRestart ecosystem.config.js --env production",
    "start": "NODE_ENV=production node node_modules/electrode-ota-server",
    "development": "NODE_ENV=development node node_modules/electrode-ota-server"
  }
}

启动服务

yarn serve

或者

pm2 start ecosystem.config.js --env production

配置域名

安装 nginx

vi /etc/yum.repos.d/nginx.repo

内容如下:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=0
enabled=1

然后执行下面命令安装:

yum install -y

添加虚拟主机

/etc/nginx/conf.d 目录下新建一个虚拟主机配置文件:

vi /etc/nginx/conf.d/ota.domain.com.conf

内容如下:

upstream parcmg_ota {
    server 127.0.0.1:9001; 
    keepalive 64;
}

server {
  listen               80;
  listen               [::]:80;
  server_name          ota.parcmg.com;
  return 301 https://$host$request_uri;
}

server {

  listen 443 ssl;
  
  ssl_certificate   cert.d/YOUR_PEM.com.pem;
  ssl_certificate_key  cert.d/YOUR_KEY.com.key;
  ssl_session_timeout 5m;
  ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;

  server_name ota.parcmg.com;

  charset utf-8;

  # Global restrictions configuration file.
  # Designed to be included in any server {} block.
  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }

  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }

  # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
  # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
  location ~ /\. {
    deny all;
  }

  location / {
    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      #
      # Custom headers and headers various browsers *should* be OK with but aren't
      #
      add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      #
      # Tell client that this pre-flight info is valid for 20 days
      #
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain; charset=utf-8';
      add_header 'Content-Length' 0;
      return 204;
    }
    if ($request_method = 'POST') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
    if ($request_method = 'GET') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }

      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_set_header X-Nginx-Proxy true;
      proxy_set_header Connection "";
      proxy_pass http://parcmg_ota; 
  }
}

启动 nginx

systemctl start nginx
systemctl enable nginx
如果没有 SSL 证书,可以上 Aliyun 或者 QCloud 上面去申请免费的,当然,也可以直接使用 http 协议。

Electrode OTA Desktop

牛逼的Walmart Labs 还提供了一个可视化的管理工具,咱现在就先用上,直接去 https://github.com/electrode-io/electrode-ota-desktop/releases 下载最新版本即可,打开之后,会看到登录界面。暂时离开一会儿,回到 Terminal 中去。

Electrode OTA Desktop Login Screen@2x.png

登录 OTA Server

退出已有 code-push 会话

如果你以前已经使用了 Appcenter.ms 的服务,那么现在可以退出登录了。

code-push logout

注册

重新在私有 OTA 服务中注册帐号:

code-push register https://ota.parcmg.com

此时会跳转到 https://ota.parcmg.com 的授权页面,在页面最下方点击 Github 完成 OAuth 授权之后,会得到一个 Access Token,复制该 Token,在 Terminal 中粘贴,按回车,即可登录成功,同时,将该 Token 粘贴至 Electrode OTA Desktop 应用的登录框的 Token中,在服务地址中填写你的 OTA 服务地址即可完成会话登录。

添加 App

Electrode OTA Desktop 里面,创建两个新的应用,就跟使用 appcenter.ms 一样,比如:

MyApp-Android
MyApp-IOS

创建成功之后,会分别生成对应的 Production 以及 Staging Key,在接下面我们会用到。

code push 服务迁移到自己的私有服务器

IOS

打开 info.plist 文件,我们需要修改以前的 Code Push 配置,找到:

<key>CodePushDeploymentKey</key>
<string>SecrtKey-----------Here</string>

在此处,将 MyApp-IOSProduction Key粘贴至此处,同时还需要添加一个配置项目:

<key>CodePushServerURL</key>
<string>https://ota.parcmg.com</string>

完整配置如下:

<key>CodePushDeploymentKey</key>
<string><%YourKeyHere%></string>
<key>CodePushServerURL</key>
<string>https://ota.parcmg.com</string>

如果你使用的不是 https 协议 ,那么还需要增加:

<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>ota.parcmg.com</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

Android

MainApplication.java 文件中,找到下面这一行:

new CodePush(getResources().getString(R.string.reactNativeCodePush_androidDeploymentKey), getApplicationContext(), BuildConfig.DEBUG)

添加一个参数如下,表示我需要使用这个作为 code push 的服务。

new CodePush(getResources().getString(R.string.reactNativeCodePush_androidDeploymentKey), getApplicationContext(), BuildConfig.DEBUG, "https://ota.parcmg.com")

大功告成了,需要测试的可以直接使用我的 ota 服务: https://ota.parcmg.com,但请不要在生产中使用,鬼知道我什么时候就会把这个停用了。

最近研究 React Native、Redux Saga 以及 TypeScript 相关的内容,整理成了一个 React Native Template,可以直接使用下面的命令创建一个新的应用:

react-native init MyApp --template=parcmg

初始化完成之后,按下面的方式执行命令:

cd MyApp
node setup.js
npm install
react-native link react-native-gesture-handler

完成之后,即可像往常一样开发了:

react-native run-ios

初始化 NPM 项目

mkdir project-name
cd project-name
npm init

添加开发基础包

添加 TypeScript

yarn add typescript -D

添加 Jest 测试工具

yarn add jest ts-jest @types/jest -D

添加 @types/node

yarn add @types/node -D

初始化 TypeScript 配置

./node_modules/.bin/tsc --init

这会在你的项目根目录新建一个 tsconfig.json 文件

现在的目录结构如下:

.
├── node_modules
├── package.json
├── tsconfig.json
└── yarn.lock

文件解析

tsconfig.json

这是 TypeScript 的配置文件,默认仅启用了几项,我一般的配置如下:

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
    "lib": [
      "es6"
    ] /* Specify library files to be included in the compilation. */,
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true /* Generates corresponding '.d.ts' file. */,
    "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./dist" /* Redirect output structure to the directory. */,
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "incremental": true,                   /* Enable incremental compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
    "strictNullChecks": true /* Enable strict null checks. */,
    "strictFunctionTypes": true /* Enable strict checking of function types. */,
    "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
    "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
    "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
    "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,

    /* Additional Checks */
    "noUnusedLocals": true /* Report errors on unused locals. */,
    "noUnusedParameters": true /* Report errors on unused parameters. */,
    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
    "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "src/__tests__"]
}

package.json

添加了几条 scripts

{
  "name": "project-name",
  "version": "0.0.0-development",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "rm -rf ./dist && tsc",
    "watch": "yarn test --watch",
    "setup": "yarn install && npx npm-install-peers",
    "test": "jest"
  },
  "author": "",
  "license": "SEE LICENSE IN LICENSE FILE",
  "devDependencies": {
    "@types/jest": "^24.0.13",
    "@types/node": "^12.0.3",
    "jest": "^24.8.0",
    "ts-jest": "^24.0.2",
    "typescript": "^3.4.5"
  }
}

添加 jest.config.js 文件

内容如下:

module.exports = {
  // We always recommend having all TypeScript files in a src folder in your project. We assume this is true and specify this using the roots option.
  roots: ["<rootDir>/src"],
  transform: {
    "^.+\\.tsx?$": "ts-jest" //The transform config just tells jest to use ts-jest for ts / tsx files.
  }
};

添加 .gitignore 文件

该文件告诉 git 哪些文件不需要跟踪

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

# map
*.map

# gitbook
_book

初始化 git 项目

git init
jest watch 需要有 git 环境才能运行

添加 src/index.ts 文件

内容如下:

export const sum = (a: number, b: number): number => a + b;

添加 src/__tests__

按我们的 jest 测试用例:src/__tests__/index.test.ts,内容如下:

import { sum } from "..";

test("sum", () => {
  expect(sum(1, 2)).toBe(3);
});

开始开发

yarn jest

此时终端显示如下信息:

 PASS  src/__tests__/index.test.ts
  ✓ sum (15ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.133s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.

现在修改一下 index.ts,文件内容改为下面这样:

export const sum = (a: number, b: number): number => a + b;

export const multiply = (a: number, b: number): number => a * b;

再修改 __tests__/index.test.ts,内容如下:

import { sum, multiply } from "..";

test("sum", () => {
  expect(sum(1, 2)).toBe(3);
});

test("multiply", () => {
  expect(multiply(2, 3)).toBe(6);
});

会发现终端会自动测试你的代码:

 PASS  src/__tests__/index.test.ts
  ✓ sum (4ms)
  ✓ multiply

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.164s
Ran all test suites related to changed files.

完整的代码可以在下面看到:

在 iOS App 开发过程中,经常会遇到该问题:

linker command failed with exit code 1 (use -v to see invocation) 

我在解决该问题的过程中,收集整理了一般引发该问题的原因以及对应的解决方法。

BitCode

新建一个 iOS 项目时, xcode 默认会将 bitcode 项设置为 YES,即启用,如果我们引入了一些不支持 bitcode 的第三方库,会引起这个问题。

bitcode 是一种编译结果中间态,它并不能直接运行,但是它包含了一个程序所需要的所有内容,它最终将被编译成为可运行的二进制包,启用 bitcode 的好处是,苹果可以随时根据自己的优化,基于 bitcode 生成更优化的二进制包,而不需要我们重新上传新的版本。 iOS 默认开启,但是可以关闭,watchOS 下则是必须开启的,mac OS 不支持,如果我们开发的程序只支持 iOS,那么可以选择关闭它。

尝试使用以下方式解决:

  • 打开 Build Settings
  • Enable Bitcode 设置为 NO

添加了第三方库,且不是静态库

如果添加了第三方库,且该库不是静态库之后发生此问题,那么可以尝试:

  • 先按上面 BitCode 的方式解决,若不行
  • 打开 Build Settings
  • 找到 Linking
  • Other Linker Flags 改为 -all_load 或者 -ObjC,视情况而定,多试几次。

引入了重复的包

……
duplicate symbol _OBJC_IVAR_$_RCTHTTPRequestHandler._session in:
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libReact.a(RCTHTTPRequestHandler.o)
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libRCTNetwork.a(RCTHTTPRequestHandler.o)
duplicate symbol _OBJC_METACLASS_$_RCTHTTPRequestHandler in:
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libReact.a(RCTHTTPRequestHandler.o)
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libRCTNetwork.a(RCTHTTPRequestHandler.o)
ld: 485 duplicate symbols for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

这个问题是我这次遇到的,

一直以为 上面这一段提示只是警告,但是其实它才是导致这个问题产生的原因,我最后是根据提示, symbol _OBJC_METACLASS_$_RCTHTTPRequestHandler 同时在 libReact.alibRCTNetwork.a 中定义了,我后来是一个一个的删除提示里面的多余的引用,解决问题。

  • 打开 Build Phases
  • 找到 Link Binary With Libraries (N items)
  • 根据提示中,删除重复项(名称有可能不同,但是里面的内容可能是一样的)

systemLog:
   # verbosity: 0  #日志等级,0-5,默认0
   # quiet: false  #限制日志输出,
   # traceAllExceptions: true  #详细错误日志
   # syslogFacility: user #记录到操作系统的日志级别,指定的值必须是操作系统支持的,并且要以--syslog启动
   path: /Users/mhq/projects/db/mongo/logs/log.txt  #日志路径。
   logAppend: false #启动时,日志追加在已有日志文件内还是备份旧日志后,创建新文件记录日志, 默认false
   logRotate: rename #rename/reopen。rename,重命名旧日志文件,创建新文件记录;reopen,重新打开旧日志记录,需logAppend为true
   destination: file #日志输出方式。file/syslog,如果是file,需指定path,默认是输出到标准输出流中
   timeStampFormat: iso8601-local #日志日期格式。ctime/iso8601-utc/iso8601-local, 默认iso8601-local
   # component: #各组件的日志级别
   #    accessControl:
   #       verbosity: <int>
   #    command:
   #       verbosity: <int>

processManagement:
   fork: true #以守护进程运行 默认false
   # pidFilePath: <string> #PID 文件位置

net:
   port: 27017 #监听端口,默认27017
   bindIp: 127.0.0.1 #绑定监听的ip,deb和rpm包里有默认的配置文件(/etc/mongod.conf)里面默认配置为127.0.0.1,若不限制IP,务必确保认证安全,多个Ip用逗号分隔
   maxIncomingConnections: 65536 #最大连接数,可接受的连接数还受限于操作系统配置的最大连接数
   wireObjectCheck: true #校验客户端的请求,防止错误的或无效BSON插入,多层文档嵌套的对象会有轻微性能影响,默认true
   ipv6: false #是否启用ipv6,3.0以上版本始终开启
   unixDomainSocket: #unix socket监听,仅适用于基于unix的系统
      enabled: false #默认true
      pathPrefix: /tmp #路径前缀,默认/temp
      filePermissions: 0700 #文件权限 默认0700
   http: #警告 确保生产环境禁用HTTP status接口、REST API以及JSON API以防止数据暴露和漏洞攻击
      enabled: false #是否启用HTTP接口、启用会增加网络暴露。3.2版本后停止使用HTTP interface
      JSONPEnabled: false #JSONP的HTTP接口
      RESTInterfaceEnabled: false #REST API接口
   # ssl: #估计用不到,所以没有自己看
   #    sslOnNormalPorts: <boolean>  # deprecated since 2.6
   #    mode: <string>
   #    PEMKeyFile: <string>
   #    PEMKeyPassword: <string>
   #    clusterFile: <string>
   #    clusterPassword: <string>
   #    CAFile: <string>
   #    CRLFile: <string>
   #    allowConnectionsWithoutCertificates: <boolean>
   #    allowInvalidCertificates: <boolean>
   #    allowInvalidHostnames: <boolean>
   #    disabledProtocols: <string>
   #    FIPSMode: <boolean>

security:
   authorization: enabled # enabled/disabled #开启客户端认证
   javascriptEnabled:  true #启用或禁用服务器端JavaScript执行
   # keyFile: <string> #密钥路径
   # clusterAuthMode: <string> #集群认证方式
   # enableEncryption: <boolean>
   # encryptionCipherMode: <string>
   # encryptionKeyFile: <string>
   # kmip:
   #    keyIdentifier: <string>
   #    rotateMasterKey: <boolean>
   #    serverName: <string>
   #    port: <string>
   #    clientCertificateFile: <string>
   #    clientCertificatePassword: <string>
   #    serverCAFile: <string>
   # sasl:
   #    hostName: <string>
   #    serviceName: <string>
   #    saslauthdSocketPath: <string>
   

# setParameter: #设置参数
#    <parameter1>: <value1>
#    <parameter2>: <value2>

storage:
   dbPath: /Users/mhq/projects/db/mongo/test/ #数据库,默认/data/db,如果使用软件包管理安装的查看/etc/mongod.conf
   indexBuildRetry: true #重启时,重建不完整的索引
   # repairPath: <string>  #--repair操作时的临时工作目录,默认为dbPath下的一个_tmp_repairDatabase_<num>的目录
   journal: 
      enabled: true #启动journal,64位系统默认开启,32位默认关闭
      # commitIntervalMs: <num> #journal操作的最大时间间隔,默认100或30
   directoryPerDB: false #使用单独的目录来存储每个数据库的数据,默认false,如果需要更改,要备份数据,删除掉dbPath下的文件,重建后导入数据
   # syncPeriodSecs: 60 #使用fsync来将数据写入磁盘的延迟时间量,建议使用默认值
   engine: wiredTiger #存储引擎,mmapv1/wiredTiger/inMemory 默认wiredTiger
   # mmapv1:
   #    preallocDataFiles: <boolean>
   #    nsSize: <int>
   #    quota:
   #       enforced: <boolean>
   #       maxFilesPerDB: <int>
   #    smallFiles: <boolean>
   #    journal:
   #       debugFlags: <int>
   #       commitIntervalMs: <num>
   # wiredTiger:
   #    engineConfig:
   #       cacheSizeGB: <number>  #缓存大小
   #       journalCompressor: <string> #数据压缩格式 none/snappy/zlib
   #       directoryForIndexes: <boolean> #将索引和集合存储在单独的子目录下,默认false
   #    collectionConfig:
   #       blockCompressor: <string> #集合数据压缩格式 
   #    indexConfig:
   #       prefixCompression: <boolean> #启用索引的前缀压缩
   # inMemory:
   #    engineConfig:
   #       inMemorySizeGB: <number>
 
operationProfiling: #性能分析
   slowOpThresholdMs: 100 #认定为查询速度缓慢的时间阈值,超过该时间的查询即为缓慢查询,会被记录到日志中, 默认100
   mode: off #operationProfiling模式 off/slowOp/all 默认off

# replication: #复制集相关
#    oplogSizeMB: <int>
#    replSetName: <string>
#    secondaryIndexPrefetch: <string>
#    enableMajorityReadConcern: <boolean>
# sharding: #集群分片相关
#    clusterRole: <string>
#    archiveMovedChunks: <boolean>

# auditLog:
#    destination: <string>
#    format: <string>
#    path: <string>
#    filter: <string>

# snmp:
#    subagent: <boolean> #当设置为true,SNMP作为代理运行
#    master: <boolean> #当设置为true,SNMP作为主服务器运行

# basisTech:
#    rootDirectory: <string>

祖师爷 Linus 在创造了伟大的 Linux 之后,又创造了应用最广泛的代码管理工具 —— Git,极大地提高了程序员的生产力。 现如今大部分项目都在使用 Git 作为代码管理工具,不论是在代码管理、版本控制以及团队协作上,Git 相比其他版本控制软件都有着无可比拟的优势。

虽然 Git 是个优秀的工具,但是在项目中是否能够正确合理地使用,是否能够发挥其最大的优势,就我自己这几年的工作经历来看,对于大部分团队这个问题的答案是否定的。

大部分程序员对 Git 的使用基本上都停留在 git add、git commit、git push、git pull 这几个指令上,而且大部分团队也没有 Git 规范,提交信息充斥着大量的 “fix”、“update”,分支管理也很混乱,代码提交哪个分支上也没具体的规定,导致在团队协作过程中经常出现代码合并后谁的代码不见了,修过的 bug 在新版本又出现了……

我们可能面临的问题

试想遇到以下这些问题,你会采取怎样的方式去解决:

  • 需要线上某个历史版本的源码,直接在 develop 分支根据提交记录和时间找对应的节点?
  • 线上版本出现严重 bug 需要紧急修复发版本,而你的项目就一个分支,上个版本发布之后已经有大量改动了,怎么办?
  • 某个提交改动了部分代码,涉及到 10 几个文件,现在这个改动不需要了,此时要一个个找出这些文件然后再改回去么?
  • 出现了一个 bug,之前好像处理过,但是现在忘了当初怎么处理的了,在一堆写着 “fix bug”、“update” 的提交记录中,如何找到当初那笔的提交?
  • 某个功能本来准备发布的,现在突然决定这个版本不上了,现在要一处处找到之前的代码,然后再改回去?
  • ……

以上这些问题在我们的项目中都是会或多或少出现的,部分问题可能涉及到的是对 Git 的功能是否熟悉的问题,大部分问题则是涉及到一个项目的 Git 使用规范问题,如果有一个很好的规范,在项目中合理地使用 Git,很多问题压根就不是问题。

Git 规范的必要性

既然认同需要一份 Git 规范,那么这个规范需要规范哪些内容,解决哪些问题,又带来哪些好处呢?个人认为有以下几点:

1. 分支管理

  • 代码提交在应该提交的分支
  • 随时可以切换到线上稳定版本代码
  • 多个版本的开发工作同时进行

2. 提交记录的可读性

  • 准确的提交描述,具备可检索性
  • 合理的提交范围,避免一个功能就一笔提交
  • 分支间的合并保有提交历史,且合并后结果清晰明了
  • 避免出现过多的分叉

3. 团队协作

  • 明确每个分支的功用,做到对应的分支执行对应的操作
  • 合理的提交,每次提交有明确的改动范围和规范的提交信息
  • 使用 Git 管理版本迭代、紧急线上 bug fix、功能开发等任务

以上就是一份 Git 规范的作用和使命。

接下来结合 Git-Flow 和个人实际的项目经验,总结了一份项目中使用 Git 的规范,其中大部分内容都是对 Git-Flow 进行一个解读和扩展,告诉大家为什么这么做以及怎么做。

以下是 Git-Flow 的经典流程图:

git-flow.png

如果你熟悉 Git-Flow,那么你对上图中的各种分支和线应该都能够理解,如果你之前没了解过相关的知识,那你可能会有点懵,不过在读完本文之后再看这张图,应该就能够理解了。

分支管理规范

分支说明和操作

  • master 分支

    • 主分支,永远处于稳定状态,对应当前线上版本
    • 以 tag 标记一个版本,因此在 master 分支上看到的每一个 tag 都应该对应一个线上版本
    • 不允许在该分支直接提交代码
  • develop 分支

    • 开发分支,包含了项目最新的功能和代码,所有开发都依赖 develop 分支进行
    • 小的改动可以直接在 develop 分支进行,改动较多时切出新的 feature 分支进行

      注: 更好的做法是 develop 分支作为开发的主分支,也不允许直接提交代码。小改动也应该以 feature 分支提 merge request 合并,目的是保证每个改动都经过了强制代码 review,降低代码风险

  • feature 分支

    • 功能分支,开发新功能的分支
    • 开发新的功能或者改动较大的调整,从 develop 分支切换出 feature 分支,分支名称为 feature/xxx
    • 开发完成后合并回 develop 分支并且删除该 feature/xxx 分支
  • release 分支

    • 发布分支,新功能合并到 develop 分支,准备发布新版本时使用的分支
    • 当 develop 分支完成功能合并和部分 bug fix,准备发布新版本时,切出一个 release 分支,来做发布前的准备,分支名约定为release/xxx
    • 发布之前发现的 bug 就直接在这个分支上修复,确定准备发版本就合并到 master 分支,完成发布,同时合并到 develop 分支
  • hotfix 分支

    • 紧急修复线上 bug 分支
    • 当线上版本出现 bug 时,从 master 分支切出一个 hotfix/xxx 分支,完成 bug 修复,然后将 hotfix/xxx 合并到 master 和 develop 分支(如果此时存在 release 分支,则应该合并到 release 分支),合并完成后删除该 hotfix/xxx 分支

以上就是在项目中应该出现的分支以及每个分支功能的说明。 其中稳定长期存在的分支只有 master 和 develop 分支,别的分支在完成对应的使命之后都会合并到这两个分支然后被删除。简单总结如下:

  • master 分支: 线上稳定版本分支
  • develop 分支: 开发分支,衍生出 feature 分支和 release 分支
  • release 分支: 发布分支,准备待发布版本的分支,存在多个,版本发布之后删除
  • feature 分支: 功能分支,完成特定功能开发的分支,存在多个,功能合并之后删除
  • hotfix 分支: 紧急热修复分支,存在多个,紧急版本发布之后删除

分支间操作注意事项

在团队开发过程中,避免不了和其他人一起协作, 同时也会遇到合并分支等一些操作,这里提交 2 个个人觉得比较好的分支操作规范。

同一分支 git pull 使用 rebase

首先看一张图:

git_pull_no_rebase.jpg

看到这样的  提交线图,想从中看出一条清晰的提交线几乎是不可能的,充满了 Merge remote-tracking branch 'origin/xxx' into xxx 这样的提交记录,同时也将提交线弄成了交错纵横的图,没有了可读性。

这里最大的原因就是因为默认的 git pull 使用的是 merge 行为,当你更新代码时,如果本地存在未推送到远程的提交,就会产生一个这样的 merge 提交记录。因此在同一个分支上更新代码时推荐使用 git pull --rebase

下面这张图展示了默认的 git pullgit pull --rebase 的结果差异,使用 git pull --rebase 目的是修整提交线图,使其形成一条直线。

git_pull_rebase_diff.jpg

默认的 git pull 行为是 merge,可以进行如下设置修改默认的 git pull 行为:

# 为某个分支单独设置,这里是设置 dev 分支
git config branch.dev.rebase true
# 全局设置,所有的分支 git pull 均使用 --rebase
git config --global pull.rebase true
git config --global branch.autoSetupRebase always

这里需要说明一下,在我看来使用 git pull --rebase 操作是比较好的,能够得到一条很清晰的提交直线图,方便查看提交记录和 code review,但是由于 rebase 会改变提交历史,也存在一些不好的影响。这里就不做过多的讨论了,有兴趣的话可以移步知乎上的讨论:在开发过程中使用 git rebase 还是 git merge,优缺点分别是什么?

分支合并使用 --no-ff

  # 例如当前在 develop 分支,需要合并 feature/xxx 分支
  git merge --no-ff feature/xxx

在解释这个命令之前,先解释下 Git 中的 fast-forward: 举例来说,开发一直在 develop 分支进行,此时有个新功能需要开发,新建一个 feature/a 分支,并在其上进行一系列开发和提交。当完成功能开发时,此时回到 develop 分支,此时 develop 分支在创建 feature/a 分支之后没有产生任何的 commit,那么此时的合并就叫做 fast-forward。

fast-forward 合并的结果如下图所示,这种 merge 的结果就是一条直线了,无法明确看到切出一个新的 feature 分支,并完成了一个新的功能开发,因此此时比较推荐使用 git merge --no-ff,得到的结果就很明确知道,新的一系列提交是完成了一个新的功能,如果需要对这个功能进行 code review,那么只需要检视叉的那条线上的提交即可。

git_merge_diff.svg

关于以上两个分支间的操作建议,如果需要了解更多,可以阅读洁癖者用 Git:pull --rebase 和 merge --no-ff 这篇文章。

项目分支操作流程示例

这部分内容结合日常项目的开发流程,涉及到开发新功能、分支合并、发布新版本以及发布紧急修复版本等操作,展示常用的命令和操作。

  1. 切到 develop 分支,更新 develop 最新代码

    git checkout develop
    git pull --rebase
    
  2. 新建 feature 分支,开发新功能

    git checkout -b feature/xxx
    ...
    git add <files>
    git commit -m "feat(xxx): commit a"
    git commit -m "feat(xxx): commit b"
    # 其他提交
    ...
    

    如果此时 develop 分支有一笔提交,影响到你的 feature 开发,可以 rebase develop 分支,前提是 该 feature 分支只有你自己一个在开发,如果多人都在该分支,需要进行协调:

    # 切换到 develop 分支并更新 develop 分支代码
    git checkout develop
    git pull --rebase
    
    # 切回 feature 分支
    git checkout feature/xxx
    git rebase develop
    
    # 如果需要提交到远端,且之前已经提交到远端,此时需要强推(强推需慎重!)
    git push --force
    

    上述场景也可以通过 git cherry-pick 来实现,有兴趣的可以去了解一下这个指令。

  3. 完成 feature 分支,合并到 develop 分支

    # 切到 develop 分支,更新下代码
    git check develop
    git pull --rebase
    
    # 合并 feature 分支
    git merge feature/xxx --no-ff
    
    # 删除 feature 分支
    git branch -d feature/xxx
    
    # 推到远端
    git push origin develop
    
  4. 当某个版本所有的 feature 分支均合并到 develop 分支,就可以切出 release 分支,准备发布新版本,提交测试并进行 bug fix

    # 当前在 develop 分支
    git checkout -b release/xxx
    
    # 在 release/xxx 分支进行 bug fix
    git commit -m "fix(xxx): xxxxx"
    ...
    
  5. 所有 bug 修复完成,准备发布新版本

    # master 分支合并 release 分支并添加 tag
    git checkout master
    git merge --no-ff release/xxx --no-ff
    # 添加版本标记,这里可以使用版本发布日期或者具体的版本号
    git tag 1.0.0
    
    # develop 分支合并 release 分支
    git checkout develop
    git merge --no-ff release/xxx
    
    # 删除 release 分支
    git branch -d release/xxx
    

    至此,一个新版本发布完成。

  6. 线上出现 bug,需要紧急发布修复版本

    # 当前在 master 分支
    git checkout master
    
    # 切出 hotfix 分支
    git checkout -b hotfix/xxx
    
    ... 进行 bug fix 提交
    
    # master 分支合并 hotfix 分支并添加 tag(紧急版本)
    git checkout master
    git merge --no-ff hotfix/xxx --no-ff
    # 添加版本标记,这里可以使用版本发布日期或者具体的版本号
    git tag 1.0.1
    
    # develop 分支合并 hotfix 分支(如果此时存在 release 分支的话,应当合并到 release 分支)
    git checkout develop
    git merge --no-ff hotfix/xxx
    
    # 删除 hotfix 分支
    git branch -d hotfix/xxx
    

    至此,紧急版本发布完成。

提交信息规范

提交信息规范部分参考 Angular.js commit messgae

git commit 格式 如下:

<type>(<scope>): <subject>

各个部分的说明如下:

  • type 类型,提交的类别

    • feat: 新功能
    • fix: 修复 bug
    • docs: 文档变动
    • style: 格式调整,对代码实际运行没有改动,例如添加空行、格式化等
    • refactor: bug 修复和添加新功能之外的代码改动
    • perf: 提升性能的改动
    • test: 添加或修正测试代码
    • chore: 构建过程或辅助工具和库(如文档生成)的更改
  • scope 修改范围

    主要是这次修改涉及到的部分,简单概括,例如 login、train-order

  • subject 修改的描述

    具体的修改描述信息

  • 范例

    feat(detail): 详情页修改样式
    fix(login): 登录页面错误处理
    test(list): 列表页添加测试代码
    

这里对提交规范加几点说明:

  1. type + scope 能够控制每笔提交改动的文件尽可能少且集中,避免一次很多文件改动或者多个改动合成一笔。
  2. subject 对于大部分国内项目而已,如果团队整体英文不是较高水平,比较推荐使用中文,方便阅读和检索。
  3. 避免重复的提交信息,如果发现上一笔提交没改完整,可以使用 git commit --amend 指令追加改动,尽量避免重复的提交信息。

参考资料

命名规则

避免使用一个字母命名

eslint

// bad
function q() {
  // ...
}

// good
function query() {
  // ...
}

使用小驼峰式命名对象、函数、实例

eslint

// bad
const OBJEcttsssss = {};
const this_is_my_object = {};
function c() {}

// good
const thisIsMyObject = {};
function thisIsMyFunction() {}

使用大驼峰式命名类

eslint

// bad
function user(options) {
  this.name = options.name;
}

const bad = new user({
  name: 'nope',
});

// good
class User {
  constructor(options) {
    this.name = options.name;
  }
}

const good = new User({
  name: 'yup',
});

不要使用前置或后置下划线(除非引入的第三方库本身使用)

eslint

JavaScript 没有私有属性或私有方法的概念。尽管前置下划线通常的概念上意味着“private”,事实上,这些属性是完全公有的,因此这部分也是你的API的内容。这一概念可能会导致开发者误以为更改这个不会导致崩溃或者不需要测试。 如果你想要什么东西变成“private”,那就不要让它在这里出现。
// bad
this.__firstName__ = 'Panda';
this.firstName_ = 'Panda';
this._firstName = 'Panda';

// good
this.firstName = 'Panda';

不要保存引用 this

用箭头函数或函数绑定——Function#bind

// bad
function foo() {
  const self = this;
  return function () {
    console.log(self);
  };
}

// bad
function foo() {
  const that = this;
  return function () {
    console.log(that);
  };
}

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

保证文件名、export 模块名以及 import 模块名一致

// file 1 contents
class CheckBox {
  // ...
}
export default CheckBox;

// file 2 contents
export default function fortyTwo() { return 42; }

// file 3 contents
export default function insideDirectory() {}

// in some other file
// bad
import CheckBox from './checkBox'; // PascalCase import/export, camelCase filename
import FortyTwo from './FortyTwo'; // PascalCase import/filename, camelCase export
import InsideDirectory from './InsideDirectory'; // PascalCase import/filename, camelCase export

// bad
import CheckBox from './check_box'; // PascalCase import/export, snake_case filename
import forty_two from './forty_two'; // snake_case import/filename, camelCase export
import inside_directory from './inside_directory'; // snake_case import, camelCase export
import index from './inside_directory/index'; // requiring the index file explicitly
import insideDirectory from './insideDirectory/index'; // requiring the index file explicitly

// good
import CheckBox from './CheckBox'; // PascalCase export/import/filename
import fortyTwo from './fortyTwo'; // camelCase export/import/filename
import insideDirectory from './insideDirectory'; // camelCase export/import/directory name/implicit "index"
// ^ supports both insideDirectory.js and insideDirectory/index.js

export default 一个函数时、函数名小驼峰式,文件与函数名一致

function makeStyleGuide() {
  // ...
}

export default makeStyleGuide;

export 一个结构体、类、单例、函数库或者对象时,使用大驼峰式

const Helpers = {
  guid: () => return uuid(),
};

export default Helpers;

简称或缩写应该全部大写或者全部小写

名字是给人读的,不是为了适应电脑的算法
// bad
import SmsContainer from './containers/SmsContainer';

// bad
const HttpRequests = [
  // ...
];

// good
import SMSContainer from './containers/SMSContainer';

// good
const HTTPRequests = [
  // ...
];

// best
import TextMessageContainer from './containers/TextMessageContainer';

// best
const Requests = [
  // ...
];

使用全大写字母设置静态变量

  1. 导出变量
  2. const 定义的, 保证不能被改变
  3. 这个变量是可信的,他的子属性都是不能被改变的

一般我们都将项目的全局参数使用这种 全大写+下划线分隔的常量 来定义一些系统配置参数导出,比如 const LIST_VIEW_PAGE_SIZE = 10 可以表示列表页每次加载10条数据;

如果导出项目是一个对象,那么必须保证这个对象的所有属性都是不可变的,同时,它的属性不再是全大写,而是使用小写驼峰式。

// bad
const PRIVATE_VARIABLE = 'should not be unnecessarily uppercased within a file';

// bad
export const THING_TO_BE_CHANGED = 'should obviously not be uppercased';

// bad
export let REASSIGNABLE_VARIABLE = 'do not use let with uppercase variables';

// ---

// allowed but does not supply semantic value
export const apiKey = 'SOMEKEY';

// better in most cases
export const API_KEY = 'SOMEKEY';

// ---

// bad - unnecessarily uppercases key while adding no semantic value
export const MAPPING = {
  KEY: 'value'
};

// good
export const MAPPING = {
  key: 'value'
};

访问器

若非必要,不要使用访问器

由于 JavaScript 的 getters/setters 是有副作用的,而且会让他人在查看代码的时候难以理解,后期也会难以维护,所以不推荐使用访问器函数,如果非要使用,可以使用自己实现的 getVal()setVal()

// bad
class Dragon {
  get age() {
    // ...
  }

  set age(value) {
    // ...
  }
}

// good
class Dragon {
  getAge() {
    // ...
  }

  setAge(value) {
    // ...
  }
}

如果属性或者方法是一个布尔判断值,那么使用 isVal() 或者 hasVal()

// bad
if (!dragon.age()) {
  return false;
}

// good
if (!dragon.hasAge()) {
  return false;
}

如果非要使用 get()set(),那么它们两者必须同时使用

class Jedi {
  constructor(options = {}) {
    const lightsaber = options.lightsaber || 'blue';
    this.set('lightsaber', lightsaber);
  }

  set(key, val) {
    this[key] = val;
  }

  get(key) {
    return this[key];
  }
}

事件

当你需要向事件附加数据时,将数据封装成为一个对象,而不是使用原始值,这会使得以后可以很方便的增加附加值的字段。

// bad
$(this).trigger('listingUpdated', listing.id);

// ...

$(this).on('listingUpdated', (e, listingID) => {
  // do something with listingID
});

而是:

// good
$(this).trigger('listingUpdated', { listingID: listing.id });

// ...

$(this).on('listingUpdated', (e, data) => {
  // do something with data.listingID
});

jQuery

为所有 jQuery 对象加上 $ 前缀

// bad
const sidebar = $('.sidebar');

// good
const $sidebar = $('.sidebar');

// good
const $sidebarBtn = $('.sidebar-btn');

缓存 jQuery 结果

// bad
function setSidebar() {
  $('.sidebar').hide();

  // ...

  $('.sidebar').css({
    'background-color': 'pink',
  });
}

// good
function setSidebar() {
  const $sidebar = $('.sidebar');
  $sidebar.hide();

  // ...

  $sidebar.css({
    'background-color': 'pink',
  });
}

使用级联查询 $('.sidebar ul') 或者子父级查询 $('.sidebar > ul')

在 jQuery 对象查询作用域下使用 find 方法

// bad
$('ul', '.sidebar').hide();

// bad
$('.sidebar').find('ul').hide();

// good
$('.sidebar ul').hide();

// good
$('.sidebar > ul').hide();

// good
$sidebar.find('ul').hide();

ES5 兼容性

直接参考 Kangax 提供的 ES5 兼容性列表

类型

基本类型

你可以直接获取到基本类型的值

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
const foo = 1;
let bar = foo;

bar = 9;

console.log(foo, bar); // => 1, 9
注意Symbols 不能被完整的 polyfill,所以,在不支持 Symbols 的环境下中,不应该使用 symbol 类型。

复杂类型

复杂类型赋值就是获取到他的引用的值,相当于引用传递

  • object
  • array
  • function
const foo = [1, 2];
const bar = foo;

bar[0] = 9;
console.log(foo[0], bar[0]); // => 9, 9

参考

永远都使用 const

为了确保你不会改变你的初始值,重复引用会导致一些不可预见的 bug,还会让代码难以理解,所有的赋值都应该使用 const,避免使用 var

eslint

// bad
var a = 1;
var b = 2;

// good
const a = 1;
const b = 2;

可以使用 let

如果你一定要对参数重新赋值,那就使用 let,而不是 varlet 是块级作用域,而 ver 是函数级作用域。

eslint

// bad
var count = 1;
if (true) {
  count += 1;
}

// good
let count = 1;
if (true) {
  count += 1;
}

注意 constlet 的块级作用域

constlet 声明的常量与变量都只存在于定义它们的那个块级作用域中。

{
  let a = 1;
  const b = 1;
}

console.log(a); // ReferenceError
console.log(b); // ReferenceError

对象

永远使用字面量创建对象

eslint

// bad
const obj = new Object();

// good
const obj = {};

使用计算属性名

当你需要创建一个带有 动态属性名 的对象时,请将所有的属性定义放在一起,可以使用 计算属性名

function getKey(key) {
  return `a key named ${key}`;
}

// bad
const obj = {
  id: 1,
  name: 'Parc MG',
};
obj[getKey('enabled')] = true;

// good
const obj = {
  id: 1,
  name: 'Parc MG',
  [getKey('enabled')]: true
};

对象方法简写

eslint

// bad
const atom = {
  value: 1,
  add: function (value) {
    return atom.value + value;
  }
};

// good
const atom = {
  value: 1,
  add(value) {
    return atom.value + value;
  }
};

属性值缩写

eslint

const name = 'Parc MG';

// bad
const org = {
  name: name,
};

// good
const org = {
  name,
};

将所有属性值缩写放在对象声明的最前面

const name = 'Parc MG';
const url = 'https://parcmg.com';

// bad
const org = {
  email: 'contact@parcmg.com',
  name,
  created: new Date(),
  url,
};

// good
const org = {
  name,
  url,
  email: 'contact@parcmg.com',
  created: new Date(),
};

若非必要,属性名不使用 ''

eslint

// bad
const bad = {
  'foo': 1,
  'bar': 2,
  'foo-bar': 3,
};

// good
const good = {
  foo: 1,
  bar: 2,
  'foo-bar': 3,
};

不直接调用对象原型上的方法

不直接调用一个对象的 hasOwnPropertypropertyIsEnumerableisPrototypeOf 等这些原型的方法,在某些情况下,这些方法可能会被屏蔽掉,比如 { hasOwnProperty: false } 或者是一个空对象 Object.create(null)

// bad
obj.hasOwnProperty(key);

// good
Object.prototype.hasOwnProperty.call(obj, key);

// best
const has = Object.prototype.hasOwnProperty;

has.call(obj, key);

积极使用扩展及解构运算 ...

  • 在对象的 浅拷贝 时,更推荐使用扩展运算 { ...obj },而不是 Object.assign
  • 在获取对象指定的几个属性时,使用解构运算 { foo, bar, ...rest } = obj

eslint

// very bad
const original = { a: 1, b: 2 };
const copied = Object.assign(original, { c: 3 }); // 这将导致 original 也被修改

delete copied.a; // 这样操作之后会导致 original 也被修改
console.log(original); // => {b: 2, c: 3}

// bad
const original = { a: 1, b: 2 };
const copied = Object.assign({}, original, { c: 3}};

// good
const original = { a: 1, b: 2 };
const copied = { ...original, c: 3 };

// 解构运算与 `rest` 赋值运算
const obj = { a: 1, b: 2, c: 3 };
const { a, b } = obj; // 从对象 obj 中解构出 a, b 两个属性的值,并赋值给名为 a,b 的常量
const { a, ...rest } = obj; // 从对象 obj 中解构出 a 的值,并赋值给名为 a 的常量,同时,创建一个由所有其它属性组成的名为 `rest` 的新对象
console.log(rest); // => { b: 2, c: 3 }

// bad
function getFullName(user) {
  const firstName = user.firstName;
  const lastName = user.lastName;
  
  return `${firstName} ${lastName}`;
}

// good
function getFullName(user) {
  const { firstName, lastName } = user;
  return `${firstName} ${lastName}`;
}

// best
function getFullName({ firstName, lastName}) {
  return `${firstName} ${lastName}`;
}

// the most best
const getFullName = ({ firstName, lastName }) => `${firstName} ${lastName}`;

返回多值时,使用对象解构,而非数组结构

由于 JavaScript 不支持多值返回,当一个函数或者方法有多个值需要创建时,请为每一个值命名,并以所有值组成的对象为单一值返回,而不是以数组的形式返回。

// bad
function processInput(input) {
  return [left, right, top, bottom];
}

const [left, _, top] = processInput(input); // 调用者需要在调用时,明确的知道每一个索引上的值是什么 ,且无法跳越前面的值取后面的值

// good
function processInput(input) {
  return { left, right, top, bottom };
}

const { left, top } = processInput(input); // 调用者可以明确的指定需要哪个值,而且不需要创建多余的变量

数组

使用字面量赋值

eslint

// bad
const items = new Array();

// good
const items = [];

使用 .push 方法代替直接索引赋值

const items = [];

// bad
items[items.length] = 'new item';

// good
items.push('new item');

使用扩展运算符进行浅拷贝

const items = [1, 2, 3, 4, 5];

// bad
const length = items.length;
const copied = [];
let index;

for (index = 0; index < length; index += 1) {
  copied[index] = items[index];
}

// good
const copied = [ ...items ];

使用 ... 运算符代替 Array.from

当需要将一个可迭代的对象转换成数组时,推荐使用 ... 操作符。

const elements = document.querySelectorAll('.foobar');

// not bad
const nodes = Array.from(elements);

// good
const nodes = [ ...elements ];

使用 ... 解构数组

const array = [1, 2, 3, 4, 5];

// bad
const first = array[0];
const second = array[1];

// good
const [first, second, ...rest] = array;
console.log(rest); // => [3, 4, 5]

使用 Array.from 将类数组对象转成数组

参考:Typed Arrays
const arrayLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 }

// bad
const array = Array.prototype.slice.call(arrayLike);

// good
const array = Array.from(arrayLike);

使用 Array.from 对类数组对象进行遍历

Array.from(arrayLike[, mapFn[, thisArg]]) 方法,参考 Array.from
const arrayLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 }

// bad
const array = [...arrayLike].map(mapFn);

// good
const array = Array.from(arrayLike, mapFn);

在数组方法的回调函数中,永远返回正确的值

// bad - 当第一次迭代完成之后, acc 就变成了 undefined 了
[[0, 1], [2, 3], [4, 5]].reduce((acc, item, index) => {
  const flatten = acc.concat(item);
  acc[index] = flatten;
});

// good
[[0, 1], [2, 3], [4, 5]].reduce((acc, item, index) => {
  const flatten = acc.concat(item);
  acc[index] = flatten;
  return flatten;
});

// bad
messages.filter(msg => {
  const { subject, author } = msg;
  if (subject === 'ParcMG') {
    return author === 'MG';
  } else {
    return false;
  }
});

// good
messages.filter(msg => {
  const { subject, author } = msg;
  if (subject === 'ParcMG') {
    return author === 'MG';
  }
  return false;
});

// bad
[1, 2, 3].map(x => {
  const y = x + 1;
  return x * y;
}

// good
[1, 2, 3].map(x => x * (x + 1));

一个数组有多行时,在 [] 处断行

// bad
const array = [
  [0, 1], [2, 3], [4, 5], [6, 7]
];

const objectArray = [{
  id: 1,
}, {
  id: 2,
}];

const numberArray = [
  1, 2,
];

// good
const array = [[0, 1], [2, 3], [4, 5], [6, 7]];

const objectArray = [
  {
    id: 1,
  },
  {
    id: 2,
  }
];

const numberArray = [1, 2];

const numberArray = [
  1,
  2,
];

字符串

string 永远使用单引号 ''

eslint

// bad
const name = "Parc M.G";

const name = `Parc M.G`;

// good
const name = 'Parc M.G';

超长的字符串,不应该使用多行串联

// bad
const content = '《学而》是《论语》第一篇的篇名。《论语》中各篇一般都是以第\
一章的前二三个字作为该篇的篇名。《学而》一篇包括16章,内容涉及诸多方面。其中重\
点是「吾日三省吾身」;「节用而爱人,使民以时」;「礼之用,和为贵」以及仁、孝、\
信等道德范畴。';

const content = '《学而》是《论语》第一篇的篇名。《论语》中各篇一般都是以第' +
'一章的前二三个字作为该篇的篇名。《学而》一篇包括16章,内容涉及诸多方面。其中重' +
'点是「吾日三省吾身」;「节用而爱人,使民以时」;「礼之用,和为贵」以及仁、孝、' +
'信等道德范畴。';

// good
const content = '《学而》是《论语》第一篇的篇名。《论语》中各篇一般都是以第\一章的前二三个字作为该篇的篇名。《学而》一篇包括16章,内容涉及诸多方面。其中重点是「吾日三省吾身」;「节用而爱人,使民以时」;「礼之用,和为贵」以及仁、孝、信等道德范畴。';

使用模板而非拼接来组织可编程字符串

eslint

// bad
function hello(name) {
  return '你好,' + name + '!';
}

function hello(name) {
  return ['你好,', name, '!'].join('');
}

function hello(name) {
  return `你好,${ name }!`;
}

// good
function hello(name) {
  return `你好,${name}!`;
}

永远不使用 eval()

eslint

若非必要,不使用转义字符

eslint

// bad
const foo = '\'this\' \i\s \"quoted\"';

// good
const foo = '\this\' is "quoted"';

// best
const foo = `'this' is "quoted"`;

函数

使用命名函数表达式,而不是函数声明

eslint

使用函数声明,它的作用域会被提前,这意味着很容易在一个文件里面引用一个还未被定义的函数,这样大大伤害了代码的可读性和可维护性,若一个函数很大很复杂,那么应该考虑将该函数单独提取到一个文件中,抽离成一个模块,同时不要忘记给表达式显示的命名,这消除了由匿名函数在错误调用栈中产生的所有假设。
// bad
function foo() {
  // ...
}

// bad
const foo = function () {
  // ...
}

// good
const foo = function foo() {
  // ...
}

// best
const foo = function longUniqueMoreDescriptiveLexicalFoo() {
  // ...
}

把立即执行函数包裹在圆括号里

eslint

(function () {
  console.log('Welcome to the ParcMG world.');
}());

不要在非函数块内声明函数

虽然运行环境允许你这样做,但是不同环境的解析方式不一样。

eslint

//bad 
for (var i=10; i; i--) {
    (function() { return i; })();
}

while(i) {
    var a = function() { return i; };
    a();
}

do {
    function a() { return i; };
    a();
} while (i);

let foo = 0;
for (let i = 0; i < 10; ++i) {
    // Bad, `foo` is not in the loop-block's scope and `foo` is modified in/after the loop
    setTimeout(() => console.log(foo));
    foo += 1;
}

for (let i = 0; i < 10; ++i) {
    // Bad, `foo` is not in the loop-block's scope and `foo` is modified in/after the loop
    setTimeout(() => console.log(foo));
}
foo = 100;

// good
var a = function() {};

for (var i=10; i; i--) {
    a();
}

for (var i=10; i; i--) {
    var a = function() {}; // OK, no references to variables in the outer scopes.
    a();
}

for (let i=10; i; i--) {
    var a = function() { return i; }; // OK, all references are referring to block scoped variables in the loop.
    a();
}

var foo = 100;
for (let i=10; i; i--) {
    var a = function() { return foo; }; // OK, all references are referring to never modified variables.
    a();
}

注意:在 ECMA-262 中,块(block 的定义是:一系列语句,但函数声明不是一个语句,命名函数表达式是一个语句。

// bad
if (currentUser) {
  function test() {
    console.log('Nope.');
  }
}

// good
let test;
if (currentUser) {
  test = () => {
    console.log('Yup.');
  };
}

不允许使用 arguments 命名参数

arguments 的优先级高于高于每个函数作用域自带的 arguments 对象,这会导致函数自带的 arguments 值被覆盖。

// bad
function foo(name, options, arguments) {
  // ...
}

// good
function foo(name, options, args) {
  // ...
}

不要在函数体内使用 arguments,使用 ...rest 代替

eslint

... 明确出你想用那个参数,同时,rest 是一个真数组,而不是一个类数组的 arguments
// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}

// good
function concatenateAll(...args) {
  return args.join('');
}

使用默认参数,而不是在函数体内对参数重新赋值

// really bad
function handleThings(options) {
  options = options || {};
}

// still bad
function handleTings(options) {
  if (options === void 0) {
    options = {};
  }
}

// good
function handleThings(options = {}) {
}

默认参数要避免副作用

// bad
let v = 1;
const count = function count(a = v++) {
  console.log(a);
}

count();   // => 1
count();   // => 2
count(3);  // => 3
count();   // => 3

// maybe
const v = 1;
const count = function count(a = v) {
  console.log(a);
}

把默认参数放在最后

// bad
function handleTings(options = {}, name) {
  // ...
}

// good
function handleTings(name, options = {}) {
  // ...
}

不要使用函数构造器构造函数

eslint

// bad
var add = new Function('a', 'b', 'return a + b');

// still bad
var subtract = Function('a', 'b', 'return a - b');

// good
const subtract = (a, b) => a + b;

函数签名部分要有空格

eslint

// bad
const f = function(){};
const g = function (){};
const h = function() {};

// good
const f = function a() {};

不修改参数

eslint

函数签名时定义的参数,在函数体内不允许被重新赋值(包含参数本身,若参数为对象,还包括该对象所有属性的值),
一个函数应该是没有任何副作用的。
// bad
function f1 (obj) {
  obj.key = 1;
};

function f2 (a) {
  a = 1;
  // ...
}

function f3 (a) {
  if (!a) { a = 1; }
  // ...
}

// good
function f4(obj) {
  const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1;
};

function f5(a) {
  const b = a || 1;
  // ...
}

function f6(a = 1) {
  // ...
}

使用 spread 操作符 ... 调用多变参数函数

eslint

// bad
const x = [1, 2, 3, 4, 5];
console.log.apply(console, x);

// good
const x = [1, 2, 3, 4, 5];
console.log(...x);

// bad
new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]));

// good
new Date(...[2016, 8, 5]);

若函数签名包含多个参数需要使用多行,那就每行有且仅有一个参数

// bad
function foo(bar,
             baz,
             quux) {
  // ...
}

// good 缩进不要太过分
function foo(
  bar,
  baz,
  quux,
) {
  // ...
}

// bad
console.log(foo,
  bar,
  baz);

// good
console.log(
  foo,
  bar,
  baz,
);

箭头函数

当你一定要用函数表达式的时候,就使用箭头函数

eslint

// bad
[1, 2, 3].map(function (x) {
  const y = x + 1;
  return x * y;
});

// good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

如果函数体有且仅有一个没有副作用的表达式,那么删除大括号和 return

eslint

// bad
[1, 2, 3].map(number => {
  const nextNumber = number + 1;
  `A string containing the ${nextNumber}.`;
});

// good
[1, 2, 3].map(number => `A string containing the ${number}.`);

// good
[1, 2, 3].map((number) => {
  const nextNumber = number + 1;
  return `A string containing the ${nextNumber}.`;
});

// good
[1, 2, 3].map((number, index) => ({
  [index]: number
}));

// 表达式有副作用就不要用隐式return
function foo(callback) {
  const val = callback();
  if (val === true) {
    // Do something if callback returns true
  }
}

let bool = false;

// bad
// 这种情况会return bool = true, 不好
foo(() => bool = true);

// good
foo(() => {
  bool = true;
});

若表达式包含多行,用圆括号包裹起来

// bad
['get', 'post', 'put'].map(httpMethod => Object.prototype.hasOwnProperty.call(
    httpMagicObjectWithAVeryLongName,
    httpMethod
  )
);

// good
['get', 'post', 'put'].map(httpMethod => (
  Object.prototype.hasOwnProperty.call(
    httpMagicObjectWithAVeryLongName,
    httpMethod
  )
));

若函数只有一个参数,且没有大括号,那就删除圆括号,否则,参数总是放在圆括号里。

eslint

// bad
[1, 2, 3].map((x) => x * x);

// good
[1, 2, 3].map(x => x * x);

// good
[1, 2, 3].map(number => (
  `A long string with the ${number}. It’s so long that we don’t want it to take up space on the .map line!`
));

// bad
[1, 2, 3].map(x => {
  const y = x + 1;
  return x * y;
});

// good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

避免箭头函数(=>)和比较操作符(<=, >=)混淆.

eslint

// bad
const itemHeight = item => item.height > 256 ? item.largeSize : item.smallSize;

// bad
const itemHeight = (item) => item.height > 256 ? item.largeSize : item.smallSize;

// good
const itemHeight = item => (item.height > 256 ? item.largeSize : item.smallSize);

// good
const itemHeight = (item) => {
  const { height, largeSize, smallSize } = item;
  return height > 256 ? largeSize : smallSize;
};

在隐式return中强制约束函数体的位置, 就写在箭头后面

eslint

// bad
(foo) =>
  bar;

(foo) =>
  (bar);

// good
(foo) => bar;
(foo) => (bar);
(foo) => (
   bar
)

类与构造器

使用构造器,而不是 prototype

// bad
function Queue(contents = []) {
  this.queue = [...contents];
}
Queue.prototype.pop = function () {
  const value = this.queue[0];
  this.queue.splice(0, 1);
  return value;
};


// good
class Queue {
  constructor(contents = []) {
    this.queue = [...contents];
  }
  pop() {
    const value = this.queue[0];
    this.queue.splice(0, 1);
    return value;
  }
}

使用 extends 实现继承

它是一种内置的方法来继承原型功能而不打破 instanceof

// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
  Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function () {
  return this._queue[0];
}

// good
class PeekableQueue extends Queue {
  peek() {
    return this._queue[0];
  }
}

方法可以返回 this 实现方法链

// bad
Jedi.prototype.jump = function () {
  this.jumping = true;
  return true;
};

Jedi.prototype.setHeight = function (height) {
  this.height = height;
};

const luke = new Jedi();
luke.jump(); // => true
luke.setHeight(20); // => undefined

// good
class Jedi {
  jump() {
    this.jumping = true;
    return this;
  }

  setHeight(height) {
    this.height = height;
    return this;
  }
}

const luke = new Jedi();

luke.jump()
  .setHeight(20);

只要保证可以正常工作且没有副作用,可以自已定制 toString 方法

class Jedi {
  constructor(options = {}) {
    this.name = options.name || 'no name';
  }

  getName() {
    return this.name;
  }

  toString() {
    return `Jedi - ${this.getName()}`;
  }
}

不要写无用的构造函数

eslint

// bad
class Jedi {
  constructor() {}

  getName() {
    return this.name;
  }
}

// bad
class Rey extends Jedi {
  // 这种构造函数是不需要写的
  constructor(...args) {
    super(...args);
  }
}

// good
class Rey extends Jedi {
  constructor(...args) {
    super(...args);
    this.name = 'Rey';
  }
}

避免重复类成员

eslint

// bad
class Foo {
  bar() { return 1; }
  bar() { return 2; }
}

// good
class Foo {
  bar() { return 1; }
}

// good
class Foo {
  bar() { return 2; }
}

模块

使用 import / export

// bad
const Button = require('./Button');
module.exports = Button.es6;

// ok
import Button from './Button';
export default Button.es6;

// best
import { es6 } from './Button';
export default es6;

不要 import 通配符

// bad
import * as Component from './Component';

// good
import Component from './Component';

不要直接从 importexport

虽然一行是简洁的,有一个明确的方式进口和一个明确的出口方式来保证一致性。
// bad
export { es6 as default } from './Component';

// good
import { es6 } from './Component';
export default es6;

一个路径只 import 一次

eslint

从同一个路径下import多行会使代码难以维护
// bad
import foo from 'foo';
// … some other imports … //
import { named1, named2 } from 'foo';

// good
import foo, { named1, named2 } from 'foo';

// good
import foo, {
  named1,
  named2,
} from 'foo';

若非必要,不要 export 可变量

eslint

变化通常都是需要避免,特别是当你要输出可变的绑定。虽然在某些场景下可能需要这种技术,但总的来说应该导出常量。
// bad
let foo = 3;
export { foo }

// good
const foo = 3;
export { foo }

在一个单一导出模块里,使用 export default

eslint

鼓励使用更多文件,每个文件只做一件事情并导出,这样可读性和可维护性更好。
// bad
export function foo() {}

// good
export default function foo() {}

import 应该放在所有其它语句之前

eslint

// bad
import foo from 'foo';
foo.init();

import bar from 'bar';

// good
import foo from 'foo';
import bar from 'bar';

foo.init();

多行import应该缩进,就像多行数组和对象字面量

花括号与样式指南中每个其他花括号块遵循相同的缩进规则,逗号也是。
// bad
import {longNameA, longNameB, longNameC, longNameD, longNameE} from 'path';

// good
import {
  longNameA,
  longNameB,
  longNameC,
  longNameD,
  longNameE,
} from 'path';

若使用 webpack,不允许在 import 中使用 webpack loader 语法

eslint

一旦用 Webpack 语法在 import 里会把代码耦合到模块绑定器。最好是在 webpack.config.js 里写 webpack loader 语法
// bad
import fooSass from 'css!sass!foo.scss';
import barCss from 'style!css!bar.css';

// good
import fooSass from 'foo.scss';
import barCss from 'bar.css';

迭代器与生成器

不要使用遍历器

eslint

用JavaScript高级函数代替 for-infor-of

  • 这强调了我们不可变的规则。 处理返回值的纯函数比副作用更容易。
  • 用数组的这些迭代方法: map()every()filter()find()findIndex()reduce()some()……
  • 用对象的这些方法 Object.keys()Object.values()Object.entries 去产生一个数组, 这样你就能去遍历对象了。
const numbers = [1, 2, 3, 4, 5];

// bad
let sum = 0;
for (let num of numbers) {
  sum += num;
}
sum === 15;

// good
let sum = 0;
numbers.forEach(num => sum += num);
sum === 15;

// best (use the functional force)
const sum = numbers.reduce((total, num) => total + num, 0);
sum === 15;

// bad
const increasedByOne = [];
for (let i = 0; i < numbers.length; i++) {
  increasedByOne.push(numbers[i] + 1);
}

// good
const increasedByOne = [];
numbers.forEach(num => increasedByOne.push(num + 1));

// best (keeping it functional)
const increasedByOne = numbers.map(num => num + 1);

不要用 generator

eslint

它在es5上支持的不好

如果一定要用,那么一定需要注意一点:function* 是同一概念关键字, * 并不是 function 的修饰符, function* 是一个与 function 完全不一样的独特结构。

// bad
function * foo() {
  // ...
}

// bad
const bar = function * () {
  // ...
}

// bad
const baz = function *() {
  // ...
}

// bad
const quux = function*() {
  // ...
}

// bad
function*foo() {
  // ...
}

// bad
function *foo() {
  // ...
}

// very bad
function
*
foo() {
  // ...
}

// very bad
const wat = function
*
() {
  // ...
}

// good
function* foo() {
  // ...
}

// good
const foo = function* () {
  // ...
}

属性

访问属性使用 .

eslint

这条,涉及一个曾经阿里出过一个看似简单,实则很难的面试题,你就算猜对一个,你也不一定能说出原理:

a.b.c.d和a'b'['d'],哪个性能更高

到这里,突然想起这个梗,有兴趣的可以翻看一下 这里

const luke = {
  jedi: true,
  age: 28,
};

// bad
const isJedi = luke['jedi'];

// good
const isJedi = luke.jedi;

当获取属性名称本身是一个变量是,使用 [] 访问

const luke = {
  jedi: true,
  age: 28,
};

function getProp(prop) {
  return luke[prop];
}

const isJedi = getProp('jedi');

幂等使用 ** 操作符

eslint

// bad
const binary = Math.pow(2, 10);

// good
const binary = 2 ** 10;

变量

永远使用 const 或者 let,不使用 var

eslint

// bad
superPower = new SuperPower();

// good
const superPower = new SuperPower();

每一个变量都用一个 const 或者 let

eslint

扯蛋的理由:这种方式很容易去声明新的变量,你不用去考虑把;调换成,,或者引入一个只有标点的不同的变化。

真正的理由:做法也可以是你在调试的时候单步每个声明语句,而不是一下跳过所有声明。

// bad
const items = getItems(),
    goSportsTeam = true,
    dragonball = 'z';

const items = getItems(),
    goSportsTeam = true;
    dragonball = 'z';

// good
const items = getItems();
const goSportsTeam = true;
const dragonball = 'z';

尘归尘,土归土

在同一个块中,所有的 const 放在一起,所有的 let 放在一起

// bad
let i, len, dragonball,
    items = getItems(),
    goSportsTeam = true;

// bad
let i;
const items = getItems();
let dragonball;
const goSportsTeam = true;
let len;

// good
const goSportsTeam = true;
const items = getItems();
let dragonball;
let i;
let length;

在你需要的地方声明变量,但要放在合理的位置

// bad
function checkName(hasName) {
  const name = getName();

  if (hasName === 'test') {
    return false;
  }

  if (name === 'test') {
    this.setName('');
    return false;
  }

  return name;
}

// good
function checkName(hasName) {
  if (hasName === 'test') {
    return false;
  }

  // 在需要的时候分配
  const name = getName();

  if (name === 'test') {
    this.setName('');
    return false;
  }

  return name;
}

不使用链接变量分配

eslint

链接变量分配会隐匿创建全局变量

// bad
(function example() {
  // JavaScript 将这一段解释为
  // let a = ( b = ( c = 1 ) );
  // let 只对变量 a 起作用; 变量 b 和 c 都变成了全局变量
  let a = b = c = 1;
}());

console.log(a); // undefined
console.log(b); // 1
console.log(c); // 1

// good
(function example() {
  let a = 1;
  let b = a;
  let c = a;
}());

console.log(a); // undefined
console.log(b); // undefined
console.log(c); // undefined

// `const` 也是如此

不使用一元自增自减运算(++--

eslint

根据 eslint 文档,一元增量和减量语句受到自动分号插入的影响,并且可能会导致应用程序中的值递增或递减的无声错误。 使用 num + = 1 而不是 num ++num ++ 语句来表达你的值也是更有表现力的。 禁止一元增量和减量语句还会阻止您无意地预增/预减值,这也会导致程序出现意外行为。

// bad

let array = [1, 2, 3];
let num = 1;
num++;
--num;

let sum = 0;
let truthyCount = 0;
for(let i = 0; i < array.length; i++){
  let value = array[i];
  sum += value;
  if (value) {
    truthyCount++;
  }
}

// good

let array = [1, 2, 3];
let num = 1;
num += 1;
num -= 1;

const sum = array.reduce((a, b) => a + b, 0);
const truthyCount = array.filter(Boolean).length;

赋值时不换行

eslint

如果赋值语句超出了 max-len 配置,那么给值前面加上括号。

// bad
const foo =
  superLongLongLongLongLongLongLongLongFunctionName();

// bad
const foo
  = 'superLongLongLongLongLongLongLongLongString';

// good
const foo = (
  superLongLongLongLongLongLongLongLongFunctionName()
);

// good
const foo = 'superLongLongLongLongLongLongLongLongString';

不允许声明不使用的变量

eslint

// bad

var some_unused_var = 42;

// 写了没用
var y = 10;
y = 5;

// 变量改了自己的值,也没有用这个变量
var z = 0;
z = z + 1;

// 参数定义了但未使用
function getX(x, y) {
    return x;
}

// good
function getXPlusY(x, y) {
  return x + y;
}

var x = 1;
var y = a + 2;

alert(getXPlusY(x, y));

// 'type' 即使没有使用也可以可以被忽略, 因为这个有一个 rest 取值的属性。
// 这是从对象中抽取一个忽略特殊字段的对象的一种形式
var { type, ...coords } = data;
// 'coords' 现在就是一个没有 'type' 属性的 'data' 对象

变量提升

永远不要使用 var

var 声明会将变量声明提升到作用域的最前面,但是他的值却只有在运行到代码行时才会被赋值,永远都使用 constlet,了解 时效区(Temporal Dead Zones) 的相关知识,也还要知道为什么 typeof 不再安全

// 我们知道这个不会工作,假设没有定义全局的notDefined
function example() {
  console.log(notDefined); // => throws a ReferenceError
}

// 在你引用的地方之后声明一个变量,他会正常输出是因为变量作用域上升。
// 注意: declaredButNotAssigned的值没有上升
function example() {
  console.log(declaredButNotAssigned); // => undefined
  var declaredButNotAssigned = true;
}

// 解释器把变量声明提升到作用域最前面,
// 可以重写成如下例子, 二者意义相同
function example() {
  let declaredButNotAssigned;
  console.log(declaredButNotAssigned); // => undefined
  declaredButNotAssigned = true;
}

// 用 const, let就不一样了
function example() {
  console.log(declaredButNotAssigned); // => throws a ReferenceError
  console.log(typeof declaredButNotAssigned); // => throws a ReferenceError
  const declaredButNotAssigned = true;
}

匿名函数表达式与 var 的情况一样

function example() {
  console.log(anonymous); // => undefined

  anonymous(); // => TypeError anonymous is not a function

  var anonymous = function () {
    console.log('anonymous function expression');
  };
}

已命名函数表达式只提升变量名,而不是函数名或者函数体

function example() {
  console.log(named); // => undefined

  named(); // => TypeError named is not a function

  superPower(); // => ReferenceError superPower is not defined

  var named = function superPower() {
    console.log('Flying');
  };
}

// 函数名和变量名一样是也如此
function example() {
  console.log(named); // => undefined

  named(); // => TypeError named is not a function

  var named = function named() {
    console.log('named');
  };
}

函数声明则提升了函数名和函数体

function example() {
  superPower(); // => Flying

  function superPower() {
    console.log('Flying');
  }
}

比较操作符

永远使用 ===!==,而不是 ==!=

eslint

if 条件语句的强制 toBoolean

if 条件语句的强制 toBoolean 总是遵循以下规则:

  • Objects 总是计算成 true
  • Undefined 总是计算 成 false
  • Null 总是计算成 false
  • Booleans 计算成它本身的布尔值
  • Numbers

    • +0-0 或者 NaN 总是计算成 false
    • 其它的全部为 true
  • Strings

    • '' 计算成 false
    • 其它全部为 true
注意NaN 是不等于 NaN 的,请使用 isNaN() 检测。
if ([0] && []) {
  // true
  // 数组(即使是空数组)是对象,对象会计算成true
}

console.log(NaN === NaN) // => false
console.log(isNaN(NaN))  // => true

布尔值要使用缩写,但是字符串与数字要明确比较对象

// bad
if (isValid === true) {
  // ...
}

// good
if (isValid) {
  // ...
}

// bad
if (name) {
  // ...
}

// good
if (name !== '') {
  // ...
}

// bad
if (collection.length) {
  // ...
}

// good
if (collection.length > 0) {
  // ...
}

switchcasedefault 分句中使用大括号创建语法声明区域

eslint

语法声明在整个 switch 的代码块里都可见,但是只有当其被分配后才会初始化,他的初始化时当这个 case 被执行时才产生。 当多个 case 分句试图定义同一个事情时就出问题了

// bad
switch (foo) {
  case 1:
    let x = 1;
    break;
  case 2:
    const y = 2;
    break;
  case 3:
    function f() {
      // ...
    }
    break;
  default:
    class C {}
}

// good
switch (foo) {
  case 1: {
    let x = 1;
    break;
  }
  case 2: {
    const y = 2;
    break;
  }
  case 3: {
    function f() {
      // ...
    }
    break;
  }
  case 4:
    bar();
    break;
  default: {
    class C {}
  }
}

三元运算符不能被嵌套

eslint

// bad
const foo = maybe1 > maybe2
  ? "bar"
  : value1 > value2 ? "baz" : null;

// better
const maybeNull = value1 > value2 ? 'baz' : null;

const foo = maybe1 > maybe2
  ? 'bar'
  : maybeNull;

// best
const maybeNull = value1 > value2 ? 'baz' : null;

const foo = maybe1 > maybe2 ? 'bar' : maybeNull;

避免不必要的三元表达式

// bad
const foo = a ? a : b;
const bar = c ? true : false;
const baz = c ? false : true;

// good
const foo = a || b;
const bar = !!c;
const baz = !c;

除非优先级显而易见,否则使用圆括号来混合操作符

eslint

开发者需要以最显而易见的方式明确自己的意图与逻辑

// bad
const foo = a && b < 0 || c > 0 || d + 1 === 0;

// bad
const bar = a ** b - 5 % d;

// bad
// 别人会陷入(a || b) && c 的迷惑中
if (a || b && c) {
  return d;
}

// good
const foo = (a && b < 0) || c > 0 || (d + 1 === 0);

// good
const bar = (a ** b) - (5 % d);

// good
if (a || (b && c)) {
  return d;
}

// good
const bar = a + b / c * d;

区块

用大括号包裹多行代码

eslint

// bad
if (test)
  return false;

// good
if (test) return false;

// good
if (test) {
  return false;
}

// bad
function foo() { return false; }

// good
function bar() {
  return false;
}

if 以及 elseif 的关闭大括号在同一行

eslint

// bad
if (test) {
  thing1();
  thing2();
}
else {
  thing3();
}

// good
if (test) {
  thing1();
  thing2();
} else {
  thing3();
}

if 语句中的 return

eslint

  • no-else-return
  • 如果 if 语句中总是需要用 return 返回,那么后续的 else 就不需要写了
  • 如果 if 块中包含 return,它后面的 else if 也包含了 return,那就应该把 else ifreturn 分到多个 if 语句块中去。
// bad
function foo() {
  if (x) {
    return x;
  } else {
    return y;
  }
}

// bad
function cats() {
  if (x) {
    return x;
  } else if (y) {
    return y;
  }
}

// bad
function dogs() {
  if (x) {
    return x;
  } else {
    if (y) {
      return y;
    }
  }
}

// good
function foo() {
  if (x) {
    return x;
  }

  return y;
}

// good
function cats() {
  if (x) {
    return x;
  }

  if (y) {
    return y;
  }
}

// good
function dogs(x) {
  if (x) {
    if (z) {
      return y;
    }
  } else {
    return z;
  }
}

控制语句

当你的控制语句 (ifwhile)等太长,或者超过最大长度限制时,把每一个判断条件都放到单独一行去,逻辑操作符放在行首

// bad
if ((foo === 123 || bar === 'abc') && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) {
  thing1();
}

// bad
if (foo === 123 &&
  bar === 'abc') {
  thing1();
}

// bad
if (foo === 123
  && bar === 'abc') {
  thing1();
}

// bad
if (
  foo === 123 &&
  bar === 'abc'
) {
  thing1();
}

// good
if (
  foo === 123
  && bar === 'abc'
) {
  thing1();
}

// good
if (
  (foo === 123 || bar === 'abc')
  && doesItLookGoodWhenItBecomesThatLong()
  && isThisReallyHappening()
) {
  thing1();
}

// good
if (foo === 123 && bar === 'abc') {
  thing1();
}

不要用选择操作符代替控制语句

// bad
!isRunning && startRunning();

// good
if (!isRunning) {
  startRunning();
}

注释

多行注释使用 /** ... */

// bad
// make() 基于传入的 `tag` 名返回一个新元素
//
// @param {String} 标签名
// @return {Element} 新元素
function make(tag) {

  // ...

  return element;
}

// good
/**
 * make() 基于传入的 `tag` 名返回一个新元素
 * @param {String} 标签名
 * @param {Element} 新元素
 */
function make(tag) {

  // ...

  return element;
}

单行注释用 //

将单行注释放在被注释区域上面。如果注释不是在第一行,那么注释前面就空一行

// bad
const active = true;  // is current tab

// good
// 当前激活状态的 tab
const active = true;

// bad
function getType() {
  console.log('fetching type...');
  // 设置默认 `type` 为 'no type'
  const type = this._type || 'no type';

  return type;
}

// good
function getType() {
  console.log('fetching type...');

  // 设置默认 `type` 为 'no type'
  const type = this._type || 'no type';

  return type;
}

// also good
function getType() {
  // 设置默认 `type` 为 'no type'
  const type = this._type || 'no type';

  return type;
}

所有注释开头空一个,方便阅读

eslint

// bad
//当前激活的 tab
const active = true;

// good
// 当前激活的 tab
const active = true;

// bad
/**
 *make() 基于传入的 `tag` 名返回一个新元素
 *@param {String} 标签名
 *@param {Element} 新元素
 */
function make(tag) {

  // ...

  return element;
}

// good
/**
 * make() 基于传入的 `tag` 名返回一个新元素
 * @param {String} 标签名
 * @param {Element} 新元素
 */
function make(tag) {

  // ...

  return element;
}

积极使用 FIXMETODO

当你的注释需要向注释阅读者或者代码的后继开发者明确的表述一种期望时,应该积极使用 FIXME 以及 TODO 前缀,这有助于其他的开发理解你指出的需要重新访问的问题,也方便自己日后有时间的时候再次回顾当时没有解决或者计划去做而没有做的事情。

  • FIXME:这里有一个问题,现在还没有影响大局,但是更希望解决这个问题或者找到一个更优雅的方式去实现
  • TODO:计划在这里去实现某些功能,现在还没有实现
// 使用 FIXME: 
class Calculator extends Abacus {
  constructor() {
    super();

    // FIXME: 不应该在此处使用全局变量
    total = 0;
  }
}

// 使用 TODO: 
class Calculator extends Abacus {
  constructor() {
    super();

    // TODO: total 应该应该从一个参数中获取并初始化
    this.total = 0;
  }
}

空格

代码缩进总是使用两个空格

eslint

// bad
function foo() {
∙∙∙∙const name;
}

// bad
function bar() {
∙const name;
}

// good
function baz() {
∙∙const name;
}

在大括号前空一格

eslint

// bad
function test(){
  console.log('test');
}

// good
function test() {
  console.log('test');
}

// bad
dog.set('attr',{
  age: '1 year',
  breed: 'Bernese Mountain Dog',
});

// good
dog.set('attr', {
  age: '1 year',
  breed: 'Bernese Mountain Dog',
});

关键字空格

eslint

在控制语句( if, while 等)的圆括号前空一格。在函数调用和定义时,参数列表和函数名之间不空格。

// bad
if(isJedi) {
  fight ();
}

// good
if (isJedi) {
  fight();
}

// bad
function fight () {
  console.log ('Swooosh!');
}

// good
function fight() {
  console.log('Swooosh!');
}

用空格来隔开运算符

eslint

// bad
const x=y+5;

// good
const x = y + 5;

文件结尾加一个换行

eslint

// bad
function doSmth() {
  var foo = 2;
}
// bad
function doSmth() {
  var foo = 2;
}\n

使用多行缩进的方式进行一个长方法链调用

eslint

// bad
$('#items').find('.selected').highlight().end().find('.open').updateCount();

// bad
$('#items').
  find('.selected').
    highlight().
    end().
  find('.open').
    updateCount();

// good
$('#items')
  .find('.selected')
    .highlight()
    .end()
  .find('.open')
    .updateCount();

// bad
const leds = stage.selectAll('.led').data(data).enter().append('svg:svg').classed('led', true)
    .attr('width', (radius + margin) * 2).append('svg:g')
    .attr('transform', `translate(${radius + margin},${radius + margin})`)
    .call(tron.led);

// good
const leds = stage.selectAll('.led')
    .data(data)
  .enter().append('svg:svg')
    .classed('led', true)
    .attr('width', (radius + margin) * 2)
  .append('svg:g')
    .attr('transform', `translate(${radius + margin},${radius + margin})`)
    .call(tron.led);

// good
const leds = stage.selectAll('.led').data(data);

在一个代码块后下一条语句前空一行

// bad
if (foo) {
  return bar;
}
return baz;

// good
if (foo) {
  return bar;
}

return baz;

// bad
const obj = {
  foo() {
  },
  bar() {
  },
};
return obj;

// good
const obj = {
  foo() {
  },

  bar() {
  },
};

return obj;

// bad
const arr = [
  function foo() {
  },
  function bar() {
  },
];
return arr;

// good
const arr = [
  function foo() {
  },

  function bar() {
  },
];

return arr;

不要用空白行填充块

eslint

// bad
function bar() {

  console.log(foo);

}

// also bad
if (baz) {

  console.log(qux);
} else {
  console.log(foo);

}

// good
function bar() {
  console.log(foo);
}

// good
if (baz) {
  console.log(qux);
} else {
  console.log(foo);
}

圆括号里不加空格

eslint

// bad
function bar( foo ) {
  return foo;
}

// good
function bar(foo) {
  return foo;
}

// bad
if ( foo ) {
  console.log(foo);
}

// good
if (foo) {
  console.log(foo);
}

方括号里,首尾都不要加空格与元素分隔

eslint

// bad
const foo = [ 1, 2, 3 ];
console.log(foo[ 0 ]);

// good, 逗号分隔符还是要空格的
const foo = [1, 2, 3];
console.log(foo[0]);

花括号里要加空格

eslint

// bad
const foo = {clark: 'kent'};

// good
const foo = { clark: 'kent' };

避免一行代码超过100个字符(包含空格)

eslint

为了确保代码的人类可读性与可维护性,代码行应避免超过一定的长度

// bad
const foo = jsonData && jsonData.foo && jsonData.foo.bar && jsonData.foo.bar.baz && jsonData.foo.bar.baz.quux && jsonData.foo.bar.baz.quux.xyzzy;

// bad
$.ajax({ method: 'POST', url: 'https://parcmg.com/', data: { name: 'John' } }).done(() => console.log('Congratulations!')).fail(() => console.log('You have failed this city.'));

// good
const foo = jsonData
  && jsonData.foo
  && jsonData.foo.bar
  && jsonData.foo.bar.baz
  && jsonData.foo.bar.baz.quux
  && jsonData.foo.bar.baz.quux.xyzzy;

// good
$.ajax({
  method: 'POST',
  url: 'https://apis.parcmg.com/',
  data: { name: 'John' },
})
  .done(() => console.log('Congratulations!'))
  .fail(() => console.log('You have failed this city.'));

作为语句的花括号里不应该加空格

eslint

// bad
function foo() {return true;}
if (foo) { bar = 0;}

// good
function foo() { return true; }
if (foo) { bar = 0; }

, 前不要空格,,后需要空格

eslint

// bad
var foo = 1,bar = 2;
var arr = [1 , 2];

// good
var foo = 1, bar = 2;
var arr = [1, 2];

计算属性内要空格

eslint

// bad
obj[foo ]
obj[ 'foo']
var x = {[ b ]: a}
obj[foo[ bar ]]

// good
obj[foo]
obj['foo']
var x = { [b]: a }
obj[foo[bar]]

调用函数时,函数名和小括号之间不要空格

eslint

// bad
func ();

func
();

// good
func();

在对象的字面量属性中, keyvalue 之间要有空格

eslint

// bad
var obj = { "foo" : 42 };
var obj2 = { "foo":42 };

// good
var obj = { "foo": 42 };

行末不要空格

eslint

避免出现连续多个空行,文件末尾只允许空一行

eslint

// bad
var x = 1;



var y = 2;

// good
var x = 1;

var y = 2;

逗号

不要前置逗号

eslint

// bad
const story = [
    once
  , upon
  , aTime
];

// good
const story = [
  once,
  upon,
  aTime,
];

// bad
const hero = {
    firstName: 'Ada'
  , lastName: 'Lovelace'
  , birthYear: 1815
  , superPower: 'computers'
};

// good
const hero = {
  firstName: 'Ada',
  lastName: 'Lovelace',
  birthYear: 1815,
  superPower: 'computers',
};

额外结尾逗号

eslint

就算项目有可能运行在旧版本的浏览器中,但是像 Babel 这样的转换器都会在转换代码的过程中删除这些多余逗号,所以,大胆使用它,完全不会有副作用产生,相反的,他能让我们更方便的给对象或者多行数组增加、删除属性或者元素,同时,还能让我们的 git diffs 更清洁。
// bad - 没有结尾逗号的 git diff
const hero = {
     firstName: 'Florence',
-    lastName: 'Nightingale'
+    lastName: 'Nightingale',
+    inventorOf: ['coxcomb chart', 'modern nursing']
};

// good - 有结尾逗号的 git diff
const hero = {
     firstName: 'Florence',
     lastName: 'Nightingale',
+    inventorOf: ['coxcomb chart', 'modern nursing'],
};
// bad
const hero = {
  firstName: 'Dana',
  lastName: 'Scully'
};

const heroes = [
  'Batman',
  'Superman'
];

// good
const hero = {
  firstName: 'Dana',
  lastName: 'Scully',
};

const heroes = [
  'Batman',
  'Superman',
];

// bad
function createHero(
  firstName,
  lastName,
  inventorOf
) {
  // does nothing
}

// good
function createHero(
  firstName,
  lastName,
  inventorOf,
) {
  // does nothing
}

// good (在一个 "rest" 元素后面,绝对不能出现逗号)
function createHero(
  firstName,
  lastName,
  inventorOf,
  ...heroArgs
) {
  // does nothing
}

// bad
createHero(
  firstName,
  lastName,
  inventorOf
);

// good
createHero(
  firstName,
  lastName,
  inventorOf,
);

// good (在一个 "rest" 元素后面,绝对不能出现逗号)
createHero(
  firstName,
  lastName,
  inventorOf,
  ...heroArgs
)

分号

永远明确的使用分号结束你的代码行

eslint

当 JavaScript 遇到没有分号结尾的一行,它会执行 自动插入分号 Automatic Semicolon Insertion 这一规则来决定行末是否加分号。如果JavaScript在你的断行里错误的插入了分号,就会出现一些古怪的行为。当新的功能加到JavaScript里后, 这些规则会变得更复杂难懂。显示的结束语句,并通过配置代码检查去捕获没有带分号的地方可以帮助你防止这种错误。
// bad
(function () {
  const name = 'Skywalker'
  return name
})()

// good
(function () {
  const name = 'Skywalker';
  return name;
}());

// good, 行首加分号,避免文件被连接到一起时立即执行函数被当做变量来执行。
;(() => {
  const name = 'Skywalker';
  return name;
}());

强类型转换

在语句开始执行强制类型转换

使用 String 进行字符类型转换

eslint

// => this.reviewScore = 9;

// bad
const totalScore = new String(this.reviewScore); // typeof totalScore is "object" not "string"

// bad
const totalScore = this.reviewScore + ''; // invokes this.reviewScore.valueOf()

// bad
const totalScore = this.reviewScore.toString(); // 不保证返回string

// good
const totalScore = String(this.reviewScore);

使用 Number 进行数字类型转换

eslint

使用 parseInt 转换 string 通常都需要带上基数。

const inputValue = '4';

// bad
const val = new Number(inputValue);

// bad
const val = +inputValue;

// bad
const val = inputValue >> 0;

// bad
const val = parseInt(inputValue);

// good
const val = Number(inputValue);

// good
const val = parseInt(inputValue, 10);

在注释中说明为什么要使用移位运算

如果你感觉 parseInt 满足不要你的需求,想使用移位进行运算,那么你一定要写明白,这是因为 性能问题,同时,你还需要注意,数字使用 64 位 表示的,但移位运算常常返回的是 32 位的整形,移位运算对于大于 32 位的整数会导致一些 意外行为,最大的32位整数是 2,147,483,647

// good
/**
 * parseInt是代码运行慢的原因
 * 用Bitshifting将字符串转成数字使代码运行效率大幅增长
 */
const val = inputValue >> 0;

2147483647 >> 0 //=> 2147483647
2147483648 >> 0 //=> -2147483648
2147483649 >> 0 //=> -2147483647

布尔

const age = 0;

// bad
const hasAge = new Boolean(age);

// good
const hasAge = Boolean(age);

// best
const hasAge = !!age;

什么是 Lerna?

直接引用官方的一句话描述:

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

Lerna 是一个工具,它的主要工作,就是优化基于 gitnpm 工具管理多包(multi-packages)构架项目的工作流。

初始化 Lerna 项目

mkdir catx
cd catx

初始化完成之后,项目目录如下:

.
├── lerna.json         // lerna 配置文件
├── package.json       // npm 包配置文件
└── packages           // 子包

package.json 文件内容如下:

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^3.14.1"
  }
}

这个是当前的根项目,我们需要把里面的 name 属性改成自己根项目的名称,我的叫 catx

lerna.json 文件内容如下:

{
  "packages": [        // 本项目有哪些子包
    "packages/*"       // packages 目录下面的所有包均是
  ],
  "version": "0.0.0"   // 当前版本号
}

新建第一个子包:api

cd packages
mkdir api && cd api
npm init -y

现在整个工程的目录结构如下:

.
├── lerna.json
├── package.json
└── packages
    └── api
        └── package.json

lerna 是如何工作的?

lerna 允许你在 固定版本自主版本 两种方式中任选其一(Fixed or Independent)。

固定模式(Fixed)

简单来说,就是在 lerna.json 配置文件中保存一个 version 号,当你 lerna publish 时, lerna 会检测项目中所有的子包,只要有任何一个子包更新了,那么所有包都会同步更新版本,这是 Babel 项目现在使用的方式,如果有一个子包更新了主版本号,那么所有的子包都会同步更新。

自主模式

与固定模式相对的,自主模式允许你完全自主跟踪每一个子包的版本,包与包之间没有强关联。使用下面的命令初始化自主模式:

./node_modules/.bin/lerna init --independent

此时,再次查看 package.json 文件,内容如下:

{
  "packages": [
    "packages/*"
  ],
  "version": "independent"    // root 下 package.json 的版本号被设置为了 `independent`
}

npm init 执行完成之后,会在当前目录下面生成 package.json 文件,这时,我们经常会使用 npm install package-name 或者 npm install development-required-package-name --save-dev 来安装一些依赖的第三方包,它们分别是运行时依赖和开发时依赖,但是还有一个不太常见的依赖叫 对等依赖,即 peerDependencies,使用 npm install --save-peer 即可添加。

dependencies 依赖

假设我们现在有一个项目的的 package.json 文件如下:

{
  "name": "peer-dependencies-demo",
  "version": "0.0.0",
  "description": "Peer Dependencies Demo",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "pantao <ofcrab@gmail.com>",
  "license": "MIT",
  "dependencies": {
    "peer-dependencies-plugin": "^1.0.0"
  }
}

peer-dependencies-plugin 这个模块的 package.json 中有下面这样的依赖:

{
  ...
  "dependencies": {
    "peer-dependencies-plugin-core": "^1.0.0"
  }
  ...
}

安装完成之后,我们的项目目录结构将是下面这样的:

.
├──package.json
├──src
│  └──index.js
└──node_modules
   └──peer-dependencies-plugin
      └──node_modules
         └──peer-dependencies-plugin-core

此时,在 index.js 中,我们可以像下面这样引入 peer-dependencies-plugin

import PeerDependenciesPlugin from 'peer-dependencies-plugin'

但是,却不能像下面这样引用 peer-dependencies-plugin-core

import PeerDependenciesPluginCore from 'peer-dependencies-plugin-core'

这是因为,即使 peer-dependencies-plugin-core 已经安装进了 node_modules 里面的,但是却不在 node_modules 下,而是在他的子目录下的一个模块里面,import 的时候,只会在当前项目根目录的 node_modules 下查找,并不会去查找它的子目录。

所以,如果你在项目里面还要使用 peer-dependencies-plugin-core 的话,你还需要手工的安装对 peer-dependencies-plugin-core 依赖,这个时候,你的项目安装完成之后,目录结构就是下面这样的了:

.
├──package.json
├──src
│  └──index.js
└──node_modules
   └──peer-dependencies-plugin-core
   └──peer-dependencies-plugin
      └──node_modules
         └──peer-dependencies-plugin-core

这势必会靠成很多不必要的麻烦,首当齐冲的就是,你的项目依赖的是 1.0.0,而你依赖的另一个插件却只能支持到 0.0.8,这个时候,导致一个项目里面依赖了两次 peer-dependencies-plugin-core,而且还不是同一个版本。

peerDependencies 的引入

为了解决上面这种问题, peer-dependencies-plugin 在声明对 peer-dependencies-plugin-core 的依赖的时候,设置为 peerDependencies

{
  ...
  "peerDependencies": {
    "peer-dependencies-plugin-core": "^1.0.0"
  }
  ...
}

它会告诉 npm:**如果某个 package 依赖我,那么这个 package 也应该对 peer-dependencies-plugin-core 依赖,这个时候,你 npm install peer-dependencies-plugin 的时候,将得到下面这样的目录:

.
├──package.json
├──src
│  └──index.js
└──node_modules
   └──peer-dependencies-plugin-core
   └──peer-dependencies-plugin

npm2 中,就算当前项目的 package.json 中没有对 peer-dependencies-plugin-core 的依赖,它也会被直接安装进 node_modules 目录下,但是如果你现在使用的是 npm3 ,那么安装完成之后, npm 并不会主动的帮你安装模块的 peerDependencies,但是会发出一个警告,告诉你本次安装是否正确,可能是下面这样的:

peer-dependencies-plugin-core 是一个需要的依赖,但是还没有被安装。

此时,你需要手动的在 package.json 中指定对 peer-dependencies-plugin-core 的依赖。

什么时候使用 peerDependencies

npm 文档中对 peerDependencies 的介绍是:

In some cases, you want to express the compatibility of your package with a host tool or
library, while not necessarily doing a require of this host. This is usually referred to
as a plugin. Notably, your module may be exposing a specific interface, expected and
specified by the host documentation.

大概的意思就是:通常是在插件开发的场景下,你的插件需要某些依赖的支持,但是你又没必要去安装,因为插件的宿主会去安装这些依赖,你就可以用peerDependencies去声明一下需要依赖的插件和版本,如果出问题npm就会有警告来提醒使用者去解决版本冲突问题。

一个组件的文件结构该如何组织?

  • 一个组件放在一个文件夹下面
  • 有一个 index.md 文件,说明组件使用说明及注意事项
  • __test__,每一个组件需要进行单元测试,使用的是 Jest
  • 样式单独的 style 目录,入口名称统一为 index
  • types 目录里面添加了 index.d.ts
button/
├── __test__
│   └── button.test.js
├── index.js
├── index.md
└── style
    └── index.scss

判断当前宿主环境

export const ENV_TYPE = {
  WEAPP: 'WEAPP',
  WEB: 'WEB',
  RN: 'RN',
  SWAN: 'SWAN',
  ALIPAY: 'ALIPAY',
  TT: 'TT'
}

export function getEnv () {
  if (typeof wx !== 'undefined' && wx.getSystemInfo) {
    return ENV_TYPE.WEAPP
  }
  if (typeof swan !== 'undefined' && swan.getSystemInfo) {
    return ENV_TYPE.SWAN
  }
  if (typeof my !== 'undefined' && my.getSystemInfo) {
    return ENV_TYPE.ALIPAY
  }
  if (typeof tt !== 'undefined' && tt.getSystemInfo) {
    return ENV_TYPE.TT
  }
  if (typeof global !== 'undefined' && global.__fbGenNativeModule) {
    return ENV_TYPE.RN
  }
  if (typeof window !== 'undefined') {
    return ENV_TYPE.WEB
  }
  return 'Unknown environment'
}

在做插件开发时,如何导出更优雅?

分别使用 exportexport default,你可以通过一个上下文挂载所有导出,也可以通过解构去导入你想要的指定导出。

export {
  Component, Events, eventCenter, getEnv, ENV_TYPE, render, internal_safe_get, internal_safe_set, internal_inline_style, internal_get_original, noPromiseApis, onAndSyncApis,
  otherApis, initPxTransform
}

export default {
  Component, Events, eventCenter, getEnv, ENV_TYPE, render, internal_safe_get, internal_safe_set, internal_inline_style, internal_get_original, noPromiseApis, onAndSyncApis,
  otherApis, initPxTransform
}

一个逆天的正则

下面是来源于一个叫 js-token 的模块。

/((['"])(?:(?!\2|\\).|\\(?:\r\n|[\s\S]))*(\2)?|`(?:[^`\\$]|\\[\s\S]|\$(?!\{)|\$\{(?:[^{}]|\{[^}]*\}?)*\}?)*(`)?)|(\/\/.*)|(\/\*(?:[^*]|\*(?!\/))*(\*\/)?)|(\/(?!\*)(?:\[(?:(?![\]\\]).|\\.)*\]|(?![\/\]\\]).|\\.)+\/(?:(?!\s*(?:\b|[\u0080-\uFFFF$\\'"~({]|[+\-!](?!=)|\.?\d))|[gmiyus]{1,6}\b(?![\u0080-\uFFFF$\\]|\s*(?:[+\-*%&|^<>!=?({]|\/(?![\/*])))))|(0[xX][\da-fA-F]+|0[oO][0-7]+|0[bB][01]+|(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?)|((?!\d)(?:(?!\s)[$\w\u0080-\uFFFF]|\\u[\da-fA-F]{4}|\\u\{[\da-fA-F]+\})+)|(--|\+\+|&&|\|\||=>|\.{3}|(?:[+\-\/%&|^]|\*{1,2}|<{1,2}|>{1,3}|!=?|={1,2})=?|[?~.,:;[\](){}])|(\s+)|(^$|[\s\S])/g

看下面的代码:

const js = `const ENV_TYPE = {
  WEAPP: 'WEAPP',
  WEB: 'WEB',
  RN: 'RN',
  SWAN: 'SWAN',
  ALIPAY: 'ALIPAY',
  TT: 'TT'
}`

const reg = /((['"])(?:(?!\2|\\).|\\(?:\r\n|[\s\S]))*(\2)?|`(?:[^`\\$]|\\[\s\S]|\$(?!\{)|\$\{(?:[^{}]|\{[^}]*\}?)*\}?)*(`)?)|(\/\/.*)|(\/\*(?:[^*]|\*(?!\/))*(\*\/)?)|(\/(?!\*)(?:\[(?:(?![\]\\]).|\\.)*\]|(?![\/\]\\]).|\\.)+\/(?:(?!\s*(?:\b|[\u0080-\uFFFF$\\'"~({]|[+\-!](?!=)|\.?\d))|[gmiyus]{1,6}\b(?![\u0080-\uFFFF$\\]|\s*(?:[+\-*%&|^<>!=?({]|\/(?![\/*])))))|(0[xX][\da-fA-F]+|0[oO][0-7]+|0[bB][01]+|(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?)|((?!\d)(?:(?!\s)[$\w\u0080-\uFFFF]|\\u[\da-fA-F]{4}|\\u\{[\da-fA-F]+\})+)|(--|\+\+|&&|\|\||=>|\.{3}|(?:[+\-\/%&|^]|\*{1,2}|<{1,2}|>{1,3}|!=?|={1,2})=?|[?~.,:;[\](){}])|(\s+)|(^$|[\s\S])/g

console.log(JSON.stringify(js.match(reg)))

输出是:

["const"," ","ENV_TYPE"," ","="," ","{","\n  ","WEAPP",":"," ","'WEAPP'",",","\n  ","WEB",":"," ","'WEB'",",","\n  ","RN",":"," ","'RN'",",","\n  ","SWAN",":"," ","'SWAN'",",","\n  ","ALIPAY",":"," ","'ALIPAY'",",","\n  ","TT",":"," ","'TT'","\n","}"]

lerna

A tool for managing JavaScript projects with multiple packages.

一个用户管理具有多包的 JavaScript 项目的工具

我单独研究之后再另起一篇文章说明一下这个,以前一直没找相关的工具,这次从 TaroJS 中看到了,初看很不错。

rollup

这个不多说了,一个不错的选择。

Interface (接口)

在 TypeScript 中,使用 interface (接口)来定义对象的类型,在面向对象语言中,接口是对行为的抽象,而具体如何行动,则需要 class 或者 implement 去实现,在 TypeScript 中,接口可以对 类的一部分行为 进行抽象,也常常用于对 对象的形状(Shape)进行描述

interface Person {
  name: string,
  age: number,
}

const me: Person = {
  name: 'Pan Tao',
  age: 30
}

在上面的示例中,先定义了一个名为 Person 的接口,接着定义了一个常量 me,它的类型是 Person,这样就约束了 me 的形状必须与接口 Person 一致,接口一般首字母大写。

注意:有一些编程语言建议接口的名称前面加上 I 作为前缀,表示这是一个 Interface

当定义了一个变量的接口类型之后,他的属性就不能比定义的多也不能比定义的少,赋值的时候,变量的形状必须和接口的形状保持一致

interface Person {
    name: string;
    age: number;
}

let me: Person = {
    name: 'Pan Tao'
};

上面会报错:

index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
  Property 'age' is missing in type '{ name: string; }'.
interface Person {
    name: string;
    age: number;
}

let me: Person = {
    name: 'Pan Tao',
    age: 30,
    gender: 'male'
};

上面会报错:

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可选属性与任意属性以及只读属性

有时候我们希望不要完全匹配,那么就可以使用可选属性或者任意属性

可选属性

interface Person {
  name: string;
  age?: number;
}

const me: Person = {
  name: 'Pan Tao'
}

在定义一个属性是,如果属性名后面加一个 ? 号,则表示该属性是可选的,在定义对象时,可以提供也可以不提供

任意属性

interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}


const me: Person = {
    name: 'Pan Tao',
    age: 30,
    gender: 'male'
};

const you: Person = {
  name: 'Name'
}

[propName: string]: any; 定义了属性名为 string 类型,值可为任意类型的属性。

如果我们给任意属性的值定义了类型,那么所有确定属性和可选属性的值类型也必须是任意属性值类型的子集

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

const me: Person = {
    name: 'Pan Tao',
    age: 30,
    gender: 'male'
};

上面的代码会报错:

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

只读属性

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

const me: Person = {
    id: 1,
    name: 'Pan Tao',
    gender: 'male'
};

tom.id = 2;

id 是只读属性,只能在创建的时候被赋值,之后不再允许被修改:

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

描述一个函数

interface 可以像下面这样描述一个函数:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

interface CreatePerson {
  (name: string, age: number) => Person;
}

extends

interface Person {
  readonly id: number;
  name: string;
  age?: number;
  [propName: string]: any;
}

interface Programmer extends Person {
  skills: Array<string>;
}

Type (类型)

区别

初始化 npm 项目

yarn init

添加依赖

yarn add hapi

添加开发依赖

要在开发中使用 TypeScrip,同时至少需要有一个工具,可以一直监听项目文件的变更,并实时的将变更更新至启动的服务中,我选择使用 Nodemon,首先添加以下几个开发依赖

yarn add typescript -D
yarn add nodemon -D

接下来,我们需要为 nodehapi 安装类型定义库:

yarn add @types/node -D
yarn add @types/hapi -D

安装完成之后, package.json 文件看起来像下面这样的:

{
  "name": "hapiserver",
  "version": "0.0.1",
  "description": "API server",
  "main": "index.js",
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "hapi": "^18.1.0"
  },
  "devDependencies": {
    "@types/hapi": "^18.0.2",
    "@types/node": "^12.0.2",
    "nodemon": "^1.19.0",
    "typescript": "^3.4.5"
  }
}
注意:你的 dependenciesdevDependencies 配置中,版本号可能与我的不同。

配置 TypeScript

设计项目文件目录结构

在项目的根目录下,创建一个名为 src 的目录,用于包含系统的所有源代码文件,接着,创建一个名为 dist 的目录,用于保存由 typescript 编译后的 javascript 文件。

注意:文件结构并不是强制的,你可以完全按照自己的习惯和规范来进行
.
├── dist
├── node_modules
├── package.json
├── src
└── yarn.lock

tsconfig.json

TypeScript 会查询名为 tsconfig.json 的配置文件来查找项目的入口文件以及编译设置,关于它的详细使用说明,可以从 https://www.typescriptlang.org/docs/handbook/tsconfig-json.html 查阅,在这里,我们先填入以下内容:

{
  "compilerOptions": {
    "outDir": "./dist",
    "allowJs": false,
    "target": "es6",
    "sourceMap": true,
    "module": "commonjs",
    "moduleResolution": "node"
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules"]
}

tsconfig.json 文件中,我们定义了 outDir 的值为 ./dist,它告诉编译器,编译后的输出目录为 ./dist 文件夹,现在可以直接在项目根目录执行以下代码,即可编译 src 目录下的 TypeScript 代码至 dist 目录下的 JavaScript 文件了。

node_modules/typescript/bin/tsc

用 TypeScript 开发 Hapi 服务应用

src 目录下,创建一个名为 server.ts 的文件,内容如下:

import * as hapi from "hapi";

// 创建一个服务器,监听 `localhost` 上的 `8000` 商品
const server: hapi.Server = new hapi.Server({
  host: "localhost",
  port: 8000
});

// 添加路由
server.route({
  method: "GET",
  path: "/hello",
  handler: function(request, h) {
    return "Hello! TypeScript!";
  }
});

// 启动服务
async function start() {
  try {
    await server.start();
  } catch (err) {
    console.log(err);
    process.exit(1);
  }
  console.log("Server running at:", server.info.uri);
}

// 不要忘记启动服务
start();

由于我们的代码是由 TypeScript 写的,所以现在还没有办法直接运行,需要先将其编译为 JavaScript 代码之后再运行:

使用下面的命令编译代码:

node_modules/typescript/bin/tsc

编译完成之后,将得到下面这样的两个文件:

dist
├── server.js
└── server.js.map

此时,执行下面的代码,启动服务:

node dist/server.js

启动成功之后,终端将显示:

Server running at: http://localhost:8000

使用 curl 测试一下我们的服务:

$ curl -i http://localhost:8000/hello
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
cache-control: no-cache
content-length: 18
accept-ranges: bytes
Date: Fri, 17 May 2019 01:58:50 GMT
Connection: keep-alive

Hello! TypeScript!

已经启动成功了。

完成所有配置

我们总不能每改一次代码,都手工执行一次编译,再重新启动服务,可以在 package.json 中添加两个命令:

{
  ...
  "scripts": {
    "start": "./node_modules/nodemon/bin/nodemon.js -e ts  --exec \"yarn run compile\"",
    "compile": "tsc && node ./dist/server.js"
  },
  ...
}

现在,只需要在项目根目录下执行以下代码,即可启动一个实时编译代码并自动重新服务的开发环境了:

yarn start

它的作用是:nodemon 启动一个服务,监听文件的变更,当有任何文件变更之后,执行 yarn run compile 命令(即执行:tsc && node ./dist/server.js,以重启服务。

初始化一个应用

使用 react-native init AppName 命令初始化一个 React Native 应用

react-native init Chongai
mv Chongai chongai-app
cd chongai-app
我习惯性的使用 CamelCase 规范命名 App 名称,同时将文件夹改为 foobar-app 方式,上面那一行 mv 命令不一定都需要,我是为了保证项目的 git 地址没有大写字母

设置 git

git init
git remote add origin git@git.domain.com:namespace/project-name.git
git push -u origin master

安装 PrettierESLint

在本地项目中安装开发依赖

yarn add -D eslint prettier
yarn add -D eslint-config-airbnb
npx install-peerdeps --dev eslint-config-airbnb
yarn add -D eslint-config-prettier eslint-plugin-prettier

添加 .eslintrc.json 文件:

{
  "extends": ["airbnb", "prettier"],
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": ["error"]
  }
}

添加 .prettierrc

{
  "printWidth": 100,
  "singleQuote": true
}

配置编辑器

我使用的是 VSCode:

分别安装 ESLintPrettier 插件。

设置配置项目:

{
  ...
  "editor.formatOnSave": true
  ...
}

安装常用库

yarn add react-native-gesture-handler
react-native link react-native-gesture-handler

yarn add react-navigation

yarn add react-native-video
react-native link react-native-video

yarn add react-native-camera
react-native link react-native-camera

yarn add react-native-device-info
react-native link react-native-device-info

yarn add react-native-image-picker
react-native link react-native-image-picker

早期的身份证号码称之为社会保障号,为15位,1999年开始,更名为公众民份证号码,即第二代身份证号码,为18位,且终身不变。

公民身份证号码是特征组合码,由前17位数字本体码和最后一位数字校验码组成,排列顺序从左至右依次为六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。

地址码: 表示编码对象常住户口所在县(市、旗、区)的行政区划代码。对于新生儿,该地址码为户口登记地行政区划代码。需要没说明的是,随着行政区划的调整,同一个地方进行户口登记的可能存在地址码不一致的情况。行政区划代码按GB/T2260的规定执行。

出生日期码:表示编码对象出生的年、月、日,年、月、日代码之间不用分隔符,格式为YYYYMMDD,如19880328。按GB/T 7408的规定执行。原15位身份证号码中出生日期码还有对百岁老人特定的标识,其中999、998、997、996分配给百岁老人。

顺序码: 表示在同一地址码所标识的区域范围内,对同年、同月、同日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配给女性。

校验码: 根据本体码,通过采用ISO 7064:1983,MOD 11-2校验码系统计算出校验码。算法可参考下文。前面有提到数字校验码,我们知道校验码也有X的,实质上为罗马字符X,相当于10。

15 位与 18 位身份证号码的差异

  • 出生日期码:15位身份证号码中出生日期码为6位,其中年份代码仅有2位,如590328,代表1959年生。
  • 校验码:15位身份证号码中无校验位。

校验码算法

将本体码各位数字乘以对应加权因子并求和,除以11得到余数,根据余数通过校验码对照表查得校验码。

加权因子如下:

位置序号1234567891011121314151617
加权因子7910584216379105842

校验码:

余数012345678910
校验码10X98765432

比如本体码为 11010519491231002

  1. 各位数与对应加权因子乘积求和:

    1 * 7 + 1 * 9 + 0 + 10 + ... + 0 * 2 = 167
  2. 对求和进行除 11 得余数 167 % 11 = 2
  3. 根据余数 2 对照校验码得 X

因此完整身份证号码为: 11010519491231002X