微信小程序開發(fā)基礎(chǔ)1

我們的項(xiàng)目中是使用 TypeScript 寫代碼,通過 rollup 編譯成 javascript府阀,在微信開發(fā)者工具打開 build 后的目錄缆镣,即可運(yùn)行,開發(fā)模式下在每次保存完代碼后都會(huì)進(jìn)行編譯试浙。仍使用 yarn add xxx 集成第三方 package董瞻,在使用 import 引入第三方依賴的時(shí)候,會(huì)將第三方文件打包進(jìn)去,因此不用特殊處理钠糊。當(dāng)然如果沒有必要引用或者沒有必要全部引用的挟秤,盡量不引用或引用局部文件,防止將所有文件都打包進(jìn)去最終超過 2M 的限制抄伍。

請(qǐng)注意因?yàn)槭褂昧?ts艘刚,再引入第三方 package 時(shí),有時(shí) ts 校驗(yàn)會(huì)報(bào)錯(cuò)截珍,可以關(guān)閉一次編輯器重新打開確認(rèn)一下是否真的有校驗(yàn)錯(cuò)誤攀甚,我在引入 moment 時(shí)就遇到了這個(gè)問題,第一次引入時(shí)有校驗(yàn)報(bào)錯(cuò)岗喉,第二天再試時(shí)就好了秋度。

編譯后 process.env.NODE_ENV 的報(bào)錯(cuò)

使用 rollup 編譯后的文件,在微信開發(fā)者工具中運(yùn)行時(shí)钱床,會(huì)由于沒有 process 變量但引用了 process.env.NODE_ENV 而報(bào)錯(cuò)荚斯。解決辦法是使用 rollup-plugin-replace 插件绊茧,在 rollup.config.js 配置文件中添加如下代碼即可控妻。

import replace from 'rollup-plugin-replace';

export default {
  ...
  plugins: [
    ...
    replace({
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'development',
      ),
    }),
  ],
};

編譯后 whatwg-fetch 中 this 為 undefined 的問題

項(xiàng)目中有安裝包依賴了 whatwg-fetch瓷式,由于小程序既不是瀏覽器環(huán)境也不是 node 環(huán)境狱窘,其中使用的 this 編譯后變成了 undefined鸥诽。解決辦法是在 rollup.config.js 配置中間中指定 whatwg-fetch 上下文即可七兜,但這個(gè)上下文要是小程序中無需定義而存在的上下文锯厢,經(jīng)過試驗(yàn)可使用 global 變量酥郭。代碼如下懂衩。

export default {
  moduleContext: {
    [require.resolve('whatwg-fetch')]: 'global',
  },
};

集成 Redux撞叨、Redux-Persist、Graphql浊洞、Apollo-Client

yarn add redux redux-persist
yarn add apollo-client graphql-tag

Redux的使用參考官方文檔即可牵敷。小程序中不能使用 react-redux,為了能夠像以前 react 使用 redux 一樣在小程序中使用 redux法希,我參考了 小程序 Redux 綁定庫(kù)枷餐,將其中的 warning.js、shallowEqual.js苫亦、wrapActionCreators.js毛肋、connect.js、Provider.js 簡(jiǎn)單修改為 ts 文件集成到項(xiàng)目中屋剑,就可以用使用 react-redux 的方式使用 redux 了润匙。

在使用的過程中,connect 在 Page 上使用沒有問題唉匾,但小程序中 Component 使用 connect 沒有效果孕讳,這是因?yàn)?Component 的聲明周期中沒有 onLoad、onUnload,有的是 attached厂财、detached 方法芋簿,因此修改 connect.ts 文件,通過傳入固定參數(shù) type 為 Component 來決定使用哪兩個(gè)生命周期方法璃饱,這樣支持了 Component 也能通過 connect 使用 redux与斤。

