欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS开发之Weex嵌入已有应用(三)

程序员文章站 2024-03-22 22:52:34
...

前言

1.官方环境部署

2.纯Weex开发简单的App

前两个文章介绍了一下我遇到看到的一些需要注意的东西,其实按照官方的或者其他博主写的Weex文章,虽然不多,但是很多人都是用嵌入应用的方式做项目的,如果纯Weex开发,可以点击上面的文章,自己写着玩应该还不错,下面介绍下自己如何集成到项目中写页面的


集成已经项目

一 添加依赖

官方介绍如何集成

如果你是原生开发,那很简单,直接用Cocoapods来集成,一般来讲一个项目都会由这个来管理,我们找到对应的Podfile文件,添加SDK如下:

iOS开发之Weex嵌入已有应用(三)

打开命令行,切换到你已有项目 Podfile 这个文件存在的目录,执行 pod install,没有出现任何错误表示已经完成环境配置。


二 初始化SDK

在AppDelegate里面引入如下头文件

#import <WeexSDK/WeexSDK.h>
#import "WXConfigCenterProtocol.h"
#import "WXConfigCenterDefaultImpl.h"
#import "WXNavigationHandlerImpl.h"
#import "WXImgLoaderDefaultImpl.h"

然后在启动方法里面初始化

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
#pragma mark weex
- (void)initWeexSDK
{
    [WXAppConfiguration setAppGroup:@"MTJF"];
    [WXAppConfiguration setAppName:@"MinTouJF"];
    [WXAppConfiguration setExternalUserAgent:@"2.8.6"];
    
    [WXSDKEngine initSDKEnvironment];
    
    [WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)];
    [WXSDKEngine registerHandler:[WXConfigCenterDefaultImpl new] withProtocol:@protocol(WXConfigCenterProtocol)];
    [WXSDKEngine registerHandler:[WXNavigationHandlerImpl new] withProtocol:@protocol(WXNavigationProtocol)];
    
    
#ifdef DEBUG
    [WXLog setLogLevel:WXLogLevelLog];
#else
    [WXLog setLogLevel:WXLogLevelError];
#endif
}
registerComponent  自定义组件注册

registerModule        自定义模块注册

registerHandler       实现协议的类注册(图片下载,导航跳转) 项目中只用了协议模块注册



三 Weex渲染的容器设置

Weex 支持整体页面渲染和部分渲染两种模式,你需要做的事情是用指定的 URL 渲染 Weex 的 view,然后添加到它的父容器上,父容器一般都是 viewController

项目是用MVVM的架构,想要了解的可以点击点击打开链接

主要是把代码丢到控制器的页面的ViewDidLoad里面去

- (void)mtf_ios_setupLayout{
    [super mtf_ios_setupLayout];
    
    self.view.backgroundColor = kDefaultBackgroundColor;
    [self.view setClipsToBounds:YES];
    
    self.viewModel.showNavigationBar = NO;
    [self.navigationController setNavigationBarHidden:self.viewModel.showNavigationBar];
    _weexHeight = self.view.frame.size.height - CGRectGetMaxY(self.navigationController.navigationBar.frame);
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationRefreshInstance:) name:@"RefreshInstance" object:nil];
    [self render];
}

