webpack系统学习之路——基础篇

2,225 阅读22分钟

前言

用vue-cli脚手架自动生成项目,里面的webpack配置苦涩难懂,于是下定决心想要系统学习webpack,写下此文来记录学习过程。

一. webpack主要是做什么的?

首先,webpack是前端流行的模块打包工具,使前端项目可以面向对象开发(模块化),与Node.js模块不同,webpack模块可以用多种方式表达它们的依赖关系。以下是几个例子:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD definerequire 语句
  • @import 语句可以引入css/sass/less文件
  • 样式表 url(...) 或 HTML <img src=...> 文件中的图像url

二. 安装并配置webpack.config.js文件

2.1 安装webpack

// 1. 初始化项目
npm init -y

// 2. 安装webpack以及安装webpack-cli(用于在命令行上运行webpack的工具), 不建议全局安装
npm install webpack webpack-cli --save-dev

2.2 配置webpack.config.js文件

  1. 默认配置

    在没有配置webpack的出入口文件时,webpack有为我们提供默认配置,此时只需要执行以下命令行:

    // 必须告诉webpack你的入口文件是index.js
    npx webpack index.js 
    

    其中index.js(index.js依赖着a.js,a.js依赖着b.js,b.js依赖着c.js)是入口文件,webpack的默认配置会自动在当前项目的根目录下新建dist/main.js作为出口文件,入口文件经过webpack的打包,就可以将模块之间的依赖关系翻译出浏览器能够识别的代码,此时只需要在index.html文件引入<script src="./dist/main.js"></script>

    但是npx webpack index.js 中的npx 是 什么意思?

    譬如,当我们全局安装webpack时,输入命令行webpack -v就可以直接获取版本,但当我们在项目内局部安装(--save-dev) webpack时,输入命令行webpack -v 会报错(全局webpack删除的情况下),因为系统会默认到本地文件(全局)中搜索webpack,此时是搜索不到的,因此会报错。但是输入npx webpack -v 就可以获取当前项目内的webpack版本。所以,npx的含义就是在当前项目下的依赖包node_modules 去查找webpack,同理可得npx webpack index.js的含义就是在当前项目的node_modules中找到webpack并执行webpack index.js命令打包入口文件。


  2. 自己配置

在当前项目的根目录下新建webpack.config.js文件

// webpack.config.js

const path = require('path')

module.exports = {
  // mode的作用是配置运行环境(development/production)
  // development:打包后的bundle.js文件不会被压缩; production: 打包后的bundle.js会被压缩成一行代码
  mode: 'development',
  // 入口文件
  entry: './src/index.js',
  // 出口文件
  output: {
    // 文件名
    filename: 'bundle.js',
    // 打包后的文件放在哪个文件夹,采用绝对路径,在webpack.config.js当前目录下新建文件夹dist
    path: path.resolve(__dirname, 'dist')
  }
}

 此时再输入命令行

npx webpack

就会发现打包完成并且在项目根目录下多了dist/bundle.js文件,

此时在index.html文件引入<script src="./dist/bundle.js"></script>

值得注意的是,webpack.config.js文件名是webpack默认的配置文件名,如果把webpack.config.js换成其他名字如config.js后再输入npx webpack会发现配置失败

解决方法是,输入以下命令行

// 意思就是告诉webpack我的配置文件叫做config.js,并帮我打包
npx webpack --config config.js

如何简化命令?

可以在package.json文件(一开始初始化项目npm init -y所生成的package.json文件)的"scripts"属性添加

"scripts": {
    "build": "webpack"
}

这样以后要打包文件,直接运行以下命令行就可以完成打包

npm run build


现在,我们项目的目录结构是:


为了接下来操作方便,我们将Index.html放入dist根目录下(index.html修改相关配置<script src=".bundle"></script>


三. 更多基础配置

webpack除了可以打包js文件,还可以打包其他文件,如:css/img

那么我们来尝试加载.jpg文件

// index.js
const img = require('img.jpg')

运行npm run build

报错:
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type.

意思就是:
模块解析失败:意外字符'�'(1:0]您可能需要适当的加载程序来处理此文件类型。

因此,对于非js文件,webpack虽然可以打包,但是webpack却不知如何打包,必须你去告诉webpack该怎么执行,这时就需要用到loader

3.1 使用loader打包非js文件


打开我们的webpack.config.js,添加"module"属性

const path = require('path')

module.exports = {
  // mode的作用是配置运行环境(development/production)
  // development: 打包后的bundle.js不会被压缩; production: 打包后的bundle.js会被压缩成一行代码
  mode: 'development',
  // 入口文件
  entry: './src/index.js',
  // 出口文件
  output: {
    // 文件名
    filename: 'bundle.js',
    // 打包后的文件放在哪个文件夹,采用绝对路径
    path: path.resolve(__dirname, 'dist')
  },
  // 该模块表示当打包除了js文件外webpack不知如何打包时在这里查看打包规则
  module: {
    rules: [{
      test: /\.(jpg|png|gif)$/,        // 当打包以 .jpg|.png|.gif 结尾的文件      use: {                    // 使用file-loader来打包
        loader: 'file-loader'
      }
    }]
  }
}

配置完后,不要忘记下载file-loader依赖npm install file-loader --save-dev

此时再重新打包,会发现.jpg打包成功。此时打开dist文件,里面除了bundle.js文件,还多了一个.jpg文件

在index.js尝试打印require('img.jpg')的返回值,发现返回值是img在dist目录下的文件名


此时我们可以得出loader的打包机制:

发现非js文件-->将非js文件复制一份放到dist并修改名称-->将文件名返回来方便你去根据路径引用它,接下来将对loader进行详细的介绍


  1.  使用Loader 打包静态资源(图片篇)—— file-loader/url-loader

    a. 使用file-loader对图片进行打包

    上面的简单例子让我们对loader有初步的认识,那么如果当我们打包后的图片文件名不想要一串哈希值的时候,又或者不想直接存放在dist目录,想在dist目录下创建一个images目录专门存放图片时该怎么办?继续打开webpack.config.js,给use添加"options"属性

    const path = require('path')
    
    module.exports = {
      // mode的作用是配置运行环境(development/production)
      // development: 打包后的bundle.js不会被压缩; production: 打包后的bundle.js会被压缩成一行代码
      mode: 'development',
      // 入口文件
      entry: './src/index.js',
      // 出口文件
      output: {
        // 文件名
        filename: 'bundle.js',
        // 打包后的文件放在哪个文件夹,采用绝对路径
        path: path.resolve(__dirname, 'dist')
      },
      // 该模块表示当打包除了js文件外webpack不知如何打包时在这里查看打包规则
      module: {
        rules: [{
          test: /\.(jpg|png|gif)$/,        // 当打包以 .jpg|.png|.gif 结尾的文件
          use: {                    // 使用file-loader来打包
            loader: 'file-loader',
    	options: {        // 可选的
                // placeholder 占位符配置打包后的文件名:[name]是原先的文件名,[ext]是原先的文件后缀,[hash]是一串哈希值
                 name: '[name]_[hash].[ext]', 
                // 打包后在dist目录下的路径
                 outputPath:  'images/'    // 打包后图片存放在dist/images
            }
          }
        }]
      }
    }

    以上是关于图片打包的常用配置,可以满足大多数业务需求,关于file-loader的更多配置,详情可见webpack的官方文档

    b. 使用url-loader对图片进行打包

    事实上,url-loader可以完成file-loader所能完成的任务

    先下载url-loader npm install url-loader --save-dev

    并把webpack.config.js中的loader配置换成'url-loader'

    重新对项目进行打包试试

    // webpack.config.js
    
    const path = require('path')
    
    module.exports = {
      // mode的作用是配置运行环境(development/production)
      // development: 打包后的bundle.js不会被压缩; production: 打包后的bundle.js会被压缩成一行代码
      mode: 'development',
      // 入口文件
      entry: './src/index.js',
      // 出口文件
      output: {
        // 文件名
        filename: 'bundle.js',
        // 打包后的文件放在哪个文件夹,采用绝对路径
        path: path.resolve(__dirname, 'dist')
      },
      // 该模块表示当打包除了js文件外webpack不知如何打包时在这里查看打包规则
      module: {
        rules: [{
          test: /\.(jpg|png|gif)$/,        // 当打包以 .jpg|.png|.gif 结尾的文件
          use: {                    // 使用url-loader来打包
            loader: 'url-loader',
    	options: {        // 可选的
                // placeholder 占位符配置打包后的文件名:[name]是原先的文件名,[ext]是原先的文件后缀,[hash]是一串哈希值
                 name: '[name]_[hash].[ext]', 
                // 打包后在dist目录下的路径
                 outputPath:  'images/'    // 打包后图片存放在dist/images
            }
          }
        }]
      }
    }

    // index.js
    
    const avatar = require('../avatar.jpg')
    const root = document.querySelector('.root')
    const image = new Image()
    image.src = avatar
    root.append(image)

    // index.html
    
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport"
            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div class="root"></div>
    <script src="./bundle.js" type="text/javascript"></script>
    </body>
    </html>
    
    

    打包完成会发现,dist目录下并没有image目录,也没有图片文件,但是在网页上打开index.html却发现图片依然可以显示在页面上,这是我们打开bundle.js下拉到最下面发现


    我们再打开index.html页面查看img元素

    于是我们可以得出结论:url-loader可以将打包后的图片生成base64并嵌入bundle.js文件中,这样带来的结果是:

    好处:减少页面对图片发送http请求,提升了性能

    坏处:若图片太大,会导致bundle.js文件太大,网页加载js请求时间太长,而这段时间页面上没有任何东西,用户体验差。

    那我们是否做一个限制:当图片比较小的时候,用url-loader对图片进行打包以减少http请求;当图片比较大时,用file-loader打包,提高用户体验。

    于是我们继续打开webpack.config.js为options添加属性"limit"

    // webpack.config.js
    
    const path = require('path')
    
    module.exports = {
      // mode的作用是配置运行环境(development/production)
      // development: 打包后的bundle.js不会被压缩; production: 打包后的bundle.js会被压缩成一行代码
      mode: 'development',
      // 入口文件
      entry: './src/index.js',
      // 出口文件
      output: {
        // 文件名
        filename: 'bundle.js',
        // 打包后的文件放在哪个文件夹,采用绝对路径
        path: path.resolve(__dirname, 'dist')
      },
      // 该模块表示当打包除了js文件外webpack不知如何打包时在这里查看打包规则
      module: {
        rules: [{
          test: /\.(jpg|png|gif)$/,        // 当打包以 .jpg|.png|.gif 结尾的文件
          use: {                    // 使用url-loader来打包
            loader: 'url-loader',
    	options: {        // 可选的
                // placeholder 占位符配置打包后的文件名:[name]是原先的文件名,[ext]是原先的文件后缀,[hash]是一串哈希值
                 name: '[name]_[hash].[ext]', 
                // 打包后在dist目录下的路径
                 outputPath:  'images/'    // 打包后图片存放在dist/images,
                 limit: 204800       // 当图片小于200KB时使用url-loader打包
            }
          }
        }]
      }
    }

    重新打包,通过修改limit的值,可以发现当小于limit值,图片以base64的形式打包到bundle.js,当大于limit值,图片被打包到dist/images目录下

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

    a. 使用style-loader 和 css-loader对css文件进行打包

    首先我们在src目录下新建一个index.css文件,对上文的图片添加样式

    // src/index.css
    
    .avatar {
        width: 150px;
        height: 200px;
        transform: translate(100px, 100px);
    }

    在index.js中引入

    // src/index.js
    
    import './index.css'
    import avatar from '../avatar.jpg'
    
    const root = document.querySelector('.root')
    const image = new Image()
    image.src = avatar
    image.classList.add('avatar')
    root.append(image)
    
    

    然后重新对项目进行打包,如果对loader理解透彻的话不用打包也知道webpack是不知道如何打包非js文件的,需要借助loader,因此现在打包文件肯定会报错。

    打开webpack.config.js进行配置,添加以下的配置,并下载style-loader以及css-loader,下载完成后重新打包


    打包后打开Index.html,发现图片确实添加上了样式



    仔细一看发现样式被自动添加到index.html的<style></style>标签下面,现在我们大概可以知道style-loader和css-loader的作用:

    style-loader: 将打包后的css代码添加到index.html的<style></style>标签

    css-loader: 将从js文件下引入的css文件打包

    b. 使用sass-loader对.scss文件进行打包

    由于我们在开发项目时更多地是使用sass/stylus/less来写样式,下文举sass的例子,stylus/less是类似的操作

    将index.css的后缀改成.scss后对index.css的代码修改成.scss的语法

    // index.scss
    
    body {
        .avatar {
            width: 150px;
            height: 200px;
            transform: translate(100px, 100px);
        }
    }

    在webpack.config.js的配置中添加sass-loader,并下载

    sass-loader需要您自己安装Node Sass或Dart Sass,这里我们下载node-sass。这使您可以控制所有依赖项的版本,并选择要使用的Sass实现.

    npm install sass-loader node-sass webpack --save-dev



    c. 使用postcss-loader

    上面我们使用到了css3的transform这个属性,我们知道css3的新属性需要兼容浏览器,但是如果我们每次都手动去兼容浏览器会很繁琐,于是webpack为我们提供了postcss-loader

    首先下载

    npm install postcss-loader --save-dev

    打开webpack.config.js配置postcss-loader


    官方文档要求使用postcss-loader需要单独在项目根目录下创建一个postcss.config.js,当webpack打包时发现需要用到postcss-loader时,就会去postcss.config.js查看

    // postcss.config.js
    
    module.exports = {
      plugins: [
        // 插件autoprefixer,需要下载(npm install autoprefixer --save-dev),用来自动添加兼容浏览器的厂商前缀
         require('autoprefixer')
      ]
    }

    重新打包打开页面发现已经自动为我们兼容了css3了


    d. 补充——importLoaders, modules

    webpack遇到scss文件打包时,会自下而上/自右向左调用webpack.config.js的loader,如

    根据我们上面的配置,webpack会依次调用postcss-loader-->sass-loader-->css-loader-->style-css

    但有时候我们的scss文件不止一个,比如在index.scss文件中引入@import "./avatar.scss",

    此时对@import进来的scss文件,webpack可能已经调用了postcss-loader-->sass-loader,所以此时@import进来的scss文件会错过这两个loader,为了防止这种情况的发生,我们需要对css-loader进一步配置

    {
          test: /\.scss$/,
          use: [
             // 将css添加到<style></style>标签
            'style-loader',
             // 将css文件能被webpack打包
            {
              loader: 'css-loader',
              options: {
                // 2 ==> 加载css-loader之前都必须先重新加载一次postcss-loader-->sass-loader
                importLoaders: 2
              }
            },
             // 将sass代码能被webpack转换成css代码
            'sass-loader',
             // 将sass代码中相关的css3属性添加浏览器兼容代码
             'postcss-loader'
          ]
    }

    另一方面,如果我们怕在index.js中全局引入的index.scss会污染其他scss文件造成冲突甚至覆盖,也就是说我们想要css也能模块化编程,避免耦合 。

    {
          test: /\.scss$/,
          use: [
             // 将css添加到<style></style>标签
            'style-loader',
             // 将css文件能被webpack打包
            {
              loader: 'css-loader',
              options: {
                // 2 ==> 加载css-loader之前都必须先重新加载一次postcss-loader-->sass-loader
                importLoaders: 2,
                // 使css文件模块化,互不干扰
                modules: true
              }
            },
             // 将sass代码能被webpack转换成css代码
            'sass-loader',
             // 将sass代码中相关的css3属性添加浏览器兼容代码
             'postcss-loader'
          ]
    }

    此时我们在index.html中引用index.scss时,可以采用模块化编程



  3.  使用Loader 打包静态资源(字体文件篇)

    如何引入字体文件详情可见 icomoon的使用

    这里只讲如何打包图标字体相关的文件woff|eot|ttf|otf


