歡迎來(lái)到深圳市志博科技有限公司網站!
您當前的(de)位置:深圳APP開發 > 新聞資訊 > 網站建設資訊 >
人(rén)
已閱讀

基于Vue和(hé)TS的(de)Web移動端發開項目實戰心得(de)

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

因爲公司在這(zhè)方面沒有太多(duō)技術沉澱,所以在開發期間遇到了(le)很多(duō)坑,經過一年多(duō)的(de)技術攻克積累,最終形成了(le)這(zhè)套比較完善的(de)解決方案,總結出來(lái)希望能夠幫助到大(dà)家,尤其是對(duì)一些中小公司這(zhè)方面經驗不足的(de)(PS: 大(dà)公司估計有他(tā)們自己的(de)一套方案了(le))。

好了(le)廢話(huà)不多(duō)說,先亮下(xià)這(zhè)個(gè)庫的(de) GitHub 地址,後面還(hái)會不斷完善,歡迎 star:

mobile-web-best-practice[2]

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

其中有三個(gè)點尚在完善中:領域驅動設計(DDD)應用(yòng)、微前端、性能監控,後續完成後會以單獨的(de)文章(zhāng)發出來(lái)。其中性能監控還(hái)沒有太好的(de)選擇,類似錯誤監控 sentry 那種開源免費而且功能強大(dà)的(de)工具,如果有人(rén)知道的(de)麻煩告知下(xià)。文中難免有些錯誤或者更好的(de)方案,也(yě)歡迎不吝賜教。

目錄

  • 組件庫[6]

  • JSBridge[7]

  • 路由堆棧管理(lǐ)(模拟原生 APP 導航)[8]

  • 請求數據緩存[9]

  • 構建時(shí)預渲染[10]

  • Webpack 策略[11]

    • 基礎庫抽離[12]

  • 手勢庫[13]

  • 樣式适配[14]

  • 表單校驗[15]

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

  • 通(tōng)過 UA 獲取設備信息[17]

  • mock 數據[18]

  • 調試控制台[19]

  • 抓包工具[20]

  • 異常監控平台[21]

  • 常見問題[22]

組件庫

vant[23]

vux[24]

mint-ui[25]

cube-ui[26]

vue 移動端組件庫目前主要就是上面羅列的(de)這(zhè)幾個(gè)庫,本項目使用(yòng)的(de)是有贊前端團隊開源的(de) vant。

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

定制主題[30]

推薦一篇介紹各個(gè)組件庫特點的(de)文章(zhāng):

Vue 常用(yòng)組件庫的(de)比較分(fēn)析(移動端)[31]

JSBridge

DSBridge-IOS[32]

DSBridge-Android[33]

WebViewJavascriptBridge[34]

混合應用(yòng)中一般都是通(tōng)過 webview 加載網頁,而當網頁要獲取設備能力(例如調用(yòng)攝像頭、本地日曆等)或者 native 需要調用(yòng)網頁裏的(de)方法,就需要通(tōng)過 JSBridge 進行通(tōng)信。

開源社區(qū)中有很多(duō)功能強大(dà)的(de) JSBridge,例如上面列舉的(de)庫。本項目基于保持 iOS android 平台接口統一原因,采用(yòng)了(le) DSBridge,各位可(kě)以選擇适合自己項目的(de)工具。

本項目以 h5 調用(yòng) native 提供的(de)同步日曆接口爲例,演示如何在 dsbridge 基礎上進行兩端通(tōng)信的(de)。下(xià)面是兩端的(de)關鍵代碼摘要:

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

public class JsApi {
    /**
     * 同步日曆接口
     * msg 格式如下(xià):
     * ...
     */
    @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 端同步日曆核心代碼(通(tōng)過裝飾器來(lái)限制調用(yòng)接口的(de)平台)

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);
  }

  // 調用(yòng) native 接口出錯向 sentry 發送錯誤信息
  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} - 接口限制的(de)平台
 * @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(
          `當前處在 ${window.$platform} 環境,無法調用(yòng)接口哦`
        );
      };
    }

    return descriptor;
  };
}

