分享 webpack学习记录,包括手写简单的loader、plugin

311 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

快速上手

  1. 安装
yarn add webpack webpack-cli --dev
  1. 打包
yarn webpack

Webpack 配置文件

// webpack.config.js
const path = require("path")

module.exports = {
    entry: "./src/main.js",//入口文件
    output:{
      filename:"bundle.js", //生成的 文件名
      path:path.join(__dirname,"output")  //输出的文件目录 必须是绝对路径
    } //输出文件
}

webpack工作模式

const path = require("path")

module.exports = {
    mode: "none",  //工作模式 development production none
    entry: "./src/main.js",//入口文件
    output:{
      filename:"bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist")  //输出的文件目录 必须是绝对路径
    } ,//输出文件

}

Webpack常用加载器(loader)

const path = require("path")

module.exports = {
    mode: "none",  //工作模式 development production none
    entry: "./src/main.js",//入口文件
    output:{
      filename:"bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist"),  //输出的文件目录 必须是绝对路径
      publicPath:"dist/"
    } ,//输出文件
    module: {
        rules: [
            {
                test:/.css$/,
                use: [
                    "style-loader",
                    "css-loader"
                ]
            },
            // {
            //     test:/.png$/,
            //     use:"file-loader"
            // }, //较大文件,单独提取存放,提高加载速度
            {
                test:/.png$/,
                use: {
                    loader: "url-loader",
                    options: {
                        limit: 10 * 1024  //10KB  限制大小之后超过限制的图片会默认调取file-loader所以也需要安装 file-loader
                    }
                }
            }//较小文件使用,减少请求次数  
        ]
    } 
}
  • 编译转换类
  1. css-loader
  • 文件加载类型
  1. file-loader
  • 代码检查类
  1. eslint-loader

Webpack与ES2015

  • Webpack只是打包工具不会编译ES6的代码可以通过 加载器来实现
  • 加载器可以用来编译转换代码
//babel-loader
{
               test:/.js$/,
               use: {
                   loader: "babel-loader",
                   options:{
                       presets: ["@babel/preset-env"]
                   }
               } 
},

Webpack 模块加载方式

  • 遵循ES Modules标准的import声明
  • 遵循CommonJS标准的require函数
  • 遵循AMD标准的define函数和requir函数
  • 部分Loader加载的资源中一些用法也会触发资源模块加载
  1. @import url()
  2. background-image:url()
  3. html中的src

Webpack开发一个Loader

markdown-loader

const marked = require("marked") //解析markdown语法
module.exports = source => {
    // console.log(source);
    // return "console.log('hello---')"
    const html = marked (source)
    // return `module.ecports = ${JSON.stringify(html)}`
    // return `export default  ${JSON.stringify(html)}`
    // 返回html 字符串交给下一个loader处理
    return html
}
{
    test:/.md$/,
    use:["html-loader","./markdown-loader.js"] //按照从后往前的处理
},

loder负责资源文件从输入到输出的转换,对于同一个资源可以依次使用多个loader

Webpack插件机制

  • 增强Webpack自动化能力
  1. eg. 清除dist目录
  2. eg. 拷贝静态文件至输出目录
  3. eg. 压缩输出代码
const path = require("path")
const { CleanWebpackPlugin } = require("clean-webpack-plugin") //清理dist目录
const HtmlWebpackPlugin = require("html-webpack-plugin")//自动生成html插件
const CopyWebpackPlugin = require("copy-webpack-plugin")
module.exports = {
    mode: "none",  //工作模式 development production none
    entry: "./src/main.js",//入口文件
    output:{
      filename:"bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist"),  //输出的文件目录 必须是绝对路径
    //   publicPath:"dist/"
    } ,//输出文件
    module: {
        rules: [
            {
              test:/.md$/,
              use:["html-loader","./markdown-loader.js"]
            },
            {
               test:/.js$/,
               use: {
                   loader: "babel-loader",
                   options:{
                       presets: ["@babel/preset-env"]
                   }
               } 
            },
            {
                test:/.css$/,
                use: [
                    "style-loader",
                    "css-loader"
                ]
            },
            // {
            //     test:/.png$/,
            //     use:"file-loader"
            // }, //较大文件,单独提取存放,提高加载速度
            {
                test:/.png$/,
                use: {
                    loader: "url-loader",
                    options: {
                        limit: 10 * 1024  //10KB  限制大小之后超过限制的图片会默认调取file-loader所以也需要安装 file-loader
                    }
                }
            }//较小文件使用,减少请求次数  
        ]
    },
    plugins:[
        // 用于清除dist目录
        new CleanWebpackPlugin(),
        // 用于生成index.html
        new HtmlWebpackPlugin({
            title: "Webpack Plugin Sample",
            meta: {
                viewport: "width-device-width"
            },
            template: "./src/index.html"
        }),
        // 用于生成about.html
        // new HtmlWebpackPlugin({
        //     filename:"about.html"
        // }),
        // 用于复制文件到dist
        new CopyWebpackPlugin({
            patterns:[
               {
                   from:"public",  // "public/**" 可以是目录 也可以使用通配符
                   to :"public"
               }
            ]
           
        })
    ] 
}

相比于Loader,plugin拥有更宽的能力范围,plugin通过钩子机制实现 要求插件是一个函数或者是一个包含apply方法的对象

Webpack 开发一个插件

const path = require("path")
const { CleanWebpackPlugin } = require("clean-webpack-plugin") //清理dist目录
const HtmlWebpackPlugin = require("html-webpack-plugin")//自动生成html插件
const CopyWebpackPlugin = require("copy-webpack-plugin")
class MyPlugin {
    apply(compiler){
       console.log("MyPlugin");
       compiler.hooks.emit.tap("MyPlugin", compilation => {
        //    可以理解为此次打包的上下文
        for( const name in compilation.assets ) {
           if(name.endsWith(".js")) {
               const contents = compilation.assets[name].source()
               const withoutComments = contents.replace(/\/\*\*+\*\//g,"")
               compilation.assets[name] = {
                   source: () => withoutComments,
                   size: () => withoutComments.length
               }
           }
        } 
       })
    }
}
module.exports = {
    mode: "none",  //工作模式 development production none
    entry: "./src/main.js",//入口文件
    output:{
      filename:"bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist"),  //输出的文件目录 必须是绝对路径
    //   publicPath:"dist/"
    } ,//输出文件
    module: {
        rules: [
            {
              test:/.md$/,
              use:["html-loader","./markdown-loader.js"]
            },
            {
               test:/.js$/,
               use: {
                   loader: "babel-loader",
                   options:{
                       presets: ["@babel/preset-env"]
                   }
               } 
            },
            {
                test:/.css$/,
                use: [
                    "style-loader",
                    "css-loader"
                ]
            },
            // {
            //     test:/.png$/,
            //     use:"file-loader"
            // }, //较大文件,单独提取存放,提高加载速度
            {
                test:/.png$/,
                use: {
                    loader: "url-loader",
                    options: {
                        limit: 10 * 1024  //10KB  限制大小之后超过限制的图片会默认调取file-loader所以也需要安装 file-loader
                    }
                }
            }//较小文件使用,减少请求次数  
        ]
    },
    plugins:[
        // 用于清除dist目录
        new CleanWebpackPlugin(),
        // 用于生成index.html
        new HtmlWebpackPlugin({
            title: "Webpack Plugin Sample",
            meta: {
                viewport: "width-device-width"
            },
            template: "./src/index.html"
        }),
        // 用于生成about.html
        // new HtmlWebpackPlugin({
        //     filename:"about.html"
        // }),
        // 用于复制文件到dist
        new CopyWebpackPlugin({
            patterns:[
               {
                   from:"public",  // "public/**" 可以是目录 也可以使用通配符
                   to :"public"
               }
            ]
           
        }),
        new MyPlugin()
    ] 
}

通过在生命周期的钩子中挂载函数实现扩展

Webpack开发体验问题

  • Webpack自动编译
yarn webpack --watch
  • Webpack编译过后 自动编译 自动刷新浏览器 Webpack Dev Server
yarn webpack-dev-serve --open
// 相关配置
devServer: {
    contentBase:"./public",
    proxy: {
        "/api" : {
            target:"https://api.github.com",  //http://localhost:8080/api/users -> https://api.github.com/api/users  将以api开头的接口 替换掉
            pathRewrite:{
                "^/api":"" //http://localhost:8080/api/users -> https://api.github.com/users  使用正则 替换
            },
            changeOrigin: true //不能使用localhost:8080作为请求 github的主机名
        }
    }
},

Source Map

源代码地图

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

Webpack 配置 Source Map

devtool:"source-map",

Webpack 配置 Source Map eval模式下的Source Map

速度最快 定位只能定位到文件不能定位到行列信息 eval 是否使用eval执行模块代码 cheap-Source Map 是否包含行信息 module 是否能够得到Loader处理之前的源代码

选择合适的Source Map

  • 开发模式 cheap-moudle-eval-source-map
  • 生产模式 none 不生成Source Map 会暴露源代码 或者 nosources-source-map

Webpack自动刷新的问题 HMR 介绍

页面不刷新的前提下,模块也可以及时更新 模块热替换

hot: true,  //devServer  开启热更新

处理JS模块热替换

js的热替换需要手动对应 不同的模块去单独的处理

// 热替换处理函数
module.hot.accept("模块文件名", () => {
    // 手动处理
})

处理图片的模块的热替换

// 热替换处理函数
module.hot.accept("模块文件名", () => {
    // 手动处理
    img.src = background
})

HMR注意事项

  1. 处理HMR的代码报错会导致自动刷新 配置中 修改为 hotOnly 为 true hot 为 true时 当热替换失败时会自动刷新页面
  2. 没启用HMR的情况下,HMRAPI报错 //使用时 先判断 module.hot是否存在
  3. 在代码中写了很多与业务功能无关的代码

不同环境下的配置

  1. 配置文件根据环境不同导出不同的配置 导出一个函数去判断
//  yarn webpack --env production
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = (env, argv) => {
  const config = {
    mode: 'development',
    entry: './src/main.js',
    output: {
      filename: 'js/bundle.js'
    },
    devtool: 'cheap-eval-module-source-map',
    devServer: {
      hot: true,
      contentBase: 'public'
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.(png|jpe?g|gif)$/,
          use: {
            loader: 'file-loader',
            options: {
              outputPath: 'img',
              name: '[name].[ext]'
            }
          }
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Webpack Tutorial',
        template: './src/index.html'
      }),
      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.common.js    yarn webpack --config webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            outputPath: 'img',
            name: '[name].[ext]'
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorial',
      template: './src/index.html'
    })
  ]
}


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

module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    hot: true,
    contentBase: 'public'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
})





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

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public'])
  ]
})

Webpack DefinePlugin

注入变量

new webpack.DefinePlugin({
    API_BASE_URL:"'https://api.example.com'"  //传入 的是JS的代码片段
})  

webpack 体验 Tree Shaking

前提 由webpack打包的代码必须使用ESM 摇掉代码中为引用的部分 未引用代码(dead-code) 去掉冗余的代码

// 新版的babel-loader 不会失效  如果要确保 可以正常使用 tree shaking 可以配置
{
    test:/.js$/,
    use: {
        loader: "babel-loader",
        options:{
            presets: [
                ["@babel/preset-env",{ modules:false }] //确保是ESM方式 保证 tree shaking 能够正常运行
            ]
        }
    } 
},
optimization: {
        usedExports: true, //在输出结果中只导出外部使用了的成员  负责 标记
        minimize:true //未引用的代码 将会移除掉    负责去除
}

webpack 合并模块

optimization: {
        usedExports: true, //在输出结果中只导出外部使用了的成员  负责 标记
        minimize:true, //未引用的代码 将会移除掉    负责去除
        concatenateModules:true  //极可能的将所有的模块合并输出到一个函数中  提升运行效率,减少了代码的体积
}

webpack sideEffects 副作用

模块执行时除了导出成员之外所作的事情 一般用于npm包标记是否有副作用

optimization: {
    sideEffects:true, //开启这个功能    开启后 会检查当前代码所属的package.json中 有没有标识  假如有标识 没有用到的模块 就不会被打包
}

// package.json
"sideEffects": false //标识代码没有副作用
"sideEffects": [
    "./src/*.css" 
]  //写成数组将会忽略其中的路径

webpack 代码分割/分包 Code Splitting

  • 多入口打包 常应用于多页面程序 一个页面对应一个打包入口 公共部分单独提取
entry:{
    index:"./src/index.js",
    aldum:"./src/aldum.js"
}
output:{
    filename: "[name].bundle.js"
}
new HtmlWebpackPlugin({
    title: "Webpack Plugin Sample",
    meta: {
        viewport: "width-device-width"
    },
    filename:"index.html"
    template: "./src/index.html"
    chunks:['index']
}),
new HtmlWebpackPlugin({
    title: "Webpack Plugin Sample",
    meta: {
        viewport: "width-device-width"
    },
    filename:"aldum.html"
    template: "./src/aldum.html"
    chunks:['aldum']
}),
  • 提取公共模块 不同入口中肯定会有公共部分模块
optimization: {
    splitChunks: {
        chunks: "all"
    }
}
  • 动态导入 动态导入的模块会被自动分包
import("./posts/posts").then(({ default: posts }) => { //结构出default
     处理程序
}) 

webpack 魔法注释

动态导入的模块会被自动分包 魔法注释可以来命名 包名

/* webpackChunkName: "album" */

//假如设置的 打包名称相同 会打包到 同一文件夹

webpack MiniCssExtractPlugin

提取css到单个文件 css 样式超过 155kb 后 提取比较好

//  yarn add mini-css-extract-plugin --dev
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module:{
    rules:[
        {
            test:/.css$/,
            use: [
                // "style-loader",  将样式通过style标签注入
                MiniCssExtractPlugin.loader, 
                "css-loader"
            ]
        }
    ]
},
plugins: {
    new MiniCssExtractPlugin()
}

webpack OptimizeCssAssetsWebpackPlugin 压缩输出的css文件

//  yarn add optimize-css-assets-webpack-plugin --dev
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin")
plugins: {
    new OptimizeCssAssetsWebpackPlugin()
}
// 应该 放在minimizer   可由 minimize  来开启
// yarn add terser-webpack-plugin --dev
const TerserWebpackPlugin = require("terser-webpack-plugin")
 optimization: {
        usedExports: true, //在输出结果中只导出外部使用了的成员  负责 标记
        minimizer:[  //压缩代码的 插件 放在这里  在 minimize 为 true时 开启
            new OptimizeCssAssetsWebpackPlugin(),
            new TerserWebpackPlugin()   
        ]
        minimize:true, //未引用的代码 压缩代码时 将会移除掉    负责去除
        concatenateModules:true  //极可能的将所有的模块合并输出到一个函数中  提升运行效率,减少了代码的体积
 }

webpack 输出文件名 Hash

生产模式下,文件名使用Hash

// chunkhash 同一个chunk的hash文件名才会改变
output:{
      filename:"[name]-[chunkhash].bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist"),  //输出的文件目录 必须是绝对路径
    //   publicPath:"dist/"
}
plugins: {
    new OptimizeCssAssetsWebpackPlugin({
        filename:"[name]-[chunkhash].bundle.css"
    })
}
// hash  一个文件改动  所有的文件名 都会改变
output:{
      filename:"[name]-[hash].bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist"),  //输出的文件目录 必须是绝对路径
    //   publicPath:"dist/"
}
plugins: {
    new OptimizeCssAssetsWebpackPlugin({
        filename:"[name]-[hash].bundle.css"
    })
}
// contenthash  文件级别的hash 文件内容发生变化 更改hash[contenthash:8] 表示长度  默认是20位
output:{
      filename:"[name]-[contenthash].bundle.js", //生成的 文件名
      path:path.join(__dirname,"dist"),  //输出的文件目录 必须是绝对路径
    //   publicPath:"dist/"
}
plugins: {
    new OptimizeCssAssetsWebpackPlugin({
        filename:"[name]-[contenthash].bundle.css"
    })
}