Javascript is required

前端工程化

2024年6月09日 16:57
301
阅读150分钟

前端工程化

一.Webpack

1.关于Webpack

Webpack到底是什么?

  • 我们先来看一下官方的解释:

    image-20231123111704804

  • webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序;

  • 我们来对上面的解释进行拆解:

    • 打包bundler:webpack可以将帮助我们进行打包,所以它是一个打包工具
    • 静态的static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
    • 模块化module:webpack默认支持各种模块化开发,ES Module、CommonJS、AMD等;
    • **现代的modern:**我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了webpack的出现和发展;

    image-20231123111744523

  • 前端我们已经学习了webpack的基础知识,这里我简单做一个回顾。

    • https://xxpromise.gitee.io/webpack5-docs/

    • image-20231123111813156

    • Mode配置

      • Mode配置选项,可以告知webpack使用相应模式的内置优化:

        • 默认值是 production(什么都不设置的情况下);
        • 可选值有:'none' | 'development' | 'production';
      • 配置有什么区别?

        • image-20231123111855473
      • image-20231123111914650

        大体就是设置mode后会默认添加如下的配置

        devtool:设置source-map用的

2.模块化原理

  • Webpack打包的代码,允许我们使用各种各样的模块化,但是最常用的是CommonJS、ES Module。

    • 那么它是如何帮助我们实现了代码中支持模块化呢?
  • 我们来研究一下它的原理,包括如下原理:

    • CommonJS模块化实现原理;

    • ES Module实现原理;

    • CommonJS加载ES Module的原理;

    • ES Module加载CommonJS的原理;

  • 这里不再给出代码,查看课堂代码的注释解析;

3.source-map

https://xxpromise.gitee.io/webpack5-docs/senior/enhanceExperience.html#sourcemap

1.认识source-map

  • 我们的代码通常运行在浏览器上时,是通过打包压缩的:
    • 也就是真实跑在浏览器上的代码,和我们编写的代码其实是有差异的;
    • 比如ES6的代码可能被转换成ES5
    • 比如对应的代码行号、列号在经过编译后肯定会不一致;
    • 比如代码进行丑化压缩时,会将编码名称等修改;
    • 比如我们使用了TypeScript等方式编写的代码,最终转换成JavaScript
  • 但是,当代码报错需要调试时(debug),调试转换后的代码是很困难的
  • 但是我们能保证代码不出错吗?不可能
  • 那么如何可以调试这种转换后不一致的代码呢?答案就是source-map
    • source-map是从已转换的代码映射到原始的源文件
    • 使浏览器可以重构原始源并在调试器中显示重建的原始源

2.使用source-map

  • 如何可以使用source-map呢?两个步骤:

    • 第一步:根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成source-map;

      module.exports = {
        // 其他省略
        mode: "production",
        devtool: "source-map",
      };
      
      

      使用source-map

    • 第二步:在转换后的代码,最后添加一个注释,它指向sourcemap;

      image-20231123184923473

      //# sourceMappingURL=common.bundle.js.map
      
      • 浏览器会根据我们的注释,查找相应的source-map,并且根据source-map还原我们的代码,方便进行调试。
  • 在Chrome中,我们可以按照如下的方式打开source-map:

    image-20231123113811659

3.分析source-map

  • 最初source-map生成的文件大小是原始文件的10倍,第二版减少了约50%,第三版又减少了50%,所以目前一个133kb的文件, 最终的source-map的大小大概在300kb。

  • 目前的source-map长什么样子呢?

    • version:当前使用的版本,也就是最新的第三版;

    • sources:从哪些文件转换过来的source-map和打包的代码(最初始的文件);

      标注原始文件的具体位置路径

      image-20231123185536508
    • names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名称);

    • mappings:source-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQ(veriable-length quantity可变 长度值)编码;

    • file:打包后的文件(浏览器加载的文件);

    • sourceContent:转换前的具体代码信息(和sources是对应的关系);

    • sourceRoot:所有的sources相对的根目录;

      image-20231123190050581

    image-20231123114316680

4.生成source-map

  • 如何在使用webpack打包的时候,生成对应的source-map呢?

    • webpack为我们提供了非常多的选项(目前是26个),来处理source-map;
    • https://webpack.docschina.org/configuration/devtool/
    • 选择不同的值,生成的source-map会稍微有差异,打包的过程也会有性能的差异,可以根据不同的情况进行选择;
  • 下面几个值不会生成source-map

    • false:不使用source-map,也就是没有任何和source-map相关的内容。

    • noneproduction模式下的默认值(什么值都不写) ,不生成source-map。

    • evaldevelopment模式下的默认值,不生成source-map

      • 但是它会在eval执行的代码中,添加 //# sourceURL=;

      • 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码;

      • image-20231123114431884

      eval的效果

5.devtool全解

1.eval
  • development模式下的默认值,不生成source-map,一般都使用在 development 情况下

    • 构建速度快

    • 但是它会在eval执行的代码中,添加 //# sourceURL=;

    • 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码;

      • image-20231123114431884

        通过eval一些注释等等时,也能还原代码,不过没有那么准确

2.source-map
  • source-map:

    • 一般使用在 生产模式下(production)
    • 生成一个独立的source-map文件,并且在bundle文件中有一个注释,指向source-map文件;
  • bundle文件中有如下的注释:

    • 开发工具会根据这个注释找到source-map文件,并且解析;

      //# sourceMappingURL=bundle.js.map
      

      image-20231123191823207

3.eval-source-map
  • eval-source-map:会生成sourcemap,但是source-map是以DataUrl添加到eval函数的后面

    • 生产模式

    image-20231123191920071

4.inline-source-map
  • inline-source-map:会生成sourcemap,但是source-map是以DataUrl添加到bundle文件的后面

    • 生产模式

    image-20231123192042251

5.cheap-source-map
  • cheap-source-map

    • 开发模式用

    • 会生成sourcemap,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping)

    • 因为在开发中,我们只需要行信息通常就可以定位到错误了

      image-20231123192445869

6.cheap-module-source-map
  • cheap-module-source-map

    • 会生成sourcemap,类似于cheap-source-map,但是对源自loader的sourcemap处理会更好
  • 这里有一个很模糊的概念:对源自loader的sourcemap处理会更好,官方也没有给出很好的解释

    • 其实是如果loader对我们的源码进行了特殊的处理,比如babel;

      例如源代码中是有一些空行的,经过babel处理后,将空行删了,此时浏览器经过cheap-source-map处理后,还原的代码是没有空行的,导致报错位置不准确

  • 如果我这里使用了babel-loader

    • 可以先按照我的babel配置演练;

      image-20231123192738361

  • cheap-source-map和cheap-module-source-map的区别:

    image-20231123192804905

6.hidden-source-map
  • hidden-source-map:

    • 生产环境

    • 会生成sourcemap,但是不会对source-map文件进行引用

    • 相当于删除了打包文件中对sourcemap的引用注释

      // 被删除掉的
      //# sourceMappingURL=bundle.js.map
      
  • 如果我们手动添加进来,那么sourcemap就会生效了

7.nosources-source-map
  • nosources-source-map

    • 会生成sourcemap,但是生成的sourcemap只有错误信息的提示,不会生成源代码文件;
  • 正确的错误提示:

    image-20231123194622319

  • 点击错误提示,无法查看源码:

    image-20231123194633276

6.多个值的组合

  • 事实上,webpack提供给我们的26个值,是可以进行多组合的。

    • 组合的规则如下:

      • inline-|hidden-|eval:三个值时三选一;

      • nosources:可选值;

      • cheap可选值,并且可以跟随module的值;

        [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
        
  • 那么在开发中,最佳的实践是什么呢?

    • 开发阶段:推荐使用 source-map或者cheap-module-source-map
      • 这分别是vue和react使用的值,可以获取调试信息,方便快速开发;
    • 测试阶段:推荐使用 source-map或者cheap-module-source-map
      • 测试阶段我们也希望在浏览器下看到正确的错误提示;
    • 发布阶段:false、缺省值(不写)

4.Babel

1.关于babel

  • 事实上,在开发中我们很少直接去接触babel,但是babel对于前端开发来说,目前是不可缺少的一部分

    • 开发中,我们想要使用ES6+的语法,想要使用TypeScript,开发React项目,它们都是离不开Babel的;
    • 所以,学习Babel对于我们理解代码从编写到线上的转变过程直观重要;
    • 了解真相,你才能获得真知的自由!
  • 那么,Babel到底是什么呢

    • Babel是一个工具链,主要用于旧浏览器或者环境中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript;

    • 包括:语法转换源代码转换Polyfill实现目标环境缺少的功能等;

      image-20231123200133143

2.命令行使用

  • babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配置来单独使用。

  • 如果我们希望在命令行尝试使用babel,需要安装如下库:

    • @babel/core:babel的核心代码,必须安装;

    • @babel/cli:可以让我们在命令行使用babel;

      npm install @babel/cli @babel/core
      
  • 使用babel来处理我们的源代码:

    • src:是源文件的目录;

    • --out-dir:指定要输出的文件夹dist;

      npx babel src --out-dir dist
      

3.使用插件及预设

  • 比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件

    npm install @babel/plugin-transform-arrow-functions -D
    
    npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions 
    
  • 查看转换后的结果:我们会发现 const 并没有转成 var

    • 这是因为 plugin-transform-arrow-functions,并没有提供这样的功能;

      ()=>

    • 我们需要使用 plugin-transform-block-scoping 来完成这样的功能; 插件的使用

      const

    npm install @babel/plugin-transform-block-scoping -D 
    
     npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping ,@babel/plugin-transform-arrow-functions
    

image-20231123210549975

image-20231123210600427

  • 但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset)

    • 后面我们再具体来讲预设代表的含义;

    • 安装@babel/preset-env预设:

      npm install @babel/preset-env -D
      
    • 执行如下命令:

      npx babel src --out-dir dist --presets=@babel/preset-env
      

      会自动开启严格模式

4.底层原理

  • babel是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?

    • 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
    • 就是编译器,事实上我们可以将babel看成就是一个编译器
    • Babel编译器的作用 就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码
  • Babel也拥有编译器的工作流程:

    • 解析阶段(Parsing)
      1. 词法分析
      2. 语法分析
    • 转换阶段(Transformation)
      • AST 抽象语法树 可以使用plugins将原来的AST转换为另一个AST
    • 生成阶段(Code Generation)

    image-20231123200818415

  • 当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:

    image-20231123200836052

  • https://github.com/jamiebuilds/the-super-tiny-compiler

    主要用js代码演示编译器的流程,值得看

    欢迎使用超级微型编译器!

    这是用易于阅读的 JavaScript 编写的现代编译器的所有主要部分的超简化示例。

    image-20231123201030051

5.babel-loader

  • 在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用的,比如在webpack中。

  • 那么我们就需要去安装相关的依赖

    • 如果之前已经安装了@babel/core,那么这里不需要再次安装;

      npm install babel-loader @babel/core
      
  • 我们可以设置一个规则,在加载js文件时,使用我们的babel:

    image-20231123201256889

  • 我们必须指定使用的插件才会生效

    image-20231123201321281

6.babel-preset

  • 如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset, webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。

  • 比如常见的预设有三个:

    • env
    • react
    • TypeScript
  • 安装preset-env:

    npm install @babel/preset-env
    

    image-20231123201414865

7.浏览器兼容性

  • 我们来思考一个问题:开发中,浏览器的兼容性问题,我们应该如何去解决和处理

    • 当然这个问题很笼统,这里我说的兼容性问题不是指屏幕大小的变化适配
    • 我这里指的兼容性是针对不同的浏览器支持的特性:比如css特性、js语法之间的兼容性;
  • 我们知道市面上有大量的浏览器:

    • 有Chrome、Safari、IE、Edge、Chrome for Android、UC Browser、QQ Browser等等;
    • 它们的市场占率是多少?我们要不要兼容它们呢?
  • 其实在很多的脚手架配置中,都能看到类似于这样的配置信息:

    • 这里的百分之一,就是指市场占有率

      > 1%
      last 2 versions
      not dead
      
  • 但是在哪里可以查询到浏览器的市场占有率呢?

8.browserslist

1.关于browserslist工具
  • 但是有一个问题,我们如何可以在css兼容性和js兼容性下共享我们配置的兼容性条件呢?

    • 就是当我们设置了一个条件: > 1%;
    • 我们表达的意思是css要兼容市场占有率大于1%的浏览器,js也要兼容市场占有率大于1%的浏览器
    • 如果我们是通过工具来达到这种兼容性的,比如我们讲到的postcss-preset-env、babel、autoprefixer等
  • 如何可以让他们共享我们的配置呢?

    • 这个问题的答案就是Browserslist
  • Browserslist是什么?Browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置

  • 我们可以编写类似于这样的配置:

    > 1%
    last 2 versions
    not dead
    
  • 那么之后,这些工具会根据我们的配置来获取相关的浏览器信息,以方便决定是否需要进行兼容性的支持:

    • 条件查询使用的是caniuse-lite的工具,这个工具的数据来自于caniuse的网站上;

      image-20231123202359987

2.browserslist编写规则

编写规则1:

  • 那么在开发中,我们可以编写的条件都有哪些呢?(加粗部分是最常用的
  • defaults:Browserslist的默认浏览器(> 0.5%, last 2 versions, Firefox ESR, not dead)。
  • 5%:通过全局使用情况统计信息选择的浏览器版本。 >=,<和<=工作过。
    • 5% in US:使用美国使用情况统计信息。它接受两个字母的国家/地区代码。
    • > 5% in alt-AS:使用亚洲地区使用情况统计信息。有关所有区域代码的列表,请参见caniuse-lite/data/regions
    • > 5% in my stats:使用自定义用法数据。
    • > 5% in browserslist-config-mycompany stats:使用 来自的自定义使用情况数据browserslist-config-mycompany/browserslist-stats.json。
    • cover 99.5%:提供覆盖率的最受欢迎的浏览器。
    • cover 99.5% in US:与上述相同,但国家/地区代码由两个字母组成。
    • cover 99.5% in my stats:使用自定义用法数据。
  • dead:24个月内没有官方支持或更新的浏览器。现在是IE 10,IE_Mob 11,BlackBerry 10,BlackBerry 7, Samsung 4和OperaMobile 12.1。
  • last 2 versions:每个浏览器的最后2个版本。
    • last 2 Chrome versions:最近2个版本的Chrome浏览器。
    • last 2 major versions或last 2 iOS major versions:最近2个主要版本的所有次要/补丁版本。

编写规则2:

  • node 10和node 10.4:选择最新的Node.js10.x.x 或10.4.x版本。
    • current node:Browserslist现在使用的Node.js版本。
    • maintained node versions:所有Node.js版本,仍由 Node.js Foundation维护。
  • iOS 7:直接使用iOS浏览器版本7
    • Firefox > 20:Firefox的版本高于20 >=,<并且<=也可以使用。它也可以与Node.js一起使用。
    • ie 6-8:选择一个包含范围的版本。
    • Firefox ESR:最新的[Firefox ESR]版本。
    • PhantomJS 2.1和PhantomJS 1.9:选择类似于PhantomJS运行时的Safari版本。
  • extends browserslist-config-mycompany:从browserslist-config-mycompanynpm包中查询 。
  • supports es6-module:支持特定功能的浏览器。
    • es6-module这是“我可以使用” 页面feat的URL上的参数。有关所有可用功能的列表,请参见 。caniuse-lite/data/features
  • **browserslist config:在Browserslist配置中定义的浏览器。**在差异服务中很有用,可用于修改用户的配置,例如 browserslist config and supports es6-module。
  • since 2015或last 2 years:自2015年以来发布的所有版本(since 2015-03以及since 2015-03-10)。
  • unreleased versions或unreleased Chrome versions:Alpha和Beta版本。
  • not ie <= 8:排除先前查询选择的浏览器。
3.命令行使用
  • 我们可以直接通过命令来查询某些条件所匹配到的浏览器

    npx browserslist ">1%, last 2 version, not dead"
    

    image-20231123202947142

4.配置browserslist
  • 我们如何可以配置browserslist呢?两种方案:

    • 方案一:在package.json中配置;
    • 方案二:单独的一个配置文件.browserslistrc文件;
  • 方案一:package.json配置:

    image-20231123203030696

  • 方案二:.browserslistrc文件

    image-20231123203037815

  • 如果没有配置,那么也会有一个默认配置:

    image-20231123203102673

  • 我们编写了多个条件之后,多个条件之间是什么关系呢

    image-20231123203110133

9.设置目标浏览器 targets

  • 我们最终打包的JavaScript代码,是需要跑在目标浏览器上的,那么如何告知babel我们的目标浏览器呢?

    • browserslist工具
    • target属性
  • 之前我们已经使用了browserslist工具,我们可以对比一下不同的配置,打包的区别:

    image-20231123203244765

  • 我们也可以通过targets来进行配置

    image-20231123203314415

  • 那么,如果两个同时配置了,哪一个会生效呢?

    • 配置的targets属性会覆盖browserslist
    • 但是在开发中,更推荐通过browserslist来配置,因为类似于postcss工具,也会使用browserslist,进行统一浏览器的适配;

10.Stage-X

  • 要了解Stage-X,我们需要先了解一下TC39的组织:

    • TC39是指技术委员会(Technical Committee)第 39 号;
    • 它是 ECMA 的一部分,ECMA 是 “ECMAScript” 规范下的 JavaScript 语言标准化的机构;
    • ECMAScript 规范定义了 JavaScript 如何一步一步的进化、发展;
  • TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage

    • Stage 0:strawman(稻草人),任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的"稻草人";
    • Stage 1:proposal(提议),提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响;
    • Stage 2:draft(草稿),Stage 2 的提案应提供规范初稿、草稿。此时,语言的实现者开始观察 runtime 的具体实现是否 合理;
    • Stage 3:candidate(候补),Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终 规范上签字。Stage 3 的提案不会有太大的改变,在对外发布之前只是修正一些问题;
    • Stage 4:finished(完成),进入 Stage 4 的提案将包含在 ECMAScript 的下一个修订版中;
  • 在babel7之前(比如babel6中),我们会经常看到这种设置方式:

    • 它表达的含义是使用对应的 babel-preset-stage-x 预设;
    • 但是从babel7开始,已经不建议使用了,建议使用preset-env来设置;

    image-20231123203521255

11.polyfill

  • Polyfill是什么呢?

    • 翻译:一种用于衣物、床具等的聚酯填充材料, 使这些物品更加温暖舒适;
    • 理解:更像是应该填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript;
  • 为什么时候会用到polyfill呢?

    • 比如我们使用了一些语法特性(例如:Promise, Generator, Symbol等以及实例方法例如Array.prototype.includes等)
    • 但是某些浏览器压根不认识这些特性,必然会报错;
    • 我们可以使用polyfill来填充或者说打一个补丁,那么就会包含该特性了;
  • babel7.4.0之前,可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了:

    image-20231123203915152

  • babel7.4.0之后,可以通过单独引入core-js和regenerator-runtime来完成polyfill的使用:

    npm install core-js regenerator-runtime --save
    

    image-20231123203928333

12.babel的配置文件

  • 像之前一样,我们可以将babel的配置信息放到一个独立的文件中,babel给我们提供了两种配置文件的编写:

    • babel.config.json(或者.js,.cjs,.mjs)文件;
    • .babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
  • 它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等);

    • .babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的;
    • babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐;

    image-20231123203649973

配置babel.config.js

  • 我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性:
    • useBuiltIns:设置以什么样的方式来使用polyfill;
    • corejs:设置corejs的版本,目前使用较多的是3.x的版本,比如我使用的是3.8.x的版本;
      • 另外corejs可以设置是否对提议阶段的特性进行支持;

        Stage-X

      • 设置 proposals属性为true即可;

useBuiltIns属性设置

  • useBuiltIns属性有三个常见的值

  • 第一个值:false

    • 打包后的文件不使用polyfill来进行适配;
    • 并且这个时候是不需要设置corejs属性的;
  • 第二个值:usage

    • 会根据源代码中出现的语言特性,自动检测所需要的polyfill;

    • 这样可以确保最终包里的polyfill数量的最小化,打包的包相对会小一些;

    • 可以设置corejs属性来确定使用的corejs的版本;

      image-20231123204218545

      image-20231127154024284

  • 第三个值:entry

    • 如果我们依赖的某一个库本身使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器可能会报错;
    • 所以,如果你担心出现这种情况,可以使用 entry;
    • 并且需要在入口文件中添加 `import 'core-js/stable'; import 'regenerator-runtime/runtime';
    • 这样做会根据 browserslist 目标导入所有的polyfill,但是对应的包也会变大;

    image-20231123204309510

13.JSX支持

  • 在我们编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的。

    App.jsx

    import React, { memo, useState } from 'react'
    
    const App = memo(() => {
      const [state, setState] = useState(0)
      return (
        <div>
          <h1>Count:{state}</h1>
          <button onClick={(e) => setState(state + 1)}>+1</button>
        </div>
      )
    })
    
    export default App
    
    

    main.js

    // import 'core-js/stable'
    // import 'regenerator-runtime/runtime'
    import React from 'react'
    import ReactDom from 'react-dom/client'
    import App from './react/index.jsx'
    // ES6
    const message = 'Hello Babel'
    console.log(message)
    
    const foo = () => {
      console.log('foo function')
    }
    
    foo()
    
    const obj = {
      name: '11',
      age: 18
    }
    const { name, age } = obj
    
    const nickname = 'Mingcomity'
    console.log(nickname.includes('coder'))
    
    // 编写react代码
    const root = ReactDom.createRoot(document.getElementById('root'))
    root.render(<App />)
    
    

    webpack.condig.js

    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    /** @type {import('webpack').Configuration} */
    const config = {
      mode: 'development',
      devtool: false,
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './build'),
        filename: 'bundle.js',
        // 删除原先的文件
        clean: true
      },
      resolve: {
        extensions: ['.js', '.json', '.jsx', '.wasm']
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: [
                  [
                    '@babel/preset-env'
                    // {
                    //   target:"",
                    //   corejs: 3,
                    //   useBuiltIns: 'usage' // false:表述不使用 | "usage":自动填充
                    // }
                  ],
                  ['@babel/preset-react']
                ],
                /**
                 * 让Babel自动检测代码的模块类型,从而避免出现模块类型不匹配的问题(报错)
                 */
                sourceType: 'unambiguous'
              }
            }
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: './index.html'
        })
      ]
    }
    module.exports = config
    
    
  • 对react jsx代码进行处理需要如下的插件:

    • @babel/plugin-syntax-jsx
    • @babel/plugin-transform-react-jsx
    • @babel/plugin-transform-react-display-name
  • 但是开发中,我们并不需要一个个去安装这些插件,我们依然可以使用preset来配置:

    npm install @babel/preset-react -D
    

    image-20231123204423112

14.Typescript编译

  • 在项目开发中,我们会使用TypeScript来开发,那么TypeScript代码是需要转换成JavaScript代码。

  • 可以通过TypeScript的compiler来转换成JavaScript:

    npm i typescript -D
    
  • 另外TypeScript的编译配置信息我们通常会编写一个tsconfig.json文件:

    tsc --init
    
  • 生成配置文件如下:

    image-20231123204535880

  • 之后我们可以运行 npx tsc来编译自己的ts代码:

    npx tsc
    
1.使用ts-loader
  • 如果我们希望在webpack中使用TypeScript,那么我们可以使用ts-loader来处理ts文件:
npm i ts-loader -D
  • 配置ts-loader:

    image-20231123204635154

  • 之后,我们通过npm run build打包即可。

2.使用babel-loader
  • 除了可以使用TypeScript Compiler来编译TypeScript之外,我们也可以使用Babel:

    • Babel是有对TypeScript进行支持;
    • 我们可以使用插件: @babel/tranform-typescript;
    • 但是更推荐直接使用preset:@babel/preset-typescript;
  • 我们来安装@babel/preset-typescript:

    npm install @babel/preset-typescript -D
    

    image-20231123204720556

3.如何选择
  • 那么我们在开发中应该选择ts-loader还是babel-loader呢?

  • 使用ts-loader(TypeScript Compiler)

    • 来直接编译TypeScript,那么只能将ts转换成js;
    • 如果我们还希望在这个过程中添加对应的polyfill,那么ts-loader是无能为力的;
    • 我们需要借助于babel来完成polyfill的填充功能;
  • 使用babel-loader(Babel)

    • 来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能;

    • 但是babel-loader在编译的过程中,不会对类型错误进行检测;

  • 那么在开发中,我们如何可以同时保证两个情况都没有问题呢?

4.最佳实践
  • 事实上TypeScript官方文档有对其进行说明:

    image-20231123204912104

  • 也就是说我们使用Babel来完成代码的转换,使用tsc来进行类型的检查。

  • 但是,如何可以使用tsc来进行类型的检查呢?

    • 在这里,我在scripts中添加了两个脚本,用于类型检查;

      image-20231123204932064

    • 我们执行 npm run type-check可以对ts代码的类型进行检测;

    • 我们执行 npm run type-check-watch可以实时的检测类型错误;

5.本地服务器

https://xxpromise.gitee.io/webpack5-docs/base/server.html#_1-%E4%B8%8B%E8%BD%BD%E5%8C%85

1.关于本地服务器

  • 目前我们开发的代码,为了运行需要有两个操作:
    • 操作一:npm run build,编译相关的代码;
    • 操作二:通过live server或者直接通过浏览器,打开index.html代码,查看效果;
  • 这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示
  • 为了完成自动编译,webpack提供了几种可选的方式:
    • webpack watch mode;
    • webpack-dev-server**(常用)**;
    • webpack-dev-middleware;

2.webpack-dev-server

  • 上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:

    • 当然,目前我们可以在VSCode中使用live-server来完成这样的功能;
    • 但是,我们希望在不适用live-server的情况下,可以具备live reloading(实时重新加载)的功能;
  • 安装webpack-dev-server

    npm install webpack-dev-server -D
    
  • 修改配置文件,启动时加上serve参数:

    image-20231127205401237

  • webpack-dev-server 在编译之后不会写入到任何输出文件,而是将 bundle 文件保留在内存中

    • 事实上webpack-dev-server使用了一个库叫memfs(memory-fs webpack自己写的)
1.static
  • devServer中static对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包后的资源,又依赖于 其他的一些资源,那么就需要指定从哪里来查找这个内容

    • 比如在index.html中,我们需要依赖一个 abc.js 文件,这个文件我们存放在 public文件 中;

    • 在index.html中,我们应该如何去引入这个文件呢?

      • 比如代码是这样的:<script src="./public/abc.js"></script>
      • 但是这样打包后浏览器是无法通过相对路径去找到这个文件夹的;
      • 所以代码是这样的: <script src="/abc.js"></script>;
      • 但是我们如何让它去查找到这个文件的存在呢? 设置static即可;

      image-20231127205612429

2.hotOnly、host
  • hotOnly是当代码编译失败时,是否刷新整个页面
    • 默认情况下当代码编译失败修复后,我们会重新刷新整个页面;
    • 如果不希望重新刷新整个页面,可以设置hotOnly为true;
  • host设置主机地址
    • 默认值是localhost;
    • 如果希望其他地方也可以访问,可以设置为 0.0.0.0;
  • localhost 和 0.0.0.0 的区别
    • localhost:本质上是一个域名,通常情况下会被解析成127.0.0.1;
    • 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
      • 正常的数据库包经过 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ;
      • 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
      • 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的;
    • 0.0.0.0:监听IPV4上所有的地址,再根据端口找到不同的应用程序;
      • 比如我们监听 0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的
3.port、open、compress
  • port设置监听的端口,默认情况下是8080

  • open是否打开浏览器

    • 默认值是false,设置为true会打开浏览器;
    • 也可以设置为类似于 Google Chrome等值;
  • compress是否为静态文件开启gzip compression

    • 默认值是false,可以设置为true;

    image-20231127205827454

4.Proxy代理
  • proxy是我们开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:

  • 我们可以进行如下的设置:

    • target:表示的是代理到的目标地址,比如 /api-hy/moment会被代理到 http://localhost:8888/api/moment;

    • pathRewrite:默认情况下,我们的 /api 也会被写入到URL中,如果希望删除,可以使用pathRewrite;

      image-20231127215315393

    • changeOrigin:它表示是否更新代理后请求的headers中host地址;

      • 这个 changeOrigin官方说的非常模糊,通过查看源码我发现其实是要修改代理请求中的headers中的host属性:

        image-20231127215635264

        image-20231127210134334

5.historyApiFallback
  • historyApiFallback是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404 的错误

  • boolean值:默认是false

    • 如果设置为true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容;
  • object类型的值,可以配置rewrites属性:

    • 可以配置from来匹配路径,决定要跳转到哪一个页面;
  • 事实上devServer中实现historyApiFallback功能是通过connect-history-api-fallback库的:

6.性能优化

  • webpack作为前端目前使用最广泛的打包工具,在面试中也是经常会被问到的。
  • 比较常见的面试题包括:
    • 可以配置哪些属性来进行webpack性能优化?
    • 前端有哪些常见的性能优化?(问到前端性能优化时,除了其他常见的,也完全可以从webpack来回答)
  • webpack的性能优化较多,我们可以对其进行分类:
    • 优化一:打包后的结果,上线时的性能优化。(比如分包处理、减小包体积、CDN服务器等)
    • 优化二:优化打包速度,开发或者构建时优化打包速度。(比如exclude、cache-loader等)
  • 大多数情况下,我们会更加侧重于优化一,这对于线上的产品影响更大
  • 在大多数情况下webpack都帮我们做好了该有的性能优化:
    • 比如配置mode为production或者development时,默认webpack的配置信息;
    • 但是我们也可以针对性的进行自己的项目优化;
  • 接下来,我们来学习一下webpack性能优化的更多细节。

1.代码分离

https://xxpromise.gitee.io/webpack5-docs/senior/optimizePerformance.html#code-split

image-20231128100058583

分包处理的必要性

  • 代码分离(Code Splitting)是webpack一个非常重要的特性
    • 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件
    • 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页 的加载速度;
    • 代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能
  • Webpack中常用的代码分离有三种:
    • 入口起点:使用entry配置手动分离代码;
    • 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
    • 动态导入:通过模块的内联函数调用来分离代码;
1.入口起点
  • 入口起点的含义非常简单,就是配置多入口:

    • 比如配置一个index.js和main.js的入口;
    • 他们分别有自己的代码逻辑;

    image-20231127230018450

    image-20231128102128298

  • 假如我们的index.js和main.js都依赖两个库:lodash、dayjs

    • 如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs
    • 事实上我们可以对他们进行共享

    image-20231127230051649

    可以有多个shared

2.动态导入
  • 另外一个代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:
    • 第一种,使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
    • 第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;
  • 比如我们有一个模块 bar.js:
  • 该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
  • 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件
  • 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码
  • 这个时候我们就可以使用动态导入
  • 注意:使用动态导入bar.js:
    • 在webpack中,通过动态导入获取到一个对象
    • 真正导出的内容,在该对象的default属性中,所以我们需要做一个简单的解构

动态导入的文件命名

  • 动态导入的文件命名:

    • 因为动态导入通常是一定会打包成独立的文件的,所以并不会在cacheGroups中进行配置;

    • 那么它的命名我们通常会在output中,通过 chunkFilename 属性来命名;

      image-20231127231132091

  • 但是,你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的

    • 如果我们希望修改name的值,可以**通过magic comments(魔法注释)**的方式;

      image-20231127231144983

代码的懒加载

  • 动态import使用最多的一个场景是懒加载(比如路由懒加载)

    • 封装一个component.js,返回一个component对象;
    • 我们可以在一个按钮点击时,加载这个对象;

    image-20231127231227684

3.防止重复-SplitChunks
  • 另外一种分包的模式是splitChunk,它底层是使用SplitChunksPlugin来实现的:

    • 因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件
    • 只需要提供SplitChunksPlugin相关的配置信息即可;
  • Webpack提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置:

    • 比如默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all;(默认为 async)

    image-20231127230210189

SplitChunks自定义配置解析

  • Chunks:
    • 默认值是async
    • 另一个值是initial,表示对通过的代码进行处理
    • all表示对同步和异步代码都进行处理
  • minSize
    • 拆分包的大小, 至少为minSize;
    • 如果一个包拆分出来达不到minSize,那么这个包就不会拆分;
  • maxSize
    • 将大于maxSize的包,拆分为不小于minSize的包;
  • cacheGroups
    • 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;
    • test属性:匹配符合规则的包;
    • name属性:拆分包的name属性;
    • filename属性:拆分包的名称,可以自己使用placeholder属性;

image-20231127230427935

解决注释的单独提取

  • 默认情况下,webpack在进行分包时,有对包中的注释进行单独提取。

    image-20231127230458235

  • 这个包提取是由另一个插件默认配置的原因:

    image-20231127230509378

    详细见 Terser 一节

optimization.chunkIds配置

  • optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成

  • 有三个比较常见的值:

    • natural:按照数字的顺序使用id;

    • named:development下的默认值,一个可读的名称的id;

      image-20231128142932704

    • deterministic:确定性的,在不同的编译中不变的短数字id

      • 在webpack4中是没有这个值的;

      • 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题;

        image-20231128142855840

  • 最佳实践:

    • 开发过程中,我们推荐使用named;
    • 打包过程中,我们推荐使用deterministic;

optimization.runtimeChunk配置

  • 配置runtime相关的代码是否抽取到一个单独的chunk中:

    • runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码
    • 比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;
  • 抽离出来后,有利于浏览器缓存的策略

    • 比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
    • 比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;
  • 设置的值:

    • true/multiple:针对每个入口打包一个runtime文件;
    • single:打包一个runtime文件;
    • 对象:name属性决定runtimeChunk的名称;

    image-20231128090044099

2.Prefetch和Preload

https://xxpromise.gitee.io/webpack5-docs/senior/optimizePerformance.html#preload-prefetch

  • webpack v4.6.0+ 增加了对预获取和预加载的支持

  • 在声明 import 时,使用下面这些内置指令,来告知浏览器:

    • prefetch(预获取):将来某些导航下可能需要的资源

    • preload(预加载):当前导航下可能需要资源

      image-20231128090202650

  • 与 prefetch 指令相比,preload 指令有许多不同之处

    • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
    • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
    • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。

3.CDN

1.关于CDN
  • CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)

    • 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器;
    • 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户;
    • 来提供高性能、可扩展性及低成本的网络内容传递给用户; 什么是CDN?

    image-20231128090346241

  • 在开发中,我们使用CDN主要是两种方式:

    • 方式一:打包的所有静态资源,放到CDN服务器, 用户所有资源都是通过CDN服务器加载的;
    • 方式二:一些第三方资源放到CDN服务器上
2.购买CDN服务器
  • 如果所有的静态资源都想要放到CDN服务器上,我们需要购买自己的CDN服务器;

    • 目前阿里、腾讯、亚马逊、Google等都可以购买CDN服务器;

    • 我们可以直接修改publicPath,在打包时添加上自己的CDN地址;

      image-20231128090432614

3.第三方库的CDN服务器
  • 通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的CDN服务器上

    • 国际上使用比较多的是unpkg、JSDelivr、cdnjs;
    • 国内也有一个比较好用的CDN是bootcdn;
  • 在项目中,我们如何去引入这些CDN呢

    • 第一,在打包的时候我们不再需要对类似于lodash或者dayjs这些库进行打包;
    • 第二,在html模块中,我们需要自己加入对应的CDN服务器地址;
  • 第一步,我们可以通过webpack配置,来排除一些库的打包

    image-20231128213717738

  • 第二步,在html模块中,加入CDN服务器地址

    image-20231128090541711

4.shimming

1.关于shimming
  • shimming是一个概念,是某一类功能的统称:
    • shimming翻译过来我们称之为 垫片,相当于给我们的代码填充一些垫片来处理一些问题;
    • 比如我们现在依赖一个第三方的库,这个第三方的库本身依赖lodash,但是默认没有对lodash进行导入(认为全局存在 lodash),那么我们就可以通过ProvidePlugin来实现shimming的效果;
  • 注意:webpack并不推荐随意的使用shimming
    • Webpack背后的整个理念是使前端开发更加模块化;
    • 也就是说,需要编写具有封闭性的、不存在隐含依赖(比如全局变量)的彼此隔离的模块;
2.shimming预支全局变量
  • 目前我们的lodash、dayjs都使用了CDN进行引入,所以相当于在全局是可以使用_和dayjs的

    • 假如一个文件中我们使用了axios,但是没有对它进行引入,那么下面的代码是会报错的;

      image-20231128221242938

  • 我们可以通过使用ProvidePlugin来实现shimming的效果

    • ProvidePlugin能够帮助我们在每个模块中,通过一个变量来获取一个package;

    • 如果webpack看到这个模块,它将在最终的bundle中引入这个模块;

    • 另外ProvidePlugin是webpack默认的一个插件,所以不需要专门导入;

      image-20231128221337119

      image-20231128090924218

  • 这段代码的本质是告诉webpack:

    • 如果你遇到了至少一处用到 axios变量的模块实例,那请你将 axios package 引入进来,并将其提供给需要用到它的模块。

5.MiniCssExtractPlugin

https://xxpromise.gitee.io/webpack5-docs/base/optimizeCss.html#%E6%8F%90%E5%8F%96-css-%E6%88%90%E5%8D%95%E7%8B%AC%E6%96%87%E4%BB%B6

  • MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中,该插件需要在webpack4+才可以使用。

  • 首先,我们需要安装 mini-css-extract-plugin:

    npm install mini-css-extract-plugin -D
    
  • 配置rules和plugins:

    image-20231128091034742

    image-20231128223519428

    image-20231128223535904

6.Hash、ContentHash、ChunkHash

https://xxpromise.gitee.io/webpack5-docs/senior/optimizePerformance.html#network-cache

  • 在我们给打包的文件进行命名的时候,会使用placeholder,placeholder中有几个属性比较相似
    • hash、chunkhash、contenthash

      image-20231128225142583

    • hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);

      image-20231128225152526

  • hash值的生成和整个项目有关系
    • 比如我们现在有两个入口index.js和main.js;

    • 它们分别会输出到不同的bundle文件中,并且在文件名称中我们有使用hash;

    • 这个时候,如果修改了index.js文件中的内容,那么hash会发生变化

      明明只修改了index.js却打包后的main.js的文件的hash也发生了变化,对于部署后不利于浏览器缓存的

    • 那就意味着两个文件的名称都会发生变化;

  • chunkhash可以有效的解决上面的问题,它会根据不同的入口进行解析来生成hash值
    • 比如我们修改了index.js,那么main.js的chunkhash是不会发生改变的;
  • contenthash表示生成的文件hash名称,只和内容有关系
    • 比如我们的index.js,引入了一个style.css,style.css有被抽取到一个独立的css文件中;
    • 这个css文件在命名时,如果我们使用的是chunkhash;
    • 那么当index.js文件的内容发生变化时,css文件的命名也会发生变化;
    • 这个时候我们可以使用contenthash;
  • 尽量使用contenthash

7.DLL库

  • DLL是什么呢?

    • DLL全程是动态链接库(Dynamic Link Library),是为软件在Windows中实现共享函数库的一种实现方式;
    • 那么webpack中也有内置DLL的功能,它指的是我们可以将能够共享,并且不经常改变的代码,抽取成一个共享的库
    • 这个库在之后编译的过程中,会被引入到其他项目的代码中
  • DLL库的使用分为两步:

    • 第一步:打包一个DLL库;
    • 第二步:项目中引入DLL库;
  • 注意:在升级到webpack4之后,React和Vue脚手架都移除了DLL库(下面的vue作者的回复),所以知道有这么一个概念即 可。

    image-20231128091349305

    webpack4开始提供了足够的性能了,所以不需要使用DLL库了进行优化了

8.JS压缩-Terser

1.关于Terser
  • 什么是Terser呢?

    • Terser是一个JavaScript的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集;
    • 早期我们会使用 uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的语法;
    • Terser是从 uglify-es fork 过来的,并且保留它原来的大部分API以及适配 uglify-es和uglify-js@3等;
  • 也就是说,Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小

  • 因为Terser是一个独立的工具,所以它可以单独安装:

    # 全局安装
    npm install terser -g
    # 局部安装
    npm install terser -D
    
2.命令行使用
3.部分配置
  • Compress option:

    • arrows:class或者object中的函数,转换成箭头函数;

      image-20231130112836883

    • arguments:将函数中使用 arguments[index]转成对应的形参名称;

      image-20231130112929846

    • dead_code:移除不可达的代码(tree shaking);

      image-20231130113039506

    • 其他属性可以查看文档;

  • Mangle option

    • toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换);

      image-20231130113138762

    • keep_classnames:默认值是false,是否保持依赖的类名称;

      image-20231130113352507

    • keep_fnames:默认值是false,是否保持原来的函数名称;

      image-20231130113252738

    • 其他属性可以查看文档;

      npx terser ./src/abc.js -o abc.min.js -c 
      arrows,arguments=true,dead_code -m 
      toplevel=true,keep_classnames=true,keep_fnames=true
      
4.在Webpack中配置
  • 真实开发中,我们不需要手动的通过terser来处理我们的代码,我们可以直接通过webpack来处理

    • 在webpack中有一个minimizer属性,在production模式下默认就是使用TerserPlugin来处理我们的代码的;
    • 如果我们对默认的配置不满意,也可以自己来创建TerserPlugin的实例,并且覆盖相关的配置;
  • 首先,我们需要打开minimize,让其对我们的代码进行压缩(默认production模式下已经打开了)

  • 其次,我们可以在minimizer创建一个TerserPlugin:

    • extractComments:默认值为true,表示会将注释抽取到一个单独的文件中;
      • 在开发中,我们不希望保留这个注释时,可以设置为false;
    • parallel:使用多进程并发运行提高构建的速度,默认值是true
      • 并发运行的默认数量: os.cpus().length - 1;
      • 我们也可以设置自己的个数,但是使用默认值即可;
    • terserOptions:设置我们的terser相关的配置
      • compress:设置压缩相关的选项;
        • unused:没用到的代码进行保留 (false)
      • mangle:设置丑化相关的选项,可以直接设置为true;
      • toplevel:顶层变量是否进行转换;
      • keep_classnames:保留类的名称;
      • keep_fnames:保留函数的名称

    image-20231128091948497

9.CSS压缩

  • 另一个代码的压缩是CSS

    • CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;
    • CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin;
    • css-minimizer-webpack-plugin是使用cssnano工具来优化、压缩CSS(也可以单独使用);
  • 第一步,安装 css-minimizer-webpack-plugin

    npm install css-minimizer-webpack-plugin -D 
    
  • 第二步,在optimization.minimizer中配置

    image-20231128092044337

10.Tree Shaking

1.关于Tree Shaking
  • 什么是Tree Shaking呢?
    • Tree Shaking是一个术语,在计算机中表示消除死代码(dead_code);
    • 最早的想法起源于LISP,用于**消除未调用的代码(**纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式 编程时,尽量使用纯函数的原因之一);
    • 后来Tree Shaking也被应用于其他的语言,比如JavaScript、Dart
  • JavaScript的Tree Shaking:
    • 对JavaScript进行Tree Shaking是源自打包工具rollup
    • 这是因为Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);
    • webpack2正式内置支持了ES2015模块,和检测未使用模块的能力;
    • 在webpack4正式扩展了这个能力,并且通过 package.json的 sideEffects属性作为标记,告知webpack在编译时,哪里文 件可以安全的删除掉;
    • webpack5中,也提供了对部分CommonJS的tree shaking的支持
2.Webpack实现
  • 事实上webpack实现Tree Shaking采用了两种不同的方案:

    • usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的;

      • 将mode设置为development模式: (production 模式自动启用usedExports)

        • 为了可以看到 usedExports带来的效果,我们需要设置为 development 模式
        • 因为在 production 模式下,webpack默认的一些优化会带来很大的影响。
      • 设置usedExports为true和false对比打包后的代码

        • 在usedExports设置为true时,会有一段注释:unused harmony export mul;

          image-20231130172501655

        • 这段注释的意义是什么呢?告知Terser在优化时,可以删除掉这段代码;

      • 这个时候,我们讲 minimize设置true

        • usedExports设置为false时,mul函数没有被移除掉;
        • usedExports设置为true时,mul函数有被移除掉;
      • 所以,usedExports实现Tree Shaking是结合Terser来完成的

        image-20231128092515507

    • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用;

      • sideEffects用于告知webpack compiler哪些模块时有副作用的

        • 副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义;
        • 副作用的问题,在讲React的纯函数时是有讲过的;
      • 在package.json中设置sideEffects的值

        • 如果我们将sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports;

          image-20231130173630119

        • 如果有一些我们希望保留,可以设置为数组;

          image-20231130173701191

      • 比如我们有一个format.js、style.css文件

        • 该文件在导入时没有使用任何的变量来接受;

        • 那么打包后的文件,不会保留format.js、style.css相关的任何代码;

          image-20231130173933810

          image-20231130173920155

      image-20231128092406172

3.最佳实践
  • 所以,如何在项目中对JavaScript的代码进行TreeShaking呢(生成环境)?
    • 在optimization中配置usedExports为true,来帮助Terser进行优化;
    • 在package.json中配置sideEffects,直接对模块进行优化;
4.CSS实现
  • 上面我们学习的都是关于JavaScript的Tree Shaking,那么CSS是否也可以进行Tree Shaking操作呢

    • CSS的Tree Shaking需要借助于一些其他的插件;
    • 在早期的时候,我们会使用PurifyCss插件来完成CSS的tree shaking,但是目前该库已经不再维护了(最新更新也是在4年前 了);
    • 目前我们可以使用另外一个库来完成CSS的Tree Shaking:PurgeCSS,也是一个帮助我们删除未使用的CSS的工具;
  • 安装PurgeCss的webpack插件

    npm install purgecss-webpack-plugin -D
    
    const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
    
  • 配置这个插件(生成环境)

    • paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;

      pnpm i glob -D
      
      const glob = require('glob')
      
    • 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;

       new PurgeCSSPlugin({
            // 整个src文件夹
            paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, {
              nodir: true
            }),
            safelist: function () {
              return {
                standard: ['html', 'body']
              }
            }
          })
      

      image-20231128092915616

  • purgecss也可以对less文件进行处理(所以它是对打包后的css进行tree shaking操作)

11.Scope Hoisting

  • 什么是Scope Hoisting呢?
    • Scope Hoisting从webpack3开始增加的一个新功能;
    • 功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快;
  • 默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE
    • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
    • Scope Hoisting可以将函数合并到一个模块中来运行
  • 使用Scope Hoisting非常的简单,webpack已经内置了对应的模块
    • production模式下,默认这个模块就会启用

    • 在development模式下,我们需要自己来打开该模块;

      const webpack = require('webpack')
      
      plugins: [
         	......, 
          new webpack.optimize.ModuleConcatenationPlugin()
        ],
      

      image-20231130182217281

12.HTTP压缩

1.关于HTTP压缩
  • HTTP压缩是一种内置在 服务器 和 客户端 之间的**,以改进传输速度和带宽利用率的方式**;

  • HTTP压缩的流程什么呢?

    • 第一步:HTTP数据在服务器发送前就已经被压缩了;(可以在webpack中完成)

    • 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;

      image-20231128093201566

    • 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;

      image-20231128093211362

2.目前的压缩格式
  • 目前的压缩格式非常的多:
    • compress – UNIX的“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用gzip或deflate);
    • deflate – 基于deflate算法(定义于RFC 1951)的压缩,使用zlib数据格式封装;
    • gzip – GNU zip格式(定义于RFC 1952),是目前使用比较广泛的压缩算法;
    • br – 一种新的开源压缩算法,专为HTTP内容的编码而设计;
3.Webpack实现

https://blog.csdn.net/qq_41499782/article/details/118650279?spm=1001.2014.3001.5501

  • webpack中相当于是实现了HTTP压缩的第一步操作,我们可以使用CompressionPlugin。

  • 第一步,安装CompressionPlugin

    npm install compression-webpack-plugin -D
    
  • 第二步,使用CompressionPlugin即可

    const CompressionPlugin = require('compression-webpack-plugin') 
    new CompressionPlugin({
          algorithm: 'gzip', // 压缩格式 有:gzip、brotliCompress,
          test: /\.(js|css|svg)$/,
          threshold: 500, // 只处理比这个值大的资源,按字节算
          minRatio: 0.8, //只有压缩率比这个值小的文件才会被处理,压缩率=压缩大小/原始大小,如果压缩后和原始文件大小没有太大区别,就不用压缩
          deleteOriginalAssets: false //是否删除原文件,最好不删除,服务器会自动优先返回同名的.gzip资源,如果找不到还可以拿原始文件
        })
    

    image-20231128093341061

13.HTML压缩

  • 我们之前使用了HtmlWebpackPlugin插件来生成HTML的模板,事实上它还有一些其他的配置:

  • inject:设置打包的资源插入的位置

    • true、 false 、body、head
  • cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)

  • minify:默认会使用一个插件html-minifier-terser

    image-20231128093601506

14.性能测试

1.打包的时间分析
  • 如果我们希望看到每一个loader、每一个Plugin消耗的打包时间,可以借助于一个插件:speed-measure-webpack-plugin

    • 注意:该插件在最新的webpack版本中存在一些兼容性的问题(和部分Plugin不兼容)

      此次测试:

      • 不兼容 mini-css-extract-plugin 2.7.6

      • 兼容 mini-css-extract-plugin 1.3.6 版本

    • 截止2021-3-10日,但是目前该插件还在维护,所以可以等待后续是否更新;

    • 我这里暂时的做法是把不兼容的插件先删除掉,也就是不兼容的插件不显示它的打包时间就可以了;

  • 第一步,安装speed-measure-webpack-plugin插件

    npm install speed-measure-webpack-plugin -D
    
  • 第二步,使用speed-measure-webpack-plugin插件

    • 创建插件导出的对象 SpeedMeasurePlugin;

    • 使用 smp.wrap 包裹我们导出的webpack配置;

      image-20231128093732704

2.打包后文件分析

方案1

  • 生成一个stats.json的文件

    "buiebpack ld:stats": "w--config ./config/webpack.common.js --env production --profile --json=stats.json"
    
  • 通过执行npm run build:status可以获取到一个stats.json的文件:

    • 这个文件我们自己分析不容易看到其中的信息;

    • 可以放到 http://webpack.github.com/analyse,进行分析

      image-20231128093857538

      anlyse项目启动报错:Error: error:0308010C:digital envelope routines::unsupported

      启动命令更改为:image-20231130200917482

方案2

  • 使用webpack-bundle-analyzer工具

    • 另一个非常直观查看包大小的工具是webpack-bundle-analyzer。
  • 第一步,我们可以直接安装这个工具:

    npm install webpack-bundle-analyzer -D
    
  • 第二步,我们可以在webpack配置中使用该插件:

    image-20231128094001972

  • 在打包webpack的时候,这个工具是帮助我们打开一个8888端口上的服务,我们可以直接的看到每个包的大小。

    • 比如有一个包时通过一个Vue组件打包的,但是非常的大,那么我们可以考虑是否可以拆分出多个组件,并且对其进行懒加载;
    • 比如一个图片或者字体文件特别大,是否可以对其进行压缩或者其他的优化处理;

7.自定义Loader

  • Loader是用于对模块的源代码进行转换(处理),之前我们已经使用过很多Loader,比如css-loader、style-loader、babelloader等。
  • 这里我们来学习如何自定义自己的Loader:
    • Loader本质上是一个导出为函数的JavaScript模块
    • loader runner库会调用这个函数,然后将上一个loader产生的结果或者资源文件传入进去;

1.编写Loader

  • 编写一个hy-loader01.js模块这个函数会接收三个参数:

    • content:资源文件的内容;

      image-20231204151053497

    • map:sourcemap相关的数据;

    • meta:一些元数据;

    image-20231130103540076

2.引入Loader

  • 注意:传入的路径和context是有关系的,在前面我们讲入口的相对路径时有讲过

    image-20231130103621214

  • 但是,如果我们依然希望可以直接去加载自己的loader文件夹,有没有更加简洁的办法呢?

    • 配置resolveLoader属性;

    image-20231130103936525

3.Loader的执行顺序

  • 创建多个Loader使用,它的执行顺序是什么呢?

    • 从后向前、从右向左的

    image-20231130104030386

4.pitch-loader和enforce

  • 事实上还有另一种Loader,称之为PitchLoader

    image-20231130104131631

    image-20231204153947196

5.执行顺序和enforce

  • 其实这也是为什么loader的执行顺序是相反的

    • run-loader先优先执行PitchLoader,在执行PitchLoader时进行loaderIndex++;
    • run-loader之后会执行NormalLoader,在执行NormalLoader时进行loaderIndex--;
  • 那么,能不能改变它们的执行顺序呢

    • 我们可以拆分成多个Rule对象,通过enforce来改变它们的顺序
  • enforce一共有四种方式

    • 默认所有的loader都是normal

    • 在行内设置的loader是inline(import 'loader1!loader2!./test.js');

      image-20231204162529795

    • 也可以通过enforce设置 pre 和 post

  • 在Pitching和Normal它们的执行顺序分别是

    • post, inline, normal, pre; (Pitching)

    • pre, normal, inline, post;(Normal)

      image-20231204162922614

6.同步的Loader

  • 什么是同步的Loader呢?

    • 默认创建的Loader就是同步的Loader

    • 这个Loader必须通过 return 或者 this.callback 来返回结果,交给下一个loader来处理;

      image-20231204163906652

    • 通常在有错误的情况下,我们会使用 this.callback

  • this.callback的用法如下:

    • 第一个参数必须是 Error 或者 null;
    • 第二个参数是一个 string或者Buffer;

    image-20231130104331446

7.异步的Loader

  • 什么是异步的Loader呢?

    • 有时候我们使用Loader时会进行一些异步的操作;
    • 我们希望在异步操作完成后,再返回这个loader处理的结果;
    • 这个时候我们就要使用异步的Loader了;
  • loader-runner已经在执行loader时给我们提供了方法,让loader变成一个异步的loader:

    image-20231204163951706

    image-20231130104415413

8.传入和获取参数

  • 在使用loader时,传入参数。

  • 我们可以通过一个webpack官方提供的一个解析库 loader-utils,安装对应的库。

    npm install loader-utils -D
    
  • 或者使用this.getOptions()

image-20231130104500614

image-20231204164813384

9.校验参数

  • 我们可以通过一个webpack官方提供的校验库 schema-utils,安装对应的库:

    npm install schema-utils -D
    
    {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "名称!!"
        },
        "age": {
          "type": "number",
          "description": "年龄!!"
        }
      }
    }
    
    
    const { validate } = require('schema-utils')
    const loader4Schema = require('./schema/mc_loader4_schema.json')
    module.exports = function (content, map, meta) {
      // 获取参数
      // 1.早期使用loader-utils库
      // 2.使用this.getOptions()
      const options = this.getOptions()
      console.log('loader 04:', content, options)
    
      // 校验参数
      validate(loader4Schema, options)
      return content
    }
    
    

    image-20231130104616048

    image-20231130104630196

10.案例

  • 我们知道babel-loader可以帮助我们对JavaScript的代码进行转换,这里我们定义一个自己的babel-loader:

    • pnpm i @babel/core -D pnpm install @babel/plugin-transform-arrow-functions -D npm install @babel/preset-env

      const babel = require('@babel/core')
      
      module.exports = function (content, map, meta) {
        const callback = this.async()
        const options = this.getOptions()
        if (Object.keys(options).length) {
          // babel的配置文件
          options = require('../babel.config')
        }
        // 使用babel转换
        babel.transform(content, options, (err, result) => {
          if (err) {
            callback(err)
          } else {
            callback(null, result.code)
          }
        })
      }
      
      

      image-20231204171610431

    image-20231130104714974

  • 一个处理md文档的loader

    • pnpm i marked -D
      
      import code from './demo.md'
      const message = 'Hello World'
      console.log(message)
      
      const foo = () => {
        console.log('foo')
      }
      
      // 将code显示到页面中
      document.body.innerHTML = code
      
      

      main.js

      const { marked } = require('marked')
      module.exports = function (content, map, meta) {
        const htmlContent = marked(content)
      
        const innerContent = '`' + htmlContent + '`'
        const moduleContent = `var code = ${innerContent}; export default code;`
      
        // 返回的结果需要是模块化的东西
        return moduleContent
      }
      
      

      md-loader

      const path = require('path')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      /**
       * @type {import('webpack').Configuration}
       */
      const config = {
        ......,
        module: {
          rules: [
            
            {
              test: /\.js$/,
              use: [
                {
                  loader: 'loaders',
                  options: {
                    // plugins: ['@babel/plugin-transform-arrow-functions']
                    presets: ['@babel/preset-env']
                  }
                }
              ]
            },
            {
              test: /\.md$/,
              use: [
                {
                  loader: 'md-loader'
                }
              ]
            }
          ]
        },
        plugins: [new HtmlWebpackPlugin()]
      }
      module.exports = config
      
      

      webpack.config.js

      image-20231204175105578

      效果图,可以再自行编写一些css样式添加进去

      • pnpm i highlight.js

      注意版本问题

      image-20231204181312334

      经测试,此两个版本有效实现

      import code from './demo.md'
      // 自己编写优化样式
      import './css/style.css'
      // 使用默认样式
      import 'highlight.js/styles/default.css'
      
      const message = 'Hello World'
      console.log(message)
      
      const foo = () => {
        console.log('foo')
      }
      
      // 将code显示到页面中
      document.body.innerHTML = code
      
      
      const { marked } = require('marked')
      const hljs = require('highlight.js')
      
      module.exports = function (content, map, meta) {
        // 让marked库解析语法时,将代码高亮内容标识出来
        marked.setOptions({
          highlight: function (code, lang) {
            return hljs.highlight(lang, code).value
          }
        })
      
        const htmlContent = marked(content)
      
        const innerContent = '`' + htmlContent + '`'
        const moduleContent = `var code = ${innerContent}; export default code;`
      
        // 返回的结果需要是模块化的东西
        return moduleContent
      }
      
      
      pre {
        background-color: #dadada;
        padding: 20px 10px;
        font-size: 22px;
      }
      
      .hljs-keyword {
        color: red;
      }
      
      .hljs-string {
        color: blue;
      }
      
      

      image-20231130104728732

8.自定义Plugin

1.Webpack和Tapable

  • 我们知道webpack有两个非常重要的类:Compiler和Compilation
    • 他们通过注入插件的方式,来监听webpack的所有生命周期;
    • 插件的注入离不开各种各样的Hook,而他们的Hook是如何得到的呢?
    • 其实是创建了Tapable库中的各种Hook的实例;
  • 所以,如果我们想要学习自定义插件,最好先了解一个库:Tapable
    • Tapable是官方编写和维护的一个库;
    • Tapable是管理着需要的Hook,这些Hook可以被应用到我们的插件中;

2.Tapable有哪些Hook

image-20231130105621941

3.Hook分类

  • 同步和异步的:
    • sync开头的,是同步的Hook;

      const { SyncHook } = require('tapable')
      
      class Compiler {
        constructor() {
          this.hooks = {
            syncHook: new SyncHook(['name', 'age'])
          }
      
          // 用hooks监听事件
          this.hooks.syncHook.tap('event1', (name, age) => {
            console.log('event1事件监听执行了', name, age)
          })
          this.hooks.syncHook.tap('event2', (name, age) => {
            console.log('event2事件监听执行了', name, age)
          })
        }
      }
      
      const compiler = new Compiler()
      compiler.hooks.syncHook.call('Mingcomity', 20)
      
      

      image-20231204222206888

    • async开头的,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调;

  • 其他的类别
    • bail:当有返回值时,就不会执行后续的事件触发了;

      const { SyncBailHook } = require('tapable')
      
      class Compiler {
        constructor() {
          this.hooks = {
            // 有返回值就可以阻断后续事件执行
            bailHook: new SyncBailHook(['name', 'age'])
          }
      
          // 用hooks监听事件
          this.hooks.bailHook.tap('event1', (name, age) => {
            console.log('event1事件监听执行了', name, age)
            return 123
          })
          this.hooks.bailHook.tap('event2', (name, age) => {
            console.log('event2事件监听执行了', name, age)
          })
        }
      }
      
      const compiler = new Compiler()
      setTimeout(() => {
        compiler.hooks.bailHook.call('Mingcomity', 20)
      }, 2000)
       
      

      image-20231204222650549

    • Loop:当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;

      const { SyncLoopHook } = require('tapable')
      
      let count = 0
      
      class Compiler {
        constructor() {
          this.hooks = {
            // 有返回值就可以阻断后续事件执行
            loopHook: new SyncLoopHook(['name', 'age'])
          }
      
          // 用hooks监听事件
          this.hooks.loopHook.tap('event1', (name, age) => {
            if (count < 5) {
              console.log('event1事件监听执行了', name, age)
              count++
              return true
            }
          })
          this.hooks.loopHook.tap('event2', (name, age) => {
            console.log('event2事件监听执行了', name, age)
          })
        }
      }
      
      const compiler = new Compiler()
      setTimeout(() => {
        compiler.hooks.loopHook.call('Mingcomity', 20)
      }, 2000)
      
      

      image-20231204223001626

    • Waterfall:当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数;

      const { SyncWaterfallHook } = require('tapable')
      
      class Compiler {
        constructor() {
          this.hooks = {
            // 有返回值就可以阻断后续事件执行
            waterfallHook: new SyncWaterfallHook(['name', 'age'])
          }
      
          // 用hooks监听事件
          this.hooks.waterfallHook.tap('event1', (name, age) => {
            console.log('event1事件监听执行了', name, age)
      
            return '哈哈哈'
          })
          this.hooks.waterfallHook.tap('event2', (name, age) => {
            console.log('event2事件监听执行了', name, age)
          })
        }
      }
      
      const compiler = new Compiler()
      setTimeout(() => {
        compiler.hooks.waterfallHook.call('Mingcomity', 20)
      }, 2000)
      
      

      image-20231204223247518

    • Parallel:并行,会同时执行多次事件处理回调,不会等到上一个事件回调执行结束 ,才执行下一次事件处理回调;

      const { AsyncParallelHook } = require('tapable')
      
      class Compiler {
        constructor() {
          this.hooks = {
            // 有返回值就可以阻断后续事件执行
            parallel: new AsyncParallelHook(['name', 'age'])
          }
      
          // 用hooks监听事件
          this.hooks.parallel.tapAsync('event1', (name, age) => {
            setTimeout(() => {
              console.log('event1事件监听执行了', name, age)
            }, 3000)
          })
          this.hooks.parallel.tapAsync('event2', (name, age) => {
            setTimeout(() => {
              console.log('event2事件监听执行了', name, age)
            }, 3000)
          })
          // 3s后都同时执行了
        }
      }
      
      const compiler = new Compiler()
      setTimeout(() => {
        compiler.hooks.parallel.callAsync('Mingcomity', 20)
      }, 0)
      
      

      image-20231204223913303

      3s后都同时执行了

    • Series:串行,会等待上一是异步的Hook;

      const { AsyncSeriesHook } = require('tapable')
      
      class Compiler {
        constructor() {
          this.hooks = {
            // 有返回值就可以阻断后续事件执行
            seriesHook: new AsyncSeriesHook(['name', 'age'])
          }
      
          // 用hooks监听事件
          this.hooks.seriesHook.tapAsync('event1', (name, age, callback) => {
            setTimeout(() => {
              console.log('event1事件监听执行了', name, age)
              callback()
            }, 3000)
          })
          this.hooks.seriesHook.tapAsync('event2', (name, age, callback) => {
            setTimeout(() => {
              console.log('event2事件监听执行了', name, age)
              callback()
            }, 3000)
          })
          // 3s执行了enevt1,随后3s执行了event2,随后执行callback打印‘所有~~’
        }
      }
      
      const compiler = new Compiler()
      setTimeout(() => {
        compiler.hooks.seriesHook.callAsync('Mingcomity', 20, () => {
          console.log('所有任务都完成~')
        })
      }, 0)
      
      

      image-20231204224403254

      3s执行了enevt1,随后3s执行了event2,随后执行callback打印‘所有~~’

4.Hook的使用

  • 第一步:创建Hook对象

    image-20231130105916949

  • 第二步:注册Hook中的事件

    image-20231130105927555

  • 第三步:触发事件

    image-20231130105941207

5.自定义Plugin

  • 在之前的学习中,我们已经使用了非常多的Plugin:
    • CleanWebpackPlugin
    • HTMLWebpackPlugin
    • MiniCSSExtractPlugin
    • CompressionPlugin
    • 等等。。。
  • 这些Plugin是如何被注册到webpack的生命周期中的呢?
    • 第一:在webpack函数的createCompiler方法中,注册了所有的插件;

    • 第二:在注册插件时,会调用插件函数或者插件对象的apply方法;

    • 第三:插件方法会接收compiler对象,我们可以通过compiler对象来注册Hook的事件

      image-20231205091901162

      部分hooks

    • 第四:某些插件也会传入一个compilation的对象,我们也可以监听compilation的Hook事件;

6.开发自己的插件

  • 如何开发自己的插件呢?

    • 目前大部分插件都可以在社区中找到,但是推荐尽量使用在维护,并且经过社区验证的;

    • 这里我们开发一个自己的插件:将静态文件自动上传服务器中;

  • 自定义插件的过程:

    • 创建AutoUploadWebpackPlugin类;

    • 编写apply方法:

      • 通过ssh连接服务器;

      • 删除服务器原来的文件夹;

      • 上传文件夹中的内容;

      pnpm i node-ssh -D

      const { NodeSSH } = require('node-ssh')
      class AutoUploadWebpackPlugin {
        /**@param {{host: string, username: string, password: string, remotePath: string}} options */
        constructor(options) {
          this.options = options
          this.ssh = new NodeSSH()
        }
        /**@param {import('webpack').Compiler} compiler */
        apply(compiler) {
          // 注册hooks监听事件,等到已经输出到output目录时,完成自动上传
          compiler.hooks.afterEmit.tapAsync(
            'AutoPlugin',
            async (compilation, callback) => {
              // 1.获取输出文件夹路径
              const outputPath = compilation.outputOptions.path
              // 2.链接远程服务器 SHH
              await this._connectServer({
                password: this.options.password,
                host: this.options.host,
                username: this.options.username
              })
              // 3.删除原有文件夹的类
              const remotePath = this.options.remotePath
              this.ssh.execCommand(`rm -rf ${remotePath}/*`)
              // 4.将文件夹中的操作上传到服务器中
              await this._uploadFiles(outputPath, remotePath)
              // 5.关闭ssh链接
              this.ssh.dispose()
              // 完成所有操作
              callback()
            }
          )
        }
      
        async _connectServer({ host, username, password }) {
          await this.ssh.connect({
            host,
            username,
            password
          })
          console.log('连接成功')
        }
      
        async _uploadFiles(localPath, romotePath) {
          const state = await this.ssh.putDirectory(localPath, romotePath, {
            recursive: true,
            concurrency: 10
          })
          if (state) {
            console.log('文件上传成功')
          }
        }
      }
      
      module.exports = AutoUploadWebpackPlugin
      module.exports.AutoUploadWebpackPlugin = AutoUploadWebpackPlugin
      
    • 在webpack的plugins中,使用AutoUploadWebpackPlugin类;

      const path = require('path')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const AutoUploadWebpackPlugin = require('./plugins/AutoUploadWebpackPlugin')
      /**
       * @type {import('webpack').Configuration}
       */
      const config = {
        mode: 'development',
        devtool: false,
        entry: './src/main.js',
        output: {
          path: path.resolve(__dirname, './build'),
          filename: 'bundle.js'
        },
        resolveLoader: {
          modules: ['node_modules', './loaders']
        },
        module: {},
        plugins: [
          new HtmlWebpackPlugin(),
          new AutoUploadWebpackPlugin({
            host: '127.0.0.1',
            username: 'ming',
            password: 'xxxx',
            remotePath: '/root/xxx'
          })
        ]
      }
      module.exports = config
      
      

二.自动化工具Gulp

1.关于Gulp

  • 什么是Gulp?

    • A toolkit to automate & enhance your workflow;
    • 一个工具包,可以帮你自动化和增加你的工作流;

    image-20231205102146475

2.Gulp和Webpack

  • gulp的核心理念是task runner
  • 可以定义自己的一系列任务,等待任务被执行;
  • 基于文件Stream的构建流;
  • 我们可以使用gulp的插件体系来完成某些任务;
  • webpack的核心理念是module bundler
    • webpack是一个模块化的打包工具
    • 可以使用各种各样的loader来加载不同的模块
    • 可以使用各种各样的插件在webpack打包的生命周期完成其他的任务;
  • gulp相对于webpack的优缺点
    • gulp相对于webpack思想更加的简单、易用,更适合编写一些自动化的任务
    • 但是目前对于大型项目(Vue、React、Angular)并不会使用gulp来构建,比如默认gulp是不支持模块化的;

3.使用Gulp

1.基础使用

创建示例:

  1. pnpm i gulp -d

  2. 根目录创建 gulpfile.js 文件

    // const gulp = require('gulp')
    
    // 编写简单的任务
    const foo = (cb) => {
      console.log('第一个gulp任务')
      cb()
    }
    
    // 异步任务
    const bar = (cb) => {
      setTimeout(() => {
        console.log('任务bar')
        cb()
      }, 2000)
    }
    
    // 早期编写任务
    // gulp.task('foo2', (cb) => {
    //   console.log('第一个gulp任务')
    //   cb()
    // })
    
    // 导出的任务
    module.exports = {
      foo,
      bar
    }
    
    
  3. 最后执行gulp命令npx gulp foo

    image-20231205133658741

2.创建任务

  • 每个gulp任务都是一个异步的JavaScript函数:

    • // 异步任务
      const bar = (cb) => {
        setTimeout(() => {
          console.log('任务bar')
          cb()
        }, 2000)
      }
      
      // 导出的任务
      module.exports = {
        bar
      }
      
    • 此函数可以接受一个callback作为参数,调用callback函数那么任务会结束;
    • 或者是一个返回stream、promise、event emitter、child process或observable类型的函数;
  • 任务可以是public或者private类型的:

    • 公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过 gulp 命令直接调用;

      npx gulp foo / npx dulp bar

      // 导出的任务
      module.exports = {
        bar,
        foo
      }
      
    • 私有任务(Private tasks) 被设计为在内部使用,通常作为 series() 或 parallel() 组合的组成部分;

  • 补充:gulp4之前, 注册任务时通过gulp.task的方式进行注册的

    image-20231205102701157

3.默认任务

  • 我们可以编写一个默认任务:

    image-20231205102759346

  • 执行 gulp 命令:

    npx gulp
    

4.任务组合series和parallel

  • 通常一个函数中能完成的任务是有限的(放到一个函数中也不方便代码的维护),所以我们会将任务进行组合。

  • gulp提供了两个强大的组合方法:

    • series():串行任务组合;

      const { series } = require('gulp')
      
      const foo1 = (cb) => {
        setTimeout(() => {
          console.log('任务foo1')
          cb()
        }, 1000)
      }
      const foo2 = (cb) => {
        setTimeout(() => {
          console.log('任务foo2')
          cb()
        }, 2000)
      }
      const foo3 = (cb) => {
        setTimeout(() => {
          console.log('任务foo3')
          cb()
        }, 3000)
      }
      
      const seriesFoo = series(foo1, foo2, foo3)
      
      // 导出的任务
      module.exports = {
        seriesFoo
      }
      
      

      image-20231205135112174

    • parallel():并行任务组合;

      const { series, parallel } = require('gulp')
      
      const foo1 = (cb) => {
        setTimeout(() => {
          console.log('任务foo1')
          cb()
        }, 1000)
      }
      const foo2 = (cb) => {
        setTimeout(() => {
          console.log('任务foo2')
          cb()
        }, 2000)
      }
      const foo3 = (cb) => {
        setTimeout(() => {
          console.log('任务foo3')
          cb()
        }, 3000)
      }
      
      const seriesFoo = series(foo1, foo2, foo3)
      const parallelFoo = parallel(foo1, foo2, foo3)
      
      // 导出的任务
      module.exports = {
        seriesFoo,
        parallelFoo
      }
      
      

      image-20231205135235295

      image-20231205102940937

5.读取和写入文件

  • gulp 暴露了 src() 和 dest() 方法用于处理计算机上存放的文件

    • src() 接受参数,并从文件系统中读取文件然后生成一个Node流(Stream),它将所有匹配的文件读取到内存中并通过流 (Stream)进行处理

    • 由 src() 产生的流(stream)应当从任务(task函数)中返回并发出异步完成的信号

    • dest() 接受一个输出目录作为参数,并且它还会产生一个 Node流(stream),通过该流将内容输出到文件中;

      const { src, dest } = require('gulp')
      
      const copyFile = (cb) => {
        // 1.读取文件 2.写入文件
        return src('./src/**/*.js').pipe(dest('./dist'))
        // cb()
      }
      
      // 导出的任务
      module.exports = {
        copyFile
      }
      
      

      image-20231205140403876

      image-20231205103212177

  • 流(stream)所提供的主要的 API 是 .pipe() 方法,pipe方法的原理是什么呢?

    • pipe方法接受一个 转换流(Transform streams)可写流(Writable streams)
    • 那么转换流或者可写流,拿到数据之后可以对数据进行处理,再次传递给下一个转换流或者可写流

6.对文件进行转换

  • 如果在这个过程中,我们希望对文件进行某些处理,可以使用社区给我们提供的插件。

    • 比如我们希望ES6转换成ES5,那么可以使用babel插件;

      1. pnpm i @babel/preset-env gulp-babel -D

        const { src, dest } = require('gulp')
        const babel = require('gulp-babel')
        
        const jsTask = (cb) => {
          // 1.读取文件 2.babel转换 3.写入文件
          return src('./src/**/*.js')
            .pipe(
              babel({
                presets: ['@babel/preset-env']
              })
            )
            .pipe(dest('./dist'))
          // cb()
        }
        
        // 导出的任务
        module.exports = {
          jsTask
        }
        
        
    • 如果我们希望对代码进行压缩和丑化,那么可以使用uglify或者terser插件;

      1. pnpm add gulp-terser -D

        const { src, dest } = require('gulp')
        const babel = require('gulp-babel')
        const terser = require('gulp-terser')
        
        const jsTask = (cb) => {
          // 1.读取文件 2.babel转换 3.写入文件
          return src('./src/**/*.js')
            .pipe(
              babel({
                presets: ['@babel/preset-env']
              })
            )
            .pipe(
              terser({
                mangle: {
                  toplevel: true
                }
              })
            )
            .pipe(dest('./dist'))
          // cb()
        }
        
        // 导出的任务
        module.exports = {
          jsTask
        }
        
        

    image-20231205104218285

7.glob文件匹配

  • src() 方法接受一个 glob 字符串或由多个 glob 字符串组成的数组作为参数,用于确定哪些文件需要被操作。
    • glob 或 glob 数组必须至少匹配到一个匹配项,否则 src() 将报错;
  • glob的匹配规则如下:
    • (一个星号*):在一个字符串中,匹配任意数量的字符,包括零个匹配;
      • '*.js'
    • (两个星号**):在多个字符串匹配中匹配任意数量的字符串,通常用在匹配目录下的文件;
      • 'scripts/**/*.js'
    • (取反!):
      • 由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的;
      • 所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面;
      • 第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分;
        • 'script/**/*.js','!script/vendor/'

6.Gulp的文件监听

  • gulp api 中的 watch() 方法利用文件系统的监控程序(file system watcher)将 与进行关联。

    const { src, dest, watch } = require('gulp')
    const babel = require('gulp-babel')
    const terser = require('gulp-terser')
    
    const jsTask = (cb) => {
      // 1.读取文件 2.babel转换 3.写入文件
      return src('./src/**/*.js')
        .pipe(
          babel({
            presets: ['@babel/preset-env']
          })
        )
        .pipe(
          terser({
            mangle: {
              toplevel: true
            }
          })
        )
        .pipe(dest('./dist'))
      // cb()
    }
    watch('./src/**/*.js', jsTask)
    // 导出的任务
    module.exports = {
      jsTask
    }
    
    

    image-20231205142631644

    image-20231205104501223

4.Gulp案例

  • 接下来,我们编写一个案例,通过gulp来开启本地服务和打包:

    • 打包html文件;

      • 使用gulp-htmlmin插件;

        • pnpm i gulp-htmlmin -d

        • const htmlMin = require('gulp-htmlmin')
          
          // 1.对html进行打包
          const htmlTask = () => {
            return src('./src/**/*.html')
              .pipe(
                htmlMin({
                  collapseWhitespace: true
                })
              )
              .pipe(dest('./dist'))
          }
          
    • 打包JavaScript文件;

      • 使用gulp-babel,gulp-terser插件;

      • pnpm add gulp-terser @babel/preset-env gulp-babel -D

      • const { src, dest } = require('gulp')
        const babel = require('gulp-babel')
        const terser = require('gulp-terser')
        
        // 2.对JavaScript进行处理
        const jsTask = (cb) => {
          return src('./src/**/*.js')
            .pipe(
              babel({
                presets: ['@babel/preset-env']
              })
            )
            .pipe(
              terser({
                mangle: {
                  toplevel: true
                }
              })
            )
            .pipe(dest('./dist'))
        }
        
    • 打包less文件;

      • 使用gulp-less插件;

      • pnpm i gulp-less -D

      • const less = require('gulp-less')
        
        // 3.对less文件进行处理
        const lessTask = (cb) => {
          return src('./src/**/*.less').pipe(less()).pipe(dest('./dist'))
        }
        
        module.exports = {
          htmlTask,
          jsTask,
          lessTask
        }
        
        
    • 打包postcss

      https://www.npmjs.com/package/gulp-postcss

    • html资源注入

      • 使用gulp-inject插件;

      • pnpm i gulp-inject -D

      • <!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>Document</title>
            <!-- inject:css -->
            <!-- endinject -->
          </head>
          <body>
            <!-- inject:js -->
            <!-- endinject -->
          </body>
        </html>
        
        

        插件要求需要注释

        const inject = require('gulp-inject')
        
        // 4.在html 中注入js和css
        const injectTask = (cb) => {
          return src('./dist/**/*.html')
            .pipe(
              inject(src(['./dist/**/*.js', './dist/**/*.css']), { relative: true })
            )
            .pipe(dest('./dist'))
        }
        
    • 开启本地服务器

      • 使用browser-sync插件;

      • pnpm i browser-sync -D

      • const browserSync = require('browser-sync')
        
        // 5.开启本地服务
        const bs = browserSync.create()
        const serveTask = () => {
          bs.init({
            port: 8080,
            open: true,
            files: './dist/*',
            server: {
              baseDir: './dist'
            }
          })
        }
        

        npx gulp serveTask

    • 创建打包任务

      npx gulp buildTask

      const buildTask = series(parallel(htmlTask, jsTask, lessTask), injectTask)
      
      
      module.exports = {
        buildTask
      }
      
    • 创建开发任务

      npx gulp serveTask

      
      // 5.开启本地服务
      const bs = browserSync.create()
      const serve = () => {
        watch('./src/**', buildTask)
        bs.init({
          port: 8080,
          open: true,
          files: './dist/*',
          server: {
            baseDir: './dist'
          }
        })
      }
      
      const buildTask = series(parallel(htmlTask, jsTask, lessTask), injectTask)
      
      const serveTask = series(buildTask, serve)
      
      module.exports = {
        buildTask,
        serveTask
      }
      
      

    image-20231205104622406

    const { src, dest, parallel, series, watch } = require('gulp')
    const htmlMin = require('gulp-htmlmin')
    const babel = require('gulp-babel')
    const terser = require('gulp-terser')
    const less = require('gulp-less')
    const inject = require('gulp-inject')
    const browserSync = require('browser-sync')
    // 1.对html进行打包
    const htmlTask = () => {
      return src('./src/**/*.html')
        .pipe(
          htmlMin({
            collapseWhitespace: true
          })
        )
        .pipe(dest('./dist'))
    }
    
    // 2.对JavaScript进行处理
    const jsTask = (cb) => {
      return src('./src/**/*.js')
        .pipe(
          babel({
            presets: ['@babel/preset-env']
          })
        )
        .pipe(
          terser({
            mangle: {
              toplevel: true
            }
          })
        )
        .pipe(dest('./dist'))
    }
    
    // 3.对less文件进行处理
    const lessTask = (cb) => {
      return src('./src/**/*.less').pipe(less()).pipe(dest('./dist'))
    }
    
    // 4.在html 中注入js和css
    const injectTask = (cb) => {
      return src('./dist/**/*.html')
        .pipe(
          inject(src(['./dist/**/*.js', './dist/**/*.css']), { relative: true })
        )
        .pipe(dest('./dist'))
    }
    
    // 5.开启本地服务
    const bs = browserSync.create()
    const serve = () => {
      watch('./src/**', buildTask)
      bs.init({
        port: 8080,
        open: true,
        files: './dist/*',
        server: {
          baseDir: './dist'
        }
      })
    }
    
    const buildTask = series(parallel(htmlTask, jsTask, lessTask), injectTask)
    
    const serveTask = series(buildTask, serve)
    
    module.exports = {
      buildTask,
      serveTask
    }
    
    

    image-20231205215759051

三.库打包工具Rollup

1.关于Rollup

  • 我们来看一下官方对rollup的定义:
    • Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.
    • Rollup是一个JavaScript的模块化打包工具,可以帮助我们编译小的代码到一个大的、复杂的代码中,比如一个库或者一个应用程序
  • 我们会发现Rollup的定义、定位和webpack非常的相似:
    • Rollup也是一个模块化的打包工具,但是Rollup主要是针对ES Module进行打包的;
    • 另外webpack通常可以通过各种loader处理各种各样的文件,以及处理它们的依赖关系
    • rollup更多时候是专注于处理JavaScript代码的(当然也可以处理css、font、vue等文件);
    • 另外rollup的配置和理念相对于webpack来说,更加的简洁和容易理解
    • 在早期webpack不支持tree shaking时,rollup具备更强的优势;
  • 目前webpack和rollup分别应用在什么场景呢?
    • 通常在实际项目开发过程中,我们都会使用webpack(比如react、angular项目都是基于webpack的);
    • 对库文件进行打包时,我们通常会使用rollup(比如vue、react、dayjs源码本身都是基于rollup的,Vite底层使用Rollup);

2.Rollup基本使用

https://www.rollupjs.com/command-line-interface/

1.安装及使用

  • 我们可以先安装rollup:

    # 全局安装
    npm install rollup -g
    # 局部安装
    npm install rollup -D
    
  • 创建main.js文件,打包到bundle.js文件中:

    # 打包浏览器的库
    npx rollup ./src/main.js -f iife -o dist/bundle.js
    # 打包AMD的库
    npx rollup ./src/main.js -f amd -o dist/bundle.js
    # 打包CommonJS的库
    npx rollup ./src/main.js -f cjs -o dist/bundle.js
    # 打包通用的库(必须跟上name)
    npx rollup ./src/main.js -f umd --name mcUtils -o dist/bundle.js
    

    -f 标识使用环境

    image-20231205223622048

    image-20231205223629525

    image-20231205223501293

2.配置文件

https://www.rollupjs.com/configuration-options/

  • 我们可以将配置信息写到配置文件中rollup.config.js文件:

    image-20231205105331309

  • 我们可以对文件进行分别打包,打包出更多的库文件(用户可以根据不同的需求来引入):

    image-20231205105340654

  • 示例:

    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './lib/index.js',
      // 出口
      output: [
        {
          format: 'umd',
          name: 'mcUtils',
          file: './build/bundle.umd.js'
        },
        {
          format: 'cjs',
          file: './build/bundle.cjs.js'
        },
        {
          format: 'amd',
          name: 'mcUtils',
          file: './build/bundle.amd.js'
        },
        {
          format: 'iife',
          name: 'mcUtils',
          file: './build/bundle.iife.js'
        }
      ]
    }
    module.exports = config
    
    

    npx rollup -c

3.解决commonjs和第三方库问题

比如库中使用了一些第三方库,导致打包时,被打包进去,(但是使用lodash库没有被打包,是因为这个包使用的是cjs模块导出的,而rollup是使用es模块的,导致默认没有打包。所以按照下述教程使用commonjs库来解决。其次loadsh库是在node_modules中的,所以使用下述中解决node_modules的库的方法来解决在node_modules中的包不进行打包的问题)

image-20231205225415681

  • 安装解决commonjs的库:

    用来解决commonjs模块无法使用的问题

    image-20231205230224744

    image-20231205230235902

    npm install @rollup/plugin-commonjs -D
    
    const commonjs = require('@rollup/plugin-commonjs')
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      ...,
      plugins: [commonjs()]
    }
    module.exports = config
    
    
  • 安装解决node_modules的库:

    解决node_modules中的库不被打包的问题

    image-20231205231023656

    image-20231205231036255

    npm install @rollup/plugin-node-resolve -D
    

    @rollup/plugin-node-resolve 插件的主要职责是帮助 Rollup 解析和处理 Node.js 风格的模块导入语句。
    在 Node.js 环境中,模块可以通过相对路径、绝对路径、npm 包名(如 import something from 'my-package')等多种方式进行导入。
    然而,Rollup 默认仅支持 ES 模块导入(即通过相对或绝对路径导入本地文件)。
    使用 @rollup/plugin-node-resolve 插件后,Rollup 能够识别并正确处理 Node.js 的模块导入机制,包括解析 node_modules 目录下的依赖包以及处理 package.json 中的 mainmodulebrowser 等字段。

  • 打包和排除lodash

    一般不打包的话,则需要由用户自己引入,打包时进行排除

    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './lib/index.js',
      // 出口
      output: [
        {
          format: 'umd',
          name: 'mcUtils',
          file: './build/bundle.umd.js',
          // 给lodash包取一个全局的名字
          globals: {
            lodash: '_'
          }
        }
      ],
      external: ['lodash'],
      plugins: [commonjs(), nodeReslove()]
    }
    module.exports = config
    

    image-20231205105432019

4.Babel转换代码

  • 如果我们希望将ES6转成ES5的代码,可以在rollup中使用babel。

  • 安装rollup对应的babel插件:

    npm install @rollup/plugin-babel -D
    
  • 修改配置文件:

    • 需要配置babel.config.js文件;

    • babelHelpers:

      image-20231205105518268

      const commonjs = require('@rollup/plugin-commonjs')
      const nodeReslove = require('@rollup/plugin-node-resolve')
      
      // babel
      const babel = require('@rollup/plugin-babel')
      
      /**@type {import('rollup').RollupOptions} */
      const config = {
        // 入口
        input: './lib/index.js',
        // 出口
        output: [
          {
            format: 'umd',
            name: 'mcUtils',
            file: './build/bundle.umd.js',
            // 给lodash包取一个全局的名字
            globals: {
              lodash: '_'
            }
          }
        ],
        external: ['lodash'],
        plugins: [
          commonjs(),
          nodeReslove(),
          babel({
            presets: ['@babel/preset-env']
          })
        ]
      }
      module.exports = config
      
      

5.Teser代码压缩

  • 如果我们希望对代码进行压缩,可以使用@rollup/plugin-terser:

    rollup-plugin-terser 是支持2.0版本的

    npm install @rollup/plugin-terser -D
    
  • 配置terser:

    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    
    // babel
    const { babel } = require('@rollup/plugin-babel')
    
    // terser
    const terser = require('@rollup/plugin-terser')
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './lib/index.js',
      // 出口
      output: [
        {
          format: 'umd',
          name: 'mcUtils',
          file: './build/bundle.umd.js',
          // 给lodash包取一个全局的名字
          globals: {
            lodash: '_'
          }
        }
      ],
      external: ['lodash'],
      plugins: [
        commonjs(),
        nodeReslove(),
        babel({
          presets: ['@babel/preset-env'],
          babelHelpers: 'bundled',
          exclude: /node_modules/
        }),
        terser()
      ]
    }
    module.exports = config
    
    

    image-20231205105557987

6.处理css文件

  • 如果我们项目中需要处理css文件,可以使用postcss:

    npm install rollup-plugin-postcss postcss-preset-env postcss -D
    
  • 配置postcss的插件:

    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    
    // babel
    const { babel } = require('@rollup/plugin-babel')
    
    // terser
    const terser = require('@rollup/plugin-terser')
    
    // postcss
    const postcss = require('rollup-plugin-postcss')
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './src/index.js',
      // 出口
      output: [
        {
          format: 'iife',
          name: 'mcUtils',
          file: './build/bundle.iife.js'
        }
      ],
      plugins: [
        commonjs(),
        nodeReslove(),
        babel({
          presets: ['@babel/preset-env'],
          babelHelpers: 'bundled',
          exclude: /node_modules/
        }),
        terser(),
        postcss({
          plugins: [require('postcss-preset-env')]
        })
      ]
    }
    module.exports = config
    
    

    image-20231205105645002

    image-20231206112044471

7.处理vue文件

  • 处理vue文件我们需要使用rollup-plugin-vue插件:

    • 但是注意:默认情况下我们安装的是vue3.x的版本,所以我这里指定了一下rollup-plugin-vue的版本;

      npm install rollup-plugin-vue @vue/compiler-sfc -D
      
  • 使用vue的插件:

    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    
    // babel
    const { babel } = require('@rollup/plugin-babel')
    
    // terser
    const terser = require('@rollup/plugin-terser')
    
    // postcss
    const postcss = require('rollup-plugin-postcss')
    
    // vue
    const vue = require('rollup-plugin-vue')
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './src/index.js',
      // 出口
      output: [
        {
          format: 'iife',
          name: 'mcUtils',
          file: './build/bundle.iife.js'
        }
      ],
      plugins: [
        commonjs(),
        nodeReslove(),
        babel({
          presets: ['@babel/preset-env'],
          babelHelpers: 'bundled',
          exclude: /node_modules/
        }),
        terser(),
        postcss({
          plugins: [require('postcss-preset-env')]
        }),
        vue()
      ]
    }
    module.exports = config
    
    

    image-20231205105738330

8.打包vue报错

  • 在我们打包vue项目后,运行会报如下的错误:

    image-20231205105812334

  • 这是因为在我们打包的vue代码中,用到 process.env.NODE_ENV,所以我们可以使用一个插件 rollup-plugin-replace 设置 它对应的值:

    npm install @rollup/plugin-replace  -D
    

    rollup-plugin-replace ? 这两个包都可以,推荐使用@rollup/plugin-replace

  • 配置插件信息:

    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    
    // babel
    const { babel } = require('@rollup/plugin-babel')
    
    // terser
    const terser = require('@rollup/plugin-terser')
    
    // postcss
    const postcss = require('rollup-plugin-postcss')
    
    // vue
    const vue = require('rollup-plugin-vue')
    
    // 环境变量
    const replace = require('rollup-plugin-replace')
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './src/index.js',
      // 出口
      output: [
        {
          format: 'iife',
          name: 'mcUtils',
          file: './build/bundle.iife.js'
        }
      ],
      plugins: [
        commonjs(),
        nodeReslove(),
        babel({
          presets: ['@babel/preset-env'],
          babelHelpers: 'bundled',
          exclude: /node_modules/
        }),
        terser(),
        postcss({
          plugins: [require('postcss-preset-env')]
        }),
        vue(),
        replace({
          'process.env.NODE_ENV': JSON.stringify('production')
        })
      ]
    }
    module.exports = config
    
    

    image-20231205105832157

9.搭建本地服务器

  • 第一步:使用rollup-plugin-serve搭建服务

    npm install rollup-plugin-serve -D
    

    此处多次提示报错,因这个包不是cjs导出而无法使用

    image-20231205105922345

  • 第二步:当文件发生变化时,自动刷新浏览器

    npm install rollup-plugin-livereload -D
    

    image-20231205105949504

  • 第三步:启动时,开启文件监听

    npx rollup -c -w
    
    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    
    // babel
    const { babel } = require('@rollup/plugin-babel')
    
    // terser
    const terser = require('@rollup/plugin-terser')
    
    // postcss
    const postcss = require('rollup-plugin-postcss')
    
    // vue
    const vue = require('rollup-plugin-vue')
    
    // 环境变量
    const replace = require('@rollup/plugin-replace')
    
    // 服务
    const serve = require('rollup-plugin-serve')
    const livereload = require('rollup-plugin-livereload')
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './src/index.js',
      // 出口
      output: [
        {
          format: 'iife',
          name: 'mcUtils',
          file: './build/bundle.iife.js'
        }
      ],
      plugins: [
        commonjs(),
        nodeReslove(),
        babel({
          presets: ['@babel/preset-env'],
          babelHelpers: 'bundled',
          exclude: /node_modules/
        }),
        terser(),
        postcss({
          plugins: [require('postcss-preset-env')]
        }),
        vue(),
        replace({
          'process.env.NODE_ENV': JSON.stringify('production')
        }),
        serve({
          port: 8000,
          open: true,
          contentBase: '.'
        }),
        livereload()
      ]
    }
    module.exports = config
    
    

10.区分开发环境

  • 我们可以在package.json中创建一个开发和构建的脚本:

    image-20231205110043105

    const commonjs = require('@rollup/plugin-commonjs')
    const nodeReslove = require('@rollup/plugin-node-resolve')
    
    // babel
    const { babel } = require('@rollup/plugin-babel')
    
    // terser
    const terser = require('@rollup/plugin-terser')
    
    // postcss
    const postcss = require('rollup-plugin-postcss')
    
    // vue
    const vue = require('rollup-plugin-vue')
    
    // 环境变量
    const replace = require('@rollup/plugin-replace')
    
    // 服务
    const serve = require('rollup-plugin-serve')
    const livereload = require('rollup-plugin-livereload')
    
    const isProduction = process.env.NODE_ENV === 'production'
    
    const plugins = [
      commonjs(),
      nodeReslove(),
      babel({
        presets: ['@babel/preset-env'],
        babelHelpers: 'bundled',
        exclude: /node_modules/
      }),
      postcss({
        plugins: [require('postcss-preset-env')]
      }),
      vue(),
      replace({
        'process.env.NODE_ENV': JSON.stringify('production')
      })
    ]
    
    if (isProduction) {
      plugins.push(terser())
    } else {
      const extraPlugins = [
        serve({
          port: 8000,
          open: true,
          contentBase: '.'
        }),
        livereload()
      ]
      plugins.push(...extraPlugins)
    }
    
    /**@type {import('rollup').RollupOptions} */
    const config = {
      // 入口
      input: './src/index.js',
      // 出口
      output: [
        {
          format: 'iife',
          name: 'mcUtils',
          file: './build/bundle.iife.js'
        }
      ],
      plugins
    }
    
    module.exports = config
    
    

四.快速开发工具Vite

1.关于Vite

  • 什么是vite呢?

    • 官方的定位:下一代前端开发与构建工具;
  • 如何定义下一代开发和构建工具呢?

    • 我们知道在实际开发中,我们编写的代码往往是不能被浏览器直接识别的,比如ES6、TypeScript、Vue文件等等;
    • 所以我们必须通过构建工具来对代码进行转换、编译,类似的工具有webpack、rollup、parcel;
    • 但是随着项目越来越大,需要处理的JavaScript呈指数级增长,模块越来越多;
    • 构建工具需要很长的时间才能开启服务器,HMR也需要几秒钟才能在浏览器反应出来;
    • 所以也有这样的说法:天下苦webpack久矣
  • Vite (法语意为 "快速的",发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验。

    image-20231205110256728

2.Vite的构造

image-20231206230621646

  • 它主要由两部分组成:
    • 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;
    • 一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;
  • 在浏览器支持 ES 模块之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发
    • 这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。
    • 时过境迁,我们见证了诸如 webpack、Rollup 和 Parcel 等工具的变迁,它们极大地改善了前端开发者的开发体验。
    • 然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相 当普遍。
    • 基于 JavaScript 开发的工具就会开始遇到性能瓶颈:通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。
  • Vite 旨在利用生态系统中的新进展解决上述问题
    • 浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。
    • the rise of JavaScript tools written in compile-to-native languages.

3.浏览器原生支持模块化

import { sum, mul } from './utils/math.js'
import _ from '../node_modules/lodash-es/lodash.default.js'
const message = 'Hello Vite'
console.log(message)

const foo = () => {
  console.log('foo-function')
}
foo()

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

console.log(_.join(['abc', 'cba']))

main.js

image-20231205110447051

  • 但是如果我们不借助于其他工具,直接使用ES Module来开发有什么问题呢?
    • 首先,我们会发现在使用loadash时,加载了上百个模块的js代码,对于浏览器发送请求是巨大的消耗;

      image-20231206232529692

    • 其次,我们的代码中如果有TypeScript、less、vue等代码时,浏览器并不能直接识别;

  • 事实上,vite就帮助我们解决了上面的所有问题。

4.Vite的基本使用

1.安装

image-20231207110054575

文件目录

image-20231207110235222

main.js

  • 首先,我们安装一下vite工具:

    npm i vite -g
    npm i vite -d
    
  • 通过vite来启动项目:

    npx vite
    

2.对css的支持

  • vite可以直接支持css的处理

    • 直接导入css即可;
  • vite可以直接支持css预处理器,比如less

    • 直接导入less;

    • 之后安装less编译器;

    • npm install less -D
      
  • vite直接支持postcss的转换:

    • 只需要安装postcss,并且配置 postcss.config.js 的配置文件即可;

      npm install postcss postcss-preset-env -D
      

      image-20231205110807124

      postcss.config.js

3.对TypeScript的支持

  • vite对TypeScript是原生支持的,它会直接使用ESBuild来完成编译:

    • 只需要直接导入即可;
  • 如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:

    • 这是因为vite中的服务器Connect会对我们的请求进行转发;
    • 获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析;
  • 注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器

    image-20231205110854586

4.对vue的支持

  • vite对vue提供第一优先级支持:

  • 安装支持vue的插件:

    npm install @vitejs/plugin-vue -D
    
  • 在vite.config.js中配置插件:

    import vue from '@vitejs/plugin-vue'
    import { defineConfig } from 'vite'
    
    // /**@type {import('vite').UserConfig} */
    // const config = {
    //   plugins: [vue()]
    // }
    
    export default defineConfig({
      plugins: [vue()]
    })
    
    

    image-20231205111016369

5.对react的支持

  • .jsx 和 .tsx 文件同样开箱即用,它们也是通过 ESBuild来完成的编译:

    • 所以我们只需要直接编写react的代码即可;
    • 注意:在index.html加载main.js时,我们需要将main.js的后缀,修改为 main.jsx 作为后缀名;
    import { sum, mul } from './utils/math'
    import _ from 'lodash-es'
    import { formatPrice } from './ts/format'
    import './css/style.css'
    import './css/normal.less'
    import VueApp from './vue/App.vue'
    import { createApp } from 'vue'
    import ReactApp from './react/app.jsx'
    import React from 'react'
    import ReactDom from 'react-dom/client'
    const message = 'Hello Vite'
    console.log(message)
    
    const foo = () => {
      console.log('foo-function')
    }
    foo()
    
    console.log(sum(1, 2))
    console.log(mul(1, 2))
    
    // node_modules
    console.log(_.join(['abc', 'cba']))
    
    // ts代码
    console.log(formatPrice(10000))
    
    // DOM操作
    const titleEl = document.createElement('h1')
    titleEl.textContent = '我是标题哈哈哈哈'
    titleEl.classList.add('title')
    document.body.append(titleEl)
    
    // vue代码
    const vueApp = createApp(VueApp)
    vueApp.mount(document.querySelector('#VueApp'))
    
    // react代码
    const root = ReactDom.createRoot(document.querySelector('#ReactApp'))
    root.render(<ReactApp />)
    
    

    main

    image-20231205111114113

6.Vite打包项目

  • 我们可以直接通过vite build来完成对当前项目的打包工具:

    npx vite build
    

    image-20231205111208471

  • 我们可以通过preview的方式,开启一个本地服务来预览打包后的效果:

    npx vite preview
    

5.Vite脚手架工具

  • 在开发中,我们不可能所有的项目都使用vite从零去搭建,比如一个react项目、Vue项目;

    • 这个时候vite还给我们提供了对应的脚手架工具;
  • 所以Vite实际上是有两个工具的:

    • vite:相当于是一个构件工具,类似于webpack、rollup;
    • @vitejs/create-app:类似vue-cli、create-react-app;
  • 如果使用脚手架工具呢?

    npm create vite
    yarn create vite
    pnpm create vite
    

6.ESBuild解析

  • ESBuild的特点

    • 超快的构建速度,并且不需要缓存;
    • 支持ES6和CommonJS的模块化;
    • 支持ES6的Tree Shaking;
    • 支持Go、JavaScript的API;
    • 支持TypeScript、JSX等语法编译;
    • 支持SourceMap;
    • 支持代码压缩;
    • 支持扩展其他插件;
  • ESBuild的构建速度和其他构建工具速度对比:

    image-20231205112248842

  • ESBuild为什么这么快呢?

    • 使用Go语言编写的,可以直接转换成机器代码,而无需经过字节码;
    • ESBuild可以充分利用CPU的多内核,尽可能让它们饱和运行;
    • ESBuild的所有内容都是从零开始编写的,而不是使用第三方,所以从一开始就可以考虑各种性能问题;
    • 等等...
  • image-20231207161026051

五.脚手架开发

https://github.com/coderwhy/coderwhy 借鉴项目

1.基础搭建

1.执行命令

  1. pnpm init

  2. 目录结构

    image-20231207162109771

  3. 更改 package.json

    image-20231207162144055

  4. 更改 ./lib/index.js

    #!/usr/bin/env node
    
    console.log('mc cli code exec')
    

    注释是为了提示用什么(node)来执行

  5. 执行 npm link

    image-20231207162511544

    建立软连接,解决 mccli 没有环境变量的问题

  6. 执行 mccli

    image-20231207162536546

  7. 此时在 ./lib/index.js 编写相应逻辑即可

2.传递参数

image-20231207163225738

自己来解析的化,会很繁琐,使用 Commander.js 工具

  1. pnpm i commander

  2. 处理版本号 ./lib/index.js

    #!/usr/bin/env node
    const { program } = require('commander')
    
    // 启动提示
    console.log('mc cli code exec')
    
    // 处理版本号参数
    const version = require('../package.json').version
    program.version(version, '-v --version')
    program.parse(process.argv)
    
    

    可执行 mccli -v mccli -version

    image-20231207164003308

  3. 增强参数

    #!/usr/bin/env node
    const { program } = require('commander')
    
    // 启动提示
    console.log('mc cli code exec')
    
    // 处理版本号参数
    const version = require('../package.json').version
    program.version(version, '-v --version')
    
    // 增强参数
    program.option('-m --mc', 'a mc cli program~')
    program.option('-d --dest', 'a destination folder, 例如: -d src/components')
    
    // 解析process.argv参数
    program.parse(process.argv)
    
    

    执行 mccli --help

    image-20231207164213271

  4. 传入参数

    image-20231207164406826

    执行 mccli -d src/sss.vue

    image-20231207164438349

  5. 增强 --help 的提示

    
    program.on('--help', () => {
      console.log('')
      console.log('others:')
      console.log('  提示1:')
      console.log('  提示2:')
    })
    
    

    image-20231207164813081

  6. 分离

    image-20231207165101881

    image-20231207165108945

    image-20231207165116881

2.增强功能

1.添加create命令

该命令用于创建项目的模板

  1. 添加create命令 lib/index.js

    #!/usr/bin/env node
    const { program } = require('commander')
    const helpOptions = require('./core/heko-options')
    
    // 1.配置所有的options
    helpOptions()
    
    // 2.增加具体的功能操作
    program
      .command('create <project> [..others]')
      .description(
        'create vue project into a folder, 比如: mccli create blog_project'
      )
    
    // 解析process.argv参数
    program.parse(process.argv)
    
    // console.log(program.opts().dest)
    
    

    image-20231207165950646

  2. 添加create命令的功能

    // 2.增加具体的功能操作
    program
      .command('create <project> [..others]')
      .description(
        'create vue project into a folder, 比如: mccli create blog_project'
      )
      .action((project) => {
        console.log('创建出来一个项目:', project)
      })
    

    image-20231207170331684

  3. 添加库 pnpm i download-git-repo

  4. 抽离action代码

    image-20231207170926191

  5. 编写action代码

    // promisify 可以将函数转为promise
    const { promisify } = require('util')
    const download = promisify(require('download-git-repo'))
    // 'direct:https://github.com/coderwhy/vue3_template.git#main'
    const { VUE_REPO } = require('../config/repo')
    
    async function createProjectAction(project) {
      try {
        // 1.添加模板,一般可以放到github仓库上,然后git clone
        await download(VUE_REPO, project, { clone: true })
      } catch (error) {
        console.log('github链接失败,请稍后尝试')
      }
    }
    
    module.exports = {
      createProjectAction
    }
    
    

    image-20231207171724744

    image-20231207171737267

  6. 继续完善提示

    async function createProjectAction(project) {
      try {
        // 1.添加模板,一般可以放到github仓库上,然后git clone
        await download(VUE_REPO, project, { clone: true })
    
        // 2.完善提示
        console.log(`cd ${project}`)
        console.log('npm install')
        console.log('npm run dev')
      } catch (error) {
        console.log('github链接失败,请稍后尝试')
      }
    }
    

    image-20231207172414579

  7. 完善自动执行脚本

    const execCommand = require('../utils/exec-command')
    async function createProjectAction(project) {
      try {
        // 1.添加模板,一般可以放到github仓库上,然后git clone
        await download(VUE_REPO, project, { clone: true })
    
        // 2.完善提示
        // console.log(`cd ${project}`)
        // console.log('npm install')
        // console.log('npm run dev')
    
        // 3.也可帮助执行 npm install 需要node子进程 npm.cmd 是因为win系统下
        const commandName = process.platform === 'win32' ? 'pnpm.cmd' : 'npm'
        await execCommand(commandName, ['install'], { cwd: `./${project}` })
    
        // 4.执行 npm run dev
        await execCommand(commandName, ['run', 'dev'], { cwd: `./${project}` })
      } catch (error) {
        console.log(error)
      }
    }
    

    index.js

    const { spawn } = require('child_process')
    function execCommand(...args) {
      return new Promise((reslove) => {
        // npm install/npm run dec
    
        // 1.开启子进程执行命令
        const childProcess = spawn(...args)
    
        // 2.获取子进程的输出和错误信息(为啥经过测试输出的是一样的东西?)
        childProcess.stdout.pipe(process.stdout)
        childProcess.stdout.pipe(process.stderr)
    
        // 3.监听子进程执行结束,关闭掉
        childProcess.on('close', () => {
          reslove()
        })
      })
    }
    
    module.exports = execCommand
    
    

    exec-command.js

    image-20231207180412399

2.添加addcpn命令

该命令用于创建组件

  1. 编写模板 template/component.vue.ejs

    <template>
      <div class="<%= lowername %>">
        <h2>
          <%= name %>:{{ message }}
        </h2>
      </div>
    </template>
    
    <script setup>
      import { ref } from 'vue'
      const message = ref('hhhh')
    </script>
    <style scoped>
      .<%=lowername %> {
        color: red;
      }
    </style>
    
    
  2. pnpm i ejs

  3. 编写工具函数 utils/compile-ejs.js

    将ejs模板进行转换

    const path = require('path')
    const ejs = require('ejs')
    
    function compileEjs(tempName, data) {
      return new Promise((resolve, reject) => {
        // 1.获取模板路径
        const tempPath = `../template/${tempName}`
        const absolutePath = path.resolve(__dirname, tempPath)
    
        // 2.使用ejs引擎编译模板
        ejs.renderFile(absolutePath, data, (err, result) => {
          if (err) {
            console.log('编译模板失败:', err)
            reject(err)
          } else {
            resolve(result)
          }
        })
      })
    }
    
    module.exports = compileEjs
    
    
  4. 编写工具函数 utils/write-file.js

    const fs = require('fs')
    
    function writeFile(path, content) {
      return fs.promises.writeFile(path, content)
    }
    
    module.exports = writeFile
    
    
  5. 编写 mccli addcpn 命令

    const { createProjectAction, addComponetnAction } = require('./core/actions')
    
    program
      .command('addcpn <cpnname> [..others]')
      .description(
        'add vue component into a folder, 比如: mccli addcpn NavBar -d src/components'
      )
      .action((cpnname) => {
        addComponetnAction(cpnname)
      })
    

    index.js

    async function addComponetnAction(cpnname) {
      // 1.创建一个组件:创建一个模板,在模板中添加数据,得到其模板
      const result = await compileEjs('component.vue.ejs', {
        name: cpnname,
        lowername: cpnname.toLowerCase()
      })
      // 2.将result写到对应的路径中
      writeFile(`src/components/${cpnname}.vue`, result)
      console.log('创建组件成功:', `${cpnname}.vue`)
    }
    
    module.exports = {
      createProjectAction,
      addComponetnAction
    }
    

    core/action.js

  6. 动态获取 --dest xxx 的地址

    image-20231207183810536

    前面注册时,注册过 --dest 的命令,这里使用 dest 来获取路径

    image-20231207183904608

    image-20231207183916065

评论 0
0 / 500
  • Mingcomity

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    2024年3月14日 10 1
    abc

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    2024年3月14日 10 回复
    bbbbb回复abc

    述文本描述文本描述文本描述文本描述文本描述文本

    述文本描述文本描述文本描述文本描述文本描述文本

    2024年3月14日 10 回复
    Mingcomity

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    2024年3月14日 10 回复

    查看全部3条回复

  • Mingcomity

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    2024年3月14日 10 10
  • Mingcomity

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本

    2024年3月14日 10 10
联系方式:2662504913@qq.com