【转】Webpack 快速上手(中)

因为文章篇幅较长,为了更好的阅读体验,本文分为上、中、下三篇:javascript

  • 上篇介绍了什么是 webpack,为何须要 webpack,webpack 的文件输入和输出css

  • 中篇介绍了 webpack 在输入和输出这段中间所作的事情,也就是 loader 和 pluginshtml

  • 下篇介绍了 webpack 的优化,以及在开发环境和生产环境的不一样用法前端

在上一篇中,介绍了经过设置 entry(入口文件)和output(出口文件),来对源代码进行处理,可是在处理过程当中,webpack 是如何针对不一样的文件进行打包的呢?这就是 loader 和 plugins 的要作的事情了。vue

loader

我的认为 loader 是 webpack 中最厉害的一个功能了,它让咱们能够在项目随意 import 各类类型的文件,css scss html img 等等都不在话下,若是有相关的 loader 支持,甚至能够 import 其它语言的代码。java

简单的说 loader 就是一个处理器,在 webpack 中配置好相应的 loader 以后,就能够在代码中像加载 JavaScript 模块同样使用 import 把其它类型的代码当作 JavaScript 模块加载。node

loader 的用法有三种

  1. webpack.config.js 中配置,这种方式是最经常使用的,下面会着重介绍。
  2. 在代码中显示的指定 loader ,下面的代码表示从 styles.css 加载样式文件,用 style-loadercss-loader 来处理 css 文件。
import styles from 'style-loader!css-loader?modules!./styles.css';
  1. 在命令行中为某些类型文件执行 loader 。下面的命令表示在打包过程当中,对 .css 文件使用 style-loadercss-loader 来处理。
webpack --module-bind 'css=style-loader!css-loader'

loader 的配置

loader 的配置其实比较简单,只是提供了太多简写,让新手有点摸不着头脑,首先用 JavaScript、TypeScript、css、scss 来展现经常使用的几种配置方式:react

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.js$/,
      loader: 'babel-loader',
      options: { presets: ['env'] },
      include: __dirname + '/src'
    },
    { test: /\.tsx?$/, use: 'ts-loader' },
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader']
    },
    {
      test: /\.scss$/,
      use: [
        { loader: 'style-loader' },
        {
          loader: 'css-loader',
          options: { modules: true }
        },
        { loader: 'postcss-loader' },
        { loader: 'sass-loader' }
      ]
    }]
  }
}

从上面的代码中咱们能够发现,use 这个选项的配置是最没节操了,它能够是字符串、数组、甚至被 loader 这个选项代替,其实这都是简写。rules.loaderloader.optionsrules.use: [ {loader, options} ] 的简写。这些配置项的含义分别是:webpack

  • test: 正则表达式,用来匹配文件的扩展名。
  • use: 对匹配出的文件使用的 loader 配置,如上面所说,该选项配置灵活,能够简写
  • loader: loader 名称
  • options: loader 的额外配置选项
  • include / exclude: 包括 或 排除 的文件夹,两个选项只能同时出现一个,上面的例子中 include: __dirname + '/src' 表示 babel-loader 只编译 /src 文件下的文件,其它的不作处理;相反的,exclude: __dirname + '/src' 表示不编译 /src 下的文件。

下面就来详细的介绍一下经常使用的 loader 和其配置。git

(题外话,原本是先将 babel-loader 放到第一个介绍的,可是因为篇幅较长,且有些难懂,因此将其放到了后面)

样式处理

npm i style-loader css-loader less less-loader node-sass sass-loader postcss-loader autoprefixer -D

对于样式文件的处理,咱们(我)一般会用到如下这些 loader :

  • style-loader
  • css-loader
  • postcss-loader
  • less-loader
  • sass-loader

那么这些 loader 的使用场景和区别是什么呢?

  1. 首先介绍 less-loader 和 sass-loader 。less 和 sass 都是 css 预处理器,可让 css 编写起来更爽,可是不能直接在浏览器中运行,因此须要先将 .less.scss 文件先转换成 css 。这就是 less-loader 和 sass-loader 的做用。

  2. 不管是直接编写的 css ,仍是由 less 或 sass 转换而来的 css 都不是 JavaScript 模块,这时候就要用到 css-loader ,它的做用就是把 css 转成 JavaScript 模块插入到代码中。

  3. 样式文件已经转换好了,但并不会产生任何效果。由于这些样式尚未添加到页面中,这时候就该轮到 style-loader 出场了,它的做用就是把转换后的样式添加到页面中,就像下面这样。

