整合營銷服務商

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

          免費咨詢熱線:

          1.7萬個JavaScript文件,Etsy大型代碼庫如何完成向TypeScript遷移

          tsy 的 Web 平臺團隊在過去幾年中花費了大量時間來更新我們的前端代碼。僅在一年半以前,我們才將 JavaScript 構建系統現代化 ,以實現更高級的特性,比如 箭頭函數 和 類 ,從 2015 年起,它們被添加到了這個語言中。盡管這個升級意味著我們對代碼庫的未來驗證已經完成,并且可以編寫出更加習慣化、更可擴展的 JavaScript,但是我們知道還有改進的空間。

          Etsy 已經有十六年的歷史了。自然地,我們的代碼庫變得相當龐大; Monorepo (單體倉庫)擁有超過 17000 個 JavaScript 文件,并且跨越了網站的很多迭代。如果開發者使用我們的代碼庫,很難知道哪些部分仍被視為最佳實踐,哪些部分遵循傳統模式或者被視為技術債務。JavaScript 語言本身使得這個問題更加復雜:雖然在過去的幾年里,為該語言增加了新的語法特性,但是 JavaScript 非常靈活,對如何使用也沒有多少可強制性的限制。這樣,在編寫 JavaScript 時,如果沒有事先研究依賴關系的實現細節,就很有挑戰性。盡管文檔在某種程度上有助于減輕這個問題,但是它只能在很大程度上防止 JavaScript 庫的不當使用,從而最終導致不可靠的代碼。

          所有這些問題(還有更多?。┒际俏覀冋J為 TypeScript 可能為我們解決的問題。TypeScript 自稱是“JavaScript 的超集”。換言之,TypeScript 就是 JavaScript 中的一切,可以選擇增加類型。類型從根本上來說,在編程中,類型是通過代碼移動的數據的期望的方式:函數可以使用哪些類型的輸入,變量可以保存哪些類型的值。(如果你不熟悉類型的概念,TypeScript 的手冊有一個 很好的介紹 )。

          TypeScript 被設計成可以很容易地在已有的 JavaScript 項目中逐步采用,尤其是在大型代碼庫中,而轉換成一種新的語言是不可能的。它非常擅長從你已經編寫好的代碼中推斷出類型,并且其類型語法細微到足以正確地描述 JavaScript 中普遍存在的“怪癖”。此外,它由微軟開發,已被 Slack 和 Airbnb 等公司使用,根據 去年的“State of JavaScript”調查 ,它是迄今為止使用最多、最流行的 JavaScript。若要使用類型來為我們的代碼庫帶來某種秩序,TypeScript 看起來是一個非??煽康馁€注。

          因此,在遷移到 ES6 之后,我們開始研究采用 TypeScript 的路徑。本文將講述我們如何設計我們的方法,一些有趣的技術挑戰,以及如何使一家 Etsy 級別的公司學習一種新的編程語言。

          在高層次上采用 TypeScript

          我并不想花太多時間向你安利 TypeScript,因為在這方面還有很多其他的 文章 和 講座 ,都做得非常好。相反,我想談談 Etsy 在推出 TypeScript 支持方面所做的努力,這不僅僅是從 JavaScript 到 TypeScript 的技術實現。這也包括許多規劃、教育和協調工作。但是如果把細節弄清楚,你會發現很多值得分享的學習經驗。讓我們先來討論一下我們想要的采用是什么樣的做法。

          采用策略

          TypeScript 在檢查代碼庫中的類型時,可能多少有點“嚴格”。據 TypeScript 手冊 所述,一個更嚴格的 TypeScript 配置 “能更好地保證程序的正確性”,你可以根據自己的設計,根據自己的需要逐步采用 TypeScript 的語法及其嚴格性。這個特性詩 TypeScript 添加到各種代碼庫中成為可能,但是它也使“將文件遷移到 TypeScript”成為一個定義松散的目標。很多文件需要用類型進行注釋,這樣 TypeScript 就可以完全理解它們。還有許多 JavaScript 文件可以轉換成有效的 TypeScript,只需將其擴展名從 .js 改為 .ts 即可。但是,即使 TypeScript 對文件有很好的理解,該文件也可能會從更多的特定類型中獲益,從而提高其實用性。

          各種規模的公司都有無數的文章討論如何遷移到 TypeScript,所有這些文章都對不同的遷移策略提出了令人信服的論點。例如,Airbnb 盡可能地 自動化 了他們的遷移。還有一些公司在他們的項目中啟用了較不嚴格的 TypeScript,隨著時間的推移在代碼中添加類型。

          確定 Etsy 的正確方法意味著要回答一些關于遷移的問題:

          • 我們希望 TypeScript 的味道有多嚴格?
          • 我們希望遷移多少代碼庫?
          • 我們希望編寫的類型有多具體?

          我們決定將嚴格性放在第一位;采用一種新的語言需要付出大量的努力,如果我們使用 TypeScript,我們可以充分利用其類型系統(此外,TypeScript 的檢查器在更嚴格的類型上 執行得更好 )。我們還知道 Etsy 的代碼庫相當龐大;遷移每個文件可能并不能充分利用我們的時間,但是確保我們擁有類型用于我們網站的新的和經常更新的部分是很重要的。當然,我們也希望我們的類型盡可能有用,容易使用。

          我們采用的是什么?

          以下是我們的采用策略:

          1. 使 TypeScript 盡可能地嚴格,并逐個文件地移植代碼庫。
          2. 添加真正優秀的類型和真正優秀的支持文檔,包括產品開發者常用的所有實用程序、組件和工具。
          3. 花時間教工程師們學習 TypeScript,并讓他們逐個團隊地啟用 TypeScript 語法。

          讓我們再仔細看看這幾點吧。

          逐步遷移到嚴格的 TypeScript

          嚴格的 TypeScript 能夠避免許多常見的錯誤,所以我們認為最合理的做法是盡量嚴格的。這一決定的缺點是我們現有的大多數 JavaScript 都需要類型注釋。它還需要以逐個文件的方式遷移我們的代碼庫。使用嚴格的 TypeScript,如果我們嘗試一次轉換所有的代碼,我們最終將會有一個長期的積壓問題需要解決。如前所述,我們在單體倉庫中有超過 17000 個 JavaScript 文件,其中很多都不經常修改。我們選擇把重點放在那些在網站上積極開發的區域,明確地區分哪些文件具有可靠的類型,以及哪些文件不使用 .js 和 .ts 文件擴展名。

          一次完全遷移可能在邏輯上使改進已有的類型很難,尤其是在單體倉庫模式中。當導入 TypeScript 文件時,出現被禁止的類型錯誤,你是否應該修復此錯誤?那是否意味著文件的類型必須有所不同才能適應這種依賴關系的潛在問題?哪些具有這種依賴關系,編輯它是否安全?就像我們的團隊所知道的,每個可以被消除的模糊性,都可以讓工程師自己作出改進。在增量遷移中,任何以 .ts 或 .tsx 結尾的文件都可以認為存在可靠的類型。

          確保實用程序和工具有良好的 TypeScript 支持

          當我們的工程師開始編寫 TypeScript 之前,我們希望我們所有的工具都能支持 TypeScript,并且所有的核心庫都有可用的、定義明確的類型。使用 TypeScript 文件中的非類型化依賴項會使代碼難以使用,并可能會引入類型錯誤;盡管 TypeScript 會盡力推斷非 TypeScript 文件中的類型,但是如果無法推斷,則默認為“any”。換句話說,如果工程師花時間編寫 TypeScript,他們應該能夠相信,當他們編寫代碼的時候,語言能夠捕捉到他們所犯的類型錯誤。另外,強制工程師在學習新語言和跟上團隊路線圖的同時為通用實用程序編寫類型,這是一種讓人們反感 TypeScript 的好方法。這項工作并非微不足道,但卻帶來了豐厚的回報。在下面的“技術細節”一節中,我將對此進行詳細闡述。

          逐個團隊地進行工程師適職培訓

          我們已經花了很多時間在 TypeScript 的教育上,這是我們在遷移過程中所做的最好的決定。Etsy 有數百名工程師,在這次遷移之前,他們幾乎沒有 TypeScript 的經驗(包括我)。我們知道,要想成功地遷移,人們首先必須學習如何使用 TypeScript。打開這個開關,告訴所有人都要這么做,這可能會使人們迷惑,使我們的團隊被問題壓垮,也會影響我們產品工程師的工作速度。通過逐步引入團隊,我們能夠努力完善工具和教學材料。它還意味著,沒有任何工程師能在沒有隊友能夠審查其代碼的情況下編寫 TypeScript。逐步適職使我們的工程師有時間學習 TypeScript,并把它融入到路線圖中。

          技術細節

          在遷移過程中,有很多有趣的技術挑戰。令人驚訝的是,采用 TypeScript 的最簡單之處就是在構建過程中添加對它的支持。在這個問題上,我不會詳細討論,因為構建系統有許多不同的風格,但簡單地說:

          • 用 Webpack 來構建我們的 JavaScript 。Webpack 使用 Babel 將我們的現代 JavaScript 移植到更古老、更兼容的 JavaScript。
          • Babel 有個可愛的插件 babel-preset-typescript ,可以快速地將 TypeScript 轉換成 JavaScript,但希望你能自己進行類型檢查。
          • 要檢查我們的類型,我們運行 TypeScript 編譯器作為我們測試套件的一部分,并配置它不 使用 noEmit 選項 來實際轉譯任何文件。

          上面所做的工作花費了一到兩個星期,其中大部分時間是用于驗證我們發送到生產中的 TypeScript 是否會發生異常行為。在其他 TypeScript 工具上,我們花費了更多的時間,結果也更有趣。

          使用 typescript-eslint 提高類型特異性

          我們在 Etsy 中大量使用了自定義的 ESLint Lint 規則。它們為我們捕捉各種不良模式,幫助我們廢除舊代碼,并保持我們的 pull request(拉取請求)評論不跑題,沒有吹毛求疵。如果它很重要,我們將嘗試為其編寫一個 Lint 規則。我們發現,有一個地方可以利用 Lint 規則的機會,那就是強化類型特異性,我一般用這個詞來表示“類型與所描述的事物之間的精確匹配程度”。

          舉例來說,假設有一個函數接受 HTML 標簽的名稱并返回 HTML 元素。該函數可以將任何舊的字符串作為參數接受,但是如果它使用這個字符串來創建元素,那么最好能夠確保該字符串實際上是一個真正的 HTML 元素的名稱。

          // This function type-checks, but I could pass in literally any string in as an argument.
          function makeElement(tagName: string): HTMLElement {
             return document.createElement(tagName);
          }
          // This throws a DOMException at runtime
          makeElement("literally anything at all");

          假如我們努力使類型更加具體,那么其他開發者將更容易正確地使用我們的函數。

          // This function makes sure that I pass in a valid HTML tag name as an argument.
          // It makes sure that ‘tagName’ is one of the keys in 
          // HTMLElementTagNameMap, a built-in type where the keys are tag names 
          // and the values are the types of elements.
          function makeElement(tagName: keyof HTMLElementTagNameMap): HTMLElement {
             return document.createElement(tagName);
          }
          // This is now a type error.
          makeElement("literally anything at all");
          // But this isn't. Excellent!
          makeElement("canvas");

          遷移到 TypeScript 意味著我們需要考慮和解決許多新實踐。 typescript-eslint 項目給了我們一些 TypeScript 特有的規則,可供我們利用。例如, ban-types 規則允許我們警告不要使用泛型 Element 類型,而使用更具體的 HTMLElement 類型。

          此外,我們也作出了一個(有一點爭議)決定,在我們的代碼庫中 允許使用 非空斷言 和 類型斷言 。前者允許開發者告訴 TypeScript,當 TypeScript 認為某物可能是空的時候,它不是空的,而后者允許開發者將某物視為他們選擇的任何類型。

          // This is a constant that might be ‘null’.
          const maybeHello = Math.random() > 0.5 ? "hello" : null;
          // The `!` below is a non-null assertion. 
          // This code type-checks, but fails at runtime.
          const yellingHello = maybeHello!.toUpperCase()
          // This is a type assertion.
          const x = {} as { foo: number };
          // This code type-checks, but fails at runtime.
          x.foo;

          這兩種語法特性都允許開發者覆蓋 TypeScript 對某物類型的理解。很多情況下,它們都意味著某種類型更深層次問題,需要加以修復。消除這些類型,我們強迫這些類型對于它們所描述得更具體。舉例來說,你可以使用“ as ”將 Element 轉換為 HTMLElement ,但是你可能首先要使用 HTMLElement。TypeScript 本身無法禁用這些語言特性,但是 Lint 使我們能夠識別它們并防止它們被部署。

          作為防止人們使用不良模式的工具,Lint 確實非常有用,但是這并不意味著這些模式是普遍不好的:每個規則都有例外。Lint 的好處在于,它提供了合理的逃生通道。在任何時候,如果確實需要使用“as”,我們可以隨時添加一次性的 Lint 例外。

          // NOTE: I promise there is a very good reason for us to use `as` here.
          // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
          const x = {} as { foo: number };

          將類型添加到 API

          我們希望我們的開發者能夠編寫出有效的 TypeScript 代碼,所以我們需要確保為盡可能多的開發環境提供類型。乍一看,這意味著將類型添加到可重用設計組件、輔助實用程序和其他共享代碼中。但是理想情況下,開發者需要訪問的任何數據都應該有自己的類型。幾乎我們網站上所有的數據都是通過 Etsy API 實現的,所以如果我們能在那里提供類型,我們很快就可以涵蓋大部分的代碼庫。

          Etsy 的 API 使用 PHP 實現的,并且我們為每個端點生成 PHP 和 JavaScript 配置,從而幫助簡化請求的過程。在 JavaScript 中,我們使用一個輕量級封裝 EtsyFetch 來幫助處理這些請求。這一切看起來就是這樣的:

          // This function is generated automatically.
          function getListingsForShop(shopId, optionalParams = {}) {
             return {
                 url: `apiv3/Shop/${shopId}/getLitings`,
                 optionalParams,
             };
          }
          // This is our fetch() wrapper, albeit very simplified.
          function EtsyFetch(config) {
             const init = configToFetchInit(config);
             return fetch(config.url, init);
          }
          // Here's what a request might look like (ignoring any API error handling).
          const shopId = 8675309;
          EtsyFetch(getListingsForShop(shopId))
             .then((response) => response.json())
             .then((data) => {
                 alert(data.listings.map(({ id }) => id));
             });

          在我們的代碼庫中,這種模式是非常普遍的。如果我們沒有為 API 響應生成類型,開發者就得手工寫出它們,并且想讓它們與實際的 API 同步。我們需要嚴格的類型,但是我們也不希望我們的開發者為了得到這些類型而折騰。

          最后,我們在 開發者 API 上做了一些工作,將端點轉換成 OpenAPI 規范 。OpenAPI 規范是以類似 JSON 等格式描述 API 端點的標準化方式。雖然我們的開發者 API 使用了這些規范來生成面向公共的文檔,但是我們也可以利用它們生成用于 API 的響應的 TypeScript 類型。在編寫和改進 OpenAPI 規范生成器之前,我們已經花費了大量的時間來編寫和改進,它可以適用于我們所有的內部 API 端點,然后使用一個名為 openapi-typescript 的庫,將這些規范轉換成 TypeScript 類型。

          在為所有端點生成 TypeScript 類型之后,仍然需要以一種可利用的方式將它們整合到代碼庫中。我們決定將生成的響應類型編入我們所生成的配置中,然后更新 EtsyFetch,以便在它返回的 Promise 中使用這些類型。把所有這些放在一起,看起來大致是這樣的:

          // These types are globally available:
          interface EtsyConfig<JSONType> {
             url: string;
          }
          interface TypedResponse<JSONType> extends Response {
             json(): Promise<JSONType>;
          }
          // This is roughly what a generated API config file looks like:
          import OASGeneratedTypes from "api/oasGeneratedTypes";
          type JSONResponseType = OASGeneratedTypes["getListingsForShop"];
          function getListingsForShop(shopId): EtsyConfig<JSONResponseType> {
             return {
                 url: `apiv3/Shop/${shopId}/getListings`,
             };
          }
          // This is (looooosely) what EtsyFetch looks like:
          function EtsyFetch<JSONType>(config: EtsyConfig<JSONType>) {
             const init = configToFetchInit(config);
             const response: Promise<TypedResponse<JSONType>> = fetch(config.url, init);
             return response;
          }
          // And this is what our product code looks like:
          EtsyFetch(getListingsForShop(shopId))
             .then((response) => response.json())
             .then((data) => {
                 data.listings; // "data" is fully typed using the types from our API
             });

          這一模式的結果非常有用。目前,對 EtsyFetch 的現有調用具有開箱即用的強類型,不需要進行更改。而且,如果我們更新 API 的方式會引起客戶端代碼的破壞性改變,那么類型檢查器就會失敗,而這些代碼將永遠不會出現在生產中。

          鍵入我們的 API 還為我們提供了機會,使我們可以在后端和瀏覽器之間使用 API 作為唯一的真相。舉例來說,如果我們希望確保支持某個 API 的所有區域都有一個標志的表情符號,我們可以使用以下類型來強制執行:

          type Locales  OASGeneratedTypes["updateCurrentLocale"]["locales"];
          const localesToIcons : Record<Locales, string> = {
             "en-us": ":us:",
             "de": ":de:",
             "fr": ":fr:",
             "lbn": ":lb:",
             //... If a locale is missing here, it would cause a type error.
          }

          最重要的是,這些特性都不需要改變我們產品工程師的工作流程。他們可以免費使用這些類型,只要他們使用他們已經熟悉的模式。

          通過分析我們的類型來改善開發體驗

          推出 TypeScript 的一個重要部分是密切關注來自我們工程師的投訴。在我們進行遷移的早期階段,有人提到過在提供類型提示和代碼完成時,他們的編輯器很遲鈍。例如,一些人告訴我們,當鼠標懸停在一個變量上時,他們要等半分鐘才能顯示出類型信息。考慮到我們可以在一分鐘內對所有的 TS 文件運行類型檢查器,這個問題就更加復雜了;當然,單個變量的類型信息也不應該這么昂貴。

          幸運的是,我們和一些 TypeScript 項目的維護者舉行了一次會議。他們希望看到 TypeScript 能夠在諸如 Etsy 這樣獨特的代碼庫上獲得成功。對于我們在編輯器上的挑戰,他們也很驚訝,而且更讓他們吃驚的是,TypeScript 花了整整 10 分鐘來檢查我們的整個代碼庫,包括未遷移的文件和所有文件。

          在反復討論后,確定我們沒有包含超出需求的文件后,他們向我們展示了當時他們剛剛推出的性能跟蹤功能。跟蹤結果表明,當對未遷移的 JavaScript 文件進行類型檢查時,TypeScript 就會對我們的一個類型出現問題。以下是該文件的跟蹤(這里的寬度代表時間)。

          結果是,類型中存在一個循環依賴關系,它幫助我們創建不可變的對象的內部實用程序。到目前為止,這些類型對于我們處理的所有代碼來說都是完美無缺的,但在代碼庫中尚未遷移的部分,它的一些使用卻出現了問題,產生了一個無限的類型循環。如果有人打開了代碼庫的這些部分文件,或者在我們對所有代碼運行類型檢查器時,就會花很多時間來嘗試理解該類型,然后放棄并記錄類型錯誤。修復了這個類型之后,檢查一個文件的時間從將近 46 秒減少到了不到 1 秒。

          這種類型在其他地方也會產生問題。當進行修正之后,檢查整個代碼庫的時間大約為此前的三分之一,并且減少了整整 1GB 的內存使用。

          如果我們沒有發現這個問題,那么它最終將導致我們的測試(以及生產部署)速度更慢。它還會使每個人在編寫 TypeScript 的時候非常非常不愉快。

          教育

          采用 TypeScript 的最大障礙,無疑是讓大家學習 TypeScript。類型越多的 TypeScript 就越好。假如工程師對編寫 TypeScript 代碼感到不適應,那么完全采用這種語言將是一場艱難的斗爭。就像我在上面提到的,我們決定逐個團隊推廣是建立某種制度化的 TypeScript 的最佳方式。

          基礎

          我們通過直接與少數團隊合作來開始我們的推廣工作。我們尋找那些即將開始新項目并且時間相對靈活的團隊,并詢問他們是否對用 TypeScript 編寫項目感興趣。在他們工作的時候,我們唯一的工作就是審查他們的拉取請求,為他們需要的模塊實現類型,并在他們學習時與他們配對。

          在此期間,我們可以完善自己的類型,并且為 Etsy 代碼庫中難以處理的部分開發專門的文檔。由于只有少數工程師正在編寫 TypeScript,所以很容易從他們那里得到直接的反饋,并迅速解決他們遇到的問題。這些早期的團隊為我們提供了很多 Lint 規則,這可以確保我們的文檔清晰、有用。它還為我們提供了足夠的時間來完成遷移的技術部分,比如向 API 添加類型。

          讓團隊接受教育

          當我們感覺大多數問題已經解決后,我們決定讓任何有興趣和準備好的團隊加入。為使團隊能夠編寫 TypeScript,我們要求他們先完成一些培訓。我們從 ExecuteProgram 找到了一門課程,我們認為這門課程以互動和有效的方式,很好地教授了 TypeScript 的基礎知識。當我們認為團隊的所有成員都需要完成這門課程(或具有一定的同等經驗),我們才能認為他們準備好了。

          我們努力使人們對 TypeScript 非常感興趣,以吸引更多的人參加互聯網上的課程。我們與 Dan Vanderkam 取得了聯系,他是《 Effective TypeScript 》(暫無中譯本)的作者,我們想知道他是否對做一次內部講座感興趣(他答應了,他的講座和書都非常棒)。此外,我還設計了一些非常高質量的虛擬徽章,我們會在課程作業的期中和期末發給大家,以保持他們的積極性(并關注大家學習 TypeScript 的速度)。

          然后我們鼓勵新加入的團隊騰出一些時間遷移他們團隊負責的 JS 文件。我們發現,遷移你所熟悉的文件是學習如何使用 TypeScript 的一個好方法。這是一種直接的、親手操作類型的方式,然后你可以馬上在別處使用。實際上,我們決定不使用更復雜的自動遷移工具( 比如 Airbnb 寫的那個 ),部分原因是它剝奪了一些學習機會。另外,一個有一點背景的工程師遷移文件的效率比腳本要高。

          逐個團隊推廣的后勤保障

          一次一個團隊的適職意味著我們必須防止個別工程師在其團隊其他成員準備就緒之前編寫 TypeScript。這種情況比你想象的要多;TypeScript 是一種非??岬恼Z言,人們都渴望去嘗試它,尤其是在看到代碼庫中使用它后。為了避免這種過早地采用,我們編寫了一個簡單的 git 提交鉤子,禁止不屬于安全列表的用戶修改 TypeScript。當一個團隊準備好時,我們只需將他們加入到安全列表即可。

          此外,我們努力與每一個團隊的工程師經理發展直接交流。將電子郵件發送到整個工程部門很容易,但是和每一個經理密切合作可以確保沒有人對我們的推出感到驚訝。它還給了我們一個機會來解決團隊所關心的問題,比如學習一門新語言。尤其在大公司中,強制要求變更可能是一種負擔,雖然直接的溝通層很小,但會有很大的幫助(即使它需要一個相當大的電子表格來跟蹤所有的團隊)。

          適職后支持團隊

          事實證明,審查 PR 是早期發現問題的一種很好的方法,并為以后 Lint 規則的制定提供了許多參考。為有助于遷移,我們決定對包含 TypeScript 的每個 PR 進行明確的審查,直到推廣順利。我們將審查的范圍擴大到語法本身,并隨著我們的發展,向那些已經成功適職的工程師尋求幫助。我們將這個小組稱為 TypeScript 顧問,他們是新晉 TypeScript 工程師的重要支持來源。

          在推廣過程中最酷的一個方面就是很多學習過程是如何有機進行的。有些小組舉行了大型的結對會議,他們共同解決問題,或者嘗試遷移文件,我們并不知道。一些小組甚至建立了讀書會來閱讀 TypeScript 書籍。這類遷移確實需要付出大量的努力,但是我們很容易忘記,其中有多少工作是由熱情的同事和隊友完成的。

          我們現在在哪里?

          在今秋早些時候,我們已經開始要求使用 TypeScript 編寫所有新文件。大概有 25% 的文件是類型,這個數字還不包括被丟棄的特性、內部工具和死代碼。到撰寫本文時,每一個團隊都已成功地使用 TypeScript。

          “完成向 TypeScript 的遷移”并不是一個明確的定義,特別是對于大型代碼庫而言。盡管我們可能還會有一段時間在我們的倉庫中沒有類型的 JavaScript 文件,但從現在開始,我們的每一個新特性都將進行類型化。撇開這些不談,我們的工程師已經在有效地編寫和使用 TypeScript,開發自己的工具,就類型展開深思熟慮的討論,分享他們認為有用的文章和模式。雖然很難說,但是人們似乎很喜歡一種去年這個時候幾乎沒人用過的語言。對于我們來說,這是一次成功的遷移。

          html網頁上傳到網站根目錄下

          本人通過寶塔面板上傳,wordpress的根目錄一般為/www/wwwroot/wordpress/,表示網址打開的網站就在此目錄下。如果為單個網頁,只要將html網頁上傳到此目錄下即可,假設網頁為10.html,直接放到wordpress根目錄,網址就是www.xxx.com/10.html。如果為多個網頁,那么可以在根目錄下新建一個文件夾,再將網頁上傳到此新建文件夾下,網址中增加文件夾名。

          如上圖所示,我在根目錄下新建文件夾suanshu,然后再將10.thml、20.html和100.html 這3個網頁上傳到suanshu文件夾,然后網址分別為www.joephy.com/suanshu/10.html

          www.joephy.com/suanshu/20.html

          www.joephy.com/suanshu/100.html

          另外,在wordprss后臺,可以通過菜單的方式將網址添加到菜單項方便使用。

          S6 Modules (Browsers)

          瀏覽器從Safari 10.1、Chrome 61、Firefox 60和Edge 16開始支持直接加載ECMAScript模塊(不需要Webpack等工具)。

          不需要使用Node.js的擴展名;瀏覽器完全無視模塊/腳本的文件擴展名。

          <script type="module">
            import { hello } from './hello.js'; 
            hello('world');
          </script>
          export function hello(text) {
            const div = document.createElement('div');
            div.textContent = `Hello ${text}`;
            document.body.appendChild(div);
          }

          ES6 Modules (Browsers+Dynamic)

          腳本根據需要動態加載其他的js文件。

          <script type="module">
            import('hello.js').then(module => {
                module.hello('world');
              });
          </script>

          ES6 Modules (Node.js)

          module.exports = {
             hello: function() {
                return "Hello";
             }
          }
          const myModule = require('./mymodule');
          let val = myModule.hello(); // val is "Hello"   

          Fetch

          fetchInject([
            'https://cdn.jsdelivr.net/momentjs/2.17.1/moment.min.js'
          ]).then(() => {
            console.log(`Finish in less than ${moment().endOf('year').fromNow(true)}`)
          })

          Script標簽加載

          HTML中添加一個帶有腳本URL的腳本標簽。

          腳本甚至可以在不同的服務器上(),<script>標簽注入到網頁的<head>中,或者插入到關閉的</body>標簽之前。


          主站蜘蛛池模板: 久久毛片一区二区| 精品一区二区三区免费毛片爱| 国产成人无码精品一区二区三区| 国产精品香蕉在线一区| 手机看片一区二区| 亚洲国产成人久久一区WWW| 精品乱人伦一区二区三区| 亚洲国产国产综合一区首页| 无码人妻aⅴ一区二区三区有奶水| 国产伦精品一区二区免费| 中文字幕无线码一区| 亚洲av不卡一区二区三区| 久久人妻内射无码一区三区| 乱精品一区字幕二区| 亚洲sm另类一区二区三区| 91一区二区在线观看精品| 狠狠综合久久AV一区二区三区 | 日韩人妻一区二区三区免费 | 亚洲日韩精品国产一区二区三区| 性色AV一区二区三区无码| 波多野结衣的AV一区二区三区| 3d动漫精品啪啪一区二区免费| 国产精品视频无圣光一区| 无码人妻精品一区二区三区久久| 精品国产亚洲第一区二区三区| 亚洲综合无码AV一区二区 | 免费无码AV一区二区| 亚洲国产精品综合一区在线 | 亚洲av日韩综合一区在线观看| 波多野结衣电影区一区二区三区 | 人妻少妇精品视频一区二区三区| 在线成人一区二区| 国产99精品一区二区三区免费 | 国产亚洲福利精品一区| 麻豆AV天堂一区二区香蕉| 亚洲国产精品第一区二区| 一区二区精品久久| 国产怡春院无码一区二区| 亚洲熟女少妇一区二区| 日本一区二区三区不卡视频中文字幕| 台湾无码一区二区|