前言
《React移动web极致优化》也提到了,构建工具是前端优化的重要一环。而React的推荐构建工具则是Webpack。这篇文章我们就来聊聊如何在Webpack构建的过程中如何针对React的应用做一些优化。
如果还没看过《webpack使用优化(基本篇)》这篇文章,建议去看看,因为针对React的优化往往也离不开Webpack那些最基本的优化点。此外,在这里将Webpack视作构建可能招来一些人的反对,他们会将Webpack定位成打包的工具。但实际项目中,除了合图以外,家校群项目已将Webpack将为最核心的构建工具。
本篇文章的成果最终都会转化成一个开源的boilerplate,steamer-react。
目录结构
@H_403_13@
构建工具离不开目录的设计,我们需要安排号文件存放的位置才便于构建工程的开展。在src目录下一级的文件,除了page文件夹是react的主体逻辑文件之外,其它的像img,js,css,libs,都属于各个页面都会用到的公共文件,如utils,上报等。
page目录下,common文件夹主要旋转跟React相关的一些公共的文件,如公共的component,中间件等。而其它的文件夹就是每个页面的主体逻辑和资源,另外就是页面对应的html文件。
由于家校群采用的是React+ Redux这套方案,我们文件夹的名字也很能体现这套方案的特色。首先组件Component方面,我们比较认同Redux推崇的smart component(container)和dump component,因此它们分别放置在container和component文件夹下。那container和component文件夹下面放在什么呢?我们放置了组件相关的逻辑js和样式scss文件。我们暂时没将图片放在组件这一层,而是放在页面这一层,是因为我们业务不同组件间共用了不少图片,因此放在更上一层更为合适。
而store,reducer,action,connect都是跟Redux这个数据处理框架相关。像root这样的文件夹则是项目的主入口,里面有root.dev.js和root.prod.js,用于区分开发环境与生产环境对应需要引入的组件。
如果你还用到React-Router,可能你还需要多加一个route的文件夹,里面用存放项目route的配置文件。
这套文件架构比较传统的gulp和grunt复杂,但却更符合React + Redux这套方案的开发思路。
针对React的优化点
需要维护两套构建配置
Webpack跟Gulp和Grunt不同,前者属于配置型构建(当然也可以通过插件去做一些流程),后两者属于任务型的构建。以前在用Gulp开发的时候,也会写一些任务专门针对开发或者生产的环境,分别再建两条任务流,分别去处理开发与生产环境的构建。同理Webpack也需要去处理开发与生产环境的构建,因此也需要两套配置去实现。
如果搞不清楚什么任务应该放在开发环境,什么应该放在生产环境,可以参考《性能优化三部曲之一——构建篇》,里有有详情参考;如果不知道如何去区分开发与生产环境,可以参考《webpack使用优化(基本篇)》(https://github.com/lcxfs1991/blog/issues/2)。
这里想提出来说的有2点:
第一,是建议开发环境的配置是生产环境的子集。
这样顺着写下来比较流畅,毕竟我们是先考虑开发,等开发完之后才会去构思生产环境的部署。这时我们可以直接用Object.assign去复制开发环境写好的配置,进行修改便是。也可以添加一些方法方便处理更新配置,例如生产环境想去添加新的插件,我参考了之前看过的一个boilerplate:
devConfig.addPlugins = function(plugin,opt) { devConfig.plugins.push(new plugin(opt)); };
第二,是React是否使用外链问题。在开发环境下,建议直接引入node_modules包的,因为里面有许多有用的报错和提示,方便开发时快速发现问题。而生产环境自然是建议外链,否则Webpack就会自作主线地把React和你的业务逻辑打包到一起,比分开打包要大得多。
React的ES2015编译
ES2015近2年很火热,我们也来尝尝鲜。用ES2015的最大好处就是可以使用许多方便的特性,但有一个小小的坏处就是,你可能忽略ES5的写法,而ES5的写法很多时候能够清楚地表示出React的实现方式,对理解框架和原理更有帮助。
另外就是,用这些新的特性,会有一些不稳定的因素,就是不知道转换之后会成什么样子,转换后的代码兼容性如何(具体可参《babel到底将代码转换成什么鸟样?》])。果不其然,我们发布列表页之后发现一个报错:
Uncaught TypeError: Cannot assign to read only property '__esModule' of #<Object>。
解决办法,就是babel编译使用ES2015-Loose而不是ES2015的preset。具体转换的代码如下:
@H_403_13@
具体在Webpack的loaders里可以这样写你的编译配置
{ test: /\.js?$/,loader: 'babel',query: { plugins: ['transform-decorators-legacy'],/// 使用decorator写法 presets: [ 'es2015-loose',// ES2015 loose mode 'react',] },exclude: /node_modules/,}
除此之外,我们也发现每个Webpack打包的模块,最终编译都会生成一堆类似的代码:
function _interoprequiredefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance,Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } ......
这些是Babel对ES2015转义生成的代码片段。多的时候,每个模块生成的这些代码都接近1kb。如果你的component多,例如像家校群有超过20组件,后面的详情页的组件可能更多,这么多组件合起来重复的代码可能就近20kb。在PC端20kb没有所谓,但到移动端却对bundle的大小锱铢必较。建议写一个Webpack plugin/loader对这些代码进行去重(反正没看到Webpack有插件,机会就留给你们了)。
如何热替换css
打包css的时候,我们习惯使用ExtractTextPlugin让css单独生成一个文件。但如果你想让css也能够热替换,在开发环境的时候请去掉这个插件让样式内联。
Webpack慎用devtools的inline-source-map模式
使用此模式会内联一大段便于定位bug的字符串,查错时可以开启,不是查错时建议
关闭,否则开发时加载的包会非常大。
如果不使用devtools查错,你看到的会是合成之后的bundle,上万行代码,也不知道是哪个文件:
@H_403_13@。
使用了之后,你就很很清晰是在哪个文件,哪一行了:
@H_403_13@
如果无法使用服务器构建,开发时请让小伙伴统一开发路径
webpack的bug导致如果本地开发目录路径不一致,编译出来的md5会不一致。所以推荐使用服务器构建。
React项目的合图
在搭项目构建的时候,曾经尝试过用Webpack一个合图插件,但因不够成熟而弃用,转而考虑转投向gulp的合图插件的怀抱。由于家校群功能页面是一个多页项目,每个页面都会有合图,因此我们选用了gulp.spritesmith-multi。
以前使用gulp的构建项目,css都同一放在一层,引用图片的路径都放在一个css里面。但面对React的项目,我们每一个component都有自己对应的index.js(处理逻辑)和index.scss(处理样式),由于这个合图插件只能标出一个图片路径,因此如果合图的引用发生在不同层次的component,绝对会发生找不到文件的报错,因此我们统一将引用放在container的样式文件中,权宜地解决这个问题,以下是大致gulpfile写法:
gulp.task('sprites',function (cb) { var spriteData = gulp.src(Config.filePath.src + 'img/sprites/**/*.png') .pipe(spritesmith({ spritesmith: function(options) { options.imgPath = Config.sprites.imgPath + options.imgName; options.cssName = options.cssName.replace('.css','.scss'); // customized generated css template options.cssTemplate = './config/scss.template.handlebars'; } })); // Pipe image stream through image optimizer and onto disk var imgStream = spriteData.img // DEV: We must buffer our stream into a Buffer for `imagemin` .pipe(gulp.dest(Config.sprites.imgDest)); // Pipe CSS stream through CSS optimizer and onto disk var cssStream = spriteData.css .pipe(gulp.dest(Config.sprites.cssDest)); // Return a merged stream to handle both `end` events return merge(imgStream,cssStream); });
以下是样式引用办法:
@import"../../../css/sprites/list_s"; ...... .info-right.tiku-status3 { @include sprite($remark); }
如有错误,恳请斧正!