可能是史上最全的 weex 踩坑攻略

4,659 阅读5分钟
> 这是一篇有故事的文章 --- 来自一个weex在生产环境中相爱相杀的小码畜..

image.png

故事一: Build

虽然weex的口号是一次撰写 多端运行, 但其实build环节是有差异的, native端构建需要使用weex-loader, 而web端则是使用vue-loader,除此以外还有不少差异点, 所以webpack需要两套配置.

最佳实践

使用webpack生成两套bundle,一套是基于vue-routerweb spa, 另一套是native端的多入口的bundlejs

首先假设我们在src/views下开发了一堆页面


image.png

build web配置

web端的入口文件有
render.js

import weexVueRenderer from 'weex-vue-render'
Vue.use(weexVueRenderer)

main.js

import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './routes'
Vue.use(VueRouter)
var router = new VueRouter({
  routes
})
/* eslint-disable no-new */
new Vue({
  el: '#root',
  router,
  render: h => h(App)
})

router.push('/')

App.vue

<template>
  <transition name="fade" mode="out-in">
    <router-view class=".container" />
  </transition>
</template>
<script>
export default {
    // ...
}
</script>
<style>
// ...
</style>

webpack.prod.conf.js入口

const webConfig = merge(getConfig('vue'), {
  entry: {
    app: ['./src/render.js', './src/app.js']
  },
  output: {
       path: path.resolve(distpath, './web'),
       filename: 'js/[name].[chunkhash].js',
       chunkFilename: 'js/[id].[chunkhash].js'
  },
    ...
 module: {
     rules: [
         {
             test: /\.vue$/,
             loader: 'vue-loader'
         }
     ]
 }
})

build native配置

native端的打包流程其实就是将src/views下的每个.vue文件导出为一个个单独的vue实例, 写一个node脚本即可以实现

// build-entry.js
require('shelljs/global')
const path = require('path')
const fs = require('fs-extra')

const srcPath = path.resolve(__dirname, '../src/views') // 每个.vue页面
const entryPath = path.resolve(__dirname, '../entry/') // 存放入口文件的文件夹
const FILE_TYPE = '.vue'

const getEntryFileContent = path => {
  return `// 入口文件
import App from '${path}${FILE_TYPE}'
/* eslint-disable no-new */
new Vue({
  el: '#root',
  render: h => h(App)
})

  `
}
// 导出方法
module.exports = _ => {
  // 删除原目录
  rm('-rf', entryPath)
  // 写入每个文件的入口文件
  fs.readdirSync(srcPath).forEach(file => {
    const fullpath = path.resolve(srcPath, file)
    const extname = path.extname(fullpath)
    const name = path.basename(file, extname)
    if (fs.statSync(fullpath).isFile() && extname === FILE_TYPE) {
    //写入vue渲染实例
      fs.outputFileSync(path.resolve(entryPath, name + '.js'), getEntryFileContent('../src/views/' + name))
    }
  })
  const entry = {}
    // 放入多个entry
  fs.readdirSync(entryPath).forEach(file => {
    const name = path.basename(file, path.extname(path.resolve(entryPath, file)))
    entry[name] = path.resolve(entryPath, name + '.js')
  })
  return entry
}

webpack.build.conf.js中生成并打包多入口

const buildEntry = require('./build_entry')
// ..
// weex配置
const weexConfig = merge(getConfig('weex'), {
  entry: buildEntry(), // 写入多入口
  output: {
    path: path.resolve(distPath, './weex'),
    filename: 'js/[name].js' // weex环境无需使用hash名字
  },
  module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'weex-loader'
            }
        ]
  }
})

module.exports = [webConfig, weexConfig]

最终效果


image.png

image.png

image.png

故事二: 使用预处理器

vue单文件中, 我们可以通过在vue-loader中配置预处理器, 代码如下

{
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
        loaders: {
          scss: 'vue-style-loader!css-loader!sass-loader', // <style lang="scss">
          sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax' // <style lang="sass">
        }
    }
}

weex在native环境下其实将css处理成json加载到模块中, 所以...

  • 使用vue-loader配置的预处理器在web环境下正常显示, 在native中是无效的
  • native环境下不存在全局样式, 在js文件中import 'index.css'也是无效的

解决问题一

研究weex-loader源码后发现在.vue中是无需显示配置loader的, 只需要指定<style lang="stylus">并且安装stylus stylus-loader即可,weex-loader会根据lang去寻找对应的loader.
但因为scss使用sass-loader, 会报出scss-loader not found, 但因为sass默认会解析scss语法, 所以直接设置lang="sass"是可以写scss语法的, 但是ide就没有语法高亮了. 可以使用如下的写法

