初识weex

现在公司的项目是基于weex开发的,我负责的前端业务也是采用Vue,早先学了点Android的东西,前段时间一直在学习Vue的源码,于是现在打算来理一理weex相关的知识,后面也可以跟客户端的同学探讨一下。

<!--more-->

参考:

1. 环境

1.1. 开发环境

踩坑记

npm大法好,全局安装weex-toolkit即可。使用weex -v查看版本:

   v1.1.0-beta.7
 - weexpack : v0.4.7-beta.26
 - weex-builder : v0.2.13-beta.4
 - weex-previewer : v1.3.13-beta.10

由于初学,也不需要使用weex init创建项目,下面开箱即食

  • 新建目录
  • 新建demo1.we文件,打开内置组件文档,复制个官方demo进去
  • weex demo1.we文件,然后就可以在浏览中预览文件了

会发现.we文件和.vue文件的格式貌似是一模一样的打开万能的webstrom,会提示安装*.we文件插件,找到weex插件,然后安装,即可以在侧边栏直接预览页面(这让我想起了HBuilder,然而侧边栏调试并没有浏览器中好用),总之有代码提示和语法高亮就够了。

上面的过程非常简单,然而我却跳进了一个坑:为了测试内置组件(比如text),新建了一个text.we,然后执行weex text.we,在控制台会发现堆栈溢出了~,去搜issue才发现原来是we文件命名与web组件名相同导致的

然后又测试了事件绑定,文档上面采用的是Vue的@click="handleClick"这样的简写方法,但是我在web下测试会报错

@change' is not a valid attribute name

需要调整成onclick="handleClick"这样的形式,是不是我版本的问题。然后查了半天没找到如何查看weex的版本,貌似weex-toolkitweex版本是独立的。后来发现在控制台输出weex.config才发现,weexVersionv0.5.0,所以说,这个内置的预览不靠谱,上面这趟的这些坑都是脑子进的水,老老实实建个新项目吧。

开发环境

weex内置了一套开发环境

npm run dev & npm run serve

查看webpack.config.js可以发现,实际存在webConfigweexConfig两套配置,这是因为在打包的时候,客户端使用weex-loader进行处理,而web端使用vue-loader进行处理。

webpack.dev.js开发模式下,使用的是webConfig。这里还有不少坑(比如自动生成的入口文件),后面会提到

1.2. Android环境

由于目前iOS方面我只是了解一点点基本语法,因此先从Android开始学习,后面再进行iOS相关的处理。

根据官方文档的要求,搭建对应版本的android环境,添加相关依赖

compile 'com.android.support:recyclerview-v7:25.3.0'
compile 'com.android.support:appcompat-v7:25.1.1'
compile 'com.alibaba:fastjson:1.1.45'
compile 'com.taobao.android:weex_sdk:0.9.5@aar'
compile 'com.github.bumptech.glide:glide:3.7.0'

ImageAdapter

这里我选择使用了Glide实现图片的加载


public class ImageAdapter implements IWXImgLoaderAdapter {
    @Override
    public void setImage(String url, ImageView view, WXImageQuality quality, WXImageStrategy strategy) {
        Glide.with(view.getContext())
                .load(url)
                .into(view);
    }
}

实现自定义Application

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        InitConfig config=new InitConfig.Builder().setImgAdapter(new ImageAdapter()).build();
        WXSDKEngine.initialize(this,config);
    }
}

记得一定要在manifests中设置自定义的application

 <application
        android:name=".App" // 这个一定要加上
 >
 </application>

加载对应的js文件

执行npm run build之后打包获得的JS文件位于dist/index.js,将其拷贝到Android项目的assets目录下。由于操作比较频繁可以定义一个脚本,直接加载服务器资源即可

"scripts": {
  "copy:android": "cp dist/index.js ../android/app/src/main/assets/index.js"
}

