1

写在最前面

最近工作中webpack用的比较多,想系统的学习一下,找到了极客时间的教程,跟着视频上讲的学了一遍,收获挺大的。虽然视频教程是2019年出的,有点老,有些知识已经过时了,但并不妨碍学习。那些过时的知识全当了解,可以搜索寻找替代方案。学习资源:玩转webpack,下面是我的学习笔记。

构建工具

前端技术不断发展,比如:ReactjsxVueAngular指令,CSS预处理器,最新的ES语法等,浏览器都不能识别。

构建工具的作用就是将这些浏览器不能识别的语法(高级语法)转换成了浏览器能识别的语法(低级语法)。

还有一个作用是将代码压缩混淆,在压缩代码体积的同时也让代码不易阅读。

webpack是现在前端流行的构建工具。

初识webpack

配置script脚本

安装好webpack后,在命令行中使用webpack时,有两种方式:

  1. 指定路径./node_modules/.bin/webpack
  2. 使用npx工具,npx webpack

这两种方法使用挺麻烦的。有种简单的方式使用package.json中的scripts,它能够读到node_modules/.bin目录下的命令。

在命令行中执行npm run build即可。

"scripts": {
  "build": "webpack"
}

配置文件

webpack的默认配置文件webpack.config.js,可以通过webpack --config来指定配置文件。

比如,生产环境的配置文件webpack.config.prod.js,开发环境的配置文件是webpack.config.dev.js

"scripts": {
  "build": "webpack --config webpack.config.prod.js",
  "build:dev": "webpack --config webpack.config.dev.js"
}

核心概念

webpack有 5 个核心概念:entryoutputmodeloadersplugins

entry

打包时的入口文件,默认是./src/index.js

entry是有两种方式:单入口、多入口。

单入口用于单页面应用,entry是个字符串

module.exports = {
  entry: "./src/index.js",
};

多入口用于多页面应用,entry是对象形式

module.exports = {
  entry: {
    index: "./src/index.js",
    app: "./src/app.js",
  },
};

output

打包后的输出文件,默认是./dist/main.js

entry是单入口,output可通过修改参数pathfilename

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
};

entry是多入口,outputfilename需要用[name]占位符,用来指定打包后的名称,对应的是entry中的key

const path = require("path");

module.exports = {
  entry: {
    index: "./src/index.js",
    app: "./src/app.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
};

mode

可设置为developmentproductionnone,默认是production

  • development:开发环境,设置process.env.NODE_ENV的值为development。开启NamedChunksPluginNamedModulesPlugin
  • production:生产环境,设置process.env.NODE_ENV的值为production。开启FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitonErrorsPluginOccurrenceOrderPluginSideEffectsFlagPluginTerserPlugin
  • none:不开启任何优化选项
ps: development模式下开启的两个插件在代码热更新阶段,会在控制台打印出哪个模块发生了热更新,这个模块的路径是啥。production模式下开启的插件会在压缩代码。

loaders

webpack只支持jsjson两种文件类型,loader的作用就是用来处理其他的文件,并将它们添加到依赖图中。

loader是个函数,接收源文件作为参数,返回转换后的结果。

一般loader命名的方式都是以-loader为后缀,比如css-loaderbabel-loader

test是指定匹配的规则,use是指定使用loader的名称

module.exports = {
  module: {
    rules: [
      {
        test: /.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ],
  },
};

一个loader一般只做一件事,在解析less时,需要用less-loaderless转成css,由于webpack不能识别css,又需要用css-loadercss转换成commonjs对象放到js中,最后需要用style-loadercss插入到页面的style中。

ps: loader的组合通常由两种方式,一种是从左到右(类似unixpipe),另一种是从右到左(compose)。webpack选择的是compose,从右到左一次执行loader

plugins

任何loader没法做的事情,都可以用plugin解决,它主要用于文件优化、资源管理、环境变量注入,作用于整个构建过程。

一般plugin命名的方式是以-webpack-plugin为后缀结尾,也有是以-plugin为后缀结尾的。

const CleanWebpackPlugin = require("clean-webpack-plugin");

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

webpack资源解析

解析es6

需要安装@babel/core@babel/preset-envbabel-loader

在根目录下面新建.babelrc文件,将@babel/preset-env添加到presets

{
  "presets": ["@babel/preset-env"]
}

webpack中配置babel-loader

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        use: "babel-loader",
      },
    ],
  },
};

还有一种配置的方式,不使用.babelrc文件,将它配置在use的参数options

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

解析css

需要安装css-loaderstyle-loader

module.exports = {
  moudle: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

解析less

需要安装less-loadercss-loaderstyle-loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ],
  },
};

解析图片和字体

需要安装file-loader

module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpeg|jpg|gif)$/,
        use: ["file-loader"],
      },
      {
        test: /.(woff|woff2|eot|ttf|otf)$/,
        use: ["file-loader"],
      },
    ],
  },
};

url-loader

url-loader可以将较小的图片转换成base64

module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpeg|jpg|gif)$/,
        use: {
          loader: "url-loader",
          options: {
            limit: 10240, // 小于 10k 图片,webpack 在打包时自动转成 base64
          },
        },
      },
    ],
  },
};

配置vue

配置vue开发环境,需要安装vuevue-loadervue-template-compiler

const { VueLoaderPlugin } = require("vue-loader");

module.export = {
  plugins: [new VueLoaderPlugin()],
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: "vue-loader",
      },
    ],
  },
};
ps: 这里使用的vue-loader15.x版本,我安装最新的版本16.x有问题,一直没有解决。

配置react

配置react开发环境需要安装reactreact-dom@babel/preset-react

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

webpack文件监听及热更新

文件监听

每次修改代码后,都需要手动构建,影响开发效率。webpack提供了文件监听的作用。开启监听时,webpack会调用nodefs模块来判断文件是否发生变化,如果发生了变化,自动重新构建输出新的文件。

webpack开启监听模式,有两种方式:(需要手动刷新浏览器)

  • 启动webpack命令时,带上--watch参数
"scripts": {
 "watch": "webpack --watch"
}
  • webpack.config.js中设置watch: true
module.exports = {
  watch: true,
};

文件监听分析

webpack文件监听判断依据是看文件的最后编辑时间是否发生变化。

它会将修改的时间保存起来,当文件修改时,它会和上一次的修改时间进行比对。

如果发现不一致,它并不会马上告诉监听者,而是先把文件的修改缓存起来,等待一段时间,如果这段时间内还有其他文件发生变化,它就会把这段时间内的文件列表一起构建。这个等待的时间就是aggregateTimeout

module.exports = {
  watch: true,
  watchOptions: {
    ignored: /node_modules/,
    aggregateTimeout: 300,
    poll: 1000,
  }
}
  • watch:默认为false
  • watchOptions:只有watchtrue时,才生效

    • ignored:需要忽略监听的文件或者文件夹,默认为空,忽略node——modules性能会有所提升。
    • aggregateTimeout:监听到文件变化后,等待的时间,默认300ms
    • poll:轮询时间,1s一次

热更新

热更新需要webpack-dev-serverHotModuleReplacementPlugin两个插件背后使用。

watch相比,它不输出文件,直接方式内存中,所以它的构建熟读更快。

热更新在开发模式中才会使用。

const webpack = require("webpack");
const path = require("path");

module.exports = {
  mode: "development",
  plugins: [new webpack.HotModuleReplacementPlugin()],
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    hot: ture,
  },
};

package.json中配置命令

webpack4.x

"scripts": {
  "dev": "webpack-dev-server --open"
}

webpack5.x

"scripts": {
  "dev": "webpack server"
}
ps: webpack5.xwebpack-dev-server有冲突,不能使用--open打开浏览器。

HotModuleReplacementPlugin

热更新的核心是HMR ServerHMR Runtime

  • HMR Server:是服务端,用来将变化的js模块通过websocket的消息通知给浏览器端
  • HMR Runtime:是浏览器端,用于接收HMR Server传递过来的模块数据,浏览器端可以看到.hot-update.json文件

hot-module-replacement-plugin的作用:webpack本身构建出来的bundle.js本身是不具备热更新的,HotModuleReplacementPlugin的作用就是HMR Runtime注入到bundle.js,使得bundle.js可以和HMR Server建立websocket的通信能力。一旦磁盘里的文件发生修改,那么HMR Server将有修改的js模块通过websocket发送给HMR Runtime,然后HMR Runtime去局部更新页面的代码,这种方法不会刷新浏览器。

webpack-dev-server的作用:提供bundle server的能力,就是生成的bundle.js可以通过localhost://xxx的方式去访问,另外也提供livereload(浏览器自动刷新)。

文件指纹

什么是文件指纹

文件指纹是指打包后输出的文件名的后缀。比如:index_56d51795.js

通常用于用于版本管理

文件指纹类型

  • hash: 和项目的构建相关,只要项目文件改变,构建项目的hash值就会改变。采用hash计算的话,每一次构建后的哈希值都不一样,假如说文件内容没有发生变化,那这样子是没法实现缓存的。
  • chunkhash:和webpack打包的chunk有关,不同的entry会生成不同的chunkhash。生产环境会将一些公共库和源码分开来,单独用chunkhash构建,只要不改变公共库的代码,它的哈希值就不会变,就可以实现缓存。
  • contenthash:根据文件内容来定义hash,文件内容不变,则contenthash不变。一般用于css资源,如果css资源使用chunkhash,那么修改了jscss资源就会变化,缓存就会失效,所以css使用contenthash

ps:

  1. 使用hash的场景要结合mode来考虑,如果modedevelopment,在使用HMR情况下,使用chunkhash是不适合的,应该使用hash。而modeproduction时,应该用chunkhash
  2. js使用chunkhash是便于寻找资源,因为js的资源关联度更高。css使用contenthash是因为css一般是根据不同的页面书写的,css资源之间的关联度不高,也就不用在其他资源修改时,重新更新css

js文件指纹

设置outputfilename,使用[chunkhash]

const path = require("path");

module.exports = {
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name]_[chunkhash:8].js", // 取 hash 的前 8位
  },
};

css文件指纹

需要安装mini-css-extract-plugin

style-loader是将css插入到页面的head中,mini-css-extract-plugin是提取为单独的文件,它们之间是互斥的。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name]_[contenthash:8].css",
    }),
  ],
};

图片文件指纹

需要安装file-loader

占位符名称含义
[ext]资源后缀名
[name]文件名称
[path]文件的相对路径
[folder]文件所在的文件夹
[contenthash]文件内容的hash,默认是md5
[hash]文件内容的hash,默认是md5
[emoji]一个随机的指代文件内容的emoji

图片的hashcss/jshash概念不一样,图片的hash是有图片的内容决定的。

module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpeg|jpg|gif)$/,
        use: {
          loader: "file-loader",
          options: {
            filename: "[folder]/[name]_[hash:8].[ext]",
          },
        },
      },
      {
        test: /.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              filename: "[name]_[hash:8].[ext]",
            },
          },
        ],
      },
    ],
  },
};

代码压缩

代码压缩主要分为js压缩,css压缩,html压缩。

js压缩

webpack内置了uglifyjs-webpack-plugin插件,默认打包后的文件都是压缩过的,这里无需额外配置。

可以手动安装这个插件,可以额外设置一下配置,比如开启并行压缩,需要将parallel设置为true

const UglifyjsWebpackPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new UglifyjsWebpackPlugin({
        parallel: true,
      }),
    ],
  },
};

css压缩

安装optimize-css-webpack-plugin,同时安装css预处理器cssnano

const OptimizeCssWebpackPlugin = require("optimize-css-webpack-plugin")
module.exports = {
  plugins: [
    new OptimizeCssWebpackPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require("cssnano)
    })
  ]
}

html压缩

需要安装html-webpack-plugin,通过设置压缩参数。

ps: 多页面应用需要写多个new HtmlWebpackPlugin({...})
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
     template: path.join(__dirname, "src/search.html"),    //html 模版所在的位置,模版里面可以使用 ejs 的语法
      filename: "search.html",              // 打包出来后的 html 名称
      chunks: ["search"],           // 打包出来后的 html 要使用那些 chunk
      inject: true,                // 设置为 true,打包后的 js css 会自动注入到 HTML 中
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
    }),
    new HtmlWebpackPlugin({...})
  ],
};

HtmlWebpackPlugin参数minify里面的minifyJSminifyCSS的作用是用于压缩一开始就内联在html里的js(不能使用ES6语法)和css

chunks对应的是entry中的key。你希望哪个chunk自动注入,就将哪个chunk写到chunks

chunkbundlemodule区别:

  • chunk:每个chunk是又多个module组成,可以通过代码分割成多个chunk
  • bundle:打包生成的最终文件
  • modulewebpack中的模块(jscss、图片)

自动清理构建目录

每次在构建的时候,会产生新的文件,造成输出目录output文件越来越多。

最常见的清理方法用命令rm去删除,在打包之前先执行rm -rf命令将dist目录删除,在进行打包。

"scripts": {
  "build": "rm -rf ./dist && webpack"
}

另一种是使用rimraf进行删除。

安装rimraf,使用时也是先在打包前先将dist目录进行删除,然后在打包。

"scripts": {
  "build": "rimraf ./dist && webpack"
}

这两种方案虽然都能将dist目录清空,但不太优雅。

webpack提供clean-webpack-plugin,它会自动清理output.path下的文件。

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

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

自动补齐样式前缀

不同的浏览器厂商在实现css特性的标准没有完全统一,比如display: flex,在webkit内核中要写成display: -webkit-box

在开发的时候一个个加上内核,将是一个巨大的工程。

webpack中可以用loader去解决自动补齐css前缀的问题。

安装postcss-loader,以及它的插件autoprefixer

autoprefixer是根据can i use这个网站提供的css兼容性进行不全前缀的。

autoprefixer是后置处理器,它和预处理器不同,预处理器是在打包的时候处理,而autoprefixer是在代码处理好之后,样式已经生成了再进行处理。

webpack4.x中安装postcss-loader@3.0.0autoprefixer@9.5.1

方式一: 直接在webpack.config.js中配置

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
          {
            loader: "postcss-loader",
            options: {
              plugins: () => [
                require("autoprefixer")({
                  overrideBrowserslist: ["last 2 version", ">1%", "ios 7"], //最新的两个版本,用户大于1%,且兼容到 ios7
                }),
              ],
            },
          },
        ],
      },
    ],
  },
};

方式二: 利用postcss配置文件postcss.config.jswebpack.config.js直接写postcss-loader即可。

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
          "postcss-loader",
        ],
      },
    ],
  },
};

postcss.config.js

module.exports = {
  plugins: [
    require("autoprefixer")({
      overrideBrowserslist: ["last 2 version", ">1%", "ios 7"],
    }),
  ],
};

方法三: 浏览器的兼容性可以写package.json中,postcss.config.js中只需加载autofixer即可。

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
          "postcss-loader",
        ],
      },
    ],
  },
};

postcss.config.js

module.exports = {
  plugins: [require("autofixer")],
};

package.json

"browserslist": {
  "last 2 version",
  "> 1%",
  "ios 7"
}

资源内联

在一些场景中需要使用到资源内联,常见的有:

  1. 页面框架的初始化脚本
  2. 减少http网络请求,将一些小的图片变成base64内容到代码中,较少请求
  3. css内联增加页面体验
  4. 尽早执行的js,比如REM方案

html内联

在多页面项目中,head中有很多公用的标签,比如meta,想要提升维护性,会将它提取成一个模版,然后引用进来。

安装raw-loader@0.5.1

<head>
  ${require("raw-loader!./meta.html")}
</head>

js内联

REM方案中,需要尽早的html标签的font-size,那么这段js就要尽早的加载、执行。

<head>
  <script>
    ${require('raw-loader!babel-loader!./calc-root-fontsize.js')}
  </script>
</head>

css内联

为了更好的体验,避免页面闪动,需要将打包好的css内联到head中。

安装html-inline-css-webpack-plugin,这个插件需要放在html-webpack-plugin后面。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlInlineCssWebpackPlugin = require("html-inline-css-webpack-plugin");

module.exports = {
  module: {
    rules: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name]_[contenthash:8].css",
    }),
    new HtmlWebpackPlugin(),
    new HtmlInlineCssWebpackPlugin(),
  ],
};

需要先将css提取成单独的文件才行。

ps: style-laoderhtml-inline-css-webpack-plugin的区别是:

  • style-loader:插入样式是一个动态的过程,打包后的源码不会有style标签,在执行的时候通过js动态插入style标签
  • html-inline-css-webpack-plugin:是在构建的时候将css插入到页面的style标签中

多页面应用

每个页面对应一个entry,同时对应plugins中一个html-webpack-plugin

这种方式有个缺点,每个增加一个entry就要增加一个html-webpack-plguin

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: {
    index: "./src/index.js",
    search: "./src/search.js",
  },
  plugins: [
    new HtmlWebpackPlugin({ template: "./src/index.html" }),
    new HtmlWebpackPlugin({ template: "./src/search.html" }),
  ],
};

借助glob可以实现通用化的配置方式。

安装glob,同时所有的页面都要放在src下面,并且入口文件都要叫做index.js

const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));

  entryFiles.forEach((entryFile) => {
    const match = entryFile.match(/src\/(.*)\/index\.js/);
    const pageName = match && match[1];

    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: [pageName],
        inject: true,
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false,
        },
      })
    );
  });

  return {
    entry,
    htmlWebpackPlugins,
  };
};

const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
  entry,
  plugins: [].concat(htmlWebpackPlugins),
};

sourceMap使用方法

发布到生成环境中的代码,要将代码进行压缩混淆,但是在压缩混淆之后,看代码就像在看天书一样。

sourceMap提供了压缩后的代码和源代码之间的映射关系。

sourceMap在开发环境开启,线上环境中关闭,在线上排查问题时将source map上传到错误监控系统。

source map关键字

  • eval:使用eval包裹模块代码
  • source map:产生map文件
  • cheap:不包含列信息
  • inline:将.map作为DataURI嵌入,不单独生成.map文件
  • module:包含loadersourcemap

source map类型

devtool首次构建二次构建是否适合生产环境可以定位的代码
(none)++++++yes最终输出的代码
eval++++++nowebpack生成的代码(一个个的模块)
cheap-eval-source-map+++no经过 loader 转换后的代码(只能看到行)
cheap-module-eval-source-mapo++no源代码(只能看到行)
eval-source-map--+no源代码
cheap-source-map+oyes经过loader转换后的代码只能看到行)
cheap-module-source-mapo-yes源代码(只能看到行)
inline-cheap-source-map+-no经过loader转换后的代码(只能看到行)
inline-cheap-module-source-mapo-no源代码(只能看到行)
source-map----yes源代码
Inline-source-map----no源代码
hidden-source- map----yes源代码

提取页面公共资源

在项目中,多个页面中都会使用到一些基础库,比如reactreact-dom,还有一写公共的代码,当在打包时这些都会被打包进最终的代码中,这是比较浪费的,而且打包后的体积也比较大。

webpack4内置了SplitChunksPlugin插件。

chunks参数说明:

  • async异步引入的库进行分离(默认)
  • initial同步引入的库进行分离
  • all所有引入的库进行分离(推荐)

抽离出基础库的名称cacheGroups里中的filename放到html-webpack-pluginchunks中,会自动导入:

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
        chunks: ["vendors", "commons", "index"], //打包出来后的 html 要使用那些 chunk
      })
    );
  ],
  optimization: {
    splitChunks: {
      chunks: "async",
      minSize: 30000, // 抽离的公共包最小的大小,单位字节
      maxSize: 0, // 最大的大小
      minChunks: 1, // 资源使用的次数(在多个页面使用到), 大于1, 最小使用次数
      maxAsyncRequests: 5, // 并发请求的数量
      maxInitialRequests: 3, // 入口文件做代码分割最多能分成3个js文件
      automaticNameDelimiter: "~", // 文件生成时的连接符
      automaticNameMaxLength: 30, // 自动自动命名最大长度
      name: true, //让cacheGroups里设置的名字有效
      cacheGroups: {
        //当打包同步代码时,上面的参数生效
        vendors: {
          test: /[\\/]node_modules[\\/]/, //检测引入的库是否在node_modlues目录下的
          priority: -10, //值越大,优先级越高.模块先打包到优先级高的组里
          filename: "vendors.js", //把所有的库都打包到一个叫vendors.js的文件里
        },
        default: {
          minChunks: 2, // 上面有
          priority: -20, // 上面有
          reuseExistingChunk: true, //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
        },
      },
    },
  },
};

tree shaking(摇树优化)

在一个模块中有多个方法,用到的方法会被打包进bundle中,没有用到的方法不会打包进去。

tree shaking就是只把用到的方法打包到bundle,其余没有用到的会在uglify阶段擦除。

webpack默认支持,在.babelrc里设置modules: false即可。production阶段默认开启。

必须是ES6语法,CJS的方式不支持。

ps: 如果把ES6转成ES5,同时又要开启tree shaking,需要在.babelrc里设置module: false,不然babel默认会把ES6转成CJS规范的写法,这样就不能进行tree shaking

DCE(deal code elimination)

DEC全称 deal code elimination,中文意思是死代码删除,主要是下面三种:

  1. 代码不会被执行
if (false) {
  console.log("代码不会被执行");
}
  1. 代码执行的结果不会被用到
function a() {
  return "this is a";
}
let A = a();
  1. 代码只写不读
let a = 1;
a = 2;

tree shaking原理

对代码进行静态分析,在编译阶段,代码有没有用到是要确定下来的,不能通过在代码运行时在分析哪些代码有没有用到,tree shaking会把没用到的代码用注释标记出来,在uglify阶段将这些无用代码擦除。

利用ES6模块的特点:

  • 只能作为模块顶层的语句出现
  • import的模块名只能是字符串常量
  • import bindingimmutable

删除无用的css

  • purgecss-webpack-plugin:遍历代码,识别已经用到的css class

    • mini-css-extract-plugin配合使用
  • uncsshtml需要通过jsdom加载,所有的样式通过postCSS解析,通过document.querySelector来识别在html文件里面不存在的选择器
const PurgecssPlugin = require("purgecss-webpack-plugin");

const PATHS = {
  src: path.join(__dirname, "src"),
};

module.exports = {
  plugins: [
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
    }),
  ],
};

scope hoisting使用和原理分析

构建够的代码会产生大量的闭包,如下图所示:

1.png

当引入外部模块时,会用一个函数包裹起来。当引入的模块越多时,就会产生大量的函数包裹代码,导致体积变大,在运行的时候,由于创建的函数作用域越多,内存开销也会变大。

  • webpack转换后的模块会被包裹一层
  • import会被转换成__webpack_require

分析

  1. 打包出来的是一个IIFE(匿名闭包)
  2. modules是一个数组,每一项是一个模块初始化函数
  3. __webpack_require用来加载模块,返回module.exports
  4. 通过WEBPACK_REQUIRE_METHOD(0)启动程序

scope hoisting原理

将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重名一些变量以防止变量名冲突。

模块调用有先后顺序,a模块调用b模块,因为有函数包裹,a模块和b模块的顺序无所谓,如果消除包裹代码的话,需要根据模块的引用顺序来排放模块,就要将b模块放到a模块之前,b模块才能被a模块调用。

通过scope hoisting可以减少函数声明和内存开销。production阶段默认开启。

必须是ES6语法,CJS不支持。

ps: scope hoisting把多个作用域变成一个作用域。当模块被引用的次数大于1次时,是不产生效果的。如果一个模块引用次数大于1次,那么这个模块的代码会被内联多次,从而增加打包后文件的体积。

使用ESLint

ESLint能够统一团队的代码风格,能够帮助发现带错误。

两种使用方法:

  • CI/CD集成
  • webpack集成

webpackCI/CD集成

需要安装husky

增加scripts,通过lint-staged检查修改文件。

"scripts": {
  "precommit": "lint-staged"
},
"lint-staged": {
  "linters": {
    "*.{js,less}": ["eslint --fix", "git add"]
  }
}

webpackESLint集成

使用eslint-loader,构建是检查js规范

安装插件babel-eslinteslinteslint-config-airbnbeslint-config-airbnb-baseeslint-loadereslint-plugin-importeslint-plugin-jsx-allyeslint-plugin-react

新建.eslintrc.js文件

module.exports = {
  parser: "babel-eslint",
  extends: "airbnb",
  env: {
    browser: true,
    node: true,
  },
  rules: {
    indent: ["error", 2],
  },
};

webpack.config.js文件配置eslint-loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["babel-loader", "eslint-loader"],
      },
    ],
  },
};

代码分割和动态import

对于大的web应用来说,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊情况下才会被用到。

webpack有一个功能是将你的代码分割成chunks(语块),当代码运行到需要它们的时候在进行加载。

使用场景:

  • 抽离相同代码到一个共享块
  • 脚本懒加载,使得初始下载的代码更小

懒加载js脚本的方式

  • CJSrequire.ensure
  • ES6import(目前还没有原生支持,需要bable转换)

安装@bable/plugin-syntax-dynamic-import插件

.babelrc文件:

{
  plugins: ["@bable/plugin-syntax-dynamic-import"];
}

例子:

class Index extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      Text: null,
    };
  }
  loadComponent = () => {
    import("./text.js").then((Text) => {
      this.setState({ Text: Text.default });
    });
  };
  render() {
    const { Text } = this.state;
    return (
      <div className="search">
        react1
        {Text && <Text />}
        <div onClick={this.loadComponent}>点我</div>
        <img src={img} alt="" />
      </div>
    );
  }
}

这里要注意一点,当配置了cacheGroups时,minChunks设置了1,上面设置的懒加载脚本就不生效了,因为import在加载时是静态分析的。

cacheGroups: {
  commons: {
    name: "commons",
    chunks: "all",
    priority: -20, //值越大,优先级越高.模块先打包到优先级高的组里
    minChunks: 1,
  }
}

多进程/多实例:并行压缩

方法一:使用webpack-parallel-uglify-plugin插件

const WebpackParalleUglifyPlugin = require("webpack-parallel-uglify-plugin");

module.exports = {
  plugins: [
    new WebpackParalleUglifyPlugin({
      uglifyJs: {
        output: {
          beautify: false,
          comments: false,
        },
        compress: {
          warnings: false,
          drop_console: true,
          collapse_vars: true,
          reduce_vars: true,
        },
      },
    }),
  ],
};

方法二:使用uglifyjs-webapck-plugin开启parallel参数

const UglifyJsPlugin = require("uglifyjs-webpack-plugin")

modules.exports = {
  plugins: [
    UglifyJsPlugin: {
      warnings: false,
      parse: {},
      compress: {},
      mangle: true,
      output: null,
      toplevel: false,
      nameCache: null,
      ie8: false,
      keep_fnames: false,
    },
    parallel: true
  ]
}

方法三:terser-webpack-plugin开启parallel参数(webpack4推荐)

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: 4,
      }),
    ],
  },
};

提升构建速度

思路:将reactreact-domreduxreact-redux基础包和业务基础包打包成一个文件

方法:使用DLLPlugin进行分包,DLLReferencePluginmanifest.json引用

使用DLLPlugin进行分包

const path = require("path");
const webpack = require("webpack");

module.exports = {
  context: process.cwd(),
  resolve: {
    extensions: [".js", ".jsx", ".json", ".less", ".css"],
    modules: [__dirname, "nodu_modules"],
  },
  entry: {
    library: ["react", "react-dom", "redux", "react-redux"],
  },
  output: {
    filename: "[name].dll.js",
    path: path.resolve(__dirname, "./build/library"),
    library: "[name]",
  },
  plugins: [
    new webpack.DllPlugin({
      name: "[name]",
      path: "./build/library/[name].json",
    }),
  ],
};

webpack.config.js引入

module.exports = {
  plugins: [
    new webapck.DllReferencePlugin({
      manifest: require("./build/library/manifest.json"),
    }),
  ],
};

在项目使用了webpack4,对dll的依赖没那么大了,使用dll相对来说提升也不是特别明显,而hard-source-webpack-plugin可以极大的提升二次构建。
不过从实际的前端工厂中来说,dll还是很有必要的。对于一个团队而言,基本是使用相同的技术栈,要么react,要么vue。这时候,通常的做法都是把公共框架打成一个common bundle文件供所有项目使用。dll就可以很好的满足这种场景:将多个npm包打成一个公共包。因此团队里面的分包方案使用dll还是很有价值。

splitChunks也可以做DllPlugin的事情,但是推荐使用splitChunks去提取页面间的公共js文件,因为使用splitChunks每次去提取基础包还是需要耗费构建时间的,如果是DllPlugin只需要预编译一次,后面的基础包时间都是可以省略掉的。

提升二次构建速度

方法一:使用terser-webpack-plugin开启缓存

module.exports = {
  optimization: {
    minimizer: [
      new TerserWebpackPlugin({
        parallel: true,
        cache: true,
      }),
    ],
  },
};

方法二:使用cache-loader或者hard-source-webpack-plugin

module.exports = {
  plugins: [new HardSourceWebpackPlugin()],
};

缩小构建目标

比如babel-loader不解析node_modules

module.exports = {
  rules: {
    test: /\.js$/,
    loader: "happypack/loader",
    // exclude: "node_modules"
    /-- 或者 --/
    // include: path.resolve("src"),
  }
}

减少文件搜索范围

  • 优化resolve.modules配置(减少模块搜索层级)
  • 优化resolve.mainFields配置

    • 先查找package.jsonmain字段指定的文件 -> 查找根目录的index.js -> 查找lib/index.js
  • 优化resolve.extensions配置

    • 模块路径的查找,import xx from "index"会先找.js后缀的
  • 合理使用alias
module.exports = {
  resolve: {
    alias: {
      react: path.resolve(__dirname, "./node_modules/react/dist/react.min.js"),
    },
    modules: [path.resolve(__dirname, "node_modules")], // 查找依赖
    extensions: [".js"], // 查找模块路径
    mainFields: ["main"], // 查找入口文件
  },
};

uccs
759 声望89 粉丝

3年 gis 开发,wx:ttxbg210604