webpack

概述

  • 官网 https://www.webpackjs.com/concepts/
  • 安装
    • npm i webpack webpack-cli -D
    • webpack-cli(此工具用于在命令行中运行 webpack):
  • 编译
    • npx webpack 入口文件 --mode-编译模式(开发/生产)
    • npx webpack --config 指定配置文件
  • webpack本身只识别js资源,需要通过loader对css、图片等资源进行预处理

配置

  • entry
    • 入口
  • output 出口
    • path 路径
    • filename 文件名
  • loader 加载器
    • 对文件进行预处理
    • 用于识别webpack不能识别的文件
  • plugins 插件
    • 用于扩展webpack的功能
  • node 模式
    • 'none' | 'development' | 'production'
import path = require('path')
module.exports = {
    entry: '',
    // entry: { app: '', main: '' } //多入口打包
    output: {
        path: path.resolve(__dirname, 'dest'),
        filename: 'main.js',
        // filename: '[name].js',//根据入口文件的名字生成打包后的文件名
        clean: true//清空上次打包内容
        // filename: 'js/main.js'//指定js文件目录
        //chunkFilename:'js/[name].js'//输出的其他文件的命名规则
        //chunkFilename:'js/[name][contenthash:10].js'//contenthash根据文件内容生成hash值
        // assetModuleFilename: 'static/[hash:10][ext][query]'//静态资源(type: 'asset/*')统一的命名规则
    },
    module: {//loader
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],//执行顺序从右到左,从下到上
       },
       {
         test: /\.less$/i,
         use: ['style-loader', 'css-loader', 'less-loader'],//执行顺序从右到左,从下到上
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
        //  generator: {
        //     filename: 'static/[hash][ext][query]'//自定义输出文件路径和名字
        //  }

        //生成Base64格式
        //  type: 'asset',
        //  parser: {
        //     dataUrlCondition: {
        //         maxSize: 4 * 1024 // 4kb
        //     }
        // }
       },
      {
        test: /\.(woff|woff2|eot|ttf|otf|mp3|mp4)$/i,
        type: 'asset/resource',
      },
     ],
   },
   plugins: [
    // new HtmlWebpackPlugin()
   ],
   mode: 'development',
   resolve: {//设置模块如何被解析
    // alias: {},//路径别名配置
    extensions: ['.js', '.json', '.wasm'],
   },
  //  performance:false//关闭打包时的性能分析
}
  • 打包编译时启用eslint检测
    • npm install eslint-webpack-plugin eslint --save-dev
    • .eslintrc.js 配置文件
    • .eslintignore 忽略检查配置
    • 配合vscode插件ESlint可在编写代码时进行错误提示
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
  // ...
  plugins: [
    new ESLintPlugin({
        context: path.resolve(__dirname, 'src')//指定检测文件目录
    })
],
  // ...
};
  • 在webpack中使用Babel
    • 将es6+的高级语法向后做兼容处理
    • npm install -D babel-loader @babel/core @babel/preset-env
    • babel-config.js 配置文件
module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        // options: {
        //   presets: ['@babel/preset-env']//智能预设处理es6新语法
        // plugins: ['@babel/plugin-transform-runtime']//Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。你可以引入 Babel runtime 作为一个独立模块,来避免重复引入。
        // }
      }
    }
  ]
}
  • 使用html插件
    • npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
        template: path.resolve(__dirname, 'public/index.html'),//使用模板文件,需要自定义生成后的html时必须使用模板
    })
],
};
  • 使用devServer
    • 开启本地服务器,实时监听文件变化,实时编译
    • 安装:npm i webpack-dev-server -D
    • 启动命令:npx webpack serve
    • 开发模式下会将打包内容输出到内存,不会生成具体文件
const path = require('path');
module.exports = {
  //...
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    compress: true,//启用 gzip 压缩
    port: 9000,
    open:true,//是否自动打开
    historyApiFallback: true,//解决history路径刷新404的问题
  },
};
  • 开发模式和生产模式分离
import path = require('path')
//开发模式
module.exports = {
    entry: './src/main.js',
    output: {
        path: undefined,//devServer开发模式下无具体文件输出所以不需要路径
        filename: 'main.js',
    },
    module: {//loader
        rules: [],
    },
    plugins: [],
    mode: 'development',
}
//生产模式
module.exports = {
    entry: '',
    output: {
        path: path.resolve(__dirname, '../dest'),//如果配置文件不在主目录下,拼接绝对路径时需要回退到主目录再拼接
        filename: 'main.js',
    },
    module: {//loader
        rules: [],
    },
    plugins: [],
    mode: 'production',
}

sourceMap

  • SourceMap是一种源文件映射技术。当项目运行后,如果出现错误,错误信息只能定位到打包后文件中错误的位置。如果想查看在源文件中错误的位置,则需要使用映射关系,找到对应的位置。
  • 它会生成一个xxx.map映射文件,保存的是编译后文件和源文件的一一映射关系
  • devtool
    • 控制是否生成,以及如何生成 source map。
    • 默认:none
  • 使用 SourceMapDevToolPlugin 可以进行更细粒度的配置。
  • 使用 source-map-loader 可以对已有的 source map进行特殊处理。
// sourceMap配置
module.exports = {
    // ...
    mode: 'development',
    devtool: 'cheap-module-source-map'//开发常用模式

    // mode: 'production',
    // devtool: 'source-map'//生成常用模式
}

loader 配置

  • Rule.oneOf
    • 规则数组,当规则匹配时,只使用第一个匹配到规则。
module.exports = {
  //...
  module: {
    rules: [
      {
        oneOf: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.less$/i,
                use: ['style-loader', 'css-loader', 'less-loader'],
            },
        ],
      },
    ],
  },
};
  • Rule.exclude
    • 排除所有符合条件的模块
  • Rule.include
    • 引入指定的模块
  • cache
    • 缓存生成的 webpack 模块和 chunk,来改善二次构建速度
//loader中使用cache
{
    module: {
    rules: [
        {
        test: /\.m?js$/,
        use: {
            loader: 'babel-loader',
            options: {
                cacheDirectory: true,
                cacheCompression: false//默认值为 true。当设置此值时,会使用 Gzip 压缩每个 Babel transform 输出。如果你想要退出缓存压缩,将它设置为 false -- 如果你的项目中有数千个文件需要压缩转译,那么设置此选项可能会从中收益。
            }
        }
        }
    ]
    }
}
//plugin中使用缓存
module.exports = {
  // ...
  plugins: [
    new ESLintPlugin({
        cache: true,
        cacheLocation: path.resolve(__dirname, '.test_cache'),//指定缓存位置/文件
    })
    ],
  // ...
};

热模块更新

  • 可以使用插件:HotModuleReplacementPlugin
    • 热模块替换,提高打包效率
    • 修改一个模块的代码时,不再重新打包所有文件,只打包被修改的文件,其他的走缓存
    • 不能被用在生产环境
  • 也可以在devServer中直接配置
// webpack.config.js
module.exports = {
  //...
  devServer: {
    hot: true,//也可以使用devServer的hot配置开启热模块更新,但是这个默认不支持js的热更新,需要主动在js中使用module.hot.accept函数处理
  },
};
//入口文件,使用module.hot.accept函数处理js热更新,实际开发中应使用loader来解决js热更新问题,如:vue-loader、react-hot-loader
if (module.hot) {
  module.hot.accept('./library.js', function() {
    // 对更新过的 library 模块做些事情...
  });
}

多进程多文件打包

  • webpack 可以利用电脑多cpu的特点,开启多进程进行打包
  • 每个 进行worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右,同时会限制跨进程的数据交换,文件较少时反而降低打包速度,请仅在耗时的操作中使用
  • 在很多loader或plugin中都有多进程配置选项,如:
    • thread-loader
    • TerserWebpackPlugin
    • ESLintPlugin

tree shaking

  • 移除js中没有使用的代码
  • 使用es module模块化的代码才有效
  • webpack5生产环境下已经开启了tree shaking

代码分隔 code split

  • 多入口多输出
  • 使用Optimization.splitChunks配置,会根据你选择的 mode 来执行不同的优化
    • 公共模块单独打包
    • 源代码一般称Chunks,打包后的代码一般称bundle
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',//all async initial
      //其他都用默认配置就行
      //使用all后,所有的modules中的第三方插件和使用了动态引入语法的js都会被打包成单独文件
      // cacheGroups:{}//自定义打包分隔规则
    },
    // runtimeChunk: {//把文件之间的依赖关系,生成单独文件进行管理,某个文件变化时只更新当前文件和依赖关系中的文件名
    //   name: (entrypoint) => `runtime~${entrypoint.name}`,
    // },
    // minimize:true//是否需要压缩
    // minimizer:[]//自定义压缩覆盖默认压缩工具(minimizer)。
  },
};

按需加载

  • 动态导入
document.body.onclick = function(){
  //webpack遇到动态导入的语法时,会按需加载,比如这里会把count.js单独打包,且只有在点击时才加载到页面。vue中路由的按需加载就是基于这个原理做的
  import(/* webpackChunkName: 'count' */'./count.js').then(res=>{
    console.log(res.default)
    // /* webpackChunkName: 'count' */ webpack魔法命名,需要配合webpack配置中output.chunkFilename才有效
  }).catch(err=>{
    console.log(err)
  })
}
//eslint默认不支持import动态引入语法,需要在.eslintrc.js配置文件中添加以下配置
{
  // ...
  plugins: ['import ']
  //...
}

预加载

  • Preload 是一种浏览器机制,告诉浏览器立即加载资源,只加载当前页面修养的资源,兼容性较好
  • Prefetch 是一种浏览器机制,告诉浏览器在空闲时才开始加载资源,可以加载其他页面需要的资源,兼容性较差
  • webpack常用插件preload-webpack-plugin
{
  plugins: [
    new HtmlWebpackPlugin(),
    new PreloadWebpackPlugin({
      rel: 'preload',
      as: 'script'
    })// 打包后的加载标签 <link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">

    // new PreloadWebpackPlugin({
    //   rel: 'prefetch',
    // })// 打包后的加载标签 <link rel="prefetch" href="chunk.31132ae6680e598f8879.js">
  ]
}

core.js

  • core-js 它是JavaScript标准库的 polyfill(垫片/补丁),将新功能的es'api'转换为大部分现代浏览器都可以支持 运行的一个'api'补丁包集合。
// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",//按需加载,自动引入补丁
        "corejs": "3.22"//指定corejs版本
      }
    ]
    // 'react-app' //babel-reset-react-app官方预设处理 
  ]
}

loader推荐

  • postcss-loader
    • 预处理css,解决css兼容问题
  • thread-loader
    • 每个 进行worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。请仅在耗时的操作中使用此 loader!
  • vue-loader
    • 将解析vue文件,提取每个语言块,如有必要,将它们通过其他加载器进行管道传输,最后将它们组装回ES 模块,其默认导出为 Vue.js 组件选项对象
  • vue-template-compiler
    • 编译vue模板的包,传入模板返回AST抽象语法树。
  • vue-style-loader
    • 处理vue文件中的样式

插件推荐

  • MiniCssExtractPlugin
    • 本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。提高加载效率
  • CssMinimizerWebpackPlugin
    • 这个插件使用 cssnano 优化和压缩 CSS。
    • 就像 optimize-css-assets-webpack-plugin 一样,但在 source maps 和 assets 中使用查询字符串会更加准确,支持缓存和并发模式下运行
    • html和js在生产模式下默认就已经压缩了,不再需要额外配置
  • TerserWebpackPlugin
    • 该插件使用 terser 来压缩 JavaScript。
  • ImageMinimizerWebpackPlugin
    • 图片压缩插件
  • CopyWebpackPlugin
    • 静态资源复制
  • DefinePlugin
    • webpack内置插件
    • 允许在 编译时 将你代码中的变量替换为其他值或表达式。就是定义全局常量

PWA

  • PWA 全称Progressive Web Apps(渐进式Web应用程序),旨在使用现有的web技术提供用户更优的使用体验,即使在离线的情况下也能正常访问页面。PWA背后不是一种新的技术,而是集合当前多种web技术的一种集合。分别利用各自的功能来完成渐进式的整体需求。
  • webpack中的渐进式网络应用程序
    • 添加 Workbox:npm install workbox-webpack-plugin --save-dev
//webpack.config.js添加配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
    print: './src/print.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Output Management',
      title: 'Progressive Web Application',
    }),
    new WorkboxPlugin.GenerateSW({
      // 这些选项帮助快速启用 ServiceWorkers
      // 不允许遗留任何“旧的” ServiceWorkers
      clientsClaim: true,
      skipWaiting: true,
    }),
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
};

//入口文件 注册 Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js').then(registration => {
      console.log('SW registered: ', registration);
    }).catch(registrationError => {
      console.log('SW registration failed: ', registrationError);
    });
  });
}

loader

  • loader就是一个函数,webpack在解析时,会调用配置相关的loader,并把文件内容作为参数传给loader函数,loader函数对内容处理后返回新内容,用于处理webpack默认不能识别的文件类型(如:css)
  • loader参数
    • function(content, map, meta){}
    • content
      • 文件内容
    • map
      • source map
    • meta
      • 其他loader传递的参数
  • 种类
    • pre前置,normal普通,inline内联,post后置
  • 所有的loader的执行顺序都有两个阶段:pitching和normal阶段,类似于js中的事件冒泡、捕获阶段
    • Pitching阶段: post > inline > normal > pre
    • Normal阶段:pre > normal > inline > post
  • 同一阶段,相同的loader执行顺序
    • 从右到左,从下到上
// 使用inline内联loader,多个loader用!分隔
import Styles from 'style-loader!css-loader?modules!./styles.css'

//内联loader三种前缀的使用
//! 跳过normal 
import Styles from '!style-loader!css-loader?modules!./styles.css' 
//-! 跳过 pre 和 normal loader
import Styles from '-!style-loader!css-loader?modules!./styles.css' 
//!! 跳过 pre、 normal 和 post loader
import Styles from '!!style-loader!css-loader?modules!./styles.css' 
  • 定义loader的方式
//同步方式
module.exports = function(content, map, meta){
  return content;//直接返回
}
module.exports = function(content, map, meta){
  this.callback(null, content, map, meta);//调用callback交给给下一个loader继续处理;第一个参数可以传错误信息
}
//异步方式
module.exports = function(content, map, meta){
  const callback = this.async();
  settimeout(()=>{
    callback(null, content, map, meta)
  },1000)
}
//Raw loader 处理文件、图片、图标等数据
module.exports = function(content, map, meta){
  return content;//得到的content是Buffer数据
}
module.exports.raw = true
//Pitching loader
module.exports = function(content, map, meta){
  return content;
}
module.exports.pitch = function(){
  console.log('pitch')//pitch函数会优先执行,所有loader等pich函数执行完后才执行普通loader函数
  // return 'res'//如果有返回值,则后续loader函数包括自身的普通函数将都不再执行,称为熔断机制
}

loader 常用api

  • this.async()
    • 告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。
  • this.callback()
    • 交给给下一个loader继续处理
  • this.getOptions(schema)
    • 提取给定的 loader 选项,接受一个可选的 JSON schema 验证规则作为参数
    • JSON Schema指的是数据交换中的一种虚拟的“合同”。
      • 约束JSON对象的规则,如:定义可以使用哪些关键字,类型是什么,是否可以自定义内容
  • this.emitFile()
    • 向外发送一个文件
  • this.utils
    • this.utils.contextify(): 返回一个相对路径
    • this.utils.absolutify(): 返回一个绝对路径

plugins 插件

  • 用于扩展webpack,加入自定义的构建行为
  • webpack在构建时,会触发一系列的钩子函数,我们可以在这些钩子函数中注册自定义事件,实现特定的功能
  • 生命周期
/**
 * webpack
 * compiler
 * compiler.run()
 * compiler.compilation()
 * compiler.make() -> compilation
 *                   compilation.buildModule()
 *                   compilation.seal()
 *                   compilation.optimize()
 *                   compilation.reviveChunks()
 *                   compilation.unseal()
 * compiler.afterCompile()
 * compiler.emit()
 * compiler.emitAssets()//最后将资源输出出去
 */
  • 在 webpack 中的许多钩子对象都扩展自 Tapable 类。
    • tapable 是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。
    • tapable 暴露了三个方法用于注册不同类型的行为
      • tap
      • tapAsync
      • tapPromise
  • compiler 钩子
    • Compiler 模块是 webpack 的主要引擎,保存了webpack所有的环境配置,compilation 实例。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。
    • 当webpack打包时,首先会创建Compiler实例对象,而且仅创建一个
  • compilation 钩子
    • Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。
    • 一个compilation对象代表一个资源的构建,它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
/**
 * webpack加载配置时,在使用插件时通常会实例化对象,如:new TestPlugin()
 * webpack编译前会先创建Compiler实例对象
 * 在遍历插件配置时,会调用插件的apply方法
 * 接着触发钩子函数
 */
class TestPlugin {
  constructor(){}
  apply(compiler){
    // cnosole.log(compiler)
    compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
      console.log(entry)
    });
  }
}
module.exports = TestPlugin
  • nodejs调试webpack
    • node:通过node启动
    • inspect:调试模式
    • brk:在首行打一个断点,调试代码会在首行停住
    • ./node_modules/webpack/bin/webpack.js:调试对象及其路径
// 在xxx.js中设置 debugger,当点击下一步,会在这里停住
class TestPlugin {
  constructor(){}
  apply(compiler){
    debugger;
    cnosole.log(compiler)
  }
}
// packge.json
{
  "script": {
    "debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  }
}
// 运行指令:npm debug
// 此时在浏览器(如:webpack官网)F12可以查看到控制台多个node绿色按钮
// 点击绿色按钮,进入到调试工具,前面指令设置了brk,这里代码停留在第一行
// 点击下一步,代码会停留在debugger处

推荐文章