然后在对应活动中通过实例化WXSDKInstance对象

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        mWXSDKInstance = new WXSDKInstance(this);
        mWXSDKInstance.registerRenderListener(this);
        // mWXSDKInstance.render("WXSample", WXFileUtils.loadAsset("index.js",this), null, null, -1, -1, WXRenderStrategy.APPEND_ASYNC);
          mWXSDKInstance.renderByUrl("WXSample", "http://10.0.2.2:8081/dist/index.js", null, null, -1, -1, WXRenderStrategy
                .APPEND_ASYNC);
    }

其中8081是weex脚手架开启的服务器,记得在manifests中配置网络权限。在项目中更常用的做法是加载服务器上的JS文件,实现客户端的热更新,这里涉及到一系列的缓存问题,我们先把demo跑起来

2. 语法

2.1. 样式

由于在打包时会将css转换成对应的json语法,加之官方文档解释为了提高解析性能,因此与传统的CSS还是有一些区别的。下面是几个需要注意的地方

  • 部分样式都不支持简写,如borderbackground
  • position不支持z-index,默认靠后的元素层级较高
  • 文本样式不支持继承,后代选择器无效
  • 默认以750 * 1334尺寸,跟小程序的rpx单位比较相似。这导致在浏览器已像素预览正常,而在客户端发现所有尺寸都小了一倍
  • transformtransiton的支持有限

2.2. 组件

weex内置了一些组件

  • div中不能直接添加文本,文本只能在text中添加。官方建议div嵌套不宜过深
  • a链接只是默认调用了event模块的openURL,却没有实现相关的方法,所以需要自己实现WXEventModule并注册
  • Android下默认overflow:hidden,为了实现内容滚动只能使用scroller组件
  • image用来加载图片,需要原生实现图片加载器,且需要在样式中手动设置宽高,否则不会显示图片。image组件的功能比较强大,包括占位图和下载图片等
  • web组件用来加载网页,可以使用webview内置模块来控制webview的行为,不过功能比较弱,暂时没有找到对webview组件相关设置(如允许弹窗等)

2.3. 模块

weex内置了一些模块,用于封装底层的api,下面先介绍了几个比较常用的模块

  • model模块用于提供与原生类似的模态框
  • navigator模块用于控制weex页面的跳转,在下面的路由章节中有对该模块的使用
  • stream模块提供了网络请求相关接口

大部分模块使用的是回调函数形式,我们可以对其封装,使其Promise化。比如下面对于stream模块的封装

let stream = weex.requireModule('stream')
let baseUrl = 'http://10.0.2.2:9999/'

let get = function(url){
    return new Promise((resolve, reject)=>{
        stream.fetch({
            method: 'GET',
            url: baseUrl+url,
            type:'json'
        }, function(ret) {
            if(!ret.ok){
                reject(ret)
            }else{
                let data = ret.data;
                resolve(data)
            }
        });
    })
}

export default {
    get
}

在下面的原生交互章节再去了解实现自定义模块和组件。

3. 开发环境

3.1. 样式表

预编译器style上添加lang="scss"指定css语法,但是在build会报错

Module not found: Error: Can't resolve 'scss-loader'

解决办法是将lang="sass",然后@import './index.scss'

字体图标

在之前的微信小程序项目中,通过将ttf字体转换成base64文件,然后引入样式表中就可以愉快地使用字体图标了。

但是在weex中不支持这种形式,其原因是不支持after伪元素,直接引入字体然后在text组件中使用对应的字体编码是可以渲染的~这个就很蛋疼了

关于字体图标的使用,这里有一个讨论

3.2. 服务器

为了方便测试,我使用的是Koa搭建静态资源和接口服务器,位于/server下,服务器主要用于提供静态资源(如图标)和数据接口。

在开发的时候,使用的是npm run server开启的express服务器,这样可以方便同步查看效果。在项目上线的时候,也需要将JSBundle部署到静态资源服务器上

3.3. 模块化

由于使用webpack进行打包,因此我们可以很方便地使用CommonJS或ES6模块来组织代码,常见的比如网络请求、路径配置等功能,都可以按照具体业务需求进行模块化处理

4. 路由

Weex推荐使用单页独立开发的,即每个vue文件对应一个页面,在build的时候会将每个vue文件都打包成对应的JSBundle文件(在webpack.config.js中可以看见其原理是遍历/src文件夹)。这样可以使页面更加精简,方便维护和增量更新。

