分类 前端笔记 下的文章

MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据。

但是在拷贝包含function的对象时候会报错,提示无法copy

示例代码:

function copy(e) {
    const { port1, port2 } = new MessageChannel()
    port1.postMessage(e)
    return new Promise((resolve, reject) => {
        port2.onmessage = (event) => {
            resolve(event.data)
        }
    })
}

const test =  async function (params) {
    const a = {
        a: 1,
        b: 2,
    }
    const b = await copy(a)
    a.a = 5
    console.log(a, b);
}
test()

创建基础项目

### 安装nestcli
yarn global add @nestjs/cli

### 创建项目
nest new ctnode
.
├── README.md
├── nest-cli.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock

- 阅读剩余部分 -

全栈养成技

理想的开发状态是开发者只需要关注业务逻辑和产品设计

去年9月,微信团队与腾讯云就已经推出了“云开发”平台,云开发是这样介绍的

开发者可以使用云开发开发微信小程序、小游戏,无需搭建服务器,即可使用云端能力。云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。

在了解云开发的时候,有过几个疑问:

  • 云开发有什么能力
  • 在已有的开发架构上融入云开发的兼容性是否良好,是如何处理的
  • 如何请求和接收外部的网络
  • 对于一个小程序来说,一般都至少要有一个web后台来供运营使用,运营后台是否也可以使用云开发
  • 是否支持其他平台(支付宝、头条、淘宝)小程序的开发和使用
  • 企业微信的兼容性如何
  • 云开发对接企业微信或者其他平台是否方便

对于这些疑问,实际体验一下或许可以更快解答

动手尝鲜

明确需求

用于连通线上业务和线下门店的预约到店小程序插件对于很多电商小程序来说是刚需,我在云启星辰开发过一款店铺预约的小程序插件,这个插件现在已经使用在了周大福、老凤祥等一线珠宝品牌的线上小程序中。我们来看看如何使用云开发来改造这款插件

插件使用后UI样式如图,样式代码不再展示,我们详细看说一下云开发的过程

需求样式

开通云开发

云开发的开通需要在微信开发者工具上进行开通。点击上方工具栏的云开发按照流程创建环境即可开通云开发。

尝试了一下,免费版似乎只能创建两个环境。一个用来开发测试、一个部署正式的线上服务和数据。

先看一下云开发有什么能力:云存储、云数据库、云函数、日志运维、监控,分析统计,我们在实际体验中一个一个来看

云存储

云存储顾名思义就是把资源储存在云端上,我们可以用来储存用户的一些头像、生成的小程序码、上传的图片视频等,也可以用来储存小程序的UI文件,对于日益复杂的小程序来说,把UI资源储存在云端还是很有必要的。

所以我们先来尝试一下云储存,把相关的UI文件放在云储存中。

添加和读取云储存的数据有三种方式:

  • 小程序通过云开发API调用
  • 云函数通过API调用
  • 在开发者工具中进行上传查看和添加

开发过程中的UI资源,我们可以直接新建一个文件夹进行上传储存,之后直接获取资源链接进行调用就可以了。

云数据库

和云储存一样,增删改查云数据库数据有三种方式:

  • 小程序通过云开发API调用
  • 云函数通过API调用
  • 在开发者工具直接处理

其实使用过MongoDB的同学再来使用云数据库会有熟悉感。

预约店铺,肯定是要有店铺的数据的,预约成功之后还要保存预约记录。

  • 店铺数据

我们首先来创建一个store的集合,手动添加一条记录:

{
    "address":"北京市东城区东长安街",
    "location":{
        "latitude":39.90374,
        "longitude":116.397827
    },
    "city":"深圳",
    "province":"广东",
    "country":"南山区",
    "banner":"https://7971-yq-demo-test-47yk3-1300688546.tcb.qcloud.la/UI/reserve/storeBanner.png?sign=fd1f7e9be2340c9249b18140af379442&t=1573793815",
    "store_name":"迦南的小宝藏",
    "icon":"icon",
    "introduction":"introduction",
    "mobile":"19989897878",
    "status":1
}

默认情况下,新增的每条数据会有一个_id的字段,该字段是UUID的形式,一个集合里该字段唯一

  • 预约记录

预约成功肯定是要把记录存起来的,所以还需要一个预约记录的集合reserve

小程序端API尝鲜

  • 初始化

小程序在使用云开发的时候要先进行初始化,如果是开发小程序,我们就直接放在app.jsonLaunch中即可,现在我们开发的是插件,所以放在插件目录的index.js中即可


