webpack构建工具

重要通知

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。

基本概况

> pnpm install --save-dev webpack
> webpack -v

命令行接口:https://www.webpackjs.com/api/cli/open in new window

> webpack # webpack指令默认执行 webpack --config webpack.config.js

package.json

{
  "scripts": {
    "dev": "",
    "serve": "",
    "build": "",
    "test": "",
    "lint": "",
  }
}

配置结构文件

webpack.config.js

const path = require('path');

module.exports = {
  /*
   * 入口(entry)
   */
  // 单个入口
  entry: '',
  // 对象语法
  entry: {},





  /*
   * 出口(output)
   */
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  // 多个入口起点
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    filename: '[name]-[chunkhash].js'
  },
  




  // 混用语法,从输出的 bundle 中排除依赖
  externals: [
    {
      jquery: 'jQuery',
      react: 'react',
      lodash: {
        commonjs: 'lodash',
        amd: 'lodash',
        root: '_', // 指向全局变量
      },
      subtract: {
        root: ['math', 'subtract'],
      },
    },
    // 函数
    function ({ context, request }, callback) {
      if (/^yourregex$/.test(request)) {
        return callback(null, 'commonjs ' + request);
      }
      callback();
    },
    // 正则表达式
    /^(jquery|\$)$/i,
  ],





  /*
   * 转换器(loader)
   */
  module: {},





  /*
   * 插件(plugins)
   */
  plugins: [],

  /*
   * 开发中 Server(devServer)
   */
  devServer: {
    contentBase: false,
    port: 8080,
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        pathRewrite: {"^/api" : ""}
      }
    }
  }
}

webpack.dev.config.js

const { merge } = require("webpack-merge");

module.exports = {
  mode: 'development',
}

webpack.prod.config.js

const { merge } = require("webpack-merge");

module.exports = {
  mode: 'production',
}

webpack.dll.config.js

webpack.vendor.config.js

公共变量与函数

// 开发环境
if (process.env.NODE_ENV === 'development') {
  //
}
// 生产环境
if (process.env.NODE_ENV === 'production') {
  //
}

动态链接库

模块功能loader与plugin

typescript模块

安装:pnpm install --save-dev typescript ts-loader

module.exports = {
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [ '.tsx', '.ts', '.js' ]
  },
} 

业务功能实现示例

CSS片段与文件

  • 抽离提取CSS并独立为CSS文件
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("styles.css")
  ]
}
  • 自动补全前缀autoprefixer

package.json

{
  "browserslist": [
    "> 1%",
    "last 5 versions",
    "last 10 Chrome versions",
    "last 5 Firefox versions",
    "Safari >= 6",
    "ie > 8"
  ]
}

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

js片段与文件

  • 编译ES6
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)|path.resolve(__dirname, "src")/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}
  • 分割chunk
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new webpack.optimize.AggressiveSplittingPlugin({
      minSize: 20, // 字节,分割点。默认:30720
      maxSize: 30, // 字节,每个文件最大字节。默认:51200
      chunkOverhead: 0, // 默认:0
      entryChunkMultiplicator: 1, // 默认:1
    }),
    
    // 展示出打包后的各个bundle所依赖的模块
    new BundleAnalyzerPlugin()
  ]
}
  • 压缩代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  module: {
    rules: [

    ]
  },
  plugins: [
    //ES6需要编译,否则会压缩异常报错
    new UglifyJsPlugin({
      test: /\.js($|\?)/i
    })
  ]
}
  • 拆分 bundles:DLLPlugin 和 DLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('../static/vendor-mainfest.json') // 指向这个json
    }),
    new webpack.DllPlugin({
      path: path.join(__dirname, '../static/', '[name]-mainfest.json'), // 描述依赖对应关系的json文件
      name: '[name]_library', 
      context: __dirname // 执行的上下文环境,对之后DllReferencePlugin有用
    })
  ]
}
  • 通过对 entry 文件进行处理,业务逻辑将等待所依赖的 externals 文件加载完成后再开始执行
