前端项目构建框架重构

前言

原项目结构使用了gulp+webpack进行项目自动化流程构建,核心页面使用react框架开发,存在比较严重的问题是由于产品功能的丰富,项目构建耗时越来越长,加上尤其是release项目发布时还使用了 Google Closure Compile 进行代码压缩,整个打包过程耗时近一分钟,实在忍不了,于是搞清楚整个项目构建框架,到底是因为什么导致的。

原项目结构

优点:

  • webpack配置了externals,把React,ReactDOM,以及Redux,ReactRedux都排除在打包范围外,这样做减少了打包文件大小,也提高了打包效率
  • 功能需要使用amr解码,于是引入了音频编码库amrnb.js,采用了按需加载方式,值得肯定
  • gulp工程化做得很好,包括iconfont字体、静态文件、图片等的压缩和批处理,优化做得不错

缺点:

  • externals剔出的库都写到了html的标签中加载,大概有6个js文件和4个css文件,碎片化严重
  • 开发过程中没有热加载,每次修改功能需要调试时,都需要手动执行一次编译命令,然后刷新页面
  • 编译打包时间过慢,即便在开发环境下,也需耗时近一分钟,原因是每次打包都进行了代码压缩操作

解决方案

优化开发环境下打包

使用webpack.DefinePlugin配置好开发环境变量,开发环境时,去掉GCC压缩,并添加调试环境代码,比如我在项目中新加了一个叫 why-did-you-update 的库用来检查React多余的渲染,进行性能优化(这个库原先没有的,优化打包性能时新加的,所以最后比较大小时这个库原版本代码本就没有,不计在内),我们只需要在开发环境打包进去,生产环境下是不需要的:

1
2
3
4
5
// 开发和生产环境代码
if (process.env.NODE_ENV !== 'production') {
const { whyDidYouUpdate } = require('why-did-you-update')
whyDidYouUpdate(React);
}