3.2 使用plugins(插件)使打包更加便捷

  1. html-webpack-plugin

    由于我们的index.html文件在dist目录下,但是项目新建使并没有dist目录,这意味我们需要先打包,打包完成后再手动添加index.html文件在dist目录并引入bundle.js

    为了解决这个繁琐的过程,webpack为我们提供了一个插件——html-webpack-plugin,它使得我们打包完成后自动在dist目录下生成index.html文件并自动导入bundle.js

    首先,下载 npm install html-webpack-plugin --save-dev
    在webpack.config.js

    // webpack.config.js
    
    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports =  {
      mode: 'development',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
      },
      // 该属性是关于webpack插件的配置
      plugins: [
    // 实例化     new HtmlWebpackPlugin()
      ]
    }
    
    
    

       打包,打开dist目录下


     另一个问题出现,如果我们需要对index.html文件做一些初始化呢,比如需要添加一个根节点呢<div id="app"></div>。我们可以在实例化new HtmlWebpackConfig 的时候进行一些配置

// webpack.config.js

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

module.exports =  {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  // 跟webpack插件相关的配置
  plugins: [
     new HtmlWebpackPlugin({
       template: 'src/index.html'     // 模板
     })
  ]
}

// src/index.html ==> 模板

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>html 模板</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

