webpack小白必备知识

1,936 阅读14分钟

什么是webpack

webpack1.png webpack官网的解释:

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

简单说,webpack就是可以把你的项目的分散的相同类型的文件打包到一个文件中(当然也可以分成多个文件),然后再index.html文件中 只需要引入一个js一个css文件就可以了。当然,webpack发展这么久,其能力已经被大大扩展:

  • 代码转换

  • 代码分割、分离

  • 代码缓存

  • 懒加载

  • 代码热更新

  • ......

现如今,三大开发框架三分天下,他们都是基于webpack进行项目打包的,如果你使用过vue-cli和creat-react-app等工具, 也会发现他们生成的项目也是基于webpack构建的,所以,目前webpack是前端开发的趋势,webpack的出现为前端工程化提供了基础。

安装

本次搭建的webpack工程,webpack版本是4.x,后续涉及的babel版本是7.x

首先创建项目文件夹并初始化,安装webpack。


mkdir webpack-demo

cd webpack-demo

npm init

npm i webpack webpack-cli -D

创建工程

之后在项目的根目录创建webpack.config.js文件,作为webpack的配置文件。

然后创建我们的源代码目录src,其中添加源文件index.js,并在其中输入下面的代码

let str = '测试'

console.log(str);

等待全部模块安装完成后,我们在控制台输入webpack命令,如果不出意外的话,你的根目录会出现一个dist文件夹,而且里面 有main.js文件,打开该文件,搜索测试字样,我们可以发现,在代码的最后会有console.log('测试')代码,可以发现,我们写的 代码已经打包进最后生成的文件,而且,它还为我们自动做了优化,删除了无用的变量。

等下,你此时肯定会发现,控制台有如下警告:

webpack2.png

那么,接下来就让我们一同来解决这个问题,并一步步的配置webpack吧。

模式mode

模式mode主要是告知webpack应该使用哪种模式下的内置优化方式进行打包,他是webpack的一个配置项。

它的可选项为三个 'none' | 'development' | 'production',分别表示不设置,开发环境,生产环境,并且在不同的 环境下,webpack会启用不同的打包内置插件。

用法

可以在配置文件中配置该项

module.exports = {
  mode: 'production'
}

也可以在cli参数中传递

webpack --mode=production

这里我们由于后续会把开发环境和生产环境的配置分离,而且开发命令和生产命令会分开,理论上讲, 两种方式对我们都是可以的,这里可以根据自己的个人喜好,我们这里采用第二种配置方式。

采用第二中配置方式的话,我们就需要在package.json中定义两个命令,方便使用。如果你还不知道为什么可以在package.json 中配置命令的话,请自定去npm官网查看具体文档。

我们定义两条命令,一条开发命令,一条生产命令:

{
  "scripts": {
    "build": "webpack --mode=production",
    "dev": "webpack --mode=development"
  }
}

这样的话,我们想打开发的包就运行npm run dev,想打生产的包就运行npm run build,其实vue项目也是采用这种命名方式的。

webpack3.png

可以发现,已经没有了警告,且文件正常打包了。

接下来,就让我们进行下一个配置项吧!

入口entry

入口对象entry是用于 webpack 查找开始构建 bundle 的地方。上下文是入口文件所处的目录的绝对路径的字符串。entry分为单文件入口和多文件入口。

单文件入口

配置方式:

  • 1、字符串形式
const path = require('path')
module.exports = {
  context: path.resolve(__dirname, './'),
  entry: './src/index.js'
}
  • 2、对象形式
const path = require('path')
module.exports = {
  context: path.resolve(__dirname, './'),
  entry: {
    main: './src/index.js'
  }
}

上面两种写法均可以实现将src/index.js作为入口文件,生成名为main.js的打包文件。如果想要自定义输出文件的名称则可以这样写

module.exports = {
  entry: {
    app: './src/index.js'
  }
}

这样,打包生成的文件就会称为app.js,与文件对象的key值相同。

如果输入文件为数组形式会如何呢,就像下面这样:

module.exports = {
  entry: {
    app: ['./src/index.js', './src/main.js']
  }
}

根据webpack官方说法,输入为数组形式,则数组的所有子元素都会作为构建的入口,但是他们最终会打包出一个文件为app.js,而不是 想多文件入口那样打包出多个文件。

一些单页应用项目会采用在入口处添加@babel/polyfill,作为入口文件,这样来做浏览器的兼容性处理。

entry1.gif

多文件入口

使用多文件入口会生成多个打包文件,如下所示

module.exports = {
  entry: {
    app: './src/index.js',
    main: './src/main.js'
  }
}

使用此配置最后会生成app.jsmain.js文件。

entry2.gif

entry决定了我们的应用程序从哪里开始构建我们的应用程序,并且可以根据项目需求配置单入口还是多入口。 现在我们的项目此次只需要有一个入口,则最终我们的webpack配置如下:

const path = require('path')

module.exports = {
  context: path.resolve(__dirname, './'),
  entry: {
    app: './src/index.js',
  }
}

出口output

配置 output 选项可以控制 webpack 如何向硬盘写入编译文件。

webpack官网中介绍的output的配置项很多,但是大部分是不需要更改的,这里我们只对需要我们手动更改的配置项进行说明。

output.path

output.path的值应是一个绝对路径,表示打包的文件的输出位置。一下代码表示将文件输出在demo文件夹下

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'demo')
  }
}

output1.gif

output.publicPath

output.publicPath指定了在加载静态资源时的统一前缀。

例如:

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'demo'),
    publicPath: '/assets/'
  }
}

我们如此配置的话,在生成的文件引用地址前都会加上/assets/前缀。一般此项配置为'./',表示当前路径下。

output.filename

output.filename表示了输出打包文件的名称。可以指定具体的文件名称,也可以根据变量来动态生成文件名称。

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

我们采用如下配置,看看输出的效果:

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: './',
    filename: '[name].[hash].js'
  }
}

output2.gif

我们可以发现,生成的文件是我们配置的入口的app,然后紧跟一个20位的hash值。

::: tip 可以采用[hash:8]的写法来指定生成的hash的位数。 :::

output.chunkFilename

output.chunkFilename用来指定生成的非入口文件生成的模块名称,例如,在Vue项目中,我们一般会采用动态路由的方式来优化首页加载速度, 采用动态路由就需要为每个路由页面生成单独的chunkFile,用于webpack动态加载,我们需要为每个chunkFile指定一个文件名称,方便我们 查看是哪个路由页面的文件,此时,output.chunkFilename就派上用场了。

output.chunkFilename同样可以使用变量来动态设置,例如:

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: './',
    filename: '[name].[hash].js',
    chunkFilename: '[name].[chunkhash].js'
  }
}

这样设置的话,生成的chunFile会按照name.hash的名称生成文件。这里的name是在/* webpackChunkName: login */这样的注释中获取的,写过Vue项目的同学 一定不会陌生这种写法,如果你还不太熟悉Vue这种写法,name我的这篇关于Vue动态路由的章节 可能会帮助到你。

我们可以设置

module.exports = {
  ...
  output: {
    ...
    filename: 'js/[name].[hash].js',
    chunkFilename: 'js/[name].[chunkhash].js'
  }
}

来使生成的js文件都放在dist/js/文件夹下,便于组织打包后的目录结构。

output配置项可以指定输出文件的位置和名称,不仅可以指定静态文件名,还可以根据用户的配置生成动态的文件名。

最终,我们采用如下配置:

const path = require('path')

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

loader

webpack本身只能处理js文件,但是在我们的项目中不只包含js文件,项目中还会有css文件,图片文件,html文件,以及特定的vue文件,jsx文件等。 为了让webpack可以处理这些类型的文件,loader概念就诞生了。

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”, 并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。 loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

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是webpack的概念,loader是处理一类文件的工具,所以,配置loader如何去处理文件才是关键。