//  初始化云开发
wx.cloud.init({
  env: '环境ID'  // 这里的ID是我们之前创建环境时候的环境ID,不是环境的名字,环境一旦创建,ID将是唯一的。
})
  • 初始化数据库

在插件中组件的js文件attached进行数据库引用

const testDB = wx.cloud.database({
  env: '环境ID'  // 这里的ID是我们之前创建环境时候的环境ID,不是环境的名字,环境一旦创建,ID将是唯一的。
})
  • 查询店铺数据

在正常的业务逻辑中,进入插件时是已经拿到店铺ID的,所以我们直接用已经有的店铺ID来查询店铺数据就好。

云开发的接口不管是小程序端开始服务端都是支持promise的,所以我们直接使用promise的方式使用。

首先通过collection来获取集合的引用,之后获取记录的引用即可,使用方式如下:

        // 查询store集合中`_id : that.data.storeId`的记录
        db.collection('store').doc(that.data.storeId).get().then(res => {
            let { data } = res;
            this.setData({
                store: data
            })
        }).catch((error) => {
            wx.showToast({
                title: '加载店铺信息失败,请重新进入',
                icon: 'none'
            })
            console.error(error)
        })

云函数尝鲜

我们已经获取到店铺的数据了,之后就要尝试提交数据了。

在小程序端是可以直接使用add来提交数据的,但是并不建议这么做,考虑到预约成功会发送模板消息,以及要给插件适用方回调数据,使用小程序端API显然是不合适的。

在插件中使用云开发其实这一点是有点点不爽的,因为自动创建的插件开发结构里是不包含云开发的,我尝试了把云开发的目录复制到插件开发的结构中并不可以,所以我们需要新建一个同样AppId的小程序进行云函数的开发。

创建好的云开发结构中会比通常的小程序开发结构多一个cloudfunctions的目录(可以在project.config.json里修改cloudfunctionRoot),这里用来储存云函数。云函数的每个目录里都有一个package.json文件,所以到这里,我在想是不是可以直使用npm来引入我们所需要的module,比如axios等,查看了官方文档之后发现确实支持。

创建云函数

创建云函数可以直接在cloudfunctions目录中新建一个目录,名称为函数名,目录内至少包含一个index.js文件,改文件必须要有一个exports.main的入口函数。或者可以在开发者工具的编辑器中,云函数的本地根目录上右键,新建Node.js云函数。也可以直接在云开发控制台新建一个云函数,之后在开发者工具中同步函数列表。

新建好的云函数index.js模板代码如下:

// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()

  return {
    event,
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID,
  }
}

入口函数中有两个参数eventcontext

event就是调用云函数时候传入的参数,以及自动注入的小程序用户的openid和小程序的appidcontext对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。

简单改造尝试云函数

官方文档给了一个处理数据相加的例子,我们来试试。

// 官方示例
exports.main = async (event, context) => {
  return {
    sum: event.a + event.b
  }
}

在小程序中调用云函数

wx.cloud.callFunction({
  // 云函数名称
  name: 'add',
  // 传给云函数的参数
  data: {
    a: 1,
    b: 2,
  },
  success: function(res) {
    console.log(res.result.sum) // 3
  },
  fail: console.error
})

到这里我们尝试时发现返回的并不是理想中的sum = 3,这里需要在云开发控制台中打开本地调试或者要在开发者工具中将该云函数右键上传并部署。其实这一步对于我们正常的开发和联调来说是略微有些麻烦的,并不能像我们正常node开发中,把环境改为本地域名,之后直接运行就可以,并且还要打开好几个窗口,略微有点点不爽。

另外本地调试之前,我们要先进入云函数的目录中npm install或者yarn一下,否则本地调试会报错。

我们现在本地调试中进行调试,选择本地调试窗口左侧要调试的云函数,勾选右侧的本地调试,之后修改请求参数(可以保存为模板方向下次调试使用),点击调试,即可在Source中打断点以及在Console中看见返回的内容。

在服务端查询数据库

云函数的Hello World 就体验完了,我们尝试编码需求中所需要的云函数。

新建一个云函数reserve

初始化云函数

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

引用数据库

const db = cloud.database()

在集合上新增记录


  await db.collection('todos').add({
    data: {
      mobile,
      name,
      reserve_at,
      time
    }
  })

修改返回结果

  return {
    code: 1,
    msg: '添加成功'
  }

