溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何理解構(gòu)建的抽象

發(fā)布時間:2021-10-28 15:49:45 來源:億速云 閱讀:138 作者:iii 欄目:編程語言

這篇文章主要講解了“如何理解構(gòu)建的抽象”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“如何理解構(gòu)建的抽象”吧!

引子 1:從 Java 的編譯說起

絕大多數(shù)程序員都是從 hello, world! 開始自己復(fù)制、粘貼的人生生涯。對于那些剛上手 Java 的程序員也是類似的:

javac HelloWorld.java

而當(dāng)我們依賴于其它的軟件包時,就需要在編譯時和運行時加入 classpath 來加入依賴項。于是,對應(yīng)的運行命令就如下所示:

java -classpath .:libs/joda-time-2.10.6.jar HelloWorld

這樣,我們就能得到預(yù)期的結(jié)果了:

Hello, World Millisecond time: in.getMillis(): 1599284014762

而如果我們需要打成 jar 包就需要一個復(fù)雜一點的過程:

jar cvfm hello.jar manifest.txt HelloWorld.class libs/*

這個過程中,涉及到幾個關(guān)鍵的要素:

工具鏈。即 java 和 javac,以及對應(yīng)的 Runtime 等。

構(gòu)建過程。即我要先執(zhí)行 javac 進行編譯,再通過 java 命令來啟動應(yīng)用。

依賴管理。即我們的 joda-time-2.10.6.jar 的位置獲取等問題,以及在打包時加入的過程。

源碼配置。即轉(zhuǎn)換過程中的 class 和 java

過程中的輸入和輸出。

引子 2:任務(wù)及任務(wù)的輸入和輸出

對于一個制品的構(gòu)建來說,我們往往會把它拆分為一系列的任務(wù),每個任務(wù)有自己的輸入和輸出。當(dāng)輸入發(fā)生變化的時候,需要變化對應(yīng)的輸出。緊接著,我們只需要對任務(wù)進行編排即可:

exports.build = series( clean, parallel( cssTranspile, series(jsTranspile, jsBundle) ), parallel(cssMinify, jsMinify), publish );

如上展示的是:哪些任務(wù)可以并行,哪些任務(wù)需要按順序執(zhí)行——也可以認(rèn)為是任務(wù)的依賴。

當(dāng)然了,還有一種任務(wù)是 watch 任務(wù),只用于開發(fā)時,而非構(gòu)建時。如下是 Node.js 中的 Gulp 構(gòu)建工具的文件監(jiān)控示例:

function javascript(cb) { // body omitted cb(); }  function scss(cb) { // body omitted cb(); }  watch('src/*.scss', scss); watch('src/*.js', series(javascript));

兩間結(jié)合之下,我們就會看到增量任務(wù)的概念:只針對修改的部分進行編譯,以提升構(gòu)建效率。在這方面做得比較好的就是 Gradle  ,看個官方的示例InputChanges:

abstract class IncrementalReverseTask extends DefaultTask { @Incremental @InputDirectory abstract DirectoryProperty getInputDir()  @OutputDirectory abstract DirectoryProperty getOutputDir()  @TaskAction void execute(InputChanges inputChanges) { inputChanges.getFileChanges(inputDir).each { change -> if (change.fileType == FileType.DIRECTORY) return  def targetFile = outputDir.file(change.normalizedPath).get().asFile if (change.changeType == ChangeType.REMOVED) { targetFile.delete() } else { targetFile.text = change.file.text.reverse() } } } }

同樣的,它也需要我們監(jiān)控對應(yīng)的輸入和輸出。稍有不同的是,Gradle 會對文件進行索引,每次只提供變化的部分,讓我們根據(jù)自己的實際需要進行處理。

增量構(gòu)建相關(guān)資源:

  • tup 是用于 Linux、OSX 和 Windows 的基于文件的構(gòu)建系統(tǒng)。它輸入文件的更改列表和有向無環(huán)圖(DAG),然后處理DAG  以執(zhí)行更新依賴文件所需的適當(dāng)命令。

  • ninja 是一個專注于速度的小型構(gòu)建系統(tǒng),類似于GNU Make。

  • SCons 是一套由Python 語言編寫的開源構(gòu)建系統(tǒng),類似于GNU Make。

引子 3:可選的依賴管理(地獄)

關(guān)于依賴的管理槽點,我已經(jīng)寫過一系列的文章,諸如于:管理依賴的 11 個策略、依賴孿生:低成本的依賴安全方案。

單純從構(gòu)建這件事情上,對于依賴的管理是可有可無的。出現(xiàn)這個狀況的主要原因是:歷史上的編程語言都不考慮這個問題。所以,在古老的 C/C++  語言中,構(gòu)建系統(tǒng)就是一個頭疼的問題。當(dāng)然了,新晉的 Golang 也缺少良好的設(shè)計。

好在,對于依賴管理來說,這個過程并不復(fù)雜:

  1. 包命名和版本機制

  2. 包管理服務(wù)器

  3. 構(gòu)建和運行時的依賴管理

  4. 包沖突處理

  5. ……

構(gòu)建的抽象

好了,有了上面的這一系列基礎(chǔ)知識之后,接下來我們就可以看看不同的構(gòu)建系統(tǒng)里,對于同一概念的抽象,整合了 Bazel、Gradle、Cargo、NPM  等之后有了一個基礎(chǔ)的抽象層次:

  • 工作空間(workspace)。工作空間是一個或者多個軟件包的集成,它們可以共享依賴、輸出目錄配置等等。典型的有 Java 中的 Gradle  settings.gradle、Rust 中的 Cargo 的 Cargo.toml 等。

  • 倉庫。倉庫可以映射到 Git 的 repository 中,代表一個可獨立構(gòu)建的軟件。

  • 包。最小的可執(zhí)行單位的項目結(jié)構(gòu)。

  • 包布局。對應(yīng)于不同的語言、構(gòu)建系統(tǒng)來說,它用于定義代碼的存放位置和結(jié)構(gòu)。

  • 制品。即構(gòu)建產(chǎn)生的產(chǎn)物,可能是可復(fù)用的軟件包,也可能是可運行的應(yīng)用。

  • 任務(wù)。定義構(gòu)建的規(guī)則,并執(zhí)行。

FAQ

為什么是沒有項目?在業(yè)務(wù)領(lǐng)域和技術(shù)領(lǐng)域,我們對于項目的定義存在著一定的歧義性。為了減少二義性,我們使用工作空間 +  倉庫來解決這個問題。工作空間可以視為一個完整的業(yè)務(wù)項目。而倉庫呢,則是單一個的代碼庫,可能是一個庫,也可能是包含庫的完整工程。

現(xiàn)有的最佳方案是 Bazel。

工作區(qū)

工作空間是一個或者多個軟件包的集成,它們可以共享依賴、輸出目錄配置等等。典型的有 Java 中的 Gradle settings.gradle、Rust  中的 Cargo 的 Cargo.toml 等。

我們可以將其視為最終的產(chǎn)物,如 Android 生成的 APK,Rust  最后生成的可執(zhí)行文件。過程中,生成的共享的包都是為了支持這個工程的一部分。

先看 CMakeLists.txt 的目錄,我們在工作區(qū)的根節(jié)點,定義了這個工程,并添加了 projectA 和 projectB。

cmake_minimum_required(VERSION 3.2.2) project(globalProject)  add_subdirectory(projectA) add_subdirectory(projectB)

以用于生成最后的構(gòu)建產(chǎn)物。相似的還有 Rust 中的 workspace:

[workspace]  members = [ "adder", ]

又或者是前端的 Yarn 中的工作區(qū):

{ "private": true, "workspaces": ["workspace-a", "workspace-b"] }

它們做的都是相同的事情。

倉庫

這個概念的再提取是來源于  Bazel。倉庫是一系列包的合集,我們可以將其視為團隊的邊界,從某種意義上可以看作是代碼倉庫。對于一個龐大的工程來說,它的代碼來源是多種多樣的,來自組織內(nèi)的其它團隊,來自組織外的其它團隊。每個獨立的部分,即是一個倉庫。

值得注意的是,從最終產(chǎn)物來看,每個團隊的產(chǎn)出都是倉庫,但是呢,在團隊內(nèi)部,他們就是工作區(qū)。

讓我們看個 Gradle 的多項目構(gòu)建示例(Android 工程):

. ├── README.md ├── library_a ├── app │   ├── build.gradle │   └── src ├── build.gradle ├── local.properties ├── settings.gradle └── third-partys ├── ... ├── build.gradle └── settings.gradle

從目錄結(jié)構(gòu)來看,這個是一個工作區(qū),而在工作區(qū)呢,它包含了一些三方的代碼倉庫(third-partys),以及自身的庫 library_a 和應(yīng)用  app。

因此,在這里的 library_a 和 third-partys 的各個項目都算是倉庫。

包是一系列代碼的合集,它可大可小。最主要的原因在于,因為構(gòu)建時,我們可能會把一個倉庫(哪怕是最小的 Gradle 項目)產(chǎn)出多個包,如 Java 項目中的  src/main 和 src/test。

于是在諸如 bazel 這樣的構(gòu)建工具中,支持自定義的包:

src/my/app/BUILD src/my/app/app.cc src/my/app/data/input.txt src/my/app/tests/BUILD src/my/app/tests/test.cc

對于一個包來說,往往我們還需要定義一系列的相關(guān)信息,如包名、依賴信息、入口等等。如 Bazel 中對于 Java 構(gòu)建的示例:

java_binary( name = "ProjectRunner", srcs = ["src/main/java/com/phodal/ProjectRunner.java"], main_class = "com.phodal.ProjectRunner", deps = [":greeter"], )

這已經(jīng)實現(xiàn)了對于不同包的信息抽象。順帶的再看個 Java 包中的 MANIFEST 的示例:

Main-Class: HelloWorld Class-Path: libs/joda-time-2.10.6.jar

我們就可以知道之間的聯(lián)系。

包定義

在打包階段,我們以簡單的形式定義了這個包——因為它并非那么重要,我們也不關(guān)心。而當(dāng)我們決定發(fā)布這個包到互聯(lián)網(wǎng)時,我們就需要好好定義這個包。對應(yīng)的一些必要信息有:

  • name

  • version

  • authors

  • license

  • description

  • ……

這些信息用于在包管理中心展示,并向使用者提供包相關(guān)的信息等。不同的語言中使用的是不同的形式,Rust 使用了自定義的 toml,而諸如 Maven  倉庫中則使用了 XML:

<groupId>...</groupId> <artifactId>...</artifactId> <version>...</version> <packaging>...</packaging> <dependencies>...</dependencies> <name>...</name> <description>...</description>

類似的在 NPM 的 package.json 中也使用了類似的字段: name、 verison 等信息。

而在這些編程語言中,這個東西就設(shè)計得過于簡單了,如 Python 的 pip 中使用的 requirements.txt  來管理依賴,當(dāng)你要發(fā)布包的時候使用 setup.py 進行配置。于是,你的應(yīng)用如果不發(fā)布,那就沒有包名了&hellip;&hellip;。

包布局

構(gòu)建工具在設(shè)計的時候,會設(shè)計默認(rèn)的軟件包分層結(jié)構(gòu),這個分層架構(gòu)就是包布局(package  layout)。構(gòu)建工具通過這個布局,來獲取所需的輸入源和配置等信息。它也包含了一些默認(rèn)的配置,如 src/main 指向了源碼的目錄, src/test  指向的是測試代碼(不會加入到制品中)

├── build.gradle └── src ├── main └── test

對于使用者來說,它們也可以針對于它們的需要擴展這個布局,如 Gradle 里的 SourceSets:

sourceSets { main { output.resourcesDir = file('out/bin') java.outputDir = file('out/bin') } }

對于其它語言也是類似的。但是呢,對于某些語言來說,并非有這么強的關(guān)聯(lián),如在 Golang  中,就沒有這么強的約束。只是呢,原先是默認(rèn)值,現(xiàn)在需要開發(fā)人員來手動配置。

制品

制品是最終的構(gòu)建產(chǎn)物。同樣的,在不同的語言中有不同的命名方式。在 Gradle 中稱為 artifacts,在 Rust 中稱為  targets&hellip;&hellip;。制品,主要涉及到的是各種文件的流轉(zhuǎn)及其流轉(zhuǎn)規(guī)則。

舉個簡單的例子,一個 jar 文件中必須包含一個 MANIFEST.MF,以用于配置應(yīng)用程序、擴展和類裝載器等相關(guān)信息。而相關(guān)的文件又會以  META-INF 的方式組織起來。

因此在整個制品的創(chuàng)建過程中,就是復(fù)制對應(yīng)的文件,進行相應(yīng)的轉(zhuǎn)換,如 java ->  .class,再復(fù)制到對應(yīng)的目錄,最后再打包在一起的過程。

任務(wù):規(guī)則引擎 + DSL

在上述我們看到的例子中,很多就是創(chuàng)建了自身的  DSL,而后用于構(gòu)建。只有這樣才能讓使用者得到最大的方便。這是一個相當(dāng)復(fù)雜的過程,它相當(dāng)于我們要設(shè)計一個和平臺、語言無關(guān)的  DSL。而這種演變方式有多種:

使用 API 抽象的內(nèi)部 DSL。諸如于 Webpack、Gulp 等實現(xiàn)。

自制的外部 DSL 語言。如 Gradle 所使用的 Groovy、多語言的 Bazel。

規(guī)則引擎本身是一組關(guān)于任務(wù)的 DSL,看個 Gradle 的例子:

task copyReportsDirForArchiving2(type: Copy) { from("$buildDir") { include "reports/**" } into "$buildDir/toArchive" }

它所做的事情就是復(fù)制。對應(yīng)的 Gradle 打包示例也是蠻簡單的 DSL 抽象:

task packageDistribution(type: Zip) { archiveFileName = "my-distribution.zip" destinationDirectory = file("$buildDir/dist")  from "$buildDir/toArchive" }

Gradle 使用的就是外部 DSL。再看看 Webpack 的打包示例:

module.exports = { entry: './path/to/my/entry/file.js', output: { filename: 'my-first-webpack.bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.(js|jsx)$/, use: 'babel-loader' } ] }, plugins: [ new webpack.ProgressPlugin(), new HtmlWebpackPlugin({template: './src/index.html'}) ] };

這里的 rules 就是一個簡單的規(guī)則引擎(使用正則表達式來匹配)

兩種模式各自有自己的優(yōu)缺點,復(fù)雜場景下,使用 DSL + 自定義的腳本更容易完成。

PS:看來有空,我也應(yīng)該寫一個的規(guī)則引擎

構(gòu)建的擴展

對于主流的構(gòu)建系統(tǒng)來說,他們都支持不同形式的擴展支持:

  1. 外部 DSL 擴展

  2. 插件化的接口編程

  3. 項目內(nèi)編程語言擴展

  4. 項目外編程語言擴展

感謝各位的閱讀,以上就是“如何理解構(gòu)建的抽象”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對如何理解構(gòu)建的抽象這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI