李续铖的博客


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

safari iframe 跨域问题解决

发表于 2017-10-31 | 分类于 前端

最近遇见一个问题, 我在维护一个a.com域名下的UISDK,这个UISDK是要开放出去给其他开发者使用的,开发者可以通过iframe来使用. 那么就存在一个跨域的问题,在chrome下还好.问题出在safari中,发现所有的cookie都设置不上.

触发原因

找寻资料后发现,safari和其他一些现代浏览器,有一个安全策略,就是非用户主动打开的web, 禁止其设置cookie.而通过iframe打开的界面,就被判定为非用户主动打开的界面,所以a.com这个网站任何cookie都设置不了.

解决方案

网上目前有以下几种解决方案:

  1. 出镜次数最多的方案, 设置P3P Header, 这个方案应该是最简单的了,需要打开iframe的网站添加一个P3P的请求头,但是这个方法safari浏览器不支持,IE系列还好,而且让开发者在自己的网站中做这种修改,是比较不友好的,而且可能会有安全问题
  2. 模拟post表单提交,这是个比较不常规的方法,在safari高版本中已经修复了,而且听说谷歌因为一直绕过safari的安全策略读取第三方cookie,被苹果起诉了,所以这种方法也已经凉了.
  3. 根据safari的安全策略,如果用户曾经访问过a.com,那么即使通过iframe的方式使用也可以设置cookie.所以可以在iframe中进行判断,如果设置不了cookie,通过window.parent.location.href = ‘a.com’; 的方式使浏览器主动访问一次 a.com,然后a.com再跳回原界面,这样之后也会正常设置cookie
  4. 此方案应该是和3是一样的,如果浏览器中存在a.com的cookie,那么再通过iframe访问也可以设置cookie.这个可以通过3的方法去解决.但是3,4这两种方法用户感知太强烈,不是很友好

网上找的几种方案都不合适,只能自己想办法了.

寻求新方案

cookie存不了,但是localStorage是可以的啊,那就可以把数据存在localStorage中

开始解决:

首先要把数据传到前端,之前是通过set-cookie的方式,那这里肯定用不了,那就通过query吧

在前端通过query拿到数据,成功存到localStorage, 完成第一步

又发现一个问题,没有cookie的话,后续请求接口也没有cookie,用户信息带不上,后端没法处理

那既然数据都存在localStorage中,把他拿出来放到请求头 的 cookie 中不就可以了吗

尝试后发现, 这个方法浏览器不允许, 这是一种不安全的方法, 所以这条路行不通

又陷入了一个死局,后端通过cookie去拿用户信息,这里cookie带不上,怎么解决,只能改变后端拿数据的方式, 总不能又把数据放到query中传过去,那岂不是每个接口都要改,而且query中带了太多东西

最后的最后,在http请求中发现了有用的东西,那就是
ctx.request.headers['referer'], 因为最开始为了将数据传到前端将其放在了url的query中,那么以后从这个界面发起的请求中就已经全部带着这些数据, 当从cookie中拿不到数据的时候,从referer中取一下就好了,至此这个问题以一种很奇葩的方式解决了

反思

问题虽然解决了,但是我觉得这种方案并不能通用,通过query + cookie(referer)来取数据还有一些约束,比如referer很可能在客户端被改变等等


那么最好的方案是什么呢,在我看来,使用jwt来做这个事情是非常合适的,将很少的信息加密后通过query或者其他方式放到前端,前端可以使用任何方式存储,最主要的是可以通过设置Authorization将数据很方便的带到后端, 可以带来更好的安全性和稳定性


graphQL入门基础(一)

发表于 2017-07-30 | 分类于 后端

1. 什么是GraphQL?

A query language for your API

官网给他的定义是一门API的查询语言。他会给你API中的数据一份完整的描述,这样客户端就可以根据这份描述来确定他具体需要什么。下面总结一下他的特点:

  1. 根据需要查询数据。客户端可以发送一个GraphQL到服务端API,只获取客户端想要的数据。查询总是会返回预测的结果,说白了就是你想要什么就会拿到什么(前提是API有想要的数据)。
  2. 在一份请求中获取多种数据。GraphQL不只是会访问某一种资源,还可以访问数据之间的引用,即使这些数据后端需要从多个URL加载,这个速度也是非常快的。
  3. 使用类型系统描述什么是可以获取的。GraphQL API根据类型和字段进行组织。使用类型来确保客户端只会获取肯能的内容,并且会提供很明确的错误反馈。避免了服务端手动对请求数据进行解析。
  4. 强大的开发工具。这个也是我非常喜欢的一点,可以很直观的知道可以从服务端获取哪些数据,数据结构是怎样的,并且还会有每个字段的描述,简直比开发文档还要方便。

