# webpack4教程

# 一、webpack究竟是什么?

webpack是模块打包工具,最初只能打包js文件,现在可以打包任何形式的模块文件,比如css,可以使用const style = require('./index.css')或者import style from './index.css'

# 二、webpack的正确安装方式

webpack是居于node.js开发的模块打包工具,本质是由node实现的,所以要使用webpack得先安装node。尽量安装新版本的node.js,会很大程度提高webpack打包速度。高版本的webpack会利用node中的新特性来提高它的打包速度。

安装好node.js后,新建webpack-demo文件夹,然后运行npm init,一路回车,就会生成一个package.json文件,在里面添加一条"private":true,代表是私人仓库,同时把“main”:“index.js"去掉,因为不会被外部引用。

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "IFE",
  "license": "ISC"
}
1
2
3
4
5
6
7
8
9
10
11

然后就可以安装webpack了,有两种方式安装:

1,全局模式(不推荐):npm install webpack webpack-cli -g,命令行运行webpack -v如果能打印出版本号,说明全局安装好了。但如果有多个项目的情况下,都使用全局安装的webpack,可能会出现版本冲突,导致某些项目起不来。卸载npm uninstall webpack webpack-cli -g

2,项目内安装(推荐):在项目根目录里运行npm install webpack webpack-cli -D ,-D和-save-dev是等价的,表示是开发时候依赖的东西。也可以在webpack后加@版本号,表示要安装哪个具体版本。安装好了后运行webpack -v会报错:-bash: webpack: command not found,因为输入webpack命令的时候,webpack会尝试到全局的模块目录中去找webpack,但全局并没有安装,就会报错。node提供了npx命令,npx webpack -v就可以运行了,因为npx会在当前项目的node_modules里找webpack。

两种方式都需要安装webpack-cli,它使得我们可以在命令行里使用webpack命令。

# 三、webpack的配置文件

如果想在项目中编写自己的webpack配置文件,需要在根目录下新建webpack.config.js文件。通过module.exports导出配置,提供一个入口文件作为entry和打包输出文件output,下面的代码将打包后的内容输出到dist目录下,index.js文件内。

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}
1
2
3
4
5
6
7
8
9

也可以使用npx webpack —config otherconfig.js指定用一个配置文件。

在package.json文件里的script里添加一个命令:“bundle": "webpack",现在运行npm run bundle就会执行webpack打包。

打包后命令行会有如下显示

Hash对应本次打包唯一的hash值,Version为这次打包webpack的版本,Time是整体打包耗时,Built打包的时间,Asset打包出来的文件,Size是文件大小,Chunks放每个js的ID,Chunk Names放js对应的名字。

entry: './src/index.js'entry: {main: './src/index.js'}的简写

下面警告提示我们没有指定打包的模式,默认按production模式打包,打包后的js都会压缩在一行内,在module.exports里增加一行mode: 'development'后,就不会报警告,而且代码不会被压缩。

# 四、webpack的核心概念

# 1,loader

webpack默认只知道打包js模块,对于非js结尾的样式文件、图片等就不知道怎么打包了,需要另外在配置文件里配置。

module.exports的对象里新增module属性的对象,该对象里需要rules属性数组,里面配置打包规则。

    module: {
        rules: [{
            test: /\.(png|jpg|jpeg)$/, 
            use: ['url-loader?limit=2048']
        }]
    }
1
2
3
4
5
6

规则的写法:test为一个正则表达式,检测是否是匹配的模块,use为使用的loader名,loader需要安装才能使用,上面👆配置的含义是:将所有以png、jpg、jpeg结尾小于2kb的图片模块,使用url-loader,打包成base64形式的字符串,然后直接放到bundle.js里,就不用再发HTTP请求节省了时间。

webpack 使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。

# 打包图片

在使用loader的时候,可以额外配置一些参数,放在options配置项里。

