[FE] webpack群俠傳(八):childCompiler

前幾篇文章中菱肖,我們介紹了webpack v4.20.2相關的內(nèi)容酵紫,
但是很多老項目,還在使用webpack 3丑勤,
也要一些常用的代碼庫在webpack 4中是不兼容的华嘹。

例如,extract-text-webpack-plugin法竞,目前仍不兼容webpack 4耙厚,
可以參考github中這個issue,Webpack 4 compatibility岔霸。

而且薛躬,我在學習webpack源碼的過程中,
extract-text-webpack-plugin這個插件秉剑,確實給我造成了不小的困擾泛豪,
它用到了childCompiler這個概念,很值得一看侦鹏。

本文我們自成體系诡曙,來看看webpack 3項目,以及extract-text-webpack-plugin的實現(xiàn)邏輯略水。
一圖勝千言价卤,

1. webpack 3示例應用

1.1 初始化

$ mkdir ~/Test/debug-webpack3
$ cd ~/Test/debug-webpack3
$ npm init -f

1.2 安裝依賴

$ npm i -D \
webpack@3.11.0 \
babel-loader@7.1.3 \
babel-core@6.26.0 \
babel-preset-env@1.6.1 \
extract-text-webpack-plugin@3.0.2 \
css-loader@0.28.10 \
less-loader@4.0.6 \
less@2.7.3

1.3 配置webpack

新建webpack.config.js,

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'src/index.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js',
    },
    module: {
        rules: [
            { test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['babel-preset-env'] } } },
            {
                test: /\.less$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        { loader: 'css-loader' },
                        { loader: 'less-loader' },
                    ],
                }),
            },
        ]
    },
    plugins: [
        new ExtractTextPlugin({
            filename: '[name].css',
        }),
    ]
};

以上代碼中渊涝,我們使用了extract-text-webpack-plugin慎璧,
(1)對于 .less 文件,使用ExtractTextPlugin.extract配置loader
(2)在plugins中跨释,增加了一個ExtractTextPlugin的實例

注:
雖然我們已經(jīng)為ExtractTextPlugin實例配置了filename胸私,
但是extract-text-webpack-plugin仍然需要webpack.config.js導出output.filename
所以鳖谈,我們在第10output屬性中增加了filename字段岁疼。

1.4 添加npm scripts

打開package.json,為scripts屬性添加一個build字段缆娃,值為"webpack"

{
  ...
  "scripts": {
    ...
    "build": "webpack"
  },
  ...
}

1.5 新建源碼文件

(1)src/index.js

import './index.less';

alert();

(2)src/index.less

body {
    background: gray;
}

1.6 編譯打包

$ npm run build

> debug-webpack3@1.0.0 build ~/Test/debug-webpack3
> webpack

Hash: 1b8999f3bb679ecffd56
Version: webpack 3.11.0
Time: 673ms
    Asset      Size  Chunks             Chunk Names
 index.js   2.64 kB       0  [emitted]  index
index.css  29 bytes       0  [emitted]  index
   [0] ./src/index.js 49 bytes {0} [built]
   [1] ./src/index.less 41 bytes {0} [built]
    + 1 hidden module
Child extract-text-webpack-plugin node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist node_modules/_css-loader@0.28.10@css-loader/index.js!node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!src/index.less:
       [0] ./node_modules/_css-loader@0.28.10@css-loader!./node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!./src/index.less 211 bytes {0} [built]
        + 1 hidden module

1.7 查看編譯結果

(1)dist/index.js

/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};
/******/
/******/    // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }
/******/
/******/
/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;
/******/
/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;
/******/
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, {
/******/                configurable: false,
/******/                enumerable: true,
/******/                get: getter
/******/            });
/******/        }
/******/    };
/******/
/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };
/******/
/******/    // Object.prototype.hasOwnProperty.call
/******/    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";
/******/
/******/    // Load entry module and return exports
/******/    return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";


__webpack_require__(1);

alert();

/***/ }),
/* 1 */
/***/ (function(module, exports) {

// removed by extract-text-webpack-plugin

/***/ })
/******/ ]);

(2)dist/index.css

body {
  background: gray;
}

2. 調(diào)試webpack3

2.1 新建debug.js

const webpack = require('webpack');
const options = require('./config/webpack.config');

const compiler = webpack(options);

compiler.run((...args) => {
    console.log(...args);
});

2.2 使用vscode進行調(diào)試

在以上代碼第6行中捷绒,打個斷點瑰排,保持光標位于該文件中,按F5暖侨。
然后程序停在了斷點處椭住,

2.3 輕車熟路

前幾篇中,我們已經(jīng)對webpack v4.20.2有了一定的了解字逗,
現(xiàn)在雖然是webpack3(v.3.11.0)京郑,我們還是能夠駕輕就熟。

