文章

Weex

Weex

Weex 开发环境安装

开发环境

1
2
3
4
5
6
JDK7.0+
AS
Node.js
npm
weex
webpack

注意: 如果安装失败,用 root 运行,前面加 sudo

1、安装 nodejs

直接https://nodejs.org/en/download/下载安装>

检测安装成功:

1
node -v

2、安装 npm

安装完 nodejs 就会自带了 npm,也可以使用淘宝的 cnpm,npm install -g cnpm --registry=https://registry.npm.taobao.org,之后就可以用 cnpm 替代 npm 了

检测安装成功:

1
npm -v

3、安装 weex-toolkit

安装 weex-toolkit

1
npm install weex-toolkit -g

检测安装成功:

1
weex -v

4、安装 weexpack

1
npm install weexpack -g

检测安装成功:

1
weexpack -V // 大写的-V或--version

5、安装全局 webpack

使用 npm 安装 webpack,如果你安装很慢,这个是可以使用 cnpm 来进行安装的

1
2
npm install webpack -g
sudo npm install webpack-cli -g // 可能需要手动安装webpack-cli

检测安装成功:

1
webpack -v

开发工具

  • WebStorm
  • VSCode

插件 vetur

创建 weex 工程

  • 新建 helloweex(不能大写)
1
2
3
weex create helloweex
# 或者
weex w init helloweex
  • 添加 Android 应用支持 (安装好后可以去项目根目录的 platforms 下看到 android 文件夹,说明应用支持添加成功。)
1
weex platform add android
  • 安装依赖的第三方 js 包
1
npm install
  • 启动本地 web 服务,浏览器预览渲染效果
1
2
weex run start
// 或者 npm start
  • 运行项目和服务器
1
2
weex run dev 
weex run serve
  • 运行到 Android 设备
1
2
3
weex run android // 选择设备进行安装
// 或者
npm run android
  • 打包 Android
1
weex run pack:android
  • clean
1
weex run clean:android

  • 常用的 npm 命令
1
2
3
4
5
npm start  // 在本地server,打开浏览器预览

npm run android // 运行android

npm run pack:android // 打个android apk包

Weex 环境变量

https://weex.apache.org/zh/docs/api/weex-variable.html

每个 Weex 页面的 JS 上下文中都有一个相互独立的 weex 变量,它可以像全局变量一样使用,不过它在不同页面中是隔离而且只读的。

注意: weex 实例变量只在 Vue 框架中暴露了,目前还不支持在 Rax 框架中使用。

1
2
3
4
5
6
declare type Weex = {
  config: WeexConfigAPI;
  document: WeexDocument;
  requireModule: (name: string) => Object | void;
  supports: (condition: string) => boolean | void;
}

config

document

requireModule

supports

Weex 语法

https://weex.apache.org/zh/guide/introduction.html

  • weex 最外层只能使用 <template>,且只能有一个子标签
  • 在写 Css 样式时,必须使用类名或者 ID 进行设置,其他选择器不起作用

Weex 自定义发送事件 (native→js)

https://weex.apache.org/zh/docs/api/android-apis.html#自定义发送事件

fireEvent

向 JS 环境发送一些事件,比如 click 事件

  1. void fireEvent(elementRef,type)
  2. void fireEvent(elementRef,type, data)
  3. void fireEvent(elementRef,type,data,domChanges)
1
2
3
4
elementRef(String):产生事件的组件id
type(String): 事件名称,weex默认事件名称格式为"onXXX",比如OnPullDown
data(Map<String, Object>): 需要发送的一些额外数据,比如click时,view大小,点击坐标等等。
domChanges(Map<String, Object>): 目标组件的属性和样式发生的修改内容

通过 WXSDKInstace 调用

注:在 WxComponent 可以直接调用 fireEvent

fireGlobalEvent

https://weex.apache.org/zh/docs/modules/globalEvent.html#globalevent

Weex 常用命令和热更新