这是否意味着官方不建议使用vue-router来实现页面跳转呢~

4.1. native->weex

由于weex页面可以作为单个组件嵌入应用,因此原生跳转到weex页面,实际上就是原生之之间的跳转,在Andorid中,通过startActivity来实现页面跳转。

4.2. weex->native

// todo

4.3. weex->weex

weex内置了navigator模块,用来实现weex页面与weex页面之间的跳转。

其原理是通过拦截器,来确定需要显示的Activity,然后加载对应的JSBundle资源,渲染weex页面。具体的使用可以参考这里

声明拦截器

声明一个用于处理weex跳转的拦截器

<activity
    android:name=".WeexActivity">
    <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>

声明活动

这里专门新建了一个Activity处理weex页面的跳转,实际上也可以在同一个Activity内处理。

public class WeexActivity extends AppCompatActivity implements IWXRenderListener {
    private static String TEST_URL = "http://10.0.2.2:8081/dist/index.js";
    private WXSDKInstance mWXSDKInstance;
    private FrameLayout mContainer;
    private Uri mUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weex);

        mContainer = (FrameLayout) findViewById(R.id.container);

        mWXSDKInstance = new WXSDKInstance(this);
        mWXSDKInstance.registerRenderListener(this);
        String url = this.getJSPath();

        if (url.isEmpty()){
            url = TEST_URL;
        }

        HashMap<String, Object> options = this.getWeexOpts(url);

        mWXSDKInstance.renderByUrl("WXSample", url,options,null, WXRenderStrategy.APPEND_ONCE);
    }
    private HashMap<String, Object> getWeexOpts(String jsPath){
        HashMap<String, Object> options = new HashMap<String, Object>();
        options.put(WXSDKInstance.BUNDLE_URL, jsPath);
        return options;
    }
    // 从intent中获取js地址
    private String getJSPath(){
        Uri uri = getIntent().getData();
        Bundle bundle = getIntent().getExtras();

        if (uri != null) {
            mUri = uri;
        }

        if (bundle != null) {
            String bundleUrl = bundle.getString("bundleUrl");
            if (!TextUtils.isEmpty(bundleUrl)) {
                mUri = Uri.parse(bundleUrl);
            }
        }

        return mUri.toString();
    }
    // ...
}

5. 原生交互

weex提供了实现原生扩展的接口,包括模块扩展和组件扩展,参考文档

5.1. 模块扩展

实现在原生实现相关的模块

public class MyModule extends WXModule {
    @JSMethod(uiThread = true)
    public void showToast(String msg){
        Toast.makeText(mWXSDKInstance.getContext(),msg,Toast.LENGTH_LONG).show();
    }
}

然后在自定义application中注册模块

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        InitConfig config=new InitConfig.Builder().setImgAdapter(new ImageAdapter()).build();
        WXSDKEngine.initialize(this,config);
        // 注册相关模块
        try {
            WXSDKEngine.registerModule("myModule", MyModule.class);
        }catch (WXException e){
            e.printStackTrace();
        }
    }
}

然后在weex中引用相关模块,其中通过模块名进行关联

 weex.requireModule('@weex-module/myModule').showToast('Hello Native')

这里遇见的一个问题是模块注册失败,一查发现有不少类似的问题

5.2. 自定义组件

weex支持Vue的组件系统,此外,还支持通过原生扩展的组件系统,这里暂时没有进一步了解

6. 小结

经过一段时间的折腾,自己体会到了weex的一些优点和劣势:

  • 优点自然是跨平台,极大程度上降低开发成本
  • 劣势是在三端的表现都如原生,且内置的功能有限;深入开发的成本也不低,还需要处理不同平台的差异,可能会提高后续维护成本

实际上我是在之前阅读Vue源码的时候,看见在渲染函数中生成的AST,在不同的运行平台下有不一样的处理方式,于是来了解下weex。后面有时间应该会去尝试下React Native

感觉这种跨平台构建前端应用是未来前端的发展趋势(包括微信小程序这种),因此还是应该围观一下。