这次参与了支付宝的《繁星计划*支付宝校园生活小程序大赛》,本篇文章整理了在开发过程中遇到的一些问题,解决方法和一些关于知识点的总结。
遇到的问题和解决方法
对于小程序的安装了环境的配置就不多说了,由于这次开发的小程序是校园应用类的。不可避免的就需要用到校园账号的登录授权。
登录授权
我们采用Oauth2.0
的授权码模式(authorization code
),它是功能最完整、流程最严密的授权模式。具体流程如下图。
小程序核心流程
本次小程序简化了以上的流程,通过一个接口封装了以下ABCD流程。
- A. 访问业务服务器接口
该接口由第三方应用传入,在服务器中生成state
并写入session
,组装好授权URL
,并重定向到授权URL
- B. 访问授权页面
访问授权页面前需要先登录,未登录会重定向到登录页面,如果之前登陆过但没有进行授权,且认证服务器的登录态仍有效,则直接能访问授权页面
- C. 重定向到指定
url
,并带上授权码code
重定向的URL是由第三方应用在调起浏览器过程中传入的,认证服务器会在URL
的query
中在加入code
参数和state
参数,重定向请求到第三方应用的业务后台,然后从query
中取出授权码code
和state
,比对session
的state
与query
中的state
是否一致
(如果不一致,证明该请求不是来自需要授权的客户端,授权失败,可能遭到CSRF攻击)
- D.
code
换取用去用户凭证token
业务后台拿到授权码后,需要请求认证服务器换取登录凭证token
,token
可用于调用开放平台所提供的服务
- E. 授权成功,返回后台登录态
skey
授权成功后,业务后台需要根据自身需求,管理用户的登录态,并给客户端返回自身后台的登录态
- F. 获取业务后台登录态,退出浏览器
第三方应用客户端获取到登录态,并退出浏览器
授权成功后,后台将返回一个skey
给前端,前端则需要将skey
存储在本地,供以后各项功能的使用。
skey的作用
- 用户只要输入一次校园网账号密码,以后进入小程序都不需要再次手动进行登录(除非登陆态过期失效)。
- 用户在使用一些需要用到校园网账户身份的功能(比如查个人课表,查个人成绩)时,小程序前端就会将该用户的
skey
传入后台,假如该skey
是有效的,那么后台可以根据skey
找到用户的校园网账号user_id
,然后再使用user_id
去调用接口实现用户需求。在此处,skey
的作用是将课程表用户身份转换为校园网用户身份
图片上传至七牛云对象存储
客户端上传前需要先从服务端获取上传凭证,并在上传资源时将上传凭证作为请求内容的一部分。后台获取token流程参考官方文档:七牛云上传凭证
此处主要是前端代码,以上传个人支付宝头像为例。
- 首先获取个人支付宝信息
my.getAuthCode({
scopes: 'auth_user',
fail: (error) => {
console.error('getAuthCode', error)
},
success: () => {
my.getAuthUserInfo({ // 获取支付宝个人信息
fail: (error) => {
console.error('getAuthUserInfo', error)
},
success: (res) => {
that.downloadAvatar(res.avatar)
}
})
}
})
- 获取个人头像后本地下载
// function downloadAvatar(avatar)
my.downloadFile({
url: avatar,
success(res) {
that.uploadQiniu(userInfo, res.apFilePath)
}
})
- 获取上传到七牛云服务器的
token
(由后台返回)
getToken: function () {
return request({
url: api.get_avatar_token, // 后台返回token接口
method: 'POST',
}).then(res => {
if (res.data.code !== '0') {
throw new ErrorMessage('不能获取上传头像的token, 错误码为', res.data.code)
}
return Promise.resolve(res)
})
},
- 将头像上传到七牛云
my.uploadFile({
url: 'https://up-z2.qiniup.com/',
fileType: 'image',
fileName: 'file',
filePath: tempFilePaths,
header: {
"Content-Type": "multipart/form-data"
},
formData: {
file: tempFilePaths, // 刚才下载的头像图片路径
key: res.data.expected_Key,
token: res.data.token, // 后台返回的上传凭证
},
success: function (res) {
const data = JSON.parse(res.data)
// 成功上传
},
fail: function (res) {
console.log(res)
}
})
解析HTML并显示
在实现对别的网页里的文章的展示的时候,而此时后台返回的内容是一整个HTML
时,我们知道虽然小程序有富文本组件rich-text
,但其nodes
属性只支持使用 Array 类型。针对HTML
字符流则需要自己将其转化为 nodes
数组,此时我们需要一个工具将HTML
解析。这个工具就是mini-html-parser2
- 首先安装
mini-html-parser2
npm install mini-html-parser2 --save
- 在项目里引入
import parse from 'mini-html-parser2'
- 对
html
字符串进行解析
// 传入的html为后台返回的html字符流
parse(html, (err, nodes) => {
if (!err) {
that.setData({
article: {
content: nodes
}
})
}
})
// .axml页面
<rich-text nodes="{{ article.content }}"></rich-text>
格式化时间
有时候我们需要将当前日期2019\2\2
格式化成2019-02-02
,可以使用以下方法
dateFormly: function (seconds) {
let date = (new Date(seconds)).toLocaleDateString()
const [year, month, day] = date.split('/')
return `${ year }-${ this.dataLeftCompleting(month) }-${ this.dataLeftCompleting(day) }`
},
// 数字补全
dataLeftCompleting: function (value) {
return parseInt(value, 10) < 10 ? "0" + value : value
},
其他注意事项
-
<scroll-view>
组件的使用需要给定高度height
,不然无法触发onScrollToLower
等边界事件 - 将需要用到的域名添加到白名单!
自制工具包
工具包是一些封装过的小程序接口,作用是为了开发人员在写代码的时候可以只专注于前端页面的开发,省去一些不必要的逻辑和重复的代码。
接口Promise化
众所周知,promise是异步操作,可以把执行代码和处理结果的代码清晰地分离了。此处将请求的接口promise化,有利于防止回调地狱,增强代码的可读性。
const aliPromisify = function (fn) {
return function (obj = {}) {
return new Promise((resolve, reject) => {
obj.success = function (res) {
// 成功
resolve(res, obj)
}
obj.fail = function (res) {
// 失败
reject(res)
}
fn(obj)
})
}
}
这里运用到了javascript
的一个知识,即函数的返回值可以是函数
*稍微举个例子,不过多展开
function createCompare(propertyName){
return function(ob1,ob2){
return ob1[propertyName] < pb2[propertyName]
}
}
var data = [{name:'aaa'}{name:'bbb'}]
data.sort(createCompare('name'))
然后将需要封装的接口抛出去供开发者使用。
const aliGetUserInfo = wxPromisify(my.getOpenUserInfo)
const aliRequest = wxPromisify(my.request)
export { aliPromisify, aliGetUserInfo, aliRequest }
自定义错误类
有时候因为各种各样的原因,比如请求出错,账号密码填错,后台问题等,需要将错误提示抛出展现给用户和开发人员。但一般来说用户不需要接触底层的错误,而开发人员则会想错误的根本来源是哪里。所以定义了一个类来展示错误信息。
class ErrorMessage {
constructor(msg, code, options) {
const error = new Error(msg)
// 去掉该函数的错误栈
error.stack = error.stack.replace(/\n.*\n/, '')
let option = options
if (typeof code === 'string') {
error.message += `错误码: ${code}`
} else {
option = code
}
console.error(error)
return error
}
}
// 使用方法如下,在需要抛出错误的地方添加下面句子
throw new ErrorMessage('请求xxx接口出错', code)
知识点整理
this的指向
*this永远指向最后调用它的对象
-
直接调用:fn则是普通函数,this指向全局对象window,匿名函数的this永远指向window
var name = 'window' function a() { return function () { console.log(this.name) } } var b = a b.call({name:'111'})() // window , a的this现在指向111 b().call({name:'111'}) // 111 , return函数的this指向111
- new操作:fn就是构造函数,this指向新创建的对象
- 对象调用:fn就是方法,this指向调用它的那个方法
-
箭头函数:声明函数时所在上下文中的this。但这个this有时候也会改变,如指向构造函数的闭包
var name = 'window' var person1 = { show2: () => console.log(this.name) } person1.show2() // window person1.show2.call({name:'111'}) // window
改变this指向
-
箭头函数
- 箭头函数没有this,如果箭头函数被非箭包含,则this指向最近一层的非箭,否则为undefined(严格),window(非严格)
- 无法通过
call
、apply
、bind
绑定箭头函数的this
- 在函数内部使用 that = this
- 使用apply、call、bind
- new实例化一个对象
构造函数
优点
- 没有显示创建对象
- 直接将变量和方法赋给this对象
-
没有返回值
- 无返回值或者返回基本类型,则new 返回的是实例化对象
- 返回值是引用类型,则new返回的是该引用类型
缺点
每个方法都要在实例上重新创造一遍,即每个实例的方法指向的都不是同一个对象。
解决方法:使用原型模式
new的过程
- 创建一个新的对象
- 将构造函数的作用域赋给新对象(this指向新对象
- 执行构造函数的代码(将对象的__proto__指向原型的prototype)
- 返回对象
constructor属性:记录临时对象由哪个函数创建
如果不用new来创建,则作为普通函数调用,其属性会添加到全局变量中
原型对象
prototype是一个指针,指向函数的原型对象,可以访问通过构造函数创建的实例对象的原型对象。可以用来达到实例之间共享资源(属性和方法)
hasOwnProperty():检测一个属性是存在实例里还是原型里
person.prototype.isPrototypeOf(person1):确定括号里的对象是否是调用者的实例
Object.getPrototypeOf():取得对象的原型
实例化对象找不到属性回去原型对象(Object.prototype)去找
不能在实例上重写原型的属性,使用delete方法可以删除实例上的属性从而搜索原型上的同名属性
遍历属性
通过in返回true和hasOwnProperty返回fasle确定属性是在原型中
Object.keys():取得对象上所有可枚举的实例属性,接受一个对象,返回字符串数组
Object.getOwnPropertyNames():得到实例所有属性
以上两种方法可以用来代替for-in循环!!!
*注意:
重写原型对象,其实例对象还是指向最初的原型
原型对象里的引用类型数据,如果通过实例对象对其进行修改,则所有实例对象都会修改
person1.friends.push('AA'); // person2.friends //'AA'
数据属性
特性
- [[configurable]]:表示能否通过delete删除属性从而重新定义属性,能否把属性修改为访问器属性
- [[Enumerable]]:表示能否通过for-in遍历
- [[Writable]]:表示能否修改属性的值
- [[Value]]:包含该属性的数据值
object.defineProperty():
- 用法:修改以上特性。
- 参数:属性所在对象、属性名、特性名
- 注意:一旦把属性定义为不可配置(configurable:false),则不能再变回可配置
object.defineProperties():
- 用法:通过描述符一次定义多个属性
- 参数:属性所在对象、对前面对象属性一一对应的对象
object.getOwnPeopertyDescriptor():
- 用法:取得给定属性的描述符
- 参数:属性所在对象、属性名
访问器属性(不包含数据值)
特性
- [[configurable]]:表示能否通过delete删除属性从而重新定义属性,能否把属性修改为访问器属性
- [[Enumerable]]:表示能否通过for-in遍历
- [[Get]]:在读取属性时调用的函数
- [[Set]]:在写入属性时调用的函数
用途
设置一个值会导致另外的值变化
var food = {
_fish: 10,
vagetable: 2
}
Object.defineProperty(food, "dinner",{
get: function(){
return this._fish
},// 将_fish的指针赋给dinner
set: function(newValue){
if(newValue > 5){
this._fish = newValue
this.vagetable += newValue - 4
}
}
})
food.dinner = 6 // dinner=6,vagetable=4