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

前言

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

一. webpack主要是作什么的?

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

  • 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有为咱们提供默认配置,此时只须要执行如下命令行:html

    // 必须告诉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 是 什么意思?vue

    譬如,当咱们全局安装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命令打包入口文件。


    java

  2. 本身配置
    node

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

// 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')
  }
}复制代码

 此时再输入命令行css3

npx webpack复制代码

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

此时在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'
                        }]
                    ]
                }
            }
        ]
    }
}复制代码

从新打包后发现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新特性,所以polyfill不会帮咱们加载



  • 进阶配置

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

可是由于@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进阶版(填坑中)

相关文章
相关标签/搜索