随着web应用功能越来越复杂,模块打包后体积越来越大,如何实现静态资源的按需加载,最大程度的减小首页加载模块体积和首屏加载时间,成为模块打包工具的必备核心技能。
webpack作为当下最为流行的模块打包工具,成为了react、vue等众多热门框架的官方推荐打包工具。其提供的Code Splitting(代码分割)特性正是实现模块按需加载的关键方式。
什么是Code Splitting
官方定义:
Code splitting is one of the most compelling features of webpack. It allows you to split your code into various bundles which you can then load on demand — like when a user navigates to a matching route, or on an event from the user. This allows for smaller bundles, and allows you to control resource load prioritization, which if used correctly, can have a major impact on your application load time
翻译过来大概意思就是:
代码分割功能允许将web应用代码分割为多个独立模块,当用户导航到一个匹配的路由或者派发某些特定事件时,来按需加载这些模块。这样就可以将大型的模块分割为多个小型的模块,实现系统资源加载的最大优化,如果使用得当,能够极大的减少我们的应用首屏加载时间。
Code splitting 分类
一、缓存和并行加载的资源分割
这种方法是将某些第三方基础框架模块(例如:moment、loadash)或者多个页面的公用模块(js、css)拆分出来独立打包加载,通常这些模块改动频率很低,将其与业务功能模块拆分出来并行加载,一方面可以最大限度的利用浏览器缓存,另一方面也可以大大降低多页面系统的代码冗余度。
按照资源类型不同又可以分为js公共资源分割
和css资源分割
两类。
1、js公共资源分割
例如:系统应用入口文件index.js
中使用日期功能库 momentjs。
index.js
|
|
webpack.config.js
定义多个entry入口
main
为主入口模块文件vendor
为公共基础库模块,名字可随意设定。称为initial chunk
|
|
执行webpack打包命令:
|
|
可以看到最终打包为两个js文件 main.js
、vendor.js
,但如果检查者两个文件会发现moment
模块代码被重复打包到两个文件中,而这肯定不是我们想要的,这时候就需要 webpack的plugin发挥作用了。
使用CommonsChunkPlugin
|
|
执行webpack打包命令,我们发现moment
只被打包进vendor.js
中。
webpack运行时模块(manifest)
- 在前面的
步骤2
当中webpack在浏览器中加载js模块的运行时代码块也打包进了vendor.js
,如果为打包的js文件添加chunkhash
,则每次修改index.js
后再次编译打包,由于运行时代码需要重新编译生成,导致vendor.js
重新打包并生成新的chunkhash
。
webpack运行时代码块部分:
- 实际项目中我们希望修改业务功能后打包时只重新打包业务模块,而不打包第三方公共基础库。这里我们可以将webpack的
运行时代码
提取到独立的manifest
文件中,这样每次修改业务代码只重新打包生成业务代码模块main.js
和运行时代码模块manifest.js
,就实现了业务模块和公共基础库模块的分离。
names
字段支持以数组格式来指定基础库模块名称
和运行时代码模块名称
。
|
|
2、CSS代码分割
实际项目开发当中经常使用webpack的
css-loader
来将css样式导入到js模块中,再使用style-loader
将css样式以<style>
标签的形式插入到页面当中,但这种方法的缺点就是无法单独加载并缓存css样式文件,页面展现必须依赖于包含css样式的js模块,从而造成页面闪烁的不佳体验。因此有必要将js模块当中import的css模块提取出来,这时候就需要用到
extract-text-webpack-plugin
。
注意webpack2.x需要使用相应版本的plugin。
|
|
index.js
|
|
webpack.config.js
|
|
二、按需加载代码分割
前面介绍的
静态资源分离打包
需要开发者在webpack配置文件中明确分割点来提取独立的公共模块,这种方式适合提取第三方公共基础库
(vue、react、moment等)以及webpack 的运行时代码模块
。除此之外webpack还提供了按需加载的代码分割功能,常用于在web应用路由或者用户行为事件逻辑中动态按需加载特定的功能模块
chunk
,这就是我们本文中后面要重点介绍的。
Code splitting with require.ensure
webpack1提供了CommonJS风格的 require.ensure()
实现模块chunk
的异步加载,通过require.ensure()
在js代码中建立分割点,编译打包时webpack会将此分割点所指定的代码模块都打包为一个代码模块chunk,然后通过jsonp
的方式来按需加载打包后的模块chunk
。
- require.ensure()语法
|
|
Code Splitting with ES2015
webpack2 的ES2015 loader中提供了import()
方法在运行时动态按需加载ES2015 Module
。
webpack将import()
看做一个分割点并将其请求的module打包为一个独立的chunk
。import()
以模块名称作为参数名并且返回一个Promise
对象。
- import() 语法
|
|
import()使用须知
- import()目前还是处于TC39 proposal阶段。
- 在Babel中使用
import()
方法,需要安装 dynamic-import插件并选择使用babel-preset-stage-3
处理解析错误。
动态表达式 Dynamic expressions
import()
中的传参可支持部分表达式的写法了,如果之前有接触过CommonJS中require()
表达式写法,应该不会对此感到陌生。它的操作其实和 CommonJS 类似,给所有可能的文件创建一个环境,当你传递那部分代码的模块还不确定的时候,webpack 会自动生成所有可能的模块,然后根据需求加载。这个特性在前端路由的时候很有用,可以实现按需加载资源。
import()
会针对每一个读取到的module创建独立的chunk
。1234function route(path, query) {return import(`./routes/${path}/route`).then(route => new route.Route(query));}
bundle-loader
bundle-loader 是webpack官方提供的loader
,其作用就是对require.ensure
的抽象封装为一个wrapper
函数来动态加载模块代码,从而避免require.ensure
将分割点所有模块代码打包为一个chunk
体积过大的问题。
- 使用语法:
|
|
- wrapper函数:
默认普通模式wrapper:
|
|
lazy模式wrapper:
|
|
使用
bundle-loader
在代码中require文件的时候只是引入了wrapper
函数,而且因为每个文件都会产生一个分离点,导致产生了多个打包文件,而打包文件的载入只有在条件命中的情况下才产生,也就可以按需加载。
- 支持自定义Chunk名称:
|
|
promise-loader
promise-loader是bundle-loader
的lazy模式的变种,其核心就是使用Promise
语法来替代原先的callback
回调机制。与bundle-loader
类似,require
模块的时候只是引入了wrapper
函数,不同之处在于调用函数时得到的是一个对模块引用的promise
对象,需要在then
方法中获取模块对象,并可以使用catch
方法来捕获模块加载中的错误。
promise-loader
支持使用第三方Promise基础库(如:bluebird)或者使用global
参数来指定使用运行环境已经存在的Promise库。
- 使用语法:
|
|
- wrapper函数:
|
|
es6-promise-loader
es6-promise-loader相比 promise-loader
区别就在于使用原生的ES6 Promise
对象。
- 使用语法:
|
|
- wrapper函数:
|
|
React按需加载实现方案
React router动态路由
react-router
的
- getComponent异步方法
使用语法:
|
|
这些组件会在需要的时候异步加载。这些组件仍然会在同一个文件中,并且你的应用看起来不会有任何不同。
- require.ensure
webpack提供的require.ensure
可以定义分割点来打包独立的chunk,再配合react-router
的getComponent
方法就可以实现React组件的按需加载,具体可参照以下文章:
React懒加载组件
文章前面提到使用React动态路由来按需加载react组件,但实际项目开发中很多时候不需要或者没法引入react-router,我们就可以使用webpack官方提供的React懒加载组件
来动态加载指定的React组件,源代码见 LazilyLoad。
LazilyLoad懒加载组件
- LazilyLoad使用:
webpack2.x import
方法异步加载ES2015模块文件,返回一个Promise对象。
|
|
webpack 1.x 使用前文中提到的promise-loader
或者es6-promise-loader
封装按需加载组件。
|
|
importLazy
方法是为了兼容Babel/ES2015模块,返回模块的default
属性。
|
|
React高阶组件懒加载
高阶组件 (Higher Order Component)就是一个 React 组件包裹着另外一个 React 组件。
可参考React官方文档说明。
a higher-order component is a function that takes a component and returns a new component
这种模式通常使用工厂函数
来实现。
封装懒加载组件LazilyLoad的高阶组件工厂函数
LazilyLoadFactory
|
|
使用高阶组件实现按需加载
webpack 2.x
|
|
webpack 1.x
|
|
ES Decorator
除了工厂函数方式扩展实现高阶组件,还可通过 ES草案中的 Decorator(https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841) 语法来实现。Decorator 可以通过返回特定的 descriptor 来”修饰” 类属性,也可以直接”修饰”一个类。即传入一个已有的类,通过 Decorator 函数”修饰”成了一个新的类。
- 使用方法:
|
|
引用被高阶组件包裹的普通组件
|
|
基于webpack 1.x实现react组件的懒加载示例
参考资料
- 基于Webpack 2的React组件懒加载
- react-router动态路由与Webpack分片thunks
- Lazy Loading - React
- es6-promise-loader
- promise-loader
- bundle-loader
- es6-modules-overview
- Implicit Code Splitting and Chunk Loading with React Router and Webpack
- 在Webpack中使用Code Splitting实现按需加载
- Code Splitting - Using require.ensure
- Code Splitting with ES2015
- 深入理解 React 高阶组件
- ES7 Decorator 装饰者模式
- ES Decorators简介
- Higher-Order Components