compiler.run扳肛,會跳入Compiler.js 第226行run方法中傻挂,

run(callback) {
    ...
    this.applyPluginsAsync("before-run", this, err => {
        ...
        this.applyPluginsAsync("run", this, err => {
            ...
            this.readRecords(err => {
                ...
                this.compile(onCompiled);
            });
        });
    });
}

與之前的v4.20.2對比一下, webpack 4.20.2 Compiler.js 第198行挖息,

run(callback) {
    ...
    this.hooks.beforeRun.callAsync(this, err => {
        ...
        this.hooks.run.callAsync(this, err => {
            ...
            this.readRecords(err => {
                ...
                this.compile(onCompiled);
            });
        });
    });
}

我們發(fā)現(xiàn)金拒,webpack3中的this.applyPluginsAsync("before-run", this, err => {
剛好對應與webpack4中的this.hooks.beforeRun.callAsync(this, err => { 套腹。
其余幾個hooks調(diào)用也類似绪抛。

下文中,我們?nèi)匀环Q插件中實現(xiàn)的切面為hooks电禀。
所以幢码,我們還是可以按以前的分析,知道compiler.run調(diào)用了this.compile尖飞,
于是我們在compile方法中打一個斷點症副。

compile(callback) {
    // 斷點
    
    ...
    this.applyPluginsAsync("before-compile", params, err => {
        ...
        this.applyPluginsParallel("make", compilation, err => {
            ...
            compilation.seal(err => {
                ...
                this.applyPluginsAsync("after-compile", compilation, err => {
                    ...
                });
            });
        });
    });
}

注意斷點的位置,是在compile方法的入口處政基,
還沒調(diào)用compiler.hooks.make贞铣,也沒調(diào)用compilation.seal

然后沮明,見證奇跡的時候到了辕坝。。
我們按下F5荐健,讓程序繼續(xù)運行酱畅,
結果程序運行了一會之后,又跑到了現(xiàn)在這個斷點江场。

這真是太奇怪了纺酸。
值得一提的是,run方法中的this.compile處如果打一個斷點址否,
我們會發(fā)現(xiàn)this.compile卻沒有被第二次調(diào)用吁峻。

2.4 調(diào)用堆棧

還好vscode的調(diào)試工具提供了查看調(diào)用堆棧的功能,

我們可以點擊某個棧幀在张,來查看程序的執(zhí)行過程用含。
點擊第二行runAsChild,我們發(fā)現(xiàn)this.compile是由runAsChild調(diào)用的帮匾,
runAsChildCompiler類的實例方法啄骇,位于 Compiler.js 第286行

runAsChild(callback) {
    this.compile((err, compilation) => {
        ...
    });
}

那么runAsChild是哪里調(diào)用的呢瘟斜?
我們點擊第三行pitch缸夹,結果runAsChild是由extract-text-webpack-plugin(v3.0.2)調(diào)用的,
代碼位置在螺句,extract-text-webpack-plugin loader.js 第81行虽惭,

childCompiler.runAsChild((err, entries, compilation) => {
    ...
}

這下就很清楚了,
extract-text-webpack-plugin創(chuàng)建了一個childCompiler蛇尚,
然后調(diào)用了這個childCompilerrunAsChild方法芽唇,結果導致this.compiler再次被調(diào)用了。

extract-text-webpack-plugin這樣做取劫,會對我們調(diào)試compiler.hooks.makecompilation.seal產(chǎn)生困擾匆笤,
因為this.compile會觸發(fā)兩次,
結果compiler.hooks.makecompilation.seal也會觸發(fā)兩次谱邪。

注:
每次加載一個 .less 文件炮捧,都會新建一個childCompiler
因此惦银,如果工程中用到了很多 .less 文件咆课,
this.compile方法會甚至會觸發(fā)成百上千次

至于為什么會這樣扯俱,我們繼續(xù)往下看书蚪。

3. extract-text-webpack-plugin

3.1 LOADER_EXECUTION

我們繼續(xù)跟蹤調(diào)用堆棧,點到第四行LOADER_EXECUTION蘸吓,
這個名字我們似曾相識善炫,是的,我們在第四篇runLoaders一節(jié)介紹過它库继,
它位于 loader-runner LoaderRunner.js 第118行箩艺。

var result = (function LOADER_EXECUTION() {
    return fn.apply(context, args);
}());

LOADER_EXECUTION做的事情是,使用已載入的loader宪萄,來加載相匹配的資源艺谆。
此時,已載入的loader是extract-text-webpack-plugin extract方法返回的loader拜英,
匹配的資源是待載入的less文件静汤。

我們來驗證下這個結論,在LOADER_EXECUTION 函數(shù)中打個斷點,
然后重新啟動調(diào)試虫给。

程序第一次來到這里的時候藤抡,是為了加載src/index.js,
體現(xiàn)在context.resource字段抹估,

~/Test/debug-webpack3/src/index.js

然后我們按F5缠黍,等待程序第二次來到這里,
此時药蜻,context.resource變成了瓷式,

~/Test/debug-webpack3/src/index.less

表示當前正在加載 src/index.less。

3.2 childCompiler

現(xiàn)在我們用單步調(diào)試语泽,進入到fn.apply(context, args)這個調(diào)用里面贸典。
結果程序跳轉(zhuǎn)到了 extract-text-webpack-plugin loader.js pitch函數(shù)中。

export function pitch(request) {
    ...
    if (...) {
        ...
    } else if (...) {
        ...
    } else if (...) {
        ...
        const childCompiler = this._compilation.createChildCompiler(`extract-text-webpack-plugin ${NS} ${request}`, outputOptions);
        ...
        childCompiler.runAsChild((err, entries, compilation) => {
            ...
        });
    }
}

看到了吧踱卵,每一次加載 .less文件廊驼,都會執(zhí)行LOADER_EXECUTION
每次執(zhí)行LOADER_EXECUTION 都會調(diào)用pitch函數(shù)颊埃,
pitch函數(shù)中每次都會創(chuàng)建一個新的childCompiler蔬充,然后調(diào)用childCompiler.runAsChild

3.3 this._compilation

如果我們想知道this._compilation.createChildCompiler 做了什么事情班利,
就必須知道this._compilation是怎么來的饥漫,
因此,也就必須搞清楚this是什么罗标。

this實際上就是pitch的上下文庸队,我們需要看pitch是如何被調(diào)用的,
翻看上文的調(diào)用鏈路闯割,我們知道了彻消,
pitch是通過fn.apply(context, args)調(diào)用的,其中fn的值就是pitch宙拉。

因此宾尚,pitch中的this指向了fn.apply(context, args)中的context
通過查看調(diào)用堆棧谢澈,我們最終定位到煌贴,
這個context是在 webpack NormalModule.js doBuild中調(diào)用createLoaderContext創(chuàng)建的,

doBuild(options, compilation, resolver, fs, callback) {
    ...
    const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);

    runLoaders({
        ...
        context: loaderContext,
        ...
    }, (err, result) => {
        ...
    });
}

createLoaderContextNormalModule的實例方法锥忿,
它的定義在牛郑,NormalModule.js 第112行

createLoaderContext(resolver, options, compilation, fs) {
    const loaderContext = {
        ...
        _compilation: compilation,
        ...
    };
    ...
    return loaderContext;
}

因此敬鬓,這個_compilation淹朋,就是doBuild參數(shù)中的compiation笙各。
而這個compiler就是在Compiler.js中第497行,觸發(fā)compiler.hooks.make之前新建的那個compilation础芍。

const compilation = this.newCompilation(params);
this.applyPluginsParallel("make", compilation, err => {
   ...
});

3.4 this._compilation.createChildCompiler

我們就可以去Compilation.js 第1416行中查看createChildCompiler方法了杈抢,

createChildCompiler(name, outputOptions, plugins) {
    ...
    return this.compiler.createChildCompiler(this, name, idx, outputOptions, plugins);
}

它調(diào)用了compiler.createChildCompiler,在Compiler.js 第413行者甲,

createChildCompiler(compilation, compilerName, compilerIndex, outputOptions, plugins) {
    const childCompiler = new Compiler();
    ...
    for(const name in this._plugins) {
        if(["make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"].indexOf(name) < 0)
            childCompiler._plugins[name] = this._plugins[name].slice();
    }
    ...
    compilation.applyPlugins("child-compiler", childCompiler, compilerName, compilerIndex);

    return childCompiler;
}

它會新建一個Compiler實例春感,然后把原來父compiler上的_plugins淺拷貝過去。
因此虏缸,以前掛載在compiler上的hooks同樣也會掛載到childCompiler上,
只是嫩实,當hooks被調(diào)用時刽辙,才會觸發(fā)回調(diào)。

其中"make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"甲献,
這些_plugin不拷貝宰缤。

假如我們寫了一個這樣的webpack3插件,
(只需將webpack4中插件的寫法從hooks改成plugin即可)

class Plugin {
    apply(compiler) {
        compiler.plugin('compilation', compilation => {
            compilation.plugin('seal', () => {
                ...
            });
        });
    }
}

則當childCompiler調(diào)用compiler.hooks.compilation時晃洒,
以上為父compiler注冊的事件也會在childCompiler上觸發(fā)慨灭,
唯一不同是參數(shù)compilation不同。

所以接下來球及,compilation.plugin('seal', () => { 氧骤,
就為這個新compilation實現(xiàn)了一個新的hooks.seal

3.5 hooks的多次觸發(fā)

我們來看下實際使用這個插件時的日志信息吃引。

(1)新建plugin.js

const log = require('debug')('debug-webpack plugin.js');

class Plugin {
    apply(compiler) {
        compiler.plugin('compilation', compilation => {
            log('in: compilation');
            compilation.plugin('seal', () => {
                log('in: seal, compilation.entries: %s', compilation.entries.map(({ resource }) => resource).join());
            });
        });
    }
}

module.exports = Plugin;

(2)在webpack.config.js中使用它

...
const Plugin = require('./plugin');

module.exports = {
    ...
    plugins: [
        ...
        new Plugin,
    ]
};

(3)運行一下

$ DEBUG=debug-wepack* npm run build

> debug-webpack3@1.0.0 build ~/Test/debug-webpack3
> webpack

  debug-webpack plugin.js in: compilation +0ms
  debug-webpack plugin.js in: seal, compilation.entries: ~/Test/debug-webpack3/src/index.js +600ms
  debug-webpack plugin.js in: compilation +7ms
  debug-webpack plugin.js in: seal, compilation.entries: ~/Test/debug-webpack3/src/index.less +28ms
Hash: 1b8999f3bb679ecffd56
Version: webpack 3.11.0
Time: 657ms
    Asset      Size  Chunks             Chunk Names
 index.js   2.64 kB       0  [emitted]  index
index.css  29 bytes       0  [emitted]  index
   [0] ./src/index.js 49 bytes {0} [built]
   [1] ./src/index.less 41 bytes {0} [built]
    + 1 hidden module
Child extract-text-webpack-plugin node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist node_modules/_css-loader@0.28.10@css-loader/index.js!node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!src/index.less:
       [0] ./node_modules/_css-loader@0.28.10@css-loader!./node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!./src/index.less 211 bytes {0} [built]
        + 1 hidden module

我們看到compilation.hooks.seal總共觸發(fā)了兩次筹陵,
第一次的entry是~/Test/debug-webpack3/src/index.js,
第二次為~/Test/debug-webpack3/src/index.less镊尺。

第二次 .less 文件觸發(fā)compilation.hooks.seal的流程如下朦佩,
webpack在加載 .less 文件時,使用了extract-text-webpack-plugin庐氮,
每次加載一個 .less 文件语稠,都會創(chuàng)建一個新的 childCompiler

這個childCompiler會把父compiler中所有的hooks都拷貝過去弄砍,
然后就調(diào)用了childCompiler.runAsChild仙畦,它會調(diào)用this.compile,此時thischildCompiler输枯,
然后this.compile中议泵,會觸發(fā)compiler.hooks.compilation這個hooks(見Compiler.js 第465行)。

這個hooks是從父compiler那里拷貝過來的桃熄,
因此就會觸發(fā)我們的插件注冊的那個回調(diào)先口,只是傳入一個新創(chuàng)建的compilation實例作為參數(shù)型奥。

compiler.plugin('compilation', compilation => {
    ...
});

接著為這個新compilation實例,在這個回調(diào)中注冊了compilation.hooks.seal事件碉京。
然后webpack在對 .less 文件 seal的時候厢汹,觸發(fā)hooks.seal事件時,就引發(fā)了這個回調(diào)谐宙。


參考

extract-text-webpack-plugin: Webpack 4 compatibility
webpack v3.11.0
webpack v4.20.2
loader-runner v2.3.1

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末烫葬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子凡蜻,更是在濱河造成了極大的恐慌搭综,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件划栓,死亡現(xiàn)場離奇詭異兑巾,居然都是意外死亡,警方通過查閱死者的電腦和手機忠荞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門蒋歌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人委煤,你說我怎么就攤上這事堂油。” “怎么了碧绞?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵府框,是天一觀的道長。 經(jīng)常有香客問我头遭,道長寓免,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任计维,我火速辦了婚禮袜香,結果婚禮上,老公的妹妹穿的比我還像新娘鲫惶。我一直安慰自己蜈首,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布欠母。 她就那樣靜靜地躺著欢策,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赏淌。 梳的紋絲不亂的頭發(fā)上踩寇,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音六水,去河邊找鬼俺孙。 笑死辣卒,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的睛榄。 我是一名探鬼主播荣茫,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼场靴!你這毒婦竟也來了啡莉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤旨剥,失蹤者是張志新(化名)和其女友劉穎咧欣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泞边,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡该押,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了阵谚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡烟具,死狀恐怖梢什,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情朝聋,我是刑警寧澤嗡午,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站冀痕,受9級特大地震影響荔睹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜言蛇,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一僻他、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腊尚,春花似錦吨拗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至民宿,卻和暖如春娇妓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背活鹰。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工哈恰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留只估,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓蕊蝗,卻偏偏與公主長得像仅乓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蓬戚,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容