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 {