Webpack5学习笔记——基础认识

3,903 阅读18分钟

本笔记为CoderWhy老师Webpack5教程的学习笔记,内容来自CoderWhy老师的PPT以及Webpack官方文档。

基本配置

webpack配置文件

  • webpack默认的配置文件为 webpack.config.js
  • 可以通过 webpack --config shirllyuan.config.js的指令将webpack的配置文件修改为当前终端根目录下的shirllyuan.config.js
  • 可以在package.json中script中添加如下配置,来使用npm run build进行构建,此时配置文件目录相对于package.json所在目录
  "scripts": {
    "build": "webpack --config shirllyuan.config.js",
    "serve": "webpack serve --config shirllyuan.config.js",
  },
  • 入口和出口基本配置:
const path = require('path')
​
module.exports = {
    mode: "development",
    entry: "./src/service.js",
    output: {
        filename: "main.js",
        path: path.resolve(__dirname,"./dist")
    }
}

loader的使用

module.rules

  • test属性: 用于对 resource(资源)进行匹配的,通常会设置成正则表达式;

  • use属性:用来声明需要使用那些loader,loader的顺序按照从右到左,从下到上的顺序加载[useEntry]

    • useEntry一个对象,webpack支持使用字符串进行简写(当不存在options时可使用loader的字符串进行表示)

      • loader:必须有一个 loader属性,对应的值是一个字符串;
      • options: 可选的属性,值是一个字符串或者对象,值会被传入到loader中
  • loader属性:这个是在只使用一个loader时use属性的简写形式

CSS-loader

  • 可以用来读取css文件
  • 内联方式加载
import "css-loader!./css/index.css"
  • 配置方式
module: {
    rules;[
        {
            test: /.css$/,  //使用正则表达式匹配资源
            use: [
                {
                    loader: 'css-laoder',  //使用的loader名字
                    option: {   //额外选项,此处未css-loader的配置
                        importLoaders: 1,
                    }
                }
            ]
        }
    ]
}

Style-loader

  • 只使用css-loader只能使得webpack对css文件进行解析,无法将css注入我们的js代码中,可以使用style-loader将我们引入的css文件注入到代码中
  • style-loader会生成一个style的dom节点,并将我们的css代码插入该dom中,最后将该dom节点挂载到html文档中
  • style-loader配置:
use: [
    "style-loader",
    "css-loader"
]
//必须按照这个顺序进行执行,css-loader对文件进行解析,然后交给style-loader插入文档

Less-loader

  • 开发过程中我们会用到less、sasa等css预处理器,加载对应的文件就需要对应的loader
  • 配置信息:
{
    test: /.less$/,
    use:[
        "style-loader",
        "css-loader",
        "less-loader"
    ]
}
//过程: less-loader调用less将less转为css,css-loader与style-loader进行解析代码和插入文档

Browserslist

  • 一个用于查询浏览器市场占有率的node脚本,主要调用调用caniuse-lite工具,在caniuse.com这个网站上来查询市场占有率等信息。

  • 可以使用命令行进行调用npx browserslist “>1%, last 2version. not dead”

  • 配置规则

    • 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 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个月内没有官方支持或更新的浏览器。

    • last 2 versions:每个浏览器的最后2个版本。

      • last 2 Chrome versions:最近2个版本的Chrome浏览器。
      • last 2 major versions或last 2 iOS major versions:最近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:排除先前查询选择的浏览器

  • 配置文件为.browserslistrc文件运行browserslist时会使用改文件的配置

  • 也可以在package.json中进行配置

"browserslist": [
    "last 2 version",
    "not dead",
    "> 0.5%"
]
  • 多个条件之间的关系(来自browserslist文档)

browserslist.png

PostCSS

  • PostCSS可以借助node环境进行样式转换,如:使用autoprefixer插件添加浏览器前缀等。
  • PostCSS可以使用Cli工具进行独立使用(postcss-cli)
npx postcss --use autoprefixer -o end.css ./src/css/index.css
  • PostCSS配置文件为postcss.config.js
