云开发·多次订阅一次性订阅消息后定时发送

小程序一次性订阅消息,订阅1次可以发送1条消息,订阅10次可以发送10条消息

1. 前情提要,完成订阅到发送的过程

订阅部分参考 实战分享: 小程序云开发玩转订阅消息 就可以完成从小程序订阅、存入云开发数据库、利用定时触发器定期发送消息了。

完成上面的步骤,你应该已经在云端做到了:
1. 定时任务
1. 查询所有订阅消息
1. 循环发送消息
1. 发送后根据_id标记状态为已发送

但是上文的订阅消息,适用于只订阅一次的情况,查看github的源码,甚至为了避免重复,同一个用户不能订阅多次。

我们要做的逻辑是,同一个一次性订阅消息,用户可以订阅多次,订阅几次就发送几次。

2. 改造查询,支持每个用户只发送一条

保存处代码不用修改,用户多次订阅就会插入多条记录。查询代码需要做修改,目前的查询代码是这样的:

    const messages = await db
      .collection('messages')
      .where({
        done: false,
      })
      .get();

查询到了所有未发送的消息,此时一个用户订阅2次,都会查出来,并收到2条订阅消息。

我们使用 aggregate 把这里改为类似MySQL中的 distinct 查询:

db.collection('messages')
  .aggregate()
  .match({
    done: false,
  })
  .group({
    "_id": '$touser',
    "idList": $.addToSet("$_id")
  })
  .end()

注意这里的 addToSet 是把前面查询的结果,根据touser(也就是用户的openid)重新聚合,把每条消息的 _id 放到一个列表中,看起来如下:

[
  {
    "_id": "oiRsI0RKU8IG2Y9Z_Y6Y5aC9JGt0",// openid
    "idList": [
      "112557505f9001d1001f90eb23d5894b", // 数据库_id
      "2c9645925f8f097d001dd5170f0b7727"  // 数据库_id
    ]
  },
  {
    "_id": "oiRsI0d8Zp8e1edtDlPeuNUZWB68",
    "idList": [
      "2c9645925f900210001eb0814df399fc",
      "d9ea4cfd5f90020c001b5ab04fc0ed01",
      "8f29e52a5f8f0972001b9015226aaad9"
    ]
  }
]

这样我们发送时,从 idList 中取一个_id标记为已发送,就能实现每次只消耗1次“资格”了。

3. 分页与循环查询

官方文档指出云端函数Collection.limit 最多 1000 条,但是实际上有人测试过Aggregate 聚合操作可以 最多查询 10000 条!。具体限制各位自己测试。如果你的订阅非常多的话,就需要加入循环也分页了。

先查询记录总数,再分页查询,然后再聚合。

注意上面的顺序,因为是先分页再聚合,所以最终出来的结果可能会少于每页条数,不过我们都是汇总再发送,对我们影响不大了。

4. 附加说明

因为我的需求比较简单,前面的查询代码没有区分模板消息种类,有需要的同学可以增加查询条件,如果需要在集合里展示更多字段,addToSet可以这样写:

$.addToSet({'id':"$_id","templateId":"$templateId"});

外卖先领优惠券

以上小程序已开源,欢迎围观[4]

##参考资料

[1]
实战分享: 小程序云开发玩转订阅消息: https://developers.weixin.qq.com/community/develop/article/doc/000608a12b07b8ea81599420e56013
[2]
官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.limit.html#%E8%AF%B4%E6%98%8E
[3]
最多查询 10000 条!: https://developers.weixin.qq.com/community/develop/article/doc/000624c67c8b48611dba2b12058c13
[4]
欢迎围观: https://github.com/PlayerYK/coupon_open

[译]你只加了两行代码,为什么要花两天时间?

来源: https://www.mrlacey.com/2020/07/youve-only-added-two-lines-why-did-that.html

这个问题看起来很合理,但是仔细想想,这个问题背后有一些很极端的假设:
– 代码行数 = 工作量
– 代码行数 = 价值
– 所有的代码都是等价的

以上皆错。

那么为什么修复一些看起来很简单的问题需要花两天呢?

1. 因为报告问题的人没有描述如何重现