既然他是一个这么好的东西,现在都有谁在用呢,靠谱么?

由于它是由Facebook提出的,所以从2012年开始Facebook的移动应用就已经开始使用GrqphQL了。而且现在Github的API v4也已经使用了GraphQL。目前该项已经技术比较成熟,可以在项目中进行使用了。

2. 项目搭建

本项目的目的是搭建一个由GraphQL建立的服务端,并且提供查询工具。

2.1 技术选择

主要的就下面几个

Koa GraphQL TypeScript GraphiQL

还有几个工具库

graphql-relay koa-graphiql koa-graphql superagent

2.2 搭建

服务端基础搭建就不做介绍了,就是使用koa + TypeScript搭建了一个简单的服务器。
由于GeaphQL是基于类型和字段的,那么先对数据进行类型定义吧。

先简单的来一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
user.ts
const GQUser = new GraphQLObjectType({
name: 'GQUser',
fields: {
name: {
type: GraphQLString,
resolve(user) {
return user.name;
},
},
age: {
type: GraphQLInt,
resolve() {
return 22
}
}
}
});

在这里name就是定义的类型名字,fields就是他所具备的字段,当然每个字段也有自己的类型,resolve函数反回的是这个字段最终的值。它的参数来自使用它的类型的resolve返回,具体下面介绍。

类型定义完成后就可以使用这个类型了,
首先定义查询入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
schema.ts
class User {
public name: string;
public age: number;
constructor() {
this.name = 'lxc';
this.age = 22;
}
}
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => ({
user: {
type: GQUser,
description: '用户',
resolve: async () => {
return new User();
}
}
})
});
export default new GraphQLSchema({
query: Query,
});

发现这里也有一个类型:query,这是每个GraphQL都会有的一个类型,他和其他类型没有什么区别,唯一特殊的是它是程序查询的入口。
我在这里给query类型添加了一个叫user的字段,它的类型是我们上面定义的GQUser,同时他的resolve函数返回了一个User对象,那么这个返回的对象就会传给GQUser的的resolve函数。

下面就是建立GraphQL服务了,由于使用了koa-graphql ,这个过程非常简单

1
2
3
4
5
6
const graphqlHTTP = require('koa-graphql');
router.all('/graphql', graphqlHTTP({
schema,
graphiql: true,
}));

这样就搭建了一个GraphQL的服务,同时为了方便用户查询数据结构和描述,这里使用了GraphiQL开发工具。

2.3 查询

查询数据

1
2
3
4
5
query{
user {
name
}
}

获得结果是

1
2
3
4
5
6
7
{
"data": {
"user": {
"name": "lxc",
}
}
}

这里就会发现,我在这里只希望拿到user的name属性,服务端返回的也是只有name属性,并没有多返回什么东西。我想要拿什么就可以在查询语句中改什么,并不需要对服务端的代码进行任何的改动,无形中降低了很多开发量,减少了很多沟通,提高了开发的速度和效率。

graphQL入门基础(二)

发表于 2017-07-30 | 分类于 后端

前面介绍了如何搭建一个简单的GrqphQl服务,那么下面说一些其他的,这次的数据并不会直接拿到,而是需要服务端再去获取,更符合实际情况。

1. 类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const GQPoi = new GraphQLObjectType({
name: 'poi',
fields: {
id: {
type: GraphQLInt,
resolve: e => e.id
},
poiName: {
type: GraphQLString,
resolve: e => e.pointName
},
address: {
type: GraphQLString
}
},
});

2. 设置查询入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => ({
pois: {
type: new GraphQLList(GQPoi),
description: '店铺列表',
resolve: async () => {
return await getPois();
}
},
poi: {
type: GQPoi,
description: '店铺查询',
args: {
id: {
type: GraphQLInt
}
},
resolve: async (source, args, ctx, info) => {
console.log(args);
const id = args.id;
const pois = await getPois();
const poi = pois.filter(e => e.id === id );
return poi[0];
}
}
})
});
export default new GraphQLSchema({
query: Query,
});

