用Swift写WeexDemo

进来公司要用Weex做个项目试试水,于是我就写下来一些自己的记录了,说不好会从入门到放弃到转RN,但是既然折腾了,就记录一下吧。

基本环境什么的就要看官方文档进行了,然后下面我们参考着集成Weex到已有应用使用weex-toolkit来做。

创建Weex项目

执行命令:

  1. cd ~/Documents切换到文档目录,
  2. weex create darkweex创建一个weex项目,
  3. cd darkweex进入项目目录,
  4. weex platform add ios添加ios demo工程,
  5. weex run ios跑起ios demo工程。

执行完最后一条命令过了一会儿,如果有错误,你可能需要挂梯子,没错误的话,终端提示:

? Choose one of the following devices
iPhone 5s ios: 9.0
iPhone 6 ios: 10.0
❯ iPhone 6 ios: 11.2
iPhone 6 ios: 8.1
iPhone 6 ios: 9.0
iPhone 6 Plus ios: 10.0
iPhone 6 Plus ios: 11.2
(Move up and down to reveal more choices)

按方向下键移动到你想执行的目标设备,我这里选iPhone 6 11.2,跑起来就是这个鬼样子了:

经过第一次weex run ios,接下来你就可以执行./start来跑起web服务器了(会跳出一个网站),它可以随时刷新显示vue文件最新的代码效果。注意如果没有跳出网站,你留意控制台打印出来的:

server is running! Please open http://192.168.1.121:8080/
Project is running at http://192.168.1.121:8081/
webpack output is served from /
404s will fallback to /index.html

这个http://192.168.1.121:8081就是web地址了,在浏览器打开就能看到和上面app一样的样子:

这个页面是如何产生的呢?答案就在src/index.vue里:

<template>
<div class="wrapper" @click="update">
<image :src="logoUrl" class="logo"></image>
<text class="title">Hello {{target}}</text>
<text class="desc">Now, let's use vue to build your weex app.</text>
</div>
</template>

<style>
.wrapper { align-items: center; margin-top: 120px; }
.title { padding-top:40px; padding-bottom: 40px; font-size: 48px; }
.logo { width: 360px; height: 156px; }
.desc { padding-top: 20px; color:#888; font-size: 24px;}
</style>

<script>
export default {
data: {
logoUrl: 'http://img1.vued.vanthink.cn/vued08aa73a9ab65dcbd360ec54659ada97c.png',
target: 'World'
},
methods: {
update: function (e) {
this.target = 'Weex'
console.log('target:', this.target)
}
}
}
</script>

我们把Now, let's use vue to build your weex app.改成hahaha,然后看看刚才浏览器的页面:

我们甚至都没有刷新页面,这是start脚本带来的效果,它根据你vue文件代码的改变,实时编译出效果(个人猜测)。

至于我们的iPhone 6,似乎没有变化呀?

少年,app是要Xcode来编译的……我们来打开platforms/ios目录下的WeexDemo.xcworkspaceCMD + R看看。

咦?似乎没有变成hahaha……我们来看看weex是如何加载出这个页面的,就知道为什么没有变化了。

index.js

首先必须看didFinishLaunchingWithOptions啦,发现就一句值得留意[WeexSDKManager setup];,进去看看,忽略预编译代码,重点就是两句了:

[self initWeexSDK];
[self loadCustomContainWithScannerWithUrl:url];

url是常量BUNDLE_URL:

#define BUNDLE_URL [NSString stringWithFormat:@"file://%@/bundlejs/index.js",[NSBundle mainBundle].bundlePath]

看到这里就真相大白了,用WXDemoViewController去加载一个js文件,具体路径是bundlejs/index.js,我们去项目目录看看这个文件:

虽然代码乱了点,这是因为这个index.js是从index.vue编译出来的。

那我们如果要在ios demo项目获得最新的index.js的话,就要执行编译vue到js的命令,然后把dist文件夹的内容全部复制到platforms/ios/bundlejs里:

我们可以加一条便捷的命令在package.json里面,注意上一行末尾要加逗号:

"buildios": "npm run build && rm -rf ./platforms/ios/bundlejs && cp -r ./dist/ ./platforms/ios/bundlejs/"

加入之后保存文件,终端执行npm run buildios:

在Xcode里CMD+R重新运行项目,这次hahaha就出来了。

集成Weex

接下来其实我们就可以参照darkweex这个ios demo的配置来创建自己的Project了。

首先在Xcode新建一个Project叫SwiftWeex,类型是Single View的就行了,然后按你自己的喜好组织好文件,编译通过。
由于我们的项目是整个由web前端写,然后我们native只负责提供一些特性功能给web端使用,所以接下来我会以native为壳,加载web前端用Vue.JS写的东西。

先来看看AppDelegate原始状态:

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

let vc = ViewController()
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = UIColor.white
window?.rootViewController = UINavigationController(rootViewController: vc)
window?.makeKeyAndVisible()
return true
}
}

看看darkweekpodfile有什么第三方,都添加到我们SwiftWeex项目的podfile里,然后pod install

platform :ios, '9.0'

target 'SwiftWeex' do
use_frameworks!

