Java使用Nashorn,調用Promise實現服務端渲染

Nashorn JavaScript引擎是Java SE 8 的一部分,而且和其它獨立的引擎例如 Google V8(用於Google Chrome和Node.js的引擎)互相競爭。Nashorn經過在JVM上,以原生方式運行動態的JavaScript代碼來擴展Java的功能。javascript

使用Nashorn

Java代碼中簡單的HelloWorld以下所示:html

ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval("print('Hello World!');");

爲了在Java中執行JavaScript,你首先要經過javax.script包建立腳本引擎。
JavaScript代碼既能夠經過傳遞JavaScript代碼字符串,也能夠傳遞指向你的JS腳本文件的FileReader來執行:前端

ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("script.js"));

Nashorn JavaScript基於 ECMAScript 5.1,可是它的後續版本會對ES6提供支持:java

Nashorn的當前策略遵循ECMAScript規範。當咱們在JDK8中發佈它時,它將基於ECMAScript 5.1。Nashorn將來的主要發佈基於ECMAScript 6。

在Java中調用JavaScript函數

Nashorn 支持從Java代碼中直接調用定義在腳本文件中的JavaScript函數。你能夠將Java對象傳遞爲函數參數,而且從函數返回數據來調用Java方法。react

var fun1 = function(name) {
    print('Hi there from Javascript, ' + name);
    return "greetings from javascript";
};

var fun2 = function (object) {
    print("JS Class Definition: " + Object.prototype.toString.call(object));
};

ScriptEngine內置了invokeFunction方法,提調用javascript函數並返回結果。webpack

ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("script.js"));

Object result = invocable.invokeFunction("fun1", "Peter Parker");
System.out.println(result);
System.out.println(result.getClass());

// Hi there from Javascript, Peter Parker
// greetings from javascript
// class java.lang.String

在JavaScript中調用Java方法

在JavaScript中調用Java方法十分容易。咱們首先須要定義一個Java靜態方法。git

static String fun1(String name) {
    System.out.format("Hi there from Java, %s", name);
    return "greetings from java";
}

Java類能夠經過Java.typeAPI擴展在JavaScript中引用。它就和Java代碼中的import相似。只要定義了Java類型,咱們就能夠天然地調用靜態方法fun1(),而後像sout打印信息。因爲方法是靜態的,咱們不須要首先建立實例。es6

var MyJavaClass = Java.type('my.package.MyJavaClass');

var result = MyJavaClass.fun1('John Doe');
print(result);

// Hi there from Java, John Doe
// greetings from java

在使用JavaScript原生類型調用Java方法時,Nashorn 如何處理類型轉換?讓咱們經過簡單的例子來弄清楚。
下面的Java方法簡單打印了方法參數的實際類型:github

static void fun2(Object object) {
    System.out.println(object.getClass());
}

爲了理解背後如何處理類型轉換,咱們使用不一樣的JavaScript類型來調用這個方法:web

MyJavaClass.fun2(123);
// class java.lang.Integer

MyJavaClass.fun2(49.99);
// class java.lang.Double

MyJavaClass.fun2(true);
// class java.lang.Boolean

MyJavaClass.fun2("hi there")
// class java.lang.String

MyJavaClass.fun2(new Number(23));
// class jdk.nashorn.internal.objects.NativeNumber

MyJavaClass.fun2(new Date());
// class jdk.nashorn.internal.objects.NativeDate

MyJavaClass.fun2(new RegExp());
// class jdk.nashorn.internal.objects.NativeRegExp

MyJavaClass.fun2({foo: 'bar'});
// class jdk.nashorn.internal.scripts.JO4

JavaScript原始類型轉換爲合適的Java包裝類,而JavaScript原生對象會使用內部的適配器類來表示。要記住jdk.nashorn.internal中的類可能會有所變化,因此不該該在客戶端面向這些類來編程。

ScriptObjectMirror

在向Java傳遞原生JavaScript對象時,你可使用ScriptObjectMirror類,它其實是底層JavaScript對象的Java表示。ScriptObjectMirror實現了Map接口,位於jdk.nashorn.api中。這個包中的類能夠用於客戶端代碼。

下面的例子將參數類型從Object改成ScriptObjectMirror,因此咱們能夠從傳入的JavaScript對象中得到一些信息。

static void fun3(ScriptObjectMirror mirror) {
    System.out.println(mirror.getClassName() + ": " +
        Arrays.toString(mirror.getOwnKeys(true)));
}

當向這個方法傳遞對象(哈希表)時,在Java端能夠訪問其屬性:

MyJavaClass.fun3({
    foo: 'bar',
    bar: 'foo'
});

// Object: [foo, bar]

咱們也能夠在Java中調用JavaScript的成員函數。讓咱們首先定義JavaScript Person類型,帶有屬性firstNamelastName,以及方法getFullName

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.getFullName = function() {
        return this.firstName + " " + this.lastName;
    }
}

JavaScript方法getFullName能夠經過callMember()ScriptObjectMirror上調用。

static void fun4(ScriptObjectMirror person) {
    System.out.println("Full Name is: " + person.callMember("getFullName"));
}

當向Java方法傳遞新的Person時,咱們會在控制檯看到預期的結果:

var person1 = new Person("Peter", "Parker");
MyJavaClass.fun4(person1);

// Full Name is: Peter Parker

實戰

經過使用promise實現服務端渲染。

polyfill

因爲當前Nashorn基於es5,不支持部分es6對象,咱們須要引入polyfill文件,nashorn-polyfill
該polyfill經過java與JavaScript的結合使用,使Nashorn支持console, process, Blob, Promise等對象和setTimeout, clearTimeout, setInterval, clearInterval等函數。

Nashorn工具類

實例化工具類,經過該類操做Javascript對象。

public class NashornHelper {

    /**
     * 用於本類的日誌
     */
    private static final Logger       logger                       = LoggerFactory.getLogger(NashornHelper.class);

    private static final String       JAVASCRIPT_DIR               = "static"; // js文件目錄

    private static final String       LIB_DIR                      = JAVASCRIPT_DIR + File.separator + "lib";

    private static final String[]     VENDOR_FILE_NAME             = {"vendor.js"}; // webpack打包的三方庫,如react,lodash

    private static final String       SRC_DIR                      = JAVASCRIPT_DIR + File.separator + "src"; // 文件目錄

    private static final String       POLYFILL_FILE_NAME           = "nashorn-polyfill.js";

    private final NashornScriptEngine engine;

    private static NashornHelper      nashornHelper;

    private static ScriptContext            sc                        = new SimpleScriptContext();

    private static ScheduledExecutorService globalScheduledThreadPool = Executors.newScheduledThreadPool(20);

    // 單例模式
    public static synchronized NashornHelper getInstance() {
        if (nashornHelper == null) {
            long start = System.currentTimeMillis();
            nashornHelper = new NashornHelper();
            long end = System.currentTimeMillis();
            logger.error("init nashornHelper cost time {}ms", (end - start));
        }
        return nashornHelper;
    }

    private NashornHelper(){
        long start = System.currentTimeMillis();
        engine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn");
        Bindings bindings = new SimpleBindings();
        bindings.put("logger", logger); // 向nashorn引擎注入logger對象
        sc.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
        sc.getBindings(ScriptContext.ENGINE_SCOPE).putAll(bindings);
        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);

        long end = System.currentTimeMillis();
        logger.info("init nashornHelper cost time {}ms", (end - start));

        try {  // 執行js文件
            engine.eval(read(LIB_DIR + File.separator + POLYFILL_FILE_NAME));
            for (String fileName : NashornHelper.VENDOR_FILE_NAME) {
                engine.eval(read(SRC_DIR + File.separator + fileName));
            }
            engine.eval(read(SRC_DIR + File.separator + "app.js"));
        } catch (ScriptException e) {
            logger.error("load javascript failed.", e);
        }
    }
    // 獲取Nashorn做用域下的對象
    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);
    }

實例化工具類

NashornHelper engine = NashornHelper.getInstance();

執行調用javasript,java中獲取promise對象

ScriptObjectMirror promise = (ScriptObjectMirror) engine.callRender("ssr_render");

執行promisethen方法,等待執行完成並回調

promise.callMember("then", fnResolve);
    ScriptObjectMirror nashornEventLoop = engine.getGlobalGlobalMirrorObject("nashornEventLoop");

    nashornEventLoop.callMember("process"); // 執行nashornEventLoops.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) {
        }
        totalWaitTime = totalWaitTime + interval;
        if (interval < 500) interval = interval * 2;
        i = i + 1;
    }

回調函數

private Consumer<Object> fnResolve = object -> {
    synchronized (promiseLock) {
        html = (String) object;
        promiseResolved = true;
    }
  };

最後結果已字符串形式存在html中,可將其渲染到頁面中.

最後

適用場景非瀏覽器渲染頁面,如java離線渲染前端頁面到Pdf。
實戰 github地址, 前端js項目地址

相關文章
相關標籤/搜索