在Query中添加了两个字段,pois是用来获取店铺列表,poi是用来查询某一个门店的信息,和之前不一样的是门店列表需要异步获取。poi还需要一个查询的id参数。

3. 查询

先来个简单的查询

1
2
3
4
5
6
query{
pois {
id
poiName
}
}

下面使用poi来查询某一个门店的信息

1
2
3
4
5
6
query{
poi(id: 111) {
poiName
poiId
}
}

获取到的结果是

1
2
3
4
5
6
7
8
{
"data": {
"poi": {
"poiName": "肯德基",
"id": 111
}
}
}

可以发现GraphQL的查询是非常灵活的

Docker 命令记录

发表于 2017-07-18 | 分类于 运维
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 下载镜像
docker pull $name
// 查看所有镜像
docker images
// 查看所有容器
docker ps -a
// 删除容器
docker rm $name
// 删除所有容器
docker rm $(docker ps -a -q)
// 构建
docker build -t $name:tag .
// 启动
docker run -it -p 8000:8000 --name learn-app --link mongodb:mongoDB learn-app:v3
// 停止容器
docker stop $name
// 删除虚悬镜像
docker rmi $(docker images -q -f dangling=true)

使用Docker部署nodeJS+MongoDB应用

发表于 2017-07-18 | 分类于 运维

需要提前安装node,mongo镜像

1. 构建node应用

在node应用根目录下新建Dockerfile文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM node
# 创建工作目录
RUN mkdir /app
WORKDIR /app
# 复制项目文件
COPY . /app
# 声明暴露端口
EXPOSE 8000
# 安装包
RUN [ "rm", "-rf", "node_modules"]
RUN [ "npm", "install"]
# 启动应用
CMD [ "npm", "run", "s"]

修改node应用连接数据库方式

1
2
// mongoDB下面会介绍
mongoose.connect('mongodb://mongoDB/lp-platform');

开始构建

$ docker build -t learn-app:v1 .

2. 启动mongoDB容器

$ docker run -it –name mongodb mongo

 后台运行

$ docker run -d –name mongodb mongo

3. 启动node应用容器

$ docker run -it -p 8000:8000 –name learn-app –link mongodb:mongoDB learn-app:v1

需要注意的是这里的 link 命令的作用是连接node容器和mango数据库容器,这样在node应用中就可以使用mongoDB来连接数据库

4. 完成

容器启动完成之后就可以在浏览器中访问了,这里记录一下我遇见的一个问题:

容器启动完成之后,通过curl命令是可以正常访问node
APP,但是在浏览器中并不能访问,一直加载中,尝试了很多方法之后发现重启浏览器可以解决。。。。。。。。。

mongoDB的安装与使用

发表于 2017-03-21 | 分类于 后端

1 安装

1
2
3
4
5
// 先更新brew
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
// 安装mongodb
$ brew install mongodb

等待安装成功

1
2
3
4
To have launchd start mongodb now and restart at login:
brew services start mongodb
Or, if you don't want/need a background service you can just run:
mongod --config /usr/local/etc/mongod.conf

根据提示启动服务

1
$ brew services start mongodb

2 使用

1
2
3
4
5
6
$ mongo
> db.test.insert({name: 'lxc'})
WriteResult({ "nInserted" : 1 })
> db.test.find()
{ "_id" : ObjectId("58d0c768a060bd0547d979a3"), "name" : "lxc" }
>

创建数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> show dbs
admin 0.000GB
local 0.000GB
> use test
switched to db test
// 这时候并不会显示test,要插入一条数据才可以
> show dbs
admin 0.000GB
local 0.000GB
> db.test.insert({name: 'lxc'})
WriteResult({ "nInserted" : 1 })
> show dbs
admin 0.000GB
local 0.000GB
test 0.000GB
>

删除数据库

1
2
3
4
5
6
7
8
> db
test
> db.dropDatabase();
{ "dropped" : "test", "ok" : 1 }
> show dbs
admin 0.000GB
local 0.000GB
>

