我們的項(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({...});