經過這篇文章,能夠學到node
先看一段代碼webpack
const {name = 'xxx', age} = { name: null, age: 18}
console.log(name);
複製代碼
name輸出的是null
,由於解構賦值的默認值只有當值爲undefined
時纔會生效,這點若是不注意就會引發bug。咱們組內最近就遇到了由於這點而引發的一個bug,服務端返回的數據,由於使用瞭解構賦值的默認值,結果由於值爲null沒有被賦值,而致使了問題。web
那麼如何能避免這種問題呢?typescript
咱們最終的方案有兩種,第一種服務端返回數據以後遞歸的設置默認值,以後就不須要再作判斷,直接處理就行。第二種是當取屬性的時候去作判斷,若是爲null或undefined就設置默認值。爲了支持這兩種方案,咱們封裝了一個工具函數包 @qnpm/flight-common-utils。npm
這個工具包首先要包含setDefaults、getProperty這兩個函數,第一個是遞歸設置默認值的,第二個是取屬性並設置默認值的。除此以外還能夠包含一些別的工具函數,把一些通用邏輯封裝進來以跨項目複用。好比判空isEmpty,遞歸判斷對象和屬性是否相等isEqual等json
由於用了typscript,通用函數考慮的狀況不少,爲了更精準的類型提示,類型的邏輯寫的很複雜,比實現邏輯的代碼都多。。數組
這裏只介紹類型較爲複雜的setDefaults、getProperty。bash
這個函數的參數是一個待處理對象,若干個默認對象,最後一個參數能夠傳入一個函數自定義處理邏輯函數
function setDefaults(obj, ...defaultObjs) {
}
複製代碼
這裏的類型的特色是函數返回值是原對象和一些默認對象的合併,而且參數個數不肯定。因此用到了函數類型的重載,加上any的兜底。工具
type SetDefaultsCustomizer = (objectValue: any, sourceValue: any, key?: string, object?: {}, source?: {}) => any;
複製代碼
SetDefaultsCustomizer是自定義處理函數的類型,接受兩個須要處理的值,和key的名字,還有兩個對象。
而後是setDefauts的類型,這裏重載了不少狀況的類型
function setDefaults<TObject>(object: TObject): TObject;
複製代碼
若是隻有一個參數,那麼直接返回這個對象。
function setDefaults<TObject, TSource>(object: TObject, source: TSource, customizer: SetDefaultsCustomizer): TObject & TSource;
複製代碼
當傳入一個source對象時,返回的對象爲兩個對象的合併TObject & TSource
function setDefaults<TObject, TSource1, TSource2>(object: TObject, source1: TSource1, source2: TSource2, customizer: SetDefaultsCustomizer): TObject & TSource1 & TSource2;
function setDefaults<TObject, TSource1, TSource2, TSource3>(object: TObject, source1: TSource1, source2: TSource2, source3: TSource3, customizer: SetDefaultsCustomizer): TObject & TSource1 & TSource2 & TSource3;
function setDefaults<TObject, TSource1, TSource2, TSource3, TSource4>(object: TObject,source1: TSource1,source2: TSource2,source3: TSource3,source4: TSource4,customizer: SetDefaultsCustomizer): TObject & TSource1 & TSource2 & TSource3 & TSource4;
function setDefaults<TResult>(object: any, ...defaultObjs: any[]): TResult;
複製代碼
由於參數數量不固定,因此須要枚舉參數爲1,2,3,4的狀況,同時加一個any的狀況來兜底,這樣聲明當用戶寫4個和如下參數的時候都是有提示的,但超過4個就只能提示any了,能覆蓋大多數使用場景。
實現這個函數:
type AnyObject = Record<string | number | symbol, any>;
function setDefaults<TResult>(obj: any, ...defaultObjs: any[]): TResult {
// 把數組賦值一份
const defaultObjsArr = Array.prototype.slice.call(defaultObjs);
// 取出自定義處理函數
const customizer = (function() {
if (defaultObjsArr.length && typeof defaultObjsArr[defaultObjs.length - 1] === "function") {
return defaultObjsArr.splice(-1)[0];
}
})();
// 經過reduce循環設置默認值
return defaultObjsArr.reduce((curObj: AnyObject, defaultObj: AnyObject) => {
return assignObjectDeep(curObj, defaultObj, customizer);
}, Object(obj));
}
複製代碼
Record是內置類型,具體實現是:
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
複製代碼
因此,AnyObject 其實就是一個值爲any類型的對象。
把參數數組賦值一份後,取出自定義處理函數,經過reduce循環設置默認值。 assignObjectDeep實現的是給一個對象遞歸設置默認值的邏輯。
const assignObjectDeep = <TObj extends AnyObject, Key extends keyof TObj>(
obj: TObj,
srcObj: TObj,
customizer: SetDefaultsCustomizer
): TObj => {
for (const key in Object(srcObj)) {
if (
typeof obj[key] === "object" &&
typeof srcObj[key] === "object" &&
getTag(srcObj[key]) !== "[object Array]"
) {
obj[key as Key] = assignObjectDeep(obj[key], srcObj[key], customizer);
} else {
obj[key as Key] = customizer
? customizer(obj[key], srcObj[key],key, obj, srcObj)
: obj[key] == void 0
? srcObj[key]
: obj[key];
}
}
return obj;
};
複製代碼
類型只限制了必須是一個對象也就是 TObj extends AnyObject
,同時key必須是這個對象的索引Key extends keyof TObj
。
經過for in遍歷這個對象,若是是對象或者數組,那麼就遞歸,不然合併兩個對象,當有customizer時,調用該函數處理,不然判斷該對象的值是否爲null或undefined,是則用默認值。(void 0是undefeind,== void 0就是判斷是否爲null或undefeind)
getProperty有三個參數,對象,屬性路徑和默認值。
function getProperty(object, path, defaultValue){}
複製代碼
由於重載狀況較多,類型比較複雜,這是工具類函數的特色。 首先聲明幾個用到的類型
type AnyObject = Record<string | number | symbol, any>;
type Many<T> = T | ReadonlyArray<T>;
type PropertyName = string | number | symbol;
type PropertyPath = Many<PropertyName>;
interface NumericDictionary<T> {
[index: number]: T;
}
複製代碼
AnyObject爲值爲any的對象類型。 Record 和ReadonlyArray是內置類型。PropertyName爲對象的索引類型,只有三種,string、number、symbol,PropertyPath是path的類型,能夠是單個的name,也能夠是他們的數組,因此寫了一個工具類型Many來生成這個類型。NumericDictionary是一個name類型爲number,值類型固定的對象,相似數組。
首先是object爲null和undefined的狀況:
function getProperty( object: null | undefined, path: PropertyPath ): undefined;
function getProperty<TDefault>( object: null | undefined, path: PropertyPath, defaultValue: TDefault ): TDefault;
複製代碼
而後是object爲數組時的類型:
function getProperty<T>(
object: NumericDictionary<T>,
path: number
): T;
function getProperty<T>(
object: NumericDictionary<T> | null | undefined,
path: number
): T | undefined;
function getProperty<T, TDefault>(
object: NumericDictionary<T> | null | undefined,
path: number,
defaultValue: TDefault
): T | TDefault;
複製代碼
接下來是object爲對象的狀況,這裏的特色和setDefaults同樣,path可能爲元素任意個的數組,又要聲明他們的順序,這裏只是寫了參數分別爲 1個2個3個4個的類型,而後加上any來兜底。
當path的元素只有一個的時候:
function getProperty<TObject extends object, TKey extends keyof TObject>( object: TObject, path: TKey | [TKey] ): TObject[TKey];
function getProperty<TObject extends object, TKey extends keyof TObject>( object: TObject | null | undefined, path: TKey | [TKey] ): TObject[TKey] | undefined;
function getProperty<TObject extends object, TKey extends keyof TObject, TDefault>( object: TObject | null | undefined, path: TKey | [TKey], defaultValue: TDefault ): Exclude<TObject[TKey], undefined> | TDefault;
複製代碼
當傳入默認值時,返回值多是默認值TDefault,也多是對象的值TObject[TKey],但TObject[TKey]必定不是undefined,因此這裏這麼寫
Exclude<TObject[TKey], undefined> | TDefault
複製代碼
而後是path有2個元素的時候:
function getProperty<TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1]>( object: TObject | null | undefined, path: [TKey1, TKey2] ): TObject[TKey1][TKey2] | undefined;
function getProperty<TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TDefault>( object: TObject | null | undefined, path: [TKey1, TKey2], defaultValue: TDefault ): Exclude<TObject[TKey1][TKey2], undefined> | TDefault;
複製代碼
3個4個也是同樣,就不列了。
兜底類型:
function getProperty( object: any, path: PropertyPath, defaultValue?: any ): any;
複製代碼
實現思路是先處理null和undefined的狀況,而後循環取屬性值,若是值爲undefined則返回默認值,不然返回取到的值。這裏參考了lodash的實現。
function getProperty(object: any, path: PropertyPath, defaultValue?: any): any {
//處理null 和undefined
const result = object == null ? undefined : baseGet(object, path)
//若是取到的值是undefined則返回默認值
return result === undefined ? defaultValue : result
}
function baseGet (object: any, path: PropertyPath): any {
path = castPath(path, object)
let index = 0
const length = path.length
// 循環取path對象的屬性值
while (object != null && index < length) {
object = object[toKey(path[index++])]
}
// 若是取到了最後一個元素,則返回該值,不然返回undefined
return (index && index === length) ? object : undefined
}
複製代碼
測試使用的ts-mocha
組織測試用例,使用chai
作斷言。
getProperty的測試,測試了object爲無效值、對象、數組,還有path寫錯的時候的邏輯。
describe('getProperty', () => {
const obj = { a: { b: { c: 1, d: null } } }
const arr = [ 1, 2, 3, {
obj
}]
it('對象爲無效值時,返回默認值', () => {
assert.strictEqual(getProperty(undefined, 'a.b.c', 1), 1)
assert.strictEqual(getProperty(null, 'a.b.c', 1), 1)
assert.strictEqual(getProperty('', 'a.b.c', 1), 1)
})
it('能拿到對象的屬性path的值', () => {
assert.strictEqual(getProperty(obj, 'a.b.c'), 1)
assert.strictEqual(getProperty(obj, 'a[b][c]'), 1)
assert.strictEqual(getProperty(obj, ['a', 'b', 'c']), 1)
assert.strictEqual(getProperty(obj, 'a.b.d.e', 1), 1)
})
it('錯誤的屬性path的值會返回默認值', () => {
assert.strictEqual(getProperty(obj, 'c.b.a', 100), 100)
assert.strictEqual(getProperty(obj, 'a[c]', 100), 100)
assert.strictEqual(getProperty(obj, [], 100), 100)
})
it('數組能取到屬性path的值', () => {
assert.strictEqual(getProperty(arr, '1'), 2)
assert.strictEqual(getProperty(arr, [1]), 2)
assert.strictEqual(getProperty(arr, [3, 'obj', 'a', 'b', 'c']), 1)
})
})
複製代碼
測試經過
工具函數包須要打包成cmd、esm、umd三種規範的包,同時要支持typescript,因此要導出聲明文件。
經過typescript編譯器能夠分別編譯成 cmd、esm版本,也支持導出 .d.ts聲明文件,umd的打包使用rollup。
其中,tsconfig.json爲:
{
"compilerOptions": {
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": false,
"allowSyntheticDefaultImports": true,
"sourceMap": false,
"types": ["node", "mocha"],
"lib": ["es5"]
},
"include": [
"./src/**/*.ts"
]
}
複製代碼
而後esm和cjs還有types都繼承了這個配置文件,重寫了module的類型。
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"outDir": "./dist/cjs"
}
}
複製代碼
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"target": "es5",
"removeComments": false,
"outDir": "./dist/esm"
},
}
複製代碼
同時,types的配置要加上declaration爲true,並經過declarationDir指定類型文件的輸出目錄
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "es2015",
"removeComments": false,
"declaration": true,
"declarationMap": false,
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"rootDir": "./src"
}
}
複製代碼
還有rollup的ts配置文件也須要單獨出來,module類型爲esm,rollup會作接下來的處理。
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"target": "es5"
}
}
複製代碼
而後是rollup的配置,rollup用來作umd的打包
import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import replace from 'rollup-plugin-replace'
import { terser } from 'rollup-plugin-terser'
import pkg from './package.json'
const env = process.env.NODE_ENV
const config = {
input: 'src/index.ts',
output: {
format: 'umd',
name: 'FlightCommonUtils'
},
external: Object.keys(pkg.peerDependencies || {}),
plugins: [
commonjs(),
nodeResolve({
jsnext: true
}),
typescript({
tsconfig: './tsconfig.esm.rollup.json'
}),
replace({
'process.env.NODE_ENV': JSON.stringify(env)
})
]
}
if (env === 'production') {
config.plugins.push(
terser({
compress: {
pure_getters: true,
unsafe: true,
unsafe_comps: true,
warnings: false
}
})
)
}
複製代碼
其中peerDependencies做爲external外部聲明,經過commonjs把識別cjs模塊,經過nodeResolve作node模塊查找,而後typescript作ts編譯,經過replace作全局變量的設置,生產環境下使用terser來作壓縮。
package.json中註冊scripts
{
"scripts": {
"build:cjs": "tsc -b ./tsconfig.cjs.json",
"build:es": "tsc -b ./tsconfig.esm.json",
"build:test": "tsc -b ./tsconfig.test.json",
"build:types": "tsc -b ./tsconfig.types.json",
"build:umd": "cross-env NODE_ENV=development rollup -c -o dist/umd/flight-common-utils.js",
"build:umd:min": "cross-env NODE_ENV=production rollup -c -o dist/umd/flight-common-utils.min.js",
"build": "npm run clean && npm-run-all build:cjs build:es build:types build:umd build:umd:min",
"clean": "rimraf lib dist es"
}
}
複製代碼
接下來,在package.json中對不一樣的模塊類型的文件作聲明
main是node會查找的字段,是cjs規範的包,module是webpack和rollup會讀取的,是esm規範的包,types是tsc讀取的,包含類型聲明。umd字段只是一個標識。
文檔經過jsdoc生成,能夠根據註釋生成文檔,可是並不支持ts,因此我是經過打包完以後在基於打包結果作jsdoc生成。
而且我但願文檔直接拼接在README.md裏面,因此寫了一個小腳本。
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const binPath = path.resolve(__dirname, '../node_modules/.bin/jsdoc2md')
const srcPath = path.resolve(__dirname, '../dist/esm')
const files = fs.readdirSync(srcPath)
let docStr = ''
files.filter(filename => filename.indexOf('.js') > -1 && filename !== 'index.js').forEach(item => {
const filePath = path.resolve(srcPath, item)
docStr += execSync(`${binPath} ${filePath} `).toString('utf-8')
})
let readmeContent = fs.readFileSync(path.resolve(__dirname, './README.md.template')).toString('UTF-8')
readmeContent += docStr
fs.writeFileSync(path.resolve(__dirname, '../README.md'), readmeContent)
複製代碼
寫了一個模版,而後把生成的jsdoc拼接進去,寫入README.md。 一樣註冊到npm scripts
{
"scripts": {
"generateDocs": "npm run build:es && node ./scripts/generateDoc.js",
}
}
複製代碼
#總結
本文詳細講述了封裝這個包的緣由,以及一些通用函數的實現邏輯,特別是複雜的類型如何去寫。而後介紹了ts-mocha
+ chai
來作測試,rollup
+ typescript
作編譯打包,使用jsdoc
生成文檔。 一個工具函數庫就這麼封裝的。其中typescript的類型聲明算是比較難的部分吧,想寫出類型簡單,把類型寫的準確就不簡單了,特別是工具函數,狀況特別的多。但願你們能有所收穫。
歡迎關注個人公衆號,會持續分享一些源碼類的或者我作的工具。