还在困惑项目脚手架代码为什么那么写?那这篇webpack5 + react + typescript环境配置代码完全指南送给你

4,572 阅读55分钟

前言

本文是对某开源的项目webpack5 + react + typescript项目地址逐行代码做分析,解剖一个成熟的环境所有配置的意义,理清一些常见的问题,比如

  • 文件中的 importes5webpack 编译成了什么?webpack的分包配置splitChunks咋用? 你真的理解其选项值chunks的值async或者initial或者all是什么意思吗,这个可对分包优化至关重要啊!为什么package.json没有依赖的包,在node_modules下面会出现,npm install包是以什么结构安装npm包呢?

  • babel/core有什么用,它跟babel-loader的区别, babelrc文件中配置项presetsplugin的区别是什么,babelrc常用设置项知道多少,这个不清楚?那项目代码校验和格式化用到editorConfig、prettier、eslint,stylelint他们的关系和区别是什么?如何配置防止它们冲突,比如eslint也有css校验,怎么让stylelint跟它不起冲突,这些你要晋升为前端主管怎么能心里没数?

  • 如果你用的vscode,如何在工作区配置ctrl+s自动保存,让你的js和css文件自动格式化,并配置为prettier格式化,webpack54的配置中的变化、等等。。。

以上提到的知识点对我们深入了解项目环境搭建非常重要, 你的项目你来时一般环境都是搭建好的,试过从0自己搭建不?是不是抄别人的配置,都一头雾水,完全不知道这些配置项时啥意思呢?

现在!本篇本章专解这个问题!废话少说,

我们先从package.json说起,里面的每一行代码是什么意思。

package.json

package.json里面有很多有趣的内容,我们先从依赖包说起,解释这个项目中,下面的依赖包分别有什么用。

  "devDependencies": {
    "@babel/core": "^7.13.13",
    "@babel/plugin-transform-runtime": "^7.13.10",
    "@babel/preset-env": "^7.13.12",
    "@babel/preset-react": "^7.13.13",
    "@babel/preset-typescript": "^7.13.0",
    "@commitlint/cli": "^12.0.1",
    "@commitlint/config-conventional": "^12.0.1",
    "@types/react": "^17.0.3",
    "@types/react-dom": "^17.0.3",
    "@types/webpack-env": "^1.16.0",
    "@typescript-eslint/eslint-plugin": "^4.19.0",
    "@typescript-eslint/parser": "^4.19.0",
    "babel-loader": "^8.2.2",
    "chalk": "^4.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "conventional-changelog-cli": "^2.1.1",
    "copy-webpack-plugin": "^8.1.0",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.0",
    "css-minimizer-webpack-plugin": "^1.3.0",
    "detect-port-alt": "^1.1.6",
    "error-overlay-webpack-plugin": "^0.4.2",
    "eslint": "^7.22.0",
    "eslint-config-airbnb": "^18.2.1",
    "eslint-config-prettier": "^8.1.0",
    "eslint-import-resolver-typescript": "^2.4.0",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-prettier": "^3.3.1",
    "eslint-plugin-promise": "^4.3.1",
    "eslint-plugin-react": "^7.23.1",
    "eslint-plugin-react-hooks": "^4.2.0",
    "eslint-plugin-unicorn": "^29.0.0",
    "fork-ts-checker-webpack-plugin": "^6.2.0",
    "html-webpack-plugin": "^5.3.1",
    "husky": "^4.3.8",
    "ip": "^1.1.5",
    "is-root": "^2.1.0",
    "lint-staged": "^10.5.4",
    "mini-css-extract-plugin": "^1.4.0",
    "node-sass": "^5.0.0",
    "postcss": "^8.2.8",
    "postcss-flexbugs-fixes": "^5.0.2",
    "postcss-loader": "^5.2.0",
    "postcss-preset-env": "^6.7.0",
    "prettier": "^2.2.1",
    "sass-loader": "^11.0.1",
    "style-loader": "^2.0.0",
    "stylelint": "^13.12.0",
    "stylelint-config-prettier": "^8.0.2",
    "stylelint-config-rational-order": "^0.1.2",
    "stylelint-config-standard": "^21.0.0",
    "stylelint-declaration-block-no-ignored-properties": "^2.3.0",
    "stylelint-order": "^4.1.0",
    "stylelint-scss": "^3.19.0",
    "terser-webpack-plugin": "^5.1.1",
    "typescript": "^4.2.3",
    "webpack": "^5.37.1",
    "webpack-bundle-analyzer": "^4.4.0",
    "webpack-cli": "^4.5.0",
    "webpack-dev-server": "^3.11.2",
    "webpack-merge": "^5.7.3",
    "webpackbar": "^5.0.0-3"
  },
  "dependencies": {
    "@babel/runtime-corejs3": "^7.13.10",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }

@babel/core有啥用?

babel 的功能在于「代码转译」,具体一点,即将目标代码转译为能够符合期望语法规范的代码。在转译的 过程中,babel 内部经历了「解析 - 转换 - 生成」三个步骤。而 @babel/core 这个库则负责「解析」,具体的「转换」「生成」步骤则交给各种插件(plugin)和预设(preset)来完成。

你可以从@babel/core自己的依赖里看到其中有三个包,叫@babel/generator (将ast生成代码)、 @babel/parser(将源代码转换为AST)、@babel/traverse(转换AST),有这三个包,就能转换你的代码,案例如下:

import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';

const code = 'const n = 1';

// 将源代码转换为AST
const ast = parse(code);

// 转换AST
traverse(ast, {
  enter(path) {
    // in this example change all the variable `n` to `x`
    if (path.isIdentifier({ name: 'n' })) {
      path.node.name = 'x';
    }
  },
});

// 生成代码 <- ast
const output = generate(ast, code);
console.log(output.code); // 'const x = 1;'

这应该非常清楚的了解babel/core有什么用了吧,至于说怎么在traverse阶段改变代码,就要用到其他的插件了,我们马上说一下babel-loader,让你明白它跟babel/core的区别

babel-loader

我们知道webpack需要各种loader,这些loader的作用就是把文件做转化,比如babel-loader是用来转化jsjsxtstsx文件的。

比如我们写的js代码是es6import xx模块 from ‘xx模块’,为了浏览器兼容性,我们需要转化为es5的写法,转译import,那么这个时候就需要babel-loader来帮忙了。

比如说一个简单的loader怎么写呢,我们就知道babel-loader大概是个什么东西了,

module.exports = source => {
	// source 就是加载到的文件内容
	console.log(source)
	return "hello ~" // 返回一个字符串
}

上面我们把任何加载到的文件内容转化为一个字符串,也就是loader无非是加工读到的文件,所以babel-loader就是读取对应的jsx?|tsx?文件,然后加工后返回而已

prest家族:@babel/preset-env、@babel/preset-react、@babel/preset-typescript、

  • @babel/preset-typescript: 主要是用来编译ts文件的。

目前 TypeScript 的编译有两种方式。一种是使用 TypeScript 自家的编译器 typescript 编译(简称 TS 编译器),一种就是使用 Babel + @babel/preset-typescript 编译。

其中最好的选择就是使用Babel + @babel/preset-typescript,主要原因是:

  • Babel 能够指定需要编译的浏览器环境。这一点 TS 编译器是不支持的。在babelrc文件里可以设置编译的target属性(在preset-env插件上设置)为比如
"targets": {
  "browsers": ["last 2 versions", "safari >= 7"], // 配置safari的版本大于7的语法才转译
  "node": "6.10" // node版本支持到6.10
  • TS 编译器在编译过程中进行类型检查,类型检查是需要时间的,而 babel 不做类型检查,编译速度就更快

@babel/preset-react: 主要是编译jsx文件的,也就是解析jsx语法的,比如说react生成div,我们举一个例子,在jsx里面是这样的,转换成什么了呢?

<div></div>

转化后的reactapi

const reactElement = React.createElement(
  	... // 标签名称字符串/ReactClass,
  	... // [元素的属性值对对象],
  	... // [元素的子节点]
)
reactElement('div', null, '')
  • @babel/preset-env:

@babel/preset-env将基于你的实际浏览器及运行环境,自动的确定babel插件及polyfill,在不进行任何配置的情况下,@babel/preset-env所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段),将其转换成ES5代码。例,那么只配置 @babel/preset-env,转换时会抛出错误,需要另外安装相应的插件。

//.babelrc

{

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

}

注意:@babel/preset-env会根据你配置的目标环境,生成插件列表来编译。Babel 官方建议我们把 targets 的内容保存到 .browserslistrc文件中 或者 package.json 里增加一个browserslit节点,不然除了babel外,其他的工具,例如browserslistpost-css等无法从 babel 配置文件里读取配置

如果你不是要兼容所有的浏览器和环境,推荐你指定目标环境,这样你的编译代码能够保持最小。

具体用法我们会在将babelrc文件配置(babel的配置文件)的时候详细说明。

@babel/plugin-transform-runtime、@babel/runtime-corejs

为什么我们需要它,我们来看看@babel/prest-env编译完js文件后,会有哪些问题

  • 比如我们使用字符串的inclues语法(es5中并不支持它,需要转译), 例如 Array.from 等静态方法,直接在 global.Array 上添加;对于例如 includes 等实例方法,直接在global.Array.prototype上添加。这样直接修改了全局变量的原型。

  • babel 转译 syntax 时,有时候会使用一些辅助的函数来帮忙转,比如:

class 语法中,babel 自定义了 _classCallCheck这个函数来辅助;typeof 则是直接重写了一遍,自定义了 _typeof 这个函数来辅助。这些函数叫做 helpers。每个项目文件都写无意是不合理的。

作用是将 helper(辅助函数) 和 polyfill(不修改全局变量原型的静态方法等) 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的。

具体配置不详细说明了,到后面讲babelrc文件的的时候说。

  • @babel/runtime-corejs:

上面我们看到了@babel/prest-env带来的问题,这两个问题@babel/plugin-transform-runtime可以解决,那@babel/runtime-corejs又是个什么东西呢?

其中 @babel/plugin-transform-runtime 的作用是转译代码,转译后的代码中可能会引入 @babel/runtime-corejs 里面的模块,也就是说具体转译代码的函数是单独在另一个包里,就是@babel/runtime-corejs里面

types家族:@types/react @types/react-dom @types/webpack-env

  • @types/react、@types/react-dom这两个是react的typescript类型定义

  • @types/webpack-env 是webpack的typescript类型定义

eslint家族:eslint、eslint-config-airbnb、eslint-config-prettier...

  • eslint:是一个插件化并且可配置的 JavaScript 语法规则和代码风格的检查工具。这个就不多说了,大家都知道吧,不用eslint的前端项目应该很少。

  • eslint-config-airbnb:Airbnb的eslint规则的标准,它依赖eslint, eslint-plugin-import, eslint-plugin-react, and eslint-plugin-jsx-a11y等插件,并且对各个插件的版本有所要求。

  • eslint-config-prettier:prettier是一个代码格式化工具,比如说规范项目都使用单引号,还是双引号。而且,Prettier 还给予了一部分配置项,可以通过 .prettierrc 文件修改。

  • 所以相当于 Prettier 接管代码格式的问题,而使用 Prettier + ESLint 就完完全全解决了代码格式和代码语法规则校验的问题。

但实际上使用起来配置有些小麻烦,但也不是什么大问题。因为 PrettierESLint 一起使用的时候会有冲突,我们需要使用 eslint-config-prettier 来关掉 (disable) 所有和 Prettier 冲突的 ESLint 的配置,

eslint-plugin-prettier 将 prettier 的 rules 以插件的形式加入到 ESLint 里面方法就是在 .eslintrc 里面将 prettier 设为最后一个 extends

// .eslintrc    
{      
    "plugins": ["prettier"],      
    "rules": {        
        "prettier/prettier": "error"      
    }    
}

将上面两个步骤和在一起就是下面的配置,也是官方的推荐配置

// .eslintrc
{
  "extends": ["plugin:prettier/recommended"]
}
  • eslint-plugin-import:用于校验es6import规则,如果增加import plugin,在我们使用webpack的时候,如果你配置了resolve.config.jsalias,那么我们希望import plugin的校验规则会从这里取模块的路径,此时需要配置,注意,此时同时要下载eslint-import-resolver-webpack插件才能像下面一样设置
“rules”: {},
       /** 这里传入webpack并不是import插件能识别webpack,
       * 而且通过npm安装了「eslint-import-resolver-webpack」,
       * 「import」插件通过「eslint-import-resolver-」+「webpack」找到该插件并使用,
       * 就能解析webpack配置项。使用里面的参数。
       **/
"settings": {
        // 使用webpack中配置的resolve路径
        "import/resolver": "webpack" 
}

eslint-import-resolver-typescript:它也是「eslint-import-resolver-」家族的一员,它的作用是

  • import/require 扩展名为 .ts/.tsx 的文件
  • 使用 tsconfig.json 中定义的paths路径
  • 优先解析@types/* 定义而不是普通的 .js

eslint-plugin-jsx-a11y: 该插件为你的 JSX 中的无障碍问题提供了 AST 的语法检测反馈。

eslint-plugin-react: 一些 reacteslintrules 规范

eslint-plugin-react-hooks:检测react hooks的一些语法规范,并提供相应的rules

postcss家族:postcss、postcss-flexbugs-fixes、postcss-loaderpostcss-preset-env,autoprefixer

postcss: 是一个使用JavaScript插件来转换CSS的工具。

PostCSS本身很小,其只包含CSS解析器,操作CSS节点树的APIsource map,以及一个节点树字符串化工具,其它功能都是通过插件来实现的,比如说插件有

1、添加浏览器内核前缀的

2、有检测css代码的工具等等

postcss-flexbugs-fixes: 修复在一些浏览器上flex布局的bug,比如说

  • 在ie10和标准的区别 ----|标准| flex: 1 flex: 1 1 0% flex: 1 0 0px flex: auto flex: 1 1 auto flex: 1 0 auto
缩写声明标准转义IE10转义
(no flex declaration)flex: 0 1 autoflex: 0 0 auto
flex: 1flex: 1 1 0%flex: 1 0 0px
flex: autoflex: 1 1 autoflex: 1 0 auto

postcss-loader:loader的功能在上面已经说明,这个loaderpostcss用来改变css代码的loader

postcss-preset-env:这个插件主要是集成了(有了它不用下载autoprefixer插件)

autoprefixer:用于解析 CSS 并使用 Can I Use 中的值向 CSS 规则添加供应商前缀

style-resoures-loader:这个插件比较重要,即使这个项目没有用,我也建议大家项目用上。它的作用就是避免重复在每个样式文件中@import导入,在各个css 文件中能够直接使用变量和公共的样式。

webpack家族:webpack、webpack-bundle-analyzer、webpack-cli、webpack-dev-server、webpack-merge、webpackbar

webpack:这个不用描述了吧。。。

webpack-cli:

  • 是使用 webpack的命令行工具,在 4.x 版本之后不再作为 webpack 的依赖了,我们使用时需要单独安装这个工具。

webpack-bundle-analyzer: webpack打包体积分析工具,会让我们知道打包后的文件分别是由哪些文件组成,并且体积是多少,是一款优化分析打包文件的工具

webpack-dev-server:是一个小型的Node.js Express服务器,它使用webpack-dev-middleware来服务于webpack的包,除此自外,它还有一个通过Sock.js来连接到服务器的微型运行时

webpack-merge:

  • 一般情况,我们会把webpack文件分为,webpack.common.js(后面两个js文件共同的内容抽离出来),webpack.pro.js(生产环境独有的内容),webpack.dev.js(开发环境独有的内容)。
  • 此时,我们需要一个方法来合并webpack.common.jswebpack.pro.js变为生产环境的内容,同理commondev也是如此。我们就需要webpack-merge方法了。它的作用如下
const merge = require("webpack-merge");
merge(
    {a : [1],b:5,c:20},
    {a : [2],b:10, d: 421}
)
//合并后的结果
{a : [1,2] ,b :10 , c : 20, d : 421}

从上面的案例,我们可以看出来, 数组内容会合并,基础类型的值会被覆盖,这比较符合我们webpack.common.js有一些plugins:[],webpack.pro.js 也有一些plugins是合并的需要,而不是覆盖。

stylelint 家族: stylelint、、stylelint-config-rational-order...

stylelint:stylelint 用于样式规范检查与修复,支持 .css .scss .less .sss

stylelint-config-prettier:关闭所有不必要的或可能与 Prettier 冲突的规则。

stylelint-config-rational-order:它对你的css样式排序会有要求,具体为

Positioning -- 定位
Box Model -- 盒模型
Typography -- 版式
Visual -- 可见性(显示和隐藏)
Animation -- 动画
Misc -- 其它杂项

.declaration-order {
  /* 1.Positioning 位置属性 */ 
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 10;

  /* 2.Box Model 盒子属性 */
  display: block;
  float: right;
  width: 100px;
  height: 100px;
  margin: 10px;
  padding: 10px;

  /* 3.Typography 文字属性 */
  color: #888;
  font: normal 16px Helvetica, sans-serif;
  line-height: 1.3;
  text-align: center;

  /* 4.Visual 视觉属性 */
  background-color: #eee;
  border: 1px solid #888;
  border-radius: 4px;
  opacity: 1;

  /* 5.Animation Misc 其他 */
  transition: all 1s;
  user-select: none;
}

你不按上面的顺序写css的话,会警告或者报错。

stylelint-order:这个实现的功能也是排序,不过它跟上面的插件的区别是,它按照字母(英文是alpha sort)排序,所以两个插件要配合使用。

stylelint-config-standard:该风格是 Stylelint 的维护者汲取了 GitHub、Google、Airbnb 多家之长生成的一套css风格规则。

stylelint-declaration-block-no-ignored-properties:这个插件的作用是警告那些不起作用的属性。比如说你设置了display:inline,width: 200px,其实这里的width是不起作用的,此时这个插件就会发出警告

chalk

打印有颜色文字的插件:用法比如说

// 控制台打印红色的hello
require('chalk').red('hello')

clean-webpack-plugin

webpack使用的插件,一般用在production环境,用来清除文件夹用的,就是类似rm -rf ./dist

conventional-changelog-cli、@commitlint/cli、@commitlint/config-conventional

commitlint 可以帮助我们进行 git commit 时的 message 格式是否符合规范,conventional-changelog 可以帮助我们快速生成 changelog

@commitlint/config-conventional 类似 eslint 配置文件中的 extends ,它是官方推荐的 angular 风格的 commitlint 配置

copy-webpack-plugin

在webpack中拷贝文件和文件夹

cross-env

它是运行跨平台设置和使用环境变量(Node中的环境变量)的脚本。因为在windows和linux|mac里设置环境变量的方法不一致,比如说

// 在windows系统上,我们使用:
"SET NODE_ENV=production && webpack --config build/webpack.config.js"
// 在Lunix系统和安装并使用了bash的windows的系统上,我们会使用:
"EXPORT  NODE_ENV=production && webpack --config build/webpack.config.js"

mini-css-extract-plugin、css-minimizer-webpack-plugin

webpack 4.0以后,官方推荐使用mini-css-extract-plugin插件来打包css文件(从css文件中提取css代码到单独的文件中,对css代码进行代码压缩等)

相对的,如果你不想提取css,可以使用style-loader,将css内嵌到html文件里。

使用方法和效果如下:(后面会在webpack配置文件分析里看到),

先举一个基础配置的例子。 webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css'
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' // postcss-loader 可选
        ],
      },{
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader, 'css-loader','postcss-loader','less-loader' // postcss-loader 可选
        ],
      }
    ],
  },
};
  • 实战案例

基于以上配置

  • 如果入口 app.js 中引用了 Root,js
  • Root引入了 Topics.js
  • Root.js 中引用样式 main.css
  • Topics.js 中引用了 topics.css
// 入口文件 app.js
import Root from './components/Root'

// Root.js
import '../styles/main.less'
import Topics from './Topics'

// Topics.js
import "../styles/topics.less"

这种情况下,Topics 会和 Root 同属一个 chunk,所以会一起都打包到 app.js 中, 结果就是 main.less 和 topics.less 会被提取到一个文件中:app.css。而不是生成两个 css 文件。

            Asset       Size  Chunks                    Chunk Names
          app.css  332 bytes       1  [emitted]         app
           app.js    283 KiB       1  [emitted]  [big]  app
  • 代码情景二

但是,如果 Root.js 中并没有直接引入 Topics 组件,而是配置了代码分割 ,比如模块的动态引入(也就是说你的topics模块,是impot()动态引入的),那么结果就不一样了:

            Asset       Size  Chunks                    Chunk Names
          app.css  260 bytes       1  [emitted]         app
           app.js    281 KiB       1  [emitted]  [big]  app
 topics.bundle.js   2.55 KiB       4  [emitted]         topics
       topics.css   72 bytes       4  [emitted]         topics

因为这个时候有两个 chunk,对应了两个 JS 文件,所以会提取这两个 JS 文件中的 CSS 生成对应的文件。这才是“为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件”的真正含义。

  • 情景三

