webpack打包工具

217 阅读21分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

webpack打包工具

问题

  • ES Module存在环境兼容问题
  • 模块文件过多,网络请求过于频繁
  • 所有前端资源都需要模块化,不仅仅js

解决思路:

  • 把新标准js文件转位ES5代码
  • 把所有js文件打包到一个文件
  • 不仅仅打包js文件还要打包其他文件,css、html文件等

模块打包工具 - webpack为例

webpack核心特性满足了上面的需求

  • 模块打包工具:把零散的模块代码打包到一起
  • 模块加载器:模块代码转换
  • 代码拆分:按照代码需求打包
  • 支持资源模块:支持文件按照资源方式引入

打包工具解决前端整体的模块化,不单单指前面所说的js模块化

webpack上手

  1. 创建三个文件,使用ES Moudle模式,这里可以使用serve运行一下看效果

    <body>
      <!-- 引入js文件,设置为mudule模式 -->
      <script type="module" src="./src/index.js"></script>
    </body>
    
    // index.js
    
    // 引入模块
    import h1 from './code.js'
    // 使用模块
    const h = h1()
    document.body.append(h)
    
    // code.js
    
    // 导出模块
    export default () => {
      const h1 = document.createElement('h1')
      h1.innerText = '123'
      h1.onclick = function () {
        console.log('123')
      }
      return h1
    }
    
  2. 利用yarn创建新项目:yarn init --yes

  3. 利用yarn安装两个依赖:yarn webpack webpack-cli --dev

  4. 利用yarn查看一下安装的版本:yarn webpack --version

  5. 利用yarn执行webpack打包:yarn webpack

  6. 会默认打包到dist目录下,更改htmnl的drc属性即可,同时可以删掉type="module",因为webpack已经把文件转换为ES5

可以使用npm脚本设置自动化指令

webpack配置文件

webpack4以后,可以0配置,默认将src下面的index作为打包入口,打包到dist的main.js文件如果想自定义一些配置,需要在根目录下添加一个webpack.config.js文件,在文件内配置

// webpack.config.js


// 引入path模块,后面要使用其中的方法
const path = require('path')
// webpack配置文件需要导出
module.exports = {
  // 设置js入口文件,默认为src下面的index.js文件
  entry: './src/one.js',
  // 设置输出相关
  output: {
    // 输出文件的名称,默认main.js
    filename: 'output.js',
    // 设置输出路径,这里只能设置绝对路径,所以利用path的方法设置路径
    path: path.join(__dirname, 'output')
  }
}

webpack工作模式

可理解成针对不同环境的预设的配置,目前一共有三种模式:

  • production:线上模式,默认打包模式,压缩代码体积最小,但是不方便阅读
  • development:开发模式,速度快,会添加一些有用的注释信息
  • none:原始模式,只是打包,不会做其他处理

书写方式

  • yarn webpack --mode 模式名中断运行

  • 书写在配置文件,这是就不需要在命令中设置模式了

    module.exports = {
      mode: 'development'
    };
    

webpack打包原理

webpack将我们需要引入的所有模块全部打包在一个文件中,然后使用一些webpack内部的代码实现保持这些模块的依赖效果

资源加载模块

webpack只会默认打包js文件,如果没有特殊操作无法打包其他文件

y1Vn8s.png

如果想要处理其他文件需要使用加载器(loader)进行处理,就需要安装依赖

这里解析css文件要安装的两个依赖分别是:style-loader 和 css-loader两个插件

npm install --save-dev style-loader css-loader
yarn add --dev css-loader style-loader(如果你安装了yarn)
// webpack.config.js

// 配置文件
module.exports = {
  module: {  
    //配置其他加载器
    rules: [{ // 配置css打包
      // test:是一个正则表达式,这里表示要匹配的文件要以css结尾
      test: /\.css$/i,
      // use: 要使用的加载器,这里使用css-loader打包css,使用style-loader把打包的css利用style标签的形式插入页面
      use: ['style-loader', 'css-loader'],
    }, ],
  }
};


// 注意:加载器是从后往前加载的!!!!!!!!!!
// 这里的use: ['style-loader', 'css-loader']表示先使用css-loader再使用style-loader

loader是整个webpack的核心特性,借助不同的loader实现加载不同类型的资源

导入资源模块

通常情况下,我们还是以js文件为打包入口,我们在入口文件中引入我们需要的css文件,从而再使用打包

// 入口文件


// 引入模块
import h1 from './code.js'
// 在这里引入css模块,因为是直接运行,就不需要命名
import './main.css'
// 使用模块
const h = h1()
document.body.append(h)

// 然后把webpack的配置文件入口文件设置好即可

