模块化开发【webpack】

前置演练

1.初始化

先准备一个file,npm init -y初始化,package.json中添加:

"devDependencies": {
  "webpack":"5.91.0",
  "webpack-cli": "5.1.4"
}

2.添加entry,思考问题:🚩为什么要打包构建:

  • 转换文件,以获得更好的兼容性(babel和core-js来polyfill);
  • 将众多资源(js、css、img、font)合并,按需使用;
  • 产物进行优化,代码压缩、代码丑化,减少文件体积和源码安全。

3.添加output

  • webpack.config.js添加output,并运行npm install
output:{
  filename:"bundle.js",
  path:__dirname + "/dist",
  clear:true,//每次打包清除旧的包
  environment:{}//打包时支持的环境特性
}
  • 在 package.json 中添加 "build":"webpack",运行npm run build,项目下生成了dist/bundle.js就是出口文件。
"scripts": {
  "build":"webpack"
},

4.添加module

  • module中是关于JS、CSS、图片、字体文件都需要加工的模块的不同规则

1️⃣ 【JS】module中的babel-loaderopen in new window,有两种配置方式:🚗

🚗 配置方式一:

 module:{
  rules:[{//针对于js的处理
    test:/\.js/,
    use:{
      loader:"babel-loader",//将babel和webpack进行桥接的加载器
      options:{presets:["@babel/preset-env","@babel/preset-typescript"]}}}]}

🚗 配置方式二:项目根目录下配置.babelrc:以下这些presets也需安装对应基本依赖

{ // babel presets是干嘛的?官网 => https://www.babeljs.cn/docs/presets
  // babel presets是一系列插件的集合预设🧭,这些插件用来转换 ES6/ES7/ES8 到 ES5
  "presets": [
    "@babel/preset-env",//转换plugins的封装
    "@babel/preset-typescript"//转换ts的处理
]}
"devDependencies": {
    ...
    "babel-loader": "9.1.3",
    "@babel/core": "7.24.3",
    "@babel/preset-env": "7.24.3",
    "@babel/preset-typescript": "7.24.1"
  }

2️⃣ 【CSS】module中 css-loader 和 mini-css-extract-pluginopen in new window的结合使用,打包产物让css和js分离。

使用场景:需要单独提取css样式,开发的是通用模块,实现css高度复用。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [new MiniCssExtractPlugin()],
  module: {
    rules: [{ 
      test: /\.css$/i,
      use: [MiniCssExtractPlugin.loader, "css-loader"]}
]}}
"devDependencies": {
    ...
    "css-loader": "6.10.0",
    "style-loader": "3.3.4",
    "mini-css-extract-plugin": "2.8.1",
  }

5.mode:webpack5必须指定mode,否则打包会报错

6.optimization的splitChunks优化

OptimizationWebpackopen in new window根据选择的模式运行优化,但所有优化都可用于手动配置和覆盖。splitChunks是其中之一。

output:{
  filename: "[name].[contenthash].js"
  // 除了contenthash,还有 hash、chunkhash
},
 optimization: {
    splitChunks: {
      minChunks: 2,
      maxSize: 20 * 1024,
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          // cacheGroupKey here is `commons` as the key of the cacheGroup
          name(module, chunks, cacheGroupKey) {
            console.log("🚀 ~ name ~ chunks:", chunks);
            const moduleFileName = module
              .identifier()
              .split("/")
              .reduceRight((item) => item);
            const allChunksNames = chunks.map((item) => item.name).join("~");
            return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
          },
          chunks: "all",
        },
        defaultVendors: {
          test: /vender/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
  }}}}

🚩chunks被切分得却多就越好吗?其实不是,切分得越多,打包产物文件越多,请求就越多;分割chunk,需要考虑文件大小和请求数等综合情况。

webpack 核心配置

1.Entry

🛴入口,webpack执行构建的第一步将从此开始。

关于以下内容 并不是所有的都属于entry对象里的内容,而是和其相关联的部分

  • context:webpack在寻找相对路径的文件时会以 context 为根目录,context 默认为执行启动 webpack 时所在的当前的工作目录。若想改变 context 的默认配置,可设置:

    module.exports = {
      context: path.resolve(__dirname, 'app')
    }
    //Entry 的路径和其依赖的模块的路径可能采用相对于 context 的路径来描述,
    //context 会影响到这些相对路径所指向的真实文件。
    
  • entry:🚩必填项,webpack执行构建的第一步将从入口开始搜寻及递归解析所有入口依赖的模块:(也可以解释vue项目中为什么main.js为入口文件,引入了所有依赖)

    // 入口模块的文件路径
    // 单个文件也可 以字符串形式展现'xx';
    // 数组['xx','xx'...],则搭配 output.library 配置项使用时,只有🚩数组里的最后一个入口文件的模块会被导出;
    // 对象类型配置多个入口,每个入口生成一个chunk:
    entry:{ 
      main:"./src/index.ts",
      vendor:'./src/vendor.ts'
    },
    
  • Chunk名称:webpack 会为每一个生成的 Chunk 取一个名称,Chunk 的名称和 Entry 的配置有关:

    1. 若 entry 是 string/array,只会生成一个 chunk (打包后的产物),此时chunk的名称是 main;
    2. 若 entry 是 Object,会出现多个 chunk,此时chunk的名称是 Object 里键值对的键的名称。
  • 动态配置 Entry:针对项目中,多个页面需要为每个页面的入口配置一个 Entry,且这些页面可能不断增长的情况:

    // 同步函数
    entry: () => {
      return {
        a:'./pages/a',
        b:'./pages/b',
      }
    };
    // 异步函数
    entry: () => {
      return new Promise((resolve)=>{
        resolve({
          a:'./pages/a',
          b:'./pages/b',
        })
      })
    }
    

2.Output

  • filename:只输出一个,静态字符串类型;多个chunk要输出,借助模板和变量;
  • 若想打包产物为多个js,可在webpack.config.js配置entryoutput
  • output中的clean:true,让webpack每次打包前自动清理上一次打包的文件夹,clean-webpack-plugin有同样的效果,但这个插件支持更细粒度地控制清理行为。
output: {
  filename: "[name].[contenthash].js",
  // [name].js chunk的名称
  // id chunk的唯一标识 从0开始
  // hash 针对文件本身
  // contenthash 针对文件内容
  // chunkhash
  /*🚩
  hash 是全局性的,受到构建过程中所有文件的影响;
  chunkhash 是针对单个 chunk 内部文件的,只有发生变化的 chunk 的 chunkhash 会变化;
  contenthash 是针对单个文件内容的,只有文件内容发生变化时,对应的contenthash才会变化。
  */
 clean:true
},
entry: {
  main: "./src/index.ts",
  vendor: "./src/vendor.ts",
},
  • 还有chunkFilename、path、publicPath、crossOriginLoading、library/libraryTarget、libraryExport等基础配置,详情见官网open in new window

3.Module