下面👇的配置使打包出的图片名字和后缀跟打包前一样,并加上hash值,打包到images/目录下,如果小于2kb的打包成base64字符串

    module: {
        rules: [{
            test: /\.(png|jpg|jpeg)$/, 
            use: {
            		loader: 'url-loader',
            		options: {
                  	// placeholer 占位符
            				name: '[name]_[hash:5].[ext]',
                  	outputPath: 'images/',
                  	limit: 2048
            		}
            }
        }]
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 打包样式

打包样式文件的时候一般用到不止一个loader,use里就不使用对象了,而是数组。

        {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }
1
2
3
4

css-loader将 CSS 转化成 CommonJS 模块,会帮我们分析出几个css文件的关系,最终合并成一段css.

style-loader将 JS 字符串生成为 style 节点,在得到css-loader生成的内容后,挂载到页面的head部分.

如果要使用Less、Scss、Stylus编写样式,需要再添加对应的loader。比如'sass-loader',将 Sass 编译成 CSS,默认使用 Node Sass. 安装npm install sass-loader node-sass --save-dev

        {
            test: /\.scss$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }
1
2
3
4
5
6
7
8

webpack的配置里,loader的执行是由先后顺序的:从下到上,从右到左

如果使用css3的新特性,为了兼容性需要加前缀,比如-webkit-transform: translate(10px, 10px)

postcss-loader可以实现这个功能,安装npm i -D postcss-loader还需要安装插件npm install autoprefixer -D

需要我们创建一个postcss.config.js文件,在这个文件里做配置。

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}
1
2
3
4
5

css-loader配置

        {
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2
                    }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

importLoaders的作用是:如果打包一个index.scss文件里,@import引用了其他other.scss文件,那other.scss在打包的时候可能不会走postcss-loader和sass-loader了,而是直接走css-loader了。importLoaders可以让import进来的样式文件,也走下面的两个配置。

css module

如果一个文件内直接通过import './index.scss'这种方式引入css文件,会影响到其他文件,相当于样式是全局的。很容易出现样式冲突。css模块化可以让引入的css只在这个模块内有效。

只需要在css-loader里再加一项配置:modules:true

                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2,
                        modules: true
                    }
                },
1
2
3
4
5
6
7

而引入css的方式也变成:import style from './index.scss', 给元素添加类名也变成style.classname

打包字体

当使用iconfont的时候,需要打包几个字体文件(eot,svg,ttf,woff)

        {
            test: '/\.(eot|ttf|svg)$/',
            use: {
                loader: 'file-loader'
            }
        }
1
2
3
4
5
6

# 2,pluguns

plugin可以在webpack运行到某个时刻的时候,帮助做一些事情

# html-webpack-plugin

会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个HTML文件中。

npm install --save-dev html-webpack-plugin
1
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html'
  })]
};
1
2
3
4
5
6
7
8
9
10
11
12
13

将会产生一个包含以下内容的文件 dist/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>webpack App</title>
  </head>
  <body>
    <script src="index_bundle.js"></script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10

如果你有多个 webpack 入口点, 他们都会在生成的HTML文件中的 script 标签内。

如果你有任何CSS assets 在webpack的输出中(例如, 利用 MiniCssExtractPlugin 提取CSS), 那么这些将被包含在HTML head中的<link>标签内。

可以给它**添加模板文件,**比如在src目录下创建了一个HTML模板文件,可以通过new HtmlWebpackPlugin({ template: 'src/index.html'})来引用。

# clean-webpack-plugin(第三方)

重新打包的时候,在打包之前,自动先把dist目录删除

npm install clean-webpack-plugin -D

以下为3.0.0版本的使用方式,

引用:const {CleanWebpackPlugin} = require('clean-webpack-plugin');

在plugins数组里添加

new CleanWebpackPlugin({
		cleanAfterEveryBuildPatterns: ['dist']
})
1
2
3

最近升级了,直接new CleanWebpackPlugin(),就好,默认是remove的是output.path,不用设置任何参数!

# 3,Entry与Output的基础配置

