[webpack]知识点总结

293 阅读5分钟

本文记录了关于webpack的一些知识点

webpack的核心定义

一个模块打包器(能识别ES Moudule和CommonJS规范和其他如css等格式的文件)

提高构建和打包效率

  • 让node和webpack版本最新
  • 使用happypack利用CPU多核

package.json文件

 {
   "private": true, // 私有项目, 不会被发布到npm线上仓库
   "main": "index.js", // 被外部引用的js文件
 }

npm script

 $ npm init -y # 用默认配置初始项目
 $ npm info webpack # 查看包的历史版本号
 $ npx webpack -v # 使用nodemodules里的包

entry config

 entry: './src/index.js'
 // 其实是如下的简写
 entry: {
   main: './src/index.js',
   sub: './src/index.js',
 }

output config

// https://webpack.js.org/guides/output-management
output: {
  publicPath: 'http://cdn.com.cn',
  // name会对应 main 和 sub
  filename: '[name].[hash].bundle.js',
  path: path.resolve(__dirname, 'dist'),
  chunkFilename: '[name].[chunkhash].chunk.js'
}

// 对不同的entry, 打包的output文件名称不一样, 如有的加hash, 有的不需要hash
// https://webpack.js.org/configuration/output/#outputfilename
module.exports = {
  //...
  output: {
    filename: (chunkData) => {
      return chunkData.chunk.name === 'main' ? '[name].js': '[name]/[name].js'
    },
  }
}

mode config

 mode: 'development' // 代码不会被压缩, 默认为 production, 代码会被压缩

loader 非js模块的打包

module: {
  rules: [
    {
      test: /\.(jpg|png|gif)$/,
      use: {
        // 会把图片转成base64, 如果不配置limit会直接放到js里
        loader: 'url-loader',
        options: {
          // placeholder 占位符语法
          name: '[name]_[hash].[ext]',
          outputPath: 'images/',
          // limit: 1024 // file-loader没有这个配置项
        }
      }
    },
    {
      // 字体文件
      test: /\.(eot|ttf|svg)$/,
      use: {
        loader: 'file-loader',
      }
    },
    {
      test: /\.css$/,
      use: [
        'style-loader', // 将css内容挂载到head
        'css-loader', // 分析css文件的引用关系
      ]
    },
    {
      test: /\.scss$/,
      // use 顺序, 从下到上, 从右到左
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            // 在scss文件里再@import scss文件时用后面两个loader
            importLoaders: 2,
            // 开启css的模块化打包, 不然样式都是全局的
            // import style from './index.scss'
            // img.classList.add(style.avatar)
            modules: true
          }
        },
        // https://webpack.js.org/loaders/sass-loader
        // npm install sass-loader node-sass webpack --save-dev
        'sass-loader',
        // https://webpack.js.org/loaders/postcss-loader
        'postcss-loader' // 自动添加厂商前缀等
      ]
    }
  ]
}

plugins 可以在webpack运行到某个时刻, 做一些事情

plugins: [
  // 打包结束后, 挂载js到模板
  // npm install --save-dev html-webpack-plugin
  // 对于多个入口需要挂载到多个模板上, 可以new 多个HtmlWebpackPlugin
  new HtmlWebpackPlugin({
    template: '../src/index.html'
  }),
  // npm i clean-webpack-plugin -D
  new CleanWebpackPlugin([
    'dist'
  ],{
    root: path.resolve(__dirname, '../')
  }),
  new webpack.HotModuleReplacementPlugin()
]

postcss

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

sourcemap 源码映射

// https://webpack.js.org/configuration/devtool#devtool
devtool: 'none' // 关闭sourcemap
devtool: 'source-map' // 会生成一个.map文件
devtool: 'inline-source-map' // .map文件会被打包到js文件里, 错误提示会精确到第几行第几列
devtool: 'cheap-inline-source-map' // 只精确到行, 不精确到列, 提示性能, 而且只会提示业务代码的错误, 不提示loader和第三方模块的错误
devtool: 'cheap-module-inline-source-map' // 提示loader和第三方模块的错误
devtool: 'eval' // 用js eval效率最高, 提示不全面
devtool: 'cheap-module-eval-source-map' // 开发时的最佳实践
devtool: 'cheap-module-source-map' // 生产时的最佳实践

webpack devServer

// https://webpack.js.org/configuration/dev-server
// npm i webpack-dev-server -D
// 会将打包的内容放到内存
devServer: {
  contentBase: './dist', // 服务器根路径
  open: true, // 自动打开浏览器
  hot: true, // 开启 HMR
  hotOnly: true, // 即使HMR不生效, 浏览器也不刷新
}

// package.json
"scripts": {
  "start": "webpack-dev-server"
}

// server.js
// npm i express webpack-dev-middleware -D
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const config = require('./webpack.config.js')
// https://webpack.js.org/api/node
// 在node中使用webpack
const complier = webpack(config)

const app = express()
app.use(webpackDevMiddleware(complier, {
  publicPath: config.output.publicPath
}))
app.listen(3000, () => {

})

HMR 页面无刷新更新视图

// https://webpack.js.org/guides/hot-module-replacement
// https://webpack.js.org/api/hot-module-replacement
// https://webpack.js.org/concepts/hot-module-replacement
import number from './number'

number()

// 如果有HMR
if (module.hot) {
  // 监测 ./number 模块
  module.hot.accept('./number', () => {
    number()
  })
}
// 以上代码已在 css-loader, vue-loader, babel-preset等里已做处理

babel

// https://babeljs.io/setup#installation
// npm install --save-dev babel-loader @babel/core
// npm i -D @babel/core@^7.0.0-0
// babel-loader 只是webpack和babel做通信的一个桥梁
// webpack并不会把es6语法翻译成es5语法
module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            targets: {
              chrome: '67' // 生产运行的环境
            },
            // babel集成了polyfill,
            // 根据业务代码按需做polyfill,
            // 而且不需要手动import '@babel/polyfill'
            useBuiltIns: 'usage',
          ]
        ]
      }
    }
  ]
}
// npm install @babel/preset-env --save-dev
// 这个模块把es6语法翻译成es5语法等
// .babelrc.json
{
  "presets": ["@babel/preset-env"]
}
// 为兼容更低版本的浏览器
// https://babeljs.io/docs/en/babel-polyfill
// npm install --save @babel/polyfill
import '@babel/polyfill'

// 打包组件库和类库时, 不污染全局
// https://babeljs.io/docs/en/babel-plugin-transform-runtime
// npm install --save-dev @babel/plugin-transform-runtime
// npm install --save @babel/runtime
// 可把options内容放到.babelrc
options: {
  // presets: [
  //   [
  //     '@babel/preset-env',
  //     targets: {
  //       chrome: '67' // 生产运行的环境
  //     },
  //     useBuiltIns: 'usage', // 根据业务代码按需做polyfill
  //   ]
  // ]
  'plugins': [
    '@babel/plugin-transform-runtime',
    {
      'corejs': 2, // npm install --save @babel/runtime-corejs2
      'helpers': true,
      'regenerator': true,
      'useESModules': false
    }
  ]
}

tree shaking 根据引入的按需打包, 摇晃掉模块里无用的部分, 根树没有关联的模块

// Tree Shaking只支持 ES Module(静态引入), 不支持Common JS(动态引入)
// development mode 默认没有tree shaking
// production mode 不需要这个optimization
optimization: {
  usedExports: true
}

// package.json
// 不然打包时会忽略 @babel/polly-fill, 因为其没有导出对象, 只在window上挂载了对象
"sideEffects": false, // false时对所有模块摇树
"sideEffects": [
  "@babel/polly-fill",
  "*.css" // 对css不摇树
  ],

mode

// package.json
"scripts": {
  "dev": "webpack-dev-server --config webpack.dev.js"
  "dev-build": "webpack --config webpack.dev.js"
  "build": "webpack --config webpack.prod.js"
}
// npm i webpack-merge -D
const merge = require('webpack-merge')
merge(commonConfig, devConfig)

code splitting 代码分割

  • 分割业务代码和库代码, 不然打包文件会很大, 首次访问加载时间会很长
  • 而且如果不分割, 修改业务代码后, 重新访问, 又全部得重新加载库代码
  • 两种分割方式: 配置 + 同步引入 与 异步引入(无需做任何配置)
function getComponent () {
  // jsonp引入
  // 动态的import, 实验性的语法
  // npm i babel-plugin-dynamic-import-webpack -D
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '*')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})

// .babelrc 动态引入
// npm i -D babel-plugin-dynamic-import-webpack
{
  plugins: ['dynamic-import-webpack']
}

magic comment

// webpack官方提供的动态引入插件
// npm i -D @babel/plugin-syntax-dynamic-import
{
  plugins: ['@babel/plugin-syntax-dynamic-import']
}

function getComponent () {
  return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '*')
    return element
  })
}

splitPlugin 配置

optimization: {
    // SplitChunksPlugin config
    // 如下是官方默认配置
    splitChunks: {
      // async 只对异步代码生效
      // all 对同步异步都做代码分割, 但是同步代码还需cacheGrops配置
      // initial 只对同步代码做分割
      chunks: 'async',
      // 如果引入的模块大于minSize才做代码分割
      minSize: 30000,
      // 对于大于maxsize的模块尝试进行二次代码分割
      maxSize: 0,
      // 打包后的文件至少有多少个chunk文件引入这个模块才进行代码分割
      minChunks: 1,
      // 同时加载的模块数量,
      // 在打包前5个库的时候会生成5个js文件,
      // 超过5个就不再做代码分割
      maxAsyncRequests: 5,
      // 入口文件做代码分割的最大文件数量
      maxInitialRequests: 3,
      // 自动命名定界符
      automaticNameDelimiter: '~',
      // 让cacheGroups里的filename生效
      name: true,
      // 缓存组, 把库文件先放到缓存里, 再根据test规则分组合并打包
      cacheGroups: {
        // vendors: false
        vendors: {
          // 如果是node_modules里面的文件, 就打包到vendors组里
          test: /[\\/]node_modules[\\/]/,
          // 分组时的优先级
          priority: -10
          // // 组文件的名字 vendors.js, 不然会是 vendors~main.js
          // filename: 'vendors.js'
        },
        // 被分割的代码的默认的配置, 没有test, 所有模块都符合要求
        default: {
          // 至少被引用了2次
          minChunks: 2,
          priority: -20,
          // 复用已被分割打包过了的模块
          reuseExistingChunk: true,
          // // 组的文件名
          // filename: 'common.js',
        }
      }
    }
  }

lazy loading

// 点击页面才会加载lodash代码
function getComponent () {
  // 懒加载并不是webpack里面的一个概念, 而是ES的import语法,
  // webpack能识别这种语法, 对import引入的模块做代码分割
  return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '*')
    return element
  })
}

async function getComponent () {
  const { default: _ } = await import(/* webpackChunckName: 'lodash' */'lodash')
  // 懒加载并不是webpack里面的一个概念, 而是ES的import语法,
  // webpack能识别这种语法, 对import引入的模块做代码分割
  const element = document.createElement('div')
  element.innerHTML = _.join(['a', 'b'], '*')
  return element
}



window.document.addEventListener('click', () => {
  getComponent().then(el => window.document.body.appendChild(el))
})

chunk

  • 每一个文件都是一个chunk

打包分析

# 生成stats.json文件
$ webpack --profile --json > stats.json

prefeching/preloading

实现第一次加载的时候就是最快的, webpack推荐交互的代码放到异步加载的模块里去写 prefeching/preloading可实现网页空闲时预先加载异步模块

// console > Sources > command + shift + P > Show Coverage > 录屏
// 输入 coverage 代码利用率

// click.js
export default funciton handleClick () {
  console.log('clicked')
}

// index.js
window.document.addEventListener('click', () => {
  // prefetch会等待核心代码加载完成, 页面空闲时去加载prefetch的文件
  // webpackPreload会和核心代码一起加载
  import(/* webpackPrefetch: true */'./click.js').then({ default: func } => func())
})

css代码分割

output: {
  publicPath: 'http://cdn.com.cn',
  // name会对应入口文件entry里的 main 和 sub
  filename: '[name].[hash].bundle.js',
  path: path.resolve(__dirname, 'dist'),
  // entry之外的, 被间接引入的模块走chunkFilename
  chunkFilename: '[name].[chunkhash].chunk.js'
}
$ npm install --save-dev mini-css-extract-plugin
// prodConfig
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader,
        'css-loader', // 分析css文件的引用关系
      ]
    },
    {
      test: /\.scss$/,
      // use 顺序, 从下到上, 从右到左
      use: [
        MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader',
          options: {
            // 在scss文件里再@import scss文件时用后面两个loader
            importLoaders: 2,
            // 开启css的模块化打包, 不然样式都是全局的
            // import style from './index.scss'
            // img.classList.add(style.avatar)
            modules: true
          }
        },
        // https://webpack.js.org/loaders/sass-loader
        // npm install sass-loader node-sass webpack --save-dev
        'sass-loader',
        // https://webpack.js.org/loaders/postcss-loader
        'postcss-loader' // 自动添加厂商前缀等
      ]
    }
  ]
}

optimization: {
  // https://github.com/NMFR/optimize-css-assets-webpack-plugin
  // npm install --save-dev optimize-css-assets-webpack-plugin
  // 压缩与合并css代码
  new OptimizeCSSAssetsPlugin({}),
  splitChunks: {
    cacheGroups: {
      // 把所有入口的css打包
      styles: {
        name: 'styles',
        test: /\.css$/,
        chunks: 'all',
        // 强制代码拆分, 不管minSize之类的设置
        enforce: true,
      },
      // https://webpack.js.org/plugins/mini-css-extract-plugin
      // mini-css-extract-plugin也依赖splitChunksPlugin
      // 将foo和bar的样式打包到不同的文件夹里面
      fooStyles: {
        name: 'foo',
        test: (m, c, entry = 'foo') =>
          m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
        chunks: 'all',
        enforce: true,
      },
      barStyles: {
        name: 'bar',
        test: (m, c, entry = 'bar') =>
          m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
        chunks: 'all',
        enforce: true,
      },
    },
  },
  // 用于摇树
  usedExports: true
}

plugins: [
  new MiniCssExtractPlugin({
    // filename 被html直接引用
    filename: '[name].css',
    // chunkFilename 被html间接引用
    chunkFilename: '[name].chunk.css'
  })
]

// package.json tree shaking时忽略css文件
{
  "sideEffects": [
    "*.css"
  ]
}

webpack与浏览器缓存

利用contenthash

// webpackConfig
// 关闭性能提示
performance: false

// 老版本, 避免每次打包, 即使没有更改内容, hash也不一样
// 业务逻辑与库代码之间的关联, 放在manifest里
// 默认manifest既存在main.js也存在vendors.js里
// 旧版webpack打包, 每次manifest会有差异
// 配置了runtimeChunk, 打包时, 会把manifest抽离到runtime.js里
optimization: {
  rutimeChunk: {
    name: 'runtime'
  }
}

output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js',
}

shimming

垫片, 兼容, 实现webpack原始实现不了的功能

plugins: [
  new webpack.ProvidePlugin({
    // 发现jQuery.ui.js模块里面用了$, 就会自动在这个模块里引入jQuery库
    $: 'jquery',
    // _join = lodash.join
    _join: ['lodash', 'join']
  })
]


use: [
  {
    loader: 'babel-loader'
  },
  // npm i -D imports-loader
  // 让模块中的this指向window
  {
    loader: 'imports-loader?this=>window'
  }
]

Library的打包

// 希望满足如下引入方式
import library from 'lib'
const lib = require('lib')
require(['lib'], function () {
})
<script src="lib.js"></script>
lib.math

output: {
  // 打包后的代码挂载到lib这全局变量上
  library: 'lib',
  // u universal 通用
  libraryTarget: 'umd'
}

output: {
  // 打包后的代码挂载到lib这全局变量上
  library: 'lib',
  // lib挂载在this上或者window, global上
  libraryTarget: 'this' // 或者 'window', 'global'
}

// 打包时忽略lodash库
externals: ['lodash']

externals: {
  lodash: 'lodash'
}

externals: {
  // 引入时为 const lodash = require('lodash')
  lodash: {
    commonjs: 'lodash'
  }
}

// package.json
{
  // 给别人使用时的入口
  "main": "./dist/index.js",
}

// npm add user
// npm publish

打包PWA

// npm i -D workbox-webpack-plugin
const WorkboxPlugin = require('workbox-webpack-plugin')
plugins: [
  // 利用service worker技术, 相当于一个另类的缓存
  // 打包后会多出来, service-worker.js和precache-manifest.js文件
  new WorkboxPlugin.GenerateSW({
    clientsClaim: true,
    skipWaiting: true
  })
]

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('service-worker registed')
      })
      .catch(err => {
        console.log('service-worker register error')
      })
  })
}

TypeScript项目配置

TS提高项目可维护性

// npm i -D ts-loader typescript
module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: 'ts-loader',
      exclude: /node_modules/,
    }
  ]
}

// tsconfig.json
{
  "compilerOptions": {
     // 配了output, 写不写都行
    "outDir": "./dist",
    // 引入模块时用 import
    "module": "es6",
    // 打包最终转换的形式
    "target": "es5",
    // 允许引入JS文件
    "allowJs": true,

  }
}

// 使用lodash需 npm i -D @types/lodash
// 安装类型定义文件
// https://github.com/DefinitelyTyped/DefinitelyTyped
// https://microsoft.github.io/TypeSearch/

import * as _ from 'lodash'

proxy

axios.get('/react/api/header.json')

devSever: {
  proxy: {
    '/react/api': {
      // 突破对origin的限制
      changeOrigin: true,
      // 转发https的网址
      secure: 'false',
      target: 'http://server.com',
      pathRewrite: {
        // 请求header.json时去请求demo.json
        'header.json': 'demo.json'
      }
    }
  }
}

处理本地js库

开发时引入本地js库

// webpack配置
devServer: {
    contentBase: path.join(__dirname, 'to-your-libs')
    // ...

生产时打包本地js库

$ npm i -D script-loader
// webpack配置
// module: {
    // rules: [
      // ...
        { 
          test: /\.exec\.js$/,
          use: [ 'script-loader' ]
        }
      // ...

// 在你的src文件中引入库
require('path-to-your-libs/lib.exec.js')