使用 redux-persist 可以將 store 的整個(gè)數(shù)據(jù)保存到指定的 storage 中,如瀏覽器的 LocalStorage帜平、react-native 的 AsyncStorage 等幽告。將微信 storage 的 api 進(jìn)行封裝梅鹦,也可直接使用redux-persist-weapp-storage裆甩,可指定使用微信的 storage。參考 redux-persist 的文檔齐唆,將 store 存儲(chǔ)到 storage 可使用 persistStore 方法嗤栓,或?qū)?active 置為 true 在每次 store 變化時(shí)都保存到 storage 中。但在程序初始化時(shí)將 storage 中保存的數(shù)據(jù)放入 store 的操作在文檔中沒找到箍邮,官方提供的方式是針對(duì) react 組件的茉帅,我自己找了兩種可以達(dá)到該效果的方式,一種是直接從 storage 中讀出數(shù)據(jù)锭弊,另一種是使用 getStoredState 讀出數(shù)據(jù)堪澎,具體代碼參考下面。

Apollo-Client 默認(rèn)是使用 fetch 進(jìn)行網(wǎng)絡(luò)請(qǐng)求的味滞,但是小程序中沒有 fetch樱蛤,只能使用 wx.request 進(jìn)行網(wǎng)絡(luò)請(qǐng)求,在官方文檔也沒有找到可以自定義或傳入 fetch 的方式剑鞍。后來查了源碼昨凡,在 new ApolloClient 的 networkInterface 參數(shù)中可以傳入 query 參數(shù),這樣將 wx.request 進(jìn)行封裝通過 query 參數(shù)傳入蚁署,就可以使用 Apollo-Client 進(jìn)行網(wǎng)絡(luò)請(qǐng)求了便脊。在我們的項(xiàng)目中有使用輪詢的需求,使用的是 Apollo-Client 的 watchQuery 方法光戈,因?yàn)槊看涡枰付?pollInterval 參數(shù)哪痰,感覺不太方便管理,因此對(duì) watchQuery 的使用進(jìn)行了封裝久妆,具體代碼參考下面晌杰。

現(xiàn)在雖然能使用 Apollo-Client 進(jìn)行網(wǎng)絡(luò)請(qǐng)求了,但還沒有辦法直接拿到請(qǐng)求返回的結(jié)果镇饺,在 Web 端是使用 react-apollo 的 compose 將請(qǐng)求結(jié)果通過 props 傳入組件乎莉,但是小程序無法使用。目前我使用了兩種不是很好的方式臨時(shí)解決的這個(gè)問題,如果是 mutation惋啃,直接使用 then 來拿到返回結(jié)果哼鬓,如果是 query,是在 mapPropsToState 中边灭,使用 Apollo-Client 的 readQuery 拿到請(qǐng)求的返回結(jié)果异希,進(jìn)行處理后傳入 Page 的 data。

主要代碼如下:

configureStore.ts

import ApolloClient from 'apollo-client';
import { applyMiddleware, combineReducers, createStore, Store } from 'redux';
import { getStoredState, persistReducer, persistStore } from 'redux-persist';
import thunk from 'redux-thunk';

import createApolloClient from './createApolloClient';
import reducer from './reducers/index';
import WxStorage from './storage';

interface CreateRootReducer {
  apolloClient: ApolloClient;
}

function createRootReducer({ apolloClient }: CreateRootReducer) {
  return combineReducers({
    apollo: apolloClient.reducer(),
    ...reducer,
  });
}

let store: Store<{}>;

export default function configureStore() {
  const apolloClient = createApolloClient();
  const middleware = [thunk, apolloClient.middleware()];
  const enhancer = applyMiddleware(...middleware);
  const rootReducer = createRootReducer({ apolloClient });

  const persistConfig = {
    // active: true, // store 在每次變化后都會(huì)同步保存到 storage 中
    key: 'root',
    storage: WxStorage,
    version: 2,
  };

  const persistedReducer = persistReducer(persistConfig, rootReducer);
  // 將 storage 中保存的數(shù)據(jù)初始化給 store
  // 方式一
  const storedState = wx.getStorageSync('persist:root');
  const state: any = {};
  if (typeof storedState === 'string' && storedState) {
    const rawState = JSON.parse(storedState);
    Object.keys(rawState).forEach(key => {
      state[key] = JSON.parse(rawState[key]);
    });
  }
  // 方式二
  getStoredState(persistConfig)
    .then(res => {
      store.dispatch({
        key: 'root',
        payload: res,
        type: 'persist/REHYDRATE',
      });
    })
    .catch(error => {
      throw error;
    });

  store = createStore(persistedReducer, {}, enhancer);
  return {
    apolloClient,
    persistStore: () => persistStore(store), // 將 store 數(shù)據(jù)保存到 storage 中
    store,
  };
}