🛴模块,webpack中一个模块对应一个文件,从Entry开始递归找出所有依赖的模块。

  • module 配置如何处理模块,module.rules配置模块的读取和解析规则,通常用来配置Loader

    1. 通过test、include、exclude三个配置项命中 Loader 要应用规则的文件;
    2. 应用规则:对选中的文件通过 use 配置项来应用 Loader,可以只应用一个 Loader 或者按照从后往前的顺序应用一组 Loader,同时还可以分别给 Loader 传入参数;
    3. 重置顺序:一组 Loader 的执行顺序是从右到左执行,通过 enforce 选项可以让其中一个 Loader 的执行顺序放到最前或最后
    module: {
      rules: [
        {
          // 命中 JavaScript 文件
          test: /\.js$/,
          // 用 babel-loader 转换 JavaScript 文件
          // ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度
          use: ['babel-loader?cacheDirectory'],
          // 只命中src目录里的js文件,加快 Webpack 搜索速度
          include: path.resolve(__dirname, 'src')
        },
        {
          // 命中 SCSS 文件
          test: /\.scss$/,
          // 使用一组 Loader 去处理 SCSS 文件。
          // 处理顺序为从后到前,即先交给 sass-loader 处理,再把结果交给 css-loader 最后再给 style-loader。
          use: ['style-loader', 'css-loader', 'sass-loader'],
          // 排除 node_modules 目录下的文件
          exclude: path.resolve(__dirname, 'node_modules'),
        },
        {
          // 对非文本文件采用 file-loader 加载
          test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
          use: ['file-loader'],
        },
      ]
    }
    
    1. 在 Loader 需要传入很多参数时,你还可以通过一个 Object 来描述,例如在上面的 babel-loader 配置中有如下代码:
    use: [
      {
        loader:'babel-loader',
        options:{
          cacheDirectory:true,
        },
        // enforce:'post' 的含义是把该 Loader 的执行顺序放到最后
        // enforce 的值还可以是 pre,代表把 Loader 的执行顺序放到最前面
        enforce:'post'
      },
      // 省略其它 Loader
    ]
    
    1. 上面的例子中 test include exclude 这三个命中文件的配置项只传入了一个字符串或正则,其实它们还都支持数组类型,使用如下:
    {
      test:[
        /\.jsx?$/,
        /\.tsx?$/
      ],
      include:[
        path.resolve(__dirname, 'src'),
        path.resolve(__dirname, 'tests'),
      ],
      exclude:[
        path.resolve(__dirname, 'node_modules'),
        path.resolve(__dirname, 'bower_modules'),
      ]
    }
    
  • module.noParse:webpack的性能优化,让webpack忽略 对部分没采用 模块化的文件的 递归解析和处理,提高构建性能。如jQuery、ChartJs库,它们庞大又没有采用模块化标准,让webpack去解析耗时又没有意义。🌸

    module:{
      // noParse 是可选配置项,类型需要是 RegExp、[RegExp]、function 其中一个
      // 1.使用正则表达式
      noParse: /jquery|chartjs/
      // 2.使用函数,从 Webpack 3.0.0 开始支持
      noParse: (content)=> {
        // content 代表一个模块的文件路径
        // 返回 true or false
        return /jquery|chartjs/.test(content);
      }
    }
    

    tips:被忽略的文件里不应该包含importrequiredefine等模块化语句,否则会导致构建出来的代码中包含无法在浏览器环境下执行的模块化语句。

  • module.parser:由于webpack是以模块化的js文件为入口,所以内置了对模块化js的解析功能,支持 AMD、CommonJS、SystemJS、ES6.parser可以更细粒度的配置哪些模块语法是否需要解析。和noParse的区别在于,parser可以精确到语法层面,noParse只能控制哪些文件不被解析。

    module: {
      rules: [
        {
          test: /\.js$/,
          use: ['babel-loader'],
          parser: {
            amd: false, // 禁用 AMD
            commonjs: false, // 禁用 CommonJS
            system: false, // 禁用 SystemJS
            harmony: false, // 禁用 ES6 import/export
            requireInclude: false, // 禁用 require.include
            requireEnsure: false, // 禁用 require.ensure
            requireContext: false, // 禁用 require.context
            browserify: false, // 禁用 browserify
            requireJs: false, // 禁用 requirejs
          }
        }
    ]}
    

4.Plugin

🛴扩展插件,Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

5.Loader

🛴模块转化器,用于把模块原内容按照需求转化成新内容。

6.Chunk

🛴代码块,一个Chunk由多个模块组合而成,用于代码合并和分割。

7.Resolve

webpack在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve则配置webpack如何寻找模块所对应的文件。Webpack内置JS模块化语法解析的功能,默认采用模块化标准里约定好的规则去寻找,用户也可以自定义修改规则。

  • resolve.alias通过别名把原导入路径映射成新的导入路径

    resolve: {
      alias: {
        "@": __dirname + "/src",//类型别名定义,引入文件就可以不用./了
      },
      // 定义了 webpack 如何自动添加后缀名
      extensions: [".ts", ".js", ".json"],
    },
    
  • resolve.modules配置webpack去哪些目录下寻找第三方模块,默认是只会去 node_modules目录下寻找。当项目中有一些模块大量被其他模块依赖和导入,由于其他模块的位置分布不定,针对不同的文件都要去计算被导入模块文件的相对路径,这个路径就会很长。

    //就像这样 import '../../../components/button' 这时你可以利用 modules 配置项优化,
    //假如那些被大量导入的模块都在 ./src/components 目录下,把 modules 配置成:
    resolve:{
      modules:['./src/components','node_modules']
    }
    //然后,就可以简单通过 import 'button' 导入。
    
  • resolve.extensions在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在。 resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:

    extensions: ['.js', '.json']
    //当遇到 require('./data') 这样的导入语句时,Webpack 会先去寻找 ./data.js 文件,
    //如果该文件不存在就去寻找 ./data.json 文件, 如果还是找不到就报错。
    //也可自定义,如下优先寻找ts文件:
    extensions: ['.ts', '.js', '.json']
    

8.总结

  • 想让源文件加入到构建流程中被 webpack 控制,配置 entry;
  • 想自定义输出文件的位置和名称,配置 output;
  • 想自定义寻找依赖模块时的策略,配置 resolve;
  • 想自定义解析和转换文件的策略,配置 module,一般是module.rules.loader;
  • 其他大部分需要需要 plugin 去实现。

webpack 原理解析

需要从以下三个方便理解。

1.基本概念

🛴详细参考本文webpack-核心配置

2.流程概括

一个串行的过程,依次执行:

  1. 初始化参数:从配置文件和Shell语句中读取和合并参数,得出最终参数;
  2. 开始编译:根据最终参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译;
  3. 确定入口:根据配置的.entry找出所有入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的loader对模块进行编译,再找出对应的依赖,递归本步骤直到所有的依赖都经历此步;
  5. 完成编译模块:完成loader编译后,得到每个模块被编译的最终内容和其依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

3.流程细节

常见 Loaders:

加载文件

编译模板

转换脚本语言

转换样式文件

检查代码

其他

常见 Plugins:

用于修改行为

用于优化

其他

🚩 webpack 优化(interview)

一、缩小文件搜索范围

INFO

webpack启动后从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。在遇到导入语句时,webpack会做两件事:

  1. 根据导入语句找到对应的要导入的文件;
  2. 根据找到的导入文件的后缀,使用配置中的 loader 去处理文件,如 ES6 开发的js文件要用babel-loader;

当项目变大,文件量很多,做以上两件事很影响构建速度,所以需人为缩小文件搜索范围。

  • 优化 module.rules 中的 loader 配置,通过 test、include、exclude 命中 loader 要应用规则的文件:

    module.exports = {
      module: {
        rules: [{
          // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
          test: /\.js$/,
          // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
          use: ['babel-loader?cacheDirectory'],
          // 只对项目根目录下的 src 目录中的文件采用 babel-loader
          include: path.resolve(__dirname, 'src'),
        }]
    }}
    
  • 优化 resolve.modules 配置

    resolve.modules 的默认值是 ['node_modules'],含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。

    当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:

    module.exports = {
      resolve: {
        // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
        // 其中 __dirname 表示当前工作目录,也就是项目根目录
        modules: [path.resolve(__dirname, 'node_modules')]
      }
    };
    
  • 优化 resolve.mainFields 配置

    resolve.mainFields 用于配置第三方模块使用哪个入口文件。在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main 字段去描述入口文件的位置,可以这样配置 Webpack:

    module.exports = {
      resolve: {
        // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
        mainFields: ['main'],
      },
    };
    
  • 优化 resolve.alias 配置

    // 默认情况下 Webpack 会从入口文件 ./node_modules/react/react.js 
    // 开始递归的解析和处理依赖的几十个文件,这会时一个耗时的操作。
    module.exports = {
      resolve: {
        // 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
        // 减少耗时的递归解析操作
        alias: {
          'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js'), // react15
          // 'react': path.resolve(__dirname, './node_modules/react/umd/react.production.min.js'), // react16
        }
    }}
    
  • 优化 resolve.extensions 配置,尽可能的减少后缀尝试的可能性

  • 优化 module.noParse 配置,见本文🌸

二、使用 DLLPluginopen in new window

  • 什么是DLL?

    webpack中的 dill-plugin 的使用是引入了微软的动态链接库(DLL)思想。

    DLL,用过 Windows 系统的人应该会经常看到以 .dll 为后缀的文件,这些文件称为动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据。

    为web项目加入 DLL 思想后,提高了构建速度。因为包含大量复用模块的动态链接库只需编译一次,在之后的构建过程中被动态链接库包含的模块将不会再重新编译,而是直接使用动态链接库中的代码。动态链接库大多包含的是常用的第三方代码,如react、react-dom,只要不升级就不会重新编译。

  • Webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入:

    1. DllPlugin:用于打包出一个个单独的动态链接库文件;
    2. DllReferencePlugin:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。

    使用教程open in new window

三、使用 ParallelUglifyPlugin

  • 使用背景

    webpack内置了UglifyJSopen in new window用于压缩代码。

    UglifyJS是一个代码压缩库,它在构建用于开发环境的代码完成速度快,但在生产环境构建时会一直卡在一个时间点没有反应,因为一直在代码压缩中……

    由于压缩JS代码需要先把代码解析成 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST ,导致这个过程计算量巨大,耗时很多。

  • parallelUglifyPluginopen in new window应用了多进程并行处理的思想。 UglifyJS是一个个挨着压缩再输出,parallelUglifyPlugin则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。

    const path = require('path');
    const DefinePlugin = require('webpack/lib/DefinePlugin');
    const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
    
    module.exports = {
      plugins: [
        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
          // 传递给 UglifyJS 的参数
          uglifyJS: {
            output: {
              // 最紧凑的输出
              beautify: false,
              // 删除所有的注释
              comments: false,
            },
            compress: {
              // 在UglifyJs删除没有用到的代码时不输出警告
              warnings: false,
              // 删除所有的 `console` 语句,可以兼容ie浏览器
              drop_console: true,
              // 内嵌定义了但是只用到一次的变量
              collapse_vars: true,
              // 提取出出现多次但是没有定义成变量去引用的静态值
              reduce_vars: true,
            }
    }})]}
    

四、使用自动刷新

开发时修改源码后想看到运行后的效果,需重新编译再运行。这个过程可以借助自动化的手段,在监听到源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。

  • 文件监听,webapack提高的两大模块webpackopen in new windowwebpack-dev-serveropen in new window
    module.export = {
      // 只有在开启监听模式时,watchOptions 才有意义
      // 默认为 false,也就是不开启
      watch: true,
      // 监听模式运行时的参数
      // 在开启监听模式时,才有意义
      watchOptions: {
        // 不监听的文件或文件夹,支持正则匹配
        // 默认为空
        ignored: /node_modules/,
        // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
        // 默认为 300ms
        aggregateTimeout: 300,
        // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
        // 默认每隔1000毫秒询问一次
        poll: 1000
      }
    }
    
  • 优化文件监听性能:开启监听模式时,默认情况下会监听配置的 Entry 文件和所有其递归依赖的文件。 在这些文件中会有很多存在于 node_modules 下,因为如今的 Web 项目会依赖大量的第三方模块。 在大多数情况下我们都不可能去编辑 node_modules 下的文件,而是编辑自己建立的源码文件。 所以一个很大的优化点就是忽略掉 node_modules 下的文件,不去监听它们。
    module.export = {
      watchOptions: { ignored: /node_modules/}// 不监听的 node_modules 目录下的文件
    }
    

五、开启热模块替换

要做到实时预览,除了刷新整个网页外,DevServer还支持模块热替换(Hot Module Replacement)的技术。它能在不刷新网页的情况下做到超灵敏的实时浏览

原理是当一个源码发生改变时,只重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块。

  • 原理:和自动刷新类似,都需要往开发的网页中注入一个代理客户端用于连接 DevServer 和网页,不同在于热模块替换的独特模块替换机制。

  • 使用:专用于开发环境,生产不可用。

    // 方法一:在启动时带上参数 `--hot`,完整命令是 `webpack-dev-server --hot`。
    // 方法二:Plugin如下
    const HotModuleReplacementPlugin = require('webpack/liHotModuleReplacementPlugin');
    module.exports = {
      entry:{
        // 为每个入口都注入代理客户端
        main:['webpack-dev-server/client?http://localhost:8080/', 
        'webpack/hot/dev-server','./src/main.js'],
      },
      plugins: [
        // 该插件的作用就是实现模块热替换,
        //实际上当启动时带上 `--hot` 参数,会注入该插件,生成 .hot-update.json 文件。
        new HotModuleReplacementPlugin(),
      ],
      devServer:{
        // 告诉 DevServer 要开启模块热替换模式
        hot: true,      
      }  
    };
    
  • 优化热模块替换:

六、区分环境

  • 为了尽可能的复用代码,在构建的过程中需要根据目标代码要运行的环境而输出不同的代码,我们需要一套机制在源码中去区分环境。

  • 原理是借助于环境变量的值去判断执行哪个分支。processopen in new window语句出现,webpack自动打包进process模块以支持Node.js的运行环境。源码如下:

    if (process.env.NODE_ENV === 'production') {
      console.log('你正在线上环境');
    } else {
      console.log('你正在使用开发环境');
    }
    
  • 第三方库中的环境区分

    if (process.env.NODE_ENV !== 'production') {
      warning(false, '%s(...): Can only update a mounted or mounting component.... ')
    }
    

七、压缩代码

压缩代码可以提升网页加载速度,减少网络传输流量,提升代码安全。

  • 压缩 Javascript

    目前成熟的压缩工具是UglifyJS,它会分析JS语法树,理解代码含义,去掉无效代码,缩短变量名等优化。

    webpack中接入UglifyJS需要两个成熟的插件:

    1️⃣ UglifyJsPlugin:通过封装 UglifyJS 实现压缩;当前采用的是 UglifyJS2open in new window而不是老的 UglifyJS1;除此之外 Webpack 还提供了一个更简便的方法来接入 UglifyJSPlugin,直接在启动 Webpack 时带上 --optimize-minimize 参数,即 webpack --optimize-minimize, 这样 Webpack 会自动为你注入一个带有默认配置的 UglifyJSPlugin。

    2️⃣ ParallelUglifyPlugin:多进程并行处理压缩。

    const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
    module.exports = {
      plugins: [
        // 压缩输出的 JS 代码
        new UglifyJSPlugin({
          compress: {
            // 在UglifyJs删除没有用到的代码时不输出警告
            warnings: false,
            // 删除所有的 `console` 语句,可以兼容ie浏览器
            drop_console: true,
            // 内嵌定义了但是只用到一次的变量
            collapse_vars: true,
            // 提取出出现多次但是没有定义成变量去引用的静态值
            reduce_vars: true,
          },
          output: {
            // 最紧凑的输出
            beautify: false,
            // 删除所有的注释
            comments: false,
          }
    })]}
    
  • 压缩 ES6

    目前大部分 JavaScript 引擎还不完全支持 ES6 中的新特性,但在一些特定的运行环境下已经可以直接执行 ES6 代码了,例如最新版的 Chrome、ReactNative 的引擎 JavaScriptCore。

    ES6 的代码相比于转换后的 ES5 代码有如下优点:

    • 一样的逻辑用 ES6 实现的代码量比 ES5 更少;
    • JavaScript 引擎对 ES6 中的语法做了性能优化,例如针对 const 申明的变量有更快的读取速度;

    所以在运行环境允许的情况下,我们要尽可能的使用原生的 ES6 代码去运行,而不是转换后的 ES5 代码。

    为了压缩 ES6 代码,需要使用专门针对 ES6 代码的 UglifyESopen in new window

    npm i -D uglifyjs-webpack-plugin@beta
    
    const UglifyESPlugin = require('uglifyjs-webpack-plugin')
    module.exports = {
      plugins: [
        new UglifyESPlugin({
          // 多嵌套了一层
          uglifyOptions: {
            compress: {
              // 在UglifyJs删除没有用到的代码时不输出警告
              warnings: false,
              // 删除所有的 `console` 语句,可以兼容ie浏览器
              drop_console: true,
              // 内嵌定义了但是只用到一次的变量
              collapse_vars: true,
              // 提取出出现多次但是没有定义成变量去引用的静态值
              reduce_vars: true,
            },
            output: {
              // 最紧凑的输出
              beautify: false,
              // 删除所有的注释
              comments: false,
            }
    }})]}
    

    同时,为了不让babel-loader输出 ES5 的代码,去掉babel-preset-env

  • 压缩 CSS

    成熟的CSS压缩工具cssnanoopen in new window,基于PostCSS.

    cssnano 接入到 Webpack 中也非常简单,因为 css-loader 已经将其内置了,要开启 cssnano 去压缩代码只需要开启 css-loader 的 minimize 选项。

    const path = require('path');
    const {WebPlugin} = require('web-webpack-plugin');
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,// 增加对 CSS 文件的支持
            // 提取出 Chunk 中的 CSS 代码到单独的文件中
            use: ExtractTextPlugin.extract({
              // 通过 minimize 选项压缩 CSS 代码
              use: ['css-loader?minimize']
        })}]
      },
      plugins: [
        // 用 WebPlugin 生成对应的 HTML 文件
        new WebPlugin({
          template: './template.html', // HTML 模版文件所在的文件路径
          filename: 'index.html' // 输出的 HTML 的文件名称
        }),
        new ExtractTextPlugin({
          filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 Hash 值
    })]}
    

八、CDN加速

网络首次打开时间长,根本原因在于网络传输过程耗时大,CDN的作用就是加速网络传输。

Content Delivery Network,内容分发网络。通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,加速资源的获取速度。

目前很多大公司都会建立自己的 CDN 服务,就算你自己没有资源去搭建一套 CDN 服务,各大云服务提供商都提供了按量收费的 CDN 服务。

  • 业界成熟的 CDN 做法

    • 针对 HTML 文件,放到自己的服务器上,关闭自己服务器的缓存,只提供 HTML 文件和数据接口;
    • 针对静态的js、img、font等,开启CDN和缓存,上传到CDN服务器上,同时给每个文件名带上由内容算出的hash值。
  • Webpack中接入 CDN

    const path = require('path');
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    const {WebPlugin} = require('web-webpack-plugin');
    module.exports = {
      // 省略 entry 配置...
      output: {
        // 给输出的 JavaScript 文件名称加上 Hash 值
        filename: '[name]_[chunkhash:8].js',
        path: path.resolve(__dirname, './dist'),
        // 指定存放 JavaScript 文件的 CDN 目录 URL
        publicPath: '//js.cdn.com/id/',
      },
      module: {
        rules: [
          {
            // 增加对 CSS 文件的支持
            test: /\.css$/,
            // 提取出 Chunk 中的 CSS 代码到单独的文件中
            use: ExtractTextPlugin.extract({
              // 压缩 CSS 代码
              use: ['css-loader?minimize'],
              // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
              publicPath: '//img.cdn.com/id/'
            }),
          },
          {
            // 增加对 PNG 文件的支持
            test: /\.png$/,
            // 给输出的 PNG 文件名称加上 Hash 值
            use: ['file-loader?name=[name]_[hash:8].[ext]'],
          },
          // 省略其它 Loader 配置...
        ]
      },
      plugins: [
        // 使用 WebPlugin 自动生成 HTML
        new WebPlugin({
          // HTML 模版文件所在的文件路径
          template: './template.html',
          // 输出的 HTML 的文件名称
          filename: 'index.html',
          // 指定存放 CSS 文件的 CDN 目录 URL
          stylePublicPath: '//css.cdn.com/id/',
        }),
        new ExtractTextPlugin({
          // 给输出的 CSS 文件名称加上 Hash 值
          filename: `[name]_[contenthash:8].css`,
        }),
        // 省略代码压缩插件配置...
      ],
    };
    

九、tree shaking【🟡webpack2.0引入】

Tree Shaking能剔除js中用不到的死代码。它依赖静态的ES6语法,若使用ES5会出错。Tree Shaing最先在 Rollup中出现,webpack2.0将其引入。

.babelrc配置

{
  "presets": [
    [
      "env",
      {
        "modules": false//关闭babel模块的转换功能,保留原本的ES6语法
      }
    ]
]}

配置好 Babel 后,重新运行 Webpack,在启动 Webpack 时带上 --display-used-exports 参数,以方便追踪 Tree Shaking 的工作。

WARNING

Tree Shaking 正常工作的前提是交给 Webpack 的 JavaScript 代码必须是采用 ES6 模块化语法的, 因为 ES6 模块化语法是静态的(导入导出语句中的路径必须是静态的字符串,而且不能放入其它代码块中),这让 Webpack 可以简单的分析出哪些 export 的被 import 过了。 如果你采用 ES5 中的模块化,例如 module.export={...}、require(x+y)、if(x){require('./util')},Webpack 无法分析出哪些代码可以剔除。

十、提取公共代码

webpack内置了专门用于提取多个 Chunk 中公共部分的插件CommonsChunkPlugin

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
new CommonsChunkPlugin({
  // 从哪些 Chunk 中提取
  chunks: ['a', 'b'],
  // 提取出的公共部分形成一个新的 Chunk,这个新 Chunk 的名称
  name: 'common'
})

每个CommonsPlugin都会生成一个新的 Chunk,包含此入口文件和入口依赖的文件,故需要在项目中写一个 base.js 描述 base Chunk 所依赖的模块:

// 所有页面都依赖的基础库
import 'react';
import 'react-dom';
// 所有页面都使用的样式
import './base.css';

为了从 common 中提取出 base 也包含的部分,还需要配置一个 CommonsChunkPlugin,相关代码如下:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
new CommonsChunkPlugin({
  // 从 common 和 base 两个现成的 Chunk 中提取公共的部分(从哪里取)
  chunks: ['common', 'base'],
  // 把公共的部分放到 base 中,(新chunk的名称是base)
  name: 'base'
})

以上,重新构建后将得到:

  • base.js:所有网页都依赖的基础库组成的代码;
  • common.js:网页A、B都需要的,但又不在 base.js 文件中出现过的代码;
  • a.js:网页 A 单独需要的代码;
  • b.js:网页 B 单独需要的代码;

为了让网页正常运行,以网页 A 为例,你需要在其 HTML 中按照以下顺序引入以下文件才能让网页正常运行:

<script src="base.js"></script>
<script src="common.js"></script>
<script src="a.js"></script>

十一、分割代码按需加载【🟡webpack3.0引入】

webpack内置了强大的分割代码的功能实现按需加载:

/* webpackChunkName: "show" / 的含义是为动态生成的 Chunk 赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的 Chunk 的名称,默认名称将会是 [id].js。

window.document.getElementById('btn').addEventListener('click', function () {
  // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});

import(/* webpackChunkName: "show" */ './show'),webpack内置了对import(*)语句的支持:

  • ./show.js为入口生成一个新的Chunk
  • 当代码执行到 import 所在语句才会加载由Chunk对应生成的文件
  • import 返回 Promise,文件加载成功时在.then方法中获取到show.js导出内容
//为了正确的输出在 /* webpackChunkName: "show" */ 中配置的 ChunkName,还需要配置下 Webpack
module.exports = {
  // JS 执行入口文件
  entry: {
    main: './main.js',
  },
  output: {
    // 为从 entry 中配置生成的 Chunk 配置输出文件的名称
    filename: '[name].js',
    // 为动态加载的 Chunk 配置输出文件的名称
    chunkFilename: '[name].js',
  }
};

十二、开启 Scope Hoisting【🟡webpack3.0引入】

  • 是什么:作用域提升,让webpack打包出的代码文件更小、运行更快。

  • 原理:分析模块间的依赖关系,尽可能的把打散的模块合并到一个函数中去,前提不能造成代码冗余。源码必须使用ES6模块化语句,原因和Tree Shaking类似。

  • 使用:Webpack 内置的功能,只需要配置一个插件ModuleConcatenationPlugin

    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
    module.exports = {
      plugins: [
        // 开启 Scope Hoisting
        new ModuleConcatenationPlugin(),
      ],
    };
    

    同时,考虑到 Scope Hoisting 依赖源码需采用 ES6 模块化语法,还需要配置 mainFields

    module.exports = {
      resolve: {
        // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
        mainFields: ['jsnext:main', 'browser', 'main']
      },
    };
    

十三、输出分析 🚩

需要对输出结果分析,以决定下一步的优化方向。

直接的方式,阅读webpack输出的代码,但可阅读性太差,需借助可视化分析工具。

  • webpack --profile --json > stats.json

    "scripts": {
      "build": "webpack",
      "generateAnalyzFile": "webpack --profile --json > stats.json"
    },
    
    1. --profile:记录下构建过程中的耗时信息;
    2. --json:以 JSON 的格式输出构建结果,最后只输出一个 .json 文件,这个文件中包括所有构建相关的信息;
    3. stats.json 是 UNIX/Linux 系统中的管道命令、含义是把 webpack --profile --json 输出的内容通过管道输出到 stats.json 文件中。

    在启动 Webpack 时带上以上两个参数,启动命令如下 webpack --profile --json > stats.json,你会发现项目中多出了一个 stats.json 文件。 这个 stats.json 文件是给可视化分析工具使用的。

  • 官方可视化工具Webpack Analyseopen in new window

    上传stats.json的网站,本地解析,保证数据安全。从Modules、Chunks、Assets、Warnings、Errors、Hints分析。

  • webpack-bundle-analyzeropen in new window

    相比webpack Analyse更直观清晰,且不容易报错。

    //pnpm i webpack-bundle-analyzer
    const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
     plugins: [
      new BundleAnalyzerPlugin()
    ],
    - 打包出的文件中都包含了什么;
    - 每个文件的尺寸在总体中的占比,一眼看出哪些文件尺寸大;
    - 模块之间的包含关系;
    - 每个文件的 Gzip 后的大小。