pod 'WeexSDK'
pod 'WXDevtool'
pod 'WeexPluginLoader'
pod 'SDWebImage', '3.7.5'
pod 'SocketRocket', '0.5.1'

end

darkweekbundlejs文件夹复制到我们的新项目文件夹下,记得以真实(蓝色)文件夹的方式添加到Xcode里:

接下来仿照AppDelegate的代码写就行了,但是首先还是要创建一个桥接头文件(Bridging Header),后面写module,handler,component等要用到。创建桥接这个也是Swift/OC混编必备技能,不详细说,看图:

然后AppDelegate先import WeexSDK,然后写代码咯:

// MARK: - 各种配置加载
extension AppDelegate {

/// 初始化weex配置
fileprivate func configWeex() {
// 基本信息
WXAppConfiguration.setAppGroup("delite")
WXAppConfiguration.setAppName("SwiftWeexDemo")
WXAppConfiguration.setAppVersion("1.0.0")

// Log详细程度
WXLog.setLogLevel(.log)

// 注册各种module,component,handler
WXSDKEngine.registerHandler(WXImageLoader(), with: WXImgLoaderProtocol.self)

// 初始化 WeexSDK
WXSDKEngine.initSDKEnvironment()
}
}

WXImageLoader是我自己实现的图片加载类:

import UIKit
import WeexSDK
import SDWebImage

class WXImageLoader: NSObject, WXImgLoaderProtocol {

func downloadImage(withURL url: String!, imageFrame: CGRect, userInfo options: [AnyHashable : Any]! = [:], completed completedBlock: ((UIImage?, Error?, Bool) -> Void)!) -> WXImageOperationProtocol! {
Log("weex加载图片:", url)
guard let url = url else {
Log("weex传入的图片URL无效")
return nil
}
let fullURL = url.hasPrefix("//") ? ("http:" + url) : url
guard let validURL = URL(string: fullURL) else {
Log("weex图片URL无效:", fullURL)
return nil
}

let downloadTask = SDWebImageManager.shared().downloadImage(with: validURL, options: .retryFailed, progress: nil) { (image, error, cacheType, finished, imageURL) in
completedBlock?(image, error, finished)
}
return downloadTask as? WXImageOperationProtocol
}

}

然后参照demo工程里的WXDemoViewController,把我们的ViewController改造成加载index.js的容器,我简化了一些我自己觉得暂时用不到的代码(比如viewWillLayoutSubview什么的):

import UIKit
import WeexSDK

class ViewController: UIViewController {

var jsURL: URL?

fileprivate var instance: WXSDKInstance?
fileprivate var weexView = UIView()


override func viewDidLoad() {
super.viewDidLoad()
assert(jsURL != nil, "未赋值jsURL")
title = "示例"
view.backgroundColor = .white
navigationController?.setNavigationBarHidden(true, animated: false)
render()
}

deinit {
instance?.destroy()
Log("deinit")
}


override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateInstanceState(to: .WeexInstanceAppear)
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
updateInstanceState(to: .WeexInstanceDisappear)
}



func render() {
guard jsURL != nil else {
Log("jsURL is nil")
return
}
instance?.destroy()
instance = WXSDKInstance()
instance?.viewController = self
instance?.frame = CGRect(origin: .zero, size: view.bounds.size)
instance?.onCreate = { [unowned self] view in
guard let v = view else { return }
self.weexView.removeFromSuperview()
self.weexView = v
self.view.addSubview(self.weexView)
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, v)
}
instance?.onFailed = { error in
Log("instance failed:\(error?.localizedDescription ?? "")")
}
instance?.renderFinish = { [weak self] view in
Log("instance render finish")
self?.updateInstanceState(to: .WeexInstanceAppear)
}
instance?.updateFinish = { view in
Log("instance update finish")
}
instance?.render(with: jsURL, options: ["bundleUrl": WeexBundleFolder], data: nil)
}


fileprivate func updateInstanceState(to newState: WXState) {
guard instance?.state != newState else { return }
instance?.state = newState
switch newState {
case .WeexInstanceAppear:
WXSDKManager.bridgeMgr().fireEvent(instance?.instanceId, ref: WX_SDK_ROOT_REF, type: "viewappear", params: nil, domChanges: nil)
case .WeexInstanceDisappear:
WXSDKManager.bridgeMgr().fireEvent(instance?.instanceId, ref: WX_SDK_ROOT_REF, type: "viewdisappear", params: nil, domChanges: nil)
default:
break
}
}

}

然后didFinishLaunchingWithOptions变成这样了:

configWeex()

let vc = ViewController()
vc.jsURL = URL(string: WeexBundleFolder + "index.js")
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = UIColor.white
window?.rootViewController = UINavigationController(rootViewController: vc)
window?.makeKeyAndVisible()
return true

顺带一提,WeexBundleFolder是我定义的一个全局常量:

let WeexBundleFolder = String(format: "file://%@/bundlejs/", Bundle.main.bundlePath)

CMD+R,你会发现文字变成了hahaha,但是图片却不见了,这是怎么回事?留意一下控制台,你会发现是ATS的问题,去Info.plist添加一下就好了。

demo代码

感觉简单的demo写到这里就刚刚好了,其他插件调用什么的留到下一篇再写了,SwiftWeex的代码在github这里了。