无码H版动漫在线观看_国产成人91国精品_国产三级在线播放第一页_色综合久久久久综合体桃花网_亚洲中文字幕1235

歡迎來(lái)到深圳市來(lái)科信科技有限公司網(wǎng)站!

已閱讀

基于Vue和TS的Web移動(dòng)端發(fā)開(kāi)項(xiàng)目實(shí)戰(zhàn)心得

來(lái)源:lexintech.com ?? ?? 發(fā)布時(shí)間:2019-09-27
筆者在公司用 web 技術(shù)開(kāi)發(fā)移動(dòng)端應(yīng)用已經(jīng)有一年多的時(shí)間了,開(kāi)始主要以 vue 技術(shù)棧配合 native 為主,目前演進(jìn)成 vue + react native 技術(shù)架構(gòu),vue 主要負(fù)責(zé)開(kāi)發(fā) OA 業(yè)務(wù),比如報(bào)銷、出差、crm 等等,react native 主要負(fù)責(zé)即時(shí)通信部分,是在 mattermost-mobile[1] 的基礎(chǔ)上修改的(mattermost 是一個(gè)開(kāi)源的即時(shí)通訊方案)。

因?yàn)楣驹谶@方面沒(méi)有太多技術(shù)沉淀,所以在開(kāi)發(fā)期間遇到了很多坑,經(jīng)過(guò)一年多的技術(shù)攻克積累,最終形成了這套比較完善的解決方案,總結(jié)出來(lái)希望能夠幫助到大家,尤其是對(duì)一些中小公司這方面經(jīng)驗(yàn)不足的(PS: 大公司估計(jì)有他們自己的一套方案了)。

好了廢話不多說(shuō),先亮下這個(gè)庫(kù)的 GitHub 地址,后面還會(huì)不斷完善,歡迎 star:

mobile-web-best-practice[2]

移動(dòng)端 web 最佳實(shí)踐,基于 vue-cli3[3] 搭建的 typescript[4] 項(xiàng)目,可以用于 hybrid 應(yīng)用或者純 webapp 開(kāi)發(fā)。以下大部分內(nèi)容同樣適用于 react[5] 等前端框架。

其中有三個(gè)點(diǎn)尚在完善中:領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)應(yīng)用、微前端、性能監(jiān)控,后續(xù)完成后會(huì)以單獨(dú)的文章發(fā)出來(lái)。其中性能監(jiān)控還沒(méi)有太好的選擇,類似錯(cuò)誤監(jiān)控 sentry 那種開(kāi)源免費(fèi)而且功能強(qiáng)大的工具,如果有人知道的麻煩告知下。文中難免有些錯(cuò)誤或者更好的方案,也歡迎不吝賜教。

目錄

  • 組件庫(kù)[6]

  • JSBridge[7]

  • 路由堆棧管理(模擬原生 APP 導(dǎo)航)[8]

  • 請(qǐng)求數(shù)據(jù)緩存[9]

  • 構(gòu)建時(shí)預(yù)渲染[10]

  • Webpack 策略[11]

    • 基礎(chǔ)庫(kù)抽離[12]

  • 手勢(shì)庫(kù)[13]

  • 樣式適配[14]

  • 表單校驗(yàn)[15]

  • 阻止原生返回事件[16]

  • 通過(guò) UA 獲取設(shè)備信息[17]

  • mock 數(shù)據(jù)[18]

  • 調(diào)試控制臺(tái)[19]

  • 抓包工具[20]

  • 異常監(jiān)控平臺(tái)[21]

  • 常見(jiàn)問(wèn)題[22]

組件庫(kù)

vant[23]

vux[24]

mint-ui[25]

cube-ui[26]

vue 移動(dòng)端組件庫(kù)目前主要就是上面羅列的這幾個(gè)庫(kù),本項(xiàng)目使用的是有贊前端團(tuán)隊(duì)開(kāi)源的 vant。

vant 官方目前已經(jīng)支持自定義樣式主題,基本原理就是在 less-loader[27] 編譯 less[28] 文件到 css 文件過(guò)程中,利用 less 提供的 modifyVars[29] 對(duì) less 變量進(jìn)行修改,本項(xiàng)目也采用了該方式,具體配置請(qǐng)查看相關(guān)文檔:

定制主題[30]

推薦一篇介紹各個(gè)組件庫(kù)特點(diǎn)的文章:

Vue 常用組件庫(kù)的比較分析(移動(dòng)端)[31]

JSBridge

DSBridge-IOS[32]

DSBridge-Android[33]

WebViewJavascriptBridge[34]

混合應(yīng)用中一般都是通過(guò) webview 加載網(wǎng)頁(yè),而當(dāng)網(wǎng)頁(yè)要獲取設(shè)備能力(例如調(diào)用攝像頭、本地日歷等)或者 native 需要調(diào)用網(wǎng)頁(yè)里的方法,就需要通過(guò) JSBridge 進(jìn)行通信。

開(kāi)源社區(qū)中有很多功能強(qiáng)大的 JSBridge,例如上面列舉的庫(kù)。本項(xiàng)目基于保持 iOS android 平臺(tái)接口統(tǒng)一原因,采用了 DSBridge,各位可以選擇適合自己項(xiàng)目的工具。

本項(xiàng)目以 h5 調(diào)用 native 提供的同步日歷接口為例,演示如何在 dsbridge 基礎(chǔ)上進(jìn)行兩端通信的。下面是兩端的關(guān)鍵代碼摘要:

安卓端同步日歷核心代碼,具體代碼請(qǐng)查看與本項(xiàng)目配套的安卓項(xiàng)目 mobile-web-best-practice-container[35]

public class JsApi {
    /**
     * 同步日歷接口
     * msg 格式如下:
     * ...
     */
    @JavascriptInterface
    public void syncCalendar(Object msg, CompletionHandler handler) {
        try {
            JSONObject obj = new JSONObject(msg.toString());
            String id = obj.getString("id");
            String title = obj.getString("title");
            String location = obj.getString("location");
            long startTime = obj.getLong("startTime");
            long endTime = obj.getLong("endTime");
            JSONArray earlyRemindTime = obj.getJSONArray("alarm");
            String res = CalendarReminderUtils.addCalendarEvent(id, title, location, startTime, endTime, earlyRemindTime);
            handler.complete(Integer.valueOf(res));
        } catch (Exception e) {
            e.printStackTrace();
            handler.complete(6005);
        }
    }
}

h5 端同步日歷核心代碼(通過(guò)裝飾器來(lái)限制調(diào)用接口的平臺(tái))

class NativeMethods {
  // 同步到日歷
  @p()
  public syncCalendar(params: SyncCalendarParams) {
    const cb = (errCode: number) => {
      const msg = NATIVE_ERROR_CODE_MAP[errCode];

      Vue.prototype.$toast(msg);

      if (errCode !== 6000) {
        this.errorReport(msg, 'syncCalendar', params);
      }
    };
    dsbridge.call('syncCalendar', params, cb);
  }

  // 調(diào)用 native 接口出錯(cuò)向 sentry 發(fā)送錯(cuò)誤信息
  private errorReport(errorMsg: string, methodName: string, params: any) {
    if (window.$sentry) {
      const errorInfo: NativeApiErrorInfo = {
        error: new Error(errorMsg),
        type: 'callNative',
        methodName,
        params: JSON.stringify(params)
      };
      window.$sentry.log(errorInfo);
    }
  }
}

/**
 * @param {platforms} - 接口限制的平臺(tái)
 * @return {Function} - 裝飾器
 */
function p(platforms = ['android', 'ios']) {
  return (target: AnyObject, name: string, descriptor: PropertyDescriptor) => {
    if (!platforms.includes(window.$platform)) {
      descriptor.value = () => {
        return Vue.prototype.$toast(
          `當(dāng)前處在 ${window.$platform} 環(huán)境,無(wú)法調(diào)用接口哦`
        );
      };
    }

    return descriptor;
  };
}

另外推薦一個(gè)筆者之前寫(xiě)的一個(gè)基于安卓平臺(tái)實(shí)現(xiàn)的教學(xué)版 JSBridge[36],里面詳細(xì)闡述了如何基于底層接口一步步封裝一個(gè)可用的 JSBridge:

JSBridge 實(shí)現(xiàn)原理[37]

路由堆棧管理(模擬原生 APP 導(dǎo)航)

vue-page-stack[38]

vue-navigation[39]

vue-stack-router[40]

在使用 h5 開(kāi)發(fā) app,會(huì)經(jīng)常遇到下面的需求:從列表進(jìn)入詳情頁(yè),返回后能夠記住當(dāng)前位置,或者從表單點(diǎn)擊某項(xiàng)進(jìn)入到其他頁(yè)面選擇,然后回到表單頁(yè),需要記住之前表單填寫(xiě)的數(shù)據(jù)??墒悄壳?vue 或 react 框架的路由,均不支持同時(shí)存在兩個(gè)頁(yè)面實(shí)例,所以需要路由堆棧進(jìn)行管理。

其中 vue-page-stack 和 vue-navigation 均受 vue 的 keepalive 啟發(fā),基于 vue-router[41],當(dāng)進(jìn)入某個(gè)頁(yè)面時(shí),會(huì)查看當(dāng)前頁(yè)面是否有緩存,有緩存的話就取出緩存,并且清除排在他后面的所有 vnode,沒(méi)有緩存就是新的頁(yè)面,需要存儲(chǔ)或者是 replace 當(dāng)前頁(yè)面,向棧里面 push 對(duì)應(yīng)的 vnode,從而實(shí)現(xiàn)記住頁(yè)面狀態(tài)的功能。

而邏輯思維前端團(tuán)隊(duì)的 vue-stack-router 則另辟蹊徑,拋開(kāi)了 vue-router,自己獨(dú)立實(shí)現(xiàn)了路由管理,相較于 vue-router,主要是支持同時(shí)可以存活 A 和 B 兩個(gè)頁(yè)面的實(shí)例,或者 A 頁(yè)面不同狀態(tài)的兩個(gè)實(shí)例,并支持原生左滑功能。但由于項(xiàng)目還在初期完善,功能還沒(méi)有 vue-router 強(qiáng)大,建議持續(xù)關(guān)注后續(xù)動(dòng)態(tài)再做決定是否引入。

本項(xiàng)目使用的是 vue-page-stack,各位可以選擇適合自己項(xiàng)目的工具。同時(shí)推薦幾篇相關(guān)文章:

【vue-page-stack】Vue 單頁(yè)應(yīng)用導(dǎo)航管理器 正式發(fā)布[42]

Vue 社區(qū)的路由解決方案:vue-stack-router[43]

請(qǐng)求數(shù)據(jù)緩存

mem[44]

在我們的應(yīng)用中,會(huì)存在一些很少改動(dòng)的數(shù)據(jù),而這些數(shù)據(jù)有需要從后端獲取,比如公司人員、公司職位分類等,此類數(shù)據(jù)在很長(zhǎng)一段時(shí)間時(shí)不會(huì)改變的,而每次打開(kāi)頁(yè)面或切換頁(yè)面時(shí),就重新向后端請(qǐng)求。為了能夠減少不必要請(qǐng)求,加快頁(yè)面渲染速度,可以引用 mem 緩存庫(kù)。

mem 基本原理是通過(guò)以接收的函數(shù)為 key 創(chuàng)建一個(gè) WeakMap,然后再以函數(shù)參數(shù)為 key 創(chuàng)建一個(gè) Map,value 就是函數(shù)的執(zhí)行結(jié)果,同時(shí)將這個(gè) Map 作為剛剛的 WeakMap 的 value 形成嵌套關(guān)系,從而實(shí)現(xiàn)對(duì)同一個(gè)函數(shù)不同參數(shù)進(jìn)行緩存。而且支持傳入 maxAge,即數(shù)據(jù)的有效期,當(dāng)某個(gè)數(shù)據(jù)到達(dá)有效期后,會(huì)自動(dòng)銷毀,避免內(nèi)存泄漏。

