整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          我是如何在 Go 中構建 Web 服務的

          用了近十年的 C# 轉到 Go 是一個有趣的旅程。有時,我陶醉于 Go 的簡潔[1];也有些時候,當熟悉的 OOP (面向對象編程)模式[2]無法在 Go 代碼中使用的時候會感到沮喪。幸運的是,我已經摸索出了一些寫 HTTP 服務的模式,在我的團隊中應用地很好。

          當在公司項目上工作時,我傾向把可發現性放在最高的優先級上。這些應用會在接下來的 20 年運行在生產環境中,必須有眾多的開發人員和網站可靠性工程師(可能是指運維)來進行熱補丁,維護和調整工作。因此,我不指望這些模式能適合所有人。

          Mat Ryer 的文章[3]是我使用 Go 試驗 HTTP 服務的起點之一,也是這篇文章的靈感來源。

          代碼組成

          Broker

          一個 Broker 結構是將不同的 service 包綁定到 HTTP 邏輯的膠合結構。沒有包作用域結級別的變量被使用。依賴的接口得益于了 Go 的組合[4]的特點被嵌入了進來。

          type Broker struct {
              auth.Client             // 從外部倉庫導入的身份驗證依賴(接口)
              service.Service         // 倉庫的業務邏輯包(接口)
          
              cfg    Config           // 該 API 服務的配置
              router *mux.Router      // 該 API 服務的路由集
          }
          

          broker 可以使用阻塞[5]函數 New() 來初始化,該函數校驗配置,并且運行所有需要的前置檢查。

          func New(cfg Config, port int) (*Broker, error) {
              r := &Broker{
                  cfg: cfg,
              }
          
              ...
          
              r.auth.Client, err = auth.New(cfg.AuthConfig)
              if err != nil {
                  return nil, fmt.Errorf("Unable to create new API broker: %w", err)
              }
          
              ...
          
              return r, nil
          }
          

          初始化后的 Broker 滿足了暴露在外的 Server 接口,這些接口定義了所有的,被 route 和 中間件(middleware)使用的功能。service 包接口被嵌入,這些接口與 Broker 上嵌入的接口相匹配。

          type Server interface {
              PingDependencies(bool) error
              ValidateJWT(string) error
          
              service.Service
          }
          

          web 服務通過調用 Start() 函數來啟動。路由綁定通過一個閉包函數[6]進行綁定,這種方式保證循環依賴不會破壞導入周期規則。

          func (bkr *Broker) Start(binder func(s Server, r *mux.Router)) {
              ...
          
              bkr.router = mux.NewRouter().StrictSlash(true)
              binder(bkr, bkr.router)
          
              ...
          
              if err := http.Serve(l, bkr.router); errors.Is(err, http.ErrServerClosed) {
                  log.Warn().Err(err).Msg("Web server has shut down")
              } else {
                  log.Fatal().Err(err).Msg("Web server has shut down unexpectedly")
              }
          }
          

          那些對故障排除(比如,Kubernetes 探針[7])或者災難恢復方案方面有用的函數,掛在 Broker 上。如果被 routes/middleware 使用的話,這些僅僅被添加到 webserver.Server 接口上。

          func (bkr *Broker) SetupDatabase() { ... }
          func (bkr *Broker) PingDependencies(failFast bool)) { ... }
          

          啟動引導

          整個應用的入口是一個 main 包。默認會啟動 Web 服務。我們可以通過傳入一些命令行參數來調用之前提到的故障排查功能,方便使用傳入 New() 函數的,經過驗證的配置來測試代理權限以及其他網絡問題。我們所要做的只是登入運行著的 pod 然后像使用其他命令行工具一樣使用它們。

          func main() {
              subCommand := flag.String("start", "", "start the webserver")
          
              ...
          
              srv := webserver.New(cfg, 80)
          
              switch strings.ToLower(subCommand) {
              case "ping":
                  srv.PingDependencies(false)
              case "start":
                  srv.Start(BindRoutes)
              default:
                  fmt.Printf("Unrecognized command %q, exiting.", subCommand)
                  os.Exit(1)
              }
          }
          

          HTTP 管道設置在 BindRoutes() 函數中完成,該函數通過 ser.Start() 注入到服務(server)中。

          func BindRoutes(srv webserver.Server, r *mux.Router) {
              r.Use(middleware.Metrics(), middleware.Authentication(srv))
              r.HandleFunc("/ping", routes.Ping()).Methods(http.MethodGet)
          
              ...
          
              r.HandleFunc("/makes/{makeID}/models/{modelID}", model.get(srv)).Methods(http.MethodGet)
          }
          

          中間件

          中間件(Middleware)返回一個帶有 handler 的函數,handler 用來構建需要的 http.HandlerFunc。這使得 webserver.Server 接口被注入,同時所有的安靜檢查只在啟動時執行,而不是在所有路由調用的時候。

          func Authentication(srv webserver.Server) func(h http.Handler) http.Handler {
              if srv == nil || !srv.Client.IsValid() {
                  log.Fatal().Msg("a nil dependency was passed to authentication middleware")
              }
          
              // additional setup logic
              ...
          
              return func(next http.Handler) http.Handler {
                  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                      token := strings.TrimSpace(r.Header.Get("Authorization"))
                      if err := srv.ValidateJWT(token); err != nil {
                          ...
                          w.WriteHeader(401)
                          w.Write([]byte("Access Denied"))
          
                          return
                      }
          
                      next.ServeHTTP(w, r)
                  }
              }
          }
          

          路由

          路由有著與中間件有著類似的套路——簡單的設置,但是有著同樣的收益。

          func GetLatest(srv webserver.Server) http.HandlerFunc {
              if srv == nil {
                  log.Fatal().Msg("a nil dependency was passed to the `/makes/{makeID}/models/{modelID}` route")
              }
          
              // additional setup logic
              ...
          
              return func(w http.ResponseWriter, r *http.Request) {
                  ...
          
                  makeDTO, err := srv.Get
              }
          }
          

          目錄結構

          代碼的目錄結構對可發現性進行了高度優化。

          ├── app/
          |   └── service-api/**
          ├── cmd/
          |   └── service-tool-x/
          ├── internal/
          |   └── service/
          |       └── mock/
          ├── pkg/
          |   ├── client/
          |   └── dtos/
          ├── (.editorconfig, .gitattributes, .gitignore)
          └── go.mod
          
          • app/ 用于項目應用——這是新來的人了解代碼傾向的切入點。dd
            • ./service-api/ 是該倉庫的微服務 API;所有的 HTTP 實現細節都在這里。
          • cmd/ 是存放命令行應用的地方。
          • internal/ 是不可以被該倉庫以外的項目引入的一個特殊目錄[8]
            • ./service/ 是所有領域邏輯(domain logic)所在的地方;可以被 service-apiservice-tool-x,以及任何未來直接訪問這個目錄可以帶來收益的應用或者包所引入。
          • pkg/ 用于存放鼓勵被倉庫以外的項目所引入的包。
            • ./client/ 是用于訪問 service-api 的 client 庫。其他團隊可以使用而不是自己寫一個 client,并且我們可以借助我們在 cmd/ 里面的 CI/CD 工具來 “dogfood it[9]” (使用自己產品的意思)。
            • ./dtos/ 是存放項目的數據傳輸對象,不同包之間共享的數據且以 json 形式在線路上編碼或傳輸的結構體定義。沒有從其他倉庫包導出的模塊化的結構體。/internal/service 負責 這些 DTO (數據傳輸對象)和自己內部模型的相互映射,避免實現細節的遺漏(如,數據庫注釋)并且該模型的改變不破壞下游客戶端消費這些 DTO。
          • .editorconfig,.gitattributes,.gitignore 因為所有的倉庫必須使用 .editorconfig,.gitattributes,.gitignore[10]
          • go.mod 甚至可以在有限制的且官僚的公司環境[11]工作。

          最重要的:每個包只負責意見事情,一件事情!

          HTTP 服務結構

          └── service-api/
              ├── cfg/
              ├── middleware/
              ├── routes/
              |   ├── makes/
              |   |   └── models/**
              |   ├── create.go
              |   ├── create_test.go
              |   ├── get.go
              |   └── get_test.go
              ├── webserver/
              ├── main.go
              └── routebinds.go
          
          • ./cfg/ 用于存放配置文件,通常是以 JSON 或者 YAML 形式保存的純文本文件,它們也應該被檢入到 Git 里面(除了密碼,秘鑰等)。
          • ./middleware 用于所有的中間件。
          • ./routes 采用類似應用的類 RESTFul 形式的目錄對路由代碼進行分組和嵌套。
          • ./webserver 保存所有共享的 HTTP 結構和接口(Broker,配置,Server等等)。
          • main.go 啟動應用程序的地方(New()Start())。
          • routebinds.go BindRoutes() 函數存放的地方。

          你覺得呢?

          如果你最終采用了這種模式,或者有其他的想法我們可以討論,我樂意聽到這些想法!


          via: https://www.dudley.codes/posts/2020.05.19-golang-structure-web-servers/

          作者:James Dudley[12]譯者:dust347[13]校對:unknwon[14]

          本文由 GCTT[15] 原創編譯,Go 中文網[16] 榮譽推出

          參考資料

          [1]

          簡潔: https://www.youtube.com/watch?v=rFejpH_tAHM

          [2]

          模式: https://en.wikipedia.org/wiki/Software_design_pattern

          [3]

          Mat Ryer 的文章: https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html

          [4]

          Go 的組合: https://www.ardanlabs.com/blog/2015/09/composition-with-go.html

          [5]

          阻塞: https://stackoverflow.com/questions/2407589/what-does-the-term-blocking-mean-in-programming

          [6]

          閉包函數: https://gobyexample.com/closures

          [7]

          Kubernetes 探針: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)0

          [8]

          特殊目錄: https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface

          [9]

          dogfood it: https://en.wikipedia.org/wiki/Eating_your_own_dog_food

          [10]

          所有的倉庫必須使用 .editorconfig,.gitattributes,.gitignore: https://www.dudley.codes/posts/2020.02.16-git-lost-in-translation/

          [11]

          有限制的且官僚的公司環境: https://www.dudley.codes/posts/2020.04.02-golang-behind-corporate-firewall/

          [12]

          James Dudley: https://www.dudley.codes/

          [13]

          dust347: https://github.com/dust347

          [14]

          unknwon: https://github.com/unknwon

          [15]

          GCTT: https://github.com/studygolang/GCTT

          [16]

          Go 中文網: https://studygolang.com/

          oahead是一種輕量級嵌入式web服務器,全部代碼開源,可以在多種平臺編譯運行,gahead具備以下典型特性:

          ·支持虛擬服務器
          ·可使用 SNMP 代理
          ·支持 SSL v3
          ·具有搜索引擎
          ·支持 ASP、JavaScript、CGI、DHTML
          ·容易移植和系統集成
          ·可不使用文件系統

          下載源路徑如下:

          https://gitee.com/mirrors/GoAhead

          goahead在linux下的安裝和簡單使用可以參考以下鏈接:

          https://blog.csdn.net/weihan0208/article/details/118483839

          以linux環境為例

          goahead的源碼在簡易使用如僅僅用于請求文件時,基本無需修改源代碼,按照流程編譯運行即可,比如將網頁文件(index.html)放在 /var/www/goahead路徑下,服務器的IP地址為192.168.0.20,綁定80端口。

          在瀏覽器內輸入 192.168.0.20/index.html,即可訪問成功。

          在大多數情況下,簡單的網頁盡可以在本地直接使用瀏覽器打開,但是當網頁代碼中存在ifreme時,加載子網頁就會存在報錯,此時goahead便可以發揮作用,如下圖:

          當然網頁可執行的功能不僅包括文件請求,還有大量紛繁復雜的操作,大多數操作goahead也是支持的,但是需要使用路由文件定義相關的操作,部分操作需要修改服務器代碼才可以實現,goahead源碼提供了基本的route.txt文件,路徑為 ./src/route.txt,用于支持基礎訪問,其內容如下:

          路由文件的基本使用方法可以參鏈接:

          https://blog.csdn.net/weixin_44074105/article/details/124930732

          Handler

          作用

          action

          用于將URL的請求與C函數綁定的處理程序。

          continue

          不進行任何操作的偽處理程序,常用于登錄驗證。

          cgi

          為 CGI 程序提供服務的處理程序。

          file

          用于提供網頁、圖像和靜態資源的處理程序。

          jst

          為動態內容提供 Javascript 模板的處理程序。

          options

          用于提供 HTTP 選項和跟蹤方法的處理程序。

          redirect

          處理route重定向的處理程序。

          upload

          處理文件上傳的處理程序。

          在源碼文件中提供的例程中

          1.“route uri=/”

          可以認為是萬能文件路由,瀏覽器的任何文件訪問,均可以通過此請求到相應的資源文件。

          2.“route uri=/action handler=action”

          Action請求需要在服務器的源文件中添加對應的action代碼實現,例如用戶登錄、服務器參數設置等操作,均可以用action實現。

          3.“route uri=/cgi-bin dir=cgi-bin handler=cgi”

          cgi請求常用于調用CGI子程序,一般來說,cgi-bin只是用于標記請求為cgi,dir=cgi-bin用于標記CGI程序的存儲位置,cgi的編譯及安裝。

          4.“route uri=/jst extensions=jst handler=jst”

          Jst實際是JSP中的(JavaServer? Pages Standard Tag Library)。

          5.Goahead同時提供了簡單的用戶登錄驗證過程

          使用以下路由可以完成表單驗證形式:

          route uri=/pub/

          Route uri=/action/login methods=POST handler=action redirect=200@/ redirect=401@/pub/login.html

          route uri=/action/logout methods=POST handler=action redirect=200@/pub/login.html

          route uri=/ auth=form handler=continue redirect=401@/pub/login.html

          其中第一行為登錄前可訪問的公共文件;

          第二行用于登錄驗證操作,第三行用于退出登錄操作;

          第四行為萬能路由,可以訪問一切文件,但是前提是完成登錄驗證,否側將被重定向只至登錄界面。

          基本驗證形式:

          route uri=/auth/basic/ auth=basic abilities=manage

          route uri=/auth/digest/ auth=digest abilities=manage

          總結

          GOahead的路由文件可以視作一種順序執行,逐行匹配的腳本,可訪問的資源文件,由根據不同的method handler redirect auth等操作,可以產生條件執行的簡單邏輯,根據此思路結合表單用戶登錄過程,用戶可以自行修改route文件實現對應的操作,最終完成整個服務器,當然,goahead不僅僅只有上邊的幾個操作,還可以實現文件下載、文件上傳等操作,感興趣的讀者可以自行查找相關的資料。

          本文誕生在項目開發實踐中,用于某型號的物聯網設備,以實現參數配置,在實際使用中,還發現如果要實現直接使用192.168.0.20進入網頁還需要在源碼中添加部分代碼,否則,瀏覽器的訪問會出現以下問題:

          在瀏覽器輸入192.168.0.20,實際被服務器重定向到192.168.0.43/index.html(瀏覽器所在電腦的ip),經過一番查找,最終找到問題所在。

          在源碼中使用了auth=form登錄驗證,或者只有uri=/路由時,瀏覽器僅使用IP地址訪問服務器,服務器找不到瀏覽器找不到具體的請求,將會直接進入void websRedirect(Webs *wp, cchar *uri)(位于http.c文件)函數,重新向瀏覽器的訪問位置,實際上由于未更改任何源碼,所以代碼中:

          host = websHostUrl ? websHostUrl : wp->ipaddr;

          websHostUrl = NULL,host實際為wp->ipaddr,即瀏覽器所在終端的IP地址,如果要解決此問題,可以在goahead.c文件中

          函數:MAIN(goahead, int argc, char **argv, char **envp)

          中添加如下內容

          websSetHostUrl("192.168.0.20");

          websSetIndex("index.html");

          "192.168.0.20"為服務器ip,實際使用中需要使用系統的接口獲取之后轉化為字符串,或者存在公網ip或者域名時(外網訪問),填寫公網ip或者域名index.html為默認重定向文件。


          快節奏的 Web 開發世界中,在最新框架的熱潮中,最好的解決方案往往在于簡單。 在最近的一篇文章中,我們談到了使用 Go 和 htmx 進行動態 Web 內容的本地化。 在這篇文章中,我們將進一步探討 htmx 和 Go 的結合、最佳實踐和可維護性。 采用“Lindy”方法進行 Web 開發。 林迪效應是一個概念,它斷言一個不易腐爛的想法或技術的未來預期壽命與其當前的年齡成正比; 它越老,它存在的時間可能就越長。

          回顧一下,htmx 是一個輕量級的 Javascript 庫,它允許我們直接從 HTML 訪問現代瀏覽器功能,從而消除了對繁重的客戶端框架,尤其是客戶端狀態的需求。 IE。 編寫 Javascript(htmx 庫)以避免編寫 Javascript(在您的網站上)。 htmx 的總體理念與 Go 對簡單性的關注非常吻合。 盡管如此,這種組合似乎是“現代”網絡開發所構成的一種逆向立場。

          htmx 哲學

          htmx 的核心在于對精益代碼和敏捷開發的信念。 專注于創建無狀態客戶端:

          HTML 在線:htmx 通過在線發送 HTML 來實現動態用戶界面,簡化服務器端開發和維護,并最大限度地減少客戶端狀態。 htmx 遵循 HATEOAS 約束,通過利用 HTML,使用超媒體作為應用程序狀態的引擎。

          漸進增強:使用 htmx,您可以從基本的 HTML 頁面開始,然后根據需要對其進行增強。 這種方法提高了可訪問性和可用性,這也意味著如果 Javascript 由于任何原因失敗,您的網站仍然可以工作。

          簡單性:htmx 相信盡可能降低復雜性。 沒有構建過程,沒有依賴關系,沒有特殊的服務器端技術,并且需要學習的 API 面積也很小。

          互操作性:htmx 旨在與您現有的 HTML 和服務器端代碼配合使用。 它不需要您學習新的模板語言或更改服務器端架構。

          恢復簡單性

          htmx 和 Go 有著共同的理念,都傾向于極簡主義和組合而不是繼承。 該原則有利于通過組合簡單對象而不是使用分層繼承結構來創建復雜對象。 組合允許對象通過合并實現所需功能的其他對象來實現復雜的行為,從而形成更靈活和可維護的代碼結構。 htmx 簡化了服務器端渲染,通過 AJAX 增強了 HTML,而 Go 則因其編寫后端服務的品質而受到贊賞。 它們共同為網站與后端服務的交互提供了全新的視角,統一了連貫開發體驗的原則。

          使用 html

          最基本的是,htmx 允許您只需向 HTML 添加一些屬性即可發出 AJAX 請求。 例如,如果您有一個按鈕,并且希望在單擊該按鈕時發出 AJAX 請求,您可以這樣做:

          <button hx-post="/clicked" hx-target="#message">Click me</button>
          <div id="message"></div>

          當您單擊按鈕時,htmx 會向 /clicked URL 發送 POST 請求。 當服務器使用一些 HTML 進行響應時,該 HTML 會被放置到 #message div 中。 htmx 提供了許多其他屬性和選項,用于發出 AJAX 請求、處理事件、處理響應等。 這些功能使您能夠使用最少的 JavaScript 構建復雜的動態界面。 添加等待填充內容的空 <div> 元素感覺像 jQuery 風格,但讓人回想起 jQuery 和 PHP 時代開發的敏捷性。 可以說是一種更清潔的方法。 對于事件類型,服務器發送的事件可以簡單地實現如下:

          <div hx-sse="connect:/user/updates swap:userUpdate">
              Updates will appear here.
          </div>

          使用 Go 模板,我們從后端數據注入變量:

          <a href="/"
             hx-trigger="click"
             hx-indicator="#spinner"
             hx-get="/data/{{$sport.Key}}"
             hx-target="#data-table">Fetch</a>

          htmx Go 模板最佳實踐

          我們發現了在 Web 應用程序中使用 htmx 時的一些最佳實踐:

          服務器端狀態管理:除了身份驗證等必要的cookie之外,所有狀態都應該在服務器端進行管理。

          HATEOAS with HTML:使用 HTML(而不是 JSON)實現超媒體作為應用程序狀態引擎 (HATEOAS)。 在根頁面加載時,呈現頁面上可以動態的所有元素并為它們提供服務,而無需額外的請求。 這可以通過渲染到 Go 緩沖區中來實現,并在需要時將它們與 template.HTML 一起注入到根模板中。 適當利用緩存。 這符合完全服務器端渲染 (SSR) 登陸頁面并增強 SEO。 用戶體驗非常好,因為所有元素都很活潑,而且似乎沒有任何內容異步加載。 一個附帶的好處是,即使禁用了 Javascript,第一頁加載也始終有效,因為 html 完全從服務器呈現。 更改元素時,它們會從各自的端點動態加載。 例如。 登錄和注銷狀態的區域。

          動態元素的 ID 可尋址性:所有動態元素都應具有唯一的 ID。 這使得它們可尋址,允許從各個 Go 端點和 hx-target 進行尋址。

          動態元素的單獨模板和端點:每個動態元素應該有一個相應的 Go 模板和端點。 這導致了模塊化且可維護的代碼。

          使用 go:embed 進行發布,從磁盤加載進行開發:利用 Go 的嵌入功能進行生產,同時允許在開發過程中進行磁盤加載。 這確保了高效的性能和熱重載。

          跳過開發中的 API 步驟:明智地選擇 ORM,最好是代碼生成。 開發過程中直接從數據庫過渡到HTML,省略API層。 這是執行中的乘數。 對于我們在訪問服務數據時自然需要的 API,我們使用 Go ent。

          為動態端點實施服務器端事件 (SSE):利用 SSE 處理動態數據。 根據數據的不同,動態部分直接是 SSE 元素,并且應該在加載時訂閱自身。 Go 處理程序將在第一次調用時推送初始狀態。 或者,第一個調用照常從 GET 請求呈現,然后 GET 請求提供訂閱自身的模板。

          項目結構

          有組織的項目結構不僅僅是美觀或偏好的問題; 這是直接影響開發過程的一個基本方面。 結構良好的項目可以促進團隊成員之間的協作,增強可維護性,并加快新開發人員的入職速度。 我們遵循一種簡單、可能常見的方法,我們內部稱之為“golean”。 使用 Go ent 生成模型、文檔和 API,布局類似如下。 我們將在以后的文章中討論這一點。

          我們將 Go Web 服務器和其他服務分離到不同的存儲庫中,以充分利用 Go 的模塊系統進行依賴管理和版本控制。 我們的存儲庫將遵循典型的結構:

          .
          ├── cmd
          │   └── web                     // The main application for this project
          │       ├── handlers.go
          │       ├── l10n
          │       │   └── translations.json
          │       ├── main.go             // The entry point for the server application
          │       ├── session.go
          │       ├── static              // Directory for static files
          │       │   └── favicon.svg
          │       └── templates           // Directory for HTML/X template files
          │           ├── base.html
          │           ├── head.html
          │           ├── partials
          │           │   └── welcome.html
          │           └── root.html
          ├── docs
          │   └── openapi.json            // OpenAPI spec generated by entoas via Go ent
          ├── internal                    // Internal packages used by the server application
          │   └── ...
          ├── go.mod
          ├── go.sum
          └── ... // Optionally Dockerfile, drone.yml, dotenv etc.

          這種分離確保了應用程序的不同部分之間的清晰區分,遵循許多 Go 最佳實踐。 cmd 目錄用于應用程序,它們是程序的入口點。 內部目錄用于在此存儲庫內的多個程序之間共享的包,但不打算在此存儲庫之外使用。 Web 目錄包含由 Web 服務器提供的靜態文件和模板。

          與服務交互

          我們的 Go Web 服務器就位后,該服務可能會或可能不會附帶數據庫。 如果存在,我們希望從其他服務訪問它。 htmx 方法自然會跳過構建前端使用的 RESTful JSON API。 然而,我們確實利用 Go ent 并首先編寫我們的 Go 模型,使用 entoas 和 ogent 生成一個完全連接的 CRUD API。 我們之前在 Go 中基于模型的生成微服務的文章中談到了這種注釋驅動的方法。

          然后我們安裝代理 API 并使用中間件保護它,例如 /api 路由前綴,我們可以與我們的服務數據庫進行交互,而無需額外的工作。 這允許在可能使用或操作 Web 服務數據的前端和后端服務之間清楚地分離關注點。

          結論——林迪效應的作用

          林迪效應以紐約市的林迪餐廳命名,百老匯演出的未來受歡迎程度與其當前的演出長度直接相關,其應用范圍遠遠超出了單純的演出。 它成為技術領域的一個令人心酸的觀察,強調了經過驗證的方法的可靠性和持久性。

          通過 htmx 和 Go 的結合,這一理念得以體現,回歸到 Web 開發的本質基礎。 通過強調服務器端狀態管理、在線 HTML 以及讓人想起 jQuery 和 PHP 等早期 Web 技術的簡單性,htmx 和 Go 不僅擁抱 Lindy 效應中蘊藏的智慧,而且在經典原則和當代技術之間取得了平衡。 這種新舊結合、經過測試和創新的結合,為現代 Web 應用程序提供了強大的解決方案,并設定了一個立足于歷史但面向未來發展的方向,引導開發人員走向既創新又植根于經過時間考驗的實踐的未來。


          主站蜘蛛池模板: 色窝窝无码一区二区三区色欲| 影院无码人妻精品一区二区| 尤物精品视频一区二区三区 | 久久精品国产一区二区三区| 亚洲一区二区三区久久久久| 日韩综合无码一区二区| 成人在线视频一区| 波多野结衣一区二区三区高清av| 中文无码一区二区不卡αv| 人妻少妇精品视频一区二区三区 | 亚洲日本一区二区三区| 伦理一区二区三区| 中文字幕人妻丝袜乱一区三区 | 精品人妻码一区二区三区| 国产一区视频在线| 日本欧洲视频一区| 无码日韩精品一区二区三区免费| 夜夜嗨AV一区二区三区| 香蕉久久av一区二区三区| 国模私拍一区二区三区| 无码免费一区二区三区免费播放| 国产在线精品一区免费香蕉| 日韩AV无码一区二区三区不卡毛片 | 免费无码一区二区三区蜜桃 | 国产香蕉一区二区三区在线视频| 亚洲熟妇AV一区二区三区浪潮 | 国产一区二区高清在线播放 | 亚洲av无码片vr一区二区三区| 中文字幕视频一区| 另类国产精品一区二区| 国产丝袜一区二区三区在线观看| 日本精品一区二区在线播放| 色系一区二区三区四区五区| 国产天堂一区二区综合| 中文字幕在线一区二区三区| 亚洲爆乳无码一区二区三区 | 91国在线啪精品一区| 久久久久人妻精品一区蜜桃 | 国产亚洲无线码一区二区| 精品无码一区二区三区爱欲九九| 精品深夜AV无码一区二区老年|