module.exports = {
    plugins: [
                // require("autoprefixer"), //require("postcss-preset-env"),已经包含autoprefixer
                require("postcss-preset-env"),
                // require("postcss-preset-env")({
                //   //此处可添加参数
                // }),
                // 也可以不使用require
                // "autoprefixer":{
                //     //此处添加配置
                // }
              ]
}
  • 也可以在webpack.config.js中进行配置对postcss-loader设置
          //如果postcss.config.js未配置则必须在这里配置
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  // require("autoprefixer"), //require("postcss-preset-env"),已经包含
                  require("postcss-preset-env"),
                  // require("postcss-preset-env")({
                  //   //此处可添加参数
                  // }),
                ]
              }
            }
          }
Autoprefixer
  • 根据browserslist的结果来为我们的CSS添加浏览器前缀
postcss-preset-env
  • postcss-preset-env是postcss的环境预设,该预设已经包含autoprefixer等插件,也会为我们的CSS环境添加polyfill

webpack对css文件完整配置

  module: {
    rules: [
      {
        // 规则使用正则表达式
        test: /.css$/, // 匹配资源
        use: [
          // { loader: "css-loader" },
          // 注意: 编写顺序(从下往上, 从右往做, 从后往前)
          "style-loader", 
          {
            loader: "css-loader",
            options: {
              // url: false,
              importLoaders: 1,
            }
          },
          //如果postcss.config.js未配置则必须在这里配置
          // {
          //   loader: "postcss-loader",
          //   options: {
          //     postcssOptions: {
          //       plugins: [
          //         // require("autoprefixer"), //require("postcss-preset-env"),已经包含
          //         require("postcss-preset-env"),
          //         // require("postcss-preset-env")({
          //         //   //此处可添加参数
          //         // }),
          //       ]
          //     }
          //   }
          // }
          // "css-loader",
          //配置信息也配置在postcss.config.js中,此时只传入字符串即可
          "postcss-loader"
        ],
        // loader: "css-loader"
      },
      {
        test: /.less$/,
        use: [
          "style-loader",
          {
            loader: "css-loader", 
            options: {
              importLoaders: 2
            }
          },
          "postcss-loader",
          "less-loader"
        ]
      }
    ]
  },

图片、字体等文件的加载

file-loader

  • file-loader的作用就是帮助我们处理import/require()方式引入的一个文件资源,并且会将它放到我们输出的 文件夹中;
  • 基本使用方式:
{
    test: /.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: "file-loader"
    }
}
  • 为了保留原来文件的文件名、拓展名,以及防止文件重复,我们会采用占位符(PlaceHolder)来进行对文件命名

  • 常用的PlaceHolder:

    • [ext] : 处理文件拓展名
    • [name] : 处理文件的名称
    • [hash] : 文件的内容,使用MD4的散列函数处理,生成的一个128位的hash值(32个十六进制)
    • [contentHash]:在file-loader中和[hash]结果是一致的(在webpack的一些其他地方不一样,在打包优化的chunk的地方会说到);
    • [hash:]:截图hash的长度,默认32个字符太长了
    • [path]:文件相对于webpack配置文件的路径
  • 在file-loader中设置文件名称:

{
    test: /.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: "file-loader",
        options: {
            name: "img/[name].[hash:8][ext]",    //存放路径为相对Output根目录下img文件夹
            //name: "[name].[hash:8][ext]"
            //outputPath: "img"    //与上面一样路径
        }
    }
}

url-loader

  • 使用方法与file-loader类似,file-loader会默认将体积较小的图片转为Base64编码插入js内,来减少文档加载阶段的请求次数。、
  • 可以配置limit属性,用于设置转换的限制
{
    test: /.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: "url-loader",
        options: {
            limit: 100 * 1024,  //则100kb以下的图片会被转为base64形式
            name: "img/[name].[hash:8][ext]",    //存放路径为相对Output根目录下img文件夹
        }
    }
}

