前端学习-webpack

149 阅读7分钟

快速上手

  • yarn init --yes
    初始化项目

  • yarn add webpack webpack-cli --dev
    安装核心模块和cli

  • yarn webpack --version
    查看当前webpack版本

  • yarn webpack
    打包项目内的JS文件

配置文件

  • 默认打包路径为
    src/index.js -> dist/main.js

修改配置文件

//webpack.config.js

const path = require('path')

module.exports = {
  mode:'development',//修改工作模式,默认为production,其余可选参数为none
  entry: './src/main.js',//入口文件的路径和文件名
  output: {
    filename: 'bundle.js',//导出的文件名
    path: path.join(__dirname, 'output')//导出的路径,必须为绝对路径
  }
}

资源模块加载

  • yarn add css-loader --dev
    将css文件进行打包
  • yarn add style-loader --dev
    载入样式

修改配置文件

//webpack.config.js

module.exports = {
  //修改入口文件为css文件
  entry: './src/main.css',
  
  //添加module字段
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
          //loader执行顺序为从后往前执行
        ]
      }
    ]
  }
}

导入资源模块

在JS文件载入当前模块需要的样式文件更符合webpack的设计理念

import './heading.css

文件资源加载器

  • yarn add file-loader --dev
    安装资源加载器

修改配置文件

module.exports = {
  output: {
    //当资源文件不在网站的根目录时需要修改路径
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      //新增资源加载器
      {
        test: /.png$/,
        use: 'file-loader'
      }
    ]
  }
}
import icon from './icon.png'

const img = new Image()
img.src = icon

document.body.append(img)

webpack URL加载器

Data URLs

data:image/png;base64,iVBORw0KGgoAAAANSUhE...SuQmCC

//协议
data:

//媒体类型和编码
image/png;base64,

//文件内容
iVBORw0KGgoAAAANSUhE...SuQmCC!~
  • yarn add url-loader --dev
    安装URL加载器

建议小文件使用Data URLs,减少请求次数。 大文件单独提取存放,提高加载速度

const path = require('path')

module.exports = {  
  module: {
    rules: [      
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 将10 KB以下的文件采用Data URL的形式进行打包
          }
        }
      }
    ]
  }
}

常用加载器分类

  • 编译转换类
    将加载到的资源模块转换成JS代码,例如css loader
  • 文件操作类
    把加载到的资源文件复制到输出目录,把访问的文件路径向外导出
  • 代码检查类
    把加载到的资源文件进行校验,统一风格提高代码质量

webpack与ES2015

  • yarn add babel-loader @babel/core @babel/preset-env --dev
    安装babel加载器,核心文件,转换插件的集合
const path = require('path')

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

webpack加载资源的方式

  • 遵循 ES Modules 标准的 import 声明
  • 遵循 CommonJS 标准的 require 函数
  • 遵循 AMD 标准的 define 函数和 require 函数
  • 样式代码中的 @import 指令和 url 函数
@import url(reset.css);
  • HTML 代码中图片标签的 src 属性

yarn add html-loader --dev
安装html加载器

const path = require('path')

module.exports = {
  
  module: {
    rules: [
      {
        test: /.html$/,
        use: {
          loader: 'html-loader',
          options: {
            attrs: ['img:src', 'a:href']
          }
        }
      }
    ]
  }
}

webpack常用插件

clean-webpack-plugin

清空缓存文件

yarn add clean-webpack-plugin --dev

//webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {  
  plugins: [
    new CleanWebpackPlugin()
  ]
}

html-webpack-plugin

用于生成html文件

yarn add html-webpack-plugin --dev

可以用过多个实例对象来生成多个html文件

//webpack.config.js

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

module.exports = {  
  plugins: [
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // 用于生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    })
  ]
}

页面内也可以访问插件的配置参数

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Webpack</title>
</head>
<body>
  <div class="container">
    <h1><%= htmlWebpackPlugin.options.title %></h1>
  </div>
</body>
</html>

copy-webpack-plugin

复制文件

yarn add copy-webpack-plugin --dev

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

module.exports = {  
  plugins: [    
    new CopyWebpackPlugin([
      // 'public/**'
      'public'//指定需要复制的目录
    ])
  ]
}

增强webpack开发体验

自动编译

webpack的监视模式,修改内容后自动重新打包

yarn webpack --watch

自动刷新浏览器

browser-sync dist --files "**/*"

Webpack Dev Server

安装:

yarn add webpack-dev-server --dev

运行:

yarn webpack-dev-server

在后面添加open参数会唤起浏览器并实时刷新
yarn webpack-dev-server --open

webpack支持配置代理API

module.exports = {  
  devServer: {
    //指定需要额外加载的资源的路径
    contentBase: './public',
    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 作为请求 GitHub 的主机名
        changeOrigin: true
      }
    }
  }
}

Source Map

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

module.exports = {
  devtool: 'eval'
}
devtoolbuildrebuildproductionquality
(none)fastestfastestyesbundled code
evalfastestfastestnogenerated code
cheap-eval-source-mapfastfastestnotransformed code (lines only)
cheap-module-eval-source-mapslowfastestnooriginal source (lines only)
eval-source-mapslowestfastnooriginal source
cheap-source-mapfastslowyestransformed code (lines only)
cheap-module-source-mapslowsloweryesoriginal source (lines only)
inline-cheap-source-mapfastslownotransformed code (lines only)
inline-cheap-module-source-mapslowslowernooriginal source (lines only)
source-mapslowestslowestyesoriginal source
inline-source-mapslowestslowestnooriginal source
hidden-source-mapslowestslowestyesoriginal source
nosources-source-mapslowestslowestyeswithout source content

建议:

开发模式:cheap-module-eval-source-map

每行代码不超过80个字符
代码经过loader转换过后的差异较大
重新打包相对较快

生产模式:none / nosources-source-map

避免暴露源代码

MHR

Hot Module Replacement 模块热更新

开启的命令为

webpack-dev-server --hot

也可以通过配置文件开启

//载入webpack模块
const webpack = require('webpack')

module.exports = {  
  devServer: {
    hot: true
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading,在debug时建议开启
  },  
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

HMR API

自定义的JS模块需要手动调用MHR API来实现热更新

module.hot.accept('依赖模块的路径', () => {依赖路径更新过后的处理函数})
import createEditor from './editor'
import background from './better.png'
import './global.css'

const editor = createEditor()
document.body.appendChild(editor)

const img = new Image()
img.src = background
document.body.appendChild(img)

// ============ 以下用于处理 HMR,与业务代码无关 ============

// console.log(createEditor)

if (module.hot) {
  let lastEditor = editor
  module.hot.accept('./editor', () => {
    // console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
    // console.log(createEditor)

    const value = lastEditor.innerHTML
    document.body.removeChild(lastEditor)
    const newEditor = createEditor()
    newEditor.innerHTML = value
    document.body.appendChild(newEditor)
    lastEditor = newEditor
  })

  module.hot.accept('./better.png', () => {
    img.src = background
    console.log(background)
  })
}

  • 在编写HMR的代码情况下,建议只使用hotOnly:true,便于发现代码中的问题
  • 在编写HMR代码之前添加判断条件,确定当前项目是否开启了HMR
  • HMR不会被打包到最后编译完成的文件中去

生产环境优化

  • 配置文件根据环境不同导出不同配置
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 //禁用source map
    config.plugins = [ //添加一部分生产环境所需要的插件
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    ]
  }

  return config
}

  • 一个环境对应一个配置文件 提取公共的部分,其余的部分在单独文件里配置,将公共的部分合并过来

安装合并所需要的模块
yarn add webpack-merge --dev

打包时需要指定配置文件
yarn webpack --config webpack.prod.js

公共配置文件
webpack.common.js

// 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

//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
//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'])
  ]
})

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('https://api.example.com')
    })
  ]
}

编写代码时

console.log(API_BASE_URL)

打包完成后自动使用键值

console.log("https://api.example.com")

Tree Shaking

移除未使用的代码

//webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    // 压缩输出结果
    minimize: true
  }
}

Tree Shaking & Babel

Tree Shaking 前提是ES Modules,由webpack打包的代码必须使用ESM
为了转换代码中的ECMAScript的新特性,babel有可能会将ES Modules转换成CommonJS

最新版本的Babel并不会将代码转换成CommonJS形式

可以在配置文件中选择是否开启ESM转换

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }
    ]
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    // concatenateModules: true,
    // 压缩输出结果
    // minimize: true
  }
}

sideEffects

模块执行时除了导出成员之外所做的事情
一般用于npm包标记是否有副作用
当标记为没有副作用时,没有用到的模块就不会被打包

//webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    //生产环境下默认会开启
    sideEffects: true,
  }
}
//package.json
{
  "name": "31-side-effects",
  "version": "0.1.0",
  "main": "index.js",
  "author": "zce <w@zce.me> (https://zce.me)",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "css-loader": "^3.2.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.9"
  },
  "sideEffects": [
    //可以指定有副作用的文件,都没有副作用时候可以返回一个false布尔值
    "./src/extend.js",
    "*.css"
  ]
}

代码分割

多入口打包

适用于多页应用程序,一个页面对应一个打包入口,公共部分单独提取

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

module.exports = {
  mode: 'none',
  entry: {
    //利用对象的形式配置多个入口
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    //根据入口动态生成文件名
    filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      //通过chunks属性指定需要注入的bundle
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

提取公共的模块

module.exports = {  
  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  }
}

动态导入

需要用到某个模块时,再加载这个模块
动态导入的模块会被自动分包

const render = () => {
  const hash = window.location.hash || '#posts'
  const mainElement = document.querySelector('.main')
  mainElement.innerHTML = ''

  if (hash === '#posts') {
    //      导入模块的路径            解构
    import('./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    //     对分包产生的文件进行重命名
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)


提取CSS

可以利用插件将css提取到单独文件

MiniCssExtractPlugin

yarn add mini-css-extract-plugin --dev

压缩CSS和JS

OptimizeCssAssetsWebpackPlugin

yarn add optimize-css-assets-webpack-plugin --dev

TerserWebpackPlugin

yarn add terser-webpack-plugin --dev

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      //指定压缩的插件
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()//将CSS导出
  ]
}

输出文件名Hash

webpack支持在filename添加[hash]字段来给文件添加哈希值

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name]-[hash].bundle.js'
  }
}

支持的hash形式有3种

//只要项目中有任何改动所有文件的hash值都会发生变化
filename: '[name]-[hash].bundle.js'

//只有同一个chunk内的文件发生改变,自己的hash值才会变化
filename: '[name]-[chunkhash].bundle.js'

//只有当前文件发生变化hash值才会改变
filename: '[name]-[contenthash].bundle.js'


//可以设定hash值的长度
filename: '[name]-[contenthash:8].bundle.js'