戰(zhàn))
上周剛處理完一個(gè)工單用戶反饋訂單備注里的換行在詳情頁全擠成了一行。我翻了半天代碼發(fā)現(xiàn)又是老問題數(shù)據(jù)里有\(zhòng)n頁面卻原樣輸出了個(gè)空格。這種問題在小程序開發(fā)里太常見了尤其是從 H5 轉(zhuǎn)過來的人前三天必踩。小程序里的換行和空格表面上是“寫個(gè)符號(hào)、調(diào)個(gè)樣式”的事實(shí)際上牽扯到 WXML 渲染機(jī)制、CSS 空白符處理規(guī)則、組件屬性差異甚至不同機(jī)型的textarea行為。這篇文章就把這塊一次性講透。無論你是做原生微信小程序、用 uni-app 或 Taro 跨端開發(fā)還是在小程序里接圖表庫、富文本渲染這篇文章都適合花十分鐘讀一遍。我不只給結(jié)論還會(huì)把每個(gè)方案背后的原理說清楚并附上實(shí)際項(xiàng)目里踩坑、排查的經(jīng)驗(yàn)。1. 先搞懂小程序里換行和空格為什么總是不按套路出牌1.1 換行失效和空格合并根子出在哪在普通網(wǎng)頁里HTML 渲染文本時(shí)有個(gè)廣為人知的規(guī)則多個(gè)連續(xù)空格會(huì)被合并成一個(gè)\n換行符默認(rèn)也會(huì)被當(dāng)作普通空白。CSS 里有個(gè)屬性專門管這事兒叫white-space默認(rèn)值是normal。你寫一千個(gè)空格到 HTML 里最終展示出來的就是一個(gè)空格你寫十個(gè)換行符頁面上可能也只看到一個(gè)空格而不是空白的十行。小程序雖然在視覺上長得像網(wǎng)頁底層渲染卻是基于 WebView 或 Skyline 的自繪引擎。但為了兼容前端開發(fā)習(xí)慣WXML 的文本渲染在大多數(shù)場景下仍然遵循類似的空白符折疊規(guī)則。也就是說你在 JS 數(shù)據(jù)里放了\n如果組件不做任何處理渲染層可能把它當(dāng)成普通空白符直接折疊掉界面上自然看不到換行。這里要特別強(qiáng)調(diào)一個(gè)容易混淆的地方換行數(shù)據(jù)顯示異常很多時(shí)候不是 CSS 寫錯(cuò)了而是數(shù)據(jù)在進(jìn)入 WXML 的時(shí)候就已經(jīng)“退化”了。比如你這樣做Page({ data: { content: 第一行\(zhòng)n第二行\(zhòng)n第三行 } })然后在 WXML 里直接寫view{{content}}/view大部分情況下你看到的是一行“第一行 第二行 第三行”中間的\n變成了空格。原因就是view的默認(rèn)white-space: normal把換行符折疊了。1.2 WXML不是HTML你以為的“約定俗成”在這里往往不靈很多從 H5 轉(zhuǎn)到小程序的朋友會(huì)不自覺地把 HTML 的寫法搬進(jìn)來最典型的就是在文本里用br/。在 HTML 里br/是一個(gè)強(qiáng)制換行元素寫一個(gè)就換一行。但在小程序的 WXML 中view、text這類組件不是 HTML 標(biāo)簽br/這個(gè)元素其實(shí)并不被 WXML 原生支持。如果你在小程序里寫view第一行br/第二行/viewWXML 解析器不會(huì)報(bào)錯(cuò)但它會(huì)把這個(gè)br/當(dāng)成一個(gè)無法識(shí)別的節(jié)點(diǎn)實(shí)際渲染效果非常不可控。正確的做法是使用rich-text組件或者直接用 CSS 控制換行再或者在數(shù)據(jù)層面直接處理。另外小程序里有一個(gè)被很多教程忽略的組件屬性就是text組件的decode屬性。decode的作用是讓nbsp;、lt;、gt;、apos;這類 HTML 實(shí)體在text組件里被正確解碼。注意decode并不會(huì)直接讓\n變成換行但它會(huì)影響你對(duì)“空格”這個(gè)問題的處理策略這一點(diǎn)下面會(huì)詳細(xì)說。還有人會(huì)遇到一個(gè)情況同樣的代碼在開發(fā)者工具里預(yù)覽是正常的換行一上真機(jī)就失效。這種問題多半跟組件的white-space、平臺(tái)渲染差異有關(guān)。開發(fā)者工具默認(rèn)的 WebView 渲染內(nèi)核和真機(jī)的 WebView 版本不一定完全一致尤其涉及white-space: pre-line這類與文本布局密切相關(guān)的屬性時(shí)版本差異容易被放大。2. 換行方案全梳理從\n到CSS的多種實(shí)現(xiàn)2.1 JS字符串在數(shù)據(jù)源頭就把換行處理好最簡單、也最不容易出錯(cuò)的方案是在 JS 層就把換行處理好。如果后端返回的數(shù)據(jù)里有\(zhòng)n前端展示時(shí)想讓換行生效我建議優(yōu)先在數(shù)據(jù)層面對(duì)字符串做一次統(tǒng)一轉(zhuǎn)義。比如封裝一個(gè)工具函數(shù)function formatText(str) { return str.replace(/\r\n/g, \n).replace(/\n/g, String.fromCharCode(10)); }這里要提一個(gè)容易踩的坑不同平臺(tái)上傳的文本換行符可能是\r\nWindows 風(fēng)格也可能是\nUnix 風(fēng)格。如果直接展示后者能正常換行前者可能會(huì)多出一個(gè)隱藏的回車符造成一些莫名其妙的間距問題。統(tǒng)一替換成\n可以規(guī)避這個(gè)坑。有了干凈的\n數(shù)據(jù)之后還需要搭配正確的渲染方式。如果你用的是text組件并且版本支持decode開啟后\n在多數(shù)新版本中可以被正確識(shí)別為換行。但如果你是view組件直接在數(shù)據(jù)中間放\n通常不會(huì)生效。這時(shí)候就要用到下面兩種方式之一CSS 的white-space或者把文本拆成多個(gè)節(jié)點(diǎn)展示。2.2 用text組件的decode屬性讓轉(zhuǎn)義符重新生效來看一段真實(shí)有效的代碼text decode{{true}}{{content}}/text當(dāng)content是“第一行\(zhòng)n第二行”時(shí)在基礎(chǔ)庫較新的版本里text組件會(huì)解析\n為換行。同時(shí)decode還會(huì)解析nbsp;為空格。這個(gè)屬性對(duì)富文本類的輕量展示很有用但它不是萬能的decode不會(huì)幫你解析類似br/這樣的 HTML 標(biāo)簽。我在實(shí)際項(xiàng)目中遇到過一個(gè)問題iOS 上text組件對(duì)decode的支持一直比較穩(wěn)定但安卓低版本 WebView 上偶爾出現(xiàn)decode不生效、\n原樣顯示的情況。所以如果你的業(yè)務(wù)對(duì)換行展示要求比較高我建議不要只依賴decode而是采用更通用的 CSS 方案。2.3 CSS樣式兜底white-space才是最終裁判white-space屬性是處理換行和空格問題時(shí)的“終極武器”。它有normal、pre、pre-wrap、pre-line、nowrap等幾個(gè)值理解這幾個(gè)值的區(qū)別是解決此類問題的核心。屬性值作用換行符連續(xù)空格適用場景normal默認(rèn)自動(dòng)合并空白無效合并普通文本pre像 pre 標(biāo)簽一樣保留生效保留代碼展示場景pre-wrap保留空白與換行并自動(dòng)換行生效保留聊天消息、評(píng)論列表pre-line保留換行合并連續(xù)空格生效合并常規(guī)文本內(nèi)容nowrap不換行所有的換行符和空格都不生效無效合并滾動(dòng)提示、單行省略我在項(xiàng)目中用得最多的是pre-wrap。比如做聊天記錄、訂單備注這類場景我會(huì)這樣寫.msg-content { white-space: pre-wrap; word-break: break-all; }word-break: break-all也很重要。因?yàn)槿绻婚L串英文或者鏈接沒有空格white-space: pre-wrap雖然可以保留換行符但長單詞本身不會(huì)被折行結(jié)果就是文本超出容器。加上了word-break才能保證長文本也能乖乖換行。2.4 rich-text與富文本內(nèi)容里的換行處理如果你的數(shù)據(jù)是富文本比如后端返回了一段 HTML里面包含p、br/等標(biāo)簽?zāi)敲茨憔筒荒苤竿鹴ext組件來渲染了。小程序提供了rich-text組件它支持解析 HTML 字符串rich-text nodes{{htmlContent}}/rich-textrich-text對(duì)br/、p這類標(biāo)簽的解析能力比較完善所以富文本內(nèi)容里的換行一般交給它解決就行。但是這里有兩個(gè)細(xì)節(jié)要注意。第一rich-text的性能上限不高大量節(jié)點(diǎn)或長文本時(shí)頁面會(huì)明顯卡頓。因?yàn)閞ich-text的節(jié)點(diǎn)解析和渲染開銷遠(yuǎn)高于普通text。如果你只是展示一段幾萬字的協(xié)議文本我建議把它拆成多個(gè)段落分別用viewtext渲染而不是整體塞進(jìn)rich-text。第二rich-text的樣式繼承規(guī)則和普通組件不一樣。它對(duì)class的支持有限很多 CSS 選擇器不生效尤其是嵌套選擇器。給rich-text內(nèi)的文本設(shè)置樣式時(shí)最好使用內(nèi)聯(lián)樣式或者在nodes數(shù)組里直接指定attrs.style否則容易出現(xiàn)排版和換行都不可控的情況。3. 空格方案全梳理從全角空格到letter-spacing3.1 基礎(chǔ)空格\u00A0、\u3000、\u2003到底怎么選很多新手處理空格第一反應(yīng)是直接敲空格鍵。但在小程序文本渲染中連續(xù)普通空格默認(rèn)會(huì)被合并你敲十下空格和敲一下沒有本質(zhì)區(qū)別。真正可靠的是使用 Unicode 空格字符。字符Unicode 編碼寬度說明普通空格U0020約等于一個(gè)窄空格易被合并不間斷空格U00A0約等于一個(gè)普通空格nbsp;對(duì)應(yīng)的編碼不會(huì)被合并全角空格U3000一個(gè)漢字寬度中文排版常用三倍em空格U2003三個(gè)零寬空格的寬度常見于 Markdown 縮進(jìn)在 JS 數(shù)據(jù)里我會(huì)這樣處理Page({ data: { text: 姓名張三\u00A0\u00A0\u00A0年齡25\u00A0\u00A0\u00A0城市北京 } })這里用了\u00A0來保證兩個(gè)“姓名”“年齡”“城市”之間的對(duì)齊。但如果只是加一兩個(gè)空格\u00A0沒問題想實(shí)現(xiàn)精確對(duì)齊尤其是中文文本還是建議用下面要講的text組件space屬性或者 CSS 的letter-spacing。3.2 text組件的space屬性小程序給我們準(zhǔn)備好的快捷方式小程序的text組件有一個(gè)很實(shí)用的屬性space。它接受三個(gè)值nbsp、ensp、emsp分別對(duì)應(yīng)不間斷空格、半個(gè)中文字符寬度空格、一個(gè)中文字符寬度空格。text spaceemsp這是一段使用emsp空格的文本/text這個(gè)屬性的價(jià)值在于你不用在數(shù)據(jù)里折騰各種 Unicode 字符直接在 WXML 中就能控制空格寬度。它非常適合表單回顯、消息列表、信息展示這類需要一定對(duì)齊效果的場景。但注意space只對(duì)text組件生效對(duì)view直接包著的文本不生效。而且它給的空格是等寬空間不是彈性的不會(huì)像 CSSflexjustify-content: space-between那樣把兩段文字推到兩端。所以在做“左文字右數(shù)值”對(duì)齊時(shí)用 flex 布局通常比空格更靠譜。3.3 用CSS精確控制間距l(xiāng)etter-spacing與word-spacing的正確用法如果空格需求不是“插入一個(gè)空字符”而是“控制字與字之間、字符與字符之間的距離”CSS 提供的方案更優(yōu)雅。letter-spacing控制所有字符之間的間距。比如做標(biāo)題時(shí)想讓文字更舒展.title { letter-spacing: 2px; }這樣“雙十一活動(dòng)”會(huì)顯示成“雙 十 一 活 動(dòng)”的效果比在大段文本里手動(dòng)插空格要穩(wěn)定得多。word-spacing則控制詞與詞之間的距離主要對(duì)英文等有詞邊界的語言有效對(duì)中文基本沒用因?yàn)橹形臎]有空格分隔的詞邊界概念。還有一個(gè)冷門技巧利用 CSS 的text-indent做首行縮進(jìn)。中文排版習(xí)慣是段落開頭空兩個(gè)全角空格但你不需要真的插兩個(gè)全角空格設(shè)置text-indent: 2em就行這樣縮進(jìn)的寬度會(huì)跟隨字體大小縮放整體更統(tǒng)一。4. 綜合實(shí)戰(zhàn)消息列表、ECharts Tooltip、Markdown展示4.1 實(shí)戰(zhàn)一仿IM聊天消息的多行文本與時(shí)間對(duì)齊聊天消息是最典型的換行場景。用戶發(fā)的內(nèi)容里既有長中文又有帶鏈接的英文還要在消息旁邊顯示發(fā)送時(shí)間。先看完整代碼view classmsg-item view classmsg-bubble text classmsg-text{{item.content}}/text /view text classmsg-time{{item.time}}/text /view.msg-bubble { max-width: 60%; background: #f5f5f5; border-radius: 8px; padding: 12px 16px; } .msg-text { white-space: pre-wrap; word-break: break-all; line-height: 1.6; } .msg-time { margin-top: 4px; font-size: 22rpx; color: #999; display: block; }這個(gè)做法的核心就是給文本區(qū)域設(shè)置pre-wrap。我自己做消息列表時(shí)還遇到過content以\n開頭導(dǎo)致首行出現(xiàn)空白條的情況所以處理數(shù)據(jù)時(shí)我會(huì)順手把首尾空白去掉content.replace(/^\n|\n$/g, )。還有一點(diǎn)值得注意line-height不要設(shè)得太小尤其是消息內(nèi)容有多行時(shí)line-height: 1.4以下在部分安卓機(jī)會(huì)出現(xiàn)文字?jǐn)D壓、上下行貼在一起的問題。我一般用1.6觀感更舒服。4.2 實(shí)戰(zhàn)二ECharts tooltip自動(dòng)換行與空格對(duì)齊小程序里用 ECharts 做圖表時(shí)tooltip 的換行格式化是高頻需求。ECharts 的 tooltip 默認(rèn)支持\n換行但很多人踩的坑是在小程序里數(shù)據(jù)中的\n經(jīng)過網(wǎng)絡(luò)傳輸后經(jīng)常被解析成\\n字符串而不是真正的換行符。一個(gè)穩(wěn)妥的處理方式是在 formatter 里面重新轉(zhuǎn)義tooltip: { trigger: axis, formatter: function(params) { let res params[0].name \n; params.forEach(item { res item.marker item.seriesName item.value \n; }); return res; } }這樣寫tooltip 里每一項(xiàng)都會(huì)獨(dú)占一行。如果還想讓指標(biāo)名和數(shù)值之間對(duì)齊可以用\u00A0\u00A0填充間隔因?yàn)槠胀崭裨?formatter 輸出的 HTML 中同樣會(huì)被折疊。我提醒大家ECharts 在小程序里使用的是 canvas 渲染tooltip 默認(rèn)是通過 canvas 繪制出來的不是 DOM 節(jié)點(diǎn)所以不能用 CSS 偽元素或絕對(duì)定位去調(diào)整它的換行和空格。想控制 tooltip 樣式要靠 formatter 參數(shù)和textStyle配置比如textStyle: { fontSize: 12, lineHeight: 18 }。4.3 實(shí)戰(zhàn)三Markdown內(nèi)容落地小程序的換行轉(zhuǎn)義在小程序里展示 Markdown 內(nèi)容現(xiàn)在社區(qū)方案比較多常見的是towxml或mp-html。但我發(fā)現(xiàn)不管用哪個(gè)庫都需要提前處理換行符號(hào)的兼容問題。Markdown 的換行規(guī)則比較特殊段落之間需要一個(gè)空行才表示換段落單行末尾兩個(gè)空格表示強(qiáng)制換行。轉(zhuǎn)到小程序時(shí)我一般做兩步預(yù)處理第一步把\r\n統(tǒng)一替換成\n避免 Windows 換行符干擾。第二步將單換行段落內(nèi)部的換行轉(zhuǎn)換成br/或空格將雙換行段落之間的空行轉(zhuǎn)換成/pp之類的標(biāo)簽。否則 Markdown 原文里那種普通的換行到了小程序端可能會(huì)被直接忽略。例如function markdownLineBreak(src) { // 先把連續(xù)兩個(gè)換行保留為段落分隔 let temp src.replace(/\r\n/g, \n).replace(/\n{2,}/g, \u0001); // 單換行轉(zhuǎn)為 br/ temp temp.replace(/\n/g, br/); // 恢復(fù)段落分隔 temp temp.replace(/\u0001/g, \n\n); return temp; }這個(gè)預(yù)處理做完再交給解析庫渲染就能避免“該換段的地方?jīng)]換、不該換行的地方胡亂換行”的問題。強(qiáng)調(diào)一下這個(gè)處理方案來自我的工程實(shí)踐Markdown 語法細(xì)節(jié)里還有一種“行尾兩個(gè)空格強(qiáng)制換行”的規(guī)則如果你們團(tuán)隊(duì)用的 Markdown 編輯器支持這個(gè)規(guī)則那str.replace(/ {2}\n/g, br/\n)也要加上。5. 高頻問題排查與避坑實(shí)錄5.1 換行明明寫了\n界面上就是不換行這種情況最常見的兩個(gè)原因一個(gè)是沒開啟text組件的decode一個(gè)是view的white-space是normal。排查順序建議是這樣先打印數(shù)據(jù)在onLoad里console.log(JSON.stringify(this.data.content))看\n是否真的存在于字符串中。這個(gè)步驟很關(guān)鍵因?yàn)楹蠖私涌诜祷氐腬n經(jīng)常被轉(zhuǎn)義成\\n或者直接被過濾掉了。確認(rèn)使用的組件是text還是view。如果是view直接給它加white-space: pre-wrap。如果已經(jīng)加了pre-wrap還是不換行檢查一下content里面是不是\r而不是\n或者兩者混用。這種情況一般先用.replace(/\r\n/g, \n)處理。5.2 連續(xù)空格被壓成一個(gè)怎么設(shè)都沒用這個(gè)問題本質(zhì)是瀏覽器和 WebView 的空白符折疊機(jī)制在起作用。你手敲的普通空格無論敲多少默認(rèn)都只算一個(gè)。解決思路有三個(gè)數(shù)據(jù)層使用\u00A0代替普通空格使用text spaceemsp等屬性用 CSSletter-spacing或padding控制間距優(yōu)先級(jí)方面做純文本展示且需要等寬間隔的先試text的space屬性涉及復(fù)雜對(duì)齊的直接上 flex 布局別靠空格硬撐。5.3 安卓正常、iOS異常這類“機(jī)型差異”如何定位微信小程序在不同系統(tǒng)上的渲染引擎還是有差異的。我遇到過幾次安卓真機(jī)上white-space: pre-wrap表現(xiàn)正常iOS 上同一段代碼卻把換行符當(dāng)成空格。處理這類差異我的經(jīng)驗(yàn)是這樣第一步在開發(fā)者工具里把“模擬器”切換成不同機(jī)型看但這只能做初篩不能完全代表真機(jī)。第二步用體驗(yàn)版二維碼在安卓和 iOS 真機(jī)分別測試同時(shí)打開調(diào)試把實(shí)際渲染出來的Some text對(duì)比看。第三步如果 iOS 確實(shí)異常優(yōu)先檢查基礎(chǔ)庫版本。iOS 上的 WebView 版本普遍比安卓低一些一些較新的 CSS 屬性支持程度可能不夠??梢試L試在 app.json 里設(shè)置lazyCodeLoading: requiredComponents來優(yōu)化組件加載但這個(gè)對(duì)渲染差異沒有直接影響。最穩(wěn)妥的兜底方案是不依賴 CSS而是把每行文本用數(shù)組循環(huán)渲染出來。思路是把\n拆成數(shù)組然后循環(huán)text或viewPage({ data: { lines: [第一行, 第二行, 第三行] } })view classlines-wrap view classline wx:for{{lines}} wx:keyindex{{item}}/view /view這種方案在原生開發(fā)者工具、安卓、iOS 上的表現(xiàn)基本一致因?yàn)槊恳恍卸际仟?dú)立節(jié)點(diǎn)不依賴文本渲染引擎去解析換行符。5.4 常見場景速查表下面這個(gè)表是我自己整理的做小程序文本處理時(shí)基本上照著套就行。場景推薦方案備注普通長文本段落view white-space: pre-wrap避免數(shù)據(jù)中的換行被折疊聊天消息、評(píng)論列表text decode pre-wrap消息內(nèi)容本身要有干凈的 \n代碼塊展示view white-space: pre同時(shí)配合 overflow-x: auto固定寬度下的小字說明text spaceemsp不太需要復(fù)雜的換行時(shí)首行縮進(jìn)兩個(gè)漢字text-indent: 2em不要手動(dòng)插全角空格富文本 HTML 片段rich-text注意節(jié)點(diǎn)數(shù)量長文慎用ECharts tooltip 多行展示formatter 中拼接 \n不要在數(shù)據(jù)里直接存轉(zhuǎn)義字符串Markdown 單換行轉(zhuǎn)段內(nèi)換行數(shù)據(jù)預(yù)處理成交給 mp-html 等庫之前做精確對(duì)齊的標(biāo)簽-值布局flex 或 grid用空格對(duì)齊永遠(yuǎn)不靠譜5.5 我的排查順序建議把這類問題系統(tǒng)化之后我現(xiàn)在的排查順序基本固定了先看數(shù)據(jù)源復(fù)制出一段字符串放到編輯器里打開“顯示符號(hào)”確認(rèn)換行符是什么、空格符是什么然后看組件類型text還是view再看 CSS 里的white-space最后再看是否有平臺(tái)差異。按照這個(gè)順序絕大多數(shù)換行空格問題能在五分鐘內(nèi)定位。我個(gè)人的體會(huì)是這類問題最坑的點(diǎn)通常不在某一個(gè)環(huán)節(jié)而在于“多個(gè)環(huán)節(jié)疊加”。比如數(shù)據(jù)里有\(zhòng)r\n組件用了viewCSS 又沒設(shè)pre-wrap三個(gè)問題疊加在一起表現(xiàn)就非常詭異。你單獨(dú)修一個(gè)看似沒效果其實(shí)方向是對(duì)的。所以遇到這種問題耐心一點(diǎn)一步一個(gè)變量去排查最后一定能揪出元兇。最后再分享一個(gè)小技巧在小程序里做文本預(yù)覽類功能時(shí)盡量用一個(gè)統(tǒng)一的formatRichText工具函數(shù)先處理所有文本字段把\r\n、\\n、\u00A0這類隱藏字符統(tǒng)一標(biāo)準(zhǔn)化。這個(gè)函數(shù)放好在公共模塊里后面項(xiàng)目里所有團(tuán)隊(duì)成員的換行空格問題都會(huì)少很多。