深入淺出Webpack 摘要 了解Loader Plugin的編寫(xiě)

Loader

一個(gè)Loader的職責(zé)是單一的,只需要完成一種轉(zhuǎn)換。如果一個(gè)源文件需要經(jīng)歷多步轉(zhuǎn)換才能正常使用,就通過(guò)多個(gè)Loader去轉(zhuǎn)換。在調(diào)用多個(gè)Loader去轉(zhuǎn)換一個(gè)文件時(shí),每個(gè)Loader都會(huì)鏈?zhǔn)降仨樞驁?zhí)行。第1個(gè)Loader將會(huì)拿到需處理的內(nèi)容,上一個(gè)Loader處理后的結(jié)果會(huì)被傳給下一個(gè)Loader接著處理,最后的Loader將處理后的最終結(jié)果返回給Webpack。所以,在開(kāi)發(fā)一個(gè)Loader時(shí),請(qǐng)保持其職責(zé)的單一性,我們只需關(guān)心輸入和輸出。

Webpack是運(yùn)行在Node.js上的,一個(gè)Loader其實(shí)就是一個(gè)Node.js模塊,這個(gè)模塊需要導(dǎo)出一個(gè)函數(shù)。這個(gè)導(dǎo)出的函數(shù)的工作就是獲得處理前的原內(nèi)容,對(duì)原內(nèi)容執(zhí)行處理后,返回處理后的內(nèi)容。

// 最基本的結(jié)構(gòu)
// Loader運(yùn)行在Node.js中,所以可以調(diào)用任何Node.js的API
const sass = require('sass');

module.exports = function (source) {
    // source 為compiler 傳遞給Loader 的一個(gè)文件的原內(nèi)容
    return sass(source)
}
關(guān)于獲取Loader的options
// 借助loader-utils

const loaderUtils = require('loader-utils');

module.exports = function (source) {
    // 獲取用戶為當(dāng)前Loader 傳入的options
    const options = loaderUtils.getOptions(this);
    return source
}

上面的Loader 都只是返回了原內(nèi)容轉(zhuǎn)換后的內(nèi)容,但在某些場(chǎng)景下還需要返回除了內(nèi)容之外的東西。

以用babel-loader 轉(zhuǎn)換ES6 代碼為例,它還需要輸出轉(zhuǎn)換后的ES5 代碼對(duì)應(yīng)的SourceMap ,以方便調(diào)試源碼。為了將Source Map 也一起隨著ES5 代碼返回給Webpack ,還可以這樣寫(xiě):

module.exports = function (source) {
    // 通過(guò)this.callback告訴webpack 返回的結(jié)果
    this.callback(null, source, sourceMaps);

    // 當(dāng)使用this.callback返回內(nèi)容時(shí),該Loader必須返回undefined
    // 以讓W(xué)ebpack知道該Loader返回的結(jié)果在this.callback中,而不是return中
    return;

    // 其中this.callback 是 webpack 向Loader中注入的API,以方便Loader和webpack之間的通信。
}

this.callback的詳細(xì)使用方法,即可以接受那些內(nèi)容從Loader中傳到webpack

this.callback(
    // 當(dāng)無(wú)法轉(zhuǎn)換原內(nèi)容時(shí),為Webpack 返回一個(gè)Error
    err: Error | null,
    // 原內(nèi)容轉(zhuǎn)換后的內(nèi)容
    content: string | Buffer,
    // 可選,用于通過(guò)轉(zhuǎn)換后的內(nèi)容得出原內(nèi)容的Source Map ,以方便調(diào)試
    // webpack 會(huì)通過(guò) this.sourceMap API 告訴Loader用戶是否配置需要sourceMap
    sourceMap?: SourceMap,
    // 如果本次轉(zhuǎn)換為原內(nèi)容生成了AST 語(yǔ)法樹(shù),則可以將這個(gè)AST 返回,
    // 以方便之后需要AST 的Loader 復(fù)用該AST ,避免重復(fù)生成AST ,提升性能
    abstractSyntaxTree?: AST 
)
同步與異步

Loader 有同步和異步之分,上面介紹的L oader 都是同步的Loader ,因?yàn)樗鼈兊霓D(zhuǎn)換流程都是同步的,轉(zhuǎn)換完成后再返回結(jié)果。但在某些場(chǎng)景下轉(zhuǎn)換的步驟只能是異步完成的,例如我們需要通過(guò)網(wǎng)絡(luò)請(qǐng)求才能得出結(jié)果,如果采用同步的方式,則網(wǎng)絡(luò)請(qǐng)求會(huì)阻塞整個(gè)構(gòu)建,導(dǎo)致構(gòu)建非常緩慢。

如果是異步轉(zhuǎn)換,則我們可以這樣做:

module.exports = function(source) {
    // 告訴Webpack 本次轉(zhuǎn)換是異步的, Loader 會(huì)在callback 中回調(diào)結(jié)果
    var callback = this.async();

    // 異步操作函數(shù)
    function someAsyncOperation(source, cb) {
        var err, result, sourceMaps, ast;

        // 模擬異步
        new Promise((res, rej) => {
            resolve();
        }).then(res => {
            // ...異步操作成功后 將正確處理過(guò)需要返回的值返回
            cb(err, result, sourceMaps, ast)
        })
    }

    someAsyncOperation(source, function(err, result, sourceMaps, ast){
        // 通過(guò)callback 返回異步執(zhí)行后的結(jié)果
        callback(err, result, sourceMaps, ast)
    })
}
處理二進(jìn)制數(shù)據(jù)

在默認(rèn)情況下, Webpack 傳給Loader 的原內(nèi)容都是UTF-8 格式編碼的字符串。但在某些場(chǎng)景下Loader 不會(huì)處理文本文件,而會(huì)處理二進(jìn)制文件如fil e-loader ,這時(shí)就需要Webpack為L(zhǎng)oader 傳入二進(jìn)制格式的數(shù)據(jù)。為此,我們需要這樣編寫(xiě)Loader:

module.exports = function(source) {
    console.log(source instanceof Buffer);
    // true , webpack 傳進(jìn)來(lái)的source會(huì)是二進(jìn)制數(shù)據(jù)

    // 但是不管module.exports.raw 是不是 true ,Loader都可以返回二進(jìn)制數(shù)據(jù)
    return source
}

// 通過(guò)exports.raw 屬性告訴Webpack 該Loader 是否需要二進(jìn)制數(shù)據(jù)
// 如果沒(méi)有這一行 則Loader只能拿到字符串
module.exports.raw = true;
緩存加速

在某些情況下,有些轉(zhuǎn)換操作需要大量的計(jì)算,非常耗時(shí),如果每次構(gòu)建都重新執(zhí)行重復(fù)的轉(zhuǎn)換操作,則構(gòu)建將會(huì)變得非常緩慢。為此,Webpack會(huì)默認(rèn)緩存所有Loader的處理結(jié)果,也就是說(shuō)在需要被處理的文件或者其依賴的文件沒(méi)有發(fā)生變化時(shí),是不會(huì)重新調(diào)用對(duì)應(yīng)的Loader去執(zhí)行轉(zhuǎn)換操作的。

// 如果我們不想讓W(xué)ebpack 不緩存該Loader 的處理結(jié)果,則可以這樣:

module.exports = function(source) {
    // 關(guān)閉該Loader 的緩存功能
    this.cacheable(false);
    return source
}
其他Loader API

在Loader中還可以使用以下API

  • this.context:當(dāng)前處理的文件所在的目錄,假如當(dāng)前Loader處理的文件是/src/main.js,則this.context等于/src。

  • this.resource:當(dāng)前處理的文件的完整請(qǐng)求路徑,包括querystring,例如/src/main.js?name=l。

  • this.resourcePath:當(dāng)前處理的文件的路徑,例如/src/main.js。

  • this.resourceQuery :當(dāng)前處理的文件的querystring

  • this.target:等于Webpack 配置中的Target

  • this.loadModule: 當(dāng)Loader在處理一個(gè)文件時(shí),如果依賴其他文件的處理結(jié)果才能得出當(dāng)前文件的結(jié)果,就可以通過(guò)this.loadModule(request:string,callback:function(err,source,sourceMap,module))去獲取request對(duì)應(yīng)的文件的處理結(jié)果。

  • this.resolve:像require語(yǔ)句一樣獲得指定文件的完整路徑,使用方法為resolve(context:string, request:string, callback:function(err,result:string))。

  • this.addDependency(file:string) : 為當(dāng)前處理的文件添加其依賴的文件,以便其依賴的文件發(fā)生發(fā)生變化時(shí),重新調(diào)用Loader 處理該文件

  • this.addContextDependency(file: string) : 同上,是將整個(gè)目錄加入當(dāng)前正在處理的文件的依賴中,以便其依賴的文件發(fā)生發(fā)生變化時(shí),重新調(diào)用Loader 處理該目錄下的文件。

  • this.clearDependencies :清除當(dāng)前正在處理的文件的所有依賴

  • this.emitFile :輸出一個(gè)文件,使用方法為emitFile(name : string ,content: Buffer | string , sourceMap: { ... })。

加載本地Loader

如果向采用第三方loader的方式去使用本地開(kāi)發(fā)的Loader,將會(huì)很麻煩,因?yàn)槲覀冃枰_保編寫(xiě)的Loader 的源碼在node_modules 目錄下。為此需要先將編寫(xiě)的Loader 發(fā)布到Npm倉(cāng)庫(kù), 再安裝到本地項(xiàng)目中使用。

有兩種方法,可以解決上述問(wèn)題,不需要加本地loader發(fā)布到Npm中

Npm link

Npm link 專(zhuān)門(mén)用于開(kāi)發(fā)和調(diào)試本地的Npm 模塊,能做到在不發(fā)布模塊的情況下, 將本地的一個(gè)正在開(kāi)發(fā)的模塊的源碼鏈接到項(xiàng)目的node_modules 目錄下,讓項(xiàng)目可以直接使用本地的Npm 模塊。由于是通過(guò)軟鏈接的方式實(shí)現(xiàn)的,編輯了本地的Npm 模塊的代碼,所以在項(xiàng)目中也能使用到編輯后的代碼。

1.確保正在開(kāi)發(fā)的本地Npm 模塊(也就是正在開(kāi)發(fā)的Loader ) 的package.json 已經(jīng)正確配置好。
2.在本地的Npm 模塊根目錄下執(zhí)行npm link ,將本地模塊注冊(cè)到全局。
3.在項(xiàng)目根目錄下執(zhí)行npm link loader-name ,將第2步注冊(cè)到全局的本地Npm模塊鏈接到項(xiàng)目的node_moduels 下,其中的loader-name 是指在第1步的package.json文件中配置的模塊名稱(chēng)。

Resolveloader

配置resolveLoader,配置如何尋找Loader,默認(rèn)回去node_modules中尋找。

module.exports = {
    resolveLoader: {
        // 去哪些目錄下尋找Loader 有先后之分,先去node_modules中找,找不到再去./loader/目錄中尋找。
        modules: ['node_modules', './loaders/']
    }
}

Plugin

在Webpack 運(yùn)行的生命周期中會(huì)廣播許多事件, Plugin 可以監(jiān)昕這些事件,在合適的時(shí)機(jī)通過(guò)Webpack 提供的API改變輸出結(jié)果。

一個(gè)最基礎(chǔ)的Plugin代碼如下:

class BasicPlugin {
    // 在構(gòu)造函數(shù)中獲取用戶為該插件傳入的配置
    constructor(options) {
        this.options = options
    },
    // Webpack會(huì)調(diào)用BasicPlugin實(shí)例的apply方法為插件實(shí)例傳入compiler對(duì)象
    apply(compiler) {
        compiler.plugin('compilation', function(compilation){
            // ... 監(jiān)聽(tīng)到compilation事件后的一系列操作
        })
    }
}

// 導(dǎo)出
module.exports = BasicPlugin;

在webpack的配置中這樣使用

const BasicPlugin = require('./BasicPlugin.js');
module.exports = {
    Plugins: [
        new BasicPlugin(options),
    ]
}

Webpack啟動(dòng)后,在讀取配置的過(guò)程中會(huì)先執(zhí)行new BasicPlugin(options),初始化一個(gè)BasicPlugin并獲得其實(shí)例。在初始化compiler對(duì)象后,再調(diào)用basicPlugin.apply(compiler)為插件實(shí)例傳入compiler對(duì)象。插件實(shí)例在獲取到compiler對(duì)象后,就可以通過(guò)compiler.plugin(事件名稱(chēng),回調(diào)函數(shù))監(jiān)聽(tīng)到Webpack廣播的事件,并且可以通過(guò)compiler對(duì)象去操作Webpack。

Compiler 和 Compilation

Compiler 對(duì)象包含了Webpack 環(huán)境的所有配置信息,包含options 、loaders 、plugins等信息。這個(gè)對(duì)象在Webpack 啟動(dòng)時(shí)被實(shí)例化,它是全局唯一的,可以簡(jiǎn)單地將它理解為Webpack 實(shí)例。

Compilation 對(duì)象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。當(dāng)Webpack以開(kāi)發(fā)模式運(yùn)行時(shí),每當(dāng)檢測(cè)到一個(gè)文件發(fā)生變化,便有一次新的Compilation 被創(chuàng)建。Compilation 對(duì)象也提供了很多事件回調(diào)供插件進(jìn)行擴(kuò)展。通過(guò)Compilation也能讀取到Compiler 對(duì)象。

Compiler 和Compilation 的區(qū)別在于: Compiler 代表了整個(gè)Webpack 從啟動(dòng)到關(guān)閉的生命周期,而Compilation 只代表一次新的編譯。

事件流

使用了觀察者模式,插件中進(jìn)行訂閱,webpack進(jìn)行發(fā)布(開(kāi)發(fā)的插件中也能發(fā)布其他事件)。

// 發(fā)布
compiler.apply('event-name', params);

// 插件中訂閱的事件回調(diào)就會(huì)被調(diào)用
compiler.plugin('event-name', function(params){
    // 監(jiān)聽(tīng)到了事件的發(fā)布 該回調(diào)就會(huì)調(diào)用。
})

只要能拿到Compiler 或Compilation 對(duì)象,就能廣播新的事件,所以在新開(kāi)發(fā)的插件中也能廣播事件,為其他插件監(jiān)聽(tīng)使用。傳給每個(gè)插件的Compiler 和Compilation 對(duì)象都是同一個(gè)引用。也就是說(shuō),若在一個(gè)插件中修改了Compiler或Compilation 對(duì)象上的屬性,就會(huì)影響到后面的插件。有些事件是異步的,這些異步的事件會(huì)附帶兩個(gè)參數(shù),第2個(gè)參數(shù)為回調(diào)函數(shù),在插件處理完任務(wù)時(shí)需要調(diào)用回調(diào)函數(shù)通知Webpack,才會(huì)進(jìn)入下一個(gè)處理流程。

compiler.plugin('emit', function(compilation, callback){
    // 處理完畢后執(zhí)行callback 以通知Webpack
    // 如果不執(zhí)行callback ,運(yùn)行流程將會(huì)一直卡在這里而不往后執(zhí)行
    callback();
})

常用API

讀取輸出資源、代碼塊、模塊及其依賴

emit 事件發(fā)生時(shí),代表源文件的轉(zhuǎn)換和組裝己經(jīng)完成,在這里可以讀取到最終將輸出的資源、代碼塊、模塊及其依賴,并且可以修改輸出資源的內(nèi)容。插件的代碼如下:

class Plugin {
    constructor() {

    },
    apply(complier) {
        complier.plugin('emit', function(compilation, callback){
            // compilation.chunks : Array 存放所有代碼塊
            compilation.chunks.forEach(function(chunk) {
                // chunk就是chunks數(shù)組中每一個(gè)單獨(dú)代碼塊
                // 而chunk本身又是由很多個(gè)Modules組成的,所以也是一個(gè)Array
                // 可以使用forEachModule來(lái)遍歷
                chunk.forEachModule(function (module) {
                    // module就是chunk中每一個(gè)單獨(dú)的模塊
                    // module.fileDependencies 存放當(dāng)前模塊的所有依賴的文件路徑,是一個(gè)數(shù)組
                    module.fileDependencies.forEach(function(filePath){
                        // filePath 就是 module中每一個(gè)依賴文件的路徑
                    })
                })

                // Webpack 會(huì)根據(jù)Chunk 生成輸出的文件資源,每個(gè)Chunk 都對(duì)應(yīng)一個(gè)及以上的輸出文件
                // 例如在chunk 中包含css 模塊并且使用了ExtractTextPlugin 時(shí)
                // 該chunk 就會(huì)生成.js 和 .css 兩個(gè)文件
                // chunk.files 就是該chunk將要輸出的文件名稱(chēng)組成的數(shù)組
                chunk.files.forEach(function(filename){
                    // compilation.assets 存放著當(dāng)前即將輸出的所有資源
                    // 通過(guò)該文件名在compilation.assets中獲取當(dāng)前輸出資源,調(diào)用一個(gè)輸出資源的source()方法能獲取輸出資源的內(nèi)容
                    let source = compilation.assets[filename].source();
                })
            })
            // 這是一個(gè)異步事件,要記得調(diào)用callback 來(lái)通知Webpack 本次事件監(jiān)聽(tīng)處理結(jié)束
            callback();
        })
    }
}
監(jiān)聽(tīng)文件的變化

Webpack 會(huì)從配置的入口模塊出發(fā),依次找出所有依賴模塊, 當(dāng)入口模塊或者其依賴的模塊發(fā)生變化時(shí), 就會(huì)觸發(fā)一次新的Compilation 。

在開(kāi)發(fā)插件時(shí)經(jīng)常需要知道是哪個(gè)文件發(fā)生的變化導(dǎo)致了新的Compilation,可以監(jiān)聽(tīng)watch-run事件,當(dāng)依賴的文件發(fā)生變化時(shí)會(huì)被觸發(fā)。

compiler.plugin('watch-run', function(watching, callback){
    // 獲取發(fā)生變化的文件列表
    const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
    // changedFiles 格式為鍵值對(duì),鍵為發(fā)生變化的文件路徑
    if (changedFiles['./show.js'] !== undefined) {
        // show.js 發(fā)生了變化
    }
    // 異步事件
    callback();
})

Webpack 只會(huì)監(jiān)視入口和其依賴的模塊是否發(fā)生了變化,所以Webpack 不會(huì)監(jiān)聽(tīng)HTML 文件的變化,編輯HTML 文件時(shí)就不會(huì)重新觸發(fā)新的Compilation。某些情況下會(huì)需要監(jiān)聽(tīng)HTML的變化,這時(shí)候可以將HTML 文件加入依賴列表中,為此可以使用如下代碼:

// after-compile 會(huì)在一次Compilation 執(zhí)行完成
comper.plugin('after-compile', (compilation, callback) => {
    // 將HTML 文件添加到文件依賴列表中,因此在HTML 模板文件發(fā)生變化時(shí)重新啟動(dòng)一次編譯
    compilation.fileDependencies.push('./test.html');
    callback();
})
修改輸出資源

在某些場(chǎng)景下插件需要修改、增加、刪除輸出的資源,要做到這一點(diǎn), 則需要監(jiān)聽(tīng)emit事件,因?yàn)榘l(fā)生emit 事件時(shí)所有模塊的轉(zhuǎn)換和代碼塊對(duì)應(yīng)的文件已經(jīng)生成好,需要輸出的資源即將輸出,因此emit 事件是修改Webpack 輸出資源的最后時(shí)機(jī)。

上面已提到過(guò) compilation.assets存放著當(dāng)前即將輸出的所有資源,是一個(gè)鍵值對(duì),鍵為需要輸出的文件名稱(chēng),值為文件對(duì)應(yīng)的內(nèi)容。

// 修改輸出文件
compiler.plugin('emit', (compilation, callback) => {
    // 設(shè)置名稱(chēng)為fileName 的輸出資源
    compilation.assets[fileName] = {
        // source()用來(lái)輸出文件內(nèi)容,在source方法中對(duì)文件進(jìn)行修改并輸出
        source: () => {
            // 文件內(nèi)容 既可以是代表文本文件的字符串,也可以是代表二進(jìn)制文件的Buffer
            return fileContent;
        },
        // 返回文件的大小
        size: () => {
            return Buffer.byteLength(fileContent, 'utf8');
        }
    }
    callback();
})