loader需要在模块(module)下进行配置,每个规则匹配一类文件,使用一种或是多种loader进行处理。

下面介绍配置项:

module.rules

module.rules是一个数组,用于定义规则数组,它看起来像这样:

module.exports = {
  ...
  module: {
    rules: [{
      ...
    },{
      ...
    }]
  }
}

其中的每项规则,我们用Rule来表示。以下篇幅的Rule也是单个规则配置项的意思。

Rule.test

Rule.test的值是一个正则表达式,用于匹配对应的文件,使用此配置,便可以将一类文件归于一种loader处理。它看起来像这样

module.exports = {
  ...
  module: {
    rules: [{
      // 匹配css文件
      test: /\.css$/
    },{
      // 匹配多种图片类型的文件
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/
    }]
  }
}

Rule.use

Rule.use用来表示匹配Rule.test的文件用哪种loader来解析。Rule.use接受一个数组,其中包含了loader种类和loader需要的配置参数,它看起来像这样:

module.exports = {
  ...
  module: {
    rules: [{
      // 匹配css文件
      test: /\.css$/,
      use: [{
        loader: 'style-loader',
        options: {}
      },{
        loader: 'css-loader',
        options: {
          sourceMap: true
        }
      }]
    },{
      // 匹配多种图片类型的文件
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use: ['url-loader']
    }]
  }
}

以上配置告诉webpack,当你遇到.css结尾的文件时,你需要先把该文件交给css-loader处理,然后将返回值交给style-loader处理,当然,在options中可以 根据不同的loader的配置要求进行必要的配置。

::: tip 建议大家在配置loader时,都在github上搜索一下对应的loader项目源码,其中的readme.md文件中会详细的介绍该loader的作用以及配置规则,避免因为版本的 迭代造成配置项的改动进而影响打包结果。 :::

Rule.includeRule.exclude

Rule.includeRule.exclude是一对反义词,前者表示需要包含的文件,后者表示不需要包含的文件。

他们的取值都是一个字符串或字符串数组,表示需要处理的文件目录,他们看起来像这样:

module.exports = {
  ...
  module: {
    rules: [{
      // 匹配css文件
      test: /\.css$/,
      include: [path.resolve(__dirname, 'src')],
      exclude: [path.resolve(__dirname, 'src', 'demo')]
      use: [{
        loader: 'style-loader',
        options: {}
      },{
        loader: 'css-loader',
        options: {
          sourceMap: true
        }
      }]
    },{
      // 匹配多种图片类型的文件
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use: ['url-loader']
    }]
  }
}

以上的配置可以理解为,解析src文件夹下除了demo文件夹下的所有css文件。

下面我们详细说一下各种文件解析的具体配置方法。

loader和plugin是互相配合的,plugin可以增强loader的作用,所以很多开源库都是loader和plugin配合使用的,次篇文章主要是讲loader的, 所以涉及plugin的内容我们会放到下一节中描述,这里只做基本配置,不做具体说明。

css文件

处理css文件的loader有很多,其中css-loader用来将css转化为js可以识别的模块,而style-loader可以帮助我们将css以style标签的方式插入html文档中。

所以可以这样配置:

module.exports = {
  ...
  module: {
    rules: [{
      // 匹配css文件
      test: /\.css$/,
      use: [{
        loader: 'style-loader',
        options: {}
      },{
        loader: 'css-loader',
        options: {}
      }]
    }]
  }
}

但是在实际项目开发中,我们更希望css作为单独的文件已link标签的方式进行引入,那么我们就需要分离css文件,并在最后的html文件中自动插入css文件路径。

所以最后的配置如下:

首先安装:

npm i css-loader -D
npm i html-webpack-plugin -D
npm i mini-css-extract-plugin -D