我花了好几个小时才能稳定的重现问题。有些开发者可能会立即去找报告问题的人,在开始调查问题之前试着了解更多信息。有些开发者不喜欢修复bug,所以他们尽量避免这些问题,没有足够的信息就是一个不错的接口。我不是不想帮,是帮不了。我对上报问题的用户很感激,我知道上报错误不是很容易,所以我在联系用户前,会尽量利用已有信息解决问题。

2. 因为报告中提到的功能我不熟悉

这个功能我很少用,也从来没详细使用过。也就是说我要花比平常更久的时间才能理解怎么使用这个功能,还要再花一些时间理解有bug时怎样影响这个功能。

3. 因为我在花时间调查真正的原因,而不是仅仅观察问题的表现

不能头疼医头脚疼医脚,隐藏错误不是解决问题。藏起来一个问题很有可能会导致其他副作用,我不想以后再跟这个bug打交道。

4. 因为我在查产生这个问题的其他方式,而不是仅解决报告中的步骤

按照步骤重现问题很简单,但实际上的问题可能是更深层次的。找到问题的原因,找到重现这个问题的其他步骤,可以洞察到更有价值的内容。例如代码实际上是如何使用的,有没有其他地方存在类似的问题,或者可能会发现同样功能的代码写法不一致,有些代码没有同步复制到另一处?

5. 因为我在花时间验证代码中是否有其他部分受到类似的影响

如果一个错误导致了bug,代码库中可能还会有类似的错误。这时候趁热查找最容易发现。

6. 因为我找到原因后,尝试用最简单的方法修复,避免出错风险

而不是用最快的方法,我不想修复一个问题未来产生更多的问题。

7. 因为我对改动做了彻底的测试,并且验证了受影响的所有路径

我不想依靠别人测试我的改动是否正确。我不想放下这段代码后再出现bug。程序员切换 Context 是很昂贵很疲惫的事情。

我不喜欢改bug,部分原因是,这感觉就像是在面对我以前做的错事。还有一个原因是我更喜欢做新东西。

比改bug更糟的事是什么?
多次修复同一个bug。
我更愿意花时间确保一个bug被完全修复了,不需要再一次去面对、去调查、去修复、去测试。

【译】【UX】一个页面可以有多个面包屑导航吗?

原讨论地址

提问

面包屑导航可以让用户在网站中定位,展示了到达当前页面的路径。如果一个网页属于不同的类别,能用多个面包屑导航吗,或者有没有更好的展示方式?

例如一个作家可以有不同的归类:

Writers // Origin // England // William Shakespeare
Writers // Periods // XVI century // William Shakespeare
Writers // Schools // Dramaturgy // William Shakespeare

这时候放多个面包屑导航是正确的做法吗?

评论

#1 这个页面是怎么来的?直接打开还是一步一步点进来的?
比如莎士比亚的页面,是怎么进来的,是从

www.MyWeb.Site/Writers/Origin/England/William%20Shakespeare.php
还是
www.MyWeb.Site/Writers/Periods/XVI%20century/William%20Shakespeare.php
或者就只是
www.MyWeb.Site/pages/William%20Shakespeare_(writer).php
看路径就知道该展示哪一个了吧?

回答 1

面包屑路径只有一条,最好不要打破这个惯例。一般来说用户看到面包屑主要是为了这几件事:

  • 我现在在哪
  • 我怎么来到这儿的
  • 向后导航

如果有多个路径,完成上面几个任务就很麻烦,有的还做不了。

如果想要面包屑路径交互性更好,还想要多个分支,可以考虑下拉菜单。

breadcrumbs1

同级别的其他分类链接放到下拉菜单中,可以方便的导航到其他分类,还能保持页面展示的路径不变。

breadcrumbs2

这种做法也不常用,建议使用前做个用户测试。

回答 1 的评论

我觉得这已经不能叫面包屑导航了。

回答 2

你这种场景就不应该用面包屑。

你的问题已经描述的很清楚了,从ShakespeareWriter中间有多条路径,但是面包屑只有一条路径。

怎么办?
换成 tags,按照你的例子,莎士比亚应该会关联到英格兰十六世纪戏剧标签。

tag