两个入口打包出两个文件,publicPath用于把打包出的js文件加上地址.

    entry: {
        main: './src/index.js',
        sub: './src/index.js'
    },
    output: {
    		publicPath: 'http://cdn.com.cn',
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
1
2
3
4
5
6
7
8
9

参考管理输出

# 4,SourceMap

希望开发时代码打包出错时,能告诉我们,到底是哪里的代码出了问题。sourceMap是一个映射关系,它知道打包出来的js代码对应的原代码的位置。

在module.export里添加:devtool: 'source-map',会在dist目录里生成一个map文件。

这种映射比较耗费性能,因为会精确到第几行第几个字符,而cheap-inline-source-map只会告诉第几行出了问题,性能更好。inline代表不会生成map文件,而是以字符串的形式放到打包生成的文件中。而cheap-module-source-map还会管第三方模块和loader的代码。而cheap-module-eval-source-map,通过eval这种形式,后面跟sourceURL来指向来源的代码表明映射关系,执行效率最高,性能最好。在开发模式下是最佳实践。如果是生产环境,可以使用cheap-module-source-map,提示效果更全面。

# 5,WebpackDevServer

希望每次修改代码后能自动编译打包:

  1. "bundle": "webpack —watch",在webpack后接'--watch',只要源代码发生变化,webpack就能监听到,并重新打包生成bundle.js文件,但需要手动刷新页面。

  2. 使用webpack-dev-server

    安装npm install webpack-dev-server -D

    只需要添加以下配置,然后在scripts里添加配置"start": "webpack-dev-server"

    devServer: {
    	contentBase: './dist',
    	open: true,
    	proxy: {
          '/api': 'http://localhost:3000'
     }
    }
    
    1
    2
    3
    4
    5
    6
    7

    现在运行npm run start,会默认在localhost:8080端口上启动服务,并自动打开浏览器并访问服务器的地址。如果代码发生变化,也能自动重新编译打包,并重启服务和自动刷新浏览器。以为是http服务器,所以能发ajax请求。

    Proxy请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users

  3. 在node中直接使用webpack

    首先添加一条命令:"server": "node server.js",再在根目录下创建一个server.js文件。下面介绍如何使用express启动node服务器。

    安装expressnpm install express -D,因为要监听webpack文件的变化并自动重新打包,需要借助一个webpack的中间件webpack-dev-middleware,安装npm install webpack-dev-middleware -D

    在output里添加publicPath: '/'

    编写server.js

    const express = require('express')
    const webpack = require('webpack')
    const webpackDevMiddleware = require('webpack-dev-middleware')
    const config = require('./webpack.config.js')
    // 编译器,每执行一次都会重新打包代码
    const complier = webpack(config)
    
    const app = express()
    app.use(webpackDevMiddleware(complier, {
        publicPath: config.output.publicPath
    }))
    
    app.listen(3000, () => {
        console.log('server is running')
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    现在执行npm run server就会在3000端口起node服务,并打包。

# 6,Hot Module Replacement热模块更新

Webpack-dev-server会把打包的目录放到电脑内存里,这样打包更快。

希望改变样式代码后,不要重新刷新页面,只是替换样式代码,实现热模块更新

首先,在dev-server的配置中,增加hot: true

    devServer: {
        contentBase: './dist',
        open: true,
        hot: true,
        hotOnly: true
    }
1
2
3
4
5
6

hotOnly: true的含义是:即使HMR不生效,也不刷新浏览器。

使用插件:

先引入webpack,const webpack = require('webpack')

再在plugins配置项里,添加new webpack.HotModuleReplacementPlugin()

更改webpack配置后要重启项目,热模块更新就生效了。如果只改了css文件,就不会替换js渲染出的内容,而只替换修改了的css的内容,因为css-loader底层帮我们实现了这个功能。

如果只改了js文件一个模块的内容,同样可以不影响其他模块。

if (module.hot) {
    module.hot.accept('./content', () => {
        // 如果只是改了content模块的内容,就只让content重新执行
        // 删除原有content模块,重新生成新的content模块
        // ...
    })
}
1
2
3
4
5
6
7

Vue-loader,react的babel-preset都内置了HMR这样功能的实现。

模块热更新实现原理

# 7,Babel处理ES6语法

希望在项目中使用ES6语法,而又要兼顾浏览器的兼容性,可以使用babel把ES6的语法转化成ES5的语法。

安装babel: npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/polyfill

babel/core是babel的核心库,能够让babel识别js代码的内容,转化成AST抽象语法树,再编译转化成新的语法。

@babel/preset-env包含了所有ES6转ES5的规则。

@babel/polyfill把一些低版本浏览器不兼容的对象(Promise)和函数(map)转换成polyfill,通过全局变量的形式注入。

再在module里添加配置:

module: {
  rules: [
            {   
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: "babel-loader",
                options: {
                    "presets": [["@babel/preset-env",{
                        useBuiltIns: 'usage'
                    }]]
                }
            },
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

**useBuiltIns: 'usage'**只会加载代码要使用的polyfill,精简体积。

babel的其他配置:

1, 指定兼容到浏览器版本

      targets: {
        edge: "17",
        firefox: "60",
        chrome: "67",
        safari: "11.1",
      },
1
2
3
4
5
6

2,transform-runtime

plugin-transform-runtime可以把polyfill以闭包的形式注入,不污染全局环境。

npm install --save-dev @babel/plugin-transform-runtime @babel/runtime @babel/runtime-corejs2

在options里添加插件

"plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": 2,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
1
2
3
4
5
6
7
8
9
10
11
12

3,使用.babelrc配置文件,把options里的内容提取到一个单独的文件内

{
    "presets": [["@babel/preset-env", {
        targets: {
            edge: "17",
            firefox: "60",
            chrome: "67",
            safari: "11.1",
        },
        useBuiltIns: 'usage'
    }]]
}
1
2
3
4
5
6
7
8
9
10
11

4, babel-plugin-dynamic-import-webpack异步加载的代码做代码分割

安装npm install --save-dev babel-plugin-dynamic-import-webpack

在babelrc文件下添加

"plugins": ["dynamic-import-webpack"]
1

# 8,对React打包

react的jsx语法需要编译打包成js才能被浏览器识别

npm install --save-dev @babel/preset-react

在.babelrc里配置

{
    "presets": [
        ["@babel/preset-env", {
            targets: {
                edge: "17",
                firefox: "60",
                chrome: "67",
                safari: "11.1",
            },
            useBuiltIns: 'usage'
        }],
        "@babel/preset-react"
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

presets的执行顺序是从下到上,从右往左。先把react的jsx语法装换,然后再把转换过后的ES6 的代码转换成ES5的代码。

到目前为止webpack.config.js的内容为

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack')

module.exports = {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    entry: {
        main: './src/index.js'
    },
    resolve: {
        extendsions: ['.js', '.jsx']// 引入模块时先找js结尾的文件,再找jsx结尾的文件
      	mainFiles: ['index', 'main']// 引入一个目录时,优先引入index命名的文件,其次main命名的文件
        alias: {
            header: path.resolve(__dirname, '../src/header') // 设置别名
        }
    },
    devServer: {
        contentBase: './dist',
        open: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, // .js文件和.jsx文件都会使用babel-loader
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    "presets": [["@babel/preset-env", {
                        targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                        },
                        useBuiltIns: 'usage'
                    }]]
                }
            },
            {
                test: /\.(png|jpg|jpeg)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name]_[hash:5].[ext]',
                        outputPath: 'images/',
                        limit: 2048
                    }
                }
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
            {
                test: '/\.(eot|ttf|svg)$/',
                use: {
                    loader: 'file-loader'
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin({
            cleanAfterEveryBuildPatterns: ['dist']
        }),
        new webpack.HotModuleReplacementPlugin()
    ],
    output: {
        // publicPath: 'http://cdn.com.cn',
        publicPath: '/',
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
}
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

# 五、webpack高级概念

# 1,Tree Shaking

希望使用什么才引入什么,Tree Shaking只会打包一个模块中要使用的内容,不会引入的东西剔除掉。Tree Shaking只支持ES module的引入。因为ES module底层是静态引入的方式而common.js是动态引入的方式。

在module.exports里添加配置:

    optimization: {
        // 只打包那些被使用的模块
        usedExports: true
    },
1
2
3
4

在package.json里添加:"sideEffects": false,,如果有模块虽然不导出内容,但仍需要,比如babel/poly-fill以及css文件, 可以这样设置"sideEffects": ["@babel/poly-fill", "*.css"],

注意Tree Shaking只在production模式下才会生效,development的模式下做打包时,即使用了Tree Shaking,也不会生效只有提示。production模式下也不用添加以上module.exports里的配置。

# 2,Develoment和Producttion模式的区分打包

Develoment-开发模式下:devserver可以帮我们起一个服务器,集成了HMR特性,修改了代码会实时展示在devserver对应的网页上。

Producttion-线上环境:代码压缩,sourceMap可以简洁

可以写两个配置文件,webpack.dev.js用来写开发环境的配置,webpack.prod.js用来写线上环境的配置

在package.json里添加命令:

"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack-dev-server --config webpack.prod.js",
1
2

开发阶段使用:npm run dev本地开发,要线上时使用:npm run build打包线上版本。

因为开发环境和线上环境的配置有很多相同的部分,可以提取出来成webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack')

module.exports = {
    entry: {
        main: './src/index.js',
        sub: './src/index.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    "presets": [["@babel/preset-env", {
                        targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                        },
                        useBuiltIns: 'usage'
                    }]]
                }
            },
            {
                test: /\.(png|jpg|jpeg)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name]_[hash:5].[ext]',
                        outputPath: 'images/',
                        limit: 2048
                    }
                }
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
            {
                test: '/\.(eot|ttf|svg)$/',
                use: {
                    loader: 'file-loader'
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin({
            cleanAfterEveryBuildPatterns: ['dist']
        })
    ],
    output: {
        // publicPath: 'http://cdn.com.cn',
        publicPath: '/',
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
}
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
74
75
76
77

拆分后需要代码进行合并再输出,需要引入第三方模块webpack-merge,安装:npm install webpack-merge -D

使用:webpack.dev.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const devConfig = {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        hotOnly: true  //即使不支持HMR也不重新刷新浏览器
    },   
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization: {
        // 只打包那些被使用的模块
        usedExports: true
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

webpack.prod.js

const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map',
}

module.exports = merge(commonConfig, prodConfig)
1
2
3
4
5
6
7
8
9

# 3,Code Splitting代码分割

写业务代码的时候常常会引入各种第三方库,如果把它们全部打包到bundle.js里,文件会很大,加载时间会很长,而且每次修改业务代码后,要重新打包,用户要重新加载bundle.js。

可以把一些框架或库以及公用的、一般不会修改的代码,单独打包成js文件,业务逻辑代码另外打包,这样用户首次加载后,公用代码会有缓存,如果我们修改了业务代码,就只加载业务代码,请求更快。

webpack4提供了代码分割的插件,只需要添加以下配置,会自动把公用的类库生成一个文件,再把业务逻辑拆分成一个文件。

    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    },
