前言

从七月初到九月中旬这段时间都在准备校招,抽不出太多的时间来写博客,九月中旬拿到最后一家offer后休息了两天,新的一周又开始了每周一篇博客记录。写这篇博客的初衷是因为在秋招的面试过程中被问到过相关知识,发现自己在 webpack 原理方面的理解还不够透彻,想通过这种记录的方式,来进一步的理解和巩固相关知识。这篇文章我暂时准备从三个方面来写,分别是 webpack的配置webpack的优化webpack的原理。对我来说也算是一个不小的挑战。希望这三篇文章对你在webpack相关知识能所有提升。

相关代码我会放在我的 git仓库内,如有需要,请自提。

什么是webpack?

我们在学习一门技术之前,首先需要先了解这个技术是什么?

从webpack的官方文档上的描述来看,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

从上面的官方文档的介绍来看,在前端模块化的日益发展的趋势下,我们可以把webpack简单理解为是一个javascript的模块打包工具。

webpack有四个核心概念

  • 入口(entry)
  • 输出(output)
  • loader
  • 插件(plugin)

下面我讲从这四个概念来进行介绍

开始的第一次的打包

首先,我们新建一个文件夹 webpack-demo,然后通过 npm 的方式来初始化一个项目,这里由于是演示,就直接使用 npm init -y

image-20200921200305697

我们使用webpack,便需要安装webpack,安装webpack的方式有两种,一种是全局安装(不推荐),一种是局部安装。因为全局安装会产生很多版本不兼容的问题,在本文中就不介绍全局安装的方式了。

在上面我们创建的文件中,进行安装 webpack,此demo通过yarn来进行包管理,在控制台输入 yarn add webpack webpack-cli

接着,我们在根目录创建一个 src的文件夹,后面我们都会通过webpack对里面的内容进行打包,然后再创建一个 webpack.config.js 文件,作为打包配置文件。

在src目录下,我们创建一个 index.js 的文件,然后任意输入代码,然后在 webpack.config.js 中输入下面这段代码,稍后我将一一解释。

const path = require('path')
module.exports = {
    mode:"production",
    entry:'./src/index.js',
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    }
}

上面通过 commonJs 的规范来进行导入导出,在 module.exports 内,我们可以看到上面提到的两个核心概念,entryoutput。首先,entry就是整个webpack的打包入口,output是打包后,将输出文件的放置位置,我们可以看到,上面使用了nodepath模块,所以,本文需要你掌握webpack相关的知识。在output 中,filename是输出的文件名,path是输出文件的文件路径。上面还有一个 mode的配置项,它一般有两个值,production和development,关于这两项的区别不是本文讨论重点,具体区别可以上webpack的官方文档进行查看。

接下来,我们在控制台上输入 npx webpack,就可以看到新增了一个dist的文件夹,点进去看就可以看到,有一个bundle.js的文件,这就是我们打包过后的文件了。写到这儿,你就已经完成了一个非常简单的webpack打包了。

你可能有点疑惑,平时我们都是通过npm或者yarn来进行打包的,怎么这里是用 npx 来进行打包。所以,我们现在打开package.json文件,发现里面有个scripts的配置项,我们在里面加入 "start": "webpack" ,接下来我们使用yarn start就相当于 npx webpack,可能在这儿你还有一点疑问,在scripts里面我只写了webpack,没有写npx,它如何进行 “等价代换”的,其实在sctipts里面直接写webpack,实际上它会自动得在当前目录下的node_modules内进行查找。所以我们在这儿不用写npx,如果非要写,也不是不行。

静态资源的处理

在上面,我们通过webpack进行了简单的打包,但在我们日常开发过程中,经常会遇到像图片、css文件等一些的静态资源文件,这个时候,我们就需要通过webpack的另一个核心概念loader 对其进行打包。注:因为本文采用循序渐进的方式来进行书写,所以入口index.html文件先手动创建,在后面,我再一一进行解释。

图片资源处理

在dist内,我们创建一个index.html的文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
</body>
<script src="bundle.js"></script>
</html>

src/index.js 内容如下:

console.log('hello world');

接下来在控制台输入 yarn start

然后在浏览器上打开这个 index.html 文件,可以看到在控制台输出了 hello world

image-20200921213216015

那如果我们想在页面上显示一张图片呢?首先找一张图片,放在src下面新增的img文件夹里面,于是修改一下 src/index.js的内容:

import Img from './img/img.png'

let img = new Image()
img.src = Img

let root = document.getElementById('root')
root.appendChild(img)

