<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          好家伙,這是從零寫了一個(gè)“l(fā)erna”

          共 12580字,需瀏覽 26分鐘

           ·

          2022-06-10 22:44

          點(diǎn)擊上方 前端Q,關(guān)注公眾號(hào)

          回復(fù)加群,加入前端Q技術(shù)交流群

          背景

          相信大家多多少少都接觸過(guò)或聽過(guò) monorepo 這個(gè)東西,為了方便多個(gè)項(xiàng)目進(jìn)行調(diào)試、復(fù)用和管理,采取一種單git倉(cāng)庫(kù)管理多個(gè)項(xiàng)目方式,市面上流行的庫(kù)vue、react、bable等都采用 monorepo 方式進(jìn)行管理。帶來(lái)便利的同時(shí),也帶來(lái)了很多挑戰(zhàn)。

          我們應(yīng)該以何種方式統(tǒng)一管理這些包呢?包版本升級(jí)的時(shí)候、發(fā)布的時(shí)候、依賴冗余這些問(wèn)題都擺在我們面前。市面上也有很多成熟的解決方案,我司則是選擇了 yarn + lerna 做 monorepo 管理。

          pnpm、yarn、npm

          node_modules 一直是前端人的頭痛,一個(gè)依賴黑洞魔物。為了解決依賴黑洞問(wèn)題,yarn 和 npm采取平鋪方式,去復(fù)用部分依賴,但其中還是存在著不少問(wèn)題:依賴非法訪問(wèn)、依賴分身等問(wèn)題,面對(duì)多包管理的場(chǎng)景,這種問(wèn)題尤為突出。

          這時(shí)候,pnpm 英雄登場(chǎng),pnpm 下載的文件會(huì)統(tǒng)一放到存儲(chǔ)中心,項(xiàng)目里面node_modules 會(huì)硬鏈接到存儲(chǔ)中心,以一種特殊快捷方式存在,而不是直接在磁盤上生成全新的副本,節(jié)省空間。還會(huì)通過(guò)軟鏈鏈接依賴的依賴,有效解決依賴分身這個(gè)問(wèn)題。而且硬鏈接還有一個(gè)特性,當(dāng)存儲(chǔ)中心建立起硬鏈接的文件鏈接數(shù)為0時(shí),還會(huì)自動(dòng)清除該文件,及時(shí)回收磁盤空間,跟WeakSet這些弱引用效果一樣。

          定位

          上面說(shuō)到了包管理器,大家也看到了 pnpm 的優(yōu)勢(shì),所以我這邊決定給它來(lái)個(gè)大革新,yarn 換成 pnpm,而 lerna 已經(jīng)不維護(hù)了,而且我個(gè)人更喜歡包管理器就做包管理器的事,monorepo 就做 monorepo 該做的事,大部分功能重合,這并不是什么好事,容易帶來(lái)混亂,同一件事,有的人用a去做,有的人用b去做,也許可以制定規(guī)則去規(guī)范大家,但是從一開始就不存在這些事情的話,不是更好嗎

          開展

          上面給出的定位是只做本分,所以現(xiàn)在要實(shí)現(xiàn)的功能有兩個(gè):versionpublish。但是在實(shí)現(xiàn)這部分功能之前,我們要分析這個(gè)項(xiàng)目的包結(jié)構(gòu),為后續(xù)功能做一個(gè)支撐

          分析圖譜

          假定一個(gè)項(xiàng)目的結(jié)構(gòu)如下????,接下來(lái)要為項(xiàng)目生成一個(gè)分析圖譜對(duì)象

          // 假設(shè)下面的包是有依賴關(guān)系,并且b依賴a,c依賴b跟a
          - packages
            - a 
              - package.json
            - b
              - package.json
            - c
              - package.json
          - package.json
          復(fù)制代碼

          通過(guò)globby把里面所有的packages路徑拿到手,然后調(diào)用fs-extra讀取packages.json,將這些信息轉(zhuǎn)換成下面的 contextAnalysisDiagram

          // 每個(gè)包的具體格式
          interface AnalysisBlockObject {
            packageJson: IPackageJson // package.json
            filePath: string // pacakge.json文件路徑
            dir: string // 包的路徑
            relyMyDir: string[] // 依賴該包的包路徑
            myRelyDir: string[] // 該包依賴的包路徑
          }

          // 項(xiàng)目整體格式
          type ContextAnalysisDiagram = Record<string, AnalysisBlockObject>

          // 例子:b包的AnalysisBlockObject
          {
            packageJson: {
              "name""@test/b"
              "version""0.0.0"
              "dependencies": {
                "@test/a""workspace:~0.0.0"
              }
            },
            filePath: "packages/b/package.json",
            dir: "packages/b",
            relyMyDir: [ “packages/c” ] // c依賴了b
            myRelyDir: [ “packages/a” ] // b依賴了a
          }
          // 然后ContextAnalysisDiagram是以dir作為key,去對(duì)應(yīng)每個(gè)包的AnalysisBlockObject
          contextAnalysisDiagram = {
            "": 根目錄的AnalysisBlockObject,
            "packages/a": a包的AnalysisBlockObject,
            "packages/b": b包的AnalysisBlockObject,
            "packages/c": c包的AnalysisBlockObject,
          }
          復(fù)制代碼

          其中轉(zhuǎn)換比較特別點(diǎn)的是 relyMyDir 和 myRelyDir

          // 讀取項(xiàng)目每個(gè)包的package.json以下屬性,把包依賴關(guān)系都提取出來(lái)
          const RELY_KEYS = [
            'bundleDependencies',
            'bundledDependencies',
            'optionalDependencies',
            'peerDependencies',
            'devDependencies',
            'dependencies',
          ]
          復(fù)制代碼

          正如上面所說(shuō), relyMyDir 是用來(lái)保存所有依賴該包的包路徑數(shù)組,麻煩的是,它不像 myRelyDir 可以在當(dāng)前package.json里面全部找出來(lái)。

          常規(guī)的方法是對(duì)包循環(huán)的時(shí)候,再在里面加一個(gè)循環(huán)來(lái)找當(dāng)前包的依賴,但是這種做法會(huì)使時(shí)間復(fù)雜度達(dá)到O(n ^ 2)。所以我這邊做了一個(gè)優(yōu)化,通過(guò)一個(gè)對(duì)象數(shù)組,以每個(gè)包的name作為key,對(duì)應(yīng)的值為數(shù)組,在每次循環(huán)的時(shí)候都對(duì)當(dāng)前包的依賴搜索一遍,發(fā)現(xiàn)匹配到包name,就將當(dāng)前dir push進(jìn)去,這樣就實(shí)現(xiàn)了一次循環(huán),把所有依賴都保存起來(lái)

          // 對(duì)象數(shù)組
          const relyMyMap = {
            "root": [],
            "@test/a": [],
            "@test/b": [],
            "@test/c": [],
          }
          // dirs:包的路徑數(shù)組。
          dirs.forEach((dir, index) => {
            **偽代碼**
            // 用來(lái)獲取當(dāng)前dir的packageJson依賴,把命中的依賴放到relyMyMap對(duì)應(yīng)的數(shù)組
            setRelyMyDirhMap(dir, packageJson, relyMyMap)
            **偽代碼**
            this.contextAnalysisDiagram[dir] = {
              packageJson,
              dir,
              filePath: filesPath[index],
              // 賦值到relyMyDir,因?yàn)槭且妙愋停罄m(xù)命中的依賴都會(huì)出現(xiàn)在對(duì)應(yīng)的relyMyDir
              relyMyDir: relyMyMap[packageJson.name],
              myRelyDir,
            }
          })
          復(fù)制代碼

          模式

          lerna 有固定模式獨(dú)立模式,而我這邊對(duì)應(yīng)的是sync 模式diff模式,大同小異。

          命令

          // lerna的使用方法
          lerna version
          lerna publish
          // 我為當(dāng)前工具起了個(gè)pkgs命令名
          pkgs version
          pkgs publish
          復(fù)制代碼

          如何定義一個(gè)命令行工具呢

          1. 創(chuàng)建一個(gè)bin目錄,在里面創(chuàng)建一個(gè)index.js。第一句的意思是告訴系統(tǒng)用node去執(zhí)行這個(gè)文件
          #!/usr/bin/env node
          console.log('abmao')
          復(fù)制代碼
          1. 在package.json定義bin
          // package.json
          {
            "bin": {
              "pkgs""bin/index.js" // 指向bin目錄
            },
          }
          復(fù)制代碼

          屬于你的命令實(shí)現(xiàn)了

          image.png

          我先攤牌,下面是已經(jīng)實(shí)現(xiàn)了的命令函數(shù),使用commander處理復(fù)雜命令和友好提示

          #!/usr/bin/env node
          const { program } = require('commander')
          const {
            executeCommand,
            executeCommandTag,
            executeCommandInit,
            executeCommandRun,
          } = require('../dist/pkgs.cjs.min'// 打包好的構(gòu)建物
          const pkg = require('../package.json')
          function handleExecuteCommand (type, cmd{
            executeCommand(type, {
              [type]: cmd,
            })
          }
          program
            .version(pkg.version)
            .description('Simple monorepo combined with pnpm')
          program
            .command('version')
            .description('version package')
            .option('--mode <type>''sync | diff')
            .option('-m, --message <message>''commit message')
            .action(cmd => {
              // pkgs version
              handleExecuteCommand('version', cmd)
            })
          program
            .command('publish')
            .description('publish package')
            .option('--mode <type>''sync | diff')
            .option('--tag <type>''npm publish --tag <type>')
            .option('-m, --message <message>''commit message')
            .action(cmd => {
              // pkgs publish
              handleExecuteCommand('publish', cmd)
            })

          program
            .command('tag')
            .description('pkgs tag, diff mode: Compare according to tag')
            .option('-p''publish tag')
            .option('-v''version tag')
            .action(cmd => {
              // pkgs tag
              executeCommandTag(cmd)
            })

          program
            .command('init')
            .description('create pkgs file')
            .action(() => {
              // pkgs init
              executeCommandInit()
            })
          program
            .command('run <cmd> [mode]')
            .description('run diff scripts.\n mode: work | stage | repository, default: work')
            .option('-r <boolean>''Include rootPackage''true')
            .action((cmd, mode, option) => {
              // pkgs run <cmd>
              executeCommandRun(cmd, mode, option.r !== 'false')
            })
          program.parse(process.argv)

          復(fù)制代碼

          version 命令

          version 命令是用來(lái)升級(jí)各個(gè)包的版本。一開始我是用了antfu大佬的bumpp來(lái)做命令行交互,但是這個(gè)庫(kù)每次選擇完版本都會(huì)寫進(jìn)package.json文件,我更想要的是在內(nèi)存里面獲取到newVersion,然后等所有包都選擇完版本后,再一次過(guò)寫進(jìn)去。因此我提交pr的同時(shí)也publish了一個(gè)新庫(kù),今天看了下,pr還沒(méi)人理,估計(jì)沒(méi)怎么關(guān)注這個(gè)庫(kù)了

          image.png

          sync 模式

          sync模式是提供給組織性質(zhì)比較強(qiáng)的項(xiàng)目使用,同時(shí)迭代,同時(shí)發(fā)布。調(diào)用命令的時(shí)候,只會(huì)選擇一次版本,然后同步所有包的package.json,最后git commit并打上git tab

          pkgs version
          // 默認(rèn)是sync,可以省略后面這段
          pkgs version --mode sync
          復(fù)制代碼

          diff 模式

          diff模式只會(huì)對(duì)修改過(guò)和被修改影響到的包觸發(fā),對(duì)每個(gè)需要版本升級(jí)的包都單獨(dú)選擇一次版本,然后分別寫到對(duì)應(yīng)的package.json

          pkgs version --mode diff
          復(fù)制代碼

          問(wèn):那么它是如何找到上述包進(jìn)行版本升級(jí)呢?

          答:首先它會(huì)根據(jù)git提交的最新記錄和最近的一個(gè)gitTab,拿到之間修改過(guò)的文件,再通過(guò)bump當(dāng)前包獲取到newVersion,再與contextAnalysisDiagram分析relyMyDir里面的依賴關(guān)系,根據(jù)版本語(yǔ)義化*^~等進(jìn)行判斷是否要升級(jí)該依賴,是的話,會(huì)調(diào)用bump,如此類推。當(dāng)然,還要把已經(jīng)升級(jí)過(guò)的包儲(chǔ)存到一個(gè)Set里面,防止多次調(diào)用bump。最后git commit并打上git tab

          publish 命令

          對(duì)于需要發(fā)布到npm上的包,提供了publish命令,用于包發(fā)布

          pkgs publish
          // 添加npm標(biāo)簽
          pkgs publish --tag beta
          復(fù)制代碼
          • 對(duì)@開頭的組織包,還會(huì)在末尾自動(dòng)添加上--access public
          • 如果是0.0.8-beta.1這種帶標(biāo)簽的版本號(hào),會(huì)自動(dòng)為其添加標(biāo)簽。當(dāng)然你可以通過(guò)--tag手動(dòng)添加

          同時(shí)publish命令也是有sync模式和diff模式,sync模式對(duì)于publish來(lái)說(shuō)比較雞肋,所以一般都是diff,或者是配合sync的version一起使用。而publish的diff沒(méi)有version那么復(fù)雜,只需要拿到最新commit和第一個(gè)gittag就可以,并不需要計(jì)算影響這一步。

          插曲

          了解過(guò)lerna的小伙伴可能意識(shí)到,這不就是lerna的實(shí)現(xiàn)原理嗎?雀實(shí),我有個(gè)壞習(xí)慣,對(duì)于不熟悉的領(lǐng)域我都喜歡擼了再說(shuō),我比較喜歡那種兩倍痛苦帶來(lái)的兩倍快樂(lè),我事先是閉門造車的思考了這套方案,等我實(shí)現(xiàn)了的時(shí)候,發(fā)現(xiàn)跟 lerna 一模一樣,是壞消息的同時(shí)也是好消息。其實(shí)一不一樣我都是可以接受,不一樣,可以得到兩套思想的碰撞??,你可以非常非常地深入到兩種思想中,探討其中的優(yōu)劣,如果能得出更好的方案,更是一種思考上的升華。一樣的話,是不是可以證明,已經(jīng)達(dá)到了一定的水平了呢?(自吹自擂ing

          tag 命令

          問(wèn):為什么會(huì)有 tag 這個(gè)命令呢?

          答:上述的 version 和 publish 命令的 diff 模式都是基于git tag打上跟 pkgs 定義好的tag實(shí)現(xiàn)的,為了讓 lerna 之類的項(xiàng)目無(wú)痛遷移到 pkgs,保證正確的 diff,放出了 tag 命令,先對(duì)項(xiàng)目使用 tag 命令,就可以保證后面正確的 diff

          git tag // 打上publish標(biāo)簽和version標(biāo)簽
          git tag -p // 打上pbulish標(biāo)簽
          git tag -v // 打上version標(biāo)簽
          復(fù)制代碼

          init 命令

          創(chuàng)建pkgs相關(guān)模板,如下。當(dāng)然,創(chuàng)建的時(shí)候先檢察當(dāng)前文件目錄是否已經(jīng)存在,存在會(huì)跳過(guò)當(dāng)前文件目錄的創(chuàng)建

          pkgs init
          復(fù)制代碼
          - packages
          - package.json
          - pkgs.json
          復(fù)制代碼

          自定義命令

          一個(gè)經(jīng)常有的場(chǎng)景,修改了包后,其他很多包依賴著這個(gè)包,運(yùn)行測(cè)試的時(shí)候有兩種

          1. 全部運(yùn)行npm run test
          2. 指定某些包npm run test

          這兩種方法都不好,浪費(fèi)、繁雜、并且人工指定容易出錯(cuò)。

          如果有一個(gè)功能,繼承了上面說(shuō)的 diff 功能,并且可以對(duì)工作區(qū)、暫存區(qū)和版本區(qū)進(jìn)行分析,只對(duì)修改過(guò)和被影響的包運(yùn)行npm命令,這聽起來(lái)是不是很棒(??????)??

          有沒(méi)有一種可能,pkgs 已經(jīng)實(shí)現(xiàn)了。是的,已經(jīng)提供了該功能啦~??

          // 可以看成是monorepo版本的`npm run`
          pkgs run <cmd>
          // 測(cè)試
          pkgs run test
          // 默認(rèn)是工作區(qū),可以忽略wokr
          pkgs run test work
          // 暫存區(qū)
          pkgs run test stage
          // 版本區(qū)
          pkgs run test repository
          復(fù)制代碼

          優(yōu)先排序

          還有一種場(chǎng)景,a包依賴b包,那么build的時(shí)候,正確的順序是等b包完成構(gòu)建才到a包構(gòu)建,為了實(shí)現(xiàn)這個(gè)功能,要對(duì)contextAnalysisDiagrammyRelyDir進(jìn)行分析:

          1. 找到當(dāng)前包依賴的包,然后將當(dāng)前包塞進(jìn)去stack,同時(shí)還要?jiǎng)?chuàng)建一個(gè)result數(shù)組,用來(lái)保存包的順序
          2. 對(duì)依賴包循環(huán),找到該依賴包是否也有依賴,如此類推,有依賴,則繼續(xù)遞歸
          3. 那么如何才停止遞歸呢?只需要判斷該依賴是否在stack或者result里面,如果是在stack里面,則彈出一個(gè),放到result里面,如果result已經(jīng)保存有,則忽略。同時(shí)會(huì)跳過(guò)對(duì)該依賴的搜索。

          這樣就拿到所有包的運(yùn)行順序,解決了上述場(chǎng)景的問(wèn)題

          配置

          為了減少命令行輸入的參數(shù),一般都會(huì)有一個(gè)對(duì)應(yīng)的配置文件,把參數(shù)都預(yù)先都寫上去

          pkgs.json

          放到根目錄即可

          {
            rootPackagetrue// 自定義命令是否包括根目錄
            mode'sync'// sync | diff 模式
            version: {
              modeundefined// sync | diff 模式,更高的優(yōu)先級(jí)
              message'chore: version'// version命令后的commit message
            },
            publish: {
              modeundefined// sync | diff 模式,更高的優(yōu)先級(jí)
              tag''// npm包標(biāo)簽
            },
          }
          復(fù)制代碼

          pnpm-workspace.yaml

          pkgs會(huì)讀取pnpm-workspace.yaml文件的工作區(qū),默認(rèn)是packages/**

          構(gòu)建

          使用rollup進(jìn)行打包,因?yàn)橛玫氖莟s,所以一開始是使用了esno進(jìn)行運(yùn)行

          依賴循環(huán)

          在構(gòu)建過(guò)程中發(fā)現(xiàn)很多cjs的老牌包存在依賴循環(huán)的包,因?yàn)樯婕暗膱?chǎng)景太多了,提pr改的話是不會(huì)采納的,我是直接fork出來(lái),調(diào)整重新發(fā)包

          esbuild

          用了esbuild是真的快,而且還可以運(yùn)行ts。上手成本非常低,但是效果杠杠的

          測(cè)試

          其實(shí)一開始測(cè)試這種類型我是懵逼的,如何測(cè)試git?如何測(cè)試命令行?在我觀摩了多個(gè)相關(guān)開源項(xiàng)目的測(cè)試后總結(jié)的一種方式:

          • 測(cè)試git:為了保證不污染環(huán)境,使用node的臨時(shí)目錄,在其創(chuàng)建項(xiàng)目模板,使用process.chdir切換工作目錄到臨時(shí)目錄上,就ok了
          • 測(cè)試命令行:總不能測(cè)試的時(shí)候,要命令行交互選擇版本號(hào)吧,給npm那邊publish吧。查看了bumpp測(cè)試代碼,發(fā)現(xiàn)可以直接輸入版本號(hào),繞過(guò)命令行交互形式。publish的話,我則是直接修改源碼,判斷當(dāng)前環(huán)境是否測(cè)試,是的話,就不走命令,而是返回命令的字符串,我再進(jìn)行對(duì)比。

          測(cè)試工具

          一開始用的是老牌測(cè)試框架jest,但是對(duì)ts的支持還是試驗(yàn)性,用起來(lái)非常的麻煩。正好最近關(guān)注vitest,就直接拿來(lái)用,因?yàn)檎Z(yǔ)法一樣,切換成本也是非常的低,而且開箱就支持了ts,妙啊

          除了那些不必要的分支,基本都覆蓋到所有功能了。

          發(fā)布

          pkgs的版本管理和發(fā)布,都是用了本身的versionpublish邏輯去管理,而pkgs并不是一個(gè)多包項(xiàng)目,所以這個(gè)庫(kù)是無(wú)論你是多包還是單包,都可以使用。

          // release.ts
          import { execSync } from 'child_process'
          import colors from 'colors'
          import { executeCommand } from '../index'
          console.log(`${colors.cyan.bold('release: start')} ??`);
          (async function () {
            // 運(yùn)行測(cè)試
            execSync('npm run test', { stdio: 'inherit' })
            // 打包
            execSync('npm run build', { stdio: 'inherit' })
            // 版本選擇,相當(dāng)于pkgs version
            await executeCommand('version')
            // 發(fā)布,相當(dāng)于pkgs publish
            await executeCommand('publish')
          })()
          console.log(`${colors.cyan.bold('release: success')} ??????????`)
          復(fù)制代碼
          // 這樣就可以開始發(fā)布啦
          npm run release
          // 安裝
          npm i -g @abmao/pkgs
          復(fù)制代碼

          TODO

          接下來(lái)記錄下想解決的事,或者說(shuō)大伙有啥想法的不?

          構(gòu)建物過(guò)大

          真的非常大,而且還存在著依賴循環(huán)問(wèn)題,愁啊,有空看下解決,依賴大估計(jì)是fs-extra的問(wèn)題

          狀態(tài)分析

          后面想要實(shí)現(xiàn)一個(gè)state的功能,查看包之間的狀態(tài)

          pkgs state
          復(fù)制代碼

          謝幕

          也許你可以來(lái)個(gè)贊或者評(píng)論或者星,沒(méi)有的話,下次再問(wèn)

          pkgs-github:https://github.com/hengshanMWC/pkgs

          關(guān)于本文

          來(lái)自:科目三后吃飯

          https://juejin.cn/post/7084596115177209886


          往期推薦


          如果面試遇到水貨面試官,怎么辦?
          忍受不了糟糕的工作氛圍,我退出了 Google WebAssembly 團(tuán)隊(duì)
          Web3.0開發(fā)入門

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 106
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天摸日日干 | 神马午夜97 | 狠狠操免费视频 | 久久久久偷拍 | 免费的黄色视频网站在线 |