weex 中常用的 npm 命令。这些 npm xxx 的命令,其实都是在 package.json 里设置的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"scripts": {
    "start": "npm run serve",
    "build": "webpack --env.NODE_ENV=common",
    "build:prod": "webpack --env.NODE_ENV=production",
    "build:prod:web": "webpack --env.NODE_ENV=release",
    "build:plugin": "webpack --env.NODE_ENV=plugin",
    "clean:web": "rimraf ./release/web",
    "clean:ios": "rimraf ./release/ios",
    "clean:android": "rimraf ./release/android",
    "dev": "webpack --env.NODE_ENV=common --progress --watch",
    "unit": "karma start test/unit/karma.conf.js --single-run",
    "test": "npm run unit",
    "lint": "eslint --ext .js,.vue src  test/unit --fix",
    "serve": "webpack-dev-server --env.NODE_ENV=development --progress",
    "ios": "weex run ios",
    "web": "npm run serve",
    "android": "weex run android",
    "pack:ios": "npm run clean:ios && weex build ios",
    "pack:android": "npm run clean:android && weex build android",
    "pack:web": "npm run clean:web && npm run build:prod:web"
},

npm run dev

给我们进行实时的压缩混淆操作(也叫编译操作),生成 dist 目录下的 index.js 文件和 index.web.js 文件。
命令开启后,我们每次修改 src 下的文件会自动给我们编译。所以我们在开发时需要最先开启这个命令。

1
npm run dev

npm run serve

虽然可以用 Android Studio 进行看效果,但是我们还是需要一个 web 端来支持我们开发预览的,这时候我们可以启用 npm run serve 来解决问题,他会给我们在浏览器中打开我们的 vue 页面(但是需要注意的是,你现在看到的并不是程序的最终样式和结果)。

1
npm run serve

npm run build

npm run build:prod

打包

Weex Adapter

image

http

websocket

Weex 加载图片

网络图片

本地图片

drawable 图片

assets 图片

案例

以 Fresco 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
final class ImageUrlHelper {

    private static final String TAG = "qsbk.weex";
    private static final String PREFIX_ASSETS = "asset://";
    private static final String PREFIX_DRAWALBE = "res://";
    private static final String PREFIX_MIPMAP = "mipmap://";

