webpack5基础知识

337 阅读25分钟

注意,本文使用 webpack5.68.0,不同版本可能会有细微区别。

webpack是一个模块化打包 JavaScript 的工具,在 Wepack 里一切文件皆模块,通过Loader翻译转化文件,通过Plugin注入钩子,最后输出多个模块组合的文件,而Webpack专注于构建模块化项目。官网的首页图说明了Webpack是什么:

1-2webpack.png

对于webpack的基础配置及概念可以从官网文档中学习,本文列举webpack的在生产环境和开发环境中通用的PluginLoader及配置。

本文后续会出现经常提及chunkbundle在这里我们先解析一下这个概念

chunk

chunk从字面意思是代码块,在Webpackchunk代表许多关联module的集合。比如在webpack.config.js声明了入口模块entry,入口模块又关联了其他模块,这一系列关联的模块就是一个chunk

webpack中产生chunk有三种途径。

entry 产生的 chunk

webpack.config.js中可以声明entry来指代入口模块,entry合法类型有string | Array | object

entry字段值如果是string | Array会产生一个名为mainchunk;如果是Array则将数组里的源代码都打包到一个chunk中。如下:

module.exports = {
  entry: "./src/main.js",
  entry: ["./src/main.js", "./src/other.js"],
  // ...
};

如果是一个对象,则会声明与key数量相同chunk,并且会使用key来作为chunk的名称。这就是为什么entry是对象,output.filename如果写死名称会直接报错的原因,因为一个名称不够让多个chunk输出。

module.export = {
  entry: {
    main: "./src/main.js",
    other: "./src/other.js",
  },
  output: {
    path: path.join(__dirname, "./dist"),
    // 会产生 main.js  other.js两个文件
    filename: "[name].js",
  },
};

按需加载(异步)产生的 chunk

按需加载(异步)加载的模块也会产生chunk,这个chunk名称可以在代码中使用webpackChunkName自行定义。如果需要打包的时候使用chunk名称,则需要在output.chunkFilename中引用。

// webpack.config.js
module.export = {
  output: { chunkFilename: "[name].js" },
};

// module
import(/* webpackChunkName: "async-model" */ "./async-model");

代码分隔产生的 chunk

webpack5中代码分割使用SplitChunksPlugin插件实现,这个插件内置在webpack中,在使用时直接用配置的方式即可。代码分隔时也会产生chunk,我们用代码来说明一下:

// webpack.config.js
module.export = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js",
  },
  optimization: {
    // 分隔webpack运行时代码
    runtimeChunk: "single",
    splitChunks: {
      chunks: "all",
      // 分隔组
      cacheGroups: {
        // 抽取第三方模块
        vendors: {
          test: /node_modules/,
          prioity: -10,
          reuseExistingChunk: true,
        },
        // 抽取
        commons: {
          minSize: 0, // 抽取的chunk最小大小
          minChunks: 2, // 最小引用数
          prioity: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

// a.js
import "c.js";
import $ from "jquery";

console.log($);
// b.js
import "c.js";

// commons.js
console.log("hello");

上方代码使用了代码分隔,总共会产生 6 个chunk

  • 两个入口分别分配到名为abchunk
  • runtimeChunk声明抽离webpack运行时代码抽离到一个唯一的名称的chunk,我们有两个入口,有两份运行时chunk
  • jquery符合cacheGroups.vendors规则,抽离到名为vendorschunk
  • commons.js符合cacheGroups.commons规则,抽离到名为commonschunk

如下图所示

build.png

bundle

bundle就是我们最终输出的一个或多个文件,大多数情况下一个chunk至少会产生一个bundle,但是不完全是一对一的关系。比如我们在模块中引用图片又经过url-loader打包到外部;或者是引用了样式,通过extract-text-webpack-plugin抽离出来,这样一个chunk就会出现产生多个bundle的情况。

简单来说bundle就是chunk在构建完成的呈现。

entry 和 context

entrywebpack的启动模块入口,webpack将根据指定的这个起点来查找模块,生成chunkentry的合法值有:

  1. string,入口起点的模块路径,webapck将生成名为mainchunk
  2. Array,入口起点的一组模块的路径,webpack将组的中每个模块拼合在一起,并生成名为mainchunk
  3. { [key in string]: string | Array },多个入口起点,webpack根据value (跟 1,2 合法值一致) 为入口起点,key为名称的chunk
  4. function,获取入口起点的方法,返回值为 1,2,3 或Promise<1,2,3>,我们可以指定动态入口。
module.exports = {
  entry: ["./app/entry1", "./app/entry2"],
  entry: {
    a: "./app/entry-a",
    b: ["./app/entry-b1", "./app/entry-b2"],
  },
  entry: () => "./app/entry-a",
  entry: () => new Promise((resolve) => resolve("./app/entry-a")),
  // ...
};

我们看到上方代码的入口模块值都是相对路径,默认情况下webpack想以当前目录为基础路径来查找。我们可以通过context来更改基础路径。

webapck查找loader和启动入口模块时,会以配置中的context为基础目录。下面我们来改上方代码

module.exports = {
  context: path.join(__dirname, "./app"),
  entry: ["./entry1", "./entry2"],
  entry: {
    a: "./entry-a",
    b: ["./entry-b1", "./entry-b2"],
  },
  entry: () => "./entry-a",
  entry: () => new Promise((resolve) => resolve("./entry-a")),
  // ...
};

output

默认情况下webpack会在dist输出chunk生成的bundle,文件名就是chunk名称。

filename 和 chunkFilename

我们可以通过output.filename来修改非按需加载chunk生产bundle的名称。output.filename的合法值有string | (pathData: PathData) => string,默认值为[name].js

如果我们的项目中只有一个非按需加载的chunk(几乎不存在),可以使用静态名称来定义生产的bundle名称,如果有多个chunk就不行了,因为多个chunk无法放到一个bundle

module.exports = {
  output: {
    filename: "bundle.js",
  },
};

我们可以使用webpack内提供的模板字符串来定义bundle文件名,下表列举了常用的模板字符串:

模板描述稳定性
[name]chunk的名称只要chunk名称不修改就不会变化
[hash]根据所有chunk生成的hash工程中某个chunk被修改就会引起变化
[chunkhash]根据chunk生成的hash某个chunk被修改,只会引起被修改chunkhash
[contenthash]根据bundle内容内容产生的hashchunk中某个bundle被修改,只会引起被修改bundlehash

下面代码例举了如何使用模板。

module.exports = {
  outpit: {
    // 直接使用
    filename: "[name].js",
    filename: "[hash].js",
    filename: "[chunkhash].js",
    filename: "[contenthash].js",
    // 组合使用
    filename: "[name]-[contenthash].js",
    // 限定hash位数
    filename: "[hash:5].js",
    filename: "[contenthash:5].bundle.js",
  },
};

在使用[hash]时需要注意,因为它是所有chunk都共享的,所以直接用[hash]可能会引起错误,跟上方使用静态变量一样,多个chunk无法放置到一个bundle

前面我们讲解了filename的配置方式,它们对非按需加载(异步)的chunk生效,如果需要对按需加载的chunk配置,那就需要用到outpit.chunkFilename了,它和filename的使用方式是一样的,这里不在赘述。

module.exports = {
  outpit: {
    chunkFilename: "[name].js",
    chunkFilename: "[hash].js",
    chunkFilename: "[chunkhash].js",
    chunkFilename: "[hash:5].js",
    chunkFilename: "[contenthash:5].bundle.js",
  },
};

path

默认情况下webpack会将打包的文件输出到dist文件夹中,我们可以通过配置output.path来更改输出目录。

path可以使用[hash]的模板字符串,因为每次文件修改后hash都不一样,很容易就能生成所有版本构建后代码文件。

const path = require("path");

module.exports = {
  output: {
    path: path.join(__dirname, "./dist-[hash]"),
  },
};

html 自动引入 bundle 文件

如果要实现缓存目的,可以使hash来输出bundle。由于每次打包后的hash都可能会更改,在html手动引入bundle很麻烦,我们可以借助html-webpack-plugin插件来实现自动引入bundle

html-webpack-plugin的使用方式非常简单,只需创建一个实例,放置到plugins中就会自动生成在output.path中生成一个默认的html,这个html将会自动引入所有chunk生成的bundle。接下来我们创建最一个简单的使用。

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

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

在创建html-webpack-plugin实例时可以传入许多参数,可以通过官方文档了解,接下来我们看看最常用的几种定制。

定制 html

template参数可以指定生成html的模板,我们可以在这个html中编写自己需要的东西。 filename参数可以指定html-webpack-pluginhtml输出到哪里,这个参数的配置方式跟outpit.filename一样。 minify参数可以决定是否压缩html,在production模式下会自动为true

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "assets/template.html"),
      filename: "index.html",
      minify: true,
    }),
  ],
};

多页面

上面我们说的html-webpack-plugin会自动产生一个html并引入所有chunk,这很适合SPA (单页面) 开发。而MPA需要多个html,而且每个html需要的chunk也不一样,接下来我们看看这个插件是如何适配MPA开发的。

一个html-webpack-plugin实例会生产输出一个html,多个实例就会生产输出多个html,只需要在plugins中添加多个实例即可。html-webpack-plugin中有个chunks参数来指定html要关联的所有chunk

module.exports = {
  content: path.join(__dirname, './src'),
  entry: {
    login: './login.js',
    home: './home.js',
    base: './base.js'
  }
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'assets/template.html'),
      filename: 'login.html',
      chunks: ['login', 'base']  //需要关联的所有chunk
    }),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'assets/template.html'),
      filename: 'home.html',
      chunks: ['home', 'base']  //需要关联的所有chunk
    })
  ]
}

cdn 加速和缓存处理

如果我们把bundle都上传到了cdn上,就需要对打包后的资源引入路径进行修改,我们可以通过publicPath参数给我们指定资源的前缀路径。 使用cdn一般会产生缓存问题,如果你的output.filename没有使用hash那更新后的文件不会立即生效。我们可以使用hash参数给引入路径加上此次构建缓存。 (更好的方式是使用 output.filename),因为如果使用构建缓存意味着每次更新所有缓存都会被刷新。

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "assets/template.html"),
      publicPath: "//www.cdn.com",
      hash: true,
      filename: "login.html",
      chunks: ["login", "base"], //需要关联的所有chunk
    }),
  ],
};

扩展

html-webpack-plugintemplate是使用ejs 模板语言html-webpack-plugin注入了许多变量给模板使用,我们可以直接在模板内使用这些变量来自定义扩展我们的html

变量描述
htmlWebpackPlugin.options创建HtmlWebpackPlugin实例的参数
htmlWebpackPlugin.fileshtmlWebpackPlugin准备注入的bundle,如果injecttrue则自动注入
webpackConfigwebpack的配置
compilationwebpack编译对象

htmlWebpackPlugin.files的类型为

type File {
  publicPath: string;
  js: string[];
  css: string[];
  manifest?: string;
  favicon?: string;
}

接下来我们做个 demo,关闭inject,手动将webpack打包后的cssjs都内联到html中:

// webpack.config.js
module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "assets/template.ejs"),
      inject: false,
      title: 'login',
      filename: "login.html",
      chunks: ["login", "base"], //需要关联的所有chunk
    }),
  ],
};
<!-- template.ejs -->
<!DOCTYPE html>
<html lang="en">
<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">
  <title> <%= htmlWebpackPlugin.options.titlea %> </title>
  <% for (cssFile of htmlWebpackPlugin.files.css ) { %>
    <style>
      <%= compilation.assets[cssFile.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>    
    </style>
  <% } %>
</head>
<body>

  <% for (jsFile of htmlWebpackPlugin.files.js ) { %>
    <script>
      <%= compilation.assets[jsFile.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>    
    </script>
  <% } %>
</body>
</html>

抽取公用代码

SplitChunksPlugin是改进CommonsChunkPlugin而重新设计和实现的代码分片插件。SplitChunksPlugin可以配置式的定义如何抽取chunk,这是个十分有用的功能。

  • 例如有一个模块被两个入口模块引用,默认情况下这个公用模块在两个chunk都会有,默认情况下生成的两个bundle都会包含这个公用模块的内容,我们可以通过SplitChunksPlugin将公用模块单独抽成一个chunk,生成三个bundle,两个入口bundle引用这个公用的bundle
  • 第三方库 (通常在node_modules) 通常情况下是不怎么会有大的变动的,为了充分利用缓存,我们可以把项目业务的代码和第三方库代码分成两个chunk,生成两份bundle,这样我们修改业务代码,第三方库的代码并不会变动,也就能利用上缓存了。 (需要将output.filename修改成[chunkhash|contenthash].js)

在默认情况下,webpack只会分片按需加载 (只针对import(...)异步加载)chunks。影响范围是在optimization.splitChunks.chunks中声明的,他们分别表示:

  • initial只影响入口chunk关联
  • async (默认) 只影响按需加载的chunk
  • all 入口和按需加载都影响

默认情况下的提取规则及主要配置代码:

  • chunk被多次引用或是来自node_modules
  • js chunk体积大于30K
  • css chunk体积大于50K
  • 按需加载并行数小于等于30 (同时间import(...)的数量)
  • 首次加载并行数小于等于30(在html初次加载的 script引用数量)
// splitChunks 默认配置
const options = {
  // 工作于哪里 async initial(只对入口的chunk生效) all: async + all
  chunks: 'async',
  // 抽取时chunk最小大小
  minSize: 20000,

  // 另一种方式,指定模块类型
  // minSize: {
  //   javascript: 300000,
  //   style: 500000
  // },

  // 最少被多少个chunk使用
  minChunks: 1,
  // 按需加载并行数最小值
  maxAsyncRequests: 30,
  // 首次加载最大并行数
  maxInitialRequests: 30

  // 拆分后体积最小多少
  minRemainingSize: 0,

  // 体积大于多少强制拆分
  enforceSizeThreshold: 50000,

  // 名称分隔符如模块A被chunk B和chunk C同时引用名称可能是default~B~C
  automaticNameDelimiter: '~',

  // 自定义抽取的chunk的名称,切忌不要设置固定值,因为chunk名称相同所有分离代码将会合并
  // 设置为false 将使用合并chunk的名称并用automaticNameDelimiter作为分隔生成为chunk名称
  name: false,

  // 上方是公用配置,可以自定义组配置,如果没有覆盖则会集成上方配置
  // key 为抽取后的chunk name
  cacheGroups: {
    // 没覆盖的会继承上方配置,比如minChunks: 1,
    vendors: {
      //  group 特有属性 提取第三方模块
      test: /[\\/]node_modules[\\/]/,
      // 优先级,优先级会影响webpack选中哪个group
      // 比如一个chunk即符合default规则也符合vendors规则,则查看优先级,哪个高用哪个
      priority: -10,

      // 告诉webpack强制拆分,忽略除test外条件
      enforce: false
    },
    // 多次引用提取
    default: {
      // 最小被两个chunk引用
      minChunks: 2,
      // 优先级
      prority: -20,
    }
  }
}

更多相关配置查看官网

使用非模块化库

一些历史项目上有些第三方库不支持模块化,或者因为其他局限性不能模块化,只能全局导入。比如jquery如果使用模块化方式导入,有第三方的插件将找不到它,因为它们是直接用全局版本的。如果直接在全局上引用jquery是可以解决问题,但我们项目上良好的ts提示也没有了。我们可以使用webpackexternals属性来解决这类问题。

externals能放置将import的包打包到bundle中,而是在运行时根据配置找到我们声明的扩展依赖并使用它。

module.exports = {
  // ...
  externals: {
    // key 为我们import的包名, value 为全局中变量名
    jquery: 'jQuery',
  }
}

// 使用
import { ajax } from 'jquery'
import $ from 'jquery'

// 将会转化为 jQuery.ajax({ ... })
ajax({ ... })
// 将会转化为 jQuery('#app')
$('#app')

更多相关内容参考官网

快捷路径访问模块

如果我们的项目非常复杂而且项目路径比较深就可以考虑建立访问快捷路径,比如我们项目结构如下

project
|---packages
|-----components
|-------assets
|         1.png
|-----sdk
|-------config
|         default.json
|-----pro1
|       main.js
|-----pro2
|   scripts
|   webpack.config.js

main.js中要访问components中的1.png就需要import img from '/packages/components/assets/1.png',路径非常的长。为了解决这个问题我们可以使用webpackresplve.alias

module.exports = {
  resolve: {
    alias: {
      '@ui': path.resolve(__dirname, './packages/components'),
      '@sdk': path.resolve(__dirname, './packages/sdk'),
      // 末尾用$标识精准匹配  只匹配 import '~sdkConfig'
      // import '~sdkConfig/xxx'不会流向这里
      '~sdkConfig$': path.resolve(__dirname, './packages/sdk/config/default.json'),
    }
  }
}

然后我们就可以直接使用import img from '@ui/assets/1.png'了。

使用ES语法新特性

ES6发布以后,TC39规定每年都会发布新的版本。通常把ES5及其之前的版本统称做ES5。为了明确区分各个版本的内容,可以按照版本发布的时间进行描述,比如ES2015,ES2016。虽然目前部分浏览器都开始支持了,但由于各个浏览器标准支持不全,以及新特性支持没那么快,这导致在开发中不敢全面地使用新特性。

通常我们需要给新的API注入polyfill或者把新的ES6语法用ES5来使用新特性,babel就可以方便的完成。

Babel是一个JavaScript编译器,能将新特性语法代码转为ES5代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。在于webpack结合时需要使用babel-loader作为桥梁。

在编写配置之前我们需要安装babel核心库@babel/corebabel各个新特性转换的库@babel/preset-env以及babel-loader

npm install  --save-dev @babel/core @babel/preset-env babel-loader
module.exports = {
  // ...
  model: {
    rules: [
      {
        test: /.js/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env']
            ]
          }
        }
      }
    ]
  }
}

目标环境

preset-env会根据你编写代码按需加载支持代码,在没有配置的情况下会转换ES2015及以上的所有特性。我们可以通过targets参数来传递需要打包的目标环境,babel会根据caniuse的数据来决定是否需要转换。

module.exports = {
  // ...
  model: {
    rules: [
      {
        test: /.js/,
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env'],
            // 指定目标环境
            targets: {
                {
                  // chrome: '90',
                  // ie: 11
                  // node: 'current',
                  browsers: ['last 2 versions', 'ie > 10']
                }
            }
          ]
        }
      }
    ]
  }
}

抽离转换代码

默认情况下babel的所有转换代码都会直接内联到js bundle中,假如我们有多个chunk那每个chunk生成的js bundle都有转换代码,我们需要将他们统一抽离,减少js bundle的大小。我们可以使用babel提供的@babel/plugin-transform-runtime插件解决这一需求

npm install  --save-dev @babel/plugin-transform-runtime
module.exports = {
  // ...
  model: {
    rules: [
      {
        test: /.js/,
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env'],
            targets: {
                { browsers: ['last 2 versions', 'ie > 10'] }
            }
          ],
          plugins: ['@babel/plugin-transform-runtime']
        }
      }
    ]
  }
}

开启编译缓存

babel转换是件耗时的事,我们可以通过babel-loadercacheDirectory来开启缓存,不变动的文件没必要再次编译,加快编译。默认情况下缓存文件会存放在node_modules/.cache中。

module.exports = {
  // ...
  model: {
    rules: [
      {
        test: /.js/,
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env'],
            targets: {
                { browsers: ['last 2 versions', 'ie > 10'] }
            }
          ],
          plugins: ['@babel/plugin-transform-runtime'],
          cacheDirectory: true
        }
      }
    ]
  }
}

资源模块管理

资源模块是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外。在webpack5中使用资源模块来代替raw-loaderurl-loaderfile-loader管理资源模块。

资源模块类型总共有4中类型来替换这些loader

  • asset/resource 发送一个单独的文件并导出URL。之前通过使用file-loader实现。
  • asset/inline 导出一个资源的data URI。之前通过使用url-loader实现。
  • asset/source 导出资源的源代码。之前通过使用raw-loader实现。
  • asset 在导出一个data URI和发送一个单独的文件之间自动选择,默认下小于8k的将视为inline。之前通过使用url-loader,并且配置资源体积限制实现。
module.exports = {
  // ...
  module: {
    rules: [
      {
        // 模型文件,统一导出为 URL
        test: /.(obj|mtl)/i,
        type: 'asset/resource',
        // 覆盖outpit.assetModuleFilename 自定义导出bundle路径
        generator: {
          filename: 'static/model/[hash][ext]'
        }
      },
      {
        // 图片文件,如果大于 10KB将导出为URL否则内联源代码
        test: /\.(png|jpg|gif)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024
          }
        }
      },
      {
        // svg比较特殊,没有指定要raw时用URL,指定时用源码,所以需要二选一rules
        oneOf: [
          {
            test: /.svg$/i,
            // 匹配模块引用路径query是否有raw,如果有则返回源码 
            // 如 import svg from 'a.svg?raw'
            resourceQuery: /raw/,
            type: 'asset/source'
          },
          {
            test: /.svg$/i,
            type: 'asset/resource'
          }
        ]
      }
    ],
    output: {
      // 输出asset资源
      assetModuleFilename: 'images/[hash][ext]'
    }
  }
}

outpit.assetModuleFilenamegenerator.filenameoutput.filename相同不过适用于Asset Modules,用法参照上方output.filename

加载样式文件

如果不是写lib库,那css对于前端来说是必不可少的,对于webpack来说一切皆模块,我们只需要在loader中定义好css支持,即可引入css文件。

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.css/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}

上面使用了css-loader来解析css模块生成代码文件,style-loader将生成的代码自动嵌入到html。但是这样有个问题,css文件无法使用缓存,我们需要将生成的css代码抽出到一个bundle中。mini-css-extract-plugin插件就能帮助我们完成这个任务。

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.css/,
        use: [
          { loader: MiniCssExtractPlugin.loader },
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'style/[name]-[contenthash:5].css',
      chunkFilename: 'style/[name]-[chunkhash:5].css'
    })
  ]
}

其中创建mini-css-extract-plugin实例的参数的filenamechunkFilename可配置的值与output.filename是一样的。filename表示输出非按需加载的chunkcssbundlechunkFilename表示输出按需加载的chunkcssbundle

开启css模块

我们知道开发大部分时候css选择器是根据类名去匹配元素的,如果工程比较大多人开发,或者有两个组件使用了一个相同的类名,后者就会把前者的样式给覆盖掉,为了解决这个问题产生出了CSS模块化概念。

其实css-loader已经内置了模块化功能,在默认情况下,css-loader会用/\.module\.\w+$/i.test(filename)匹配文件名,如果匹配上了就开启模块化。也就是说我们的css文件名以module.css结尾即可开启模块化。开启模块化的css文件,模块中 (css文件) 每个类选择器和id选择器都会替换成hash名称,如果我们不需要替换可以使用:global()来包裹住不需要替换的选择器。

/* login.module.css 打包前 */
.name {
  width: 1px;
}
.user-age {
  width: 1
}
#pwd {
  width: 1px;
}
.user .age div {
  width: 1px;
}
.user :global(.age) div {
  width: 2px;
}

/* 打包后 */
.ah4VvUCzdSHinav53q40 {
  width: 1px;
}
.M7m9Yez7JZkfnGJZgkWc {
  width: 1
}
#UB1EFfX0F7izHsHajHV8 {
  width: 1px;
}
.Oos422SahRxya3J3gTZ7 .cs0aFUaAnLU8hThBYcPk div {
  width: 1px;
}
.Oos422SahRxya3J3gTZ7 .age div {
  width: 2px;
}

由于打包后的名称会更改,无法引用,所以我们使用时需要换一种使用方式。使用css-loader为我们生成的对象来引用名称。

import style from '../style/login.module.css'

const $app = documnet.querySelect('#app')

$app.innerHTML = `
  <div>名称:<input class="${style.name}"></div>
  <div>年龄:<select class="${style['user-age']}"></select>
`

优化模块化

虽然上面开启了模块化,但是我们需要对其进行优化,给css-loader传入参数

  • 在使用默认的配置下模块化的css只会产生一个默认对象,来引用所有生成名称,如果我们想用es5 module需要将namedExport属性改为true
  • 使用默认的name生成方式非常难调试,因为生成名称基本上没有阅读性。我们需要自定义生成名称,需要自定义localIdentName属性,localIdentName也是字符串模板,下面列出可支持使用模板:
    • [name] 源文件名称
    • [folder] 文件夹相对于compiler.context或者modules.localIdentContext配置项的相对路径。
    • [path] 源文件相对于compiler.context或者modules.localIdentContext配置项的相对路径。
    • [file] - 文件名和路径。
    • [ext] - 文件拓展名。
    • [hash] - 字符串的哈希值。基于localIdentHashSaltlocalIdentHashFunctionlocalIdentHashDigestlocalIdentHashDigestLengthlocalIdentContextresourcePathexportName生成。
    • [local] - 原始类名。
  • 编写css文件时一般采用-来代替驼峰命名,我们需要将-转化为驼峰,需要将exportLocalsConvention属性改为camelCaseOnly
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.css/,
        use: [
          {
            loader: 'css-loader',
            options: {
              // 模块化定制
              modules: {
                // 更改模块化导出的样式名称 使用文件名称加原始类名加hash
                localIdentName: '[name]__[local]--[hash:base64:5]',
                // 驼峰化对象,并保留原始key 比如 userAge user-age都可用
                exportLocalsConvention: 'camelCaseOnly',
                // 启用es5 模块
                namedExport: true
              }
            }
          }
        ]
      }
    ]
  }
}

接下来我们看看更换后打包结果,

.login-module__name--ah4Vv {
  width: 1px;
}
.login-module__user-age--M7m9Y {
  width: 1
}
#login-module__pwd--UB1EF {
  width: 1px;
}
.login-module__user--Oos42 .login-module__age--cs0aF div {
  width: 1px;
}
.login-module__user--Oos42 .age div {
  width: 2px;
}

使用方式

import { name, userAge } from '../style/login.module.css'

const $app = documnet.querySelect('#app')

$app.innerHTML = `
  <div>名称:<input class="${name}"></div>
  <div>年龄:<select class="${userAge}"></select>
`

使用scss、less

要使用scssless就首先要把这两种预处理语言转化成css语言,scss使用node-sass库来转化,而less则是使用less库。转化成功后要将结果给到webpack使用,我们可以使用sass-loaderless-loader来完成这个工作。我们看看怎么集成

npm install --save-dev less-loader less
npm install --save-dev sass-loader node-sass
const cssRuleLoader = [
  { loader: MiniCssExtractPlugin.loader },
  {
    loader: 'css-loader',
    options: {
      modules: {
        localIdentName: '[name]__[local]--[hash:base64:5]',
        exportLocalsConvention: 'camelCaseOnly',
        namedExport: true
      }
    }
  }
]

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.less/,
        use: [
          ...cssRuleLoader,
          'less-loader'
        ]
      },
      {
        test: /.scss/,
        use: [
          ...cssRuleLoader,
          'sass-loader'
        ]
      }
    ]
  }
}

scssless同样也可以开启模块化,我们前面说了只要符合/\.module\.\w+$/i.test(filename)即可,所以我们只需将文件名改为login.module.scsslogin.module.less即可开启模块化。

使用PostCSS

PostCSS是一个CSS处理工具,它通过插件机制可以灵活的扩展其支持的特性。目前用法最广泛的就是为CSS自动添加厂商前缀、使用下一代CSS语法,然后转化为现代浏览器能识别的语法,这个工作可以借助postcsspostcss-preset-env插件完成。使用时需要将样式内容传入postcss库,然后将生成的内容提供给webpack,需要使用postcss-loader完成这一工作。

npm install --save-dev postcss-loader postcss postcss-preset-env
const cssRuleLoader = [
  { loader: MiniCssExtractPlugin.loader },
  {
    loader: 'css-loader',
    options: {
      modules: {
        localIdentName: '[name]__[local]--[hash:base64:5]',
        exportLocalsConvention: 'camelCaseOnly',
        namedExport: true
      }
    }
  },
  // 在传入webpack前需要先使用postcss转化
  {
    loader: 'post-loader',
    options: {
      // postss需要使用的插件
      plugins: [
        // 数组,第一个使用插件,第二个参数
        [
          'postcss-preset-env',
          {
            stage: 2,
            browsers: {
              // 正式环境
              production: [ '> 0.2%', 'ie > 10' ],
              // 开发环境
              development: [
                "last 1 chrome version",
              "last 1 firefox version",
              "last 1 safari version"
              ]
            }
          }
        ]
      ]
    }
  }
]

stage决定哪些css特性需要被polyfill,他们的值分别代表

  • 0 非官方草案
  • 1 编辑草案或早期工作草案
  • 2 工作草案
  • 3 候选版本
  • 4 推荐标准

browsers来配置需要支持的浏览器环境,其中数据和是否需要加前缀是根据caniuse决定的。查看的更多browsers定义规则

@import处理

如果我们使用上方的配置在样式文件中使用@import导入其他样式文件时会有出乎预料情况发生。我们看看打包情况

/* ------------打包前------------- */
/* base.scss */
* {
  margin: 0;
  padding: 0;
  border: 0;
}

.user {
  .name {
    color: #12312312;
  }
   display: flex;
}

:fullscreen {
  height: auto;
}

/* login.scss */
@import url('./base.scss');

.name {
  .age {
    background: #12312312;
    height: 100px;
  }
}

:global(.age) {
  display: flex;
}

:fullscreen {
  height: 100px;
}

/* ---------------------------- */


/* ------------打包后------------- */
* {
  margin: 0;
  padding: 0;
  border: 0;
}

.base__user--VIoKM {
  .base__name--BOmiJ {
    color: #12312312;
  }
   display: flex;
}

:fullscreen {
  height: auto;
}

.home-module__name--x_p3i .home-module__age--jCPqC {
  background: rgba(18,49,35,0.07059);
  height: 100px; }

.age {
  display: -ms-flexbox;
  display: flex; }

:-webkit-full-screen {
  height: 100px; }

:-ms-fullscreen {
  height: 100px; }

:fullscreen {
  height: 100px; }


/* ---------------------------- */

我们看到被@import进来的样式文件样式开启了模块化,证明它经过了css-loader处理。但是并有将scss转化为css,该添加的前缀也被加,并没有被postcss-loaderscss-loader处理。这是因为@import是在css-loader中处理的,默认导入的css只从当前loader开始处理,然后流向下一个loader

css-loader有一个参数可以定义一个参数来配置由@import进来的文件如何处理,importLoaders设置在css-loader之前应用的loader的数量。上方的情况在css-loader之前还需要应用到postcss-loaderscss-loader所以应该设置为2。

const cssRuleLoader = [
  { loader: MiniCssExtractPlugin.loader },
  {
    loader: 'css-loader',
    options: {
      importLoaders: 2,
      modules: {
        localIdentName: '[name]__[local]--[hash:base64:5]',
        exportLocalsConvention: 'camelCaseOnly',
        namedExport: true
      }
    }
  },
  // 在传入webpack前需要先使用postcss转化
  {
    loader: 'post-loader',
    options: {
      // postss需要使用的插件
      plugins: [
        // 数组,第一个使用插件,第二个参数
        [
          'postcss-preset-env',
          {
            stage: 2,
            browsers: {
              // 正式环境
              production: [ '> 0.2%', 'ie > 10' ],
              // 开发环境
              development: [
                "last 1 chrome version",
                "last 1 firefox version",
                "last 1 safari version"
              ]
            }
          }
        ]
      ]
    }
  }
]

更改配置后的打包结果就正常了:

* {
  margin: 0;
  padding: 0;
  border: 0; }

.base__user--VIoKM {
  display: -ms-flexbox;
  display: flex; }
  .base__user--VIoKM .base__name--BOmiJ {
    color: rgba(18,49,35,0.07059); }

:-webkit-full-screen {
  height: auto; }

:-ms-fullscreen {
  height: auto; }

:fullscreen {
  height: auto; }

.home-module__name--x_p3i .home-module__age--jCPqC {
  background: rgba(18,49,35,0.07059);
  height: 100px; }

.age {
  display: -ms-flexbox;
  display: flex; }

:-webkit-full-screen {
  height: 100px; }

:-ms-fullscreen {
  height: 100px; }

:fullscreen {
  height: 100px; }

使用ts

TypeScriptJavaScript的一个超集,主要提供了类型检查系统和对最新特性语法的支持。首先需要一个将ts编译成js的编译器typescript,然后将它集成到webpack中,使用ts-loader

npm install --save-dev typescript ts-loader

我们还需要一个配置编译选项文件告诉typescript如何编译它,编译器默认会读取和使用在当前项目根目录下的tsconfig.json文件。

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2016",
    "incremental": true,
    "tsBuildInfoFile": "./node_modules/ts-cache",
    "sourceMap": true,
    "rootDir": "./",
    "baseUrl": "./",
  },
  "include": ["src/**/*.ts"],
  "exclude": []
}

然后我们需要在webpack中声明ts文件的处理

const jsUseLoader = () => ({
  loader: 'babel-loader',
  options: {
    presets: [
      ['@babel/preset-env']
    ]
  }
})

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.js/,
        use: jsUseLoader()
      },
      { 
        test: /.tsx?$/,
        use: [jsUseLoader(), 'ts-loader']
      }
    ]
  },
  resolve: {
    // 没有填写后缀时使用下列顺讯寻找
    extensions: ['.ts', '.tsx', '.js']
  }
}

tsconfig.json

下面是这个tsconfig.json文件的常用配置及说明:

{
  "compilerOptions": {
    /* -----------------------基本选项----------------------- */

    // 开启增量编译:TS 编译器在第一次编译的时候,会生成一个存储编译信息的文件,下一次编译的时候,会根据这个文件进行增量的编译,以此提高 TS 的编译速度 
    "incremental": true,

    // 指定存储增量编译信息的文件位置
    "tsBuildInfoFile": "./node_modules/ts-cache",

    // 控制编译后输出的是什么js版本,默认值为es3。
    "target": "es2016",

    // 指定要引入的库文件,默认值为 target === 'ES6' ? [DOM,ES6,DOM.Iterable,ScriptHost] : [DOM,ES5,ScriptHost]
    // 可选值有   
    //  JavaScript 功能: es5 es6 es2015 es7 es2016 es2017 esnext
    //  运行环境: dom dom.iterable webworker scripthost
    //  ESNext功能选项: es2015.core es2015.collection es2015.generator es2015.iterable es2015.promise 
    //                 es2015.proxy es2015.reflect es2015.symbol es2015.symbol.wellknown es2016.array.include 
    //                 es2017.object es2017.sharedmemory esnext.asynciterable
    "lib": ["esnext", "dom"],

    // 是否对js文件进行编译,默认false
    "allowJs": false,

    // 报告 javascript 文件中的错误
    "checkJs": false,

    // 指定jsx代码用于的开发环境:'preserve','react-native',or 'react
    // 'react' 模式下:TS 会直接把 jsx 编译成 js
    // 'preserve' 模式下:TS 不会把 jsx 编译成 js,会保留 jsx
    "jsx": "preserve",

    // 生成相应的 '.d.ts' 文件
    "declaration": false,

    // 生成相应的 '.map' 文件
    "sourceMap": true,

    // 是否生成sourceMap,默认false
    "sourceMap": false,

    // 将输出文件合并为一个文件
    "outFile": "./"

    // 输入文件的根目录
    "rootDir": ".",

    // 指定编译结果的输出目录的,默认是将编译结果输出文件输出到源文件目录下
    "outDir": "dist",

    // 删除注释
    "removeComments": false,

    /* -----------------------模块解析处理----------------------- */

    // 支持使用es模块引入commonjs包,并让ts默认处理
    "esModuleInterop": true,

    // 指定类型声明文件的查找路径。默认值为node_modules/@types
    "typeRoots": ['node_modules/@types', './typings'],

    // 配合typeRoots使用,指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来
    "types": ["jest", "node"],

    // 拓宽引入非相对模块时的查找路径的。其默认值就是"./",
    // 如果找不到模块还会到baseUrl中指定的目录下查找
    "baseUrl": ".",

    // 配合baseUrl一起使用的,用于到baseUrl所在目录下指定的路径映射。
    "paths": {
      "@": ["src/"],
    }

    // 指定要使用的模块标准,如果不显式配置module,那么其值与target的配置有关,其默认值为target === "es3" or "es5" ?"commonjs" : "es6"
    // 'None', 'CommonJS', 'AMD', 'System', 'UMD', 'ES6'/'ES2015', 'ES2020' or 'ESNext'
    "module": "esnext",

    // 模块的解析规则,默认值为 module ==="amd" or "system" or "es6" or "es2015"? "classic" : "node"
    // classic 
    //    对于相对路径模块: 只会在当前相对路径下查找是否存在该文件(.ts文件)
    //    非相对路径模块: 编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的ts文件或者d.ts类型声明文件
    // node
    //    对于相对路径模块:除了会在当前相对路径下查找是否存在该文件(.ts文件)外,还会作进一步的解析,如果在相对目录下没有找到对应的.ts文件,
    //      那么就会看一下是否存在同名的目录,如果有,那么再看一下里面是否有package.json文件,然后看里面有没有配置,main属性,如果配置了,
    //      则加载main所指向的文件(.ts或者.d.ts),如果没有配置main属性,那么就会看一下目录里有没有index.ts或者index.d.ts,有则加载。
    //    对于非相对路径模块: 对于非相对路径模块,那么会直接到a.ts所在目录下的node_modules目录下去查找,也是遵循逐层遍历的规则,查找规则同上
    "moduleResolution": "node",

    // 允许导入.json文件
    "resolveJsonModule": true,

    /* -----------------------模块解析处理----------------------- */

    // 开启全局严格检查,默认false
    "strict": true,

    // 是否检查检查未使用的局部变量,默认false
    "noUnusedLocals": true,

    // 不允许使用隐式的 any 类型
    "noImplicitAny": false,

    // 不允许把 null、undefined 赋值给其他类型变量
    "strictNullChecks": false,

    // 不允许函数参数双向协变 如下方将不被允许
    // type fn = (a: number) => void  
    // let a: number | string 
    // fn(a) 
    "strictFunctionTypes": true,

    // 使用 bind/call/apply 时,严格检查函数参数类型
    "strictBindCallApply": true,

    // 有未使用到的函数参数时报错
    "noUnusedParameters": true,

    // 不允许 switch 的 case 语句贯穿
    "noFallthroughCasesInSwitch": true,

    /* -----------------------其他选项----------------------- */

    // 开启装饰器特性
    "experimentalDecorators": true,

    // 给源码里的装饰器声明加上设计类型元数据。
    "emitDecoratorMetadata": false,

    // 开启使用Object.defineProperty替换class声明中的字段
    "useDefineForClassFields": false,

  },

  // 编译包含的源文件
  "include": ["src/"]

  // 需要排除的源文件
  "exclude": ['src/test']
}

更全配置参考官网,接下来就要接入webpack

导入静态资源文件

webpack一切皆模块可以在js中导入如png、css等文件在loader中解析,但是ts并不认识这些文件,如果不做处理直接引入会导致ts编译失败。我们需要对这些模块类型声明,ts并不处理这些模块,在转译成js后在webpack处理即可。

接下来我们声明一份global.d.ts然后将它放到源码根目录,ts就可以识别到他们了。

// global.d.ts
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'

declare module '*.css'
declare module '*.sass'
declare module '*.scss'
declare module '*.less'

vscode ts支持模块化css

上方的类型声明只是让ts支持这些模块的导入,如果我们使用模块化css无法检测导入变量是否正确,也无法享受ts的提示,为了解决这个我们可以使用typescripttypescript-plugin-css-modules插件。首先我们需要安装它

npm install  --save-dev typescript-plugin-css-modules

我们需要修改tsconfig.json来引用这个插件

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2016",
    "incremental": true,
    "tsBuildInfoFile": "./node_modules/ts-cache",
    "sourceMap": true,
    "rootDir": "./",
    "baseUrl": "./",
    "plugins": [
      {
        "name": "typescript-plugin-css-modules",
        "options": {
          // classname转化 与css-loader的exportLocalsConvention保持一致
          "classnameTransform": "camelCaseOnly",
          // 启用es5模块 与 css-loader的namedExport保持一致
          "namedExports": true
        }
      }
    ]
  },
  "include": ["src/**/*.ts"],
  "exclude": []
}

因为这个插件并不是针对真正ts编译的,而是针对vscode编辑器的,所以我们还需要将编辑器的当前使用的TypeScript的版本改成当前工作区版本。

  • 随意打开一个ts文件
  • 点击底部{} Typescript选择版本
  • 选择使用工作区版本

css-model-ts.png

选择后如果没生效重启vscode因为有可能有缓存。然后就可以看到效果了

css-module-tip.png

ts 快捷路径访问模块

我们在webpack建立的快捷路径访问模块,typescript并不认识它,除了在webpack.config.js建立外,我们还需要在tsconfig.json中配置快捷路径访问模块,而且两者配置规则不太一样

  "compilerOptions": {
    "paths": {
      "@ui/*": ["packages/components/*"],
      "@sdk/*": ["packages/sdk/*"],
      "~sdkConfig": ["packages/sdk/config/default.json"]
    },
  }

如果可能我们应该尽可能只保留一份配置文件,这样方便管理,我们保留tsconfig.json版本,然后写一个函数来生成webpackresolve.alias。我们可以用convert-tsconfig-paths-to-webpack-aliases包来做这件事。

npm install --save-dev convert-tsconfig-paths-to-webpack-aliases
const tsconfigPathToAlias = require('convert-tsconfig-paths-to-webpack-aliases').default
const tsconfig = require('./tsconfig.json')
const aliass = tsconfigPathToAlias(tsconfig)

module.exports = {
  // ...
  resolve: {
    alias: aliass
  }
}

使用ejs

在没有使用vuereact等框架提供方便模板和jsx环境下编写项目下,编写html可能会显得比较吃力,我们可以使用一些模板来替代它们的工作,这里介绍一下ejs如何集成到webpack

要将ejs如何集成到webpack首先要使用ejs-loader,然后再loader预设。

npm install --save-dev ejs-loader
module.exports = {
  module: {
    rules: [
      {
        test: /.ejs$/
        exclude: /node_modules|bower_compunents/,
        use: {
          loader: 'ejs-loader',
          options: {
            // 因为ejs使用了with语法,在esModule模式下会禁止使用,直接报错
            esModule: false
          }
        }
      }
    ]
  }
}

然后我们在js的模块中就可以使用它们了,如果是ts则还需要为这类模块进行声明。

declare module '*.ejs' {
  const EjsTemplate: (args: { [key in string]: any }) => string
  export default EjsTemplate
}

源码

// webpack.config.js
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const coverPathsToAliases = require('convert-tsconfig-paths-to-webpack-aliases').default

const aliass = coverPathsToAliases(require('./tsconfig.json'))

const getPath = (p) => {
  const ps = Array.isArray(p) ? p : [p]
  return path.join(__dirname, ...ps)
}

const cssLoaders = [
  {
    loader: MiniCssExtractPlugin.loader
  },
  {
    loader: 'css-loader',
    options: {
      url: true,
      importLoaders: 8,
      modules: {
        localIdentName: "[name]__[local]--[hash:base64:5]",
        exportLocalsConvention: 'camelCaseOnly',
        namedExport: true,
      }
    }
  },
  {
    loader: 'postcss-loader',
    options: {
      postcssOptions: {
        plugins: [
          [
            'postcss-preset-env',
            {
              stage: 2,
              browsers: {
                // 正式环境
                production: [ '> 0.2%', 'ie > 10' ],
                // 开发环境
                development: [
                  "last 1 chrome version",
                  "last 1 firefox version",
                  "last 1 safari version"
                ]
              }
            }
          ]
        ]
      }
    }
  }
]

const scriptLoaders = [
  {
    loader: 'babel-loader',
    options: {
      presets: [
        [
          '@babel/preset-env',
          {
            targets: {
              chrome: '90',
              ie: 11
            }
          }
        ],
      ],
      plugins: [
        '@babel/plugin-transform-runtime',
        '@babel/plugin-syntax-top-level-await'
      ],
      cacheDirectory: true
    }
  }
]


module.exports = {
  context: getPath('src'),
  entry: {
    login: './app/login.js',
    home: './app/home.js',
  },
  experiments: {
    topLevelAwait: true
  },
  output: {
    clean: true, 
    path: getPath('./dist'),
    chunkFilename: '[name]-[contenthash:5].js',
    assetModuleFilename: 'images/[hash][ext]',
    filename: '[name]-[contenthash:5].js'
  },
  module: {
    rules: [
      {
        test: /.(obj|mtl)/i,
        type: 'asset/resource',
        generator: { filename: 'static/model/[hash][ext]' }
      },
      {
        test: /\.(png|jpg|gif)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: { maxSize: 10 * 1024 }
        }
      },
      {
        oneOf: [
          {
            test: /.svg$/i,
            resourceQuery: /raw/,
            type: 'asset/source'
          },
          {
            test: /.svg$/i,
            type: 'asset/resource'
          }
        ]
      },
      {
        test: /\.scss/,
        use: [ ...cssLoaders, 'sass-loader' ]
      },
      {
        test: /\.less/,
        use: [ ...cssLoaders, 'less-loader' ]
      },
      {
        test: /\.css/,
        use: cssLoaders
      },
      {
        test: /\.js$/,
        exclude: /node_modules|bower_compunents/,
        use: scriptLoaders,
      },
      {
        test: /.ts$/,
        exclude: /node_modules|bower_compunents/,
        use: [ ...scriptLoaders, { loader: 'ts-loader' } ]
      },
      {
        test: /.ejs$/,
        exclude: /node_modules|bower_compunents/,
        use: {
          loader: 'ejs-loader',
          options: { esModule: false }
        }
      }
    ],
  },
  devtool: 'source-map',
  optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: 'all',
      minChunks: 1,
      automaticNameDelimiter: '~',
      cacheGroups: {
        vendors: {
          name: false,
          test: /node_modules/,
          priority: 10,
          automaticNameDelimiter: '~',
          reuseExistingChunk: true
        },
        commons: {
          minSize: 0,
          minChunks: 2,
          priority: 20,
          reuseExistingChunk: true
        }
      }
    }
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'style/[name]-[contenthash:5].css',
      chunkFilename: 'style/[name]-[chunkhash:5].css'
    }),
    new HtmlWebpackPlugin({
      template: getPath('./src/pages/login.ejs'),
      filename: 'login.html',
      titlea: 'login',
      chunks: ['login'],
    })
  ],
  resolve: {
    extensions: ['.js', '.ts'],
    alias: aliass
  },
  externals: {
    jquery: 'jQuery',
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2016",
    "incremental": true,
    "tsBuildInfoFile": "./node_modules/ts-cache",
    "sourceMap": true,
    "typeRoots": ["node_modules/@types"],
    "rootDir": "./",
    "types": ["node"],
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "plugins": [
      {
        "name": "typescript-plugin-css-modules",
        "options": {
          "classnameTransform": "camelCaseOnly",
          "namedExports": true
        }
      }
    ]
  },
  "include": ["src/**/*.ts"],
  "exclude": []
}

到这里webpack5的基础知识我们就讲完了,下一章我们说说如何优化开发环境,使webpack能更快的构建本地应用,并充分利用编译缓存。