重新打包,点开打包后的dist/index.html文件查看



2.  clean-webpack-plugin

如果我们把打包后的js文件名bundle.js更改成app.js,重新打包,会发现dist目录下原先的bundle.js没有删除,为了避免这种情况,我们就需要每次打包前手动删除dist目录。

同样,为了解决这个繁琐的过程,我们可以使用第三方插件(webpack官方没有的插件)—— clean-webpack-plugin,它会帮我们在每次打包前把output配置的相应目录删除

下载 npm i clean-webpack-plugin -D 

在webpack.config.js

// webpack.config.js

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

module.exports =  {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  // 跟webpack插件相关的配置
  plugins: [
     // 自动在dist目录下生成index.html
     new HtmlWebpackPlugin({
       template: 'src/index.html'     // 模板
     }),
     // 每次打包前先自动删除output配置的目录
     new CleanWebpackPlugin()
  ]
}


更多插件的使用见 webpack官网


3.3 entry 和 output 的基础配置

// webpack.config.js


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

module.exports =  {
  mode: 'development',

  // entry: './src/index.js', ==> 相当于 entry: { main: './src/index.js' }
  entry: {
    // 默认情况路径的键名为main
    main: './src/index.js',

    // 第二次打包,当打包入口文件超过一个使,需要在output使用占位符进行相应的配置
    sub: './src/index.js'
  },

  output: {
    // 公共路径:publicPath与filename拼接形成最终路径如http://cdn.com.cn/main.js,
    // 会在webpack打包形成的index.html文件中自动引入该路径
    publicPath: 'http://cdn.com.cn',

    // [name]占位符,值对应的是entry的键名(main, sub)
    filename: '[name].js',

    path: path.resolve(__dirname, 'dist')
  },

  // 跟webpack插件相关的配置
  plugins: [
     // 自动在dist目录下生成index.html
     new HtmlWebpackPlugin({
       template: 'src/index.html'     // 模板
     }),
     // 每次打包前先自动删除output配置的目录
     new CleanWebpackPlugin()
  ]
}

上面代码的常用配置有详细的注释,就不过多说,更多配置可见 官方文档


3.4 source-map的配置

当js有异常抛出时,我们到浏览器console查看只能查看到在打包后bundle.js中对应的位置,但是在开发环境下,我们更希望能映射到我们src目录下的源代码对应的文件及行数,于是为了解决这个问题,sourceMap就出现了,它使得我们能够知道这种映射关系。

下图是关于webpack.config.js文件中配置devtool属性去配置source-map

如: devtool: "source-map"

每个选项各有利弊,下文开始分析


  • none
    打包速度最快,但是js抛出的异常所在的具体位置无法被映射出来
  • source-map
    打包速度最慢,但是js抛出的异常所在的具体位置会被映射出来(精确到某个文件的哪一行哪一列),dist目录下有bundle.js.map文件
  • inline-source-map
    打包速度最慢,效果与source-map一致,但是dist目录下没有bundle.js.map文件,而是以base64字符串的形式放在了bundle.js文件的底部,因此带有inline的选项就是source-map是以base64的形式打包到bundle.js
  • cheap-inline-source-map / cheap-source-map
    带有cheap的选项的效果是让source-map的映射关系忽略掉列映射,只要映射出某个文件的某一行即可,同时也只映射业务代码,不会映射第三方模块(node_modules)。这样会使打包速度有所
  • inline-cheap-module-source-map / cheap-moudle-source-map
    由于cheap会忽略掉第三方模块的映射关系,而moudle的作用是让cheap不要忽略第三方模块,因此cheap-module的作用就是使业务代码和第三方模块能够映射,但是只映射某个文件某行,忽略列的映射。打包速度会稍微慢些
  • eval
    带有eval的效果执行效率最快,打包速度最快,但是只能精确地映射到错误的文件,对于行数的映射可能牛出错

    综上所述,我们可以搭配每个字段的效果来使用,这里提供两种方案:
    开发环境development建议:cheap-module-eval-source-map
    线上环境production建议:cheap-module-source-map


