webpack 基础配置解析

针对webpack,是大家(前端开发)在日常的开发中都会遇见的,通过书写的方式输出,学习到的关于前端工程化的小知识点的总结和学习,形成自己的知识体系

概念

webpack官网定义:

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

webpack 资源打包

在开始了解webpack配置前,首先需要理解四个核心概念

  1. 入口(entry):webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
  2. 输出(output):webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。
  3. loader:能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
  4. 插件(plugins):用于执行范围更广的任务。

安装及构建

// npm 安装
npm install webpack webpack-cli -g

// yarn 安装
yarn global add webpack webpack-cli 

安装好后,可以在不适用配置文件的方法,直接对文件进行打包:
webpack <entry> [<entry>] -o <output>

新建一个项目,就一个入口文件,测试webpack打包:


项目结构

运行打包命令:
webpack index.js

webpack构建

在这里我们会看见一个WARNING的信息,这是因为没有设置mode,我们只需要加一个参数-p即可:

webpack -p index
webpack构建

这样默认会生成一个dist文件夹,里面有个main.js文件:

webpack构建

有了入口文件我们还需要通过命令行定义一下输入路径dist/bundle.js

 webpack -p index.js -o dist/bundle.js
webpack构建

webpack 配置文件

命令行的打包构建方式仅限于简单的项目,如果在生产中,项目复杂,多个入口,我们就不可能每次打包都输入一连串的入口文件地址,也难以记住;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:

webpack [--config webpack.config.js]

配置文件默认的名称就是webpack.config.js,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的额文件,通过--config来进行更换:

// 开发环境
webpack --config webpack.config.dev.js

// 生产环境
webpack --config webpack.config.prod.js

多种配置类型

config配置文件通过module.exports导出一个配置对象:

// webpack.config.js
const path = require('path')

const resolve = function (dir) {
    return path.resolve(__dirname, dir)
}

module.exports = {
    entry: {
        app: resolve('../index.js')
    },
    output: {
        filename: '[name].[hash:8].js',
        path: resolve('../dist')
    },
}

除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们可以通过ENV来区分不同环境:

const path = require('path')

const resolve = function (dir) {
    return path.resolve(__dirname, dir)
}

module.exports = function(ENV, argv) {
    return {
        // 其他配置
        entry: resolve('../index.js'),
        output: {}
    }
}

还可以导出为一个Promise,用于异步加载配置,比如可以动态加载入口文件:

entry: () => './demo'

或

entry: () => new Promise((resolve) => resolve(['./demo', './demo2']))

入口

正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:

module.exports = {
    entry: resolve('../index.js')
}

但是我们项目中可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了:

module.exports = {
    entry: [
        '@babel/polyfill',
        resolve('../index.js')
    ]
}

如果我们项目中有多个入口起点,则就需要用到对象形式了:

// webpack 就会构建两个不同的依赖关系
module.exports = {
    entry: {
        app: resolve('../index.js'),
        share: resolve('../share.js')
    }
}

输出

output选项用来控制webpack如何输入编译后的文件模块;虽然可以有多个entry,但是只能配置一个output

module.exports = {
    entry: resolve('../index.js'),
    output: {
        filename: 'index.js',
        path: resolve('../dist')
    },
}

这里我们配置了一个单入口,输出也就是index.js;但是如果存在多入口的模式就行不通了,webpack会提示Conflict: Multiple chunks emit assets to the same filename,即多个文件资源有相同的文件名称;webpack提供了占位符来确保每一个输出的文件都有唯一的名称:

module.exports = {
    entry: {
        app: resolve('../index.js'),
        share: resolve('../index.js'),
    },
    output: {
        filename: '[name].bundle.js',
        path: resolve('../dist')
    },
}

这样webpack打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的bundle文件;还有以下不同的占位符字符串:

占位符 描述
[hash] 模块标识符(module identifier)的 hash
[chunkhash] chunk 内容的 hash
[name] 模块名称
[id] 模块标识符
[query] 模块的 query,例如,文件名 ? 后面的字符串

在这里引入modulechunkbundle的概念,上面代码中也经常会看到有这两个名词的出现,那么他们三者到底有什么区别呢?首先我们发现module是经常出现在我们的代码中,比如module.exports;而chunk经常和entry一起出现,bundle总是和output一起出现。

  • module:我们写的源码,无论是commonjs还是amdjs,都可以理解为一个个的module
  • chunk:当我们写的module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk文件, webpack 会对这些chunk文件进行一些操作
  • bundle:webpack处理好chunk文件后,最后会输出bundle文件,这个bundle文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。

