Facebook 小游戏加载远程图片

问题背景

Facebook Instant Game中,大部分的资源是直接打到游戏包里的,但是,在某些情况下,我们想要加载远程图片来更新游戏内容,比如更新关卡,或者远程更新交叉推广的游戏图标等。

示例

像这个游戏中,交叉推广的游戏就从服务器远程加载的。

游戏测试地址:https://fb.gg/play/twistglidecolor

问题

如果直接使用Cocos Creator的cc.loader.load来加载远程服务器上的图片,则会出现2种错误:

  1. 跨域资源访问的问题 (CORS = Cross-Origin Resource Sharing)

关于 CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

  1. 内容安全策略 (CSP = Content Security Policy)

如果在服务器端开放了跨域访问,还可能遇到这个问题,因为facebook只允许从特定域名下加载图片。

关于CSP: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

解决方法

进过一番调查,发现facebook instant game支持用blob读取图片。

https://stackoverflow.com/questions/49122173/instant-games-content-security-policy?rq=1

https://stackoverflow.com/questions/52592577/facebook-instant-games-loading-remote-images-during-the-game-doesnt-work

那么,方法也就明确了。

第一步,服务端开放跨域访问

以Nginx举例,可以在配置文件中添加以下设置:

# https://enable-cors.org/server_nginx.html
#
# Wide-open CORS config for nginx
#
location / {
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #
        # Custom headers and headers various browsers *should* be OK with but aren't
        #
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        #
        # Tell client that this pre-flight info is valid for 20 days
        #
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
     }
     if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
     }
     if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
     }
}

第二步,使用blob协议来读取图片数据

有两种方式可以读取blob数据

  1. 使用XMLHttpRequest接口
// 该段代码来自 stackoverflow,未测试
function loadXHR(url) {
    return new Promise(function(resolve, reject) {
        try {
            var xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.responseType = "blob";
            xhr.onerror = function() {reject("Network error.")};
            xhr.onload = function() {
                if (xhr.status === 200) {resolve(xhr.response)}
                else {reject("Loading error:" + xhr.statusText)}
            };
            xhr.send();
        }
        catch(err) {reject(err.message)}
    });
}
  1. 使用fetch 接口

关于fetch: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

// 自用代码
async loadIcon(iconUrl:string, gameId:string){
    console.info("===> loading icon: ", iconUrl);
    try{
        let resp = await fetch(iconUrl, {
            method: 'GET',
            mode: 'cors',
            // cache: 'default',
        });

        let blobObj = await resp.blob();
        if(blobObj){
            console.info("===> image blob: ", blobObj);
            // TODO:显示blob格式的图片
        }else{
            console.warn("===> loading blob failed");
        }
    }catch(e){
        console.warn("===> something wrong: ", e);
    }
}

第三步,显示blob格式的图片

Cocos Creator的参考代码

function showBlobImage(blobObj){
    let img = new Image();
    img.src = URL.createObjectURL(blobObj);
    img.onload = function(){
        var texture = new cc.Texture2D();
        texture.initWithElement(img);
        texture.handleLoadedTexture();
        var newframe = new cc.SpriteFrame(texture);
        if(newframe){
            if(self.sprite){
                self.sprite.spriteFrame = newframe;
                self.node.width = 100;
                self.node.height = 100;    
                console.info("===> update icon success!");    
            }
        }else{
            console.warn("===> create sp failed");
        }
    }
}

其他解决方法

  1. 将图片保存在facebook允许的域名下

这个理论上可行的,网上也有人是这么做的,但是我没有去测试。

  1. 使用base64或者其他自定义编码来传图片数据,然后显示。

这也是个可行的方法,但是有几个明显的问题:
1). 数据大,传输效率低
2). base64解码的效率低
3). 需要服务端将图片转换为base64编码

是否会违反facebook规定?

不会。

1). fetch是facebook支持的api

https://developers.facebook.com/docs/games/instant-games/faq/

2). 从stackoverflow上的回复来看,facebook允许blob协议来传输图片。

https://stackoverflow.com/questions/49122173/instant-games-content-security-policy?rq=1