记录 Webpack 学习

85 阅读12分钟

webpack

1.Commonjs 与 ESModule

1.1 ESModule特性

1.模块化规范

1.自动采用严格模式,忽略”use strict“

2.每个 ESM 模块都是单独的私有作用域

3.ESM 是通过 CORS 去请求外部 JS 模块的

<script src="http://114.55.108.177:8080/" type="module">// 存在跨域 报错
</script> 

4.ESM 的 script 标签会延迟执行脚本(异步)不会阻塞 script 标签下面的语句

5.commonjs 规范是 node 中提出的规范,commonjs 约定的是一套同步加载的规范。node在启动时加载模块,在运行时使用模块。但是在浏览器中使用同步会出现 性能 问题

  <body>
    <!-- <script src="http://114.55.108.177:8080/" type="module"></script> -->
    <script type="module">
      alert('我被阻塞了')
    </script>
      
    <!-- 页面会显示 aaaa 后在执行上面 module 语句 -->
    <div>aaaa</div>
  </body>

2.ESM 导入导出

ESM 导出 export {a,b} 这里导出的是对应的地址,而不是ES6对象简写形式去导出对象、函数或是变量。

ESM 导入 import {a,b} from 'xxx地址' 这里引入的是对应关系,而不是ES6解构赋值(该对象、函数或是变量。详情可见:1.2 Commonjs 特性),所以引入的值是不能修改的,比如 :a = 10 ,这里会报错。这样的引入完成了模块化功能

在ESM导出的空间里改变 a,b的值,导入的值也会随之修改。说明了:导入的值是一段引用关系

导出变量的方式,使用默认导出 export defult {a,b}

// a.js 文件
export let a = 100 //定义一个 a 变量并且导出
setTimeout(()=>{
    a = 200	// 一秒后给 a 赋值
},1000)

// b.js 文件
import { a } from './a.js'
console.log(a) //引入a并且打印 a = 100
setTimeout(() => {
  //a模块有一个定时器,并且在一秒后将 a 赋值成 200,我们在两秒后打印它
  console.log(a) //两秒后 a = 200
}, 2000)

esm 动态导入:使用 import方法 ,该方法是个promise函数,使用.then获取成功的回调。根据需要来动态传参

// a.js 文件
export default { a,b } //导出 a,b
// b.js 文件
import("./a.js").then((port)=>{ 
// method是导入模块,成功回调时自行赋值。使用导入模块
 console.log(port.a)
})	

esm 同时导入默认非默认成员

import {a,b,defult as c} from  "xxx地址"    //这样同时导入了默认和非默认成员。
import c,{a,b} from "xx地址"  //第二种写法: 这样导入,c指的是 "xx地址中的默认成员" 

ESM 一次性导入所有成员

import {* as all} from "xx地址" //这样 all 中包含所有成员

esm导出,将导入的成员直接导出,那么导入后导出的成员无法在该导入作用域下使用。

export {a,b} from "导出文件的地址" //导入后的成员,再次导出
export default a from './a.js'	//也可以默认导出

console.log(a) //导出后本地使用导入的成员会 报错

3.在 Node 中使用 ESM

Node 环境需要大于 8.5 ,创建文件的文件名为 .mjs

//创建文件目录
//	01_node使用ESM
	//----a.mjs
	//----b.mjs

// a.mjs
export const a = 100 //导出
console.log(a); // a = 100
// b.mjs
import { a } from './a.mjs'
console.log('我是b文件 引入a', a) // a = 100

##使用 node 运行 .mjs 文件时需要添加 --experimental-modules 语句
$ node --experimental-modules .\b.mjs 

1.2 Commonjs 特性

1.模块化规范

Commonjs导出,默认导出为 export.defult = { k:value k导出成员,value导出值},也可以写成 export.k = value。两者的导出方式是等同的

export.defult = { k : value }  // k导出成员,value导出值
export.k = value //两者相同

可以使用 ESM 导入 CommonJS 模块

// a.js
module.exports = {
    foo:'commonJS export value' //CommonJS 中导出对象
}

// a.mjs
import foo from './a.js' // ESM 引入 CommonJS 中导出的对象
console.log(foo);   // 结果为 { foo: 'commonJS export value' }

