diff --git a/packages/universal-app-runtime/package.json b/packages/universal-app-runtime/package.json new file mode 100644 index 0000000000..2d85bfeedb --- /dev/null +++ b/packages/universal-app-runtime/package.json @@ -0,0 +1,16 @@ +{ + "name": "universal-app-runtime", + "version": "0.1.1", + "description": "Provide framework level runtime support of universal app.", + "main": "lib/index.js", + "author": "Rax Team", + "dependencies": { + "history": "^4.9.0", + "querystring": "^0.2.0", + "rax-use-router": "^2.0.2", + "universal-env": "^1.0.1" + }, + "peerDependencies": { + "rax": "^1.0.8" + } +} diff --git a/packages/universal-app-runtime/src/app.js b/packages/universal-app-runtime/src/app.js new file mode 100644 index 0000000000..b8d56b2fa8 --- /dev/null +++ b/packages/universal-app-runtime/src/app.js @@ -0,0 +1,12 @@ +import invokeCycle from './invokeCycle'; + +export const appCycles = {}; + +export function useAppEffect(cycle, callback) { + const cycles = appCycles[cycle] = appCycles[cycle] || []; + cycles.push(callback); +} + +export function invokeAppCycle(cycle, ...args) { + invokeCycle(appCycles, cycle, ...args); +} diff --git a/packages/universal-app-runtime/src/index.js b/packages/universal-app-runtime/src/index.js new file mode 100644 index 0000000000..4a0ce0212d --- /dev/null +++ b/packages/universal-app-runtime/src/index.js @@ -0,0 +1,12 @@ +import { useAppEffect, invokeAppCycle as _invokeAppCycle } from './app'; +import { usePageEffect } from './page'; +import { useRouter, push, go, goBack, goForward, canGo, replace } from './router'; + +export { + // core app + useAppEffect, _invokeAppCycle, + // core page + usePageEffect, + // core router + useRouter, push, go, goBack, goForward, canGo, replace, +}; diff --git a/packages/universal-app-runtime/src/invokeCycle.js b/packages/universal-app-runtime/src/invokeCycle.js new file mode 100644 index 0000000000..e9c3f9e2d0 --- /dev/null +++ b/packages/universal-app-runtime/src/invokeCycle.js @@ -0,0 +1,16 @@ + +export default function invokeCycle(cycleMap, cycle, ...args) { + if (cycleMap.hasOwnProperty(cycle)) { + const cycles = cycleMap[cycle]; + let fn; + let error; + while (fn = cycles.shift()) { // eslint-disable-line + try { + fn(...args); + } catch (err) { + error = err; + } + } + if (error) throw error; + } +} diff --git a/packages/universal-app-runtime/src/page.js b/packages/universal-app-runtime/src/page.js new file mode 100644 index 0000000000..e2f36f48e8 --- /dev/null +++ b/packages/universal-app-runtime/src/page.js @@ -0,0 +1,52 @@ +import { useEffect } from 'rax'; +import { isWeb, isWeex } from 'universal-env'; + +const visibleListeners = { + show: [], + hide: [], +}; +let initialShow = false; +let prevVisibleState = true; + +export function usePageEffect(cycle, callback) { + switch (cycle) { + case 'show': + case 'hide': + useEffect(() => { + visibleListeners[cycle].push(callback); + return () => { + const index = visibleListeners[cycle].indexOf(callback); + visibleListeners[cycle].splice(index, 1); + }; + }); + } + + // Invoke first time show. + if (cycle === 'show') { + if (initialShow === false) { + initialShow = true; + useEffect(() => { + invokeCycle('show'); + }); + } + } +} + +function invokeCycle(cycle, ...args) { + for (let i = 0, l = visibleListeners[cycle].length; i < l; i++) { + visibleListeners[cycle][i](...args); + } +} + +if (isWeb) { + document.addEventListener('visibilitychange', function() { + const currentVisibleState = document.visibilityState === 'visible'; + if (prevVisibleState !== currentVisibleState) { + invokeCycle(currentVisibleState ? 'show' : 'hide'); + } + prevVisibleState = currentVisibleState; + }); +} else if (isWeex) { + // require('@weex/module') + // todo support weex. +} diff --git a/packages/universal-app-runtime/src/router.js b/packages/universal-app-runtime/src/router.js new file mode 100644 index 0000000000..37d1e9ed20 --- /dev/null +++ b/packages/universal-app-runtime/src/router.js @@ -0,0 +1,74 @@ +import { createElement } from 'rax'; +import * as RaxUseRouter from 'rax-use-router'; +import { createHashHistory } from 'history'; +import encodeQS from 'querystring/encode'; + +let _history = null; + +export function useRouter(routerConfig) { + const { history = createHashHistory(), routes } = routerConfig; + _history = history; + + function Router(props) { + const { component } = RaxUseRouter.useRouter(() => routerConfig); + + if (!component || Array.isArray(component) && component.length === 0) { + // Return null directly if not matched. + return null; + } else { + return createElement(component, props); + } + } + + return { Router }; +} + +export function Link(props) { + const { to, query, hash, state, type = 'span', onClick, ...others } = props; + const throughProps = Object.assign({}, others, { + onClick: (evt) => { + if (to) { + let url = to; + if (query) url += '?' + encodeQS(query); + if (hash) url += '#' + hash; + push(url, state); + } + onClick(evt); + } + }); + return createElement(type, throughProps); +} + +export function push(path, state) { + checkHistory(); + return _history.push(path, state); +} + +export function replace(path, state) { + checkHistory(); + return _history.replace(path, state); +} + +export function go(n) { + checkHistory(); + return _history.go(n); +} + +export function goBack() { + checkHistory(); + return _history.goBack(); +} + +export function goForward() { + checkHistory(); + return _history.goForward(); +} + +export function canGo(n) { + checkHistory(); + return _history.canGo(n); +} + +function checkHistory() { + if (_history === null) throw new Error('Router not initized properly, please call useRouter first.'); +}