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

安全生产 👷🏻‍ ==========

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

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-consoleconsole.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-consoleconsole[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-consoleconsole.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 不支持

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)会有兼容性问题,建议使用头发丝效果

不同机型字重对应表

字重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 转换,以便于在客户端进行“脱敏”。

网络安全成长路线图

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成hei客大神,这个方向越往后,需要学习和掌握的东西就会越来越多,以下是学习网络安全需要走的方向:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值