意見徵求:新的JS API

由 Natalie Weizenbaum 發佈於 2021 年 8 月 5 日

我很興奮地正式公開一個已經開發一段時間的東西:一個(針對)全新的 Sass JavaScript API 的提案。這個 API 是根據從 Node Sass API 和多年來其他各種語言中的其他歷史 Sass API 中學到的經驗教訓,從頭開始重新設計的,它解決了現有 API 的許多缺點。

API 有四個主要組成部分,我將在這篇文章中涵蓋所有這些內容。

在您继续阅读时,请记住,此 API 仍只是一个提案。我们希望听到您的意见,我们的用户,它是否满足您的需求,以及我们如何在将其锁定到正式发布之前改进它。所以,请在問題追蹤器上發表您的意見!

為什麼要使用新的 API為什麼要使用新的 API? 永久連結

現有的 JavaScript API 已經過時了。它早於 Dart Sass,最初是為 node-sass 套件設計的,該套件包裝了現在已棄用LibSass 實作。(這就是為什麼我們稱之為「Node Sass API」!)它與 LibSass 一起有機地、而且經常雜亂無章地發展,最終出現了不少令人尷尬的遺留行為。這些行為中的許多行為更多的是實作上的痛苦,但也有一些行為讓生活變得相當困難。

  • 導入器 API 是圍繞檔案路徑而不是 URL 構建的,並且與實體檔案系統緊密耦合。這使得覆蓋*所有*基於檔案的載入並呈現完全虛擬的檔案系統變得不可能,並導致自訂 Node 導入器與新的模組系統互動不良。

  • 函式 API 是圍繞可變值物件構建的,這與 Sass 的不可變特性背道而馳。它也沒有提供實用方法(例如在映射中查找鍵)來更容易地實作慣用的自訂函式,並且沒有提供對值的關鍵資訊的存取,例如字串是否帶引號。

  • 所有非同步函式都是基於回呼而不是基於 Promise 的。

新的 API 使用現代的、慣用的 API 解決了這些問題以及更多問題,這將使從 JS 使用 Sass 變得輕而易舉。

編譯編譯 永久連結

API 的核心是四個執行實際 Sass 編譯的函式,兩個同步的和兩個非同步的。它們在此以 TypeScript 語法呈現,以闡明它們的確切輸入和返回,但您始終可以從純 JS 呼叫它們。

function compile(
  path: string,
  options?: Options<'sync'>
): CompileResult;

function compileString(
  source: string,
  options?: StringOptions<'sync'>
): CompileResult;

function compileAsync(
  path: string,
  options?: Options<'async'>
): Promise<CompileResult>;

function compileStringAsync(
  source: string,
  options?: StringOptions<'async'>
): Promise<CompileResult>;

compile()compileAsync() 函式從磁碟上的路徑載入 Sass 檔案,而 compileString()compileStringAsync() 編譯作為字串傳入的 Sass 原始碼。所有這些都採用以下選項:

  • alertAscii:錯誤和警告是否應僅使用 ASCII 字元(而不是例如 Unicode 方框繪製字元)。
  • alertColor:是否錯誤和警告訊息應該使用終端機顏色。
  • loadPaths:用於查找要載入檔案的檔案路徑清單,就像舊 API 中的 includePaths 一樣。
  • importers:用於載入 Sass 原始碼檔案的自訂匯入器清單。
  • functions:一個物件,其鍵是 Sass 函式簽章,值是自訂函式
  • quietDeps:是否隱藏依賴項中的棄用警告。
  • logger:用於發出警告和除錯訊息的自訂記錄器
  • sourceMap:是否在編譯期間產生原始碼對應。
  • style:輸出樣式,'compressed''expanded'
  • verbose:是否發出每個遇到的棄用警告。

compileString()compileStringAsync() 函式需要一些額外的選項。

  • syntax:檔案的語法,'scss'(預設值)、'indented''css'
  • url:檔案的標準 URL
  • importer:被視為檔案來源的自訂匯入器。如果傳入此選項,則此匯入器將用於解析此樣式表中的相對載入。

