# 通过webpack优化前端环境

# 优化前端资源加载 - 图片加载与代码压缩

我们总是希望浏览器在加载页面时用的时间越短越好,所以构建出来的文件应该越少越小越好,一来减少浏览器需要发起请求的数量,二来减少下载请求资源的时间

其实webpack把多个代码文件打包成几个必须的静态资源,已经很大程度减少了静态资源请求数量了

下面我们使用webpack实现更多的前端资源加载的优化需求

# Css Sprites

Css Sprites技术是前端领域一种常见的用于减少图片资源请求数的优化方式

如果使用的是webpack 3.x,需要Css Sprites的话,可以使用webpack-spritesmith或者sprite-webpack-plugin

以webpack-spritesmith为例子,安装依赖  npm install webpack-spritesmith --save-dev

修改webpack配置

module: {
    loader: [
        // ... 这里需要有处理图片的loader,如file-loader
    ]
},
resolve: {
    modules: [
        'node_modules',
        'spritesmith-generated', // webpack-spritesmith 生产所需文件的目录
    ]
},
plugins: [
    new SpritesmithPlugin({
        src: {
            cwd: path.resovle(__dirname, 'src/ico'), // 多个图片所在的目录
            glob: '*.png'  // 匹配图片的路径
        },
        target: {
            // 生产最终图片的路径
            image: path.resovle(__dirname, 'src/spritesmith-generated/sprite.png'),
            // 生成所需 SASS/LESS/Stylus mixins代码,我们使用Stylus预处理器作为例子
            css: path.resolve(__dirname, 'src/spritesmith-generated/sprite.styl'),
        },
        apiOptions: {
            cssImgRef: "~sprite.png"
        }
    })
]
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
在你需要的样式代码中使用`sprite.styl`,只需引入后调用需要的`mixins`

@import `~sprite.styl`

.close-button
    sprite($close)
.open-button
    sprite($open)
1
2
3
4
5
6
7
8

webpack-spritesmithsprite-webpack-plugin还没更新到webpack 4.x,需要配合postcsspostcss-sprites,才能实现

# 图片压缩

图片资源会占用前端资源的很大一部分,file-loader可以用来处理图片文件,在次基础上,我们还可以再添加一个image-webpack-loader来压缩处理图片

module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /.*\.(gif|png|jpe?g|svg|webp)$/i,
                use: [
                    {
                        loader: 'file-loader',
                        options: {}
                    },
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: { // 压缩 jpeg 的配置
                                progressive: true,
                                quality: 65
                            },
                            optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
                                enabled: false,
                            },
                            pngquant: { // 使用 imagemin-pngquant 压缩 png
                                quality: '65-90',
                                speed: 4
                            },
                            gifsicle: { // 压缩 gif 的配置
                                interlaced: false,
                            },
                            webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
                                quality: 75
                            }
                        }
                    }
                ]
            }
        ]
    }
}
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

# 使用DataURL

有些时候我们会有一些很小的图片,并不想用Css Sprites的方式来处理,那么我们可以在webpack中使用url-loader来处理这些很小的图片 (常用)

url-loaderfile-loader 的功能类似,但是在处理文件的时候,可以通过配置指定一个大小,当文件小于这个配置时,url-loader会将其转换成一个base64编码的DataURL

module.exports = {
    // ...
    modules:{
        rules: [
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192, // 单位是 Byte,当文件小于 8KB 时作为 DataURL 处理
                        },
                    },
                ],
            }
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 代码压缩

webpack 4.x版本运行的时候,modeproduction时会启动压缩Js的插件,而webpack 3.x,使用压缩Js代码插件的是uglifyjs-webpack-plugin

在生产环境中,压缩Js代码基本是一个必不可少的步骤,这样可以大大减小JavaScript的体积

除了压缩Js代码之外,我们还可以压缩Html,Css,虽然这种压缩只能移除空格换行等无功能性字符,但也能在一定程度上减小文件大小,在webpack中的配置使用也不是特别麻烦,所以我们通常也会使用

