鵝廠原創 | 可能是史上最全的weex踩坑攻略
作者:applecatkay
原文:可能是史上最全的weex踩坑攻略 - 騰訊Web前端 IMWeb 團隊社區
> 這是一篇有故事的文章 --- 來自一個weex在生產環境中相愛相殺的小碼畜..
故事一: 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-rendernVue.use(weexVueRenderer)n
main.js:
import App from ./App.vuenimport VueRouter from vue-routernimport routes from ./routesnVue.use(VueRouter)nvar router = new VueRouter({n routesn})n/* eslint-disable no-new */nnew Vue({n el: #root,n router,n render: h => h(App)n})nnrouter.push(/)n
App.vue
<template>n <transition name="fade" mode="out-in">n <router-view class=".container" />n </transition>n</template>n<script>nexport default {n // ...n}n</script>n<style>n// ...n</style>n
webpack.prod.conf.js入口:
const webConfig = merge(getConfig(vue), {n entry: {n app: [./src/render.js, ./src/app.js]n },n output: {n path: path.resolve(distpath, ./web),n filename: js/[name].[chunkhash].js,n chunkFilename: js/[id].[chunkhash].jsn },n ...n module: {n rules: [n {n test: /.vue$/,n loader: vue-loadern }n ]n }n})n
build native配置
native端的打包流程其實就是將src/views下的每個.vue文件導出為一個個單獨的vue實例, 寫一個node腳本即可以實現。
// build-entry.jsnrequire(shelljs/global)nconst path = require(path)nconst fs = require(fs-extra)nnconst srcPath = path.resolve(__dirname, ../src/views) // 每個.vue頁面nconst entryPath = path.resolve(__dirname, ../entry/) // 存放入口文件的文件夾nconst FILE_TYPE = .vuennconst getEntryFileContent = path => {n return `// 入口文件nimport App from ${path}${FILE_TYPE}n/* eslint-disable no-new */nnew Vue({n el: #root,n render: h => h(App)n})nn `n}n// 導出方法nmodule.exports = _ => {n // 刪除原目錄n rm(-rf, entryPath)n // 寫入每個文件的入口文件n fs.readdirSync(srcPath).forEach(file => {n const fullpath = path.resolve(srcPath, file)n const extname = path.extname(fullpath)n const name = path.basename(file, extname)n if (fs.statSync(fullpath).isFile() && extname === FILE_TYPE) {n //寫入vue渲染實例n fs.outputFileSync(path.resolve(entryPath, name + .js), getEntryFileContent(../src/views/ + name))n }n })n const entry = {}n // 放入多個entryn fs.readdirSync(entryPath).forEach(file => {n const name = path.basename(file, path.extname(path.resolve(entryPath, file)))n entry[name] = path.resolve(entryPath, name + .js)n })n return entryn}n
webpack.build.conf.js中生成並打包多入口:
const buildEntry = require(./build_entry)n// ..n// weex配置nconst weexConfig = merge(getConfig(weex), {n entry: buildEntry(), // 寫入多入口n output: {n path: path.resolve(distPath, ./weex),n filename: js/[name].js // weex環境無需使用hash名字n },n module: {n rules: [n {n test: /.vue$/,n loader: weex-loadern }n ]n }n})nnmodule.exports = [webConfig, weexConfig]n
最終效果:
故事二: 使用預處理器
在vue單文件中, 我們可以通過在vue-loader中配置預處理器, 代碼如下:
{n test: /.vue$/,n loader: vue-loader,n options: {n loaders: {n scss: vue-style-loader!css-loader!sass-loader, // <style lang="scss">n sass: vue-style-loader!css-loader!sass-loader?indentedSyntax // <style lang="sass">n }n }n}n
而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">n @import ./index.scssn</style>n
語法高亮, 完美!
解決問題二
雖然沒有全局樣式的概念, 但是支持單獨import樣式文件。
<style lang="sass">n @import ./common.scssn @import ./variables.scssn // ...n</style>n
故事三: 樣式差異
這方面官方文檔已經有比較詳細的描述, 但還是有幾點值得注意的。
簡寫
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 () {n return process.env.COMPILE_ENV === weex // 需要在webpack中自定義n }nn export default {nn methods: {nn push (path) {n if (isWeex()) {n const toUrl = weex.config.bundleUrl.split(/).slice(0, -1).join(/) + / + path + .js // 將a.js的絕對地址轉為b.js的絕對地址n weex.requireModule(navigator).push({n url: toUrl,n animated: truen })n } else {n this.$router.push(path) // 使用vue-routern }n },nn pop () {n if (isWeex()) {n weex.requireModule(navigator).pop({n animated: truen })n } else {n window.history.back()n }n }nn }nn }n
- 這樣就組件里使用this.push(url), this.pop()來跳轉。
跳轉配置
- iOS下頁面跳轉無需配置, 而android是需要的, 使用weexpack platform add android生成的項目是已配置的, 但官方的文檔里並沒有對於已存在的應用如何接入進行說明。
- 其實android中是通過intent-filter來攔截跳轉的。
<activityn android:name=".WXPageActivity"n android:label="@string/app_name"n android:screenOrientation="portrait"n android:theme="@android:style/Theme.NoTitleBar">n <intent-filter>n <action android:name="android.intent.action.VIEW"/>n <action android:name="com.alibaba.weex.protocol.openurl"/>nn <category android:name="android.intent.category.DEFAULT"/>n <category android:name="com.taobao.android.intent.category.WEEX"/>nn <data android:scheme="http"/>n <data android:scheme="https"/>n <data android:scheme="file"/>n </intent-filter>n </activity>n
- 然後我們新建一個WXPageActivity來代理所有weex頁面的渲染, 核心的代碼如下:
[@Override](/user/Override)n protected void onCreate(Bundle saveInstanceState) {n // ...n Uri uri = getIntent().getData();nBundle bundle = getIntent().getExtras();nn if (uri != null) {n mUri = uri;n }nn if (bundle != null) {n String bundleUrl = bundle.getString("bundleUrl");n if (!TextUtils.isEmpty(bundleUrl)) {n mUri = Uri.parse(bundleUrl);n }n }nn if (mUri == null) {n Toast.makeText(this, "the uri is empty!", Toast.LENGTH_SHORT).show();n finish();n return;n }n String path = mUri.toString();n // 傳來的url參數總會帶上http:/ 應該是個bug 可以自己判斷是否本地url再去跳轉n String jsPath = path.indexOf("weex/js/") > 0 ? path.replace("http:/", "") : path;n HashMap<String, Object> options = new HashMap<String, Object>();n options.put(WXSDKInstance.BUNDLE_URL, jsPath);n mWXSDKInstance = new WXSDKInstance(this);n mWXSDKInstance.registerRenderListener(this);n mWXSDKInstance.render("weex", WXFileUtils.loadAsset(jsPath, this), options, null, -1, -1, WXRenderStrategy.APPEND_ASYNC);n }n
來自@荔枝我大哥 的補充
安卓和蘋果方面可以在原生代碼接管`navigator`這個模塊,安卓方面只需要實現`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/..
{n test: /.(png|jpe?g|gif|svg)$/,n loader: url-loader,n query: {n limit: 1,n name: images/[hash:8].[name].[ext]n }n }n
- 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{n if ([url hasPrefix:@"//"]) {n url = [@"http:" stringByAppendingString:url];n }n // 載入本地圖片n if ([url hasPrefix:@"file://"]) {n NSString *newUrl = [url stringByReplacingOccurrencesOfString:@"/images/" withString:@"/"];n UIImage *image = [UIImage imageNamed:[newUrl substringFromIndex:7]];n completedBlock(image, nil, YES);n return (id<WXImageOperationProtocol>) self;n } else {n // 載入網路圖片n return (id<WXImageOperationProtocol>)[[SDWebImageManager sharedManager]downloadImageWithURL:[NSURL URLWithString:url] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {n } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {n if (completedBlock) {n completedBlock(image, error, finished);n }n }];n }n}n
Android代碼如下:
[@Override](/user/Override)n public void setImage(final String url, final ImageView view,n WXImageQuality quality, final WXImageStrategy strategy) {nn WXSDKManager.getInstance().postOnUiThread(new Runnable() {nn [@Override](/user/Override)n public void run() {n if(view==null||view.getLayoutParams()==null){n return;n }n if (TextUtils.isEmpty(url)) {n view.setImageBitmap(null);n return;n }n String temp = url;n if (url.startsWith("//")) {n temp = "http:" + url;n }n if (temp.startsWith("/images/")) {n //過濾掉所有相對位置n temp = temp.replace("../", "");n temp = temp.replace("./", "");n //替換asset目錄的配置n temp = temp.replace("/images/", "file:///android_asset/weex/images/");n Log.d("ImageAdapter", "url:" + temp);n }n if (view.getLayoutParams().width <= 0 || view.getLayoutParams().height <= 0) {n return;n }nn if(!TextUtils.isEmpty(strategy.placeHolder)){n Picasso.Builder builder=new Picasso.Builder(WXEnvironment.getApplication());n Picasso picasso=builder.build();n picasso.load(Uri.parse(strategy.placeHolder)).into(view);nn view.setTag(strategy.placeHolder.hashCode(),picasso);n }nn Picasso.with(WXEnvironment.getApplication())n .load(temp)n .into(view, new Callback() {n [@Override](/user/Override)n public void onSuccess() {n if(strategy.getImageListener()!=null){n strategy.getImageListener().onImageFinish(url,view,true,null);n }nn if(!TextUtils.isEmpty(strategy.placeHolder)){n ((Picasso) view.getTag(strategy.placeHolder.hashCode())).cancelRequest(view);n }n }nn [@Override](/user/Override)n public void onError() {n if(strategy.getImageListener()!=null){n strategy.getImageListener().onImageFinish(url,view,false,null);n }n }n });n }n },0);n }n
故事七: 生產環境的實踐
增量更新
方案一
可以使用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也是極好的
推薦閱讀:
※npm 三點疑問?
※前端周刊第58期:送你 3 道面試題
※國外優秀的H5頁面,nice到不行
※前端周刊第61期:你離 CTC 有多遠?