// ESM 无法使用对象的方式去引入 CommonJS 中的导出,会报错。说明 import 引入并不是对象解构赋值,而是单纯的导入语法
import {foo} from './a.js'	// 报错
console.log(foo); 

但是在 CommonJS 中通过 require() 模块载入ESModule也是不被允许的,会直接报错。

// b.mjs
export let b = 'bbb' //ESM 导出 b

// b.js
const b = require('./b.mjs') // CommonJS 中引入 ESM 
console.log(b);	//报错

2. ESM 中没有的 CommonJS 模块全局成员

CommonJS 的部分模块无法在 ESM 中使用

// a.js 在 CommonJS 中以下语句运行良好,但是在 ESM 中没有这些模块会报错
//加载模块函数
console.log(require)
// 模块对象
console.log(module)
//导出对象别名
console.log(exports)
//当前文件绝对路径
console.log(__filename)
//当前文件所在目录
console.log(__dirname)

// a.mjs  在 ESM 中实现相同方法
import { fileURLToPath } from 'url'
import { dirname } from 'path'	
const __filename = fileURLToPath(import.meta.url)
console.log(__filename) //获取当前路径
const __dirname = dirname(__filename)
console.log(__dirname) //获取相对路径


1.3 使用 package.json 配置 直接使用 ESM

在 package.json 配置 type 属性。设置文件运行环境

// package.json
{
  "type": "module",	//以 ESM 模块使用 目录文件
}

// a.js
import {b} from './b.js'
console.log(b) //输出 b

$ node --experimental-modules .\a.js //使用 ESM 的 .js文件

如何在 MSM 规范中使用 CommonJS 呢,只需要将文件名改为 .cjs

// b.js
const path = require('path')
console.log(path.join(__dirname,'COM'));
$ node --experimental-modules b.js // 报错 ReferenceError: require is not defined

// b.cjs
const path = require('path')
console.log(path.join(__dirname,'COM'));
$ node --experimental-modules b.cjs	//成功输出当前目录 C:\Users\Administrator\Desktop\webpack\COM

2.Babel

安装 Babel

$ npm i babel-loader @babel/node @babel/core @babel/preset-env --dev

使用 babel 打包

 module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader', //babel 
        },
      },
    ]
 }

打包后发现 es6 语法并没有转换为 es5 语法,那是因为 babel 只是提供的一个平台,你还需要通过插件来转换具体的特性

  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader', //babel
          options: {
            presets: ['@babel/preset-env'],	// 使用 @babel/preset-env 
          },
        },
      },
     ]
    }

3.使用 webpack 打包

1.1使用 webpack 打包一个程序

创建一个 webpack.config.js 文件,在文件配置打包属性


const path = require('path')
module.exports = {
  mode: 'none',
  entry: './src/main.js', //入口文件
  output: {
    filename: 'index.js', // 打包后的文件名
    path: path.join(__dirname, 'output'), //打包后存放的位置
  },
}

1.2 Loader

Loader 专注于资源模块的加载,以及整体项目的打包

1.打包其它非js后缀的文件

处理 css 文件的打包,直接打包css文件会产生错误,需要配置 css-loader 。使用 npm 安装loader

$ npm install --save-dev css-loader	//安装 css-loader

使用 loader 处理对应后缀的文件

const path = require('path')
module.exports = {
  mode: 'none',
  entry: './src/index.css', //入口文件
  output: {
    filename: 'index.js', // 打包后的文件名
    path: path.join(__dirname, 'output'), //打包后存放的位置
  },
  module:{
    rules:[
        {
            test:/.css$/, // 指定打包结尾的文件
            use:'css-loader' // 使用的 loader
        }
    ]
  }
}


这里我们发现 css-loader 将 css 文件转换成了 js 文件,可是页面并没有因为打包后的 js 而改变样式。那是因为打包后文件里的方法没有被使用。这时我们需要用到 style-loader 这个 loader 的作用,是将 css-loader 转换过后的结果,通过style标签的形式追加到页面上

安装 style-loader

$npm install --save-dev style-loader