建议分开安装,合并在一起安装经常会出现不可预测的问题。

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  ...
  module: {
    rules: [{
      test: /\.css$/,
      use: [{
        loader: MiniCssExtractPlugin.loader,
        options: {
          publicPath: '../'
        }
      }, {
        loader: 'css-loader',
        options: {}
      }]
    }]
  },
  plugins: [
    // 提取css为单独文件的插件
    new MiniCssExtractPlugin({
      // 将css打包到执行文件夹
      filename: 'css/[name].[hash:8].css'
    }),
    // 打包html插件,将动态的js插入HTML中
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/index.html'
    }),
  ]
}

sass文件

处理sass文件的loader为sass-loader,这里我们还需要配置postcss来帮助我们增强css

首先安装:

npm i sass node-sass sass-loader -D
npm i postcss-loader postcss-import postcss-url autoprefixer -D

sass和node-sass的版本如果不匹配会出现问题,这里建议下载匹配版本。

作者使用版本:

  • "sass": "^1.22.12",
  • "sass-loader": "^7.2.0",
  • "style-loader": "^1.2.1",

配置postcss需要在项目根目录下创建.postcssrc.js配置文件,了解具体的配置信息请查看

module.exports = {
  "plugins": {
    "postcss-import": {},
    "postcss-url": {},
    "autoprefixer": {}
  }
}

最终的webpack配置信息:

module.exports = {
  ...
  module: {
    rules: [{
      test: /\.s[ac]ss$/,
      use: [{
        loader: MiniCssExtractPlugin.loader,
        options: {
          publicPath: '../'
        }
      }, {
        loader: 'css-loader',
        options: {
          sourceMap: true
        }
      }, {
        // 需要添加postcss配置文件
        loader: 'postcss-loader',
        options: {
          sourceMap: true
        }
      }, {
        loader: 'sass-loader',
        options: {
          sourceMap: true
        }
      }]
    }]
  }
}

js文件

在工程项目中,我们一般会使用较新的es6语法来编写js代码,但是为了兼容不同的浏览器(万恶的ie),我么需要降低js语法的版本为es5来支持不同浏览器。 为此,我们需要babel大杀器来帮助我们转换代码。

首先安装babel以及配套插件,这里我提供下我的配置版本:

{
  "@babel/core": "^7.10.5",
  "@babel/preset-env": "^7.10.4",
  "babel-eslint": "^10.1.0",
  "babel-loader": "^8.1.0",
  "babel-plugin-syntax-jsx": "^6.18.0",
  "babel-plugin-transform-vue-jsx": "^3.7.0"
}

具体配置如下:

module.exports = {
  ...
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: 'babel-loader'
      }],
      include: [resolve('src')]
    }]
  }
}

为了让babel更好的工作,你需要在根目录提供babel的配置文件babel.config.js,这里我们不具体说如何配置babel,仅展示配置结果, 如果你对babel感兴趣,那么你可能会对我的babel系列教程感兴趣。

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'entry',
      // useBuiltIns: 'usage',
      corejs: 3,
      modules: false
    }]
  ],
  plugins: ['transform-vue-jsx', '@babel/plugin-transform-runtime']
};

静态资源文件

静态资源文件我们都使用url-loader来解析

首先安装:

npm i url-loader -D

具体配置如下:相关配置项可参考

  • url-loader

  • file-loader

  • 1、url-loader依赖file-loader

  • 2、当使用url-loader加载图片,图片大小小于上限值,则将图片转base64字符串,;否则使用file-loader加载图片,都是为了提高浏览器加载图片速度。

  • 3、使用url-loader加载图片比file-loader更优秀

module.exports = {
  module: {
    rules: [{
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'img/[name].[hash:8].[ext]',
        // 因为file-loader将此项配置默认为true,方便tree shaking
        // 但是这样会导致使用src='./images/....png'的引用方式失效,故设置为false使其生效
        // 建议采用ES modules的方式引入,例如:
        // const imgsrc = require('./images/login-img.png');
        esModule: false
      }
    },
    {
      test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'media/[name].[hash:7].[ext]',
        esModule: false
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'fonts/[name].[hash:7].[ext]',
        esModule: false
      }
    }]
  }
}

