作者
写在前面
大家好,我是LINE漫画的JavaScript开发工程师@sunderls。
这是LINE Advent Calendar 2017的第22日的文章。今天我介绍一个webpack loader – grow-loader。 之前在LINE漫画:通过Page Stack实现流畅的页面切换这篇文章中介绍过,LINE应用内部的漫画APP使用的是web技术。
为了提供接近于原生APP的体验,我们进行了很多尝试。今天介绍下我们是如何实现code-splitting的。
为什么要做code-splitting
LINE漫画之前一直将JS打包为一个bundle文件,随着应用变得越来越复杂,页面越来越多,bundle文件变得越来越大。考虑到未来可能还会更复杂,打包到一个文件这种方式变得很不合适。所以就开始了分割。
一般的实现
最开始我们也尝试了react-loadable之类的HOC(Higher Order Component)方法,不过遇到了一些问题:
1. 页面切换动画效果的即时性消失
原因很清楚。用户的操作之后才开始加载下一个页面,所以这一段加载时间会让人感觉延迟。对于LINE漫画而言这是致命的。
2. preload可以尽心一定程度的改善,但是长页面的话preload也无能为力
用户的点击操作发生之前,提前加载(preload)可能跳转到的页面的话,可以避免用户能感觉到的加载时间。但是,LINE漫画应用中页面非常多,每一个页面都去添加preloa代码非常麻烦难以管理,而且提前加载一些用户根本没有点击的页面也是某种低效率的体现。
另外如果页面比较长的话,DOM生成时间比较明显,提前加载也不能解决这个问题。
3. 可以采用统一的加载图标(Loading Indicator)来保证即时性,不过LINE漫画应用不好利用
LINE漫画几乎每个页面都有各自的Placeholder Component,所以使用统一的加载图标将是某种用户体验上的倒退。Placeholder Component的实现上采用的是模拟数据,而不是HOC的方式所以也不能很简单的分割。
4. 自定义componnet hook没法很简单的支持
为了实现用户点击的瞬时反馈,LINE漫画采用了Page Stack的页面组织方式,提供了componentHide的自定义hook。如果用HOC的方式进行代码分割的话,为了保证这些hook,Router等很多部分都需要修改。尝试了过后发现改动太大就放弃了。
总而言之,LINE漫画非常非常重视体验上的流畅。
LINE漫画的解决办法
分析了以上问题,我们可以得到如下结论:
- 每个页面的placeholder如果能够保留在main bundle中的话,流畅性不会下降
- 每个页面的placeholder以外的部分能够分隔开的话,较长的页面也会因为两次render,初回显示也会变得更快
- 手动进行文件分割感觉不太可行,那就用一个loader自动分割好了。
于是乎grow-loader诞生了。
grow-loader简介
通过grow-loader,只需要在需要分割的方法上添加decorator @grow
即可。
以下是例:
class SampleClass {
@grow
methodToGrow() {
// ...
}
@grow
methodToGrowAndBind = () => {
// ...
}
methodToBeBundled(){
}
}
使用了grow-loader的话,以上的class会被如下处理:
- 被
@grow
标记的两个方法(methodToGrow
和methodToGrowAndBind
)会被分割到另外一个文件 - 新方法
grow()
会被添加到class中。调用该方法后,分割出去的两个方法会被dynamic import回来。
以下是使用例:
const sample = new SampleClass();
console.assert(a.methodToGrow === undefined);
console.assert(a.methodToGrowAndBind === undefined);
sample.grow().then(() => {
sample.methodToGrow();
sample.methodToGrowAndBind();
});
React中的使用方法
以上例子作为基础,React Component环境下的代码分割也可以简单的实现。
比如,有如下组件
export default class LongPage extends React.Component {
methodToGrow() {
// ..
}
methodToGrowAndBind = () => {
// ..
}
methodToBeBundled(){
// ...
}
render(){
return <div>
this is a very long page
</div>
}
}
首先,做一个公用的base组件:
class GrowablePage extends React.Component {
// componentDidMountの時点でロードを始める。
componentDidMount() {
if (this.grow) {
this.grow().then(() => {
this.hasGrown = true;
this.forceUpdate();
});
}
}
}
然后将render()
进行适当的分割,然后在适合动态加载的方法上用@grow
标记,比如这样:
export default class LongPage extends GrowablePage {
@grow
methodToGrow() {
// ...
}
@grow
methodToGrowAndBind = () => {
// ...
}
methodToBeBundled(){
// ...
}
@grow
renderMore() {
return <div>
this is below the first view
</div>
}
render(){
return <div>
this is basic part
{ this.hasGrown ? this.renderMore() : null}
</div>
}
}
grow-loader使用后的效果
grow-loader只是进行了代码变化而已,实际能够让bundle size减少多少取决于使用方法。从上面的粒子上可以看出,render()
的内容被分割的越多,@grow()
使用的越多,分割效率就会越高。
LINE漫画使用grow-loader后,entry bundle文件size下降了15%。这没有一般的HOC分割效果效果,但是由于页面的两次渲染,我们的首页的mount时间(constructor()
到componentDidMount()
的时间下降了40%,加上使用方法上的简单和灵活,我们觉得可以接受。
写在最后
grow-loader已经在GitHub上公开,有兴趣的同学还请尝试一下,如果有bug或者更好的建议,可以直接创建issue告诉我们。
转自 http://www.infoq.com/cn/news/2018/01/use-webpack-code-splitting