- (void)render
{
    CGFloat width = self.view.frame.size.width;
    //    if ([_url.absoluteString isEqualToString:HOME_URL]) {
    //        [self.navigationController setNavigationBarHidden:YES];
    //    }
    [_instance destroyInstance];
    _instance = [[WXSDKInstance alloc] init];
    if([WXPrerenderManager isTaskExist:[self.viewModel.url absoluteString]]){
        _instance = [WXPrerenderManager instanceFromUrl:self.viewModel.url.absoluteString];
    }
    
    _instance.viewController = self;
    UIEdgeInsets safeArea = UIEdgeInsetsZero;
    
#ifdef __IPHONE_11_0
    if (@available(iOS 11.0, *)) {
        safeArea = self.view.safeAreaInsets;
    } else {
        // Fallback on earlier versions
    }
#endif
    
    _instance.frame = CGRectMake(self.view.frame.size.width-width, 0, width, _weexHeight-safeArea.bottom);
    
    __weak typeof(self) weakSelf = self;
    _instance.onCreate = ^(UIView *view) {
        [weakSelf.weexView removeFromSuperview];
        weakSelf.weexView = view;
        [weakSelf.view addSubview:weakSelf.weexView];
        UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, weakSelf.weexView);
    };
    _instance.onFailed = ^(NSError *error) {
        if ([[error domain] isEqualToString:@"1"]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSMutableString *errMsg=[NSMutableString new];
                [errMsg appendFormat:@"ErrorType:%@\n",[error domain]];
                [errMsg appendFormat:@"ErrorCode:%ld\n",(long)[error code]];
                [errMsg appendFormat:@"ErrorInfo:%@\n", [error userInfo]];
                
                UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"render failed" message:errMsg delegate:weakSelf cancelButtonTitle:nil otherButtonTitles:@"ok", nil];
                [alertView show];
            });
        }
    };
    
    _instance.renderFinish = ^(UIView *view) {
        WXLogDebug(@"%@", @"Render Finish...");
        [weakSelf updateInstanceState:WeexInstanceAppear];
    };
    
    _instance.updateFinish = ^(UIView *view) {
        WXLogDebug(@"%@", @"Update Finish...");
    };
    if (!self.viewModel.url) {
        WXLogError(@"error: render url is nil");
        return;
    }
    if([WXPrerenderManager isTaskExist:[self.viewModel.url absoluteString]]){
        WX_MONITOR_INSTANCE_PERF_START(WXPTJSDownload, _instance);
        WX_MONITOR_INSTANCE_PERF_END(WXPTJSDownload, _instance);
        WX_MONITOR_INSTANCE_PERF_START(WXPTFirstScreenRender, _instance);
        WX_MONITOR_INSTANCE_PERF_START(WXPTAllRender, _instance);
        [WXPrerenderManager renderFromCache:[self.viewModel.url absoluteString]];
        return;
    }
    _instance.viewController = self;
    NSURL *URL = [self testURL: [self.viewModel.url absoluteString]];
    NSString *randomURL = [NSString stringWithFormat:@"%@%@random=%d",URL.absoluteString,aaa@qq.com"&":@"?",arc4random()];
    [_instance renderWithURL:[NSURL URLWithString:randomURL] options:@{@"bundleUrl":URL.absoluteString} data:nil];
}

最后记得一定要释放内存销毁Weex Instance

- (void)dealloc
{
    
    [_instance destroyInstance];
#ifdef DEBUG
    [_instance forceGarbageCollection];
#endif
    
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    NSLog(@"dealloc--->%s",object_getClassName(self));
}

四 如何加载使用JS页面变成App页面

首先控制器ViewModel接收的url可以是服务器地址或者是本地Bundle地址

先看下如何使用服务器地址

iOS开发之Weex嵌入已有应用(三)

这里不详细介绍Weex项目的目录了,需要了解的可以看头部两个文章介绍和配置,Demo我已经写好放到Github了,直接下载跟着配置就好了

首先下面两个js文件是我们配置好需要打包成js文件的

npm run serve

启动本地服务,我们就可以在本地进行访问了

vm.url = [NSURL URLWithString:@"http://192.168.1.47:8081/dist/FourthPage.js"];

具体端口可以根据本地启动服务进行查看,如果上面的本地地址能访问到你的js文件,那么服务器的路径就可以测试了,或者你可以放到你公司的服务器上面,这里只是本地服务器测试为主


然后看下如何配置生成本地js地址

一般你运行Weex项目,执行 weex run ios,都会把你配置好的js入口文件打包到dist目录下面

下面是如何放入到Xcode目录下面

现在项目根目录下面新建一个Group,这里选择的是 New Group without Folder

iOS开发之Weex嵌入已有应用(三)

然后把文件丢到项目根目录下面,把对应的文件拖入Xcode引用即可

iOS开发之Weex嵌入已有应用(三)

OK,这就是本地目录js文件

一般来讲,你的js文件生成是没有压缩的。我们需要进行压缩,一种是生产环境压缩,还有种就是自己的开发环境进行配置。

自己配置:

/**
 * Plugins for webpack configuration.
 */
