Weex 從無到有開發(fā)一款上線應(yīng)用 1

iOS調(diào)試Demo
WeexDemo

初始化APPFrame

終端創(chuàng)建 Weex工程
weex init xxx_weex
安裝IDE插件

WebStorm安裝Weex語法支持和插件。由于最開始Weex文件為.we,現(xiàn)在Weex已經(jīng)完美支持Vue,所以現(xiàn)在也是用Vue開發(fā)。
具體安裝步驟如下


打開偏好設(shè)置.png
plugins.png

同樣的下載Vue.js
安裝后重啟WebStorm。
由于WebStorm還不能直接創(chuàng)建Vue文件,所以需要先添加一下Vue文件模板。操作如下

添加Vue文件模板.png

模板內(nèi)容如下

<template>
    <div class="view">
    </div>
</template>

<script>
    let stream = weex.requireModule('stream')
    let modal = weex.requireModule('modal')
    let navigator = weex.requireModule('navigator')
    let globalEvent = weex.requireModule('globalEvent');
    let apiHost = ''
    export default {
        data () {
            return {

            }
        },
        methods: {

        },
        created () {

        },
        mounted()
        {

        },
        components: {
            
        }
    }
</script>

<style scoped>
    .view {
        width: 750px;
        height: 1334px;
        background-color:#fff ;
    }
</style>
新建AppFrame.Vue文件
AppFrame.png

到這里Weex開發(fā)環(huán)境已經(jīng)完成。接下來我們就要開始擴(kuò)展組件和模塊了。

擴(kuò)展原生AppFrame Component

由于Weex開發(fā)的是單個頁面,也沒有系統(tǒng)的ViewController 和 NavigationController以及TabBarController,那么我們怎么開始一個架設(shè)一個動態(tài)的應(yīng)用框架呢?這就需要我們書寫一個原生的AppFrame組件。
因?yàn)槭菑臒o到有開發(fā),我們就新建一個Xcode工程。
如果是已有工程那么就創(chuàng)建一個新的Target或者Project,或者新建工程通過pod導(dǎo)入。
創(chuàng)建一個 WXComponent子類。
然后重寫-(instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance方法,
方法實(shí)現(xiàn)如下

-(instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance
{
    if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
        
        [self firstInitAPPFrameWithAttributes:attributes];
        
    }
    return self;
}

類文件如下圖


AppFrameComponent.png
這樣做的思路:

tabbarItem的數(shù)據(jù)通過服務(wù)端下發(fā)的形式進(jìn)行創(chuàng)建。
以及tabbarViewController.ViewControllers也通過服務(wù)端下發(fā)的形式進(jìn)行創(chuàng)建。
-------------2017.05.24 明晚再寫-----------------
如果App設(shè)計(jì)是最常用的TabbarUI,那么下發(fā)的tabbarItem的數(shù)據(jù)結(jié)構(gòu)如下:

 {
//                            標(biāo)題
                            title:'',
//                            沒選中字體顏色
                            normalTitleColor:'999999',
//                            選中字體顏色
                            selectedTitleColor:'FF3C00',
//                            背景顏色
                            tintColor:'FF3C00',
//                            選中圖片
                            selectedImage:'',
//                            沒選中圖片
                            image:''
}

當(dāng)然有非常規(guī)的UI那么同樣需要配置的數(shù)據(jù)有這些,其他就需要針對應(yīng)用來自己設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)。
導(dǎo)航欄也同樣需要考慮系統(tǒng)的是否滿足需求,不滿足的那么就在當(dāng)前頁面做一個假的導(dǎo)航欄(這樣有一定的缺陷,比如我們再通話中使用app那么假的導(dǎo)航欄需要處理成新的高度)。下邊是針對系統(tǒng)導(dǎo)航欄的下發(fā)數(shù)據(jù)