另外推薦一個(gè)筆者之前寫的(de)一個(gè)基于安卓平台實現的(de)教學版 JSBridge[36],裏面詳細闡述了(le)如何基于底層接口一步步封裝一個(gè)可(kě)用(yòng)的(de) JSBridge:

JSBridge 實現原理(lǐ)[37]

路由堆棧管理(lǐ)(模拟原生 APP 導航)

vue-page-stack[38]

vue-navigation[39]

vue-stack-router[40]

在使用(yòng) h5 開發 app,會經常遇到下(xià)面的(de)需求:從列表進入詳情頁,返回後能夠記住當前位置,或者從表單點擊某項進入到其他(tā)頁面選擇,然後回到表單頁,需要記住之前表單填寫的(de)數據。可(kě)是目前 vue 或 react 框架的(de)路由,均不支持同時(shí)存在兩個(gè)頁面實例,所以需要路由堆棧進行管理(lǐ)。

其中 vue-page-stack 和(hé) vue-navigation 均受 vue 的(de) keepalive 啓發,基于 vue-router[41],當進入某個(gè)頁面時(shí),會查看當前頁面是否有緩存,有緩存的(de)話(huà)就取出緩存,并且清除排在他(tā)後面的(de)所有 vnode,沒有緩存就是新的(de)頁面,需要存儲或者是 replace 當前頁面,向棧裏面 push 對(duì)應的(de) vnode,從而實現記住頁面狀态的(de)功能。

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

本項目使用(yòng)的(de)是 vue-page-stack,各位可(kě)以選擇适合自己項目的(de)工具。同時(shí)推薦幾篇相關文章(zhāng):

【vue-page-stack】Vue 單頁應用(yòng)導航管理(lǐ)器 正式發布[42]

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

請求數據緩存

mem[44]

在我們的(de)應用(yòng)中,會存在一些很少改動的(de)數據,而這(zhè)些數據有需要從後端獲取,比如公司人(rén)員(yuán)、公司職位分(fēn)類等,此類數據在很長(cháng)一段時(shí)間時(shí)不會改變的(de),而每次打開頁面或切換頁面時(shí),就重新向後端請求。爲了(le)能夠減少不必要請求,加快(kuài)頁面渲染速度,可(kě)以引用(yòng) mem 緩存庫。

mem 基本原理(lǐ)是通(tōng)過以接收的(de)函數爲 key 創建一個(gè) WeakMap,然後再以函數參數爲 key 創建一個(gè) Map,value 就是函數的(de)執行結果,同時(shí)将這(zhè)個(gè) Map 作爲剛剛的(de) WeakMap 的(de) value 形成嵌套關系,從而實現對(duì)同一個(gè)函數不同參數進行緩存。而且支持傳入 maxAge,即數據的(de)有效期,當某個(gè)數據到達有效期後,會自動銷毀,避免内存洩漏。

選擇 WeakMap 是因爲其相對(duì) Map 保持對(duì)鍵名所引用(yòng)的(de)對(duì)象是弱引用(yòng),即垃圾回收機制不将該引用(yòng)考慮在内。隻要所引用(yòng)的(de)對(duì)象的(de)其他(tā)引用(yòng)都被清除,垃圾回收機制就會釋放該對(duì)象所占用(yòng)的(de)内存。也(yě)就是說,一旦不再需要,WeakMap 裏面的(de)鍵名對(duì)象和(hé)所對(duì)應的(de)鍵值對(duì)會自動消失,不用(yòng)手動删除引用(yòng)。

