1. 前言
大家好,我是若川,歡迎關注我的公眾號:若川視野。我傾力持續組織了 3 年多每週大家一起學習 200 行左右的源碼共讀活動,感興趣的可以點此掃碼加我微信 ruochuan02 參與。另外,想學源碼,極力推薦關注我寫的專欄《學習源碼整體架構系列》,目前是掘金關注人數(5.8k+人)第一的專欄,寫有幾十篇源碼文章。
我們開發業務時經常會使用到組件庫,一般來説,很多時候我們不需要關心內部實現。但是如果希望學習和深究裏面的原理,這時我們可以分析自己使用的組件庫實現。有哪些優雅實現、最佳實踐、前沿技術等都可以值得我們借鑑。
相比於原生 JS 等源碼。我們或許更應該學習,正在使用的組件庫的源碼,因為有助於幫助我們寫業務和寫自己的組件。
如果是 Vue 技術棧,開發移動端的項目,大多會選用 vant 組件庫,目前(2024-05-02) star 多達 22.7k,已經正式發佈 4.9.0。我們可以挑選 vant 組件庫學習,我會寫一個vant 組件庫源碼系列專欄,歡迎大家關注。
vant 組件庫源碼分析系列:
- 1.vant 4 即將正式發佈,支持暗黑主題,那麼是如何實現的呢
- 2.跟着 vant 4 源碼學習如何用 vue3+ts 開發一個 loading 組件,僅88行代碼
- 3.分析 vant 4 源碼,如何用 vue3 + ts 開發一個瀑布流滾動加載的列表組件?
- 4.分析 vant 4 源碼,學會用 vue3 + ts 開發毫秒級渲染的倒計時組件,真是妙啊
- 5.vant 4.0 正式發佈了,分析其源碼學會用 vue3 寫一個圖片懶加載組件!
這次我們來學習 highlight 高亮文本組件,可以點此查看 highlight 文檔體驗。
學完本文,你將學到:
1. 如何學習組件庫的源碼
2. 如何將使用了 rsbuild 的最新版本的 vant-cli 配置開啓 sourceMap 進行調試源碼
3. 高亮文本組件的原理和具體實現
2. 準備工作
看一個開源項目,第一步應該是先看 README.md 再看貢獻文檔 github/CONTRIBUTING.md。
2.1 克隆源碼 && 跑起來
You will need Node.js >= 18 and pnpm.
# 推薦克隆我的項目
git clone https://github.com/ruochuan12/vant-analysis
cd vant-analysis/vant-v4.x
# 或者克隆官方倉庫
git clone git@github.com:youzan/vant.git
cd vant
# 啓用 pnpm 包管理器
corepack enable
# 安裝依賴,如果沒安裝 pnpm,可以用 npm i pnpm -g 安裝,或者查看官網通過其他方式安裝
pnpm i
# 啓動服務
pnpm dev
執行 pnpm dev 後,這時我們打開高亮文本組件 http://localhost:8080/#/zh-CN/highlight。
3. pnpm run dev => vant-cli dev
我們從 package.json 腳本查看 dev 命令。
// vant-v4.x/package.json
{
"private": true,
"scripts": {
"prepare": "husky install",
"dev": "pnpm --dir ./packages/vant dev",
},
"engines": {
"pnpm": ">= 9.0.0"
},
"packageManager": "pnpm@9.0.6",
}
限制了 pnpm 版本大於 9.0.0,如果運行報版本錯誤,可以升級(比如:npm i -g pnpm) pnpm 版本到 9.x。
我們繼續跟着 vant/package.json 腳本查看 dev 命令。
// vant-v4.x/packages/vant/package.json
{
"name": "vant",
"version": "4.9.0",
"scripts": {
"dev": "vant-cli dev",
}
}
我們繼續跟着 vant-cli/package.json 腳本查看 bin 命令。
// vant-v4.x/packages/vant-cli/package.json
{
"name": "@vant/cli",
"version": "7.0.2",
"type": "module",
"bin": {
"vant-cli": "./bin.js"
},
}
// vant-v4.x/packages/vant-cli/bin.js
#!/usr/bin/env node
import './lib/cli.js';
從 package.json 中的 bin 屬性可以看出,vant-cli 最終入口文件是lib/cli.js。
3.1 lib/cli.js
// vant-v4.x/packages/vant-cli/lib/cli.js
import { Command } from 'commander';
import { cliVersion } from './index.js';
const program = new Command();
program.version(`@vant/cli ${cliVersion}`);
program
.command('dev')
.description('Run dev server')
.action(async () => {
const { dev } = await import('./commands/dev.js');
return dev();
});
// vant-v4.x/packages/vant-cli/lib/commands/dev.js
import { setNodeEnv } from '../common/index.js';
import { compileSite } from '../compiler/compile-site.js';
export async function dev() {
setNodeEnv('development');
await compileSite();
}
我們可以找到對應的源文件是:vant-v4.x/packages/vant-cli/src/compiler/compile-site.ts
我們可以從 vant-cli changelog 得知,最新 7.x 版本,採用了 rsbuild,作為打包構建工具,棄用了原有的 vite。
這時我們查閲下 rsbuild 文檔,找到配置 sourceMap 的方法。
rsbuild output.sourceMap
// 類型
type SourceMap = {
js?: RspackConfig['devtool'];
css?: boolean;
};
// 默認值
const defaultSourceMap = {
js: isDev ? 'cheap-module-source-map' : false,
css: false,
};
可以搜索 vant-v4.x/packages/vant-cli 項目中的搜索 sourceMap 知道配置開啓 sourceMap。
// vant-v4.x/packages/vant-cli/lib/compiler/compile-site.js
const rsbuildConfig = {
// 省略若干代碼 ...
output: {
assetPrefix,
// make compilation faster
sourceMap: {
// 代碼裏是js false,關閉,可以關閉,啓用默認值
// js: false,
css: false,
},
}
}
往期講述了很多工具函數和腳手架相關的等,所以在此不再贅述。
3.2 利用 demo 調試源碼
帶着"高亮文本是如何實現的"問題我們直接找到 highlight demo 文件:vant/packages/vant/src/highlight/demo/index.vue。為什麼是這個文件,我在之前文章跟着 vant4 源碼學習如何用 vue3+ts 開發一個 loading 組件,僅88行代碼分析了其原理,感興趣的小夥伴點擊查看。這裏就不贅述了。
文檔上的 demo 圖如下:
對應的是以下代碼:
// vant-v4.x/packages/vant/src/highlight/demo/index.vue
<script setup lang="ts">
import VanHighlight from '..';
import { useTranslate } from '../../../docs/site';
const t = useTranslate({
'zh-CN': {
text1: '慢慢來,不要急,生活給你出了難題,可也終有一天會給出答案。',
keywords1: '難題',
keywords2: ['難題', '終有一天', '答案'],
keywords3: '生活',
multipleKeywords: '多字符匹配',
highlightClass: '設置高亮標籤類名',
},
'en-US': {
text1:
'Take your time and be patient. Life itself will eventually answer all those questions it once raised for you.',
keywords1: 'questions',
keywords2: ['time', 'life', 'answer'],
keywords3: 'life',
multipleKeywords: 'Multiple Keywords',
highlightClass: 'Highlight Class Name',
},
});
</script>
<template>
<demo-block :title="t('basicUsage')">
<van-highlight :keywords="t('keywords1')" :source-string="t('text1')" />
</demo-block>
<demo-block :title="t('multipleKeywords')">
<van-highlight :keywords="t('keywords2')" :source-string="t('text1')" />
</demo-block>
<demo-block :title="t('highlightClass')">
<van-highlight
:keywords="t('keywords3')"
:source-string="t('text1')"
highlight-class="custom-class"
/>
</demo-block>
</template>
4. 高亮
我們可以看到入口文件 src/highlight/index.ts。
4.1 入口文件 src/highlight/index.ts
// vant-v4.x/packages/vant/src/highlight/index.ts
import { withInstall } from '../utils';
import _Highlight from './Highlight';
export const Highlight = withInstall(_Highlight);
export default Highlight;
export { highlightProps } from './Highlight';
export type { HighlightProps } from './Highlight';
export type { HighlightThemeVars } from './types';
declare module 'vue' {
export interface GlobalComponents {
vanHighlight: typeof Highlight;
}
}
withInstall 函數在之前文章5.1 withInstall 給組件對象添加 install 方法 也有分析,這裏就不贅述了。
我們可以繼續看主文件 src/highlight/Highlight.tsx。
4.2 主文件 src/highlight/Highlight.tsx
// vant-v4.x/packages/vant/src/highlight/Highlight.tsx
import {
defineComponent,
computed,
type ExtractPropTypes,
type PropType,
} from 'vue';
import {
createNamespace,
makeRequiredProp,
makeStringProp,
truthProp,
} from '../utils';
const [name, bem] = createNamespace('highlight');
export const highlightProps = {
autoEscape: truthProp,
caseSensitive: Boolean,
highlightClass: String,
highlightTag: makeStringProp<keyof HTMLElementTagNameMap>('span'),
keywords: makeRequiredProp<PropType<string | string[]>>([String, Array]),
sourceString: makeStringProp(''),
tag: makeStringProp<keyof HTMLElementTagNameMap>('div'),
unhighlightClass: String,
unhighlightTag: makeStringProp<keyof HTMLElementTagNameMap>('span'),
};
export type HighlightProps = ExtractPropTypes<typeof highlightProps>;
上面代碼主要是 Props 定義:
定義了一系列 props,包括控制高亮的各種配置項,如是否自動轉義、是否區分大小寫、高亮樣式類名等。可直接參見文檔中的API屬性。
我們可以在這些文件,任意位置加上 debugger 調試源碼。比如在 renderContent 函數 debugger 調試。如下圖所示:
如果不知道怎麼調試,可以看我之前的文章新手向:前端程序員必學基本技能——調試JS代碼
// vant-v4.x/packages/vant/src/highlight/Highlight.tsx
export default defineComponent({
name,
props: highlightProps,
setup(props) {
const highlightChunks = computed(() => {
// 省略這裏的代碼,後文講述...
});
const renderContent = () => {
const {
// 慢慢來,不要急,生活給你出了難題,可也終有一天會給出答案。
sourceString,
// 高亮和非高亮樣式名和標籤名
highlightClass,
unhighlightClass,
highlightTag,
unhighlightTag,
} = props;
return highlightChunks.value.map((chunk) => {
/**
* highlightChunks.value 調試查看數值
[
{
"start": 0,
"end": 14,
"highlight": false
},
{
"start": 14,
"end": 16,
"highlight": true
},
{
"start": 16,
"end": 29,
"highlight": false
}
]
*
*/
const { start, end, highlight } = chunk;
// 取出文本
const text = sourceString.slice(start, end);
debugger;
if (highlight) {
return (
<highlightTag class={[bem('tag'), highlightClass]}>
{text}
</highlightTag>
);
}
return <unhighlightTag class={unhighlightClass}>{text}</unhighlightTag>;
});
};
return () => {
const { tag } = props;
return <tag class={bem()}>{renderContent()}</tag>;
};
},
});
這段代碼不多,就是把高亮的文本組成一個新的標籤,可以支持自定義標籤和自定義class,渲染結果如下圖所示:
setup 函數:
在 setup 函數中,通過 computed 創建了一個名為 highlightChunks 的 computed 屬性,該屬性根據傳入的關鍵詞在原始字符串中生成併合並高亮塊。
highlightChunks 的計算過程包括將關鍵詞轉為正則表達式,匹配原始字符串中的位置,並生成含有高亮樣式標記的塊。
renderContent 函數:
renderContent 函數根據 highlightChunks 的結果在原始字符串中提取每個塊並生成相應的高亮或非高亮段落。
返回函數:
返回一個渲染函數,在渲染時根據 props 中的設置,生成相應的高亮標籤或非高亮標籤,並以適當的方式組織和呈現高亮內容。
實現原理概述:
提取關鍵詞:
首先,根據傳入的關鍵詞(可以是字符串或字符串數組),將其轉換為數組形式。
生成高亮塊:
遍歷關鍵詞數組,根據是否需要轉義和是否區分大小寫,生成正則表達式進行匹配,找出原始字符串中的關鍵詞位置,並記錄下每個關鍵詞的起始和結束位置以及是否需要高亮。
合併相鄰塊:
將相鄰的高亮塊合併為一個塊,以減少多餘的高亮標記。
生成最終內容:
根據高亮塊的信息,在原始字符串中按要求插入高亮標籤或非高亮標籤,形成最終的高亮內容。
通過以上這些步驟,highlight 組件實現了在給定字符串中根據關鍵詞進行高亮展示的功能。
整體思路是根據關鍵詞通過正則匹配生成高亮塊,然後在渲染時根據這些塊的信息插入合適的標籤和自定義樣式名實現高亮效果。
4.3 highlightChunks 函數
我們簡單分析下 setup 中的 highlightChunks 函數。不用細看,可以在自己動手調試源碼時再細看。
// vant-v4.x/packages/vant/src/highlight/Highlight.tsx
const highlightChunks = computed(() => {
const { autoEscape, caseSensitive, keywords, sourceString } = props;
// 是否區分大小寫
const flags = caseSensitive ? 'g' : 'gi';
// 轉數組
const _keywords = Array.isArray(keywords) ? keywords : [keywords];
// 生成分組
// generate chunks
let chunks = _keywords
.filter((keyword) => keyword)
.reduce<Array<{ start: number; end: number; highlight: boolean }>>(
(chunks, keyword) => {
// 是否自動轉義
if (autoEscape) {
keyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// 用正則匹配
const regex = new RegExp(keyword, flags);
// 遍歷關鍵詞匹配值,最後生成 [{start, end, highlight: false}] 開始和結束值,高亮與否的數組
let match;
while ((match = regex.exec(sourceString))) {
const start = match.index;
const end = regex.lastIndex;
if (start >= end) {
regex.lastIndex++;
continue;
}
chunks.push({
start,
end,
highlight: true,
});
}
return chunks;
},
[],
);
// 合併分組
// merge chunks
chunks = chunks
.sort((a, b) => a.start - b.start)
.reduce<typeof chunks>((chunks, currentChunk) => {
const prevChunk = chunks[chunks.length - 1];
if (!prevChunk || currentChunk.start > prevChunk.end) {
const unhighlightStart = prevChunk ? prevChunk.end : 0;
const unhighlightEnd = currentChunk.start;
if (unhighlightStart !== unhighlightEnd) {
chunks.push({
start: unhighlightStart,
end: unhighlightEnd,
highlight: false,
});
}
chunks.push(currentChunk);
} else {
prevChunk.end = Math.max(prevChunk.end, currentChunk.end);
}
return chunks;
}, []);
const lastChunk = chunks[chunks.length - 1];
// 沒有關鍵詞時,沒匹配到 chunks 的時候
if (!lastChunk) {
chunks.push({
start: 0,
end: sourceString.length,
highlight: false,
});
}
if (lastChunk && lastChunk.end < sourceString.length) {
chunks.push({
start: lastChunk.end,
end: sourceString.length,
highlight: false,
});
}
return chunks;
});
5. 總結
本文主要講述了,如何閲讀組件庫的源碼,如何將使用了 rsbuild 的最新版本的 vant-cli 配置開啓 sourceMap 進行調試源碼。
學習了高亮文本組件的原理和具體實現。實現原理是根據關鍵詞通過正則匹配生成高亮塊,然後在渲染時根據這些塊的信息插入合適的標籤和自定義樣式名實現高亮效果。
組件代碼雖不多,但實現相對比較優雅。
學會寫一個組件就能學會多個組件。建議自己多打斷點調試源碼,哪裏不懂調試哪裏。常看我的源碼文章的讀者都知道,我經常推薦要自己多動手調試源碼,這樣印象更為深刻。避免出現看懂了,但動手實踐就不知道如何操作了的情況。紙上得來終覺淺,絕知此事要躬行。
6. 加源碼共讀交流羣
如果看完有收穫,歡迎點贊、評論、分享、收藏支持。你的支持和肯定,是我寫作的動力。
作者:常以若川為名混跡於江湖。所知甚少,唯善學。若川的博客,github blog,可以點個 star 鼓勵下持續創作。
最後可以持續關注我@若川,歡迎關注我的公眾號:若川視野。我傾力持續組織了 3 年多每週大家一起學習 200 行左右的源碼共讀活動,感興趣的可以點此掃碼加我微信 ruochuan02 參與。另外,想學源碼,極力推薦關注我寫的專欄《學習源碼整體架構系列》,目前是掘金關注人數(5.8k+人)第一的專欄,寫有幾十篇源碼文章。