1
2
3
4
5

对于异步加载的代码也可以做代码分割,使用 @babel/plugin-syntax-dynamic-import插件,无需做其他配置,自动分割到新的而文件中。

安装npm install --save-dev @babel/plugin-syntax-dynamic-import

在.babelrc文件里添加配置

{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
1
2
3

使用:以下配置会将异步加载的jquery库打包生成vendors~jquery.js文件, es2015规格中的import()本身是不支持指定动态导入模块生成的chunk文件的名称的,不过,现在webpack支持使用注释的方式给动态导入的模块添加chunk name

function getJquery() {
    return import(/* webpackChunkName:"jquery" */ 'jquery').then(({ default: $}) => {
        // 引入的jQuery库会被放到$变量里
        console.log($)
    })
}
1
2
3
4
5
6

SplitChunksPlugin配置参数

optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: false,
        default: false
      }
    }
  }
1
2
3
4
5
6
7
8

当splitChunks: {}不配置的时候,会使用下面的默认配置:

splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          filename: 'vendors',
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

chunks: 'async'-----表示只对异步代码做分割,如果想对同步代码也做分割, 设置chunks: 'all',vendors里的test配置,会查找引入的库是否在node_modules里,如果是,就会打包到vendors组,打包成以vendors.js形式的文件。webpack中,打包出的每个文件都是一个chunk

