小程序最佳实践之『安全生产』篇 👷🏻‍♂️

927 阅读12分钟

前言:接下来会开启一个小程序最佳实践的知识小册,从小程序作为切入点,但绝不限于此。将涵盖几大篇章『命名篇』、『单测篇』、『代码风格篇』、『注释篇』、『安全生产篇』、『代码设计篇』、『性能篇』、『国际化篇』和『Why 小程序开发最佳实践』。每一篇都会包含正面和反面示例以及详细的解释。

文中示例都是在 CR 过程中发现的一些典型的问题。希望大家能通过该知识小册提前规避此类问题,让我们的 CR 关注点更多聚焦在业务逻辑而非此类琐碎或基本理念的缺失上而最终形成高质量的 CR。

安全生产 👷🏻‍

接下来将开启所有篇中最为特殊的一篇,很重要因为关乎到线上安全,但又很容易被新手或老手忽略。首先介绍安全生产的几个原则:

1、线上变更必须合乎『三板斧』即:可监控、可灰度、可应急。

2、不是所有时间段都能执行线上变更。

一般公司都会规范发布时间,避开饭点和非工作日时间,避免出故障后所有方(前后端测试 SRE等)不在工位不能及时处理。

3、线上故障需追责且必须有明确的故障等级和责任划分。比如影响人数在 xx 范围内触发 Pn 故障,对引发故障的个人影响是 yy。

下面将从多个角度和手段发力确保线上的安全生产 👷🏻‍♂️。

业务流水日志上报

前端自定义业务流水日志上报有助于快速排查线上问题。通常有两种方式:

  • 方式一:通过埋点,给一个虚拟的 c.d 位发送点击或曝光事件(因为时效性原因推荐点击),如 c00000.d00000,其他信息通过扩展参数上传。传统手段则要依赖后端去日志捞取返回值,需等很长时间,通过埋点方式上报,日志实时上报实时分析。
  • 方式二:通过自定义的 jsapi,如果有建议使用方式二,因为方式一占用了埋点的流量,给其增加了不必要的负担。

先来看一个案例,给一个直观的认识。

Bad

针对线上问题后端返回脏值导致某投放模块 A 显示了未被插值处理的源码 ${title},前端改进后决定针对此种情形不展示该模块,但此时很难辨别是后端返回值不对还是确实没有投放导致的,线上问题不能迅速定位,前端默认隐藏处理导致问题不能马上暴露,表面一片祥和。

if (!isDirtyValue(title) && !isDirtyValue(subtitle) && !isDirtyValue(actionUrl)) {
  // 展示投放模块 A
}
Good

上报『脏数据』日志并结合监控,可迅速定位是后端返回字段不对导致 👍。

if (hasDirtyValue[title, subtitle, actionUrl]) {
    reportLog({
    code: 'DATA_INVALID',
    msg: `dirtyValue: title (${title}), subtitle (${subtitle}) or actionUrl (${actionUrl}) contains "\$\{"`,
    response: resp,
  });

  return;
}

// 此时可正常展示投放模块 A
...

埋点上报日志规范

不同项目或应用需制定日志规范,只要熟悉了该套规范,以后接手新项目时能显著降低维护成本。

/**
 * 流水日志规范 spec。
 * 来自最佳实践
 */
interface ILog {
  /** 监控码 */
  code?: number,

  /** 其他自定义请求的 path 或 http 请求 api、jsapi 的 name */
  api?: string;
  /** 简要描述 */
  msg?: string;

  /** 日志来源 */
  from?: string;
  /** 日志发生时间 */
  at?: number,

  type?: 'RPC' | 'HTTP' | 'JSAPI' | 'MTOP' | 'JS_Builtin_Func';

  /** 完整 error */
  error?: Error | object;
  /** 比如,HTTP 请求 method */
  subType?: string;
  /** 请求体 */
  request?: object;
  /** 响应体 */
  response?: object;
}

如何上报

方式一:通过埋点

/**
 * 上报业务自定义埋点
 */
export const reportLog = (log: ILog = {}): void => {
  try {
    const $spm = getSpm();

    const {
      type,
      api,
      msg,
      subType,
      error = {},
      request = {},
      response = {},
    } = log;

    /** 防止日志太长引发性能问题 */
    const MAX_LOG_LENGTH = 2000;

    const errorStr = jsonStringifySafely(error).slice(0, MAX_LOG_LENGTH);

    $spm?.click('c00000.d00000', {
      // 加 _ 开头是为了和其他内置埋点字段区分开
      _type: type,
      _api: api,
      _msg: msg,
      _subType: subType,

      _error: `name:${error.name}|message:${error.message}|error:${errorStr}`,
      _request: jsonStringifySafely(request).slice(0, MAX_LOG_LENGTH),
      _response: jsonStringifySafely(response).slice(0, MAX_LOG_LENGTH),
    });
  } catch (err) {
    console.error('reportLog:', log, 'failed:', err);
  }
};

方式二:jsapi

// src/common/utils/remoteLog.js
// 在公共文件中初始化RemoteLogger实例
import { RemoteLogger } from '@yourcompany/RemoteLogger';

const remoteLogger = new RemoteLogger({
  bizType: 'BIZ_TYPE',
  appName: '应用名',
});

const withdrawRemoteLogger = new RemoteLogger({
  bizType: 'BIZ_TYPE',
  appName: '应用名-页面名',
});

/**
 *
 * @param {ILog} log
 * @param {'info' | 'error'} level
 */
function send(log, level) {
  if (typeof log !== 'object') {
    // eslint-disable-next-line no-console
    console.warn(
      'remoteConsole.info: log must be an object, but see: typeof log',
      typeof log,
      ', log:',
      log,
    );

    return;
  }

  const formatted = formatLog(log);

  // eslint-disable-next-line no-console
  console[level] && console[level](`[${Date.now()}] RemoteLog:`, formatted);

  const logger = resolveLogger(log.from);

  logger[level] && logger[level](log.api || log.msg, log.msg || '', formatted);
  
  // 上报监控
  if (MonitorCodeEnum[log.code]) {
    reportLog(toMonitorLog(formatted));
  }
}

/**
 * @param {ILog['from']} from
 * @returns {RemoteLogger}
 */
function resolveLogger(from) {
  return loggers[from] || remoteLogger;
}

/**
 * 上报流水日志
 */
export const remoteConsole = {
  /**
   * 上报正常流水日志
   * @param {ILog} log
   */
  info(log) {
    send(log, 'info');
  },

  /**
   * 上报异常流水日志
   * @param {ILog} log
   */
  error(log) {
    send(log, 'error');
  },
};

const PAGE_NAMES = {
  withdraw: {
    home: 'withdraw',
  },
};

const loggers = {
  [PAGE_NAMES.withdraw.home]: withdrawRemoteLogger,
};

/**
 * 一个页面一个 remote console
 */
export const withdrawHomeRemoteConsole = {
  /**
   * 上报正常流水日志
   * @param {ILog} log
   */
  info(log) {
    remoteConsole.info({ ...log, from: PAGE_NAMES.withdraw.home });
  },

  /**
   * 上报异常流水日志
   * @param {ILog} log
   */
  error(log) {
    remoteConsole.error({ ...log, from: PAGE_NAMES.withdraw.home });
  },
};

/**
 * @param {ILog} log
 * @returns {ILog | ILog & { error: { name: string; message: string; stack: string; error: string } }}
 */
function formatLog(log) {
  const { error } = log || {};

  if (error instanceof Error) {
    // eslint-disable-next-line no-console
    console.error(error);

    return {
      ...log,

      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
        errorToString: error.toString(),
        error,
      },
    };
  }

  return log;
}

示例

给提现过程上报错误流水日志

import { withdrawHomeRemoteConsole as rc } from '/common/utils/remoteLog';

withdraw()
  .catch((error) => {
    const isExpectedError = showErrorModal(error);
  
    if (!isExpectedError) {
      rc.error({
        msg: 'withdraw unknown error',
        error,
      });
    }
  });

上报告警,只需增加一个 code 即可

import { withdrawHomeRemoteConsole as rc } from '/common/utils/remoteLog';

withdraw()
  .catch((error) => {
    const isExpectedError = showErrorModal(error);
  
    if (!isExpectedError) {
      rc.error({
+       code: 'WITHDRAW_UNKNOWN_ERROR'
      	msg: 'unknown error',
      	error,
      });
    }
  });

如何查询上报的日志

根据公司而定,一般公司都有自己的埋点平台。

JS 兼容

小程序须支持 iOS 9+,故开发时须查阅 caniuse:社区 caniuse小程序 caniuse

Bad

没有考虑兼容性 Object.valuesiOS 9 甚至一部分 iOS 10 不支持

image.png

Good

使用 lodash values 兼容性更好

import values from 'lodash/values';

values(obj);

JSAPI 兼容性

若文档表明需要兼容则必须做业务兼容,不限于 jsapi,比如一些 native 组件,如果不兼容需注释理由。

Bad

导致线上 JS worker 报错“my.hideBackHome+is+not+a+function”

onLoad() {
  my.hideBackHome();
}
Good
onLoad() {
  my.hideBackHome && my.hideBackHome();
}
Better

进一步封装

// utils/system.ts
export function canIUse(jsapiName: string): boolean {
  return my.canIUse(jsapiName) && typeof my[jsapiName] === 'function';
}

// use in index.ts
onLoad() {
  // 隐藏自动充 home 按钮,自动充是一个独立的应用
  canIUse('hideBackHome') && my.hideBackHome();
}

CSS 兼容

头发丝效果

1rpx(0.5px)会有兼容性问题,建议使用头发丝效果

不同机型字重对应表

image.png

字重font-weight英文
常规体400 (normal)PingFang-Regular
极细体200PingFang-Ultralight
纤细体100PingFang-Thin
细体300PingFang-Light
中黑体500PingFang-Medium
中粗体600PingFang-Semibold
粗体700 (bold)PingFang-Bold
  1. Sketch导出的PingFang字体是iOS特有,安卓不支持,因此无需在css指定font-family。
  2. 中文iOS下“半粗”(font-weight为600)的文本,在安卓下全部失效,而粗体(font-weight为700)则表现正常。
Bad

1rpx 会有兼容性问题,建议使用头发丝效果

.item-content{
  height: 145rpx;
  border-bottom: 1rpx solid #eeeeee;
}
Good

定义头发丝效果

// mixins.less

// 头发丝 移动端 1px 边框
// NOTICE: 父元素必须加 position relative 或 fixed
.hairline(@color: #eee, @position: top) {
  &::before {
    content: '';
    width: 200%;
    top: 0;
    left: 0;
    position: absolute;
    border-@{position}: 1px solid @color;
    
    -webkit-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: left top;
    transform-origin: left top;
  }
}

使用

.item-content{
  height: 145rpx;
  
  .hairline(@position: bottom);
}

禁止使用同步 JSAPI

比如 my.getSystemInfoSync / my.getStorageSync,同步会阻塞 js worker 执行,极大影响用户体验。必须使用对应的异步方法 my.getSystemInfo / my.getStorage,并建议封装成 Promise

Bad
const { version } = my.getSystemInfoSync();
Good

使用 promisify 后的异步 getSystemInfo,并自带防止并发重复调用的缓存和失败上报日志逻辑 👍🏻。

// lib/system.ts

/**
 *
 * @param {any} obj
 * @returns {boolean}
 */
function isPromise(obj) {
  return obj && typeof obj.then === 'function';
}

let cachedSystemInfoPromise;

/**
 * 带缓存的 getSystemInfo
 * 请勿使用同步 getSystemInfoSync
 * 默认 500ms 超时
 *
 * @throws no error
 * @returns {Promise<{ platform: 'iOS' | 'iPhone OS' | 'Android', version: string, }>} return `{}` on error
 */
export function getSystemInfo({ timeout = 1 * 1000 } = {}) {
  if (cachedSystemInfoPromise) {
    return cachedSystemInfoPromise;
  }

  cachedSystemInfoPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new RangeError(`getSystemInfo timeout for ${timeout}ms`));
    }, timeout);

    my.getSystemInfo({
      success(res) {
        resolve(res);
      },

      fail(error) {
        reject(error);
      },
    });
  }).catch((error) => {
    const isTimeout = error instanceof RangeError;
    const msg = isTimeout ? error.message : 'getSystemInfo failed';

    rc.error({
      msg,
      request: { timeout },
      error,
    });

    return {};
  });

  return cachedSystemInfoPromise;
}

使用

const { version } = await getSystemInfo();

慎重引入自研 util 依赖包

请引入业界成熟的具备单测的三方 npm 包,比如 lodash,针对业务特殊性封装的二方包必须编译成 es5,且不能将 src 发布

用户输入参数一律不信任

禁止无脑根据自定义参数跳转

需要通过白名单控制安全风险,因为第三方页面通过我们应用跳转,对用户而言,这是我们应用或者支付宝默认安全的页面。

解决:通过白名单方式,白名单分严格匹配白名单和规则匹配白名单,严格匹配优先

💡 命名小 Tips:白名单对应的英文不能是带种族歧视性的 whitelist 而应该是 allowlist,黑名单是 denylist。

Bad

某应用 A 会针对 query 传入的目标地址跳转,该需求存在安全风险,若一个钓鱼网站借由该应用跳转,而用户认为被该网站是被应用 A 信任的,信任会传递(用户心里的 PageRank 算法),会误导用户该三方网站是合法网站。

Page({
  onLoad(options: IOptions) {
    const { goPage } = options;
    
    if (goPage) {
      this.commit('setState', {
        goPage,
      });
    }
  },
  
  // 完成某个操作后去跳转页面,并未对页面地址做校验
  onExitCamera() {
    const { goPage } = this.state;

    if (goPage) {
      jump(goPage, {}, {
        type: 'redirectTo',
      });
    } else {
      goBack();
    }
  },
})
Good
Page({
  onLoad(options: IOptions) {
    const { goPage } = options;
    
    // 采用严格匹配,白名单内链接方可跳转
    if (isTrustedRedirectUrl(goPage)) {
      this.commit('setState', {
        goPage,
      });
    }
  },
});

function isTrustedRedirectUrl(url) {
  return ALLOWLIST.includes(url);
}

版本号比较

版本号不能直接做字符串比较,请使用公司自定义的库,或使用下面的 snippets,进一步封装 gtgteltlteeq 等可读性更强的方法。

因为按照字符比较 1 < 9,从而 '10.9.7' < '9.9.7'

function compareInternal(v1: string, v2: string, complete: boolean) {
  // 当v2为undefined时,v1取客户端的版本号
  if (v2 === undefined) {
    v2 = v1;
    v1 = getClientVersion();
  }
  
  v1 = versionToString(v1);
  v2 = versionToString(v2);
  
  if (v1 === v2) {
    return 0;
  }
  
  const v1s: any[] = v1.split(delimiter);
  const v2s: any[] = v2.split(delimiter);
  const len = Math[complete ? 'max' : 'min'](v1s.length, v2s.length);
  
  for (let i = 0; i < len; i++) {
    v1s[i] = typeof v1s[i] === 'undefined' ? 0 : parseInt(v1s[i], 10);
    v2s[i] = typeof v2s[i] === 'undefined' ? 0 : parseInt(v2s[i], 10);
    
    if (v1s[i] > v2s[i]) {
      return 1;
    }
    
    if (v1s[i] < v2s[i]) {
      return -1;
    }
  }
  
  return 0;
}

export function compareVersion(v1, v2) {
  return compareInternal(v1, v2, true);
}

export function gt(v1, v2) {
  return compareInternal(v1, v2, true) === 1;
}
Bad
if (version > '10.1.88') {
  // ...
}
Good
import version from "lib/system/version";

if (version.compare(version, '10.1.88') > 0) {
// 或
if (version.gt(version, '10.1.88')) {
  // ...
}

禁止修改函数入参

修改函数入参可能会引入不可预期的问题,建议学习函数式编程的无副作用原则,不修改形参。

Bad

sort 默认会修改原数组,导致 origData 内的 availableTaskList 被篡改。

 function mapTaskDataToState(origData) {
   return getIn(origData, ['availableTaskList'], [])
     .sort((a, b) => {
       // 获取的任务按照优先级降序排序
       return getIn(b, ['taskConfigInfo', 'priority']) - getIn(a, ['taskConfigInfo', 'priority']);
     });
 }
Good

通过扩展操作符浅拷贝一份

 function mapTaskDataToState(origData) {
   return [...getIn(origData, ['availableTaskList'], [])]
     .sort((a, b) => {
       // 获取的任务按照优先级降序排序
       return getIn(b, ['taskConfigInfo', 'priority']) - getIn(a, ['taskConfigInfo', 'priority']);
     });
 }

面向防御编程

Try Catch

catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。

说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。

Bad

本意只想针对异步请求 fetchSelfOperationRecommendInfo 做 catch,但是将大段不可能出错的代码也都纳入 try-catch,貌似没有问题,而且这不更安全么?其实这是不负责任的懒惰的表现,不应该让 try-catch 给我们做兜底,不可能出错的代码如果报错了,应该在代码编写的时候就要解决,而不是等到线上用 try-catch 去兜底。

export async function querySelfOperationRecommendInfo({ commit, dispatch }) {
  try {
    let selfOperationRecommendInfo = await fetchSelfOperationRecommendInfo();

    const stardSelfOperation = formatSelfOperationRecommendData(selfOperationRecommendInfo);
    commit('updateSelfOperationRecommendInfo', stardSelfOperation);

    // 如果图文咨询的数据不存在走兜底数据
    if (stardSelfOperation.filter(item => item.value === 'TWZX').length === 0) {
      dispatch('fetchArticles');
    } else {
      commit('updateComputeSelfTabsHeight', true);
    }
  } catch (error) {
    return console.error('querySelfOperationRecommendInfo error', error);
  }
}
Good

请求错误无法掌控,故需要错误处理,但其他代码出错,就是逻辑 bug 导致,不应该让 catch 兜底,而应该尽早发现和修复。

export async function querySelfOperationRecommendInfo({ commit, dispatch }) {
  let selfOperationRecommendInfo: IRecommendInfo[];

  try {
    selfOperationRecommendInfo = await fetchSelfOperationRecommendInfo();
  } catch (error) {
    return console.error('querySelfOperationRecommendInfo error', error);
  }

  // 继续处理正常逻辑
}

勿滥用 get

嵌套属性访问应道避免不必要的 get,因为会损失可读性、变量跳转、属性智能提示、自动补全等好处,也可能对性能有损。

  • 防空优先推荐 TS 可选级联语法,即 Optional ChainingmaybeObj?.a?.b?.c
  • foo.bar || ''
  • 其次请使用 lodash.get,请不要自己写
  • 最后才是自己写或公司内部的的 getIn
Bad
const tmallCarStoresList = getIn(result, [ 'data', 'data' ], [])
Good

TS 内置语法更简洁,既保持了流畅的阅读体验,不破坏可读性,仍然保持了属性的智能提示 👍。

const tmallCarStoresList = result?.data?.data || []
Bad

多次重复属性 getIn,损失性能

function foo(array) {
  return array.map(item => {
    return {
      taskId: getIn(item, ['taskConfigInfo', 'id'], ''),
      iconUrl: getIn(item, ['taskConfigInfo', 'iconUrl'], ''),
      name: getIn(item, ['taskConfigInfo', 'name'], ''),
      actionText: getIn(item, ['taskConfigInfo', 'actionText'], '立即前往'),
      actionUrl: getIn(item, ['taskConfigInfo', 'actionUrl'], ''),
      status: getIn(item, ['status'], ''),
      description: getIn(item, ['taskConfigInfo', 'description'], ''),
    };
  });
}
Good

利用解构赋值,无性能损耗,而且通过添加参数类型额外收获了智能提示 👍

/**
 * @param tasks {{ taskConfigInfo: { id: string; iconUrl: string; } }[]} 
 */
function foo(tasks) {
  return tasks.map(item => {
    const {
      id = '',
      iconUrl = '',
      name = '',
      actionText = '立即前往',
      actionUrl = '',
      status = '',
      description = '',
    } = item.taskConfigInfo || {};

    return {
      taskId: id,
      iconUrl,
      name,
      actionText,
      actionUrl,
      status,
      description,
    };
  });
}

危险的底层函数

JS 中一些原生函数当传入不当参数时会抛错,导致功能不可用,严重甚至白屏,比如 JSON.parseJSON.stringifyencodeURIComponentdecodeURIComponent 等。

【强制】:必须使用将危险的底层方法封装成任何时候都不会抛错的方法。

Bad

一旦后端返回值不符合预期,JSON.parse 报错,功能将无法继续使用,严重甚至导致白屏。

const memberBenefits: IMemberBenefit[] = JSON.parse(data.memberBenefits || '[]')
  .map((benefit, index) => {
    // ...
  })
;
Good

将 JSON.parse 封装成通用的 jsonParseSafely

export function jsonParseSafely<T>(str: string, defaultValue: any = {}): T {
  try {
    return JSON.parse(str);
  } catch (error) {
    console.warn('JSON.parse', str, 'failed', error);

    return defaultValue;
  }
}

使用

const memberBenefits = jsonParseSafely<IMemberBenefit[]>(data.memberBenefits, [])
  .map((benefit, index) => {
    // ...
  })
;

其他 Safe 函数。

function decodeURIComponentSafely(url = '') {
  try {
    return decodeURIComponent(url);
  } catch (error) {
    console.error('decodeURIComponent error', error);
    
    return '';
  }
}

export function jsonStringifySafely(obj: any): string {
  try {
    return JSON.stringify(obj);
  } catch (error) {
    console.warn('JSON.stringify obj:', obj, 'failed:', error);

    return '';
  }
}

使用安全优雅的解构赋值

[建议] 使用解构赋值,会逼迫开发者考虑防空,而且会使得所有防空逻辑集中处理。

Bad
Promise.all([this.dispatch('getNewTaskList'), this.dispatch('getNewAdTaskList')])
  .then(result => {
    const taskOriginData = result[0];
    const adTaskOriginData = result[1];

    // ...
  });  
Better
Promise.all([this.dispatch('getNewTaskList'), this.dispatch('getNewAdTaskList')])
  .then(([ originalTasksResp = {}, originalAdTasksResp = {} ]) => {
    // ...
  });

view 渲染内容禁止使用 &&

{{ a && b }} 当条件不满足会让用户看到 undefined 。故 view 里面禁止时候用 && 做渲染,可使用三目运算符等解决方案。和 react 类似。

Bad

会出现 undefined%

<text>{{ feeModalInfo.feeRate && feeModalInfo.feeRate }}%</text>
Good

条件不满足,则不展示让用户看起来似乎页面出现了 bug 的奇怪字符。其次如果该字段对用户很重要,最好是能上报异常日志。

<text>{{ feeModalInfo.feeRate ? `${feeModalInfo.feeRate}%` : '' }}</text>

异步方法或函数一律增加 async 描述符

为了避免返回值不一定是 promise 导致调用处使用 then 出现空指针,增加 async 能够确保函数任何时候都能返回 promise。

Bad
function alertOnEmpty(title) {
  if (!title) {
    reportLog({
      api: 'alertOnEmpty',
      msg: 'won\'t alert because title is empty. It must be a bug',
    });

    return;
  }
  
  return new Promise((resolve) => {
    resolve();
  })
}

调用时,当 title 为空,会出现空指针『Uncaught TypeError: Cannot read property 'then' of undefined』。

alertOnEmpty('').then(console.log('success'))

💡:本质上是违背了函数返回值不一致的规范,假如忠实的描述了返回值 Promise<void> | void 则 VSCode 也会报错。

Good

为空则返回 Promise.resolve();。但是没法确保所有 return 都能返回 Promise 😓。

function alertOnEmpty(title) {
  if (!title) {
    reportLog({
      api: 'alertOnEmpty',
      msg: 'won\'t alert because title is empty. It must be a bug',
    });

    return Promise.resolve();
  }

  return new Promise((resolve) => {
    resolve();
  })
}
Better

只要异步就加 async,简简单单增加一个描述符就能保证函数任何时候必定返回 Promise。

async function alertOnEmpty(title) {
  if (!title) {
    reportLog({
      api: 'alertOnEmpty',
      msg: 'won\'t alert because title is empty. It must be a bug',
    });

    return false;
  }

  return new Promise((resolve) => {
    resolve(true);
  })
}

前端兜底请慎重

前端写兜底逻辑要慎之又慎,相当于数据造假,很容易出现线上问题。兜底逻辑务必征得 PD 同意方可使用,开发者禁止私自兜底。

Bad

文案兜底很容易引起用户舆情,若必须兜底必须同步测试和 PD。

resultPageInfo: {
  arriveDateDes: '预计两小时到账', // 到账时间描述
  withdrawFee: '0.01', // 服务费
},
Good
resultPageInfo: {
  arriveDateDes: '',
  withdrawFee: '',
},

勿滥用 for 循环

[建议] 请勿滥用 for 循环,包括 for-in 和 for-of,应优先使用 map reduce filter forEach find includes some any every 等没有副作用的高阶函数,如果要用请注释你的理由。

原因:for 循环属于命令式代码,繁琐易出错。首先要设置一个下标变量 i,小心翼翼确保不要越界,要记得每次循环加一。而且越界条件还有多种写法: i < tabs.lengthi <= tabs.length - 1 ,累加也有多种写法: i++++ii += 1

参考 1:airbnb JavaScript 编码规约 Prefer JavaScript’s higher-order functions instead of loops like for-in or for-of.

参考 2:过程式、函数式、命令式、声明式编程模式不同点

Bad
for (let i = 0; i <= tabs.length - 1; i++) {
  console.log(tabs[i]);
}
Good

简洁而不易出错

tabs.forEach(tab => {
  console.log(tab);
});

勿滥用 map

[强制] 对应 eslint rule array-callback-return

  • 禁止在使用 map 的场景不对返回值做处理
  • 禁止在 map 内修改 item
Bad

把 map 当做 forEach 用 👎。

let invoiceDetailLst = [];

invoiceDetailType.map((item) => {
  if (invoiceDetail[item.key]) {
    invoiceDetailLst.push({
      type: item.key,
      key: item.value,
      value: invoiceDetail[item.key],
    });
  }
});
Good

逻辑分成先过滤或处理后更清晰,没有临时变量更优雅 👍🏻。

const invoiceDetailLst = invoiceDetailType
  .filter(item => invoiceDetail[item.key])
  .map(item => ({
    type: item.key,
    key: item.value,
    value: invoiceDetail[item.key],
  }));

禁止在 this 上随意添加属性

处于性能考虑,非直接渲染需要的变量禁止放到 state 或 data 中,且考虑到当前对象不是自己构建的,那么在 this 指针的使用过程中,禁止添加新的属性,避免覆盖了原 this 中的属性可能带来不可预期的情况。

Bad

this 是小程序内页面示例,内含需要关键属性或方法,擅自添加属性可能会覆盖内置属性导致不可预期的运行时错误。

Page({
    onLoad(options) {
    this.options = options
  },
  
  // ...
  
  fetchInfo() {
    const { code } = this.options
  }
})
Good

通过页面级别变量存储

let customData = {}

Page({
    onLoad(options) {
    customData = options
  },
  
  // ...
  
  fetchInfo() {
    const { code } = customData
    ...
  }
})
Good

通过自定义数据对象,建议放置到顶部显眼位置。

Page({
  customData: {},
  
    onLoad(options) {
    this.customData = options
  },
  
  //...
  
  fetchInfo() {
    const { code } = customData
    ...
  }
})

不要依赖 setData 的同步性

同步是指 setData 后能从 data 中立即拿到刚刚设置到值。

setData 目前实践测试是同步的,但是不稳定,小程序插件内出现过依赖同步性导致的 bug,而且是偶发的很难排查,小程序框架开发者也没有承诺过 setData 一定是同步的,故『不要依赖 setData 的同步性』。

Bad
{
    async bar() {
    const { subBizType } = await foo(bizType);

    this.setData({ subBizType });

    await this.fetchNotice();
  },

  async fetchNotice() {
    let res;

    try {
      res = await getCommonData(this.data.subBizType);
    } catch (e) {
      myLogger.error('fetchNotice失败', { e });
      
      return;
    }
    
    this.setData({
      noticeList: res.noticeList,
    });
  },
}
Good

不依赖 setdata 是同步的,将 subBizType 当做函数参数。另一个收益是依赖更明显了。explicit is better than implicit

{
    async bar() {
    const { subBizType } = await foo(bizType);

    this.setData({ subBizType });

    await this.fetchNotice(subBizType);
  },

  async fetchNotice(subBizType) {
    let res;

    try {
      res = await getCommonData();
    } catch (e) {
      myLogger.error('fetchNotice失败', { e });
      
      return;
    }
    
    this.setData({
      noticeList: res.noticeList,
    });
  },
}

脱敏规范和工具

请勿在前端或客户端实现脱敏逻辑,且需使用符合公司最新的脱敏规范的脱敏工具,而非自行实现。

💡 注意: 脱敏应该在 BFF 层进行,不要尝试将 BFF 层的库使用 babel 转换,以便于在客户端进行“脱敏”。