然后我们在控制台运行 yarn start,发现出现了下面的报错

image-20200921213606949

所以对于图片资源我们需要做一些处理,修改webpack.config.js 的内容如下:

const path = require('path')
module.exports = {
    mode: "production",
    entry: './src/index.js',
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/,
                use: ['file-loader']
            }
        ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

因为上面使用了 file-loader 所以,使用yarn add file-loader 安装这个包。然后使用 yarn start 打包。

这时,我们可以发现,在dist文件夹下,新增了一个图片的文件,然后打开index.html 可以发现图片显示成功了。

在这里,再介绍一个loader - url-loader 使用方法和 file-loader 类似,不过多了一些配置项。修改

webpack.config.js 的内容如下:

const path = require('path')
module.exports = {
    mode: "production",
    entry: './src/index.js',
    module: {
        rules: [
            {
                test: /\.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        //输出的文件名称,具体配置可以查看官网
                        name: '[name]_[hash].[ext]',
                        //输出文件路径
                        outputPath: "images/",
                        //文件大小最小限制单位为字节,未超过限定大小,将以base64的方式编码
                        limit: 204800
                    }
                }
            }
        ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

样式处理

在前端开发过程中,难免会遇到一些css的编写,在webpack 中编写css,我们也需要做一些loader,来处理css文件。

例如在我们这个demo中,我们在src 目录下新增一个index.css的文件,然后修改这个图片的 heightwidth ,并且给这个img标签添加类 pic 如下:

index.js

import Img from './img/img.png'
import './index.css'
let img = new Image()
img.src = Img
img.classList.add('pic')

let root = document.getElementById('root')
root.appendChild(img)

index.css

.pic{
    width: 100px;
    height: 100px;
}

然后使用 yarn start进行打包,发现结果如下:

image-20200922093653504

所以,针对于css文件,我们还需要单独处理,这里需要用到两个 loader :css-loader、style-loader。所以我们安装一下这两个 loaderyarn add style-loader css-loader,然后打开 webpack.config.js 配置一下,如下:

const path = require('path')
module.exports = {
    mode: "production",
    entry: './src/index.js',
    module: {
        rules: [
            {
                test: /\.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name]_[hash].[ext]',
                        outputPath: "images/",
                        limit: 204800
                    }
                }
            },
            {
                test: /\.css$/,
                //注意顺序
                use: ['style-loader','css-loader']
            },
        ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

现在我们查看页面,发现样式生效了,这下就成功了,但是在实际开发过程中,会使用到很多css3的样式,我们一般都需要为这些样式添加厂商前缀,例如下面这个例子:

.pic {
    width    : 100px;
    height   : 100px;
    transform: translate(100px, 100px);
}

我们打包后发现样式正确生效了,针对于这种新特性,我们一般会给它加上厂商前缀,这里我们就可以使用另一个loaderpostcss-loader以及一个plugin:autoprefixer,plugin后面再进行细致的讲解,这里就演示一下。我们使用这两个loader和plugin,首先也要进行安装 yarn add postcss-loader autoprefixer,然后再配置一下 webpack.config.js

const path = require('path')
module.exports = {
    mode: "production",
    entry: './src/index.js',
    module: {
        rules: [
            {
                test: /\.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name]_[hash].[ext]',
                        outputPath: "images/",
                        limit: 204800
                    }
                }
            },
            {
                test: /\.css$/,
                //注意顺序
                use: ['style-loader','css-loader','postcss-loader']
            },
        ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

使用这个插件的话,需要再跟目录新建一个文件 postcss.config.js,内容如下:

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

因为针对于不同版本的浏览器,我们还需要在 package.json 配置一下:

 "browserslist": [
    "defaults",
    "not ie < 11",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]

然后运行 yarn start ,因为版本的问题,你可能会遇到下面这个报错(如果没有报错,请自动忽略即可):

image-20200922101543882

如果遇到上面的报错,你可以重新安装一下 autoprefixer 9.x的版本,yarn add autoprefixer@9.0.0然后你就可以发现自动帮我们把厂商前缀加上了。

使用less和sass等预处理器的话,只需要添加对应的loader进行配置即可less(less less-loader),sass(sass sass-loader node-sass),这里就不用过多篇幅进行解释了。

让你的打包更便捷

html-webpack-plugin

在上面,我们都是手动在dist目录下新增的index.html 文件,如果每次打包都需要新增或者修改这个文件,岂不是很麻烦,所以我们可以使用 webpackplugin html-webpack-plugin 来解决这种问题,还是按照惯例,使用plugin就首先安装plugin,控制台输入 yarn add html-webpack-plugin

接下来打开 webpack.config.js 进行配置:

const path = require('path')
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
    mode: 'production',
    entry: "./src/index.js",
    module: {
        rules: [{
            test: /\.png$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: "images/",
                    limit: 204800
                }
            }
        },
        {
            test: /\.less$/,
            use: ['style-loader', 'css-loader', 'less-loader', 'postcss-loader']
        },
        {
            test: /\.scss$/,
            use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
        },]
    },
    plugins: [
        new HtmlWebpackPlugin()
    ],
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