選擇 WeakMap 是因?yàn)槠湎鄬?duì) Map 保持對(duì)鍵名所引用的對(duì)象是弱引用,即垃圾回收機(jī)制不將該引用考慮在內(nèi)。只要所引用的對(duì)象的其他引用都被清除,垃圾回收機(jī)制就會(huì)釋放該對(duì)象所占用的內(nèi)存。也就是說(shuō),一旦不再需要,WeakMap 里面的鍵名對(duì)象和所對(duì)應(yīng)的鍵值對(duì)會(huì)自動(dòng)消失,不用手動(dòng)刪除引用。

mem 作為高階函數(shù),可以直接接受封裝好的接口請(qǐng)求。但是為了更加直觀簡(jiǎn)便,我們可以按照類的形式集成我們的接口函數(shù),然后就可以用裝飾器的方式使用 mem 了(裝飾器只能修飾類和類的類的方法,因?yàn)槠胀ê瘮?shù)會(huì)存在變量提升)。下面是相關(guān)代碼:

import http from '../http';
import mem from 'mem';

/**
 * @param {MemOption} - mem 配置項(xiàng)
 * @return {Function} - 裝飾器
 */
export default function m(options: AnyObject) {
  return (target: AnyObject, name: string, descriptor: PropertyDescriptor) => {
    const oldValue = descriptor.value;
    descriptor.value = mem(oldValue, options);
    return descriptor;
  };
}

class Home {
  @m({ maxAge: 60 * 1000 })
  public async getUnderlingDailyList(
    query: ListQuery
  ): Promise<{ total: number; list: DailyItem[] }> {
    const {
      data: { total, list }
    } = await http({
      method: 'post',
      url: '/daily/getList',
      data: query
    });

    return { total, list };
  }
}

export default new Home();

 

構(gòu)建時(shí)預(yù)渲染

針對(duì)目前單頁(yè)面首屏渲染時(shí)間長(zhǎng)(需要下載解析 js 文件然后渲染元素并掛載到 id 為 app 的 div 上),SEO 不友好(index.html 的 body 上實(shí)際元素只有 id 為 app 的 div 元素,真正的頁(yè)面元素都是動(dòng)態(tài)掛載的,搜索引擎的爬蟲(chóng)無(wú)法捕捉到),目前主流解決方案就是服務(wù)端渲染(SSR),即從服務(wù)端生成組裝好的完整靜態(tài) html 發(fā)送到瀏覽器進(jìn)行展示,但配置較為復(fù)雜,一般都會(huì)借助框架,比如 vue 的 nuxt.js[45],react 的 next[46]。

其實(shí)有一種更簡(jiǎn)便的方式--構(gòu)建時(shí)預(yù)渲染。顧名思義,就是項(xiàng)目打包構(gòu)建完成后,啟動(dòng)一個(gè) Web Server 來(lái)運(yùn)行整個(gè)網(wǎng)站,再開(kāi)啟多個(gè)無(wú)頭瀏覽器(例如 Puppeteer[47]Phantomjs[48] 等無(wú)頭瀏覽器技術(shù))去請(qǐng)求項(xiàng)目中所有的路由,當(dāng)請(qǐng)求的網(wǎng)頁(yè)渲染到第一個(gè)需要預(yù)渲染的頁(yè)面時(shí)(需提前配置需要預(yù)渲染頁(yè)面的路由),會(huì)主動(dòng)拋出一個(gè)事件,該事件由無(wú)頭瀏覽器截獲,然后將此時(shí)的頁(yè)面內(nèi)容生成一個(gè) HTML(包含了 JS 生成的 DOM 結(jié)構(gòu)和 CSS 樣式),保存到打包文件夾中。

根據(jù)上面的描述,我們可以其實(shí)它本質(zhì)上就只是快照頁(yè)面,不適合過(guò)度依賴后端接口的動(dòng)態(tài)頁(yè)面,比較適合變化不頻繁的靜態(tài)頁(yè)面。

實(shí)際項(xiàng)目相關(guān)工具方面比較推薦 prerender-spa-plugin[49] 這個(gè) webpack 插件,下面是這個(gè)插件的原理圖。不過(guò)有兩點(diǎn)需要注意:

一個(gè)是這個(gè)插件需要依賴 Puppeteer,而因?yàn)閲?guó)內(nèi)網(wǎng)絡(luò)原因以及本身體積較大,經(jīng)常下載失敗,不過(guò)可以通過(guò) .npmrc 文件指定 Puppeteer 的下載路徑為國(guó)內(nèi)鏡像;

另一個(gè)是需要設(shè)置路由模式為 history 模式(即基于 html5 提供的 history api 實(shí)現(xiàn)的,react 叫 BrowserRouter,vue 叫 history),因?yàn)?hash 路由無(wú)法對(duì)應(yīng)到實(shí)際的物理路由。(即線上渲染時(shí) history 下,如果 form 路由被設(shè)置成預(yù)渲染,那么訪問(wèn) /form/ 路由時(shí),會(huì)直接從服務(wù)端返回 form 文件夾下的 index.html,之前打包時(shí)就已經(jīng)預(yù)先生成了完整的 HTML 文件 )

本項(xiàng)目已經(jīng)集成了 prerender-spa-plugin,但由于和 vue-stack-page/vue-navigation 這類路由堆棧管理器一起使用有問(wèn)題(原因還在查找,如果知道的朋友也可以告知下),所以 prerender 功能是關(guān)閉的。

同時(shí)推薦幾篇相關(guān)文章:

vue 預(yù)渲染之 prerender-spa-plugin 解析(一)[50]

使用預(yù)渲提升 SPA 應(yīng)用體驗(yàn)[51]

Webpack 策略

基礎(chǔ)庫(kù)抽離

對(duì)于一些基礎(chǔ)庫(kù),例如 vue、moment 等,屬于不經(jīng)常變化的靜態(tài)依賴,一般需要抽離出來(lái)以提升每次構(gòu)建的效率。目前主流方案有兩種:

一種是使用 webpack-dll-plugin[52] 插件,在首次構(gòu)建時(shí)就講這些靜態(tài)依賴單獨(dú)打包,后續(xù)只需引入早已打包好的靜態(tài)依賴包即可;

另一種就是外部擴(kuò)展 Externals[53] 方式,即把不需要打包的靜態(tài)資源從構(gòu)建中剔除,使用 CDN 方式引入。下面是 webpack-dll-plugin 相對(duì) Externals 的缺點(diǎn):

  1. 需要配置在每次構(gòu)建時(shí)都不參與編譯的靜態(tài)依賴,并在首次構(gòu)建時(shí)為它們預(yù)編譯出一份 JS 文件(后文將稱其為 lib 文件),每次更新依賴需要手動(dòng)進(jìn)行維護(hù),一旦增刪依賴或者變更資源版本忘記更新,就會(huì)出現(xiàn) Error 或者版本錯(cuò)誤。

  2. 無(wú)法接入瀏覽器的新特性 script type="module",對(duì)于某些依賴庫(kù)提供的原生 ES Modules 的引入方式(比如 vue 的新版引入方式)無(wú)法得到支持,沒(méi)法更好地適配高版本瀏覽器提供的優(yōu)良特性以實(shí)現(xiàn)更好地性能優(yōu)化。

  3. 將所有資源預(yù)編譯成一份文件,并將這份文件顯式注入項(xiàng)目構(gòu)建的 HTML 模板中,這樣的做法,在 HTTP1 時(shí)代是被推崇的,因?yàn)槟菢幽軠p少資源的請(qǐng)求數(shù)量,但在 HTTP2 時(shí)代如果拆成多個(gè) CDN Link,就能夠更充分地利用 HTTP2 的多路復(fù)用特性。

不過(guò)選擇 Externals 還是需要一個(gè)靠譜的 CDN 服務(wù)的。

本項(xiàng)目選擇的是 Externals,各位可根據(jù)項(xiàng)目需求選擇不同的方案。

更多內(nèi)容請(qǐng)查看這篇文章(上面觀點(diǎn)來(lái)自于這篇文章):

Webpack 優(yōu)化——將你的構(gòu)建效率提速翻倍[54]

手勢(shì)庫(kù)

hammer.js[55]

AlloyFinger[56]