vue单文件

在项目中我们为了更直观的编写vue代码,会选择使用vue的单文件组件方式书写代码,但是一个新的文件后缀对于webpack来说是根本无法识别的, 那么这时候我们就需要专门对vue文件做预处理,vue-loader就是专门来处理这类文件的。具体内容请参阅

首先安装:

npm i vue-loader vue-template-compiler -D

注意你应该将 vue-loadervue-template-compiler 一起安装,并且需要保证你的vuevue-template-compiler的版本保持一致。

const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 请确保引入这个插件!
    new VueLoaderPlugin()
  ]
}

引入VueLoaderPlugin这个插件是必须的,它是用来解析vue文件的对应块来适应你已经配置的规则,例如: 我们前面配置了js和css的规则,那么这个插件就会将对应的<script>块的内容解析为普通js提供给webpack处理,同理 <style>块中的内容会解析给对应的语言提供给webpack处理。

vue单文件配置就这样很简单的结束了,基本不需要修改任何参数。

eslit

工程项目一般会开启eslint代码检查,但是我们更希望当我们在编写代码时可以实时监测我们的代码,那么配置eslint-loader是webpack是非常 有必要的。

由此此篇文章主要针对loader讲解,所以涉及eslint的知识,这里不会多说,只会给出基本配置,如果你对 eslint感兴趣,那么你可能会对我的eslint系列教程感兴趣。

module.exports = {
  root: true,
  env: {
    browser: true,
    es6: true,
    node: true
  },
  // 使用vue-eslint-parser解析vue文件
  parser: 'vue-eslint-parser',
  parserOptions: {
    // 使用babel-eslint解析js文件以及vuescript标签内的内容
    parser: 'babel-eslint',
    ecmaVersion: 8,
    sourceType: 'module'
  },
  extends: [
    // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
    // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
    'plugin:vue/essential',
    // https://github.com/standard/standard/blob/master/docs/RULES-en.md
    'standard'
  ],
  // required to lint *.vue files
  plugins: [
    'vue'
  ],
  // add your custom rules here
  rules: {
    // allow async-await
    'generator-star-spacing': 'off',
    // allow debugger during development
    'no-debugger': 'error',
    semi: ['error', 'always'],
    'no-var': 0 // 禁用var,用let和const代替
  }
};

我的eslint相关插件版本

{
  "eslint": "^7.6.0",
  "eslint-config-standard": "^14.1.1",
  "eslint-friendly-formatter": "^4.0.1",
  "eslint-loader": "^4.0.2",
  "eslint-plugin-import": "^2.22.0",
  "eslint-plugin-node": "^11.1.0",
  "eslint-plugin-promise": "^4.2.1",
  "eslint-plugin-standard": "^4.0.1",
  "eslint-plugin-vue": "^6.2.2"
}

下面提供eslint-loader的配置:

const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /\.(js|vue)$/,
        loader: 'eslint-loader',
        // 表示eslint-loader先执行,与babel-loader存在前后顺序,eslint-loader
        // 要检查装换之前的代码,所以eslint-loader先执行
        enforce: 'pre',
        include: [resolve('src')],
        options: {
          // 错误报告的输出规范
          formatter: require('eslint-friendly-formatter')
        }
      }
    ]
  },
  plugins: [
    // 请确保引入这个插件!
    new VueLoaderPlugin()
  ]
}

eslint-loader的作用是帮助我们检查我们书写的源代码的格式规范,所以他必须是最先接受源码的loader,如果代码已经经过了babel-loader的转换,再传给eslint-loader, 那么检测的代码就是转换之后的代码,此时的代码检查已经没有了任何意义,所以,必须保证eslint-loader是第一个接手源码的loader,这里采用配置项enforce: 'pre'来 告诉webpack,当你遇到js或是vue文件时,先交给eslint-loader处理,处理完之后再交给其他loader处理。