storage.ts

interface Storage {
  getItem(key: string, ...args: any[]): any;
  setItem(key: string, value: any, ...args: any[]): any;
  removeItem(key: string, ...args: any[]): any;
}

const WxStorage: Storage = {
  getItem: key =>
    new Promise((resolve, reject) => {
      wx.getStorage({
        fail: res => {
          reject(res);
        },
        key,
        success: res => {
          resolve(res.data);
        },
      });
    }),
  removeItem: key =>
    new Promise((resolve, reject) => {
      wx.removeStorage({
        fail: res => {
          reject(res);
        },
        key,
        success: res => {
          resolve(res);
        },
      });
    }),
  setItem: (key, data) =>
    new Promise((resolve, reject) => {
      wx.setStorage({
        data,
        fail: res => {
          reject(res);
        },
        key,
        success: res => {
          resolve(res);
        },
      });
    }),
};

export default WxStorage;

warning.ts

export default function warning(message: string) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message);
  }
  try {
    // This error was thrown as a convenience so that if you enable
    // "break on all exceptions" in your console,
    // it would pause the execution at this line.
    throw new Error(message);
  } catch (e) {
    console.log(e);
  }
}

shallowEqual.ts

export default function shallowEqual(objA: any, objB: any) {
  if (objA === objB) {
    return true;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  let result = true;
  // Test for A's keys different from B.
  const hasOwn = Object.prototype.hasOwnProperty;
  keysA.forEach(keyA => {
    if (!hasOwn.call(objB, keysA) || objA[keyA] !== objB[keyA]) {
      result = false;
      return false;
    }
    return;
  });

  return result;
}

wrapActionCreators.ts

function bindActionCreator(actionCreator: any, dispatch: any) {
  return () => dispatch(actionCreator.apply(undefined, arguments));
}

function bindActionCreators(actionCreators: any, dispatch: any) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      'bindActionCreators expected an object or a function, instead received ' +
        (actionCreators === null ? 'null' : typeof actionCreators) +
        '. ' +
        'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?',
    );
  }

  const keys = Object.keys(actionCreators);
  const boundActionCreators: any = {};
  keys.forEach(actionKey => {
    const tempActionCreator = actionCreators[actionKey];
    if (typeof tempActionCreator === 'function') {
      boundActionCreators[actionKey] = bindActionCreator(
        tempActionCreator,
        dispatch,
      );
    }
  });

  return boundActionCreators;
}

export default function wrapActionCreators(actionCreators: any) {
  return (dispatch: any) => bindActionCreators(actionCreators, dispatch);
}

connect.ts

import shallowEqual from './shallowEqual';
import warning from './warning';
import wrapActionCreators from './wrapActionCreators';

const defaultMapStateToProps = (state: object) => {
  console.log(state);
  return {};
};
const defaultMapDispatchToProps = (dispatch: any) => ({ dispatch });

