"Safe Production" of Mini Program Best Practices

"Safe Production" of Mini Program Best Practices

Introduction: Next, we will open a small program best practice knowledge booklet, starting from the small program as an entry point, but it is by no means limited to this. It will cover several chapters " Naming ", " Single Test ", " Code Style ", " Comment ", "Safety Production", " Code Design ", "Performance", "Internationalization" and "Why Mini Program Development Best Practice". Each article will contain positive and negative examples and detailed explanations.

The examples in this article are some typical problems found in the CR process. I hope that everyone can avoid such problems in advance through this knowledge booklet, and let our CR focus more on business logic rather than such trivial or lack of basic concepts, and ultimately form a high-quality CR.

Safe production

Next, I will open the most special one of all the articles. It is very important because it is related to online security, but it is easy to be ignored by novices or veterans. First introduce several principles of safe production:

1. Online changes must comply with the "three axes", that is, monitorable, grayscale, and emergency.

2. It is not possible to perform online changes in all time periods.

Generally, companies will standardize the release time, avoid meals and non-working days, and avoid that all parties (front-end and back-end testing, SRE, etc.) are not in the work station and cannot be processed in time after a failure.

3. Online faults need to be held accountable and there must be a clear fault level and responsibility division. For example, if the number of affected persons triggers a Pn failure within the range of xx, the personal impact on the failure is yy.

The following will work from multiple angles and methods to ensure safe production online .

Business log report

The front-end custom business log reporting helps to quickly troubleshoot online problems. There are usually two ways:

  • Method 1: Send a click or exposure event to a virtual cd bit by burying points (click is recommended for time-sensitive reasons), such as c00000.d00000 , and other information is uploaded through extended parameters. Traditional methods rely on the backend to retrieve the return value from the log, which requires a long time to be reported, and the log is reported in real time for real-time analysis.
  • Method two: through custom jsapi, if there is a suggestion to use method two, because method one occupies the traffic of the buried point and adds unnecessary burden to it.

Let's look at a case first and give an intuitive understanding.

Bad

In response to the online problem, the backend returned a dirty value, causing a certain delivery module A to display the source code that has not been interpolated.

${title}
After the front-end was improved, it was decided not to display the module for this situation, but at this time it is difficult to distinguish whether the back-end return value is incorrect or it is indeed not put. The surface is peaceful.

if (!isDirtyValue(title) && !isDirtyValue(subtitle) && !isDirtyValue(actionUrl)) { //Show delivery module A } Copy code
Good

Reporting the "dirty data" log and combining with monitoring can quickly locate the wrong field returned by the backend .

if (hasDirtyValue[title, subtitle, actionUrl]) { reportLog({ code : 'DATA_INVALID' , msg : `dirtyValue: title ( ${title} ), subtitle ( ${subtitle} ) or actionUrl ( ${actionUrl} ) contains "\$\{"` , response : resp, }); return ; } //At this time, the delivery module A can be displayed normally ... Copy code

Buried point reporting log specification

Different projects or applications need to develop log specifications. As long as you are familiar with this set of specifications, you can significantly reduce maintenance costs when you take over new projects in the future.

/** * The running log specification spec. * From best practice */ interface ILog { /** Monitoring code*/ code?: number , /** Other custom request path or http request api, jsapi name */ api?: string ; /** Brief description*/ msg?: string ; /** log source*/ from ?: string ; /** log occurrence time*/ at?: number , type ?: 'RPC' | 'HTTP' | 'JSAPI' | 'MTOP' | 'JS_Builtin_Func' ; /** Complete error */ error?: Error | object ; /** For example, HTTP request method */ subType?: string ; /** Request body*/ request?: object ; /** Response body*/ response? : object ; } Copy code

How to report

Method one: by burying points

/** * Report business custom buried points */ export const reportLog = (log: ILog = ()): void => { try { const $spm = getSpm(); const { type, api, msg, subType, error = {}, request = {}, response = {}, } = log; /** Prevent the log from being too long to cause performance problems*/ const MAX_LOG_LENGTH = 2000 ; const errorStr = jsonStringifySafely(error).slice( 0 , MAX_LOG_LENGTH); $spm?.click( 'c00000.d00000' , { //Add _ at the beginning to distinguish it from other built-in buried point fields _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); } }; Copy code

Method 2: jsapi

//src/common/utils/remoteLog.js //Initialize the RemoteLogger instance in the public file import {RemoteLogger} from '@yourcompany/RemoteLogger' ; const remoteLogger = new RemoteLogger({ bizType : 'BIZ_TYPE' , appName : 'application name' , }); const withdrawRemoteLogger = new RemoteLogger({ bizType : 'BIZ_TYPE' , appName : 'application name-page name' , }); /** * * @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); //Report monitoring if (MonitorCodeEnum[log.code]) { reportLog(toMonitorLog(formatted)); } } /** * @param {ILog['from']} from * @returns {RemoteLogger} */ function resolveLogger ( from ) { return loggers[ from ] || remoteLogger; } /** * Report flow log */ export const remoteConsole = { /** * Report normal flow log * @param {ILog} log */ info ( log ) { send(log, 'info' ); }, /** * Report abnormal flow log * @param {ILog} log */ error ( log ) { send(log, 'error' ); }, }; const PAGE_NAMES = { withdraw : { home : 'withdraw' , }, }; const loggers = { [PAGE_NAMES.withdraw.home]: withdrawRemoteLogger, }; /** * One remote console per page */ export const withdrawHomeRemoteConsole = { /** * Report normal flow log * @param {ILog} log */ info ( log ) { remoteConsole.info({ ...log, from : PAGE_NAMES.withdraw.home }); }, /** * Report abnormal flow log * @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; } Copy code

Example

Report an error log for the withdrawal process

import {withdrawHomeRemoteConsole as rc} from '/common/utils/remoteLog' ; withdraw() .catch( ( error ) => { const isExpectedError = showErrorModal(error); if (!isExpectedError) { rc.error({ msg : 'withdraw unknown error' , error, }); } }); Copy code

To report an alarm, just add a 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, }); } }); Copy code

