接下来将开启所有篇中最为特殊的一篇,很重要因为关乎到线上安全,但又很容易被新手或老手忽略。首先介绍安全生产的几个原则:
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.values
在 iOS 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 |
极细体 | 200 | PingFang-Ultralight |
纤细体 | 100 | PingFang-Thin |
细体 | 300 | PingFang-Light |
中黑体 | 500 | PingFang-Medium |
中粗体 | 600 | PingFang-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,进一步封装 gt
、gte
、lt
、lte
、eq
等可读性更强的方法。
因为按照字符比较
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 Chaining:
maybeObj?.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.parse
、 JSON.stringify
、 encodeURIComponent
、 decodeURIComponent
等。
【强制】:必须使用将危险的底层方法封装成任何时候都不会抛错的方法。
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.length
、 i <= tabs.length - 1
,累加也有多种写法: i++
、 ++i
、 i += 1
。
参考 1:airbnb JavaScript 编码规约 Prefer JavaScript’s higher-order functions instead of loops like for-in
or for-of
.
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客大神,这个方向越往后,需要学习和掌握的东西就会越来越多,以下是学习网络安全需要走的方向: