前端面试题之工程化

118 阅读33分钟

该面试题只是为了记录我自己的面试笔记,大多数摘自行内有关大佬总结,本人只是搬运工,有关链接已放置相关笔记的下面

webpack

bundle,chunk,Module

bundle

bundle由许多不同的模块生成,包含已经经过加载和编译过程的源文件的最终版本

chunk

chunk主要是在内部用于管理捆绑过程。输出是由bundle由chunk组成,其中有几种类型entry child等。通常,chunk直接与bundle对应,但是有些配置不会产生一对一的关系,例如MiniCssExtractPlugin可从chunk中抽离出css文件,单独生成bundle。生成chunk有三种方式,entry、动态加载、splitChunks抽取共有代码

Module

module是离散功能块,相对于完整程序提供了更小的接触面。一般是module可提供抽象和封装界限,使得应用程序中每个模块都具有明确的目的

Webpack 分包原理

手动分包

原理

顾名思义,就是要先手动的将公共文件先单独打包出来,成为动态链接库dll(Dynamic Link Library),生成一个资源清单(manifest.json)。至于什么是dll还是在上学时接触.net网站开发了解的,在分包中简单的理解为一个代码仓库,你要哪些东西直接去里面拿,没了解过的同学自行百度哈,这里就不过多解释了。

怎么做?该如何配置?在实践中会详细讲解,有了动态链接库资源清单后,我们就可以正常进行打包了,在正常打包的过程中,如果发现导入的路径资源清单中记录的模块名称相同,那么就会使用动态链接库中的文件,就不会将依赖打包进自己的文件中。

这里有一个小问题,这里为什么打包出来的jquerylodash要直接使用var暴露全局变量出来?

我们直接来分析下webpack的打包结果就一目了然了,使用mode=dev开发模式打包,看的更清楚一些。

// dist/dll/main.6321.js
// ....其他代码....

"dll-reference jquery":
    (function (module, exports) {
      eval("module.exports = jquery;\n\n//# sourceURL=webpack:///external_%22jquery%22?");
    }),
    
"dll-reference lodash":
    (function (module, exports) {
      eval("module.exports = lodash;\n\n//# sourceURL=webpack:///external_%22lodash%22?");
    })

可以发现并没有将jquerylodash一大堆源码打入到main.js中。而是使用了module.exports的方式导出一个全局变量。

为什么? 因为在前面生成的资源清单中有关于jquery或lodash的描述。打包过程中分析依赖凡是看到依赖的名称为jquery的,都会去资源清单的content的路径进行匹配,也就是./node_modules/jquery/dist/jquery.js

// 我们平时写的引入路径
import $ from "jquery" === import $ from "node_modules/jquery/dist/jquery.js"

node_modules/jquery/dist/jquery.js正好匹配到了jquery.manifest.json资源清单文件的content路径,所以在打包结果中jquery的源码变成了:module.exports = 该资源清单的name

// jquery清单文件
// jquery.manifest.json 
{
    "name": "jquery",
    "content": {
        "./node_modules/jquery/dist/jquery.js": {
            "id": 1,
            "buildMeta": {
                "providedExports": true
            }
        }
    }
}

之后直接在模版index.html中直接引入公共代码dist/dll/jquery.js就可以完成手动分包的目的。有些懵?没事,走一遍实践就明白了。

实践

前面提到的资源清单动态链接库如何生成呢?新建webpack.dll.config.js,配置如下:

// webpack.dll.config.js
const webpack = require("webpack");
const path = require("path");

module.exports = {
    entry: {
        //有几个公共文件就写几个入口
        jquery: ["jquery"],
        lodash: ["lodash"]
    },
    output: {
        //打包到dist/dll目录下
        filename: "dll/[name].js",
        library: "[name]"
    },
    plugins: [
        new webpack.DllPlugin({
            //资源清单的保存位置
            path: path.resolve(__dirname, "dll", "[name].manifest.json"),  
            //资源清单中,暴露的变量名
            name: "[name]"                                                 
        }),
    ]
};

然后在packages.json中添加script脚本dll: "webpack --mode=production --config webpack.dll.config.js"指明配置文件。运行npm run dll

可以看到我们想要的文件都被打包出来了,用来描述清单的manifest文件。以及打包结果中的dll文件夹。我们在来看一下打包后的jquery.jslodash.js是不是导出了一个全局变量。

哦♂ yeah~ 我们的配置生效了,公共代码分出来了,前置工作已经做好了,接下来就可以正常进行打包了。进一步完善webpack.config.js

这里安装两个新插件html-webpack-pluginclean-webpack-plugin(5.x版本已内置),在plugins中新增两个webpack.DllReferencePlugin,配置一下manifest指明清单文件。

//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const webpack = require("webpack");

module.exports = {
    mode: "production",
    entry: {
        main: "./src/index.js",
        handler: "./src/handler.js",
    },
    output: {
        filename: "[name].[hash:4].js"
    },
    plugins: [
        // 打包分析工具
        new BundleAnalyzerPlugin(),
        new CleanWebpackPlugin({
            // 不删除手动打包出来dll文件,这样每次打包就不会删除dist/dll文件了
            cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
        }),
        new HtmlWebpackPlugin({
            template: "./index.html"
        }),
        // 指明清单文件
        new webpack.DllReferencePlugin({
            manifest: require("./dll/jquery.manifest.json"),
        }),
        // 指明清单文件
        new webpack.DllReferencePlugin({
            manifest: require("./dll/lodash.manifest.json")
        })
    ]
}

然后在模版文件index.html中手动引入dist/dll下的公共代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>webpack sep packages</title>
</head>
<body>
    <script src="./dll/jquery.js"></script>
    <script src="./dll/lodash.js"></script>
</body>
</html>

然后敲下npm run prod,可以看到打包分析工具启动端口打开页面,呈现在我们眼前的就是文章前面提到的优化后打包的样子啦。至此,手动分包的原理及使用就全部ok了。但是有没有觉得有一丝繁琐?还有一个问题就是如果第三方库依赖其他的第三方库,那我是不是要先把依赖的依赖先打包,然后再打包依赖?这些就是手动分包的一些缺点,如何解决?先看看自动分包怎么做。

自动分包

原理

手动分包不同的是,自动分包无需每次都要手动先将公共代码先打包一次,它不针对某个具体的包分出去,我们只需要配置好分包策略webpack每次都会自动的完成分包的流程,更符合我们的开发方式,无需关注以后会新增哪些公共代码,所以我们一般优化基本上都用的是自动分包,一次配置,永久畅享~

事实上,webpack内部完成分包依赖的是SplitChunksPlugin来实现的,可以在官方文档中看见过去是使用CommonsChunkPlugin来实现的分包,从v4版本后就换成了SplitChunksPlugin。所以自动分包的策略实际上是对配置文件webpack.config.jsoptimization.splitChunks配置项的修改。

从图中可以看到,经过分包策略后webpack开启了一个新的chunk,对公共代码进行打包,并且在输出文件的时被提取出来形成了一个单独的文件,它是新chunk打包出来的产物。

最后在公共代码打包出来的文件内挂载一个全局变量window.webpackJsonp = [common1、common2、...],然后使用到公共代码chunk从这个数组中拿,今后有再分出来的包继续添加到数组中。最后把打包出去的模块从原始包中删除,并且修正原始包的代码。

实践

了解到大概原理其实根据分包策略开启若干个新chunk并打包形成一个单独的文件,并且挂载一个全局变量webpackJsonp来存放公共代码。如何配置?最优解是什么?那么就进入到实践环节。

基本配置

这里就不新开一个单独的项目来演示了,在原有项目中新建webpack.auto.config.js专门用来做自动分包(正常项目中直接在默认的config文件中配置就行,不用专门新建文件)。

//webpack.auto.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  mode: "production",
  entry: {
    main: "./src/index.js",
    handler: "./src/handler.js",
  },
  output: {
    filename: "[name].[hash:4].js"
  },
  optimization: {
    splitChunks: {
        //优化配置项...
    }
  },
  plugins: [
    new BundleAnalyzerPlugin(),
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template:"./index.html",
    })
  ],
};

除了最基本的配置外,主要就是对webpack提供optimization的优化配置项中的splitChunks进行修改。那么可以写哪些配置呢,我单独列了自动分包可能用到的配置项,想要拓展的朋友可以去英文文档上自行翻阅。

名称作用默认值
chunks哪些chunk需要应用分包策略async(异步chunk)
maxSize分出来的包超过了多少字节需要继续进行拆分0
minChunks模块被chunk引用了多少次才会进行分包1
minSize模块超过多少字节才会进行拆分20000
automaticNameDelimiter公共代码的新chunk名称的分隔符

1. chunks

chunks有三个取值,默认值为async,表示只对异步的chunk开启分包,就是懒加载的模块。所以如果没有懒加载的模块并不配置的话直接打包是看不什么效果的,所以一般使用all来表示对所有的chunk都要应用分包策略。

  optimization: {
    splitChunks: {
        //优化配置项...
        chunks: "all",
    }
  }

2. maxSize

maxSize表示如果某个包(包括经过分包后的包)体积大于了设定的值,那么webpack会尽最大努力继续再次分离,什么意思呢?比如开启了chunks: "all"后的打包结果是这样的。

可以看到开启了all后,jquerylodash被合并打包成了一个新的文件,而不是像手动分包那样将每个源码都映射一份出来。那么这个新的文件vendors~handler~main.2fca.js是公共代码对应的新chunk的产物。但是如果觉得160kb还是有点大,能不能再继续分一分?这就要用到maxSize 配置项了。

  optimization: {
    splitChunks: {
        //优化配置项...
        chunks: "all",
        maxSize: 50 * 1024, 
    }
  },

表示如果包大于了50kb那么webpack还会去进一步的继续尝试着分离包。运行npm run auto

能发现比刚刚多了一个文件,都是vendors开头的。点开进去之后可以看到分别是jquerylodash的源码。同上所说,打包出来的文件都去定义一个全局变量webpackJsonp,那如果存在多个文件不就覆盖了吗?

其实不然,真正每个verdors文件第一行是(window.webpackJsonp = window.webpackJsonp || []).push(序号索引),按照分出来的顺序,序号索引就是0、1、...,这样就无论分几个包,最后其实还都是会push到同一个window.webpackJsonp数组中。

细心的同学应该会发现一个问题就是明明设置的是50kb呀,为什么拆分出来的两个文件都还是大于设置的值呢?仔细看前面说的如果超过了设定值,webpack会尽最大努力继续分离,但是,是以模块为基础的,再怎么分,都不可能把原来的一个整体的模块直接打乱掉代码分割出去,最小的单元就是一个模块。所以最小也只能jquerylodash完整的代码再单独各分出去。

走到这大家就会发现有点不对劲,再怎么分总体积是不变的,只不过是拆分了很多份,有些时候反而对性能是负提升。但是如果某个浏览器支持多线程请求的话,可能会对性能有帮助。大家对这个属性仁者见仁,智者见智就好,谨慎使用

3. minChunks

表示一个chunk被引用了多少次,才会进行分包。默认值是1,只要被引用过就要进行分包优化,这个配置就很简单,可以试一试设置为一个大点的值,那么就不会进行分包了。

    optimization: {
        splitChunks: {
            //优化配置项...
            chunks: "all",
            maxSize: 60 * 1024,
            minChunks: 20,
        }
    },

注意: 这里设置的minChunks是针对于我们自己写的一些公共模块想要进行分包处理的最小引用数,针对于引用依赖node_modules中的文件是不生效的,因为存在缓存组单独针对node_modules的规则,下面会说到,所以这个配置对第三方库是不生效的!

4. minSize

此配置表示当一个达到多大体积才会进行分包,默认值为20000。对第三方库同样生效。如果设置一个很大的值,那么就都不会进行分包。

    optimization: {
        splitChunks: {
            //优化配置项...
            chunks: "all",
            maxSize: 60 * 1024,
            minChunks: 20,
            minSize: 1000000,
        }
    },

5. automaticNameDelimiter

这个配置项十分简单,只是修改新chunk生成的文件名中的分隔符,默认值~。比如现在改成---,那么新chunk的文件名分隔符就是vendors---handler---main.b457.js

    optimization: {
        splitChunks: {
            //优化配置项...
            chunks: "all",
            // maxSize: 60 * 1024,
            // minChunks: 20,
            minSize: 20000,
            automaticNameDelimiter: "---"
        }
    },

缓存组

前面提到了minChunks配置对第三方库不生效,是因为有缓存组的存在,那么缓存组到底是什么?在之前设置的配置都是基于全局的,实际上,分包策略是基于缓存组的,每一个缓存组都是一套单独的分包策略,可以设置不同的缓存组来针对不同的文件进行分包webpack默认开启了两个缓存组,即cacheGroups

    optimization: {
        splitChunks: {
            //优化配置项...
            chunks: "all",
            cacheGroups: {
                    vendors: {    //属性名即是缓存的名称,改成common看看
                    test: /[\/]node_modules[\/]/, 
                    priority: 2
                },
                default: {        //默认缓存组设置,会覆盖掉全局设置
                    minChunks: 2,
                    priority: 1,
                    reuseExistingChunk: true
                }
            }
        }
    },

cacheGroups中,每一个对象就是一个缓存组分包策略,属性名便是缓存组的名称,对象中设置的值可以继承全局设置的属性(如minChunks等),也存在只有缓存组独特的属性,比如test(匹配模块名称规则)、priority(缓存组的优先级),reuseExistingChunk(重用已经被分离出的chunk)。

webpack会根据缓存组的优先级(priority)来依次处理每个缓存组,被缓存组处理过的模块不需要再次分包。所以前面为什么minChunks对第三方库没有生效,是因为有默认缓存组的存在,已经针对node_modules定了一套独有的分包策略。

可以把默认的vendors的属性名改成其他的,比如common,那么打包结果中,经过缓存组处理过的node_modules分出来的包就是以common开头了。

正常情况下,缓存组对我们来说没有太多意义,webpack提供的默认缓存组就已经够用了。但是大家可以想一想,它其实还可以用来做对公共样式的抽离,比如两个css文件有相同的样式,那么我们可以用test匹配css文件来设置公共样式的打包。这个就属于拓展了,有兴趣的同学可以试一试找我交流~

总结

两者分包的区别在于手动分包可以极大的提升编译构建速度,但是使用起来比较繁琐,一旦今后有新增的公共代码都需要手动去处理。自动分包的话可以极大的提高开发效率,只要配置好分包策略后就一劳永逸了。深入了解两者分包的原理及优缺点,还有一些比较冷门的点后,就已经基本拿捏了,斯国一!遇到相同的业务场景时,可以选择合适的分包手段来进行优化。

但是分包也是有局限性的,比如已经分到不能再分的时候,就只能通过代码压缩tree shaking懒加载等等手段来继续优化了,本文就不过多赘述了,以后可以出个续集。最后,谢谢大家的观看,对本文内容有异议或交流欢迎评论~

链接:分包王!🧨优化项目体积减少20%! - 掘金

webpack中的hash、chunkhash和contenthash

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容发生改变的话,那么对应文件hash值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从原服务器上拉取对应数据,进而更新本地缓存。但是实际使用时,这三种hash计算还是有一定区别。

hash

hash是对webpack整个一次构建而言,在webpack构建中,文件都会带上对应的MD5值,构建生成的文件hash值都是一样的。如果出口是hash,那么一旦针对项目中任何一个文件的修改,都会构建整个项目,重新获取hash值。如果有目的性的缓存就会失败。

chunkhash(js)

chunkhash的范围可以针对某个模块而言,它会从入口出发,对依赖文件进行解析,构建对应的chunk和hash值。一般的使用是在生产环境对公共库和程序入口文件单独抽离开,单独打包构建,用chunkhash的方式对这些打包后的文件带上相应hash值。在线上,只要公共库和入口没变,其hash值就不会改变,从而达到缓存的目的。

contenthash(css)

contenthash表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样。在项目中,通常做法是把项目中css都抽离出对应的css文件来加以引用。

原文链接:webpack4中hash、chunkhash和contenthash三者的区别_bubbling_coding的博客-CSDN博客

原文链接:webpack中的hash、chunkhash和contenthash_Lawliet_ZMZ的博客-CSDN博客

webpack loader

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

  1. 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
  2. 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码

一、webpack的打包原理

  1. 识别入口文件
  2. 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
  3. webpack做的就是分析代码,转换代码,编译代码,输出代码
  4. 最终形成打包后的代码

二、什么是loader

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

  1. 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
  2. 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码

三、什么是plugin

在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

四、loader和plugin的区别

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程

plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

webpack loader

webpack 本身只能处理 JavaScript 和 JSON 文件,而 loader 为 webpack 添加了处理其他类型文件的能力。loader 将其他类型的文件转换成有效的 webpack modules(如 ESmodule、CommonJS、AMD),webpack 能消费这些模块,并将其添加到依赖关系图中。

loader 本质上是一个函数,该函数对接收到的内容进行转换,返回转换后的结果。

常见的 loader 有:

  • raw-loader:加载文件原始内容。
  • file-loader:将引用文件输出到目标文件夹中,在代码中通过相对路径引用输出的文件。
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式将文件内容注入到代码中。
  • babel-loader:将 ES 较新的语法转换为浏览器可以兼容的语法。
  • style-loader:将 CSS 代码注入到 JavaScript 中,通过 DOM 操作加载 CSS。
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性。

使用 loader 的方式主要有两种:

  1. 在 webpack.config.js 文件中配置,通过在 module.rules 中使用 test 匹配要转换的文件类型,使用 use 指定要使用的 loader。
module.exports = {
  module: {
    rules: [{ test: /.ts$/, use: "ts-loader" }],
  },
};
  1. 内联使用

import Styles from "style-loader!css-loader?modules!./styles.css";

参考链接 谈下 webpack loader 的机制 | HZFE - 剑指前端 Offer

Tree-shaking 核心原理

一、treeshaking原理

Tree-shaking的原理是利用ES6模块化规范的特性,在编译时通过静态分析代码,识别出未被使用的代码(dead code)并在打包时去除。具体来说,比如在代码中引入了一个模块,但实际上只使用了其中的一部分代码,通过静态分析可以识别出未被使用的代码,删掉这部分代码从而减小bundle的大小。

Tree-shaking的实现借助了ES6模块化的特性,ES6模块化规范是静态的,也就是说,在编译时就可以确定模块的依赖关系,因此可以通过静态分析来判断哪些代码没有被使用。

二、谢可寅shaking

treeshaking的发明人是谢可寅,tree-shaking这个词的由来其实是源于webpack社区的。webpack的开发者认为把未使用的代码从打包结果中摇掉很像树上的果实,因此用tree-shaking来形容这个过程。

三、treeshaking配置

对于webpack用户来说,使用tree-shaking非常方便,只需要在webpack配置文件中开启optimization.minimize选项就可以了。optimization.minimize选项默认会开启tree-shaking,并使用内置的UglifyJsPlugin压缩代码,从而生成一个更小的bundle。

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    minimize: true,
  },
};

需要注意的是,只有引入ES6 module的代码才能启用tree-shaking。对于CommonJS或AMD模块化的代码,由于不带有静态分析的特性,无法利用tree-shaking功能。

四、treeshaking怎么读

对于英文不太好的开发者来说,"tree-shaking"这个词还是挺难理解的。它到底是什么意思呢?

实际上,tree-shaking这个词的意思可以通过拆分词汇来理解。Tree是树的意思,是一种数据结构。Shake是摇动的意思,可以引申为“震动”。因此,treeshaking可以理解为“震动树”(摇动树的果实掉落下来的意思)。

五、treeshaking不生效

虽然tree-shaking看上去很美好,但实际上开发者们会发现有些时候它并不会生效。有以下一些情况可能导致tree-shaking不生效。

  1. 在代码中使用了process.env.NODE_ENV变量,会导致webpack将整个模块打包进去。
  2. 有些库会使用类似于全局注册的方式注册组件,比如Ant Design Vue的组件,这会导致tree-shaking失效,因为在编译时无法知道哪些组件被使用。
  3. 使用动态导入(如import())时,由于要在运行时决定使用哪个模块,编译时不会对这部分代码进行分析。
  4. 代码中使用了webpack的require.ensure()或require.include()等动态加载模块的方式。

需要注意的是,尽管使用tree-shaking会减小bundle的大小,但并不一定会提升应用程序的性能。这是因为虽然tree-shaking会减小bundle的大小,但整个应用程序的总体积可能并没有得到明显的减少,因为一些库的体积可能还是非常大。

六、treeshaking副作用

虽然tree-shaking在很多情况下可以减小bundle的大小,但使用不当也会带来一些副作用。

  1. 可读性差。优化过度的代码可能会失去可读性,这会给维护和代码优化带来困难。
  2. 可能会破坏代码的正确性。对一些代码进行tree-shaking可能会破坏代码的正确性,导致应用程序无法正常运行。
  3. 代码冗余。有时候对代码进行tree-shaking会导致生成更多的代码,这可能会导致bundle的大小反而更大。

七、treeshaking对怎样的包不生效

treeshaking并不是万能的,对某些类型的包并不会起作用。比如:

  1. 对于只有一个入口文件的包或库,tree-shaking会对整个文件进行编译,而不是只编译其中被使用的部分。
  2. 对于内置模块(比如fs、http等),由于它们没有使用ES6的模块化规范,所以tree-shaking并不会起作用。

最后,需要注意一点的是,虽然tree-shaking非常方便,但也不是解决所有性能问题的银弹。代码优化应该是一个综合性的过程,需要综合考虑代码的质量、代码的体积、代码的可读性以及代码的运行效率等多个方面。

完整代码示例

// index.js
import { sum } from './math';

console.log(sum(1, 2));


// math.js
export function sum(a, b) {
  return a + b;
}

export function minus(a, b) {
  return a - b;
}

从多个方面详解tree-shaking_笔记大全_设计学院

常见loader

thread-loader配置参数

use: [
  {
    loader: "thread-loader",
    // 有同样配置的 loader 会共享一个 worker 池(worker pool)
    options: {
      // 产生的 worker 的数量,默认是 cpu 的核心数
      workers: 2,

      // 一个 worker 进程中并行执行工作的数量
      // 默认为 20
      workerParallelJobs: 50,

      // 额外的 node.js 参数
      workerNodeArgs: ['--max-old-space-size', '1024'],

      // 闲置时定时删除 worker 进程
      // 默认为 500ms
      // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
      poolTimeout: 2000,

      // 池(pool)分配给 worker 的工作数量
      // 默认为 200
      // 降低这个数值会降低总体的效率,但是会提升工作分布更均一
      poolParallelJobs: 50,

      // 池(pool)的名称
      // 可以修改名称来创建其余选项都一样的池(pool)
      name: "my-pool"
    }
  },
  "expensive-loader"
]

image-webpack-loader

chainWebpack: config => {
  if(IS_PROD){
    config.module
      .rule('images')
        .exclude.add(resolve('src/assets/icons')) // 排除icons目录,这些图标已用 svg-sprite-loader 处理,打包成 svg-sprite 了
        .end()
        .use('url-loader')
          .tap(options => ({
            limit: 10240, // 稍微改大了点
            fallback: {
              loader: require.resolve('file-loader'),
              options: {
                // 在这里修改file-loader的配置
                // 直接把outputPath的目录加上,虽然语义没分开清晰但比较简洁
                name: 'static/img/[name].[hash:8].[ext]'
                // 从生成的资源覆写 filename 或 chunkFilename 时,assetsDir 会被忽略。
                // 因此别忘了在前面加上静态资源目录,即assetsDir指定的目录,不然会直接在dist文件夹下
                // outputPath: 'static/img' 
              }
            }
        }))
        .end()
        .use('image-webpack-loader')
          .loader('image-webpack-loader')
          .options({
            mozjpeg: { progressive: true, quality: 50 }, // 压缩JPEG图像
            optipng: { enabled: true }, // 压缩PNG图像
            pngquant: { quality: [0.5, 0.65], speed: 4 }, // 压缩PNG图像
            gifsicle: { interlaced: false } // 压缩GIF图像
          })
          .end()
          .enforce('post') // 表示先执行配置在下面那个loader,即image-webpack-loader
  }
}

vite

Vite 和 webpack

  • 基础概念不同

    • webpack是一个模块打包器,它可以把许多不同类型的模块和资源文件打包为静态资源。它具有高度的可配置性,可以通过插件和loader扩展其功能。
    • vite,由Vue.js作者尤雨溪开发并维护,是一个基于浏览器原生 ES imports 的开发服务器。它能够提供丰富的功能,如快速冷启动、即时热更新和真正的按需编译等。
  • 编译方式不同

    • webpack在编译过程中,会将所有模块打包为一个bundle.js文件,然后再运行这个文件。
    • 而vite在开发模式下,没有打包的步骤,它利用了浏览器的ES Module Imports特性,只有在真正需要时才编译文件。在生产模式下,vite使用Rollup进行打包,提供更好的tree-shaking,代码压缩和性能优化。
  • 开发效率不同

    • webpack的热更新是全量更新,即使修改一个小文件,也会重新编译整个应用,这在大型应用中可能会导致编译速度变慢。
    • vite的热更新是增量更新,只更新修改的文件,所以即使在大型应用中也能保持极快的编译速度。
  • 扩展性不同

    • webpack有着成熟的插件生态,几乎可以实现任何你想要的功能,扩展性非常强。
    • vite虽然也支持插件,但相比webpack的生态,还有一些距离。
  • 应用场景不同

    • webpack由于其丰富的功能和扩展性,适合于大型、复杂的项目。

    • 而vite凭借其轻量和速度,更适合于中小型项目和快速原型开发。

参考链接 vite和webpack的区别 • Worktile社区

Vite 相关(兼容性)

Vite 配置篇:日常开发掌握这些配置就够了! - 掘金

Antd 与 vite 兼容性问题

兼容低版本

  • 如果要支持低版本浏览器可以使用官方提供的插件 @vitejs/plugin-legacy,plugin-legacy 会将代码打包两套
  • 如果浏览器支持
  • 如果浏览器不支持ESM

Vite 性能篇:掌握这些优化策略,一起纵享丝滑! - 掘金

babel

polyfill兼容

用了babel还需要polyfill吗??? - 掘金

打包

Npm 打包原理

1. npm 模块安装机制:

  • 发出npm install命令

  • 查询node_modules目录之中是否已经存在指定模块

    • 若存在,不再重新安装

    • 若不存在

      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

2. npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

执行工程自身 preinstall

当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

确定首层依赖模块

首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。

模块扁平化(dedupe)

上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules
-- foo
---- lodash@version1
-- bar
---- lodash@version2
假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:
node_modules
-- foo
-- bar
-- lodash(保留的版本为兼容版本)
假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:
node_modules
-- foo
-- lodash@version1
-- bar
---- lodash@version2

安装模块

这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。

参考 npm 模块安装机制简介

详解npm的模块安装机制

npm install的实现原理

参考链接:npm/yarn lock真香

参考链接:第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块? · Issue #22 · Advanced-Frontend/Daily-Intervie

Npm 版本锁定原理

package-lock.json相当于本次install的一个快照,它不仅记录了package.json指明的直接依赖的版本,也记录了间接依赖的版本。

semver通配符

符号^:表示安装不低于该版本的应用,但是大版本号需相同,例如:vuex: "^3.1.3",3.1.3及其以上的3.x.x都是满足的。

符号~:表示安装不低于该版本的应用,但是大版本号和小版本号需相同,例如:vuex: "^3.1.3",3.1.3及其以上的3.1.x都是满足的。

无符号:无符号表示固定版本号,例如:vuex: "3.1.3",此时一定是安装3.1.3版本

npm 源

项目根目录下创建一个 .npmrc 的文件,在其中指定 npm 源,以保证团队的成员使用的是统一的 npm 源

npm ci

是根据 package-lock.json 去安装确定的依赖,package.json 只是用来验证是不是有不匹配的版本,假设 package-lock.json 中存在一个确定版本的依赖 A,如果 package.json 中不存在依赖 A 或者依赖 A 版本和 lock 中不兼容,npm ci 就会报错。

原文链接:npm的package.json和package-lock.json更新策略_semver-range version_码飞飞的博客-CSDN博客

Yarn 打包原理

yarn 的出现主要目标是解决上面描述的由于语义版本控制而导致的 npm 安装的不确定性问题。虽然可以使用 npm shrinkwrap 来实现可预测的依赖关系树,但它并不是默认选项,而是取决于所有的开发人员知道并且启用这个选项。 yarn 采取了不同的做法。每个 yarn 安装都会生成一个类似于npm-shrinkwrap.jsonyarn.lock 文件,而且它是默认创建的。除了常规信息之外,yarn.lock 文件还包含要安装的内容的校验和,以确保使用的库的版本相同。

yarn 的主要优化

yarn 的出现主要做了如下优化:

  • 并行安装:无论 npm 还是 yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 yarn 是同步执行所有任务,提高了性能。
  • 离线模式:如果之前已经安装过一个软件包,用 yarn 再次安装时之间从缓存中获取,就不用像 npm 那样再从网络下载了。
  • 安装版本统一:为了防止拉取到不同的版本,yarn 有一个锁定文件 (lock file) 记录了被确切安装上的模块的版本号。每次只要新增了一个模块,yarn 就会创建(或更新)yarn.lock 这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。
  • 更好的语义化yarn 改变了一些 npm 命令的名称,比如 yarn add/remove,比 npm 原本的 install/uninstall 要更清晰。

安装依赖树流程

  1. 执行工程自身 preinstall。 当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  2. 确定首层依赖。 模块首先需要做的是确定工程中的首层依赖,也就是 dependenciesdevDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  3. 获取模块。 获取模块是一个递归的过程,分为以下几步:

    1. 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic versionsemver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.jsonpackage-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 package.json 中某个包的版本是 ^1.1.0npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
    2. 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
    3. 查找该模块依赖,如果有依赖则回到第 1 步,如果没有则停止。
  4. 模块扁平化(dedupe)。 上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadshB 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。yarn 和从 npm5 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

  5. 安装模块。 这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstallinstallpostinstall 的顺序)。

  6. 执行工程自身生命周期。 当前 npm 工程如果定义了钩子此时会被执行(按照 installpostinstallprepublishprepare 的顺序)。

举例说明

插件 htmlparser2@^3.10.1dom-serializer@^0.2.2 都有使用了 entities 依赖包,不过使用的版本不同,同时我们自己安装一个版本的 entities 包。具体如下:

--htmlparser2@^3.10.1
  |--entities@^1.1.1

--dom-serializer@^0.2.2
  |--entities@^2.0.0

--entities@^2.1.0

参考链接:npm/yarn lock真香

jencks docker