minSize-----表示引入的模块大于多少字节时才会做代码分割,否则不做分割。

maxSize----表示如果引入的模块大于这个大小,会尝试将模块二次拆分成这个大小。

minChunks——表示一个模块至少被引用了几次后才做代码分割。

maxAsyncRequests----表示同时加载的模块数最多是几个,如果大于这个数只会打包前几个,超过后不会再做代码分割。

maxInitialRequests----表示入口文件引入的其他js文件或者库,如果做代码分割最多分割几个。

automaticNameDelimiter----表示打包生成的文件名中间的连接符

cacheGroups——表示把符合组的模块打包到一个文件中去,比如上面👆的配置会把node_modules中的模块打包到一个名为vendors.js的文件中去。

priority----表示优先级,如果有模块符合多个组,会被打包到priority的值高的组中。

reuseExistingChunk----表示一个模块如果已经被打包过了,再打包就忽略这个模块。

# 4,Lazy Loading懒加载

通过import()异步地加载模块,只有需要的时候才会被加载。比如用react写的网页,希望访问首页的时候,只加载首页相关模块的代码,而其他页面模块的代码暂不加载,放到哪个页面的时候才加载哪个页面,就是懒加载。可以使用**import()**来实现。

function getJquery() {
    return import(/* webpackChunkName:"jquery" */ 'jquery').then(({ default: $}) => {
        // 引入的jQuery库会被放到$变量里
        console.log($)
    })
}
1
2
3
4
5
6