3.5 使用 WebpackDevServer 来提升开发效率

我们每次修改完代码,都必须重新执行npm run build 来重新打包,效率低下。

我们希望每次修改完代码后,webpack能够自动为我们重新打包。要实现这一需求,

webpack为我们提供了三种做法:

  1.  webpack watch mode(webpack 观察模式)

    // package.json
    
    "scripts": {
        // --watch使得webpack会去监听我们src下的目录,一旦发生改变,会马上重新重新打包
        "build": "webpack --watch"
    },
    
    

      但缺点是我们需要手动去刷新浏览器,也没办法帮我们启动一个服务器,无法发送ajax请求。懒人拯救世界,于是webpack-dev-server应运而生


   2. webpack-dev-server

      webpack-dev-server 为你提供了一个简单的 web server,并且具有 live reloading(实时重新加载) 功能。设置如下:

       先下载 npm i webpack-dev-server -D 

// package.json

"scripts": {
    // --watch使得webpack会去监听我们src下的目录,一旦发生改变,会马上重新重新打包
    "build": "webpack --watch",
    // webpac-dev-server会监听src源代码的变化重新打包并自动刷新浏览器,打包完成会自动打开浏览器...
    "start": "webpack-dev-server"
},

// webpack.config.js
// 省略其他配置

module.exports = {
  devServer: {
    // 配置告知webpack-dev-server,将dist目录下的资源作为server可访问文件
    contentBase: './dist',
    // 打包完成后自动帮我们弹出浏览器
    open: true,
    // 服务器端口号,默认8080
    port: 8000
  }
}

现在,在命令行中运行 npm run start,我们会看到浏览器自动加载页面。如果你更改任何源文件并保存它们,web server 将在编译代码后自动重新加载。

值得一提的是,我们看到文件已经打包完成了,但是在dist目录里并没有看到文件,这是因为

webpack
是把编译好的文件放在缓存中,没有磁盘上的IO,但是我们是可以访问到的


3. webpack-dev-middleware

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。 webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。下面是一个 webpack-dev-middleware 配合 express server 的示例。


首先,安装 expresswebpack-dev-middleware

npm install --save-dev express webpack-dev-middleware

// package.json

"scripts": {
    // --watch使得webpack会去监听我们src下的目录,一旦发生改变,会马上重新重新打包
    "build": "webpack --watch",
    // webpac-dev-server会监听src源代码的变化重新打包并自动刷新浏览器,打包完成会自动打开浏览器...
    "start": "webpack-dev-server",
    // 自己构建的server
    "server": "node server.js"
},

webpack.config.js

// webpack.config.js
// 忽略其他配置

module.export = {
  output: {
    // 我们将会在 server 脚本使用 publicPath
    // 以确保文件资源能够正确地 serve 在 http://localhost:3000 下,稍后我们会指定 port number(端口号)。
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
}


在项目根目录下新建server.js

// server.js

const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
// 引入webpack配置文件
const config = require('./webpack.config.js')
// 在node中使用webpack
// webpack(config)会返回一个编译器complier,每执行一次complier,就会重新编译一次
const complier = webpack(config)

const app = express()

// webpackDevMiddleware的作用就是只要代码发生变化,就会重新运行comliper,
// 重新运行后生成的文件的输入内容的publicPath是config.output.publicPath的内容
app.use(webpackDevMiddleware(complier, {
  publicPath: config.output.publicPath
}))

// 指定端口号port:3000
app.listen(3000, () => {
  console.log('server is running')
})

现在,打开浏览器,访问 http://localhost:3000。应该看到webpack 应用程序已经运行!


3.6 Hot Module Replacement 热模块更新

hot module replacement简称HMR,它可以使我们在不刷新浏览器(dev-server也不帮我们刷新)的情况下可以更新模块,这带来的最大好处就是我们每次修改完代码不用通过刷新页面来重新加载模块,提升了我们的开发效率。

使用HMR我们必须借助插件HotModuleReplacementPlugin ,该插件是webpack 自带的插件,因此我们无需安装,只需要在配置文件引入

// webpack.config.js
// 省略配置

const webpack = require('webpack')
module.exports = {
    devServer: {
        contentBase: './dist',
        //开启热更新
        hot: true
    },
    plugins: [
        // HMR插件
        new webpack.HotModuleReplacementPlugin()
    ]
}

现在,修改 index.js 文件,以便在其他模块内部发生变更时,告诉 webpack 接受该更新对应的模块了。

// index.js

import a from './a.js'
import b from './b.js'

b()

// 判断是否开启了HMR
if (module.hot) {
    // 监听a.js,若该模块发生变化时调用回调函数
    module.hot.accept('./a.js', () => {
        // 更新模块(即重新执行)
        a()
    })
}

// a.js

export defalut function() {
    console.log('hello world')
}

// b.js

export defalut function() {
    const btn = document.createElement('button')
    btn.innerHTML = 1
    btn.setAttribute('class', 'btn')
    btn.onclick = function() {
        btn.innerHTML = parseInt(btn.innerHTML) + 1
    }
    document.body.appendChild(btn)
}

打开localhost:8080,点击btn改变数字大小,然后修改a.js代码,回到浏览器发现btn的数字不变,而console却打印出了新修改的内容。

这样子就实现了在不刷新浏览器的情况下,更新模块同时也不影响其他模块。

如果给btn添加样式:

// style.css

.btn {
    background: red;
}

运行页面后修改样式,神奇的事情发生了,我们明明在index.js没有配置监听style.css,为什么会也能做到不刷新浏览器的情况修改样式?

其实css-loader已经帮我们内部实现了类似上面监听a.js的代码了,所以我们无需自己配置。

同样, vue-loader等许多loader都有内部实现配置了HMR去监听模块。


3.7 使用 Babel 处理es6语法

  • 基础配置

由于es6语法并不是所有浏览器都能识别的,因此我们必须借此webpack来将我们的es6语法都转义成浏览器可识别的es5语法。

babel 是专门用于es6转义,打开babel官方文档 查看在webpack的使用:

下载babel-loader(webpack打包js文件的方式), @babel/core(babel的核心库,用于翻译es6)

npm install babel-loader @babel/core --save-dev

// webpack.config.js
// 忽略其他配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                // 不转义node_modules第三方库的js文件
                exclude: path.resolve(__dirname, 'node_modules'),
                loader: 'babel-loader'
            }
        ]
    }
}

// index.js

// ...随便写点es6语法
const arr = [
   new Promise(() => {}),
   new Promise(() => {})
]
arr.map(item => {
  console.log(item)
})

此时打包 npx webpack.config.js

打开dist/bundle.js,拉到最下面,发现webpack并没有把我们的es6转义成es5


事实上,babel-loader并不能转义es6,只能起到建立一个webpack到babel之间的桥梁。 要做到转义es6,话需要下载es6转移规则==>@babel/preset-env

npm install @babel/preset-env --save-dev

// webpack.config.js
// 忽略其他配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: path.resolve(__dirname, 'node_modules'),
                loader: 'babel-loader',
                options: {
                    presets: ["@babel/preset-env"]
                }
            }
        ]
    }
}

重新打包后打开dist/bundle.js查看es6代码发现被转义


但是像Promise等等es6的新特性,并不是没有浏览器都有的,所以我们还需要对这些新特性进行补充。

Babel包含一个polyfill,其中包含一个自定义的再生器运行时和core-js。
这将模拟完整的ES2015 +环境(不包含第4阶段的提议),并且打算在应用程序中使用,而不是在库/工具中使用。
(使用babel-node时会自动加载此polyfill)。
这意味着您可以使用新的内置函数(例如Promise或WeakMap),静态方法(例如Array.from或Object.assign),实例方法(例如Array.prototype.includes)和生成器函数(前提是您使用了regenerator插件)。
为了做到这一点,polyfill和诸如String之类的本地原型添加到了全局范围。


npm install @babel/polyfill --save

因为这是一个polyfill(它将在您的源代码之前运行),所以我们需要它是一个dependency,而不是devDependency,所以需要用--save安装而不是--save-dev