async/await介绍

发表于 2016-07-29

对于异步回调带来的麻烦,有很多方案可以解决,ES6中添加了原生支持的Promise,还引入了Generator,但是在ES7中又提出了新的方案,就是async和await。

async 是什么呢?

其实,async函数就是Generator函数的语法糖。相比于*和yield,语义更清楚,async表示函数内有异步操作,await表示跟在他后面的表达式需要等待结果。

用法

当函数执行的时候,遇见await就会先返回,等到后面的异步操作完成,就会再接着执行下面的语句,就相当于同步的函数。

1
2
3
4
5
6
7
8
9
10
11
12
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 5000);

上面的代码会在5s后输出‘hello world’

co入门

发表于 2016-07-27

1 什么是CO

co是基于Generator和Promise的异步解决方案,co的最大好处就是允许你以同步的方式写异步的代码流程,并且可以使用try/catch。

2 用法

1
2
3
4
5
6
7
8
co(function *(){
try {
var res = yield fetch(...);
console.log(res);
} catch(e) {
console.log(e)
}
})()

3 源码解读

co的源码只有200多行,还是很短的,那就看看吧。

1
2
3
4
5
6
7
8
9
10
11
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
...
}

可以发现:
co返回的最终结果是一个Promise对象;第6行,这里已经调用gen了,所以下面就可以通过next()遍历了。
然后发现运行了一个叫onFulfilled()的函数,这是什么呢
,往下看就知道了

1
2
3
4
5
6
7
8
9
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}

看第四行我知道原来这调用了gen.next()去遍历generator了,同时将返回的结果放倒ret并且当作参数执行了next()函数,这里的next函数并不是gen.next,那这是什么呢?继续看

1
2
3
4
5
6
7
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}

这个函数首先判断了ret.done ,这是在判断generator是否遍历完成,如果没完成继续向下走,这里又运行了一个toPromise函数,根据名字就知道这是将上一步返回的value转换成Promise对象,然后下一步为这个promise添加了成功和失败回调,如果上一步状态是成功那么就会再去运行onFulfilled()函数,并且把结果传递下去,如果某一步运行失败,那么她就会调用onRejected函数,这是什么呢

1
2
3
4
5
6
7
8
9
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

这原来就是对于上一步发生的错误抛出异常便于外层捕获。如果异常抛出成功,就会继续next遍历generator对象直到结束。
这就是co运行的大概流程,下面看一下他所提供的一下其他方法。

1
2
3
4
5
6
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}

根据名字就知道这个函数是判断一个函数是否是Generator函数:
首先获取obj的构造函数,如果构造函数没有自然就不是了,然后只要判断构造函数的name活着displayname就可以了

1
2
3
function isPromise(obj) {
return 'function' == typeof obj.then;
}

这个函数的作用是判断一个对象是否是promise对象,那我们只需要判断这个对象里面是否有then函数就可以了

Promise简单实现

发表于 2016-07-26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function Promise(fn) {
var doneList = [];
var failList = [];
var state = 'pending';
var value;
//注册回调函数 为什么要加入状态机制呢,因为如果当前的fn已经执行完成了,再去用
//then绑定回调,就不会执行了,所以加入状态之后,即使已经运行完成也会继续执行回调函数
this.then = (done, fail) => {
switch (state) {
case 'pending':
doneList.push(done);
failList.push(fail);
return this;
break;
case 'fulfilled':
done(value);
return this;
break;
case 'rejected':
fail(value);
return this;
break;
}
}
//更改对象状态为‘resolved’并执行成功回调
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
//使用setTimeout的原因是如果fn不是异步的,在fn瞬间运行完成,resolve也会立刻运行,
//但是这时候doneList还是空的,所以我们要等待then添加完成,使用setTiemout强行将
//函数的运行放到第一次循环之后运行
setTimeout(function() {
doneList.forEach(done => {
done(newValue);
})
}, 0);
}
//更改对象状态为‘rejected’并执行失败回调
function reject(err) {
value = err;
state = 'rejected';
setTimeout(function() {
failList.forEach(fail => {
fail(err);
})
}, 0);
}
fn(resolve, reject);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var p = new Promise((resolve, reject) => {
setTimeout(function(){
console.log('done');
//resolve('messsage');
reject('fail');
},3000)
})
p.then(function(message){
console.log('then1:',message);
},function(err){
console.log('then1err:',err);
}).then(function(message){
console.log('then2:',message);
},function(err){
console.log('then2err:',err);
})