{
//                            導(dǎo)航欄標(biāo)題
                            title:'',
//                            透明導(dǎo)航欄的字體顏色
                            clearTitleColor:'ffffff',
//                            高斯模糊導(dǎo)航欄的字體顏色
                            blurTitleColor:'000000',
//                            左邊選項(xiàng)按鈕
                            leftItemsInfo:
                                [
                                    {
//                                        原生調(diào)取 weex方法名
                                        aciton:'',
//                                        渲染 按鈕的鏈接  如果以http開頭 會渲染網(wǎng)絡(luò)JSBundle 反之 渲染本地JSBundle
                                        itemURL:''
                                    }
                                ],
//                            右邊選項(xiàng)按鈕
                            rightItemsInfo:
                                [
                                    {
                                        aciton:'',
                                        itemURL:host + dirctoryPath + 'DemoRefreshRightItem.js',
//                                        設(shè)計(jì)圖中container的大小
                                        frame:'{{0, 0}, {32, 16}}'
                                    }
                                ],
//                            把導(dǎo)航欄變成透明的
                            clearNavigationBar:true,
//                            把導(dǎo)航欄隱藏
                            hiddenNavgitionBar:false,
//                            導(dǎo)航欄背景顏色
                            navigationBarBackgroundColor:'',
//                            導(dǎo)航欄背景圖片
                            navgationBarBackgroundImage:'',
//                            自定義標(biāo)題視圖的JSBundle地址 如果以http開頭 會渲染網(wǎng)絡(luò)JSBundle 反之 渲染本地JSBundle
                            customTitleViewURL:'',
//                        這個是當(dāng)前導(dǎo)航欄棧頂?shù)腏SBundleURL
                            rootViewURL:host + dirctoryPath + 'index.js',
                        }
渲染過程

根據(jù)應(yīng)用的設(shè)計(jì)我們需要作出不同的AppFrame調(diào)整,這個是毋庸置疑。再次回到原生APPFrameComponent類中。這里大體說一下WXSDKInstance在iOS端的渲染過程,具體的還是要看源碼,每個人讀的時候都有自己的見解認(rèn)識。
1.從- (void)renderWithURL:(NSURL *)url開始,[WXResourceRequest requestWithURL:url resourceType:WXResourceTypeMainBundle referrer:@"" cachePolicy:NSURLRequestUseProtocolCachePolicy]處理URLRequest,_mainBundleLoader = [[WXResourceLoader alloc] initWithRequest:request]; [_mainBundleLoader start];下載JSBundle源。不贅述如何處理的URLRequest,看一下在下載之前都處理什么(代碼有缺失,詳盡請看源碼)。

