­

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

  • 2019 年 12 月 4 日
  • 筆記

故事一: Build

雖然weex的口號是一次撰寫 多端運行, 但其實build環節是有差異的, native端構建需要使用weex-loader, 而web端則是使用vue-loader,除此以外還有不少差異點, 所以webpack需要兩套配置.

最佳實踐

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

首先假設我們在src/views下開發了一堆頁面

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]

最終效果

故事二: 使用預處理器

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>

語法高亮, 完美!

解決問題二

雖然沒有全局樣式的概念, 但是支援單獨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()來跳轉

跳轉配置

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](/user/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來實現跳轉需求

來自@荔枝我大哥 的補充

Android和蘋果方面可以在原生程式碼接管`navigator`這個模組,Android方面只需要實現`IActivityNavBarSetter`,蘋果方面好像是`WXNavigatorProtocol`,然後在app啟動初始化weex時註冊即可。

故事五: 頁面間數據傳遞

  • native -> weex: 可以在native端調用render時傳入的option中自定義欄位, 例如NSDictary *option = @{@"params": @{}}, 在weex中使用weex.config.params取出數據
  • weex -> weex: 使用storage
  • weex -> native: 使用自定義module

故事六: 圖片載入

官網有提到如何載入網路圖片 但是載入本地圖片的行為對於三端肯定是不一致的, 也就意味著我們得給native重新改一遍引用圖片的路徑再打包…

但是當然是有解決辦法的啦

  • 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](/user/Override)    public void setImage(final String url, final ImageView view,                         WXImageQuality quality, final WXImageStrategy strategy) {        WXSDKManager.getInstance().postOnUiThread(new Runnable() {          [@Override](/user/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](/user/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](/user/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
方案二

來自 @荔枝我大哥的補充

我們所有的jsBundle全部載入的線上文件,通過http頭資訊設置`E-Tag`結合`cache-control`來實現快取策略,最終效果就是,A.vue -> A.js, app第一次載入A.js是從網路下載下來並且保存到本地,app第二次載入A.js是直接載入的保存到本地的 A.js文件,線上A.vue被修改,A.vue -> A.js, app第三次載入A.js時根據快取策略會知道線上A.js 已經和本地A.js 有差異,於是重新下載A.js到本地並載入. (整個流程通過http快取策略來實現,無需多餘編碼,參考https://developers.google.cn/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn)

還可以參考很多ReactNative的成熟方案, 本質上都是js的熱更新

降級處理

一般情況下, 我們會同時部署一套web端介面, 若線上環境的weex頁面出現bug, 則使用webview載入web版, 推薦依賴服務端api來控制降級的切換

總結

weex的優勢: 依託於vue, 上手簡單. 可以滿足以vue為技術主導的公司給native雙端提供簡單/少底層交互/熱更新需求的頁面的需求

weex的劣勢: 在native端調整樣式是我心中永遠的痛.. 以及眾所周知的生態問題, 維護組沒有花太多精力解答社區問題, 官方文檔錯誤太多, 導致我在看的時候就順手提了幾個PR(逃

對於文章中提到的沒提到的問題, 歡迎來和筆者討論, 或者參考我的weex-start-kit, 當然點個star也是極好的