# 核心概念

# 一、Webpack入门

# 1、安装webpack

项目目录下安装webpack,npm install webpack webpack-cli --save-dev。webpack-cli的作用是支持在命令行里面使用webpack命令。

命令行方式执行打包:npx webpack ./index.js (4.x方式)。npx 就是调用项目内部安装的模块,让项目内部安装的模块用起来更方便。npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。 由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。另外命令行才需要使用npx,如果是在项目内的script里,就可以直接用webpack,不需要加npx。

不建议使用全局安装的方式,因为不同项目,webpack的版本可能不一样。

# 2、几个命令

// 查看webpack的历史版本
npm info webpack
// 安装指定版本的webpack
npm install webpack@4.16.5 -D
1
2
3
4

# 3、使用配置文件

项目根目录下新建 webpack.config.js,即webpack的默认配置文件,默认配置文件名就是 webpack.config.js。

// webpack.config.js
const path = require('path')
module.exports = {
    entry: './index.js', // 入口文件
    //entry: {  等价于下面这个main
    //    main:'./index.js', // 入口文件
    // },
    output: {
        filename: 'bundle.js', // 打包后的文件的名字
        path: path.resolve(__dirname, 'bundle') // 打包后的目录,必须使用绝对路径,要用到path.resolve
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

执行打包npx webpack,webpack会自动用webpack.config.js作为配置文件。

如果配置文件的文件名不是webpack.config.js,而是webpackconfig.js,那么执行打包命令就需要用 --config参数了,指定配置文件的名字。

npx webpack --config webpackconfig.js
1

还有一种打包就是使用npm script

// package.json
{
  "name": "learn-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "webpack": "^4.39.3",
    "webpack-cli": "^3.3.7"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

打包用 npm run start

# 二、使用Loader打包静态资源

# 1、打包图片

# (1)file-loader

配置webpack,并安装file-loader模块,npm i file-loader -D

// 配置相关loader
module: {
    rules: [{
        test: /\.(jpg|png|jpeg)/,
        use:{
            loader:'file-loader',
            options: {
               name: '[name].[ext]', // 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
               outputPath:'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
            }
        }
    }]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果不配置 options.name,那么打包后图片名字格式如下:e3b63a0b1b4f4ca82cf2a5c686d58ae1.png 需要注意的是,引入一张图片,会返回图片打包后的地址,如下

import logo from "./images/logo.png";
var img = new Image();
img.src = logo;
document.body.append(img);
1
2
3
4

# (2)url-loader

url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。即base64 url。

module: {
    rules: [{
        test: /\.(jpg|png|jpeg)$/,
        use: {
            loader: 'url-loader', // 使用 url-loader 打包,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
            options: {
                name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                limit: 2048  // 图片小于2048字节就会打包成base64
            }
        }
    }]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

更多loader参阅官网

# 2、打包样式

# (1)支持普通css

安装这两个loader,npm i style-loader css-loader -D

style-loader能够在需要载入的html中创建一个<style></style>标签,标签里的内容就是CSS内容。

css-loader是允许在js中import一个css文件,会将css文件当成一个模块引入到js文件中。

# (2)支持sass语法

如果需要支持sass语法,则需要再安装 sass-loader 和 node-sass。

npm i sass-loader node-sass -D
1
// webpack配置
{
    test: /\.(css|scss)$/,
    use: ['style-loader','css-loader','sass-loader']
}
1
2
3
4
5

要注意这3个loader的顺序,是从右到左解析的,即先把scss翻译成css,然后插入到html的style标签里。

# (3)css厂商前缀

使用postcss-loader可以自动补充厂商前缀,npm i postcss-loader -D。 根目录下新增postcss.config.js文件,里面是postcss相关配置。

module.exports = {
    plugins: [
        require('autoprefixer')({
            // browsers: ["last 5 versions"]
            overrideBrowserslist: ["last 5 versions"]
        })
    ]
}
1
2
3
4
5
6
7
8

需要安装autoprefixer插件,npm i autoprefixer -D

webpack配置:

{
    test: /\.(css|scss)$/,
    use: ['style-loader','css-loader','sass-loader','postcss-loader']
}
1
2
3
4

# (4)scss文件支持@import语法和模块化css

可以给css-loader加上具体的配置

 {
    test: /\.(css|scss)$/,
    use: [
    'style-loader',
    {
        loader: 'css-loader',
        options: {
            importLoaders:2, // scss支持@import语法
            modules:true  // css支持模块化
        }
    }
    ,'sass-loader',
    'postcss-loader'
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • importLoaders:2
    作用是支持在scss文件里面使用@import语法引入其它scss文件。
  • modules:true
    作用是将css模块化,避免造成css全局污染。
//import './style/index.scss';  全局引入css
import style from './style/index.scss';  // 模块化引入css
var img = new Image();
img.src = logo;
// img.classList.add('avatar');  全局引入css时的写法
img.classList.add(style.avatar);  // 模块化引入css时的写法
document.body.append(img);
1
2
3
4
5
6
7

# 3、打包字体

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

# 三、使用plugins插件打包

plugins会在webpack打包时做一些事情。

# 1、html-webpack-plugin

html-webpack-plugin会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件里面。

安装 html-webpack-plugin

npm i html-webpack-plugin -D
1

webpack配置

const HtmlWebpackPlugin = require('html-webpack-plugin');

// 这里省略其它配置
plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    })
]
1
2
3
4
5
6
7
8

# 2、clean-webpack-plugin

清空打包后的目录。安装 clean-webpack-plugin,npm i clean-webpack-plugin -D

webpack配置

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

// 这里省略其它配置
plugins: [
    new CleanWebpackPlugin()
]
1
2
3
4
5
6

# 四、多入口配置和sourcemap

# 1、多入口配置

webpack配置

entry: {
    main: './src/index.js', // 第一个入口文件
    sub: './src/index.js', // 第二个入口文件
},
// 打包出口
output: {
    publicPath: 'http://cdn.com/',  // 在打包后生成的js地址前面加上域名,即http://cdn.com/main.js
    filename: '[name].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
    path: path.resolve(__dirname, 'dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
},
plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(),
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

打包后会生成main.js和sub.js,并且会插入到同一个html文件里。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="head"></div>
    <div id="footer"></div>
<script type="text/javascript" src="main.js"></script><script type="text/javascript" src="sub.js"></script></body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2、sourcemap

sourceMap 它是一个映射关系,它知道dist目录下main.js文件xx行实际上对应的是src目录下index.js文件中的第xx行。

  • webpack配置(关闭sourcemap):
mode: 'development', // 开发者模式
devtool:'none',  // 关闭sourcemap
1
2

关闭sourcemap后,如果源代码有错误。那么就只能在打包后的js文件里面找到错误,无法在源代码里定位错误。

  • webpack配置(开启sourcemap):
mode: 'development', // 开发者模式
devtool:'source-map',  // 开启sourcemap
1
2

开启sourcemap后,如果源代码有错误,那么可以直接定位到源代码里的位置。打包后会生成main.js和main.js.map。其中main.js.map就是一个存储映射关系的文件。

sourcemap参数

1、source-map

会生成map格式的文件,里面包含映射关系的代码.

2、inline-source-map

不会生成map格式的文件,包含映射关系的代码会放在打包后生成的代码中.

3、inline-cheap-source-map

cheap有两种作用:一是将错误只定位到行,不定位到列。二是映射业务代码,不映射loader和第三方库等。会提升打包构建的速度。

4、inline-cheap-module-source-map

module会映射loader和第三方库.

5、eval

用eval的方式生成映射关系代码,效率和性能最佳。但是当代码复杂时,提示信息可能不精确。

sourcemap最佳实战

mode: 'development', // 开发模式
devtool:'cheap-module-eval-source-map',  // 开启sourcemap
1
2

速度最快,打包错误提示最全。

mode: 'development', // 生产模式
devtool:'cheap-module-source-map',  // 开启sourcemap,或者直接去掉
1
2

# 五、自动编译,优先使用webpackDevServer

每次要编译代码时,手动运行 npm run build 就会变得很麻烦。webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:

  • webpack Watch Mode
  • webpack-dev-server
  • webpack-dev-middleware

# 1、--watch参数

webpack --watch,加--watch参数,那么webpack监听到了src目录下的文件变化就会自动重新打包,但不会自动刷新页面。

# 2、webpackDevServer

不但会监听文件变化自动重新打包,也会自动刷新页面,也可以自动打开浏览器,还可以支持跨域。安装 npm i webpack-dev-server -D

配置:

// webpack.config.js
plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(),
],
devServer: {  // 配置devServer
    contentBase: './dist',  // 打包后起服务的目录名
    open:true,   // 自动打开浏览器
    proxy: {
      '/api': 'http://localhost:3000'  // 跨域代理转发
    },
    port: 8090  // 指定端口号
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// package.json
"scripts": {
    "server": "webpack-dev-server",  // 配置webpack-dev-server
    "watch": "webpack --watch"
},
1
2
3
4
5

这样就会起一个服务,端口默认是8080。

# 3、利用node来实现

安装express和webpack-dev-middleware,npm i express webpack-dev-middleware -D

配置webpack

// webpack.config.js
 output: {
    publicPath: '/',  // ******在打包后生成的js地址前面加上 / 根路径,确保打包生成的文件路径不会有问题******
    filename: '[name].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
    path: path.resolve(__dirname, 'dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
}
1
2
3
4
5
6
// package.json
 "scripts": {
    "middware": "node server.js"
  },
1
2
3
4

加 publicPath: '/'

publicPath 也会在服务器脚本用到,以确保文件资源能够在 http://localhost:3000 下正确访问。

// 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

# 六、模块热替换(HMR)

模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。

# webpack配置

// webpack.config.js
const webpack = require('webpack');  // 引入webpack
// 省略其它配置
// 配置devServer
devServer: {  // 配置devServer
    contentBase: './dist',  // 打包后起服务的目录名
    open: true,   // 自动打开浏览器
    proxy: {
        '/api': 'http://localhost:3000'  // 跨域代理转发
    },
    port: 8090,  // 指定端口号
    hot: true,  // ******开启热模块替换******
    hotOnly:false  // ******如果热模块替换没有生效,则让浏览器自动刷新******
}

// 配置plugins
plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(),
    new webpack.NamedModulesPlugin(),  // 更容易查看要修补(patch)的依赖
    new webpack.HotModuleReplacementPlugin()  // 使用热模块替换插件
],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

注意,我们还添加了 NamedModulesPlugin,以便更容易查看要修补(patch)的依赖。

对于js文件的改变,需要我们使用 module.hot.accept 手动更新,因为js文件默认没有实现HMR。

// index.js
import number from './number';
number();

if (module.hot) {
    module.hot.accept('./number.js', function () {
        console.log('Accepting the updated printMe module!');
        number();
    })
}

// number.js
export default ()=>{
    console.log('test')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

对于css文件,因为css-loader底层实现了HMR,所以就会自动刷新。

# 七、使用babel处理ES6

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

babel-loader:将webpack和loader打通桥梁,不会转义ES6。

@babel/core: babel的核心库。

@babel/preset-env:将ES6转义成ES5。

@babel/polyfill:低版本浏览器打补丁,比如补充Promise这些东西,还有map方法这些东西,低版本浏览器比如ie就没有内置这些东西。

# 配置webpack

// webpack.config.js
module: {
  rules: [
    {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
            presets: ["@babel/preset-env"]
        }
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
import "@babel/polyfill";   // 引入polyfill,兼容低版本浏览器

const arr=[
    new Promise(()=>{}),
    new Promise(()=>{})
]

arr.map(item=>{
    console.log(item);
})
1
2
3
4
5
6
7
8
9
10
11

# babel/polyfil按需引入

@babel/polyfil会自动加入所有兼容的代码,如果需要按需引入兼容的代码,则需要安装core-js,

npm install --save core-js@3。 webpack配置:

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

源码中不需要引入@babel/polyfill

// index.js
// import "@babel/polyfill";   // 不需要引入polyfill,因为已经按需引入了

const arr=[
    new Promise(()=>{}),
    new Promise(()=>{})
]

arr.map(item=>{
    console.log(item);
})
1
2
3
4
5
6
7
8
9
10
11

# 根据浏览器版本决定是否要将ES6转义成ES5

因为高版本浏览器已经支持了ES6,所以需要配置在针对高版本不需要转义。

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

# 开发第三方模块时options的写法

如果我们写的是业务代码时,则使用上面的options即可,如果我们写的是第三方模块或者库代码时,则options要用下面的方案。因为上面的代码会污染全局变量,而下面的代码不会。安装相关模块,npm install --save-dev @babel/plugin-transform-runtime @babel/runtime @babel/runtime-corejs2

webpack配置:

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
13
14

使用.babelrc,即把options里面的配置写到.babelrc文件里面。

.babelrc文件:

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

# 八、支持React的打包

安装相关模块,npm install --save-dev @babel/preset-react react react-dom

配置 .babelrc

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "edge": "17",
                    "firefox": "60",
                    "chrome": "67",
                    "safari": "11.1",
                },
                "useBuiltIns": "usage",
                "corejs": "3", // 声明corejs版本
            }
        ],
        [
            "@babel/preset-react"   // 支持react的相关语法
        ]
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

完整的 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 = {
    //entry: './src/index.js', // 入口文件,等价于下面的main
    entry: {
        main: './src/index.js', // 第一个入口文件
        sub: './src/index.js', // 第二个入口文件
    },
    // 打包出口
    output: {
        //publicPath: '/',  webpackDevServer时会用到
        // publicPath: 'http://cdn.com/',  // 在打包后生成的js地址前面加上域名,即http://cdn.com/main.js
        filename: '[name].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
        path: path.resolve(__dirname, 'dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
    },
    mode: 'development', // 开发者模式
    //devtool:'none',  // 关闭sourcemap,无法定位语法错误发生在源代码里第几行
    //devtool: 'source-map',
    devtool: 'eval-source-map',
    // 配置相关loader
    module: {
        rules: [
            {
                test: /\.(jpg|png|jpeg)$/,
                use: {
                    // loader: 'file-loader',  // 使用 file-loader 打包
                    // options: {
                    //     name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                    //     outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                    // }
                    loader: 'url-loader', // 使用 url-loader 打包,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
                    options: {
                        name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                        outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                        limit: 2048  // 图片小于2048字节就会打包成base64
                    }
                }
            },
            {
                test: /\.(css|scss)$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
            {
                test: /\.(eot|ttf|svg)$/,
                use: {
                    loader: 'file-loader'  // 使用 file-loader 打包
                }
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    // 开发业务代码时的写法
                     /*
                    //presets: ["@babel/preset-env"]
                    presets: [['@babel/preset-env', {
                        targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                        },
                        useBuiltIns: 'usage',
                        corejs: "2", // 声明corejs版本
                    }]]
                    */
                    // 开发第三方模块时的写法
                    /*
                    "plugins": [
                        [
                            "@babel/plugin-transform-runtime",
                            {
                                "absoluteRuntime": false,
                                "corejs": 2,
                                "helpers": true,
                                "regenerator": true,
                                "useESModules": false
                            }
                        ]
                    ]
                    */
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(),
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {  // 配置devServer
        contentBase: './dist',  // 打包后起服务的目录名
        open: true,   // 自动打开浏览器
        proxy: {
            '/api': 'http://localhost:3000'  // 跨域代理转发
        },
        port: 8090,  // 指定端口号
        hot: true,  // 开启热模块替换
        hotOnly: 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
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
Last Updated: 4/20/2020, 3:43:25 PM