How to query the reported log

Depending on the company, most companies have their own burying point platform.

JS compatible

The applet must support iOS 9+, so please refer to caniuse during development: community caniuse , applet caniuse .

Bad

Compatibility is not considered

Object.values
It is not supported in iOS 9 or even part of iOS 10 .

Good

Use lodash values for better compatibility

Import values from 'lodash/values' ; values(obj); Copy code

JSAPI compatibility

If the document indicates that compatibility is required, business compatibility must be done. It is not limited to jsapi, such as some native components. If it is not compatible, you need to comment the reason.

Bad

Cause online JS worker to report error "my.hideBackHome+is+not+a+function"

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

Further encapsulation

//utils/system.ts export function canIUse ( jsapiName: string ): boolean { return my.canIUse(jsapiName) && typeof my[jsapiName] === 'function' ; } //use in index.ts onLoad () { //Hide the auto recharge home button, auto recharge is an independent application canIUse( 'hideBackHome' ) && my.hideBackHome(); } Copy code

CSS compatible

Shredded hair effect

1rpx (0.5px) will have compatibility issues, it is recommended to use the hair silk effect

Correspondence table of font weight for different models

Font weightfont-weightEnglish
Regular body400 (normal)PingFang-Regular
Very thin body200PingFang-Ultralight
Slim body100PingFang-Thin
Body300PingFang-Light
Medium black500PingFang-Medium
Medium bold600PingFang-Semibold
Bold700 (bold)PingFang-Bold
  1. The PingFang font exported by Sketch is unique to iOS and not supported by Android, so there is no need to specify font-family in css.
  2. The "semi-bold" (font-weight of 600) text under Chinese iOS is all invalid under Android, while the bold (font-weight of 700) text is normal.
Bad

1rpx may have compatibility issues, it is recommended to use the hair silk effect

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

Define hair strand effect

//mixins.less //1px border on the moving end of the hairline //NOTICE: The parent element must be position relative or 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 ; } } Copy code

use

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

Prohibit the use of synchronous JSAPI

such as

my.getSystemInfoSync
/
my.getStorageSync
, Synchronization will block the execution of js worker, which greatly affects the user experience. Must use the corresponding asynchronous method
my.getSystemInfo
/
my.getStorage
And it is recommended to package into
Promise
.

Bad
const {Version} = my.getSystemInfoSync (); duplicated code
Good

The asynchronous getSystemInfo after promisify is used, and it comes with cache and failure reporting log logic to prevent concurrent repeated calls .

//lib/system.ts /** * * @param {any} obj * @returns {boolean} */ function isPromise ( obj ) { return obj && typeof obj.then === 'function' ; } let cachedSystemInfoPromise; /** * GetSystemInfo with cache * Do not use synchronization getSystemInfoSync * Default 500ms timeout * * @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; } Copy code

use

const {Version} = the await GetSystemInfo (); duplicated code

Carefully introduce self-developed util dependency packages

Please introduce the industry's mature three-party npm package with single test, such as lodash, the two-party package packaged for business specificity must be compiled into es5, and src cannot be released

User input parameters are never trusted

Prohibit no brain jump according to custom parameters

Security risks need to be controlled through a whitelist, because third-party pages are redirected through our application. For users, this is the default secure page of our application or Alipay.

Solution: Through the whitelist method, the whitelist is divided into strict matching whitelist and rule matching whitelist, and strict matching takes priority

Naming Tips: The English corresponding to the whitelist cannot be a racially discriminatory whitelist but should be an allowlist, and the blacklist is a denylist.

Bad

An application A will redirect to the target address passed in by the query. This requirement poses a security risk. If a phishing website is redirected by the application and the user believes that the website is trusted by the application A, the trust will be passed (in the user's mind) PageRank algorithm), will mislead users that the third-party website is a legitimate website.

Page({ onLoad ( options: IOptions ) { const {goPage} = options; if (goPage) { this .commit( 'setState' , { goPage, }); } }, //Go to the page after completing an operation without checking the page address onExitCamera () { const {goPage} = this .state; if (goPage) { jump(goPage, {}, { type : 'redirectTo' , }); } else { goBack(); } }, }) Copy code
Good
Page({ onLoad ( options: IOptions ) { const {goPage} = options; //Adopt strict matching, only the links in the whitelist can be redirected if (isTrustedRedirectUrl(goPage)) { this .commit( 'setState' , { goPage, }); } }, }); function isTrustedRedirectUrl ( url ) { return ALLOWLIST.includes(url); } Copy code

Version number comparison

The version number cannot be directly compared with strings, please use the company-defined library, or use the snippets below to further package

gt
,
gte
,
lt
,
lte
,
eq
And other more readable methods.

Because according to the character comparison

1 <9
,thereby
'10.9.7' <'9.9.7'
.

function compareInternal ( v1: string, v2: string, complete: boolean ) { //When v2 is undefined, v1 takes the version number of the client 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 ; } Copy code
Bad
if (version> '10.1.88' ) { //... } Copy code
Good
import version from "lib/system/version" ; if (version.compare(version, '10.1.88' )> 0 ) { //or if (version.gt(version, '10.1.88' )) { //... } Copy code

It is forbidden to modify function parameters

Modifying function input parameters may introduce unexpected problems. It is recommended to learn the principle of no side effects of functional programming and not modify formal parameters.

Bad

sort
The original array will be modified by default, resulting in
origData
inside
availableTaskList
Has been tampered with.

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 ) => { //The acquired tasks are sorted in descending order of priority return getIn(b, [ 'taskConfigInfo' , 'priority' ])-getIn(a, [ 'taskConfigInfo' , 'priority']); }); } Copy code

Defensive programming

Try Catch

When catching, please distinguish stable code and non-stable code. Stable code refers to code that will not go wrong anyway. For the catch of unstable code, try to distinguish the exception types as much as possible, and then do the corresponding exception handling.

Explanation: Try-catch a large section of code, so that the program cannot make correct stress response according to different abnormalities, and it is not conducive to locating the problem. This is an irresponsible performance.

Bad

The intention is only for asynchronous requests

fetchSelfOperationRecommendInfo
Do catch, but include a large section of code that is impossible to make mistakes into try-catch. It seems that there is no problem, and is it not safer? In fact, this is a manifestation of irresponsible laziness. Try-catch should not be used to help us. If an error is reported in code that cannot be wrong, it should be resolved when the code is written, instead of waiting for online try-catch Go to the bottom.

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); } } Copy code
Good

Request errors cannot be controlled, so error handling is required, but other code errors are caused by logic bugs. Catch should not be caught, but should be found and fixed as soon as possible.

export async function querySelfOperationRecommendInfo ( {commit, dispatch} ) { let selfOperationRecommendInfo: IRecommendInfo[]; try { selfOperationRecommendInfo = await fetchSelfOperationRecommendInfo(); } catch (error) { return console .error( 'querySelfOperationRecommendInfo error' , error); } //continue processing normal logic } Copy code

Don't abuse
get

Nested attribute access should avoid unnecessary

get
, Because it will lose the benefits of readability, variable jumps, attribute intelligent prompts, and auto-completion, and it may also hurt performance.

  • Air defense priority recommends TS optional cascading syntax, namely Optional Chaining :
    maybeObj?.a?.b?.c
  • or
    foo.bar ||''
  • Secondly, please use lodash.get, please do not write by yourself
  • The last one is written by yourself or inside the company
    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 = 'Go now', actionUrl = '', status = '', description = '', } = item.taskConfigInfo || {}; return { taskId: id, iconUrl, name, actionText, actionUrl, status, description, }; }); } Copy code

Dangerous low-level functions

Some native functions in JS throw errors when passing improper parameters, resulting in unavailable functions, serious or even blank screens, such as

JSON.parse
,
JSON.stringify
,
encodeURIComponent
,
decodeURIComponent
Wait.

[Mandatory]: The dangerous bottom method must be encapsulated into a method that will not throw a mistake at any time.

Bad

Once the backend return value does not meet expectations, JSON.parse reports an error, the function will not be able to continue to be used, severely even lead to a white screen.

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

Encapsulate JSON.parse into a universal

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

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