- (void)renderWithURL:(NSURL *)url options:(NSDictionary *)options data:(id)data
{
    WXResourceRequest *request = [WXResourceRequest requestWithURL:url resourceType:WXResourceTypeMainBundle referrer:@"" cachePolicy:NSURLRequestUseProtocolCachePolicy];
    [self _renderWithRequest:request options:options data:data];
}
- (void)_renderWithRequest:(WXResourceRequest *)request options:(NSDictionary *)options data:(id)data;
{
    NSURL *url = request.URL;
    _scriptURL = url;
    _jsData = data;
    NSMutableDictionary *newOptions = [options mutableCopy] ?: [NSMutableDictionary new];
    
    if (!newOptions[bundleUrlOptionKey]) {
        newOptions[bundleUrlOptionKey] = url.absoluteString;
    }
    // compatible with some wrong type, remove this hopefully in the future.
    if ([newOptions[bundleUrlOptionKey] isKindOfClass:[NSURL class]]) {
        WXLogWarning(@"Error type in options with key:bundleUrl, should be of type NSString, not NSURL!");
        newOptions[bundleUrlOptionKey] = ((NSURL*)newOptions[bundleUrlOptionKey]).absoluteString;
    }
    _options = [newOptions copy];
  //獲取網(wǎng)站主頁地址
    if (!self.pageName || [self.pageName isEqualToString:@""]) {
        self.pageName = [WXUtility urlByDeletingParameters:url].absoluteString ? : @"";
    }
    //這里會配置一些請求信息 比如手機(jī)系統(tǒng) 手機(jī)型號 app名字 屏幕信息等
    request.userAgent = [WXUtility userAgent];
    //告訴監(jiān)視器WXMonitor開始下載JSBundle
    WX_MONITOR_INSTANCE_PERF_START(WXPTJSDownload, self);
    __weak typeof(self) weakSelf = self;
    _mainBundleLoader = [[WXResourceLoader alloc] initWithRequest:request];;
    _mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        //這里省略源碼,此處驗(yàn)證請求回調(diào)數(shù)據(jù)是否有效 并告知WXMonitor請求結(jié)果
        NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        if (!jsBundleString) {
            WX_MONITOR_FAIL_ON_PAGE(WXMTJSDownload, WX_ERR_JSBUNDLE_STRING_CONVERT, @"data converting to string failed.", strongSelf.pageName)
            return;
        }
        WX_MONITOR_SUCCESS_ON_PAGE(WXMTJSDownload, strongSelf.pageName);
        WX_MONITOR_INSTANCE_PERF_END(WXPTJSDownload, strongSelf);
        [strongSelf _renderWithMainBundleString:jsBundleString];
    };
    _mainBundleLoader.onFailed = ^(NSError *loadError) {
//此處省略源碼 處理報(bào)錯信息
    };
    [_mainBundleLoader start];
}