style-loader

  1. 最后还有 postcss-loader 它的做用也很强大,最经常使用的功能就是帮助咱们自动为一些样式属性名添加私有前戳(-moz、-ms、-webkit)。写过 vue 的同窗都知道,当咱们给 style 标签添加 scope 属性的时候,打包后的类名会自动添加自定义属性(例如 .panel[_v-72f4cef2]),这个功能就是基于 postcss-loader 实现的。

postcss 须要一份配置文件,这份配置文件以写在单独的文件中 (postcss.config.js),也能够写在 package.jsonpostcss 属性中:

{
  "postcss": {
    "plugins": {
      "autoprefixer":{}
    }
  }
}

对 postcss 感兴趣的同窗能够看看这篇文章: PostCSS真的太好用了! (https://segmentfault.com/a/1190000014782560)

这些 loader 的执行顺序是 :

sass-loader or less-loaderpostcss-loadercss-loaderstyle-loader

经过对这些 loader 的配置,咱们就能够把样式文件当作 js 文件同样引入了。

// styles.css
.red { color: red; }
// index.js
import './styles.css';

这里须要在额外提一下 css module ,这也是一个很好的特性,写 react 的朋友对它应该很熟悉:

// index.js
import styles from './styles.css';

export default () => (
  <h2 className={styles.red}>css module</h2>
);

从上面的代码中能够看出,咱们将样式当作 对象 styles 导入 jsx 中,那么该样式下的全部类名就是 styles 的属性名了。

这样的写法也一样适用于 ES6 的模板字符串:

// index.js
import styles from './styles.css';

const html = `<h2 class="${styles.title}">css module</h2>`;

document.body.innerHTML = html;

只要在 css-loader 的 options 中设置 { modules: true } 既能够开启此功能。

上面的这些配置,能够帮咱们将 css 封装成 js对象 ,打包在 .js 文件中,而后运行的时候,以 <style></style> 的方式动态插入到页面中,但咱们更但愿能够将这些样式从 js 文件中抽取出来放到 css 文件,一来这样显得更优雅一些,二来能够减小 js 为文件体积,避免动态建立 style 标签所带来的性能损耗。这个功能须要在 plugins 中进行设置,下面也会讲到。

file-loader、url-loader

npm i file-loader url-loader -D

若是咱们在页面中经过相对路径来引入图片、音频、视频、字体等文件资源时,在 webpack 环境中可能出现路径错误404的问题。主要缘由是 开发时的目录结构打包后的目录结构 通常都是不同的,所以致使路径失效,而 file-loader 就是为了解决这个问题的。

  • file-loader 能够解析页面中引入的资源的路径,而后根据配置,将这些资源拷贝到打包后的目录中。

  • url-loader 则是对 file-loader 进行了一次封装,若是解析的资源是图片,则能够将改图片转成 base64 从而减小 http 请求一提高性能,同时也能够设置 limit。 只对指定大小的图片进行转换。

一样的也能够在 js 中引入资源

// index.js
import logo from './images/logo.png';

const img = new Image();
img.addEventListener('load', () => document.body.appendChild(img));
img.src = logo;

下面是 url-loader 的简单配置参考:

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use: [{
        loader: 'url-loader',
        options: {
          limit: 10000, // 10KB 转换为base64
          name: 'images/[name].[ext]' // 拷贝到 images 目录下
        }
      }]
    }]
  }
}

html-loader

npm i html-loader -D

在 Web 开发中,一般会用到不少 html 模板,传统的方式是将模板存在服务端,前端经过 http 请求加载模板,或者在 JavaScript 中拼接字符串,或者在页面中将模板内容写在 <script type="text/template"></script> 内。

而在 webpack 环境下,咱们也能够把 html模板 当作 JavaScript 的模块来加载,以 Vue 为例:

<!-- template.html -->
<h2>{{ title }}</h2>
// index.js
import tpl from './template.html'

new Vue({
  el: '#app',
  template: tpl,
  data: {
    title: 'Hello Webpack'
  }
});

在上面代码中,咱们将 template.html 的内容以字符串方式导出,这正是 html-loader 的功能,也能够在配置只启用压缩功能。

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.html$/,
      use: [{
        loader: 'html-loader',
        options: {
          minimize: true // 开启压缩
        }
      }]
    }]
  }
}