用标签有很多优点:

  • 很容易识别,用户可以不用点击也不用离开当前页面,就直接看到所有的类别。
  • 还可以改进不确定的搜索,用户可以从多个标签中选择一个搜索
  • 标签占用的空间,不会超过原本类别需要的空间

总之标签不但可以提升作家页面的体验,还可以简化用户寻找其他作家的方式。并且用户在线上购物网站,早已经对这个体验很熟悉了。

面包屑最后变成了这样:

Writers // William Shakespeare

用来过滤和加书签的 url 使用类似这样的结构:

example.org/writers?origin=England&Schools=Dramaturgy

回答 3

维基百科已经解决过这个问题了
面向对象编程 举例,右侧的 编程范式 框中,有多个层级的链接指向当前页面。

实际上这已经不是面包屑导航了,而是一个网站地图,同时展示相关的其他类别。根据网站规模,可以适当缩减站点地图的范围,有必要的话,也可以默认隐藏一些分支,可以点击展开。

站点地图的有点是它包含了用户需要的所有信息,让这些信息尽可能的方便访问。如果用户想看波兰的作家,只需要点击一次就行了。此外,网站地图在整个网站任何页面看起来都是内容一致的,因此用户很容易熟悉使用。

用户在读关于莎士比亚的内容时,可能会参考一下网站地图,但是用户对莎士比亚以外的内容感兴趣的时候,就会去认真读网站地图。因此不应该只列出莎士比亚所在的类别,而是列出一些用户可能会感兴趣的其他类别。

2019年了,高手们现在怎么写JavaScript代码

2019年了,高手们现在都是怎么写JavaScript代码的,有没有一些看了就能用上的技巧,近些年有没有什么新的最佳实践?本文就是应StackOverFlow邀请所做的分享,作者慷慨分享了自己使用的方法和经验。读者可以根据实际情况自己选择,就算不接受也算开阔一下思路。原文评论争论比较激烈,也可以进去看看其他人的观点。

作者:Ryland Goldstein
原文链接: Practical Ways to Write Better JavaScript

有什么切实有效的方法写出更好的 JavaScript ,我看谈论这个话题的人不多,本文就是我最常用的一些实践。

1. 使用 TypeScript

改善JS的第一件事就是不要写JS。对不了解的人说明一下,TypeScript(TS) 是JS的严格超集(现有的JS程序都是合法的TS程序)。TypeScript 为原生 JavaScript 加上了(可选的)全面的类型批注。很长一段时间里,TS的支持不是很好,我也不推荐使用。但是现在大多数框架都支持了。知道了TS是什么,我们来看看为什么要用它。

1.1 TypeScript 强制类型安全

类型安全是这样实现的,编译时检查代码中所有的类型都以合法的方式使用。比如你创建了一个函数 foo 接受一个数字类型参数:

function foo(someNum: number): number {
  return someNum + 5;
}

调用这个 foo 函数时,只能传入数字:

// 正确
console.log(foo(2)); // 7
// 错误
console .log(foo("two")); // 不合法的 TS 代码

除了为代码添加类型的工作量,强制类型安全没有任何副作用。但是这样做的收益不容忽视。类型安全能防止一些常见的错误或者bug,这对弱语法的JS来说是一件幸事。

1.2 TypeScript 让大型应用得以重构

重构大型JS应用真的是一场噩梦,过程中最大的痛苦来自于没有强制参数类型检查,也就是说JS函数一点容错性都没有,假设我有一个函数 myAPI 被一千个服务调用:

function myAPI(someNum, someString) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}

这时候我把参数改成这样:

function myAPI(someString, someNum) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}

我必须保证这一千个函数调用,都要正确地修改参数,漏掉一个,程序就会出错。而在TS中是这样的:

// before
function myAPITS(someNum: number, someString: string) { ... }
// after
function myAPITS(someString: string, someNum: number) { ... }

如你所见,函数 myAPITS 做了与JS一样的修改,但在JS中不会报错的代码,在TS中会通不过编译,所有调用的地方都会报出类型错误。及早发现错误,而不会有bug隐藏在代码中。

1.3 TypeScript 让团队架构沟通更容易