但是,如果分割了 chunk,还是只希望只生成一个 CSS 文件怎么办呢?也是可以做到的。但需要借助 Webpack 的配置 optimization.splitChunks.cacheGroups

先来看看配置怎么写的:

optimization: {
  splitChunks: {
    cacheGroups: {
      // Extracting all CSS/less in a single file
      styles: {
      	name: 'styles',
        test: /\.(c|le)ss$/,
        chunks: 'all',
        enforce: true,
      },
    }
  }
},

打包结果:

            Asset       Size  Chunks                    Chunk Names
           app.js    281 KiB       2  [emitted]  [big]  app
 styles.bundle.js  402 bytes       0  [emitted]         styles
       styles.css  332 bytes       0  [emitted]         styles
 topics.bundle.js   2.38 KiB       5  [emitted]         topics

继续加强上面的配置,压缩上面分理处的代码, css-minimizer-webpack-plugin是用来压缩分离出来的css的。使用方法如下:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
      },
    ],
  },
  optimization: {
    minimizer: [
      // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
};

detect-port-alt

这个包用来检测对应端口是否被占用,比如项目里发现启动3000端口被占用的话就+1,直到选择一个不被占用的端口(端口上限是65535)。

error-overlay-webpack-plugin

它提供了和 create-react-app 一样的错误遮罩:

用法如下:

const ErrorOverlayPlugin = require('error-overlay-webpack-plugin')

module.exports = {
  plugins: [new ErrorOverlayPlugin()],
  devtool: 'cheap-module-source-map', // 'eval' is not supported by error-overlay-webpack-plugin
}

image.png

@typescript-eslint/eslint-plugin、@typescript-eslint/parser

@typescript-eslint/parser:ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码

@typescript-eslint/eslint-plugin:这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范

配置如下所示:

module.exports = {
    parser:  '@typescript-eslint/parser', // 定义ESLint的解析器
    extends: ['plugin:@typescript-eslint/recommended'],// 定义文件继承的子规范
    plugins: ['@typescript-eslint'],// 定义了该eslint文件所依赖的插件
    env:{                          // 指定代码的运行环境
        browser: true,
        node: true,
    }                               
}

fork-ts-checker-webpack-plugin

它在一个单独的进程上运行类型检查器,该插件在编译之间重用抽象语法树,并与TSLint共享这些树。可以通过多进程模式进行扩展,以利用最大的CPU能力。

html-webpack-plugin

这个插件非常常用,几乎是必备的。

它的作用是:当使用 webpack打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中。简单实用如下(讲webpack文件时会更详细介绍api):

{
  entry: 'index.js',
  output: {
    path: __dirname + '/dist', 
    filename: 'bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'My App', 
      filename: 'assets/admin.html'  // 在  output.path 目录下生成 assets/admin.html 文件
    })
  ]
}

husky、lint-staged

husky是一个npm包,安装后,可以很方便的在package.json配置git hook 脚本 。

比如,在 package.json 内配置如

 "scripts": {
    "lint": "eslint src"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint"
    }
  },

那么,在后续的每一次git commit 之前,都会执行一次对应的 hook 脚本npm run lint 。其他hook同理.

  • lint-staged

如果我们 想对git 缓存区最新改动过的文件进行以上的格式化和 lint 规则校验,这就需要 lint-staged了 。

如下:

{
    "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
    }
  },
  "lint-staged": {
    // 首先,我们会对暂存区后缀为 `.ts .tsx .js` 的文件进行 eslint 校验,
    // --config 的作用是指定配置文件。
    "*.{ts,tsx,js}": [
      "eslint --config .eslintrc.js"
    ],
    // 同理 是stylelint的校验
    "*.{css,less,scss}": [
      "stylelint --config .stylelintrc.js"
    ],
    // prettier格式化
    "*.{ts,tsx,js,json,html,yml,css,less,scss,md}": [
      "prettier --write"
    ]
  },
}

这里没有添加 --fix 来自动修复不符合规则的代码,因为自动修复的内容对我们不透明,这样不太好。

terser-webpack-plugin

是一个使用 terser 压缩jswebpack 插件。

如果你使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

简易用法如下,详细介绍留到后面webpack配置文件详解

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

module.exports = {
  optimization: {
    minimizer: [new TerserPlugin(
      parallel: true   // 多线程
    )],
  },
};

package.json里的其它比较重要的字段

{
  "main": "index.js",
  "scripts": {
    "start": "cross-env NODE_ENV=development node scripts/server",
    "build": "cross-env NODE_ENV=production webpack --config ./scripts/config/webpack.prod.js",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "lint": "npm run lint-eslint && npm run lint-stylelint",
    "lint-eslint": "eslint -c .eslintrc.js --ext .ts,.tsx,.js src",
    "lint-stylelint": "stylelint --config .stylelintrc.js src/**/*.{less,css,scss}"
  },
  "browserslist": [">0.2%", "not dead", "ie >= 9", "not op_mini all"],
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint --config .commitlintrc.js -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.{ts,tsx,js}": ["eslint --config .eslintrc.js"],
    "*.{css,less,scss}": ["stylelint --config .stylelintrc.js"],
    "*.{ts,tsx,js,json,html,yml,css,less,scss,md}": ["prettier --write"]
  }
}

这里的main需要跟一些其它字段来一起比较。比如browser,module,main三个字段都可以出现在package.json中,它们有什么区别呢?

我们直接说结论,具体详细分析,详情参考这篇文章开发插件package.json在webpack构建中的表现

结论

  • webpack 选择 web 浏览器环境
  • 插件的 package.json 是否配置了 browser 字段
    • 存在:选择 browser 作为入口
    • 不存在:
    • 插件的 package.json 是否配置了 module 字段
      • 存在:选择 module 作为入口
      • 不存在:以 main 作为入口
  • webapack 选择 node环境
    • 插件的 package.json 是否配置了 module 字段
      • 存在:选择 module 作为入口
      • 不存在:以 main 作为入口

根据上面的行为总结,我们在开发插件的时候,需要考虑插件是提供给web环境还是node环境,如果都涉及到且存在区别,就要明确指出 browser、module 字段。如果没有任何区别的话,使用 main 入口足以

.vscode中settings文件

这个文件对于使用vscode的用户比较重要,有一些设置非常棒,比如点击ctrl+s自动格式化你的文件,设置如下:

   "editor.formatOnSave": true, // 自动格式化代码,我们使用的是prettier
   "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true, // 保存自动修复 eslint报错(有些报错必须手动修复)
    "source.fixAll.stylelint": true // 保存自动修复 stylelint报错,也就是css报错
  }

下图是具体的settings文件,逐一注释其中的作用

{
  "css.validate": false, // 禁用vscode本身的css校验功能
  "less.validate": false, // 禁用vscode本身的less校验功能
  "scss.validate": false, // 禁用vscode本身的scss校验功能

  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], // eslint 校验的文件格式
  "search.exclude": { // 搜索文件时排除的文件夹
    "**/node_modules": true,
    "dist": true,
    "build": true
  },
  "editor.formatOnSave": true,  // 保存时,自动格式化
  "editor.codeActionsOnSave": { // 保存时自动格式化eslint的规则和stylint的规则
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
  },
  "[javascript]": { // 底下类似是指校验js,jsx,ts,tsx的校验器是prettier,而不是vscode默认的
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

上面的保存时自动格式化eslint的规则和stylint的规则,需要注意的是,有些规则是必须手动修改的,不会自动保存格式化。

babelrc文件解析

下面的presets和plugins的区别是,

  • presets是一些预设,插件的对应名字是babel-preset-xxxBabel插件一般尽可能拆成小的力度,开发者可以按需引进。但是一个一个引进有时候很麻烦,能不能把一些常用的插件打成一个包给我们用呢,这就是presets的作用和。

  • plugins就是一个一个的插件集合,你要配特定的功能就可以加入到plugins中 以下的所有插件之前都介绍过,可以试着回忆一下哦

module.exports = {
  presets: [
    [
      '@babel/preset-env',  // 将基于你的实际浏览器及运行环境,自动的确定babel插件及polyfill
      {
        useBuiltIns: 'usage', // 按需使用
        modules: false, // 意思是不转义import语法,主要是为了tree-shaking
      },
    ],
    '@babel/preset-react', // 转化js、jsx文件的插件集合
    '@babel/preset-typescript', // 转化ts,tsx文件的插件集合
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',// 优化polyfill的插件
      {
        corejs: {
          version: 3,
          proposals: true,
        },
      },
    ],
  ],
};

这里详细解释一下@babel/preset-env这个插件的详细的常见使用参数,因为它很重要,是babel转义我们代码的关键插件:

  • targets属性,最常见的是
    • targets.node : 它可以指定编译当前node版本,或者 "node": true 或者 "node": "current", 它与 "node": process.versions.node 相同。
    • targets.browsers:可以利用 browserslist 查询选择的浏览器 (例如: last 2 versions, > 5%) 但是这里不建议把browsers信息写在eslinttc里面,因为可能其他的插件也需要浏览器信息,最好写在package.json中。 例如:
"browserslist": [">0.2%", "not dead", "ie >= 9", "not op_mini all"],
  • modules属性,如果是false,就是说导出方式是按es6 module,默认是commonjs规范

  • useBuiltIns:规定如何引入polyfill,比如说有些浏览器不支持promise,我们需要引入polyfill去兼容这些不支持promise的浏览器环境

    • 值为usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加,并且使用了useBuiltIns: 'usage'之后,就不必手动在入口文件中import '@babel/polyfill'`
    • 值为 entry 配置项时, 根据target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill
    • corejs选项, 这个选项只会在与useBuiltIns: usage或者useBuiltIns: entry一起使用时才会生效, 确保@babel/preset-env为你的core-js版本注入了正确的引入

接着,我们解释一下在'@babel/plugin-transform-runtime'插件的配置:

  • corejs: 比如['@babel/plugin-transform-runtime', { corejs: 2 }],指定一个数字将引入corejs来重写需要polyfillAPIhelpers,这需要使用@babel/runtime-corejs2作为依赖

技术细节:transform-runtime转换器插件会做三件事:

  • 当你使用generators/async函数时,自动引入@babel/runtime/regenerator(可通过regenerator选项切换)
  • 若是需要,将使用core-js作为helpers,而不是假定用户已经使用了polyfill(可通过corejs选项切换)
  • 自动移除内联的 Babel helpers并取而代之使用@babel/runtime/helpers模块(可通过helpers选项切换)

最后,我们要提一个问题,就是import 通过webpack转义之后,变成了什么样子,我们用案例来说。

如下是一个非常简单的webpack编译的模块。

import { tmpPrint } from './tmp.js'
export function print () {
  tmpPrint() 
  console.log('我是 num.js 的 print 方法')
}

会被webpack编译为以路径为key,以函数为value的对象。

{
"./src/num.js":
      (function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, "print", function () { return print; });
        // 加载 ./src/tmp.js 模块
        var _tmp_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/tmp.js");
        function print() {
          Object(_tmp_js__WEBPACK_IMPORTED_MODULE_0__["tmpPrint"])()
          console.log('我是 num.js 的 print 方法')
        }
        //# sourceURL=webpack:///./src/num.js?");
      }),
}

我们接着看一下__webpack_require__.r,webpack_require.d,__webpack_require__分别是什么:

function __webpack_require__(moduleId) {
    // 所有模块都会被缓存,如果在缓存里就直接从缓存里拿
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 这里是缓存的定义,i是id的意思,l是load的意思,exports是导出的内容
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // 如果不是在缓存里,取出模块把module, module.exports, __webpack_require__作为参数放入到模块里
    // 如下的modules[moduleId]中保存的内容就相当于这块内容:
    // (function (module, __webpack_exports__, __webpack_require__) {
    //    "use strict";
    //    __webpack_require__.r(__webpack_exports__);
    //    __webpack_require__.d(__webpack_exports__, "print", function () { return // print; });
        // 加载 ./src/tmp.js 模块
       // var _tmp_js__WEBPACK_IMPORTED_MODULE_0__ = // __webpack_require__("./src/tmp.js");
        // function print() {
         //  Object(_tmp_js__WEBPACK_IMPORTED_MODULE_0__["tmpPrint"])()
         // console.log('我是 num.js 的 print 方法')
       // }
        //# sourceURL=webpack:///./src/num.js?");
     // }),
// }

    
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    这下是不是懂了,很简单啊这个函数!就是加载和暴露模块用的
    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }
  
  
 __webpack_require__.d = function (exports, name, getter) {
     // __webpack_require__.o这个函数的意思是检查name是否是exports的属性
    if (!__webpack_require__.o(exports, name)) {
      // 如果exports原本没有name属性,就用defineProperty去定义name属性
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

   // 这个函数就是用来标识是否是es模块的
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };

好了,我们接着回头接续分析那个src/sum.js的模块

{
"./src/num.js":
// 大家还记得上面讲require有一句
// modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 我们可以看到 module就等于下面函数的 module,代表这个模块对象
// module.exports对应__webpack_exports__,也就是__webpack_exports__代表module导出的对象
// __webpack_require__对应function的 __webpack_require__参数,也就是导入模块函数
      (function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        // 这句话的意思把导出的exports对象标记为esmodule
        __webpack_require__.r(__webpack_exports__);
        // 这句话的意思是把模块下面的print函数,放入exports导出对象
        __webpack_require__.d(__webpack_exports__, "print", function () { return print; });
        // 加载 ./src/tmp.js 
        var _tmp_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/tmp.js");
        function print() {
          Object(_tmp_js__WEBPACK_IMPORTED_MODULE_0__["tmpPrint"])()
          console.log('我是 num.js 的 print 方法')
        }
        //# sourceURL=webpack:///./src/num.js?");
      }),
}

经过上面分析,发现最关键的一句就是__webpack_require__.r(webpack_exports),把导出对象标记为esModule,如果你没有用import,而是用commonjs的require,那么就不会有这一句

那问题又来了,如果是表示esmodule了,有啥用啊!这部分我就不写了,要不就是一篇专门讲import和require区别的文章了。

我直接说结论了

  • 如果是import Header form './Header',在webpack里会转译为类似
require('./Header').default
  • 如果是import * as Header form './Header',在webpack里会转译为类似
const Header = require('./Header')
Header.default 表示导出的Header组件
Header.A代表导出的A

// Header.js
export default Header;
export const A=1
  • 如果是import { A } form './Header',在webpack里会转译为类似
require('./Header').A
  • export default Header 会被挂在exports的default属性上
  • export const A=1,会被挂在exports的A属性上

意思是es6模块实际上被webpack的一套规则还是变味了commonjs规范而已。

上面没看懂?没关系的,更具体更清晰的推论,在tsconfig.js文件的esModuleInterop参数讲解中会有更清晰的解释(这个是站在webpack编译的角度,下面esModuleInterop参数是在ts编译的角度,其实原理都是一样的)。

以下比较简单的文件,我就在文件注释中解释参数了

.commitlintrc文件解析

如下的rules的规范如下:rulename和配置数组组成,如:'name:[0, 'always', 72]',数组中第一位为level,可选0,1,20disable1warning2error,第二位为应用与否,可选always|never,第三位该rule的值,下面的值代表你的commit开头必须是这些字段

module.exports = {
  extends: ['@commitlint/config-conventional'], // 这个插件继承的是angular团队的提交规范
  rules: {
    'type-enum': [ // 解释上面已经提过数组每一位的意思
      2,
      'always',
      ['build', 'ci', 'chore', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
    ],
  },
};

.editorconfig文件分析

// 表明是最顶层的配置文件,发现设为 true 时,才会停止查找.editorconfig 文件
root = true

[*]
// tab 为 hard-tabs,space 为 soft-tabs 表示缩进符号,我们选的空格
indent_style = space
// 设置整数表示规定每级缩进的列数和 soft-tabs 的空格数。
// 如果设定为 tab,则会使用 tab_width 的值(如果已指定)
indent_size = 2
// 定义换行符,支持 lf、cr 和 crlf
end_of_line = lf
// 编码格式,支持 latin1、utf-8、utf-8-bom、utf-16be 和 utf-16le,不建议使用 uft-8-bom
charset = utf-8
// 设为 true 表示会除去换行行首的任意空白字符,false 反之
trim_trailing_whitespace = true
// 设为 true 表明使文件以一个空白行结尾,false 反之
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

.eslintrc文件分析

const OFF = 0;
const WARN = 1;
const ERROR = 2;

module.exports = {
  // 要在配置文件中指定环境,请使用env键并通过将每个设置为来指定要启用的环境true。
  // 例如,以下启用浏览器和Node.js环境:
  // es6表示对于新的ES6全局变量,比如Set的支持,注意跟下面parserOptions的ecmaVersion对比一下
  // ecmaVersion: 6 表示启用对于ES6语法的校验
  //
  env: {
    browser: true,
    es6: true,
    node: true,
  },
  // 
  extends: [
    'airbnb',
    'airbnb/hooks',
    'plugin:react/recommended',
    'plugin:unicorn/recommended',
    'plugin:promise/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  // 指定解析器
  // 默认情况下,ESLint使用Espree作为其解析器。您可以选择指定在配置文件中使用其他解析器
  parser: '@typescript-eslint/parser',
  // parserOptions属性在文件中设置解析器选项
  // ecmaVersion-设置为3、5(默认),6、7、8、9、10或11,以指定要使用的ECMAScript语法的版本。
  // sourceType-设置为"script"(默认),或者"module"代码在ECMAScript模块中。
  // ecmaFeatures -一个对象,指示您要使用哪些其他语言功能,参数如下
  //   globalReturn-允许return在全局声明
  //   impliedStrict-启用全局严格模式(如果ecmaVersion大于等于5)
  //   jsx-启用JSX
  parserOptions: {
    ecmaFeatures: {
      impliedStrict: true,
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['react', 'unicorn', 'promise', '@typescript-eslint', 'prettier'],
  settings: {
     // 这里的improt/resolver针对插件是eslint-import-resolver-xxx
     // 比如下面的typescript里的规则,针对的就是插件eslint-import-resolver-typescript
     // 再下面的node就是配置eslint-import-resolver-node
     // 有人说我们没依赖eslint-import-resolver-node,哪里来的呢,是因为
     // eslint-import-plugin插件依赖,所以也安装了它,这就要涉及到npm依赖包平铺的规则了,下面会讲
    'import/resolver': {
      typescript: {
        directory: './tsconfig.json', // 这里主要解决的是别名的问题,tsconfig.json里有别名设置
      },
      node: {
        extensions: ['.tsx', '.jsx', '.ts', '.js'],
      },
    },
  },
  rules: {
    // 以下规则就不详细讲了,因为很多都是因为typescript插件bug跟eslint冲突不得不关闭一些规则
    'import/extensions': [
      ERROR,
      'ignorePackages',
      {
        ts: 'never',
        tsx: 'never',
        js: 'never',
      },
    ],
    'import/no-extraneous-dependencies': [ERROR, { devDependencies: true }],
    'import/prefer-default-export': OFF,
    'import/no-unresolved': ERROR,
    'import/no-dynamic-require': OFF,

    'unicorn/better-regex': ERROR,
    'unicorn/prevent-abbreviations': OFF,
    'unicorn/filename-case': [
      ERROR,
      {
        cases: {
          // 中划线
          kebabCase: true,
          // 小驼峰
          camelCase: true,
          // 下划线
          snakeCase: false,
          // 大驼峰
          pascalCase: true,
        },
      },
    ],
    'unicorn/no-array-instanceof': WARN,
    'unicorn/no-for-loop': WARN,
    'unicorn/prefer-add-event-listener': [
      ERROR,
      {
        excludedPackages: ['koa', 'sax'],
      },
    ],
    'unicorn/prefer-query-selector': ERROR,
    'unicorn/no-null': OFF,
    'unicorn/no-array-reduce': OFF,

    '@typescript-eslint/no-useless-constructor': ERROR,
    '@typescript-eslint/no-empty-function': WARN,
    '@typescript-eslint/no-var-requires': OFF,
    '@typescript-eslint/explicit-function-return-type': OFF,
    '@typescript-eslint/explicit-module-boundary-types': OFF,
    '@typescript-eslint/no-explicit-any': OFF,
    '@typescript-eslint/no-use-before-define': ERROR,
    '@typescript-eslint/no-unused-vars': WARN,
    'no-unused-vars': OFF,

    'react/jsx-filename-extension': [ERROR, { extensions: ['.tsx', 'ts', '.jsx', 'js'] }],
    'react/jsx-indent-props': [ERROR, 2],
    'react/jsx-indent': [ERROR, 2],
    'react/jsx-one-expression-per-line': OFF,
    'react/destructuring-assignment': OFF,
    'react/state-in-constructor': OFF,
    'react/jsx-props-no-spreading': OFF,
    'react/prop-types': OFF,

    'jsx-a11y/click-events-have-key-events': OFF,
    'jsx-a11y/no-noninteractive-element-interactions': OFF,
    'jsx-a11y/no-static-element-interactions': OFF,

    'lines-between-class-members': [ERROR, 'always'],
    // indent: [ERROR, 2, { SwitchCase: 1 }],
    'linebreak-style': [ERROR, 'unix'],
    quotes: [ERROR, 'single'],
    semi: [ERROR, 'always'],
    'no-unused-expressions': WARN,
    'no-plusplus': OFF,
    'no-console': OFF,
    'class-methods-use-this': ERROR,
    'jsx-quotes': [ERROR, 'prefer-single'],
    'global-require': OFF,
    'no-use-before-define': OFF,
    'no-restricted-syntax': OFF,
    'no-continue': OFF,
  },
};

上面提到一个重要的点,就是eslint-import-resolver-node,我们并没有在package.json声明,咋node_modules里面就有它了呢?入下

- node_modules
 - eslint-import-resolver-node // 为啥没有安装它,它确在第一层

这就涉及到npm安装依赖包的规则了,因为eslint-import-plugin依赖eslint-import-resolver-node,所以,node_modules里面就会有,我们就简单讲一下npm包安装(install)规则。

这问题曾经我面试的时候也遇到过,接下来我们简单了解一下:

嵌套结构

我们都知道,执行 npm install 后,依赖包被安装到了 node_modules,在 npm 的早期版本, npm 处理依赖的方式简单粗暴,以递归的形式,严格按照 package.json 结构以及子依赖包的 package.json 结构将依赖安装到他们各自的 node_modules 中。直到有子依赖包不在依赖其他模块。也就是说,假如你的package.json如下

{
     A模块:"1.0.0",
     B模块:"1.0.0"
}

然后B模块有依赖C模块,B模块的package.json如下

{
    C模块:"1.0.0"
}

那么整个项目依赖就是嵌套的,如下:

node_modules
 - A模块
 - B模块
  - C模块

在 Windows 系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。

扁平结构

为了解决以上问题,NPM 在 3.x 版本做了一次较大更新。其将早期的嵌套结构改为扁平结构:

安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在 node_modules 根目录。

还是上面的依赖结构,我们在执行 npm install 后将得到下面的目录结构:

node_modules
 - A模块
 - B模块
 - C模块

当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。

就是说假如C模块依赖A模块的2.0.0版本,依赖图如下:

node_modules
 - A模块 1.0.0
 - B模块
 - C模块
    - A模块 2.0.0

其实铺平的结构也会有问题,我们这里就不详述了,上面提到的那篇文章真的不错,推荐详细看,里面设置npm相关知识的点比这里谈到的多得多。

.npmrc文件分析

image.png

.prettierrc文件分析

{
  // tab缩进大小,默认为2
  tabWidth: 2,
  // 使用tab缩进,默认false
  useTabs: true,
  // 使用分号, 默认true
  semi: false,
  // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)
  singleQuote: true,
  // 行尾逗号,默认none,可选 none|es5|all
  // es5 包括es5中的数组、对象
  // all 包括函数对象等所有可选
  TrailingCooma: "none",
  // 对象中的空格 默认true
  // true: { foo: bar }
  // false: {foo: bar}
  bracketSpacing: true,
  // JSX标签闭合位置 默认false
  // false: <div
  //          className=""
  //          style={{}}
  //       >
  // true: <div
  //          className=""
  //          style={{}} >
  jsxBracketSameLine:false,
  // 箭头函数参数括号 默认avoid 可选 avoid| always
  // avoid 能省略括号的时候就省略 例如x => x
  // always 总是有括号
  arrowParens: 'always', 
}

.stylelintrc文件分析

module.exports = {
// stylelint的配置可以在已有配置的基础上进行扩展,之后你自己书写的配置项将覆盖已有的配置。
// 配置的含义,我们前面已经讲过简单提一下stylelint-config-rational-order是配置书写顺序的
  extends: ['stylelint-config-standard', 'stylelint-config-rational-order', 'stylelint-config-prettier'],
  // plugins一般是由社区提供的,对stylelint已有规则进行扩展
  // 也就说有些规则原本stylelint没有,就要插件自定义规则了
  // 'stylelint-declaration-block-no-ignored-properties'这个插件的作用是警告那些不起作用的属性
  plugins: ['stylelint-order', 'stylelint-declaration-block-no-ignored-properties', 'stylelint-scss'],
  rules: {
  // rules不详述了,可以访问这个网站搜寻,https://stylelint.docschina.org/user-guide/plugins/
    'plugin/declaration-block-no-ignored-properties': true,
    'comment-empty-line-before': null,
    'declaration-empty-line-before': null,
    'function-name-case': 'lower',
    'no-descending-specificity': null,
    'no-invalid-double-slash-comments': null,
    'block-no-empty': null,
    'value-keyword-case': null,
    'rule-empty-line-before': ['always', { except: ['after-single-line-comment', 'first-nested'] }],
    'at-rule-no-unknown': null,
    'scss/at-rule-no-unknown': true,
  },
  // 忽略校验的文件,其中/**/*是glob语法,指的是所有文件和文件夹
  ignoreFiles: ['node_modules/**/*', 'build/**/*', 'dist/**/*'],
};

tsconfig.json文件分析

为什么使用 tsconfig.json?

通常我们可以使用 tsc 命令来编译少量 TypeScript 文件, 但如果实际开发的项目,很少是只有单个文件,当我们需要编译整个项目时,就可以使用 tsconfig.json 文件,将需要使用到的配置都写进 tsconfig.json 文件

{
   // 编译选项,跟编译ts相关
  "compilerOptions": {
    // 指定编译的ECMAScript目标版本。
    // 枚举值:"ES3", "ES5", "ES6"/ "ES2015", "ES2016", "ES2017","ESNext"。
    // 默认值: “ES3”,ESNext包含提案的内容
    "target": "ES5",
    // 指定生成哪个模块系统代码。枚举值:"None", "CommonJS", "AMD", "System", "UMD",
    // "ES6", "ES2015","ESNext"。默认值根据--target选项不同而不同,当target设置为ES6时,
    // 默认module为“ES6”,否则为“commonjs”
    "module": "ESNext",
    // 编译过程中需要引入的库文件的列表。比如没有esnext,Set、Reflect等api会被ts报错
    "lib": ["dom", "dom.iterable", "esnext"],
    // 是否允许编译javascript文件。如果设置为true,js后缀的文件也会被typescript进行编译
    "allowJs": true,
    // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "jsx": "react",
    // 下面详解 
    "isolatedModules": true,
    // 用于指定是否启用严格的类型检查,不过到底具体怎么严格我也不知道
    "strict": true,
    // 下面详解
    "moduleResolution": "node",
    // 下面详解
    "esModuleInterop": true,
    "resolveJsonModule": true,
    // 下面详解
    "baseUrl": "./",
    // 路径别名,跟webpack alias一样,注意你是ts的话,必须webpack和ts都配
    "paths": {
      "Src/*": ["src/*"],
      "Components/*": ["src/components/*"],
      "Utils/*": ["src/utils/*"]
    },
    
    // 以下两个是跟装饰器功能有关,experimentalDecorators是 是否开启装饰器
    // emitDecoratorMetadata是装饰器里的一个功能,如果你使用依赖注入,有可能需要开启它
    // 依赖注入不懂的同学可以略过,后面会写一篇关于学习nestjs前置知识的文章
    // 会讲怎么使用emitDecoratorMetadata实现依赖注入
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,

    // 禁止对同一个文件的不一致的引用。主要是文件大小写必须一致,比如引用a.js和A.js是不一样的
    "forceConsistentCasingInFileNames": true,
    // 忽略所有的声明文件( `*.d.ts`)的类型检查
    "skipLibCheck": true,
    // 下面详解
    "allowSyntheticDefaultImports": true,
    // 不生成输出文件
    "noEmit": true
  },
  "exclude": ["node_modules"]
}

上面的jsx选项可以有三个值选择,我们详细解释一下:

jsx可选项包括:preserve, react 和 react-native。

这些模式仅影响编译阶段 - 类型检查不受影响。

  • preserve模式将保持JSX作为输出的一部分,又后面的编译器继续编译(例如Babel)。 此外,输出将具有.jsx文件扩展名。
  • react模式将编译React.createElement,在使用之前不需要经过JSX转换,输出将具有.js文件扩展名。
  • react-native模式相当于保留,因为它保留了所有JSX,但输出将具有.js文件扩展名

isolatedModules,这个选项有点复杂,查阅了不少资料。。。下面详细讲一下:

  • 导出非值标识符

在 TypeScript 中,你可以引入一个类型,然后再将其导出:

import { someType, someFunction } from "someModule";


someFunction();


export { someType, someFunction };
Try

由于 someType 并没有值,所以生成的 export 将不会导出它(否则将导致 JavaScript 运行时的错误):

export { someFunction };

单文件转译器并不知道 someType 是否会产生一个值,所以导出一个只指向类型的名称会是一个错误。

  • 非模块文件

如果设置了 isolatedModules,则所有的实现文件必须是模块 (也就是它有某种形式的 import/export)。如果任意文件不是模块就会发生错误:

function fn() {}
'index.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.'index.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.Try

此限制不适用于 .d.ts 文件

  • 指向 const enum 成员

在 TypeScript 中,当你引用一个 const enum 的成员时,该引用在生成的 JavaScript 中将会被其实际值所代替。这会将这样的 TypeScript 代码:

declare const enum Numbers {
  Zero = 0,
  One = 1,
}
console.log(Numbers.Zero + Numbers.One);

转换为这样的 JavaScript:

"use strict";
console.log(0 + 1);

在不知道这些成员值的情况下,其他转译器不能替换对 Numbers 的引用。如果无视的话则会导致运行时错误(运行时没有 Numbers) 对象。 正因如此,当启用 isolatedModules 时,引用环境中的 const enum 成员将会是一个错误

  • moduleResolution (参考《tsconfig详细配资》,详见文章底部) 可选值: classic | node

我们举一个例子,看看两种模式的工作机制,假设用户主目录下有一个ts-test的项目,里面有一个src目录,src目录下有一个a.ts文件,即/Users/**/ts-test/src/a.ts

  • classic模块解析规则:
    • 对于相对路径模块: 只会在当前相对路径下查找是否存在该文件(.ts文件),不会作进一步的解析,如"./src/a.ts"文件中,有一行import { b } from "./b",那么其只会检测是否存在"./src/b.ts",没有就算找不到。

    • 对于非相对路径模块: 编译器则会从包含导入文件的目录开始依次向上级目录遍历尝试定位匹配的ts文件或者d.ts类型声明文件,如果/Users/**/ts-test/src/a.ts文件中有一行import { b } from "b",那么其查找过程如下:

/Users/**/ts-test/src/b.ts
/Users/**/ts-test/src/b.d.ts
/Users/**/ts-test/b.ts
/Users/**/ts-test/b.d.ts
/Users/**/b.ts
/Users/**/b.d.ts
/Users/b.ts
/Users/b.d.ts
/b.ts
/b.d.ts
  • node模块解析规则:
    • 对于相对路径模块:除了会在当前相对路径下查找是否存在该文件(.ts文件)外,还会作进一步的解析,如果在相对目录下没有找到对应的.ts文件,那么就会看一下是否存在同名的目录
    • 如果有,那么再看一下里面是否有package.json文件,然后看里面有没有配置,main属性
    • 如果配置了,则加载main所指向的文件(.ts或者.d.ts),如果没有配置main属性,那么就会看一下目录里有没有index.ts或者index.d.ts,有则加载。
    • 对于非相对路径模块: 对于非相对路径模块,那么会直接到a.ts所在目录下的node_modules目录下去查找,也是遵循逐层遍历的规则,查找规则同上,同上node模块解析规则查找如下(一般情况下是找):
/Users/**/ts-test/src/node_modules/b.ts
/Users/**/ts-test/src/node_modules/b.d.ts
/Users/**/ts-test/src/node_modules/b/package.json(如果指定了main)
/Users/**/ts-test/src/node_modules/b/index.ts
/Users/**/ts-test/src/node_modules/b/index.d.ts

/Users/**/ts-test/node_modules/b.ts
/Users/**/ts-test/node_modules/b.d.ts
/Users/**/ts-test/node_modules/b/package.json(如果指定了main)
/Users/**/ts-test/node_modules/index.ts
/Users/**/ts-test/node_modules/index.d.ts

/Users/**/node_modules/b.ts
/Users/**/node_modules/b.d.ts
/Users/**/node_modules/b/package.json(如果指定了main)
/Users/**/node_modules/index.ts
/Users/**/node_modules/index.d.ts

/Users/node_modules/b.ts
/Users/node_modules/b.d.ts
/Users/node_modules/b/package.json(如果指定了main)
/Users/node_modules/index.ts
/Users/node_modules/index.d.ts

/node_modules/b.ts
/node_modules/b.d.ts
/node_modules/b/package.json(如果指定了main)
/node_modules/index.ts
/node_modules/index.d.ts

以上需要注意一点的是,还有一个typeRoots属性,默认是node_modules/@types,并且不管是classic解析还是node解析,都会到node_modules/@types目录下查找类型声明文件,即typeRoots和模块的解析规则无关

  • baseUrl

这个是用于拓宽引入非相对模块时的查找路径的。其默认值就是"./" ,比如当moduleResolution属性值为node的时候,如果我们引入了一个非相对模块,那么编译器只会到node_modules目录下去查找,但是如果配置了baseUrl,那么编译器在node_modules中没有找到的情况下,还会到baseUrl中指定的目录下查找;

同样moduleResolution属性值为classic的时候也是一样,除了到当前目录下找之外(逐层),如果没有找到还会到baseUrl中指定的目录下查找;就是相当于拓宽了非相对模块的查找路径范围

  • allowSyntheticDefaultImports

当设置为 true, 并且模块没有显式指定默认导出时,allowSyntheticDefaultImports 可以让你这样写导入:

import React from "react";

而不是:

import * as React from "react";

例如:allowSyntheticDefaultImports 不为 true 时:

// @filename: utilFunctions.js
Module '"/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/utilFunctions"' has no default export.Module '"/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/utilFunctions"' has no default export.
const getStringLength = (str) => str.length;


module.exports = {
  getStringLength,
};


// @filename: index.ts
import utils from "./utilFunctions";


const count = utils.getStringLength("Check JS");

这段代码会引发一个错误,因为没有“default”对象可以导入,即使你认为应该有。 为了使用方便,Babel 这样的转译器会在没有默认导出时自动为其创建,使模块看起来更像:

// @filename: utilFunctions.js
const getStringLength = (str) => str.length;
const allFunctions = {
  getStringLength,
};
module.exports = allFunctions;
module.exports.default = allFunctions;

本选项不会影响 TypeScript 生成的 JavaScript,它仅对类型检查起作用。

  • esModuleInterop

这个参数涉及到es6模块和commonjs模块互相转换知识点了。具体参考这篇文章(这一参数就是一篇文章 esModuleInterop 到底做了什么?, 我这里简引用一下这篇文章的关键点。

首先我们看一下import语法在ts中是如何被转译的!

  • TS 默认编译规则

TS 对于 import 变量的转译规则为:

 // before
 import React from 'react';
 console.log(React)
 // after
 var React = require('react');
 console.log(React['default'])


 // before
 import {Component} from 'react';
 console.log(Component);
 // after
 var React = require('react');
 console.log(React.Component)
 

 // before 
 import * as React from 'react';
 console.log(React);
 // after
 var React = require('react');
 console.log(React);

结论,可以看到:

  • 对于 import 导入默认导出的模块,TS 在读这个模块的时候会去读取上面的 default 属性
  • 对于 import 导入非默认导出的变量,TS 会去读这个模块上面对应的属性
  • 对于 import *,TS 会直接读该模块

TS、babel 对 export` 变量的转译规则为:(代码经过简化)

 // before
 export const name = "esm";
 export default {
   name: "esm default",
 };

 // after
 exports.__esModule = true;
 exports.name = "esm";
 exports["default"] = {
   name: "esm default"
 }

可以看到:

  • 对于 export default 的变量,TS 会将其放在 module.exports 的 default 属性上
  • 对于 export 的变量,TS 会将其放在 module.exports 对应变量名的属性上
  • 额外给 module.exports 增加一个 __esModule: true 的属性,用来告诉编译器,这本来是一个 esm 模块

TS 开启 esModuleInterop 后的编译规则

回到标题上,esModuleInterop 这个属性默认为 false。改成 true 之后,TS 对于 import 的转译规则会发生一些变化(export 的规则不会变):

 // before
 import React from 'react';
 console.log(React);
 // after 代码经过简化
 // __importDefault规则如下:
 // 如果目标模块是 esm,就直接返回目标模块;否则将目标模块挂在一个对象的 defalut 上,返回该对象
 var react = __importDefault(require('react'));
 console.log(react['default']);


 // before
 import {Component} from 'react';
 console.log(Component);
 // after 代码经过简化
 var react = require('react');
 console.log(react.Component);
 
 
 // before
 import * as React from 'react';
 console.log(React);
 // after 代码经过简化
 // _importStar 规则如下
 // 如果目标模块是 esm,就直接返回目标模块。否则
 // 将目标模块上所有的除了 default 以外的属性挪到 result 上
 // 将目标模块自己挂到 result.default 上
 var react = _importStar(require('react'));
 console.log(react);

可以看到,对于默认导入和 namespace(*)导入,TS 使用了两个 helper 函数来帮忙

// 代码经过简化
var __importDefault = function (mod) {
  return mod && mod.__esModule ? mod : { default: mod };
};

var __importStar = function (mod) {
  if (mod && mod.__esModule) {
    return mod;
  }

  var result = {};
  for (var k in mod) {
    if (k !== "default" && mod.hasOwnProperty(k)) {
      result[k] = mod[k]
    }
  }
  result["default"] = mod;

  return result;
};

其实这个参数对于我们项目而言没有用,因为@babel/preset-typescript会把类型清除掉,webpack 不会调用 tsctsconfig.json 也会被忽略掉。

但是可以帮助我们拓宽视野,这样面试官让你聊es6模块和commonjs模块转换的话题(cjs 导入 esm (一般不会这样使用,除开这种情况),就会游刃有余

webpack相关配置

首先是工具文件:

env.js

// 判读是否是生产环境,这里这个项目的作者取了一个巧,判断非develop环境是这样的
// process.env.NODE_ENV !== 'production'
// 这样写不要好,有可能你们公司有很多环境,比如还有预发、灰度环境等等
const isDevelopment = process.env.NODE_ENV !== 'production';
const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
  isDevelopment,
  isProduction,
};

path.js

// 以下是两个node模块
const path = require('path');
const fs = require('fs');

// 同步获取node执行的文件的工作目录, 我们的工作目录一般都是项目的根目录,这里就表示根目录
// 为啥这么说呢,因为package.json写着webpack --config ./scripts/config/webpack.prod.js
// webpack就是借助node的能力,它的 ./scripts就暴露是以项目目录为根目录
// 这里需要注意process.cwd和__dirname的区别
// process.cwd()返回当前工作目录。如:调用node命令执行脚本时的目录。
// __dirname返回源代码所在的目录
const appDirectory = fs.realpathSync(process.cwd());

// 获取绝对路径的方法函数
function resolveApp(relativePath) {
  return path.resolve(appDirectory, relativePath);
}

// 默认extentions
const moduleFileExtensions = ['ts', 'tsx', 'js', 'jsx'];

/**
 * Resolve module path
 * @param {function} resolveFn resolve function
 * @param {string} filePath file path
 */
function resolveModule(resolveFn, filePath) {
  // Check if the file exists
  const extension = moduleFileExtensions.find((ex) => fs.existsSync(resolveFn(`${filePath}.${ex}`)));

  if (extension) {
    return resolveFn(`${filePath}.${extension}`);
  }
  return resolveFn(`${filePath}.ts`); // default is .ts
}

module.exports = {
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public'),
  appIndex: resolveModule(resolveApp, 'src/index'), // Package entry path
  appHtml: resolveApp('public/index.html'),
  appNodeModules: resolveApp('node_modules'), // node_modules path
  appSrc: resolveApp('src'),
  appSrcComponents: resolveApp('src/components'),
  appSrcUtils: resolveApp('src/utils'),
  appProxySetup: resolveModule(resolveApp, 'src/setProxy'),
  appPackageJson: resolveApp('package.json'),
  appTsConfig: resolveApp('tsconfig.json'),
  moduleFileExtensions,
};

webpack.common.js

这是webpack生产环境和开发环境共同的配置文件 以下需要特别注意的参数是'css-loader'里有个importLoaders的参数,它的意思是需要举一个例子就明白了,

如下图:importLoader是1 image.png

  • 我们在写sass或者less的时候可以@import去引入其他的sassless文件,此时引用的文件如何被loader处理就跟这个参数有关了。

  • css-loader处理index.scss文件,读取到@import语句的时候, 因为将importLoaders设置为1,那么a.scssb.scss会被postcss-loader给处理

  • 如果将importLoaders设置为2,那么 a.scssb.scss就会被postcss-loadersass-loader给处理

下面的externals属性是一个常见webpack优化点,比如你会把react,react-dom放入cdn,这样就不用打包他们

这里还有一些webpack5webpack4相同功能但配置有些区别的点:

  • 之前使用 file-loader ,但是 webpack5 现在已默认内置资源模块,根据官方配置,现在可以改为以下配置方式,不再需要安装额外插件:
module.exports = {
  output: {
    // ...
    assetModuleFilename: 'images/[name].[contenthash:8].[ext]',
  },
  // other...
  module: {
    rules: [
      // other...
      {
        test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024,
          },
        },
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2?)$/,
        type: 'asset/resource',
      },
    ]
  },
  plugins: [//...],
}

缓存

这里提一个醒dllwebpack里已经过时了!过时了!以后谁给你推荐这个webpack优化就别理他就行了!因为配置hard-source-webpack-plugin都比配置dll容易的多,这还是webpack4的配置。都过时了

之前可以使用插件 hard-source-webpack-plugin 实现缓存,大大加快二次编译速度,现在webpack5现在默认支持缓存,我们只需要以下配置即可:

module.exports = {
  //...
  cache: {
   // 默认type是memory也就是缓存放到内存中
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
  //...
};

cache.buildDependencies,它可以指定构建过程中的代码依赖。它的值类型有两种:文件和目录。

  • 目录类型必须以斜杠(/)结尾。其他所有内容都解析为文件类型。
  • 对于目录类型来说,会解析其最近的 package.json 中的 dependencies。
  • 对于文件类型来说,我们将查看 node.js 模块缓存以寻找其依赖。

如下示例的意思是: __filename 变量指向 node.js 中的当前文件。

cache.buildDependencies: {
    // 它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效
    config: [__filename]
}

注意:当设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。 从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。 写入缓存将同时写入内存缓存和文件系统缓存。也就是说它比memory模式更好

// 插件把 webpack 打包后的静态文件自动插入到 html 文件当中
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 用来分离css为单独的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 添加打包进度条插件
const WebpackBar = require('webpackbar');
// 它在一个单独的进程上运行类型检查器,该插件在编译之间重用抽象语法树,并与TSLint共享这些树。
// 可以通过多进程模式进行扩展,以利用最大的CPU能力。
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
// 在webpack中拷贝文件和文件夹
const CopyPlugin = require('copy-webpack-plugin');
// 引入路径工具,上文已讲
const paths = require('../paths');
// 引入环境判断工具,上文已讲
const { isDevelopment, isProduction } = require('../env');
// 引入配置文件,上文已讲
const { imageInlineSizeLimit } = require('../conf');

// 这个函数是用来加载css相关loader的函数
// 如果是开发环境用style-loader,将css内嵌到html中,反之css单独打包
const getCssLoaders = (importLoaders) => [
  isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
  {
    loader: 'css-loader',
    options: {
      modules: false,
      sourceMap: isDevelopment,
      importLoaders,
    },
  },
  {
    loader: 'postcss-loader',
    options: {
      postcssOptions: {
        plugins: [
          require('postcss-flexbugs-fixes'),
          isProduction && [   // 开发环境不使用postcss-preset-env加浏览器前缀,加快打包时间
            'postcss-preset-env',
            {
              autoprefixer: {
                grid: true,
                flexbox: 'no-2009',
              },
              stage: 3,
            },
          ],
        ].filter(Boolean),
      },
    },
  },
];

module.exports = {
  // 入口信息
  entry: {
    app: paths.appIndex,
  },
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
  // 这里可以设置extensions和别名
  // extensions就是webpack会识别的文件后缀的顺序,
  // 如果你是tsx建议放到第一位,否则你写成['ts','tsx']会先检测是否是ts文件,不是才接着看是不是tsx
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.json'],
    alias: {
      Src: paths.appSrc,
      Components: paths.appSrcComponents,
      Utils: paths.appSrcUtils,
    },
  },
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    axios: 'axios',
  },
  module: {
    rules: [
      {
        test: /\.(tsx?|js)$/,
        loader: 'babel-loader',
        options: { cacheDirectory: true }, // 这是一个webpack优化点,使用缓存
        exclude: /node_modules/, // 这个也是webpack优化的点 exclude排除不需要编译的文件夹
      },
      {
        test: /\.css$/,
        use: getCssLoaders(1),  // 这个讲得就是importLoaders属性运用,上面已经讲了
      },
      {
        test: /\.scss$/,
        use: [
          ...getCssLoaders(2),
          {
            loader: 'sass-loader',
            options: {
              sourceMap: isDevelopment,
            },
          },
        ],
      },
      {
        test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 
        type: 'asset', // webpack5自带的loader,webpack4依赖file-loader
        parser: {
          dataUrlCondition: {
            maxSize: imageInlineSizeLimit,
          },
        },
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2?)$/,
        type: 'asset/resource', // webpack5自带的loader,webpack4依赖file-loader
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ // 这个模块是重点,下面详细讲
      template: paths.appHtml,
      cache: true,
    }),
    new CopyPlugin({ // 这个是复制文件或者目录的插件
      patterns: [
        {
          context: paths.appPublic,
          from: '*',
          to: paths.appBuild,
          toType: 'dir',
          globOptions: {
            dot: true,
            gitignore: true,
            ignore: ['**/index.html'],
          },
        },
      ],
    }),
    // 打包进度条插件
    new WebpackBar({
      name: isDevelopment ? 'RUNNING' : 'BUNDLING',
      color: isDevelopment ? '#52c41a' : '#722ed1',
    }),
    // 插件功能上面已写
    new ForkTsCheckerWebpackPlugin({
      typescript: {
        configFile: paths.appTsConfig,
      },
    }),
  ],
};

  • HtmlWebpackPlugin

    • title

    生成html文件的标题

    • filename

    就是html文件的文件名,默认是index.html

    • template

    指定你生成的文件所依赖哪一个html文件模板,模板类型可以是html、ejs

    如果你设置的 title 和 filename于模板中发生了冲突,那么以你的title 和 filename 的配置值为准。

    • inject**

      inject有四个值: true body head false

      • true 默认值,script标签位于html文件的 body 底部
      • body script标签位于html文件的 body 底部
      • head script标签位于html文件的 head中
      • false 不插入生成的js文件,这个几乎不会用到的
    • favicon

    给你生成的html文件生成一个 favicon ,值是一个路径

    plugins: [
        new HtmlWebpackPlugin({
            ...
            favicon: 'path/to/my_favicon.ico'
        }) 
    

    然后再生成的html中就有了一个 link 标签

    • minify

    使用minify会对生成的html文件进行压缩。注意,不能直接这样写:minify: true , 使用时候必须给定一个 { } 对象 )

    plugins: [
        new HtmlWebpackPlugin({
            ...
            minify: {
                removeAttributeQuotes: true // 移除属性的引号
            }
        })
    ]
    
    • chunks

    chunks主要用于多入口文件,当你有多个入口文件,那就回编译后生成多个打包后的文件,那么chunks 就能选择你要使用那些js文件

    entry: {
        index: path.resolve(__dirname, './src/index.js'),
        devor: path.resolve(__dirname, './src/devor.js'),
        main: path.resolve(__dirname, './src/main.js')
    }
    
    plugins: [
        new httpWebpackPlugin({
            chunks: ['index','main']
        })
    ]
    

    那么编译后:

    <script type=text/javascript src="index.js"></script>
    <script type=text/javascript src="main.js"></script>
    
    • 如果你没有设置chunks选项,那么默认是全部显示

    • chunksSortMode

script的顺序,默认四个选项: none auto dependency {function}

'dependency' 不用说,按照不同文件的依赖关系来排序。

这里重点讲解一下function的用法

如何配置出我们想要的顺序

    new HtmlWebpackPlugin({
        ...
        chunksSortMode: function (chunk1, chunk2) {
            var order = ['common', 'public', 'index'];
            var order1 = order.indexOf(chunk1.names[0]);
            var order2 = order.indexOf(chunk2.names[0]);
            return order1 - order2;  
        }
    })

以上配置的顺序就是['common', 'public', 'index'],为什么呢,因为chunksSortMode这个函数就是数组的sort方法里的自定义函数,这里说白了就是数组[0, 1, 2]按升序排列。

接下来还有webpack.dev.js和webpack.prod.js两个文件(有点写不下去了,这篇文章查了n多资料,搞得现在脑袋有点昏啊)

image.png

我就快速写重点内容了,不贴代码了

  • webpack.dev.js里面的重点是devServer属性的配置

  • devServer配置详解:

devServer: {
    // 提供静态文件目录地址
    // 基于express.static实现, 所以这里你如果不请求静态文件,这个属性没啥用
    contentBase: path.join(__dirname, 'dist'),
    // 任意的 404 响应都被替代为 index.html
    // 基于node connect-history-api-fallback包实现
    // 我们知道vue和react有hash路由和history路由
    // history路由需要设置这个参数为true,要不你刷新页面会空白屏
    historyApiFallback: true,
    // 是否一切服务都启用 gzip 压缩
    // 基于node compression包实现
    compress: true,
    // 是否隐藏bundle信息
    noInfo: true,
    // 发生错误是否覆盖在页面上
    overlay: true,
    // 是否开启热加载
    // 必须搭配webpack.HotModuleReplacementPlugin 才能完全启用 HMR。
    // 如果 webpack 或 webpack-dev-server 是通过 --hot 选项启动的,那么这个插件会被自动添加
    hot: true,
    // 热加载模式
    // true代表inline模式,false代表iframe模式
    inline: true, // 默认是true
    // 是否自动打开
    open: true,
    // 设置本地url和端口号
    host: 'localhost',
    port: 8080,
    // 代理
    // 基于node http-proxy-middleware包实现
    proxy: {
        // 匹配api前缀时,则代理到3001端口
        // 即http://localhost:8080/api/123 = http://localhost:3001/api/123
        // 注意:这里是把当前server8080代理到3001,而不是任意端口的api代理到3001
        '/api': 'http://localhost:3001',
        // 设置为true, 本地就会虚拟一个服务器接收你的请求并代你发送该请求
        // 主要解决跨域问题
        changeOrigin: true,
        // 针对代理https
        secure: false,
        // 覆写路径:http://localhost:8080/api/123 = http://localhost:3001/123
        pathRewrite: {'^/api' : ''}
    }
}
  • webpack.prod.js的重点是配置TerserPlugin,和optimization配置其中splitChunks是重点中的重点)
// 这个插件最开始讲了,一下的插件就略过都讲过了
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const common = require('./webpack.common.js');
const paths = require('../paths');
const { shouldOpenAnalyzer, ANALYZER_HOST, ANALYZER_PORT } = require('../conf');