为了防止出现错误,我们使用try来包裹一下

完整的云函数文件下

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
});
const db = cloud.database();
// 云函数入口函数
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  try {
    let { mobile, name, reserve_at, time } = event
    await db.collection('reserve').add({
      data: {
        mobile,
        name,
        reserve_at,
        time,
        openid: wxContext.OPENID,
        appid: wxContext.APPID,
        unionid: wxContext.UNIONID,
      }
    })
    return {
      code: 0,
      msg: '添加成功'
    }
  } catch (e) {
    console.error(e)
    return {
      code: -1,
      msg: '添加失败'
    }
  }
}

本地调试成功返回了{code: 0,msg: '添加成功'}

之后去数据库查看,已经多出了如下数据:

{
    "_id":"05a1947c5dd2606b01333ceb6ca8f34f",
    "reserve_at":"2019-11-12",
    "time":"上午",
    "openid":"orjAC5bpYbrTRRRWLdeh8Bbi8MuE",
    "appid":"wxba6cc8995c8d1ac3",
    "mobile":"19989897878",
    "name":"殷迦南"
}

增加回调

这是一个插件,必然要给插件的使用方回调相应的数据,所以我们试试对外发起请求是否方便。

在这里我们使用axios

在云函数目录下npm install axios --save 或者yarn add axios

  • 引入axios
const axios = require('axios')
  • 增加回调
    const callback = await axios({
    method: 'post',
    url: 'https://dev.xxxxx.com/xxxxx',
    data
    })

判断callback的状态码是否200判断是否回调成功。

尝试企业微信支持情况

在不使用云开发的情况下,wx.qy.login获取到code后,请求企业微信API是只能获取到userid,获取不到openidunionid,但是在云开发的测试体验中是可以获取的到,其结果和个人微信相同,
所以在企业微信的开发中依旧不能避免走wx.qy.login来获取code从而获取到用户信息。但是其他方面支持的还是很完善的。

与外部网络交互情况

在之前做回调的时候,是可以用请求外部网络的,那外部网络如何访问云函数或者云数据库呢?

在仔细看了云开发文档之后发现,HTTP触发云函数和调用数据库必须要有access_token,所以肯定不能用做企业微信等平台的回调请求,类似的请求依旧是需要单独开发后端接口来接收回调,后端接口在获取access_token之后请求云函数或云数据库进行下一步操作。

因此,也不适合基于云开发开发一套后台web管理系统,web后台依旧需要对应的后端服务。

日志运维、监控、分析统计

日志运维、监控和分析统计是云开发的一大亮点。

每一次请请求日志都会被记录下来,包括错误信息以及请求接口的情况,另外还有高级日志支持andor逻辑运算连接符等操作进行查询,极大的方便了错误排查和运维。

开发者平台中的监控可以监控函数调用次数、错误次数、请求时间以、外网流量以及资源使用情况等。分析统计具体的用户访问情况,结合日志,可以清楚的看到每个用户在小程序上的具体操作,这对开发和运营来说也是一大助力。

但是这些东西似乎目前只能在微信开发者工具中使用和查看,我很期待支持触发回调、一键同步到TAPD指派具体开发人员等操作。

是否支持其他平台

与其他平台打通的前提是拥有自身的一套用户体系,单独使用云开发是不现实的,与接收外部请求一样,依旧需要现有后端代码进行"转发"之后才有实现的可能性。

总结

  • 云开发有什么能力
  • 在已有的开发架构上融入云开发的兼容性是否良好,是如何处理的
  • 如何请求和接收外部的网络
  • 对于一个小程序来说,一般都至少要有一个web后台来供运营使用,运营后台是否也可以使用云开发
  • 是否支持其他平台(支付宝、头条、淘宝)小程序的开发和使用
  • 企业微信的兼容性如何
  • 云开发对接企业微信或者其他平台是否方便

先解答一下最开始的几个疑惑

Q:云开发有什么能力
A:云存储、云数据库、云函数、日志运维、监控,分析统计

Q:在已有的开发架构上融入云开发的兼容性是否良好,是如何处理的
A:云开发和已有架构是并不冲突的,可以随时加入云开发,减少原本服务器压力

Q:如何请求和接收外部的网络
A:可以使用node进行对外的请求,在接收外部请求的时候必须要携带access_token进行安全校验

Q:运营后台是否也可以使用云开发
A:从外部网络请求云函数和云数据库来看,并不是很友好,需要使用自己的服务端进行“中转“