我们通过下面这张图看可以加深对这三个概念的理解:


enter description here

hash、chunkhash、contenthash

理解了chunk的概念,相信上面表中chunkhash和hash的区别也很容易理解了;

  • hash:是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
  • chunkhash:跟入口文件的构建有关,根据入口文件构建对应的chunk,生成每个chunk对应的hash;入口文件更改,对应chunk的hash值会更改。
  • contenthash:跟文件内容本身相关,根据文件内容创建出唯一hash,也就是说文件内容更改,hash就更改。

模式

在webpack2和webpack3中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在webpack4中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack还会发出警告。

module.exports = {
    mode: 'development/production'
}

开发模式是告诉webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。

生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle。看到这里用到了很多Plugin,不用慌,下面我们会一一解释他们的作用。

相信很多童鞋都曾有过疑问,为什么这边DefinePlugin定义环境变量的时候要用JSON.stringify("production"),直接用"production"不是更简单吗?

我们首先来看下JSON.stringify("production")生成了什么;运行结果是""production"",注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"多嵌套了一层引号

我们可以简单的把DefinePlugin这个插件理解为将代码里的所有process.env.NODE_ENV替换为字符串中的内容。假如我们在代码中有如下判断环境的代码:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.DefinePlugin({ 
      "process.env.NODE_ENV": "production"
    }),
  ]
}
// index.js
if (process.env.NODE_ENV === 'production') {
    console.log('production');
}

这样生成出来的代码就会编译成这样:

//dist/bundle.js
//代码中并没有定义production变量
if (production === 'production') {
    console.log('production');
}

但是我们代码中可能并没有定义production变量,因此会导致代码直接报错,所以我们需要通过JSON.stringify来包裹一层:

//webpack.config.js
module.exports = {
  plugins: [
    new webpack.DefinePlugin({ 
      //"process.env.NODE_ENV": JSON.stringify("production")
      //相当于
      "process.env.NODE_ENV": '"production"'
    }),
  ]
}
//dist/bundle.js
if ("production" === 'production') {
    console.log('production');
}

生成HTML文件(html-webpack-plugin)

在上面的代码中我们发现都是手动来生成index.html,然后引入打包后的bundle文件,但是这样太过繁琐,而且如果生成的bundle文件引入了hash值,每次生成的文件名称不一样,因此我们需要一个自动生成html的插件;首先我们需要安装这个插件:
yarn add html-webpack-plugin -D 或者 npm install html-webpack-plugin -D

使用:

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

module.exports = {
    // 其他代码
    plugins: [
        new HtmlWebpackPlugin({
            // 模板文件
            template: resolve('../public/index.html'),
            // 生成的html名称
            filename: 'index.html',
            // icon
            favicon: resolve('../public/logo.ico')
        }),
    ]
}

webpack loader

loader 用于对模块的源代码进行转换。默认webpack只能识别commonjs代码,但是我们在代码中会引入比如vue、ts、less等文件,webpack就处理不过来了;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。

module.rules允许我们配置多个loader,能够很清晰的看出当前文件类型应用了哪些loader。

module.exports = {
      module: {
            rules: [
                  { test: /\.css$/, use: 'css-loader' },
                  { test: /\.ts$/, use: 'ts-loader' }
            ]
      }
};

loader 特性

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

loader 通过(loader)预处理函数,为 JavaScript 生态系统提供了更多能力。 用户现在可以更加灵活地引入细粒度逻辑,例如压缩、打包、语言翻译和其他更多。

babel-loader

兼容低版本浏览器的痛相信很多童鞋都经历过,写完代码发现自己的js代码不能运行在IE10或者IE11上,然后尝试着引入各种polyfill;babel的出现给我们提供了便利,将高版本的ES6甚至ES7转为ES5;我们首先安装babel所需要的依赖:
yarn add -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime

由于babel-loader的转译速度很慢,在后面我们加入了时间插件后可以看到每个loader的耗时,babel-loader是最耗时间;因此我们要尽可能少的使用babel来转译文件,正则上使用$来进行精确匹配,通过exclude将node_modules中的文件进行排除,include将只匹配src中的文件;可以看出来include的范围比exclude更缩小更精确,因此也是推荐使用include。

// 省略其他代码
module: {
      rules: [
            {
                  test: /\.js$/,
                  exclude: /node_modules/,
                  include: [resolve('src')]
                  use: {
                    loader: 'babel-loader',
                    options: {
                      presets: [
                        ['@babel/preset-env', { targets: "defaults" }]
                      ],
                      plugins: ['@babel/plugin-proposal-class-properties']
                    }
              }
            }
      ]
}