因为import()返回的是promise类型,为了兼容老版本浏览器,必须使用polyfill,babel新版本内置了polyfill,在.babelr文件"presets"里配置"@babel/preset-react",会自动帮我们注入polyfill。

也可以用异步函数的写法:

async function getJquery() {
		const {default: $} = await import(/* webpackChunkName:"jquery" */ 'jquery')
    console.log($)
		// to do something
}
1
2
3
4
5

# 5,打包分析

如果想要对打包的代码进行一定的分析。

在package.json中添加命令:webpack --profile --json > stats.json,把打包过程中的描述,放到stats.json文件中。

然后把生成的json文件上传到https://github.com/webpack/analyse网站上。会生成下图的分析报告。

其他类似的工具还有:webpack-chartwebpack-visualizerwebpack-bundle-analyzerwebpack bundle optimize helper

还可以通过Chrome浏览器的 Coverage工具查看代码利用率。

提高性能的方式:尽量把以后才会用到的代码通过异步加载的方式引入,提升首屏。比如首页有登录框,点击登录的时候才显示,那么就可以把登录框的代码写成异步地形式,等页面主要的内容加载完后,空闲时再加载登录框。

document.addEventListener('click', () => {
    import('./loginBox.js').then(({default: func}) => {
        func()
    })
})
1
2
3
4
5

# 6, 预取/预加载模块 prefetch/preload module

webpack v4.6.0+ 添加了预取和预加载的支持。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 "resource hint(资源提示)",来告知浏览器:

  • prefetch(预取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源
document.addEventListener('click', () => {
    import(/* webpackPrefetch: true */ './loginBox.js').then(({default: func}) => {
        func()
    })
})
1
2
3
4
5

上面👆的代码会生成 <link rel="prefetch" href="loginBox.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 loginBox.js 文件。

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

# 7,CSS代码分割及代码压缩

# CSS代码分割

webpack打包会把css打包到js文件里,如果想把css单独打包成文件,可以使用mini-css-extract-plugin插件。

安装npm install --save-dev mini-css-extract-plugin

因为这个插件暂时不支持HMR热更新,所以不推荐在开发环境使用。

修改线上环境的配置webpack.prod.js, 把style-loader替换成MiniCssExtractPlugin.loader

# CSS代码压缩

使用 optimize-css-assets-webpack-plugin插件

安装:npm install --save-dev optimize-css-assets-webpack-plugin

自动代码压缩和合并到一行

现在配置文件如下:

const TerserJSPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map',
    modules: {
        rules: [{
            test: /\.scss$/,
            use: [
                'MiniCssExtractPlugin.loader',
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2,
                        modules: true
                    }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }, {
            test: /\.css$/,
            use:[
                'MiniCssExtractPlugin.loader',
                'css-loader',
                'postcss-loader'
            ]
        }],
        optimization: {
            minimizer: [
              new TerserJSPlugin({}), // 压缩js
              new OptimizeCSSAssetsPlugin({})
            ]
          },
        plugins: [
            new MiniCssExtractPlugin({
                filename: '[name].css', // 直接引入的css文件
                chunkFilename: '[name].chunk.css' // 间接引入的css文件
            })
        ]
    }
}

