可能是史上最全的weex踩坑攻略
- 2019 年 12 月 4 日
- 筆記
故事一: Build
雖然weex
的口號是一次撰寫 多端運行
, 但其實build
環節是有差異的, native
端構建需要使用weex-loader
, 而web
端則是使用vue-loader
,除此以外還有不少差異點, 所以webpack
需要兩套配置.
最佳實踐
使用webpack
生成兩套bundle
,一套是基於vue-router
的web 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
: 使用storageweex -> 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也是極好的