<style lang="sass">
    @import './index.scss'
</style>

语法高亮, 完美!


image.png

解决问题二

虽然没有全局样式的概念, 但是支持单独import样式文件

<style lang="sass">
    @import './common.scss'
    @import './variables.scss'
    // ...
</style>

故事三: 样式差异

这方面官方文档已经有比较详细的描述, 但还是有几点值得注意的

简写

weex中的样式不支持简写, 所有类似margin: 0 0 10px 10px的都是不支持

背景色

android下的view是有白色的默认颜色的, 而iOS如果不设置是没有默认颜色的, 这点需要注意

浮点数误差

weex默认使用750px * 1334px作为适配尺寸, 实际渲染时由于浮点数的误差可能会存在几px的误差, 出现细线等样式问题, 可以通过加减几个px来调试

嵌套写法

即使使用了预处理器, css嵌套的写法也是会导致样式失效

故事四: 页面跳转

weex下的页面跳转有三种形式

  • native -> weex: weex页面需要一个控制器作为容器, 此时就是native间的跳转
  • weex -> native: 需要通过module形式通过发送事件到native来实现跳转
  • weex -> weex: 使用navigator模块, 假设两个weex页面分别为a.js, b.js, 可以定义mixin方法

      function isWeex () {
        return process.env.COMPILE_ENV === 'weex' // 需要在webpack中自定义
      }
    
      export default {
    
        methods: {
    
          push (path) {
            if (isWeex()) {
              const toUrl = weex.config.bundleUrl.split('/').slice(0, -1).join('/') + '/' + path + '.js' // 将a.js的绝对地址转为b.js的绝对地址
              weex.requireModule('navigator').push({
                url: toUrl,
                animated: 'true'
              })
            } else {
              this.$router.push(path) // 使用vue-router
            }
          },
    
          pop () {
            if (isWeex()) {
              weex.requireModule('navigator').pop({
                animated: 'true'
              })
            } else {
              window.history.back()
            }
          }
    
        }
    
      }

    这样就组件里使用this.push(url), this.pop()来跳转

跳转配置

  1. iOS下页面跳转无需配置, 而android是需要的, 使用weexpack platform add android生成的项目是已配置的, 但官方的文档里并没有对于已存在的应用如何接入进行说明

    • 其实android中是通过intent-filter来拦截跳转的

                <activity
                android:name=".WXPageActivity"
                android:label="@string/app_name"
                android:screenOrientation="portrait"
                android:theme="@android:style/Theme.NoTitleBar">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <action android:name="com.alibaba.weex.protocol.openurl"/>
      
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="com.taobao.android.intent.category.WEEX"/>
      
                <data android:scheme="http"/>
                <data android:scheme="https"/>
                <data android:scheme="file"/>
            </intent-filter>
        </activity>
    • 然后我们新建一个WXPageActivity来代理所有weex页面的渲染, 核心的代码如下

        @Override
        protected void onCreate(Bundle saveInstanceState) {
        // ...
            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);
          }
        }
      
        if (mUri == null) {
          Toast.makeText(this, "the uri is empty!", Toast.LENGTH_SHORT).show();
          finish();
          return;
        }
        String path = mUri.toString();
        // 传来的url参数总会带上http:/ 应该是个bug 可以自己判断是否本地url再去跳转
        String jsPath = path.indexOf("weex/js/") > 0 ? path.replace("http:/", "") : path;
        HashMap<String, Object> options = new HashMap<String, Object>();
        options.put(WXSDKInstance.BUNDLE_URL, jsPath);
        mWXSDKInstance = new WXSDKInstance(this);
        mWXSDKInstance.registerRenderListener(this);
        mWXSDKInstance.render("weex", WXFileUtils.loadAsset(jsPath, this), options, null, -1, -1, WXRenderStrategy.APPEND_ASYNC);
        }

顺便说下... weex官方没有提供可定制的nav组件真的是很不方便..经常需要通过module桥接native来实现跳转需求

故事五: 页面间数据传递

  • native -> weex: 可以在native端调用render时传入的option中自定义字段, 例如NSDictary *option = @{@"params": @{}}, 在weex中使用weex.config.params取出数据
  • weex -> weex: 使用storage
  • weex -> native: 使用自定义module

故事六: 图片加载

官网有提到如何加载网络图片
但是加载本地图片的行为对于三端肯定是不一致的, 也就意味着我们得给native重新改一遍引用图片的路径再打包...


但是当然是有解决办法的啦


