Webpack

231 阅读5分钟

webpack将各类web资源作为模块来处理并根据模块间的依赖关系最终打包生成静态文件。

1.五个核心概念

  • Entry 入口文件
  • output 打包输出的文件位置以及命名
  • loader 增强webpack让其能处理更多给非JavaScript类型的文件(webpack本身只理解javascript
  • plugins webpack许多特性是通过插件的形式来实现的,因为这使webpack更加灵活
  • mode 分为developmentproduction,便于开发调试和代码优化上线

2.安装与编译打包

安装: 全局安装或作为项目开发依赖

npm install webpack webpack-cli -g npm install webpack webpack-cli -D

编译打包

  • 开发环境指令,功能包括能编译打包jsjson文件,并能将 es6模块化语法转换成浏览器能识别的语法
    • webpack src/js/index.js -o build/js/build.js --mode=development
  • 生产环境指令,开发环境的功能 + 压缩代码
    • webpack src/js/index.js -o build/js/built.js --mode=production

注:非 js / json 文件的编译打包以及将ES6基本语法转为ES5以下语法需要通过 loader 等配置来完成

3.配置文件 webpack.config.js

1.打包CSS文件和HTML文件

const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production', // or 'development'
  entry: './src/index.js',
  output: {
    filename: 'built.js',
    // __dirname nodejs的变量,代表当前文件的目录绝对路径
    path: resolve(__dirname, 'build')
  },
  // 配置各种loader,以针对处理各种不同类型的文件
  module: {
    rules: [
      {
        test: /\.css$/, // 正则匹配需要处理的文件
        // 使用哪些loader进行处理
        use: [
          // use数组中loader执行顺序:从右到左,从下到上 依次执行
          // 创建style标签,将js中的样式资源插入,添加到head中生效
          'style-loader',
          //将 @import and url() 编译为 import/require() 并解析它们
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: ['style-loader','css-loader','less-loader' // Compiles Less to CSS
        ]
      }
    ]
  },
  // plugins的配置
  plugins: [
    new HtmlWebpackPlugin({
      // 复制 './src/index.html' 文件,并自动引入打包输出的所有资源(JS/CSS)
      template: './src/index.html',
      minify:{  // 压缩html代码
        // 移除空格
        collapseWhitespace: true,
        // 移除注释
        removeComments: true
      }
    })
  ],
}

2.打包图片资源

module:{
    rules:[
      {
        // 问题:默认处理不了html中img图片
        // 处理图片资源
        test: /\.(jpg|png|gif)$/,
        // 下载 url-loader file-loader
        loader: 'url-loader',
        options: {
          // 图片大小小于8kb,就会被base64处理
          // 优点: 减少请求数量(减轻服务器压力)
          // 缺点:图片体积会更大(文件请求速度更慢)
          limit: 8 * 1024,
          // 问题:因为url-loader默认使用es6模块化解析,而html-loader引入图片是commonjs
          // 解析时会出问题:[object Module]
          // 解决:关闭url-loader的es6模块化,使用commonjs解析
          esModule: false,
          // 给图片进行重命名
          // [hash:10]取图片的hash的前10位
          // [ext]取文件原来扩展名
          name: '[hash:10].[ext]'
        }
      },
      {
        test: /\.html$/,
        // 处理html文件的img图片(负责引入img,从而能被url-loader进行处理)
        loader: 'html-loader'
      }
    ]
}

3.打包其它资源

module:{
    rules:[
        {
            exclude: /\.(css|js|html|less)/, // 排除这些文件类型之外的资源
            loader: 'file-loader',
            options:{
                name: '[hash:10].[ext]'
            }
        }
    ]
}

4.devServer

// 开发服务器:自动化(自动编译并打开浏览器,自动刷新浏览器[hotreload]),让代码运行起来
// 特点:只会在内存中编译打包,不会有任何输出[webpack 指令会将打包结果输出]
// 启动指令:npx webpack-dev-server
 module.export = {
    devServer: {
        contentBase: resolve(__dirname, 'build'), // 项目构建后路径
        compress: true, // gzip压缩
        port: 3000,
        open: true // 自动打开浏览器
    }
 }

5.生产环境配置

1.提取 css 为单独文件

//使用 plugin:mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 这个loader取代style-loader。作用:提取js中的css成单独文件
          MiniCssExtractPlugin.loader,
          // 将css文件整合到js文件中
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/built.css' // 对输出的css文件重命名
    })
  ]
};

2.css兼容处理

npm install --save-dev postcss-loader postcss-preset-env

/*
    css兼容性处理:postcss --> postcss-loader postcss-preset-env

    帮postcss找到package.json中browserslist里面的配置,通过配置加载指定的css兼容性样式

    "browserslist": {
      // 开发环境 --> 设置node环境变量:process.env.NODE_ENV = development
      "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ],
      // 生产环境:默认是看生产环境
      "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
      ]
    }
*/
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              plugins: () => [
                // postcss的插件
                require('postcss-preset-env')()
              ]
            }
          }
        ]
      }
    ]
  }
}

3.压缩css

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
    plugins:[
        new OptimizeCssAssetsWebpackPlugin()
    ]
}

4.js语法检查

npm install --save-dev eslint-loader eslint eslint-config-airbnb-base eslint-plugin-import

/*
    语法检查: eslint-loader  eslint
      注意:只检查自己写的源代码,第三方的库是不用检查的
      设置检查规则:
        package.json中eslintConfig中设置~
          "eslintConfig": {
            "extends": "airbnb-base"
          }
        airbnb --> eslint-config-airbnb-base  eslint-plugin-import eslint
*/
module.exports = {
    module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'eslint-loader',
            options: {
              fix: true // 自动修复eslint的错误
            }
          }
        ]
    }
}

5.js兼容处理

npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/polyfill core-js

/*
    js兼容性处理相关package:
    - babel-loader 
    - @babel/core:babel的核心功能;
    - @babel/cli:让 terminal 也能使用babel
    - @babel/preset-env: 基本js兼容性处,只转换基本语法,如promise高级语法不能转换
    - @babel/polyfill: 全部js兼容性处理,不太适用于解决部分兼容性问题,因为将所有兼容性代码全部引入,体积较大;
        - @babel/polyfill包含core-js 和 a custom regenerator runtime
        - 解决方案:若明确知道需要兼容哪些特性,可以直接从 core-js中按需加载
*/  
module.exports = {
    module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          // 预设:指示babel做怎么样的兼容性处理
          presets: [
            [
              '@babel/preset-env',
              {
                // 按需加载
                useBuiltIns: 'usage',
                // 指定core-js版本
                corejs: {
                  version: 3
                },
                // 指定兼容性做到哪个版本浏览器
                targets: {
                  chrome: '60',
                  firefox: '60',
                  ie: '9',
                  safari: '10',
                  edge: '17'
                }
              }
            ]
          ]
        }
      }
    ]
  }
}

6.js压缩

module.exports = {
    mode:"production" // 生产模式下会自动压缩js
}

7.总览

const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 定义nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production';

// 复用loader
const commonCssLoader = [
  MiniCssExtractPlugin.loader,
  'css-loader',
  {
    // 还需要在package.json中定义browserslist
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: () => [require('postcss-preset-env')()]
    }
  }
];

module.exports = {
  mode: 'production',
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [...commonCssLoader]
      },
      {
        test: /\.less$/,
        use: [...commonCssLoader, 'less-loader']
      },
      /*
        一个文件只能被一个loader处理。
        当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
          先执行eslint 再执行babel
      */
      {
        // 在package.json中eslintConfig --> airbnb
        test: /\.js$/,
        exclude: /node_modules/,
        enforce: 'pre', // 优先执行
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: [
            [
              '@babel/preset-env',
              {
                useBuiltIns: 'usage',
                corejs: {version: 3},
                targets: {
                  chrome: '60',
                  firefox: '50'
                }
              }
            ]
          ]
        }
      },
      {
        test: /\.(jpg|png|gif)/,
        loader: 'url-loader',
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          outputPath: 'imgs',
          esModule: false
        }
      },
      {
        test: /\.html$/,
        loader: 'html-loader'
      },
      {
        exclude: /\.(js|css|less|html|jpg|png|gif)/,
        loader: 'file-loader',
        options: {
          outputPath: 'media'
        }
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/built.css'
    }),
    new OptimizeCssAssetsWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    })
  ],
};

4.优化环境配置

  • 开发环境性能优化
    • 优化打包速度:HMR
    • 优化代码调试:source-map
  • 生产环境性能优化
    • 优化打包构建速度
      • oneOf
      • babel缓存
      • 多进程打包
      • externals
      • dll
    • 优化代码运行的性能
      • 缓存(hash-chunkhash-contenthash)
      • tree shaking
      • 懒加载/预加载
      • pwa

1.HMR & source-map

/*
  HMR: hot module replacement 模块热替换
  作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块)极大提升构建速度
      样式文件:可以使用HMR功能,因为style-loader内部实现了
      js文件:默认不能使用HMR功能 --> 需要修改js代码,添加支持HMR功能的代码
              注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。
      html文件: 默认不能使用HMR功能同时会导致问题:html文件不能热更新了 
        解决:修改entry入口,将html文件引入
*/
module.exports = {
    devtool:'eval-source-map',
    entry: ['./src/js/index.js', './src/index.html'],
    devServer: {
        contentBase: resolve(__dirname, 'build'),
        compress: true,
        port: 3000,
        open: true,
        // 开启HMR功能
        // 当修改了webpack配置,新配置要想生效,必须重启webpack
        hot: true
      }
}
/*
source-map: 提供一种源代码到构建后代码映射的技术,便于调试 (如果构建后代码出错了,通过映射可以追踪源代码错误)
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

source-map:外部
  错误代码准确信息 和 源代码的错误位置
inline-source-map:内联
  只生成一个内联source-map
  错误代码准确信息 和 源代码的错误位置
hidden-source-map:外部
  错误代码错误原因,但是没有错误位置
  不能追踪源代码错误,只能提示到构建后代码的错误位置
eval-source-map:内联
  每一个文件都生成对应的source-map,都在eval
  错误代码准确信息 和 源代码的错误位置
nosources-source-map:外部
  错误代码准确信息, 但是没有任何源代码信息
cheap-source-map:外部
  错误代码准确信息 和 源代码的错误位置 
  只能精确的行
cheap-module-source-map:外部
  错误代码准确信息 和 源代码的错误位置 
  module会将loader的source map加入

内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快

开发环境:速度快,调试更友好
  速度快(eval>inline>cheap>...)
    eval-cheap-souce-map
    eval-source-map
  调试更友好  
    souce-map
    cheap-module-souce-map
    cheap-souce-map
  --> eval-source-map  / eval-cheap-module-souce-map

生产环境:源代码要不要隐藏? 调试要不要更友好
  内联会让代码体积变大,所以在生产环境不用内联
  nosources-source-map 全部隐藏
  hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
  --> source-map / cheap-module-souce-map
*/

2.oneOf

module.exports = {
    module:{
        rules:[
            {
                // 以下loader只能匹配一个,不能同时有两个配置处理同类型文件
                oneOf:[ 
                  {
                    test: /\.css$/,
                    use: [...commonCssLoader]
                  },
                  {
                    test: /\.less$/,
                    use: [...commonCssLoader, 'less-loader']
                  },
                  {
                    test: /\.(jpg|png|gif)/,
                    loader: 'url-loader',
                    options: {
                      limit: 8 * 1024,
                      name: '[hash:10].[ext]',
                      outputPath: 'imgs',
                      esModule: false
                    }
                  }
                ]
            }
        ]
    }
}

3.缓存

/*
    1.babel缓存
      cacheDirectory: true
      --> 让第二次打包构建速度更快
    2.文件资源缓存
       - hash: 每次wepack构建时会生成一个唯一的hash值。
        问题: 因为js和css同时使用一个hash值。
        若重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
       - chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
        问题: js和css的hash值还是一样的
        因为css是在js中被引入的,所以同属于一个chunk
       - contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样    
    --> 让代码上线运行缓存更好使用
*/
module.exports = {
    output:{
        filename:'js/build.[contenthash:10].js',
        path:resolve(__dirname,'build')
    },
    module:{
        rules:[
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {
                  presets: [
                    [
                      '@babel/preset-env',
                      {
                        useBuiltIns: 'usage',
                        corejs: { version: 3 },
                        targets: {
                          chrome: '60',
                          firefox: '50'
                        }
                      }
                    ]
                  ],
                  // 开启babel缓存,第二次构建时,会读取之前的缓存
                  cacheDirectory: true
                }
            }
        ]
    }
}

4.Tree Shaking

/*
delete dead code,reduce size of code
有效前提:1.必须使用ES6模块化语法  2.开启production模式

在package.json中配置 
  "sideEffects": false 所有代码都没有副作用(都可以进行tree shaking)
    问题:可能会把css / @babel/polyfill (副作用)文件干掉
  "sideEffects": ["*.css", "*.less"]
*/

5.code split

module.exports = {
    // 1.多入口,每个入口最终都会对应打包一个bundle
    entry:{
        index:'./src/index.js',
        other:'./src/other.js'
    },
    // 2.optimization
    optimization:{
        splitChunks:{
            chunks:'all'
        }
    }

6.externals

module.exports = {
    //拒绝 jQ 被打包进来
    externals:{
        jquery:'jQuery'
    }
}

7.dll

/*
  使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包
    当你运行 webpack 时,默认查找 webpack.config.js 配置文件
    需求:需要运行 webpack.dll.js 文件
      --> webpack --config webpack.dll.js
*/
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
  entry: {
    // 最终打包生成的[name] --> jquery
    // ['jquery'] --> 要打包的库是jquery
    jquery: ['jquery'],
  },
  output: {
    filename: '[name].js',
    path: resolve(__dirname, 'dll'),
    library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins: [
    // 打包生成一个 manifest.json --> 提供和jquery映射
    new webpack.DllPlugin({
      name: '[name]_[hash]', // 映射库的暴露的内容名称
      path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
    })
  ],
  mode: 'production'
};