iOS開發之Weex嵌入已有應用(三)
前言
1.官方環境部署
前兩個文章介紹了一下我遇到看到的一些需要注意的東西,其實按照官方的或者其他博主寫的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
registerComponent 自定義元件註冊#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 }
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的語法來寫頁面了。
參考文章: