自 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 高度重合,可以直接使用 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!
微信小程序搜索:黑客说,或者扫码:
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!