const plugins = [
  /*
   * Plugin: BannerPlugin
   * Description: Adds a banner to the top of each generated chunk.
   * See: https://webpack.js.org/plugins/banner-plugin/
   */
  new webpack.BannerPlugin({
    banner: '// { "framework": "Vue"} \n',
    raw: true,
    exclude: 'Vue'
  }),
  new webpack.optimize.UglifyJsPlugin({
  compress: {
    warnings: false
  },
  //保留banner
  comments: `/{ "framework": "Vue"}/`,
  sourceMap: false
  })
];

找到Weex目录下面的configs,然后找到webpack.common.conf.js文件,把里面的插件替换掉即可,你再运行weex run ios的时候就会进行压缩的,但是有个问题,他会把原本项目头部标记是Vue文件的注释都会压缩,然后你这个js文件是无法被Xcode里面的SDK识别的,会报错,之前的文章有介绍,可以看看

iOS开发之Weex嵌入已有应用(三)

如果压缩出来没有头部的注释是无法识别的,有时候会没有,需要注意下一下,可能哪里没配置对,自己加上去也行,如果觉得这样不靠谱,那就用官方配置下的生成环境打包


生产环境打包:

/**
 * Webpack configuration for weex.
 */
const weexConfig = webpackMerge(commonConfig[1], {
    /*
     * Add additional plugins to the compiler.
     *
     * See: http://webpack.github.io/docs/configuration.html#plugins
     */
    plugins: [
      /*
       * Plugin: UglifyJsparallelPlugin
       * Description: Identical to standard uglify webpack plugin
       * with an option to build multiple files in parallel
       *
       * See: https://www.npmjs.com/package/webpack-uglify-parallel
       */
      new UglifyJsparallelPlugin({
        workers: os.cpus().length,
        mangle: true,
        compressor: {
          warnings: false,
          drop_console: true,
          drop_debugger: true
        }
      }),
      // Need to run uglify first, then pipe other webpack plugins
      ...commonConfig[1].plugins
    ]
})

webpack.pro.conf.js 其实这个文件下都有打包压缩配置,但是common环境下如果需要就要自己配置,common下的配置我是网上找来的方法,了解下就好,如果要一样,直接复制weex写的那个,或者直接自己运行到pro环境

npm run build:prod

执行完之后,就会在dist下出现压缩后的js文件,要么放到服务器,要么拖进Xcode作为本地文件。

iOS开发之Weex嵌入已有应用(三)

写两句代码,在Xcode把js文件运行起来

#define BUNDLE_URL(path) [NSString stringWithFormat:@"file://%@/bundlejs/%@.js",[NSBundle mainBundle].bundlePath,path]
    MTFWeexViewModel *vm = [[MTFWeexViewModel alloc] init];
    MTFWeexViewController *vc = [[MTFWeexViewController alloc] initWithViewModel:vm];
//    vm.url = [NSURL URLWithString:@"http://192.168.1.47:8081/dist/实际路径"];
    vm.url = [NSURL URLWithString:BUNDLE_URL(@"本地路径文件名")];
    vm.titleName = kAccountActivityTitle;
    [self pushNormalViewController:vc];

写到这里,直接Push一个页面,把之前写的js文件编译好,然后直接让Weex控制器读取对应的url即可。


五 以一个简单的列表页面为例

vuejs结构代码

<template>
    <div class="media-con" :style="mainStyle">
        <r-l-list ref="dylist" :listItemName="itemClass" :listData="list" :bottomEmpty="listBottomEmpty"
                      :listHeight="listHeight"
                      :forLoadMore="onLoadMore" :forRefresh="onRefresh" :itemClick="itemClick" class="mikejing"></r-l-list>
    </div>
</template>

<script>


    import RLList from './widget/RLList.vue'
    
    import repository from '../core/net/repository'
    // import {Utils} from 'weex-ui';
    import {getEntryPageStyle, getListBottomEmpty, getListHeight, navigatorbBarHeight,mainTabBarHeight,getPageSize,MTF_CMD_URL_MediaReport,MTF_CMD_URL_Notice,MTF_CMD_STATICS_HOST} from "../config/Config"

