Follow me on my blog shymean.com
In the past development experience to deal with a variety of strange bugs, realize that code robustness (robustness) is an important indicator to improve work efficiency, quality of life, this paper mainly sorted out some thoughts to improve code robustness.
I’ve written about code robustness before
- Seriously write unit tests for JavaScript
- How to log in code
This article continues to explore ways to help improve the robustness of JavaScript code in addition to unit testing and logging.
More secure access to objects
Don’t trust interface data
Don’t trust the parameters passed from the front and don’t trust the data returned from the back
For example, an API/XXX /list interface, as documented
{
code: 0,
msg: "",
data: [
/ /... The specific data]};Copy the code
The front-end code might be written
const {code, msg, data} = await fetchList()
data.forEach(() = >{})
Copy the code
Because we assume that the data returned in the background is an array, we directly use data.forEach, if some exceptions are missed during the syndication
- Data is expected to return when there is no data
[]
Empty array, but the background implementation does not returndata
field - When the interface is updated, data is changed from an array to a dictionary, and the front end is not synchronized in time
In these cases, an error is reported using data.foreach,
Uncaught TypeError: data.forEach is not a function
So it’s a good idea to add type detection where you directly use the value returned by the background interface
Array.isArray(data) && data.forEach(() = >{})
Copy the code
Similarly, when processing front-end request parameters, the background should also conduct related type detection.
Null-value merge operator
Due to the dynamic nature of JavaScript, it is best to check the presence of x and y when querying an object property such as X.Y.Z
let z = x && x.y && x.y.z
Copy the code
While this is often cumbersome, safely accessing object properties in DART is much easier
varz = a? .y? .z;Copy the code
A draft of null-value merge operators has been proposed in ES2020, including?? And? . Operator that provides the same secure access to object properties as DART. You can now test it by opening the latest version of Chrome
Before we do that, we can encapsulate a method that safely obtains object properties
function getObjectValueByKeyStr(obj, key, defaultVal = undefined) {
if(! key)return defaultVal;
let namespace = key.toString().split(".");
let value,
i = 0,
len = namespace.length;
for (; i < len; i++) {
value = obj[namespace[i]];
if (value === undefined || value === null) return defaultVal;
obj = value;
}
return value;
}
var x = { y: { z: 100,}};var val = getObjectValueByKeyStr(x, "y.z");
// var val = getObjectValueByKeyStr(x, "zz");
console.log(val);
Copy the code
The front end inevitably has to deal with a variety of browsers and devices, one of the most important issues is compatibility, especially now that we are used to developing code using ES2015 features, Polyfill can help solve most of our problems.
Remember exception handling
Reference:
- JS error handling MDN
- This series of articles is very well written
Exception handling is the primary guarantee of code robustness, and there are two aspects to exception handling
- Proper error handling can feed the user experience, gracefully notifying the user when the code goes wrong
- Encapsulating error handling reduces development and decouples error handling from code
Error object
A custom error object can be thrown through a throw statement
// Create an object type UserException
function UserException (message){
// Contains two attributes: message and name
this.message=message;
this.name="UserException";
}
// Override the toString of the default [object object]
UserException.prototype.toString = function (){
return this.name + '"' + this.message + '"';
}
// Throw a custom error
function f(){
try {
throw new UserException("Value too high");
}catch(e){
if(e instanceof UserException){
console.log('catch UserException')
console.log(e)
}else{
console.log('unknown error')
throw e
}
}finally{
// You can perform some exit operations, such as closing a file or closing loading
console.log('done')
return 1000 // If a value is returned in finally, the return value or exception in the previous try or catch is overridden
}
}
f()
Copy the code
Synchronization code
For synchronous code, errors can be encapsulated using the chain of responsibility pattern, which means that if the current function can handle the error, it handles it in a catch: if it can’t handle the corresponding error, the catch is thrown up again
function a(){
throw 'error b'
}
// When B can handle an exception, it is no longer thrown up
function b(){
try{
a()
}catch(e){
if(e === 'error b') {console.log('Handled by B')}else {
throw e
}
}
}
function main(){
try {
b()
}catch(e){
console.log('the top catch')}}Copy the code
Asynchronous code
Since a catch cannot get an exception thrown in asynchronous code, to implement the chain of responsibility, exception handling needs to be passed to the asynchronous task as a callback function
function a(errorHandler) {
let error = new Error("error a");
if (errorHandler) {
errorHandler(error);
} else {
throwerror; }}function b(errorHandler) {
let handler = e= > {
if (e === "error b") {
console.log("Handled by B");
} else{ errorHandler(e); }};setTimeout(() = > {
a(handler);
});
}
let globalHandler = e= > {
console.log(e);
};
b(globalHandler);
Copy the code
Exception handling in Prmise
Promise contains only three states: Pending, Rejected and depressing
let promise2 = promise1.then(onFulfilled, onRejected)
Copy the code
Here are a few rules for making promises throw exceptions
function case1(){
// If promise1 is promised-i, but onRejected returns a value (including undipay), then promise2 will still be depressing. Therefore, I pay therefore there is no need to move upward. Therefore, I pay therefore, therefore I am still depressed.
var p1 = new Promise((resolve, reject) = >{
throw 'p1 error'
})
p1.then((res) = >{
return 1
}, (e) = >{
console.log(e)
return 2
}).then((a) = >{
// If onReject is registered, subsequent Promise execution will not be affected
console.log(a) // A 2 is received})}function case2(){
// If (p1) {// if (p1) {// if (p1) {// If (p1) {// If (p1) {// If (p1) {// If (p1) {//
var p1 = new Promise((resolve, reject) = >{
throw 'p1 error'
})
p1.then((res) = >{
return 1
}, (e) = >{
console.log(e)
throw 'error in p1 onReject'
}).then((a) = >{}, (e) = >{
// If p1 onReject throws an exception
console.log(e)
})
}
function case3(){
// If promise1 is in the Rejected state and onRejected is not specified, promise2 is also in the Rejected state.
var p1 = new Promise((resolve, reject) = >{
throw 'p1 error'
})
p1.then((res) = >{
return 1
}).then((a) = >{
console.log('not run:', a)
}, (e) = >{
// If p1 onReject throws an exception
console.log('handle p2:', e)
})
}
function case4(){
// // If the promise1 is a big pity but onFulfilled and onRejected are abnormal, promise2 will also be fulfilled and obtain the rejection reason or exception of promise1.
var p1 = new Promise((resolve, reject) = >{
resolve(1)
})
p1.then((res) = >{
console.log(res)
throw 'p1 onFull error'
}).then(() = >{}, (e) = >{
console.log('handle p2:', e)
return 123})}Copy the code
Therefore, we can deal with the error of the current promise in onRejected, and if not, throw it to the next promise
async
Async /await is essentially the syntactic sugar of promise, so a similar capture mechanism like promise.catch can also be used
function sleep(cb, cb2 =()=>{},ms = 100) {
cb2()
return new Promise((resolve, reject) = > {
setTimeout(() = > {
try {
cb();
resolve();
}catch(e){
reject(e)
}
}, ms);
});
}
// Catch by promise.catch
async function case1() {
await sleep(() = > {
throw "sleep reject error";
}).catch(e= > {
console.log(e);
});
}
/ / by the try... Catch catch
async function case2() {
try {
await sleep(() = > {
throw "sleep reject error"; })}catch (e) {
console.log("catch:", e); }}// If an error is thrown by reject, it cannot be caught
async function case3() {
try {
await sleep(() = >{}, () = > {
// Throws an error that was not promised reject
throw 'no reject error'
}).catch((e) = >{
console.log('cannot catch:', e)
})
} catch (e) {
console.log("catch:", e); }}Copy the code
More stable third-party modules
When implementing some relatively small functions, such as date formatting, we may not be used to find a mature library from NPM, but write a feature package by ourselves. Due to the lack of development time or test cases, when encountering some boundary conditions that are not considered, bugs will easily occur.
This is also why NPM tends to have some very small modules, such as this odd package: isOdd, which has 600,000 downloads per week.
An important reason to use more mature libraries is that they have been tested by a lot of test cases and the community and are certainly more secure than our handy tool code.
An example of personal experience is that UA determines the user’s current access to the device. The normal idea is to use the re to match, so I wrote one myself to save trouble
export function getOSType() {
const ua = navigator.userAgent
const isWindowsPhone = / (? :Windows Phone)/.test(ua)
const isSymbian = / (? :SymbianOS)/.test(ua) || isWindowsPhone
const isAndroid = / (? :Android)/.test(ua)
// Check if it is flat
const isTablet =
/ (? :iPad|PlayBook)/.test(ua) ||
(isAndroid && !/ (? :Mobile)/.test(ua)) ||
(/ (? :Firefox)/.test(ua) && / (? :Tablet)/.test(ua))
// Is it an iPhone
const isIPhone = / (? :iPhone)/.test(ua) && ! isTablet// Whether it is a PC
constisPc = ! isIPhone && ! isAndroid && ! isSymbian && ! isTabletreturn {
isIPhone,
isAndroid,
isSymbian,
isTablet,
isPc
}
}
Copy the code
After going online, it was found that the logical judgment of some Xiaomi tablet users was abnormal, and the UA was found in the log
"Mozilla / 5.0 (Linux; U; Android 8.1.0; zh-CN; MI PAD 4 Build/OPM1.171019.019) AppleWebKit/537.36 (KHTML, Like Gecko) Version/4.0 Chrome/57.0.2987.108 Quark/3.8.5.129 Mobile Safari/537.36Copy the code
Even if the MI PAD is added to the re judgment as a temporary fix, what if special UAs from other devices appear later? As a result, it is difficult to consider all the issues written in my own experience, so I will replace them with the mobile-Detect library.
The disadvantage of using modules is that
- It may increase file dependency volume, increase packaging time, etc. This problem can be solved by packaging configuration, packaging third-party modules that do not change often into vendor file configuration cache
- In some projects, the use of third-party modules may be reduced due to security concerns, or the source code review may be required first
Of course, various considerations should also be taken into consideration when selecting modules, including stability, compatibility with older versions, unresolved issues and so on. When a good tool module is selected, we can focus more on the business logic.
Local configuration file
In a development environment, we may need some local switch configuration files, which exist only for local development, do not enter the code base, and do not conflict with other colleagues’ configuration.
I’m a fan of hosting mock templates in git repositories to make it easier for other colleagues to develop and debug interfaces, which brings up a problem where you might need a local switch to introduce mock files
Here is a common practice: create a local configuration file, config.local.js, and export the related configuration information
// config.local.js
module.exports = {
needMock: true
}
Copy the code
Remember to ignore this file in.gitignore
config.local.js
Copy the code
Then pass the try… catch… When loading this module, since the file does not enter the code base, the catch process will be entered when the code is updated in other places, and the local development will enter the normal module introduction process
// mock/entry.js
try {
const { needMock } = require('./config.local')
if (needMock) {
require('./index') // The corresponding mock entry
console.log('====start mock api===')}}catch (e) {
console.log('No mock introduced, create /mock/config.local and export {needMock: true} if necessary')}Copy the code
Finally in the entire application of the entry file to determine the development environment and introduction
if (process.env.NODE_ENV === 'development') {
require('.. /mock/entry')}Copy the code
This way, you can happily do various configurations at local development time without worrying about forgetting to comment out the corresponding configuration changes before committing the code
Code Review
Reference:
- Code Review is a bitter but interesting practice
Code Review should be a necessary step before going online. I think the main functions of CR are as follows
-
Be able to identify any deviation in requirements understanding and avoid wrangling
-
Optimize code quality, including redundant code, variable naming, and over-encapsulation, and at least ensure that the reviewer can understand the logic in addition to the person writing the code
On a project that requires long-term maintenance iterations, each commit and merge is critical, so it is best to review the changed code from scratch before merging it. Even if you’re on a smaller team or can’t find reviewers, take mergers seriously.
summary
This article focuses on sorting out ways to improve the robustness of your JavaScript code
- Securely access object properties to avoid code errors caused by data exceptions
- Catch exceptions and handle or report exceptions through the responsibility chain
- Using more stable and secure third-party modules,
- Take every merge seriously and check the code before going live
In addition, you need to develop good programming habits and consider as many boundary cases as possible.