Q:是否支持其他平台(支付宝、头条、淘宝)小程序的开发和使用
A:在有自身一套用户体系的前提下,使用自己的服务端中转其他平台小程序的请求和数据是可以实现多平台使用的

Q:企业微信的兼容性如何
A:使用云开发来开发企业微信和不使用云开发以及使用云开发的个人小程序来对比,1、弱化了后端和运维,降低了成本;2、可以做到快速迭代和上线;3、暂时无法进行企业微信的免鉴权;

Q:云开发对接企业微信或者其他平台是否方便
A:往往其他平台都是具有回调等常规操作的,单纯依靠云开发略有困难

大概总结一下目前版本的云开发锁具有的优缺点吧

优点:
1、对于个人微信小程序来说,小程序相关的业务逻辑完全做到了云端开发,并且天然鉴权,开发者只需编写自身业务逻辑代码
2、数据库即可在小程序端操作也可以在云函数操作
3、不需要自建CDN进行资源存储
4、可以把一个前端工程师在无任何学习成本的情况下变为一个全栈工程师,开发出一个高质量的小程序
5、基于业务逻辑的0后端0运维
5、基于大厂服务来看,非常的安全和稳定

不足:
1、对于一个功能复杂的小程序来说,非业务逻辑(web后台等)暂时做不到完全做到0后端0运维,依旧需要后端支持
2、暂不支持企业微信的天然鉴权

总体来说我还是非常看好云开发,据了解,云开发还正在进一步封装腾讯云、微信平台的其他服务,包括 AI、音视频、订阅消息、微信支付等,提供扩展能力,让开发者可以更便捷地调用,在更多业务场景中可以相关能力。平台的扩展能力还会进一步加强。这是一种新的理念和新的标准,或许未来真的可以做到所有的产品都可以使用云开发。

域名

https://api.example.com

如果API简单,无太多扩展,可考虑放主域名下

https://example.org/api/

版本

  • 将API的版本号放入URL https://api.example.com/v1/
  • 将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法

操作类型

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。
  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

过滤信息

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

状态码

  • 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

错误处理

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

{
    "error": "Invalid API key"
}

返回结果

  • GET /collection:返回资源对象的列表(数组)
  • GET /collection/resource:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档

Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

{"link": {
  "rel":   "collection https://www.example.com/zoos",
  "href":  "https://api.example.com/zoos",
  "title": "List of zoos",
  "type":  "application/vnd.yourformat+json"
}}

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。


{
  "current_user_url": "https://api.github.com/user",
  "authorizations_url": "https://api.github.com/authorizations",
  // ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。


{
  "message": "Requires authentication",
  "documentation_url": "https://developer.github.com/v3"
}

上面代码表示,服务器给出了提示信息,以及文档的网址。

注意

  • 服务器返回的数据格式,应该尽量使用JSON,避免使用XML。
  • API的身份认证应该使用OAuth 2.0框架。

参考:RESTful API 设计指南

  • initState:widget创建执行的第一个方法,可以再里面初始化一些数据,以及绑定控制器
  • didChangeDependencies:当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build() 中InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。InheritedWidget这个widget可以由父控件向子控件共享数据,案例可以参考 scoped_model开源库。
  • build :它主要是用于构建Widget子树的。
  • reassemble:此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  • didUpdateWidget:当树rebuid的时候会调用该方法。
  • deactivate:当State对象从树中被移除时,会调用此回调。
  • dispose():当State对象从树中被永久移除时调用;通常在此回调中释放资源。

注:代码中的didChangeAppLifecycleState方法复写需要State with WidgetsBindingObserver这个抽象类

@override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    print('initState');
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print(state.toString());
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    print('didChangeDependencies');
  }

  @override
  void didUpdateWidget(LifeDemo oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    print('didUpdateWidget');
  }

  @override
  Widget build(BuildContext context) {
    print('build');
    // TODO: implement build
    return MaterialApp(
      home: Center(
          child: GestureDetector(
        child: new Text('lifeCycle'),
        onTap: () {
          Navigator.of(context)
              .push(new MaterialPageRoute(builder: (BuildContext c) {
            return new Text('sdfs');
          }));
        },
      )),
    );
  }
  @override
  void reassemble() {
    // TODO: implement reassemble
    super.reassemble();
    print('reassemble');
  }
  @override
  void deactivate() {
    // TODO: implement deactivate
    super.deactivate();
    print('deactivate');
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    WidgetsBinding.instance.addObserver(this);
    print('dispose');
  }