使用 style-loader 转换 。 (需要注意配置了多个 Loader 它是从后向前执行。所以要把 css-loader 写到 style-loader 后面执行)

const path = require('path')
module.exports = {
  mode: 'none',
  entry: './src/index.css', //入口文件
  output: {
    filename: 'index.js', // 打包后的文件名
    path: path.join(__dirname, 'output'), //打包后存放的位置
  },
  module:{
    rules:[
        {
            test:/.css$/, // 指定打包结尾的文件
            use:['style-loader','css-loader'] // 这里的loader 需要写成数组形式. 需要注意配置了多个 Loader 它是从后向前执行。所以要把 css-loader 写到 style-loader 后面执行。首先将其转换成 js 文件后在使用 style-loader
        }
    ]
  }
}

2.资源文件的打包 url-loader file-loader

在打包的过程中,我们更希望使用 javascript 引入样式或资源文件,与其建立依赖文件的关系。而不是直接打包单独 css 样式。这样做更有利于管理整个

项目。再逻辑上更为合理,能确保上线项目资源不会缺失,这些都是必要的。

// index.css	样式文件
.heading {
  margin: 10px auto;
  width: 800px;
  height: 100px;
  background-color: #000;
  color: white;
  text-align: center;
  line-height: 100px;
}


// handle.js
import './index.css'	//引入样式文件
//导出一个创建 dom 的方法
export default () => {
    const dom = document.createElement('h2')
    dom.className = 'heading'		//设置类名 heading
    dom.innerText = 'Hello webpack'
    return dom
  }

// main.js
import createHeading from './handle.js'	
import icon form './icon.png' //引入资源文件
const dom = createHeading() //导入 dom
document.body.append(dom) //给 body 添加 dom
const img = new Image()
img.src = icon	//添加图片地址
document.body.append(img) // 这里导入的资源需要配置 file-loader

// 注意点: handle.js文件中使用的是 ESM 如果有用到 require() 需要将文件名改为.cjs
//最后通过 npm run build 打包 样式实现成功

配置资源文件后可能会出现资源无法加载的问题,那是因为打包过后的 index.html 没有生成在 dist 目录,而是在项目跟目录下。所以无法找到项目文件。我们需要配置 publicPath 路径

url-loader 可以设置超出 10KB 文件单独提取存放 小于10KB 文件转换为 Data URLs 嵌入代码中

module.exports = {
  mode: 'none',
  entry: './src/main.js', 
  output: {
    filename: 'index.js', // 打包后的文件名
    path: path.join(__dirname, 'dist'), //打包后存放的位置
    publicPath:'dist/' //告诉网站,打包过后的资源文件所在位置
  },
    module: {
    rules: [
      {
        test: /.css$/, // 指定打包结尾的文件
        use: ['style-loader', 'css-loader'], 
      },
      {
        test: /\.(png|jpg|gif)$/i,
        use: {
          loader:'url-loader', //使用 url loader 打包成base64
          options:{
            limit: 10 * 1024 // 10kb 设置 limit 只会将 10kb 以下的问题打包成 base64 超出内容会调用  file-loader
          }
        },
      },
    ],
  },
}

注意事项:使用 url-loader 必须安装 file-loader 。对于超出指定大小的文件会调用 file-loader 。如果没有安装会报错

3.自定义 loader

引入一个 md 类型的文件,通过自定义 loader 进行处理

// index.js
import about from './about.md' //导入 md
const cli = document.createElement('div')
cli.innerHTML = about()	
console.log(about())
document.body.append(cli)

自定义一个 loader 处理匹配文件名的数据

// markdown-loader.js
const marked = require('marked') //转换 html

module.exports = (source) => {
  const html = marked.parse(source) // 将 md 文件转换成 html 代码
  console.log('hello 啊 树')
  //return 'hello ~' // 这里会报错,webpack执行loader时希望返回的结果是一个 javascript 代码,这里不是一个标准的javascript代码。
  return `export default () => ${JSON.stringify(html)}` //html 文件已经是 html 格式,但是我们想导出一个 html 字符串可能会与 html 标签文件冲突。所以需要强转成 JSON
}

配置 loader