然后我们删除dist文件夹,再运行 yarn start,然后你会发现在根目录会新增一个dist 文件夹,里面会有一个 index.html 的文件,文件内部,也通过 script 标签引入了打包生成的 bundle.js 文件, 这就是 html-webpack-plugin的作用。

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

但是现在我们打开 index.html 文件,会发现页面没有显示,这是因为在 src/index.js 文件中,我们都是基于id为root的结点来加载的,所以,我们可以在index.html 文件中手动新增root结点,但是这种方法不怎么适用,每次打包都要新增,所以我们可以使用 html-webpack-plugintemplate 配置来解决。

首先在根目录创建一个 public/index.html,接下来打包生成的内容都会以这个文件内容为模板进行生成:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模板</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

然后修改 webpack.config.js的内容如下即可:

 new HtmlWebpackPlugin({
            template:'public/index.html'
 })

clean-webpack-plugin

在上面,重新打包后,文件名却更换了,之前的文件是不会被删除的,如果打包次数多,会造成文件冗余,这里我们就可以使用到 clean-webpack-plugin 这个插件来帮我们自动删除之前打包的文件。

首先安装 clean-webpack-pluginyarn add clean-webpack-plugin

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

module.exports = {
    mode: 'production',
    entry: "./src/index.js",
    module: {
        rules: [{
            test: /\.png$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: "images/",
                    limit: 204800
                }
            }
        },
        {
            test: /\.less$/,
            use: ['style-loader', 'css-loader', 'less-loader', 'postcss-loader']
        },
        {
            test: /\.scss$/,
            use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
        },]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),

    ],
    output: {
        filename: 'dist.js',
        path: path.resolve(__dirname, 'dist')
    }
} 

然后运行 yarn start ,我们会发现之前的bundle.js 文件被自动删除了。

自动打包的奇淫巧计

如果你按照我的操作看到了这里,你是否觉得每次打包都要输入 yarn start 的命令,还要去重新打开 index.html ,会不会觉得很麻烦,下面我给你介绍三种方式解决这种自动化打包的问题

  1. 使用webpack自带的 --watch

    打开 package.json 文件,找到最开始我们新增的那个 script 把里面的内容修改一下:

     "start": "webpack --watch"

    运行 yarn start,现在你试着修改一下 src/index.js 里面的内容,是不是发现内容修改保存后, webpack自动帮你打包啦!不过这种方式用得不是很多。

  2. dev-server

    现在使用的最多的便是这个 webpack-dev-server,无论是像 vue 的 vue-cli 还是react的 create-react-app 等脚手架,都是使用的 webpack-dev-server 。老规矩,使用插件之前都需安装,yarn add webpack-dev-server

    然后打开 webpack.config.js 进行配置:

    const path = require('path')
    const HtmlWebpackPlugin = require("html-webpack-plugin")
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    
    module.exports = {
        mode: 'production',
        entry: "./src/index.js",
        devServer:{
            //在什么文件开启一个服务器,一般指向打包生成的文件
            contentBase:'./dist',
            // 默认打包完成后,打开浏览器
            open:true,
            // 服务器的端口号,默认是8080
            port:9999,
            // 本地代理配置
            proxy:{
    
            }
        },
        module: {
            rules: [{
                test: /\.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name]_[hash].[ext]',
                        outputPath: "images/",
                        limit: 204800
                    }
                }
            },
            {
                test: /\.less$/,
                use: ['style-loader', 'css-loader', 'less-loader', 'postcss-loader']
            },
            {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            },]
        },
        plugins: [
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                template: './public/index.html'
            }),
        ],
        output: {
            filename: '[name].js',
            path: path.resolve(__dirname, 'dist')
        }
    } 
  3. 自己实现一个server

    可以通过node来写个简易的本地服务器,这里就不讲解了,如有兴趣,可以自行百度。

让你的代码跑在低版本浏览器

