.trim()
在深入探討樣板字串和標籤樣板這兩個功能之前,讓我們先檢視樣板一詞的多重含義。
儘管名稱中都有樣板,而且看起來都很相似,但以下三者有顯著的不同
文字樣板是一個從資料到文字的函式。它經常在網頁開發中使用,而且通常透過文字檔案定義。例如,以下文字定義了函式庫 Handlebars 的樣板
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}</div>
</div>
這個樣板有兩個空白需要填入:title
和 body
。它會像這樣使用
// First step: retrieve the template text, e.g. from a text file.
const tmplFunc = Handlebars.compile(TMPL_TEXT); // compile string
const data = {title: 'My page', body: 'Welcome to my page!'};
const html = tmplFunc(data);
樣板字串類似於字串字面值,但有額外的功能,例如內插。它以反引號為界
const num = 5;
.equal(`Count: ${num}!`, 'Count: 5!'); assert
在語法上,標籤樣板是一個函式(或更確切地說,是一個評估為函式的表達式)之後的樣板字串。這會導致呼叫函式。它的引數來自樣板字串的內容。
const getArgs = (...args) => args;
.deepEqual(
assertgetArgs`Count: ${5}!`,
'Count: ', '!'], 5] ); [[
請注意,getArgs()
會接收字面值的文字和透過 ${}
內插的資料。
與一般的字串字面值相比,樣板字串有兩個新功能。
首先,它支援字串內插:如果我們將動態計算的值置於 ${}
內,它會轉換成字串並插入字串常數傳回的字串中。
const MAX = 100;
function doSomeWork(x) {
if (x > MAX) {
throw new Error(`At most ${MAX} allowed: ${x}!`);
}// ···
}.throws(
assert=> doSomeWork(101),
() message: 'At most 100 allowed: 101!'}); {
其次,範本字串可以跨多行
const str = `this is
a text with
multiple lines`;
範本字串總是產生字串。
A 行的表達式是標記範本。它等同於使用 B 行陣列中列出的引數呼叫 tagFunc()
。
function tagFunc(...args) {
return args;
}
const setting = 'dark mode';
const value = true;
.deepEqual(
asserttagFunc`Setting ${setting} is ${value}!`, // (A)
'Setting ', ' is ', '!'], 'dark mode', true] // (B)
[[; )
第一個反引號之前的函式 tagFunc
稱為標記函式。它的引數為
${}
的文字片段。
['設定 ', ' 是 ', '!']
'深色模式'
和 true
字串常數的靜態(固定)部分(範本字串)與動態部分(替換)分開存放。
標記函式可以傳回任意值。
到目前為止,我們只看過範本字串的已煮熟詮釋。但標記函式實際上會取得兩種詮釋
已煮熟詮釋,其中反斜線具有特殊意義。例如,\t
會產生一個 tab 字元。範本字串的這種詮釋儲存在第一個引數的陣列中。
未煮熟詮釋,其中反斜線沒有特殊意義。例如,\t
會產生兩個字元,一個反斜線和一個 t
。範本字串的這種詮釋儲存在第一個引數(陣列)的屬性 .raw
中。
未煮熟詮釋透過 String.raw
(稍後說明) 和類似的應用程式啟用未煮熟字串常數。
下列標記函式 cookedRaw
使用這兩種詮釋
function cookedRaw(templateStrings, ...substitutions) {
return {
cooked: Array.from(templateStrings), // copy only Array elements
raw: templateStrings.raw,
,
substitutions;
}
}.deepEqual(
assertcookedRaw`\tab${'subst'}\newline\\`,
{cooked: ['\tab', '\newline\\'],
raw: ['\\tab', '\\newline\\\\'],
substitutions: ['subst'],
; })
我們也可以在標記範本中使用 Unicode 碼點跳脫(\u{1F642}
)、Unicode 碼元跳脫(\u03A9
)和 ASCII 跳脫(\x52
)
.deepEqual(
assertcookedRaw`\u{54}\u0065\x78t`,
{cooked: ['Text'],
raw: ['\\u{54}\\u0065\\x78t'],
substitutions: [],
; })
如果其中一個跳脫的語法不正確,對應的已煮熟範本字串會是 undefined
,而未煮熟版本仍會是逐字內容
.deepEqual(
assertcookedRaw`\uu\xx ${1} after`,
{cooked: [undefined, ' after'],
raw: ['\\uu\\xx ', ' after'],
substitutions: [1],
; })
不正確的跳脫會在範本字串和字串常數中產生語法錯誤。在 ES2018 之前,它們甚至會在標記範本中產生錯誤。為什麼會這樣變更?我們現在可以使用標記範本來處理以前是非法的文字,例如
windowsPath`C:\uuu\xxx\111`
latex`\unicode`
標記範本非常適合支援小型嵌入式語言(所謂的特定領域語言)。我們將繼續提供幾個範例。
lit-html 是基於標記範本的範本函式庫,由 前端架構 Polymer 使用
import {html, render} from 'lit-html';
const template = (items) => html`
<ul>
${
repeat(items,
=> item.id,
(item) , index) => html`<li>${index}. ${item.name}</li>`
(item
)}
</ul>
`;
repeat()
是用於迴圈的自訂函式。它的第二個參數會為第三個參數傳回的值產生唯一的金鑰。請注意該參數使用的巢狀標記範本。
re-template-tag 是用於組成正規表示式的簡單函式庫。標記為 re
的範本會產生正規表示式。主要優點是我們可以透過 ${}
內插正規表示式和純文字(A 行)
const RE_YEAR = re`(?<year>[0-9]{4})`;
const RE_MONTH = re`(?<month>[0-9]{2})`;
const RE_DAY = re`(?<day>[0-9]{2})`;
const RE_DATE = re`/${RE_YEAR}-${RE_MONTH}-${RE_DAY}/u`; // (A)
const match = RE_DATE.exec('2017-01-27');
.equal(match.groups.year, '2017'); assert
graphql-tag 函式庫 讓我們透過標記範本建立 GraphQL 查詢
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`;
此外,還有外掛程式可用於在 Babel、TypeScript 等中預編譯此類查詢。
原始字串文字是透過標籤函式 String.raw
實作的。它們是反斜線不會執行任何特殊動作(例如跳脫字元等)的字串文字
.equal(String.raw`\back`, '\\back'); assert
這有助於資料包含反斜線時,例如包含正規表示式的字串
const regex1 = /^\./;
const regex2 = new RegExp('^\\.');
const regex3 = new RegExp(String.raw`^\.`);
所有三個正規表示式都是等效的。使用一般字串文字時,我們必須寫入反斜線兩次,才能為該文字跳脫它。使用原始字串文字時,我們不必這麼做。
原始字串文字對於指定 Windows 檔案名稱路徑也很有用
const WIN_PATH = String.raw`C:\foo\bar`;
.equal(WIN_PATH, 'C:\\foo\\bar'); assert
所有剩餘章節都是進階的
如果我們在範本文字中放入多行文字,則兩個目標會產生衝突:一方面,範本文字應縮排以符合原始碼。另一方面,其內容的行應從最左邊的欄位開始。
例如
function div(text) {
return `
<div>
${text}
</div>
`;
}console.log('Output:');
console.log(
div('Hello!')
// Replace spaces with mid-dots:
.replace(/ /g, '·')
// Replace \n with #\n:
.replace(/\n/g, '#\n')
; )
由於縮排,範本文字很適合原始碼。唉呀,輸出也縮排了。我們不想要開頭的換行符號和結尾的換行符號加上兩個空格。
Output:
#<div>#
····
······Hello!#</div>#
···· ··
有兩種方法可以解決這個問題:透過標記範本或修剪範本文字的結果。
第一個修復方法是使用自訂範本標籤,以移除不需要的空白。它使用初始換行符號後的第 1 行來判斷文字從哪個欄位開始,並縮短每個地方的縮排。它也會移除最開頭的換行符號和最結尾的縮排。其中一個此類範本標籤是 Desmond Brand 的dedent
import dedent from 'dedent';
function divDedented(text) {
return dedent`
<div>
${text}
</div>
`.replace(/\n/g, '#\n');
}console.log('Output:');
console.log(divDedented('Hello!'));
這次,輸出沒有縮排
Output:<div>#
Hello!#</div>
.trim()
第二個修復方法較快,但較不乾淨
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim().replace(/\n/g, '#\n');
}console.log('Output:');
console.log(divDedented('Hello!'));
字串方法 .trim()
會移除開頭和結尾的多餘空白,但內容本身必須從最左邊的欄位開始。這個解決方案的優點是我們不需要自訂標籤函式。缺點是它看起來很醜陋。
輸出與 dedent
相同
Output:<div>#
Hello!#</div>
雖然範本文字看起來像文字範本,但如何使用它們進行(文字)範本處理並不明顯:文字範本從物件取得其資料,而範本文字從變數取得其資料。解決方案是在參數接收範本處理資料的函式主體中使用範本文字,例如
const tmpl = (data) => `Hello ${data.name}!`;
.equal(tmpl({name: 'Jane'}), 'Hello Jane!'); assert
作為更複雜的範例,我們想要取得一個地址陣列並產生一個 HTML 表格。這是陣列
const addresses = [
first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
{ ; ]
產生 HTML 表格的函式 tmpl()
如下所示
const tmpl = (addrs) => `
<table>
${addrs.map(
(addr) => `
<tr>
<td>${escapeHtml(addr.first)}</td>
<td>${escapeHtml(addr.last)}</td>
</tr>
`.trim()
).join('')}
</table>
`.trim();
此程式碼包含兩個範本函式
addrs
,一個包含地址的陣列,並傳回一個包含表格的字串。addr
,一個包含地址的物件,並傳回一個包含表格列的字串。請注意結尾的 .trim()
,它會移除不必要的空白。第一個範本函式透過將一個表格元素包覆在它加入字串的陣列中來產生其結果(第 10 行)。該陣列是透過將第二個範本函式對應到 addrs
的每個元素來產生的(第 3 行)。因此它包含有表格列的字串。
輔助函式 escapeHtml()
用於跳脫特殊的 HTML 字元(第 6 行和第 7 行)。其實作顯示在 下一個小節 中。
讓我們使用地址呼叫 tmpl()
並記錄結果
console.log(tmpl(addresses));
輸出為
<table>
<tr>
<td><Jane></td>
<td>Bond</td>
</tr><tr>
<td>Lars</td>
<td><Croft></td>
</tr>
</table>
下列函式跳脫純文字,以便它在 HTML 中逐字顯示
function escapeHtml(str) {
return str
.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`')
;
}.equal(
assertescapeHtml('Rock & Roll'), 'Rock & Roll');
.equal(
assertescapeHtml('<blank>'), '<blank>');
練習:HTML 範本化
練習與額外挑戰:exercises/template-literals/templating_test.mjs
測驗
請參閱 測驗應用程式。