const modal = weex.requireModule('modal');
var navigator = weex.requireModule('navigator')
    export default {
        props: {},
        components: {RLList},
        data() {
            return {
                currentPage: 0,
                itemClass: 'Media',
                list: [],
                listBottomEmpty: 0,
                listHeight:0,
                mainStyle:{}
            }   
        },
        created: function () {
            this.onRefresh();
        },
        activated: function () {
            //keep alive
            if(WXEnvironment.platform === 'Web') {
                this.init();
            }
        },
        methods: {
            init() {},
            fetchMediaLists(type) {

                repository.getMediaListDao(this.currentPage)
                    .then((res)=>{
                            this.resolveResult(res,type);
                        })
            },
            resolveResult(res,type) {
                if (res && res.result) {
                    if (type === 1) {
                        this.list = res.data.data.cmsNoticeDTO;
                        // this.list = ['1','2','3','1','2','3','1','2','3'];
                    } else {
                        this.list = this.list.concat(res.data.data.cmsNoticeDTO);
                    }
                }

                if (type === 1) {
                    if (this.$refs.dylist) {
                        this.$refs.dylist.stopRefresh();
                    }
                } else if (type === 2) {
                    if (this.$refs.dylist) {
                        this.$refs.dylist.stopLoadMore();
                    }
                }
                if (this.$refs.dylist) {
                    if (!res.data || res.data.data.cmsNoticeDTO.length < getPageSize()) {
                        console.log('隐藏底部');
                        this.$refs.dylist.setNotNeedLoadMore();
                    } else {
                        console.log('显示底部');
                        this.$refs.dylist.setNeedLoadMore();
                    }
                }
            },
            loadData(type) {
                this.fetchMediaLists(type);
            },
            onLoadMore() {
                this.currentPage++;
                this.loadData(2)
            },
            onRefresh() {
                this.currentPage = 0;
                this.loadData(1)
            },
            itemClick(index) {
                console.log('clickItem---->' + index);
                var item = this.list.length > index ? this.list[index] : '';
                if (item) {
                   navigator.push({
                    type:'WEB',
                    url: MTF_CMD_STATICS_HOST + MTF_CMD_URL_MediaReport + item.id,
                    animated: "true"
                }, event => {
                    modal.toast({ message: 'callback: ' + event })
                }) 
                }
            }
        }
    }
</script>

<style scoped>
.media-con{
    justify-content: center;
    align-items: flex-start;
}
/* 测试下flex = 1来代替listHeight的状态*/

.mikejing{
    flex: 1;
}
    
</style>

以一个页面为例,上面是Weex写的Vue结构代码,下面就是实际页面效果图

iOS开发之Weex嵌入已有应用(三)


这里有几个点:

1.tableView其实就是Weex中的 <list> list组件

2.stream模块去请求数据 stream模块

fetch(path, requestParams, type = 'json') {
        const stream = weex.requireModule('stream');
        return new Promise((resolve, reject) => {
            stream.fetch({
                method: requestParams.method,
                url: path,
                headers: requestParams.headers,
                type: type,
                body: requestParams.method === 'GET' ? "" : requestParams.body
            }, (response) => {
                if (response.status == 200 || response.status === 201 || response.status === 204 || response.status === 202) {
                    console.log('succeed。。。。。。');
                    resolve(response)
                } else {
                    console.log('failure。。。。。。');
                    reject(response)
                }
            }, () => {})
        })

    }

3.navigator模块跳转 navigator模块

itemClick(index) {
                console.log('clickItem---->' + index);
                var item = this.list.length > index ? this.list[index] : '';
                if (item) {
                   navigator.push({
                    type:'WEB',
                    url: MTF_CMD_STATICS_HOST + MTF_CMD_URL_MediaReport + item.id,
                    animated: "true"
                }, event => {
                    modal.toast({ message: 'callback: ' + event })
                }) 
                }
            }

这里的跳转其实就是push一个新的Weex页面,我们也可以通过注册协议来进行拦截,以下是部分代码,具体也可以参考Weex官方Demo

@interface WXNavigationHandlerImpl : NSObject <WXNavigationProtocol>

@end

@implementation WXNavigationHandlerImpl

