整合營銷服務商

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

          免費咨詢熱線:

          Python的Web框架介紹

          Python的Web框架介紹

          前言

          Python的便利性和多功能性意味著它幾乎被用于構建各行各業的軟件。一個主要的利基是web服務,Python的開發速度和靈活性使其可以很容易地快速建立和運行網站。正如您可能猜到的那樣,Python在web框架中為您提供了大量的選擇和自由度,無論大小。畢竟,不是每個web項目都需要是企業級的。大多數應該能足夠完成工作就可以了,而不是追逐更大。本文將8個最知名的Python框架,它們強調簡單性、輕量級交付,接下來就具體點地了解下這些框架。

          1. Bottle

          Bottle(http://bottlepy.org/)可以被認為是一種迷型Flask,因為它甚至比其他的“微框架”更緊湊和簡潔。由于Bottle占用的空間很小(最小化封裝),所以它非常適合包含在其他項目中,或者用于快速交付像REST APIs這樣的小項目。(后面會討論Flask。)

          Bottle的整個代碼庫都可以放在一個文件中,并且完全沒有外部依賴關系。即便如此,Bottle還是配備了足夠的功能來構建常見的web應用程序,而不需要依賴外界的幫助。

          Bottle中的路由系統將url映射到函數,其語法與Flask幾乎完全相同。應用中你也不會被局限于一組硬連接的路徑,而是可以動態地創建它們。請求和響應數據、cookie、查詢變量、POST操作的表單數據、HTTP報頭和文件上傳都可以通過Bottle的對象訪問和操作。

          每一項能力的實現都很注意細節。例如,對于文件上傳,如果文件的命名約定與目標文件系統沖突(例如在Windows中文件名中的斜杠),則不必重命名文件。Bottle可以幫你做到這一點。

          Bottle包含有它自己的簡單HTML模板引擎。同樣,盡管模板引擎很小,但它具有所有的基本功能。模板中包含的變量默認使用安全HTML渲染;同時你必須指出,哪些變量是安全的,以便按照字面意思重新生成。如果您愿將Bottle的模板引擎換為另一個模板引擎,比如Jinja2,那么Bottle可以讓您輕松地這樣做。我更喜歡與Bottle捆綁的簡單模板系統;它速度快,語法樸實無華,并且允許您在不太困難的情況下混合代碼和模板文本。

          Bottle甚至支持多個服務器后端。它自帶了用于快速測試的內置迷你服務器(內置了開發服務器),而且還支持通用的WSGI、各種與WSGI兼容的HTTP服務器,如果需要,也可支持普通的舊式CGI。比如,在Bottle中的“Hello World”程序:

          from bottle import route, run, template
           
          @route('/hello/<name>')
          def index(name):
              return template('<b>Hello {{name}}</b>!', name=name)
           
          run(host='localhost', port=8080)

          運行此腳本或將其粘貼到Python控制臺,然后將瀏覽器指向
          http://localhost:8080/hello/world。就這樣實現了編程中“經典”。

          與其他框架相比,Bottle不需要那么多的文檔,但是它的文檔也絕不少。所有的關鍵內容都適用于單頁面(雖然很長的)。除此之外,您還可以找到每個API的完整文檔、在各種基礎設施上部署的示例、對內置模板語言的解釋和大量常用方法。

          與Flask一樣,您可以手動或通過插件擴展Bottle的功能。Bottle插件遠沒有Flask插件那么多,但是擁有著一些有用的部分,如與各種數據庫層的集成和基本的用戶身份驗證。對于異步支持,Bottle可用現有的異步運行的服務器適配器,比如aiohttp/uvloop,但是sync/await不是原生支持。

          Bottle的極簡設計的一個后果就是有些東西根本就不存在。表單驗證,包括CSRF(cross-site request forgery,跨站點請求偽造)保護特性沒有包括在內。如果您想構建一個支持高度用戶交互的web應用程序,您需要自己添加這種支持。

          Bottle的另一個問題是發展已經遲滯很久了,最新版本是0.12,最開始是在2013年發布的,后又小版本更新(最新穩定版是2019年12月發布的0.12.18,開發中的0.13版還沒正式發布)。也就是說,Bottle將繼續被維護,并且它的開發版本仍然可以用于生產。開發人員打算交付新的版本,以擺脫對Python的遺留版本的支持。

          關于Bottle下載與安裝

          使用pip install bottle安裝最新的穩定版本,或者將bottle.py(不穩定)下載到您的項目目錄中。除了Python標準庫之外,Bottle沒有硬依賴項。Bottle支持Python 2.7和Python 3。

          注意,自0.13版本開始不推薦在Python 2.5和2.6下使用Bottle,此版本去掉了對它們的支持。

          概要總結一下:Bottle是一個快速和簡單的微型框架,用于小型web應用程序。它提供了url參數支持、模板、內置HTTP服務器和許多用于第三方WSGI/HTTP- Server和模板引擎的適配器服務于請求分派(路由)——所有這些都在一個文件中,除了Python標準庫之外沒有其他依賴關系。

          2. CherryPy

          CherryPy(http://www.cherrypy.org/)已經以這樣或那樣的形式存在了將近20年,但并沒有失去它從一開始就與眾不同的極簡主義和優雅。

          CherryPy背后的目標,除了只包含服務web頁面所需要的少量內容之外,是盡可能讓人感覺不像“web框架”,而是像任何其他類型的Python應用程序。像Hulu和Netflix這樣的網站已經在生產中使用了CherryPy,因為這個框架提供了一個非常包容的構建基礎。CherryPy在底層使用線程池,更好地支持多線程服務器適配器。

          CherryPy可以讓您的web應用程序遠離核心邏輯。要將應用程序的函數映射到CherryPy提供的url或路由,需要創建一個類,其中對象的名稱空間直接映射到需要提供服務的url。例如,網站的根目錄由一個名為“index”的函數提供。傳遞給這些函數的參數用于相應的處理GET或POST方法提供的變量。

          CherryPy包含的比特意味著作為低級構建塊工作。包含了Session標識符和cookie處理,但沒有包含HTML模板。與Bottle一樣,CherryPy提供了一種將路由映射到磁盤目錄的方法,用于靜態文件服務。

          CherryPy通常會遵從現有的第三方庫來支持某個特性,而不是本地化(原生)提供它。例如,WebSocket應用程序不是由CherryPy直接支持的,而是通過ws4py庫支持的。

          CherryPy的文檔包含一個方便的教程,介紹了該程序的各個方面。與其他一些框架教程不同,它不會帶您了解完整的端到端應用程序,但仍然很有用。文檔中有關于虛擬主機部署、通過Apache和Nginx進行反向代理以及許多其他場景的便簽筆記式說明。

          應用示例代碼如下:

          import cherrypy
           
          class HelloWorld(object):
              @cherrypy.expose
              def index(self):
                  return "Hello World!"
           
          cherrypy.quickstart(HelloWorld())

          3. Falcon

          如果您正在構建基于REST的APIs,那么Falcon(
          http://falconframework.org/)就是專門為您設計的。精悍、快速,除了標準庫之外幾乎沒有依賴,Falcon提供了REST api所需的一切,僅此而已。2019年發布的Falcon2.0廢除了對Python 2.x支持,并且至少需要Python 3.5。

          Falcon獲得“輕薄”(light & slender)標簽的很大一部分原因與框架中的代碼行數無關。這是因為Falcon幾乎沒有將自己的結構強加給應用程序。Falcon應用程序所要做的就是指出哪些函數映射到哪些API端點。從端點返回的JSON只涉及設置路由以及經由Python標準庫json.dumps函數返回的數據。對異步的支持還沒有在Falcon中實現,但是正在努力在Falcon 3.0中實現。

          Falcon還采用了健全的開箱即用的默認設置,所以安裝時不需要什么修改。例如,對于沒有顯式聲明的任何路由,默認情況下都會引發404s。如果希望向客戶機返回錯誤,可以拋出與框架捆綁在一起的大量常規異常(如HTTPBadRequest)中的一個,或者使用通用的falcon.HTTPError異常。如果需要對路由進行預處理或后處理,Falcon也為它們提供了鉤子。

          Falcon對APIs的關注意味著這里很少有使用傳統HTML用戶界面構建web應用程序的內容。例如,不要對表單處理函數和CSRF保護工具抱有太多期望。但是,Falcon提供了優雅的選項來擴展其功能,因此可以構建更復雜的項目。除了上述鉤子機制之外,您還可以找到一個用于創建中間件的接口,該接口可用于包裝所有Falcon的APIs。

          與其他框架相比,Falcon的文檔比較少,但這只是因為覆蓋的內容比較少。用戶指南包括對所有主要特性的正式的逐步演示,以及快速啟動部分,允許您查看有或沒有注釋的樣例代碼。

          示例代碼如下:

          class QuoteResource:
           
              def on_get(self, req, resp):
                  """Handles GET requests"""
                  quote={
                      'quote': (
                          "I've always been more interested in "
                          "the future than in the past."
                      ),
                      'author': 'Grace Hopper'
                  }
           
                  resp.media=quote
           
           
          api=falcon.API()
          api.add_route('/quote', QuoteResource())

          4. FastAPI

          FastAPI(
          https://fastapi.tiangolo.com/)的名稱很好地總結了它所做的事情。它被構建為快速創建API端點而服務,且運行速度也很快。

          FastAPI利用Starlette項目作為其高速網絡核心,但是要使用FastAPI,您不需要了解Starlette的內部原理。定義端點的方式與Flask或Bottle應用程序非常相似——使用裝飾器(decorator)來指明哪個函數處理哪個路由——然后返回自動轉換成JSON的字典。

          您可以很容易地重寫返回內容的方式。例如,如果希望從某些端點返回HTML/XML,只需返回一個自定義響應對象即可。如果您想要添加自定義中間件,您可以彈出任何遵循ASGI標準的內容。

          FastAPI使用Python的類型提示來提供路由所接受的數據類型的約束。例如,如果您有一個類型為Optional[int]的路由,FastAPI將拒絕除整數以外的所有提交。你不需要添加數據校驗代碼到你的端點,可以只使用類型提示,然后讓FastAPI來完成這項工作。

          當然,有些東西被省略了。例如,沒有原生HTML模板引擎,但并不缺少填補這一空白的第三方解決方案。數據庫連接也是如此,但是文檔中包含了關于如何協同某些ORMs(例如Peewee)使用FastAPI的異步行為而工作的細節。

          示例代碼如下:

          from typing import Optional
           
          from fastapi import FastAPI
           
          app=FastAPI()
           
           
          @app.get("/")
          def read_root():
              return {"Hello": "World"}
           
           
          @app.get("/items/{item_id}")
          def read_item(item_id: int, q: Optional[str]=None):
              return {"item_id": item_id, "q": q}

          5. Flask框架

          許多關于Python web框架的討論都從Flask(http://flask.pocoo.org/)開始,這是有原因的。Flask構建良好,易于理解的框架,很容易使用并且很穩定。在輕量級web項目或基本REST API中使用Flask幾乎是不可能出錯的,但是如果您試圖構建任何更大的東西,你將會面臨很大的負擔。

          Flask的主要吸引力在于它的低門檻。一個基本的“hello world”應用程序可以用不到10行Python代碼完成。Flask包含了一個廣泛使用的HTML模板系統Jinja2,它可以使文本的渲染更加容易,但是Jinja2可以被替換為任意數量的其他模板引擎(比如Mustache:
          https://github.com/defunkt/pystache
          ),或者您也可以使用自己的模板引擎。

          Flask版Hello World 應用:

          from flask import Flask
          app=Flask(__name__)
           
          @app.route('/')
          def hello_world():
              return 'Hello, World!'

          把這個保存為hello.py或其它不是flask.py的文件,然后就可以按照一定的模式來運行程序了:

          $ export FLASK_APP=hello.py
          $ flask run
           * Running on http://127.0.0.1:5000/

          或者在windows下

          命令行窗口:C:\path\to\app>set FLASK_APP=hello.py
          Powershell:PS C:\path\to\app> $env:FLASK_APP="hello.py"

          或者可以使用 python -m flask:

          $ export FLASK_APP=hello.py
          $ python -m flask run
           * Running on http://127.0.0.1:5000/

          (詳情可以參考官網進一步了解)

          出于簡單性的考慮,Flask省略了一些細節,比如數據層或者ORM,并且沒有提供類似表單驗證的功能。然而,Flask可通過擴展支持機制進行擴展,已有的擴展有幾十個,覆蓋了許多常見的用例場景,比如緩存,表單處理和驗證,以及數據庫連接。這種缺省設計允許你以絕對最小的功能開始設計一個Flask應用程序,然后在你需要的時候將它們分層。

          Flask的文件是親切和容易閱讀。快速開始文檔很好地幫助您入門,同時還解釋了簡單的Flask應用程序的默認選擇的重要性,并且API文檔中有很多很好的例子。同樣優秀的還有Flash示例片段的集合,是一些如何完成特定任務的快速而粗糙的示例,例如,如果對象存在如何返回,或者如果對象不存在,如何返回404錯誤。

          在2018年,Flask發布了具有里程碑意義的1.0版本,其中Python 2.6和Python 3.3是最小的支持版本,并且它的許多行為最終都是固定不變的。Flask不顯式地支持Python的異步語法,但是一個API兼容的Flask變體Quart已經被分離出來以滿足這個需求。

          6. Pyramid框架

          小巧輕便的Pyramid(https://trypyramid.com/)非常適合于一些這樣的任務,比如將現有的Python代碼作為REST API公開,或者為開發人員承擔大部分繁重工作的web項目提供核心支持。

          “Pyramid會讓你迅速變得富有成效,并與你一起成長,”文檔簡述中說。“當你的應用程序很小的時候,它不會阻礙你;當你的應用程序變大的時候,它也不會妨礙你。”

          描述Pyramid的極簡主義的一個好方法是“不受策略約束”,在文檔中討論Pyramid如何與其他web框架相比較的部分使用了這個術語。基本上,“無策略”意味著您選擇使用哪種數據庫或哪種模板語言不是Pyramid所關心的。

          建立一個基本的Pyramid應用程序需要非常少的工作。與Bottle和Flask一樣,除了框架本身的文件外,Pyramid應用程序可以由單個Python文件組成。一個簡單的單路由API只需要十來行代碼。大部分都是樣板文件,比如…import語句和設置WSGI服務器。

          默認情況下,Pyramid包括一些在web應用程序中常見的事項,但它們是作為組件提供的,并將其拼接在一起,而不是作為成熟的解決方案。例如,對用戶會話的支持甚至附帶了CSRF保護。但對用戶帳戶的支持,如登錄或帳戶管理,不在處理的一部分。你必須自己處理它或通過插件來添加支持它。表單處理和數據庫連接也是如此。

          Pyramid甚至提供了一種從以前的Pyramid項目創建模板的方法來重用以前的工作。這些被稱為“腳手架”的模板生成了一個金字塔應用程序,具有簡單的路由和一些初始HTML/CSS模板。捆綁的腳手架包括一個示例啟動程序項目和一個通過流行的Python庫SQLAlchemy
          http://www.sqlalchemy.org/)連接到數據庫的項目。

          Pyramid體貼入微的測試和調試工具提供了優秀的功能。在Pyramid應用程序中捆綁debugtoolbar擴展組件,你可在每個網頁上得到一個可點擊的圖標,它會生成應用程序執行的詳細信息,包括發生錯誤時的詳細回溯。還提供了日志記錄和單元測試。

          Pyramid的文件是優秀的。除了對基礎知識的快速瀏覽和教程風格的演練外,還可以找到一組社區貢獻的教程和一本常見食譜式的“烹飪指南”書。后者包括了針對一系列目標環境的部署技術,從谷歌App Engine到Nginx。

          Pyramid同時支持python2和python3,但是不使用python3的異步語法。如果想在Pyramid中利用異步,請參閱aiopyramid項目,它包括一個用于異步驅動的“hello world”應用程序的腳手架。

          應用示例代碼如下:

          from wsgiref.simple_server import make_server
          from pyramid.config import Configurator
          from pyramid.response import Response
           
          def hello_world(request):
              return Response('Hello World!')
           
          if __name__=='__main__':
              with Configurator() as config:
                  config.add_route('hello', '/')
                  config.add_view(hello_world, route_name='hello')
                  app=config.make_wsgi_app()
              server=make_server('0.0.0.0', 6543, app)
              server.serve_forever()

          7. Sanic框架

          Sanic為快速和簡單而設計,與Python 3.6或更高版本一起工作,并使用Python的async/await語法(從Python 3.5開始可用)來讓您創建高效的web應用程序。

          與Flask或Bottle一樣,一個基礎的Sanic程序“hello world”大約需要運行10行代碼,其中大部分是導入和其他樣板文件。關鍵的區別是,應用程序路由必須聲明為異步定義函數(async def functions),并且必須在異步代碼中使用await來調用這些函數。如果您以前編寫過異步驅動的應用程序,那么你應已經掌握了最困難的部分。

          Sanic用于處理連接和響應的許多機制大家都很熟悉。請求和響應只是具有相似屬性的對象,比如上傳的文件、表單、JSON對象、頭等等。

          具有許多路由的應用程序變得難以管理。Sanic用“藍圖(blueprints)”來解決這個問題,這些對象可以描述一組路由,并允許你通過高級界面來管理它們。不用顯式地編寫每個路由,也不用使用過多的帶變量的路由,你可以編寫一些藍圖來描述路由在你的應用程序中是如何工作的(例如,/object/object_id/action)。藍圖可以有通用的中間件,如果您想將管理功能應用到某些路由而不是其他路由,那么這是非常有用的。

          Sanic還可以處理HTTP以外的協議。WebSocket端點只需要不同的裝飾器和更多的內部邏輯(例如,等待和處理響應)。可以通過子類化asyncio.protocol來支持自定義網絡協議。

          Sanic故意省略了數據庫連接和HTML模板等功能,而保留了用于插入這些功能的特性:中間件、集中式應用配置,等等。

          示例代碼如下:

          from sanic import Sanic
          from sanic.response import json
           
          app=Sanic("App Name")
           
          @app.route("/")
          async def test(request):
              return json({"hello": "world"})
           
          if __name__=="__main__":
              app.run(host="0.0.0.0", port=8000)

          8. Tornado框架

          Tornado
          http://www.tornadoweb.org/)是另一個針對特定用例的微型框架:異步網絡應用程序。Tornado非常適合創建那些打開大量網絡連接并使其保持活力的服務——即任何涉及WebSockets或長輪詢的服務。Tornado 6.0需要Python 3.5或更高版本,完全不支持Python 2。

          像Bottle 或 Falcon一樣,Tornado忽略了與它的中心目的無關的特征。Tornado有一個用于生成HTML和其他輸出的內置模板系統,并提供了用于國際化、表單處理、cookie設置、用戶身份驗證和CSRF保護的機制。但是它忽略了一些主要用于面向用戶的web應用程序的特性,比如表單驗證和ORM。

          Tornado擅長為需要對異步網絡進行嚴密控制的應用程序提供基礎設施。例如,Tornado不僅提供一個內置的異步HTTP服務器,而且還提供一個異步HTTP客戶機。因此,Tornado非常適合構建并行查詢其他站點并對返回的數據進行操作的應用程序,例如網絡爬蟲或網絡機器人(web scraper或bot)。

          如果您想創建一個使用HTTP以外的協議的應用程序,Tornado已經為您提供了幫助。Tornado提供對諸如DNS解析器等實用程序的低級TCP連接和套接字的訪問,以及對第三方認證服務的訪問,它還通過WSGI標準支持與其他框架的互操作。文檔雖小但并不稀疏寬泛,其中包含了實現所有這些特性的豐富示例。

          Tornado利用并補充了Python用于異步行為的本機功能。如果您使用的是Python 3.5, Tornado支持內置的async和await關鍵字,這可以提高應用程序的速度。您還可以使用futures或回調來處理對事件的響應。

          Tornado提供了一個同步原語庫(信號量、鎖等)來協調異步協程之間的事件。請注意,Tornado通常運行為單線程模式,因此這些原語與它們的線程名稱不同。但是,如果您希望在并行進程中運行Tornado以利用多個套接字和內核,那么可以使用一些工具來實現這一點。

          Tornado的文檔涵蓋了框架中的每個主要概念以及模型中的所有主要api。盡管它包含一個示例應用程序(一個web爬蟲),但它主要用于演示Tornado的排隊模塊。

          應用示例代碼如下:

          import tornado.ioloop
          import tornado.web
           
          class MainHandler(tornado.web.RequestHandler):
              def get(self):
                  self.write("Hello, world")
           
          def make_app():
              return tornado.web.Application([
                  (r"/", MainHandler),
              ])
           
          if __name__=="__main__":
              app=make_app()
              app.listen(8888)
              tornado.ioloop.IOLoop.current().start()

          另外,相比前面小而美的框架較而言,Django
          https://www.djangoproject.com/)
          是個完整的技術棧了,基本有各種支持,悠久而龐大,這里沒有提出來更多介紹。概而言之,Django是一個高級Python Web框架,它鼓勵快速開發和干凈、實用的設計。它由經驗豐富的開發人員構建,解決了Web開發中的許多麻煩,因此您可以專注于編寫應用程序,而無需重新發明輪子。它是免費和開源的,學習曲線相對來說較高,有興趣的可以去官網詳細研究一下。

          文地址: http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html 已取得原作者同意

          用Javascript的小伙伴們,是時候承認了,關于 promises 我們一直存在著問題。并非說 promises 本身有問題,Promises/A+ 是極好的。

          就我過去數年觀察大量 PouchDB API 以及其他 promise-heavy API 的使用者們與這些 API 的搏斗中我發現,最大的問題是:

          大部分使用 promises 的小伙伴們并沒有真正的理解它

          如果你不認同這個觀點,可以看看我最近在 twitter 上出的這道題:

          Q: 下面的四種 promises 的區別是什么

          doSomething().then(function () {
           return doSomethingElse();
          });
          doSomething().then(function () {
           doSomethingElse();
          });
          doSomething().then(doSomethingElse());
          doSomething().then(doSomethingElse);
          

          如果你知道正常答案,那么我要恭喜你,你是一位 promises 大拿,你完全可以不再繼續閱讀這篇文件。

          另外 99.99% 的小伙伴們,你們才是正義。沒有一個人在 twitter 上給出正確答案,甚至我自己也被 #3 的答案驚到了。恩,即使這道題是我自己出的。

          正確答案在這篇文章的結尾,在此之前,我希望首先探究一下為何為何 promises 如此復雜,并且為何有這么多人,無論是新手還是專家,都被它坑到了。同時我還會給出一個我自認為非常獨特的視角,可以讓 promises 變的更加容易理解。同時,我非常確信在了解這些之后,promises 并不會再難以理解。

          不過在開始這些之前,讓我們先了解一些 promises 的基礎知識。

          Promises 的起源

          如果你閱讀了 promises 的一些相關文獻,你會發現有一個詞 金字塔問題 經常出現。它描述的是大量的回調函數慢慢向右側屏幕延伸的一種狀態。

          Promises 的確解決了這個問題,并且不僅僅是縮進問題。就像在 Callback Hell的救贖 中描述的一樣,回調函數真正的問題在于他剝奪了我們使用 return 和 throw 這些關鍵字的能力。相反,我們的整個代碼流程都是基于副作用的: 一個函數會附帶調用其他函數。

          原文關于副作用的描述并不能很直觀的進行理解,建議參考 WIKI 。簡單來說就是一個函數除了會返回一個值之外,還會修改函數以外的狀態如全局變量等等。實際上所有異步調用都可以視為帶有副作用的行為。譯者注。

          并且實際上,回調更加惱人的是,他會將我們通常在大部分編程語言中能獲得的 堆棧 破壞。編寫代碼時如果無法獲取堆棧就像開車時沒有剎車一樣: 不到用的時候,你不會知道它有多重要。

          Promises 給予我們的就是在我們使用異步時丟失的最重要的語言基石: return, throw 以及堆棧。但是想要 promises 能夠提供這些便利給你的前提是你知道如何正確的使用它們。

          新手錯誤

          一些同學試圖通過用 卡通 來描述 promises,或者試圖用語言去描述它: “哦,你可以把它作為一個異步的值進行傳遞。”

          我認為這些解釋并不會有很大的幫助。對我來說,promises 完全是一種代碼結構和流程。因此我認為直接展示一些常見的錯誤并且演示如何修復它們更能夠說明問題。我說這些問題是 “新手問題” ,這意味著 “雖然你現在是一個新手,孩子,但是馬上你會變成一位專家”。

          小插曲: “promises” 對于不同的人有不同的理解和觀點,但是在這篇文章中我特指 正式標準 ,在現代瀏覽器中暴露為 window.Promise。雖然并非所有瀏覽器都有 windows.Promise,但是可以尋找一些 pollyfill ,比如 Lie 是目前體積最小的兼容標準的庫。

          新手錯誤 #1: promise版的金字塔問題

          觀察大家如何使用 PouchDB 這類大型的 promise 風格的API,我發現大量錯誤的 promise 使用形式。最常見的錯誤就是下面這個:

          remotedb.allDocs({
           include_docs: true,
           attachments: true
          }).then(function (result) {
           var docs=result.rows;
           docs.forEach(function(element) {
           localdb.put(element.doc).then(function(response) {
           alert("Pulled doc with id " + element.doc._id + " and added to local db.");
           }).catch(function (err) {
           if (err.status==409) {
           localdb.get(element.doc._id).then(function (resp) {
           localdb.remove(resp._id, resp._rev).then(function (resp) {
          // et cetera...
          

          是的,實際上你可以像使用回調一樣使用 promises,恩,就像用打磨機去削腳趾甲一樣,你確實可以這么做。

          并且如果你以為這樣的錯誤只限于初學者,那么你會驚訝于我實際上是在黑莓官方開發者博客上看到上面的代碼。老的回調風格的習慣難以消滅。(至開發者: 抱歉選了你的例子,但是你的例子將會有積極的教育意義)

          正確的風格應該是這樣:

          remotedb.allDocs(...).then(function (resultOfAllDocs) {
           return localdb.put(...);
          }).then(function (resultOfPut) {
           return localdb.get(...);
          }).then(function (resultOfGet) {
           return localdb.put(...);
          }).catch(function (err) {
           console.log(err);
          }); 
          

          這種寫法被稱為 composing promises ,是 promises 的強大能力之一。每一個函數只會在前一個 promise 被調用并且完成回調后調用,并且這個函數會被前一個 promise 的輸出調用,稍后我們在這塊做更多的討論。

          新手錯誤 #2: WTF, 用了 promises 后怎么用 forEach?

          這里是大多數人對于 promises 的理解開始出現偏差。一旦當他們要使用他們熟悉的 forEach() 循環 (無論是 for 循環還是 while 循環),他們完全不知道如何將 promises 與其一起使。因此他們就會寫下類似這樣的代碼。

          // I want to remove() all docs
          db.allDocs({include_docs: true}).then(function (result) {
           result.rows.forEach(function (row) {
           db.remove(row.doc); 
           });
          }).then(function () {
           // I naively believe all docs have been removed() now!
          });
          

          這份代碼有什么問題?問題在于第一個函數實際上返回的是 undefined,這意味著第二個方法不會等待所有 documents 都執行 db.remove()。實際上他不會等待任何事情,并且可能會在任意數量的文檔被刪除后執行!

          這是一個非常隱蔽的 bug,因為如果 PouchDB 刪除這些文檔足夠快,你的 UI 界面上顯示的會完成正常,你可能會完全注意不到有什么東西有錯誤。這個 bug 可能會在一些古怪的競態問題或一些特定的瀏覽器中暴露出來,并且到時可能幾乎沒有可能去定位問題。

          簡而言之,forEach()/for/while 并非你尋找的解決方案。你需要的是 Promise.all():

          db.allDocs({include_docs: true}).then(function (result) {
           return Promise.all(result.rows.map(function (row) {
           return db.remove(row.doc);
           }));
          }).then(function (arrayOfResults) {
           // All docs have really been removed() now!
          });
          

          上面的代碼是什么意思呢?大體來說,Promise.all()會以一個 promises 數組為輸入,并且返回一個新的 promise。這個新的 promise 會在數組中所有的 promises 都成功返回后才返回。他是異步版的 for 循環。

          并且 Promise.all() 會將執行結果組成的數組返回到下一個函數,比如當你希望從 PouchDB 中獲取多個對象時,會非常有用。此外一個更加有用的特效是,一旦數組中的 promise 任意一個返回錯誤,Promise.all() 也會返回錯誤。

          新手錯誤 #3: 忘記使用 .catch()

          這是另一個常見的錯誤。單純的堅信自己的 promises 會永遠不出現異常,很多開發者會忘記在他們的代碼中添加一個 .catch()。然而不幸的是這也意味著,任何被拋出的異常都會被吃掉,并且你無法在 console 中觀察到他們。這類問題 debug 起來會非常痛苦。

          類似 Bluebird 之類的 Promise 庫會在這種場景拋出 UnhandledRejectionError 警示有未處理的異常,這類情況一旦發現,就會造成腳本異常,在 Node 中更會造成進程 Crash 的問題,因此正確的添加 .catch() 非常重要。 譯者注

          為了避免這類討厭的場景,我習慣于像下面的代碼一樣使用 promise:

          somePromise().then(function () {
           return anotherPromise();
          }).then(function () {
           return yetAnotherPromise();
          }).catch(console.log.bind(console)); // <-- this is badass
          

          即使你堅信不會出現異常,添加一個 catch() 總歸是更加謹慎的。如果你的假設最終被發現是錯誤的,它會讓你的生活更加美好。

          新手錯誤 #4:使用 “deferred”

          這是一個我經常可以看到的錯誤,以至于我甚至不愿意在這里重復它,就像懼怕 Beetlejuice 一樣,僅僅是提到它的名字,就會召喚出來更多。

          簡單的說,promises 擁有一個漫長并且戲劇化的歷史,Javascript 社區花費了大量的時間讓其走上正軌。在早期,deferred 在 Q,When,RSVP,Bluebird,Lie等等的 “優秀” 類庫中被引入, jQuery 與 Angular 在使用 ES6 Promise 規范之前,都是使用這種模式編寫代碼。

          因此如果你在你的代碼中使用了這個詞 (我不會把這個詞重復第三遍!),你就做錯了。下面是說明一下如何避免它。

          首先,大部分 promises 類庫都會提供一個方式去包裝一個第三方的 promises 對象。舉例來說,Angular的 $q 模塊允許你使用 $q.when 包裹非 $q 的 promises。因此 Angular 用戶可以這樣使用 PouchDB promises.

          $q.when(db.put(doc)).then(/* ... */); // <-- this is all the code you need
          

          另一種策略是使用構造函數聲明模式,它在用來包裹非 promise API 時非常有用。舉例來說,為了包裹一個回調風格的 API 如 Node 的 fs.readFile ,你可以簡單的這么做:

          new Promise(function (resolve, reject) {
           fs.readFile('myfile.txt', function (err, file) {
           if (err) {
           return reject(err);
           }
           resolve(file);
           });
          }).then(/* ... */)
          

          完工!我們打敗了可怕的 def….啊哈,抓到自己了。:)

          關于為何這是一種反模式更多的內容,請查看 Bluebird 的 promise anti-patterns wiki 頁

          新手錯誤 #5:使用副作用調用而非返回

          下面的代碼有什么問題?

          somePromise().then(function () {
           someOtherPromise();
          }).then(function () {
           // Gee, I hope someOtherPromise() has resolved!
           // Spoiler alert: it hasn't.
          });
          

          好了,現在是時候討論一下關于 promises 你所需要知道的一切。

          認真的說,這是一個一旦你理解了它,就會避免所有我提及的錯誤的古怪的技巧。你準備好了么?

          就如我前面所說,promises 的奇妙在于給予我們以前的 return 與 throw。但是在實踐中這到底是怎么一回事呢?

          每一個 promise 都會提供給你一個 then() 函數 (或是 catch(),實際上只是 then(null, ...) 的語法糖)。當我們在 then() 函數內部時:

          somePromise().then(function () {
           // I'm inside a then() function!
          });
          

          我們可以做什么呢?有三種事情:

          1. return 另一個 promise
          2. return 一個同步的值 (或者 undefined)
          3. throw 一個同步異常

          就是這樣。一旦你理解了這個技巧,你就理解了 promises。因此讓我們逐個了解下。

          返回另一個 promise

          這是一個在 promise 文檔中常見的使用模式,也就是我們在上文中提到的 “composing promises”:

          getUserByName('nolan').then(function (user) {
           return getUserAccountById(user.id);
          }).then(function (userAccount) {
           // I got a user account!
          });
          注意到我是 `return` 第二個 promise,這個 `return` 非常重要。如果我沒有寫 `return`,`getUserAccountById()` 就會成為一個副作用,并且下一個函數將會接收到 `undefined` 而非 `userAccount`。
          

          返回一個同步值 (或者 undefined)

          返回 undefined 通常是錯誤的,但是返回一個同步值實際上是將同步代碼包裹為 promise 風格代碼的一種非常贊的手段。舉例來說,我們對 users 信息有一個內存緩存。我們可以這樣做:

          getUserByName('nolan').then(function (user) {
           if (inMemoryCache[user.id]) {
           return inMemoryCache[user.id]; // returning a synchronous value!
           }
           return getUserAccountById(user.id); // returning a promise!
          }).then(function (userAccount) {
           // I got a user account!
          });
          

          是不是很贊?第二個函數不需要關心 userAccount 是從同步方法還是異步方法中獲取的,并且第一個函數可以非常自由的返回一個同步或者異步值。

          不幸的是,有一個不便的現實是在 JavaScript 中無返回值函數在技術上是返回 undefined,這就意味著當你本意是返回某些值時,你很容易會不經意間引入副作用。

          出于這個原因,我個人養成了在 then() 函數內部 永遠返回或拋出 的習慣。我建議你也這樣做。

          拋出同步異常

          談到 throw,這是讓 promises 更加贊的一點。比如我們希望在用戶已經登出時,拋出一個同步異常。這會非常簡單:

          getUserByName('nolan').then(function (user) {
           if (user.isLoggedOut()) {
           throw new Error('user logged out!'); // throwing a synchronous error!
           }
           if (inMemoryCache[user.id]) {
           return inMemoryCache[user.id]; // returning a synchronous value!
           }
           return getUserAccountById(user.id); // returning a promise!
          }).then(function (userAccount) {
           // I got a user account!
          }).catch(function (err) {
           // Boo, I got an error!
          });
          

          如果用戶已經登出,我們的 catch() 會接收到一個同步異常,并且如果 后續的 promise 中出現異步異常,他也會接收到。再強調一次,這個函數并不需要關心這個異常是同步還是異步返回的。

          這種特性非常有用,因此它能夠在開發過程中幫助定位代碼問題。舉例來說,如果在 then() 函數內部中的任何地方,我們執行 JSON.parse(),如果 JSON 格式是錯誤的,那么它就會拋出一個異常。如果是使用回調風格,這個錯誤很可能就會被吃掉,但是使用 promises,我們可以輕易的在 catch() 函數中處理它了。

          進階錯誤

          好了,現在你已經了解了讓 promises 變的超級簡單的技巧,現在讓我們聊一聊一些特殊場景。

          這些錯誤之所以被我歸類為 “進階” ,是因為我只見過這些錯誤發生在對 promises 已經有相當深入了解的開發者身上。但是為了解決文章最開始的謎題,我們必須討論一下這些錯誤。

          進階錯誤 #1:不知道 Promise.resolve()

          如我上面所列舉的,promises 在封裝同步與異步代碼時非常的有用。然而,如果你發現你經常寫出下面的代碼:

          new Promise(function (resolve, reject) {
           resolve(someSynchronousValue);
          }).then(/* ... */);
          

          你會發現使用 Promise.resolve 會更加簡潔:

          Promise.resolve(someSynchronousValue).then(/* ... */);
          

          它在用來捕獲同步異常時也極其的好用。由于它實在是好用,因此我已經養成了在我所有 promise 形式的 API 接口中這樣使用它:

          function somePromiseAPI() {
           return Promise.resolve().then(function () {
           doSomethingThatMayThrow();
           return 'foo';
           }).then(/* ... */);
          }
          

          切記:任何有可能 throw 同步異常的代碼都是一個后續會導致幾乎無法調試異常的潛在因素。但是如果你將所有代碼都使用 Promise.resolve() 封裝,那么你總是可以在之后使用 catch() 來捕獲它。

          類似的,還有 Promise.reject() 你可以用來返回一個立刻返回失敗的 promise。

          Promise.reject(new Error('some awful error'));
          

          進階錯誤 #2:catch() 與 then(null, ...) 并非完全等價

          之前我說過 catch() 僅僅是一個語法糖。因此下面兩段代碼是等價的:

          somePromise().catch(function (err) {
           // handle error
          });
          somePromise().then(null, function (err) {
           // handle error
          });
          

          然而,這并不意味著下面兩段代碼是等價的:

          somePromise().then(function () {
           return someOtherPromise();
          }).catch(function (err) {
           // handle error
          });
          somePromise().then(function () {
           return someOtherPromise();
          }, function (err) {
           // handle error
          });
          

          如果你好奇為何這兩段代碼并不等價,可以考慮一下如果第一個函數拋出異常會發生什么:

          somePromise().then(function () {
           throw new Error('oh noes');
          }).catch(function (err) {
           // I caught your error! :)
          });
          somePromise().then(function () {
           throw new Error('oh noes');
          }, function (err) {
           // I didn't catch your error! :(
          });
          

          因此,當你使用 then(resolveHandler, rejectHandler) 這種形式時,rejectHandler 并不會捕獲由 resolveHandler 引發的異常

          鑒于此,我個人的習慣是不適用 then() 的第二個參數,而是總是使用 catch()。唯一的例外是當我寫一些異步的 Mocha 測試用例時,我可能會希望用例的異常可以正確的被拋出:

          it('should throw an error', function () {
           return doSomethingThatThrows().then(function () {
           throw new Error('I expected an error!');
           }, function (err) {
           should.exist(err);
           });
          });
          

          說到這里,Mocha 和 Chai 用來測試 promise 接口時,是一對非常好的組合。 pouchdb-plugin-seed 項目中有一些 示例 可以幫助你入門。

          進階錯誤 #3:promises vs promises factories

          當我們希望執行一個個的執行一個 promises 序列,即類似 Promise.all() 但是并非并行的執行所有 promises。

          你可能天真的寫下這樣的代碼:

          function executeSequentially(promises) {
           var result=Promise.resolve();
           promises.forEach(function (promise) {
           result=result.then(promise);
           });
           return result;
          }
          

          不幸的是,這份代碼不會按照你的期望去執行,你傳入 executeSequentially() 的 promises 依然會并行執行。

          其根源在于你所希望的,實際上根本不是去執行一個 promises 序列。依照 promises 規范,一旦一個 promise 被創建,它就被執行了。因此你實際上需要的是一個 promise factories 數組。

          function executeSequentially(promiseFactories) {
           var result=Promise.resolve();
           promiseFactories.forEach(function (promiseFactory) {
           result=result.then(promiseFactory);
           });
           return result;
          }
          

          我知道你在想什么:“這是哪個見鬼的 Java 程序猿,他為啥在說 factories?” 。實際上,一個 promises factory 是十分簡單的,它僅僅是一個可以返回 promise 的函數:

          function myPromiseFactory() {
           return somethingThatCreatesAPromise();
          }
          

          為何這樣就可以了?這是因為一個 promise factory 在被執行之前并不會創建 promise。它就像一個 then 函數一樣,而實際上,它們就是完全一樣的東西。

          如果你查看上面的 executeSequentially() 函數,然后想象 myPromiseFactory 被包裹在 result.then(...) 之中,也許你腦中的小燈泡就會亮起。在此時此刻,對于 promise 你就算是悟道了。

          進階錯誤 #4:好了,如果我希望獲得兩個 promises 的結果怎么辦

          有時候,一個 promise 會依賴于另一個,但是如果我們希望同時獲得這兩個 promises 的輸出。舉例來說:

          getUserByName('nolan').then(function (user) {
           return getUserAccountById(user.id);
          }).then(function (userAccount) {
           // dangit, I need the "user" object too!
          });
          

          為了成為一個優秀的 Javascript 開發者,并且避免金字塔問題,我們可能會將 user 對象存在一個更高的作用域中的變量里:

          var user;
          getUserByName('nolan').then(function (result) {
           user=result;
           return getUserAccountById(user.id);
          }).then(function (userAccount) {
           // okay, I have both the "user" and the "userAccount"
          });
          

          這樣是沒問題的,但是我個人認為這樣做有些雜牌。我推薦的策略是拋棄成見,擁抱金字塔:

          getUserByName('nolan').then(function (user) {
           return getUserAccountById(user.id).then(function (userAccount) {
           // okay, I have both the "user" and the "userAccount"
           });
          });
          

          …至少暫時這樣是沒問題的。一旦縮進開始成為問題,你可以通過 Javascript 開發者從遠古時期就開始使用的技巧,將函數抽離到一個命名函數中:

          function onGetUserAndUserAccount(user, userAccount) {
           return doSomething(user, userAccount);
          }
          function onGetUser(user) {
           return getUserAccountById(user.id).then(function (userAccount) {
           return onGetUserAndUserAccount(user, userAccount);
           });
          }
          getUserByName('nolan')
           .then(onGetUser)
           .then(function () {
           // at this point, doSomething() is done, and we are back to indentation 0
          });
          

          由于你的 promise 代碼開始變得更加復雜,你可能發現自己開始將越來越多的函數抽離到命名函數中,我發現這樣做,你的代碼會越來越漂亮,就像這樣:

          putYourRightFootIn()
           .then(putYourRightFootOut)
           .then(putYourRightFootIn) 
           .then(shakeItAllAbout);
          

          這就是 promises 的重點。

          進階錯誤 #5:promises 穿透

          最后,這個錯誤就是我開頭說的 promises 謎題所影射的錯誤。這是一個非常稀有的用例,并且可能完全不會出現在你的代碼中,但是的的確確震驚了我。

          你認為下面的代碼會打印出什么?

          Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
           console.log(result);
          });
          

          如果你認為它會打印出 bar,那么你就錯了。它實際上打印出來的是 foo!

          發生這個的原因是如果你像 then() 傳遞的并非是一個函數(比如 promise),它實際上會將其解釋為 then(null),這就會導致前一個 promise 的結果會穿透下面。你可以自己測試一下:

          Promise.resolve('foo').then(null).then(function (result) {
           console.log(result);
          });
          

          添加任意數量的 then(null),它依然會打印 foo。

          這實際上又回到了我之前說的 promises vs promise factories。簡單的說,你可以直接傳遞一個 promise 到 then() 函數中,但是它并不會按照你期望的去執行。then() 是期望獲取一個函數,因此你希望做的最可能是:

          Promise.resolve('foo').then(function () {
           return Promise.resolve('bar');
          }).then(function (result) {
           console.log(result);
          });
          

          這樣他就會如我們所想的打印出 bar。

          因此記住:永遠都是往 then() 中傳遞函數!

          謎題揭曉

          現在我們了解了關于 promsies 所有的知識(或者接近!),我們應該可以解決文章最開始我提出的謎題了。

          這里是謎題的所有答案,我以圖形的格式展示出來方便你查看:

          Puzzle #1

          doSomething().then(function () {
           return doSomethingElse();
          }).then(finalHandler);
          

          Answer:

          doSomething
          |-----------------|
           doSomethingElse(undefined)
           |------------------|
           finalHandler(resultOfDoSomethingElse)
           |------------------|
          

          Puzzle #2

          doSomething().then(function () {
           doSomethingElse();
          }).then(finalHandler);
          

          Answer:

          doSomething
          |-----------------|
           doSomethingElse(undefined)
           |------------------|
           finalHandler(undefined)
           |------------------|
          

          Puzzle #3

          doSomething().then(doSomethingElse())
           .then(finalHandler);
          

          Answer:

          doSomething
          |-----------------|
          doSomethingElse(undefined)
          |---------------------------------|
           finalHandler(resultOfDoSomething)
           |------------------|
          

          Puzzle #4

          doSomething().then(doSomethingElse)
           .then(finalHandler);
          

          Answer:

          doSomething
          |-----------------|
           doSomethingElse(resultOfDoSomething)
           |------------------|
           finalHandler(resultOfDoSomethingElse)
           |------------------|
          

          如果這些答案你依然無法理解,那么我強烈建議你重新讀一下這篇文章,或者實現一下 doSomething() 和 doSomethingElse() 函數并且在瀏覽器中自己試試看。

          聲明:在這些例子中,我假定 doSomething() 和 doSomethingElse() 均返回 promises,并且這些 promises 代表某些在 JavaScript event loop (如 IndexedDB, network, setTimeout) 之外的某些工作結束,這也是為何它們在某些時候表現起來像是并行執行的意義。這里是一個模擬用的 JSBin。

          關于更多 promises 的進階用法,可以參考我的 promise protips cheat sheet

          關于 promises 最后的話

          Promises 是非常贊的。如果你還在使用回調模式,我強烈建議你切換到 promises。你的代碼會變的更少,更優雅,并且更加容易理解。

          如果你不相信我,這里是證明:a refactor of PouchDB’s map/reduce module,使用 promises 替換回調。結果是:新增 290 行,刪除 555 行。

          順帶一提,寫出那令人討厭的回調代碼的人。。是我!因此這是我第一次領會到 promises 的力量,同時我感謝其他 PouchDB 的貢獻者們教導我這樣做。

          當然了,promises 并非完美。雖然它的確比回調模式要好,但是這樣說就好比說給你肚子來一拳會比在你牙齒上踹一腳好。的確,它是會略有優勢,但是如果你有選擇,你會兩者都盡力避免。

          作為回調模式的升級版,promises 依然難以理解并且容易誤用,證明之一就是我不得不寫下這篇博文。初學者與專家都很容易經常將它用錯,并且真要說的話,并非是他們的問題。問題在于 promises 的使用模式與我們寫同步代碼非常類似,但是又不盡然。

          我也認為 promises 的確難以理解并且容易誤用,證明之一就是我不得不翻譯這篇博文。 譯者注

          老實說,你不應該需要去學一堆晦澀難懂的規則和新的 API 去做在同步代碼中我們已經熟稔的 return,catch,throw 和 for 循環。在你的腦中不應該總是要提醒自己要區分有兩套并行的系統。

          期待 async/await

          這是我在 “Taming the asynchronous beast with ES7” 中提到的重點,在這篇文章中我探究了 ES7 的 async/await 關鍵字,以及它們是如何將 promises 更深度的結合入語言。不再會要求我們去編寫偽同步的代碼(以及一個假的 catch() 函數,雖然像,但是并非是 catch),ES7 將會允許我們使用真正的 try/catch/return 關鍵字,就像我們在 CS 101 上學的一樣。

          這對于 Javascript 語言來說是一個大福音。因為即使到最后,只要我們的工具不告訴我們做錯了,這些 promise 反模式依然會一直出現。

          從 JavaScript 的歷史來看,我認為公正的評價來說 JSLint 與 JSHint 對社區的貢獻是高于 JavaScript: The Good Parts 的,雖然他們包含的信息實際上是相同的。但是它們的區別在于 被告知你在你代碼中犯的錯誤 與你去閱讀一本書籍,去理解其他人犯的錯誤。

          ES7 的 async/await 的美妙在于,你的錯誤會被作為語法或者編譯器錯誤提示出來,而不是運行時的 bug。不過就目前而言,了解 promise 可以做什么以及如何在 ES5 與 ES6 中正確的使用它們依然是有必要的。

          因此當我意識到,就像 JavaScript: The Good Parts 一樣,這篇博文可能只會有非常有限的影響的時候,我希望當你發現其他人在犯同樣的錯誤的時候,你可以將這篇博文提供給他們。因為現在依然有很多同學需要承認: “I have a problem with promises!”

          更新:有人告知我 Bluebird 3.0 將會 打印警告 來避免我文中所列舉的這些錯誤。因此當我們還在等待 ES7 時,使用 Bluebird 會是另一個極好的方案。

          希望本文能幫助到您!

          點贊+轉發,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)

          關注 {我},享受文章首發體驗!

          每周重點攻克一個前端技術難點。更多精彩前端內容私信 我 回復“教程”

          原文鏈接:http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/

          作者:fangshi

          020年,仰韶遺址最新考古發現了距今5000多年前的“疑似水泥”混凝土。

          https://www.hqsbwx.com/read/773.html(參考消息網)

          其實在早些年的甘肅大地灣遺址、湖北棗陽雕龍碑遺址都發現了幾千年前類似的混凝土。

          很多人一聽混凝土,腦海里會浮現出一個畫面:一堆砂子、一堆石子、幾袋水泥和一桶水,一臺?攪拌車?。


          那是水泥,和混凝土,不是一回事。

          水泥發明于19世紀初,因為當時主要的建筑材料還是天然石材,而世界上最好的石材產自波特蘭,商家就起了個“歪心眼”,把這種水泥叫做“波特蘭水泥”。

          波特蘭石。和水泥有點像

          混凝土的使用最早可以追溯到9000年前。包括中國好幾個遺址都發現了幾千年的混凝土。

          所以不要一聽到混凝土就以為是現代的產物。

          七千年前的以色列加利利城,地板使用的就是“水泥”

          那關于金字塔為“現代”混凝土建造的說法,是怎么出來的呢?

          這就要介紹下“古代”混凝土的發現者。不是發明者。

          在20世紀80年代,法國有個工程師叫約瑟夫·大衛多維斯,發現了一種由硅鋁酸鹽礦物,骨料和堿性溶液攪拌生成的硅鋁酸鹽膠凝性水泥。他申請了專利。并稱之為“地質聚合物”。

          不過大衛多維斯提同志本著“喝水不挖井人”的精神,把榮譽給了古埃及人,稱自己只是“地質聚合物”的再發明人。

          為什么這么說呢,因為他認為:

          古埃及人已經掌握了這種技術,他們的技術如此高超導致那些人造石頭外觀看起來跟普通石灰石沒有什么區別。


          https://www.eyeofthepsychic.com/davidovits/

          麻省理工學院材料學教授林恩·霍布斯也說:

          “古埃及人可能是偉大的材料科學家,而不僅僅是偉大的土木工程師,這真是令人著迷。”

          “這些都沒有削弱古埃及人的成就,盡管我認為澆筑混凝土不如搬運巨石那么神秘,但這實際上只是表明這些人的成就比任何人想象的都要多。”

          從他們的理論可以看出: 并沒有懷疑金字塔的古老,只是認為那不是天然的巖石,而是古代發明的一種混凝土澆筑的。就類似于我們國內發現的那種幾千年前的混凝土。

          混凝土的說法對不對呢?咱們后面說,但“混凝土”這三個字給“民科”歷史發明家們聽到了。

          平時我們都會說謊,有沒有發現,最高明的人說謊,十句話里面九句是真話,但關鍵的一句甚至只要兩個字是假的。效果就完全不一樣了。

          “歷史發明家”就利用大家的一種習慣性思維—混凝土是現代發明的。

          順其自然的加了兩個字“現代”,現代混凝土。

          移花接木,“畫龍點睛”。

          歷史發明家的“先驅”福緬科出場了,他的邏輯和現在國內“義和團們”的邏輯驚人的一致:

          俄羅斯的都是最早的。如果其他國家有更早的,那就是假的。(比如金字塔,還有長城)

          再后面有這個“現代混凝土”說法的,就有想出書的,想蹭流量的。

          不過有些人比如黃教授,就不能說他是別有用心了,因為他引用的論據居然還是大衛多維斯的申明。

          怎么能用反方的論據來證明自己的觀點呢?

          我只能善意的理解為,黃教授可能沒看完那個申明。

          黃教授還有個觀點:他認為獅身人面像也是現代偽造的。證據是幾百年前的很多畫,畫中的獅身人面像和現在的不一樣。

          黃教授是搞藝術的,所以拿出來的是畫,不是照片。

          但從這些事反而能看出黃教授是個寬厚長者,不是別有心機的,就像孫衛東教授一樣,國內同位素方面權威。在某天,

          孫衛東教授對出土自殷墟的青銅器進行同位素檢測,發現殷商時期青銅的放射性與埃及的青銅特性十分相似,而與中國國產礦石的相似性卻較低。

          孫教授結合《史記》里記載,夏朝有條南向北流動的河流,而世界上唯一從南向北流動的大河流就是尼羅河。

          所以孫教授“腦洞一開 ”,夏朝在古埃及。

          孫教授和黃教授一樣,都是從自己擅長的領域切入到一個不是自己專業的學科,難免會鬧出點小笑話,情有可原。

          聽說現在還有人拿著“埃及說”的資料去找孫衛東教授探討,給他一一轟出去了。

          那到底金字塔是不是混凝土做的呢?

          當然不是。

          先不說那些考古遺址比如采石場遺址,沒切割的大塊石料,人口開采的痕跡,大量石塊的不規則性等等。

          胡夫金字塔旁的大采石場??????


          金字塔外面的覆面石就來之圖拉采石場。????


          我們?不?說??“一眼假?,一眼真”之類的?話?。

          相信科學?。

          ??看看?2008年Ioannis Liritzis教授對??吉薩建筑樣品的礦物學特征以及化學成分和結構分析:

          化石的完整性、化石分布情況與其在海底流體環境中的堆積是一致的。他們用X射線檢測不到聚合物所用的材料的存在。

          論文地址:

          https://www.academia.edu/7352152/MINERALOGICAL_ANALYSIS_OF_EGYPTIAN_MONUMENTS_FROM_GIZA_CASTING_OR_CARVING

          另有學者反駁大衛多維斯:

          地聚物配方需要生石灰?,草木灰加碳酸鈉產生強堿氫氧化鈉和氫氧化鉀,然后繼續與其中的硅鋁組分發生反應產生最終生成硅鋁酸鹽人造石。

          而要?生產如此多生石灰和草木灰,耗費的?燃料極其?巨大。

          草木灰只占植物燃燒前干重的0.43%到1.82%,碳酸鉀只占草木灰的10%。

          而北非沙漠?地區?缺少木材,燒陶都?只能用麥秸、谷皮、家畜糞、菅草等植物。

          所以?古埃及王國沒有足夠的燃料來支持大規模的燒磚,平時?大都??用曬干的粘土坯,更別說大量地去?燒石灰了(煅燒溫度900度)。

          大衛多?維護斯還??有很多?“有趣”的?觀點?,比如?,

          他認為古埃及人大量砍伐樹木燒造石灰導致埃及生態嚴重惡化,大金字塔造完后就燃料罄盡再也無法制造那么大的金字塔。

          他干脆擬定了一份從?第四王朝至十二王朝,地聚物技術的發明誕生、發展與衰落的具體時間表。

          但埃及考古從來沒有發現所謂用來制造地聚物的水池等?遺址?。

          另外?他也對?古埃及?文字?產生了?興趣?,不過,?他?對古埃及碑銘的解讀被埃及學家視為無稽之談。

          大衛?多維斯?還?聲稱在?金字塔石頭里檢驗出了頭發,但是這么多年過去了?,從來沒有得到任何科學機構的獨立驗證,他也沒有提供過任何所謂石頭里的頭發樣本和資料。

          總之,經過?他?那么多年“?折騰”,?很多人?認為?他孜孜不倦搞金字塔的目的,只?是給他的地聚物技術和地聚物研究所做?廣告?,宣傳造勢?罷了?。


          主站蜘蛛池模板: 国产成人无码一区二区在线观看 | 亚洲国产精品一区二区久久| 国产一区二区精品久久岳| 国产激情视频一区二区三区| 日韩精品福利视频一区二区三区| 中文字幕一区二区三区日韩精品 | 无码人妻精品一区二区| 国内精自品线一区91| 2021国产精品一区二区在线| 久久免费精品一区二区| 亚洲AV无码一区二区三区国产| 国产精品美女一区二区三区| 精品午夜福利无人区乱码一区| 熟妇人妻AV无码一区二区三区| 精品伦精品一区二区三区视频 | 日韩精品无码Av一区二区| 国语对白一区二区三区| 日韩在线视频一区二区三区 | 国产精品一区二区三区99| 精品一区二区三区水蜜桃| 国产精品视频无圣光一区| 日本一区免费电影| 日韩视频一区二区在线观看| 国产精品视频一区国模私拍| 免费高清av一区二区三区| 国产精品一区二区三区高清在线 | 国产成人无码一区二区在线播放| 成人精品视频一区二区三区| 国产一区二区在线观看app| 成人日韩熟女高清视频一区| 亚洲色大成网站www永久一区| 国产婷婷色一区二区三区| 性无码一区二区三区在线观看| 消息称老熟妇乱视频一区二区| 亚洲一区综合在线播放| 亚洲视频一区网站| 无码乱人伦一区二区亚洲| 无码人妻精品一区二区三区东京热| 精品无码一区二区三区爱欲| 亚洲愉拍一区二区三区| 手机福利视频一区二区|