diff --git a/packages/rax-plugin-app/src/config/web/getBase.js b/packages/rax-plugin-app/src/config/web/getBase.js
index 83fb45a8f1..f089825aa1 100644
--- a/packages/rax-plugin-app/src/config/web/getBase.js
+++ b/packages/rax-plugin-app/src/config/web/getBase.js
@@ -5,6 +5,7 @@ const babelMerge = require('babel-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UniversalDocumentPlugin = require('../../plugins/UniversalDocumentPlugin');
+const PWAAppShellPlugin = require('../../plugins/PWAAppShellPlugin');
const babelConfig = require('../babel.config');
const babelConfigWeb = babelMerge.all([{
@@ -85,6 +86,13 @@ module.exports = (rootDir) => {
render: serverRender.renderToString,
}]);
+ config.plugin('PWAAppShell')
+ .use(PWAAppShellPlugin, [{
+ rootDir,
+ path: 'src/shell/index.jsx',
+ render: serverRender.renderToString,
+ }]);
+
config.plugin('minicss')
.use(MiniCssExtractPlugin, [{
filename: '[name].css',
diff --git a/packages/rax-plugin-app/src/plugins/PWAAppShellPlugin.js b/packages/rax-plugin-app/src/plugins/PWAAppShellPlugin.js
new file mode 100644
index 0000000000..cd8877fdce
--- /dev/null
+++ b/packages/rax-plugin-app/src/plugins/PWAAppShellPlugin.js
@@ -0,0 +1,47 @@
+const path = require('path');
+const babel = require('@babel/core');
+const { RawSource } = require('webpack-sources');
+const { readFileSync, existsSync } = require('fs');
+
+const babelConfig = require('../config/babel.config.js');
+
+const NAME = 'PWAAppShellPlugin';
+
+module.exports = class PWAAppShellPlugin {
+ constructor(options) {
+ this.render = options.render;
+
+ this.rootDir = options.rootDir ? options.rootDir : process.cwd();
+ this.shellPath = options.path ? options.path : 'src/shell/index.jsx';
+ }
+
+ apply(compiler) {
+ const filename = path.resolve(this.rootDir, this.shellPath);
+ if (!existsSync(filename)) return;
+
+ // render to index html
+ compiler.hooks.emit.tapAsync(NAME, (compilation, callback) => {
+ const htmlValue = compilation.assets['index.html'].source();
+
+ // get shell code
+ const fileContent = readFileSync(filename, 'utf-8');
+ const { code } = babel.transformSync(fileContent, babelConfig);
+
+ // code export
+ const fn = new Function('module', 'exports', 'require', code);
+ fn({ exports: {} }, module.exports, require);
+ const shellElement = module.exports.__esModule ? module.exports.default : module.exports;
+
+ // get shell element string
+ const source = this.render(require('rax').createElement(shellElement, {}));
+
+ // pre-render app shell element to index.html
+ compilation.assets['index.html'] = new RawSource(htmlValue.replace(
+ /
(.*?)<\/div>/,
+ `
${source}
`
+ ));
+
+ callback();
+ });
+ }
+};
diff --git a/packages/universal-app-runtime/src/index.js b/packages/universal-app-runtime/src/index.js
index d8c616781e..9b8e499cfb 100644
--- a/packages/universal-app-runtime/src/index.js
+++ b/packages/universal-app-runtime/src/index.js
@@ -1,6 +1,7 @@
import { useAppEffect, invokeAppCycle as _invokeAppCycle } from './app';
import { usePageEffect } from './page';
-import { useRouter, Link, push, go, goBack, goForward, canGo, replace } from './router';
+
+import { useRouter, Link, push, go, goBack, goForward, canGo, replace, preload, prerender } from './router';
export {
// core app
@@ -8,5 +9,5 @@ export {
// core page
usePageEffect,
// core router
- useRouter, push, go, goBack, goForward, canGo, replace, Link,
+ useRouter, push, go, goBack, goForward, canGo, replace, Link, preload, prerender
};
diff --git a/packages/universal-app-runtime/src/router.js b/packages/universal-app-runtime/src/router.js
index 02f729a28b..264d4e2823 100644
--- a/packages/universal-app-runtime/src/router.js
+++ b/packages/universal-app-runtime/src/router.js
@@ -1,16 +1,20 @@
import { createElement } from 'rax';
import * as RaxUseRouter from 'rax-use-router';
+import { isWeb } from 'universal-env';
import { createHashHistory } from 'history';
import encodeQS from 'querystring/encode';
+
let _history = null;
+let _routerConfig = {};
export function useRouter(routerConfig) {
- const { history = createHashHistory(), routes } = routerConfig;
+ _routerConfig = routerConfig;
+ const { history = createHashHistory(), routes } = _routerConfig;
_history = history;
function Router(props) {
- const { component } = RaxUseRouter.useRouter(() => routerConfig);
+ const { component } = RaxUseRouter.useRouter(() => _routerConfig);
if (!component || Array.isArray(component) && component.length === 0) {
// Return null directly if not matched.
@@ -69,6 +73,46 @@ export function canGo(n) {
return _history.canGo(n);
}
+/**
+ * Preload WebApp's page resource.
+ * @param config {Object}
+ * eg:
+ * 1. preload({pageIndex: 0}) // preload dynamic import page bundle
+ * 2. preload({href: '//xxx.com/font.woff', as: 'font', crossorigin: true}); // W3C preload
+ */
+export function preload(config) {
+ if (!isWeb) return;
+ if (config.pageIndex !== undefined) {
+ _routerConfig.routes[config.pageIndex].component();
+ } else {
+ const linkElement = document.createElement('link');
+ linkElement.rel = 'preload';
+ linkElement.as = config.as;
+ linkElement.href = config.href;
+ config.crossorigin && (linkElement.crossorigin = true);
+ document.head.appendChild(linkElement);
+ }
+}
+
+/**
+ * Rrerender WebApp's page content.
+ * @param config {Object}
+ * eg:
+ * 1. prerender({pageIndex: 0}) // preload dynamic import page bundle for now(todo page alive)
+ * 2. prerender({href:'https://m.taobao.com'}); // W3C prerender
+ */
+export function prerender(config) {
+ if (!isWeb) return;
+ if (config.pageIndex !== undefined) {
+ _routerConfig.routes[config.pageIndex].component();
+ } else {
+ const linkElement = document.createElement('link');
+ linkElement.rel = 'prerender';
+ linkElement.href = config.href;
+ document.head.appendChild(linkElement);
+ }
+}
+
function checkHistory() {
if (_history === null) throw new Error('Router not initized properly, please call useRouter first.');
}
diff --git a/packages/universal-app-shell-loader/src/index.js b/packages/universal-app-shell-loader/src/index.js
index 9c8d077fbb..59db0e8c5e 100644
--- a/packages/universal-app-shell-loader/src/index.js
+++ b/packages/universal-app-shell-loader/src/index.js
@@ -1,5 +1,6 @@
const { join } = require('path');
const { getOptions } = require('loader-utils');
+const { existsSync } = require('fs');
const historyMemory = {
hash: 'createHashHistory',
@@ -21,11 +22,15 @@ module.exports = function(content) {
routes = [];
}
+ let appRender = '';
let fixRootStyle = '';
/**
* Weex only support memory history.
*/
- if (options.type === 'weex') historyType = 'memory';
+ if (options.type === 'weex') {
+ historyType = 'memory';
+ appRender = 'render(createElement(Entry), null, { driver: DriverUniversal });';
+ }
/**
* Web only compatible with 750rpx.
*/
@@ -35,6 +40,12 @@ module.exports = function(content) {
const html = document.documentElement;
html.style.fontSize = html.clientWidth / 750 * ${mutiple} + 'px';
`;
+ appRender = 'render(createElement(Entry), document.getElementById("root"), { driver: DriverUniversal });';
+ // app shell
+ if (existsSync(join(this.rootContext, 'src/shell/index.jsx'))) {
+ appRender = `import Shell from "${getDepPath('shell/index', this.rootContext)}";`;
+ appRender += 'render(createElement(Shell, {}, createElement(Entry)), document.getElementById("root"), { driver: DriverUniversal, hydrate: true });';
+ }
}
/**
@@ -49,10 +60,13 @@ module.exports = function(content) {
const assembleRoutes = routes.map((route, index) => {
// First level function to support hooks will autorun function type state,
// Second level function to support rax-use-router rule autorun function type component.
+ const dynamicImportComponent = `() => import(/* webpackChunkName: "${route.component.replace(/\//g, '_')}" */ '${getDepPath(route.component, this.rootContext)}').then((mod) => () => interopRequire(mod))`;
+ const importComponent = `() => () => interopRequire(require('${getDepPath(route.component, this.rootContext)}'))`;
+
return `routes.push({
index: ${index},
path: '${route.path}',
- component: () => () => interopRequire(require('${getDepPath(route.component, this.rootContext)}')),
+ component: ${options.type === 'web' ? dynamicImportComponent : importComponent}
});`;
}).join('\n');
@@ -95,7 +109,7 @@ module.exports = function(content) {
return app;
}
- render(createElement(Entry), null, { driver: DriverUniversal });
+ ${appRender}
`;
return source;
} else {