    static String findUrl(@NonNull String url) {
        String temp = url;
        if (temp.startsWith("//")) {
            temp = "https:" + temp;
            LogUtils.i(TAG, "url以//开头,认为是https图片:" + temp);
        } else if (temp.startsWith(PREFIX_DRAWALBE)) {
            // fresco加载本地drawable格式: res://packagename/resId

            String name = temp.substring(PREFIX_DRAWALBE.length());
            int resId = GlobalContext.getAppContext().getResources().getIdentifier(name, "drawable", AppUtils.getAppPackageName());
            if (resId == 0) {
                LogUtils.w(TAG, "没有找到合适的资源: " + url);
//                throw new IllegalArgumentException("没有找到合适的资源: " + url);
            } else {
                temp = PREFIX_DRAWALBE + AppUtils.getAppPackageName() + "/" + resId;
                LogUtils.i(TAG, "url以res://开头,从drawable加载图片:" + temp);
            }
        } else if (temp.startsWith(PREFIX_ASSETS)) {
//            file:///android_asset/
            String name = temp.substring(PREFIX_ASSETS.length());
            temp = PREFIX_ASSETS + "/" + name;
            LogUtils.i(TAG, "url以asset://开头,从asset加载图片:" + temp);
        }
        return temp;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<image :src="logo" class="localImage" />
<image :src="res1" class="localImage" />
<image :src="res2" class="localImage" />

logo: getPackImage('1.png'),
res1: "res://qb_s_00",
res2: "asset://qb_s_02.png",

// 包内图片必须使用改函数包裹
const getImg=function(name) {
  let platform = weex.config.env.platform
  let path = ''
  if (platform == 'Web') { path = `web/devAssets/${name}` }
  else{ path = `assets/${name}` }
  return path
}
export default getImg

Weex Module

结果回调

https://weex.apache.org/zh/docs/api/android-apis.html#结果回调

  1. void invoke(Object data);

只能调用一次,在调用结束后销毁

  1. void invokeAndKeepAlive(Object data);

可以持续调用,一般用这个

JS 调用时,有的场景需要返回一些数,比如以下例子,返回 x、y 坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WXLocation extends WXModule {
    @JSMethod
    public void getLocation(JSCallback callback){
    //Get the code for the location information .....
    Map<String,String> data=new HashMap<>();
    data.put("x","x");
    data.put("y","y");
    //notify once
    callback.invoke(data);
    //Continuous connection
    callback.invokeAndKeepAlive(data);
    //Invoke method and invokeAndKeepAlive two methods of choice  }
}

跟随 Activity 生命周期

注册时,不能注册为 global 的。

1
2
3
public static <T extends WXModule> boolean registerModule(String moduleName, Class<T> moduleClass,boolean global) throws WXException {
    return moduleClass != null && registerModule(moduleName, new TypeModuleFactory<>(moduleClass), global);
  }

可以重写的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  /** hook the Activity life cycle to Instance module**/
  public void onActivityResult(int requestCode, int resultCode, Intent data){}

  public void onActivityCreate(){}

  public void onActivityStart(){}

  public void onActivityPause(){}

  public void onActivityResume(){}

  public void onActivityStop(){}

  public void onActivityDestroy(){}

  public boolean onActivityBack() {return false;}

  public boolean onCreateOptionsMenu(Menu menu){return false;}

案例

  • PhoneInfoModule

获取手机信息 Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PhoneInfoModule extends WXModule {
    @JSMethod(uiThread = false)
    public void getPhoneInfo(JSCallback callback) {
        Map<String, String> infos = new HashMap<>();
        infos.put("board", Build.BOARD);
        infos.put("brand", Build.BRAND);
        infos.put("device", Build.DEVICE);
        infos.put("model", Build.MODEL);
        callback.invoke(infos);
    }
    @JSMethod(uiThread = true)
    public void printLog(String msg) {
        Toast.makeText(mWXSDKInstance.getContext(), msg, Toast.LENGTH_SHORT).show();
    }
}
  • 注册 Module
1
WXSDKEngine.registerModule("phoneInfo", PhoneInfoModule.class);
  • vue 是使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
  <div class="wrapper">
    <image src="https://gtms02.alicdn.com/tps/i2/TB1QHKjMXXXXXadXVXX20ySQVXX-512-512.png">111</image>
      <text style="font-size:24" @click="printLog">这是一个24size的text</text>
      <text style="font-size:24" @click="printLog('调用module的printLog,并传递参数')">调用module的printLog,并传递参数</text>
      <text class="large">text 样式</text>
      <text class="greeting" @click="getPhoneInfo">getPhoneInfo!!!</text>
  </div>
</template>

<script>
 const modal = weex.requireModule('modal');
 const phoneInfo = weex.requireModule('phoneInfo')
 
export default {
 name: 'HelloWorld',
 data() {

 },
 methods:{
    printLog(msg) {
      console.log('print:')
      phoneInfo.printLog("从weex传递过来的");
    },
      // 调用 PhoneInfoModule 中的 getPhoneInfo(),并回调给 weex
    getPhoneInfo() {
      console.log('输出getPhoneInfo:')
      phoneInfo.getPhoneInfo(function (e) {
          modal.alert({
              message: JSON.stringify(e),
              duration: 0.3
          })
      });
    }
 }
}
</script>

<style>
  .wrapper {
        justify-content:flex-start;
        align-items:flex-start;
    }
  .large{
    font-size:30;
  }
  .greeting{
        text-align: center;
        margin-top: 10px;
        font-size: 50px;
        color: #41B883;
    }
</style>

Weex Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class RichTextComponent extends WXComponent<TextView> {

    public RichTextComponent(WXSDKInstance instance, WXVContainer parent, BasicComponentData basicComponentData) {
        super(instance, parent, basicComponentData);
    }

    public RichTextComponent(WXSDKInstance instance, WXVContainer parent, int type, BasicComponentData basicComponentData) {
        super(instance, parent, type, basicComponentData);
    }

    @Override
    protected TextView initComponentHostView(@NonNull Context context) {
        final TextView textView = new TextView(context);
        textView.setTextSize(20);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), "click: " + textView.getText().toString(), Toast.LENGTH_SHORT).show();
            }
        });
        return textView;
    }

    @WXComponentProp(name = "tel")
    public void setTel(String number) {
        SpannableString spannableString = new SpannableString("tel:" + number);
        ImageSpan imageSpan = new ImageSpan(getContext(), R.mipmap.ic_launcher);
        spannableString.setSpan(imageSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        getHostView().setText(spannableString);
    }

}
  • 注册 Component
1
 WXSDKEngine.registerComponent("richtext", RichTextComponent.class, false);
  • vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
  <div class="wrapper">
      <richtext tel="10086" style="width:200px;height:200px">111</richtext>
  </div>
</template>


<style>
  .wrapper {
        justify-content:flex-start;
        align-items:flex-start;
    }
</style>

