1. WebPack 基础知识

首先需要明白webpack为什么会出现,作用是什么,总之他就是一个模块打包工具;
commonJS的模块导出方式为:

function SiderBar() {
    var dom = document.getElementById('root');
    var siderbar = document.createElement('div');
    siderbar.innerHTML = 'siderbar';
    dom.append(siderbar);
}

module.exports = SiderBar

commonJS的模块导入(引入)方式为:

var SiderBar = require('./siderbar.js')

webpack是一个打包工具,,他能识别任何一个模块引入的语法,包括ES Module(也就是 import export)、CommonJS 、CMD 、AMD都可以识别。webpack都能够进行很好的翻译。

2. Webpack 安装、使用配置文件、浅析打包输出的内容、Loader、使用 Loader 打包静态资源(图片篇)、使用 Loader 打包静态资源(样式篇)

2.1 安装有两种方式:全局安装与局部安装
  • 全局安装:npm install webpack webpack-cli -g 不推荐使用,卸载npm uninstall webpack webpack-cli -g;
  • 局部安装:npm install webpack webpack-cli --save-dev 或者npm install webpack webpack-cli -D是等价的。局部安装的直接输入webpack -v是不能运行这个命令的,可以通过 npx webpack -v去运行命令。npx会从项目的node_modul去找wenpack包,所以可以通过npx去查找我们在项目中的webpack包;可以在不同的项目中使用不同版本的webpack
    输入npm info webpack可以查看包的信息,包括版本等信息。
  • 安装指定版本的webpack:npm install webapck@版本号 webpack-cli -D
2.2 使用配置文件

webpack在进行打包的时候,首先会查找项目中是否存在配置文件webpack.config.js,如果没有,他会使用默认的配置文件进行打包。
在项目的目录下新建webpack.config.js文件,里面输入内容:

// 引入node核心模块path
const path = require('path')

module.exports = {
    // 入口文件
    entry: './src/index.js',
    // 打包出的文件配置
    output: {
      // 文件名
      filename: 'bundle.js',
      //  打包后的文件放在哪个文件夹,是一个绝对路径 
      //  __dirname就是webpack.config.js所在的当前目录的路径,改成bundle就是说,打包后的文件放在bundle文件夹中
      path: path.resolve(__dirname,'bundle')
    }
}

代码的注释很详细,就不在重复;上面代码就是说将我们项目中的/src/index.js文件进行打包到bundle文件夹下的bundle.js中。
一般情况,我们将我们的源代码放在src目录下。

需要注意的是: webpack中默认的配置文件是webpack.config.js,我们可以输入命令npx webpack --config webpackconfig.js将默认的配置文件指向webpackconfig.js这个文件。

我们每次进行打包的时候,都需要输入命令npm webpack,我们可以在webpack.config.js这个文件中进行配置我们允许的命令;如下面代码:

  "scripts": {
    "bundle": "webpack"
  },

上面的配置就是当我们输入npm run build的时候相当于输入 npm webpack,进行打包我们的文件。这里运行 npm webpack是不会进行全局检测是否安装了 webpack,而是从项目的node_module去查找。

补充:查看官网的guides中的Getting started里面的内容

2.3 浅析打包输出的内容

我们输入命令npm run bundle或者npx webpack之后,控制台会输出下面的内容:

Hash: 7d4f5a28d798a39e7e58
Version: webpack 4.31.0
Time: 293ms
Built at: 2019-05-16 08:56:10
    Asset      Size  Chunks             Chunk Names
bundle.js  1.36 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 870 bytes {0} [built]
[1] ./src/header.js 249 bytes {0} [built]
[2] ./src/siderbar.js 246 bytes {0} [built]
[3] ./src/content.js 240 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

Hash:是对应打包成功后文件的Hash值是唯一值;
bundle.js:我们打包出的文件;
Chunks:我们打包后的文件,每一个文件都会有一个自己的id值,这里面存放该文件的id值跟与其他打包后的文件有关系的对应文件的id值;
Chunk Names:存放Chunks中每一个id值对应的名字。这里的main,这里的入口文件,对应的名字就是main;我们前面说到配置打包的入口文件:

    // 入口文件
    entry: './src/index.js',

其实是下面的简写形式:

    // 入口文件
    entry: {
      main: './src/index.js',
    },

WARNING in configuration:这里的警告说,我们在进行打包的时候,没有配置打包的模式与打包的环境;其实我们在没有配置模式的情况下默认是production,如下面的代码:

// 引入node核心模块path
const path = require('path')
module.exports = {
    // 配置打包模式
    mode: 'production',
    // 入口文件
    entry: {
      main: './src/index.js',
    },
    // 打包出的文件配置
    output: {
      // 文件名
      filename: 'bundle.js',
      //  打包后的文件放在哪个文件夹,是一个绝对路径 
      //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
      path: path.resolve(__dirname,'dist')
    }
}

添加之后,再次进行打包就不会出现警告了:

Hash: 81601a0beb210cf86adc
Version: webpack 4.31.0
Time: 632ms
Built at: 2019-05-16 15:20:51
    Asset      Size  Chunks             Chunk Names
bundle.js  1.36 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 870 bytes {0} [built]
[1] ./src/header.js 249 bytes {0} [built]
[2] ./src/siderbar.js 246 bytes {0} [built]
[3] ./src/content.js 240 bytes {0} [built]

这个production模式打包后的文件,会对文件进行压缩,上面打包后的文件内容如下:打包为一行;

!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){var r=t(1),o=t(2),u=t(3);new r,new o,new u},function(e,n){e.exports=function(){var e=document.getElementById("root"),n=document.createElement("div");n.innerHTML="header",e.append(n)}},function(e,n){e.exports=function(){var e=document.getElementById("root"),n=document.createElement("div");n.innerHTML="SiderBar",e.append(n)}},function(e,n){e.exports=function(){var e=document.getElementById("root"),n=document.createElement("div");n.innerHTML="content",e.append(n)}}]);

我们将打包模式配置成mode: 'development';打包后的内容就不会进行压缩,内容如下:

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
2.4 Loader解析

webpack本身只是识别js文件的打包,当我们需要进行打包比如图片等文件时,需要在配置文件里面进行配置;webpack在进行打包的时候,首先是直接打包js文件,然后其他的文件会通过配置文件里面的module里面进行查找对应文件的打包规则;如果我们需要对图片文件进行打包,配置如下:

    module: {
      rules: [{
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: 'file-loader'
        }
      }]
    },

然后记得进行安装对应的loader;输入命令:npm install file-loader --save-dev然后运行打包命令就可以了;
loader就是一个打包方案,只要不是.js文件,就需要进行配置打包的规则。

2.5 使用 Loader 打包静态资源(图片篇)、url-loader的使用

如果我们在打包静态资源的时候,比如图片,默认打包后的结果是会生成一个字符串为图片命名,我们现在希望我们打包之后他的文件名以及扩展名,也就是文件类型都是不会改变,可以进行如下配置:[name].[ext]分别代表的是之前文件的文件名以及之前文件的后缀,他其实就是一个webpack的一个占位符。可以在官网的file-loader里面可以看到很多占位符的用法。

    module: {
        rules: [{
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'file-loader',
                options: {
                  // 占位符 Placeholders
                  name: '[name].[ext]'
                }
            }
        }]
    },

同时我们可以设置打包后输出的的文件夹的位置:下面是将图片文件打包在images文件夹中。可以查看官网的文档里面file-loader的相关配置。

    module: {
        rules: [{
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/'
                }
            }
        }]
    },
url-loader 的使用:

url-loader可以进行上面的打包,但是唯一不同的是,url-loader是将图片直接打包成一个base:64的字符串,进行显示在src中。
在这里插入图片描述
这个会有一些问题,是减少了发送http请求,但是如果文件很大,加载的时间也是很长,页面会显示空白。如果图片小,可以使用这种方式进行打包。如果图片很大,需要将图片通过file-loader打包到一个文件夹,这样更适合项目。其实在url-loader中已经考虑到了,我们可以进行添加一个配置进行限制:limit: 2048意思就是如果图片超过2048个字节,也就是2kb就将图片打包到imagees文件夹中,如果小于,就直接生成一个base:64的字符串进行显示。

    module: {
        rules: [{
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/',
                    limit: 2048
                }
            }
        }]
    },

可以查看官网API进行深入的研究这两个loader

2.6 使用 Loader 打包静态资源(样式篇)

首先需要进行配置打包css文件的打包配置,需要使用style-loader、css-loader两个loader进行打包。其中css-loader他是可以解析我们css文件之间的依赖关系,比如我们的index.css中引用了avatar.css文件,如下:@import './avatar.css';他就会进行解析,之间的依赖关系,最终将这些文件打包到一个文件中。style-loader的作用是,当得到css-loader打包后的内容之后,会将这些挂载到页面的header部分,也就是放在了style标签中。配置如下:

    module: {
        rules: [{
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }]
    },

如果我们需要打包.sass、scss这些其他css文件,我们需要引入sass-loader进行打包:npm install sass-loader node-sass webpack --save-dev如下的配置:

    module: {
        rules: [ {
            test: /\.scss$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }]
    },

其实loader的执行是有先后顺序的:从上到下、从右到左;比如上面的配置,首先是通过sass-loader进行处理,然后通过css-loader进行处理文件关系,最后通过style-loader进行挂载到页面。

2.6.1 对于 css 打包进行添加厂商前缀,使用postcss-loader自动添加厂商前缀:

首先进行安装npm i -D postcss-loader;然后新建postcss.config.js文件,然后进行安装一个autoprefixer插件,输入命令:npm install autoprefixer -Dpostcss.config.js文件配置如下:

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

最后需要记得在打包配置里面进行添加postcss-loader配置项:

    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader',
                'postcss-loader'
            ]
        }]
    },

其实我们的厂商前缀是由postcss-loader里面我们添加的autoprefixer插件给我们添加的厂商前缀。

2.6.2 css-loader 中一些常用的配置项:

我们在进行配置loader的时候,如果里面需要配置参数,那么将loader写成一个对象,而不是数组里面的字符串,我们给css-loader添加一个选项:importLoaders: 2,如下面代码:importLoaders: 2参数的意思是:如果我们在js文件里面通过import语句进行引入scss文件是可以进行打包的,按照我们前面说的,从下往上执行loader,但是如果我们的在js文件中引入的scss文件中还通过import引入了其他scss文件,那么这里的scss文件在进行打包的时候不会进行类似在js文件中scss打包的过程从下往上执行loader,所以我们需要添加配置项importLoaders: 2,意思就是我们在打包scss文件的时候(或者css)需要执行前面两个loader,这样就保证了不管scss文件(或者css)中通过import引入多少其他的scss文件(或者css),都能够正常的打包,都会依次从上到下依次执行loader进行打包。

    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }]
    },
2.6.3 css 文件模块化打包:只在一个模块内有效

我们在写css、scss的时候会直接将他们在代码中进行引入,如下:这会导致如果页面创建元素,所有的元素的样式都会受到影响,

import  './index.scss';
import avater from './avater.png';

import createAvatar from './createAvatar'

createAvatar();
// 打包图片文件--avater为打包后的文件名
var img = new Image();
img.src = avater;
img.classList.add(avater)
var dom = document.getElementById('root');
dom.append(img);

createAvatar.js文件中也是创建一个img标签:

import avater from './avater.png'
function createAvatar () {
    var img = new Image();
    img.src = avater;
    img.classList.add('avater')
    var dom = document.getElementById('root');
    dom.append(img);
}
export default createAvatar

index.scss中,代码如下:


body {
    .avater {
        height: 150px;
        width: 150px;
        transform: translate(100px, 100px);
    }
}

这样写会导致页面中两个图片显示都一样,都使用了index.scss中的样式;解决这个问题,就是我们可以在打包css这些文件的时候,实现模块化打包,如下面代码:
在打包的时候,进行配置打包为模块;modules: true意思就是开启模块化打包。

module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2,
                    modules: true
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }]
    },

然后在代码中通过style.avater这种方法,进行添加样式,这样就不影响全部文件了。

import style from './index.scss';
import avater from './avater.png';

import createAvatar from './createAvatar'

createAvatar();
// 打包图片文件--avater为打包后的文件名
var img = new Image();
img.src = avater;
img.classList.add(style.avater)
var dom = document.getElementById('root');
dom.append(img);
2.6.4 打包字体文件:

我们项目里面也经常会用到图片库中的小图片,比如iconfont里面的小图片,我们查看iconfont.css发现,里面引入了eot|ttf|svg|woff这些文件,如下面代码:

@font-face {
    font-family: "iconfont";
    src: url('./font/iconfont.eot?t=1558162704401'); /* IE9 */
    src: url('./font/iconfont.eot?t=1558162704401#iefix') format('embedded-opentype'), /* IE6-IE8 */
    url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAASIAAsAAAAACQgAAAQ7AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCfgqFVIRuATYCJAMMCwgABCAFhG0HWxvRBxHVoy2Q/SxwzuYE2hXq6ImKdq351ClK774P+U+55ftJYCczKxZYca2qhqqHlK24nUQMelK9FgLpdvvtgwTDKPAeOIriDMIYEisANjdD9/zrCMWSnOf9tMy0dKYX0l//czm9awvOD5TLXMuOegGGvYACGmPbAgq0QBL0luFVB7kbx4EAXIwsNBq16ABGoY8jAN1DB/cFS+goFcWEIdaCpQV60LG2WLoDYKX3ffpREAyQWBL61DaDGg5Anff0XpJ1r+50EPD7swBgVwEyIAugAB2FtjaQg1gWGZdtiqTqSgXBe8nrdXDEm+0fHkAkoAm6zBcT0+ISvCeLv2t8jAN/31cBVgAD/KeVEEN3tJC34tqkxgXGTXfu3rlOc+Hr8MTcgtOSZ+3EUtPhtPGde0E3oi52evhU0NLOz59jd2fHqMcOJzH9aVu+ZJ1uCLHUbjdt5HAiDzx61pbXhwd6tFHySNO01ZCj8/OxS00ix/7dJ0PLqOZyCt95MQyuS8liz5VIqjb2mDW2dlPIXA/rQrkT1MMuDA2uc6mtTW+mbvTjrn1LDzj2L9k7nXc2x/GEdvDtNJpV1zp2gypowHGWmss91q3j/kucZHGY9gDD0AZ06pO7BjY2dOwwyZt01y77UnvnPY7KoDOqqHeEt4hBq+jG0Q2HDWvo3qU1Wiwqad9EuDX31jEYFDiWezSPWuu6WdkBXdvGt4yfOiXOrmmLLgk3fnYYtWktF5e2b/3+HjE28ELhIt/x3G1z8/Kr68rbo2ub+FZxU6bGtUxo2zWhbvgrVVl9RW0Z2zqmcVRDu79wi7pB4of00ieV0apXehC7x4L0V6uIUZ1JeW/lSTsn1Ss7KmKZ/kpX76nOnVhdS//Izv3GW+tyT3G/NHRKGJQwMKEjSpfDB3r3Ghz6PXTSnDnclEwKxJNTvXY8GOzXzW/Qg3cPJnmPmsy2+BxQ37ELrJt6fsw7fT/z45fmG22bHfdtLL5pblGya8Ku8ZWrLf/OW/gez1jlt7tslshAHwCA91KekTNJ4nqek8v/ld8g8P38sHdA1T8mFADAS8u9QOzxXirAHAC7m3aCP4MPrFMUoa6XS+JZPC0pO4x+77XgihNe6K5gnvYKDVwIEEaLQOIjGmRMEqoQs8BCKAIrpjZwmRqvFkKMg4xKBZBhPgIS6AhI/F0BWaC7qEJ0g4XNN7AKDBm4ThSypZASuFU74yK4w/oDu1HSplGUX3xDU8LCcaFHfSGfNghd3aaTV0zIfcw436YX0aCZIozoNAyBIDM53KQ+RPLQNLroRfVGsVLqMy6CO1h/wG6UtNNcVObzNzQlLFyBKbO+kE/bOejUWgB6lSUQ5lx6n2/TE9HUKM0UwYgtGIwYgVw8yOEmtaNFJA8a0khDZfX4inh922CmT81okVHCriWZ/1FMit0iU0QrfosS7CXX8isp8K5UVQA=') format('woff2'),
    url('./font/iconfont.woff?t=1558162704401') format('woff'),
    url('./font/iconfont.ttf?t=1558162704401') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
    url('./font/iconfont.svg?t=1558162704401#iconfont') format('svg'); /* iOS 4.1- */
  }

我们如果直接使用webpack打包会提示报错,无法打包那些后缀的文件,我们可以通过file-loader进行打包这些字体文件,在webpack.config.js里面进行配置,如下:使得将字体文件打包到dist的相对路径里面,我们直接使用。

    module: {
        rules: [ {
            test: /\.(eot|ttf|svg|woff)$/,
            use: {
                loader: 'file-loader'
            }
        }]
    },

然后我们可以继续深入,查看官网的document中的GUIDS里面的Asset Management里面的内容,还有loader里面的sass-loader、css-loader、postcss-loader进行深入了解。

3. 使用Plugins让打包更便捷

3. 1 html-webpack-plugin 插件的使用——将html进行打包,并根据设置的html模板进行打包

我们在打包的时候,每次打包之后,打包后的文件,并没有index.html就是我们在src文件夹里面写的网页,都需要我们进行复制到dist目录里面,很是麻烦,我们可以使用webpack插件,来帮助我们解决这个问题。
首先需要安装这个插件:cnpm install --save-dev html-webpack-plugin,然后在webpack.config.js里面进行配置,代码如下:

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

module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    plugins: [new HtmlWebpakcPlugin()],
    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'bundle.js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }
}

这样,打包之后,会将我们的html文件也会进行打包,html-webpack-plugin这个插件在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html中。我们打开打包后的html文件,会发现,我们在里面的html结构没有显示,比如我们在里面写的<div></div>没有被打包显示,这个时候,我们需要对html-webpack-plugin这个插件进行配置一个template选项,让他根据模板进行打包我们的html文件,模板文件也就是我们需要打包的文件,也就是以模板文件进行打包。配置如下:

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

module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    plugins: [new HtmlWebpakcPlugin({
      template: './src/index.html'
    })],
    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'bundle.js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }
}

Plugin可以在webpack运行到某一时刻的时候,帮你做一些事情,很类似vue的声明周期函数;

3.2 clean-webpack-plugin 插件的使用——清除打包地址的文件

我们在查看我们打包后的内容,我们如果将output配置里面的输出文件名修改为其他的,我们重新进行打包,会发现,打包是成功了,但是我们修改之前打包的js文件还是存在在dist文件夹中,我们需要做的是,每次打包之前,需要将dist中之前打包的文件先进行删除,然后再次进行打包js文件。这个时候,我们需要引入一个插件clean-webpack-plugin,
首先输入命令进行安装:npm install clean-webpack-plugin -D
然后在webpack.config.js里面进行配置,如下:new CleanWebpackPlugin()意思是每次打包之前删除dist里面所有的内容。

// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    plugins: [new HtmlWebpakcPlugin({
      template: './src/index.html'
    }),new CleanWebpackPlugin()],
    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'bundle.js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }
}

这里需要注意的是HtmlWebpakcPlugin是在打包之后运行的插件,CleanWebpackPlugin是在打包之前运行的插件。还有一个需要注意的是new CleanWebpackPlugin(['dist'])这里的dist是不需要添加的,直接new CleanWebpackPlugin()就好了

4. Entry 与 Output 的基础配置

4.1 多个文件的打包

我们在打包文件的时候,经常会遇到打包多个文件,而不是一个js文件,这时候我们需要修改我们的配置;比如,下面代码,我们打包mainsub的文件:

    // 入口文件
    entry: {
        main: './src/index.js',
        sub: './src/index.js'
    },

如果我们还是用之前的打包输出的配置;就会报错,提示多个文件打包到一个文件的错误:

    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'main.js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }

这个时候我们需要配置输出的文件,让他根据入口配置的文件,也就是那个对象的键,根据键来进行打包输出文件,配置如下:

    // 打包出的文件配置
    output: {
        // 文件名
        filename: '[name].js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }

这样,打包后输出的文件分别为main.jssub.js;这样就完成了多个文件的打包。

4.2 将打包后的js文件在html中,通过配置cdn地址进行引入

我们经常会遇到,我们在打包我们的js代码之后,会将js文件放在cdn服务器上,通过cdn服务器的地址来在页面进行引入脚本。比如,我们希望在html引入js的时候地址前面加上cdn域名:http://cdn.com/main.js这样。我们需要在output里面进行配置一个publicPath的选项:

    // 打包出的文件配置
    output: {
        publicPath: 'http://cdn.com.cn',
        // 文件名
        filename: '[name].js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }

打包后html的引入如下:

<script type="text/javascript" src="http://cdn.com.cn/main.js"></script>
<script type="text/javascript" src="http://cdn.com.cn/sub.js"></script>

进一步学习:查看官网的document里的configuration中的output的配置参数。然后进行查看guides里面的output management里的内容。查看plugins里面的htmlwebpackplugin插件,查看该插件对应的官网。

5. SourceMap 的配置

我们在前面配置了我们的打包模式 为开发者模式:mode: 'development',在这个模式,默认SourceMap已经被配置进去了,我们可以关掉认SourceMap,配置如下:

    // 配置打包模式
    mode: 'development',
    devtool: 'none',

在这里插入图片描述
如果我们关闭了SourceMap,我们如果在写代码的时候,不小心制造一个小bug,比如说我们把console写成了conssole这样,在打包时,是不会出现问题的,但是在页面的控制台会提示,该方法不存在,我们点击之后,提示打包后的文件的第153行报错,其实我们真正 想知道的不是打包后文件哪里出错,而是我们在源代码里哪个位置出现错误;而SourceMap他是一个映射关系,他知道dist目录下的main.js也就是我们打包生成的文件153行实际上对应是src目录下index.js文件也就是我们的源代码文件中的第58行。我们可以通过SourceMap获取到index.js中第一行代码出错了;所以我们需要进行配置SourceMap,来根据SourceMap的映射关系,找到我们源代码出错位置:我们查看打包后的文件,会多出一个main.js.map的文件,就是我们的源文件,存放着映射关系。

    // 配置打包模式
    mode: 'development',
    devtool: 'source-map',

在这里插入图片描述
我们可以查看官网有关SourceMap的配置,也就是devtool的配置,其中如果将devtool配置成inline-cheap-source-map意思他会将打包后的SourceMap直接通过地址放在在打包的文件代码的底部:

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vc3JjL2luZGV4LmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7O0FBR0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGtEQUEwQyxnQ0FBZ0M7QUFDMUU7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxnRUFBd0Qsa0JBQWtCO0FBQzFFO0FBQ0EseURBQWlELGNBQWM7QUFDL0Q7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQ

这里,需要说明的是,前面我们进行配置成source-map,点击报错,他会提示代码报错位置的行与列,其实我们在查看问题的时候,并不需要精确到哪一列代码出现问题,只需要知道在哪一行;这时候我们可以将devtool配置成cheap-source-map,他只会提示代码在哪一行报错。
这里的cheap-source-map他就会映射我们写的业务代码,比如打包的文件index.js而不会将我们在loder里面引入的第三方模块代码的映射,如果需要映射loder里面的的代码需要将配置修改为cheap-module-inline-source-map
还有一种打包方式就是eval,他是打包速度最快的,但是如果代码过多,也会影响打包速度;

这里建议我们配置sourceMap的打包方式,如果是在development环境进行打包的时候,将devtool配置成cheap-module-eval-source-map,提示的错误比较全,打包速度也是比较快。如果我们代码已经上线,也就是在production模式下,我们可以将devtool配置成cheap-module-source-map,这样提示效果会好一些。

6. 使用 WebpackDevServer 提升开发效率

我们现在每次进行打包的时候还要需要输入打包命令,打包之后在将html打开,查看打包结果;这样操作是比较麻烦的,我们需要的是可以监听到我们改动的文件,然后自动进行重新打包,我们有三种方法进行配置;

6.1 第一种:我们可以在package.json进行配置,监听文件变化
  "scripts": {
    "watch": "webpack --watch"
  },

他的意思是,会进行监听我们打包的文件,只要打包文件发生变化,就会重新打包。

6.2 第二种:监听打包文件变化,并自动启动服务器,加载html文件

我们的第二种打包方式是,我们不仅需要监听打包文件的变化并重新打包,而且需要在打包完成之后,直接打开我们的html文件,类似在服务器中。前提是首先安装webpack-dev-server,输入命令:npm install webpack-dev-server -D
我们可以在webpack.config.js添加一个devServer配置:

module.exports = {
    devServer: {
      // 服务器启动的根路径
      contentBase: './dist'
    }
}

然后在package.json里面添加一个运行命令,来运行我们的服务器:

  "scripts": {
    "watch": "webpack --watch",
    "start": "webpack-dev-server"
  },

这时候会启动一个服务器,webpack-dev-server比我们之前配置的webpack --watch这种方式的好处是,他不仅会监听我们打包文件变化,并进行更新打包,而且也会重新刷新我们的网页。我们还可以在webpack.config.js中的devServer配置添加一个open: true选项,他会在打开服务器后,自动打开浏览器,并自动访问启动的服务器地址,打开网页,不用我们手动进行将服务器启动后的地址输入到浏览器进行打开:

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true
    },

还有一个就是我们在使用vue-cli脚手架的时候,我们会在devServer里面配置一个proxy选项,用来模拟后台API请求地址或者转发我们的接口API地址,解决跨域代理;如下面的配置:他的意思是当我们访问localhost:8080/api地址的时候,他会自动将地址转发到localhost:3000端口。

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      }
    },
6.3 第三种:自己手写一个服务器,实现类似Webpack-dev-server的效果

首先在package.json里面进行配置命令,使得运行命令之后,就运行我们的脚本文件:

  "scripts": {
    "middleware": "node server.js"
  },

我们可以借助node的框架,去搭建服务器,这里使用express框架来实现,首先进行安装该框架:npm install express webpack-dev-middleware -D,我们这里同时安装了一个webapckwebpack-dev-middleware中间件,用来监听我们打包文件的变化,来自动进行打包。
然后我们修改webpack.config.js里面的output配置,添加一个publicPath: '/'将文件打包成功后的引用地址前面都加一个根路径,确保打包生成文件的引用路径不会出现错误。

    // 打包出的文件配置
    output: {
        // 文件引入的cnd地址
        // publicPath: 'http://cdn.com.cn',
        publicPath: '/',
        // 文件名
        filename: '[name].js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }

然后我们的server.js代码如下:

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
// 进行webpack编译--执行编译器,进行打包一次代码,
// 在node中直接使用webpack
const complier = webpack(config);

const app = express();
// 只要打包的文件内容发生变化,就会重新打包,通过编译器;将打包的文件放在跟webpack.config.js配置的一致
app.use(webpackDevMiddleware(complier, {
  publicPath: config.output.publicPath
}));

app.listen(3000, () => {
  console.log('server is run')
})

上面的我们是直接通过调用webpack配置在node里面使用webpack,我们也可以直接在命令行里去执行webpack,可以在官网的API里面的Command Line Interface查找到很多的命令:比如webpack index.js -o main.jsindex.js打包成main.js输出。如果需要查找在node里面运行webpack,可以查看node.js APi里面去查找。

深入学习:打开官网的documentation里面的Guides中的development里面的内容阅读,然后查看configuration里面有关devtool、devServer里面的内容。

7. Hot Module Replacement 热模块更新

我们使用webpack-dev-server帮助我们进行打包,会发现不会产生一个dist,因为他会生成一个打包文件,不过没有放在文件里面而是放在了计算机的内存中,可以有效提升打包的速度。
我们在写代码的时候,经常会碰到,我们在修改了我们的css文件后,只是想每次在更新页面的时候,只是将样式文件重新加载,进行渲染,并不想让整个页面进行重新加载,破坏了我们页面的状态;我们想做的就是每次改完样式文件之后,只是重新加载样式文件,不破坏页面的状态;我们可以使用热模块更新来解决这个问题:我在webpack.config.js里面的devServer配置项中添加一个hot: true(让webpack-dev-server开启热模块更新的功能)选项跟hotOnly:true(即便是HTML的功能没有生效,也不让浏览器自动重新刷新,)

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      },
      hot: true,
      hotOnly: true
    },

然后我们在webpack.config.js中引入webpack,在plugin里面进行添加一个webpack的插件webpack.HotModuleReplacementPlugin();代码如下:

// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack');
module.exports = {
    //....
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpakcPlugin({
          template: './src/index.html'
        }),
        new webpack.HotModuleReplacementPlugin()
    ]
 }

这样,如果只改了css样式,页面不会从新加载,只会重新加载css文件。
如果对于js文件,也想通过热模块更新,在webpack.config.js里面的配置跟上面一样,但是我们在代码里面需要进行判断,加一些代码逻辑,用来判断是否开启热模块更新,然后进行处理我们的逻辑:需要进行监控我们需要修改的文件

// 如果支持热模块加载
if(module.hot) {
    // 监控number文件,如果发生改变,就会执行里面的代码。
    // 第一个参数为依赖的文件的名字,
  module.hot.accept('./number', ()=> {
    document.body.removeChild(document.getElementById('number'))
    number();
  })
}

查看官方文档guides里面的Hot Module Replacement中的内容深入了解,然后查看api查看Hot Module Replacement配置选项,然后进行查看concepts里面的Hot Module Replacement内容,进行更深入的了解。

8. 使用 Babel 处理 ES6 语法

8.1 使用 Babel 按需打包js文件

首先进行安装,输入命令npm install --save-dev babel-loader @babel/core,然后在webpack.config.js增加一个规则,如下:代码意思就是除了node_modules里面的js内容不检测,其他的js文件文件都通过babel-loader进行加载转换。

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader" 
    }
  ]
}

其实babel-loader只是Babelwebpack之间的一个通讯工具,babel-loader并不会将es6语法翻译为es5语法,而babel/preset-env才是真正转换的工具。首选进行安装,输入命令:npm install @babel/preset-env --save-dev,然后进行配置在webpack.config.js

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      options: {
         presets: ["@babel/preset-env"]
      }
    }
  ]
}

也可以新建一个.babelrc的文件,然后里面增加配置,启用一些插件。

{
  "presets": ["@babel/preset-env"]
}

这种转换只是一部分,低版本的有些函数还是不存在的,比如promise等函数,我们还需要借助@babel/polyfill,进行对低版本浏览器对es6中这些函数的支持,输入命令进行安装:npm install --save @babel/polyfill,然后全局引入,放在业务代码的最顶部,

import "@babel/polyfill";

这样全局引入,会将所有es6的新增的函数打包到js文件中,这样会使打包的文件体积增大。我们需要的是在该js文件中使用的es6函数的实现,在webpack.config.js进行配置;

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

就会实现按需加载,如果js中有用到es6新增函数,会将对应的实现代码打包到js中,而不是将全部es6新增的函数实现方式打包进去。

8.2 打包转换中的其他参数(设置打包支持浏览器的版本、编写类库文件babel转换配置)

我们在打包的选项中,还可以进行配置其他选项,比如可以进行配置打包支持浏览器的版本,babel可以根据打包后支持的浏览器,去进行判断需要打包哪一些es6语法的实现函数,代码如下:这句代码是设置了打包后的为高于谷歌67版本的浏览器。

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      options: {
      presets: [["@babel/preset-env",{
        targets: {
           chrome: "67",
         },
         useBuiltIns: 'usage'
        }]]
      }  
    }
  ]
}

我们如果写业务代码需要使用babel转换的时候,使用上面的配置就可以了,我们如果编写一个类库或者一个UI组件,需要进行不同的配置,配置步骤如下:
安装plugin-transform-runtime,输入命令:npm install --save-dev @babel/plugin-transform-runtime,然后进行安装babel/runtime,输入命令:npm install --save @babel/runtime,然后进行安装babel/runtime-corejs2 ,输入命令:npm install --save @babel/runtime-corejs2,然后在webpack.config.js进行配置;这样配置的好处是,我们在使用babel转换的时候,借助polyfill在全局进行注入,这样会污染全局,使用下面的配置,他会以闭包的形式,进行注入支持es6语法的函数。不存在全局污染。

    module: {
        rules: [{ 
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader",
            options: {
            "plugins": [["@babel/plugin-transform-runtime",{
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
              }]]
            } 
        }]
    },

如果我们配置的options会很多的时候,可以在项目里面新建一个.babelrc的文件,将options里面的内容写在这里。然后去掉webpack.config.js中我们配置的babel-loaderoptions里面的内容。

9. Webpack 实现对React框架代码的打包

react首选需要安装他,输入命令npm install react react-dom --save,然后在我们的js文件写react代码,如下:

import "@babel/polyfill";

import React, {Component} from 'react'

import ReactDom from 'react-dom'

class App extends Component {
  return () {
    return <div>Hello World</div>
  }
}
ReactDom.render(<App />, document.getElementById('root'))

安装babel/preset-react,进行转换;输入命令npm install --save-dev @babel/preset-react,然后在.babelrc文件中增加配置:

{
    presets: [
        [
            "@babel/preset-env", {
                targets: {
                    chrome: "67",
                },
                useBuiltIns: 'usage'
            }
        ],
        "@babel/preset-react"
    ]
}

这里的插件执行是从下往上执行的,就是先进行打包react代码,然后进行es6代码的转换。

10. Webpack 打包的一些坑

是遇到很多坑,比如使用了html-webpack-plugin插件,然后我们允许webpack-dev-server命令的时候,我们配置了打包入口的html模板,但是启动服务之后,没有页面显示,查看打包信息出现Entrypoint undefined = index.htmlwebpack.config.js配置如下:

// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
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',
        // sub: './src/index.js'
    },
    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      },
      hot: true,
      hotOnly: true
    },
    module: {
        rules: [{ 
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader"
        },
            {
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/',
                    limit: 204800
                }
            }
        }, {
            test: /\.vue$/,
            use: {
                loader: 'vue-loader'
            }
        }, {
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }, {
            test: /\.(eot|ttf|svg|woff)$/,
            use: {
                loader: 'file-loader'
            }
        }, {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader',
                'postcss-loader'
            ]
        }]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new HtmlWebpakcPlugin({
            template: './src/index.html'
        }),
        
    ],
    optimization: {
      usedExports: true
    },
    // 打包出的文件配置
    output: {
        // 文件引入的cnd地址
        // publicPath: 'http://cdn.com.cn',
        publicPath: './',
        // 文件名
        filename: '[name].js',
        //  打包后的文件放在哪个文件夹,是一个绝对路径 
        //  __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
        path: path.resolve(__dirname, 'dist')
    }
}

后来经过测试,将输出配置成如下:

    // 打包出的文件配置
    output: {
        path: path.resolve(__dirname, 'dist')
    }

这样就可以了,不要配置,打包输出的文件名以及地址就可以,虽然是解决了允许服务器没有显示页面的问题,但是这个解决办法还不是最佳。