最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 黑客说:如何做到 4 天上线一个小程序?

    正文概述 掘金(HDD)   2021-08-08   525

    自 6 月 6 号上线 “黑客说” 网页版(hackertalk.net)以来吸引了很多用户,为了进一步完善终端体验,我们决定复用已有的技术栈,实现微信端小程序,前后开发仅花了4天,本文主要从技术的角度讨论我们如何快速上线小程序。

    黑客说:如何做到 4 天上线一个小程序?

    黑客说是什么 ?

    这是我们专门为程序员群体定制的交流平台,有及时技术资讯、高质量技术问答、实用编程经验分享,还有程序员的日常生活。接近 500 个编程相关话题。

    黑客说:如何做到 4 天上线一个小程序?

    一个高度定制的 Markdown 编辑器:所见即所得,再也不用分屏预览了~

    黑客说:如何做到 4 天上线一个小程序?

    黑客说:如何做到 4 天上线一个小程序?

    ​感兴趣的小伙伴可以戳下面链接直接体验 ??

    黑客说:一个有趣的程序员交流平台

    网页端技术栈

    为了代码更好地复用和维护,我们在 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 都能无缝同步协作。方便设计师、产品经理、程序员共同工作。

    最后

    黑客说:如何做到 4 天上线一个小程序?

    欢迎各位体验!

    黑客说网页版

    HackerTalk (黑客说)第一帖:Happy hacking!

    微信小程序搜索:黑客说,或者扫码:

    黑客说:如何做到 4 天上线一个小程序?


    起源地下载网 » 黑客说:如何做到 4 天上线一个小程序?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元