对于HTML文件,可以使用html-webpack-plugin插件

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html', // 配置输出文件名和路径
      template: path.join(__dirname, './src/index.html'),// 配置文件模板
      minify: { // 压缩 HTML 的配置
        minifyCSS: true, // 压缩 HTML 中出现的 CSS 代码
        minifyJS: true // 压缩 HTML 中出现的 JS 代码
      }
    }),
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13

对于css文件,可以使用css-loader,也提供了压缩CSS代码的功能

module.exports = {
  module: {
    rules: [
      // ...
      {
        test: /\.css/,
        include: [
          path.resolve(__dirname, 'src'),
        ],
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              minimize: true, // 使用 css 的压缩功能
            },
          },
        ],
      },
    ],
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

css-loader的选项中配置minimize字段为true来使用css压缩代码的功能

css-loader是使用cssnano来压缩代码的,minimize字段也可以配置为一个对象,来将相关配置传递给 cssnano

关于cssnano内容可参考 https://cssnano.co/


# 优化前端资源加载 - 分离代码文件

如何利用浏览器的缓存以及在webpack中实现按需加载代码

# 分离代码文件

关于分离css文件,我们可以使用webpack中的extract-text-webpack-plugin插件

为什么要把css文件分离出来,而不是直接一起打包在Js中?

最主要的原因是我们希望更好地利用缓存,假设我们原本页面的静态资源都打包成一个Js文件,加载页面时虽然只需要加载一个Js文件, 但一旦我们的代码改变了,用户访问新的页面时就需要重新加载一个新的Js文件。有些情况下,我们只是单独修改了样式,这样也要重新加载整个应用的Js文件,是很不划算的

还有一种情况是我们有多个页面,它们都可以共用一部分样式,如果每个页面都单独打包成一个Js文件,那么每次访问页面都会重复加载原本可以共享的Css代码。

而如果是分离开,第二个页面就有了Css文件的缓存,访问速度自然更快,虽然对第一个页面来说多了一个请求,但缓存带来的速度提升是更可观的

那么如何使用webpacl来把代码中公共使用的部分分离成独立的文件呢?

webpack 4.xwebpack 3.x在代码分离这部分的做法区别较大

webpack 3.x以前的版本是使用CommonsChunkPlugin来做代码分离的,而webpack 4.x则是把功能移到了optimize.splitChunks中,直接使用该配置就可以实现代码分离

# webpack 4.x 的 optimization

module.exports = {
    // ... webpack配置
    optimization: {
        splitChunks: {
            chunk: "all",  // 所有的chunks代码公共的部分分离出来成为一个单独的文件
        }
    }
}
1
2
3
4
5
6
7
8

我们需要在HTML中引用两个构建出来的Js文件,并且common.js需要在入口代码之前,下面是个简单的例子

<script src="commons.js" charset="utf-8"></script>
<script src="entry.bundle.js" charset="utf-8"></script>
1
2

之前我们提到拆分文件是为了更好地利用缓存,分离公共类库很大程度上是为了让多页面利用缓存,从而减少下载的代码量,同时,也有代码变更时可以利用缓存减少下载代码量的好处。从这个角度出发,个人建议将公共使用的第三方类库显式地配置为公共的部分,而不是 webpack 自己去判断处理。因为公共的第三方类库通常升级频率相对低一些,这样可以避免因公共 chunk 的频繁变更而导致缓存失效。

显式配置共享类库操作

module.exports = {
  entry: {
    vendor: ["react", "lodash", "angular", ...], // 指定公共使用的第三方类库
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: "initial",
          test: "vendor",
          name: "vendor", // 使用 vendor 入口作为公共部分
          enforce: true,
        },
      },
    },
  },
  // ... 其他配置
}

// 或者
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /react|angluar|lodash/, // 直接使用 test 来做路径匹配
          chunks: "initial",
          name: "vendor",
          enforce: true,
        },
      },
    },
  },
}

// 或者
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: "initial",
          test: path.resolve(__dirname, "node_modules") // 路径在 node_modules 目录下的都作为公共部分
          name: "vendor", // 使用 vendor 入口作为公共部分
          enforce: true,
        },
      },
    },
  },
}
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

上述第一种做法是显示指定哪些类库作为公共部分,第二种做法实现的功能差不多,只是利用了 test 来做模块路径的匹配,第三种做法是把所有在 node_modules 下的模块,即作为依赖安装的,都作为公共部分。你可以针对项目情况,选择最合适的做法。