babel-loader(重点)

npm i @babel/core babel-loader @babel/preset-env @babel/runtime @babel/plugin-transform-runtime -D

Babel is a compiler for writing next generation JavaScript.

从官方的简短介绍中能够知道, babel 属于编译器,输入 JavaScript 源码,输出 JavaScript 源码(source to source),其做用就是将目前部分浏览器目前还不支持的 ES2015+ 语法转换为 ES5 语法。

babel-loader 则是让 babel 能够在 webpack 中使用的工具,同理若是你使用的是 gulp ,则须要用到 gulp-babel 这个包。

实际上,若是只是用 babel 的话,输入的代码和编译后输出的代码是相同的(被 webpack 混淆打包的代码与 babel 无关)。由于 babel 的转换工做全都是由 babel 的插件来完成的

关于 babel 的介绍和使用,仅仅一个小节的篇幅是彻底不够,因此这里贴一个连接,有兴趣的读者一点要点进去看一下 一口(很长的)气了解 babel

babel 也是须要进行配置的,通常有两种方式:

  1. 在根目录建立 .babelrc
  2. package.jsonbabel 属性中进行配置

我更倾向于在 package.json 进行配置,由于根目录放置太多文件,强迫症实在没法接受。不管是在 .babelrc 仍是 package.json 中配置,配置的内容都是同样的,下面以在 package.json 中配置为例:

{
  "babel": {
    "presets": [
      [
        "@babel/preset-env",
        {
          "modules": false,
          "targets": {
            "browsers": [
              "> 1%",
              "last 2 versions",
              "not ie <= 8"
            ]
          }
        }
      ]
    ],
    "plugins": [
      "@babel/plugin-transform-runtime",
      "@babel/plugin-syntax-dynamic-import"
    ]
  }
}

在该配置中,presetsplugins 对应的值都是数组,同时数组的每一项能够是 string (只指定名字),也能够是 array (指定名字,并进行更具体的配置)

plugins 表示用到的插件,好比咱们在代码中使用到了 import() 动态加载模块这个语法,那么就要在 plugins 添加 @babel/plugin-syntax-dynamic-import 这个插件了;咱们须要对 babel 编译后的代码进行去重,就须要用到 @babel/plugin-transform-runtime 。 固然,这两个插件也是须要单独安装的 npm i @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import -D

presets 一组 plugins 的集合。好比咱们能够把 @babel/plugin-transform-runtime 和 @babel/plugin-syntax-dynamic-import 打包到一块儿,叫 preset-my ,这样咱们只须要在 presets 中添加 preset-my 就能够了,省去了对 plugins 的配置 。上面的配置文件只配置一个 @babel/preset-env ,这是最经常使用的配置,@babel/preset-env 后面的对象是对 @babel/preset-env 具体配置。咱们注意到,其中有一个 targets.browsers 属性,指定了浏览器版本,这个属性也能够放在 package.jsonbrowserslist 中。

为何配置了 presets 还须要配置 plugins 呢?很简单,如上面所说, presets 是一组 plugins 的集合,也就说 babel 对不一样阶段的语法作了整合,方便咱们使用。可是在上面的配置中,咱们只使用了 @babel/preset-env 这个集合里的插件,而 import() 处于 stage-3 阶段(记不太清了,也多是 stage-2),不包含于 @babel/preset-env ,因此就须要在 plugins 单独添加 @babel/plugin-syntax-dynamic-import 插件来对 import() 语法进行转换了。

社区中也提供了一些 presets ,好比 react 的 @babel/preset-react , vue 的 @vue/babel-preset-app

babel 的执行顺序是:

读取plugins数组按正序执行plugins内插件读取presets数组按倒序执行presets内容

简单的介绍了 babel 后,开始配置 babel-loader :

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.js$/,
      loader: 'babel-loader',
      // options: { presets: ['env'] }, 该项的配置和上面babel的配置彻底相同,已经在package.json配置过,这里不须要再配置
      include: __dirname + '/src' // 只对 ./src 目录下的代码进行编译
    }]
  }
}

ts-loader

npm i typescript ts-loader -D

若是你的项目是用 typescript 开发的,这时候就要样到 ts-loader 了。

ts-loader 的配置比较简单,可是有许多须要注意的细节,详情能够参照这里:https://github.com/TypeStrong/ts-loader/blob/master/README.md#configuration

