QQ互联的注意事项

业务逻辑

最近试了一下用QQ登陆联系原有的账户体系,由于用了LeanCloud提供的后台服务,我只用关心QQ互联的部分。

首先,它的业务逻辑是:你先设置一个按钮,要跳转到QQ登陆页面,登陆完成后跳转到回调页面。回调页面即可以是用户主页也可以是一个中间页面用于跳转。

在PC上,曾经常见的模式是弹出QQ登陆小窗,登陆后原页面收到登陆成功的回调更新登陆状态。

但以上的方式如果在移动端使用,由于没有回调地址,唤起QQ登陆后无法跳转到用户界面,并且原页面也不会收到回调。

所以现在有一个模式,就是点击QQ登陆后原页面直接跳转到QQ登陆页面,登陆成功后回调地址收到accessToken与openId,之后再处理业务逻辑。

OAuth 2.0

A->B->C->D->E
先获取Code,然后再获得Token,重定向URI必须保持一致

现在你可以用他们的API来实现以上功能,比如QQ互联提供了QQ登陆地址的格式,通过Authorization获取AccessToken,通过AccessToken获取openId的接口,以及getUserInfo等api的接口。但我尝试后发现这些api没有设置跨域头,如果直接在前端获取会被浏览器拦截,因此我选择用他们的PHP SDK。

PHP SDK

SDK使用虽然很方便,但还是没有设置跨域头,你需要在页面输出前设置Access-Allow-Control-Origin。还没完,PHP SDK使用了session,不能像官方的获取用户信息的api一样直接请求,你还需要设置Access-Alllow-Control-Credentials,在前端请求时如果用AXIOS,也要加上withCredentials的配置。

由于我习惯把不涉及安全业务逻辑放到前端,所以我在callback.php里将AccessToken与OpenId传给前端的Login组件,由前端请求后判断是否是新用户等等。(也由于使用了LeanCloud,php并不能直接查数据库,所以干脆放在前端)

JS SDK

QQ互联我花了不少时间,主要是理解它的业务逻辑,还有跨域的问题。我开始用的是JS SDK,但似乎它已年久失修(12年更新过),而且它要求用script标签引入,我在前端还要检查SDK对象是否已加载完成,让我的代码很混乱,加上还是要后端协助解决跨域,所以还是不用了。

PHP&CURL 小插曲

其实还有一个小插曲,PHP SDK需要curl,但我安装的php 7.3 似乎并编译不上,而且php7.2-curl都是有的,但php7.3-curl还没有。。。只好换成7.2。

JavaScript原型链档案

原型链是JavaScript中的“继承”。

JavaScript中的“类”

事实上JavaScript一直以来都是基于对象和原型的,除了Number、String、Boolean等基本数据类型之外,JavaScript中的一切都是对象。ES6中新增的class、constructor、static、extends、super等关键字都是基于对象和原型的语法糖。

JavaScript中的“继承”

我们知道extends关键字可以用来继承类,其实类会被babel编译成函数(也是对象),而对象就有原型链的说法。

什么是原型链

通俗地讲,你创建了一个对象a,然后基于对象a又创建了一个对象b(使用Object.create(obj)),这时访问b的属性,如果b没有这个属性,那么JavaScript会在b的原型中寻找这个属性,而b的原型包含a的所有属性。这样的继承关系可以存在于更多对象间,比如a->b->c从而形成了原型链。

JavaScript中原型链的使用

如何访问原型链

在JavaScript中,每一个对象都有一个__proto__属性,我们可以通过Obj.__proto__访问原型。

构造函数constructor有一个prototype属性,用构造函数创建的对象的__proto__实际上指向了constructor的prototype属性。我们可以通过Object.prototype查看Object的原型对象。

使用Class

Class与extends完全基于原型链。

使用new