asset module type

  • 在webpack5之前,加载这些资源我们需要使用一些loader,比如raw-loader 、url-loader、file-loader;

  • 在webpack5之后,我们可以直接使用资源模块类型(asset module type),来替代上面的这些loader;

  • 资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

    • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现;
    • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现;
    • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现;
    • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源 体积限制实现;
  • asset module type的基本使用:

output: {
    filename: "main.js",
    path: path.resolve(__dirname,"./dist"),
    assetModuleFilename: "img/[name].[hash:6][ext]"   //自定义文件输出路径和文件名方式二
}
​
//也可采用以上方式自定义asset输出路径与文件名
{
    test: /.(png|jpe?g|gif|svg)$/i,
    type: "asset",
    generator: {
        filename: "img/[name].[hash:6][ext]"   //自定义文件输出路径和文件名方式一
    },
    parser: {
        dataUrlCondition: {
            maxSize: 100 * 1024   //url的limit的效果
        }
    }
}

加载字体文件

{
    test: /.(woff2?|eot|ttf)$/i,
    type: "asset/resource",
    generator: {
        filename: "font/[name].[hash:6][ext]"   //自定义文件输出路径和文件名方式一
    }
}

数据的加载

  • 此外,可以加载的有用资源还有数据,如 JSON 文件,CSV、TSV 和 XML。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from './data.json' 默认将正常运行。要导入 CSV、TSV 和 XML,你可以使用 csv-loaderxml-loader。让我们处理加载这三类文件:
 const path = require('path');
​
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
       {
         test: /.(woff|woff2|eot|ttf|otf)$/i,
         type: 'asset/resource',
       },
      {
        test: /.(csv|tsv)$/i,
        use: ['csv-loader'],
      },
      {
        test: /.xml$/i,
        use: ['xml-loader'],
      },
     ],
   },
 };
  • 通过使用 自定义 parser 替代特定的 webpack loader,可以将任何 tomlyamljson5 文件作为 JSON 模块导入。
const path = require('path');
const toml = require('toml');
const yaml = require('yamljs');
const json5 = require('json5');
​
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
       {
         test: /.(woff|woff2|eot|ttf|otf)$/i,
         type: 'asset/resource',
       },
       {
         test: /.(csv|tsv)$/i,
         use: ['csv-loader'],
       },
       {
         test: /.xml$/i,
         use: ['xml-loader'],
       },
      {
        test: /.toml$/i,
        type: 'json',
        parser: {
          parse: toml.parse,
        },
      },
      {
        test: /.yaml$/i,
        type: 'json',
        parser: {
          parse: yaml.parse,
        },
      },
      {
        test: /.json5$/i,
        type: 'json',
        parser: {
          parse: json5.parse,
        },
      },
     ],
   },
 };

Plugin的使用

Plugin与Loader的区别

  • 官方文档中:Loader是用来转换某些类型的模块;plugin是用来进行更广泛的认为,例如,打包优化,资源的管理,以及环境变量的注入等。

CleanWebpackPlugin

  • webpack打包时并不会帮我们自动删除output所在文件夹,使用该插件可以在打包前帮我们自动删除上一次打包的残留
  • Plugin都是一个构造函数
  • module.plugin中进配置
const CleanWebpackPlugin = require('clean-webpack-plugin')
​
module.export = {
    plugins: [
        new CleanWebpackplugin()
    ]
}

HtmlWebpackPlugin

  • 在进行项目部署的时,必然也是需要有对应的入口文件index.html
const HtmlWebpackPlugin = require('html-webpack-plugin')\
​
module.export = {
    plugins: [
        new HtmlWebpackPlugin({
            title: 'THis title will be found in your html'  //该注入参数,可以在HTML中被读取
            template: "./public/index.html"  //定义该index.html文件的模板位置(此处使用的ejs模板引擎)
        })
    ]
}
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <!-- BASE_URL为一个常量,由DefinedPlugin插件进行注入 -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <!--  tmlWebpackPlugin.options.title变量为Plugin中所定义的title值  -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
  <script src="./test.js"></script>
</html>

DefinePlugin

  • 可以使用该插件为我们的模板注入常量
const { DefinePlugin } = require('webpack')
​
module.export = {
    plugins: [
        new DefinePlugin({
            BASE_URL : '"./"'  //BASE_URL输出为"./"
        })
    ]
}

CopyWebpackPlugin

  • 可以将一些指定的文件复制到dist文件夹
const CopyWebpackPlugin = require("copy-webpack-plugin");
​
module.export = {
    plugins: [
        new CopyWebpackPlugin({
            patterns: [  //可以设置多个目标
                from: "public",  //复制根目录下public下的文件
                globOption: {
                    ignore: [  //设置那些文件被忽略
                        "**/index.html"  //用**表示public文件夹(from所指定的文件夹),glob文件匹配模式
                    ]
                }
            ]
        })
    ]
}

模块化与SourceMap

  • mode配置可以webpack使用响应模式的内置优化

    • 可选值有:'none' | 'development' | 'production';
  • devtool:此选项控制是否生成,以及如何生成 source map

    • 你可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 来替代使用 devtool 选项,因为它有更多的选项。切勿同时使用 devtool 选项和 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 插件。devtool 选项在内部添加过这些插件,所以你最终将应用两次插件。
  • 常见的devtool组合规则

    • inline- (不会生成单独的source文件)|hidden- (生成但隐藏sourcemap)|eval (使用eval):三个值时三选一;
    • nosources (生成的sourcemap只有错误信息提示,没有生成源代码文件):可选值;
    • cheap可选值,并且可以跟随module的值;(低开销无列映射)
    • [ inline-|hidden-|eval- ][nosources- ][cheap-[module-]]source-map

Babel及其使用

  • Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的 JavaScript;、
  • 包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等;
  • babel可以使用@babel/cli独立使用
npx babel src --out-dir dist
#代表从src文件寻找源文件,输出到dist文件夹
  • babel实现一些转化功能需要用到babel相关的插件:

    • 转化箭头函数用到的插件:@babel/plugin-transform-arrow-functions
    • 转化let、const的插件: @babel/plugin-transform-block-scoping
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
  • 我们可以使用预设来实现转化:@babel/preset-env 该预设包含以上插件的规则以及其他的一些转化规则

    • 常见的预设: env 、 React 、 TypeScript

Babel的底层原理

  • babel可以看成一个编译器,即将我们的源代码转换成另一段源代码

Babel的工作流程

  1. 解析阶段(Parsing)
  2. 转换阶段(Transformation)
  3. 代码生成阶段(Code Generation)

每个阶段会有自己的工作

  1. 词法分析(Lexical Analusis): 生成Token数组
  2. 语法分析(Syntactic Analysis): 生成AST(抽象语法树)
  3. 遍历(Traversal)
  4. 访问(Visitor)
  5. 应用插件(Plugin):生成新的AST
  6. 生成源代码

Babel-loader

 rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              ["@babel/preset-env", {
                 targets: ["chrome 88"]   //设置browserslist的条件,根据需要兼容的目标浏览器生成代码
                 enmodules: true
              }]
            ]
             plugins: [
               "@babel/plugin-transform-arrow-functions",
               "@babel/plugin-transform-block-scoping"
             ]
          }
        }
      }
    ]

Babel的配置文件

  • Babel的配置在babel.config.js(.json | .cjs | .mjs)或者.babelrc.json(.babelrc | .js | .cjs | .mjs)中进行配置

    • .babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的
    • babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐
module.exports = {
    presets: [
        ["@babel/preset-env",{
            useBuiltIns: "usage", // false usage entry
            corejs: 3,
        }],
        ["@babel/preset-react"],
        ["@babel/preset-typescript"]
    ],
    plugins: [
        // useBuiltIns是全局的,该插件是非全局的
        ["@babel/plugin-transform-runtime",{
            corejs: 3,
        }]
    ]
}
  • useBuiltIns:设置以什么样的方式来使用polyfill;

    • false: 不用任何的polyfill相关的代码

    • usage: 代码中需要哪些polyfill, 就引用相关的api

    • 手动在入口文件中导入 core-js/regenerator-runtime, 根据目标浏览器引入所有对应的polyfill

      • 如果我们依赖的某一个库本身使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器 可能会报错
      • 入口文件中添加 `import 'core-js/stable'; import 'regenerator-runtime/runtime';
  • corejs:设置corejs的版本,目前使用较多的是3.x的版本

    • 另外corejs可以设置是否对提议阶段的特性进行支持
    • 设置 proposals属性为true即可

Plugin-transform-runtime

  • 在前面我们使用的polyfill,默认情况是添加的所有特性都是全局的

    • 如果我们正在编写一个工具库,这个工具库需要使用polyfill
    • 别人在使用我们工具时,工具库通过polyfill添加的特性,可能会污染它们的代码
    • 所以,当编写工具时,babel更推荐我们使用一个插件: @babel/plugin-transform-runtime来完成polyfill 的功能

Babel对React的JSX的支持

  • 对React的JSX代码进行处理需要:

    • @babel/plugin-syntax-jsx
    • @babel/plugin-transform-react-jsx
    • @babel/plugin-transform-react-display-name
  • 也可以使用预设: @babel/preset-react

TypeScript的相关支持

  • TypeScript的配置文件为tsconfig.json: 可以使用tsc --init指令进行生成
  • 在Webpack中使用TypeScript需要使用ts-loader进行处理
      {
        test: /.ts$/,
        exclude: /node_modules/,
        use: "ts-loader"
      }
  • 除了可以使用TypeScript Compiler来编译TypeScript之外,我们也可以使用Babel

    • 我们可以使用插件: @babel/tranform-typescript;
    • 更推荐直接使用preset:@babel/preset-typescript;
      {
        test: /.ts$/,
        exclude: /node_modules/,
        // 本质上是依赖于typescript(typescript compiler)
        use: "babel-loader"
      }
  • ts-loader与babel-loader选择

    • 使用ts-loader(TypeScript Compiler)

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

      • 来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能;
      • 但是babel-loader在编译的过程中,不会对类型错误进行检测;

Glob文件匹配模式

模式说明
*匹配除了斜杠(/)之外的所有字符。 Windows上是斜杠(/)和反斜杠()
**匹配零个或多个目录及子目录。不包含 . 以及 .. 开头的。
?匹配任意单个字符。
[seq]匹配 seq 中的其中一个字符。
[!seq]匹配不在 seq 中的任意一个字符。
``转义符。
!排除符。
?(pattern_list)匹配零个或一个在 pattern_list 中的字符串。
*(pattern_list)匹配零个或多个在 pattern_list 中的字符串。
+(pattern_list)匹配一个或多个在 pattern_list 中的字符串。
@(pattern_list)匹配至少一个在 pattern_list 中的字符串。
!(pattern_list)匹配不在 pattern_list 中的字符串.
[...]POSIX style character classes inside sequences.

ESlint

  • ESLint是一个静态代码分析工具(Static program analysis,在没有任何程序执行的情况下,对代码进行分析);

  • ESLint可以帮助我们在项目中建立统一的团队代码规范,保持正确、统一的代码风格,提高代码的可读性、可维护性

  • 并且ESLint的规则是可配置的,我们可以自定义属于自己的规则

  • 可以通过eslint --init生成配置文件

  • .eslintrc.js是用来配置Eslint规则的

  • .eslintignore配置那些文件需要被eslint忽略,规则语法与.gitignore类似

  • ESlint文件解析:

    • env:运行的环境,比如是浏览器,并且我们会使用es2021(对应的ecmaVersion是12)的语法;

    • extends:可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个);

    • parserOptions:这里可以指定ESMAScript的版本、sourceType的类型

      • parser:默认情况下是espree(也是一个JS Parser,用于ESLint),但是因为我们需要编译TypeScript,所 以需要指定对应的解释器;
    • plugins:指定我们用到的插件;

    • rules:自定义的一些规则;

module.exports = {
  'env': {
    'browser': true,
    'es2021': true,
  },
//   'extends': [
//     'google',
//   ],
//   'parser': '@typescript-eslint/parser',
  'parserOptions': {
    'ecmaVersion': 'latest',
    'sourceType': 'module',
  },
//   'plugins': [
//     '@typescript-eslint',
//   ],
//   'rules': {
//   },
};
  • Eslint-loader:事实上,我们在编译代码的时候,也希望进行代码的eslint检测,这个时候我们就可以使用eslint-loader来完成了
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: ['babel-loader', 'eslint-loader'],
      },

Prettier

  • ESLint会帮助我们提示错误(或者警告),但是不会帮助我们自动修复,在开发中我们希望文件在保存时,可以自动修复这些问题,我们可以选择使用另外一个工具:prettier
  • 配置文件为.prettierrc,可以通过prettier官网进行自动生成

Vue3使用webpack搭建环境

Babel配置

module.exports = {
    presets: [
        "@babel/preset-env"
    ]
}

PostCSS配置

module.exports = {
    plugins: [
        'postcss-preset-env'
    ]
}

WebPack配置

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require("webpack");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader/dist/index');
​
module.exports = {
    mode: "development",
    entry: "./src/main.js",
    devtool: false,
    output: {
        filename: "main.js",
        path: path.resolve(__dirname,"./dist")
    },
    module: {
        rules: [
            {
                test: /.less$/,
                use: [
                    "style-loader",
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 2
                        }
                    },
                    "postcss-loader",
                    "less-loader"
                ]
            },
            {
                test: /.css$/,
                use: [
                    "style-loader",
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1
                        }
                    },
                    "postcss-loader"
                ],
            },
            {
                test: /.(jpe?g|png|gif|svg)$/,
                type: "asset",
                generator: {
                    filename: "img/[name]_[hash:6][ext]",
                },
                parser: {
                    dataUrlCondition: {
                        maxSize: 10 * 1024,
                    },
                },
            },
            {
                test: /.(eot|ttf|woff2?)$/,
                type: "asset/resource",
                generator: {
                    filename: "font/[name]_[hash:6][ext]",
                },
            },
            {
                test: /.js$/,
                loader: "babel-loader"
            },
            {
                test: /.jsx$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            },
            {
                test: /.ts$/,
                exclude: /node_modules/,
                use: "babel-loader"
            },
            {
                test: /.vue$/,
                use: "vue-loader"
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: "ChromeManifest3",
            template: "./public/index.html"
        }),
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname,"./public/manifest.json"),
                    to: "./manifest/[name].[hash:8][ext]",
                    globOptions: {
                        ignore: [
                            "**/index.html"
                        ]
                    }
                }
            ]
        }),
        new DefinePlugin({
            BASE_URL: "'./'",
            __VUE_OPTIONS_API__: true,
            __VUE_PROD_DEVTOOLS__: false
        }),
        new VueLoaderPlugin()
    ]
}

DevServer与HMR

Webpack自动编译方式

  • webpack watch mode: 不会自动刷新浏览器。

    • 在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译

    • 配置方式:

      • module.exports = {
          //...
          watch: true,
          watchOption: {
              aggregateTimeout: 600,  
              //当第一个文件更改,会在重新构建前增加延迟。这个选项允许 webpack 将这段时间内进行的任何其他更改都聚合到一次重新构建里。以毫秒为单位
              ignored: /node_modules/,
              //对于某些系统,监听大量文件会导致大量的 CPU 或内存占用。可以使用正则排除像 node_modules 如此庞大的文件夹
              poll: 1000, 
              // 通过传递 true 开启 polling,或者指定毫秒为单位进行轮询。
              followSymlinks: true,
              //根据软链接查找文件。这通常是不需要的,因为 webpack 已经使用 resolve.symlinks 解析了软链接。
              stdin: true, //当 stdin 流结束时停止监听。
          }
        };
        
      • 在启动webpack的命令中,添加 --watch的标识
  • webpack-dev-server

    • 会将打包后的文件保存在内存中(memory-fs库进行实现)
    • 可以通过Node API运行,查看相关的 webpack-dev-server API 文档
    • 也可以通过CLI的方式进行调用
npx webpack serve
  • webpack-dev-middleware

    • 可以使用webpack-dev-middleware,以达到更大灵活性的dev-server
    • //server.js
      const express = require('express');
      const webpack = require('webpack');
      const webpackDevMiddleware = require('webpack-dev-middleware');
      ​
      const app = express();
      //加载配置信息
      const config = require("./webpack.config");
      ​
      // 传入配置信息, webpack根据配置信息进行编译
      const compiler =  webpack(config);
      ​
      const middleware = webpackDevMiddleware(compiler);
      app.use(middleware);
      ​
      app.listen(3000, () => {
        console.log("服务已经开启在3000端口上~");
      });
      
    • node server.js
      #使用node进行开启
      

Hot Module Replacement (HMR)

  • dev-server开启HMR:

    •   devServer: {
          hot: true
        },
      
    • //需要指定那些模块,才能触发热模块替换
      if (module.hot) {
        module.hot.accept("./math.js", () => {
          console.log("math模块发生了更新~");
        });
      }
      
React的HMR
  • 这里安装@pmmmwh/react-refresh-webpack-plugin
  • 修改webpack.config.js与babel.config.js的配置文件
//webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
​
module.exports = {
​
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html"
    }),
    new ReactRefreshWebpackPlugin()
  ]
}
​
//babel.config.js
module.exports = {
  presets: [
    ["@babel/preset-env"],
    ["@babel/preset-react"],
  ],
  plugins: [
    ["react-refresh/babel"]
  ]
}
Vue的HMR
  • Vue的加载我们需要使用vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR的处理。
  • 安装vue-loader与vue-template-compiler
const VueLoaderPlugin = require('vue-loader/lib/plugin');
​
​
module.exports = {
  module: {
    rules: [
      {
        test: /.vue$/i,
        use: "vue-loader"
      },
      {
        test: /.css/i,
        use: [
          "style-loader",
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html"
    }),
    new VueLoaderPlugin()
  ]
}
HMR的原理
  • webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket);

  • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);

  • HMR Socket Server,是一个socket的长连接:

    • 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);
    • 当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);
    • 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
    • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新;
  • 这里有一张CoderWhy老师绘制的原理图:

HMR.png

  • runtime

    • runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
  • manifest

    • 一旦你的应用在浏览器中以 index.html 文件的形式被打开,一些 bundle 和应用需要的各种资源都需要用某种方式被加载与链接起来。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来……

    • 当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法,那些 importrequire 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。

    output与publicPath

  • output中的path的作用是告知webpack之后的输出目录:

    • 比如静态资源的js、css等输出到哪里,常见的会设置为dist、build文件夹等;
  • output中还有一个publicPath属性,该属性是指定index.html文件打包引用的一个基本路径:

    • 它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是 bundle.js;
    • 在开发中,我们也将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名+路径去请求对应的资源;
    • 如果我们希望在本地直接打开html文件来运行,会将其设置为 ./,路径时 ./bundle.js,可以根据相对路径去 查找资源;
  • devServer的publicPath
    • devServer中也有一个publicPath的属性,该属性是指定本地服务所在的文件夹:

      • 它的默认值是 /,也就是我们直接访问端口即可访问其中的资源 http:// localhost:8080;

      • 如果我们将其设置为了 /abc,那么我们需要通过 http:// localhost:8080/abc才能访问到对应的打包后的资源;

      • 并且这个时候,我们其中的bundle.js通过 http:// localhost:8080/bundle.js也是无法访问的:

        • 所以必须将output.publicPath也设置为 /abc;
        • 官方其实有提到,建议 devServer.publicPath 与 output.publicPath相同;
  • devServer的contentBase
    • devServer中contentBase对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包 后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容

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

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

        • 比如代码是这样的:< script src = "./public/abc.js">
        • 但是这样打包后浏览器是无法通过相对路径去找到这个文件夹的;
        • 所以代码是这样的:< script src = "./abc.js" >
        • 但是我们如何让它去查找到这个文件的存在呢? 设置contentBase即可;
    • 当然在devServer中还有一个可以监听contentBase发生变化后重新编译的一个属性:watchContentBase。

DevServer的一些其他配置

  • hotOnly是当代码编译失败时,是否刷新整个页面:

    • 默认情况下当代码编译失败修复后,我们会重新刷新整个页面;
    • 如果不希望重新刷新整个页面,可以设置hotOnly为true;
  • host设置主机地址:

    • 默认值是localhost;
    • 如果希望其他地方也可以访问,可以设置为 0.0.0.0;
  • port设置监听的端口,默认情况下是8080

  • open是否打开浏览器:

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

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

Proxy代理

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

    • 我们可以将请求发送到一个代理服务器,由代理服务器向跨域服务器发起请求,类似于Nginx反向代理。
  • 可以进行如下设置

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

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

      • Proxy可以根据请求路径的正则表达匹配需要转发的代理服务器,但会将用于匹配路径的部分也当做路径向服务器进行请求,此时可以通过该配置对请求路径进行改写
    • secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false

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

      • 假如我们真实的请求的地址是向http:// loacalhost:8080进行请求的
      • 我们通过代理向8888端口进行请求,此时请求头依旧是未转发前的请求头,此时host值为8080
      • 当changOrigin为true时就会修改这个请求host,显示为我们实际请求的地址host 8888

historyApiFallback

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

  • boolean值:默认是false

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

    • 可以配置from来匹配路径,决定要跳转到哪一个页面;
devServer: {        
    hot: true,
    hotOnly: true,
    // host: "0.0.0.0",
    // port: 7777,
    // open: true,
    compress: true,
    contentBase: path.resolve(__dirname, "./why"),
    watchContentBase: true,
    // publicPath: "/abc",
    proxy: {
      // "/why": "http://localhost:8888"
      "/why": {
        target: "http://localhost:8888",
        pathRewrite: {
          "^/why": ""
        },
        secure: false,
        changeOrigin: true
      }
    },
    // historyApiFallback: true
    historyApiFallback: {
      rewrites: [
        {from: /abc/, to: "/index.html"}
      ]
    }
  },

resolve解析

  • Webpack对三种路径的解析:

    • 绝对路径

      • 由于已经获得文件的绝对路径,因此不需要再做进一步解析。
    • 相对路径

      • 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
      • 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
    • 模块路径

      • 在 resolve.modules中指定的所有目录检索模块;

        • 默认值是 ['node_modules'],所以默认会从node_modules中查找文件;
      • 我们可以通过设置别名的方式来替换初识模块路径,具体后面讲解alias的配置;

  • 确定文件还是文件夹

    • 如果是一个文件:

      • 如果文件具有扩展名,则直接打包文件;
      • 否则,将使用 resolve.extensions选项作为文件扩展名解析;
    • 如果是一个文件夹

      • 会在文件夹中根据 resolve.mainFiles配置选项中指定的文件顺序查找;

        • resolve.mainFiles的默认值是 ['index'];
        • 再根据 resolve.extensions来解析扩展名;
  • extensions是解析到文件时自动添加扩展名

    • 默认值是 ['.wasm', '.mjs', '.js', '.json']
    • 所以如果我们代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,我们必须自己写上扩展名;
  • 另一个非常好用的功能是配置别名alias

    • 特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 ../../../这种路径片段;
    • 我们可以给某些常见的路径起一个别名
  resolve: {
    extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.ts', '.vue'],
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "pages": path.resolve(__dirname, "./src/pages")
    }
  },