用 typescript 寫一個工具函數庫

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"技術點介紹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工具函數的複雜類型的聲明(難點)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用 ts-mocha + chai 做單元測試"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用 ts + rollup 打不同模塊規範的包"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先看一段代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"const {name = 'xxx', age} = { name: null, age: 18}console.log(name);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"name 輸出的是 null,因爲解構賦值的默認值只有當值爲 undefined 時纔會生效,這點如果不注意就會引起 bug。我們組內最近就遇到了因爲這點而引起的一個 bug,服務端返回的數據,因爲使用瞭解構賦值的默認值,結果因爲值爲 null 沒有被賦值,而導致了問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼如何能避免這種問題呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們最終的方案有兩種,第一種服務端返回數據之後遞歸的設置默認值,之後就不需要再做判斷,直接處理就行。第二種是當取屬性的時候去做判斷,如果爲 null 或 undefined 就設置默認值。爲了支持這兩種方案,我們封裝了一個工具函數包 @qnpm\/flight-common-utils。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個工具包首先要包含 setDefaults、getProperty 這兩個函數,第一個是遞歸設置默認值的,第二個是取屬性並設置默認值的。除此之外還可以包含一些別的工具函數,把一些通用邏輯封裝進來以跨項目複用。比如判空 isEmpty,遞歸判斷對象和屬性是否相等 isEqual 等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲用了 typescript,通用函數考慮的情況很多,爲了更精準的類型提示,類型的邏輯寫的很複雜,比實現邏輯的代碼都多。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"使用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"npm install @qnpm\/flight-common-utils --save --registry=公司npm倉庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或者"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"yarn add @qnpm\/flight-common-utils --registry=公司npm倉庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現工具函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏只介紹類型較爲複雜的 setDefaults、getProperty。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"setDefaults"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個函數的參數是一個待處理對象,若干個默認對象,最後一個參數可以傳入一個函數自定義處理邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個函數的參數是一個待處理對象,若干個默認對象,最後一個參數可以傳入一個函數自定義處理邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function setDefaults(obj, ...defaultObjs) {}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"希望使用時這樣調用:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"setDefaults({a: {b: 2}}, {a: {c: 3}} );\/\/ {a: {b: 2, c: 3}}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的類型的特點是函數返回值是原對象和一些默認對象的合併,並且參數個數不確定。所以用到了函數類型的重載,加上 any 的兜底。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"type SetDefaultsCustomizer = (objectValue: any, sourceValue: any, key?: string, object?: {}, source?: {}) => any;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SetDefaultsCustomizer 是自定義處理函數的類型,接受兩個需要處理的值,和 key 的名字,還有兩個對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後是 setDefauts 的類型,這裏重載了很多情況的類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function setDefaults(object: TObject): TObject;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果只有一個參數,那麼直接返回這個對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function setDefaults(object: TObject, source: TSource, customizer: SetDefaultsCustomizer): TObject & TSource;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當傳入一個 source 對象時,返回的對象爲兩個對象的合併 TObject & TSource。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function setDefaults(object: TObject, source1: TSource1, source2: TSource2, customizer: SetDefaultsCustomizer): TObject & TSource1 & TSource2;\nfunction setDefaults(object: TObject, source1: TSource1, source2: TSource2, source3: TSource3, customizer: SetDefaultsCustomizer): TObject & TSource1 & TSource2 & TSource3;\nfunction setDefaults(object: TObject,source1: TSource1,source2: TSource2,source3: TSource3,source4: TSource4,customizer: SetDefaultsCustomizer): TObject & TSource1 & TSource2 & TSource3 & TSource4;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲參數數量不固定,所以需要枚舉參數爲 1,2,3,4 的情況,同時加一個 any 的情況來兜底,這樣聲明當用戶寫 4 個和以下參數的時候都是有提示的,但超過 4 個就只能提示 any 了,能覆蓋大多數使用場景。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現這個函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"type AnyObject = Record;\nfunction setDefaults(obj: any, ...defaultObjs: any[]): any { \/\/ 把數組賦值一份 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));}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Record 是內置類型,具體實現是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"type Record = { [P in K]: T; }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,AnyObject 其實就是一個值爲any類型的對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"把參數數組賦值一份後,取出自定義處理函數,通過 reduce 循環設置默認值。assignObjectDeep 實現的是給一個對象遞歸設置默認值的邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"const assignObjectDeep = ( 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;};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類型只限制了必須是一個對象也就是 TObj extends AnyObject,同時 key 必須是這個對象的索引 Key extends keyof TObj。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 for in 遍歷這個對象,如果是數組,那麼就遞歸,否則合併兩個對象,當有 customizer 時,調用該函數處理,否則判斷該對象的值是否爲 null 或 undefined,是則用默認值。(void 0 是 undefeind,== void 0 就是判斷是否爲 null 或 undefeind)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"getProperty"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getProperty 有三個參數,對象,屬性路徑和默認值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function getProperty(object, path, defaultValue){}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"希望使用時這樣調用:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"const object = { 'a': [{ 'b': { 'c': 3 } }] }\ngetProperty(object, 'a[0].b.c')\/\/ => 3\ngetProperty(object, ['a', '0', 'b', 'c'])\/\/ => 3\ngetProperty(object, 'a.b.c', 'default')\/\/ => 'default'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲重載情況較多,類型比較複雜,這是工具類函數的特點。首先聲明幾個用到的類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"type AnyObject = Record;type Many = T | ReadonlyArray;\ntype PropertyName = string | number | symbol;type PropertyPath = Many;\ninterface NumericDictionary { [index: number]: T;}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AnyObject 爲值爲 any 的對象類型。Record 和 ReadonlyArray 是內置類型。PropertyName 爲對象的索引類型,只有三種,string、number、symbol,PropertyPath 是 path 的類型,可以是單個的 name,也可以是他們的數組,所以寫了一個工具類型 Many 來生成這個類型。NumericDictionary 是一個 name類型爲 number,值類型固定的對象,類似數組。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是 object 爲 null 和 undefined 的情況:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function getProperty( object: null | undefined, path: PropertyPath): undefined;\nfunction getProperty( object: null | undefined, path: PropertyPath, defaultValue: TDefault): TDefault;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後是 object 爲數組時的類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"function getProperty( object: NumericDictionary, path: number): T;\nfunction getProperty( object: NumericDictionary | null | undefined, path: number): T | undefined;\nfunction getProperty( object: NumericDictionary | null | undefined, path: number, defaultValue: TDefault): T | TDefault;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來是 object 爲對象的情況,這裏的特點和 setDefaults 一樣,path 可能爲元素任意個的數組,又要聲明他們的順序,這裏只是寫了參數分別爲 1 個 、2 個 、3 個、 4 個的類型,然後加上 any 來兜底。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試使用的 ts-mocha 組織測試用例,使用 chai 做斷言。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getProperty 的測試,測試了 object 爲無效值、對象、數組,還有 path 寫錯的時候的邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"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) })\n 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) })\n 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) })\n it('數組能取到屬性path的值', () => { assert.strictEqual(getProperty(arr, '1'), 2) assert.strictEqual(getProperty(arr, [1]), 2) assert.strictEqual(getProperty(arr, [3, 'obj', 'a', 'b', 'c']), 1) })\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試通過。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bd\/bd586c41eed8bca1b32863961980a508.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"編譯打包"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工具函數包需要打包成 cmd、esm、umd 三種規範的包,同時要支持 typescript,所以要導出聲明文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 typescript 編譯器可以分別編譯成 cmd、esm 版本,也支持導出.d.ts聲明文件,umd 的打包使用 rollup。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/81\/818b928f3b23255dbf6c14695265f94f.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中,tsconfig.json 爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"compilerOptions\": {\n \"noImplicitAny\": true,\n \"removeComments\": true,\n \"preserveConstEnums\": false,\n \"allowSyntheticDefaultImports\": true,\n \"sourceMap\": false,\n \"types\": [\n \"node\",\n \"mocha\"\n ],\n \"lib\": [\n \"es5\"\n ],\n \"downlevelIteration\": true\n \/\/支持set等的迭代\n },\n \"include\": [\n \".\/src\/**\/*.ts\"\n ]\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後 esm 和 cjs 還有 types 都繼承了這個配置文件,重寫了 module 的類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"extends\": \".\/tsconfig.json\",\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"target\": \"es5\",\n \"outDir\": \".\/dist\/cjs\"\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"extends\": \".\/tsconfig.json\",\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"target\": \"es5\",\n \"removeComments\": false,\n \"outDir\": \".\/dist\/esm\"\n },\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,types 的配置要加上 declaration 爲 true,並通過 declarationDir 指定類型文件的輸出目錄。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"extends\": \".\/tsconfig.json\",\n \"compilerOptions\": {\n \"module\": \"es2015\",\n \"removeComments\": false,\n \"declaration\": true,\n \"declarationMap\": false,\n \"declarationDir\": \".\/dist\/types\",\n \"emitDeclarationOnly\": true,\n \"rootDir\": \".\/src\"\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有 rollup 的 ts 配置文件也需要單獨出來,module 類型爲 esm,rollup 會做接下來的處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"extends\": \".\/tsconfig.json\",\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"target\": \"es5\"\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中 peerDependencies 作爲 external 外部聲明,通過 commonjs 識別 cjs 模塊,通過 nodeResolve 做 node 模塊查找,然後 typescript 做 ts 編譯,通過 replace 做全局變量的設置,生產環境下使用 terser 來做壓縮。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"package.json 中註冊 scripts:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"scripts\": {\n \"build:cjs\": \"tsc -b .\/tsconfig.cjs.json\",\n \"build:es\": \"tsc -b .\/tsconfig.esm.json\",\n \"build:test\": \"tsc -b .\/tsconfig.test.json\",\n \"build:types\": \"tsc -b .\/tsconfig.types.json\",\n \"build:umd\": \"cross-env NODE_ENV=development rollup -c -o dist\/umd\/flight-common-utils.js\",\n \"build:umd:min\": \"cross-env NODE_ENV=production rollup -c -o dist\/umd\/flight-common-utils.min.js\",\n \"build\": \"npm run clean && npm-run-all build:cjs build:es build:types build:umd build:umd:min\",\n \"clean\": \"rimraf lib dist es\"\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,在 package.json 中對不同的模塊類型的文件做聲明。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/87\/876e7e007b7a83928bdded66e1164db6.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"main 是 node 會查找的字段,是 cjs 規範的包,module 是 webpack 和 rollup 會讀取的,是 esm 規範的包,types 是 tsc 讀取的,包含類型聲明。umd 字段只是一個標識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文詳細講述了封裝這個包的原因,以及一些通用函數的實現邏輯,特別是複雜的類型如何去寫。然後介紹了 ts-mocha + chai 來做測試,rollup + typescript 做編譯打包。一個工具函數庫就這麼封裝的。其中 typescript 的類型聲明算是比較難的部分吧,想寫出類型簡單,把類型寫的準確就不簡單了,特別是工具函數,情況特別的多。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"希望大家能有所收穫。"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:翟旭光"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/PRCpMc9TPzglrEUo2BiDog"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:用 typescript 寫一個工具函數庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:Qunar技術沙龍 - 微信公衆號 [ID:QunarTL]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章