rules:[
    {
    test: /.md$/i,
  	//  use: './markdown-loader', // loader 可以是本地路径
    use: ['html-loader','./markdown-loader'], //也可以通过管道 ,使用其它loader去处理我们想要生成的文件
    }
]

1.3 Plugin 插件

相比于 Lader,Plugin 拥有更宽的能力范围。Plugin 是一个函数或者是一个包含 apply 方法的对象。通过 webpack 的生命周期钩子中挂载函数实现扩展。获取打包的文件,对文件优化处理。

1.通过plugin实现自动化

Plugin 主要用于解决项目加载以外其它自动化工作。如:打包前清除 dist 目录,压缩输出代码

$npm i clean-webpack-plugin --dev	
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode:"none",
  plugins: [
    new CleanWebpackPlugin(), // 这个插件会在打包时清除存在的 dist 目录
  ],
}

2.通过 webpack 输出 HTML 文件

通过 plugin 不需要手动硬编码 HTML,可以确保路径引用正常。

$ yarn add html-webpack-plugin --dev
const HTMLWebpackPlugins = require('html-webpack-plugin') //默认导出的是webpack 插件类型
module.exports = {
  mode:"none",
  plugins: [
      new HTMLWebpackPlugins({
      title: 'xxx后台管理项目', //生成的 html 文件标题
      template: './src/index.html', //指定使用的模板文件
    }), //自动生成 dist 目录和 index.html 文件
  ],
}

// 模板文件	./src/index.html
<html lang="en">
  <head>
      <!-- 这里的title 会被打包成模板的 title 字段-->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
  </body>
</html>

// 打包后的文件 dist/index.html
<html lang="en">
  <head>
       <!-- 这里的 title 变成了 xxx后台管理项目 -->
    <title>xxx后台管理项目</title>	
  <script defer src="index.js"></script></head>
  <body>
    <script src="./dist/index.js"></script>
  </body>
</html>


3.输出多个 HTML 文件

   new HTMLWebpackPlugins({
      title: 'xxx后台管理项目', //生成的 html 文件标题
      template: './src/index.html', //指定模板文件
    }), //自动生成 dist 目录和 index.html 文件
    new HTMLWebpackPlugins({
      filename:'about.html'
    }),//生成多个 html 文件

每个 HTMLWebpackPlugins 生成一个 html 文件,由于自定义的 HTML 模板没有被 js 文件使用

4.静态资源打包

我们希望将 pubilc 文件也打包到 输出目录

const CopyPlugin = require('copy-webpack-plugin')

new CopyPlugin({
      patterns: [
        {
          // 'public/**'
          from: 'public',
          to: 'assets', //打包时会将 public 目录中所有文件拷贝到输出目录 assets 中
        },
      ],
    }),

5.自定义 plugin 插件

通过 webpack 的生命周期钩子中挂载函数实现扩展。获取打包的文件,对文件优化处理。

// 声明一个 plugin 其中必须包含 apply() 方法
class MyPlugin {
  // 注册 plugin 会调用 apply 方法
  apply(compiler) {
    //通过 compiler 可以获取 webpack 的钩子函数
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation => 可以理解为此次打包的上下文
      for (const text in compilation.assets) {
        // console.log(text) //获取的是打包文件的文件名 index.js index.html ...
        console.log(compilation.assets[text].source()) // source() 获取文件的内容
        //对文件名为js的文件进行处理
        if (text.endsWith('.js')) {
          //获取文件中的内容
          const contents = compilation.assets[text].source()
          const withoutComments = contents.replace(/\/\*\* + \*\//g, '') // 将所有内容中 /*****/ 替换为 ''

          compilation.assets[text] = {
            source: () => withoutComments, // 文件内容变成替换后的内容
            size: () => withoutComments.length, // 必须指定文件长度
          }
          console.log(compilation.assets[text].source())
        }
      }
    })	
  }
}

// webpack.config.js
plugins: [new MyPlugin()]


1.4 自动刷新和打包工具

1.安装使用 webpack-dev-server

webpack-dev-server 集成了 自动编译自动刷新浏览器 等功能

$yarn add webpack-dev-server --dev

使用 webpack-dev-server 打开程序

$yarn webpack-dev-server -open

Dev Server 默认只会 serve 打包输出文件,只要是 webpack 打包输出的文件都会被直接访问 。其它的静态资源作为开发服务器资源被访问,就需要额外的告诉 webpack-dev-server

2.配置 devServer

devServer: {
    proxy: {
      '/api': {
        // http://localhost:8080/api/users => https://api.github.com/api/users
        target: 'https://api.github.com',
        // http://localhost:8080/api/users => https://api.github.com/users
        pathRewrite: {
          '^/api': '', // 路径重写
        },
        // 不能使用 localhost:8080 作为请求的主机名
        changeOrigin: true,
      },
    },
  },	

4.Source Map 源代码地图

1.1 Source Map 映射

代码在打包的过程中,会打包成压缩的代码格式。Source Map 可以映射代码和源代码的关系。通过转换过后的 Source Map 文件可以逆向解析源代码

源代码引入 //# sourceMappingURL=build.js.map

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

//# sourceMappingURL = jquery.min.map

// webpack.config.js 配置

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  ...
  devtool: 'source-map',    // 配置解析方式,一共 12 种
  ...
}

devtool 的可配置选项

image-20230819011633978转存失败,建议直接上传图片文件

没有通过 source-map 打包的压缩文件

转存失败,建议直接上传图片文件

经过 source-map 打包后的映射文件

image-20230819012007524转存失败,建议直接上传图片文件

5 HMR API

通过 HMR 配置后 , 可以使页面不刷新的情况下执行 webpack-dev-server。他可能用到缓存等方式

module.hot.accept('./handle', () => {
  console.log('我更新了 handle') //handle 文件发生改变时打印
})

webpack-dev-server 集成了 HNR 插件

$yarn webpack-dev-server --hot  ## 启动服务

webpack 配置热替换

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

1.1 处理 JS 模块热替换

// index.js
import createHeading from './handle.js'	//handle.js 的返回值是一个 dom 元素
const dom = createHeading() //导入 dom
document.body.append(dom) //给 body 添加dom



// 记录旧的 dom 发生更新时,防止无法找到上一次更新的元素
let oldDom = dom
//  handle.js 组件文件内容修改时触发
module.hot.accept('./handle.js', () => {
  console.log('我更新了 handle') //handle 文件发生改变时打印

  // 保存旧 dom 的 innerHTML 文本,在新dom修改后返回
  const value = oldDom.innerHTML
  document.body.removeChild(oldDom)
  //  修改文件后 返回的dom
  let newDom = createHeading()
  // 渲染旧 dom 的缓存
  newDom.innerHTML = value
  //重构代码
  document.body.append(newDom)

  // 将文件修改后返回的dom 设置为旧dom
  oldDom = newDom
})

以上代码会有 bug ,因为获取的是 innerHTML 整个DOM 元素。所以在修改文本时 'Hello webpack',不会对文本进行更新。如果需要更好的热替换效果,需要自行学习研究。

手动修改 handle.js 文件的dom,并且添加一个属性 ddd ,页面将不会刷新并渲染 ddd 属性

import './index.css'
//导出一个创建 dom 的方法
export default () => {
  const dom = document.createElement('h2')
  dom.className = 'heading'
  dom.innerText = 'Hello webpack'
  // 在js文件已经渲染后手动添加一个属性。 (手动修改文件)
  dom.setAttribute('ddd', 100)
  return dom
}

热更新前

image-20230819220059368转存失败,建议直接上传图片文件

热更新后

image-20230819215820115转存失败,建议直接上传图片文件

webpack 没有办法提供一个通用的 热替代方案

6. 生产环境优化

1.1 通过打包传入环境名,配置打包环境

webpack 在使用的过程中,在开发阶段时注重开发效率。在生产环境时更注重运行效率。webpack 推出了 mode 模块。通过传入打包模式,进行不同阶段的打包。


// webpack 支持导出一个函数
module.exports = (env, argv) => {
  const config = {
    mode: 'development',
    entry: './src/index.js',
    output: {
      filename: 'index.js',
      path: path.join(__dirname, 'dist'),
    },
    /*  devServer: {
      proxy: {
        '/api': {
          // http://localhost:8080/api/users => https://api.github.com/api/users
          target: 'https://api.github.com',
          // http://localhost:8080/api/users => https://api.github.com/users
          pathRewrite: {
            '^/api': '', // 路径重写
          },
          // 不能使用 localhost:8080 作为请求的主机名
          changeOrigin: true,
        },
      },
    }, */
    devtool: 'eval',
    module: {
      rules: [
        {
          test: /\.m?js$/,
          exclude: /(node_modules|bower_components)/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
        {
          test: /\.css$/i, // 指定打包结尾的文件
          use: ['style-loader', 'css-loader'], // 使用的 loader
        },
        {
          test: /\.(png|jpg|gif)$/i,
          use: {
            loader: 'url-loader', //使用 url loader 打包成base64
            options: {
              limit: 10 * 1024, // 10kb 设置 limit url-loader 只会将 10kb 以下的问题打包成 base64
            },
          },
        },
        {
          test: /.md$/i,
          use: './markdown-loader', // loader 可以是本地路径
        },
      ],
    },
    plugins: [
      new HTMLWebpackPlugins({
        title: 'xxx后台管理项目', //生成的 html 文件标题
        template: './src/index.html', //指定模板文件
      }), //自动生成 dist 目录和 index.html 文件
      new HTMLWebpackPlugins({
        filename: 'about.html',
      }), //生成多个 html 文件
      // new MyPlugin(),
    ],
  }

  //如果打包时使用的生产模式,就会使用生产打包。使用打包时携带 --env xxx  通过env可以获得 xxx属性
  if (env.production) {		//{ WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, prod: true }
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(), //生产环境时打包后清除 dist 目录
      new CopyPlugin({
        patterns: ['public'], //生产环境将静态文件打包到 public 目录
      }),
    ]
  }
  return config
}

if 判断需要在打包时传入属性,进行判断

$ yarn webpack --env prod ## 上文 env.prod 的值为true,上文判断的是 if (env.production) 所以需要传入 production 区分环境
$ yarn webpack --env production ## 当前打包处于生产模式

1.2 通过不同环境的配置文件,配置打包环境

配置三个环境的文件,通过映射实现不同环境的打包。因此我们分别定义三个文件

##定义三个环境配置文件,实现分别打包
#	webpack.common.js 配置源文件
#	webpack.dev.js	开发环境
#	webpack.prod.js 生产环境

# 由于文件定义缺少了 webpack.config.js webpack无法识别打包文件,需要通过 --config 文件名,使用对应打包
# yarn webpack --config webpack.dev.js 开发模式打包
# yarn webpack --config webpack.prod.js 生产模式打包

webpack.common.js 配置源文件 。导出默认配置

const HTMLWebpackPlugins = require('html-webpack-plugin') //默认导出的是webpack 插件类型
const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'index.js',
    path: path.join(__dirname, 'dist'),
  },
  devtool: 'eval',
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /\.css$/i, // 指定打包结尾的文件
        use: ['style-loader', 'css-loader'], // 使用的 loader
      },
      {
        test: /\.(png|jpg|gif)$/i,
        use: {
          loader: 'url-loader', //使用 url loader 打包成base64
          options: {
            limit: 10 * 1024, // 10kb 设置 limit url-loader 只会将 10kb 以下的问题打包成 base64
          },
        },
      },
      {
        test: /.md$/i,
        use: './markdown-loader', // loader 可以是本地路径
      },
    ],
  },
  plugins: [
    new HTMLWebpackPlugins({
      title: 'xxx后台管理项目', //生成的 html 文件标题
      template: './src/index.html', //指定模板文件
    }), //自动生成 dist 目录和 index.html 文件
  ],
}

webpack.prod.js 配置生产环境文件,需要设置静态资源。可以单独写在生产环境文件中

const common = require('./webpack.common') // 获取开发打包配置
const { merge } = require('webpack-merge') // 载入合并配置函数
const { CleanWebpackPlugin } = require('clean-webpack-plugin') //删除 dist 目录重新打包
const CopyPlugin = require('copy-webpack-plugin') //复制静态资源文件

// merge 函数会自动处理合并的逻辑
module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(), //重新打包时删除之前的 dist 目录
    new CopyPlugin({
      patterns: [{ from: 'public', to: 'public' }],
    }), //复制 public 文件夹
  ],
})

webpack.dev.js 开发环境如果无需修改,可以直接导出common 文件

module.exports = require('./webpack.common') // 获取开发打包配置

最后可以将构建的命令写入 package.json 中

{
"scripts": {
    "build": "webpack --config webpack.prod.js",
    "dev":"webpack --config webpack.dev.js"
  },
}

1.3 webpack.DefinePlugin

使用 webpack.DefinePlugin 定义的键值会被注入到代码中

// webpack.config.js
const webpack = require('webpack')
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  plugins: [
    new webpack.DefinePlugin({
      API_BASE_URL: JSON.stringify('http://114.55.108.177:8080'),	//提供了一个 API_BASE_URL 的全局变量
    }),
  ],
}


// src/main.js
//通过 webpack 配置 url
console.log(API_BASE_URL) //http://114.55.108.177:8080

1.4 Tree ShaKing

Tree ShaKing 会在生产模式下自动启动。手动配置 webpack 在开发模式中使用

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
 optimization:{
 	usedExports:true,	//负责标记 [枯叶] ,只导出外部使用过的成员
 	minimize:true	//压缩输出结果
 }
}


Tree Shaking 的前提是 ESM 环境 ,由 webpack 打包的代码必须使用 ESM 。在使用 webpack 进行打包时,我们经常会使用到 babel 插件,而babel 处理代码时很有可能将 ESM 处理成 CommonJS 导致Tree ShaKing 失效

1.5 sideEffects 副作用文件

正常情况下(非生产模式 )系统不会检测模块是否有副作用。当开启 sideEffects 时。系统将不会打包引入而没有使用的模块

//webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
 optimization:{
     //以下配置生产模式下自动开启
 	usedExports:true,	//负责标记 [枯叶] ,只导出外部使用过的成员
 	minimize:true,	// js 代码压缩
    concatenateModules: true, //尽可能合并每一个模块到一个函数中
    sideEffects:true // import 引入文件未使用不会被打包
 }
}

package.json 这里的sideEffects 和 webpack 的意思不同。

// package.json
{
"name":"xxx",
"version":"xxx",
...
//"sideEffects":false	//这里是标识代码是没有副作用的
"sideEffects":[
    "*.css"	//指定有副作用文件,有副作用文件会被打包进dist目录
	]
}


1.6 分别打包

1. 多入口打包

配置 entry 为对象类型,对文件分别打包

 // webpack.config.js 
  entry: {		// 配置多个入口打包
    index: './src/main.js',
    put: ['./src/a.js', './src/b.js', './src/c.js'], // 文件名
  },
      
 output: {
    filename: 'bundle.js', // 打包文件名
  },

上面配置 html-webpack-plugin 打包后可能出现 html 导入多条 srcipt 情况

<!-- 导出文件 index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>index</title>
    <script defer="defer" src="index.bundle.js"></script>
    <script defer="defer" src="put.bundle.js"></script>
  </head>
  <body></body>
</html>

我们可以配置多个 html 文件,分别引入打包出的文件

module.exports = {
  mode: 'none',
  entry: {
    index: './src/main.js',
    put: ['./src/a.js', './src/b.js', './src/c.js'],// a b c 文件合并导出
  },
  output: {
    filename: '[name].bundle.js', //  根据 entry 属性文件名分别导出js文件
  },
   plugins: [
    // 配置多个html 分别引入 js 文件
    new HTMLWebpackPlugins({
      title: 'index', //生成的 html 文件标题
      template: './src/index.html', //指定导出模板文件
      filename: 'index.html',
      chunks: ['index'], // //设置 html srcipt 的 src 引入 entry.index 的属性
    }), //自动生成 dist 目录和 index.html 文件
    new HTMLWebpackPlugins({
      title: 'abc', //生成的 html 文件标题
      template: './src/index.html', //指定导出模板文件
      filename: 'put.html',
      chunks: ['put'], //设置 html srcipt 的 src 引入 entry.put 的属性
    }), //自动生成 dist 目录和 index.html 文件
  ],
  optimization: {
    usedExports: true, // 标记枯叶
    minimize: false,   // 代码亚索
    sideEffects: true, // 检测文件是否有副作用
  },
}

2.提取公共模块

对引入的大文件单独打包成一个文件,如 jquery 。自定义的组件

// a.js
import $ from 'jquery'
console.log($)
//b.js
import $ from 'jquery'
console.log($)

上面的两个 js 文件分别引入了 jquery 库。我们配置 webpack.config.js 对它进行提取打包到单独文件中

  optimization: {
    usedExports: true,
    minimize: true,
    sideEffects: true, // 检测文件是否有副作用
    splitChunks: {
      //import 引入的大文件会被单独打包成一个 js 导出
      chunks: 'all',
    },
  },

打包后 jquery 库 被分配到单独的js文件中

3.动态导入组件

1.import() 动态导入

import 命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行。所以 importexport 命令只能在模块的顶层,不能在代码块之中

// 报错 js 在编译时不会分析执行 if
if (x === 2) {
  import MyModual from './myModual';
}
2.魔法注释

ES2020提案 引入import()函数,支持动态加载模块。使用 /* webpackChunkName:'名字' */ 可以将代码打包到一个 js 文件中


function run() {
  // 将公共组件 sort 和 a 打包到一个文件中
  import(/* webpackChunkName:'sort' */ './a.js').then(
    // 解构赋值获取  default 内容
    ({ default: a }) => {
      console.log(a.a, 'default')
      a.name()
    }
  )
  import(/* webpackChunkName:'sort' */ './sort.js').then(
    ({ default: sort }) => {
      console.log(sort)
      console.log(sort([458, 12, 9, 4788, 231, 4, 1, 6, 9]))
    }
  )
}
run()

对 output 配置,上面的导入文件会被打包到指定目录中

 output: {
    filename: '[name].bundle.js', // 导出文件 根据 entry 属性文件名适配
    path: path.resolve(__dirname, 'dist'),
  },

上面两个import 文件将会被打包到 ./dist/sort.bundle.js 文件中

4.样式文件单独打包

1. CSS 样式单独打包

安装 MiniCssExtractPlugin

$yarn mini-css-extract-plugin --dev

webpack.config.js 配置

//单独打包 css 模块
const webpack = require('webpack')
// 导入配置模块
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js', // 导出文件 根据 entry 属性文件名适配
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
        {
        test: /\.css$/,
        use: [
          // 'style-loader', //使用样式单独打包后,这里不在使用 style loader 而是使用 miniCss 处理
          MiniCssExtractPlugin.loader, //通过命令行标签方式注入
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(), //自动提取 css 文件到单独文件中
  ],

}

css 文件将会被单独打包成单个文件,推荐 150 k以上进行打包优化。小于150k可能有负优化

2. 压缩 css 样式文件体积优化

通过压缩优化,减小 css 样式文件体积


const webpack = require('webpack')
// css 单独打包 plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// css 代码压缩
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

const path = require('path')
module.exports = {
  plugins: [
    new MiniCssExtractPlugin(), //自动提取 css 文件到单独文件中
  ],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          // 'style-loader', //使用样式单独打包后,这里不在使用 style loader 而是使用 miniCss 处理
          MiniCssExtractPlugin.loader, //通过命令行标签方式注入
          'css-loader',
        ],
      },
    ],
  },
  optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin(), //css 代码压缩
    ],
  },
}


导出的 css 文件将被压缩

3.配置 minimizer 导致 js 压缩失效问题

上面配置 minimizer 压缩 css 文件后 , webpack会认为用户想自定义压缩数组导致 js 压缩失效,所以需要手动添加 js 代码压缩

// 引入 js 代码压缩插件
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
    //...
      optimization: {
        // minimize: true, //js 代码压缩
        minimizer: [
          new OptimizeCssAssetsWebpackPlugin(), //css 代码压缩
          new TerserWebpackPlugin(), //手动添加 js代码压缩
        ],
      },
    //...
}