仙剑奇侠传的web移植版

2.1. 能玩吗?

。但在GitHub
repo里并不会包含游戏的资源文件,于是需要自己去找(嘿嘿mq2x)。由于不分发游戏资源文件,且考虑到体积,我也不会提供一个在线游玩的版本。所以基本上只有开发者或者动手能力强的同学才能玩上它了(如果你真的想玩……)

不考虑遇到BUG(无数个)造成游戏直接罢工的情况下(当然身为作者的我是可以驾轻就熟地避过这些BUG的233333),已经可以从新开游戏一直玩到大结局了,而且我已经通关两三遍了XD

JSBridge解析规则

前面的文章中有提到JSBridge的实现,但那时其实更多的是关注原理层面,那么实际上,定义的交互解析规则是什么样的呢?如下

// 以ui.toast实际调用的示例
// `${CUSTOM_PROTOCOL_SCHEME}://${module}:${callbackId}/${method}?${params}`
const uri = 'QuickHybridJSBridge://ui:9527/toast?{"message":"hello"}';

if (os.quick) {
    // 依赖于os判断
    if (os.ios) {
        // ios采用
        window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(uri);
    } else {
        window.top.prompt(uri, '');
    }
} else {
    // 浏览器
    warn(`浏览器中jsbridge无效, 对应scheme: ${uri}`);
}

原生容器中接收到对于的uri后反解析即可知道调用了些什么,上述中:

  • QuickHybridJSBridge是本框架交互的scheme标识

  • modulemethod分别代表API的模块名和方法名

  • params是对于方法传递的额外参数,原生容器会解析成JSONObject

  • callbackId是本次API调用在H5端的回调id,原生容器执行完后,通知H5时会传递回调id,然后H5端找到对应的回调函数并执行

为什么要用uri的方式,因为这种方式可以兼容以前的scheme方式,如果方案切换,变动代价下(本身就是这样升级上来的,所以没有替换的必要)

2. 自问自答的FAQ

源码

github上这个框架的实现

quickhybrid/quickhybrid

1. 无图言屌

优酷视频——有视频有JB!

图片 1图片 2

图片 3

图片 4

图片 5

图片 6

图片 7

图片 8

图片 9

统一的预处理

在上一篇API多平台的支撑中有提到如何基于Object.defineProperty实现一个支持多平台调用的API,实现起来的API大致是这样子的

Object.defineProperty(apiParent, apiName, {
    configurable: true,
    enumerable: true,
    get: function proxyGetter() {
        // 确保get得到的函数一定是能执行的
        const nameSpaceApi = proxysApis[finalNameSpace];

        // 得到当前是哪一个环境,获得对应环境下的代理对象
        return nameSpaceApi[getCurrProxyApiOs(quick.os)] || nameSpaceApi.h5;
    },
    set: function proxySetter() {
        alert('不允许修改quick API');
    },
});

...

quick.extendModule('ui', [{
    namespace: 'alert',
    os: ['h5'],
    defaultParams: {
        message: '',
    },
    runCode(message) {
        alert('h5-' + message);
    },
}]);

其中nameSpaceApi.h5的值是api.runCode,也就是说直接执行runCode(...)中的代码

仅仅这样是不够的,我们需要对调用方法的输入等做统一预处理,因此在这里,我们基于实际的情况,在此基础上进一步完善,加上统一预处理机制,也就是

const newProxy = new Proxy(api, apiRuncode);

Object.defineProperty(apiParent, apiName, {
    ...
    get: function proxyGetter() {
        ...
        return newProxy.walk();
    }
});

我们将新的运行代码变为一个代理对象Proxy,代理api.runCode,然后在get时返回代理过后的实际方法(.walk()方法代表代理对象内部会进行一次统一的预处理)

代理对象的代码如下

function Proxy(api, callback) {
    this.api = api;
    this.callback = callback;
}

Proxy.prototype.walk = function walk() {
    // 实时获取promise
    const Promise = hybridJs.getPromise();

    // 返回一个闭包函数
    return (...rest) = >{
        let args = rest;

        args[0] = args[0] || {};
        // 默认参数的处理
        if (this.api.defaultParams && (args[0] instanceof Object)) {
            Object.keys(this.api.defaultParams).forEach((item) = >{
                if (args[0][item] === undefined) {
                    args[0][item] = this.api.defaultParams[item];
                }
            });
        }

        // 决定是否使用Promise
        let finallyCallback;

        if (this.callback) {
            // 将this指针修正为proxy内部,方便直接使用一些api关键参数
            finallyCallback = this.callback;
        }

        if (Promise) {
            return finallyCallback && new Promise((resolve, reject) = >{
                // 拓展 args
                args = args.concat([resolve, reject]);
                finallyCallback.apply(this, args);
            });
        }

        return finallyCallback && finallyCallback.apply(this, args);
    };
};

从源码中可以看到,这个代理对象统一预处理了两件事情:

  • 1.对于合法的输入参数,进行默认参数的匹配

  • 2.如果环境中支持Promise,那么返回Promise对象并且参数的最后加上resolvereject

而且,后续如果有新的统一预处理(调用API前的预处理),只需在这个代理对象的这个方法中增加即可

2.8. 有可能在手机上运行吗

目前不可以,性能最好的iOS Safari尚未支持yield/generator,而Android
Chrome我目前没有关注。

性能方面没有明确的评价,在MacbookPro上CPU占用率并不高,但是内存很高(因为惨无人道的用内存,毫无优化之心),所以我觉得还是挺堪忧的。

UA约定

混合开发容器中,需要有一个UA标识位来判断当前系统。

这里Android和iOS原生容器统一在webview中加上如下UA标识(也就是说,如果容器UA中有这个标识位,就代表是quick环境-这也是os判断的实现原理)

String ua = webview.getSettings().getUserAgentString();

ua += " QuickHybridJs/" + getVersion();

// 设置浏览器UA,JS端通过UA判断是否属于quick环境
webview.getSettings().setUserAgentString(ua);

// 获取默认UA
NSString *defaultUA = [[UIWebView new] stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];

NSString *version = [[NSBundle mainBundle].infoDictionary objectForKey:@"CFBundleShortVersionString"];

NSString *customerUA = [defaultUA stringByAppendingString:[NSString stringWithFormat:@" QuickHybridJs/%@", version]];

[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":customerUA}];

如上述代码中分别在Android和iOS容器的UA中添加关键性的标识位。

2.9. 所以总的完成度?

直接搬GitHub上给(胡邹)的吧:

模块 进度
资源 90%
读档 99%
存档 40%
Surface 90%
位图 99%
Sprite 99%
地图 90%
场景 90%
调色盘 90%
文本 99%
脚本(天坑) 70%
平常UI 90%
战斗UI 90%
战斗(天坑) 70%
播片 90%
结局 95%
音乐 0%
音效 0%

关于代码规范与单元测试

项目中采用的Airbnb代码规范并不是100%契合原版,而是基于项目的情况定制了下,但是总体上95%以上是符合的

还有一块就是单元测试,这是很容易忽视的一块,但是也挺难做好的。这个项目中,基于Karma + Mocha进行单元测试,而且并不是测试驱动,而是在确定好内容后,对核心部分的代码都进行单测。
内部对于API的调用基本都是靠JS来模拟,对于一些特殊的方法,还需Object.defineProperty(window.navigator, name, prop)来改变window本身的属性来模拟。
本项目中的核心代码已经达到了100%的代码覆盖率。

具体的代码这里不赘述,可以参考源码

2.7. 现在看起来都是dev状态,什么时候会成为成品游戏?

也许永远不会,因为没动力再把各种BUG还有音频部分的坑填了……

如果有生之年真的能填,那么也许可以用node-webkit这类的东西打包成成品游戏,不过……有意思么……

返回根目录

  • 【quickhybrid】如何实现一个Hybrid框架

仙剑奇侠传的web移植版

2015/10/06 · HTML5 · 1
评论 ·
仙剑奇侠传

原文出处:
刘骥(@刘骥-JimLiu)   

API内部做了些什么

API内部只做与本身功能逻辑相关的操作,这里有几个示例

quick.extendModule('ui', [{
    namespace: 'toast',
    os: ['h5'],
    defaultParams: {
        message: '',
    },
    runCode(...rest) {
        // 兼容字符串形式
        const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message', );
        const options = args[0];
        const resolve = args[1];

        // 实际的toast实现
        toast(options);
        options.success && options.success();
        resolve && resolve();
    },
}, ...]);

quick.extendModule('ui', [{
    namespace: 'toast',
    os: ['quick'],
    defaultParams: {
        message: '',
    },
    runCode(...rest) {
        // 兼容字符串形式
        const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message');

        quick.callInner.apply(this, args);
    },
}, ...]);

以上是toast功能在h5和quick环境下的实现,其中,在quick环境下唯一做的就是兼容了一个字符串形式的调用,在h5环境下则是完全的实现了h5下对应的功能(promise也需自行兼容)

为什么h5中更复杂?因为quick环境中,只需要拼凑成一个JSBridge命令发送给原生即可,具体功能由原生实现,而h5的实现是需要自己完全实现的。

另外,其实在quick环境中,上述还不是最少的代码(上述加了一个兼容调用功能,所以多了几行),最少代码如下

quick.extendModule('ui', [{
    namespace: 'confirm',
    os: ['quick'],
    defaultParams: {
        title: '',
        message: '',
        buttonLabels: ['取消', '确定'],
    },
}, ...]);

可以看到,只要是符合标准的API定义,在quick环境下的实现只需要定义些默认参数就可以了,其它的框架自动帮助实现了(同样promise的实现也在内部默认处理掉了)

这样以来,就算是标准quick环境下的API数量多,实际上增加的代码也并不多。

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website