自定义 Component

Weex Vue 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
  <div class="wrapper">
    <animBucket ref="animBucket" class="animBucket" @click="showAnim">
      <text class="text">本地webp静图</text>
      <qbimg class="qbimg" src="laugh.png1" autoBitmapRecycle="false" placeholder="assets/4.png" />
      <text class="text">本地webp动图</text>
    </animBucket>
  </div>
</template>

<script>
const modal = weex.requireModule("modal");

export default {
  name: "index",
  data() {},
  methods: {
    showAnim() {
      modal.toast({
        message: "弹出动画从weex",
        duration: 1.5
      });
      this.$refs.animBucket.showAnimation(); // $refs表示有个ref
    }
  }
};
</script>

<style scoped>
.wrapper {
  justify-content: center;
  align-items: center;
}
.animBucket {
  width: 400px;
  height: 500px;
  background-color: transparent;
  border-width: 2px;
  border-color: #0ff;
  border-style: solid;
}
</style>

Weex Navigator

Weex 控制 Android 返回键解决方案

https://segmentfault.com/a/1190000010035286

weex 组件之 text

text

text 组件是 weex 内置的组件,他是用来放至文本的容器

1.超出显示省略号

text 组件提供一个 lines 的样式,直接把这个样式写在 css 里就可以生效了,并且带了省略号。这里的坑就是不要写在标签的属性上,而是要写在样式里。

Weex 坑集锦

GSYGithubAppWeex question

https://github.com/CarGuo/GSYGithubAppWeex/blob/master/question.md

loading-indicator 组件

loading-indicator 在 Android 上没有动画,官方 demo 也不行

richtext 组件

富文本阴影,在 iOS 会显示一行,而不是跟随内容

  1. navigator 跳转隐式意图弹个选择框
  2. navigator 多页面打开失败 FileUriExposedException

Weex navigator 多页面打开失败 FileUriExposedException

https://github.com/alibaba/weex/issues/3119
https://github.com/apache/incubator-weex/issues/1936

1
2
3
4
5
6
// 在你的Application中添加:
if (Build.VERSION.SDK_INT>=18) {
    StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
    StrictMode.setVmPolicy(builder.build());
    builder.detectFileUriExposure();
}

ActivityNotFoundException 问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 <activity
         android:name=".xxxxxx"
         android:label="@string/app_name"
         android:screenOrientation="portrait"
         android:theme="@style/AppTheme.NoActionBar">
     <intent-filter>
         <action android:name="com.taobao.android.intent.action.WEEX"/>

         <category android:name="android.intent.category.DEFAULT"/>
         <category android:name="com.taobao.android.intent.category.WEEX"/>
         <action android:name="android.intent.action.VIEW"/>

         <data android:scheme="http"/>
         <data android:scheme="https"/>
         <data android:scheme="file"/>
         <data android:scheme="wxpage" />
     </intent-filter>
 </activity>

重写 navigator module,写死 category

参考:
Weex 之页面跳转以及 Android 端多应用选择窗口的处理
https://www.jianshu.com/p/572199f9b838

  • AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<activity
                android:name="qsbk.app.voice.weex.navigator.WXPageActivity"
                android:taskAffinity="qsbk.app.voice.voicechatroom"
                android:configChanges="orientation|screenSize|keyboardHidden"
                android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="${applicationId}.intent.action.WEEX"/>

                <action android:name="android.intent.action.VIEW"/>

                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <category android:name="${applicationId}.intent.category.WEEX"/>

                <data android:scheme="http"/>
                <data android:scheme="https"/>
                <data android:scheme="file"/>
                <data android:scheme="wxpage"/>
            </intent-filter>
        </activity>
  • VoiceRoomWXNavigatorModule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
public class VoiceRoomWXNavigatorModule extends BaseWxModule {

    public static final String MODULE_NAME = "navigatorAndroid";

    public static final String MSG_SUCCESS = "WX_SUCCESS";
    public static final String MSG_FAILED = "WX_FAILED";
    public static final String MSG_PARAM_ERR = "WX_PARAM_ERR";

    public static final String CALLBACK_RESULT = "result";
    public static final String CALLBACK_MESSAGE = "message";

    private final static String INSTANCE_ID = "instanceId";
    private final static String TAG = "Navigator";
    private final static String WEEX = "com.taobao.android.intent.category.WEEX";
    private final static String WEEX_ACTION = GlobalContext.getAppContext().getPackageName() + ".intent.action.WEEX";
    private final static String WEEX_CATEGORY = GlobalContext.getAppContext().getPackageName() + ".intent.category.WEEX";
    private final static String URL = "url";

    @JSMethod(uiThread = true)
    public void open(JSONObject options, JSCallback success, JSCallback failure) {
        if (options != null) {
            String url = options.getString(Constants.Value.URL);
            JSCallback callback = success;
            JSONObject result = new JSONObject();
            if (!TextUtils.isEmpty(url)) {
                Uri rawUri = Uri.parse(url);
                String scheme = rawUri.getScheme();
                if (TextUtils.isEmpty(scheme) || Constants.Scheme.HTTP.equalsIgnoreCase(scheme) || Constants.Scheme.HTTPS.equalsIgnoreCase(scheme)) {
                    this.push(options.toJSONString(), success);
                } else {
                    try {
                        Intent intent = new Intent(Intent.ACTION_VIEW, rawUri);
                        mWXSDKInstance.getContext().startActivity(intent);
                        result.put(CALLBACK_RESULT, MSG_SUCCESS);
                    } catch (Throwable e) {
                        e.printStackTrace();
                        result.put(CALLBACK_RESULT, MSG_FAILED);
                        result.put(CALLBACK_MESSAGE, "Open page failed.");
                        callback = failure;
                    }
                }
            } else {
                result.put(CALLBACK_RESULT, MSG_PARAM_ERR);
                result.put(CALLBACK_MESSAGE, "The URL parameter is empty.");
                callback = failure;
            }

            if (callback != null) {
                callback.invoke(result);
            }
        }
    }

    @JSMethod(uiThread = true)
    public void close(JSONObject options, JSCallback success, JSCallback failure) {
        JSONObject result = new JSONObject();
        JSCallback callback = null;
        if (mWXSDKInstance.getContext() instanceof Activity) {
            callback = success;
            ((Activity) mWXSDKInstance.getContext()).finish();
        } else {
            result.put(CALLBACK_RESULT, MSG_FAILED);
            result.put(CALLBACK_MESSAGE, "Close page failed.");
            callback = failure;
        }
        if (callback != null) {
            callback.invoke(result);
        }
    }