export default function connect(mapStateToProps: any, mapDispatchToProps: any) {
  const shouldSubscribe = Boolean(mapStateToProps);
  const mapState = mapStateToProps || defaultMapStateToProps;
  const app = getApp();

  let mapDispatch: any;
  if (typeof mapDispatchToProps === 'function') {
    mapDispatch = mapDispatchToProps;
  } else if (!mapDispatchToProps) {
    mapDispatch = defaultMapDispatchToProps;
  } else {
    mapDispatch = wrapActionCreators(mapDispatchToProps);
  }

  return function wrapWithConnect(pageConfig: any) {
    function handleChange(this: any, options: any) {
      if (!this.unsubscribe) {
        return;
      }

      const state = this.store.getState();
      const mappedState = mapState(state, options);
      if (!this.data || shallowEqual(this.data, mappedState)) {
        return;
      }
      this.setData(mappedState);
    }

    let { onLoad: pageConfigOnLoad, onUnload: pageConfigOnUnload } = pageConfig;

    // 支持 Component 使用
    if (pageConfig.type === 'Component') {
      pageConfigOnLoad = pageConfig.attached;
      pageConfigOnUnload = pageConfig.detached;
    }

    function onLoad(this: any, options: any) {
      this.store = app.store;
      if (!this.store) {
        warning('Store對(duì)象不存在!');
      }
      if (shouldSubscribe) {
        this.unsubscribe = this.store.subscribe(
          handleChange.bind(this, options),
        );
        handleChange.call(this, options);
      }
      if (typeof pageConfigOnLoad === 'function') {
        pageConfigOnLoad.call(this, options);
      }
    }

    function onUnload(this: any) {
      if (typeof pageConfigOnUnload === 'function') {
        pageConfigOnUnload.call(this);
      }
      if (typeof this.unsubscribe === 'function') {
        this.unsubscribe();
      }
    }

    // 支持 Component 使用
    if (pageConfig.type === 'Component') {
      return Object.assign(
        {},
        pageConfig,
        {
          methods: {
            ...pageConfig.methods,
            ...mapDispatch(app.store.dispatch),
          },
        },
        {
          attached: onLoad,
          detached: onUnload,
        },
      );
    } else {
      return Object.assign({}, pageConfig, mapDispatch(app.store.dispatch), {
        onLoad,
        onUnload,
      });
    }
  };
}

Provider.ts

import warning from './warning';

function checkStoreShape(store: object) {
  const missingMethods = ['subscribe', 'dispatch', 'getState'].filter(
    m => !store.hasOwnProperty(m),
  );

  if (missingMethods.length > 0) {
    warning(
      'Store 似乎不是一個(gè)合法的 Redux Store對(duì)象: ' +
        '缺少這些方法: ' +
        missingMethods.join(', ') +
        '绒瘦。',
    );
  }
}

export default function Provider(store: object) {
  checkStoreShape(store);
  return (appConfig: object) => Object.assign({}, appConfig, { store });
}

config.ts

export default {
  requestUrl: 'http://127.0.0.1:8888',
  pollInterval: 3000,
};

createApolloClient.ts

import ApolloClient, {
  createNetworkInterface,
  ObservableQuery,
  WatchQueryOptions,
} from 'apollo-client';
import config from './config';

interface CustomClient extends ApolloClient {
  allQueryWatchers?: Set<ObservableQuery<{}>>;
  watchQueryStart?: (options: WatchQueryOptions) => ObservableQuery<{}>;
}

// 封裝通用 fetch称簿,gql 返回類型就是 any
export const query = (input: any) => {
  return new Promise(resolve => {
    wx.request({
      ...input,
      data: {
        query: input.query.loc.source.body, // 獲取查詢語句字符串
        variables: input.variables || {},
      },
      method: 'POST',
      header: {
        cookie: getApp().globalData.cookie,
      },
      fail: res => {
        throw res;
      },
      success: res => {
        resolve(res.data);
      },
      url: `${config.requestUrl}/graphql`,
    });
  });
};

const client: CustomClient = new ApolloClient({
  networkInterface: {
    ...createNetworkInterface({
      opts: {
        credentials: 'include',
      },
      uri: `${config.requestUrl}/graphql`,
    }),
    query,
  },
  queryDeduplication: true,
  addTypename: false,
  reduxRootSelector: state => state.apollo,
});

export default function createApolloClient() {
  client.allQueryWatchers = new Set();
  // 封裝通用 watchQuery,具有統(tǒng)一的輪詢間隔
  client.watchQueryStart = function(options: WatchQueryOptions) {
    const queryWatcher = client.watchQuery(options);
    queryWatcher.startPolling(config.pollInterval);
    if (this.allQueryWatchers) {
      this.allQueryWatchers.add(queryWatcher);
    }
    return queryWatcher;
  };
  return client;
}

發(fā)起 query 的示例如下:

import gql from 'graphql-tag';

const products = gql`
  query Products {
    products {
      id
      name
      description
      amount
      code
      lectures {
        type
        lecture {
          ... on Live {
            __typename
            id
            name
            startDate
            endDate
          }
        }
      }
    }
  }
`;
export default products;
import productsQuery from '../../graphql/products';

const pageConfig: wx.PageParam = {
    onLoad() {
        app.globalData.apolloClient.query({
            query: productsQuery,
        });
    },
    ...
};

