在react-router4中進(jìn)行代碼拆分(基于webpack)

前言

隨著前端項(xiàng)目的不斷擴(kuò)大,一個(gè)原本簡(jiǎn)單的網(wǎng)頁(yè)應(yīng)用所引用的js文件可能變得越來(lái)越龐大。尤其在近期流行的單頁(yè)面應(yīng)用中,越來(lái)越依賴一些打包工具(例如webpack),通過(guò)這些打包工具將需要處理、相互依賴的模塊直接打包成一個(gè)單獨(dú)的bundle文件,在頁(yè)面第一次載入時(shí),就會(huì)將所有的js全部載入。但是,往往有許多的場(chǎng)景,我們并不需要在一次性將單頁(yè)應(yīng)用的全部依賴都載下來(lái)。例如:我們現(xiàn)在有一個(gè)帶有權(quán)限的"訂單后臺(tái)管理"單頁(yè)應(yīng)用,普通管理員只能進(jìn)入"訂單管理"部分,而超級(jí)用戶則可以進(jìn)行"系統(tǒng)管理";或者,我們有一個(gè)龐大的單頁(yè)應(yīng)用,用戶在第一次打開(kāi)頁(yè)面時(shí),需要等待較長(zhǎng)時(shí)間加載無(wú)關(guān)資源。這些時(shí)候,我們就可以考慮進(jìn)行一定的代碼拆分(code splitting)。

實(shí)現(xiàn)方式

簡(jiǎn)單的按需加載

代碼拆分的核心目的,就是實(shí)現(xiàn)資源的按需加載。考慮這么一個(gè)場(chǎng)景,在我們的網(wǎng)站中,右下角有一個(gè)類似聊天框的組件,當(dāng)我們點(diǎn)擊圓形按鈕時(shí),頁(yè)面展示聊天組件。

btn.addEventListener('click', function(e) {
    // 在這里加載chat組件相關(guān)資源 chat.js
});

從這個(gè)例子中我們可以看出,通過(guò)將加載chat.js的操作綁定在btn點(diǎn)擊事件上,可以實(shí)現(xiàn)點(diǎn)擊聊天按鈕后聊天組件的按需加載。而要?jiǎng)討B(tài)加載js資源的方式也非常簡(jiǎn)單(方式類似熟悉的jsonp)。通過(guò)動(dòng)態(tài)在頁(yè)面中添加<scrpt>標(biāo)簽,并將src屬性指向該資源即可。

btn.addEventListener('click', function(e) {
    // 在這里加載chat組件相關(guān)資源 chat.js
    var ele = document.createElement('script');
    ele.setAttribute('src','/static/chat.js');
    document.getElementsByTagName('head')[0].appendChild(ele);
});

代碼拆分就是為了要實(shí)現(xiàn)按需加載所做的工作。想象一下,我們使用打包工具,將所有的js全部打包到了bundle.js這個(gè)文件,這種情況下是沒(méi)有辦法做到上面所述的按需加載的,因此,我們需要講按需加載的代碼在打包的過(guò)程中拆分出來(lái),這就是代碼拆分。那么,對(duì)于這些資源,我們需要手動(dòng)拆分么?當(dāng)然不是,還是要借助打包工具。下面就來(lái)介紹webpack中的代碼拆分。

代碼拆分

這里回到應(yīng)用場(chǎng)景,介紹如何在webpack中進(jìn)行代碼拆分。在webpack有多種方式來(lái)實(shí)現(xiàn)構(gòu)建是的代碼拆分。

import()

這里的import不同于模塊引入時(shí)的import,可以理解為一個(gè)動(dòng)態(tài)加載的模塊的函數(shù)(function-like),傳入其中的參數(shù)就是相應(yīng)的模塊。例如對(duì)于原有的模塊引入import react from 'react'可以寫為import('react')。但是需要注意的是,import()會(huì)返回一個(gè)Promise對(duì)象。因此,可以通過(guò)如下方式使用:

btn.addEventListener('click', e => {
    // 在這里加載chat組件相關(guān)資源 chat.js
    import('/components/chart').then(mod => {
        someOperate(mod);
    });
});

可以看到,使用方式非常簡(jiǎn)單,和平時(shí)我們使用的Promise并沒(méi)有區(qū)別。當(dāng)然,也可以再加入一些異常處理:

btn.addEventListener('click', e => {
    import('/components/chart').then(mod => {
        someOperate(mod);
    }).catch(err => {
        console.log('failed');
    });
});

當(dāng)然,由于import()會(huì)返回一個(gè)Promise對(duì)象,因此要注意一些兼容性問(wèn)題。解決這個(gè)問(wèn)題也不困難,可以使用一些Promise的polyfill來(lái)實(shí)現(xiàn)兼容。可以看到,動(dòng)態(tài)import()的方式不論在語(yǔ)意上還是語(yǔ)法使用上都是比較清晰簡(jiǎn)潔的。

require.ensure()

在webpack 2的官網(wǎng)上寫了這么一句話:

require.ensure() is specific to webpack and superseded by import().

所以,在webpack 2里面應(yīng)該是不建議使用require.ensure()這個(gè)方法的。但是目前該方法仍然有效,所以可以簡(jiǎn)單介紹一下。包括在webpack 1中也是可以使用。下面是require.ensure()的語(yǔ)法:

require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)

require.ensure()接受三個(gè)參數(shù):

  • 第一個(gè)參數(shù)dependencies是一個(gè)數(shù)組,代表了當(dāng)前require進(jìn)來(lái)的模塊的一些依賴;
  • 第二個(gè)參數(shù)callback就是一個(gè)回調(diào)函數(shù)。其中需要注意的是,這個(gè)回調(diào)函數(shù)有一個(gè)參數(shù)require,通過(guò)這個(gè)require就可以在回調(diào)函數(shù)內(nèi)動(dòng)態(tài)引入其他模塊。值得注意的是,雖然這個(gè)require是回調(diào)函數(shù)的參數(shù),理論上可以換其他名稱,但是實(shí)際上是不能換的,否則webpack就無(wú)法靜態(tài)分析的時(shí)候處理它;
  • 第三個(gè)參數(shù)errorCallback比較好理解,就是處理error的回調(diào);
  • 第四個(gè)參數(shù)chunkName則是指定打包的chunk名稱。

因此,require.ensure()具體的用法如下:

btn.addEventListener('click', e => {
    require.ensure([], require => {
        let chat = require('/components/chart');
        someOperate(chat);
    }, error => {
        console.log('failed');
    }, 'mychat');
});

Bundle Loader

除了使用上述兩種方法,還可以使用webpack的一些組件。例如使用Bundle Loader

npm i --save bundle-loader

使用require("bundle-loader!./file.js")來(lái)進(jìn)行相應(yīng)chunk的加載。該方法會(huì)返回一個(gè)function,這個(gè)function接受一個(gè)回調(diào)函數(shù)作為參數(shù)。

let chatChunk = require("bundle-loader?lazy!./components/chat");
chatChunk(function(file) {
    someOperate(file);
});

和其他loader類似,Bundle Loader也需要在webpack的配置文件中進(jìn)行相應(yīng)配置。Bundle-Loader的代碼也很簡(jiǎn)短,如果閱讀一下可以發(fā)現(xiàn),其實(shí)際上也是使用require.ensure()來(lái)實(shí)現(xiàn)的,通過(guò)給Bundle-Loader返回的函數(shù)中傳入相應(yīng)的模塊處理回調(diào)函數(shù)即可在require.ensure()的中處理,代碼最后也列出了相應(yīng)的輸出格式:

/*
Output format:
    var cbs = [],
        data;
    module.exports = function(cb) {
        if(cbs) cbs.push(cb);
            else cb(data);
    }
    require.ensure([], function(require) {
        data = require("xxx");
        var callbacks = cbs;
        cbs = null;
        for(var i = 0, l = callbacks.length; i < l; i++) {
            callbacks[i](data);
        }
    });
*/

react-router v4 中的代碼拆分

最后,回到實(shí)際的工作中,基于webpack,在react-router4中實(shí)現(xiàn)代碼拆分。react-router 4相較于react-router 3有了較大的變動(dòng)。其中,在代碼拆分方面,react-router 4的使用方式也與react-router 3有了較大的差別。
在react-router 3中,可以使用Route組件中getComponent這個(gè)API來(lái)進(jìn)行代碼拆分。getComponent是異步的,只有在路由匹配時(shí)才會(huì)調(diào)用。但是,在react-router 4中并沒(méi)有找到這個(gè)API,那么如何來(lái)進(jìn)行代碼拆分呢?
react-router 4官網(wǎng)上有一個(gè)代碼拆分的例子。其中,應(yīng)用了Bundle Loader來(lái)進(jìn)行按需加載與動(dòng)態(tài)引入

import loadSomething from 'bundle-loader?lazy!./Something'

然而,在項(xiàng)目中使用類似的方式后,出現(xiàn)了這樣的警告:

Unexpected '!' in 'bundle-loader?lazy!./component/chat'. Do not use import syntax to configure webpack loaders import/no-webpack-loader-syntax
Search for the keywords to learn more about each error.

在webpack 2中已經(jīng)不能使用import這樣的方式來(lái)引入loader了(no-webpack-loader-syntax

Webpack allows specifying the loaders to use in the import source string using a special syntax like this:

var moduleWithOneLoader = require("my-loader!./my-awesome-module");

This syntax is non-standard, so it couples the code to Webpack. The recommended way to specify Webpack loader configuration is in a Webpack configuration file.

我的應(yīng)用使用了create-react-app作為腳手架,屏蔽了webpack的一些配置。當(dāng)然,也可以通過(guò)運(yùn)行npm run eject使其暴露webpack等配置文件。然而,是否可以用其他方法呢?當(dāng)然。
這里就可以使用之前說(shuō)到的兩種方式來(lái)處理:import()require.ensure()
和官方實(shí)例類似,我們首先需要一個(gè)異步加載的包裝組件Bundle。Bundle的主要功能就是接收一個(gè)組件異步加載的方法,并返回相應(yīng)的react組件:

export default class Bundle extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mod: null
        };
    }

    componentWillMount() {
        this.load(this.props)
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps)
        }
    }

    load(props) {
        this.setState({
            mod: null
        });
        props.load((mod) => {
            this.setState({
                mod: mod.default ? mod.default : mod
            });
        });
    }

    render() {
        return this.state.mod ? this.props.children(this.state.mod) : null;
    }
}

在原有的例子中,通過(guò)Bundle Loader來(lái)引入模塊:

import loadSomething from 'bundle-loader?lazy!./About'

const About = (props) => (
    <Bundle load={loadAbout}>
        {(About) => <About {...props}/>}
    </Bundle>
)

由于不再使用Bundle Loader,我們可以使用import()對(duì)該段代碼進(jìn)行改寫:

const Chat = (props) => (
    <Bundle load={() => import('./component/chat')}>
        {(Chat) => <Chat {...props}/>}
    </Bundle>
);

需要注意的是,由于import()會(huì)返回一個(gè)Promise對(duì)象,因此Bundle組件中的代碼也需要相應(yīng)進(jìn)行調(diào)整

export default class Bundle extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mod: null
        };
    }

    componentWillMount() {
        this.load(this.props)
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps)
        }
    }

    load(props) {
        this.setState({
            mod: null
        });
        //注意這里,使用Promise對(duì)象; mod.default導(dǎo)出默認(rèn)
        props.load().then((mod) => {
            this.setState({
                mod: mod.default ? mod.default : mod
            });
        });
    }

    render() {
        return this.state.mod ? this.props.children(this.state.mod) : null;
    }
}

