自 6 月 6 號上線 “黑客說” 網頁版(hackertalk.net)以來吸引了很多使用者,為了進一步完善終端體驗,我們決定複用已有的技術棧,實現微信端小程式,前後開發僅花了4天,本文主要從技術的角度討論我們如何快速上線小程式。
黑客說是什麼 ?
這是我們專門為程式設計師群體定製的交流平臺,有及時技術資訊、高質量技術問答、實用程式設計經驗分享,還有程式設計師的日常生活。接近 500 個程式設計相關話題。
一個高度定製的 Markdown 編輯器:所見即所得,再也不用分屏預覽了~
感興趣的小夥伴可以戳下面連結直接體驗 ??
網頁端技術棧
為了程式碼更好地複用和維護,我們在 Vue 和 React 中選擇了 React,網頁端主要技術棧如下:
react + typescript + redux + immer + redux-saga + axios + tailwindcss + fakerjs
- typescript 專案必備,極大提高程式碼正確性和可維護性
- immer 替代了傳統的 immutablejs 方案,在 reducer 中實現類似 vue 的直接數值操作(簡潔性),同時保持 immutable 資料流的優點(可維護性)
- saga 保持了API介面呼叫的簡潔性、可除錯性
- axios 封裝了 http 請求,可以通過自定義 adapter 適應不同終端執行環境
- tailwindcss 通過原子化的 css 大大降低了樣式檔案體積,加快網頁載入速度,也很大程度降低了小程式包體積(2MB 限制),更多的程式碼空間可以用於 UI 介面和 JS 邏輯
- fakerjs 用於模擬資料,在開發環境中注入資料到 redux,方便除錯
小程式端技術棧
小程式端技術棧和網頁端高度重合(這也是我們能夠快速上線應用的原因),其中最大的變化是由 react 變為 react + taro。
Taro 是一個開放式跨端跨框架解決方案,支援使用 React/Vue/Nerv 等框架來開發 微信 / 京東 / 百度 / 支付寶 / 位元組跳動 / QQ 小程式 / H5 / RN 等應用
小程式端開發可謂混亂至極,原生程式碼難以組織、難以維護,通常都需要一些框架進行封裝,Taro 是我們在使用了幾個不同方案後決定採納的,和 react 高度重合,可以直接使用 hook,極大提高程式碼複用的可能性(這是以前積累的經驗基礎)。
APP 端技術棧
目前黑客說還沒有上線相關 APP,技術棧複用可以直接將 react 換為 react-native。
程式碼檔案組織
組織良好的程式碼是高度複用的關鍵,我們採用 components + containers 的程式碼分割方式,嚴格規範程式碼組織方式:
- UI 介面相關元件只能放在 components 資料夾,無狀態,不能耦合任何狀態管理庫相關程式碼
- 資料注入的容器元件只能放於 containers 資料夾,不能包含任何 UI 相關程式碼,比如
div
- 模組化、原子化:程式碼分層設計,實現元件高度複用,保持應用一致性
資料夾佈局如下:
├── assets 固定資原始檔:圖片、文字、svg 等
├── components 純 UI 元件
├── constants 全域性常量
├── containers 純容器元件
├── hooks 自定義 hooks
├── layout 佈局相關 UI 邏輯
├── locales 國際化相關
├── pages 整頁邏輯
├── services API 介面程式碼
├── store 狀態管理程式碼
├── styles 樣式程式碼
├── types ts 型別宣告
└── utils 公共工具類
Store 狀態管理
├── actions
├── reducers
├── sagas
├── selectors
└── types
saga 呼叫 API 程式碼組織如下:呼叫除錯非常方便
function* getPostById(action: ReduxAction): any {
try {
const res = yield call(postApi.getPostById, action.payload);
yield put({ type: T.GET_POST_SUCCESS, payload: res.data.data });
action.resolve?.();
} catch (e) {
action.reject?.();
}
}
其中的 postApi 來自 services 資料夾:
export function getPostById(id: string) {
return axios.get<R<Post>>(`/v1/posts/by_id/${id}`);
}
小程式端特殊適配
Cookie
由於小程式端無法支援 http cookie,無法像瀏覽器一樣使用 cookie 機制保證安全性和維護使用者登入狀態,我們需要手動模擬一個 cookie 機制,這裡我們推薦使用京東開源的一個方案:京東購物小程式cookie方案實踐,可以實現 cookie 過期、多 cookie 功能。其原理使用了 localstorage 替代 cookie。
Http Request
小程式端只能使用 wx.request
進行 http 請求,如果大量 API 直接使用這個介面編寫,程式碼將難以維護和複用,我們使用 axios 的 adapter 模式封裝 wx.request
,請求結果和 error 都按 axios 資料格式進行加工。這樣我們就能夠直接在小程式端使用 axios 了。
轉換請求引數:
function toQueryStr(obj: any) {
if (!obj) return '';
const arr: string[] = [];
for (const p in obj) {
if (obj.hasOwnProperty(p)) {
arr.push(p + '=' + encodeURIComponent(obj[p]));
}
}
return '?' + arr.join('&');
}
axios 介面卡模式(CookieUtil 程式碼參考上文京東的例子)
axios.defaults.adapter = function(config: AxiosRequestConfig) {
// 請求欄位拼接
let url = 'https://api.example.com' + config.url;
if (config.params) {
url += toQueryStr(config.params);
}
// 常規請求封裝
return new Promise((resolve: (r: AxiosResponse) => void, reject: (e: AxiosError) => void) => {
wx.request({
url: url,
method: config.method,
data: config.data,
header: {
'Cookie': CookieUtil.getCookiesStr(),
'X-XSRF-TOKEN': CookieUtil.getCookie('XSRF-TOKEN')
},
success: (res) => {
const setCookieStr = res.header['Set-Cookie'] || res.header['set-cookie'];
CookieUtil.setCookieFromHeader(setCookieStr);
const axiosRes: AxiosResponse = {
data: res.data,
status: res.statusCode,
statusText: StatusText[res.statusCode] as string,
headers: res.header,
config
};
if (res.statusCode < 400) {
resolve(axiosRes);
} else {
const axiosErr: AxiosError = {
name: '',
message: '',
config,
response: axiosRes,
isAxiosError: true,
toJSON: () => res
};
reject(axiosErr);
}
},
fail: (e: any) => {
const axiosErr: AxiosError = {
name: '',
message: '',
config,
isAxiosError: false,
toJSON: () => e
};
reject(axiosErr);
}
});
});
};
axios 適配完成後原先 API 相關程式碼無需改動一行即可直接複用。
Message
訊息彈窗和 toast 不能執行在小程式端,我們通過介面相容實現程式碼複用:
/**
* @author z0000
* @version 1.0
* message 彈窗,api 介面參考 antd,小程式向此介面相容
*/
import Taro from '@tarojs/taro';
import log from './log';
const message = {
info(content: string, duration = 1500) {
Taro.showToast({ title: content, icon: 'none', duration })
.catch(e => log.error('showToast error: ', e));
},
success(content: string, duration = 1500) {
Taro.showToast({ title: content, icon: 'success', duration })
.catch(e => log.error('showToast error: ', e));
},
warn(content: string, duration = 1500) {
Taro.showToast({ title: content, icon: 'none', duration })
.catch(e => log.error('showToast error: ', e));
},
error(content: string, duration = 1500) {
Taro.showToast({ title: content, icon: 'none', duration })
.catch(e => log.error('showToast error: ', e));
},
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
loading(content: string, _duration = 1500) {
Taro.showLoading({ title: content })
.catch(e => log.error('showLoading error: ', e));
},
destroy() {
Taro.hideLoading();
}
};
export default message;
這裡介面參考的 Antd 的 Message API,實現瀏覽器端和小程式端的相容。
History
小程式端 history 機制和瀏覽器端不一樣,為了程式碼複用,我們將小程式路由 API 轉換適配瀏覽器端介面(react router 的 history 方法):
/**
* common api 小程式向 react router 的 history 方法相容
*/
import Taro from '@tarojs/taro';
import log from "./log";
const history = {
// TODO: 增加query物件方法
push(path: string) {
Taro.navigateTo({ url: '/pages' + path }).catch(e => log.error('navigateTo fail: ', e));
},
replace(path: string) {
Taro.redirectTo({ url: path }).catch(e => log.error('redirectTo fail: ',e));
},
go(n: number) {
if (n >= 0) {
console.error('positive number not support in wx environment');
return;
}
Taro.navigateBack({ delta: -1 * n }).catch(e => log.error('navigateBack fail: ',e));
},
goBack() {
Taro.navigateBack({ delta: 1 }).catch(e => log.error('navigateBack fail: ',e));
}
};
export default history;
之後批量搜尋程式碼中 useHistory
相關 hook 程式碼,轉換為上述實現即可。
Router
小程式端不能直接使用 react-router 類似的路由管理方案,受益於程式碼模組化分割,大部分程式碼並沒有耦合 react-router-dom 相關的東西,最多的就是 <Link>
元件,這裡我們小小改造一下 Link 元件,批量替代即可:
import { FC, useCallback } from 'react';
import Taro from '@tarojs/taro';
import { View } from '@tarojs/components';
import { LinkProps } from 'react-router-dom';
const Index: FC<LinkProps> = ({ to, ...props}) => {
const onClick = useCallback(e => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages' + to as string });
}, [to]);
// @ts-ignore
return <View {...props} onClick={onClick}>{props.children}</View>
};
export default Index;
需要注意的是 Taro.navigateTo
不能直接跳轉 Tab 頁面,所有最終程式碼完成後需要 search + 測試覆蓋檢查相關問題。當然,你也可以在上面程式碼中檢查 to 引數是否為 tab 頁面,切換成 Taro.switchTab
方法。
Path Params
小程式不支援類似 /post/:id
的路由引數,我們需要將路由引數轉換為:/post?id=xx
,這個轉換通過 IDE 搜尋,批量 replace 即可。
CSS
由於小程式端的 rpx 單位、px 單位直接使用會有很大的複用問題,導致網頁端往小程式端遷移時需要大量改造 HTML 程式碼,這裡我們使用 sass 實現了 tailwindcss 類似的功能(針對小程式端進行改造),通過變數開關切換單位,可以做到不同設計稿程式碼也能相容(375px 和 750px 或者 rpx,rem 單位都可以直接相容)。
設計複用有時比程式碼複用更加重要,這是使用者體驗一致性的前提,幸運的是 tailwincss 之類的方案選型讓我們很容易做到這一點,我們後續將開源小程式端 tailwindcss 程式碼,敬請期待。
團隊協作
協作也是很重要的一環,產品成功離不開高效合作,我們使用 google doc 全家桶進行協作,包括專案文件、需求、任務管理、郵件,google 全家桶最大的好處就是多端支援,這是目前支援終端最多、協作最方便的工具。linux + android + ios + ipad + windows + mac 都能無縫同步協作。方便設計師、產品經理、程式設計師共同工作。
最後
歡迎各位體驗!
HackerTalk (黑客說)第一帖:Happy hacking!
微信小程式搜尋:黑客說,或者掃碼:
本作品採用《CC 協議》,轉載必須註明作者和本文連結