2.開始渲染JSBundle,
首先會驗(yàn)證WXSDKInstance的一些信息是否有誤,并打出相應(yīng)Log。
接下來會創(chuàng)建WXRootView調(diào)用instanceonCreate(UIView * view){ } block。
重新調(diào)用一下引擎默認(rèn)配置(

{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self _registerDefaultComponents];
        [self _registerDefaultModules];
        [self _registerDefaultHandlers];
    });
}```)
通過```WXSDKManager```取得```WXBridgeManager```實(shí)例,調(diào)用```- (void)createInstance:(NSString *)instance template:(NSString *)temp   options:(NSDictionary *)options data:(id)data
```還是會驗(yàn)證參數(shù)是否有效,看渲染堆```instanceIdStack```中是否含有該```instanceId```,有的話看是否有在渲染的```instanceId```,有就添加任務(wù),沒有就把該id置頂創(chuàng)建,讓```WXBridgeContext```實(shí)例去渲染(該操作驗(yàn)證并必須在```WXBridgeThread```)。到此就開始了JS引擎的渲染,可想而知會生成DOM樹,然后逐個解析UI Component(js call native 創(chuàng)建以及渲染UI)會從```WXComponent```的```initWithRefxxxxx```開始。
######應(yīng)用BasicWeexViewController的實(shí)現(xiàn)
不多贅述,按照官方給的思路,很容易就能封裝出。這是我項(xiàng)目中的[XMWXViewController](https://github.com/jiaowoxiaoming/XMWeex/blob/master/XMWeex/XMWeex/ViewControllers/XMWXViewController.m)
######AppFrameComponent 實(shí)現(xiàn)
接上,AppFrame要做的就是將下發(fā)的tabbarItem 和 navigationItem形成框架 和UI。不同UI設(shè)計(jì)對用不同的AppFrameComponent 實(shí)現(xiàn)。
下面是最常用最簡單(我項(xiàng)目中的[XMWXAPPFrameComponte](https://github.com/jiaowoxiaoming/XMWeex/blob/master/XMWeex/XMWeex/WeexComponent/APPFrame/Basic/XMWXAPPFrameComponte.m))的實(shí)現(xiàn):

pragma mark - private method

/**
初始化APP框架

@param attributes 返回的RenderInfo
*/
-(void)firstInitAPPFrameWithAttributes:(NSDictionary *)attributes
{
dispatch_async(dispatch_get_main_queue(), ^{
//設(shè)置APP
UIApplication * application = [UIApplication sharedApplication];
UITabBarController * tabarViewController = [[UITabBarController alloc] init];
// tabarViewController.view.alpha = 0;
UIWindow * window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[((UIResponder *)application.delegate) setValue:window forKey:@"window"];

    window.rootViewController = tabarViewController;
    
    window.backgroundColor = [UIColor whiteColor];
    
    [window makeKeyAndVisible];
    
    [self handleTabbarViewControllers:attributes tabarController:tabarViewController];
});

}
/**
創(chuàng)建tabbar.items
@param attributes component 下發(fā)數(shù)據(jù)
@param tabarController [UIApplication sharedApplication].keyWindow.rootViewController
@return 創(chuàng)建的UITabBarItem集合
*/
-(NSMutableArray <UITabBarItem *> *)handleTabbarItems:(NSDictionary *)attributes tabarController:(UITabBarController __kindof * )tabarController
{
NSString * tabItemsDictJsonString = [WXConvert NSString:attributes[XMWXAPPFrameComponteTabbarItemsKey]];

NSArray * tabItemsInfoArray = [NSJSONSerialization JSONObjectWithData:[tabItemsDictJsonString dataUsingEncoding:NSUTF8StringEncoding] options:(NSJSONReadingAllowFragments) error:nil];

NSMutableArray * tabarItems = [NSMutableArray array];

[tabItemsInfoArray enumerateObjectsUsingBlock:^(NSDictionary *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    XMWXTabbarItem * xmItem = [XMWXTabbarItem itemWithDict:obj];
    UITabBarItem * item = [[UITabBarItem alloc] init];
    
    item.title = xmItem.title;
    
    if (xmItem.tintColor.length) {
        [tabarController.tabBar setTintColor:colorWithHexString(xmItem.tintColor, 1.f)];
    }
    
    if (xmItem.normalTitleColor.length) {
        [item setTitleTextAttributes:@{NSForegroundColorAttributeName:colorWithHexString(xmItem.normalTitleColor, 1.f)} forState:(UIControlStateNormal)];
    }
    if (xmItem.selectedTitleColor.length) {
        [item setTitleTextAttributes:@{NSForegroundColorAttributeName:colorWithHexString(xmItem.selectedTitleColor, 1.f)} forState:(UIControlStateSelected)];
    }
    if ([xmItem.image hasPrefix:@"http"]) {
        
        [[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:xmItem.image] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
            
        } completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
            item.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
            [tabarController.tabBar setItems:tabarItems];
        }];
    }else
    {
        item.image = xmwx_imageForSetting(xmItem.image);
    }
    
    if ([xmItem.selectedImage hasPrefix:@"http"]) {
        
        [[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:xmItem.selectedImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
            
        } completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
            item.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];

            [tabarController.tabBar setItems:tabarItems];
        }];
        
    }else
    {
        item.selectedImage = xmwx_imageForSetting(xmItem.selectedImage);
    }
    
    [tabarItems addObject:item];
}];
return tabarItems;

}
/**
渲染TabbarViewController

@param attributes component 下發(fā)數(shù)據(jù)
@param tabarController [UIApplication sharedApplication].keyWindow.rootViewController
*/
-(void)handleTabbarViewControllers:(NSDictionary *)attributes tabarController:(UITabBarController __kindof * )tabarController
{
NSMutableArray <UITabBarItem *> * tabbarItems = [self handleTabbarItems:attributes tabarController:tabarController];
NSArray * viewControllerItems = [NSJSONSerialization JSONObjectWithData:[[WXConvert NSString:[attributes objectForKey:XMWXAPPFrameComponteViewControllerItemsKey]] dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];

NSMutableArray * viewControllers = [NSMutableArray array];

[viewControllerItems enumerateObjectsUsingBlock:^(NSDictionary *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    XMWXNavigationItem * navigationItem = [XMWXNavigationItem infoWithDict:obj];
    
    XMWXViewController * viewController = [[XMWXViewController alloc] init];
    
    viewController.renderInfo = navigationItem;
    if (navigationItem.rootViewURL.length > 0) {
        if ([navigationItem.rootViewURL hasPrefix:@"http"]) {
            viewController.renderURL = [NSURL URLWithString:navigationItem.rootViewURL];
        }else
        {
            NSString * path = [[NSBundle mainBundle] pathForResource:navigationItem.rootViewURL ofType:@""];
            if (path) {
                viewController.renderURL = [NSURL fileURLWithPath:path];
            }
        }
    }
    
    XMWXViewController * __weak weakViewController = viewController;
    viewController.instance.onCreate = ^(UIView * view)
    {
        XMWXViewController * __strong vc = weakViewController;
        [vc.view addSubview:view];
    };
    viewController.instance.frame = viewController.view.bounds;
    viewController.instance.onLayoutChange = ^(UIView *view)
    {
        XMWXViewController * __strong vc = weakViewController;
        vc.instance.frame = vc.view.bounds;
    };
    
    UINavigationController * nav = [[UINavigationController alloc] initWithRootViewController:viewController];
    nav.tabBarItem = [tabbarItems objectAtIndex:idx];
    [viewControllers addObject:nav];
}];

[tabarController setViewControllers:viewControllers animated:YES];

}
/**
數(shù)據(jù)更改的時候調(diào)用
@param attributes component屬性數(shù)據(jù)
*/
-(void)updateAttributes:(NSDictionary *)attributes
{
if ([UIApplication sharedApplication].keyWindow.rootViewController) {
[self handleTabbarItems:attributes tabarController:(UITabBarController *)[UIApplication sharedApplication].keyWindow.rootViewController];
[self handleTabbarViewControllers:attributes tabarController:(UITabBarController *)[UIApplication sharedApplication].keyWindow.rootViewController];
}else
{
[self firstInitAPPFrameWithAttributes:attributes];
}

}

到此原生Component已經(jīng)完成。
那么我們需要回到我們初始化Weex環(huán)境的方法中加入```    //通過配置這個Component參數(shù)來配置程序框架HTML標(biāo)簽名
    [WXSDKEngine registerComponent:@"AppFrame" withClass:NSClassFromString(@"xxxAPPFrameComponte")];```
######開始書寫[AppFrame.Vue](https://github.com/jiaowoxiaoming/app_weex/blob/master/app_weex/src/Components/Frame/AppFrame.vue)
回到WebStorm,加入如下代碼

<template>
<AppFrame id='AppFrame' :tabarItems="tabbarItemsJsonString" :viewControllerItems="viewControllerItemsString"></AppFrame>
</template>這樣就可以使用我們擴(kuò)展的AppFrameComponenttabbarItemsJsonStringviewControllerItemsString```需要添加下面代碼