plugins

讲完了 entry、output 和 loader,下面开始讲讲 plugins 。细心的读者应该已经发现,尚未提到代码的压缩,并且按照上面的方式打包会把 .css.js 文件打包在一块儿,而且打包后的文件体积很大,可能还会存在冗余的代码等等一些问题,plugins 就是为了解决这类问题而产生的。

这里不要把 loaderplugins 搞混了,laoder 只是把特定的文件类型转换成 JavaScript 模块plugins 是在打包过程当中对全部模块进行特定的操做plugins 的值是一个数组,全部的 webpack 都须要手动经过关键字 new 来实例化。 下面就介绍一些常见的插件。

html-webpack-plugin

npm i html-webpack-plugin -D

webpack 是对 JavaScript 进行打包的,打包出的只能是 .js 文件。 而 JavaScript 要想在浏览器中运行,那就必须在 html 中经过 script 的方式引入。在没有其余工具帮助的状况下,咱们只能手动建立 html 文件,而后再把打包后的 .js 文件和 .css 文件写到这个文件中,这样作很麻烦。这时候能够用 html-webpack-plugin 这个插件来自动完成上面的工做。

html-webpack-plugin 提供了一些配置项,若是不行配置,它会自动帮我建立一个空的 html 文件,而后将打包后的资源插入到这个页面内:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new HtmlWebpackPlugin() // 建立 /dist/index.html 文件,并将 index_bundle.js 插入到这个页面中。
  ]
}

一样,咱们也能够为其指定一个模板页:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html', // 生成的文件名称,默认为 index.html
      template: 'src/index.html', // 以 src/index.html 为模板文件
      inject: 'body', // 将打包后的文件注入到 body 区域内
      title: 'Hello webpack', // 生成文件的标题
      minify: {       // 对生成的文件进行压缩,能够设置为 true ,也能够是对向,进行更具体的配置
        collapseWhitespace: true,   // 删除空格
        minifyCSS: true,
        minifyJS: true,
        removeAttributeQuotes: true,
        removeComments: true,
        removeTagWhitespace: true,
      }
    })
  ]
}

插件也能够经过屡次实例化来重复使用:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/index.html',
      chunks: ['index', 'vendor'] // 只注入 index.bundle.js 和 vendor.bundle.js 
    }),
    new HtmlWebpackPlugin({
      filename: 'about.html',
      template: 'src/about.html',
      excludeChunks: ['index'] // 将 index.bundle.js 排除,其他的都注入
    })
  ]
}

分离css和js

  • webpack v4
npm i mini-css-extract-plugin -D

前面在介绍用 loader 处理样式的时候说到,这些样式最终会被混入到打包后的 .js 文件中,在页面运行的时候,在以 <style></style> 的方式动态的插入到 DOM 节点中,这种作法有两个很明显的缺点:

  1. js 和 css 糅杂在一块儿,增长了单个文件的体积。
  2. 在页面运行时动态的去建立 style 标签,多多少少会有些性能影响

若是能把这些 css 从打包后的 js 中抽取出来,就能够解决上面的两个问题,这时候就要用到 mini-css-extract-plugin 这个插件了。

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.css$/,
      use: [MiniCssExtractPlugin.loader, 'css-loader']
    }]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css',
    })
  ]
}

从上面的配置中能够看出,mini-css-extract-plugin 并非单独做为一个 plugin 来使用的,它还充当了 loader 的做用,代替了 style-loader 。前面在介绍 style-loader 的时候提到,它的做用是将转换后的样式插入到页面中,既然咱们如今须要将 css 和 js 分离开,因此也就不须要再用到 style-loader 了。

看成为插件使用的时候, mini-css-extract-plugin 能够接受两个可选参数:

  • filename :分离出的css文件名称,写法和 output 的 filename 选项相同,惟一区别是当你想使用缓存的时候,填写的是 contenthash 而不是 chunkhash
  • chunkFilename :切割出的css文件块名称,写法和 filename 相同

最近发现 extract-text-webpack-plugin 也支持 webpack4 用法了 mini-css-extract-plugin 彻底相同,并且相较于 mini-css-extract-plugin 还多了一些可选的配置

npm i extract-text-webpack-plugin -D
// webpack.config.js
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');

