皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

利用Webpack插件进行前端code-splitting

作者 sunderls

写在前面

大家好,我是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漫画的解决办法

分析了以上问题,我们可以得到如下结论:

  1. 每个页面的placeholder如果能够保留在main bundle中的话,流畅性不会下降
  2. 每个页面的placeholder以外的部分能够分隔开的话,较长的页面也会因为两次render,初回显示也会变得更快
  3. 手动进行文件分割感觉不太可行,那就用一个loader自动分割好了。

于是乎grow-loader诞生了。

grow-loader简介

通过grow-loader,只需要在需要分割的方法上添加decorator @grow即可。

以下是例:

class SampleClass {

    @grow
    methodToGrow() {
        // ...
    }

    @grow
    methodToGrowAndBind = () => {
        // ...
    }

    methodToBeBundled(){

    }
}

使用了grow-loader的话,以上的class会被如下处理:

  1. @grow标记的两个方法(methodToGrowmethodToGrowAndBind)会被分割到另外一个文件
  2. 新方法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