webpack4实践

252 阅读10分钟

webpack4

webpack4实践内容,每个包添加对应版本,避免打包时出现版本不兼容问题。

esm考虑兼容性polyfill

browser-es-module-loader 进行解析 babel-browser-build 进行转换兼容处理 promise-polyfill(不支持promise)

    // nomodule 可以避免执行两次 一次是转换后的执行
    <script nomodule src="xxxcdn"></script>

Node 环境使用esm

老的node版本需要进行实验特性运行 ,node12+ 版本可以支持

node --experimental-modules .\index.mjs

模块相互

ESM中可以导入commonjs的导出 反之不行,commonjs始终导出一个默认成员

mjs和cjs差别

// .mjs
import { fileURLToPath } from 'url'
import { dirname, basename, extname } from 'path'
// file:///C:/Users/admin/Desktop/Git-repositories/npm/src/use.mjs
console.log(import.meta.url)
const __filename = fileURLToPath(import.meta.url)
//转换成C:\Users\admin\Desktop\Git-repositories\npm\src\use.mjs
console.log('__filename', __filename)
//C:\Users\admin\Desktop\Git-repositories\npm\src
const __dirname = dirname(__filename)
console.log('__dirname', __dirname)
//获取扩展名 和全名
console.log(basename(__filename))
console.log(extname(__filename))


// .cjs
const path = require('path')
//获取扩展名 和全名
console.log(path.basename(__filename))
console.log(path.extname(__filename))

console.log('require', require)
console.log('module', module)
console.log('exports', exports)
// 文件的绝对路径
console.log('__filename', __filename)
// 文件所在文件目录
console.log('__dirname', __dirname)

Babel 低版本兼容

安装依赖 core是核心库 preset-env是最新特性插件的集合 具体转换使用的是插件

yarn add @babel/node @babel/core @babel/preset-env --save-dev

部分安装的插件

创建文件使用 yarn babel-node index.js --presets=@babel/preset-env 进行编译执行 没有预设执行会失败

// index.js
import name from './module.js'
console.log('打印***name', name)


// module.js
const name = 'module'
export default name

使用.babelrc 配置json文件进行设置 yarn babel-node index.js

// 使用预设
{
   "presets": [
    "@babel/preset-env"
   ]
}
// 使用插件
{
 "plugins": [
     "@babel/plugin-transform-modules-commonjs"
  ]
}

快速上手webpack4

  1. 安装 yarn add webpack@4.40.2 webpack-cli@3.3.9 --dev
  2. 创建文件。开启服务 显示HELLO 如果不添加type就会报错,使用webpack进行打包,会进行兼容type则不需要
//src/index.js 

import createElement from './module.js'
createElement(document.body)
// src/module.js

const createElement = parentNode => {
  const node = document.createElement('div')
  node.innerText = 'HELLO'
  parentNode.appendChild(node)
}
export default createElement

// /index.html
<script type="module" src="./src/index.js"></script>
// 打包后文件
<script  src="./dist/index.js"></script>

运行node src/index. js报错 两种解决方式 这就是package中为什么配置type为module的原因

  1. 运行命令 yarn webpack

    1. 出口文件自动生成dist/main.js
    2. 默认入口文件是src/index.js
    3. 会报warning 未指定打包环境 可通过 yarn webpack --mode(可选=)development/production/none
  2. webpack.config.js 配置文件 因为是在node环境下运行,使用module.exports

    1. 出口入口配置环境配置
    2. const path = require('path')
      
      module.exports = {
        // 打包环境配置 yarn webpakc时对应的环境
        mode:'production',
        // 使用相对路径
        entry: './src/module.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'output')
        }
      }
      
    3. 资源模块加载 css css-loader@3.2.0打包解析 入口改成css的
    4.   style-loader@1.0.0 生成style标签插入 主入口改成js,使用import导入css文件
    5.   通过loader可以加载任何资源文件

const path = require('path')

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  // 从右到左执行
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}
  1. 图片资源加载 file-loader@4.2.0

图片不显示,资源根目录不正确,配置publicpath

const path = require('path')

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output'),
    publicPath: 'output/' // /不能省略
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /.(jpeg|png|gif|webp|jpg)$/,
        use: 'file-loader'
      }
    ]
  }
}
  1. Data URLS指url包含数据 图片进行base64编码 url-loader@2.2.0

根据dataURL直接进行解析 例如

data:text/html;charset=UTF-8,<h1>html content</h1>
data:image/png;base64,xxxx//需要url-loader实现

  • 体积小进行转换成data url 减少请求次数
  • 大文件单独提取存放 提高加载速度
const path = require('path')
module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output'),
    publicPath: 'output/' // /不能省略
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /.(jpeg|png|gif|webp|jpg)$/,
        use: 'url-loader'
      }
    ]
  }
}

如果限制了大小,超出的还是会使用file-loader去处理,所以不能删除

{
    test: /.(jpeg|png|gif|webp|jpg)$/,
    use: {
      loader: 'url-loader',
      options: {
        limit: 40 * 1024 //10KB
      }
    }
  }
  1. 常用加载器分类

    1. 编辑转换器 css-loader 生成js代码
    2. 文件操作类 file-loader 拷贝到输出的目录
    3. 代码检查类 eslint-loader
  2. webpack和ES5 babel-loader@8.2.4 @babel/core @babel/preset-env

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

打包需要,webpack默认处理了import和export,不会处理转换js的新特性

转换后

  1. webpack加载资源的方式

    1. ES modules标准的import声明
    2. 遵循commonjs标准的require函数( esm使用默认导出,使用require导入 必须使用require().default进行导入)
    3. 遵循AMD标准的define函数和require函数
    4. *样式代码中的@import指令和url函数
  • 执行过程是先使用css-loader进行加载,遇到url使用url-loader进行加载
  • //index.js
    import './index.css'
    // index.css
    body {
      min-height: 100vh;
      background-image: url(sea.jpg);
      background-size: cover;
    }
    // webpack.config.js
    const path = require('path')
    
    module.exports = {
      mode: 'none',
      // 使用相对路径
      entry: './src/index.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/' // /不能省略
      },
      module: {
        rules: [
          {
            test: /.js$/,
            // use: 'babel-loader'
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          },
          {
            test: /.css$/,
            use: ['style-loader', 'css-loader']// 1. 先解析
          },
          {
            test: /.(jpeg|png|gif|webp|jpg)$/,
            use: { 
              loader: 'url-loader',  // 2.遇到css中url进行解析
              options: {
                limit: 40 * 1024 //10KB
              }
            }
          }
        ]
      }
    }
    
    //--- 第二种css中导入css文件
    //reset.css
    * {
      margin: 0;
      padding: 0;
    }
    // index.css
    @import url(reset.css)
    
  •   e. *HTML代码中图片标签的src属性(默认)a的href属性需要进行配置
  • 解析html使用html-loader@0.5.5
  • // footer.html
    <footer>
      <img src="./sea.jpg" alt="sea" width="250" />
    </footer>
    
    //index.js
    import footer from './footer.html'
    document.write(footer)
    
    //webpack.config.js
    const path = require('path')
    
    module.exports = {
      mode: 'none',
      // 使用相对路径
      entry: './src/index.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/' // /不能省略
      },
      module: {
        rules: [
          {
            test: /.js$/,
            // use: 'babel-loader'
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          },
          {
            test: /.css$/,
            use: ['style-loader', 'css-loader']
          },
          {
            test: /.(jpeg|png|gif|webp|jpg)$/,
            use: {
              loader: 'url-loader',
              options: {
                limit: 40 * 1024 //10KB
              }
            }
          },
          {
            test: /.html$/,
            use: 'html-loader'
          }
        ]
      }
    }
    // 默认支持img:src 属性
      {
        test: /.html$/,
        use: {
          loader: 'html-loader',
          options: {
            attrs: ['img:src', 'a:href'] // 必须全部配置 且图片必须是file loader处理不能是data-url
          }
        }
      }
    

开发一个loader

loader是资源文件的输入到输出的转换 ,对同一资源依次使用多个loader处理。工作原理:

source-> markdown-loader ->xxx.loader ->结果 返回一个js代码

// about.md
# markdown

这是一个 markdown 文本
// index.js
import about from './about.md'
// 转换成html文件
console.log('打印***about', about)

// / markdown-loader.js
// 导出函数 输入资源文件的内容
module.exports = source => {
  console.log('打印***source', source)
  return 'markdown-loader 转换结果' // 打包正常运行出错
  return `console.log('markdown-loader 转换结果') `// 正常
}
// webpack
const path = require('path')

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output'),
    publicPath: 'output/' // /不能省略
  },
  module: {
    rules: [
      {
        test: /.md$/,
        use: './markdown-loader'
      },
    ]
  }
}

正常返回js代码

两种处理方式,1.直接返回导出结果 安装一个marked@0.7.0 解析

//markdown-loader.js
// 导出函数 输入资源文件的内容
const marked = require('marked')

module.exports = source => {
  console.log('打印***source', source)
  const html = marked(source)
  return `module.exports = ${JSON.stringify(html)}`
  // return `export default ${JSON.stringify(html)}`
}

2.返回一个html 交给html-loader@0.5.5再去处理

//markdown-loader.js
// 导出函数 输入资源文件的内容
const marked = require('marked')

module.exports = source => {
  console.log('打印***source', source)
  const html = marked(source)
  return html
}

// webpack
{
    test: /.md$/,
    use: ['html-loader', './markdown-loader']
}

插件机制

解决加载资源外其他自动化工作,如清除dist 拷贝资源文件 压缩代码

  1. 自动清除原打包目录 clean-webpack-plugin@3.0.0
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output'),
    publicPath: 'output/'
  },
  plugins: [new CleanWebpackPlugin()]
}
  1. html-webpack-plugin@4 自动生成使用bundle.js的html
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output'),
    publicPath: 'output/' // /不能省略
  },
  plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin()]
}

进行打包后生成的html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="output/bundle.js"></script></body>
</html>

启动服务请求路径和之前的publicPath问题

去除publicPath 正常

进行配置

    new HtmlWebpackPlugin({
      title: 'pms',// 名称
      filename: 'pms.html',// 文件名
      meta: {
        viewport: 'device-width'
      }
    })

如果html配置过多,使用模板进行配置,新建src/index.html,去除html-loader的配置

模板语法没有被正常解析, 因为配置文件中用到了html-loader, 是的模板index.html中的配置被当做字符串处理. 而使用html-loader多用来处理 component页面. , 如果有页面同时需要html-loader和html-webpack-plugin处理不可以

// 模板的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.0" />
    <!-- 模板语法 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
  </body>
</html>

// webpack
new HtmlWebpackPlugin({
  title: 'pms444', // 名称
  template: 'src/index.html'
})

输出多个页面文件

  plugins: [
    new CleanWebpackPlugin(),
    // index.html
    new HtmlWebpackPlugin({
      title: 'pms444', // 名称
      template: 'src/index.html'
    }),
    // about.html 默认生成
    new HtmlWebpackPlugin({
      filename: 'about.html'
    })
  ]
  1. copy-webpack-plugin@5.0.4 复制文件或目录到打包后的文件
new CopyWebpackPlugin(['public']) // 文件拷贝到输出目录

开发一个plugin

钩子机制,必须是一个函数或包含apply方法的对象,通过在生命周期的钩子中挂载函数实现扩展

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

// 去除注释
class MyPlugin {
  apply(compiler) {
    console.log('MyPlugin 启动')
    //  在emit钩子时 注册插件执行
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation 理解为打包的上下文 是一个对象
      // name 每个文件的名称  ,compilation.assets[name].source()内容
      for (const name in compilation.assets) {
        // console.log('打印***name', name)
        // console.log('打印***内容', compilation.assets[name].source())
        if (name.endsWith('.js')) {
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(//**+*//g, '')
          // source和size是必须
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
    // publicPath: 'output/' // /不能省略
  },
  module: {
    rules: [
      {
        test: /.js$/,
        // use: 'babel-loader'
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /.(jpeg|png|gif|webp|jpg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 //10KB
          }
        }
      }
      // {
      //  test: /.html$/,
      //  use: {
      //    loader: 'html-loader',
      //    options: {
      //      attrs: ['img:src', 'a:href']
      //    }
      //  }
      // }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // index.html
    new HtmlWebpackPlugin({
      title: 'pms444', // 名称
      template: 'public/index.html'
    }),
    // about.html 默认生成
    new HtmlWebpackPlugin({
      filename: 'about.html'
    }),
    new CopyWebpackPlugin(['public']), // 文件拷贝到输出目录
    new MyPlugin()
  ]
}

使用前

使用后

webpack开发体验

一、原始流程

  1. 自动编译 watch工作模式 自动打包编译 yarn webpack --watch 监视文件变化后html不生成???
  2. 自动刷新浏览器 BrowserSync browser-sync output --files '**/*'

这两个搭配开发效率低

二、使用webpack-dev-server@3.8.2

yarn webpack-dev-server 运行 可待参数--open打开浏览器 具体参考 打包放在内存中,不会打包出来

webpack打包的都是可以访问,

  devServer: {
    // 字符串 数组
    contentBase: './public'
  },
  // new CopyWebpackPlugin(['public']) // 文件拷贝到输出目录 开发阶段不使用

跨域请求问题:

  devServer: {
    // 字符串 数组
    contentBase: './public',
    proxy: {
      '/api': {
        // localhost:8080/api/users --> api.github.com/users
        target: 'https://api.github.com',
        pathRewrite: { '^/api': '' },
        changeOrigin: true
      }
    }
  },

sourceMap

调试和报错都是基于运行代码,打包后代码错误无法定位。

.js 最后一行,自动请求文件,逆向解析

配置devtool 打开sourceMap

devtool: 'source-map',

如何验证,webpack可以是一个对象,也可以是一个数组(可进行多次打包),eval使用eval执行, module不进行转换,inline 使用dataURL嵌入进去,hidden生成,但是不引入,nosource 没有源代码提供行列信息

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

const allMap = [  'eval',  'cheap-eval-source-map',  'cheap-module-eval-source-map',  'eval-source-map',  'cheap-source-map',  'cheap-module-source-map',  'inline-cheap-source-map',  'inline-cheap-module-source-map',  'source-map',  'inline-source-map',  'hidden-source-map',  'nosources-source-map']

module.exports = allMap.map(item => ({
  mode: 'none',
  devtool: item,
  entry: './src/index.js',
  output: {
    filename: `js/${item}.js`,
    path: path.join(__dirname, 'output')
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  plugins: [
    // new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: `${item}.html`
    })
  ]
}))

启动serve,output文件

选择合适的sourceMap:开发环境: cheat-module-eval-source-map 生产:none nosources-source-map

自动刷新问题HMR

比如页面有输入框,输入内容后,需要调整字体颜色,修改代码后,重新打包刷新,输入内容重新写

  1. 代码写死一些内容
  2. 额外代码实现刷新前保存,刷新后读取
  3. 页面不刷新前提下,模块可以及时更新

HMR Hot Module Replacement 模版热更新

开启热更新 集成在webpack-dev-server@3.9.0中

  1. 使用yarn webpack-dev-server --hot开启热更新,修改css会变化

  1. 使用webpack配置
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  devServer: {
    hot: true //开启热更新
  },
  module: {
    rules: [
      // {
      //  test: /.md$/,
      //  use: ['html-loader', './markdown-loader']
      // },
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /.(jpeg|png|gif|webp|jpg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 //10KB
          }
        }
      },
      {
        test: /.html$/,
        use: {
          loader: 'html-loader',
          options: {
            attrs: ['img:src', 'a:href']
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/tmp.html'
    }),
    new CleanWebpackPlugin()
    new webpack.HotModuleReplacementPlugin()
  ]
}

js文件更新刷新了页面,不可以开箱即用。

HMR API

处理文件更新的热替换

// index.js
import './input.js'
module.hot.accept('./input.js', () => {
  console.log('打印***input更新')
  // 此处应该为重新执行导入的内容提要
})


// input.js
console.log(392)

重新执行后结果,不会刷新页面 

不同环境的配置

配置方式

  1. 配置文件根据环境不同导出不同配置
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

// env 是webpack打包传入的 --env=production
module.exports = function (env, argv) {
  const config = {
    mode: 'none',
    // 使用相对路径
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.join(__dirname, 'output')
    },
    devServer: {
      hot: true //开启热更新
    },
    module: {
      rules: [
        {
          test: /.js$/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          }
        },
        {
          test: /.css$/,
          use: ['style-loader', 'css-loader']
        },
        {
          test: /.(jpeg|png|gif|webp|jpg)$/,
          use: {
            loader: 'url-loader',
            options: {
              limit: 10 * 1024 //10KB
            }
          }
        },
        {
          test: /.html$/,
          use: {
            loader: 'html-loader',
            options: {
              attrs: ['img:src', 'a:href']
            }
          }
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: './src/tmp.html'
      }),
      new CleanWebpackPlugin(),
      new webpack.HotModuleReplacementPlugin()
    ]
  }
  if (env === 'production') {
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['./public'])
    ]
  }
  return config
}
  1. 一个环境对应一个配置文件

使用webpack-merge@4.2.2 进行合并配置

//webpack.common.js
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

// env 是webpack打包传入的 --env=production
module.exports = {
  mode: 'none',
  // 使用相对路径
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  devServer: {
    hot: true //开启热更新
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /.(jpeg|png|gif|webp|jpg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 //10KB
          }
        }
      },
      {
        test: /.html$/,
        use: {
          loader: 'html-loader',
          options: {
            attrs: ['img:src', 'a:href']
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/tmp.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ]
}



//webpack.dev.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
const webpack = require('webpack')

// assign会覆盖对象 不会合并
module.exports = merge(common, {
  mode: 'development',
  devServer: {
    hot: true //开启热更新
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
})




//webpack.prod.js 

const common = require('./webpack.common')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

// assign会覆盖对象 不会合并
module.exports = merge(common, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin(), new CopyWebpackPlugin(['public'])]
})

运行命令使用yarn webpack --config webpack.dev/prod.js ,可以进行配置

优化配置

  1. DefinePlugin 代码注入全局成员 process.env.NODE_ENV常量 判断原型环境
const webpack = require('webpack')
const path = require('path')
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  plugins: [
    new webpack.DefinePlugin({
       // API_BASE_URL: 'https://api.example.com'
      // API_BASE_URL: "'https://api.example.com'" 
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

// index.js
console.log('打印***API_BASE_URL', API_BASE_URL)

不序列化API_BASE_URL: 'api.example.com'

序列化后使用API_BASE_URL: "'api.example.com'" 或者用JSON.stringfy

  1. Tree Shaking

无用代码去除 dead-code,生产模式自动开启。

// index.js
import { Button } from './components'

document.body.appendChild(Button())

// components.js
export const Button = () => {
  return document.createElement('button')
  console.log('打印***dead code')
}
export const Link = () => {
  return document.createElement('a')
}
export const Heading = level => {
  return document.createElement('h' + level)
}

none模式下打包结果

配置

  optimization: {
    // 去除未导入的模块
    usedExports: true, // 标记枯树枝树叶
    // 开启代码压缩,去除了dead code
    minimize: true // 进行摇
    concatenateModules:true // 将模块放在同一个函数中 提升运行效率,减少代码体积 scope Hoisting 作用域提升
  }

  1. Tree Shaking & Babel

使用babel会失效 ,代码必须使用esm才能tree shaking,使用babel-loader可能会转换ESM-> CommonJS,

node_modules/babel-loader/lib/injectCaller.js

node_modules/@babel/preset-env/lib/index.js禁用了esm转换

强制开启

  {
    test: /.js$/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: [['@babel/preset-env', { modules: 'commonjs' }]]
      }
    }
  },
  1. sideEffects 副作用

副作用:模块执行时,除了导出成员之外所做的事情

提供更大的压缩空间,一般用于npm包标记是否有副作用,生产环境默认开启

// src/components
/**
- heading.js
export default Heading = level => {
  return document.createElement('h' + level)
}

- button.js
export default Button = () => {
  console.log('打印***button')
  return document.createElement('button')
  console.log('打印***dead code')
}

- link.js
export default Link = () => {
  return document.createElement('a')
}

- index.js
export { default as Button } from './button'
export { default as Heading } from './heading'
export { default as Link } from './link'

*/
// index.js
import { Button } from './components'

document.body.appendChild(Button())


// 需要开启副作用
  optimization: {
    sideEffects: true
  }
// package.json
"sideEffects":false // 声明代码无副作用

前提:确定代码没有副作用

例如: 为什么还是打包出来???

// 新建一个extend.js
Number.prototype.pad = function (size) {
  let ret = this + ''
  while (ret.length < size) {
    ret = '0' + ret
  }
  return ret
}
// 上述代码实际用到,但是标识了没有副作用,会影响运行
// index.js
import { Button } from './components'
import './src/extend'

document.body.appendChild(Button())

// package.json 中需要标识哪些文件没有副作用
  "sideEffects": [
    "./src/extend.js",
    "*.css"
  ]
  1. Code Splitting 分包/代码分割

bundle体积很大,分包按需加载。http1.1 并发,浪费带宽。根据不同规则

  1. 多入口打包   webpack配置文件的entry属性配置类型:

  字符串:一个入口文件

  字符串数组:多个入口文件打包到一起,相当于一个入口打包

  对象:多个入口文件分别打包

  key:入口的名称   value:入口对应的文件路径

  每个打包入口会形成一个独立的chunk,入口名称就是这个chunk的name(默认是main)。

  一旦配置为多入口,输出的文件名也需要修改:

  使用[name]占位符,动态输出文件名,[name]最终替换为入口的名称,即entry的key。

  const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  const HtmlWebpackPlugin = require('html-webpack-plugin')
  module.exports = {
    entry: {
      index: './src/index.js',
      album: './src/album.js',
    },
    output: {
      // 使用[name]占位符,动态输出文件名
      // [name]最终替换为入口的名称,即entry的key
      filename: '[name].bundle.js',
    },
    plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        filename: 'index.html',
        template: './src/index.html',
      }),
      new HtmlWebpackPlugin({
        filename: 'album.html',
        template: './src/album.html',
      }),
    ],
  }

  此时打包会输出两个html和两个bundle文件,但是发现html文件中将两个bundle文件全部引用了。   这是因为html-webpack-plugin插件会自动生成一个注入所有打包结果的html。   如果需要指定输出的html所使用的bundle,可以使用插件的chunks属性配置:

  new HtmlWebpackPlugin({
    filename: 'index.html',
    template: './src/index.html',
    chunks: ['index'],
  }),
  new HtmlWebpackPlugin({
    filename: 'album.html',
    template: './src/album.html',
    chunks: ['album'],
  }),

  每个打包入口,都会形成一个独立的chunk。   而插件的chunks属性,通过chunk的名称,指定需要注入哪些chunk。   提取公共模块 split chunks

 optimization: {
   splitChunks: {
     // all 表示将所有的公共模块都提取到单独的bundle当中
     chunks: 'all'
   }
 }
  1. 动态导入
  使用import('xxx').then()进行动态
  1. 魔法注释 Magic Comments

默认通过动态导入产生的bundle文件,它的名称是一个序号,文件名为[number].bundle.js。

可以通过webpack特有的魔法注释,给它们定义名称。

具体使用就是,在import()的参数位置(前后都可以),添加一个特定格式的行内注释:


// 格式:/*webpackChunkName:'<name>'*/
import(/* webpackChunkName: 'posts' */'./posts/posts').then(() => {})
import('./posts/posts'/* webpackChunkName: 'album' */).then(() => {})

生成文件:

posts.bundle.js

album.bundle.js

album~posts.bundle.js 提供公共模块的文件也同步变化

如果多个模块使用的相同的chunkName,那它们最终会被打包到一起,自然不需要提取公共模块,最终只会生成一个文件。

借助这个特点,就可以根据情况,灵活组织动态导入的模块所输出的文件。

  1. 提取css文件 MiniCssExtractPlugin

mini-css-extract-plugin@0.8.0,不需要style-loader ,现在通过MiniCssExtractPlugin.loader进行link标签引入

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 通过 style 标签注入
          MiniCssExtractPlugin.loader, // 通过 link 标签注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
}
  1. 压缩css代码 OptimizeCssAssetsWebpackPlugin

默认只针对js压缩

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 通过 style 标签注入
          MiniCssExtractPlugin.loader, // 通过 link 标签注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
}

打包后,就会在输出目录下,看到提取出来的css文件了,它的名称使用的是导入它的模块的名称(可能是魔法注释的名称,可能是合并打包成一个文件)。

打包效果:

css模块不会被包裹在函数中,作为数组参数的元素被使用。

而是在主入口文件执行方法中,以标签+文件路径的形式注入到html中。

建议:

如果样式内容不是很多的话,提取到单个文件的效果不是很好。

建议CSS文件超过150kb左右,才考虑提取到单个文件中。

否则css嵌入到代码中,减少一次请求,效果可能更好。

OptimizeCssAssetsWebpackPlugin 压缩输出的css文件

使用MiniCssExtractPlugin后,样式就被提取到单独的css文件中了。

前面说过,webpack在production模式下,会自动压缩优化打包的结果。

但是单独提取的css文件并没有被压缩。

这是因为webpack内置的压缩插件,仅仅支持JS文件的压缩。

对于其他类型的文件压缩,都需要额外的插件支持。

webpack推荐使用「optimize-css-assets-webpack-plugin」插件压缩样式文件。

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
  mode: 'none',
  output: {
    filename: '[name].bundle.js',
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 通过 style 标签注入
          MiniCssExtractPlugin.loader, // 通过 link 标签注入
          'css-loader'
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    new MiniCssExtractPlugin(),
    new OptimizeCssAssetsWebpackPlugin()
  ],
}

optimization.minimizer

webpack官方文档介绍时并不是将 「OptimizeCssAssetsWebpackPlugin」 插件配置在「plugins」数组中。

而是配置在 「optimization.minimizer」 数组中。

原因是:

配置在「plugins」中,webpack就会在启动时使用这个插件。

而配置在 「optimization.minimizer」 中,就只会在「optimization.minimize」这个特性开启时使用。

所以webpack推荐,像压缩类的插件,应该配置在「optimization.minimizer」数组中。

以便于通过「optimization.minimize」统一控制。(生产环境会默认开启minimize)

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
  mode: 'none',
  output: {
    filename: '[name].bundle.js',
  },
  optimization: {
    minimize: true,
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 通过 style 标签注入
          MiniCssExtractPlugin.loader, // 通过 link 标签注入
          'css-loader'
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    new MiniCssExtractPlugin(),
    // new OptimizeCssAssetsWebpackPlugin()
  ],
}

然而这样配置会导致JS不会被压缩。

原因是webpack认为,如果配置了minimizer,就表示开发者在自定以压缩插件。 内部的JS压缩器就会被覆盖掉。所以这里还需要手动将它添加回来。 webpack内部使用的JS压缩器是「terser-webpack-plugin」。 注意:手动添加需要安装这个插件才能使用。

// 只展示了添加的代码
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  // ...
}
  1. 输出文件名Hash. substitutions

一般部署前端的资源文件时,都会启用服务器的静态资源缓存。这样用户的客户端就可以缓存应用的静态资源,后续就不再需要重复请求服务器获取静态资源文件。从而整体提上了应用的响应速度。

不过开启服务器的静态资源缓存也有一些需要注意的地方:

如果在缓存策略中设置的失效时间过短,效果就不会特别明显。

如果设置的比较长,一旦这个应用发生了更新,重新部署过后,就没有办法及时更新到客户端。

为了解决这个问题,建议在生产环境中,在输出的文件名中添加哈希值(Hash)。

一旦资源文件发生改变,文件名称也会随之变化。

对于客户端而言,新的文件名就会发生新的请求,也就没有缓存,从而实现客户端及时更新。

这样就可以将缓存策略中的过期时间设置的非常长,而不用担心文件更新的问题。

webpack的filename属性,和绝大多数插件的filename属性,都支持通过占位符的方式为文件名设置hash。

不过它们支持3中hash,效果各不相同:

  • [hash]:项目级别的hash  一旦项目中有任何改动,当前打包的hash就会发生变化
  • [chunhash]:chunk级别的hash  打包时,只要是同一路的chunk,使用的hash就是一样的  动态导入的模块都会形成一个单独的chunk。这个chunk最终生成一个bundle(JS文件),如果配置了提取css文件,模块中引用的css也会被提取到css文件中。但它名义上仍然属于这个chunk。

例如:

通过动态导入方式会生成多个bundle,而这些JS模块中引入的css,如果被提取为css文件,使用的名称与JS模块一致,同样,它们使用的chunkhash也一样。而生成的这些bundle使用的chunkhash就不一样。

修改一个模块的内容,只会更新这些文件的chunkhash同一个chunk下的文件(js css)使用了这个模块的文件(因为模块名称变化,所以这个文件中引入这个模块的路径也发生了变化,相当于被动改变)相比[hash],[chunkhash]更精确一些

  • [contenthash]:文件级别 根据输出文件的内容生成的hash即不同的文件拥有不同的hash 它影响到的只有:当前模块生成的文件使用这个模块的文件

注意:

如果配置了提取CSS文件,css实际上没有被包裹模块的bundle中,而是在主bundle文件的执行方法中,通过link方式注入到html中。所以此时修改css文件,只会更新自己和主bundle的hash,而不会影响引入它的子模块。

[contenthash]精确的定位到了文件级别的hash,只有当文件更新,才会更新它的文件名或路径。它最适合解决缓存问题。

hash长度默认20位,webpack允许通过在占位符用添加冒号+一个数字的方式指定hash的长度。

建议使用8位就够了:[contenthash:8]