module.exports = merge(common, {
  mode: 'production', 这个需要细讲,下面说
  output: {
    filename: 'js/[name].[contenthash:8].js',
    path: paths.appBuild,
    assetModuleFilename: 'images/[name].[contenthash:8].[ext]',
  },
  plugins: [
     // 打包后会有dist(或者build,名字在output里设置)目录
     // 再次打包时需要把之前的dist删掉后,再次生成dist
     // 这个插件就是其删掉作用的
    new CleanWebpackPlugin(),
    // 提取css的插件
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css',
    }),
    // 开启分析工具的插件,分析包的体积
    shouldOpenAnalyzer &&
      new BundleAnalyzerPlugin({
        analyzerMode: 'server',
        analyzerHost: ANALYZER_HOST,
        analyzerPort: ANALYZER_PORT,
      }),
  ].filter(Boolean),
  // 这个重点下面讲
  optimization: {
    concatenateModules: false,
    minimize: true,
    minimizer: [
      // new TerserPlugin({ // 这个常用配置后面下面讲
      //   extractComments: false,
      //   terserOptions: {
      //     compress: { pure_funcs: ['console.log'] },
      //   },
      // }),
      new CssMinimizerPlugin(), // css压缩插件
    ],
    splitChunks: { // 这个是重点下面讲
      chunks: 'all',
      minSize: 0,
    },
  },
});

  • TerserPlugin

    • test

    默认值:/.m?js(?.*)?$/i, 用来匹配需要压缩的文件。

    • include 默认值: undefined, 匹配参与压缩的文件
    • exclude 默认值: undefined, 匹配参与压缩的文件
    • parallel 类型: Boolean|Number 默认值: true

    这个参数很重要,启用多进程构建,可以大大提高打包速度,强烈建议开启

      terserOptions: {
          format: {
            // 删除所有的注释
            comments: true,
          }
          compress: {
              // 删除未引用的函数和变量
              unused: true,
              // 删掉 debugger
              drop_debugger: true, 
              // 移除 console
              drop_console: true, 
              // 删除无法访问的代码
              dead_code: true,
              unsafe_undefined: true,
            }
    }
    
  • mode

mode有三个参数productiondevelopmentnone,前两个参数会默认安装一堆插件,用来区分是开发环境还是生产环境。而none的话,webpack就是最初的样子,无任何预设,需要从无到有开始配置。

所以我们了解是哪些插件,有啥用是理解webpack进化到现在的比较重要的知识点。

development模式下,webpack做了那些打包工作

在此mode下,就做了以下插件的事(还有其它配置,重点介绍下面的),其他都没做,所以这些插件可以省略,webpack默认就给你加上了,而且会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development

// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
 optimization: {
    moduleIds: 'named',
    chunkIds: 'named'
  }
}

我们看看moduleIdschunkIds这两个配置都做了啥,简而言之,就是帮助缓存生效的插件。

我们知道webpack最开始的版本并不会给模块加上名字,模块名都是数字,0,1,2,3,但是对于我们人来说数字不好认,要是名字多好,便于开发的时候查找。

而且,你想想,如果我们在01模块之间,再加一个模块,那么顺序就是0、新模块(现在是1)、老的1模块(现在是2),老的2模块(现在是3),这时候新模块就是1,其它老模块数字依次+1,这个时候缓存就失效了,虽然老的模块代码没变,但是这种缓存下标的方式,让缓存很容易失效,这就是为啥加上这个配置的原因

有了moduleIds,模块都拥有了姓名,而且都是独一无二的key,不管新增减多少模块,模块的key都是固定的。

除了moduleIds,还有一个chunkIds,这个是给配置的每个chunks命名,原本的chunks也是数组,没有姓名。

production

在正式版本中,所省略的插件们,如下所示,我们会一个个分析。

// webpack.production.config.js
module.exports = {
  + mode: 'production'
  - plugins: [
  -   new webpack.DefinePlugin({ "process.env.NODE_ENV": '"production"' }),
  -   new webpack.optimize.ModuleConcatenationPlugin(),
  -   new webpack.NoEmitOnErrorsPlugin(),
  -   new TerserPlugin(/* ... */),
  - ]
  }

terser-webpack-plugin

用于js代码压缩。在以前版本中,我们需要引入npm包terser-webpack-plugin来进行压缩,现在我们可以在optimize中进行配置达到同样的效果

配置之前已讲

ModuleConcatenationPlugin

这个是用来帮助作用域提升的,我们之前看了webpack打包出来的是类似

{
   文件路径1function()xx,
   文件路径2function()xx,
   文件路径3function()xx,
}

这样每个模块都在自己的function里面,都有自己的作用域,我们知道作用域链访问是有性能代价的,如果大家都提到一个作用域,对性能提升是有帮助的,这个插件就做这样的事。

NoEmitOnErrorsPlugin

这个就是用于防止程序报错,就算有错误也给我继续编译。

others

还有一些默认的插件配置,也就是可以不在plugins中引用的配置:

SideEffectsFlagPlugin

webpack.optimization.sideEffects用于实现treeshaking形式的死码删除。而为了实现treeshaking,需要满足几个条件:

  • 导入的模块已经标记了sideEffect,即package.json中的sideEffects这个属性为false。
  • 当前模块引用了无副作用的模块,且没有被使用

这样,经过SideEffectsFlagPlugin处理后,没有副作用且没有被使用的模块都会被打上sideEffectFree标记。 在ModuleConcatenationPlugin中,带着sideEffectFree标记的模块将不会被打包。

// webpack.pord.config.js
module.exports = {
  optimization: {
    sideEffects: true
  }
};

FlagIncludedChunksPlugin

即配置optimization.flagIncludedChunks。该配置项会使webpack确认,若当前标记的chunka是另外一个chunkA的子集并且已经A加载完成,则a将不会再次加载(包含关系)。

// webpack.pord.config.js
module.exports = {
  optimization: {
    flagIncludedChunks: true
  }
};

FlagDependencyUsagePlugin

标记没有用到的依赖。

splitChunks

最后1个知识点来了哦!

这个配置对象中,其它都好说,最令人困惑的是chunks属性,我们来看看是个什么东西。

  • chunks选项,决定要提取那些模块。
    • 默认是async:只提取异步加载的模块出来打包到一个文件中。

      • 异步加载的模块:通过import('xxx')require(['xxx'],() =>{})加载的模块。
    • initial:提取同步加载和异步加载模块,如果xxx在项目中异步加载了,也同步加载了,那么xxx这个模块会被提取两次,分别打包到不同的文件中。

      • 同步加载的模块:通过 import xxxrequire('xxx')加载的模块。
    • all:不管异步加载还是同步加载的模块都提取出来,打包到一个文件中。

兄弟们,但是我遇到了问题,就是上面说的这些根本不管用,下面的案例摘自stockOverFolw的高票回答,但是我用webpack5同样的配置,根本得不到跟这个回答一致的答案,百思不得其解,后面我改进了一下,就可以了,后面再介绍,大家先看案例

app.js 如下,有一个静态模块导入叫my-static-module,还有一个动态模块导入叫my-dynamic-module

//app.js
import "my-static-module";

if(some_condition_is_true){
  import ("my-dynamic-module")
}
console.log("My app is running")

``

来看看chunks参数不一样,得到的结果会是多么不一样(配置如下)

module.exports = {
  optimization: {
    chunks: async | initial | all
  }
};
  • async (default)

会生成以下两个文件

  1. bundle.js (包括 app.js + my-static-module)
  2. chunk.js (仅仅包括 my-dynamic-module)
  • initial

会生成以下两个文件

  1. app.js (仅仅包括 app.js)
  2. bundle.js (仅仅包括 my-static-module)
  3. chunk.js (仅仅包括 my-dynamic-module)
  • all

会生成以下两个文件

  1. app.js (仅仅包括 app.js only)
  2. bundle.js (仅仅包括 my-static-module + my-dynamic-module)

可以看出,all是比较极限的压缩

我无论怎么尝试,得出来的结果都是默认的async导出的结果,可能是我配错了吧,希望有熟悉这项配置的大哥评论区留个言。

我后来是怎么改,就可以符合上面的答案了呢,我把chunks配置在cacheGroups参数里,如下:

module.exports = {
   splitChunks: {
      cacheGroups: {
        common: {
          chunks: 'async' | 'all' | 'initial',
          minSize: 0,
          minChunks: 1,
        },
      },
    },
};

这里顺便介绍一下minChunks是什么意思,意思是至少引用多少次才分离公共代码,我这里是1次,只要引用过模块都分离出去。

minSize是规定被提取的模块在压缩前的大小最小值,单位为字节,默认为30000,只有超过了30000字节才会被提取,我们这里设置为0,是为了自己做实验,保证能被分离就分离出去。

接下来,介绍一下其他参数:

  • maxSize选项:把提取出来的模块打包生成的文件大小不能超过maxSize值,如果超过了,要对其进行分割并打包生成新的文件。单位为字节,默认为0,表示不限制大小。
  • maxAsyncRequests选项:最大的按需(异步)加载次数,默认为 6。
  • maxInitialRequests选项:打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件),默认为4。
  • 先说一下优先级 maxInitialRequests / maxAsyncRequests <maxSize<minSize
  • automaticNameDelimiter选项:打包生成的js文件名的分割符,默认为~
  • name选项:打包生成js文件的名称。
  • cacheGroups选项,核心重点,配置提取模块的方案。里面每一项代表一个提取模块的方案。下面是cacheGroups每项中特有的选项,其余选项和外面一致,若cacheGroups每项中有,就按配置的,没有就使用外面配置的。
    • test选项:用来匹配要提取的模块的资源路径或名称。值是正则或函数。
    • priority选项:方案的优先级,值越大表示提取模块时优先采用此方案。默认值为0。
    • reuseExistingChunk选项:true/false。为true时,如果当前要提取的模块,在已经在打包生成的js文件中存在,则将重用该模块,而不是把当前要提取的模块打包生成新的js文件。
    • enforce选项:true/false。为true时,忽略minSizeminChunksmaxAsyncRequestsmaxInitialRequests外面选项

能看到最后一定很不容易,欢迎点赞,后面会接着出文章,目前3篇正在写,也是自己最近学习完的知识

  • form表单低代码平台之渲染器实现(渲染器就是schema => 表单)
  • jest单元测试教程
  • leetcode官方面试最常见150题之简单题

参考:

mini-css-extract-plugin插件快速入门

在Typescript项目中,如何优雅的使用ESLint和Prettier

实用husky介绍

我是这样搭建typescript+react

webpack官网

webpack import和export

tsconfig常用配置

前端工程化 - 剖析npm的包管理机制