正确设置 TS 后,不先定义接口和类型就很难开始写代码。这就有了一种简洁、易沟通的架构方案。在TS之前,不是没有类似方案,但是都需要做额外的事来达到同样的效果。用TS后,我可以把下面的提案发给同事:

interface BasicRequest {
  body: Buffer;
  headers: { [header: string]: string | string[] | undefined; };
  secret: Shhh;
}

我要先写一点代码,但是我可以不用花太多时间就能让同事明白我的进度,并获得反馈。我不确定TS本质上是不是比JS更不容易出错,但是我坚信强制开发者先定义接口能写出更好的代码。开发者肯定更熟悉原生JS,但是我现在做的大多数新项目,从一开始就是用TS。

2. 使用新特性

JavaScript 是世界上最流行的编程语言(写不写之一都行),这是一个数以亿计的人使用过的,超过20年历史的语言。最近很多修改和新特性加到了JS中(严格来说叫ECMAScript),已经从根本上改变了开发者的体验。作为一个在两年前才开始用JS的人,我的优势是没有偏见和历史包袱。能让我选用或者放弃某些功能,做出更加务实的选择。

2.1 async 和 await

在很长一段时间,异步、事件驱动的回调是JS开发中不可避免的一部分:

// 传统的回调
makeHttpRequest('google.com', function (err, result) {
  if (err) {
    console.log('Oh boy, an error');
  } else {
    console.log(result);
  }
});

我不在这里花时间解释上述代码的问题(但是我以前解释过)。为了解决回调问题,JS中新增了 Promise 的概念,使用 Promise 可以不用嵌套就能写出异步逻辑。

// 使用 Promise
makeHttpRequest('google.com').then(function (result) {
  console.log(result);
}).catch(function (err) {
  console.log('Oh boy, an error');
});

Promise 最大的优点在于可读性和链式写法,虽然很棒,但Promise写法还是太像以前的回调。那么有没有一种更好的替代方法呢,ECMAScript委员会决定添加一种新的方法来使用promises、async、wait:

// async 和 await
try {
  const result = await makeHttpRequest('google.com');
  console.log(result);
} catch (err) {
  console.log('Oh boy, an error');
}

唯一需要注意的是使用 await 之前,函数要定义为 async

// 上面的代码要这样定义 makeHttpRequest 函数
async function makeHttpRequest(url) {
  // ...
}

也可以直接 await 一个 Promise ,因为实际上 async 就是一个包装后的 Promise,所以请随意使用 async/await 不必感到内疚,他们与 Promise 是等价的。

2.2 let 和 const

一直以来,JS中声明变量只有一个范围会变化的限定符: var。它的作用范围规则很独特很有趣,var 的作用域不一致,不明确,可能导致意外发生,也是很多bug的元凶。但是在 ES6 中有了替代方法:let 和 const。这就没必要再用var了。所以以后不要使用 var ,任何用 var 的地方都可以转换为等效的 constlet 代码。

至于何时使用 const 何时使用 let,我都是先用 const , const 强制性更强,不可变,写出的代码更好。并没有太多“真正的场景”需要用到 let,可以说我代码里 let 用的不到 const 的 1/20 。

这里说的“不可变”(immutablish)不是C/C++中的 const 。JS中用 const 声明的变量不是说永远不会改变,而是指向变量的地址不变。简单类型(number、booolean等)内容永远不会变,但是所有对象(class、array、dict),const不保证不改变。

2.3 箭头 => 函数

剪头函数是JS中一种更简洁的声明匿名函数方法。匿名函数就是未明确命名的函数,一般用在回调或者事件处理函数。

// 匿名函数的普通写法
someMethod(1, function () { // has no name
  console.log('called');
});

一般情况下,这种写法不算错,但是匿名函数的作用域很“有趣”,可能会导致意料之外的错误。有了箭头函数,再也不用担心作用域的问题了,下面是用箭头函数实现同样的功能:

// 用箭头定义的匿名函数
someMethod(1, () => { // has no name
  console.log('called');
});

除了更简洁,箭头函数的作用域更实用,箭头函数的 this 继承自函数定义时的作用域(译注:而不是使用时所在的作用域)。

有时候,箭头函数还可以写的更简洁:

const added = [0, 1, 2, 3, 4].map((item) => item + 1);
console.log(added) // prints "[1, 2, 3, 4, 5]"

单行的箭头函数有个隐式 return 语句,也不用写前面的括号和函数体的分号。

再多说一点,用 var 声明变量的时候可不能这么用(译注:此处是说单行箭头函数返回对象字面量行不通)。普通的匿名函数还是有他的应用场景,默认用箭头函数,比默认用普通匿名函数能节省不少调试时间。

再次声明 MDN文档真好用。

2.4 扩展运算符 …

提取一个对象的键值对放到另外一个对象是常见的场景。以前有好几种方法实现,但是看起来都有些笨拙:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
const merged = Object.assign({}, obj1, obj2);
console.log(merged) // prints { dog: 'woof', cat: 'meow' }

这种写法很常见,也很乏味,多亏了扩展运算符,再也不用像上面那样写了:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }

更棒的是,数组也可以(译注:但是不能混用):

const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4]

2.5 模板字符串

在很多语言中,字符串都是最常见的结构之一,所以内置的字符串都做不好也太尴尬了,JS就属于这种语言。有了模板字符串以后,JS变得别具一格,解决了字符串处理中两个大问题,加入变量和换行:

const name = 'Ryland';
const helloString =
`Hello
 ${name}`;

代码可以说明一切,是不是很棒。

2.6 对象解构

对象解构可以不遍历数据或者访问对象的 key 就能获取值的方法,对象、数组等(译注:有 Iterator 接口的)都可以使用。

// 以前的写法
function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict.dog, myDict.cat);

对象解构:

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

const { dog, cat } = myDict; // 对象解构
animalParty(dog, cat);

还可以在函数参数解构:

function animalParty({ dog, cat }) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict);

数组的例子:

[a, b] = [10, 20];

console.log(a); // prints 10

还有很多新增的现代特性,下面几个我也特别推荐:

3. 始终假设你的系统是分布式的

写并行程序时,你从一开始就会优化,如果一核工作三核围观,就浪费了75%的算力。阻塞式的同步操作,是并行计算的最大敌人。但我们说的是JS,这是一个单线程语言,讨论这个有意义吗?

JS是单线程但不是单文件,没有并行计算,但是有并发。一个HTTP请求可能需要几秒钟甚至几分钟,这时候如果停下来等待请求返回,页面就没法用了。

JavaScript用事件循环解决了这个问题。事件循环遍历已注册的事件,根据JS内部调度和优先级逻辑执行这些事件。这就可以同时发送数千个(译注:浏览器限制一般几个或十几个)HTTP请求,也可以同时加载多个文件。要注意的是:只有正确的使用特性才能利用这一功能,举一个简单的例子, for 循环:

let sum = 0;
const myArray = [1, 2, 3, 4, 5, ... 99, 100];
for (let i = 0; i < myArray.length; i += 1) {
  sum += myArray[i];
}

原生的 for 循环是最难并行的构造之一。让for循环并行执行最大的困难源于一些特殊的代码结构,这些结构很少见,但是只要存在,for 循环就很难并行。

let runningTotal = 0;
for (let i = 0; i < myArray.length; i += 1) {
  if (i === 50 && runningTotal > 50) {
    runningTotal = 0;
  }
  runningTotal += Math.random() + runningTotal;
}

上面代码只有按顺序迭代执行,才能产生预期结果,如果试图让多个迭代并行,可能就会进入不正确的分支,导致计算结果无效。建议仅在绝对有必要时才使用传统的for循环,平时可以用以下代码代替:

// map
// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url) => makHttpRequest(url));
const results = await Promise.all(resultingPromises);

//map with index
// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url, index) => makHttpRequest(url, index));
const results = await Promise.all(resultingPromises);

//for-each
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
// note this is non blocking
urls.forEach(async (url) => {
  try {
    await makHttpRequest(url);
  } catch (err) {
    console.log(`${err} bad practice`);
  }
});

我解释一下,为什么这些写法是对传统for循环的改进,map 并不是以顺序迭代,而是将每个元素分别提交给用户定义的事件处理函数。大多数情况下,各个迭代之间没有依赖关系,因此可以并行。也不是for循环不能做同样的事,可以这样写:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

