上一节Vue在非浏览器环境下的尝试我们利用了weex在vue中的dom实现成功的在非浏览器环境中Vue的实例,接下来我们将Vue集成到iOS当中,利用JavaScriptCore来实现界面的布局与动态数据绑定。
「一、搭建基于vue的weex编译产物环境」创建一个vue的工程
vue create ios-vue-demo
vue create ios-vue-demo
利用vue cli脚手架新建一个简单的实例工程,在原来的实例工程上小小改造一下,简化一下布局的元素
"app">
"box">
{{ msg }}
"click">事件按钮
将界面的代码简化改成这样,运行的效果如下:
接下来,利用webpack替换掉原来的npm run serve执行的命令
const path = require("path");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "web.bundle.js",
},
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000,
},
mode: "development",
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
pulbicPath: "./dist/asset/images",
outputPath: "./asset/images",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.vue$/,
use: "vue-loader",
},
],
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
filename: "index.html",
}),
],
};
const path = require("path");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "web.bundle.js",
},
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000,
},
mode: "development",
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
pulbicPath: "./dist/asset/images",
outputPath: "./asset/images",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.vue$/,
use: "vue-loader",
},
],
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
filename: "index.html",
}),
],
};
在package.json中增加一个命令
"start:dev": "webpack-dev-server --config webpack.config.web.js --open"
"start:dev": "webpack-dev-server --config webpack.config.web.js --open"
执行
npm run start:dev
npm run start:dev
可以达到与之前执行vue脚手架提供的npm run serve
一样的效果了。
通过以上的webpack配置,启动一个web server来监听代码,通过这个配置可以在开发阶段更好的在浏览器中调试界面。
使用webpack可以为接下来打包基于weex的vue产物做准备。
「二、利用weex-loader来打包vue」
首先改造一下entry main.wx.js
/* eslint-disable no-undef */
import App from "./App.vue";
App.el = "#app";
new Vue(App);
/* eslint-disable no-undef */
import App from "./App.vue";
App.el = "#app";
new Vue(App);
然后在webpack.config.web.js的基础上稍稍改动一下,生成新的配置文件webpack.config.wx.js
代码如下:
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/main.wx.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "wx.bundle.js",
},
mode: "development",
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
pulbicPath: "./dist/asset/images",
outputPath: "./asset/images",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.vue(\?[^?]+)?$/,
use: [
{
loader: "weex-loader",
},
],
},
],
},
plugins: [
new webpack.BannerPlugin({
banner: '// { "framework": "Vue"} \n',
raw: true,
exclude: "Vue",
}),
],
};
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/main.wx.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "wx.bundle.js",
},
mode: "development",
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
pulbicPath: "./dist/asset/images",
outputPath: "./asset/images",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.vue(\?[^?]+)?$/,
use: [
{
loader: "weex-loader",
},
],
},
],
},
plugins: [
new webpack.BannerPlugin({
banner: '// { "framework": "Vue"} \n',
raw: true,
exclude: "Vue",
}),
],
};
在package.json
中添加一个新的命令,用于建构产物:
"build:wx": "npx webpack --config webpack.config.wx.js"
"build:wx": "npx webpack --config webpack.config.wx.js"
「三、将weex-js-framework与wx.bundle.js集成到iOS环境中」
新建一个工程,简化一下并js-framework.js
与wx.bundle.js
,结构如下:
新建的WXJSContext用来封装所有的JS的操作逻辑,这里只是一个Demo,所以把所有的逻辑全部放在WXJSContext中完成。
初始化console.log函数回调
- (void)_setupConsoleLog {
_context[@"console"][@"log"] = ^(NSString *message) {
NSLog(@"Javascript log: %@",message);
};
}
- (void)_setupConsoleLog {
_context[@"console"][@"log"] = ^(NSString *message) {
NSLog(@"Javascript log: %@",message);
};
}
加载js-framework.js
- (void)_setupWxJSFramework {
NSString *path = [[NSBundle mainBundle] pathForResource:@"js-framework" ofType:@"js"];
NSError *error;
NSString *code = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
[_context evaluateScript:code];
}
- (void)_setupWxJSFramework {
NSString *path = [[NSBundle mainBundle] pathForResource:@"js-framework" ofType:@"js"];
NSError *error;
NSString *code = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
[_context evaluateScript:code];
}
hook js-framework
中的callNative方法,当加载模板代码创建Vue创建节点,添加节点,更新节点的布局属性与显示属性时就会回调
- (void)_hookCallNative {
_context[@"callNative"] = ^(JSValue *a, JSValue *b) {
if([a isString] && [b isObject]) {
NSString *instanceId = [a toString];
NSData *data = [NSJSONSerialization dataWithJSONObject:[b toDictionary] options:NSJSONWritingFragmentsAllowed error:NULL];
NSString *info = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"实例id: %@, 数据: %@", instanceId, info);
}
};
}
- (void)_hookCallNative {
_context[@"callNative"] = ^(JSValue *a, JSValue *b) {
if([a isString] && [b isObject]) {
NSString *instanceId = [a toString];
NSData *data = [NSJSONSerialization dataWithJSONObject:[b toDictionary] options:NSJSONWritingFragmentsAllowed error:NULL];
NSString *info = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"实例id: %@, 数据: %@", instanceId, info);
}
};
}
准备好执行wx.bundle.js
所需要的环境
1.加载完js-framework后就会在global object上挂载一系列的方法
2.通过调用global object的createInstanceContext,传递参数为实例的标识符,这里是"1"
3.创建好p实例上下文后,通过这个实例可以拿到Vue的具体定义
4.将Vue的定义挂载到global object上就可以成加载模板代码时,使用到Vue时找到
- (void)_prepareVueEnvironment {
/// 加载完js-framework后就会在global object上挂载一系列的方法
/// 通过调用global object的createInstanceContext,传递参数为实例的标识符,这里是"1"
/// 创建好p实例上下文后,通过这个实例可以拿到Vue的具体定义
/// 将Vue的定义挂载到global object上就可以成加载模板代码时,使用到Vue时找到
JSGlobalContextRef globalContextRef = _context.JSGlobalContextRef;
JSObjectRef globalObjectRef = JSContextGetGlobalObject(globalContextRef);
JSValue *v = [[_context globalObject] invokeMethod:@"createInstanceContext" withArguments:@[@"1"]];
NSArray *keys = ExtractPropertyNamesFromJSValue(v);
for(NSString *key in keys) {
if([key isEqualToString:@"Vue"]) {
/// 获取全局对象globalObject的prototype(原型链)
JSValueRef valueRef = JSObjectGetPrototype(globalContextRef, globalObjectRef);
/// 将Vue的值转化成一个Vue对象
JSObjectRef objectRef = JSValueToObject(globalContextRef, [v valueForProperty:key].JSValueRef, NULL);
/// 在Vue的原型链上增加globalObject
JSObjectSetPrototype(globalContextRef, objectRef, valueRef);
}
JSStringRef propertyName = JSStringCreateWithUTF8CString([key cStringUsingEncoding:NSUTF8StringEncoding]);
JSObjectSetProperty(globalContextRef, globalObjectRef, propertyName, [v valueForProperty:key].JSValueRef, 0, NULL);
}
}
/// 提取JSValue中的属性名称
NS_INLINE NSArray *ExtractPropertyNamesFromJSValue(JSValue *jsValue) {
NSMutableArray *keys = NULL;
JSContextRef contextRef = jsValue.context.JSGlobalContextRef;
JSValueRef exception = NULL;
JSObjectRef jsObjectRef = JSValueToObject(contextRef, jsValue.JSValueRef, &exception);
if(exception != NULL) {
NSLog(@"JSValueToObject Exception");
return @[];
}
BOOL somethingWrong = NO;
if (jsObjectRef != NULL) {
JSPropertyNameArrayRef allKeyRefs = JSObjectCopyPropertyNames(contextRef, jsObjectRef);
size_t keyCount = JSPropertyNameArrayGetCount(allKeyRefs);
keys = [[NSMutableArray alloc] initWithCapacity:keyCount];
for (size_t i = 0; i JSStringRef nameRef = JSPropertyNameArrayGetNameAtIndex(allKeyRefs, i);
size_t len = JSStringGetMaximumUTF8CStringSize(nameRef);
if (len > 1024) {
somethingWrong = YES;
break;
}
char* buf = (char*)malloc(len + 5);
if (buf == NULL) {
somethingWrong = YES;
break;
}
bzero(buf, len + 5);
if (JSStringGetUTF8CString(nameRef, buf, len + 5) > 0) {
NSString* keyString = [NSString stringWithUTF8String:buf];
if ([keyString length] == 0) {
somethingWrong = YES;
free(buf);
break;
}
[keys addObject:keyString];
}
else {
somethingWrong = YES;
free(buf);
break;
}
free(buf);
}
JSPropertyNameArrayRelease(allKeyRefs);
} else {
somethingWrong = YES;
}
if (somethingWrong) {
// may contain retain-cycle.
keys = (NSMutableArray*)[[jsValue toDictionary] allKeys];
}
return keys;
}
- (void)_prepareVueEnvironment {
/// 加载完js-framework后就会在global object上挂载一系列的方法
/// 通过调用global object的createInstanceContext,传递参数为实例的标识符,这里是"1"
/// 创建好p实例上下文后,通过这个实例可以拿到Vue的具体定义
/// 将Vue的定义挂载到global object上就可以成加载模板代码时,使用到Vue时找到
JSGlobalContextRef globalContextRef = _context.JSGlobalContextRef;
JSObjectRef globalObjectRef = JSContextGetGlobalObject(globalContextRef);
JSValue *v = [[_context globalObject] invokeMethod:@"createInstanceContext" withArguments:@[@"1"]];
NSArray *keys = ExtractPropertyNamesFromJSValue(v);
for(NSString *key in keys) {
if([key isEqualToString:@"Vue"]) {
/// 获取全局对象globalObject的prototype(原型链)
JSValueRef valueRef = JSObjectGetPrototype(globalContextRef, globalObjectRef);
/// 将Vue的值转化成一个Vue对象
JSObjectRef objectRef = JSValueToObject(globalContextRef, [v valueForProperty:key].JSValueRef, NULL);
/// 在Vue的原型链上增加globalObject
JSObjectSetPrototype(globalContextRef, objectRef, valueRef);
}
JSStringRef propertyName = JSStringCreateWithUTF8CString([key cStringUsingEncoding:NSUTF8StringEncoding]);
JSObjectSetProperty(globalContextRef, globalObjectRef, propertyName, [v valueForProperty:key].JSValueRef, 0, NULL);
}
}
/// 提取JSValue中的属性名称
NS_INLINE NSArray *ExtractPropertyNamesFromJSValue(JSValue *jsValue) {
NSMutableArray *keys = NULL;
JSContextRef contextRef = jsValue.context.JSGlobalContextRef;
JSValueRef exception = NULL;
JSObjectRef jsObjectRef = JSValueToObject(contextRef, jsValue.JSValueRef, &exception);
if(exception != NULL) {
NSLog(@"JSValueToObject Exception");
return @[];
}
BOOL somethingWrong = NO;
if (jsObjectRef != NULL) {
JSPropertyNameArrayRef allKeyRefs = JSObjectCopyPropertyNames(contextRef, jsObjectRef);
size_t keyCount = JSPropertyNameArrayGetCount(allKeyRefs);
keys = [[NSMutableArray alloc] initWithCapacity:keyCount];
for (size_t i = 0; i JSStringRef nameRef = JSPropertyNameArrayGetNameAtIndex(allKeyRefs, i);
size_t len = JSStringGetMaximumUTF8CStringSize(nameRef);
if (len > 1024) {
somethingWrong = YES;
break;
}
char* buf = (char*)malloc(len + 5);
if (buf == NULL) {
somethingWrong = YES;
break;
}
bzero(buf, len + 5);
if (JSStringGetUTF8CString(nameRef, buf, len + 5) > 0) {
NSString* keyString = [NSString stringWithUTF8String:buf];
if ([keyString length] == 0) {
somethingWrong = YES;
free(buf);
break;
}
[keys addObject:keyString];
}
else {
somethingWrong = YES;
free(buf);
break;
}
free(buf);
}
JSPropertyNameArrayRelease(allKeyRefs);
} else {
somethingWrong = YES;
}
if (somethingWrong) {
// may contain retain-cycle.
keys = (NSMutableArray*)[[jsValue toDictionary] allKeys];
}
return keys;
}
加载wx.bundle.js
模板代码
- (void)_loadBundleJSCode {
NSString *codePath = [[NSBundle mainBundle] pathForResource:@"wx.bundle" ofType:@"js"];
NSString *code = [[NSString alloc] initWithContentsOfFile:codePath encoding:NSUTF8StringEncoding error:NULL];
[_context evaluateScript:code];
}
- (void)_loadBundleJSCode {
NSString *codePath = [[NSBundle mainBundle] pathForResource:@"wx.bundle" ofType:@"js"];
NSString *code = [[NSString alloc] initWithContentsOfFile:codePath encoding:NSUTF8StringEncoding error:NULL];
[_context evaluateScript:code];
}
运行一下看到结果如下:
日志数据如下,通过不同的回调钩子,可以完成节点树的创建与更新。Vue
已经帮我们完成了数据的动态绑定,更新节点的patch.我们只需要利用好这个回调方法,将Vue
的节点同步到原生层面,就可以完成渲染了。
实例id: 1, 数据: {"0":{"module":"dom","method":"createBody","args":[{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{"flexDirection":"column","justifyContent":"center","display":"flex","alignItems":"center"},"type":"div","ref":"_root"}]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["_root",{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{"marginBottom":"20","backgroundColor":"#FF0000","width":"100","height":"100"},"type":"div","ref":"8"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["_root",{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{},"type":"div","ref":"10"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["10",{"attr":{"value":"iOS集成Vue测试工程"},"style":{},"type":"text","ref":"12"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["_root",{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{},"type":"button","event":["click"],"ref":"14"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["14",{"attr":{"value":"事件按钮"},"style":{},"type":"text","ref":"16"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"createFinish","args":[]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"createBody","args":[{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{"flexDirection":"column","justifyContent":"center","display":"flex","alignItems":"center"},"type":"div","ref":"_root"}]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["_root",{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{"marginBottom":"20","backgroundColor":"#FF0000","width":"100","height":"100"},"type":"div","ref":"8"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["_root",{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{},"type":"div","ref":"10"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["10",{"attr":{"value":"iOS集成Vue测试工程"},"style":{},"type":"text","ref":"12"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["_root",{"attr":{"@styleScope":"data-v-7ba5bd90"},"style":{},"type":"button","event":["click"],"ref":"14"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"addElement","args":["14",{"attr":{"value":"事件按钮"},"style":{},"type":"text","ref":"16"},-1]}}
实例id: 1, 数据: {"0":{"module":"dom","method":"createFinish","args":[]}}
通过hook callNative
这个方法我们可以在本地建立一个Node Tree,将Node Tree交给Yoga进行Flex布局,最终渲染出这个Node Tree.
这一小节就到此,下一节,我们来具体实现这个渲染的过程并完成事件的传递。