<script>
    export default {
        data () {
            return {
                tabbarItemsJsonString:JSON.stringify(
                    [{上邊的TabbarItem數(shù)據(jù)}]),
                viewControllerItemsString:JSON.stringify(
                    [{上邊的navigationBarItem數(shù)據(jù)}]),
            };
        },
        methods: {}
    }
</script>

完整的AppFrame.Vue
到這里其實(shí)就可以編譯我們的Xcode工程了。

開發(fā)中的聯(lián)動調(diào)試

那么如何看我們的效果呢?可能從一開始就應(yīng)該跟大家說如何聯(lián)動調(diào)試,但是我覺得讀到這里大家并沒有開始著手做,只是看,所以也就沒有中間如何聯(lián)動調(diào)試的步驟,大家真的上手做的時候,中間是少不了的,到時候大家再看下面內(nèi)容。
建議大家還是使用WebStorm,本文就是基于此IDE開發(fā)的Weex Project。
還有這里涉及到單頁面的調(diào)試還是應(yīng)用的整體調(diào)試。單頁面調(diào)試還是用Weex官方提供的Playground,如何進(jìn)行單頁面的調(diào)試,Weex文檔說明了。那么如何進(jìn)行整個應(yīng)用的聯(lián)動調(diào)試呢?
其實(shí)就是實(shí)時將Weex project 打包,然后用部署到本機(jī)服務(wù)器。通過掃碼或者本地寫死AppFrame的renderURL。
Weex沒有提及這樣的調(diào)試,這里就詳細(xì)說明。
在Weex Project中找到package.json文件更改serve后的端口(或者直接使用8080,可以更改為8081什么的)。操作如圖