在移動(dòng)端開(kāi)發(fā)中,一般都需要支持一些手勢(shì),例如拖動(dòng)(Pan),縮放(Pinch),旋轉(zhuǎn)(Rotate),滑動(dòng)(swipe)等。目前已經(jīng)有很成熟的方案了,例如 hammer.js 和騰訊前端團(tuán)隊(duì)開(kāi)發(fā)的 AlloyFinger 都很不錯(cuò)。本項(xiàng)目選擇基于 hammer.js 進(jìn)行二次封裝成 vue 指令集,各位可根據(jù)項(xiàng)目需求選擇不同的方案。

下面是二次封裝的關(guān)鍵代碼,其中用到了 webpack 的 require.context 函數(shù)來(lái)獲取特定模塊的上下文,主要用來(lái)實(shí)現(xiàn)自動(dòng)化導(dǎo)入模塊,比較適用于像 vue 指令這種模塊較多的場(chǎng)景:

// 用于導(dǎo)入模塊的上下文
export const importAll = (
  context: __WebpackModuleApi.RequireContext,
  options: ImportAllOptions = {}
): AnyObject => {
  const { useDefault = true, keyTransformFunc, filterFunc } = options;

  let keys = context.keys();

  if (isFunction(filterFunc)) {
    keys = keys.filter(filterFunc);
  }

  return keys.reduce((acc: AnyObject, curr: string) => {
    const key = isFunction(keyTransformFunc) ? keyTransformFunc(curr) : curr;
    acc[key] = useDefault ? context(curr).default : context(curr);
    return acc;
  }, {});
};

// directives 文件夾下的 index.ts
const directvieContext = require.context('./', false, /.ts$/);
const directives = importAll(directvieContext, {
  filterFunc: (key: string) => key !== './index.ts',
  keyTransformFunc: (key: string) =>
    key.replace(/^.//, '').replace(/.ts$/, '')
});

export default {
  install(vue: typeof Vue): void {
    Object.keys(directives).forEach((key) =>
      vue.directive(key, directives[key])
    );
  }
};

// touch.ts
export default {
  bind(el: HTMLElement, binding: DirectiveBinding) {
    const hammer: HammerManager = new Hammer(el);
    const touch = binding.arg as Touch;
    const listener = binding.value as HammerListener;
    const modifiers = Object.keys(binding.modifiers);

    switch (touch) {
      case Touch.Pan:
        const panEvent = detectPanEvent(modifiers);
        hammer.on(`pan${panEvent}`, listener);
        break;
      ...
    }
  }
};

另外推薦一篇關(guān)于 hammer.js 和一篇關(guān)于 require.context 的文章:

H5 案例分享:JS 手勢(shì)框架 —— Hammer.js[57]

使用 require.context 實(shí)現(xiàn)前端工程自動(dòng)化[58]

樣式適配

postcss-px-to-viewport[59]

Viewport Units Buggyfill[60]

flexible[61]

postcss-pxtorem[62]

Autoprefixer[63]

browserslist[64]

在移動(dòng)端網(wǎng)頁(yè)開(kāi)發(fā)時(shí),樣式適配始終是一個(gè)繞不開(kāi)的問(wèn)題。對(duì)此目前主流方案有 vw 和 rem(當(dāng)然還有 vw + rem 結(jié)合方案,請(qǐng)見(jiàn)下方 rem-vw-layout 倉(cāng)庫(kù)),其實(shí)基本原理都是相通的,就是隨著屏幕寬度或字體大小成正比變化。因?yàn)樵矸矫娴脑敿?xì)資料網(wǎng)絡(luò)上已經(jīng)有很多了,就不在這里贅述了。下面主要提供一些這工程方面的工具。

關(guān)于 rem,阿里無(wú)線前端團(tuán)隊(duì)在 15 年的時(shí)候基于 rem 推出了 flexible 方案,以及 postcss 提供的自動(dòng)轉(zhuǎn)換 px 到 rem 的插件 postcss-pxtorem。

關(guān)于 vw,可以使用 postcss-px-to-viewport 進(jìn)行自動(dòng)轉(zhuǎn)換 px 到 vw。postcss-px-to-viewport 相關(guān)配置如下:

"postcss-px-to-viewport": {
  viewportWidth: 375, // 視窗的寬度,對(duì)應(yīng)的是我們?cè)O(shè)計(jì)稿的寬度,一般是375
  viewportHeight: 667, // 視窗的高度,根據(jù)750設(shè)備的寬度來(lái)指定,一般指定1334,也可以不配置
  unitPrecision: 3,  // 指定`px`轉(zhuǎn)換為視窗單位值的小數(shù)位數(shù)(很多時(shí)候無(wú)法整除)
  viewportUnit: 'vw', // 指定需要轉(zhuǎn)換成的視窗單位,建議使用vw
  selectorBlackList: ['.ignore', '.hairlines'], // 指定不轉(zhuǎn)換為視窗單位的類,可以自定義,可以無(wú)限添加,建議定義一至兩個(gè)通用的類名
  minPixelValue: 1, // 小于或等于`1px`不轉(zhuǎn)換為視窗單位,你也可以設(shè)置為你想要的值
  mediaQuery: false // 媒體查詢里的單位是否需要轉(zhuǎn)換單位
}

下面是 vw 和 rem 的優(yōu)缺點(diǎn)對(duì)比圖:

關(guān)于 vw 兼容性問(wèn)題,目前在移動(dòng)端 iOS 8 以上以及 Android 4.4 以上獲得支持。如果有兼容更低版本需求的話,可以選擇 viewport 的 pollify 方案,其中比較主流的是 Viewport Units Buggyfill[65]。

本方案因不準(zhǔn)備兼容低版本,所以直接選擇了 vw 方案,各位可根據(jù)項(xiàng)目需求選擇不同的方案。

另外關(guān)于設(shè)置 css 兼容不同瀏覽器,想必大家都知道 Autoprefixer(vue-cli3 已經(jīng)默認(rèn)集成了),那么如何設(shè)置要兼容的范圍呢?推薦使用 browserslist,可以在 .browserslistrc 或者 pacakage.json 中 browserslist 部分設(shè)置兼容瀏覽器范圍。因?yàn)椴恢?Autoprefixer,還有 Babel,postcss-preset-env 等工具都會(huì)讀取 browserslist 的兼容配置,這樣比較容易使 js css 兼容瀏覽器的范圍保持一致。下面是本項(xiàng)目的 .browserslistrc 配置:

iOS >= 10  //  即 iOS Safari
Android >= 6.0 // 即 Android WebView
last 2 versions // 每個(gè)瀏覽器最近的兩個(gè)版本

最后推薦一些移動(dòng)端樣式適配的資料:

rem-vw-layout[66]

細(xì)說(shuō)移動(dòng)端 經(jīng)典的 REM 布局 與 新秀 VW 布局[67]

如何在 Vue 項(xiàng)目中使用 vw 實(shí)現(xiàn)移動(dòng)端適配[68]

表單校驗(yàn)

async-validator[69]

vee-validate[70]

由于大部分移動(dòng)端組件庫(kù)都不提供表單校驗(yàn),因此需要自己封裝。目前比較多的方式就是基于 async-validator 進(jìn)行二次封裝(elementUI 組件庫(kù)提供的表單校驗(yàn)也是基于 async-validator ),或者使用 vee-validate(一種基于 vue 模板的輕量級(jí)校驗(yàn)框架)進(jìn)行校驗(yàn),各位可根據(jù)項(xiàng)目需求選擇不同的方案。

本項(xiàng)目的表單校驗(yàn)方案是在 async-validator 基礎(chǔ)上進(jìn)行二次封裝,代碼如下,原理很簡(jiǎn)單,基本滿足需求。如果還有更完善的方案,歡迎提出來(lái)。