我们知道,js的更新速度非常快,但是浏览器厂商不可能随着js版本的更新而更新,但是开发者又要使用到这些新特性,那怎么办呢?这就需要用到babel来进行解析这些新特性。一般需要用到 babel-loader@babel/core 这两个库。具体的解析流程是一个非常复杂的过程,后面我写完三篇webpack后,我再单独写一篇进行详细解释。

现在我们简单得写一下 babel 处理,首先我们把项目工程文件简化一下,将 src 下面只留下index.js 这个文件。然后修改一下 index.js的内容,尽量使用比较新的语法,如下:

let p = new Promise((resolve,rejcet)=>{
    resolve(1)
})

p.then(res=>{
    console.log(res);
})

老规矩,使用依赖首先安装依赖:yarn add babel-loader @babel/core,然后配置 webpack.config.js 即可。

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

module.exports = {
    mode: 'production',
    entry: "./src/index.js",
    devServer: {
        //在什么文件开启一个服务器,一般指向打包生成的文件
        contentBase: './dist',
        // 默认打包完成后,打开浏览器
        open: true,
        // 服务器的端口号,默认是8080
        port: 9999,
        // 本地代理配置
        proxy: {

        }
    },
    module: {
        rules: [{
            test: /\.png$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: "images/",
                    limit: 204800
                }
            }
        },
        {
            test: /\.less$/,
            use: ['style-loader', 'css-loader', 'less-loader', 'postcss-loader']
        },
        {
            test: /\.js$/,
            //排除node_modules
            exclude: '/node_modules/',
            loader: 'babel-loader'
        },
        {
            test: /\.scss$/,
            use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
        },]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),
    ],
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
} 

你觉得就这样就可以了吗?不, babel-loader不会将es6的语法转义为es5的语法,我们还需要借助其他的模块来使用,babel-loader 的作用仅仅是将这两者连接在一起。我们就可以使用 @babel/preset-env 来将es6转换为es5代码。 yarn add @babel/preset-env,然后我们需要配置一下,注意:为了打包方便查看结果,我们把webpack-dev-server 换成了 webpack

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

module.exports = {
    mode: 'development',
    entry: "./src/index.js",
    devServer: {
        //在什么文件开启一个服务器,一般指向打包生成的文件
        contentBase: './dist',
        // 默认打包完成后,打开浏览器
        open: true,
        // 服务器的端口号,默认是8080
        port: 9999,
        // 本地代理配置
        proxy: {

        }
    },
    module: {
        rules: [{
            test: /\.png$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: "images/",
                    limit: 204800
                }
            }
        },
        {
            test: /\.less$/,
            use: ['style-loader', 'css-loader', 'less-loader', 'postcss-loader']
        },
        {
            test: /\.js$/,
            //排除node_modules
            exclude: '/node_modules/',
            loader: 'babel-loader',
            options: {
                presets: ["@babel/preset-env"]
            }
        },
        {
            test: /\.scss$/,
            use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
        },]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),
    ],
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
} 

然后运行 yarn start ,我们打开dist下面的 mian.js 文件,翻到最底部,看到我们通过箭头函数定义的函数,被转义为了普通函数的定义方法。

image-20200925134050537

但是现在还有一些问题,es5里面没有 promise 这种特性啊,怎么让他在低版本上面运行啊,这个时候就需要用上另一个秘籍了-polyfill,安装 polyfillyarn add @babel-polyfill

简单的使用方法就是,在我们的业务代码也就是 index.js 顶部直接引入 @babel-polyfill (后面配置了useBuiltIns 可以不用手动引入) ,如下:

import '@babel/polyfill'

let p = new Promise((resolve,rejcet)=>{
    resolve(1)
})

p.then(res=>{
    console.log(res);
})

然后我们打包试试,发现promise这些新特性已经被处理过了,但是我们发现,这次打包出来的文件大小比之前打包的文件大小相差很大,原因是 polyfill 帮我们实现了所以新特性的语法,但在我们这个demo里面,我们只需要它帮助我们实现 promise 即可,所以我们不能简单地通过引入的方式来处理,还需要做一些配置。

image-20200926092752831

打开 webpack.config.js,修改presets

   {
            test: /\.js$/,
            //排除node_modules
            exclude: '/node_modules/',
            loader: 'babel-loader',
            options: {
                presets: [["@babel/preset-env", {
                    useBuiltIns: "usage"
                }]]
            }
        },

然后我们重新打包看一下,发现文件大小明显比之前小很多。

image-20200926093529170

最后更新: 2020年09月26日 10:35

原始链接: http://www.kweku.top/2020/09/22/14.webpack1/