拆分TypeScript項目的學習收獲:避免緩存問題,peerDependencies,引用本地項目

最近需要將工作中的一個TS包拆出一部分代碼,以便在多個團隊和項目中共享。原以為這會是一項特別簡單的工作,但是也花了兩天才大致拆成功。因此記錄一下,也給有類似需求的同學一點經驗。

所拆項目的大致功能:整個項目的結構大致分為:

  1. 一個基類和多個實現類,我們需要拆出一個實現類到包里,因此基類也得放到這個包里
  2. 一個代碼生成工具,會讀取目錄下的所有配置文件并生成ts代碼,這個工具也得放到包里

我們希望拆完之后的項目滿足這些條件:

  1. 拆出的包(以下稱子包或子項目)可以獨立發布,方便外部用戶使用
  2. 為了快速驗證和減少開發過程中的額外步驟,原項目(以下也可能稱母項目)可以本地引用子項目,而不用每次有修改都先提一個PR發布新版本,再通過升級版本號引用最新的改動

總而言之,就是我們雖然對外發布了這個包,且外部會通過包名+版本號來引用,但是團隊內部項目開發時還是希望能通過本地引用直接引用到最新的改變。下面總結一下如何引用本地包和拆包后的代碼需要注意什么。

小心緩存帶來的編譯、包導入不生效問題

由于接下來需要經常修改子包的配置,所以要特別注意緩存帶來的問題,這樣如果遇到奇怪的問題還能有印象是緩存帶來的問題。

雖然TS老手可能已經知道緩存的坑,但是作為新手還是很容易被緩存導致的問題搞得很迷惑。如果遇到奇怪的問題,比如

  1. 敲了tsc --build卻沒有生成編譯后的文件
  2. 有的ts編譯生成了.d.ts,有的卻沒有,但是.js文件都存在,造成子項目引用時找不到類型
  3. 刪除node_modules,再跑yarn install也不會安裝依賴
  4. 子項目沒有升版本就打包,母項目引用時還是安裝的沒有修改之前的版本

這三個都是我在拆子包的開發過程中遇到的問題,經常讓我百思不得其解,還以為是自己改了什么配置改錯了,但是改回來之后還是不工作。

其中1&2都是由于ts編譯緩存造成的,緩存文件的文件名叫tsconfig.tsbuildinfo,它會記錄最近一次編譯,用于支持ts的增量編譯功能。根據配置的不同(是否開啟 composite=true),可能生成在項目根目錄或者是構建輸出目錄下。如果刪除了整個構建輸出目錄(比如下文我們會配置/lib為輸出目錄)但是沒有清除緩存,那重新跑構建命令,也不會生成構建目錄!因為ts編譯器是通過對比源文件和增量編譯緩存文件的差別來決定是否要重新編譯的,而如果擅自刪除了輸出目錄,緩存文件和輸出文件很就存在不同步的情況,這就是為什么重新構建可能不生效的原因!

3&4的問題是因為我們的項目中使用了yarn,而npm/yarn會有緩存。比如3#就是要刪除yarn.lock/package-lock.json文件后再yarn install

而4#最簡單的辦法是本地引用包時,一旦子包修改,就升一個版本再打包,等要push代碼的時候再改回原來的版本。比如在package.json里增加這些scripts

"scripts": {"build": "tsc --build","clean": "rimraf lib && rimraf tsconfig.tsbuildinfo","rebuild": "yarn clean && yarn build","package": "yarn rebuild && yarn pack","local-pack": "npm version prerelease && yarn package"},

這樣當你執行yarn rebuild或者yarn local-pack時,會自動清理lib目錄和編譯緩存;測試打包時也會自動升一個版本,避免出錯。

引用本地包的方法

在介紹拆包需要怎么改代碼之前,我們先說怎么本地引用包,這是因為如果不知道拆完的包該怎么被本地引用,那根本沒法編譯原來的項目,更別提怎么測試子項目功能是否正常了。