const WaitExternalPlugin = require('wait-external-webpack-plugin');

module.exports = {
  externals: {},
  plugins: [
    new WaitExternalPlugin({
      test: /\.js$/,  // 正则匹配需要处理的 entry,默认对所有 entry 进行处理
    }),
  ]
}

HTML片段与文件

  • 单页面
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  plugins: [
    new htmlWebpackPlugin({
      filename: 'index.html',
      title: "WebPack打包机制",  //生成的HTML文档的标题
      template: path.join(__dirname, '/public/index.html'),  //模板HTML
      inject: 'body',
      favicon: path.join(__dirname, '../resource/images/logo.png'),
      xhtml: true,
      base: '',
      meta: {},
      minify: {
        caseSensitive: false,  //是否大小写敏感
        removeEmptyAttributes: true,  //去除空属性
        collapseWhitespace: true,  //去除空格
        removeComments: true,  //去掉注释
        hash: true,  //添加HASH值,避免缓存副作用 
        cache: true, //内容变化, 重新生成文件名
        removeComments: true, // 移除注释
        collapseWhitespace: true, // 移除空白格
        minifyCSS: true, // 压缩html内嵌样式
        minifyJS: true, // 压缩html内嵌js
      }
    });    
  ]
}
  • 多页面
const glob = require('glob');
const HtmlWebpackPlugin = require('html-webpack-plugin');

function htmlWebpackConfigs () {
  var configs = []
  glob.sync('./src/apps/**/*.html').forEach(htmlPath => {
    const chunk = htmlPath.split('./src/apps/')[1].split('/index.html')[0]
    const filename = chunk + '.html'
    const htmlConf = {
      filename: filename,
      template: htmlPath,
      chunks: ['vendors', chunk]
    }
    configs.push(new HtmlWebpackPlugin(htmlConf))
  })
  return configs
}

module.exports = {
  module: {
    rules: [

    ]
  },
  plugins: [
    ...htmlWebpackConfigs()
  ]
}

文件目录

const TransferWebpackPlugin = require('transfer-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  plugins: [
    // 从指定目录复制其下的所有文件与目录到指定目录
    new TransferWebpackPlugin(patterns: array, [basePath: string]),

    // 清除文件:例如在构建生产环境代码之前,清除dist目录
    new CleanWebpackPlugin(), 

    // 拷贝资源,包括子级目录的所有文件与文件夹
    new CopyWebpackPlugin([
      {
        from: './document',
        to: './'
      }
    ]),
  ]
}
  • 合并文件
const merge = require('webpack-merge');

const config = {};
const common = {};
merge(config, common);

图片模块

  • 压缩图片
const ImageminPlugin = require('imagemin-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192  //文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new ImageminPlugin({
      test: /\.(jpe?g|png|gif|svg)$/i
    })
  ]
}

模板编译

  • jade与pug
module.exports = {
  module: {
    rules: [
      // pnpm i -D jade jade-loader
      {
        test: /\.jade$/,
        loader: "jade"
      },
      // pnpm i -D pug pug-loader pug-filters
      {
        test: /\.pug$/,
        loader: 'pug'
      }
    ]
  },
}

变量管理

const webpack = require('webpack');

module.exports = {
  plugins: [
    // 创建一个在编译时可以配置的全局常量
    new webpack.DefinePlugin({
      PRODUCTION: JSON.stringify(true),
      VERSION: JSON.stringify("5fa3b9"),
    })
  ]
}

Tree Shaking

通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。

  • 实现原理
    • 在解析编译代码的过程中,通过维护一份模块依赖关系图数据,标记中代码中需要导入的模块与被引用的模块,然后使用TerserWebpackPlugin插件实现代码删除。

HMR模块热替换

HMR(Hot Module Replacement),即模块热替换,在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。

  • 实现机制
    • 保留在完全重新加载页面时丢失的应用程序状态。
    • 只更新变更内容,以节省宝贵的开发时间。
    • 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。
  • 工作原理
    • 应用程序
      • 应用程序代码要求 HMR runtime 检查更新。
      • HMR runtime(异步)下载更新,然后通知应用程序代码。
      • 应用程序代码要求 HMR runtime 应用更新。
      • HMR runtime(同步)应用更新。
    • 编译器:除了普通资源,编译器(compiler)需要发出 "update",以允许更新之前的版本到新的版本。
      • 更新后的 manifest(JSON):manifest 包括新的编译 hash 和所有的待更新 chunk 目录。每个更新 chunk 都含有对应于此 chunk 的全部更新模块(或一个 flag 用于表明此模块要被移除)的代码。
      • 一个或多个更新后的 chunk (JavaScript):编译器确保模块 ID 和 chunk ID 在这些构建之间保持一致。通常将这些 ID 存储在内存中(例如,使用 webpack-dev-server 时),但是也可能将它们存储在一个 JSON 文件中。
    • 模块:HMR 是可选功能,只会影响包含 HMR 代码的模块。举个例子,通过 style-loader 为 style 样式追加补丁。为了运行追加补丁,style-loader 实现了 HMR 接口;当它通过 HMR 接收到更新,它会使用新的样式替换旧的样式。
    • HMR Runtime:对于模块系统的 runtime,附加的代码被发送到 parents 和 children 跟踪模块。在管理方面,runtime 支持两个方法 check 和 apply。
      • check 发送 HTTP 请求来更新 manifest。如果请求失败,说明没有可用更新。如果请求成功,待更新 chunk 会和当前加载过的 chunk 进行比较。对每个加载过的 chunk,会下载相对应的待更新 chunk。当所有待更新 chunk 完成下载,就会准备切换到 ready 状态。
      • apply 方法将所有被更新模块标记为无效。对于每个无效模块,都需要在模块中有一个更新处理函数(update handler),或者在它的父级模块们中有更新处理函数。否则,无效标记冒泡,并也使父级无效。每个冒泡继续,直到到达应用程序入口起点,或者到达带有更新处理函数的模块(以最先到达为准,冒泡停止)。如果它从入口起点开始冒泡,则此过程失败。
      • 所有无效模块都被(通过 dispose 处理函数)处理和解除加载。然后更新当前 hash,并且调用所有 "accept" 处理函数。runtime 切换回闲置状态(idle state),一切照常继续。

Loader实现原理

loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用,一般loader用于处理非JS的其他资源,例如less、sass、image、css等。

  • 工具依赖包
    • loader-utils:提供了许多有用的工具
    • schema-utils:配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
  • 代码实现

loeader文件:exmaple.js

const { urlToRequest, getHashDigest, interpolateName, isUrlRequest } = require('loader-utils');
const { validate, ValidationError } = require('schema-utils');


module.exports = function(source) {
  const content = source.replace("console.log('", "console.log('\/n 日志:");
  return content;
}
  • 配置示例
module.exports = {
  rules: [
    // 自定义loader
    {
      test: /\.js$/,
      use:[{
        loader:path.resolve(path.resolve(__dirname, '../loaders/'), "example")
      }],
      exclude: /node_modules/
    },
  ]
}

plugin插件原理

插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。

  • 一个plugin插件的构成
    • 一个 JavaScript 命名函数。
    • 在插件函数的 prototype 上定义一个 apply 方法。
    • 指定一个绑定到 webpack 自身的事件钩子。
    • 处理 webpack 内部实例的特定数据。
    • 功能完成后调用 webpack 提供的回调。
  • compiler对象:代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation对象:代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
// 一个 JavaScript 命名函数
function exampleWebpackPlugin(options) {
  console.log('插件');
};

// 在插件函数的 prototype 上定义一个 `apply` 方法。
exampleWebpackPlugin.prototype.apply = function(compiler) {
  // 同步
  compiler.hooks.compile.tap('exampleWebpackPlugin', (compilation) => {
    console.log('compiler');
  });

  // 异步
  compiler.hooks.emit.tapAsync('exampleWebpackPlugin', (compilation, callback) => {

    // 添加文件资源,插入到 webpack 构建中
    compilation.assets['Readme.md'] = {
      source: function() {
        return '指引说明'
      },
      size: function() {
        return 21;
      }
    };

    //功能完成后调用 webpack 提供的回调。
    callback();
  })
};

module.exports = exampleWebpackPlugin;

缓存机制

构建性能提升

本指南包含一些改进构建/编译性能的实用技巧,无论你正在 development 或构建 production,以下做法应该帮助到你达到最佳。

  • Loaders配置优化:合理评估loaders的引入,以及对loader配置字段include的充分使用。
{
  test: /\.js$/,
  include: path.resolve(__dirname, "src"),
  loader: "babel-loader"
}
  • 解析优化
    • 尽量减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中类目的数量,因为他们会增加文件系统调用的次数。
    • 如果你不使用 symlinks ,可以设置 resolve.symlinks: false (例如 npm link 或者 yarn link)。
    • 如果你使用自定义解析 plugins ,并且没有指定 context 信息,可以设置 resolve.cacheWithContext: false。
  • Dlls单独编译
  • chunks代码优化
    • 使用 更少/更小 的库。
    • 在多页面应用程序中使用 SplitChunksPlugin。
splitChunks: {
  chunks: "async",
  minSize: 30000,
  minChunks: 1,
  maxAsyncRequests: 5,
  maxInitialRequests: 3,
  automaticNameDelimiter: '~',
  name: true,
  cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      priority: -10
    },
    default: {
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true
    }
  }
}
  • 在多页面应用程序中以 async 模式使用 SplitChunksPlugin。
  • 移除不使用的代码。
  • 只编译你当前正在开发部分的代码。
  • Worker Pool

thread-loader 可以将非常消耗资源的 loaders 转存到 worker pool 中。

  • 持久化缓存

使用 cache-loader 启用持久化缓存。使用 package.json 中的 "postinstall" 清除缓存目录。

  • chunks代码优化

tapable

Tapable 包暴露了许多 Hook 类,可以用来为插件创建钩子。

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");

第三方脚手架工具

create-react-app

vue-cli

自定义脚手架

const program = require('commander');  // 解析命令;
const chalk = require('chalk');  // 命令行界面输出美颜
const fs = require('fs-extra');  // fs的拓展;
const shell = require('shelljs');  // 重新包装了 child_process;
const inquirer = require('inquirer');  // 交互式问答;
const ora = require('ora');  // 输出样式美化;
const ejs = require('ejs');  // 模版引擎;

const download = require('download-git-repo')  //用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
const program = require('commander') //命令行处理工具
const exists = require('fs').existsSync  //node自带的fs模块下的existsSync方法,用于检测路径是否存在。(会阻塞)
const path = require('path') //node自带的path模块,用于拼接路径
const ora = require('ora') //用于命令行上的加载效果
const home = require('user-home')  //用于获取用户的根目录
const tildify = require('tildify') //将绝对路径转换成带波浪符的路径
const chalk = require('chalk')// 用于高亮终端打印出的信息
const inquirer = require('inquirer') //用于命令行与开发者交互
const rm = require('rimraf').sync // 相当于UNIX的“rm -rf”命令
const logger = require('../lib/logger') //自定义工具-用于日志打印
const generate = require('../lib/generate')  //自定义工具-用于基于模板构建项目
const checkVersion = require('../lib/check-version') //自定义工具-用于检测vue-cli版本的工具
const warnings = require('../lib/warnings') //自定义工具-用于模板的警告
const localPath = require('../lib/local-path') //自定义工具-用于路径的处理

const isLocalPath = localPath.isLocalPath  //判断是否是本地路径
const getTemplatePath = localPath.getTemplatePath  //获取本地模板的绝对路径

webpack体系源码

Last Updated:
Contributors: 709992523