前言
隨著前端項(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)用脆淹,用戶在第一次打開頁(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è)文件惊畏,這種情況下是沒有辦法做到上面所述的按需加載的,因此密任,我們需要講按需加載的代碼在打包的過(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
并沒有區(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
中并沒有找到這個(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;
}
}
路由部分沒有變化
<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文件,如果打開這個(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)用中非常常見谍憔,對(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)做代碼拆分與組件按需加載。