file-loader 和 url-loader

file-loaderurl-loader都是用来处理图片、字体图标等文件;url-loader工作时分两种情况:当文件大小小于limit参数,url-loader将文件转为base-64编码,用于减少http请求;当文件大小大于limit参数时,调用file-loader进行处理;因此我们优先使用url-loader

module: {
        rules: [
            {
                test: /\.(jpe?g|png|gif)$/i, //图片文件
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            // 10K
                            limit: 1024,
                            //资源路径
                            outputPath: resolve('../dist/images')
                        },
                    }
                ],
                exclude: /node_modules/
            },
        ]
    }

搭建webpack开发环境

在上面我们都是通过命令行打包生成 dist 文件,然后直接打开html或者通过static-server来查看页面的;但是开发中我们写完代码每次都来打包会严重影响开发的效率,我们期望的是写完代码后立即就能够看到页面的效果;webpack-dev-server就很好的提供了一个简单的web服务器,能够实时重新加载。

webpack-dev-server的用法和wepack一样,只不过他会额外启动一个express的服务器。我们在项目中webpack.config.dev.js配置文件对开发环境进行一个配置:

module.exports = {
    mode: 'development',
    plugins: [
        new Webpack.HotModuleReplacementPlugin()
    ],
    devtool: 'cheap-module-eval-source-map',
    devServer: {
        // 端口
        port: 3300,
         // 启用模块热替换
        hot: true,
        // 自动打开浏览器
        open: true,
        // 设置代理
         proxy:{
             "/api/**":{
                 "target":"http://127.0.0.1:8075/",
                 "changeOrigin": true
            }
        }
    }
}

通过命令行webpack-dev-server来启动服务器,启动后我们发现根目录并没有生成任何文件,因为webpack打包到了内存中,不生成文件的原因在于访问内存中的代码比访问文件中的代码更快。

我们在public/index.html的页面上有时候会引用一些本地的静态文件,直接打开页面的会发现这些静态文件的引用失效了,我们可以修改server的工作目录,同时指定多个静态资源的目录:

contentBase: [
  path.join(__dirname, "public"),
  path.join(__dirname, "assets")
]

热更新(Hot Module Replacemen简称HMR)是在对代码进行修改并保存之后,webpack对代码重新打包,并且将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样就能在不刷新浏览器的前提下实现页面的更新。

webpack plugins

上面介绍了DefinePlugin、HtmlWebpackPlugin等很多插件,我们发现这些插件都能够不同程度的影响着webpack的构建过程,下面还有一些常用的插件:

clean-webpack-plugin
clean-webpack-plugin用于在打包前清理上一次项目生成的bundle文件,它会根据output.path自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过hash生成很多bundle文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大。

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

module.exports = {
    plugins: [
        new CleanWebpackPlugin(),
    ],
}

mini-css-extract-plugin

我们在使用webpack构建工具的时候,通过style-loader,可以把解析出来的css通过js插入内部样式表的方式到页面中,mini-css-extract-plugin插件也是用来提取css到单独的文件的,该插件有个前提条件,只能用于webpack 4及以上的版本,所以如果使用的webpack版本低于4,,那还是用回extract-text-webpack-plugin插件。

const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
    // 省略其他代码
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    {
                        loader: dev ? 'style-loader': MiniCssExtractPlugin.loader
                    },
                    {
                        loader: 'css-loader'
                    },
                    {
                        loader: 'less-loader'
                    }
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].[hash:8].css",
        })
    ]
}

copy-webpack-plugin
我们在public/index.html中引入了静态资源,但是打包的时候webpack并不会帮我们拷贝到dist目录,因此copy-webpack-plugin就可以很好地帮我做拷贝的工作了。

const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
    plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin([{
            from: path.resolve(__dirname, '../static'),
            to: path.resolve(__dirname, '../dist/static')
        }])
    ],
}

ProvidePlugin

ProvidePlugin可以很快的帮我们加载想要引入的模块,而不用require。一般我们加载jQuery需要先把它import

import $ from 'jquery'

$('#layout').html('test')

但是我们在config中配置ProvidePlugin插件后能够不用import,直接使用$

module.exports = {
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery'
        }),
    ]
}

在项目中引入了太多模块并且没有require会让人摸不着头脑,因此建议加载一些常见的比如jQuery、vue、lodash等。

loader和plugin的区别(面试中常遇见)

  • 对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
  • plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容