module.exports = merge(commonConfig, prodConfig)
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

# 8,浏览器缓存Caching

当用户第一次加载了页面后,浏览器会缓存html和js文件,如果下次我们修改了网页代码,但文件名没变,用户下次访问这个页面的时候会读本地缓存,而不是重新加载新的文件。

可以在线上环境的打包代码webpack.prod.js中增加如下配置:

        output: {
            filename: '[name].[contenthash].js', // 入口文件名
            chunkFilename: '[name].[contenthash].chunk.js', // 间接引用的模块
        }    
1
2
3
4

contenthash是根据内容产生的hash值,内容改变了就会重新生成新的hash值。这样如果改动了代码,用户在访问网址的时候就会重新加载已改动的文件。

如果使用老版本的webpack,发现代码没做修改也生成不同hash,可以在optimization里添加下面配置:

    optimization: {
        runtimeChunk: {
            name: 'runtime'
        },
    }
1
2
3
4
5

现在业务逻辑和类库的js是单独打包生成文件的,但业务逻辑和库之间也是有关联的,webpack中称这些关联的代码为manifest,打包后既存在业务代码中也存在库代码中,manifest在旧版webpack中每次打包可能会有差异,导致生成不同的hash文件名。

配置了runtimeChunk后,会把manifest的代码抽离出来进runtime文件里去。

# 9,使用 ProvidePlugin插件来处理像 jQuery 这样的第三方包

webpack是模块化打包,模块里的变量只能在一个模块内被使用,外部访问不到,保证模块与模块间充分解耦。

webpack compiler 能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些 third party(第三方库) 可能会引用一些全局依赖(例如 jQuery 中的 $)。因此这些 library 也可能会创建一些需要导出的全局变量。

使用 ProvidePlugin 后,能够在 webpack 编译的每个模块中,通过访问一个变量来获取一个 package。如果 webpack 看到模块中用到这个变量,它将在最终 bundle 中引入给定的 package。

在webpack.common.js的插件中,可以添加如下配置:ProvidePlugin

    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery'
        }),
    ],
1
2
3
4
5

表示如果一个模块中使用了'$'这个字符串,就会在模块里自动引入jQuery并把jQuery赋值给$

# 六、Webpack实战

# 1, TypeScript的打包配置

使用typescript的时候,需要ts-loader来对代码进行打包

npm install ts-loader typescript --save-dev

需要在根目录下配置tsconfig.json文件。

如果使用了类库希望也有类型校验,可以安装对应的类型文件,比如@type/react ,安装方式npm install @type/react --save-dev

# 2,WebpackDevServer请求转发

在使用webpack-dev-server的时候,经常需要在本地localhost模拟向发送ajax请求获取数据,但会出现跨域问题。

使用devServer.proxy跨域很方便进行本地接口的调试。

配置:

    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        hotOnly: true,  //即使不支持HMR也不重新刷新浏览器
        historyApiFallback: true, //解决单页应用路由问题
        proxy: {
            '/api/A.json': "http://test.com", // 以api开头的请求会被代理到test.com服务器上
        }
    },  
1
2
3
4
5
6
7
8
9
10
11

在前端使用单页路由时,比如react使用BrowserRouter要配置**historyApiFallback: true**这样在使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html,就可以匹配前端路由。这只在前端开发阶段有效,上线前需要后配用nginx等工具做同样的配置。

更多配置跨域阅读官方文档。

# 3,Webpack结合ESlint

安装ESlint, npm install eslint --save-dev

生成eslint配置文件:npx eslint --init

检查src目录的代码规范:npx eslint src

ESlint的详细配置

如果使用VScode编辑器,可以安装eslint插件,启用后编辑器会自动把写得不规范的代码标红。

如果在webpack中使用eslint,首先要安装eslint-loader插件:npm install eslint-loader --save-dev

再在webpack的配置文件中添加配置:

			{
            test: /\.js$/,
            exclude: /node_modules/,
            use: ["babel-loader", "eslint-loader"]
       }