用new创建对象时,对象的__proto__会指向构造器的prototype。在JavaScript中,构造器不一定是constructor(),任何一个函数(除箭头函数)都可以成为构造器,因此,只要设置好某个函数的prototype属性,new得到的对象就能使用原型链。

使用Object.create()

该函数的第一个参数就是新对象的原型。

使用JavaScript类型

例如,Array数组的原型链arr -> Array.prototype -> Object.prototype,Function类似。

结合LeanCloud做一个查询术语的单页应用

前言

计算机领域中存在大量的术语,如果恰好是自己未接触的领域,看到一连串不懂的英文缩写,一定是一件令人困惑的事。虽然有百度和谷歌等搜索引擎,如果我们能自建一个更专业的数据库,似乎对我们有很大帮助(可玩性很高)。

一些问题

虽然该项目看起来只是查询数据库,但实际开发过程中遇到了比较多的一些问题。梳理这些问题是有一定价值的,不仅能避免以后被困扰,而且能加深我对这个领域的认识。

与LeanCloud数据存储交互

最初知道LeanCloud,其实是有朋友的hexo博客下留有评论框,静态博客下的评论框引起了我的好奇,F12表明服务提供商是LeanCloud,我猜测它是以专门为app、网站等应用提供后端服务为生。

LeanCloud使用难度不算大,官方文档也还行,但我找了半天才找到我想要的查询数据库的方法和实例。

建立对SDK的引用对象

官方来源:https://leancloud.cn/docs/sdk_setup-js.html

var AV = require("leancloud-storage");
var { Query, User } = AV;
var APP_ID = "**Hidden**";
var APP_KEY = "**Hidden**";

AV.init({
  appId: APP_ID,
  appKey: APP_KEY
});

查询数据示例

这里演示了在Terms表中查找attr==某个值的记录,result是一个数组。

var query = new AV.Query("Terms");
    query.equalTo("attr", value);//选出 attr列的值 为value 的行
    //find以为还有first等方法,都返回promise对象
    query.find().then(res => {
      ...
    }).catch(err => {
        ...
    });

(补充)添加数据示例

该示例将在TestObject表新建一条words='Hello World!'的记录。