更改端口.png

然后在WebStorm中打開終端(使用終端App的話需要先cd到項(xiàng)目目錄下)執(zhí)行

npm install

npm run serve

成功如下圖


run serve.png

創(chuàng)建一個dist目錄
然后在終端輸入

weex compile src dist

上邊命令是將src目錄下全部生成JSBundle文件
下面命令是針對某一個Vue文件生成JSBundle

weex compile src/xxx.vue dist

Weex打包JSBundle.png

這個時候我們就可以將
http://本機(jī)IP:8083/dist/components/Frame/AppFrame.js
轉(zhuǎn)化成二維碼,用XMWeex編譯的App掃描生成的二維碼或者將你自己現(xiàn)在開發(fā)的加一個二維碼掃描,甚至你也可以寫死,直接渲染上述地址。
如此方式實(shí)現(xiàn)AppDelegate
代碼摘要:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    UITabBarController * tabarViewController = [[UITabBarController alloc] init];
    
    UIWindow * window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window = window;
    window.rootViewController = tabarViewController;
    
    
    window.backgroundColor = [UIColor whiteColor];
    [window makeKeyAndVisible];
    [WXAppConfiguration setAppGroup:@"application"];
    [WXAppConfiguration setAppName:@"application"];
    [WXAppConfiguration setAppVersion:@"1.0"];
    
    //init sdk enviroment
    [WXSDKEngine initSDKEnvironment];
    [WXSDKEngine registerModule:@"XMWXModule" withClass:NSClassFromString(@"XMWXModule")];
    [WXSDKEngine registerHandler:[XMWXWebImage new] withProtocol:@protocol(WXImgLoaderProtocol)];
    //通過配置這個Component參數(shù)來配置程序框架HTML標(biāo)簽名
    [WXSDKEngine registerComponent:@"AppFrame" withClass:NSClassFromString(@"XMWXAPPFrameComponte")];
    
#if TARGET_IPHONE_SIMULATOR//模擬器
    NSString * renderURL = @"http://192.167.0.3:8083/dist/components/Frame/AppFrame.js";
    //    NSString * renderURL = [NSString stringWithFormat:@"%@%@",host,@"AppFrame.weex.js"];
    [self instance:renderURL];
    
#elif TARGET_OS_IPHONE//真機(jī)
    XMWXScanViewController * scanVC = [[XMWXScanViewController alloc] init];
    tabarViewController.viewControllers = @[scanVC];
#endif
    
    [WXLog setLogLevel:WXLogLevelError];
    return YES;
}
-(WXSDKInstance *)instance:(NSString *)renderURLString
{
    if (!_instance) {
        _instance = [[WXSDKInstance alloc] init];
        [[NSURLCache sharedURLCache] removeAllCachedResponses];
//
        [_instance renderWithURL:[NSURL URLWithString:renderURLString]];
        
    }
    return _instance;
}

這個時候你已經(jīng)能得到類似如下的App框架。

AppFrame.jpg

如果讀到這,你會發(fā)現(xiàn)其實(shí)我們的這個AppFrame的頁面并沒有開發(fā)。其實(shí)渲染出的就是一個ViewController。
那么下面我們要做的就是開發(fā)每一個模塊。循序漸進(jìn),從
下一篇Weex 從無到有開發(fā)一款上線應(yīng)用 2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容