通過查找多種資料(包括問AI),大概有以下幾種引用本地包的方法:

  1. tsconfig.ts中配置包的本地映射:在compilerOptions.paths中加上一行"{packageName}":["{pathToLocalPackage}"],個人感覺這種方法對于測試堪稱完美,子項目的源碼修改會立即反映到別的項目中。它的缺陷是沒法很好地測到子包被發布出去之后,其它項目通過package.json引用時是否能正常工作,這是因為子項目中的代碼都是源碼級引用,隨母項目的編譯而編譯,因此會掩蓋一些問題,比如使用絕對路徑(比如import xx from src/moduleA來導入模塊可能并不會報錯,但是通過package.json引用時就會找不到指定模塊。
  2. 直接在package.json里使用引用本地項目的目錄:使用yarn add或者直接修改package.json,添加"{packageName}":"file:{subPeojectFolderPath}"。這種方式也能實現源碼級引用,但是它有一個很大的缺陷,file引用目錄時,會將整個項目文件夾復制到node_modules內,導致子項目里的node_modules也會被原樣拷貝過去。占用空間不說,它會導致子項目、母項目對同一包的引用出現沖突。AI還提供了一些使用peerDependencies的建議,實際上并沒有用:它并非引用包的不同版本出現了沖突,而是子項目和母項目里的node_modules起了沖突!那可能有人會說,如果我子項目不安裝依賴呢?如果子項目不安裝依賴,那也沒法編譯,也會造成很多問題。所以實際上我最不推薦這種做法。
  3. 使用yarn link:先在子項目下執行yarn link,然后在母項目下執行yarn link "{packageName}"。這種方式和1#很類似,但是子包是通過編譯后代碼來引用的(去決定于子包里package.json如何配置),更能測試出一些引用問題。
  4. 先將子項目本地打包,再通過文件引用打包出的tar ball:先在子項目下執行yarn pack,然后在母項目下執行yarn add"{packageTarBallPath}"。這種方法是最能模擬子包發布之后,被其它項目引用的行為的,因為它可以測試我們的打包配置是否正確,比如后文提到的tsconfig.jsonpackage.json這兩個配置文件里,錯誤的配置會導致包無法被正確引用。
  5. yarn workspace:創建一個workspace,將兩個項目添加到workspace中。兩個項目會共享一個node_modules依賴,又保持獨立性,避免了2#中的問題,很優雅。缺點是可能需要改變項目的目錄結構,需要用一個父目錄來包含多個子項目。

綜上,這些方法中不推薦2#,如果可以接受改變目錄結構則5#看起來是最優雅的辦法。

如果不能改變目錄結構,那追求便捷度首推1#,因為修改可以立刻反映到母項目,IDE可以立刻檢查出有沒有語法錯誤,編譯過程也很流暢,同時開發中也無需跑額外的命令來關聯兩個項目;如果按照與真實引用環境的差別排序,首推4#。剩下的3#有點雞肋,因為它不是通過修改package.json來改變項目的引用關系,而是需要跑兩個額外的命令臨時關聯兩個項目,因此只適合臨時測試。

我在自己的項目中,用的是方法4#。因為我們依賴的庫如果使用方法1#會導致一些奇怪的錯誤,子項目變動也不會很大,因此我們主要考慮保證項目能正確運行。只不過這樣每次如果需要修改并測試子項目,都要重新編譯。實際開發可以考慮1#和4#結合的方法,先用1#保證編譯通過,再用4#保證運行正確。

修改子項目

除了把代碼都拷到另一個獨立的項目之外,還有一些值得注意的點,主要是注意

  1. 配置好package.jsontsconfig.json,確保打包的代碼能被正確引用
  2. 避免絕對路徑代碼,要使用位置無關性代碼,確保子包中代碼運行結果符合預期

tsconfig.json和package.json

我們需要修改tsconfig.json來保證編譯后的代碼會生成在正確的目錄,同時為了保證發布的包可以被正確引用我們需要配置package.json,寫明項目的入口文件。

tsconfg

  • compilerOptions.outDir指明了編譯后的js/ts文件放在哪個目錄下。比如我們配置成lib,那就會在lib文件夾下看到編譯后的代碼
  • compilerOptions.rootDir指明了源碼文件的儲存路徑。編譯后生成的文件會保持rootDir為根目錄的目錄結構。通常我們的源代碼放在src目錄下,如果不設置rootDir時,默認會以項目所在的目錄為根目錄,編譯后的代碼會放在lib/src目錄下。當rootDir設置成了src,那生成的編譯后代碼就會放在lib下而不是lib/src下,使打包出的目錄層級更清晰(否則保持src目錄會讓人很迷惑,一般src用于存放源代碼而不是生成的代碼)

package.json

以下配置需要匹配tsconfig.json的配置,如果不太確定,可以跑tsc編譯看看生成的目錄里對應的文件放在哪了。

  • types:指定了編譯出的.d.ts文件。它是ts的類型聲明文件,通常用于保證ts編譯期類型系統正常工作
  • main:指定了編譯出的.js文件入口。它包含了真正的代碼實現(而編譯后的.d.ts只包含類型聲明,有點類似于接口與實現或者頭文件和源文件的差別)

設置peerDependencies

通常我們在項目有三種和依賴相關的設定:

  • dependencies:項目的依賴,基本上可以認為代碼里要import的包都需要加到這個依賴配置中
  • peerDependencies:指定了當該項目被消費方使用時,所使用的包版本。如果沒有指定,則會使用dependencies中指定的版本(自己在使用yarn時驗證了這個行為)
  • devDependencies:項目開發時需要,但是生產環境不需要的依賴,通常不是代碼里import的包。例子包括一些命令行工具(比如可用于刪除文件的rimraf包,通常用于在build之前清理生成文件),代碼格式化工具等

我相信大家已經非常了解devDependencies是什么,但對peerDependencies可能不夠了解。peerDependencies通常在庫的package.json中被聲明,它有兩方面語義:

  1. 告訴消費這個包的項目,庫需要運行在某些包的特定版本之上
  2. 對于peer里指定的包,庫會“盡量”和主項目共享同一份副本。共享同一份副本意味著二者中的代碼調用peer中的共同依賴時,會指向同一個本地路徑,不會造成包依賴沖突(比如類型不兼容)的問題。

從2#中可以看出peerDependenciesdependencies最大區別:前者會盡量共享同一個包的副本,避免重復安裝和依賴不兼容,這些問題在使用庫時是很常見的問題;而后者則大多數情況下會重復安裝,即使包的名稱和版本都相同。現在只剩一個問題沒有解決,什么叫“盡量”共享同一副本呢?根據包管理器的不同,大致如下的一些行為:

  • 如果peer版本和主項目兼容,則會共享同一個包,這一點在多個包管理工具中都是相同的
  • 如果peer版本和主項目不兼容,則可能有多種行為:
    • 繼續使用主項目的包版本,可能會有包兼容性檢查的警告
    • 如果主項目安裝了滿足peer版本的包,則主項目和庫會運行在不同版本的依賴之上
    • 報錯,依賴安裝失敗

也有一些配置項如peerDependenciesMeta會影響這些行為,但多數情況下都是使用包管理器的默認行為。這會造成一些令人迷惑的結果。比如我實際操作中,有一個依賴A的版本指定的是^0.20250101.1901000這樣的版本,在yarn 22.x版本下,它使用的是讓主項目和庫運行在不同版本依賴的策略,因此即使主項目更新了依賴A的版本,主項目調用子包時,子包使用的還是子包指定的版本。

另外再說一個有趣的知識點,^在版本號中的含義是“第一個非零的版本號一致”。對于常規版本號,比如1.2.3,就意味著1.x.x,而對于0.2.3就意味著0.2.x了。我在項目中使用的依賴是以日期作為第一個非零主版本號,因此就會導致即使只把依賴更新到第二天的版本,yarn也會認為版本是不兼容的,轉而讓主項目和子包使用不一樣的依賴版本,造成了這個隱蔽的問題。

測試

由于這兩個配置文件主要影響子包中的代碼能否被正確引用,所以通過在母項目中跑tsc --build編譯就能看出配置是否正確。

本地模塊import改為相對路徑引用

本地模塊導入指的是通過類似import {xx} from "../classA"這樣的代碼來導入本地某個模塊。如果我們的包不需要給別人用,那我們完全可以寫一個絕對路徑,比如import {xx} from "src/folderA/classA"。但是另一個項目B引用時如果遇到絕對路徑的導入,一般會以項目B的根目錄作為根目錄來查找這個模塊,而子包里寫的這句import則是以子包的根目錄作為根目錄,自然就會導致找不到模塊的錯誤。

可以搜索一下子項目里有沒有用到"src這樣的關鍵字,用到的話記得改掉。

使用外部傳入的路徑來計算外部文件的真實路徑

假如子包中有一些讀取外部文件(非子包文件)的代碼,比如讀取某個目錄下所有的配置文件,讀取某個TS模塊等,我們都要甄別這些路徑,把讀取的路徑改為真實路徑。比如曾經我們的代碼在拆之前,可能相對于讀取的目錄有固定的相對路徑比如__dirname/../anotherFolder/anotherModule,拆分后得讓代碼調用方把真實路徑傳進來,而不能還用之前基于當前模塊路徑計算的方法。從代碼層面看,這些方法需要傳的參數變多了。

可以搜索一下子項目里有沒有用到__dirname__filename這樣的關鍵字,用到的話記得改掉。

發布

確認好發布的包可以正常工作后,發布基本上是最簡單的步驟了。通常工作中會使用獨立的npm倉庫,可以用如下命令發布某個目錄下的包,或者打包好的tarball到指定倉庫

yarn publish [<tarball>|<folder>] --registry <url>

常見問題

打出的包被別人引用,看到node_modules下也成功安裝了庫文件,但是用包名引用提示“找不到模塊”

package.jsonmaintypes指定錯了,要確保目錄層級和包里package.json與指定文件的相對路徑一致

主項目運行時,提示子包內的模塊找不到(通常是require語句報錯)

通常是因為子包使用了絕對路徑而非相對路徑

主項目更新了依賴,主項目中嘗試直接調用依賴,用的是新版本依賴,但是子項目調用的還是舊版本的依賴

沒有設置好peerDependencies,詳見文章對應章節

修改了子項目的代碼,打包后安裝到主項目,發現主項目里并沒有安裝更新的包

通常是緩存問題,檢查如下方面:

  1. 最好確保每次用npm version命令升一個版本再打包安裝
  2. 生成目錄和tgz包最好每次刪掉,否則通常它們都只會增量更新

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/95769.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/95769.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/95769.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

無人機各種接頭焊接方法

無人機接頭的焊接直接關系到設備可靠性和飛行安全&#xff0c;以下是常見接頭的焊接方法及注意事項&#xff1a;一、焊接通用原則工具準備恒溫焊臺&#xff08;推薦溫度&#xff1a;$350 \pm 20^{\circ}\text{C}$&#xff09;含松芯焊錫絲&#xff08;直徑0.8mm&#xff09;助焊…

[Linux] Linux標準塊設備驅動詳解:從原理到實現

Linux標準塊設備驅動詳解&#xff1a;從原理到實現 在Linux系統中&#xff0c;塊設備是存儲系統的核心組成部分&#xff0c;涵蓋了硬盤、固態硬盤&#xff08;SSD&#xff09;、U盤、SD卡等各類持久化存儲介質。與字符設備不同&#xff0c;塊設備以固定大小的“塊”為單位進行數…

什么是壓力測試,有哪些方法

壓力測試&#xff08;Stress Testing&#xff09;是性能測試的一種&#xff0c;旨在評估系統在極端負載條件下的表現&#xff0c;驗證其穩定性、可靠性和容錯能力。通過模擬超出正常范圍的并發用戶、數據量或請求頻率&#xff0c;發現系統在高負載下的瓶頸&#xff08;如內存泄…

lua腳本在redis中執行是否是原子性?

lua腳本在redis中執行是否是原子性&#xff1f;以及是否會阻塞其他腳本的執行【客戶端的請求】&#xff1f;先解答第二個問題:是的&#xff0c;保持原子執行。這也是redis中支持lua腳本執行的原因。Lua 腳本在 Redis 中是以原子方式執行的&#xff0c;在 Redis 服務器執行EVAL命…

DeepSeek文獻太多太雜?一招制勝:學術論文檢索的“核心公式”與提問藝術

如果我們想要完成一次學術論文檢索&#xff0c;那我們可以把它想象成一次精準的“學術尋寶”。你不是在漫無目的地閑逛&#xff0c;而是一名裝備精良的“學術尋寶獵人”&#xff0c;你的目標是找到深藏在浩瀚文獻海洋中的“珍寶”&#xff08;高價值論文&#xff09;。1 你的尋…

Linux內存管理章節一:深入淺出Linux內存管理:從物理內存到ARM32的用戶與內核空間

引言 如果說操作系統是計算機的心臟&#xff0c;那么內存管理就是它的靈魂脈絡。它默默地工作在Linux內核的最底層&#xff0c;卻決定著整個系統的穩定性、安全性和性能。今天&#xff0c;我們將撥開迷霧&#xff0c;深入探索Linux內存管理的核心概念&#xff0c;并結合熟悉的A…

ECMAScript (5)ES6前端開發核心:國際化與格式化、內存管理與性能

好的&#xff0c;我將根據【國際化與格式化】和【內存管理與性能】這兩個主題&#xff0c;為你生成詳細的課件內容&#xff0c;涵蓋概念、應用和實例。 &#x1f4d7; 前端開發核心&#xff1a;國際化與格式化、內存管理與性能 1. 國際化與格式化 (Internationalization & …

3D 可視化數字孿生運維管理平臺:構建 “虛實協同” 的智慧運維新范式

3D 可視化數字孿生運維管理平臺通過 “物理空間數字化建模 實時數據動態映射 智能分析決策”&#xff0c;將建筑、園區、工業設施等物理實體 1:1 復刻為虛擬孿生體&#xff0c;打破傳統運維 “信息割裂、依賴經驗、響應滯后” 的痛點&#xff0c;實現從 “被動搶修” 到 “主…

DP-觀察者模式代碼詳解

觀察者模式&#xff1a; 定義一系列對象之間的一對多關系&#xff1b;當一個對象改變狀態&#xff0c;它的依賴都會被通知。 主要由主題&#xff08;Subject&#xff09;和觀察者&#xff08;Observer&#xff09;組成。 代碼實現 package com.designpatterns.observer;/*** 定…

1983:ARPANET向互聯網的轉變

一、ARPANET早期1969年誕生的ARPANET最初還算不上互聯網&#xff0c;不過在ARPANET構建之初就已經考慮了分組交換&#xff1a;1970年代的ARPANET:其實這個時候我就有疑問&#xff0c;TCP/IP是1983年1月1日更新到ARPANET的&#xff0c;但是1970年代的ARPANET已經連接全美的重要單…

自動化運維-ansible中的變量運用

自動化運維-ansible中的變量運用 一、變量命名規則 組成&#xff1a;字母、數字、下劃線。必須以字母開頭。 合法: app_port, web_1, varA非法: 2_var (以數字開頭), my-var (包含其他字符), _private (以下劃線開頭) 避免使用內置關鍵字&#xff1a;例如 hosts, tasks, name…

深入學習并發編程中的volatile

volatile 的作用 保證變量的內存可見性禁止指令重排序1.保證此變量對所有的線程的可見性&#xff0c;當一個線程修改了這個變量的值&#xff0c;volatile 保證了新值能立即同步到主內存&#xff0c;其它線程每次使用前立即從主內存刷新。 但普通變量做不到這點&#xff0c;普通…

使用Java獲取本地PDF文件并解析數據

獲取本地文件夾下的PDF文件要獲取本地文件夾下的PDF文件&#xff0c;可以使用Java的File類和FilenameFilter接口。以下是一個示例代碼片段&#xff1a;import java.io.File; import java.io.FilenameFilter;public class PDFFileFinder {public static void main(String[] args…

吳恩達機器學習補充:決策樹和隨機森林

數據集&#xff1a;通過網盤分享的文件&#xff1a;sonar-all-data.csv 鏈接: https://pan.baidu.com/s/1D3vbcnd6j424iAwssYzDeQ?pwd12gr 提取碼: 12gr 學習來源&#xff1a;https://github.com/cabin-w/MLBeginnerHub 文末有完整代碼&#xff0c;由于這里的代碼和之前的按…

Shell腳本一鍵監控平臺到期時間并釘釘告警推送指定人

1. 監控需求客戶側有很多平臺需要定期授權&#xff0c;授權后管理后臺才可正常登錄&#xff0c;為避免授權到期&#xff0c;現撰寫腳本自動化監控平臺授權到期時間&#xff0c;在到期前15天釘釘或其他媒介提醒。2. 監控方案2.1 收集平臺信息梳理需要監控的平臺地址信息&#xf…

華為HCIE數通含金量所剩無幾?考試難度加大?

最近網上很火的一個梗——法拉利老了還是法拉利&#xff0c;這句話套在華為HCIE數通身上同樣適用&#xff0c;華為認證中的華為數通和云計算兩大巨頭充斥著大家的視野里面&#xff0c;也更加廣為人知&#xff0c;但隨著時代的發展&#xff0c;華為認證體系的調整&#xff0c;大…

#數據結構----2.1線性表

在數據結構的學習中&#xff0c;線性表是最基礎、最核心的結構之一 —— 它是后續棧、隊列、鏈表等復雜結構的 “基石”。今天從 “是什么”&#xff08;定義&#xff09;到 “怎么用”&#xff08;基本操作&#xff09;&#xff0c;徹底搞懂線性表的核心邏輯。 一、先明確&…

2508C++,skia動畫

gif動畫原理 先了解一下gif動畫的原理: gif動畫由一系列靜態圖像(或叫幀)組成.這些圖像按特定的順序排列,每一幀都代表動畫中的一個瞬間,幀圖像是支持透明的. 每兩幀之間有指定的時間間隔(一般小于60毫秒),gif播放器每渲染一幀靜態圖像后,即等待此時間間隔,依此邏輯不斷循環渲染…

AI + 機器人:當大語言模型賦予機械 “思考能力”,未來工廠將迎來怎樣變革?

一、引言1.1 未來工廠變革背景與趨勢在科技飛速發展的當下&#xff0c;全球制造業正站在變革的十字路口。隨著消費者需求日益多樣化、市場競爭愈發激烈&#xff0c;傳統工廠模式的弊端逐漸顯現。生產效率低下、難以適應個性化定制需求、設備維護成本高昂且缺乏前瞻性等問題&…

pinia狀態管理的作用和意義

1. 什么是狀態管理 狀態管理就是統一管理應用中的數據&#xff0c;讓數據在多個組件之間共享和同步。 // 沒有狀態管理 - 數據分散在各個組件中 // 組件A const user ref({ name: 張三, age: 25 })// 組件B const user ref({ name: 張三, age: 25 }) // 重復定義// 組件C c…