上一节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">事件按钮








将界面的代码简化改成这样,运行的效果如下:



ios vue做app安卓 vue ios app_ios vue做app安卓

接下来,利用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.jswx.bundle.js,结构如下:

ios vue做app安卓 vue ios app_数据_02

新建的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];
}

运行一下看到结果如下:

ios vue做app安卓 vue ios app_vue打包成app方法_03

日志数据如下,通过不同的回调钩子,可以完成节点树的创建与更新。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.
这一小节就到此,下一节,我们来具体实现这个渲染的过程并完成事件的传递。