const mapStateToData = (state: any) => {
  const pagesInstance = getCurrentPages();
  let products: ProductType[] = [];

  pagesInstance.forEach(page => {
    // 通過路由判斷找到當(dāng)前 Page 實(shí)例惰帽,這樣可以獲取到當(dāng)前頁面的 data憨降、options 等信息
    if (page.route === 'pages/home/home') {
      const data: ProductsType =
        state.apollo.data.ROOT_QUERY && state.apollo.data.ROOT_QUERY[`products`]
          ? app.globalData.apolloClient.readQuery({
              query: productsQuery,
            })
          : [];

      if (data.products) {
        products = data.products.map(product => {...});
      }
    }
  });

  return {
    products,
  };
};

const nextPageConfig = connect(mapStateToData, undefined)(pageConfig);
Page(nextPageConfig);

發(fā)起 mutation 的示例如下:

import gql from 'graphql-tag';

const createActivityRecord = gql`
  mutation CreateActivityRecord(
    $input: ActivityRecordInput!
    $byOrder: Boolean
  ) {
    createActivityRecord(input: $input, byOrder: $byOrder) {
      id
      product {
        id
        name
      }
    }
  }
`;

export default createActivityRecord;
app.globalData.query({
    query: createActivityRecord,
    variables: {
        input: {
            activityId: this.data.activityId,
            productId: this.data.productId,
            ownerId: this.data.ownerId,
        },
        byOrder: false,
    },
}).then((res: any) => {
    if (res.data.errors) {
        wx.showToast({
        title: '操作失敗',
        icon: 'none',
        });
    } else {
        this.sendFlowerSuccess();
    }
});

集成 Lodash

yarn add lodash-es
yarn add -D @types/lodash-es

import debounce from 'lodash-es/debounce';
...

在使用的地方局部引用即可。在使用 debounce 方法時(shí)该酗,會(huì)報(bào)下圖所示的錯(cuò)誤授药。原因是小程序沒有全局的 window 對(duì)象,但查看源碼只要有全局 self呜魄、global 之一即可悔叽,通過 console 輸出看到小程序有 global 對(duì)象,因此在 app.ts 中添加如下代碼爵嗅,之后就可以正常使用 lodash 了娇澎。

// 全局 global 處理,lodash 中使用了 global
Object.assign(global, {
    Array,
    Date,
    Error,
    Function,
    Math,
    Object,
    RegExp,
    String,
    TypeError,
    setTimeout,
    clearTimeout,
    setInterval,
    clearInterval,
});

App({...});
使用 lodash 報(bào)錯(cuò)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末睹晒,一起剝皮案震驚了整個(gè)濱河市趟庄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌册招,老刑警劉巖岔激,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異是掰,居然都是意外死亡虑鼎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門键痛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炫彩,“玉大人,你說我怎么就攤上這事絮短〗ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵丁频,是天一觀的道長(zhǎng)杉允。 經(jīng)常有香客問我邑贴,道長(zhǎng),這世上最難降的妖魔是什么叔磷? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任拢驾,我火速辦了婚禮,結(jié)果婚禮上改基,老公的妹妹穿的比我還像新娘繁疤。我一直安慰自己,他們只是感情好秕狰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布稠腊。 她就那樣靜靜地躺著,像睡著了一般鸣哀。 火紅的嫁衣襯著肌膚如雪架忌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天诺舔,我揣著相機(jī)與錄音鳖昌,去河邊找鬼。 笑死低飒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懂盐。 我是一名探鬼主播褥赊,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼莉恼!你這毒婦竟也來了拌喉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤俐银,失蹤者是張志新(化名)和其女友劉穎尿背,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捶惜,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡田藐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吱七。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汽久。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖踊餐,靈堂內(nèi)的尸體忽然破棺而出景醇,到底是詐尸還是另有隱情,我是刑警寧澤吝岭,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布三痰,位于F島的核電站吧寺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏散劫。R本人自食惡果不足惜撮执,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舷丹。 院中可真熱鬧抒钱,春花似錦、人聲如沸颜凯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽症概。三九已至蕾额,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彼城,已是汗流浹背诅蝶。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留募壕,地道東北人调炬。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像舱馅,于是被迫代替她去往敵國(guó)和親缰泡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355