フロントエンド プロジェクトが拡大し続けると、元々は単純な Web アプリケーションによって参照される js ファイルがますます大きくなる可能性があります。特に最近の人気のシングルページ アプリケーションでは、一部のパッケージング ツール (webpack など) への依存が高まっており、これらのパッケージ化ツールを通じて、処理が必要で相互に依存するモジュールが別のバンドル ファイルに直接パッケージ化されます。ページが最初にロードされるときにロードされ、すべての js がロードされます。ただし、単一ページ アプリケーションのすべての依存関係を一度にダウンロードする必要がないシナリオも多くあります。例: 現在、権限を持つ単一ページの「注文管理」アプリケーションがあり、通常の管理者は「注文管理」セクションにのみ入ることができますが、スーパー ユーザーは「システム管理」を実行できます。ページ アプリケーションの場合、ユーザーは初めてページを開いたときに、無関係なリソースを読み込むために長時間待つ必要があります。このような場合、特定のコード分割を実行することを検討できます。
実装方法
シンプルなオンデマンド読み込み
コード分割の主な目的は、リソースのオンデマンド読み込みを実現することです。このシナリオを考えてみましょう。Web サイトの右下隅にチャット ボックスに似たコンポーネントがあり、円形のボタンをクリックすると、チャット コンポーネントがページに表示されます。
btn.addEventListener('click', function(e) { // 在这里加载chat组件相关资源 chat.js });
この例から、chat.js の読み込み操作を btn click イベントにバインドすることで、チャット ボタンをクリックした後のチャット コンポーネントのオンデマンド読み込みが実現できることがわかります。 js リソースを動的にロードする方法も非常に簡単です (おなじみの jsonp と同様)。
btn.addEventListener('click', function(e) { // 在这里加载chat组件相关资源 chat.js var ele = document.createElement('script'); ele.setAttribute('src','/static/chat.js'); document.getElementsByTagName('head')[0].appendChild(ele); });
コード分割は、オンデマンド読み込みを実現するために行われる作業です。パッケージ化ツールを使用してすべての js をbundle.js ファイルにパッケージ化すると想像してください。この場合、上記のオンデマンド読み込みを実現する方法はありません。したがって、オンデマンド読み込みコードについて説明する必要があります。 in パッケージ化プロセス (コード分割) 中に分割します。では、これらのリソースを手動で分割する必要があるのでしょうか?もちろんそうではありません。それでもパッケージ化ツールを使用する必要があります。次に、Webpack でのコード分割を紹介します。
コード分割
ここでアプリケーションのシナリオに戻り、Webpack でコード分割を実行する方法を紹介します。 Webpack のビルドでコード分割を実装するには、複数の方法があります。
import()
ここでのインポートは、モジュール導入時のインポートとは異なり、動的にロードされるモジュールの関数のようなものとして理解でき、渡されるパラメータは対応するモジュールです。たとえば、「react」からの元のモジュールインポート反応は、import(「react」) と書くことができます。ただし、import() は Promise オブジェクトを返すことに注意してください。したがって、次のように使用できます:
btn.addEventListener('click', e => { // 在这里加载chat组件相关资源 chat.js import('/components/chart').then(mod => { someOperate(mod); }); });
ご覧のとおり、使用方法は非常に簡単で、私たちが普段使用しているPromiseと何ら変わりません。もちろん、いくつかの例外処理を追加することもできます:
btn.addEventListener('click', e => { import('/components/chart').then(mod => { someOperate(mod); }).catch(err => { console.log('failed'); }); });
もちろん、import() は Promise オブジェクトを返すため、いくつかの互換性の問題に注意する必要があります。この問題を解決するのは難しくありません。いくつかの Promise ポリフィルを使用して互換性を実現できます。動的 import() メソッドは、セマンティクスと構文の点で比較的明確かつ簡潔であることがわかります。
require.ensure()
webpack 2 の公式 Web サイトに次の文を書きました:
require.ensure() は webpack に固有であり、import() によって置き換えられます。
したがって、webpack 2 ではrequire.ensure() メソッドの使用はお勧めできません。ただし、この方法は現在でも有効ですので、簡単に紹介します。 Webpack 1 に含まれている場合にも使用できます。 require.ensure() の構文は次のとおりです。
require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)
require.ensure() は 3 つのパラメーターを受け入れます。
最初のパラメーターの依存関係は、現在必要なモジュールのいくつかの依存関係を表す配列です。 2 番目のパラメータ callback はコールバック関数です。注意する必要があるのは、このコールバック関数にはパラメータ require があり、これを通じて他のモジュールをコールバック関数内に動的に導入できるということです。この require はコールバック関数のパラメータですが、理論上は別の名前に変更できますが、実際には変更できません。そうしないと、静的解析中に webpack が処理できなくなることに注意してください。
3 番目のパラメータ errorCallback の比較btn.addEventListener('click', e => { require.ensure([], require => { let chat = require('/components/chart'); someOperate(chat); }, error => { console.log('failed'); }, 'mychat'); });
上記 2 つの方法の使用に加えて、webpack の一部のコンポーネントを使用することもできます。たとえば、バンドル ローダーを使用します:
npm i --save bundle-loader
let chatChunk = require("bundle-loader?lazy!./components/chat"); chatChunk(function(file) { someOperate(file); });
和其他loader类似,Bundle Loader也需要在webpack的配置文件中进行相应配置。Bundle-Loader的代码也很简短,如果阅读一下可以发现,其实际上也是使用require.ensure()来实现的,通过给Bundle-Loader返回的函数中传入相应的模块处理回调函数即可在require.ensure()的中处理,代码最后也列出了相应的输出格式:
/* 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 中的代码拆分
最后,回到实际的工作中,基于webpack,在react-router4中实现代码拆分。react-router 4相较于react-router 3有了较大的变动。其中,在代码拆分方面,react-router 4的使用方式也与react-router 3有了较大的差别。
在react-router 3中,可以使用Route组件中getComponent这个API来进行代码拆分。getComponent是异步的,只有在路由匹配时才会调用。但是,在react-router 4中并没有找到这个API,那么如何来进行代码拆分呢?
在react-router 4官网上有一个代码拆分的例子。其中,应用了Bundle Loader来进行按需加载与动态引入
import loadSomething from 'bundle-loader?lazy!./Something'
然而,在项目中使用类似的方式后,出现了这样的警告:
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中已经不能使用import这样的方式来引入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.
我的应用使用了create-react-app作为脚手架,屏蔽了webpack的一些配置。当然,也可以通过运行npm run eject使其暴露webpack等配置文件。然而,是否可以用其他方法呢?当然。
这里就可以使用之前说到的两种方式来处理:import()或require.ensure()。
和官方实例类似,我们首先需要一个异步加载的包装组件Bundle。Bundle的主要功能就是接收一个组件异步加载的方法,并返回相应的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; } }
在原有的例子中,通过Bundle Loader来引入模块:
import loadSomething from 'bundle-loader?lazy!./About' const About = (props) => ( <Bundle load={loadAbout}> {(About) => <About {...props}/>} </Bundle> )
由于不再使用Bundle Loader,我们可以使用import()对该段代码进行改写:
const Chat = (props) => ( <Bundle load={() => import('./component/chat')}> {(Chat) => <Chat {...props}/>} </Bundle> );
需要注意的是,由于import()会返回一个Promise对象,因此Bundle组件中的代码也需要相应进行调整
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对象; mod.default导出默认 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}/>
这时候,执行npm run start,可以看到在载入最初的页面时加载的资源如下
而当点击触发到/chat路径时,可以看到
动态加载了2.chunk.js这个js文件,如果打开这个文件查看,就可以发现这个就是我们刚才动态import()进来的模块。
当然,除了使用import()仍然可以使用require.ensure()来进行模块的异步加载。相关示例代码如下:
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的话,也可以进行如下配置
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', },
结束
代码拆分在单页应用中非常常见,对于提高单页应用的性能与体验具有一定的帮助。我们通过将第一次访问应用时,并不需要的模块拆分出来,通过scipt标签动态加载的原理,可以实现有效的代码拆分。在实际项目中,使用webpack中的import()、require.ensure()或者一些loader(例如Bundle Loader)来做代码拆分与组件按需加载。
以上がReact-router4でのコード分割の方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。