var TestObject = AV.Object.extend('TestObject');
var testObject = new TestObject();
testObject.save({
  words: 'Hello World!'
}).then(function(object) {
  alert('LeanCloud Rocks!');

监听全局键盘事件

这个单页应用实际由两个状态组成,一个状态显示搜索框,另一个状态显示结果/详情,我们想实现搜索框聚焦时按下enter切换到详情组件很简单,但反过来就有点麻烦,因为详情组件不支持聚焦。

在网页中,只有聚焦的元素才能监听键盘事件(聪明的你也一定观察到了),同时事件冒泡的方向是自下向上,综合这两点,我们有两个方向。第一个方向是通过tabindex属性使详情组件支持聚焦,第二个方向是直接向顶级元素添加键盘监听器。除了搜索组件与详情组件,网页还有导航栏与底部,如果失去焦点就不能通过快捷键返回,这不是我们想要的效果。

我们通过向document添加事件处理实现在详情状态下按esc返回搜索状态(position用来控制状态,keyCode==27的键正是esc):

    document.onkeydown = e => {
      if (this.position == true && e.keyCode == 27) {
        this.position = false;
      }
    };

Vue-cli与Vue-router结合

Vue-router的官方文档给出了cdn接入的示例,我们这里介绍结合Vue-cli使用。

首先,在项目根目录下npm i vue-router

然后,在main.js里导入(import VueRouter from 'vue-router')并注册到Vue(Vue.use(VueRouter))

再新建一个VueRouter实例:

const router = new VueRouter({
    routes: [
            { path: '/detail', component: Detail },
            { path: '/', component: SearchBox }
        ] 
})

修改一下Vue根实例:

new Vue({
    render: h => h(App),
    router
}).$mount('#app')

剩下的部分则是将<router-link>标签与<router-view>标签添加到需要使用的地方。

布局与CSS选择器

Layout

Bootstrap-vue的布局很方便,通过b-row与b-col节点上的align-v与align-self属性可以实现对齐效果。对于col内部的行内元素对齐,我使用了verticle-align属性,通常设置bottom。

vh

对于整页布局,除了传统的对html,body,div设置100%高度,还有一种方法,就是为div设置100vh的高度,该应用中我为导航栏与底部标签各设置了5vh的高度,内容区域高度90vh,刚好占满了浏览器可视区域。这么做存在的问题是对移动设备兼容性不佳,如果要适配移动设备,可以借助媒体查询断点使用其他方式布局移动端。

自动聚焦与$refs与Vue的生命周期

其实$refs我一直没怎么使用过,最大的问题是我不能从$refs获取的节点中得到很多html结点信息,修改结点属性时,不如直接用querySelector。但这里实现自动聚焦却很实用,代码如下:

 <b-form-input ref="focusThis" @keyup.enter="$emit('match',keyword)" v-model="keyword" list="my-list-id"/>
mounted(){
     this.$refs.focusThis.focus()
  }

为什么说生命周期?我以前习惯在create钩子里进行初始化操作,但create时结点还未渲染,我不得不把聚焦操作放到mounted钩子。

DEMO

https://demo.guohere.com/term-search/

数据库暂时只有两条记录'html'与'css',请用这两个条目试验。支持快捷键enter(匹配)与esc(返回)。

function*/生成器函数

function*语句允许你声明一个生成器函数,这种函数的返回值是一个Generator对象,它允许你控制函数的暂停、继续执行。这种同步操作允许我们使用JavaScript的异步编程——function*每次只返回一个特殊的指针,并不直接真正地返回值,因此function*内外的代码可以同时执行。

function* generator(param,...)
{
...
yield ...
}

异步执行

function* add(inc){
inc++
yield inc++
yield inc++
}
let f=add(10)
f.next()//inc==11 { value: 11, done: false }
f.next()//inc==12 { value: 12, done: false }
f.next()//inc==13 { value: undefined, done: true }

遇到yield时函数会暂停执行后面的代码,直到next()方法被调用,该方法的返回值包括表达式的结果和完成状态。使用函数生成器时函数不会执行,因此需要使用一次next()。

上文的程序为什么到最后变成undefined呢?这是因为每次next()都会执行到yield关键字后的表达式处,并且将yield后的表达式结果作为value返回。而add函数内只有两个yield,虽然三次调用next()才能完成调用过程,但最后一次不返回value,在函数末尾加上return语句就能让next返回return后的表达式值。值得一提的是,函数生成器返回的是Generator对象,但这不影响在函数生成器内使用return。

为什么value=11和12,不应该12和13吗?inc++实际上是先返回inc再+1,这里就是inc++和++inc的差别。

Destructuring assignment

Array destructuring

var [a,b] = [1,2]

var [a,b] = c //c=[1,2]

Object destructuring

var {a,b} = {a : 1, b : 2}

({a,b} = {a : 1, b : 2})

var {a,b} = c //c={a : 1 , b : 2}

({a,b} = c)

also provide support for default values, assigning to new variable names

关于Object destructing

Object的key用于匹配,左边的value应该是一个变量,右边的value应该有一个值。var {c} = {c:1}实际上是var {c:c} = {c:1},key和value相同的时候可以简写是es6的语法糖。

Week 1: Vue.JS

尽可能简单地认识Vue.JS。

Vue基础

Vue简介

Vue.JS是一个JavaScript框架,它借鉴了MVVM的思想,Vue对象就像view model,使用Vue能够轻松地分离数据与视图表现,数据的变化会使视图也变化。

Vue.JS的另一个特点是组件化,一个Vue.JS项目可以抽象成一颗组件树,小型、独立、可复用的组件是大型应用构建的基础。

Vue实例

var vm = new Vue({
  el:'#app',//选择器,Vue实例挂载到选择的元素上
  data:{},//实例数据,数据变化会通过响应式系统触发视图变化
  created:function(){}//实例生命周期钩子,可以在不同阶段插入自己的代码
})

Vue模板语法

插值

<el>{{variable}}</el>展示变量文本值

<el v-html="htmlData"></el> 输出HTML

<el v-bind:id="dynamic"></el>或<el :href="url" ></el> 动态绑定元素属性

指令

v-前缀的特殊属性就是指令(Directives)

<el v-on:click="onClick"></el>或<el @click="submit"></el> 绑定事件

<form v-on:submit.prevent="onSubmit"></form> 使用修饰符,.prevent修饰符阻止默认事件

<el v-if="seen">text</el> 控制元素是否在DOM中 ,可以结合v-else-if与v-else使用。

<el v-show="ok"></el> 切换display属性控制元素是否可见。

<el v-for="item in array" >{{item}}</el>或<el v-for="item of array">{{item}}</el> v-for将一个数组或对象渲染成一组元素,最好也提供key属性以便跟踪每个节点(在组件中必须提供),对于子元素只能是特定元素的情况,可以使用is属性

计算属性和侦听器

计算属性

Vue实例中的computed对象内的函数:

computed:{//计算属性
  total: function(){
    return part1+part2;//如果part1与part2不变化,计算属性会返回缓存的结果
  }
}

计算属性的好处是,如果依赖不改变,计算属性不会重新求值。有需要的话,计算属性对象可以分成get和set。

侦听器

当variable变化,控制台输出新值和旧值。

watch:{
  variable: function(newVal,oldVal) {
    console.log(newVal+oldVal)
  }
}

也可使用api:vm.$watch('keyPath',callback) ,键路径形似a.b.c,指向对象的属性。

绑定样式

动态切换class

<div v-bind:class="{'class-name': isActive}" ></div> 用Boolean数据动态决定class-name样式是否应用

<div v-bind:class="[aActive,bActive]" ></div> 用String数据动态决定应用的样式

<div v-bind:class="[isActive ? active:'non-active', class-two,{'class-three':isThreeActive}]"></div> :class也支持三元表达式,数组中支持对象语法

绑定内联样式

<div v-bind:style="{ color:dynamicColor }"></div> 动态绑定内联样式

<div v-bind:style="styleObject"></div> 绑定样式到对象(styleObject:{color: 'red'})

<div v-bind:style="[styleObject1,styleObject2]"></div> 样式对象同样支持数组语法

事件处理

v-on指令的内容可以是方法或一个js表达式,调用方法时可以传入$event

<input v-on:keyup.enter="submit"> 监听键盘事件,KeyboardEvent.key 支持的按键名转换为kebab-case就可以作为修饰符,也可以监听鼠标事件

双向绑定

<input>、<textarea>、<select>中使用v-model指令,vue将自动控制双向绑定(监听输入、更新数据、处理特殊情况)。

Vue组件

组件简介

组件是可复用的Vue实例,除了组件的特性,与Vue根实例不同的是,组件的data必须是一个函数,这个函数的返回值才是data的内容,由于js对于对象的引用传值,函数确保了每个组件都维护一份自己的数据。

data:function(){

return{text1:'',text2:''}

}

Prop

通过prop,组件可以获得上层传来的数据。Prop的流是单向的。

prop:{weight:Number,height:Number}

<el weight="50" height="160" ></el>

Slot

slot用于在组件内渲染自定义内容,<el></el>之间的内容位于slot的位置。

组件el的template: <div><slot></slot></div>

使用方式<el>abc</el>

动态组件

通过is属性切换组件(可以用来路由的感觉)

<component :is="com1"></component>

传递事件

子组件的事件传递给父组件()

父组件<elf v-on:event-x="..."></elf>

子组件<el v-on:click="$emit('event-x')"></el>也可以在函数内使用this.$emit('',[arg],...)