// 讀取輸出文件
compiler.plugin('emit', (compilation, callback) => {
    // 讀取名稱(chēng)為fileName 的輸出資源
    const asset = compiler.assets[fileName];
    // 獲取輸出的內(nèi)容
    asset.source();
    // 獲取輸出資源的大小
    asset.size();
    callback();
})
判斷Webpack 使用了哪些插件

在開(kāi)發(fā)一個(gè)插件時(shí),我們可能需要根據(jù)當(dāng)前配置是否使用了其他插件來(lái)做下一步?jīng)Q定,因此需要讀取Webpack 當(dāng)前的插件配置情況。比如,若想判斷當(dāng)前是否使用了ExtractTextPlugin

function hasExtractTextPlugin(compiler) {
    // 當(dāng)前配置使用的所有插件列表
    const plugins = compiler.options.plugins;

    return plugins.find(plugin => {
        // 去plugins 中尋找有沒(méi)有ExtractTextPlugin 的實(shí)例
        plugin.__proto__.constructor === ExtractTextPlugin
    }) != null;
}
寫(xiě)一個(gè)簡(jiǎn)單至極的插件

該插件的名稱(chēng)為EndWebpackPlugin ,作用是在Webpack 即將退出時(shí) 針對(duì)構(gòu)建成功還是構(gòu)建失敗 附加一些額外的操作。

// 如何使用 
module.exports = {
    Plugins: [
        // 在初始化的時(shí)候,傳入兩個(gè)參數(shù),分別會(huì)成功回調(diào)和失敗回調(diào)
        new EndWebpackPlugin(() => {
            // Webpack 構(gòu)建成功,并且在文件輸出后會(huì)執(zhí)行到這里,在這里可以做發(fā)布文件操作
        }, (err) => {
            // Webpack 構(gòu)建失敗, err 是導(dǎo)致錯(cuò)誤的原因
            console.log(err);
        })
    ]
}
// 插件代碼

class EndWebpackPlugin {
    constructor(doneCb, failCb) {
        this.doneCb = doneCb;
        this.failCb = failCb;
    },
    apply(compiler) {
        compiler.plugin('done', function(stats){
            // 監(jiān)聽(tīng)webpack 的 done 事件,回調(diào)doneCb
            this.doneCb(stats)
        })

        compiler.plugin('failed', function(err){
            // 監(jiān)聽(tīng)webpack 的 done 事件,回調(diào)doneCb
            this.failCb(err)
        })
    }
}

module.exports = EndWebpackPlugin;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 編寫(xiě) Loader Loader就像是一個(gè)翻譯員,能把源文件經(jīng)過(guò)轉(zhuǎn)化后輸出新的結(jié)果,并且一個(gè)文件還可以鏈?zhǔn)降慕?jīng)過(guò)多...
    oWSQo閱讀 7,697評(píng)論 0 8
  • 工作原理概括 基本概念 Entry:入口,Webpack執(zhí)行構(gòu)建的第一步將從Entry開(kāi)始,可抽象成輸入。 Mod...
    Upcccz閱讀 397評(píng)論 0 1
  • 可以結(jié)合慕課網(wǎng)的視頻來(lái)讀這篇文章,地址:http://www.imooc.com/learn/802 Webpac...
    哈哈騰飛閱讀 2,102評(píng)論 0 7
  • 模塊化 模塊化是指把一個(gè)復(fù)雜的系統(tǒng)分解到多個(gè)模塊以方便編碼。 缺點(diǎn) 命名空間沖突,兩個(gè)庫(kù)可能會(huì)使用同一個(gè)名稱(chēng),例如...
    Upcccz閱讀 681評(píng)論 0 3
  • 伍綺詩(shī)(Celeste Ng)在美國(guó)賓夕法尼亞州和俄亥俄州長(zhǎng)大,父母均為科學(xué)家的她,是香港移民第二代。《無(wú)聲告白》...
    王麗雙閱讀 229評(píng)論 0 1

友情鏈接更多精彩內(nèi)容