image.png
  • Step 1 webpack设置将图片资源单独打包, 这个很easy, 此时bundleJs访问的图片路径就变成了/images/..
   {
     test: /\.(png|jpe?g|gif|svg)$/,
     loader: 'url-loader',
     query: {
       limit: 1,
       name: 'images/[hash:8].[name].[ext]'
     }
   }
  • Step 2 那么现在我们将同级目录下的js文件夹与images文件夹放入native中, iOS中一般放入mainBundle, Android一般放入src/main/assets, 接下来只要在imgloader接口中扩展替换本地资源路径的代码就ok了

iOS代码如下:

- (id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)options completed:(void (^)(UIImage *, NSError *, BOOL))completedBlock{
    if ([url hasPrefix:@"//"]) {
        url = [@"http:" stringByAppendingString:url];
    }
    // 加载本地图片
    if ([url hasPrefix:@"file://"]) {
        NSString *newUrl = [url stringByReplacingOccurrencesOfString:@"/images/" withString:@"/"];
        UIImage *image = [UIImage imageNamed:[newUrl substringFromIndex:7]];
        completedBlock(image, nil, YES);
        return (id<WXImageOperationProtocol>) self;
    } else {
        // 加载网络图片
        return (id<WXImageOperationProtocol>)[[SDWebImageManager sharedManager]downloadImageWithURL:[NSURL URLWithString:url] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
        } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            if (completedBlock) {
                completedBlock(image, error, finished);
            }
        }];
    }
}

Android代码如下:

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

    WXSDKManager.getInstance().postOnUiThread(new Runnable() {

      @Override
      public void run() {
        if(view==null||view.getLayoutParams()==null){
          return;
        }
        if (TextUtils.isEmpty(url)) {
          view.setImageBitmap(null);
          return;
        }
        String temp = url;
        if (url.startsWith("//")) {
          temp = "http:" + url;
        }
        if (temp.startsWith("/images/")) {
          //过滤掉所有相对位置
          temp = temp.replace("../", "");
          temp = temp.replace("./", "");
          //替换asset目录的配置
          temp = temp.replace("/images/", "file:///android_asset/weex/images/");
          Log.d("ImageAdapter", "url:" + temp);
        }
        if (view.getLayoutParams().width <= 0 || view.getLayoutParams().height <= 0) {
          return;
        }

        if(!TextUtils.isEmpty(strategy.placeHolder)){
          Picasso.Builder builder=new Picasso.Builder(WXEnvironment.getApplication());
          Picasso picasso=builder.build();
          picasso.load(Uri.parse(strategy.placeHolder)).into(view);

          view.setTag(strategy.placeHolder.hashCode(),picasso);
        }

        Picasso.with(WXEnvironment.getApplication())
                .load(temp)
                .into(view, new Callback() {
                  @Override
                  public void onSuccess() {
                    if(strategy.getImageListener()!=null){
                      strategy.getImageListener().onImageFinish(url,view,true,null);
                    }

                    if(!TextUtils.isEmpty(strategy.placeHolder)){
                      ((Picasso) view.getTag(strategy.placeHolder.hashCode())).cancelRequest(view);
                    }
                  }

                  @Override
                  public void onError() {
                    if(strategy.getImageListener()!=null){
                      strategy.getImageListener().onImageFinish(url,view,false,null);
                    }
                  }
                });
      }
    },0);
  }

故事七: 生产环境的实践

增量更新

可以使用google-diff-match-patch来实现, google-diff-match-patch拥有许多语言版本的实现, 思路如下:

  • 服务器端构建一套管理前端bundlejs的系统, 提供查询bundlejs版本与下载的api
  • 客户端第一次访问weex页面时去服务端下载bundlejs文件
  • 每次客户端初始化时静默访问服务器判断是否需要更新, 若需更新, 服务器端diff两个版本的差异, 并返回diff, native端使用patch api生成新版本的bundlejs

降级处理

一般情况下, 我们会同时部署一套web端界面, 若线上环境的weex页面出现bug, 则使用webview加载web版, 推荐依赖服务端api来控制降级的切换

总结

weex的优势: 依托于vue, 上手简单. 可以满足以vue为技术主导的公司给native双端提供简单/少底层交互/热更新需求的页面的需求

weex的劣势: 在native端调整样式是我心中永远的痛.. 以及众所周知的生态问题, 维护组没有花太多精力解答社区问题, 官方文档错误太多, 导致我在看的时候就顺手提了几个PR(逃

对于文章中提到的没提到的问题, 欢迎来和笔者讨论, 或者参考我的weex-start-kit, 当然点个star也是极好的


image.png