其中 setRules 方法是將組件中設(shè)置的 rules(符合 async-validator 約定的校驗(yàn)規(guī)則)按照需要校驗(yàn)的數(shù)據(jù)的名字為 key 轉(zhuǎn)化一個(gè)對(duì)象 validator,value 是 async-validator 生成的實(shí)例。validator 方法可以接收單個(gè)或多個(gè)需要校驗(yàn)的數(shù)據(jù)的 key,然后就會(huì)在 setRules 生成的對(duì)象 validator 中尋找 key 對(duì)應(yīng)的 async-validator 實(shí)例,最后調(diào)用實(shí)例的校驗(yàn)方法。當(dāng)然也可以不接受參數(shù),那么就會(huì)校驗(yàn)所有傳入的數(shù)據(jù)。

import schema from 'async-validator';
...

class ValidatorUtils {
  private data: AnyObject;
  private validators: AnyObject;

  constructor({ rules = {}, data = {}, cover = true }) {
    this.validators = {};
    this.data = data;
    this.setRules(rules, cover);
  }

  /**
   * 設(shè)置校驗(yàn)規(guī)則
   * @param rules async-validator 的校驗(yàn)規(guī)則
   * @param cover 是否替換舊規(guī)則
   */
  public setRules(rules: ValidateRules, cover: boolean) {
    if (cover) {
      this.validators = {};
    }

    Object.keys(rules).forEach((key) => {
      this.validators[key] = new schema({ [key]: rules[key] });
    });
  }

  public validate(
    dataKey?: string | string[]
  ): Promisestring | string[] | undefined> {
    // 錯(cuò)誤數(shù)組
    const err: ValidateError[] = [];

    Object.keys(this.validators)
      .filter((key) => {
        // 若不傳 dataKey 則校驗(yàn)全部。否則校驗(yàn) dataKey 對(duì)應(yīng)的數(shù)據(jù)(dataKey 可以對(duì)應(yīng)一個(gè)(字符串)或多個(gè)(數(shù)組))
        return (
          !dataKey ||
          (dataKey &&
            ((_.isString(dataKey) && dataKey === key) ||
              (_.isArray(dataKey) && dataKey.includes(key))))
        );
      })
      .forEach((key) => {
        this.validators[key].validate(
          { [key]: this.data[key] },
          (error: ValidateError[]) => {
            if (error) {
              err.push(error[0]);
            }
          }
        );
      });

    if (err.length > 0) {
      return Promise.reject(err);
    } else {
      return Promise.resolve(dataKey);
    }
  }
}

阻止原生返回事件

開(kāi)發(fā)中可能會(huì)遇到下面這個(gè)需求:當(dāng)頁(yè)面彈出一個(gè) popup 或 dialog 組件時(shí),點(diǎn)擊返回鍵時(shí)是隱藏彈出的組件而不是返回到上一個(gè)頁(yè)面。

為了解決這個(gè)問(wèn)題,我們可以從路由棧角度思考。一般彈出組件是不會(huì)在路由棧上添加任何記錄,因此我們?cè)趶棾鼋M件時(shí),可以在路由棧中 push 一個(gè)記錄,為了不讓頁(yè)面跳轉(zhuǎn),我們可以把跳轉(zhuǎn)的目標(biāo)路由設(shè)置為當(dāng)前頁(yè)面路由,并加上一個(gè) query 來(lái)標(biāo)記這個(gè)組件彈出的狀態(tài)。

然后監(jiān)聽(tīng) query 的變化,當(dāng)點(diǎn)擊彈出組件時(shí),query 中與該彈出組件有關(guān)的標(biāo)記變?yōu)?true,則將彈出組件設(shè)為顯示;當(dāng)用戶點(diǎn)擊 native 返回鍵時(shí),路由返回上一個(gè)記錄,仍然是當(dāng)前頁(yè)面路由,不過(guò) query 中與該彈出組件有關(guān)的標(biāo)記不再是 true 了,這樣我們就可以把彈出組件設(shè)置成隱藏,同時(shí)不會(huì)返回上一個(gè)頁(yè)面。

APP開(kāi)發(fā) 網(wǎng)站開(kāi)發(fā) 產(chǎn)品設(shè)計(jì) 微信公眾號(hào) APP開(kāi)發(fā)公司 用戶體驗(yàn) APP運(yùn)營(yíng) 微信小程序 產(chǎn)品經(jīng)理 網(wǎng)站設(shè)計(jì)