async function testCall() {
  // do async stuff here
}

for (let i = 0; i < 10; i += 1) {
  testCall();
}

是的for循环也可以,但是没有map简单,来看 map 版本:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
items.map(async (item) => {
 // do async stuff here
});

map这样写就可以了,用map还有一个好处就是如果你想等待所有异步操作完成后做处理,也可以很简单的实现,如果是用for循环的话,就需要自己管理一个数组才行,来看map怎么做:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const allResults = await Promise.all(items.map(async (item) => {
  // do async stuff here
}));

简单吧。

许多情况下,for循环与map和forEach的性能相当,可能更好一些,我仍然认为牺牲几个循环的时间,换来更清晰的API完全值得。未来也有机会改进请求数据的部分,而不用操心循环部分。for循环没有区分各种情况,要优化就麻烦的多了。

map和forEach之外还有其他可用的异步选项,例如for-await-of。

Lint和格式化

代码没有统一的风格,就很难阅读理解。因此高端的代码不管用任何语言写,都需要有统一并且合理的风格。JS语言生态系统广泛,代码格式和指南有很多种,一定要选择一种来格式化你的代码。选不选比选择哪个重要的多。

我看到很多人问到底是该用 eslint 还是 prettier。在我看来他们的用途不同,需要结合使用。Eslint 是一个老牌的lint工具,比起关注样式,更多的注重正确性。我在用AirBNB的Lint规则的配置,使用这个配置的时候以下的语句将会Lint失败:

var fooVar = 3; // airbnb 规则禁止使用 "var"

eslint 对开发的好处显而易见。从本质上说,它确保你的代码遵循最佳实践,有些规则不一定全部适合,可根据实际情况定制。

Prettier 是代码格式化工具,不太关心正确性,更注格式统一。 Prettier 不会抱怨你有没有使用 var,但是会自动对齐代码中的所有括号。我自己的开发过程中,推送代码到 git 之前总会跑一遍 Prettier 把代码格式化。还可以在每次提交git时自动格式化代码,保证入库的代码都有一致的风格。

4. 测试代码

写测试改善JS代码,是一种间接但是非常有效的方法。JS生态系统中有很多成熟的测试工具,没有一个工具可以处理所有的情况,我建议你熟悉各种测试工具,根据需求选择使用。

4.1 测试驱动程序 – Ava

Github上的Avajs

测试驱动程序,简单来说就是一个框架,在高层次提供结构和实用工具,根据测试需求不同,常常与其他特定的测试工具一起使用。

Ava 在功能和简洁之间取得很好的平衡。它的异步特性和并发运行架构是我最喜欢的。更快的运行节约了开发者的时间也为公司省钱。Ava保持简洁的同时,还内置了一些很不错的特性,比如断言。

替代选择:Jest,Mocha,Jasmin。

4.2 Spies 和 Stubs – Sinon

Github 上的 Sinon

spies 是一个函数分析工具,比如一个函数被调用了多少次,每次调用的详情等数据。

Sinon 可以做很多事,有几个工具超级好用, spiesstub 是最出色的两个,功能丰富而且语法简洁,

替代选择: testdouble

4.3 模拟请求 – Nock

github上的nock

HTTP模拟就是拼装http请求,模拟服务器端行为,结合自己的代码方便测试过程。

HTTP模拟写起来可能很麻烦,但是 nock 让这个过程方便不少,它直接重写 nodejs 的内置请求,并拦截传出的请求,让你可以完全控制响应。

替代选择:我没听说有。

4.4 Web 自动化测试 – Selenium

Github上的 Selenium

推荐 Selenium 让我心情复杂,由于它是Web自动化最流行的选择,拥有庞大的社区和在线资源。不幸的是,学习曲线相当陡峭,而且也依赖了太多的外部代码库。话虽如此,它也是唯一真正免费的选择,因此除非你是在进行企业级的Web自动化,否则 Selenium 都可以胜任。

替代选择:Cypress,,PhantomJS

永无止境的旅程