路由部分沒(méi)有變化

<Route path="/chat" component={Chat}/>

這時(shí)候,執(zhí)行npm run start,可以看到在載入最初的頁(yè)面時(shí)加載的資源如下

這里寫圖片描述

而當(dāng)點(diǎn)擊觸發(fā)到/chat路徑時(shí),可以看到

這里寫圖片描述

動(dòng)態(tài)加載了2.chunk.js這個(gè)js文件,如果打開(kāi)這個(gè)文件查看,就可以發(fā)現(xiàn)這個(gè)就是我們剛才動(dòng)態(tài)import()進(jìn)來(lái)的模塊。
當(dāng)然,除了使用import()仍然可以使用require.ensure()來(lái)進(jìn)行模塊的異步加載。相關(guān)示例代碼如下:

const Chat = (props) => (
    <Bundle load={(cb) => {
        require.ensure([], require => {
            cb(require('./component/chat'));
        });
    }}>
    {(Chat) => <Chat {...props}/>}
  </Bundle>
);
export default class Bundle extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mod: null
        };
    }

    load = props => {
        this.setState({
            mod: null
        });
        props.load(mod => {
            this.setState({
                mod: mod ? mod : null
            });
        });
    }

    componentWillMount() {
        this.load(this.props);
    }

    render() {
        return this.state.mod ? this.props.children(this.state.mod) : null
    }
}

此外,如果是直接使用webpack config的話,也可以進(jìn)行如下配置

output: {
    // The build folder.
    path: paths.appBuild,
    // There will be one main bundle, and one file per asynchronous chunk.
    filename: 'static/js/[name].[chunkhash:8].js',
    chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
  },

結(jié)束

代碼拆分在單頁(yè)應(yīng)用中非常常見(jiàn),對(duì)于提高單頁(yè)應(yīng)用的性能與體驗(yàn)具有一定的幫助。我們通過(guò)將第一次訪問(wèn)應(yīng)用時(shí),并不需要的模塊拆分出來(lái),通過(guò)scipt標(biāo)簽動(dòng)態(tài)加載的原理,可以實(shí)現(xiàn)有效的代碼拆分。在實(shí)際項(xiàng)目中,使用webpack中的import()require.ensure()或者一些loader(例如Bundle Loader)來(lái)做代碼拆分與組件按需加載。

http://blog.csdn.net/foralienzhou/article/details/73437057

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,348評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,083評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,706評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,442評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,802評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,983評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,542評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,287評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,486評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,710評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,116評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,412評(píng)論 1 294
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,224評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,462評(píng)論 2 378

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

  • GitChat技術(shù)雜談 前言 本文較長(zhǎng),為了節(jié)省你的閱讀時(shí)間,在文前列寫作思路如下: 什么是 webpack,它要...
    蕭玄辭閱讀 12,708評(píng)論 7 110
  • 無(wú)意中看到zhangwnag大佬分享的webpack教程感覺(jué)受益匪淺,特此分享以備自己日后查看,也希望更多的人看到...
    小小字符閱讀 8,218評(píng)論 7 35
  • 作者:小 boy (滬江前端開(kāi)發(fā)工程師)本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明作者及出處。原文地址:https://www.smas...
    iKcamp閱讀 2,768評(píng)論 0 18
  • 學(xué)習(xí)流程 參考文檔:入門Webpack,看這篇就夠了Webpack for React 一. 簡(jiǎn)單使用webpac...
    Jason_Zeng閱讀 3,166評(píng)論 2 16
  • 幸福雙翼《家學(xué)》是一門關(guān)于如何獲得個(gè)人及家庭幸福能力的課程,學(xué)期兩年,我剛學(xué)習(xí)了幾節(jié)課,很是受益,感謝黃志猛老師,...
    舜間永恒閱讀 1,016評(píng)論 0 2