webpack眼中js驱动更个前端应用

  • 逻辑合理,js需要资源文件的配合
  • 保证上线时确保资源文件都是必要的,不会缺失

文件资源加载器

加载文件需要使用file-loader加载器:

yarn add --dev file-loader

// 入口文件


// 引入文件,这里引入一张图片
import img from './1.png'
// 使用图片插入页面
const m = new Image()
m.src = img
document.body.append(m)
// 配置文件
output: {
    // 输出文件的名称,默认main.js
    filename: 'output.js',
    // 设置输出路径,这里只能设置绝对路径,所以利用path的方法设置路径
    path: path.join(__dirname, 'output'),
    // 在这里设置一下加载路径,否则可能出错,把加载路径设置为输出路径就可以了!!!!!!!
    publicPath: 'output/'
  },
module: {
    rules: [{ // 使用file-loader
      // 设置文件匹配
      test: /\.png$/,
      // 使用加载器
      use: 'file-loader'
    }]
  }

一定要设置一下加载路径,否则很有可能出错,注意路径后面加/,例如:publicPath: 'output/',/会和后面进行拼接形成路径

y1MI6H.png

URL加载器

通过data urls的方式表示文件,理论上可以表示任何类型的文件

data urls方式表示文件:协议:媒体类型编码,文件内容

例如:data:[<mediatype>][;base64],<data>

y1l8Zn.png

这种表示文件的方式已经包含了文件内容,使用的时候就不需要申请http请求,如果碰到无法使用文字表示的文件,比如图片、字体等文件,可以使用base64表示内容

webpack支持这种方式,需要使用url-loader加载器实现

module: {
    rules: [{// 使用url-loader
      // 和file类似的方法
      test: /\.png$/,
      
      // 如果只是想把文件转换为data-urls的方式,直接使用use: 'url-loader'即可,这样会无条件的吧文件转换为data-urls
      // use: 'url-loader'
      
      // 但是实际上,我们可能只需要把比较小的或者某些文件转换,因为base64编码转换大文件比较慢,所以更适合小文件
      // 配置转换条件
      use: {
        // 加载器还是使用url-loader
        loader: 'url-loader',
        // 配置
        options: {
          // 转换小于等于10k的文件
          limit: 10 * 1024  // 10kb,这里是按照字节计算的
        }
      }
    }]
  }

注意:使用url-loader同时要安装file-loader(除非全部转换url),因为不满足转换条件的会默认调用file-loader

工作建议

  • 小文件使用data-urls,减少请求
  • 大文件单独存放

常用加载器类型

  • 编译转换类,把加载到的资源转换为js代码,例如:css-loader
  • 文件操作类:把资源复制拷贝到输出目录,把文件的访问路径导出,例如file-loader
  • 代码检查类:对代码进行校验,统一代码风格,提高质量

webpack和ES2015

webpack因为打包的需要,会处理export和import模块,但是并不会处理其他的ES6的代码

想要处理ES6+新特性需要安装babel-loader:babel要依赖多个插件

nary add --dev babel-loader @babel/core @babel/preset-env

// 配置文件

module: {
  rules: [{ // 使用babel-loader
    // 编译js文件
    test: /\.js$/,
    // babel需要配置
    use: {
      // 使用loader
      loader: 'babel-loader',
      options: {
        // 编译库,这里包含所有需要的功能
        presets: ['@babel/preset-env']
      }
    }
  }
}

webpack只是打包工具,加载器才可以进行编译和转换

加载资源的方式

  • 遵循ES Modules标准的import声明
  • 遵循CommonJS标准的require函数
  • 遵循AMD标准的define函数和require函数

除非必要不要混合使用

loader加载非js代码也会触发资源加载 ,例如css/html-loader遇到src、url和@import(css引入css)指令也会触发资源加载,当遇到资源加载的时候也会使用相应的加载器

  • 样式代码中的@import指令和url函数
  • HTML代码中图片标签的src属性等

总结:在webpack中凡是代码中需要引用的资源及其可能性都会被找出来,根据配置交给不同的loader处理,输出到打包目录

核心工作原理

loader是webpack的核心

工作原理

  1. 根据配置找到入口文件
  2. 根据入口文件顺藤摸瓜找到所有需要加载的资源
  3. 将这些资源根据配置文件交给不同的loader(加载器)处理
  4. 最终把所有处理完的数据导出到输出目录,并把所有能够合并到一起的写入到一个js文件中
  5. js文件中继承了这些各种依赖关系,确保加载过程中不会发生错误

y1duKP.png

尝试开发一个loader

loader负责资源文件从输入到输出的转换

对于同一资源可以一次使用多个loader,可以讲此次loader的结果交给下一个loader处理

webpack要求一个loader工作之后的返回结果必须是一段js代码或者使用其他加载器返回的结果

// 写的一个loader加载器


// 安装markdown解析插件 - marked,在这里引入
const marked = require('marked')

// loader解析器是一个向外输出的结果,向外输出内容,这里接受一个参数source为我们要解析器输入的内容
module.exports = source => {
  console.log(source) // 我们可以打印出来看一下

  // return返回值要求必须输出一段js代码
  // return 'console.log(hello)'  

  // 我们可以使用marked解析一下输入的内容,会转换为一段html代码
  const html = marked(source)

  // 我们可以在这里输出,这里使用moudel.export和export default都可以输出,webpack都支持
  // 但是注意这里的html是一段html代码,直接输出可能会导致错误,我们先把他转换为json
  // return `moudel.export = ${JSON.stringify(html)}`
  // return `export default ${JSON.stringify(html)}`


  // 也可以直接输出html交给下一个loader处理
  return html
}
// 配置文件

// 引入path 后面要用
const path = require('path')
module.exports = {
  // 打包模式 
  mode: 'none',
  // 入口文件文件
  entry: './src/index.js',
  // 输出配置
  output: {
    //名字
    filename: 'dist.js',
    // 路径
    path: path.join(__dirname, 'dist'),
    // 资源路径
    publicPath: 'dist/'
  },
  // loaders
  module: {
    rules: [{
      // 解析md文件
      test: /\.md$/,
      // 如果我们写的loader直接输出可用,可以直接写use: './md-loader'
      // use: './md-loader'
      // 如果我们写的loader还需要另一个loader处理名、那么写多个loader即可,注意前后顺序,执行顺序从后往前
      use: ['html-loader', './md-loader']
    }]
  }
}

总结

  • loader就是一个从输入到输出数据的转换工具
  • loader可以看做是一种管道,我们可以使用一个loader转换文件,也可以把多个loader组合起来转换,例如css-loader到style-loader

插件机制 - Plugin

增强webpack自动化能力

Plugin解决除了资源加载之外的其他自动化工作

例如每次打包之前删除文件、压缩代码等等

常用插件

自动清除输出目录插件

插件地址:www.npmjs.com/package/cle…

使用插件:clean-webpack-plugin => yarn add --dev clean-webpack-plugin

// webpack配置文件

// 引入path 后面要用
const path = require('path')

// 引入clean-webpack-plugin插件,解构出来一个构造函数!!!!!!!
const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin');


module.exports = {
  // 打包模式 
  mode: 'none',
  // 入口文件文件
  entry: './src/index.js',
  // 输出配置
  output: {
    //名字
    filename: 'dist.js',
    // 路径
    path: path.join(__dirname, 'dist'),
    // 资源路径
    publicPath: 'dist/'
  },
  // loaders
  module: {
    rules: [{
      // 解析md文件
      test: /\.md$/,
      // 如果我们写的loader直接输出可用,可以直接写use: './md-loader'
      // use: './md-loader'
      // 如果我们写的loader还需要另一个loader处理名、那么写多个loader即可,注意前后顺序,执行顺序从后往前
      use: ['html-loader', './md-loader']
    }]
  },
  
  
  // 所有插件都要放在这里,是一个数组!!!!!
  plugins: [
    // 添加插件!!!!!
    new CleanWebpackPlugin()
  ]
}

自动生成html插件

让html文件也参与webpack的构建过程

文档地址:webpack.docschina.org/plugins/htm…

使用插件:html-webpack-plugin => yarn add --dev html-webpack-plugin

// 引入path 后面要用
const path = require('path')
// 引入clean-webpack-plugin插件,解构出来一个构造函数
const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin');

// 安装html生成模板,这个不需要解构!!!!!!!!!!!!!!!!!!!!!!!
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  // 打包模式 
  mode: 'none',
  // 入口文件文件
  entry: './src/index.js',
  // 输出配置
  output: {
    //名字
    filename: 'dist.js',
    // 路径
    path: path.join(__dirname, 'dist'),
    // 资源路径
    publicPath: 'dist/'
  },
  // loaders
  module: {
    rules: [{
      // 解析md文件
      test: /\.md$/,
      // 如果我们写的loader直接输出可用,可以直接写use: './md-loader'
      // use: './md-loader'
      // 如果我们写的loader还需要另一个loader处理名、那么写多个loader即可,注意前后顺序,执行顺序从后往前
      use: ['html-loader', './md-loader']
    }]
  },
  // 所有插件都要放在这里,是一个数组
  plugins: [
    // 添加插件
    new CleanWebpackPlugin(),
    
    // 创建新实例。里面的参数可配置!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    new HtmlWebpackPlugin({
      // 设置标题
      title: '我是标题',
      // 设置meta属性
      meta: {
        viewport: 'width=device-width'
      },
      // 如果想要代入某些结构可以使用模版,因为默认生成的是一个空的html
      template: './src/index.html'
    }),
    // 如果想生成多个html文件,需要创建多个实例
    new HtmlWebpackPlugin({
      // 设置名字,因为默认的名字为index.html,如果不重新命名会和上一个冲突
      filename: 'tow.html'
    })

  ]
}
<!-- 这是一个模版 -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div class="box">
    <!-- 模版内部可以使用模版字符串 -->
    <h1><%= htmlWebpackPlugin.options.title %></h1>
  </div>
</body>

</html>

复制文件插件

一些不参与构建的静态文件,直接复制到输出目录极即可

文档地址:webpack.docschina.org/plugins/cop…

使用插件:copy-webpack-plugin => arn add --dev copy-webpack-plugin

// 引入path 后面要用
const path = require('path')
// 引入clean-webpack-plugin插件,解构出来一个构造函数
const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin');
// 安装html生成模板,这个不需要解构
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 引入拷贝插件!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
  // 打包模式 
  mode: 'none',
  // 入口文件文件
  entry: './src/index.js',
  // 输出配置
  output: {
    //名字
    filename: 'dist.js',
    // 路径
    path: path.join(__dirname, 'dist'),
    // 资源路径
    publicPath: 'dist/'
  },
  // loaders
  module: {
    rules: [{
      // 解析md文件
      test: /\.md$/,
      // 如果我们写的loader直接输出可用,可以直接写use: './md-loader'
      // use: './md-loader'
      // 如果我们写的loader还需要另一个loader处理名、那么写多个loader即可,注意前后顺序,执行顺序从后往前
      use: ['html-loader', './md-loader']
    }]
  },
  // 所有插件都要放在这里,是一个数组
  plugins: [
    // 添加插件
    new CleanWebpackPlugin(),
    // 创建新实例。里面的参数可配置
    new HtmlWebpackPlugin({
      // 设置标题
      title: '我是标题',
      // 设置meta属性
      meta: {
        viewport: 'width=device-width'
      },
      // 如果想要代入某些结构可以使用模版,因为默认生成的是一个空的html
      template: './src/index.html'
    }),
    // 如果想生成多个html文件,需要创建多个实例


    // 使用拷贝插件!!!!!!!!!!!!!!!!!!!!!!!!!
    new CopyPlugin({
      // 配置
      patterns: [{
        // 从根目录下的src
        from: "src",
        // 拷贝到输出路径下的src
        to: "src"
      }, ],
    }),
  ]
}

尝试开发一个插件

webpack.docschina.org/api/compile…

pulgin利用的是钩子机制实现的

插件是通过在生命周期中的钩子上挂载函数的方式实现扩展的

我明白这个原理吧,老师讲的不是很细,没太看懂

webpack开发体验的问题

理想的开发环境:

  • 以HTTP server的方式运行
  • 自动编译,自动刷新
  • 提供source map支持,快速定位问题

增强开发体验

自动编译

利用webpack的watch工作模式,监听文件变化,自动重新打包

方法实现:yarn webpack --watch

运行webpack命令的时候添加--watch,这样webpack打包后不会立即退出,会进入监视模式,当文件发生变化,就会立即重新打包

自动刷新浏览器

利用browserSync工具

  • 操作上比较麻烦,要同时使用两个工具
  • 效率降低了,要不断写入读取

webpack dev server

webpack官方提供的开发工具,提供一个开发服务器,集成自动编译和自动刷新浏览器等功能

安装:yarn add --dev webpack-dev-server

执行:yarn webpack-dev-server --open,加open可以自动打开浏览器

注意

  • 注意webpack-cli版本,版本太高可能会报错,练习的时候我的版本为4X就报错了,实验可行的版本为"webpack-cli": "^3.3.12",如果你的版本报错,可以尝试刚改为这个版本
  • 此方法不会在本地进行打包,而是将打包的结果存在内存里面,从内存里面读取,所以减少了本地的读写操作,更高效
访问静态资源文件

在实际开发中,我们可能并不会一直使用copy-webpack-plugin,因为如果需要拷贝的文件过多,会导致我们每次都要拷贝,所以一般情况下,我们只有在上线之前才会使用copy

所以在webpack dev server中,我们要添加设置参数,把这些文件也要打包进去

方法,在webpack配置文件中添加devServer配置

devServer: {
  // 要拷贝的静态资源
    contentBase: '路径'
  },

注意:使用的时候要注意html文件的位置,最好放在src目录下,这样才能正确打开,我联系的时候放在了根目录,open以后只能看到静态资源,看不到页面

代理API

问题根源:开发阶段接口跨域问题

解决方案:webpack-dev-server支持配置代理服务

名词:

endpoint - 接入端点 / 入口

devServer: {
    contentBase: './123',
      
      
    // 添加代理服务
    proxy: {
      //代理地址相关设置
      '/api': {
        // 要代理的网址,例如http://localhost:8080/api/users => https://api.github.com/api/users
        target: 'https://api.github.com',
        // 重写路径,如果不想api传递过去,可以替换为空,这里面的参数会通过正则匹配
        pathRewrite: {
          '^api': ''
        }
      },
      // 设置请求头中的请求地址,替换为target地址,因为服务器种可能存在多个网站信息,我们调试使用的请求头服务器可能不认识,不使用localhost:8080
      changeOrigin: true
    }
  },

source map - 源代码地图

webpack.docschina.org/configurati…

  • 目前的调试和报错都是基于转换过后的运行代码
  • source map文件映射转换后代码和源代码之间的关系
  • 可以利用映射关系,精确定位到错误的在源代码中的错误信息

Source Map解决了源代码与运行代码不一致所产生的问题

webpack配置source map

打包结果生成source map文件

// webpack配置文件

//打包的时候创建source-map文件
devtool: 'source-map'
// 这是最简单的生成map文件的方式,但是目前一共有12种不同方式,美中方式生成的效果和速度都是不一样的

eval模式source map

eval是js中的一个函数,可以运行函数内部的代码

eval(要运行的代码 可选//sourceURL=路径)

可以在参数内部添加注释(//sourceURL)指向这段代码运行的文件

eval模式不会产生map文件,会将所有打包的代码都放到eval函数中执行,但这种方法只能定位文件,无法定位到行和列,所以构建速度最快

不同模式之间的差异

关键词

  • eval - 使用eval执行模块代码
  • cheap - 是否包含行信息,这种方式不会包含列
  • module - 得到原始未经转换的源代码

例如

**cheap-module-sval-source map **=> 未经转换的,包含行信息的使用eval执行

选择合适的模式

一般应用开发时候不会用多种模式

老师的个人选择:

  • 开发环境下:cheap-module-sval-source map
    • 一般每行不会写过多代码,不需要定位列
    • 转换后代码可能和转换之前代码差别较大,需要知道我转换之前哪里出了错误
    • 首次打包比较慢,但是重新打包相对较快
  • 生产环境(最后一次打包):none - 不生成
    • map会暴露源代码到生产环境,可能被复原代码
    • 调试应该是开发阶段的事情,生产阶段应该不需要调试
  • 如果想使用map:nosource-source map
    • 会提示位置但不会暴露源代码

webpack自动刷新问题

问题:webpack-dev-server每次都会完全刷新整个网页,一旦页面整体刷新,页面中的操作状态都会丢失

需求:在页面不刷新的前提下,代码也可以及时更新进去

HMR-模块热更新/替换

在应用程序过程中实时替换模块内容,且不影响应用运行状态,只会将修改的模块替换到应用中

开启HMR

集成在了webpack-dev-server中

开启方法

  • 方法1:运行代码时直接加--hot
    • 直接在终端运行yarn webpack-dev-server --hot
  • 方法2:修改配置文件
    • 配置文件devserver中添加hot:true
    • 配置文件中引入webpack,再创建**webpack.HotModuleReplacementPlugin()**实例
// 引入webpack
const webpack = require('webpack')

module.exports = {
  ......
  // 设置webpack-dev-server
  devServer:{
    // 开启热更新
    hot:true
  }
  // 创建实例!!!注意,这个要写在插件中(plugins)
  new webpack.HotModuleReplacementPlugin()
}

但是我们运行起来发现,css文件修改后确实可以开启热更新,但是当我们秀js文件后发现页面还是刷新了

HMR疑问

webpack中的MHR并不和其他插件一样“开箱即用”,需要手动处理模块热更新替换逻辑

css可以实现热更新是因为在style-loader里面已经进热更新进行了处理,就不需要手动配置了

css代码逻辑比较简单,有规律,可以更好的进行替换,所以可以进行热更新

而js代码没有什么特别的规律可言,可能有很多种变化,所以我们无法找到代码运行规律从而实现逻辑替换

使用框架开发的时候,每个文件都是有规律的,所以js可以进行热更新,通过脚手架创建的项目内部已经继承了HMR方案

总结:我们需要手动处理js模块更新后的热替换

HMR Apis

手动处理js热更新

  • **module.hot.accept('模块路径',回调函数)**

webpack处理js模块热替换

module.hot.accept('模块路径',回调函数)

// 引入模块
import html from './html.js'
// 引入css文件
import '../css/index.css'
// 引入图片
import src from '../img/截屏2021-02-07 11.21.07.png'

//使用html模块。生成结构添加进页面
const input = html()
document.body.append(input)
const img = new Image()

// 创建一个图片,插入页面
img.src = src
document.body.append(img)


console.log('111')


// 创建一个变量接收当前的input标签,存起来
let lastInput = input
// 配置热更新,第一个参数是模块的文件的地址,第二个参数是回调函数
module.hot.accept('./html.js', () => {
  // 回调函数内部是当模块发生更新后怎么做的代码,也就是说,如果这里为空,模块更新后就不会生效
  const value = lastInput.value
  const newInput = document.createElement('input')
  newInput.value = value
  newInput.className = 'input'
  newInput.onclick = () => {
    console.log('我是input2')
  }
})

注意事项

  • 问题:如果HMR代码错误会报错,但此时会导致自动刷新,会丢失错误信息,不容易发现

    • 解决方案:不使用hot而使用hotOnly:true,一旦系统检测到错误,就不会刷新页面,从而使报错信息得以保留

    • // webpack配置文件
      devServer: {
          hotOnly: true
      },
      
  • 问题:plugins没有启动HMR插件的情况下,HMR插件会报错

    • 解决方案:在使用MHRapi之前先判断有没有module.hot这个对象,如果有再使用,否则不使用

    • // 先判断一下,如果module.hot存在,说明启用的HMR插件
      if (module.hot) {
        .........
      }
      
  • 代码中有很多与业务功能无关的代码会不会影响生产环境

    • 解答:打包之后不会打包if语句中间的代码(在我们关闭mhr的情况下),打包也不会打包这些代码。不会影响生产环境

生产环境优化

当代码处于生产环境中的时候,我们使用webpack打包的代码可能会出现一些无用代码冗余

webpack中提供了很多模式,我们可以为不同的工作环境创建不同的配置

不同环境下的配置

  • 配置文件根据环境不同导出不同的配置(只适用于中小型项目)
    • 使用的时候执行yarn webpack --env 环境名(和配置文件对应)
  • 一个环境对应一个配置文件(大项目建议使用)
    • 可以安装webpack-merge模块用作配置对象合并yarn add webpack-merge --dev
    • 因为没有了默认配置文件,执行打包时应该输入yarn webpack --config webpack.....js,其中webpack.....js为要使用的配置文件的名字
// 方案1:根据传入的文件不同而输出不同的配置
// webpack.config.js配置文件

// 引入模块
const path = require('path') // path
const webpack = require('webpack') // webpack
const { // 清除输出目录
  CleanWebpackPlugin
} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin') // html模块
const CopyPlugin = require("copy-webpack-plugin") // 拷贝文件

// 根据传入参数不同判断环境从而输出不同配置
// module.exports支持添加一个函数,参数1表示运行环境(在命令行传入),参数2表示传入参数(一般不需要)
module.exports = (env, argv) => {
  // 创建一个开发环境下的模版
  const config = {
    // 打包模式=none
    mode: 'none',
    // 入口文件
    entry: './src/js/index.js',
    // 输出配置
    output: {
      // 输出文件名
      filename: 'index.js',
      // 输出目录
      path: path.join(__dirname, 'dist')
    },
    // 使用的loader
    module: {
      rules: [{ // 转css
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        },
        { //转图片
          test: /\.png$/,
          use: {
            loader: 'url-loader',
            options: {
              milit: 200 * 1024
            }
          }
        },
        { // 转js到ES5
          test: /\.js$/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          }
        }
      ]
    },
    // 插件配置
    plugins: [
      // html插件
      new HtmlWebpackPlugin({
        title: '练习',
      }),
      // 热更新插件
      new webpack.HotModuleReplacementPlugin()
    ],
    // webpack-dev-server
    devServer: {
      // 根目录
      contentBase: './dist',
      // 开启热更新
      hot: true
    },
    // 开启source-map
    devtool: 'eval-cheap-module-source-map'
  }
  
  // 判断传入的参数是否为production,如果传入production,修改模版
  if (env === 'production') {
    // 设置打包模式为生产模式
    config.mode = 'production'
    // 禁用source-map
    config.devtool = false
    // 设置插件,添加两个插件
    config.plugins = [
      // 这里使用ES6语法解构原有的config.plugins
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyPlugin({
        patterns: [{
          from: "./src/other",
          to: "other"
        }],
      })
    ]
  }
  
  // 返回配置,如果是开发环境直接输出原始模版,如果是生产环境,输出处理过后的模版
  return config
}

// 使用的时候执行 yarn webpack 会按照默认配置文件执行打包(因为没有传入参数,所以不会进入if语句)
// 使用的时候执行yarn webpack --env production 即可以生产环境的方式打包文件
// 方案2:给每个环境配置不同的配置文件

// 引入模版配置模块,这里直接引入模版js文件即可
const common = require('./webpack.common.js')
// 引入webpack-merge插件
const merge = require('webpack-merge')
const { // 清除输出目录插件
  CleanWebpackPlugin
} = require('clean-webpack-plugin')
const CopyPlugin = require("copy-webpack-plugin") // 拷贝文件插件

// 利用webpack-merge插件合并配置参数,这个函数会直接将两个对象合并
module.exports = merge(common, {
  // 打包模式
  mode = 'production',
  // 模块使用
  plugins: [
    new CleanWebpackPlugin(),
    new CopyPlugin({
      patterns: [{
        from: "./src/other",
        to: "other"
      }],
    })
  ]
})

DefinePlugin - 注入全局成员

  • DefinePlugin是webpack内置的一个类,可以在全局注入一个成员
  • 会将注入的成员的值直接替换到打包好代码当中
// 必须先引入webpack模块
const webpack = require('webpack')



module.exports = merge(common, {
.........
  plugins: [
    // 在模块中使用
    new webpack.DefinePlugin({
      // 这里要加上js代码片段 ,所以解析后会去掉一层引号
      API_BASE_URL: '"https://api.baidu.com"'
    })
  ]
})

Tree-shaking - 去除未引用代码

去掉代码中没有使用的代码,这样可以减少体积优化代码

一组功能搭配后的优化结果,不是某一个插件或者方法,生产模式下会自动启用

// webpack配置文件
module.exports = {
  .........
  // 配置Tree-shaking功能
  optimization: {
    // 不输出未引用代码,开启后会发现打包后的结果这些未引用的代码颜色变淡了
    usedExporta: true,
    // 开启压缩代码,会压缩打包的代码,并且删除未应用的代码
    minimize: true
  }
}

如果把代码看做一棵大树

  • usedExporta负责标记枯树叶(未应用代码)
  • minimize负责把枯树叶摇下来(删除)

concatenateModules - 合并模块函数

默认打包是把每个模块单独放在一个函数当中,如果我们模块过多就导致会有很多函数

concatenateModules可以把所有模块合并到一起,输出到一个函数中,缩小代码体积提高运行效率,也叫scope Hoisting(作用于提升)

使用方法:直接在上面的optimization中添加concatenateModules:true

Tree-shaking和babel

很多资料表示使用babel-loader后会导致Tree-shaking失效

Tree-shaking必须使用ES Module实现

新版的babel-loader已经支持ES Moudel特性,不会导致Tree-shaking失效了

如果你不能确定是否是新版本,可以强制设置babel,强制开启

module: {
  rules: [{
    // 在babel-loader中设置
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets:[
            // 强制不转换modules
            ['@babel/preset-env', {modules: flase}]
          ] 
        }
      }
    }
  ]
},

sideEffects - 副作用

一般只有在开发npm模块的时候才会用到

副作用:模块执行时除了导出成员之外所做的事情,功能开启后就不会打包没有用到的模块

使用方法

  1. 在配置文件中的optimization开启功能:sideEffects:true
  2. 在package.json中添加sideEffects字段:“sideEffects”: "true",告知系统,我们所有代码都没有副作用

注意事项

  • 问题:要确定你的代码没有副作用,否则系统会认为所有代码都没有副作用,例如一些css模块,例如给内置对象添加方法这一类操作都会被忽略掉
  • 解决方法
    • 关掉package.json中的副作用声明
    • 告诉系统哪些文件有副作用,这样系统会打包有副作用的代码“sideEffects”: [文件路径,文件路径......]

webpack 代码分割 - code splitting

问题

  • 所有代码之后都被打包到了一起,可能导致体积过大
  • 并不是每个模块都是启动后必须的,有一些代码只会在特定操作下才会起效

解决方法:分包,按需加载

  • 多入口打包
  • 动态导入

多入口打包

一般适用于传统的多页应用程序,最常见一个页面一个打包入口,公共部分单独提取

module.exports = {
  entry: {
    // 配置两个入口文件,就可以多入口打包
    index: './.../.../.js',
    index2: './.../.../.js',
  },
  output: {
    // 输出文件名适应name动态添加,name会接受两个入口的名字
    filename: '[name].bindle.js',
    path: path.join(__dirname, 'dist')
  },
  plugins: [
    // 当我们打包之后会发现html内部引用了所有打包的js代码,我们手动修改chunks让文件只引用需要的js文件
    new HtmlWebpackPlugin({
      title: '练习',
      template: './code3/src/index.js',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: '练习2',
      template: './code3/src/index2.js',
      filename: 'index2.html',
      chunks: ['index2']
    })
  ]
}

提取公共模块

多入口打包无法提取公共部分,会有很多相同的模块代码

解决方法就是把公共的模块单独提取出来,单独存放,就不需要在每个打包的文件中都存一份

// webpack配置文件

module.export = {
  .......
  optimization: {
    solitChinks: {
  		// 单独打包所有公共模块
      chunks:'all'
    }
  }
}

动态导入

按需加载:需要某个模块时候,再去加载这个模块

动态导入模块会将所有动态导入的模块都会自动打包、分包

动态导入使用的是ES Moudel的方法

// 我们可以在判断条件或者for循环中使用动态加载模块,这样代码只会在使用的时候加载,不会在启动后直接加载
if (x === 1) {
  // 导入模块,并使用then方法使用导入的模块
  import('./src/tow.js').then(({
    // 重命名导入的模块为one
    default: one
  }) => {
    // 使用one
    console.log(one)
  })
} else {
  import('./src/tow.js').then(({
    default: tow
  }) => {
    console.log(tow)
  })
}

魔法注释

给打包的文件添命名

使用方法:直接在动态导入的参数里面添加行内注释,例如:

// webpackChunkName可以给打包的文件添加前缀名字
// 给这个文件添加one前缀
import(/*webpackChunkName:'one'*/'./src/tow.js').then(({
    // 重命名导入的模块为one
    default: one
  }) => {
    // 使用one
    console.log(one)
  })


//打包结束后名字为 one.配置文件设置名.js

注意:如果两个动态导入的行内注释是相同的,那么这两个人间就会被打包到一个文件中

MiniCssExtractPlugin - css提取到单个文件

中文文档:webpack.docschina.org/plugins/min…

需要插件:yarn add mini-css-extract-plugin --dev

使用此插件就不需要使用style-loader了,因为这个插件会把css文件提取到单独文件,需要使用link引入

将style-loader替换成MiniCssExtractPlugin.loader

//上面需要引入插件,可以查看文档
{
  test: /\.css$/,
    // 这里使用MiniCssExtractPlugin.loader替换掉style-loader
  use: [MiniCssExtractPlugin.loader , 'css-loader']
},

建议:css文件超过150kb再使用MiniCssExtractPlugin.loader,否则可能会适得其反

OptimizeCssAssetsWebpackPlugin - 压缩css文件

安装插件

  • 压缩css插件:yarn add optimize-css-assets-webpack-plugin --dev
  • 压缩js插件:yarn add --dev terser-webpack-plugin
    • 因为如果把上一个插件添加到minimizer中之后系统会认为你想手动配置插件,会导致webpack默认压缩js代码失效,所以我们要手动加回来

文档

webpack没有找到yarn add optimize-css-assets-webpack-plugin,但是提供了这个插件

webpack.docschina.org/plugins/css…

使用方法

  • 直接在plugins中创建实例对象
  • 写在minimizer中
// 上面要引入插件再使用

// webpack配置文件
module.export = {
  
  // 方法1:写在minimizer中
  optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin(),
      // 如果以生产模式打包会导致会导致js无法正常压缩,因为一旦我们设置了minimizer,系统就会认为我们使用了自定义,就会不使用默认的js压缩,我们要手动引入另一款插件解决
      new TerserWebpackPlugin()
    ]
  },
  
  // 方法2:写在plugins中,也不需要设计其他插件,但官方好像不是很建议这样做,所以还是按照方法1做吧
  plugins: [
    new OptimizeCssAssetsWebpackPlugin(),
  ]
}

输出文件名Hash

建议在生产模式下,文件名使用hash

使用方法:在webpack配置文件中,需要修改名字的地方都可以使用hash,例如:outpot中的filename、html插件中的filename

// 只要是配置文件中涉及到命名的地方都可以使用
output: {
  filename: '[name]-[hash]-bundle.js'
}

new HtmlWebpackPlugin({
      filename: 'index-[[hash:8]].html',
})

// [...hash:x],x可以指定生成多少位的hash名字,例如[contenthash:8]就生成一个8位的hash

hash三种值:

  • hash:整个目录级别的,每次更改文件都会更改全部文件名
  • chunkhash:路级别的(依赖),只会更改发生更改的那一路文件的名字
  • 建议使用)contenthash:文件级别的,只会更改修改的文件的名字

建议使用8位hash就可以了