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

前言

1.官方环境部署

2.纯Weex开发简单的App

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


集成已经项目

一 添加依赖

官方介绍如何集成

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


打开命令行,切换到你已有项目 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,URL.query?@"&":@"?",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地址

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


这里不详细介绍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


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


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识别的,会报错,之前的文章有介绍,可以看看


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


生产环境打包:

/**
 * 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作为本地文件。


写两句代码,在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结构代码,下面就是实际页面效果图



这里有几个点:

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 页面如何在飞猪、手淘、支付宝进行多端投放 ?


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 淘宝飞猪









已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页