关于QuickJS
QuickJS是一个小型且可嵌入的Javascript引擎。它支持 ES2020规范,包括模块,异步生成器,代理和BigInt。
它可选地支持数学扩展,例如大十进制浮点数(BigDecimal),大二进制浮点数(BigFloat)和运算符重载。
主要特点:
- 体积小且易于嵌入:只有几个C文件,没有外部依赖关系,一个简单的hello world 程序的210 KiB x86代码。
- 快速解释器,启动时间非常短:在台式机的单个内核上,可以在大约100秒内运行ECMAScript Test Suite的75000个测试。运行时实例的完整生命周期在不到300微秒内即可完成。
- 几乎完整的ES2020 支持,包括模块,异步生成器和完整的附件B支持(旧版Web兼容性)。
- 选择ES2020功能时,将通过近100%的ECMAScript Test Suite测试。可在Test262报告中获得摘要。
- 可以将Javascript源代码编译为没有外部依赖关系的可执行文件。
- 使用引用计数进行垃圾收集(以减少内存使用并具有确定性的行为),并进行循环删除。
- 数学扩展:BigDecimal,BigFloat,运算符重载,bigint模式,数学模式。
- 使用Java语言实现的带有上下文着色的命令行解释器。
- 小型内置标准库,带有C库包装器。
QuickJS 的几个重要特性: 小型,可嵌入等都和V8引擎 有鲜明的对比,因此QuickJs在一些领域必然可以大有作为,IOT设备上 可能就是QuickJs大展拳脚的一地。
本文尝试使用QuickJS作为执行引擎,尝试打造一个可以让开发者用JavaScript 就可以开发IOT UI 的渲染容器。
设计目标
贴合 IOT 图形化设备的一些特性
- 主要展示2d为主
- 多为单屏为主,滚动较少
- 提供的功能和设备有强相关性
- 操作多为点击等简单操作
- 设备配置较低
基于对于IOT图形化设备的特性总结和了解,本文这里最终设计目标也并不是一个完善的类似浏览器的UI 渲染容器,只是尽可能的去贴合IOT 图形化设备。
贴合Web开发
为了贴合目前主流的Web开发形式,本文设计绘制UI 的形式还是基于dom操作方式。在QuickJs中实现document相关接口。如createElement,insertBefore,appendChild,createTextNode,remoteChild。后期也将模拟事件传递过程完成事件监听触发能力。
在布局样式上,本文这里采用了React Native 背后的Yoga Layout 作为布局引擎,所以这里也只支持目前流行弹性布局和盒模型的相关样式。
工程相关
我们这里选用Skia作为我们的渲染引擎,被chrome&flutter依赖,其完备渲染能力和社区支持对于本文的实践都具有非常可观的支持。
skia 的编译
1. 源码拉取
1 2 3 4 5 6
| git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH="${PWD}/depot_tools:${PATH}"
git clone https://skia.googlesource.com/skia.git cd skia python2 tools/git-sync-deps
|
2. skia的编译
Skia 编译主要有三个参数控制
is_official_build 决定是否有符号优化 是否使用系统三方库 ,如果选择true,则编译的时候需要去系统寻找一些三方库,如果libjpeg 等等,如果不存在或者版本不对都会报错
is_component_build 决定是否编译成动态链接版本
skia_use_system_xxx=false
如skia_use_system_libjpeg_turbo = false 就是不使用系统库路径搜索到的代码,使用仓库内的third_party内的源码进行编译。可以使用这个参数避免去寻找一些系统上没有带的三方库。
但是有一些三方依赖库还是需要安装一下,因为仓库内的一些gn也是引用系统库路径的代码进行编译
在linux的系统上可以执行tools/install_dependencies.sh 进行安装
其他系统可以查看文件内容和报错进行安装
最简单的编译方式
1 2
| bin/gn gen out/Release ninja -C out/Release
|
———-这样子打出来的静态库大小有一百多M
3. 在cmake中引用skia
1 2 3 4
| set(SKIADIR /Users/disheng/sources/skia) include_directories(${SKIADIR}) include_directories(/usr/local/include) link_directories(${SKIADIR}/out/Release)
|
如果在编译skia使用is_official_build 中使用了true,则需要把一些三方库也给引入进来
如link_libraries(SDL2 SDL2main pthread fontconfig freetype)
这样子
在mac上还需要引入一些library
1 2 3 4 5 6 7 8
| find_library(CoreServices CoreServices) find_library(CoreGraphics CoreGraphics) find_library(CoreText CoreText) find_library(CoreFoundation CoreFoundation) find_library(OpenGL_LIBRARY OpenGL)
add_executable(skiademo main.cpp) target_link_libraries(skiademo ${OpenGL_LIBRARY} ${CMAKE_DL_LIBS} ${CoreServices} ${CoreGraphics} ${CoreText} ${CoreFoundation} ${COCOA_LIBRARY} m )
|
yoga layout编译
yoga给我们提供了cmakelist
1 2 3
| mkdir cmake-build cmake ../ // yogalayout 布局提供一写debug能力,如果需要可以添加宏定义DEBUG,ADD_DEFINITIONS(-DDEBUG)
|
quickjs 编译和引用
QuickJs 和其描述的一样,没有外部依赖
1 2 3 4 5 6
| make && make install // 由于我们是一个cpp项目,quickjs 是一个c语言的项目,我们需要利用extern "C"关键词来做一下处理 extern "C" { #include "quickjs-libc.h" #include "quickjs.h" }
|
Dom接口设计
我们的目标是让开发者在使用dom接口时可以尽量符合w3c标准,而不用利用module的形式,
所以我们需要向执行环境注入document 对象并在内部维护一个tree和一个全局NodeMap.与此同时这颗Dom Tree 还要和 Layout Tree 进行关联,让其可以实现动态布局的能力。

全局document对象
QuickJs 在Gloabl Object上添加的方法
1
| JS_SetPropertyStr(this->context, JS_GetGlobalObject(this->context), globalName, globalValue);
|
1 2 3 4 5 6 7 8 9 10
|
QuickRuntime *runtime = new QuickRuntime(); runtime->init(); runtime->createContext();
DocumentNode* rootDocumentNode = new DocumentNode(runtime->getContext(),"root"); JSValue *document = rootDocumentNode->getJSValue(); runtime->addGloablValue("document",*document);
|
方法设计
这是 document.createElement 的入口函数
1 2 3 4 5 6
| static JSValue _createElement(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { DocumentNode *thisNode = CacheNode::getCacheNode(ctx,&this_val); auto createNode = thisNode->createElement(JS_ToCString(ctx,argv[0])); return *createNode->getJSValue(); }
|
CacheNode 在这里的主要原因是为了帮助 JSValue 和 我们内置Dom Tree上的Node关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| DocumentNode::DocumentNode(JSContext *context, string _elementType) { this->elementType = _elementType; this->layout = new Layout(500, 500); this->context = context; this->nodeContext = new NodeContext(); this->nodeContext->setLayout(this->layout); this->parentNode = nullptr; this->node = new YGNode(); int nodeID = NodeId::getNodeId(); this->nodeId = nodeID; this->node->setContext(this->nodeContext); CacheNode::insert(nodeID, this); this->toJSValue(); }
|
绘制过程
绘制过程往往是渲染容器核心,但是本文主要探究quickjs在iot ui上的应用,
在dom tree 进行变动后,我们会把根节点关联的YGNode 进行 布局计算。将此项核心工作交给yoga来进行。
1
| rootNode->calculateLayout();
|
拿到布局树的根节点,获取到yoga的计算的位置信息,进行绘制。
实践结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const rect = document.createElement('div'); rect.style = { height:400, width:400, justifyContent:'flex-start', alignItems:'flex-start' } const circle = document.createElement('div'); circle.style={ height:300, width:300, borderRadius:50, justifyContent:'center', alignItems:'center', } rect.appendChild(circle);
circle.appendChild(document.createTextNode("IOT RENDER HELLO WORLD")); document.appendChild(rect) document.style = { height:500, width:500, justifyContent:'center', alignItems:'center', }
|