# webpack 3.x 的 CommonsChunkPlugin

webpack 3.x以下的版本需要用到webpack自身提供的CommonsChunkPlugin插件

module.exports = {
    // ...
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'commons', // 公共使用的chunk的名称
            filename: 'commons.js', // 公共chunk的生成文件名
            minChunks:3, // 公共的部分必须被3个chunk共享
        })
    ]
}
1
2
3
4
5
6
7
8
9
10

chunk在这里是构建的主干,可以简单理解为一个入口对应一个chunk

以上插件配置的构建后会生成一个commons.js文件,该文件就是代码中的公共部分,上面的配置中minChunks为3,意思是当一个模块被3个以上的chunk依赖时,这个模块就会被划分到commonschunk中去。

CommonsChunkPlugin也是支持显示配置共享类库的

module.exports = {
  entry: {
    vendor: ['react', 'react-redux'], // 指定公共使用的第三方类库
    app: './src/entry',
    // ...
  },
  // ...
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor' // 使用 vendor 入口作为公共部分
      filename: "vendor.js", 
      minChunks: Infinity, // 这个配置会让 webpack 不再自动抽离公共模块
    }),
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上述配置会生成一个名为vandor.js的共享代码文件,里面包含了ReactReact-Redux库的代码,可以提供给多个不同的入口代码使用。

这里minChunks配置使用了Infinity,可以理解为webpack不自动抽离公共模块。(如果这里指定了3,那么被3个以上的chunk依赖的模块会和React,React-Redux一同打包进vendor,这样就失去显示指定的意义


# 优化前端资源加载 - 进一步控制Js大小

# 按需加载

前面讲述如何将大的代码文件进行拆分,抽离出多个页面共享的代码文件,但是当你的Web应用是个单页面应用(Vue),并且及其复杂的时候,你会发现有一些代码并不是每一个用户都需要用到的。

我们希望可以将这一部分代码抽离出去,仅当用户真正需要用到时才加载,这时就可以使用webpack的按需加载功能

webpack的构建环境中,要按需加载代码模块很简单,遵循ES标准的动态加载语法dynamic-import来编写代码即可,webpack会自动处理使用该语法编写的模块

// import 作为一个方法使用,传入模块名即可,返回一个Promise对象来获取模块暴露的对象

import('lodash').then( _ => {
    console.log(_.lash([1,2,3])) // 打印3
})


这里,webpack构建时会自动把lodash模块分离出来,并且在代码内部实现动态加载lodash的功能

动态加载代码模块依赖于网络,所以模块内容会异步返回,

因此import方法需要返回一个promise来获取动态加载的模块内容
1
2
3
4
5
6
7
8
9
10
11
12

❗ 如果使用了Babel的话,还需要Syntax Dynamic Import这个Babel插件来处理import()这种语法

由于动态加载代码模块的语法依赖于promise,对于低版本的浏览器,需要添加promisepolyfill后才能使用

# Tree Shaking

Tree Shaking可以移除Js上下文中的未引用代码,删除用不着的代码,能够有效减少JS代码文件的大小

官方例子

// src/math.js
export function square(x) {
    return x * x
}
export function cube(x) {
    return x * x * x
}

// src/index.js
import { cube } from './math.js'
console.log(cube(3))
1
2
3
4
5
6
7
8
9
10
11
12
13

很明显,square这个方法未被引用,是可以删掉的。

webpack中,只有启动了Js代码压缩功能(即使用 uglify)时,会做Tree shaking优化

  • webpack 4.x需要指定modeproduction
  • webpack 3.x需要配置UglifyJsPlugin

启动之后,构建出来的包就会移除square这部分代码

PS:

如果你在项目中使用了Babel的话,要把Babel解析模块语法的功能关掉,在.babelrc配置中增加 "modules":false 这个配置


// .babelrc
{
    "presets":[["env",{ "modules": false }]]
}

这样可以把`import/export`这一部分模块语法交由`webpack`处理,否则无法使用`Tree Shaking`的优化
1
2
3
4
5
6
7
8
9
Last Updated: 12/6/2019, 2:50:39 PM