跟世间大多数事情一样,写出更好的JavaScript是一个持续的过程,代码总是可以更干净,总会有新特性被添加,测试怎么也不嫌多,让人应接不暇疲于应付,但是有这么多可以改进的地方,如果按照自己的步伐,一步一个脚印的做,不知不觉中就会成为JavaScript高手。

云开发·云调用生成小程序码

小程序云开发已经支持云调用,开放了很多接口,一直想要的获取小程序码也支持了。这下轻量的小程序也可以有自定义小程序码的功能。

1. 需求

获得一个带参数的小程序码,传播出去以后,用户扫码进入指定页面,根据参数做不同的处理。本文只讲小程序码生成、存储、展示部分。参数处理不多介绍,可以查看 项目代码 了解更多。

2. 开通云开发

新建小程序可以从开发工具的云开发模板初始化项目,根据云开发操作指引新建项目即可。

但是这里有个问题,已发布小程序的页面才能生成小程序码。如果现有的小程序没有开通云开发,需要做以下几步:

  1. 开发工具开通云开发,设定云开发的环境;
  2. 将原来的代码(除了project.config.json以外的所有文件)放到新建的 miniprogram 目录;
  3. 新增 cloudfunctions 目录;
  4. app.json 新增配置 "cloud": true
  5. project.config.json 配置 "miniprogramRoot":"miniprogram/""cloudfunctionRoot":"cloudfunctions/"
  6. 修改小程序基础库版本,最低要 2.3.0 "libVersion": "2.3.0"

3. 生成小程序码

下面可以开始写代码开发了,开始之前,建议先看完官方教程。特别是开发工具的使用步骤,开发和调试时如果遇到奇怪的问题,可以尝试重启开发工具、重装开发工具,也可以去微信开放社区发帖。(重启和重装都是我在社区中发现的答案,能解决各种不应该存在的问题)。

3.1 准备文件

cloudfunctions目录右键新建Node.js云函数 getqr

生成小程序码需要单独指定权限。在 getqr 目录新建 config.json ,里面写以下内容:

{
  "permissions": {
    "openapi": [
      "wxacode.getUnlimited"
    ]
  }
}

小程序码的获取方式有三种,这里只用到了接口 getUnlimited,选择这个接口的原因是漂亮的圆形小程序码,数量无限制。具体区别可以去 获取小程序码官方文档查看详情。

正常情况下,这个时候云函数可以部署测试了。如果遇到部署不成功、各种权限问题,可以尝试本地部署上传所有文件、重启试试。

3.2 生成小程序码

生成小程序码的代码如下,可以指定页面和页面参数 scene,还有小程序码的尺寸。

注意这里的 scene 有限制:
1. 最大32个可见字符;
2. 只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~
3. 注意参数格式:下面实例代码生成小程序码后,扫码获得 pages/demo/demo?scene=id%3D6

try {
  const result = await cloud.openapi.wxacode.getUnlimited({
    page: 'pages/demo/demo',
    scene: 'id=6',
    width: 240,
  })
  console.log(result)
  return result
} catch (err) {
  console.log(err)
  return err
}

直接调用,比服务端调用少了 access_token 参数。

3.3 上传到云存储

返回值中的 buffer 就是图片内容,直接上传到云存储:

const uploadResult = await cloud.uploadFile({
  cloudPath: 'shareqr/' + qr_name_hash + '.jpg',
  fileContent: result.buffer,
});
  • 我在云存储新建了 shareqr 目录保存小程序码;
  • 图片名根据参数取md5摘要;
  • getUnlimited 返回的图像是 jpeg 格式,后缀硬编码写 .jpg

3.4 获取图片临时路径

直接上代码

getURLReault = await cloud.getTempFileURL({
  fileList: [uploadResult.fileID]
});
fileObj = getURLReault.fileList[0]
return fileObj

3.5 直接从存云存储获取

生成过以后图片已经保存在云存储,用同样的参数第二次调用没必要再生成一次,去掉一次网络请求,可以节省不少时间。

前面说到文件名使用请求参数摘要,知道了目录和文件名,再加上文件bucket前缀就可以拼出来 fileID,用fileID 可以查询云存储的文件。