1
2
3
4
5

这样配置处理js代码的时候,先会用eslint检查代码,再使用babel-loader转换。

再在devserver的配置中添加overlay: true, 这样就把打包过程中的错误在浏览器上显示出来。

可选项:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader",
        options: {
          // eslint options (if necessary)
          fix: true,  // enable ESLint autofix feature
          cache: true, // reducing linting time
        }
      }
    ]
  }
  // ...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 4,webpack性能优化

想提升webpack打包速度,有如下几种方法:

# 1,升级Node和webpack版本

webpack每个版本更新,内部都会做优化,升级webpack版本能有效提升打包速度。

同时webpack又是建立在Node运行环境上,如果升级了Node,那Node的运行效率会提升,间接提升webpack速度。

# 2,在尽可能少的模块上应用loader

比如在处理js文件的时候,node_modules文件夹下的文件不使用loader

# 3,plugin尽可能精简可靠

没必要使用的插件就不用,不然就降低打包速度。需要使用的插件也分开发环境和线上环境。官方推荐的插件往往性能更好。

# 4, 使用DllPlugin提高打包速度

现在打包时,第三方模块每次都要动态从node_modules里取出,并打包到源代码中,会消耗打包事件,而一般项目中引入的第三方模块(比如React,Redux等)是不会变的,可以单独打包生成一个文件,只在第一次打包的时候分析,下次以后打包的时候直接用分析好的代码

新增一个webpack.dll.js文件,添加如下内容

const path = require('path');
const webpack = require('webpack')

module.exports = {
    mode: 'production',
    entry: {
        vendors: ['react', 'react-dom', 'redux']
    },
    output: {
        filename: '[name].dll.js',
        path: path.resolve(__dirname, '../dll'),
        library: '[name]'  // 打包的内容通过全局变量暴露出来
    },
    plugins: [
        new webpack.DllPlugin({
            name: '[name]', // 对打包生成的内容进行分析
            path: path.resolve(__dirname, '../dll/[name].manifest.json') //保存第三方模块的映射关系
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在package.json文件scripts中新增命令"build:dll": "webpack --config ./build/webpack.dll.js",

安装插件npm install add-asset-html-webpack-plugin --save往html-webpack-plugin上再增加打包出的vendor.dll.js。

    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(),
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendor.dll.js')
        }),
        new webpack.DllReferencePlugin({
            // 引入的第三方模块时会到manifest.json里找映射关系
            manifest: path.resolve(__dirname, '../dll/vendor.manifest.json')
        })
    ],
1
2
3
4
5
6
7
8
9
10
11
12
13

现在第三方模块可以一次打包放到wendor.dll.js文件里,再使用这些模块的时候,从dll文件引入而不是node_modules。webpack做打包的时候通过manifest.json文件对源代码分析,如果引入的模块在dll.js中存在就会直接引入,而不去node_modules中寻找了。

# 5,使用thread-loader,parallel-webpack,happypack多进程打包

webpack默认是通过node.js运行的,打包过程是单进程的。thread-loader,配置;happypack, 原理解析可以借助node的多进程帮助打包,利用多个CPU,打包速度会提升很多。在做多页应用打包的时候,可以使用parallel-webpack对多个页面一起进行打包。

注意:thread-loader 和 happypack 对于小型项目来说打包速度几乎没有影响,是因为它本身的额外开销,例如I/O,建议只在大型项目中使用,可以先测试再投入生产环境。

6, 合理使用sourceMap

soureMap越详细,打包速度越慢,应该合理配置。

# 5,多页面打包配置

1,entry里添加入口文件

entry: {
	main: './src/index.js',
	sub: './src/sub.js'
}
1
2
3
4

2, plugins里使用HtmlWebpackPlugin生成多个html文件,使用filename定义不同的文件名,使用chunks来指定要引入哪些文件

plugins: [
  new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html',
      chunks: ['runtime', 'vendors', 'index']
	}),
	new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'sub.html',
      chunks: ['runtime', 'vendors', 'sub']
	}),
]
1
2
3
4
5
6
7
8
9
10
11
12
Last Updated: 1/24/2020, 11:09:18 PM