    @JSMethod()
    public void push(String param, JSCallback callback) {

        if (!TextUtils.isEmpty(param)) {
            if (WXSDKEngine.getActivityNavBarSetter() != null) {
                if (WXSDKEngine.getActivityNavBarSetter().push(param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }

            if (mWXSDKInstance.getContext() instanceof Activity) {
                Activity activity = (Activity) mWXSDKInstance.getContext();

                if (WXSDKEngine.getNavigator() != null
                        && WXSDKEngine.getNavigator().push(activity, param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }

            try {
                JSONObject jsonObject = JSON.parseObject(param);
                String url = jsonObject.getString(URL);
                if (!TextUtils.isEmpty(url)) {
                    Uri rawUri = Uri.parse(url);
                    String scheme = rawUri.getScheme();
                    Uri.Builder builder = rawUri.buildUpon();
                    if (TextUtils.isEmpty(scheme)) {
                        builder.scheme(Constants.Scheme.HTTP);
                    }
                    Intent intent = new Intent(Intent.ACTION_VIEW, builder.build());
                    intent.setAction(WEEX_ACTION);
                    intent.addCategory(WEEX_CATEGORY);
                    intent.putExtra(INSTANCE_ID, mWXSDKInstance.getInstanceId());
                    mWXSDKInstance.getContext().startActivity(intent);
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                }
            } catch (Exception e) {
                WXLogUtils.eTag(TAG, e);
                if (callback != null) {
                    callback.invoke(MSG_FAILED);
                }
            }
        } else if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod(uiThread = true)
    public void pop(String param, JSCallback callback) {

        if (WXSDKEngine.getActivityNavBarSetter() != null) {
            if (WXSDKEngine.getActivityNavBarSetter().pop(param)) {
                if (callback != null) {
                    callback.invoke(MSG_SUCCESS);
                }
                return;
            }
        }

        if (mWXSDKInstance.getContext() instanceof Activity) {
            Activity activity = (Activity) mWXSDKInstance.getContext();
            if (WXSDKEngine.getNavigator() != null) {
                if (WXSDKEngine.getNavigator().pop(activity, param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }

            if (callback != null) {
                callback.invoke(MSG_SUCCESS);
            }
            ((Activity) mWXSDKInstance.getContext()).finish();
        }
    }

    @JSMethod(uiThread = true)
    public void setNavBarRightItem(String param, JSCallback callback) {
        if (!TextUtils.isEmpty(param)) {
            if (WXSDKEngine.getActivityNavBarSetter() != null) {
                if (WXSDKEngine.getActivityNavBarSetter().setNavBarRightItem(param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }
        }

        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod(uiThread = true)
    public void clearNavBarRightItem(String param, JSCallback callback) {
        if (WXSDKEngine.getActivityNavBarSetter() != null) {
            if (WXSDKEngine.getActivityNavBarSetter().clearNavBarRightItem(param)) {
                if (callback != null) {
                    callback.invoke(MSG_SUCCESS);
                }
                return;
            }
        }
        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod(uiThread = true)
    public void setNavBarLeftItem(String param, JSCallback callback) {
        if (!TextUtils.isEmpty(param)) {
            if (WXSDKEngine.getActivityNavBarSetter() != null) {
                if (WXSDKEngine.getActivityNavBarSetter().setNavBarLeftItem(param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }
        }

        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }

    }

    @JSMethod(uiThread = true)
    public void clearNavBarLeftItem(String param, JSCallback callback) {
        if (WXSDKEngine.getActivityNavBarSetter() != null) {
            if (WXSDKEngine.getActivityNavBarSetter().clearNavBarLeftItem(param)) {
                if (callback != null) {
                    callback.invoke(MSG_SUCCESS);
                }
                return;
            }
        }

        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod(uiThread = true)
    public void setNavBarMoreItem(String param, JSCallback callback) {
        if (!TextUtils.isEmpty(param)) {
            if (WXSDKEngine.getActivityNavBarSetter() != null) {
                if (WXSDKEngine.getActivityNavBarSetter().setNavBarMoreItem(param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }
        }

        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod(uiThread = true)
    public void clearNavBarMoreItem(String param, JSCallback callback) {
        if (WXSDKEngine.getActivityNavBarSetter() != null) {
            if (WXSDKEngine.getActivityNavBarSetter().clearNavBarMoreItem(param)) {
                if (callback != null) {
                    callback.invoke(MSG_SUCCESS);
                }
                return;
            }
        }

        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod(uiThread = true)
    public void setNavBarTitle(String param, JSCallback callback) {
        if (!TextUtils.isEmpty(param)) {
            if (WXSDKEngine.getActivityNavBarSetter() != null) {
                if (WXSDKEngine.getActivityNavBarSetter().setNavBarTitle(param)) {
                    if (callback != null) {
                        callback.invoke(MSG_SUCCESS);
                    }
                    return;
                }
            }
        }
        if (callback != null) {
            callback.invoke(MSG_FAILED);
        }
    }

    @JSMethod
    public void setNavBarHidden(String param, final String callback) {
        String message = MSG_FAILED;
        try {
            JSONObject jsObj = JSON.parseObject(param);
            int visibility = jsObj.getInteger(Constants.Name.NAV_BAR_VISIBILITY);
            boolean success = changeVisibilityOfActionBar(mWXSDKInstance.getContext(), visibility);
            if (success) {
                message = MSG_SUCCESS;
            }
        } catch (JSONException e) {
            WXLogUtils.e(TAG, WXLogUtils.getStackTrace(e));
        }
        WXBridgeManager.getInstance().callback(mWXSDKInstance.getInstanceId(), callback, message);
    }

    private boolean changeVisibilityOfActionBar(Context context, int visibility) {
        boolean result = false;
        boolean hasAppCompatActivity = false;
        try {
            Class.forName("android.support.v7.app.AppCompatActivity");
            hasAppCompatActivity = true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        if (hasAppCompatActivity && mWXSDKInstance.getContext() instanceof AppCompatActivity) {
            android.support.v7.app.ActionBar actionbar = ((AppCompatActivity) mWXSDKInstance.getContext()).getSupportActionBar();
            if (actionbar != null) {
                switch (visibility) {
                    case Constants.Value.NAV_BAR_HIDDEN:
                        actionbar.hide();
                        result = true;
                        break;
                    case Constants.Value.NAV_BAR_SHOWN:
                        actionbar.show();
                        result = true;
                        break;
                }
            }
        } else if (mWXSDKInstance.getContext() instanceof Activity) {
            android.app.ActionBar actionbar = ((Activity) mWXSDKInstance.getContext()).getActionBar();
            if (actionbar != null) {
                switch (visibility) {
                    case Constants.Value.NAV_BAR_HIDDEN:
                        actionbar.hide();
                        result = true;
                        break;
                    case Constants.Value.NAV_BAR_SHOWN:
                        actionbar.show();
                        result = true;
                        break;
                }
            }
        }
        return result;
    }

}

weex Fresco SIGSEGV

qsbk.app.voice A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x886b100c in tid 7195 (RenderThread)
调用了 Fresco,已经关闭的 bitmap 了
见:weex fresco Fatal signal 11 (SIGSEGV), code 1, fault addr 0x886b100c in tid 7.md

vivo X5Pro D
Android5.0/Funtouch OS_2.5

问题

使用 Fresco 管线加载图片,加载后不持有图片引用。
问题:

  1. 会将用户当前不展示的图片也加载到内存中,实际占用内存变多,OOM 问题严重。
  2. 引用不被持有会导致 Bitmap 被回收后仍然被使用(3.2 - 3.4 版本 top crash—SIGSEGV (SEGV_MAPERR))

优化: 仿照 Fresco 的 DraweeController 管理 WXImageView 的 bitmap 引用。 显示时加载,隐藏时解除引用等待回收。

解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public final class FrescoImageAdapterV2 implements WxImgLoaderAdapter.ImageFactory {

    private static final String TAG = "weex";

    public FrescoImageAdapterV2() {
    }

    @Override
    public void loadImage(final String url, final ImageView imageView,
                          WXImageQuality quality, WXImageStrategy strategy) {

        if (imageView == null || imageView.getLayoutParams() == null) {
            LogUtils.w(TAG, "view为null或view.getLayoutParams()为null");
            return;
        }
        if (TextUtils.isEmpty(url)) {
            LogUtils.w(TAG, "url为empty");
            imageView.setImageBitmap(null);
            return;
        }

        if (imageView.getLayoutParams().width <= 0 || imageView.getLayoutParams().height <= 0) {
            LogUtils.w(TAG, "view的width或height不大于0,width: " + imageView.getLayoutParams().width
                    + ",height: " + imageView.getLayoutParams().height);
            return;
        }

        // 目前只有http和file://两种图片
        Context context = imageView.getContext();
        if (ImageUrlHelper.isFromHttp(url)) {
            Phoenix.with(context)
                    .setUrl(url)
                    .setResult(result -> {
                        if (result == null) {
                            LogUtils.w(TAG, "load网络图 error,bitmap is null,"
                                    + "url:" + url);
                            return;
                        }
                        LogUtils.i(TAG, "load网络图 成功,url:" + url);
                        imageView.setImageBitmap(result);
                    })
                    .load();
        } else {
            if (ImageUrlHelper.isFromFile(url)) {
                String path = url.substring(ImageUrlHelper.PREFIX_FILE.length());
                Bitmap bitmap = ImageUtils.getBitmap(path);
                imageView.setImageBitmap(bitmap);
                LogUtils.i(TAG, "load本地图 成功,url:" + url + ",path:" + path);

            }
        }
    }

    @Override
    public String url(Context context, String url) {
        return ImageUrlHelper.findUrl(url);
    }

}

Weex Module 不跟随 Activity 的生命周期

1
WXSDKEngine.registerModule(StatusModule.MODULE_NAME, StatusModule.class, true);

写了 true 后,是全局的,就监听不到 Activity 的生命周期了,改为 false 就可以监听 Activity 生命周期。

Weex image 组件问题

默认 image 组件,在 weex 页面按 home 键到后台去,会将 image 图片清空,在 App 到前台时会重新设置图片,通过 autoBitmapRecycle 属性控制,默认是 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// WXImageView
@Override
protected void onWindowVisibilityChanged(int visibility) {
    super.onWindowVisibilityChanged(visibility);
    if(mOutWindowVisibilityChangedReally){
      if(visibility == View.VISIBLE){
        autoRecoverImage();
      }else{
        autoReleaseImage();
      }
    }
}
public void autoReleaseImage(){
    if(enableBitmapAutoManage) {
      if (!isBitmapReleased) {
        isBitmapReleased = true;
        WXImage image = getComponent();
        if (image != null) {
          image.autoReleaseImage();
        }
      }
    }
}

public void autoRecoverImage(){
    if(enableBitmapAutoManage){
      if(isBitmapReleased){
        WXImage image = getComponent();
        if(image != null){
          image.autoRecoverImage();
        }
        isBitmapReleased = false;
      }
    }
}

// WXImage
public void autoReleaseImage(){
    if(mAutoRecycle){
      if(getHostView() != null){
        if (getInstance().getImgLoaderAdapter() != null) {
          getInstance().getImgLoaderAdapter().setImage(null, mHost, null, null);
        }
      }
    }
}

public void autoRecoverImage() {
    if(mAutoRecycle) {
      setSrc(mSrc);
    }
}

Weex 踩坑指南 之 weex 接入 websocketAndroid 端设置不上监听事件

官方文档上的写法,在 weex0.20 死活不行。

js websocket 接收 callback 写法有要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
methods: {
  connect:function() {
    websocket.WebSocket('wss://echo.websocket.org','');
    var self = this;
    self.onopeninfo = 'connecting...'
    websocket.onopen = function(e)
    {
      self.onopeninfo = 'websocket open';
    }
    websocket.onmessage = function(e)
    {
      self.onmessage = e.data;
    }
    websocket.onerror = function(e)
    {
      self.onerrorinfo = e.data;
    }
    websocket.onclose = function(e)
    {
      self.onopeninfo = '';
      self.oncloseinfo = 'closed';
      self.onerrorinfo = e.code;
    }
  },
  send:function(e) {
    var input = this.$refs.input;
    input.blur();
    websocket.send(this.txtInput);
    this.sendinfo = this.txtInput;
  },
  oninput: function(event) {
    this.txtInput = event.value;
  },
  close:function(e) {
    websocket.close();
  },
},

websocket.onmessage 在 Android 端没有注册 jscallback 成功,需要换一种写法,应该是 weex0.20 版本的 bug,用下面这种写法就可以:

1
2
3
4
5
6
7
8
9
websocket.onmessage(
    function onmessage(e){ 
      self.onmessage = e.data;
      modal.toast({
        message: e.data,
        duration: 0.5
      })
    }
  );

通过 npm 安装 weex 出现 npm ERR! Error  EACCES permission denied  access

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1073 warn checkPermissions Missing write access to /usr/local/lib/node_modules
1074 timing stage:rollbackFailedOptional Completed in 1ms
1075 timing stage:runTopLevelLifecycles Completed in 1367ms
1076 verbose stack Error: EACCES: permission denied, access '/usr/local/lib/node_modules'
1077 verbose cwd /Users/hacket
1078 verbose Darwin 17.5.0
1079 verbose argv "/usr/local/bin/node" "/usr/local/bin/npm" "install" "-g" "weex-toolkit@beta"
1080 verbose node v11.11.0
1081 verbose npm  v6.7.0
1082 error path /usr/local/lib/node_modules
1083 error code EACCES
1084 error errno -13
1085 error syscall access
1086 error Error: EACCES: permission denied, access '/usr/local/lib/node_modules'
1086 error  { [Error: EACCES: permission denied, access '/usr/local/lib/node_modules']
1086 error   stack:
1086 error    "Error: EACCES: permission denied, access '/usr/local/lib/node_modules'",
1086 error   errno: -13,
1086 error   code: 'EACCES',
1086 error   syscall: 'access',
1086 error   path: '/usr/local/lib/node_modules' }
1087 error The operation was rejected by your operating system.
1087 error It is likely you do not have the permissions to access this file as the current user
1087 error
1087 error If you believe this might be a permissions issue, please double-check the
1087 error permissions of the file and its containing directories, or try running
1087 error the command again as root/Administrator (though this is not recommended).
1088 verbose exit [ -13, true ]

解决:

1
sudo npm install -g weex-toolkit@beta

Weex 开源库

awesome-weex

https://github.com/joggerplus/awesome-weex

Weex-OkHttp-Adapter

https://github.com/zjutkz/Weex-OkHttp-Adapter

基于 OkHttp 的 Weex Http Adapter

eros

Weex 二次封装,对前端友好

https://github.com/bmfe/eros

weex-ui

https://github.com/alibaba/weex-ui

一个基于 Weex 的富交互、轻量级、高性能的 UI 组件库

Ref

本文由作者按照 CC BY 4.0 进行授权