1. 程式人生 > >iOS開發之Weex嵌入已有應用(三)

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,[email protected]"&":@"?",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 淘寶飛豬