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
#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地址
先看下如何使用服务器地址
这里不详细介绍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的语法来写页面了。
参考文章:
推荐阅读