欢迎光临
我们一直在努力

手搓HTML資源合併工具:自動合併CSS/JS減少請求數

手搓HTML資源合併工具:自動合併CSS/JS減少請求數

引言:網站性能優化的關鍵戰役

在現代Web開發中,網站性能直接影響用戶體驗、轉化率乃至搜索引擎排名。其中一個關鍵性能指標是頁面加載時間,而影響加載時間的重要因素就是瀏覽器需要發起的HTTP請求數量。每個CSS、JavaScript文件都需要單獨的HTTP請求,這些請求的建立、傳輸和解析都會消耗寶貴的時間。

傳統的優化方法包括手動合併CSS和JavaScript文件,但這種方法在大型項目中很快就變得難以維護。每當增加新功能或修改現有代碼時,開發者都需要重新考慮文件合併策略,這不僅耗時且容易出錯。

本文將深入探討如何從零開始構建一個自動化的HTML資源合併工具,該工具能夠智能地分析HTML文檔,自動合併CSS和JavaScript文件,並生成優化後的HTML代碼。我們將從理論基礎到實踐實現,一步步構建一個功能完整的資源合併工具。

第一章:理解資源合併的技術原理

1.1 HTTP請求開銷分析

要理解為什麼資源合併如此重要,我們首先需要分析HTTP請求的開銷:

  • DNS查詢時間:瀏覽器需要解析域名到IP地址

  • TCP握手時間:建立TCP連接需要三次握手

  • TLS協商時間(對於HTTPS):加密連接的建立

  • 請求發送時間:請求頭部的傳輸

  • 等待時間:服務器處理請求的時間

  • 響應接收時間:響應數據的傳輸時間

  • 對於小文件(如CSS和JS),這些開銷往往超過了文件本身的傳輸時間。合併多個小文件可以顯著減少這些重複的開銷。

    1.2 瀏覽器並行加載限制

    大多數瀏覽器對同一域名的並行請求數有限制(通常為6-8個)。當頁面包含大量資源時,超過限制的請求必須排隊等待,進一步延遲了頁面渲染。通過合併資源,我們可以確保關鍵資源更快下載。

    1.3 緩存效率考量

    合適的資源合併策略還能提高緩存效率。將頻繁變動的代码與穩定的库代码分開合併,可以確保用戶在下一次訪問時只需下載變動的部分。

    第二章:工具設計與架構

    2.1 整體架構設計

    我們的資源合併工具將採用模塊化設計,包含以下核心組件:

  • HTML解析器:分析HTML文檔,識別CSS和JavaScript資源

  • 資源收集器:獲取並存儲外部資源內容

  • 合併策略引擎:決定如何合併資源的邏輯

  • 資源處理器:處理CSS和JavaScript的具體合併操作

  • 輸出生成器:生成優化後的HTML和合併後的資源文件

  • 2.2 工具工作流程

    text

    輸入HTML

    HTML解析與資源識別

    資源下載與緩存

    應用合併策略

    CSS合併處理

    JavaScript合併處理

    生成合併後資源文件

    更新HTML引用

    輸出優化後HTML

    2.3 合併策略考慮

    我們需要考慮多種合併策略:

  • 按類型合併:所有CSS合併為一個文件,所有JavaScript合併為一個文件

  • 按媒體查詢合併:將相同媒體查詢的CSS合併

  • 按加載時機合併:將同步和異步加載的JavaScript分開合併

  • 按頁面區域合併:按頁面功能區域合併資源

  • 第三章:環境搭建與基礎框架

    3.1 項目初始化

    首先創建項目目錄結構:

    bash

    mkdir html-resource-merger
    cd html-resource-merger
    npm init -y

    3.2 安裝依賴包

    我們需要以下核心依賴:

    bash

    npm install cheerio jsdom css-tree terser postcss axios
    npm install -D @types/node typescript ts-node

    3.3 TypeScript配置

    創建 tsconfig.json:

    json

    {
    "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
    }

    第四章:HTML解析與資源識別

    4.1 HTML解析器實現

    我們將使用Cheerio庫來解析HTML,它提供了類似jQuery的API,便於操作DOM:

    typescript

    // src/parsers/html-parser.ts
    import * as cheerio from 'cheerio';
    import * as fs from 'fs';
    import * as path from 'path';
    import { Resource, ResourceType } from '../types/resource';

    export class HTMLParser {
    private $: cheerio.CheerioAPI;
    private htmlContent: string;
    private basePath: string;

    constructor(htmlContent: string, basePath: string = '') {
    this.htmlContent = htmlContent;
    this.basePath = basePath;
    this.$ = cheerio.load(htmlContent);
    }

    // 識別所有CSS資源
    public extractCSSResources(): Resource[] {
    const resources: Resource[] = [];

    // 查找link標籤
    this.$('link[rel="stylesheet"]').each((i, elem) => {
    const href = this.$(elem).attr('href');
    const media = this.$(elem).attr('media') || 'all';

    if (href && !href.startsWith('http') && !href.startsWith('//')) {
    const absolutePath = path.resolve(this.basePath, href);
    resources.push({
    type: ResourceType.CSS,
    url: href,
    absolutePath,
    media,
    element: elem,
    inline: false
    });
    }
    });

    // 查找style標籤中的外部資源(如@import)
    this.$('style').each((i, elem) => {
    const content = this.$(elem).html() || '';
    const importMatches = content.matchAll(/@import\\s+(url\\()?["']([^"']+)["'](\\))?/g);

    for (const match of importMatches) {
    if (match[2]) {
    const absolutePath = path.resolve(this.basePath, match[2]);
    resources.push({
    type: ResourceType.CSS,
    url: match[2],
    absolutePath,
    media: 'all',
    element: elem,
    inline: false,
    import: true
    });
    }
    }
    });

    return resources;
    }

    // 識別所有JavaScript資源
    public extractJSResources(): Resource[] {
    const resources: Resource[] = [];

    this.$('script[src]').each((i, elem) => {
    const src = this.$(elem).attr('src');
    const async = this.$(elem).attr('async') !== undefined;
    const defer = this.$(elem).attr('defer') !== undefined;
    const module = this.$(elem).attr('type') === 'module';

    if (src && !src.startsWith('http') && !src.startsWith('//')) {
    const absolutePath = path.resolve(this.basePath, src);
    resources.push({
    type: ResourceType.JS,
    url: src,
    absolutePath,
    async,
    defer,
    module,
    element: elem,
    inline: false
    });
    }
    });

    return resources;
    }

    // 獲取所有內聯資源
    public extractInlineResources(): Resource[] {
    const resources: Resource[] = [];

    // 內聯CSS
    this.$('style:not([src])').each((i, elem) => {
    const content = this.$(elem).html() || '';
    if (content.trim().length > 0) {
    resources.push({
    type: ResourceType.CSS,
    content,
    element: elem,
    inline: true
    });
    }
    });

    // 內聯JavaScript
    this.$('script:not([src])').each((i, elem) => {
    const content = this.$(elem).html() || '';
    if (content.trim().length > 0) {
    resources.push({
    type: ResourceType.JS,
    content,
    element: elem,
    inline: true
    });
    }
    });

    return resources;
    }

    // 移除資源標籤
    public removeResource(resource: Resource): void {
    if (resource.element) {
    this.$(resource.element).remove();
    }
    }

    // 添加合併後的資源引用
    public addMergedResource(
    type: ResourceType,
    url: string,
    attributes: Record<string, string> = {}
    ): void {
    if (type === ResourceType.CSS) {
    const linkTag = `<link rel="stylesheet" href="${url}" ${Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(' ')}>`;
    this.$('head').append(linkTag);
    } else if (type === ResourceType.JS) {
    const scriptTag = `<script src="${url}" ${Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(' ')}></script>`;
    this.$('body').append(scriptTag);
    }
    }

    // 獲取處理後的HTML
    public getHTML(): string {
    return this.$.html();
    }

    // 靜態方法:從文件加載HTML
    public static fromFile(filePath: string): HTMLParser {
    const content = fs.readFileSync(filePath, 'utf-8');
    const basePath = path.dirname(filePath);
    return new HTMLParser(content, basePath);
    }
    }

    4.2 資源類型定義

    創建類型定義文件:

    typescript

    // src/types/resource.ts
    export enum ResourceType {
    CSS = 'css',
    JS = 'js'
    }

    export interface Resource {
    type: ResourceType;
    url?: string;
    absolutePath?: string;
    content?: string;
    media?: string;
    async?: boolean;
    defer?: boolean;
    module?: boolean;
    element?: any;
    inline: boolean;
    import?: boolean;
    position?: number;
    }

    export interface MergedResource {
    type: ResourceType;
    content: string;
    urls: string[];
    media?: string;
    attributes?: Record<string, string>;
    }

    export interface MergeStrategy {
    name: string;
    shouldMerge: (resource1: Resource, resource2: Resource) => boolean;
    groupKey: (resource: Resource) => string;
    }

    第五章:資源下載與緩存管理

    5.1 資源下載器實現

    typescript

    // src/fetchers/resource-fetcher.ts
    import * as fs from 'fs';
    import * as path from 'path';
    import axios from 'axios';
    import { Resource } from '../types/resource';

    export class ResourceFetcher {
    private cache: Map<string, string> = new Map();

    // 獲取資源內容
    public async fetch(resource: Resource): Promise<string> {
    if (resource.absolutePath) {
    return this.fetchLocal(resource.absolutePath);
    } else if (resource.url && (resource.url.startsWith('http') || resource.url.startsWith('//'))) {
    return this.fetchRemote(resource.url);
    } else if (resource.content) {
    return resource.content;
    }
    throw new Error(`無法獲取資源: ${resource.url}`);
    }

    // 獲取本地資源
    private fetchLocal(filePath: string): string {
    const cacheKey = `local:${filePath}`;

    if (this.cache.has(cacheKey)) {
    return this.cache.get(cacheKey)!;
    }

    try {
    const content = fs.readFileSync(filePath, 'utf-8');
    this.cache.set(cacheKey, content);
    return content;
    } catch (error) {
    throw new Error(`讀取文件失敗: ${filePath}, 錯誤: ${error}`);
    }
    }

    // 獲取遠程資源
    private async fetchRemote(url: string): Promise<string> {
    const cacheKey = `remote:${url}`;

    if (this.cache.has(cacheKey)) {
    return this.cache.get(cacheKey)!;
    }

    try {
    const response = await axios.get(url, {
    timeout: 10000,
    headers: {
    'User-Agent': 'HTMLResourceMerger/1.0'
    }
    });

    const content = response.data;
    this.cache.set(cacheKey, content);
    return content;
    } catch (error) {
    throw new Error(`下載遠程資源失敗: ${url}, 錯誤: ${error}`);
    }
    }

    // 批量獲取資源
    public async fetchAll(resources: Resource[]): Promise<Map<Resource, string>> {
    const results = new Map<Resource, string>();
    const promises: Array<Promise<void>> = [];

    for (const resource of resources) {
    promises.push(
    this.fetch(resource)
    .then(content => results.set(resource, content))
    .catch(error => {
    console.error(`獲取資源失敗: ${resource.url || '內聯資源'}, 錯誤: ${error}`);
    results.set(resource, '');
    })
    );
    }

    await Promise.all(promises);
    return results;
    }

    // 清除緩存
    public clearCache(): void {
    this.cache.clear();
    }

    // 獲取緩存統計信息
    public getCacheStats(): { size: number; hits: number } {
    return {
    size: this.cache.size,
    hits: 0 // 實際項目中可以添加命中計數器
    };
    }
    }

    5.2 緩存策略實現

    為了提高性能,我們實現一個更智能的緩存系統:

    typescript

    // src/fetchers/advanced-cache.ts
    import * as crypto from 'crypto';

    interface CacheEntry {
    content: string;
    timestamp: number;
    hash: string;
    size: number;
    }

    export class AdvancedCache {
    private cache: Map<string, CacheEntry> = new Map();
    private maxSize: number;
    private ttl: number; // 生存時間(毫秒)

    constructor(maxSize: number = 100 * 1024 * 1024, ttl: number = 3600000) {
    this.maxSize = maxSize;
    this.ttl = ttl;
    }

    // 計算內容的哈希值
    private computeHash(content: string): string {
    return crypto.createHash('md5').update(content).digest('hex');
    }

    // 設置緩存
    public set(key: string, content: string): void {
    const hash = this.computeHash(content);
    const size = Buffer.byteLength(content, 'utf8');

    // 檢查緩存大小,如果超過限制則清除舊條目
    if (this.getTotalSize() + size > this.maxSize) {
    this.evictOldEntries();
    }

    this.cache.set(key, {
    content,
    timestamp: Date.now(),
    hash,
    size
    });
    }

    // 獲取緩存
    public get(key: string): string | null {
    const entry = this.cache.get(key);

    if (!entry) {
    return null;
    }

    // 檢查是否過期
    if (Date.now() – entry.timestamp > this.ttl) {
    this.cache.delete(key);
    return null;
    }

    return entry.content;
    }

    // 檢查內容是否已緩存(通過哈希)
    public hasContent(content: string): { key: string | null; cached: boolean } {
    const hash = this.computeHash(content);

    for (const [key, entry] of this.cache.entries()) {
    if (entry.hash === hash) {
    return { key, cached: true };
    }
    }

    return { key: null, cached: false };
    }

    // 清除過期條目
    private evictOldEntries(): void {
    const now = Date.now();
    const toDelete: string[] = [];

    // 首先清除過期條目
    for (const [key, entry] of this.cache.entries()) {
    if (now – entry.timestamp > this.ttl) {
    toDelete.push(key);
    }
    }

    toDelete.forEach(key => this.cache.delete(key));

    // 如果仍然超過大小限制,清除最舊的條目
    if (this.getTotalSize() > this.maxSize) {
    const sortedEntries = Array.from(this.cache.entries())
    .sort((a, b) => a[1].timestamp – b[1].timestamp);

    while (this.getTotalSize() > this.maxSize * 0.8 && sortedEntries.length > 0) {
    const [oldestKey] = sortedEntries.shift()!;
    this.cache.delete(oldestKey);
    }
    }
    }

    // 獲取總緩存大小
    private getTotalSize(): number {
    let total = 0;
    for (const entry of this.cache.values()) {
    total += entry.size;
    }
    return total;
    }

    // 獲取緩存統計信息
    public getStats(): {
    size: number;
    entries: number;
    hitRate: number;
    } {
    return {
    size: this.getTotalSize(),
    entries: this.cache.size,
    hitRate: 0 // 實際項目中可以跟蹤命中率
    };
    }

    // 清除所有緩存
    public clear(): void {
    this.cache.clear();
    }
    }

    第六章:CSS合併與處理

    6.1 CSS解析器與合併器

    typescript

    // src/processors/css-processor.ts
    import * as postcss from 'postcss';
    import * as path from 'path';
    import * as fs from 'fs';
    import { Resource, MergedResource } from '../types/resource';

    export class CSSProcessor {
    private basePath: string;

    constructor(basePath: string = '') {
    this.basePath = basePath;
    }

    // 合併多個CSS資源
    public async mergeResources(
    resources: Resource[],
    contents: Map<Resource, string>
    ): Promise<MergedResource> {
    let mergedContent = '';
    const urls: string[] = [];

    for (const resource of resources) {
    const content = contents.get(resource);
    if (content) {
    // 處理CSS中的相對路徑
    const processedContent = await this.processCSSUrls(
    content,
    resource.absolutePath ? path.dirname(resource.absolutePath) : this.basePath
    );

    // 添加來源註釋
    mergedContent += `\\n/* Source: ${resource.url || 'inline'} */\\n`;
    mergedContent += processedContent + '\\n';

    if (resource.url) {
    urls.push(resource.url);
    }
    }
    }

    // 壓縮CSS
    const compressedContent = await this.minifyCSS(mergedContent);

    return {
    type: 'css',
    content: compressedContent,
    urls,
    media: this.getCommonMediaQuery(resources)
    };
    }

    // 處理CSS中的相對URL
    private async processCSSUrls(
    cssContent: string,
    baseDir: string
    ): Promise<string> {
    const plugin = postcss.plugin('css-url-processor', () => {
    return (root) => {
    root.walkDecls((decl) => {
    // 處理background-image, background, src等屬性
    if (decl.prop.match(/background(-image)?|src|content/i)) {
    decl.value = this.processUrlValue(decl.value, baseDir);
    }
    });

    root.walkAtRules('import', (rule) => {
    // 處理@import規則
    const matches = rule.params.match(/["']([^"']+)["']/);
    if (matches) {
    const importPath = matches[1];
    if (!importPath.startsWith('http') && !importPath.startsWith('//')) {
    const absolutePath = path.resolve(baseDir, importPath);
    if (fs.existsSync(absolutePath)) {
    // 這裡可以遞歸處理@import,但為了簡單起見,我們保留原樣
    // 實際項目中應該遞歸處理
    }
    }
    }
    });
    };
    });

    const result = await postcss([plugin()]).process(cssContent, {
    from: undefined
    });

    return result.css;
    }

    // 處理URL值中的相對路徑
    private processUrlValue(value: string, baseDir: string): string {
    // 匹配url()中的路徑
    return value.replace(/url\\(["']?([^"')]+)["']?\\)/gi, (match, urlPath) => {
    // 跳過data URL和絕對路徑
    if (urlPath.startsWith('data:') ||
    urlPath.startsWith('http://') ||
    urlPath.startsWith('https://') ||
    urlPath.startsWith('//') ||
    urlPath.startsWith('/')) {
    return match;
    }

    // 轉換相對路徑為相對於合併後CSS文件的路徑
    // 注意:這是一個簡化版本,實際項目中需要更複雜的路徑計算
    const absolutePath = path.resolve(baseDir, urlPath);
    const relativeToOutput = path.relative(this.basePath, absolutePath);

    return `url("${relativeToOutput}")`;
    });
    }

    // 壓縮CSS
    private async minifyCSS(cssContent: string): Promise<string> {
    const plugins = [
    require('cssnano')({
    preset: 'default'
    })
    ];

    const result = await postcss(plugins).process(cssContent, {
    from: undefined
    });

    return result.css;
    }

    // 提取共同的媒體查詢
    private getCommonMediaQuery(resources: Resource[]): string | undefined {
    const mediaQueries = resources.map(r => r.media).filter(Boolean);

    if (mediaQueries.length === 0) {
    return undefined;
    }

    // 如果所有資源有相同的媒體查詢,返回該查詢
    const firstMedia = mediaQueries[0];
    if (mediaQueries.every(mq => mq === firstMedia)) {
    return firstMedia;
    }

    // 否則返回'all',並在合併時將媒體查詢保留在規則內部
    return 'all';
    }

    // 分析CSS資源的依賴關係
    public analyzeDependencies(cssContent: string): string[] {
    const imports: string[] = [];
    const importRegex = /@import\\s+(url\\()?["']([^"']+)["'](\\))?/g;

    let match;
    while ((match = importRegex.exec(cssContent)) !== null) {
    imports.push(match[2]);
    }

    return imports;
    }
    }

    6.2 CSS重寫規則處理

    當合併CSS時,我們需要特別注意選擇器優先級和規則順序:

    typescript

    // src/processors/css-rewriter.ts
    import * as postcss from 'postcss';

    export interface CSSRule {
    selector: string;
    declarations: Array<{ property: string; value: string }>;
    media?: string;
    specificity?: number;
    }

    export class CSSRewriter {
    // 計算選擇器優先級
    private calculateSpecificity(selector: string): number {
    // 簡化的優先級計算
    // 實際項目中應使用更準確的算法
    let score = 0;

    // ID選擇器
    const idMatches = selector.match(/#/g);
    if (idMatches) score += idMatches.length * 10000;

    // 類選擇器、屬性選擇器、偽類
    const classMatches = selector.match(/\\.|\\[|\\:/g);
    if (classMatches) score += classMatches.length * 100;

    // 元素選擇器、偽元素
    const elementMatches = selector.match(/[a-zA-Z]+(?!-)/g);
    if (elementMatches) score += elementMatches.length;

    return score;
    }

    // 重寫CSS以保持正確的層疊順序
    public async rewriteCSSForMerge(
    cssContents: Array<{ content: string; originalOrder: number }>
    ): Promise<string> {
    const allRules: CSSRule[] = [];

    // 解析所有CSS文件中的規則
    for (const { content, originalOrder } of cssContents) {
    const root = postcss.parse(content);

    root.walkRules((rule) => {
    const parent = rule.parent;
    const media = parent && parent.type === 'atrule' ? parent.params : undefined;

    const declarations = rule.nodes
    .filter(node => node.type === 'decl')
    .map(decl => ({
    property: decl.prop,
    value: decl.value
    }));

    if (declarations.length > 0) {
    allRules.push({
    selector: rule.selector,
    declarations,
    media,
    specificity: this.calculateSpecificity(rule.selector)
    });
    }
    });
    }

    // 按媒體查詢、原始順序和優先級排序
    allRules.sort((a, b) => {
    // 首先按媒體查詢分組
    const mediaA = a.media || '';
    const mediaB = b.media || '';
    if (mediaA !== mediaB) {
    return mediaA.localeCompare(mediaB);
    }

    // 然後按優先級(降序)
    if (a.specificity !== b.specificity) {
    return (b.specificity || 0) – (a.specificity || 0);
    }

    return 0;
    });

    // 生成合併後的CSS
    let result = '';
    let currentMedia = '';

    for (const rule of allRules) {
    // 開始新的媒體查詢塊
    if (rule.media !== currentMedia) {
    if (currentMedia) {
    result += '}\\n\\n';
    }
    if (rule.media) {
    result += `@media ${rule.media} {\\n`;
    }
    currentMedia = rule.media || '';
    }

    // 添加規則
    const indent = rule.media ? ' ' : '';
    result += `${indent}${rule.selector} {\\n`;

    for (const decl of rule.declarations) {
    result += `${indent} ${decl.property}: ${decl.value};\\n`;
    }

    result += `${indent}}\\n`;
    }

    // 關閉最後的媒體查詢塊
    if (currentMedia) {
    result += '}\\n';
    }

    return result;
    }

    // 移除重複的CSS規則
    public removeDuplicateRules(cssContent: string): string {
    const root = postcss.parse(cssContent);
    const ruleMap = new Map<string, { rule: any; parent: any }>();

    root.walkRules((rule) => {
    const key = `${rule.selector}|${rule.parent?.type === 'atrule' ? rule.parent.params : ''}`;

    if (ruleMap.has(key)) {
    // 合併聲明
    const existing = ruleMap.get(key)!;
    const existingDecls = new Set(
    existing.rule.nodes
    .filter((n: any) => n.type === 'decl')
    .map((d: any) => `${d.prop}:${d.value}`)
    );

    // 添加不重複的聲明
    rule.nodes.forEach((node: any) => {
    if (node.type === 'decl') {
    const declKey = `${node.prop}:${node.value}`;
    if (!existingDecls.has(declKey)) {
    existing.rule.append(node.clone());
    }
    }
    });

    // 移除當前規則
    rule.remove();
    } else {
    ruleMap.set(key, { rule, parent: rule.parent });
    }
    });

    return root.toString();
    }
    }

    第七章:JavaScript合併與處理

    7.1 JavaScript處理器實現

    typescript

    // src/processors/js-processor.ts
    import * as terser from 'terser';
    import { Resource, MergedResource } from '../types/resource';

    export class JSProcessor {
    // 合併多個JavaScript資源
    public async mergeResources(
    resources: Resource[],
    contents: Map<Resource, string>,
    options: {
    minify: boolean;
    sourceMap: boolean;
    } = { minify: true, sourceMap: false }
    ): Promise<MergedResource> {
    let mergedContent = '';
    const urls: string[] = [];

    // 按原始順序合併
    const sortedResources = […resources].sort((a, b) => {
    return (a.position || 0) – (b.position || 0);
    });

    for (const resource of sortedResources) {
    const content = contents.get(resource);
    if (content) {
    // 添加來源註釋
    mergedContent += `\\n// Source: ${resource.url || 'inline'}\\n`;

    // 處理模塊代碼
    if (resource.module) {
    mergedContent += this.wrapModule(content, resource.url || '');
    } else {
    // 添加IIFE避免全局污染
    mergedContent += this.wrapInIIFE(content);
    }

    mergedContent += '\\n';

    if (resource.url) {
    urls.push(resource.url);
    }
    }
    }

    // 可選的壓縮
    let finalContent = mergedContent;
    if (options.minify) {
    finalContent = await this.minifyJS(mergedContent, options.sourceMap);
    }

    return {
    type: 'js',
    content: finalContent,
    urls,
    attributes: this.getCommonAttributes(resources)
    };
    }

    // 將代碼包裹在IIFE中避免衝突
    private wrapInIIFE(code: string): string {
    // 檢查是否已經是IIFE
    if (code.trim().startsWith('(function')) {
    return code;
    }

    return `(function() {
    ${code}
    })();`;
    }

    // 處理ES模塊
    private wrapModule(code: string, moduleUrl: string): string {
    // 簡單的模塊包裹,實際項目中需要更完整的模塊系統
    return `// Module: ${moduleUrl}
    (() => {
    const module = { exports: {} };
    const exports = module.exports;

    ${code}

    return module.exports;
    })();`;
    }

    // 壓縮JavaScript
    private async minifyJS(
    code: string,
    generateSourceMap: boolean
    ): Promise<string> {
    try {
    const result = await terser.minify(code, {
    compress: {
    drop_console: false,
    drop_debugger: true,
    ecma: 2020,
    passes: 2
    },
    mangle: {
    toplevel: true,
    reserved: ['$', 'jQuery', 'require', 'exports', 'module']
    },
    format: {
    comments: false,
    beautify: false
    },
    sourceMap: generateSourceMap
    });

    if (result.error) {
    console.warn('JavaScript壓縮失敗:', result.error);
    return code;
    }

    return result.code || code;
    } catch (error) {
    console.warn('JavaScript壓縮異常:', error);
    return code;
    }
    }

    // 提取共同的屬性
    private getCommonAttributes(resources: Resource[]): Record<string, string> {
    const attributes: Record<string, string> = {};

    // 檢查是否所有資源都是async
    const allAsync = resources.every(r => r.async);
    const allDefer = resources.every(r => r.defer);
    const allModule = resources.every(r => r.module);

    if (allAsync) attributes.async = '';
    if (allDefer) attributes.defer = '';
    if (allModule) attributes.type = 'module';

    return attributes;
    }

    // 分析JavaScript依賴關係
    public analyzeDependencies(code: string): {
    requires: string[];
    imports: string[];
    exports: string[];
    } {
    const requires: string[] = [];
    const imports: string[] = [];
    const exports: string[] = [];

    // 簡單的正則匹配,實際項目中應使用AST分析
    const requireRegex = /require\\(["']([^"']+)["']\\)/g;
    const importRegex = /from\\s+["']([^"']+)["']|import\\s+["']([^"']+)["']/g;
    const exportRegex = /export\\s+(?:const|let|var|function|class|default)\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;

    let match;
    while ((match = requireRegex.exec(code)) !== null) {
    requires.push(match[1]);
    }

    while ((match = importRegex.exec(code)) !== null) {
    const importPath = match[1] || match[2];
    if (importPath) imports.push(importPath);
    }

    while ((match = exportRegex.exec(code)) !== null) {
    exports.push(match[1]);
    }

    return { requires, imports, exports };
    }
    }

    7.2 JavaScript代碼分析與依賴解析

    為了更準確地處理JavaScript合併,我們需要實現AST分析:

    typescript

    // src/analyzers/js-analyzer.ts
    import * as parser from '@babel/parser';
    import traverse from '@babel/traverse';
    import * as t from '@babel/types';

    export interface JSAnalysisResult {
    dependencies: string[];
    exports: {
    named: string[];
    default: boolean;
    };
    globalVariables: Set<string>;
    sideEffects: boolean;
    size: number;
    }

    export class JSAnalyzer {
    // 分析JavaScript代碼
    public analyze(code: string, filename: string = ''): JSAnalysisResult {
    try {
    const ast = parser.parse(code, {
    sourceType: 'module',
    plugins: [
    'jsx',
    'typescript',
    'asyncGenerators',
    'classProperties',
    'dynamicImport',
    'decorators-legacy'
    ]
    });

    const dependencies = new Set<string>();
    const namedExports = new Set<string>();
    let hasDefaultExport = false;
    const globalVariables = new Set<string>();
    let hasSideEffects = false;

    traverse(ast, {
    // 分析導入
    ImportDeclaration(path) {
    dependencies.add(path.node.source.value);
    },

    // 分析CommonJS require
    CallExpression(path) {
    if (t.isIdentifier(path.node.callee, { name: 'require' }) &&
    path.node.arguments.length === 1 &&
    t.isStringLiteral(path.node.arguments[0])) {
    dependencies.add(path.node.arguments[0].value);
    }
    },

    // 分析動態導入
    Import(path) {
    const parent = path.parent;
    if (t.isCallExpression(parent) &&
    parent.arguments.length === 1 &&
    t.isStringLiteral(parent.arguments[0])) {
    dependencies.add(parent.arguments[0].value);
    }
    },

    // 分析導出
    ExportNamedDeclaration(path) {
    if (path.node.declaration) {
    if (t.isVariableDeclaration(path.node.declaration)) {
    path.node.declaration.declarations.forEach(decl => {
    if (t.isIdentifier(decl.id)) {
    namedExports.add(decl.id.name);
    }
    });
    } else if (t.isFunctionDeclaration(path.node.declaration) &&
    path.node.declaration.id) {
    namedExports.add(path.node.declaration.id.name);
    } else if (t.isClassDeclaration(path.node.declaration) &&
    path.node.declaration.id) {
    namedExports.add(path.node.declaration.id.name);
    }
    }

    if (path.node.specifiers) {
    path.node.specifiers.forEach(spec => {
    if (t.isExportSpecifier(spec) && t.isIdentifier(spec.exported)) {
    namedExports.add(spec.exported.name);
    }
    });
    }
    },

    ExportDefaultDeclaration() {
    hasDefaultExport = true;
    },

    // 檢測副作用
    CallExpression(path) {
    // 檢測可能產生副作用的調用
    if (t.isMemberExpression(path.node.callee)) {
    const obj = path.node.callee.object;
    if (t.isIdentifier(obj) &&
    (obj.name === 'console' || obj.name === 'document' || obj.name === 'window')) {
    hasSideEffects = true;
    }
    }
    },

    // 分析全局變量
    AssignmentExpression(path) {
    if (t.isMemberExpression(path.node.left)) {
    const obj = path.node.left.object;
    if (t.isIdentifier(obj) && obj.name === 'window') {
    if (t.isIdentifier(path.node.left.property)) {
    globalVariables.add(path.node.left.property.name);
    }
    }
    } else if (t.isIdentifier(path.node.left)) {
    // 簡單的全局變量檢測(不考慮作用域)
    globalVariables.add(path.node.left.name);
    }
    }
    });

    return {
    dependencies: Array.from(dependencies),
    exports: {
    named: Array.from(namedExports),
    default: hasDefaultExport
    },
    globalVariables,
    sideEffects: hasSideEffects,
    size: Buffer.byteLength(code, 'utf8')
    };
    } catch (error) {
    console.warn(`分析JavaScript文件失敗 ${filename}:`, error);
    return {
    dependencies: [],
    exports: { named: [], default: false },
    globalVariables: new Set(),
    sideEffects: true, // 無法分析時假定有副作用
    size: Buffer.byteLength(code, 'utf8')
    };
    }
    }

    // 檢測代碼衝突
    public detectConflicts(
    analysis1: JSAnalysisResult,
    analysis2: JSAnalysisResult
    ): string[] {
    const conflicts: string[] = [];

    // 檢測全局變量衝突
    const globalVars1 = analysis1.globalVariables;
    const globalVars2 = analysis2.globalVariables;

    for (const varName of globalVars1) {
    if (globalVars2.has(varName)) {
    conflicts.push(`全局變量衝突: ${varName}`);
    }
    }

    // 檢測導出名稱衝突
    const exports1 = analysis1.exports.named;
    const exports2 = analysis2.exports.named;

    for (const exportName of exports1) {
    if (exports2.includes(exportName)) {
    conflicts.push(`導出名稱衝突: ${exportName}`);
    }
    }

    return conflicts;
    }

    // 建議合併順序基於依賴關係
    public suggestMergeOrder(
    files: Array<{ filename: string; analysis: JSAnalysisResult }>
    ): string[] {
    // 簡單的拓撲排序(不考慮循環依賴)
    const graph: Record<string, string[]> = {};
    const inDegree: Record<string, number> = {};

    // 初始化圖
    files.forEach(file => {
    graph[file.filename] = [];
    inDegree[file.filename] = 0;
    });

    // 構建依賴圖
    files.forEach(file1 => {
    file1.analysis.dependencies.forEach(dep => {
    files.forEach(file2 => {
    if (file2.filename === dep ||
    file2.filename.includes(dep) ||
    dep.includes(file2.filename)) {
    graph[file2.filename].push(file1.filename);
    inDegree[file1.filename]++;
    }
    });
    });
    });

    // 拓撲排序
    const queue = files.filter(f => inDegree[f.filename] === 0).map(f => f.filename);
    const result: string[] = [];

    while (queue.length > 0) {
    const current = queue.shift()!;
    result.push(current);

    graph[current].forEach(neighbor => {
    inDegree[neighbor]–;
    if (inDegree[neighbor] === 0) {
    queue.push(neighbor);
    }
    });
    }

    // 如果有剩餘節點,說明存在循環依賴,按原始順序返回
    if (result.length !== files.length) {
    return files.map(f => f.filename);
    }

    return result;
    }
    }

    第八章:合併策略引擎

    8.1 策略模式實現

    typescript

    // src/strategies/merge-strategy.ts
    import { Resource, MergeStrategy } from '../types/resource';

    // 基礎策略:按類型合併
    export class BasicMergeStrategy implements MergeStrategy {
    name = 'basic';

    shouldMerge(resource1: Resource, resource2: Resource): boolean {
    // 相同類型且都是外部資源
    return resource1.type === resource2.type &&
    !resource1.inline &&
    !resource2.inline;
    }

    groupKey(resource: Resource): string {
    return resource.type;
    }
    }

    // 媒體查詢策略:按CSS媒體查詢分組合併
    export class MediaQueryMergeStrategy implements MergeStrategy {
    name = 'media-query';

    shouldMerge(resource1: Resource, resource2: Resource): boolean {
    if (resource1.type !== 'css' || resource2.type !== 'css') {
    return false;
    }

    const media1 = resource1.media || 'all';
    const media2 = resource2.media || 'all';

    return media1 === media2;
    }

    groupKey(resource: Resource): string {
    return `css-${resource.media || 'all'}`;
    }
    }

    // 加載策略:按加載特性分組合併
    export class LoadingMergeStrategy implements MergeStrategy {
    name = 'loading';

    shouldMerge(resource1: Resource, resource2: Resource): boolean {
    if (resource1.type !== resource2.type) {
    return false;
    }

    if (resource1.type === 'css') {
    return true; // CSS總是合併
    }

    // JavaScript按加載特性分組
    const async1 = resource1.async || false;
    const async2 = resource2.async || false;
    const defer1 = resource1.defer || false;
    const defer2 = resource2.defer || false;
    const module1 = resource1.module || false;
    const module2 = resource2.module || false;

    return async1 === async2 &&
    defer1 === defer2 &&
    module1 === module2;
    }

    groupKey(resource: Resource): string {
    if (resource.type === 'css') {
    return 'css';
    }

    const parts = ['js'];
    if (resource.async) parts.push('async');
    if (resource.defer) parts.push('defer');
    if (resource.module) parts.push('module');

    return parts.join('-');
    }
    }

    // 頁面區域策略:按預定義的頁面區域分組合併
    export class PageSectionMergeStrategy implements MergeStrategy {
    private sectionPatterns: Map<RegExp, string>;

    constructor(patterns: Array<{ pattern: RegExp; section: string }> = []) {
    this.sectionPatterns = new Map();
    patterns.forEach(p => {
    this.sectionPatterns.set(p.pattern, p.section);
    });
    }

    name = 'page-section';

    shouldMerge(resource1: Resource, resource2: Resource): boolean {
    const section1 = this.getSection(resource1);
    const section2 = this.getSection(resource2);

    return resource1.type === resource2.type &&
    section1 === section2;
    }

    groupKey(resource: Resource): string {
    const section = this.getSection(resource);
    return `${resource.type}-${section}`;
    }

    private getSection(resource: Resource): string {
    if (!resource.url) return 'unknown';

    for (const [pattern, section] of this.sectionPatterns.entries()) {
    if (pattern.test(resource.url)) {
    return section;
    }
    }

    // 默認按目錄結構判斷
    const url = resource.url;
    if (url.includes('/vendor/') || url.includes('/lib/')) {
    return 'vendor';
    } else if (url.includes('/components/')) {
    return 'components';
    } else if (url.includes('/utils/')) {
    return 'utils';
    } else {
    return 'app';
    }
    }
    }

    // 複合策略:組合多個策略
    export class CompositeMergeStrategy implements MergeStrategy {
    private strategies: MergeStrategy[];

    constructor(strategies: MergeStrategy[] = []) {
    this.strategies = strategies;
    }

    name = 'composite';

    shouldMerge(resource1: Resource, resource2: Resource): boolean {
    // 所有策略都同意合併時才合併
    return this.strategies.every(strategy =>
    strategy.shouldMerge(resource1, resource2)
    );
    }

    groupKey(resource: Resource): string {
    // 組合所有策略的groupKey
    return this.strategies.map(s => s.groupKey(resource)).join('|');
    }

    addStrategy(strategy: MergeStrategy): void {
    this.strategies.push(strategy);
    }
    }

    8.2 策略管理器

    typescript

    // src/strategies/strategy-manager.ts
    import { Resource, MergeStrategy } from '../types/resource';
    import {
    BasicMergeStrategy,
    MediaQueryMergeStrategy,
    LoadingMergeStrategy,
    PageSectionMergeStrategy,
    CompositeMergeStrategy
    } from './merge-strategy';

    export type StrategyName = 'basic' | 'media-query' | 'loading' | 'page-section' | 'composite';

    export class StrategyManager {
    private strategies: Map<StrategyName, MergeStrategy>;

    constructor() {
    this.strategies = new Map();
    this.registerDefaultStrategies();
    }

    private registerDefaultStrategies(): void {
    this.strategies.set('basic', new BasicMergeStrategy());
    this.strategies.set('media-query', new MediaQueryMergeStrategy());
    this.strategies.set('loading', new LoadingMergeStrategy());
    this.strategies.set('page-section', new PageSectionMergeStrategy());

    // 默認的複合策略:加載策略 + 頁面區域策略
    const composite = new CompositeMergeStrategy([
    new LoadingMergeStrategy(),
    new PageSectionMergeStrategy()
    ]);
    this.strategies.set('composite', composite);
    }

    // 獲取策略
    public getStrategy(name: StrategyName): MergeStrategy {
    const strategy = this.strategies.get(name);
    if (!strategy) {
    throw new Error(`策略不存在: ${name}`);
    }
    return strategy;
    }

    // 註冊自定義策略
    public registerStrategy(name: string, strategy: MergeStrategy): void {
    this.strategies.set(name as StrategyName, strategy);
    }

    // 根據資源自動選擇最佳策略
    public autoSelectStrategy(resources: Resource[]): MergeStrategy {
    const cssResources = resources.filter(r => r.type === 'css');
    const jsResources = resources.filter(r => r.type === 'js');

    // 分析資源特性
    const hasMultipleMediaQueries = new Set(
    cssResources.map(r => r.media || 'all')
    ).size > 1;

    const hasMixedLoadingAttributes =
    new Set(jsResources.map(r =>
    `${r.async ? 'a' : ''}${r.defer ? 'd' : ''}${r.module ? 'm' : ''}`
    )).size > 1;

    // 根據分析結果選擇策略
    if (hasMultipleMediaQueries) {
    return this.getStrategy('media-query');
    } else if (hasMixedLoadingAttributes) {
    return this.getStrategy('loading');
    } else {
    return this.getStrategy('composite');
    }
    }

    // 分組資源
    public groupResources(
    resources: Resource[],
    strategy: MergeStrategy
    ): Map<string, Resource[]> {
    const groups = new Map<string, Resource[]>();

    // 為每個資源分配位置
    resources.forEach((resource, index) => {
    resource.position = index;
    });

    // 分組
    resources.forEach(resource => {
    const key = strategy.groupKey(resource);

    if (!groups.has(key)) {
    groups.set(key, []);
    }

    groups.get(key)!.push(resource);
    });

    // 在每個組內按原始順序排序
    groups.forEach(resourceList => {
    resourceList.sort((a, b) => (a.position || 0) – (b.position || 0));
    });

    return groups;
    }

    // 評估合併效果
    public evaluateMerge(
    originalResources: Resource[],
    mergedGroups: Map<string, Resource[]>
    ): {
    requestReduction: number;
    estimatedSizeChange: number;
    groupCount: number;
    } {
    const originalRequestCount = originalResources.filter(r => !r.inline).length;
    const mergedRequestCount = mergedGroups.size;
    const requestReduction = originalRequestCount – mergedRequestCount;

    // 注意:這裡需要實際計算大小變化
    const estimatedSizeChange = 0;

    return {
    requestReduction,
    estimatedSizeChange,
    groupCount: mergedGroups.size
    };
    }
    }

    第九章:核心合併引擎

    9.1 合併引擎主類

    typescript

    // src/core/merger-engine.ts
    import { HTMLParser } from '../parsers/html-parser';
    import { ResourceFetcher } from '../fetchers/resource-fetcher';
    import { CSSProcessor } from '../processors/css-processor';
    import { JSProcessor } from '../processors/js-processor';
    import { StrategyManager, StrategyName } from '../strategies/strategy-manager';
    import { Resource, MergedResource } from '../types/resource';
    import * as fs from 'fs';
    import * as path from 'path';
    import * as crypto from 'crypto';

    export interface MergeOptions {
    strategy?: StrategyName | 'auto';
    minifyCSS?: boolean;
    minifyJS?: boolean;
    generateSourceMap?: boolean;
    inlineThreshold?: number; // 小於此大小的資源將被內聯
    outputDir?: string;
    versioning?: boolean; // 是否添加版本號
    cacheBusting?: boolean; // 是否添加緩存破壞參數
    }

    export interface MergeResult {
    html: string;
    mergedResources: Array<{
    type: string;
    filename: string;
    size: number;
    originalResources: string[];
    }>;
    stats: {
    originalRequests: number;
    finalRequests: number;
    reductionPercentage: number;
    totalSize: number;
    estimatedLoadTime: number;
    };
    }

    export class MergerEngine {
    private htmlParser: HTMLParser;
    private resourceFetcher: ResourceFetcher;
    private cssProcessor: CSSProcessor;
    private jsProcessor: JSProcessor;
    private strategyManager: StrategyManager;

    private options: Required<MergeOptions>;

    constructor(
    htmlContent: string,
    basePath: string = '',
    options: MergeOptions = {}
    ) {
    this.htmlParser = new HTMLParser(htmlContent, basePath);
    this.resourceFetcher = new ResourceFetcher();
    this.cssProcessor = new CSSProcessor(basePath);
    this.jsProcessor = new JSProcessor();
    this.strategyManager = new StrategyManager();

    this.options = {
    strategy: 'auto',
    minifyCSS: true,
    minifyJS: true,
    generateSourceMap: false,
    inlineThreshold: 2048, // 2KB
    outputDir: path.join(basePath, 'dist'),
    versioning: true,
    cacheBusting: true,
    …options
    };

    // 確保輸出目錄存在
    if (!fs.existsSync(this.options.outputDir)) {
    fs.mkdirSync(this.options.outputDir, { recursive: true });
    }
    }

    // 執行合併
    public async merge(): Promise<MergeResult> {
    console.log('開始資源合併處理…');

    // 1. 解析HTML並提取資源
    console.log('解析HTML並提取資源…');
    const cssResources = this.htmlParser.extractCSSResources();
    const jsResources = this.htmlParser.extractJSResources();
    const inlineResources = this.htmlParser.extractInlineResources();

    const allExternalResources = […cssResources, …jsResources];
    console.log(`找到 ${allExternalResources.length} 個外部資源`);

    // 2. 選擇合併策略
    console.log('選擇合併策略…');
    const strategy = this.options.strategy === 'auto'
    ? this.strategyManager.autoSelectStrategy(allExternalResources)
    : this.strategyManager.getStrategy(this.options.strategy);

    console.log(`使用策略: ${strategy.name}`);

    // 3. 分組資源
    console.log('分組資源…');
    const resourceGroups = this.strategyManager.groupResources(
    allExternalResources,
    strategy
    );

    console.log(`分為 ${resourceGroups.size} 個組`);

    // 4. 獲取資源內容
    console.log('獲取資源內容…');
    const resourceContents = await this.resourceFetcher.fetchAll(
    allExternalResources
    );

    // 5. 處理每組資源
    console.log('處理每組資源…');
    const mergedResources: MergedResource[] = [];

    for (const [groupKey, resources] of resourceGroups.entries()) {
    console.log(`處理組: ${groupKey}, 包含 ${resources.length} 個資源`);

    const type = resources[0].type;

    if (type === 'css') {
    const merged = await this.cssProcessor.mergeResources(
    resources,
    resourceContents
    );
    mergedResources.push(merged);
    } else if (type === 'js') {
    const merged = await this.jsProcessor.mergeResources(
    resources,
    resourceContents,
    {
    minify: this.options.minifyJS,
    sourceMap: this.options.generateSourceMap
    }
    );
    mergedResources.push(merged);
    }
    }

    // 6. 生成合併後的文件
    console.log('生成合併後的文件…');
    const generatedFiles = await this.generateMergedFiles(mergedResources);

    // 7. 更新HTML
    console.log('更新HTML…');
    this.updateHTMLWithMergedResources(resources =>
    generatedFiles.find(f => f.originalUrls.some(url => resources.some(r => r.url === url)))
    );

    // 8. 處理內聯資源
    console.log('處理內聯資源…');
    this.processInlineResources(inlineResources);

    // 9. 獲取最終HTML
    const finalHTML = this.htmlParser.getHTML();

    // 10. 計算統計信息
    const stats = this.calculateStats(
    allExternalResources,
    generatedFiles,
    finalHTML
    );

    console.log('資源合併完成!');
    console.log(`請求數從 ${stats.originalRequests} 減少到 ${stats.finalRequests}`);
    console.log(`減少了 ${stats.reductionPercentage}%`);

    return {
    html: finalHTML,
    mergedResources: generatedFiles.map(f => ({
    type: f.type,
    filename: f.filename,
    size: f.size,
    originalResources: f.originalUrls
    })),
    stats
    };
    }

    // 生成合併後的文件
    private async generateMergedFiles(
    mergedResources: MergedResource[]
    ): Promise<Array<{
    type: string;
    filename: string;
    content: string;
    size: number;
    originalUrls: string[];
    }>> {
    const generatedFiles = [];

    for (const mergedResource of mergedResources) {
    // 決定是否內聯
    const shouldInline = mergedResource.content.length < this.options.inlineThreshold;

    if (shouldInline) {
    // 內聯處理
    this.inlineMergedResource(mergedResource);
    } else {
    // 生成文件
    const filename = this.generateFilename(mergedResource);
    const filePath = path.join(this.options.outputDir, filename);

    // 寫入文件
    fs.writeFileSync(filePath, mergedResource.content, 'utf-8');

    generatedFiles.push({
    type: mergedResource.type,
    filename,
    content: mergedResource.content,
    size: Buffer.byteLength(mergedResource.content, 'utf8'),
    originalUrls: mergedResource.urls
    });
    }
    }

    return generatedFiles;
    }

    // 生成文件名
    private generateFilename(mergedResource: MergedResource): string {
    const hash = crypto.createHash('md5')
    .update(mergedResource.content)
    .digest('hex')
    .substring(0, 8);

    const type = mergedResource.type;
    const timestamp = this.options.versioning ?
    `-${Date.now().toString(36)}` : '';

    let basename = `merged-${type}${timestamp}-${hash}`;

    if (type === 'css' && mergedResource.media && mergedResource.media !== 'all') {
    const mediaSlug = mergedResource.media
    .replace(/[^a-zA-Z0-9]/g, '-')
    .replace(/-+/g, '-')
    .toLowerCase();
    basename = `merged-${type}-${mediaSlug}${timestamp}-${hash}`;
    }

    return `${basename}.${type}`;
    }

    // 內聯合併的資源
    private inlineMergedResource(mergedResource: MergedResource): void {
    if (mergedResource.type === 'css') {
    // 創建style標籤
    const styleTag = `<style${mergedResource.media && mergedResource.media !== 'all' ? ` media="${mergedResource.media}"` : ''}>\\n${mergedResource.content}\\n</style>`;
    this.htmlParser.addInlineResource('style', styleTag);
    } else if (mergedResource.type === 'js') {
    // 創建script標籤
    const scriptTag = `<script>\\n${mergedResource.content}\\n</script>`;
    this.htmlParser.addInlineResource('script', scriptTag);
    }

    // 移除原始資源引用
    // 注意:這裡需要追蹤哪些原始資源被內聯了
    }

    // 更新HTML引用
    private updateHTMLWithMergedResources(
    findFile: (resources: Resource[]) => any
    ): void {
    // 這裡需要實現邏輯來替換原始資源引用為合併後的資源引用
    // 由於代碼較長,此處省略具體實現細節
    // 基本思路是:
    // 1. 移除所有原始資源標籤
    // 2. 添加合併後的資源標籤
    }

    // 處理內聯資源
    private processInlineResources(inlineResources: Resource[]): void {
    // 可以選擇壓縮內聯資源
    inlineResources.forEach(resource => {
    if (resource.type === 'css' && this.options.minifyCSS) {
    // 壓縮內聯CSS
    } else if (resource.type === 'js' && this.options.minifyJS) {
    // 壓縮內聯JS
    }
    });
    }

    // 計算統計信息
    private calculateStats(
    originalResources: Resource[],
    generatedFiles: Array<{ size: number }>,
    finalHTML: string
    ): MergeResult['stats'] {
    const originalRequests = originalResources.length;
    const finalRequests = generatedFiles.length;
    const reductionPercentage = originalRequests > 0 ?
    Math.round((1 – finalRequests / originalRequests) * 100) : 0;

    const totalSize = generatedFiles.reduce((sum, file) => sum + file.size, 0);

    // 估算加載時間(簡化模型)
    const estimatedLoadTime = this.estimateLoadTime(
    originalRequests,
    finalRequests,
    totalSize
    );

    return {
    originalRequests,
    finalRequests,
    reductionPercentage,
    totalSize,
    estimatedLoadTime
    };
    }

    // 估算加載時間
    private estimateLoadTime(
    originalRequests: number,
    finalRequests: number,
    totalSize: number
    ): number {
    // 簡化模型:
    // – 每個請求的固定開銷:100ms
    // – 帶寬:1MB/s
    const requestOverhead = 100; // ms
    const bandwidth = 1024 * 1024; // bytes per second

    const originalTime = originalRequests * requestOverhead + totalSize / bandwidth * 1000;
    const finalTime = finalRequests * requestOverhead + totalSize / bandwidth * 1000;

    return originalTime – finalTime; // 節省的時間
    }

    // 靜態方法:從文件合併
    public static async mergeFile(
    htmlFilePath: string,
    options: MergeOptions = {}
    ): Promise<MergeResult> {
    const basePath = path.dirname(htmlFilePath);
    const htmlContent = fs.readFileSync(htmlFilePath, 'utf-8');

    const merger = new MergerEngine(htmlContent, basePath, options);
    return merger.merge();
    }

    // 保存結果到文件
    public saveResult(result: MergeResult, outputPath?: string): void {
    const htmlOutputPath = outputPath ||
    path.join(this.options.outputDir, 'index-optimized.html');

    // 保存HTML
    fs.writeFileSync(htmlOutputPath, result.html, 'utf-8');

    // 保存報告
    const reportPath = path.join(this.options.outputDir, 'merge-report.json');
    fs.writeFileSync(reportPath, JSON.stringify(result, null, 2), 'utf-8');

    console.log(`優化後的HTML已保存到: ${htmlOutputPath}`);
    console.log(`合併報告已保存到: ${reportPath}`);
    }
    }

    9.2 命令行界面

    為了方便使用,我們創建一個命令行工具:

    typescript

    // src/cli/cli.ts
    #!/usr/bin/env node

    import * as commander from 'commander';
    import * as fs from 'fs';
    import * as path from 'path';
    import { MergerEngine } from '../core/merger-engine';
    import { StrategyName } from '../strategies/strategy-manager';

    const program = new commander.Command();

    program
    .name('html-resource-merger')
    .description('自動合併HTML中的CSS和JavaScript資源,減少HTTP請求數')
    .version('1.0.0');

    program
    .argument('<html-file>', '要處理的HTML文件路徑')
    .option('-o, –output <dir>', '輸出目錄', 'dist')
    .option('-s, –strategy <strategy>', '合併策略: basic, media-query, loading, page-section, composite, auto', 'auto')
    .option('–no-minify-css', '禁用CSS壓縮')
    .option('–no-minify-js', '禁用JavaScript壓縮')
    .option('–inline-threshold <kb>', '內聯閾值(KB)', '2')
    .option('–no-versioning', '禁用版本號')
    .option('–no-cache-busting', '禁用緩存破壞')
    .option('–source-map', '生成source map')
    .option('–verbose', '詳細輸出')
    .action(async (htmlFile, options) => {
    try {
    console.log(`處理文件: ${htmlFile}`);

    // 檢查文件是否存在
    if (!fs.existsSync(htmlFile)) {
    console.error(`錯誤: 文件不存在 ${htmlFile}`);
    process.exit(1);
    }

    // 處理選項
    const mergeOptions = {
    strategy: options.strategy as StrategyName | 'auto',
    minifyCSS: options.minifyCss,
    minifyJS: options.minifyJs,
    generateSourceMap: options.sourceMap,
    inlineThreshold: parseInt(options.inlineThreshold) * 1024,
    outputDir: path.isAbsolute(options.output) ?
    options.output : path.join(path.dirname(htmlFile), options.output),
    versioning: options.versioning,
    cacheBusting: options.cacheBusting
    };

    if (options.verbose) {
    console.log('合併選項:', mergeOptions);
    }

    // 執行合併
    const result = await MergerEngine.mergeFile(htmlFile, mergeOptions);

    // 保存結果
    const merger = new MergerEngine('', path.dirname(htmlFile), mergeOptions);
    merger.saveResult(result);

    // 顯示統計信息
    console.log('\\n=== 合併統計 ===');
    console.log(`原始請求數: ${result.stats.originalRequests}`);
    console.log(`最終請求數: ${result.stats.finalRequests}`);
    console.log(`請求減少量: ${result.stats.reductionPercentage}%`);
    console.log(`總文件大小: ${(result.stats.totalSize / 1024).toFixed(2)} KB`);
    console.log(`估計加載時間節省: ${result.stats.estimatedLoadTime.toFixed(0)} ms`);

    console.log('\\n合併完成!');

    } catch (error) {
    console.error('處理過程中發生錯誤:', error);
    process.exit(1);
    }
    });

    // 添加監視模式命令
    program
    .command('watch')
    .description('監視HTML文件變化並自動重新合併')
    .argument('<html-file>', '要監視的HTML文件路徑')
    .option('-o, –output <dir>', '輸出目錄', 'dist')
    .option('-s, –strategy <strategy>', '合併策略', 'auto')
    .action(async (htmlFile, options) => {
    console.log(`開始監視: ${htmlFile}`);

    // 使用chokidar或其他文件監視庫
    // 這裡簡化實現
    const chokidar = require('chokidar');

    const watcher = chokidar.watch(htmlFile, {
    persistent: true,
    ignoreInitial: true
    });

    watcher.on('change', async (path: string) => {
    console.log(`\\n檢測到變化: ${path}`);
    console.log('重新合併資源…');

    try {
    const mergeOptions = {
    strategy: options.strategy as StrategyName | 'auto',
    outputDir: path.isAbsolute(options.output) ?
    options.output : path.join(path.dirname(htmlFile), options.output),
    };

    const result = await MergerEngine.mergeFile(htmlFile, mergeOptions);
    const merger = new MergerEngine('', path.dirname(htmlFile), mergeOptions);
    merger.saveResult(result);

    console.log('重新合併完成!');
    } catch (error) {
    console.error('重新合併失敗:', error);
    }
    });

    console.log('監視中… 按Ctrl+C退出');
    });

    // 添加分析命令
    program
    .command('analyze')
    .description('分析HTML資源但不進行合併')
    .argument('<html-file>', '要分析的HTML文件路徑')
    .action(async (htmlFile) => {
    console.log(`分析文件: ${htmlFile}`);

    try {
    const basePath = path.dirname(htmlFile);
    const htmlContent = fs.readFileSync(htmlFile, 'utf-8');

    const parser = new (require('../parsers/html-parser').HTMLParser)(htmlContent, basePath);
    const cssResources = parser.extractCSSResources();
    const jsResources = parser.extractJSResources();
    const inlineResources = parser.extractInlineResources();

    console.log('\\n=== 資源分析報告 ===');
    console.log(`CSS文件: ${cssResources.length}`);
    console.log(`JavaScript文件: ${jsResources.length}`);
    console.log(`內聯資源: ${inlineResources.length}`);
    console.log(`總外部請求: ${cssResources.length + jsResources.length}`);

    if (cssResources.length > 0) {
    console.log('\\nCSS資源:');
    cssResources.forEach((resource, i) => {
    console.log(` ${i + 1}. ${resource.url} ${resource.media ? `(${resource.media})` : ''}`);
    });
    }

    if (jsResources.length > 0) {
    console.log('\\nJavaScript資源:');
    jsResources.forEach((resource, i) => {
    const attrs = [];
    if (resource.async) attrs.push('async');
    if (resource.defer) attrs.push('defer');
    if (resource.module) attrs.push('module');
    console.log(` ${i + 1}. ${resource.url} ${attrs.length > 0 ? `(${attrs.join(', ')})` : ''}`);
    });
    }

    // 建議優化策略
    console.log('\\n=== 優化建議 ===');

    if (cssResources.length + jsResources.length > 10) {
    console.log('❌ 資源過多: 考慮合併CSS和JavaScript文件');
    } else if (cssResources.length + jsResources.length > 6) {
    console.log('⚠️ 資源較多: 考慮部分合併');
    } else {
    console.log('✅ 資源數量合理');
    }

    // 檢查是否使用了HTTP/2
    console.log('\\n提示: 如果使用HTTP/2,合併小文件可能不是最優策略');
    console.log(' HTTP/2支持多路複用,可以更有效地處理多個小文件');

    } catch (error) {
    console.error('分析失敗:', error);
    process.exit(1);
    }
    });

    program.parse();

    第十章:高級功能與優化

    10.1 HTTP/2感知合併

    HTTP/2改變了資源加載的最佳實踐,因此我們的工具應該能夠感知HTTP/2:

    typescript

    // src/strategies/http2-strategy.ts
    import { Resource, MergeStrategy } from '../types/resource';

    export class HTTP2AwareStrategy implements MergeStrategy {
    name = 'http2-aware';

    // HTTP/2下,小文件合併可能不是最佳選擇
    shouldMerge(resource1: Resource, resource2: Resource): boolean {
    // 獲取文件大小(需要異步獲取)
    // 如果文件很小,在HTTP/2下不需要合併
    const size1 = this.estimateSize(resource1);
    const size2 = this.estimateSize(resource2);

    // 如果文件都很小(< 10KB),在HTTP/2下不合併
    if (size1 < 10240 && size2 < 10240) {
    return false;
    }

    // 否則使用基本合併邏輯
    return resource1.type === resource2.type;
    }

    groupKey(resource: Resource): string {
    const size = this.estimateSize(resource);

    if (size < 10240) {
    return `${resource.type}-small`;
    } else if (size < 102400) {
    return `${resource.type}-medium`;
    } else {
    return `${resource.type}-large`;
    }
    }

    private estimateSize(resource: Resource): number {
    // 簡單的估計,實際項目中需要更準確的實現
    if (resource.url) {
    if (resource.url.includes('jquery') || resource.url.includes('bootstrap')) {
    return 102400; // 大約100KB
    } else if (resource.url.includes('utils') || resource.url.includes('helpers')) {
    return 10240; // 大約10KB
    } else {
    return 30720; // 大約30KB
    }
    }
    return 0;
    }
    }

    10.2 差異化緩存策略

    typescript

    // src/cache/differential-caching.ts
    import * as fs from 'fs';
    import * as crypto from 'crypto';

    export interface CachingStrategy {
    shouldCache(resource: Resource): boolean;
    getCacheKey(resource: Resource): string;
    getCacheDuration(resource: Resource): number;
    }

    export class DifferentialCaching implements CachingStrategy {
    // 根據資源類型決定緩存策略
    shouldCache(resource: Resource): boolean {
    // 不緩存內聯資源
    if (resource.inline) return false;

    // 總是緩存第三方庫
    if (this.isThirdParty(resource)) return true;

    // 根據文件內容決定
    return true;
    }

    getCacheKey(resource: Resource): string {
    if (resource.absolutePath && fs.existsSync(resource.absolutePath)) {
    const content = fs.readFileSync(resource.absolutePath, 'utf-8');
    const hash = crypto.createHash('md5').update(content).digest('hex');
    return `${resource.type}-${hash.substring(0, 8)}`;
    }

    // 使用URL作為備用鍵
    return `${resource.type}-${crypto.createHash('md5').update(resource.url || '').digest('hex').substring(0, 8)}`;
    }

    getCacheDuration(resource: Resource): number {
    // 第三方資源緩存更長時間
    if (this.isThirdParty(resource)) {
    return 30 * 24 * 60 * 60 * 1000; // 30天
    }

    // 應用代碼緩存較短時間
    return 24 * 60 * 60 * 1000; // 1天
    }

    private isThirdParty(resource: Resource): boolean {
    if (!resource.url) return false;

    const thirdPartyPatterns = [
    /cdn\\./,
    /cloudflare\\./,
    /googleapis\\./,
    /bootstrapcdn\\./,
    /unpkg\\./,
    /jquery/,
    /vue/,
    /react/,
    /angular/,
    /lodash/
    ];

    return thirdPartyPatterns.some(pattern => pattern.test(resource.url!));
    }
    }

    10.3 資源加載時機優化

    typescript

    // src/optimizers/loading-optimizer.ts
    import { HTMLParser } from '../parsers/html-parser';
    import { Resource } from '../types/resource';

    export class LoadingOptimizer {
    // 重新排列資源以優化關鍵渲染路徑
    public optimizeCriticalPath(parser: HTMLParser): void {
    // 提取所有CSS
    const cssResources = parser.extractCSSResources();
    const jsResources = parser.extractJSResources();

    // 關鍵CSS(首屏可見內容所需)應該內聯
    const criticalCSS = this.extractCriticalCSS(cssResources);
    if (criticalCSS) {
    this.inlineCriticalCSS(parser, criticalCSS);
    }

    // 非關鍵CSS應該異步加載
    const nonCriticalCSS = cssResources.filter(r => !r.url?.includes('critical'));
    this.loadCSSAsync(parser, nonCriticalCSS);

    // JavaScript應該按需加載
    this.optimizeJSLoading(parser, jsResources);
    }

    private extractCriticalCSS(cssResources: Resource[]): string | null {
    // 簡化實現:假設第一個CSS文件包含關鍵樣式
    // 實際項目中應使用關鍵CSS提取工具如Penthouse
    return null;
    }

    private inlineCriticalCSS(parser: HTMLParser, css: string): void {
    // 在head中添加內聯style標籤
    const styleTag = `<style>${css}</style>`;
    // 實現添加到head的邏輯
    }

    private loadCSSAsync(parser: HTMLParser, cssResources: Resource[]): void {
    // 使用preload或async屬性加載CSS
    cssResources.forEach(resource => {
    if (resource.element) {
    // 修改link標籤添加media="print" onload策略
    // 實際實現需要操作DOM
    }
    });
    }

    private optimizeJSLoading(parser: HTMLParser, jsResources: Resource[]): void {
    // 將非關鍵JavaScript標記為async或defer
    jsResources.forEach(resource => {
    if (!this.isCriticalJS(resource)) {
    // 添加async或defer屬性
    // 實際實現需要操作DOM
    }
    });
    }

    private isCriticalJS(resource: Resource): boolean {
    // 判斷JavaScript是否關鍵(需要立即執行)
    const criticalPatterns = [
    /polyfill/,
    /modernizr/,
    /critical/,
    /init\\.js$/,
    /main\\.js$/
    ];

    if (!resource.url) return true; // 內聯腳本假設為關鍵

    return criticalPatterns.some(pattern => pattern.test(resource.url!));
    }
    }

    第十一章:測試與驗證

    11.1 單元測試

    typescript

    // test/unit/html-parser.test.ts
    import { HTMLParser } from '../../src/parsers/html-parser';
    import { describe, it, expect } from '@jest/globals';

    describe('HTMLParser', () => {
    const testHTML = `
    <!DOCTYPE html>
    <html>
    <head>
    <link rel="stylesheet" href="styles/main.css">
    <link rel="stylesheet" href="styles/print.css" media="print">
    <style>
    @import url("imported.css");
    body { margin: 0; }
    </style>
    </head>
    <body>
    <script src="js/jquery.js"></script>
    <script src="js/app.js" async></script>
    <script>
    console.log('inline script');
    </script>
    </body>
    </html>
    `;

    it('應該正確提取CSS資源', () => {
    const parser = new HTMLParser(testHTML, '/test');
    const cssResources = parser.extractCSSResources();

    expect(cssResources).toHaveLength(3); // 2個link + 1個@import
    expect(cssResources[0].url).toBe('styles/main.css');
    expect(cssResources[1].media).toBe('print');
    expect(cssResources[2].import).toBe(true);
    });

    it('應該正確提取JS資源', () => {
    const parser = new HTMLParser(testHTML, '/test');
    const jsResources = parser.extractJSResources();

    expect(jsResources).toHaveLength(2);
    expect(jsResources[0].url).toBe('js/jquery.js');
    expect(jsResources[1].async).toBe(true);
    });

    it('應該正確提取內聯資源', () => {
    const parser = new HTMLParser(testHTML, '/test');
    const inlineResources = parser.extractInlineResources();

    expect(inlineResources).toHaveLength(2); // 1個style + 1個script
    expect(inlineResources[0].type).toBe('css');
    expect(inlineResources[1].type).toBe('js');
    });

    it('應該正確移除資源標籤', () => {
    const parser = new HTMLParser(testHTML, '/test');
    const cssResources = parser.extractCSSResources();

    expect(cssResources).toHaveLength(3);

    parser.removeResource(cssResources[0]);
    const remainingResources = parser.extractCSSResources();

    expect(remainingResources).toHaveLength(2);
    });
    });

    11.2 集成測試

    typescript

    // test/integration/merger-engine.test.ts
    import { MergerEngine } from '../../src/core/merger-engine';
    import * as fs from 'fs';
    import * as path from 'path';
    import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';

    describe('MergerEngine 集成測試', () => {
    const testDir = path.join(__dirname, 'test-fixtures');
    const htmlFile = path.join(testDir, 'index.html');

    beforeAll(() => {
    // 創建測試目錄和文件
    if (!fs.existsSync(testDir)) {
    fs.mkdirSync(testDir, { recursive: true });
    }

    // 創建測試HTML文件
    const htmlContent = `
    <!DOCTYPE html>
    <html>
    <head>
    <title>測試頁面</title>
    <link rel="stylesheet" href="css/style1.css">
    <link rel="stylesheet" href="css/style2.css">
    </head>
    <body>
    <h1>測試</h1>
    <script src="js/script1.js"></script>
    <script src="js/script2.js"></script>
    </body>
    </html>
    `;

    fs.writeFileSync(htmlFile, htmlContent, 'utf-8');

    // 創建CSS文件
    const cssDir = path.join(testDir, 'css');
    if (!fs.existsSync(cssDir)) {
    fs.mkdirSync(cssDir, { recursive: true });
    }

    fs.writeFileSync(
    path.join(cssDir, 'style1.css'),
    'body { color: red; }',
    'utf-8'
    );

    fs.writeFileSync(
    path.join(cssDir, 'style2.css'),
    'h1 { color: blue; }',
    'utf-8'
    );

    // 創建JS文件
    const jsDir = path.join(testDir, 'js');
    if (!fs.existsSync(jsDir)) {
    fs.mkdirSync(jsDir, { recursive: true });
    }

    fs.writeFileSync(
    path.join(jsDir, 'script1.js'),
    'console.log("script1");',
    'utf-8'
    );

    fs.writeFileSync(
    path.join(jsDir, 'script2.js'),
    'console.log("script2");',
    'utf-8'
    );
    });

    afterAll(() => {
    // 清理測試文件
    if (fs.existsSync(testDir)) {
    fs.rmSync(testDir, { recursive: true });
    }
    });

    it('應該正確合併資源', async () => {
    const result = await MergerEngine.mergeFile(htmlFile, {
    outputDir: path.join(testDir, 'dist'),
    minifyCSS: false,
    minifyJS: false
    });

    expect(result.stats.originalRequests).toBe(4);
    expect(result.stats.finalRequests).toBe(2); // 1個CSS + 1個JS
    expect(result.stats.reductionPercentage).toBe(50);

    // 檢查輸出文件是否存在
    const distDir = path.join(testDir, 'dist');
    expect(fs.existsSync(distDir)).toBe(true);

    // 檢查合併後的HTML
    expect(result.html).toContain('merged-css');
    expect(result.html).toContain('merged-js');

    // 檢查合併後的資源文件
    const files = fs.readdirSync(distDir);
    const cssFiles = files.filter(f => f.endsWith('.css'));
    const jsFiles = files.filter(f => f.endsWith('.js'));

    expect(cssFiles.length).toBe(1);
    expect(jsFiles.length).toBe(1);
    }, 10000); // 設置較長超時時間
    });

    11.3 性能基準測試

    typescript

    // test/benchmark/performance.test.ts
    import { MergerEngine } from '../../src/core/merger-engine';
    import * as fs from 'fs';
    import * as path from 'path';
    import { describe, it, expect } from '@jest/globals';

    describe('性能基準測試', () => {
    const createTestHTML = (resourceCount: number): string => {
    let html = '<!DOCTYPE html><html><head><title>測試</title>';

    // 添加CSS
    for (let i = 0; i < Math.floor(resourceCount / 2); i++) {
    html += `<link rel="stylesheet" href="css/style${i}.css">\\n`;
    }

    html += '</head><body><h1>測試</h1>';

    // 添加JS
    for (let i = 0; i < Math.floor(resourceCount / 2); i++) {
    html += `<script src="js/script${i}.js"></script>\\n`;
    }

    html += '</body></html>';
    return html;
    };

    const createTestResources = (dir: string, count: number): void => {
    // 創建CSS文件
    const cssDir = path.join(dir, 'css');
    if (!fs.existsSync(cssDir)) {
    fs.mkdirSync(cssDir, { recursive: true });
    }

    for (let i = 0; i < Math.floor(count / 2); i++) {
    fs.writeFileSync(
    path.join(cssDir, `style${i}.css`),
    `.element-${i} { color: #${i.toString(16).padStart(6, '0')}; }`,
    'utf-8'
    );
    }

    // 創建JS文件
    const jsDir = path.join(dir, 'js');
    if (!fs.existsSync(jsDir)) {
    fs.mkdirSync(jsDir, { recursive: true });
    }

    for (let i = 0; i < Math.floor(count / 2); i++) {
    fs.writeFileSync(
    path.join(jsDir, `script${i}.js`),
    `console.log("script${i}");`,
    'utf-8'
    );
    }
    };

    it('應該高效處理大量資源', async () => {
    const testDir = path.join(__dirname, 'benchmark-test');
    const htmlFile = path.join(testDir, 'index.html');

    // 準備測試環境
    if (!fs.existsSync(testDir)) {
    fs.mkdirSync(testDir, { recursive: true });
    }

    const resourceCount = 50;
    const htmlContent = createTestHTML(resourceCount);
    fs.writeFileSync(htmlFile, htmlContent, 'utf-8');
    createTestResources(testDir, resourceCount);

    // 運行性能測試
    const startTime = Date.now();

    const result = await MergerEngine.mergeFile(htmlFile, {
    outputDir: path.join(testDir, 'dist'),
    minifyCSS: true,
    minifyJS: true
    });

    const endTime = Date.now();
    const duration = endTime – startTime;

    console.log(`處理 ${resourceCount} 個資源用時: ${duration}ms`);
    console.log(`請求數從 ${result.stats.originalRequests} 減少到 ${result.stats.finalRequests}`);

    // 性能斷言:處理50個資源應該在10秒內完成
    expect(duration).toBeLessThan(10000);

    // 清理
    if (fs.existsSync(testDir)) {
    fs.rmSync(testDir, { recursive: true });
    }
    }, 30000); // 30秒超時
    });

    第十二章:部署與使用

    12.1 構建與打包

    創建構建腳本 build.ts:

    typescript

    // scripts/build.ts
    import * as esbuild from 'esbuild';
    import * as fs from 'fs';
    import * as path from 'path';

    async function build() {
    console.log('開始構建…');

    try {
    // 構建CLI工具
    await esbuild.build({
    entryPoints: ['src/cli/cli.ts'],
    bundle: true,
    platform: 'node',
    target: 'node14',
    outfile: 'dist/cli.js',
    external: [
    'commander',
    'chokidar',
    'axios',
    'cheerio',
    'css-tree',
    'terser',
    'postcss',
    'cssnano',
    '@babel/parser',
    '@babel/traverse',
    '@babel/types'
    ],
    sourcemap: true,
    minify: true
    });

    // 添加shebang到CLI文件
    const cliContent = fs.readFileSync('dist/cli.js', 'utf-8');
    fs.writeFileSync('dist/cli.js', '#!/usr/bin/env node\\n' + cliContent);

    // 設置執行權限
    fs.chmodSync('dist/cli.js', '755');

    // 構建庫版本
    await esbuild.build({
    entryPoints: ['src/index.ts'],
    bundle: true,
    platform: 'node',
    format: 'cjs',
    target: 'node14',
    outfile: 'dist/index.cjs',
    external: [
    'commander',
    'chokidar',
    'axios',
    'cheerio',
    'css-tree',
    'terser',
    'postcss',
    'cssnano',
    '@babel/parser',
    '@babel/traverse',
    '@babel/types'
    ],
    sourcemap: true
    });

    await esbuild.build({
    entryPoints: ['src/index.ts'],
    bundle: true,
    platform: 'neutral',
    format: 'esm',
    target: 'es2020',
    outfile: 'dist/index.mjs',
    external: [
    'commander',
    'chokidar',
    'axios',
    'cheerio',
    'css-tree',
    'terser',
    'postcss',
    'cssnano',
    '@babel/parser',
    '@babel/traverse',
    '@babel/types'
    ],
    sourcemap: true
    });

    // 複製類型定義
    fs.cpSync('src/types', 'dist/types', { recursive: true });

    // 創建package.json用於分發
    const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
    const distPackageJson = {
    name: packageJson.name,
    version: packageJson.version,
    description: packageJson.description,
    main: './index.cjs',
    module: './index.mjs',
    types: './types/index.d.ts',
    bin: {
    'html-resource-merger': './cli.js'
    },
    dependencies: packageJson.dependencies,
    keywords: packageJson.keywords,
    author: packageJson.author,
    license: packageJson.license,
    repository: packageJson.repository,
    engines: {
    node: '>=14.0.0'
    }
    };

    fs.writeFileSync(
    'dist/package.json',
    JSON.stringify(distPackageJson, null, 2),
    'utf-8'
    );

    console.log('構建完成!');

    } catch (error) {
    console.error('構建失敗:', error);
    process.exit(1);
    }
    }

    build();

    12.2 使用示例

    創建示例目錄和文件:

    html

    <!– examples/sample-site/index.html –>
    <!DOCTYPE html>
    <html lang="zh-TW">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例網站</title>

    <!– CSS資源 –>
    <link rel="stylesheet" href="css/reset.css">
    <link rel="stylesheet" href="css/layout.css">
    <link rel="stylesheet" href="css/components/button.css">
    <link rel="stylesheet" href="css/components/modal.css">
    <link rel="stylesheet" href="css/print.css" media="print">

    <!– JavaScript資源 –>
    <script src="js/vendor/jquery.min.js"></script>
    <script src="js/utils/helpers.js"></script>
    <script src="js/components/dialog.js" defer></script>
    <script src="js/main.js" defer></script>
    </head>
    <body>
    <header>
    <h1>資源合併示例</h1>
    <button class="btn-primary">點擊我</button>
    </header>

    <main>
    <p>這是一個展示資源合併工具的示例網站。</p>
    </main>

    <footer>
    <p>© 2023 資源合併工具示例</p>
    </footer>
    </body>
    </html>

    12.3 使用文檔

    創建詳細的使用文檔:

    markdown

    # HTML資源合併工具使用文檔

    ## 安裝

    ### 全局安裝
    ```bash
    npm install -g html-resource-merger

    項目內安裝

    bash

    npm install –save-dev html-resource-merger

    基本使用

    命令行工具

    bash

    # 基本用法
    html-resource-merger index.html

    # 指定輸出目錄
    html-resource-merger index.html -o dist

    # 使用特定合併策略
    html-resource-merger index.html –strategy=media-query

    # 禁用壓縮
    html-resource-merger index.html –no-minify-css –no-minify-js

    # 監視模式
    html-resource-merger watch index.html

    Node.js API

    javascript

    const { MergerEngine } = require('html-resource-merger');

    async function optimizeHTML() {
    const result = await MergerEngine.mergeFile('index.html', {
    strategy: 'composite',
    minifyCSS: true,
    minifyJS: true,
    outputDir: 'dist'
    });

    console.log(`減少了 ${result.stats.reductionPercentage}% 的請求`);
    return result.html;
    }

    合併策略

    工具支持多種合併策略:

    1. basic (基本策略)

    • 所有CSS合併為一個文件

    • 所有JavaScript合併為一個文件

    2. media-query (媒體查詢策略)

    • 按CSS媒體查詢分組合併

    • 不同媒體查詢的CSS分開合併

    3. loading (加載策略)

    • 按加載特性分組合併

    • async、defer、module分開處理

    4. page-section (頁面區域策略)

    • 按頁面功能區域分組合併

    • 如vendor、components、utils等

    5. composite (複合策略,默認)

    • 結合loading和page-section策略

    • 智能分組,平衡緩存和並行加載

    6. auto (自動策略)

    • 自動分析資源特性

    • 選擇最合適的合併策略

    配置選項

    選項類型默認值說明
    strategy string 'auto' 合併策略
    minifyCSS boolean true 是否壓縮CSS
    minifyJS boolean true 是否壓縮JavaScript
    inlineThreshold number 2048 內聯閾值(字節)
    outputDir string 'dist' 輸出目錄
    versioning boolean true 是否添加版本號
    cacheBusting boolean true 是否添加緩存破壞參數
    sourceMap boolean false 是否生成source map

    高級功能

    1. 關鍵CSS提取

    工具可以識別並內聯關鍵CSS,優化首屏渲染。

    2. HTTP/2優化

    在HTTP/2環境下,工具會調整合併策略,避免過度合併小文件。

    3. 差異化緩存

    根據資源類型設置不同的緩存策略,提高緩存效率。

    4. 資源加載優化

    自動添加async/defer屬性,優化資源加載時機。

    集成到構建流程

    Webpack集成

    javascript

    // webpack.config.js
    const { MergerEngine } = require('html-resource-merger');

    module.exports = {
    // …其他配置

    plugins: [
    {
    apply: (compiler) => {
    compiler.hooks.afterEmit.tapPromise('HtmlResourceMerger', async () => {
    await MergerEngine.mergeFile('dist/index.html', {
    outputDir: 'dist',
    minifyCSS: true,
    minifyJS: true
    });
    });
    }
    }
    ]
    };

    Gulp集成

    javascript

    // gulpfile.js
    const { MergerEngine } = require('html-resource-merger');
    const { src, dest } = require('gulp');

    function mergeResources() {
    return src('src/*.html')
    .pipe(async (file) => {
    const result = await MergerEngine.mergeFile(file.path, {
    outputDir: 'dist'
    });
    file.contents = Buffer.from(result.html);
    return file;
    })
    .pipe(dest('dist'));
    }

    exports.merge = mergeResources;

    性能最佳實踐

  • 設置合適的內聯閾值:小於2KB的資源建議內聯

  • 使用版本號:確保瀏覽器緩存正確更新

  • 監視模式開發:開發時使用監視模式自動重新合併

  • 分析模式:定期使用分析模式檢查資源結構

  • HTTP/2考慮:如果使用HTTP/2,考慮調整合併策略

  • 故障排除

    常見問題

  • 資源路徑錯誤

    • 確保相對路徑正確

    • 使用絕對路徑或basePath配置

  • 合併後樣式錯亂

    • 檢查CSS媒體查詢

    • 使用media-query策略

  • JavaScript錯誤

    • 檢查變量衝突

    • 使用模塊包裹避免污染

  • 性能問題

    • 減少過度合併

    • 考慮HTTP/2特性

  • 貢獻指南

    歡迎貢獻代碼!請參閱CONTRIBUTING.md文件。

    許可證

    MIT License

    text

    ## 第十三章:結論與未來發展

    ### 13.1 總結

    通過本文,我們從零開始構建了一個完整的HTML資源合併工具。該工具具有以下特點:

    1. **智能解析**:準確識別HTML中的CSS和JavaScript資源
    2. **多策略合併**:支持多種合併策略,適應不同場景
    3. **資源處理**:完整的CSS和JavaScript處理管道
    4. **性能優化**:內聯、壓縮、緩存優化等多種優化手段
    5. **易用性**:提供命令行工具和Node.js API
    6. **可擴展性**:模塊化設計,易於擴展新功能

    ### 13.2 性能收益

    通過實際測試,該工具可以:

    1. 減少50%-90%的HTTP請求數
    2. 通過壓縮減少20%-60%的文件大小
    3. 優化資源加載順序,改善關鍵渲染路徑
    4. 提高瀏覽器緩存效率

    ### 13.3 局限與改進方向

    當前工具的局限性:

    1. **靜態分析限制**:無法動態分析JavaScript執行時加載的資源
    2. **CSS優先級處理**:複雜的CSS層疊上下文處理可能不夠完善
    3. **資源依賴分析**:對複雜的模塊依賴關係分析有限

    未來改進方向:

    1. **動態資源檢測**:集成Puppeteer等工具進行動態分析
    2. **AI優化策略**:使用機器學習優化合併策略
    3. **圖像優化集成**:集成圖像壓縮和WebP轉換
    4. **CDN集成**:直接上傳優化後資源到CDN
    5. **預渲染優化**:支持SPA應用的資源預加載

    ### 13.4 與現有工具的比較

    | 特性 | 本工具 | Webpack | Parcel | Vite |
    |——|——–|———|——–|——|
    | 零配置 | ✓ | ✗ | ✓ | ✓ |
    | HTML中心 | ✓ | ✗ | ✗ | ✓ |
    | 多策略合併 | ✓ | ✗ | ✗ | ✗ |
    | 靜態分析 | ✓ | ✓ | ✓ | ✓ |
    | 動態分析 | ✗ | ✓ | ✓ | ✓ |
    | 構建速度 | 快 | 慢 | 快 | 很快 |
    | 適用場景 | 傳統網站 | 現代應用 | 通用 | 現代應用 |

    ### 13.5 最終建議

    對於不同類型的項目,建議:

    1. **傳統多頁應用**:使用本工具進行資源合併優化
    2. **現代前端應用**:使用Webpack/Vite等現代構建工具
    3. **靜態網站**:在構建流程中集成本工具
    4. **遺留系統**:使用本工具進行漸進式優化

    資源合併是Web性能優化的重要手段,但並非萬能靈藥。在HTTP/2逐漸普及的今天,我們需要更加智能地判斷何時合併、何時保持獨立。本工具提供的多策略支持和HTTP/2感知功能,正是為了適應這種變化。

    通過自主開發這樣的工具,我們不僅獲得了性能優化的能力,更重要的是深入理解了Web性能優化的原理。這種理解將幫助我們在未後的項目中做出更好的技術決策。

    **附錄A:完整package.json示例**

    ```json
    {
    "name": "html-resource-merger",
    "version": "1.0.0",
    "description": "自動合併HTML中的CSS和JavaScript資源,減少HTTP請求數",
    "main": "dist/index.cjs",
    "module": "dist/index.mjs",
    "types": "dist/types/index.d.ts",
    "bin": {
    "html-resource-merger": "dist/cli.js"
    },
    "scripts": {
    "build": "ts-node scripts/build.ts",
    "dev": "ts-node src/cli/cli.ts",
    "test": "jest",
    "test:coverage": "jest –coverage",
    "lint": "eslint src/**/*.ts",
    "format": "prettier –write src/**/*.ts",
    "prepublishOnly": "npm run build && npm test"
    },
    "keywords": [
    "html",
    "css",
    "javascript",
    "merge",
    "optimization",
    "performance",
    "http-requests"
    ],
    "author": "Your Name",
    "license": "MIT",
    "dependencies": {
    "axios": "^1.3.4",
    "cheerio": "^1.0.0-rc.12",
    "commander": "^10.0.0",
    "css-tree": "^2.3.1",
    "postcss": "^8.4.21",
    "terser": "^5.16.1"
    },
    "devDependencies": {
    "@babel/parser": "^7.21.3",
    "@babel/traverse": "^7.21.3",
    "@babel/types": "^7.21.3",
    "@types/jest": "^29.4.0",
    "@types/node": "^18.14.2",
    "@typescript-eslint/eslint-plugin": "^5.54.0",
    "@typescript-eslint/parser": "^5.54.0",
    "chokidar": "^3.5.3",
    "cssnano": "^5.1.15",
    "esbuild": "^0.17.8",
    "eslint": "^8.35.0",
    "jest": "^29.4.3",
    "prettier": "^2.8.4",
    "ts-jest": "^29.0.5",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.5"
    },
    "engines": {
    "node": ">=14.0.0"
    },
    "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/html-resource-merger.git"
    },
    "bugs": {
    "url": "https://github.com/yourusername/html-resource-merger/issues"
    },
    "homepage": "https://github.com/yourusername/html-resource-merger#readme"
    }

    附錄B:項目結構

    text

    html-resource-merger/
    ├── src/
    │ ├── analyzers/ # 代碼分析器
    │ │ ├── js-analyzer.ts
    │ │ └── …
    │ ├── cache/ # 緩存管理
    │ │ ├── advanced-cache.ts
    │ │ └── …
    │ ├── cli/ # 命令行界面
    │ │ └── cli.ts
    │ ├── core/ # 核心引擎
    │ │ └── merger-engine.ts
    │ ├── fetchers/ # 資源獲取
    │ │ └── resource-fetcher.ts
    │ ├── optimizers/ # 優化器
    │ │ └── loading-optimizer.ts
    │ ├── parsers/ # 解析器
    │ │ └── html-parser.ts
    │ ├── processors/ # 資源處理器
    │ │ ├── css-processor.ts
    │ │ ├── css-rewriter.ts
    │ │ ├── js-processor.ts
    │ │ └── …
    │ ├── strategies/ # 合併策略
    │ │ ├── merge-strategy.ts
    │ │ ├── strategy-manager.ts
    │ │ └── …
    │ ├── types/ # 類型定義
    │ │ └── resource.ts
    │ └── index.ts # 主入口
    ├── test/ # 測試文件
    │ ├── unit/
    │ ├── integration/
    │ └── benchmark/
    ├── examples/ # 使用示例
    ├── scripts/ # 構建腳本
    │ └── build.ts
    ├── dist/ # 構建輸出
    ├── package.json
    ├── tsconfig.json
    ├── jest.config.js
    ├── .eslintrc.js
    ├── .prettierrc
    ├── README.md
    └── LICENSE

    通過這個完整的實現,我們不僅創建了一個實用的工具,更重要的是深入理解了Web性能優化的核心原理。無論是對於個人項目還是企業級應用,這種從底層理解問題並構建解決方案的能力,都是極其寶貴的技術資產。

    赞(0)
    未经允许不得转载:171主机测评 » 手搓HTML資源合併工具:自動合併CSS/JS減少請求數
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址