所有這些函式都會返回一個包含以下欄位的物件:

  • css:編譯後的 CSS,以字串形式呈現。
  • loadedUrls:編譯期間載入的所有 URL,沒有特定順序。
  • sourceMap:如果傳入了 sourceMap: true,則為檔案的原始碼對應,以解碼後的物件形式呈現。

與 Node Sass API 一樣,同步函式將比其非同步函式快得多。遺憾的是,新的 API 不支援使用 fibers 選項來加速非同步編譯,因為fibers 套件已停止維護

記錄器記錄器永久連結

記錄器 API 讓您可以更精細地控制警告和除錯訊息的發出方式和時機。與此提案的其他方面不同,舊 API 中也會新增 logger 選項,讓您無需立即升級到新 API 即可控制訊息。

記錄器實作以下介面:

interface Logger {
  warn?(
    message: string,
    options: {
      deprecation: boolean;
      span?: SourceSpan;
      stack?: string;
    }
  ): void;

  debug?(
    message: string,
    options: {span: SourceSpan}
  ): void;
}

warn 函式處理警告,包括來自編譯器本身的警告和來自 @warn 規則的警告。它會被傳入:

  • 警告訊息
  • 一個旗標,指示它是否特別是一個棄用警告
  • 一個範圍,指示警告的位置(如果它來自特定位置)
  • 遇到警告時的 Sass 堆疊追蹤(如果是在執行期間遇到)

debug 函式僅處理 @debug 規則,並且只會傳入訊息和規則的範圍。有關 SourceSpan 類型的更多資訊,請參閱記錄器提案

Sass 還會提供一個內建記錄器 Logger.silent,它永遠不會發出任何訊息。這將允許您輕鬆地在「靜默模式」下執行 Sass,在此模式下永遠不會顯示任何警告。

匯入器匯入器永久連結

新的 API 不是將匯入器建模為單函式回呼,而是將它們建模為公開兩種方法的物件:一種方法用於*標準化* URL,另一種方法用於*載入*標準 URL。

// Importers for compileAsync() and compileStringAsync() are the same, except
// they may return Promises as well.
interface Importer {
  canonicalize(
    url: string,
    options: {fromImport: boolean}
  ): URL | null;

  load(canonicalUrl: URL): ImporterResult | null;
}

請注意,即使是透過 compile()loadPaths 直接從檔案系統載入的樣式表,也會被視為是由導入器載入的。這個內建的檔案系統導入器會將所有路徑正規化為 file: URL,並從實體檔案系統載入這些 URL。

正規化正規化永久連結

第一步是確定樣式表的正規 URL。每個樣式表都只有一個正規 URL,而該 URL 又只指向一個樣式表。正規 URL 必須是絕對的,包含通訊協定,但其具體結構則由導入器決定。在大多數情況下,相關的樣式表會存在於磁碟上,導入器只會傳回它的 file: URL。

canonicalize() 方法接受一個 URL 字串,該字串可以是相對的或絕對的。如果導入器辨識該 URL,它會傳回一個對應的絕對 URL(包含通訊協定)。這是相關樣式表的 *正規 URL*。雖然輸入的 URL 可以省略檔案副檔名或開頭的底線,但正規 URL 必須是完整解析的。

對於從檔案系統載入的樣式表,正規 URL 將會是磁碟上實體檔案的絕對 file: URL。如果是記憶體中產生的,導入器應該選擇一個自訂的 URL 通訊協定,以確保其正規 URL 不會與任何其他導入器的 URL 衝突。

例如,如果您要從資料庫載入 Sass 檔案,您可以使用 db: 通訊協定。與資料庫中鍵值 styles 相關聯的樣式表的正規 URL 可能是 db:styles

此函式也接受一個 fromImport 選項,用於指示導入器是否是由 @import 規則(而不是 @use@forwardmeta.load-css())所呼叫的。

每個樣式表都有一個正規 URL,這讓 Sass 可以確保在新的模組系統中不會重複載入同一個樣式表。

正規化相對載入正規化相對載入永久連結

當樣式表嘗試載入相對 URL(例如 @use "variables")時,從文件本身無法清楚看出它是指相對於樣式表存在的檔案,還是指另一個導入器或載入路徑。導入器 API 就是這樣解決這個歧義的:

  • 首先,相對 URL 會相對於包含 @use(或 @forward@import)的樣式表的正規 URL 來解析。例如,如果正規 URL 是 file:///path/to/my/_styles.scss,則解析後的 URL 將會是 file:///path/to/my/variables

  • 然後,這個 URL 會傳遞給載入舊樣式表的導入器的 canonicalize() 方法。(這表示您的導入器支援絕對 URL 非常重要!)如果導入器辨識它,它會傳回正規值,然後將該值傳遞給該導入器的 load();否則,它會傳回 null

  • 如果舊樣式表的導入器無法辨識該 URL,它會依其在 options 中出現的順序傳遞給所有 importerscanonicalize 函式,然後在所有 loadPaths 中檢查。如果這些都無法辨識它,則載入會失敗。

局部相對路徑優先於其他導入器或載入路徑非常重要,因為否則您的局部樣式表可能會因為相依性新增了名稱衝突的檔案而意外損壞。

載入載入永久連結

第二個步驟實際載入樣式表的文字。load() 方法接受由 canonicalize() 返回的規範 URL,並返回該 URL 樣式表的內容。對於每個規範 URL,在每次編譯時只會呼叫一次;未來載入相同的 URL 將會重複使用現有的模組(適用於 @use@forward)或解析樹(適用於 @import)。

load() 方法返回一個包含以下欄位的物件:

  • css:已載入樣式表的文字。
  • syntax:檔案的語法:'scss''indented''css'
  • sourceMapUrl:一個可選的、瀏覽器可存取的 URL,用於在引用此檔案時包含在來源對應中。

檔案匯入器檔案匯入器永久連結

此提案還新增了一種特殊類型的匯入器,稱為「檔案匯入器」。這個匯入器讓將載入重新導向至檔案系統上某處的常見案例變得更容易。它不需要呼叫者實作 load(),因為對於磁碟上的檔案來說,這總是相同的。

interface FileImporter {
  findFileUrl(
    url: string,
    options: {fromImport: boolean}
  ): FileImporterResult | null;
}

findFileUrl() 方法接受一個相對 URL,並返回一個包含以下欄位的物件:

  • url:要載入的檔案的絕對 file: URL。這個 URL 不需要完全規範化:Sass 編譯器將會處理解析部分檔案、檔案副檔名、索引檔等等。
  • sourceMapUrl:一個可選的、瀏覽器可存取的 URL,用於在引用此檔案時包含在來源對應中。

函式函式永久連結

新的函式 API 的函式類型與舊的 API 非常相似。

type CustomFunctionCallback = (args: Value[]) => Value;

唯一的區別是:

  • 非同步函式返回一個 Promise<Value>,而不是呼叫回呼函式。
  • 值類型本身不同。

不過,第二點相當重要!新的值類型比舊版本更加完善。讓我們從父類別開始。

abstract class Value {
  /**
   * Returns the values of `this` when interpreted as a list.
   *
   * - For a list, this returns its elements.
   * - For a map, this returns each of its key/value pairs as a `SassList`.
   * - For any other value, this returns a list that contains only that value.
   */
  get asList(): List<Value>;

  /** Whether `this` is a bracketed Sass list. */
  get hasBrackets(): boolean;

  /** Whether `this` is truthy (any value other than `null` or `false`). */
  get isTruthy(): boolean;

  /** Returns JS's null if this is `sassNull`, or `this` otherwise. */
  get realNull(): null | Value;

  /** If `this` is a list, return its separator. Otherwise, return `null`. */
  get separator(): ListSeparator;

  /**
   * Converts the Sass index `sassIndex` to a JS index into the array returned
   * by `asList`.
   *
   * Sass indices start counting at 1, and may be negative in order to index
   * from the end of the list.
   */
  sassIndexToListIndex(sassIndex: Value): number;

  /**
   * Returns `this` if it's a `SassBoolean`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertBoolean(name?: string): SassBoolean;

  /**
   * Returns `this` if it's a `SassColor`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertColor(name?: string): SassColor;

  /**
   * Returns `this` if it's a `SassFunction`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertFunction(name?: string): SassFunction;

  /**
   * Returns `this` if it's a `SassMap` (or converts it to a `SassMap` if it's
   * an empty list), and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertMap(name?: string): SassMap;

  /**
   * Returns `this` if it's a `SassNumber`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertNumber(name?: string): SassNumber;

  /**
   * Returns `this` if it's a `SassString`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertString(name?: string): SassString;

  /**
   * Returns the value of `this` if it can be interpreted as a map.
   *
   * - If this is a map, returns its contents.
   * - If this is an empty list, returns an empty map.
   * - Otherwise, returns `null`.
   */
  tryMap(): OrderedMap<Value, Value> | null;

  /** Returns whether `this == other` in SassScript. */
  equals(other: Value): boolean;
}

這裡有幾點需要注意:

  • 由於 CSS 在語法上沒有嚴格區分單個元素和包含一個元素的列表,因此任何 Sass 值都可以被視為列表。Value 類別透過為每個 Value 提供 asList()hasBrackets()separator() getter,讓遵循此慣例變得更容易。

  • 以此方式返回的列表和 asMap() 返回的映射是來自 immutable 套件 的不可變類型。這反映了 Sass 所有類型的內建不可變性。雖然這些值不能直接修改,但它們的 API 讓建立應用變更的新值變得簡單高效。

  • Sass 的列表索引慣例與 JavaScript 的不同。sassIndexToListIndex() 函式讓從 Sass 索引轉換為 JS 索引變得更容易。

  • 在 Sass 中,任何值都可以在布林值上下文中使用,其中 falsenull 計為「假值」。isTruthy getter 讓遵循此慣例變得更容易。

  • assert*() 函式讓確保您接收到預期的引數,並在未接收到時拋出慣用錯誤變得更容易。它們對於 TypeScript 使用者特別有用,因為它們會自動縮小 Value 的類型。

大多數 Sass 值都有自己的子類別,但有三個單例值僅以常數形式提供:`sassTrue`、`sassFalse` 和 `sassNull` 分別代表 Sass 的 `true`、`false` 和 `null` 值。

顏色顏色永久連結

新的 API 的 `SassColor` 類別提供以 RGB、HSL 和 HWB 格式存取顏色的功能。與內建的 Sass 顏色函式一樣,無論顏色最初是如何建立的,都可以在任何顏色上存取任何屬性。

class SassColor extends Value {
  /** Creates an RGB color. */
  static rgb(
    red: number,
    green: number,
    blue: number,
    alpha?: number
  ): SassColor;

  /** Creates an HSL color. */
  static hsl(
    hue: number,
    saturation: number,
    lightness: number,
    alpha?: number
  ): SassColor;

  /** Creates an HWB color. */
  static hwb(
    hue: number,
    whiteness: number,
    blackness: number,
    alpha?: number
  ): SassColor;

  /** The color's red channel. */
  get red(): number;

  /** The color's green channel. */
  get green(): number;

  /** The color's blue channel. */
  get blue(): number;

  /** The color's hue. */
  get hue(): number;

  /** The color's saturation. */
  get saturation(): number;

  /** The color's lightness. */
  get lightness(): number;

  /** The color's whiteness. */
  get whiteness(): number;

  /** The color's blackeness. */
  get blackness(): number;

  /** The color's alpha channel. */
  get alpha(): number;

  /**
   * Returns a copy of `this` with the RGB channels updated to match `options`.
   */
  changeRgb(options: {
    red?: number;
    green?: number;
    blue?: number;
    alpha?: number;
  }): SassColor;

  /**
   * Returns a copy of `this` with the HSL values updated to match `options`.
   */
  changeHsl(options: {
    hue?: number;
    saturation?: number;
    lightness?: number;
    alpha?: number;
  }): SassColor;

  /**
   * Returns a copy of `this` with the HWB values updated to match `options`.
   */
  changeHwb(options: {
    hue?: number;
    whiteness?: number;
    blackness?: number;
    alpha?: number;
  }): SassColor;

  /** Returns a copy of `this` with `alpha` as its alpha channel. */
  changeAlpha(alpha: number): SassColor;
}

數字數字永久連結

`SassNumber` 類別將其分子和分母單位儲存為陣列而不是字串。此外,它還提供用於斷言其具有特定單位的方法(`assertNoUnits()`、`assertUnit()`)以及將其轉換為特定單位的方法(`convert()`、`convertToMatch()`、`convertValue()`、`convertValueToMatch()`、`coerce()`、`coerceValue()`、`coerceValueToMatch()`)。

Sass 的數值邏輯也與 JS 略有不同,因為 Sass 認為差異小於第 10 位小數的數字是相同的。這個 API 提供了一些方法,可以幫助在 Sass 的數值邏輯和 JavaScript 的數值邏輯之間進行轉換。

class SassNumber extends Value {
  /** Creates a Sass number with no units or a single numerator unit. */
  constructor(value: number, unit?: string);

  /** Creates a Sass number with multiple numerator and/or denominator units. */
  static withUnits(
    value: number,
    options?: {
      numeratorUnits?: string[] | List<string>;
      denominatorUnits?: string[] | List<string>;
    }
  ): SassNumber;

  /** This number's value. */
  get value(): number;

  /**
   * Whether `value` is an integer according to Sass's numeric logic.
   *
   * The integer value can be accessed using `asInt`.
   */
  get isInt(): boolean;

  /**
   * If `value` is an integer according to Sass's numeric logic, returns the
   * corresponding JS integer, or `null` if `value` isn't an integer.
   */
  get asInt(): number | null;

  /** This number's numerator units. */
  get numeratorUnits(): List<string>;

  /** This number's denominator units. */
  get denominatorUnits(): List<string>;

  /** Whether `this` has numerator or denominator units. */
  get hasUnits(): boolean;

  /**
   * If `value` is an integer according to Sass's numeric logic, returns the
   * corresponding JS integer, or throws an error if `value` isn't an integer.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertInt(name?: string): number;

  /**
   * If `value` is between `min` and `max` according to Sass's numeric logic,
   * returns it clamped to that range. Otherwise, throws an error.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertInRange(min: number, max: number, name?: string): number;

  /**
   * Returns `this` if it has no units. Otherwise, throws an error.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertNoUnits(name?: string): SassNumber;

  /**
   * Returns `this` if it has `unit` as its single (numerator) unit. Otherwise,
   * throws an error.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertUnit(name?: stringunit: string): SassNumber;

  /** Returns whether `this` has the single numerator unit `unit`. */
  hasUnit(unit: string): boolean;

  /** Returns whether this number's units are compatible with `unit`. */
  compatibleWithUnit(unit: string): boolean;

  /**
   * If this number's units are compatible with `newNumerators` and
   * `newDenominators`, returns a new number with those units that's equal to
   * `this`. Otherwise, throws an error.
   *
   * Note that unitless numbers are only compatible with other unitless numbers.
   */
  convert(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): SassNumber;

  /**
   * If this number's units are compatible with `other`'s, returns a new number
   * with `other`'s units that's equal to `this`. Otherwise, throws an error.
   *
   * Note that unitless numbers are only compatible with other unitless numbers.
   */
  convertToMatch(other: SassNumber): SassNumber;

  /** Equivalent to `convert(newNumerators, newDenominators).value`. */
  convertValue(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): number;

  /** Equivalent to `convertToMatch(other).value`. */
  convertValueToMatch(other: SassNumber): number;

  /**
   * Like `convert()`, but if `this` is unitless returns a copy of it with the
   * same value and the given units.
   */
  coerce(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): SassNumber;

  /**
   * Like `convertToMatch()`, but if `this` is unitless returns a copy of it
   * with the same value and `other`'s units.
   */
  coerceToMatch(other: SassNumber): SassNumber;

  /** Equivalent to `coerce(newNumerators, newDenominators).value`. */
  coerceValue(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): number;

  /** Equivalent to `coerceToMatch(other).value`. */
  coerceValueToMatch(other: SassNumber): number;
}

字串字串永久連結

`SassString` 類別提供關於字串是否帶引號的資訊。與列表一樣,JS 的索引概念與 Sass 不同,因此它也提供了 `sassIndexToStringIndex()` 方法,用於將 JS 索引轉換為 Sass 索引。

class SassString extends Value {
  /** Creates a string with the given `text`. */
  constructor(
    text: string,
    options?: {
      /** @default true */
      quotes: boolean;
    }
  );

  /** Creates an empty string`. */
  static empty(options?: {
    /** @default true */
    quotes: boolean;
  }): SassString;

  /** The contents of `this`. */
  get text(): string;

  /** Whether `this` has quotes. */
  get hasQuotes(): boolean;

  /** The number of Unicode code points in `text`. */
  get sassLength(): number;

  /**
   * Converts the Sass index `sassIndex` to a JS index into `text`.
   *
   * Sass indices start counting at 1, and may be negative in order to index
   * from the end of the list. In addition, Sass indexes strings by Unicode code
   * point, while JS indexes them by UTF-16 code unit.
   */
  sassIndexToStringIndex(sassIndex: Value): number;
}

列表列表永久連結

如上所述,大多數列表函式都在 `Value` 父類別上,以便輕鬆遵循 Sass 將所有值視為列表的慣例。但是,仍然可以建構 `SassList` 類別來建立新的列表。

class SassList extends Value {
  /** Creates a Sass list with the given `contents`. */
  constructor(
    contents: Value[] | List<Value>,
    options?: {
      /** @default ',' */
      separator?: ListSeparator;
      /** @default false */
      brackets?: boolean;
    }
  );

  /** Creates an empty Sass list. */
  static empty(options?: {
    /** @default null */
    separator?: ListSeparator;
    /** @default false */
    brackets?: boolean;
  }): SassList;
}

映射映射永久連結

`SassMap` 類別只是將其內容公開為來自 `immutable` 套件 的 `OrderedMap`。

class SassMap extends Value {
  /** Creates a Sass map with the given `contents`. */
  constructor(contents: OrderedMap<Value, Value>);

  /** Creates an empty Sass map. */
  static empty(): SassMap;

  /** Returns this map's contents. */
  get contents(): OrderedMap<Value, Value>;
}

函式函式永久連結

`SassFunction` 類別相當嚴格:它只允許使用同步回呼建立新的第一級函式。這些函式不能由自訂函式呼叫,但它們仍然比舊的 API 提供更多功能!

class SassFunction extends Value {
  /**
   * Creates a Sass function value with the given `signature` that calls
   * `callback` when it's invoked.
   */
  constructor(
    signature: string,
    callback: CustomFunctionCallback
  );
}

更多資訊更多資訊永久連結

如果您想了解更多關於這些提案的資訊,並查看它們最新的版本,可以在 GitHub 上完整查看它們。

我們渴望獲得您的意見回饋,所以請告訴我們您的想法!相關提案將在本部落格文章發佈後至少開放一個月,甚至可能更久,取決於圍繞它們的討論有多熱烈。