由于polyfill需要在源代码之前运行,因此我们需要在index.js最上方引入这个模块

// index.js

import "@babel/polyfill"

// ...随便写点es6语法
let a = [1,2,3,4]
let b = a.map(item => item + 1)

重新打包后发现打包后的文件大小相比没使用@babel/polyfill的时候大了不是一点点

问题来了,我们代码中只使用了map这个新特性,却导入了所有es6新特性,能否按需加载需要的新特性呢?

// webpack.config.js
// 忽略其他配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: path.resolve(__dirname, 'node_modules'),
                loader: 'babel-loader',
                options: {
                    presets: [
                        ['@babel/preset-env', {
                          // 按需加载es6新特性
                          useBuiltIns: 'usage'
                        }]
                    ]
                }
            }
        ]
    }
}

使用了useBuiltIns: 'usage'后,已经会按需帮我们载入需要的es6新特性了,此时可以删掉index.js中的import "@babel/polyfill"

重新打包后发现bundle.js文件大小缩小到32.8KiB啦


进一步优化:

// webpack.config.js
// 忽略其他配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: path.resolve(__dirname, 'node_modules'),
                loader: 'babel-loader',
                options: {
                    presets: [
                        ['@babel/preset-env', {
                          // 按需加载es6新特性
                          useBuiltIns: 'usage',
                          // 设定我们打包后要在什么浏览器的什么版本上运行
                          // 这样@babel/preset-env就会根据该浏览器版本需要补充新特性而加载
                           targets: {
                             chrome: '67'
                           }
                        }]
                    ]
                }
            }
        ]
    }
}

重新打包发现bundle.js文件大小回到跟没使用@babel/polyfill时一样

原因是在chrome67的浏览器本身已经支持es6语法和我们所需要加载的es6新特性,因此polyfill不会帮我们将es6语法翻译为es5也不会帮我们加载需要的特性



  • 进阶配置

上面的基础配置对于日常写业务的需求是够用的。

但是因为@babel/polyfill是通过全局变量的方式注入,会污染全局环境。所以如果你要开发一个组件库,类库,第三方库等时,就需要换一种方式配置(通过闭包入注而不是全局注入)去防止污染全局环境


Babel使用很小的帮助器来完成诸如的功能_extend。默认情况下,它将被添加到需要它的每个文件中。有时不需要重复,特别是当您的应用程序分布在多个文件中时。 这是@babel/plugin-transform-runtime插件的来源:所有帮助程序都将引用该模块,@babel/runtime以避免在编译后的输出中出现重复。运行时将被编译到您的构建中。 该转换器的另一个目的是为您的代码创建一个沙盒环境。如果你直接导入core-js or @babel/polyfill ,它提供了诸如内置插件Promise,Set和Map那些会污染全局范围。虽然这对于应用程序或命令行工具可能是可以的,但是如果您的代码是要发布供他人使用的库,或者您无法完全控制代码运行的环境,则将成为一个问题。 转换器会将这些内置别名作为别名,core-js因此您可以无缝使用它们,而无需使用polyfill。


安装

npm install @babel/plugin-transform-runtime --save-dev

npm install @babel/runtime --save

由于babel的配置篇幅过于长,就不单独写在webpack.config.js中,

在项目的根目录下新建文件.babelrc

// .babelrc

{
  "plugins": [['@babel/plugin-transform-runtime', {
    "corejs": 2,
    "helpers": true,
    "regenerator": true,
    "useESModules": false
  }]]
}

==> 等同于在webpack.config.js进行以下配置

// webpack.config.jsconst path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: path.resolve(__dirname, 'node_modules'),
        loader: 'babel-loader',
        // 可将options的内容放到.babelrc文件
        options: {
          "plugins": [['@babel/plugin-transform-runtime', {
             "corejs": 2,
             "helpers": true,
             "regenerator": true,
             "useESModules": false
         }]]
        }
      }
    ]
  }
}


根据官方文档,我们配置了corejs: 2, 因此必须安装依赖npm install --save @babel/runtime-corejs2

重新打包即可。



最后,如果对你有帮助,可以点个赞鼓励一下我继续写下去吗(●'◡'●)

后续会出webpack进阶版(填坑中)