module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.css$/,
      use: [ExtractCssChunksPlugin.loader, 'style-loader']
    }]
  },
  plugins: [
    new ExtractCssChunksPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css',
      hot: true, //HMR 下面会着重介绍
      orderWarning: true, // Disable to remove warnings about conflicting order between imports
      reloadAll: true, //当启用HMR时,强制从新加载全部css
      cssModules: true //若是启用了 cssModules 此选项设置为 true
    })
  ]
}

压缩css

npm i optimize-css-assets-webpack-plugin -D

在将 css 从 js 中分离出来不以前,咱们是不须要考虑压缩 css 的,由于样式都被打包进了 js 文件中,当咱们设置 mode 为 production 时,webpack 会自动压缩 js 文件。可是咱们如今将 css 从 js 中分离出来了,webpack 目前还不能自动压缩 css 文件。干!真是麻烦!这时候又要用到插件来帮我压缩分离出来的 css 文件了。

// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new OptimizeCssAssetsPlugin()
  ]
}

这里讲一个坑,在 webpack4 以前,压缩都是经过 webpack.optimize.UglifyJsPlugin 这个插件来完成。webpack4 新增了 mode 和 optimization 两个选项,当 mode 设置为 production 时会自动压缩 js 文件(这个已经提过屡次了),其实将 mode 设置为 production 时, optimization.minimize 便会默认设置为 true ,意思就是在打包的时候对 js 进行压缩。而若是你想用第三方压缩插件,你能够将插件写在 plugins 中,也能够写在 optimization.minimizer 中。可是如你将压缩插件写在 optimization.minimizer 中时,webpack 就会默认读取 ptimizatio.minimizer 这个选项了,这也就意味着,这时候若是你不手动的配置 js 压缩插件,js 文件是不会被压缩,这时候又须要寻找压缩 js 的插件,好比 uglifyjs-webpack-plugin ,而后再配置一下,说实话这样真的很烦,因此我直接将压缩的插件配置在了 plugins 中,这样就省去了对 js 压缩插件的配置。webpack 的文档中描述了相关说明 Minimizing For Production

复制静态资源

npm i copy-webpack-plugin -D

有时候咱们的项目中会有一些静态资源,好比网站的favicon、你从不知道的地方找来的不知名的js插件等等,这些静态资源并不会在项目中经过 import 的方式显式的加载进来,而是在直接写在页面中

...
<link rel="shortcut icon" href="static/favicon.ico">
...
<script src="static/xxx.js"></script>

对于这些静态资源,webpack 在打包过程当中不会对它们进行处理,全部须要咱们 copy 到打包后的目录中,从而保证项目不会由于缺乏这些静态文件而报错, copy-webpack-plugin 的做用即是 copy 这些静态资源到指定的目录中的。

// webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new CopyWebpackPlugin([
      { from:'static/**',to: 'dist/static' }
    ])
  ]
}

上面的配置表示将 static 文件夹下全部的文件都复制到 dist/static 下面,若是你熟悉 gulp 的话,你会发现这其实就是一个移除了 pipe 的 gulp。

其实对于copy文件这种脏活累活你也能够用你熟悉的方式来完成,好比 gulp、fs-extra 等。

clean-webpack-plugin

npm i clean-webpack-plugin -D

若是咱们打包输出的文件使用了 chunkhash 、 hash 等来命名的话,随着文件的变动和打包次数的增长,dist 目录会淤积不少无用的打包文件,这时候即可以借助 clean-webpack-plugin 帮咱们清除一些这些无用的文件

// webpack.config.js
const CleanWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new CleanWebpackPlugin('dist', {
      root: __dirname,
      verbose: true,
      dry: false
    })
  ]
}

和 copy 文件同样,删除文件这种话不必定非得让 webpack 来作,咱们也能够借助其余的方式来完成,好比我要再提一遍的的 gulp ,又或者 rimrafdel 等。可是区别是你须要手动的控制一下任务的流程,总不能在打包完成才删除问吧,因此用 webpack 提供的插件是不须要考虑任务流程的问题。

上面介绍了5个 webpack 的 plugin ,主要目的是让你们体会 webpack plugin 的做用基本用法。 实际上 webpack 的 plugin 还有不少不少,几乎能够知足你在项目构建中的各类需求,webpack 官网了列举不少官方推荐的 plugin https://webpack.js.org/plugins/ ,有兴趣的同窗能够前往查看。

相关文章
相关标签/搜索