Promise入门

发表于 2016-07-26

1 什么是Promise

promise是一种异步编程的解决方案,因为正常写回调很不直观,嵌套多了之后简直令人崩溃,所以有了promise这个东西。ES6则是将promise进行了统一,提供了原生的promise对象。

2 基本用法

1
2
3
4
5
6
7
8
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('ok');
}, 3000);
});
promise.then(function(value){
console.log(value);
});

3 接口

3.1 then()

then() 的作用是为Promise对象增加回调函数,第一个参数是Resolced状态的回调函数,第二个参数可选,是Rejected状态的回调函数,then() 返回的是一个新的Promise对象,所以后面还可以继续写then();

1
2
3
4
5
6
7
8
9
10
11
12
13
var getPromise = function(num){
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(num);
}, 3000);
});
}
getPromise(1).then(function(value){
console.log(value);
return getPromise(2)
}).then(function(value){
console.log(value);
})

输出结果是

1

2

3.2 catch()

catch() 方法和 then(null,rejection) 方法是等价的,都是指定promise对象发生错误时的回调函数。

1
2
3
4
5
6
7
var promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test

1
2
3
4
5
6
var promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});

上面两种写法是等价的。

==需要注意的是如果Promise对象的状态已经变成Resolved那么再抛出错误是无效的==。

3.3 all()

Promise.all() 方法用于将多个Promise实例包装成一个新的Promise实例。

1
var p = Promise.all([p1, p2, p3]);

all() 方法的参数是数组或者是具有Iterator的对象且每个成员的返回值都是Promise实例。
all方法的最终状态可以参考 与。

  • 只有每个成员的状态都变成 fulfilled , p 的状态才会变成 fulfilled;
  • 而成员的状态有一个变成 rejected ,p 的状态就会变成 rejected;

3.4 race()

race() 方法同样是将多个Promise实例包装成一个新的Promise实例。
与 all() 不同的是,他的成员中有一个状态改变,那么新的Promise状态就会跟着改变

1
2
3
4
5
6
7
8
var p = Promise.race([
fetch('...'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
p.catch(error => console.log(error))

看上面的例子,如果5s后fetch没有返回结果,那么后面的第二个Promise成员就会改变状态为 rejected ,那么p的状态就变成了 rejected ,然后就会触发catch的回调函数。

3.5 resolved()

resolved() 的作用是将现有的对象转换成Promise对象。

1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

参数分为4种情况:

  1. 参数是一个Promise对象,那么这时候就会原封不动地返回这个对象
  2. 一个thenable对象(带有then方法的对象)

    1
    2
    3
    4
    5
    var thenable = {
    then: function(resolve, reject) {
    resolve();
    }
    };

    resolve方法会将这个对象转换为新的Promise 对象并且立即执行then方法

  3. 参数不是then方法的对象,或者根本不是对象,则resolved的方法返回一个新的promise对象,状态为Resolved

    1
    2
    3
    4
    5
    6
    var p = Promise.resolve('Hello');
    p.then(function (s){
    console.log(s)
    });
    // Hello
  4. 参数为空:直接返回一个Resolved状态的Promise对象。

3.6 reject()

reject()方法也会返回一个新的Promise实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。

1
2
3
4
5
6
7
8
var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s){
console.log(s)
});
// 出错了

3.7 done()

如果回调链的最后一个方法发生错误,那么就无法捕捉,所以提供了done()方法,放在回调链最后,捕捉最后可能出现的错误。

3.8 finally

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

4 Promise使用案例(实现fs.unlink方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// unlink(path).then()
var unlink = (path) => {
return new Promise((resolve, reject) => {
fs.unlink(path, function(err) {
if (err) {
reject(err);
} else {
resolve()
}
});
})
}
var unlinkAll = (pathList) => {
var promiseList = [];
for (var path of pathList) {
promiseList.push(unlink(path));
}
return Promise.All(promiseList);
}
123
李续铖

李续铖

21 日志
4 分类
11 标签
GitHub 微博 知乎
© 2017 李续铖