打包代码增加devpack任务(其中webpack(env)是生成webpack配置并打包过程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var gulp = require('gulp'),
rename = require('gulp-rename'),
webpack = require('./webpack'),
notifier = require('./notifier'),
closure = require('gulp-closure-compiler'),
gulp.task('minpack', function() {
return gulp.src(src, {cwd: cwd})
.pipe(webpack('production'))
.pipe(rename({ dirname: src.replace(/(\/)js\/.*/, '$1') + '/', basename: 'index', extname: '.js' }))
.pipe(gulp.dest('dist/public'))
.pipe(closure({
fileName: 'index.js',
compilerFlags: {
warning_level: 'VERBOSE',
language_in: 'ECMASCRIPT5',
output_wrapper: '/* Js minimized by <chyrain_v5kf@qq.com> */\n%output%\n'
}
}))
.pipe(rename({dirname: name, suffix: '.min'}))
.pipe(gulp.dest('dist/public'))
.pipe(notifier({
title: 'WebPack完成',
message: '生成:'
}));
});
// 开发环境打包,env='development',并去掉closure压缩
gulp.task('devpack', function() {
return gulp.src(src, {cwd: cwd})
.pipe(webpack('development'))
.pipe(rename({ dirname: src.replace(/(\/)js\/.*/, '$1') + '/', basename: 'index', extname: '.js' }))
.pipe(gulp.dest('dist/public'))
.pipe(notifier({
title: 'WebPack完成',
message: '生成:'
}));
});

然后,要解决改动代码每次都要手动编译问题。考虑到项目以gulp为主,辅以webpack进行打包,为了减少改动,不使用webpack热更新,而用了gulp的watch方法,监听文件变动后执行一次打包操作(其中devpack是gulp开发环境下打包命令):

1
2
3
gulp.task('watch',function(){
gulp.watch('src/public/js/**/*.jsx', ['devpack']);
});

解决依赖库庞大和碎片化

首先想到的是使用webpack的DllPlugin,把所有第三方库打包到一个独立文件,但是由于原项目配置了externals,所有代码页都以全局变量访问的 React 等第三方框架库,为了减少改动,我们需要继续暴露出这些第三方库的全局变量,一番摸索过后,发现webpack有一个expose-loader非常合适。

于是新建一个dll打包配置文件 webpack.dll.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const webpack = require('webpack');
const path = require("path");
const PUBLIC_PATH = path.resolve(__dirname, './src/public');
const SRC_PATH = path.resolve(__dirname, './src'); // 所有源文件所在top路径
const LIB_PATH = path.resolve(PUBLIC_PATH, './lib/js'); // js 库目录
const BUILD_PATH = LIB_PATH; // 编译输出目录
const vendors = [
'react',
'react-dom',
path.resolve(LIB_PATH, 'window_redux.js'), // 'redux'的包装
path.resolve(LIB_PATH, 'window_react_redux.js'), // 'react-redux'的包装
path.resolve(LIB_PATH, 'jqlite.js')
];
module.exports = {
output: {
path: BUILD_PATH,
filename: '[name].js',
library: '[name]',
},
entry: {
dll: vendors,
},
module: {
rules: [
{
test: require.resolve('react'),
use: [{
loader: 'expose-loader',
options: 'React'
}]
},
// 除了react外要用路径
{
test: /\/node_modules\/react-dom\//, // require.resolve('react-dom'),
use: [{
loader: 'expose-loader',
options: 'ReactDOM'
}]
},
{
test: /\/public\/lib\/js\/jqlite/,
use: [{
loader: 'expose-loader',
options: '$'
},{
loader: 'expose-loader',
options: 'jQuery'
}]
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.DllPlugin({
path: path.resolve(PUBLIC_PATH, './dll/manifest.json'),
name: '[name]',
context: path.resolve(PUBLIC_PATH, './dll')
}),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
minimize: true,
sourceMap: true,
output: { comments: false }
})
]
};

然后在项目主入口打包配置文件中修改 webpack.config.js,添加 DllReferencePlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module.exports = {
output: {
path: process.cwd(),
filename: 'bundle.js'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: 'babel-loader'
}
]
},
// externals全局变量不打包
// externals: {
// 'react': 'React',
// 'react-dom': 'ReactDOM',
// },
// 把全部外部库抽出到一个xx_dll.js中在html中<script>引入
plugins: [
new webpack.DllReferencePlugin({
context: path.resolve(PUBLIC_PATH, './dll'),
manifest: require(path.resolve(PUBLIC_PATH, './dll/manifest.json')),
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
});

DllPlugin 插件会在编译完第三方依赖库之后,生成一个模块相对路径到模块 id 的映射表 manifest.json,和 dll.js 文件,然后在编译业务代码时使用 DllReferencePlugin 导入这个映射表,业务代码就能找到第三方依赖里的对应模块了。并且dll中通过 expose-loader 把原先需要的全局变量都挂载到window上,兼容了原项目编码方式,但是需要注意的是 expose-loader 的配置中 require.resolve 是用来获取模块的绝对路径(”../node_modules/react/react.js”),这里的暴露只会作用于 React 模块。并且只在 bundle 中使用到它时,才进行暴露。所以其他模块需要以路径形式获取,比如jqlite test: /\/public\/lib\/js\/jqlite/,并在项目入口index.js中引入他们(只需引入一次即可):

1
2
3
4
5
import React from 'react';
import ReactDOM from 'react-dom';
import Redux from '../lib/js/window_redux.js';
import ReactRedux from '../lib/js/window_react_redux.js';
import $ from '../lib/js/jqlite.js';

对于redux、react-redux的引入,则需要包装一下,再添加到 webpack.dll.js 进行打包:

1
2
3
4
5
6
7
// window_redux.js
import * as Redux from 'redux'
export default (window.Redux = Redux)
// window_react_redux.js
import * as ReactRedux from 'react-redux'
export default (window.ReactRedux = ReactRedux)

好了,现在可以打包测试一下啦,输入 gulp watch 命令,修改代码,自动编译,完成,刷新页面。额。。。花了总共18s,还是很慢啊,不过没办法,项目庞大,代码打包后还有2MB,一个小小的💻性能不足以支撑,可以考虑换个更强大的电脑了。不过,相比此前一分钟的打包时间,已经足够让我提升开发效率了。最后放到生产环境下在浏览器下比对,发现在服务端开启gzip的情况下,修改后的项目全部js加载后总大小比原先小了15KB,相当于gzip前小了近40多KB,不仅提升了开发效率,还提升了生产环境的带宽利用率,优化还是比较有效果的😊。