- (void)pushViewControllerWithParam:(NSDictionary *)param completion:(WXNavigationResultBlock)block withContainer:(UIViewController *)container {
    BOOL animated = YES;
    NSString *obj = [[param objectForKey:@"animated"] lowercaseString];
    if (obj && [obj isEqualToString:@"false"]) {
        animated = NO;
    }
    
    // JS传递的时候定义了三种方式 WEB  NATIVE  WEEX
    NSString *type = [param objectForKey:@"type"];
    if ([type isEqualToString:@"WEB"]) {
        // WEB跳转
        NSString *webUrl = [param objectForKey:@"url"];
        MTFWebViewController *controller = [[MTFWebViewController alloc] initWithUrlString:webUrl titleName:nil];
        controller.hidesBottomBarWhenPushed = YES;
        [container.navigationController pushViewController:controller animated:animated];
    }else if ([type isEqualToString:@"NATIVE"]){
        // 跳转到原生
        
    }else if ([type isEqualToString:@"WEEX"]){
        // 跳转到Weex页面
    }
//    WXDemoViewController *vc = [[WXDemoViewController alloc] init];
//    vc.url = [NSURL URLWithString:param[@"url"]];
//    vc.hidesBottomBarWhenPushed = YES;
//    [container.navigationController pushViewController:vc animated:animated];
}

@end
可以看到JS写中模块Push的时候跳转传的对象参数都能在param里面接收到,根据具体的参数在App中做出对应的操作即可,可以跳转Web,可以跳转原生也可以跳转Weex页面


以上就是内嵌到已有应用的所有逻辑了,基本上完成需求了,这里看到一个飞猪的文章非常详细,weex文章不多,但是有的文章还是可以的

Weex 页面如何在飞猪、手淘、支付宝进行多端投放 ?

iOS开发之Weex嵌入已有应用(三)

xxxx.html?_wx_tpl=xxxx.js:前面为降级时的 H5 地址, 后面 _wx_tpl 带的参数代表 Weex JS 地址, 当容器发现 URL 带有 _wx_tpl 参数时, 会下载后面的 JS 地址然后用 Weex 容器渲染。

还有一种为通过服务端返回内容决定渲染为 Weex 还是 H5

xxxx?wh_weex=true:前面可以是 JS 地址也可以是 H5 地址,后面是固定的参数 wh_weex=true,当容器发现 URL 带有 wh_weex=true 时, 会请求前面的 xxxx 地址, 如果发现响应的 mime type(HTTP header content-type)为 application/javascript,则使用 Weex 渲染返回的内容, 否则使用 WebView 渲染成 H5。

自己试了一下用AF请求我们放在服务器上面的js地址,如果没有配置的话,response返回是200,但是格式会报错,因此我们要把返回的格式添加一下,@"application/javascript" 试过了,因此直接放AFHTTPResponseSerializer的acceptableContentTypes就好了。

- (NSMutableSet *)acceptContentTypesWithSerializer:(NSSet *)acceptableTypes{
    NSMutableSet *newAcceptContentTypes = [NSMutableSet setWithSet:acceptableTypes];
    //扩展固定解析响应类型
    [newAcceptContentTypes addObjectsFromArray:@[@"text/plain",
                                                 @"application/json",
                                                 @"text/json",
                                                 @"application/xml",
                                                 @"application/javascript",
                                                 @"text/html",
                                                 @"image/tiff",
                                                 @"image/jpeg",
                                                 @"image/jpg",
                                                 @"image/gif",
                                                 @"image/png",
                                                 @"image/ico",
                                                 @"image/x-icon",
                                                 @"image/bmp",
                                                 @"image/x-bmp",
                                                 @"image/x-xbitmap",
                                                 @"image/x-win-bitmap"]];
    return newAcceptContentTypes;
}

返回的结果是不能转换成NSDictionary的,因此通过下面的方式打印成字符串

[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);

你就能看到你放到服务器上的JS代码

常规渲染方法他们调用的是

[_instance renderWithURL:[NSURL URLWithString:randomURL] options:@{@"bundleUrl":URL.absoluteString} data:nil];

如果根据上面的规则,降级或者服务器获取的方式,你可以直接请求到js,然后通过另一个方法渲染,source传入刚才请求到转换出来的字符串即可

/**
 * Renders weex view with source string of bundle and some others.
 *
 * @param options The params passed by user.
 *
 * @param data The data the bundle needs when rendered. Defalut is nil.
 **/
- (void)renderView:(NSString *)source options:(NSDictionary *)options data:(id)data;


基本上整体架构和渲染的逻辑搞完,剩下的就是用Vue或者说是Weex的语法来写页面了。


参考文章:

Weex如何在iOS上运行

网易严选Weex Demo

Weex-ui 淘宝飞猪