至此,我们针对css、sass、js、静态资源文件、vue单文件等都做了loader配置,并成功添加了eslint代码检查,下面展示最终配置:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

function resolve (dir) {
  return path.join(__dirname, '..', dir);
}

function processCss () {
  let obj = {};
  if (process.env.NODE_ENV === 'production') {
    obj = {
      loader: MiniCssExtractPlugin.loader,
      options: {
        publicPath: '../'
      }
    };
  } else {
    obj = {
      loader: 'vue-style-loader'
    };
  }
  return obj;
}

module.exports = {
  entry: {
    // 默认的输出文件名称,默认为main
    app: './src/index.js'
  },
  output: {
    filename: '[name].[hash:8].js',
    path: resolve('dist')
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: 'babel-loader'
      }],
      include: [resolve('src')]
    },
    {
      test: /\.(js|vue)$/,
      loader: 'eslint-loader',
      // 表示eslint-loader先执行,与babel-loader存在前后顺序,eslint-loader
      // 要检查装换之前的代码,所以eslint-loader先执行
      enforce: 'pre',
      include: [resolve('src')],
      options: {
        formatter: require('eslint-friendly-formatter')
      }
    },
    {
      test: /\.vue$/,
      use: [{
        loader: 'vue-loader'
      }]
    },
    {
      test: /\.css$/,
      use: [
        processCss(),
        {
          loader: 'css-loader',
          options: {}
        }]
    },
    {
      test: /\.s[ac]ss$/,
      use: [processCss(),
        {
          loader: 'css-loader',
          options: {
            sourceMap: true
          }
        }, {
        // 需要添加postcss配置文件
          loader: 'postcss-loader',
          options: {
            sourceMap: true
          }
        },
        {
          loader: 'sass-loader',
          options: {
            sourceMap: true
          }
        }
      ]
    },
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'img/[name].[hash:8].[ext]',
        // 因为file-loader将此项配置默认为true,方便tree shaking
        // 但是这样会导致使用src='./images/....png'的引用方式失效,故设置为false使其生效
        // 建议采用ES modules的方式引入,例如:
        // const imgsrc = require('./images/login-img.png');
        esModule: false
      }
    },
    {
      test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'media/[name].[hash:7].[ext]',
        esModule: false
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'fonts/[name].[hash:7].[ext]',
        esModule: false
      }
    }
    ]
  }
};

但是对于webpack来说,只有loader处理文件还不够,我们需要plugin(插件)来增强loader的作用,并实现一些高级功能,那么下面就让我们一同探索plugin的世界吧。

plugin

plugin扩展了webpack的能力,是的webpack可以实现任意的功能。下面提供本次项目的插件配置:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  plugins: [
    // 打包进度插件
    new webpack.ProgressPlugin(),
    // 清除打包文件夹插件
    new CleanWebpackPlugin({
      // true:模拟文件的删除即不删除文件
      // false:真是删除文件
      dry: false
    }),
    // 全局变量
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: 'production'
      }
    }),
    // 处理vue的特定插件
    new VueLoaderPlugin(),
    // 提取css为单独文件的插件
    new MiniCssExtractPlugin({
      // 将css打包到执行文件夹
      filename: 'css/[name].[hash:8].css'
    }),
    // 打包html插件,将动态的js插入HTML中
    new HtmlWebpackPlugin({
      title: '测试webpack',
      filename: 'index.html',
      // 控制将脚本插入到什么位置
      inject: true,
      // 是否压缩
      minify: true,
      template: './src/index.html'
    }),
    // 开启gzip压缩插件
    new CompressionPlugin({
      threshold: 10240
    }),
    // 复制文件夹插件
    new CopyWebpackPlugin({
      patterns: [{
        from: path.resolve(__dirname, './static'),
        to: './'
      }]
    })
  ]
}

