Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 欧美一区二区三区在线,无遮挡又黄又爽又色的视频免费,中文字幕欧美在线观看

          整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          聽說過CSS in JS,那你聽說過JS in CSS嗎


          SS in JS

          CSS in JS是一種解決css問題想法的集合,而不是一個指定的庫。從CSS in JS的字面意思可以看出,它是將css樣式寫在JavaScript文件中,而不需要獨(dú)立出.css.less之類的文件。將css放在js中使我們更方便的使用js的變量模塊化tree-shaking。還解決了css中的一些問題,譬如:更方便解決基于狀態(tài)的樣式更容易追溯依賴關(guān)系生成唯一的選擇器來鎖定作用域。盡管CSS in JS不是一個很新的技術(shù),但國內(nèi)的普及程度并不高。由于Vue和Angular都有屬于他們自己的一套定義樣式的方案,React本身也沒有管用戶怎樣定義組件的樣式[1],所以CSS in JS在React社區(qū)的熱度比較高。

          目前為止實(shí)現(xiàn)CSS in JS的第三方庫有很多:(http://michelebertoli.github.io/css-in-js/)。像JSS[2]styled-components[3]等。在這里我們就不展開贅述了(相關(guān)鏈接已放在下方),這篇文章的重點(diǎn)是JS in CSS

          JS in CSS又是什么

          在上面我們提到CSS in JS就是把CSS寫在JavaScript中,那么JS in CSS我們可以推斷出就是可以在CSS中使用JavaScript腳本,如下所示。可以在CSS中編寫Paint API的功能。還可以訪問:ctx,geom。甚至我們還可以編寫自己的css自定義屬性等。這些功能的實(shí)現(xiàn)都基于CSS Houdini[4]

          .el {
            --color: cyan;
            --multiplier: 0.24;
            --pad: 30;
            --slant: 20;
            --background-canvas: (ctx, geom) => {
              let multiplier = var(--multiplier);
              let c = `var(--color)`;
              let pad = var(--pad);
              let slant = var(--slant);
          
              ctx.moveTo(0, 0);
              ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);
              ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);
              ctx.lineTo(0, geom.height);
              ctx.fillStyle = c;
              ctx.fill();
            };
            background: paint(background-canvas);
            transition: --multiplier .4s;
          }
          .el:hover {
            --multiplier: 1;
          }
          

          Houdini 解決了什么問題

          CSS 與 JS的標(biāo)準(zhǔn)制定流程對比

          在如今的Web開發(fā)中,JavaScript幾乎占據(jù)了項(xiàng)目代碼的大部分。我們可以在項(xiàng)目開發(fā)中使用ES 2020、ES2021、甚至提案中的新特性(如:Decorator[5]),即使瀏覽器尚未支持,也可以編寫Polyfill或使用Babel之類的工具進(jìn)行轉(zhuǎn)譯,讓我們可以將最新的特性應(yīng)用到生產(chǎn)環(huán)境中(如下圖所示)。

          JavaScript標(biāo)準(zhǔn)制定流程.png

          而CSS就不同了,除了制定CSS標(biāo)準(zhǔn)規(guī)范所需的時間外,各家瀏覽器的版本、實(shí)戰(zhàn)進(jìn)度差異更是曠日持久(如下圖所示),最多利用PostCSS、Sass等工具來幫我們轉(zhuǎn)譯出瀏覽器能接受的CSS。開發(fā)者們能操作的就是通過JS去控制DOMCSSOM來影響頁面的變化,但是對于接下來的LayoutPaintComposite就幾乎沒有控制權(quán)了。為了解決上述問題,為了讓CSS的魔力不在受到瀏覽器的限制,Houdini就此誕生。

          CSS 標(biāo)準(zhǔn)制定流程.png

          CSS Polyfill

          我們上文中提到JavaScript中進(jìn)入提案中的特性我們可以編寫Polyfill,只需要很短的時間就可以講新特性投入到生產(chǎn)環(huán)境中。這時,腦海中閃現(xiàn)出的第一個想法就是CSS Polyfill,只要CSS的Polyfill 足夠強(qiáng)大,CSS或許也能有JavaScript一樣的發(fā)展速度,令人可悲的是編寫CSS Polyfill異常的困難,并且大多數(shù)情況下無法在不破壞性能的情況下進(jìn)行。這是因?yàn)镴avaScript是一門動態(tài)腳本語言[6]。它帶來了極強(qiáng)的擴(kuò)展性,正是因?yàn)檫@樣,我們可以很輕松使用JavaScript做出JavaScript的Polyfill。但是CSS不是動態(tài)的,在某些場景下,我們可以在編譯時將一種形式的CSS的轉(zhuǎn)換成另一種(如PostCSS[7])。如果你的Polyfill依賴于DOM結(jié)構(gòu)或者某一個元素的布局、定位等,那么我們的Polyfill就無法編譯時執(zhí)行,而需要在瀏覽器中運(yùn)行了。不幸的是,在瀏覽器中實(shí)現(xiàn)這種方案非常不容易。

          頁面渲染流程.png

          如上圖所示,是從瀏覽器獲取到HTML到渲染在屏幕上的全過程,我們可以看到只有帶顏色(粉色、藍(lán)色)的部分是JavaScript可以控制的環(huán)節(jié)。首先我們根本無法控制瀏覽器解析HTML與CSS并將其轉(zhuǎn)化為DOMCSSOM的過程,以及Cascade,Layout,Paint,Composite我們也無能為力。整個過程中我們唯一完全可控制的就是DOM,另外CSSOM部分可控。

          CSS Houdini草案中提到,這種程度的暴露是不確定的、兼容性不穩(wěn)定的以及缺乏對關(guān)鍵特性的支持的。比如,在瀏覽器中的 CSSOM 是不會告訴我們它是如何處理跨域的樣式表,而且對于瀏覽器無法解析的 CSS 語句它的處理方式就是不解析了,也就是說——如果我們要用 CSS polyfill讓瀏覽器去支持它尚且不支持的屬性,那就不能在 CSSOM 這個環(huán)節(jié)做,我們只能遍歷一遍DOM,找到 <style><link rel="stylesheet"> 標(biāo)簽,獲取其中的 CSS 樣式、解析、重寫,最后再加回 DOM 樹中。令人尷尬的是,這樣DOM樹全部刷新了,會導(dǎo)致頁面的重新渲染(如下如所示)。

          即便如此,有的人可能會說:“除了這種方法,我們也別無選擇,更何況對網(wǎng)站的性能也不會造成很大的影響”。那么對于部分網(wǎng)站是這樣的。但如果我們的Polyfill是需要對可交互的頁面呢?例如scrollresizemousemovekeyup等等,這些事件隨時會被觸發(fā),那么意味著隨時都會導(dǎo)致頁面的重新渲染,交互不會像原本那樣絲滑,甚至導(dǎo)致頁面崩潰,對用戶的體驗(yàn)也極其不好。

          綜上所述,如果我們想讓瀏覽器解析它不認(rèn)識的樣式(低版本瀏覽器使用grid布局),然而渲染流程我們無法介入,我們也只能通過手動更新DOM的方式,這樣會帶來很多問題,Houdini的出現(xiàn)正是致力于解決他們。

          Houdini API

          Houdini是一組底層API,它公開了CSS引擎的各個部分,如下圖所示展示了每個環(huán)節(jié)對應(yīng)的新API(灰色部分各大瀏覽器還未實(shí)現(xiàn)),從而使開發(fā)人員能夠通過加入瀏覽器渲染引擎的樣式和布局過程來擴(kuò)展CSS。Houdini是一群來自Mozilla,Apple,Opera,Microsoft,HP,Intel和Google的工程師組成的工作小組設(shè)計而成的。它們使開發(fā)者可以直接訪問CSS對象模型(CSSOM),使開發(fā)人員可以編寫瀏覽器可以解析為CSS的代碼,從而創(chuàng)建新的CSS功能,而無需等待它們在瀏覽器中本地實(shí)現(xiàn)。

          CSS Houdini-API

          Properties & Values API

          盡管當(dāng)前已經(jīng)有了CSS變量,可以讓開發(fā)者控制屬性值,但是無法約束類型或者更嚴(yán)格的定義,CSS Houdini新的API,我們可以擴(kuò)展css的變量,我們可以定義CSS變量的類型,初始值,繼承。它是css變量更強(qiáng)大靈活。

          CSS變量現(xiàn)狀:

          .dom {
            --my-color: green;
            --my-color: url('not-a-color'); // 它并不知道當(dāng)前的變量類型
            color: var(--my-color);
          }
          
          

          Houdini提供了兩種自定義屬性的注冊方式,分別是在js和css中。

          CSS.registerProperty({
            name: '--my-prop', // String 自定義屬性名
            syntax: '<color>', // String 如何去解析當(dāng)前的屬性,即屬性類型,默認(rèn) *
            inherits: false, // Boolean 如果是true,子節(jié)點(diǎn)將會繼承
            initialValue: '#c0ffee', // String 屬性點(diǎn)初始值
          });
          

          我們還可以在css中注冊,也可以達(dá)到上面的效果

          @property --my-prop {
            syntax: '<color>';
            inherits: false;
            initial-value: #c0ffee;
          }
          

          這個API中最令人振奮人心的功能是自定義屬性上添加動畫,像這樣:transition: --multiplier 0.4s;,這個功能我們在前面介紹什么是js in css那個demo[8]用使用過。我們還可以使用+使syntax屬性支持一個或多個類型,也可以使用|來分割。更多syntax屬性值:

          屬性值描述<length>長度值<number>數(shù)字<percentage>百分比<length-percentage>長度或百分比,calc將長度和百分比組成的表達(dá)式<color>顏色<image>圖像<url>網(wǎng)址<integer>整數(shù)<angle>角度<time>時間<resolution>分辨率<transform-list>轉(zhuǎn)換函數(shù)<custom-ident>ident

          Worklets

          Worklets是渲染引擎的擴(kuò)展,從概念上來講它類似于Web Workers[9],但有幾個重要的區(qū)別:

          1. 設(shè)計為并行,每個Worklets必須始終有兩個或更多的實(shí)例,它們中的任何一個都可以在被調(diào)用時運(yùn)行
          2. 作用域較小,限制不能訪問全局作用域的API(Worklet的函數(shù)除外)
          3. 渲染引擎會在需要的時候調(diào)用他們,而不是我們手動調(diào)用

          Worklet是一個JavaScript模塊,通過調(diào)用worklet的addModule方法(它是個Promise)來添加。比如registerLayout,registerPaint, registerAnimator 我們都需要放在Worklet中

          //加載單個
          await demoWorklet.addModule('path/to/script.js');
          
          // 一次性加載多個worklet
          Promise.all([
            demoWorklet1.addModule('script1.js'),
            demoWorklet2.addModule('script2.js'),
          ]).then(results => {});
          
          registerDemoWorklet('name', class {
          
            // 每個Worklet可以定義要使用的不同函數(shù)
            // 他們將由渲染引擎在需要時調(diào)用
            process(arg) {
              return !arg;
            }
          });
          
          

          Worklets的生命周期

          Worklets lifecycle

          1. Worklet的生命周期從渲染引擎內(nèi)開始
          2. 對于JavaScript,渲染引擎啟動JavaScript主線程
          3. 然后他將啟動多個worklet進(jìn)程,并且可以運(yùn)行。這些進(jìn)程理想情況下是獨(dú)立于主線程的線程,這樣就不會阻塞主線程(但它們也不需要阻塞)
          4. 然后在主線程中加載我們?yōu)g覽器的JavaScript
          5. 該JavaScript調(diào)用 worklet.addModule 并異步加載一個worklet
          6. 加載后,將worklet加載到兩個或多個可用的worklet流程中
          7. 當(dāng)需要時,渲染引擎將通過從加載的Worklet中調(diào)用適當(dāng)?shù)奶幚砗瘮?shù)來執(zhí)行Worklet。該調(diào)用可以針對任何并行的Worklet實(shí)例。

          Typed OM

          Typed OM是對現(xiàn)有的CSSOM的擴(kuò)展,并實(shí)現(xiàn) Parsing APIProperties & Values API相關(guān)的特性。它將css值轉(zhuǎn)化為有意義類型的JavaScript的對象,而不是像現(xiàn)在的字符串。如果我們嘗試將字符串類型的值轉(zhuǎn)化為有意義的類型并返回可能會有很大的性能開銷,因此這個API可以讓我們更高效的使用CSS的值。

          現(xiàn)在讀取CSS值增加了新的基類CSSStyleValue,他有許多的子類可以更加精準(zhǔn)的描述css值的類型:

          子類描述CSSKeywordValueCSS關(guān)鍵字和其他標(biāo)識符(如inherit或grid)CSSPositionValue位置信息 (x,y)CSSImageValue表示圖像的值屬性的對象CSSUnitValue表示為具有單個單位的單個值(例如50px),也可以表示為沒有單位的單個值或百分比CSSMathValue比較復(fù)雜的數(shù)值,比如有calc,min和max。這包括子類 CSSMathSum, CSSMathProduct, CSSMathMin,CSSMathMax, CSSMathNegate 和 CSSMathInvertCSSTransformValue由CSS transforms組成的CSSTransformComponent列表,其中包括CSSTranslate, CSSRotate, CSSScale, CSSSkew, CSSSkewX, CSSSkewY, CSSPerspective 和 CSSMatrixComponent

          使用Typed OM主要有兩種方法:

          1. 通過attributeStyleMap設(shè)置和獲取有類型的行間樣式
          2. 通過computedStyleMap獲取元素完整的Typed OM樣式

          使用attributeStyleMap設(shè)置并獲取

          myElement.attributeStyleMap.set('font-size', CSS.em(2));
          myElement.attributeStyleMap.get('font-size'); // CSSUnitValue { value: 2, unit: 'em' }
          
          myElement.attributeStyleMap.set('opacity', CSS.number(.5));
          myElement.attributeStyleMap.get('opacity'); // CSSUnitValue { value: 0.5, unit: 'number' };
          

          在線demo[10]

          使用computedStyleMap

          .foo {
            transform: translateX(1em) rotate(50deg) skewX(10deg);
            vertical-align: baseline;
            width: calc(100% - 3em);
          }
          
          const cs = document.querySelector('.foo').computedStyleMap();
          
          cs.get('vertical-align');
          // CSSKeywordValue {
          //  value: 'baseline',
          // }
          
          cs.get('width');
          // CSSMathSum {
          //   operator: 'sum',
          //   length: 2,
          //   values: CSSNumericArray {
          //     0: CSSUnitValue { value: -90, unit: 'px' },
          //     1: CSSUnitValue { value: 100, unit: 'percent' },
          //   },
          // }
          
          cs.get('transform');
          // CSSTransformValue {
          //   is2d: true,
          //   length: 3,
          //   0: CSSTranslate {
          //     is2d: true,
          //     x: CSSUnitValue { value: 20, unit: 'px' },
          //     y: CSSUnitValue { value: 0, unit: 'px' },
          //     z: CSSUnitValue { value: 0, unit: 'px' },
          //   },
          //   1: CSSRotate {...},
          //   2: CSSSkewX {...},
          // }
          

          Layout API

          開發(fā)者可以通過這個API實(shí)現(xiàn)自己的布局算法,我們可以像原生css一樣使用我們自定義的布局(像display:flex, display:table)。在Masonry layout library[11] 上我們可以看到開發(fā)者們是有多想實(shí)現(xiàn)各種各樣的復(fù)雜布局,其中一些布局光靠 CSS 是不行的。雖然這些布局會讓人耳目一新印象深刻,但是它們的頁面性能往往都很差,在一些低端設(shè)備上性能問題猶為明顯。

          CSS Layout API 暴露了一個registerLayout方法給開發(fā)者,接收一個布局名(layout name)作為后面在 CSS中使用的屬性值,還有一個包含有這個布局邏輯的JavaScript類。

          my-div {
            display: layout(my-layout);
          }
          
          // layout-worklet.js
          registerLayout('my-layout', class {
            static get inputProperties() { return ['--foo']; }
            
            static get childrenInputProperties() { return ['--bar']; }
            
            async intrinsicSizes(children, edges, styleMap) {}
          
            async layout(children, edges, constraints, styleMap) {}
          });
          
          await CSS.layoutWorklet.addModule('layout-worklet.js');
          

          目前瀏覽器大部分還不支持

          Painting API

          我們可以在CSS background-image中使用它,我們可以使用Canvas 2d上下文,根據(jù)元素的大小控制圖像,還可以使用自定義屬性。

          await CSS.paintWorklet.addModule('paint-worklet.js');
          
          registerPaint('sample-paint', class {
            static get inputProperties() { return ['--foo']; }
          
            static get inputArguments() { return ['<color>']; }
          
            static get contextOptions() { return {alpha: true}; }
          
            paint(ctx, size, props, args) { }
          });
          

          Animation API

          這個API讓我們可以控制基于用戶輸入的關(guān)鍵幀動畫,并且以非阻塞的方式。還能更改一個 DOM 元素的屬性,不過是不會引起渲染引擎重新計算布局或者樣式的屬性,比如 transform、opacity 或者滾動條位置(scroll offset)。Animation API的使用方式與 Paint APILayout API略有不同我們還需要通過new一個WorkletAnimation來注冊worklet。

          // animation-worklet.js
          registerAnimator('sample-animator', class {
            constructor(options) {
            }
            animate(currentTime, effect) {
              effect.localTime = currentTime;
            }
          });
          
          await CSS.animationWorklet.addModule('animation-worklet.js');
          
          // 需要添加動畫的元素
          const elem = document.querySelector('#my-elem');
          const scrollSource = document.scrollingElement;
          const timeRange = 1000;
          const scrollTimeline = new ScrollTimeline({
            scrollSource,
            timeRange,
          });
          
          const effectKeyframes = new KeyframeEffect(
            elem,
            // 動畫需要綁定的關(guān)鍵幀
            [
              {transform: 'scale(1)'},
              {transform: 'scale(.25)'},
              {transform: 'scale(1)'}
            ],
            {
              duration: timeRange,
            },
          );
          new WorkletAnimation(
            'sample-animator',
            effectKeyframes,
            scrollTimeline,
            {},
          ).play();
          

          關(guān)于此API的更多內(nèi)容:(https://github.com/w3c/css-houdini-drafts/tree/main/css-animation-worklet-1)

          Parser API

          允許開發(fā)者自由擴(kuò)展 CSS 詞法分析器。

          解析規(guī)則:

          const background = window.cssParse.rule("background: green");
          console.log(background.styleMap.get("background").value) // "green"
          
          const styles = window.cssParse.ruleSet(".foo { background: green; margin: 5px; }");
          console.log(styles.length) // 5
          console.log(styles[0].styleMap.get("margin-top").value) // 5
          console.log(styles[0].styleMap.get("margin-top").type) // "px"
          

          解析CSS:

          const style = fetch("style.css")
                  .then(response => CSS.parseStylesheet(response.body));
          style.then(console.log);
          

          Font Metrics API

          它將提供一些方法來測量在屏幕上呈現(xiàn)的文本元素的尺寸,將允許開發(fā)者控制文本元素在屏幕上呈現(xiàn)的方式。使用當(dāng)前功能很難或無法測量這些值,因此該API將使開發(fā)者可以更輕松地創(chuàng)建與文本和字體相關(guān)的CSS特性。例如:

          • flex布局: align-items baseline特性。需要知道每一個flex盒子中第一個元素的基線位置。
          • 首字母: 需要知道每個字母的基線高度和字母最大的高度,以及換行內(nèi)容的基線長度。
          • 單個字形的前進(jìn)和后退。
          • 換行: 需要訪問字體數(shù)據(jù),文本的所有樣式輸入以及布局信息(可用的段落長度等)。
          • 元素中的每一個line boxes都需要一個基線。(line boxes代表包含眾多inline boxes的這行)

          Houdini 目前進(jìn)展

          Is Houdini ready yet

          (https://ishoudinireadyyet.com/)

          Houdini 的藍(lán)圖

          了解到這里,部分開發(fā)者可能會說:“我不需要這些花里胡哨的技術(shù),并不能帶收益。我只想簡簡單單的寫幾個頁面,做做普通的Web App,并不想試圖干預(yù)瀏覽器的渲染過程從而實(shí)現(xiàn)一些實(shí)驗(yàn)性或炫酷的功能。”如果這樣想的話,我們不妨退一步再去思考。回憶下最近做過的項(xiàng)目,用于實(shí)現(xiàn)頁面效果所使用到的技術(shù),grid布局方式在考慮兼容老版本瀏覽器時也不得不放棄。我們想控制瀏覽器渲染頁面的過程并不是僅僅為了炫技,更多的是為了幫助開發(fā)者們解決以下兩個問題:

          1. 統(tǒng)一各大瀏覽器的行為
          2. JavaScript一樣,在推出新的特性時,我們可以通過Polyfill的形式快速的投入生產(chǎn)環(huán)境中。

          幾年過后再回眸,當(dāng)主流瀏覽器完全支持Houdini的時候。我們可以在瀏覽器上隨心所欲的使用任何CSS屬性,并且他們都能完美支持。像今天的grid布局在舊版本瀏覽器支持的并不友好的這類問題,那時我們只需要安裝對應(yīng)的Polyfill就能解決類似的問題。

          、前言

          雖然在 JavaScript 中對象無處不在,但這門語言并不使用經(jīng)典的基于類的繼承方式,而是依賴原型,至少在 ES6 之前是這樣的。當(dāng)時,假設(shè)我們要定義一個可以設(shè)置 id 與坐標(biāo)的類,我們會這樣寫:

          // Shape 類
          function Shape(id, x, y) {
           this.id = id;
           this.setLocation(x, y);
          }
          // 設(shè)置坐標(biāo)的原型方法
          Shape.prototype.setLocation = function(x, y) {
           this.x = x;
           this.y = y;
          };
          

          上面是類定義,下面是用于設(shè)置坐標(biāo)的原型方法。從 ECMAScript 2015 開始,語法糖 class被引入,開發(fā)者可以通過 class 關(guān)鍵字來定義類。我們可以直接定義類、在類中寫靜態(tài)方法或繼承類等。上例便可改寫為:

          class Shape {
           constructor(id, x, y) { // 構(gòu)造函數(shù)語法糖
           this.id = id;
           this.setLocation(x, y);
           }
           setLocation(x, y) { // 原型方法
           this.x = x;
           this.y = y;
           }
          }
          

          一個更符合“傳統(tǒng)語言”的寫法。語法糖寫法的優(yōu)勢在于當(dāng)類中充滿各類靜態(tài)方法與繼承關(guān)系時,class 這種對象模版寫法的簡潔性會更加突出,且不易出錯。但不可否認(rèn)時至今日,我們還需要為某些用戶兼容我們的 ES6+ 代碼,class 就是 TodoList 上的一項(xiàng):

          作為當(dāng)下最流行的 JavaScript 編譯器,Babel 替我們轉(zhuǎn)譯 ECMAScript 語法,而我們不用再擔(dān)心如何進(jìn)行向后兼容。

          本地安裝 Babel 或者利用 Babel CLI 工具,看看我們的 Shape 類會有哪些變化。可惜的是,你會發(fā)現(xiàn)代碼體積由現(xiàn)在的219字節(jié)激增到2.1KB,即便算上代碼壓縮(未混淆)代碼也有1.1KB。轉(zhuǎn)譯后輸出的代碼長這樣:

          "use strict";var _createClass=function(){function a(a,b){for(var c,d=0;d<b.length;d++)c=b[d],c.enumerable=c.enumerable||!1,c.configurable=!0,"value"in c&&(c.writable=!0),Object.defineProperty(a,c.key,c)}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}();function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var Shape=function(){function a(b,c,d){_classCallCheck(this,a),this.id=b,this.setLocation(c,d)}return _createClass(a,[{key:"setLocation",value:function c(a,b){this.x=a,this.y=b}}]),a}();
          

          Babel 僅僅是把我們定義的 Shape 還原成一個 ES5 函數(shù)與對應(yīng)的原型方法么?

          一、揭秘

          好像沒那么簡單,為了摸清實(shí)際轉(zhuǎn)譯流程,我們先將上述類定義代碼簡化為一個只有14字節(jié)的空類:

          class Shape {}
          

          首先,當(dāng)訪問器走到類聲明階段,需要補(bǔ)充嚴(yán)格模式:

          "use strict";
          class Shape {}
          

          而進(jìn)入變量聲明與標(biāo)識符階段時則需補(bǔ)充 let 關(guān)鍵字并轉(zhuǎn)為 var:

          "use strict";
          var Shape = class Shape {};
          

          到這個時候代碼的變化都不太大。接下來是進(jìn)入函數(shù)表達(dá)式階段,多出來幾行函數(shù):

          "use strict";
          function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
          var Shape = function Shape() {
           _classCallCheck(this, Shape);
          };
          

          該階段不僅替換了 class,還在類中調(diào)用了叫做 _classCallCheck 的方法。這是什么呢?

          這個函數(shù)的作用在于確保構(gòu)造方法永遠(yuǎn)不會作為函數(shù)被調(diào)用,它會評估函數(shù)的上下文是否為 Shape 對象的實(shí)例,以此確定是否需要拋出異常。接下來,則輪到 babel-plugin-minify-simplify上場,這個插件做的事情在于通過簡化語句為表達(dá)式、并使表達(dá)式盡可能統(tǒng)一來精簡代碼。運(yùn)行后的輸出是這樣的:

          "use strict";
          function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); }
          var Shape = function Shape() {
           _classCallCheck(this, Shape);
          };
          

          可以看到 if 語句中由于只有一行代碼,于是花括號被去掉。接下來上場的便是內(nèi)置的 Block Hoist ,該插件通過遍歷參數(shù)排序然后替換,Babel 輸出結(jié)果為:

          "use strict";
          function _classCallCheck(a, b) { if (!(a instanceof b)) throw new TypeError("Cannot call a class as a function"); }
          var Shape = function a() {
           _classCallCheck(this, a);
          };
          

          最后一步,minify 一下,代碼體積由最初的14字節(jié)增為338字節(jié):

          "use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var Shape=function a(){_classCallCheck(this,a)};
          

          二、再說一些

          這是一個什么都沒干的類聲明,但現(xiàn)實(shí)中任何類都會有自己的方法,而此時 Babel 必定會引入更多的插件來幫助它完成代碼的轉(zhuǎn)譯工作。直接在剛剛的空類中定義一個方法吧。

          class Shape {
           render() {
           console.log("Hi");
           }
          }
          

          我們用 Babel 轉(zhuǎn)譯一下,會發(fā)現(xiàn)代碼中包含如下這段:

          var _createClass = function () { function a(a, b) { for (var c, d = 0; d < b.length; d++) c = b[d], c.enumerable = c.enumerable || !1, c.configurable = !0, "value" in c && (c.writable = !0), Object.defineProperty(a, c.key, c); } return function (b, c, d) { return c && a(b.prototype, c), d && a(b, d), b; }; }();
          

          類似前面我們遇到的 _classCallCheck ,這里又多出一個 _createClass ,這是做什么的呢?我們稍微把代碼狀態(tài)往前挪一挪,來到 babel-plugin-minify-builtins 處理階段(該插件的作用在于縮減內(nèi)置對象代碼體積,但我們主要關(guān)注點(diǎn)在于這個階段的 _createClass 函數(shù)是基本可讀的),此時 _classCallCheck 長成這樣:

          var _createClass = function() {
           function defineProperties(target, props) {
           for (var i = 0; i < props.length; i++) {
           var descriptor = props[i];
           descriptor.enumerable = descriptor.enumerable || false;
           descriptor.configurable = true;
           if ("value" in descriptor) descriptor.writable = true;
           Object.defineProperty(target, descriptor.key, descriptor);
           }
           }
           return function(Constructor, protoProps, staticProps) {
           if (protoProps) defineProperties(Constructor.prototype, protoProps);
           if (staticProps) defineProperties(Constructor, staticProps);
           return Constructor;
           };
          } ();
          

          可以看出 _createClass 用于處理創(chuàng)建對象屬性,函數(shù)支持傳入構(gòu)造函數(shù)與需定義的鍵值對屬性數(shù)組。函數(shù)判斷傳入的參數(shù)(普通方法/靜態(tài)方法)是否為空對應(yīng)到不同的處理流程上。而 defineProperties 方法做的事情便是遍歷傳入的屬性數(shù)組,然后分別調(diào)用 Object.defineProperty 以更新構(gòu)造函數(shù)。而在 Shape 中,由于我們定義的不是靜態(tài)方法,我們便這樣調(diào)用:

          _createClass(Shape, [{
           key: "render",
           value: function render() {
           console.log("Hi");
           }
           }]);
          

          T.J. Crowder 在 How does Babel.js create compile a class declaration into ES2015? 中談到 Babel 是如何將 class 轉(zhuǎn)化為 ES5 兼容代碼時談到了幾點(diǎn),大意為:

          • constructor 會成為構(gòu)造方法數(shù);
          • 所有非構(gòu)造方法、非靜態(tài)方法會成為原型方法;
          • 靜態(tài)方法會被賦值到構(gòu)造函數(shù)的屬性上,其他屬性保持不變;
          • 派生構(gòu)造函數(shù)上的原型屬性是通過 Object.create(Base.prototype) 構(gòu)造的對象,而不是 new Base() ;
          • constructor 調(diào)用構(gòu)造器基類是第一步操作;
          • ES5 中對應(yīng) super 方法的寫法是 Base.prototype.baseMethod.call(this); ,這種操作不僅繁瑣而且容易出錯;

          這些概述大致總結(jié)了類定義在兩個 ES 版本中的一些差異,其他很多方面比如 extends ——繼承關(guān)鍵字,它的使用則會使 Babel 在轉(zhuǎn)譯結(jié)果加上 _inherits 與 _possibleConstructorReturn兩個函數(shù)。篇幅所限,此處不再展開詳述。

          三、最后

          語法糖 class 給我們帶來了很多寫法上的便利,但可能會使我們在代碼體積上的優(yōu)化努力“付諸東流”。

          另一方面,如果你是一名 React 應(yīng)用開發(fā)者,你是否已經(jīng)在想將代碼中的所有 class 寫法換為 function 呢?那樣做的話,代碼體積無疑會減少很多,但你一定也知道 PureComponent 相比 Component 的好處。所以雖然 function 給你的代碼體積減負(fù)了,但他在哪里又給你無形增加負(fù)擔(dān)了呢?

          因此,真的不推薦開發(fā)者用 class 這種寫法么,你覺得呢?

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

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

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

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

          因此,在遷移到 ES6 之后,我們開始研究采用 TypeScript 的路徑。本文將講述我們?nèi)绾卧O(shè)計我們的方法,一些有趣的技術(shù)挑戰(zhàn),以及如何使一家 Etsy 級別的公司學(xué)習(xí)一種新的編程語言。

          在高層次上采用 TypeScript

          我并不想花太多時間向你安利 TypeScript,因?yàn)樵谶@方面還有很多其他的 文章 和 講座 ,都做得非常好。相反,我想談?wù)?Etsy 在推出 TypeScript 支持方面所做的努力,這不僅僅是從 JavaScript 到 TypeScript 的技術(shù)實(shí)現(xiàn)。這也包括許多規(guī)劃、教育和協(xié)調(diào)工作。但是如果把細(xì)節(jié)弄清楚,你會發(fā)現(xiàn)很多值得分享的學(xué)習(xí)經(jīng)驗(yàn)。讓我們先來討論一下我們想要的采用是什么樣的做法。

          采用策略

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

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

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

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

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

          我們采用的是什么?

          以下是我們的采用策略:

          1. 使 TypeScript 盡可能地嚴(yán)格,并逐個文件地移植代碼庫。
          2. 添加真正優(yōu)秀的類型和真正優(yōu)秀的支持文檔,包括產(chǎn)品開發(fā)者常用的所有實(shí)用程序、組件和工具。
          3. 花時間教工程師們學(xué)習(xí) TypeScript,并讓他們逐個團(tuán)隊(duì)地啟用 TypeScript 語法。

          讓我們再仔細(xì)看看這幾點(diǎn)吧。

          逐步遷移到嚴(yán)格的 TypeScript

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

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

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

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

          逐個團(tuán)隊(duì)地進(jìn)行工程師適職培訓(xùn)

          我們已經(jīng)花了很多時間在 TypeScript 的教育上,這是我們在遷移過程中所做的最好的決定。Etsy 有數(shù)百名工程師,在這次遷移之前,他們幾乎沒有 TypeScript 的經(jīng)驗(yàn)(包括我)。我們知道,要想成功地遷移,人們首先必須學(xué)習(xí)如何使用 TypeScript。打開這個開關(guān),告訴所有人都要這么做,這可能會使人們迷惑,使我們的團(tuán)隊(duì)被問題壓垮,也會影響我們產(chǎn)品工程師的工作速度。通過逐步引入團(tuán)隊(duì),我們能夠努力完善工具和教學(xué)材料。它還意味著,沒有任何工程師能在沒有隊(duì)友能夠?qū)彶槠浯a的情況下編寫 TypeScript。逐步適職使我們的工程師有時間學(xué)習(xí) TypeScript,并把它融入到路線圖中。

          技術(shù)細(xì)節(jié)

          在遷移過程中,有很多有趣的技術(shù)挑戰(zhàn)。令人驚訝的是,采用 TypeScript 的最簡單之處就是在構(gòu)建過程中添加對它的支持。在這個問題上,我不會詳細(xì)討論,因?yàn)闃?gòu)建系統(tǒng)有許多不同的風(fēng)格,但簡單地說:

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

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

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

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

          舉例來說,假設(shè)有一個函數(shù)接受 HTML 標(biāo)簽的名稱并返回 HTML 元素。該函數(shù)可以將任何舊的字符串作為參數(shù)接受,但是如果它使用這個字符串來創(chuàng)建元素,那么最好能夠確保該字符串實(shí)際上是一個真正的 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");

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

          // 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 意味著我們需要考慮和解決許多新實(shí)踐。 typescript-eslint 項(xiàng)目給了我們一些 TypeScript 特有的規(guī)則,可供我們利用。例如, ban-types 規(guī)則允許我們警告不要使用泛型 Element 類型,而使用更具體的 HTMLElement 類型。

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

          // 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;

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

          作為防止人們使用不良模式的工具,Lint 確實(shí)非常有用,但是這并不意味著這些模式是普遍不好的:每個規(guī)則都有例外。Lint 的好處在于,它提供了合理的逃生通道。在任何時候,如果確實(shí)需要使用“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

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

          Etsy 的 API 使用 PHP 實(shí)現(xiàn)的,并且我們?yōu)槊總€端點(diǎn)生成 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 響應(yīng)生成類型,開發(fā)者就得手工寫出它們,并且想讓它們與實(shí)際的 API 同步。我們需要嚴(yán)格的類型,但是我們也不希望我們的開發(fā)者為了得到這些類型而折騰。

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

          在為所有端點(diǎn)生成 TypeScript 類型之后,仍然需要以一種可利用的方式將它們整合到代碼庫中。我們決定將生成的響應(yīng)類型編入我們所生成的配置中,然后更新 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
             });

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

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

          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.
          }

          最重要的是,這些特性都不需要改變我們產(chǎn)品工程師的工作流程。他們可以免費(fèi)使用這些類型,只要他們使用他們已經(jīng)熟悉的模式。

          通過分析我們的類型來改善開發(fā)體驗(yàn)

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

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

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

          結(jié)果是,類型中存在一個循環(huán)依賴關(guān)系,它幫助我們創(chuàng)建不可變的對象的內(nèi)部實(shí)用程序。到目前為止,這些類型對于我們處理的所有代碼來說都是完美無缺的,但在代碼庫中尚未遷移的部分,它的一些使用卻出現(xiàn)了問題,產(chǎn)生了一個無限的類型循環(huán)。如果有人打開了代碼庫的這些部分文件,或者在我們對所有代碼運(yùn)行類型檢查器時,就會花很多時間來嘗試?yán)斫庠擃愋停缓蠓艞壊⒂涗涱愋湾e誤。修復(fù)了這個類型之后,檢查一個文件的時間從將近 46 秒減少到了不到 1 秒。

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

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

          教育

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

          基礎(chǔ)

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

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

          讓團(tuán)隊(duì)接受教育

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

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

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

          逐個團(tuán)隊(duì)推廣的后勤保障

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

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

          適職后支持團(tuán)隊(duì)

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

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

          我們現(xiàn)在在哪里?

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

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


          主站蜘蛛池模板: 精品国产一区二区三区麻豆| 国内精品一区二区三区东京| 国产一区二区三区在线看| 亚洲一区中文字幕久久| 亚洲Av无码国产一区二区 | 亚洲午夜一区二区电影院| 国产一区二区精品久久岳| 制服美女视频一区| 麻豆果冻传媒2021精品传媒一区下载 | 日韩高清国产一区在线| 国产丝袜无码一区二区三区视频| 亚洲AV一区二区三区四区| 精品国产一区二区三区久久| 国产美女av在线一区| 一区二区视频在线免费观看| 日本一区二区在线免费观看| 日韩高清一区二区三区不卡 | 精品乱子伦一区二区三区高清免费播放 | 国产视频一区二区在线播放| 中文字幕一区在线观看| 成人一区二区免费视频| 在线精品国产一区二区三区| 果冻传媒一区二区天美传媒| 国产波霸爆乳一区二区| 好看的电影网站亚洲一区| 国产成人精品视频一区二区不卡 | 国精产品999一区二区三区有限| 亚洲制服中文字幕第一区| 91视频一区二区| 极品少妇一区二区三区四区| 怡红院一区二区在线观看| 波多野结衣一区在线| 亚洲AV无码一区二区三区在线观看 | 亚洲av无码一区二区乱子伦as| 无码少妇一区二区三区浪潮AV| 88国产精品视频一区二区三区| 精品香蕉一区二区三区| 亚洲国产老鸭窝一区二区三区| 亚洲制服丝袜一区二区三区| 极品少妇一区二区三区四区| 中文字幕无线码一区二区|