From 2eefd0e372efb1e7806a6ae0b094cd4baa637ef4 Mon Sep 17 00:00:00 2001 From: ianwith Date: Thu, 7 Sep 2017 16:51:36 +0800 Subject: [PATCH 1/4] Update rexxarFetch for compatible with WKWebview --- src/rexxarFetch.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/rexxarFetch.js b/src/rexxarFetch.js index e866c6a..6529249 100644 --- a/src/rexxarFetch.js +++ b/src/rexxarFetch.js @@ -9,7 +9,7 @@ const isAndroid = navigator ? /android/i.test(navigator.userAgent.toLowerCase()) /** * `rexxarFetch` wraps whatwg-fetch function. Use rexxarFetch like using the normal fetch API. * However, there are some limitation, rexxarFetch does not support Request object as - * argument when you are using for POST in Android, and `application/x-www-form-urlencoded` + * argument when you are using for HTTP POST, and `application/x-www-form-urlencoded` * must be specified as content-type. * * @param {string|object} input Url string or a Request object @@ -23,14 +23,14 @@ export default function rexxarFetch(input, init) { if (Request.prototype.isPrototypeOf(input) && !init) { request = input; - if (request.method === 'POST' && isAndroid) { - throw new Error('Please use `rexxarFetch(input, init)` for HTTP POST in Android'); + if (request.method === 'POST') { + throw new Error('rexxarFetch POST error: please use `rexxarFetch(input, init)` for HTTP POST'); } } else { request = new Request(input, init); } - if (request.method === 'POST' && isAndroid) { + if (request.method === 'POST') { let contentType = request.headers.get('content-type'); let body = init.body; @@ -38,17 +38,17 @@ export default function rexxarFetch(input, init) { input = `${input}&_rexxar_method=POST`.replace(/[&?]/, '?'); promise = fetch(input); } else if (contentType && contentType.indexOf('application/x-www-form-urlencoded') > -1) { - if (window && 'URLSearchParams' in window && URLSearchParams.prototype.isPrototypeOf(body)) { + if (window && 'URLSearchParams' in window && window.URLSearchParams.prototype.isPrototypeOf(body)) { body = body.toString(); } if (getType(body) === 'String') { input = `${input}&${body}&_rexxar_method=POST`.replace(/[&?]/, '?'); promise = fetch(input); } else { - throw new Error('rexxarFetch for Android Cannot handle this body type'); + throw new Error('rexxarFetch POST error: cannot handle this body type'); } } else { - throw new Error('rexxarFetch for Android only supports `application/x-www-form-urlencoded` as content-type'); + throw new Error('rexxarFetch POST error: only supports `application/x-www-form-urlencoded` as content-type'); } } else { promise = fetch(request); From 3f06eecce65be7b42e1134559e56bbd1388e7d63 Mon Sep 17 00:00:00 2001 From: ianwith Date: Thu, 7 Sep 2017 16:52:27 +0800 Subject: [PATCH 2/4] build for WKWebview updating --- lib/rexxarFetch.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rexxarFetch.js b/lib/rexxarFetch.js index c08ebf3..bf058e2 100644 --- a/lib/rexxarFetch.js +++ b/lib/rexxarFetch.js @@ -20,7 +20,7 @@ var isAndroid = navigator ? /android/i.test(navigator.userAgent.toLowerCase()) : /** * `rexxarFetch` wraps whatwg-fetch function. Use rexxarFetch like using the normal fetch API. * However, there are some limitation, rexxarFetch does not support Request object as - * argument when you are using for POST in Android, and `application/x-www-form-urlencoded` + * argument when you are using for HTTP POST, and `application/x-www-form-urlencoded` * must be specified as content-type. * * @param {string|object} input Url string or a Request object @@ -34,14 +34,14 @@ function rexxarFetch(input, init) { if (Request.prototype.isPrototypeOf(input) && !init) { request = input; - if (request.method === 'POST' && isAndroid) { - throw new Error('Please use `rexxarFetch(input, init)` for HTTP POST in Android'); + if (request.method === 'POST') { + throw new Error('rexxarFetch POST error: please use `rexxarFetch(input, init)` for HTTP POST'); } } else { request = new Request(input, init); } - if (request.method === 'POST' && isAndroid) { + if (request.method === 'POST') { var contentType = request.headers.get('content-type'); var body = init.body; @@ -49,17 +49,17 @@ function rexxarFetch(input, init) { input = (input + '&_rexxar_method=POST').replace(/[&?]/, '?'); promise = (0, _isomorphicFetch2.default)(input); } else if (contentType && contentType.indexOf('application/x-www-form-urlencoded') > -1) { - if (window && 'URLSearchParams' in window && URLSearchParams.prototype.isPrototypeOf(body)) { + if (window && 'URLSearchParams' in window && window.URLSearchParams.prototype.isPrototypeOf(body)) { body = body.toString(); } if ((0, _utils.getType)(body) === 'String') { input = (input + '&' + body + '&_rexxar_method=POST').replace(/[&?]/, '?'); promise = (0, _isomorphicFetch2.default)(input); } else { - throw new Error('rexxarFetch for Android Cannot handle this body type'); + throw new Error('rexxarFetch POST error: cannot handle this body type'); } } else { - throw new Error('rexxarFetch for Android only supports `application/x-www-form-urlencoded` as content-type'); + throw new Error('rexxarFetch POST error: only supports `application/x-www-form-urlencoded` as content-type'); } } else { promise = (0, _isomorphicFetch2.default)(request); From 68f5a27fce52ea9edaeae39298fde43a8e7e05be Mon Sep 17 00:00:00 2001 From: ianwith Date: Sun, 10 Sep 2017 16:16:20 +0800 Subject: [PATCH 3/4] use rexxar widget helpers, deprecate getRexxarWidget --- lib/assemblePayload.js | 38 ++++++++++++++++++++++++++++++++++++++ lib/callbackListener.js | 34 ++++++++++++++++++++++++++++++++++ lib/dispatch.js | 18 ++++++++++++++++++ lib/dispatchMessage.js | 18 ++++++++++++++++++ lib/getRexxarWidget.js | 4 +++- lib/index.js | 24 ++++++++++++++++++++++-- lib/utils.js | 36 ++++++++++++++++++++++++++++++++++++ lib/widgetMessenger.js | 23 +++++++++++++++++++++++ src/assemblePayload.js | 28 ++++++++++++++++++++++++++++ src/callbackListener.js | 24 ++++++++++++++++++++++++ src/dispatch.js | 12 ++++++++++++ src/getRexxarWidget.js | 4 ++++ src/index.js | 10 +++++++++- src/utils.js | 32 ++++++++++++++++++++++++++++++++ src/widgetMessenger.js | 12 ++++++++++++ 15 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 lib/assemblePayload.js create mode 100644 lib/callbackListener.js create mode 100644 lib/dispatch.js create mode 100644 lib/dispatchMessage.js create mode 100644 lib/widgetMessenger.js create mode 100644 src/assemblePayload.js create mode 100644 src/callbackListener.js create mode 100644 src/dispatch.js create mode 100644 src/widgetMessenger.js diff --git a/lib/assemblePayload.js b/lib/assemblePayload.js new file mode 100644 index 0000000..76770e5 --- /dev/null +++ b/lib/assemblePayload.js @@ -0,0 +1,38 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _utils = require('./utils'); + +/** + * Assemble payload for message + * + * assemblePayload({ data: '' }) + * + * @param {object} obj + * @param {string} base + * @returns {string} + */ +var assemblePayload = function assemblePayload(obj) { + var base = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + + if ((0, _utils.getType)(obj) !== 'Object' || (0, _utils.getType)(base) !== 'String') { + throw new Error('assemblePayload arguments type error'); + } + var query = Object.getOwnPropertyNames(obj).map(function (key) { + var value = obj[key]; + // Q: does callback need to be registed + if ((0, _utils.getType)(value) === 'Object') { + value = JSON.stringify(value); + } + return encodeURIComponent(key) + '=' + encodeURIComponent(value); + }).join('&'); + if (base) { + base = '/' + base; + } + return (base + '&' + query).replace(/[&?]/, '?'); +}; + +exports.default = assemblePayload; \ No newline at end of file diff --git a/lib/callbackListener.js b/lib/callbackListener.js new file mode 100644 index 0000000..bb78aaf --- /dev/null +++ b/lib/callbackListener.js @@ -0,0 +1,34 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _utils = require('./utils'); + +/** + * Curry function for callback register + * + * callbackListener('Rexxar.Widget.AlertDialog')('clickHandler')(()=>{})) + * + * @param {string} ns + * @returns {string} The callback name + */ +var callbackListener = function callbackListener(ns) { + return function (name) { + return function (callback) { + (0, _utils.namespace)(ns)[name] = function (jsonStr) { + var data = void 0; + try { + data = JSON.parse(jsonStr); + } catch (e) { + data = jsonStr; + } + callback(data); + }; + return ns + '.' + name; + }; + }; +}; + +exports.default = callbackListener; \ No newline at end of file diff --git a/lib/dispatch.js b/lib/dispatch.js new file mode 100644 index 0000000..881c593 --- /dev/null +++ b/lib/dispatch.js @@ -0,0 +1,18 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _utils = require('./utils'); + +/** + * Dispatch message to client + * + * @param {string} msg + */ +var dispatch = function dispatch(msg) { + (0, _utils.callUri)(msg); +}; + +exports.default = dispatch; \ No newline at end of file diff --git a/lib/dispatchMessage.js b/lib/dispatchMessage.js new file mode 100644 index 0000000..59c45a0 --- /dev/null +++ b/lib/dispatchMessage.js @@ -0,0 +1,18 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _utils = require('./utils'); + +/** + * Dispatch message to client + * + * @param {string} msg + */ +var dispatchMessage = function dispatchMessage(msg) { + (0, _utils.callUri)(msg); +}; + +exports.default = dispatchMessage; \ No newline at end of file diff --git a/lib/getRexxarWidget.js b/lib/getRexxarWidget.js index 5d32186..cea6c19 100644 --- a/lib/getRexxarWidget.js +++ b/lib/getRexxarWidget.js @@ -4,7 +4,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** + * IMPORTANT: This is deprecated + */ exports.default = getRexxarWidget; diff --git a/lib/index.js b/lib/index.js index d5ae946..a7366a8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,12 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.rexxarFetch = exports.getRexxarWidget = undefined; +exports.dispatch = exports.callbackListener = exports.assemblePayload = exports.widgetMessenger = exports.rexxarFetch = exports.getRexxarWidget = undefined; var _getRexxarWidget = require('./getRexxarWidget'); var _getRexxarWidget2 = _interopRequireDefault(_getRexxarWidget); +var _widgetMessenger = require('./widgetMessenger'); + +var _widgetMessenger2 = _interopRequireDefault(_widgetMessenger); + +var _assemblePayload = require('./assemblePayload'); + +var _assemblePayload2 = _interopRequireDefault(_assemblePayload); + +var _callbackListener = require('./callbackListener'); + +var _callbackListener2 = _interopRequireDefault(_callbackListener); + +var _dispatch = require('./dispatch'); + +var _dispatch2 = _interopRequireDefault(_dispatch); + var _rexxarFetch = require('./rexxarFetch'); var _rexxarFetch2 = _interopRequireDefault(_rexxarFetch); @@ -16,4 +32,8 @@ var _rexxarFetch2 = _interopRequireDefault(_rexxarFetch); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } exports.getRexxarWidget = _getRexxarWidget2.default; -exports.rexxarFetch = _rexxarFetch2.default; \ No newline at end of file +exports.rexxarFetch = _rexxarFetch2.default; +exports.widgetMessenger = _widgetMessenger2.default; +exports.assemblePayload = _assemblePayload2.default; +exports.callbackListener = _callbackListener2.default; +exports.dispatch = _dispatch2.default; \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js index 0f5ee9a..546b12c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -11,6 +11,8 @@ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = [ exports.obj2str = obj2str; exports.str2obj = str2obj; exports.getType = getType; +exports.namespace = namespace; +exports.callUri = callUri; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } @@ -64,4 +66,38 @@ function str2obj(str) { */ function getType(obj) { return Object.prototype.toString.call(obj).slice(8, -1); +} + +/** + * Declare a namespace from a string: + * + * namespace('Rexxar.Widget.AlertDialog') => Rexxar.Widget.AlertDialog = {} + * + * @param {string} ns + * @returns {object} + */ +function namespace(ns) { + var names = ns.split('.'); + var owner = window; + for (var i = 0; i < names.length; i++) { + var name = names[i]; + owner[name] = owner[name] || {}; + owner = owner[name]; + } + return owner; +} + +/** + * Go to uri + * + * @param {string} uri + */ +function callUri(uri) { + var iframe = document.createElement('iframe'); + iframe.src = uri; + iframe.style.display = 'none'; + document.documentElement.appendChild(iframe); + setTimeout(function () { + return document.documentElement.removeChild(iframe); + }, 0); } \ No newline at end of file diff --git a/lib/widgetMessenger.js b/lib/widgetMessenger.js new file mode 100644 index 0000000..1ffedef --- /dev/null +++ b/lib/widgetMessenger.js @@ -0,0 +1,23 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * Curry function returns a message wating for dispatching + * + * widgetMessenger('douban', 'rexxar-container')('widget/alert_dialog') + * + * @param {string} scheme + * @param {string} host + * @returns {function} + */ +var widgetMessenger = function widgetMessenger(scheme, host) { + return function (name) { + return function (payload) { + return scheme + "://" + host + "/" + name + payload; + }; + }; +}; + +exports.default = widgetMessenger; \ No newline at end of file diff --git a/src/assemblePayload.js b/src/assemblePayload.js new file mode 100644 index 0000000..7db27e7 --- /dev/null +++ b/src/assemblePayload.js @@ -0,0 +1,28 @@ +import { getType } from './utils'; + +/** + * Assemble payload for message + * + * assemblePayload({ data: '' }) + * + * @param {object} obj + * @param {string} base + * @returns {string} + */ +const assemblePayload = (obj, base = '') => { + if (getType(obj) !== 'Object' || getType(base) !== 'String') { + throw new Error('assemblePayload arguments type error') + } + let query = Object.getOwnPropertyNames(obj).map(key => { + let value = obj[key] + // Q: does callback need to be registed + if (getType(value) === 'Object') { + value = JSON.stringify(value) + } + return encodeURIComponent(key) + '=' + encodeURIComponent(value) + }).join('&') + if (base) { base = `/${base}` } + return `${base}&${query}`.replace(/[&?]/, '?') +} + +export default assemblePayload diff --git a/src/callbackListener.js b/src/callbackListener.js new file mode 100644 index 0000000..31cbc53 --- /dev/null +++ b/src/callbackListener.js @@ -0,0 +1,24 @@ +import { namespace } from './utils'; + +/** + * Curry function for callback register + * + * callbackListener('Rexxar.Widget.AlertDialog')('clickHandler')(()=>{})) + * + * @param {string} ns + * @returns {string} The callback name + */ +const callbackListener = (ns) => (name) => (callback) => { + namespace(ns)[name] = (jsonStr) => { + let data; + try { + data = JSON.parse(jsonStr); + } catch(e) { + data = jsonStr; + } + callback(data); + } + return `${ns}.${name}` +} + +export default callbackListener diff --git a/src/dispatch.js b/src/dispatch.js new file mode 100644 index 0000000..f2b99ba --- /dev/null +++ b/src/dispatch.js @@ -0,0 +1,12 @@ +import { callUri } from './utils'; + +/** + * Dispatch message to client + * + * @param {string} msg + */ +const dispatch = (msg) => { + callUri(msg) +} + +export default dispatch diff --git a/src/getRexxarWidget.js b/src/getRexxarWidget.js index f83d57c..1480a89 100644 --- a/src/getRexxarWidget.js +++ b/src/getRexxarWidget.js @@ -1,3 +1,7 @@ +/** + * IMPORTANT: This is deprecated + */ + import { obj2str, getType } from './utils'; /** diff --git a/src/index.js b/src/index.js index 5f67344..b7e6308 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,15 @@ import getRexxarWidget from './getRexxarWidget'; +import widgetMessenger from './widgetMessenger'; +import assemblePayload from './assemblePayload'; +import callbackListener from './callbackListener'; +import dispatch from './dispatch'; import rexxarFetch from './rexxarFetch'; export { getRexxarWidget, - rexxarFetch + rexxarFetch, + widgetMessenger, + assemblePayload, + callbackListener, + dispatch, } diff --git a/src/utils.js b/src/utils.js index 28a9c6a..93cd106 100644 --- a/src/utils.js +++ b/src/utils.js @@ -48,3 +48,35 @@ export function str2obj(str) { export function getType(obj) { return Object.prototype.toString.call(obj).slice(8, -1); } + +/** + * Declare a namespace from a string: + * + * namespace('Rexxar.Widget.AlertDialog') => Rexxar.Widget.AlertDialog = {} + * + * @param {string} ns + * @returns {object} + */ +export function namespace(ns) { + let names = ns.split('.'); + let owner = window; + for (let i = 0; i < names.length; i++) { + let name = names[i]; + owner[name] = owner[name] || {}; + owner = owner[name]; + } + return owner; +} + +/** + * Go to uri + * + * @param {string} uri + */ +export function callUri(uri) { + let iframe = document.createElement('iframe'); + iframe.src = uri; + iframe.style.display = 'none'; + document.documentElement.appendChild(iframe); + setTimeout(() => document.documentElement.removeChild(iframe), 0); +} diff --git a/src/widgetMessenger.js b/src/widgetMessenger.js new file mode 100644 index 0000000..7a5924f --- /dev/null +++ b/src/widgetMessenger.js @@ -0,0 +1,12 @@ +/** + * Curry function returns a message wating for dispatching + * + * widgetMessenger('douban', 'rexxar-container')('widget/alert_dialog') + * + * @param {string} scheme + * @param {string} host + * @returns {function} + */ +const widgetMessenger = (scheme, host) => (name) => (payload) => `${scheme}://${host}/${name}${payload}` + +export default widgetMessenger From 23917d0b6f86672331ef4e4d8c20b2c949a683ab Mon Sep 17 00:00:00 2001 From: ianwith Date: Sun, 10 Sep 2017 17:22:58 +0800 Subject: [PATCH 4/4] Update README for 0.2.0 --- README.md | 84 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index adc026a..77955bc 100644 --- a/README.md +++ b/README.md @@ -20,43 +20,45 @@ $ npm install rexxar-web --save ## 使用 -### getRexxarWidget +### widgetMessenger + +`widgetMessenger` 是一个消息组装器,分段传入参数构成消息体 ```js -import { getRexxarWidget } from 'rexxar-web'; +let messenger = widgetMessenger('douban', 'rexxar-container')('widget/alert_dialog') +``` -const RexxarWidget = getRexxarWidget({ - scheme: 'douban', - host: 'rexxar-container' -}); +### callbackListener -class Title extends RexxarWidget { - constructor(title) { - super('title'); - this.title = title; - } - show() { - super.call({ title: this.title }); - } -} -let title = new Title('My Title'); -title.show(); +`callbackListener` 将注册给定的回调函数,并挂载到全局,用于客户端调用 + +```js +let cbName = callbackListener('Rexxar.Widget.AlertDialog')('confirm')(() => {}) ``` -#### getRexxarWidget(config) +### assemblePayload(obj[, base]) -配置 Widget 协议的 scheme 和 host,获取 RexxarWidget 基类,推荐使用继承的方式编写自己的组件。 +`assemblePayload` 是将一个对象转化成消息中的payload -##### `config.scheme` +```js +let data = { + title: 'AlertDialog', + callback: cbName, +} +let payload = assemblePayload(data) +``` -Type: `String` +### dispatch(messenger) -##### `config.host` +`dispatch` 将由widgetMessenger组装好的消息分发给客户端 -Type: `String` +```js +dispatch(messenger(payload)) +``` +### rexxarFetch(input[, init]) -### rexxarFetch +`rexxarFetch` 是对 [fetch](https://github.com/github/fetch) 的包装,提供与 [window.fetch](https://fetch.spec.whatwg.org/) 一样的接口和用法。值得注意的是,在使用 POST 时会有所限制,只允许 `content-type` 为 `application/x-www-form-urlencoded` 的请求。 ```js import { rexxarFetch } from 'rexxar-web'; @@ -71,9 +73,32 @@ rexxarFetch('/request') ) ``` -#### rexxarFetch(input[, init]) +### getRexxarWidget(config) -rexxarFetch 是对 [fetch](https://github.com/github/fetch) 的包装,提供与 [window.fetch](https://fetch.spec.whatwg.org/) 一样的接口和用法。值得注意的是,在 Android Container 中使用 POST 会有所限制,只允许 `content-type` 为 `application/x-www-form-urlencoded` 的请求。 +**Deprecated** + +配置 Widget 协议的 scheme 和 host,获取 RexxarWidget 基类,推荐使用继承的方式编写自己的组件。 + +```js +import { getRexxarWidget } from 'rexxar-web'; + +const RexxarWidget = getRexxarWidget({ + scheme: 'douban', + host: 'rexxar-container' +}); + +class Title extends RexxarWidget { + constructor(title) { + super('title'); + this.title = title; + } + show() { + super.call({ title: this.title }); + } +} +let title = new Title('My Title'); +title.show(); +``` ## 开发与部署 @@ -110,6 +135,13 @@ $ gulp deploy ``` 在这个例子中我们用 `raw.githubusercontent.com` 作为我们的静态资源存放地址,iOS/Android Container 通过访问 `dist/routes.json` 获取路由文件。当然你可以使用自己的CDN服务器来存放资源,并且可以根据需求定制自己的测试环境和线上环境。 +## ChangeLog + +- v0.2.0 + 1. rexxarFetch 对于 iOS WKWebView 亦将 POST 转化成 GET 来处理,需要配合 [rexxar-ios v0.3.0](https://github.com/douban/rexxar-ios#changelog) 及以上使用 + 2. 新增 widgetMessenger, assemblePayload, callbackListener, dispatch + 3. 废弃 getRexxarWidget + ## License Rexxar is released under the MIT license. See LICENSE for details.