实践

到这里,我们大致已经配置出一个完整的vue的项目打包配置,那么下面让我们添加一些vue的项目代码,来试试打包效果。

这里我们需要在vue项目中添加动态路由、sass样式、图片资源以及静态资源static文件夹

webpack4.png

下面让我执行打包,看下效果:

webpack5.gif

我们可以发现,动态路由正常生成了对应chunk文件,而且css和静态资源也各自打包进了各自的文件夹,生成的index.html文件 也正确的引用了正确路径的css和js文件。让我们看下打包后的文件在服务端跑起来是否正常:

webpack6.gif

很不错,正常运行了!

至此,我们已经完成了vue项目的打包配置,而且能够正常打包,打包后的文件也是可以正常运行的,虽然我们的项目可以正常打包, 但是我们不能总是打包后看效果,我们需要一个开发环境,能展示我们的页面效果,要是能实时响应我们的修改那就更好了,那接下来让我们一同探索下 开发服务器的配置过程吧。

dev-server

webpack提供开箱即用的开发服务器webpack-dev-server,它能帮助我们快速开发应用,便于调试,所以配置开发服务器是很有必要的。

因为devServer的配置项很多,我们只介绍常用的几个配置项。

devServer.compress

是否开启gzip压缩

module.exports = {
  devServer: {
    compress: true
  }
}

devServer.hostdevServer.port

定义开发服务器启动的地址和端口

devServer.hot

是否启用webpack的热更替功能。在配置文件中配置改选项时,还需要配合webpack.HotModuleReplacementPlugin一同才能生效。

如果在cli命令中以--hot的形式指定,那么将不需要配置webpack.HotModuleReplacementPlugin插件,因为webpack会自动调用该插件。

devServer.inline

在 dev-server 的两种不同模式之间切换。默认情况下,应用程序启用内联模式(inline mode)。 这意味着一段处理实时重载的脚本被插入到你的包(bundle)中,并且构建消息将会出现在浏览器控制台。

webpack --inline=true

此选项最好采用cli参数的方式指定(内联模式),因为它包含来自 websocket 的 HMR 触发器,这样启动的服务器允许你在不刷新浏览器的情况下响应你的程序更新。

devServer.open

是否打开浏览器

devServer.proxy

跨域处理

配置详情

配置开发服务器非常简单,只需要以下配置:

module.exports = {
  devServer: {
    hot: true,
    compress: true,
    host: 'localhost',
    port: '8080',
    open: false,
    publicPath: '/',
    quiet: true,
    stats: 'errors-only',
    proxy: {}
  }
}

然后修改我们的dev启动命令,改成利用webpack-dev-server来启动应用:

"dev": "webpack-dev-server --mode=development --progress --inline --hot --color"

注意这里的 --inline不可以在配置文件中配置,应在cli命令中指定,这样才会有热更新的效果。

启动服务器查看效果:

webpack7.gif

查看热更新效果:

webpack8.gif

在开发环境下的vue单文件组件中,建议采用vue-style-loader来替代mini-css-extract-plugin,这样vue文件的样式变化也会相应 热更新,就像在浏览器中直接改变css样式一样。配置如下:(sass文件一样配置)

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

webpack9.gif

至此,我们已经完整的配合了整个vue开发工程,包含本地调试和打包,这也是全部webpack的基础部分的内容,我相信,如果你跟着我一步步的来创建你的vue项目的话,现在应该已经完成了一个vue项目的搭建,是不是有点小自豪呢,其实,静下心来认真研究这些工具,你会发现其实它们没有想象的那么难,工具作者暴露出来的配置项是非常实用且简练的。如果你还想探索webpack更深层次的东西,那么请继续往下看,接下来我们会介绍webpack的高级用法,代码分割和tree shaking,以及如何编写一个loader和plugin,相信这些内容会让你对webpack有更深的理解。