微信小程序和網(wǎng)頁最大的不同是小程序基于 WebView + JS引擎實現(xiàn)的雙線程渲染架構(gòu),參考微信小程序的 渲染層和邏輯層 文檔,實際上是多個 WebView 加上一個 JS引擎焕窝,在 Android 上是使用 Google v8 引擎。之所以使用雙線程模式肴焊,主要是為了安全性馒过,有限提供JS能力,小程序是不允許使用 eval
執(zhí)行JS代碼 和 new Function
創(chuàng)建函數(shù)镜盯,這樣就無法在小程序上使用動態(tài)加載代碼能力,如果邏輯層代碼是在 WebView 上運行猖败,就不受微信控制了速缆,所以這才是雙線程模型的意義。
我最近也在研究相關(guān)的小程序技術(shù)恩闻,也開源了一個JS引擎相關(guān)的框架 quickjs-android艺糜。小程序的 Page 的設(shè)計和 Vue.js 很相似,所以使用 quickjs-android 和 Vue.js 模仿微信小程序的雙線程架構(gòu)幢尚,實現(xiàn)狀態(tài)更新和事件觸發(fā)破停。
框架分析
Page
Page({
data: {
counter: 0
},
onMinusClick: function () {
this.setData({ counter: this.data.counter - 1 })
},
onAddClick: function () {
this.setData({ counter: this.data.counter + 1 })
}
});
Vue.js
var example1 = new Vue({
el: '#example-1',
data: {
counter: 0
},
methods: {
onMinusClick: function () {
this.counter--
},
onAddClick: function () {
this.counter++
}
}
})
對比微信小程序和 Vue.js,使用方式很相似尉剩,但是底層是相差非常的大真慢。
Native 層代碼
Render 接口
public interface Render {
/**
* 對應(yīng)微信小程序的setData方法
*/
void setData(JSONObject data);
/**
* 初始化渲染層
*
* @param engine 引擎對象
* @param data 對應(yīng) page 的 data
* @param methods 對應(yīng) page 的函數(shù)
*/
void initRender(JavascriptEngine engine, JSONObject data, JSONObject methods);
}
JavascriptEngine 接口
public interface JavascriptEngine {
/**
* 觸發(fā)引擎的 page 函數(shù)
* @param name 函數(shù)名
* @param params 參數(shù)
*/
void invokeFunction(String name, String params);
}
quickjs-android 引擎實現(xiàn)
public class QuickJSEngine implements JavascriptEngine {
private final Context context;
private final Render render;
private final QuickJS quickJS;
private final JSContext jsContext;
public QuickJSEngine(Context context, Render render) {
this.context = context;
this.render = render;
this.quickJS = QuickJS.createRuntimeWithEventQueue();
this.jsContext = quickJS.createContext();
initEngine();
}
private void initEngine() {
initRenderHandler();
jsContext.addPlugin(new SetTimeoutPlugin());
jsContext.addPlugin(new ConsolePlugin());
executeModule("framework/quickjs.js");
}
/**
* 注冊一個 render 對象,提供 setData 和 initRender 的回調(diào)方法
*/
private void initRenderHandler() {
JSObject renderHandler = new JSObject(jsContext);
renderHandler.registerJavaMethod((receiver, args) -> {
render.setData(args.getObject(0).toJSONObject());
}, "setData");
renderHandler.registerJavaMethod((receiver, args) -> {
JSObject data = args.getObject(0);
JSObject methods = args.getObject(1);
render.initRender(this, data.toJSONObject(), methods.toJSONObject());
}, "initRender");
this.jsContext.set("render", renderHandler);
}
public void close() {
quickJS.close();
}
public void executeModule(String fileName) {
jsContext.executeVoidScript(FileUtils.readAssetText(context, fileName), fileName);
}
@Override
public void invokeFunction(String name, String params) {
jsContext.executeFunction("invokeFunction", new JSArray(jsContext).push(name).push(params));
}
}
WebView 實現(xiàn)
class WebViewRender extends WebView implements Render {
private final LinkedList<Runnable> events = new LinkedList<>();
private boolean init = false;
private JavascriptEngine engine;
public WebViewRender(@NonNull Context context) {
super(context);
getSettings().setJavaScriptEnabled(true);
setWebChromeClient(new WebChromeClient());
setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
init = true;
postEvent(null);
}
});
this.addJavascriptInterface(new FrameworkHandler(), "_framework");
}
private class FrameworkHandler {
@JavascriptInterface
public void invokeFunction(String name, String params) {
engine.invokeFunction(name, params);
}
}
private void postEvent(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(() -> {
if (runnable != null) {
events.add(runnable);
}
if (init) {
while (!events.isEmpty()) {
Runnable first = events.pollFirst();
if (first != null) {
first.run();
}
}
}
});
}
@Override
public void setData(JSONObject data) {
postEvent(() -> loadUrl("javascript:setData(" + data.toString() + ")"));
}
@Override
public void initRender(JavascriptEngine engine, JSONObject data, JSONObject methods) {
this.engine = engine;
postEvent(() -> loadUrl(String.format("javascript:initRender(%s,%s)", data.toString(), methods.toString())));
}
public void loadHtmlFile(String fileName) {
String baseUrl = "file:///android_asset/" + fileName;
String framework = FileUtils.readAssetText(getContext(), "framework/webview.html");
String data = FileUtils.readAssetText(getContext(), fileName);
framework = framework.replace("@CONENT", data);
loadDataWithBaseURL(baseUrl, framework, "text/html", "utf-8", null);
}
}
邏輯層框架代碼
// framework/quickjs.js
function Page(page) {
globalThis._page = page;
if (page['data'] == undefined) {
page.data = {}
}
page.setData = function (data) {
var keys = Object.keys(data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
page.data[key] = data[key];
}
render.setData(data);
}
var methods = {};
Object.keys(page).forEach(function (key) {
var obj = page[key];
if (typeof obj === "function") {
methods[key] = {}
}
});
render.initRender(page.data, methods);
}
function invokeFunction(name, params) {
globalThis._page[name](params);
}
渲染層框架代碼
framework/webview.html
<!DOCTYPE html>
<head>
<title>A page written in english</title>
</head>
<body>
<div id="app">
@CONENT
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
function initRender(data, methods) {
var page = {};
page.el = '#app';
page.data = data;
page.methods = {};
Object.keys(methods).forEach(function (name) {
page.methods[name] = function (params) {
_framework.invokeFunction(name, JSON.stringify(params));
}
});
self.app = new Vue(page);
};
function setData(obj) {
var keys = Object.keys(obj);
keys.forEach(element => {
self.app[element] = obj[element];
});
};
</script>
</body>
</html>
示例
邏輯層代碼
Page({
data: {
counter: 0
},
onMinusClick: function () {
this.setData({ counter: this.data.counter - 1 })
},
onAddClick: function () {
this.setData({ counter: this.data.counter + 1 })
}
});
頁面
<div>
{{ counter }}
<div style="margin-top: 20px;">
<button v-on:click="onMinusClick">-</button>
<button v-on:click="onAddClick">+</button>
</div>
</div>
總結(jié)
本篇文章只是簡單通過 quickjs-android 和 Vue.js 去模仿微信小程序理茎,實現(xiàn)一個簡單的雙線程架構(gòu)黑界。實際上微信的邏輯層和渲染層的架構(gòu)要復(fù)雜很多,因為微信小程序的頁面并不是使用標(biāo)準(zhǔn)的HTML皂林,而是自定義的 WXML 和 WXS朗鸠,css 也使用了 WXSS 代替,渲染層的工作還很多础倍。 WXS 是微信小程序用于渲染層的腳本語言童社,語法規(guī)范類似JavaScript,但是不完全兼容著隆,這個腳本的作用是為了解決雙線程的線程切換帶來的性能問題扰楼,因為這個腳本是運行在渲染層呀癣。而這個demo的渲染層是使用標(biāo)準(zhǔn)的 HTML 和 Vue.js 實現(xiàn)的。