diff --git a/src/main/java/com/terwergreen/next/utils/NashornUtil.java b/src/main/java/com/terwergreen/next/utils/NashornUtil.java new file mode 100644 index 0000000..24ed395 --- /dev/null +++ b/src/main/java/com/terwergreen/next/utils/NashornUtil.java @@ -0,0 +1,98 @@ +package com.terwergreen.next.utils; + +import jdk.nashorn.api.scripting.NashornScriptEngine; +import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import jdk.nashorn.api.scripting.ScriptObjectMirror; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.ScriptContext; +import javax.script.ScriptException; +import javax.script.SimpleScriptContext; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Nashorn工具类 + * + * @author Terwer + * @version 1.0 + * 2019/1/20 22:35 + **/ +public class NashornUtil { + private static final Logger logger = LoggerFactory.getLogger(NashornUtil.class); + private static NashornUtil nashornUtil; + private final NashornScriptEngine engine; + private static ScriptContext sc = new SimpleScriptContext(); + private static ScheduledExecutorService globalScheduledThreadPool = Executors.newScheduledThreadPool(20); + + /** + * Vue资源文件目录 + */ + private static final String LIB_DIR = "static/lib"; + private static final String POLYFILL_FILE_NAME = "nashorn-polyfill.js"; + + public static synchronized NashornUtil getInstance() { + if (nashornUtil == null) { + long start = System.currentTimeMillis(); + nashornUtil = new NashornUtil(); + long end = System.currentTimeMillis(); + logger.info("init nashornHelper cost time {}ms", (end - start)); + } + + return nashornUtil; + } + + private NashornUtil() { + // 获取Javascript引擎 + NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); + engine = (NashornScriptEngine) factory.getScriptEngine(new String[]{"--language=es6"}); + sc.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE); + sc.setAttribute("__IS_SSR__", true, ScriptContext.ENGINE_SCOPE); + sc.setAttribute("__NASHORN_POLYFILL_TIMER__", globalScheduledThreadPool, ScriptContext.ENGINE_SCOPE); + engine.setBindings(sc.getBindings(ScriptContext.ENGINE_SCOPE), ScriptContext.ENGINE_SCOPE); + + try { + // 编译nashorn-polyfill + engine.eval(read(LIB_DIR + File.separator + POLYFILL_FILE_NAME)); +// for (String fileName : NashornUtil.VENDOR_FILE_NAME) { +// engine.eval(read(SRC_DIR + File.separator + fileName)); +// } +// engine.eval(read(SRC_DIR + File.separator + "app.js")); + // 编译server + engine.eval(VueUtil.readVueFile("server.js")); + logger.info("Vue app.js编译成功,编译引擎为Nashorn"); + } catch (ScriptException e) { + logger.error("Nashorn引擎Javascript解析错误", e); + } + } + + public NashornScriptEngine getNashornScriptEngine() { + return engine; + } + + public ScriptObjectMirror getGlobalGlobalMirrorObject(String objectName) { + return (ScriptObjectMirror) engine.getBindings(ScriptContext.ENGINE_SCOPE).get(objectName); + } + + public Object callRender(String methodName, Object... input) { + try { + return engine.invokeFunction(methodName, input); + } catch (ScriptException e) { + logger.error("run javascript failed.", e); + return null; + } catch (NoSuchMethodException e) { + logger.error("no such method.", e); + return null; + } + } + + private Reader read(String path) { + InputStream in = getClass().getClassLoader().getResourceAsStream(path); + return new InputStreamReader(in); + } +} diff --git a/src/main/java/com/terwergreen/next/vue/VueRenderer.java b/src/main/java/com/terwergreen/next/vue/VueRenderer.java index 38d4137..6d81387 100644 --- a/src/main/java/com/terwergreen/next/vue/VueRenderer.java +++ b/src/main/java/com/terwergreen/next/vue/VueRenderer.java @@ -1,50 +1,60 @@ package com.terwergreen.next.vue; -import com.terwergreen.next.utils.VueUtil; -import jdk.nashorn.api.scripting.NashornScriptEngine; -import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import com.terwergreen.next.utils.NashornUtil; +import jdk.nashorn.api.scripting.ScriptObjectMirror; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import javax.script.Bindings; -import javax.script.CompiledScript; -import javax.script.ScriptContext; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; -import javax.script.SimpleScriptContext; +import java.util.function.Consumer; +/** + * 渲染Vue + */ public class VueRenderer { private final Log logger = LogFactory.getLog(this.getClass()); - private Object renderServerFunction; + private NashornUtil engine; - public VueRenderer() { - try { - // 获取Javascript引擎 - NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); - NashornScriptEngine engine = (NashornScriptEngine) factory.getScriptEngine(new String[]{"--language=es6"}); - // 编译 - CompiledScript compiled = engine.compile(VueUtil.readVueFile("server-bundle.js")); - this.renderServerFunction = compiled.eval(); - logger.info("Vue app.js编译成功,编译引擎为Nashorn"); - } catch (ScriptException e) { - logger.error("Nashorn引擎Javascript解析错误", e); - throw new RuntimeException(e); + private final Object promiseLock = new Object(); + private volatile boolean promiseResolved = false; + private String html = null; + + private Consumer fnResolve = object -> { + synchronized (promiseLock) { + html = (String) object; + promiseResolved = true; } + }; + + public VueRenderer() { + // 获取Javascript引擎 + engine = NashornUtil.getInstance(); } public String renderContent() { - NashornScriptEngine engine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn"); try { - ScriptContext newContext = new SimpleScriptContext(); - newContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE); - Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE); - engineScope.put("renderServer", this.renderServerFunction); - engine.setContext(newContext); - Object html = engine.invokeFunction("renderServer"); - return String.valueOf(html); + ScriptObjectMirror promise = (ScriptObjectMirror) engine.callRender("renderServer"); + promise.callMember("then", fnResolve); + ScriptObjectMirror nashornEventLoop = engine.getGlobalGlobalMirrorObject("nashornEventLoop"); + // 执行nashornEventLoops.process()使主线程执行回调函数 + nashornEventLoop.callMember("process"); + int i = 0; + int jsWaitTimeout = 1000 * 60; + int interval = 200; // 等待时间间隔 + int totalWaitTime = 0; // 实际等待时间 + while (!promiseResolved && totalWaitTime < jsWaitTimeout) { + nashornEventLoop.callMember("process"); + try { + Thread.sleep(interval); + } catch (InterruptedException e) { + logger.error("Thread error:", e); + } + totalWaitTime = totalWaitTime + interval; + if (interval < 500) interval = interval * 2; + i = i + 1; + } + return html; } catch (Exception e) { throw new IllegalStateException("failed to render vue component", e); } } - } diff --git a/src/main/resources/static/lib/nashorn-polyfill.js b/src/main/resources/static/lib/nashorn-polyfill.js new file mode 100644 index 0000000..38d1d39 --- /dev/null +++ b/src/main/resources/static/lib/nashorn-polyfill.js @@ -0,0 +1,199 @@ +var self = this; +// 模拟global +var global = this; + +// 模拟process +var process = { + env: { + VUE_ENV: "server", + NODE_ENV: "production" + }, + nextTick: function (fn) { + global.setTimeout(fn, 0) + } +}; +global.process = process; + +// 模拟console +var console = {}; +console.debug = print; +console.warn = print; +console.log = print; +console.error = print; +console.trace = print; +console.assert = print; +global.console = console; + +Object.assign = function (t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; +}; + +/* + Source is originated from https://github.com/morungos/java-xmlhttprequest + Articles about Nashorn: + - https://blog.codecentric.de/en/2014/06/project-nashorn-javascript-jvm-polyglott/ + How it work: + in https://github.com/morungos/java-xmlhttprequest, it uses Timer to run setTimeout and setInterval task, + but they are run in a separate thread of the Timer creates that is different with the main JavaScript thread. + This implementation uses ScheduledExecutorService instead of Timer so the threads for task scheduling can be + reused instead of each JavasScript thread create a Timer thread when using Timer. + And most important thing is this adds global.nashornEventLoop and scheduled tasks only add function callback + object in eventLoop (ArrayQueue), and it is main JavaScript thread to run these function callback by calling + `global.nashornEventLoop.process();` at the end of JavaScript Application. It is just like browser or NodeJS + that event loop is called when the main stack is cleared. + When runs on server with Promise, remember to call `nashornEventLoop.process()` when waiting for Promise by + Thread.sleep(), and call `nashornEventLoop.reset()` if server thread (e.g. Servlet thread) decides to be + timeout so that eventLoop will be clean for next request. + */ +(function nashornEventLoopMain(context) { + 'use strict'; + + var Thread = Java.type('java.lang.Thread'); + var Phaser = Java.type('java.util.concurrent.Phaser'); + var ArrayDeque = Java.type('java.util.ArrayDeque'); + var HashMap = Java.type('java.util.HashMap'); + var TimeUnit = Java.type("java.util.concurrent.TimeUnit"); + var Runnable = Java.type('java.lang.Runnable'); + + + var globalTimerId; + var timerMap; + var eventLoop; + var phaser = new Phaser(); + + // __NASHORN_POLYFILL_TIMER__ type is ScheduledExecutorService + var scheduler = context.__NASHORN_POLYFILL_TIMER__; + + resetEventLoop(); + + console.log('main javasript thread ' + Thread.currentThread().getName()); + + function resetEventLoop() { + globalTimerId = 1; + if (timerMap) { + timerMap.forEach(function (key, value) { + value.cancel(true); + }) + } + timerMap = new HashMap(); + eventLoop = new ArrayDeque(); + } + + function waitForMessages() { + phaser.register(); + var wait = !(eventLoop.size() === 0); + phaser.arriveAndDeregister(); + + return wait; + } + + function processNextMessages() { + var remaining = 1; + while (remaining) { + // console.log('eventLoop size ' + eventLoop.size() + 'in thread ' + Thread.currentThread().getName()); + phaser.register(); + var message = eventLoop.removeFirst(); + remaining = eventLoop.size(); + phaser.arriveAndDeregister(); + + var fn = message.fn; + var args = message.args; + + try { + // console.log('processNextMessages in thread ' + Thread.currentThread().getName()); + fn.apply(context, args); + } catch (e) { + console.trace(e); + console.trace(fn); + console.trace(args); + } + } + } + + context.nashornEventLoop = { + process: function () { + console.log('nashornEventLoop.process is called in thread ' + Thread.currentThread().getName()) + while (waitForMessages()) { + processNextMessages() + } + }, + reset: resetEventLoop + }; + + function createRunnable(fn, timerId, args, repeated) { + var Runner = Java.extend(Runnable, { + run: function () { + try { + var phase = phaser.register(); + eventLoop.addLast({ + fn: fn, + args: args + }); + console.log('TimerTask add one event, and eventLoop size is:' + eventLoop.size() + ' in thread ' + Thread.currentThread().getName()); + } catch (e) { + console.trace(e); + } finally { + if (!repeated) timerMap.remove(timerId); + phaser.arriveAndDeregister(); + } + } + }); + return new Runner(); + } + + var setTimeout = function (fn, millis /* [, args...] */) { + var args = [].slice.call(arguments, 2, arguments.length); + + var timerId = globalTimerId++; + var runnable = createRunnable(fn, timerId, args, false); + + var task = scheduler.schedule(runnable, millis, TimeUnit.MILLISECONDS); + timerMap.put(timerId, task); + + return timerId; + }; + + var setImmediate = function (fn /* [, args...] */) { + var args = [].slice.call(arguments, 1, arguments.length); + return setTimeout(fn, 0, args); + } + + var clearImmediate = function (timerId) { + clearTimeout(timerId); + } + + var clearTimeout = function (timerId) { + var task = timerMap.get(timerId); + if (task) { + task.cancel(true); + timerMap.remove(timerId); + } + }; + + var setInterval = function (fn, delay /* [, args...] */) { + var args = [].slice.call(arguments, 2, arguments.length); + + var timerId = globalTimerId++; + var runnable = createRunnable(fn, timerId, args, true); + var task = scheduler.scheduleWithFixedDelay(runnable, delay, delay, TimeUnit.MILLISECONDS); + timerMap.put(timerId, task); + + return timerId; + }; + + var clearInterval = function (timerId) { + clearTimeout(timerId); + }; + + context.setTimeout = setTimeout; + context.clearTimeout = clearTimeout; + context.setImmediate = setImmediate; + context.clearImmediate = clearImmediate; + context.setInterval = setInterval; + context.clearInterval = clearInterval; +})(typeof global !== "undefined" && global || typeof self !== "undefined" && self || this); \ No newline at end of file diff --git a/src/main/webapp/App.vue b/src/main/webapp/App.vue index 8580313..ac9ef89 100644 --- a/src/main/webapp/App.vue +++ b/src/main/webapp/App.vue @@ -9,7 +9,9 @@ + + \ No newline at end of file diff --git a/src/main/webapp/src/components/themes/default/Index.vue b/src/main/webapp/src/components/themes/default/Index.vue index 357c475..2052b38 100644 --- a/src/main/webapp/src/components/themes/default/Index.vue +++ b/src/main/webapp/src/components/themes/default/Index.vue @@ -1,12 +1,12 @@ \ No newline at end of file diff --git a/src/main/webapp/src/components/themes/default/images/logo.png b/src/main/webapp/src/components/themes/default/images/logo.png new file mode 100644 index 0000000..8e3f2eb Binary files /dev/null and b/src/main/webapp/src/components/themes/default/images/logo.png differ diff --git a/src/main/webapp/src/index.html b/src/main/webapp/src/index.html index b64b3d5..3a098ea 100644 --- a/src/main/webapp/src/index.html +++ b/src/main/webapp/src/index.html @@ -1,6 +1,21 @@ - + + - Next Vue Project + + + + + + Next Vue Project In Client + +
diff --git a/src/main/webapp/src/main.js b/src/main/webapp/src/main.js index ddbe1b3..621dca9 100644 --- a/src/main/webapp/src/main.js +++ b/src/main/webapp/src/main.js @@ -1,7 +1,13 @@ -import Vue from 'vue'; +import Vue from 'vue' +// import BootstrapVue from 'bootstrap-vue' import App from '../App.vue' -new Vue({ - el: '#app', - render: h => h(App) -}); \ No newline at end of file +// 生产部署时候需要设置为false +Vue.config.productionTip = true + +// Vue.use(BootstrapVue); + +const vm = new Vue({ + render: h => h(App), +}) +vm.$mount('#app') \ No newline at end of file diff --git a/src/main/webapp/ssr/client.js b/src/main/webapp/ssr/client.js index ffefc3c..cd7371e 100644 --- a/src/main/webapp/ssr/client.js +++ b/src/main/webapp/ssr/client.js @@ -1,11 +1,13 @@ import "@babel/polyfill" - import Vue from 'vue' +// import BootstrapVue from 'bootstrap-vue' import App from '../App.vue' // 生产部署时候需要设置为false Vue.config.productionTip = false +// Vue.use(BootstrapVue); + global.renderClient = () => { const vm = new Vue({ render: h => h(App), diff --git a/src/main/webapp/ssr/event-loop.js b/src/main/webapp/ssr/event-loop.js deleted file mode 100644 index e36e6c3..0000000 --- a/src/main/webapp/ssr/event-loop.js +++ /dev/null @@ -1,205 +0,0 @@ -// name: nashorn-async -// license: MIT (http://opensource.org/licenses/MIT) -// authors: Nico Rehwaldt -// homepage: https://github.com/nikku/nashorn-async#readme -// version: 4.0.0 -// import src/main/resources/js/event-loop.js -/** - * Execution support for simple hybrid (async/sync) script execution in nashorn. - * - * Extend your nashorn script engine with async support by evaluating this script prior to the actual script. - * Then, wrap the actual script you would like to execute in a `exec(fn, timeout)` block with `fn` containing - * it. - * - * Scripts may call {@link #async} to retrieve a callback (err, result) that needs to be - * triggered before the overall script execution terminates. - * - * Warning: If {@link #async} is invoked multiple times, all callbacks - * need to be fulfilled but only the last one will decide upon the actual execution result. - * - * The function will always return synchronously and indicate script errors by throwing them. - * - * @example async - * - * exec(function() { - * var done = async(); - * - * setTimeout(function() { - * done(null, 'YES IT WORKED'); - * }, 200); - * }); - * - * @example sync - * - * exec(function() { - * throw new Error('FAILED'); - * }); - * - * - * @author Nico Rehwaldt - */ - -(function(context) { - - var Timer = Java.type('java.util.Timer'); - var TimerTask = Java.type('java.util.TimerTask'); - var Phaser = Java.type('java.util.concurrent.Phaser'); - var TimeUnit = Java.type('java.util.concurrent.TimeUnit'); - - var System = Java.type('java.lang.System'); - - var timer = new Timer('jsEventLoop', false); - - /** - * Our global synchronization barrier that we use to register async operations - * and that awaits for all of these operations to finish. - */ - var phaser = new Phaser(); - - - /** global execution results */ - var results; - - function eventLoopError(e) { - System.out.print('Exception in jsEventLoop '); - e.printStackTrace(); - } - - function async() { - phaser.register(); - - return function(err, result) { - results = { err: err, result: result }; - console.log(result); - phaser.arriveAndDeregister(); - }; - }; - - - /** - * Executes the given function after the specified timeout. - * - * @param {Function} fn - * @param {Number} [millis] - * - * @return {Object} timeout handle - */ - function setTimeout(fn, millis) { - phaser.register(); - - var task = new TimerTask({ - run: function() { - try { - fn(); - } catch(e) { - eventLoopError(e); - } finally { - phaser.arriveAndDeregister(); - } - } - }); - - timer.schedule(task, millis || 0); - - return task; - } - - - /** - * Clear timeout previously created via {@link #setTimeout}. - * - * @param {Object} task timeout handle - */ - function clearTimeout(task) { - if (task.cancel()) { - phaser.arriveAndDeregister(); - } - } - - /** - * Executes the given function with a fixed time delay. - * - * @param {Function} fn - * @param {Number} [millis] - * - * @return {Object} timeout handle - */ - function setInterval(fn, millis) { - phaser.register(); - - var task = new TimerTask({ - run: function() { - try { - fn(); - } catch (e) { - eventLoopError(e); - } - } - }); - - timer.scheduleAtFixedRate(task, millis, millis); - - return task; - } - - - /** - * Clear interval previously created via {@link #setInterval}. - * - * @param {Object} interval handle - */ - function clearInterval(task) { - if (task.cancel()) { - phaser.arriveAndDeregister(); - } - } - - /** - * Execute a function (synchronously or asynchronously) and return the result - * or throw the error that is returned by it. - * - * @param {Function} fn - * @param {Number} [timeout=5000] - */ - function exec(fn, timeout) { - timeout = timeout || 5000; - - phaser.register(); - - setTimeout(function() { - try { - results = { err: null, result: fn() }; - } catch (e) { - results = { err: e }; - } - }, 0); - - try { - if (timeout < 0) { - phaser.awaitAdvanceInterruptibly(phaser.arrive()); - } else { - phaser.awaitAdvanceInterruptibly(phaser.arrive(), timeout, TimeUnit.MILLISECONDS); - } - } catch (e) { - results = { err: new Error('execution timeout') }; - } finally { - timer.cancel(); - } - - if (results.err) { - throw results.err; - } else { - return results.result; - } - } - - context.exec = exec; - - context.async = async; - - context.setTimeout = setTimeout; - context.clearTimeout = clearTimeout; - context.setInterval = setInterval; - context.clearInterval = clearInterval; - -})(this); diff --git a/src/main/webapp/ssr/nashorn-polyfill.js b/src/main/webapp/ssr/nashorn-polyfill.js deleted file mode 100644 index fbf2e7b..0000000 --- a/src/main/webapp/ssr/nashorn-polyfill.js +++ /dev/null @@ -1,16 +0,0 @@ -var global = this; - -var console = {}; -console.debug = print; -console.error = print; -console.assert = print; -console.warn = print; -console.log = print; -global.console = console; - -var process = {}; -process.env = {}; -process.nextTick = function(fn) { - global.setTimeout(fn, 0); -}; -global.process = process; diff --git a/src/main/webapp/ssr/return-render-function.js b/src/main/webapp/ssr/return-render-function.js deleted file mode 100644 index d69909e..0000000 --- a/src/main/webapp/ssr/return-render-function.js +++ /dev/null @@ -1 +0,0 @@ -global.renderServer; diff --git a/src/main/webapp/ssr/server.js b/src/main/webapp/ssr/server.js index 645d4ef..7689ab8 100644 --- a/src/main/webapp/ssr/server.js +++ b/src/main/webapp/ssr/server.js @@ -1,27 +1,21 @@ import "@babel/polyfill" import {createRenderer} from 'vue-server-renderer' -import awaitServer from './utils/awaitServer' - const {renderToString} = createRenderer() import Vue from 'vue' import App from '../App.vue' +// import BootstrapVue from 'bootstrap-vue' // 生产部署时候需要设置为false Vue.config.productionTip = false -global.renderServer = (comments) => { - var results = awaitServer((done) => { - const vm = new Vue({ - render: h => h(App) - }); - renderToString(vm, (err, res) => { - done(err, res); - }); +// Vue.use(BootstrapVue); + +global.renderServer = () => { + const vm = new Vue({ + render: h => h(App) }); - if (results.error) { - throw results.error; - } else { - return results.result; - } -}; + var promise = renderToString(vm); + console.log("Vue server render promise:" + promise) + return promise; +}; \ No newline at end of file diff --git a/src/main/webapp/ssr/utils/awaitServer.js b/src/main/webapp/ssr/utils/awaitServer.js deleted file mode 100644 index 0250d3a..0000000 --- a/src/main/webapp/ssr/utils/awaitServer.js +++ /dev/null @@ -1,25 +0,0 @@ -const awaitServer = (fn) => { - var Phaser = Java.type('java.util.concurrent.Phaser'); - var phaser = new Phaser(); - var results = {error: null, result: ''}; - phaser.register(); - - const done = (err, res) => { - if (err) { - results.error = err; - } else { - results.result = res; - } - phaser.arriveAndDeregister(); - }; - - phaser.register(); - setTimeout(() => { - fn(done); - }, 0); - - phaser.awaitAdvanceInterruptibly(phaser.arrive()); - return results; -}; - -export default awaitServer; diff --git a/src/main/webapp/webpack.config.js b/src/main/webapp/webpack.config.js index 8d54677..344522c 100644 --- a/src/main/webapp/webpack.config.js +++ b/src/main/webapp/webpack.config.js @@ -12,10 +12,10 @@ module.exports = (env, argv) => { const renderMode = argv.renderMode ? argv.renderMode : 'client' console.log("renderMode:" + renderMode) const buildPath = renderMode === 'client' - ? path.resolve('dist') : + ? path.resolve(__dirname, 'dist') : (renderMode === 'server:client' - ? path.resolve('ssrclientdist') - : path.resolve('ssrdist') + ? path.resolve(__dirname, 'ssrclientdist') + : path.resolve(__dirname, 'ssrdist') ) console.log("buildPath:" + buildPath) const entryFile = renderMode === 'client' @@ -61,6 +61,12 @@ module.exports = (env, argv) => { : MiniCssExtractPlugin.loader, 'css-loader' ] + }, + { + test: /\.(png|svg|jpg|gif)$/, + use: [ + 'file-loader' + ] } ] },