比如我刚刚生成的 fileID 是 cloud://dev-xxxx.8888-dev-xxxx/qr/44ea42f05091c3bec771123e6e8cd4c2.jpg, 前缀就是 cloud://dev-xxxx.8888-dev-xxxx/。再拼上目录、文件名、后缀就是 fileID

注:此处的 fileID拼接方法并不是来自官方文档,只是在使用中发现这个前缀不会变。还需要官方解释说明fileID规则。
如果会改变,就需要再用云数据库存储fileID,更麻烦一些。

3.6 云函数完整代码

// 云函数入口文件
const cloud = require('wx-server-sdk');
const crypto = require('crypto');
const bucketPrefix = 'cloud://dev-xxxx.8888-idc-4d11a4-1257831628/qr/'; // env: 'dev-xxxx'

// 云函数入口函数
exports.main = async (event, context) => {
  const full_path = event.page + '?' + event.scene;
  const qr_name_hash = crypto.createHash('md5').update(full_path).digest('hex');
  const temp_id = bucketPrefix + qr_name_hash + '.jpg';
  // return {
  //   full_path,
  //   qr_name_hash,
  //   temp_id
  // }

  try {
    // 先尝试获取文件,存在就直接返回临时路径
    let getURLReault = await cloud.getTempFileURL({
      fileList: [temp_id]
    });
    // return getURLReault;
    let fileObj = getURLReault.fileList[0];
    if (fileObj.tempFileURL != '') {
      fileObj.fromCache = true;
      return fileObj;
    }

    // 生成小程序码
    const wxacodeResult = await cloud.openapi.wxacode.getUnlimited({
      scene: event.scene,
      page: event.page,
      width: 240
    })
    // return wxacodeResult;
    if (wxacodeResult.errCode != 0) {
      // 生成二维码失败,返回错误信息
      return wxacodeResult;
    }

    // 上传到云存储
    const uploadResult = await cloud.uploadFile({
      cloudPath: 'qr/' + qr_name_hash + '.jpg',
      fileContent: wxacodeResult.buffer,
    });
    // return uploadResult;
    if (!uploadResult.fileID) {
      //上传失败,返回错误信息
      return uploadResult;
    }

    // 获取图片临时路径
    getURLReault = await cloud.getTempFileURL({
      fileList: [uploadResult.fileID]
    });
    fileObj = getURLReault.fileList[0];
    fileObj.fromCache = false;

    // 上传成功,获取文件临时url,返回临时路径的查询结果
    return fileObj;

  } catch (err) {
    return err
  }

}

4. 小程序页面调用

调用页面就比较简单了,在小程序新建一个 pages/share/shareonLoad 函数调用云函数。

// 使用前记得先初始化云函数,一版放到 app.js onLaunch() 中
// wx.cloud.init({env: 'dev-8888'})

wx.cloud.callFunction({
  name: 'getqr',
  data: {
    page: 'pages/demo/demo',
    scene: 'id=6',
  }
}).then(res => {
  console.log(res.result);
  if (res.result.status == 0) {
    _this.setData({
      qr_url: res.result.tempFileURL
    })
  }else{
    wx.showToast({
      icon: 'none',
      title: '调用失败',
    })
  }
}).catch(err => {
  console.error(err);
  wx.showToast({
    icon: 'none',
    title: '调用失败',
  })
})

至此完整的调用过程已经全部完成,详细代码可以到 项目代码 查看。

代码中还对入口页面和share页面的参数做了包装,云函数可以直接使用,小程序可以稍做修改适应自己业务。

写在最后

小程序云开发已经开放了很多功能,除了这次提到的生成小程序码,云调用还可以发送模板消息。有需要的开发者又一个理由可以快速上线新功能了。

云开发还开放了HTTP API,也就是用自己的服务器调用云函数。以前看完云开发介绍文章最大的疑问就是,你说的都很好,可是后台数据怎么管理呢?不能跟自己的服务器结合,只能放一些轻量的小程序。有了 HTTP API 以后就可以用自己的服务器做管理后台了。这时候你要问,都用上服务器了,还需要云开发做什么。首先,云开发免费;其次,免费功能已经够强,就差不能做Web管理后台了;最后,获取access_token(小程序及小游戏调用不要求IP地址在白名单内。)