mem 作爲高(gāo)階函數,可(kě)以直接接受封裝好的(de)接口請求。但是爲了(le)更加直觀簡便,我們可(kě)以按照(zhào)類的(de)形式集成我們的(de)接口函數,然後就可(kě)以用(yòng)裝飾器的(de)方式使用(yòng) mem 了(le)(裝飾器隻能修飾類和(hé)類的(de)類的(de)方法,因爲普通(tōng)函數會存在變量提升)。下(xià)面是相關代碼:

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

/**
 * @param {MemOption} - mem 配置項
 * @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();

 

構建時(shí)預渲染

針對(duì)目前單頁面首屏渲染時(shí)間長(cháng)(需要下(xià)載解析 js 文件然後渲染元素并挂載到 id 爲 app 的(de) div 上),SEO 不友好(index.html 的(de) body 上實際元素隻有 id 爲 app 的(de) div 元素,真正的(de)頁面元素都是動态挂載的(de),搜索引擎的(de)爬蟲無法捕捉到),目前主流解決方案就是服務端渲染(SSR),即從服務端生成組裝好的(de)完整靜态 html 發送到浏覽器進行展示,但配置較爲複雜(zá),一般都會借助框架,比如 vue 的(de) nuxt.js[45],react 的(de) next[46]

其實有一種更簡便的(de)方式--構建時(shí)預渲染。顧名思義,就是項目打包構建完成後,啓動一個(gè) Web Server 來(lái)運行整個(gè)網站,再開啓多(duō)個(gè)無頭浏覽器(例如 Puppeteer[47]Phantomjs[48] 等無頭浏覽器技術)去請求項目中所有的(de)路由,當請求的(de)網頁渲染到第一個(gè)需要預渲染的(de)頁面時(shí)(需提前配置需要預渲染頁面的(de)路由),會主動抛出一個(gè)事件,該事件由無頭浏覽器截獲,然後将此時(shí)的(de)頁面内容生成一個(gè) HTML(包含了(le) JS 生成的(de) DOM 結構和(hé) CSS 樣式),保存到打包文件夾中。

根據上面的(de)描述,我們可(kě)以其實它本質上就隻是快(kuài)照(zhào)頁面,不适合過度依賴後端接口的(de)動态頁面,比較适合變化(huà)不頻(pín)繁的(de)靜态頁面。

實際項目相關工具方面比較推薦 prerender-spa-plugin[49] 這(zhè)個(gè) webpack 插件,下(xià)面是這(zhè)個(gè)插件的(de)原理(lǐ)圖。不過有兩點需要注意:

一個(gè)是這(zhè)個(gè)插件需要依賴 Puppeteer,而因爲國内網絡原因以及本身體積較大(dà),經常下(xià)載失敗,不過可(kě)以通(tōng)過 .npmrc 文件指定 Puppeteer 的(de)下(xià)載路徑爲國内鏡像;

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

本項目已經集成了(le) prerender-spa-plugin,但由于和(hé) vue-stack-page/vue-navigation 這(zhè)類路由堆棧管理(lǐ)器一起使用(yòng)有問題(原因還(hái)在查找,如果知道的(de)朋友也(yě)可(kě)以告知下(xià)),所以 prerender 功能是關閉的(de)。

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

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

使用(yòng)預渲提升 SPA 應用(yòng)體驗[51]

Webpack 策略

基礎庫抽離

對(duì)于一些基礎庫,例如 vue、moment 等,屬于不經常變化(huà)的(de)靜态依賴,一般需要抽離出來(lái)以提升每次構建的(de)效率。目前主流方案有兩種:

一種是使用(yòng) webpack-dll-plugin[52] 插件,在首次構建時(shí)就講這(zhè)些靜态依賴單獨打包,後續隻需引入早已打包好的(de)靜态依賴包即可(kě);

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

  1. 需要配置在每次構建時(shí)都不參與編譯的(de)靜态依賴,并在首次構建時(shí)爲它們預編譯出一份 JS 文件(後文将稱其爲 lib 文件),每次更新依賴需要手動進行維護,一旦增删依賴或者變更資源版本忘記更新,就會出現 Error 或者版本錯誤。

  2. 無法接入浏覽器的(de)新特性 script type="module",對(duì)于某些依賴庫提供的(de)原生 ES Modules 的(de)引入方式(比如 vue 的(de)新版引入方式)無法得(de)到支持,沒法更好地适配高(gāo)版本浏覽器提供的(de)優良特性以實現更好地性能優化(huà)。

  3. 将所有資源預編譯成一份文件,并将這(zhè)份文件顯式注入項目構建的(de) HTML 模闆中,這(zhè)樣的(de)做(zuò)法,在 HTTP1 時(shí)代是被推崇的(de),因爲那樣能減少資源的(de)請求數量,但在 HTTP2 時(shí)代如果拆成多(duō)個(gè) CDN Link,就能夠更充分(fēn)地利用(yòng) HTTP2 的(de)多(duō)路複用(yòng)特性。

不過選擇 Externals 還(hái)是需要一個(gè)靠譜的(de) CDN 服務的(de)。

本項目選擇的(de)是 Externals,各位可(kě)根據項目需求選擇不同的(de)方案。

更多(duō)内容請查看這(zhè)篇文章(zhāng)(上面觀點來(lái)自于這(zhè)篇文章(zhāng)):

Webpack 優化(huà)——将你的(de)構建效率提速翻倍[54]

手勢庫

hammer.js[55]

AlloyFinger[56]

在移動端開發中,一般都需要支持一些手勢,例如拖動(Pan),縮放(Pinch),旋轉(Rotate),滑動(swipe)等。目前已經有很成熟的(de)方案了(le),例如 hammer.js 和(hé)騰訊前端團隊開發的(de) AlloyFinger 都很不錯。本項目選擇基于 hammer.js 進行二次封裝成 vue 指令集,各位可(kě)根據項目需求選擇不同的(de)方案。

下(xià)面是二次封裝的(de)關鍵代碼,其中用(yòng)到了(le) webpack 的(de) require.context 函數來(lái)獲取特定模塊的(de)上下(xià)文,主要用(yòng)來(lái)實現自動化(huà)導入模塊,比較适用(yòng)于像 vue 指令這(zhè)種模塊較多(duō)的(de)場(chǎng)景:

// 用(yòng)于導入模塊的(de)上下(xià)文
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 文件夾下(xià)的(de) 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;
      ...
    }
  }
};

另外推薦一篇關于 hammer.js 和(hé)一篇關于 require.context 的(de)文章(zhāng):

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

使用(yòng) require.context 實現前端工程自動化(huà)[58]

樣式适配

postcss-px-to-viewport[59]

Viewport Units Buggyfill[60]

flexible[61]

postcss-pxtorem[62]

Autoprefixer[63]

browserslist[64]

在移動端網頁開發時(shí),樣式适配始終是一個(gè)繞不開的(de)問題。對(duì)此目前主流方案有 vw 和(hé) rem(當然還(hái)有 vw + rem 結合方案,請見下(xià)方 rem-vw-layout 倉庫),其實基本原理(lǐ)都是相通(tōng)的(de),就是随著(zhe)屏幕寬度或字體大(dà)小成正比變化(huà)。因爲原理(lǐ)方面的(de)詳細資料網絡上已經有很多(duō)了(le),就不在這(zhè)裏贅述了(le)。下(xià)面主要提供一些這(zhè)工程方面的(de)工具。

關于 rem,阿裏無線前端團隊在 15 年的(de)時(shí)候基于 rem 推出了(le) flexible 方案,以及 postcss 提供的(de)自動轉換 px 到 rem 的(de)插件 postcss-pxtorem。

關于 vw,可(kě)以使用(yòng) postcss-px-to-viewport 進行自動轉換 px 到 vw。postcss-px-to-viewport 相關配置如下(xià):

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

下(xià)面是 vw 和(hé) rem 的(de)優缺點對(duì)比圖:

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

本方案因不準備兼容低版本,所以直接選擇了(le) vw 方案,各位可(kě)根據項目需求選擇不同的(de)方案。

另外關于設置 css 兼容不同浏覽器,想必大(dà)家都知道 Autoprefixer(vue-cli3 已經默認集成了(le)),那麽如何設置要兼容的(de)範圍呢(ne)?推薦使用(yòng) browserslist,可(kě)以在 .browserslistrc 或者 pacakage.json 中 browserslist 部分(fēn)設置兼容浏覽器範圍。因爲不止 Autoprefixer,還(hái)有 Babel,postcss-preset-env 等工具都會讀取 browserslist 的(de)兼容配置,這(zhè)樣比較容易使 js css 兼容浏覽器的(de)範圍保持一緻。下(xià)面是本項目的(de) .browserslistrc 配置:

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

最後推薦一些移動端樣式适配的(de)資料:

rem-vw-layout[66]

細說移動端 經典的(de) REM 布局 與 新秀 VW 布局[67]

如何在 Vue 項目中使用(yòng) vw 實現移動端适配[68]

表單校驗

async-validator[69]

vee-validate[70]

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

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

其中 setRules 方法是将組件中設置的(de) rules(符合 async-validator 約定的(de)校驗規則)按照(zhào)需要校驗的(de)數據的(de)名字爲 key 轉化(huà)一個(gè)對(duì)象 validator,value 是 async-validator 生成的(de)實例。validator 方法可(kě)以接收單個(gè)或多(duō)個(gè)需要校驗的(de)數據的(de) key,然後就會在 setRules 生成的(de)對(duì)象 validator 中尋找 key 對(duì)應的(de) async-validator 實例,最後調用(yòng)實例的(de)校驗方法。當然也(yě)可(kě)以不接受參數,那麽就會校驗所有傳入的(de)數據。

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);
  }

  /**
   * 設置校驗規則
   * @param rules async-validator 的(de)校驗規則
   * @param cover 是否替換舊(jiù)規則
   */
  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> {
    // 錯誤數組
    const err: ValidateError[] = [];

    Object.keys(this.validators)
      .filter((key) => {
        // 若不傳 dataKey 則校驗全部。否則校驗 dataKey 對(duì)應的(de)數據(dataKey 可(kě)以對(duì)應一個(gè)(字符串)或多(duō)個(gè)(數組))
        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ě)能會遇到下(xià)面這(zhè)個(gè)需求:當頁面彈出一個(gè) popup 或 dialog 組件時(shí),點擊返回鍵時(shí)是隐藏彈出的(de)組件而不是返回到上一個(gè)頁面。

爲了(le)解決這(zhè)個(gè)問題,我們可(kě)以從路由棧角度思考。一般彈出組件是不會在路由棧上添加任何記錄,因此我們在彈出組件時(shí),可(kě)以在路由棧中 push 一個(gè)記錄,爲了(le)不讓頁面跳轉,我們可(kě)以把跳轉的(de)目标路由設置爲當前頁面路由,并加上一個(gè) query 來(lái)标記這(zhè)個(gè)組件彈出的(de)狀态。

然後監聽(tīng) query 的(de)變化(huà),當點擊彈出組件時(shí),query 中與該彈出組件有關的(de)标記變爲 true,則将彈出組件設爲顯示;當用(yòng)戶點擊 native 返回鍵時(shí),路由返回上一個(gè)記錄,仍然是當前頁面路由,不過 query 中與該彈出組件有關的(de)标記不再是 true 了(le),這(zhè)樣我們就可(kě)以把彈出組件設置成隐藏,同時(shí)不會返回上一個(gè)頁面。

APP開發 網站開發 産品設計 微信公衆号 APP開發公司 用(yòng)戶體驗 APP運營 微信小程序 産品經理(lǐ) 網站設計