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

前言

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

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

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

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

以上提到的知识点对咱们深刻了解项目环境搭建很是重要, 你的项目你来时通常环境都是搭建好的,试过从0本身搭建不?是否是抄别人的配置,都一头雾水,彻底不知道这些配置项时啥意思呢?vue

如今!本篇本章专解这个问题!废话少说,java

咱们先从package.json提及,里面的每一行代码是什么意思。node

package.json

package.json里面有不少有趣的内容,咱们先从依赖包提及,解释这个项目中,下面的依赖包分别有什么用。react

"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)来完成。linux

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

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,以及一个节点树字符串化工具,其它功能都是经过插件来实现的,好比说插件有

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

二、有检测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 auto flex: 0 0 auto
flex: 1 flex: 1 1 0% flex: 1 0 0px
flex: auto flex: 1 1 auto flex: 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
// 编码格式,支持 latin一、utf-八、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-设置为三、5(默认),六、七、八、九、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, 文件路径2:function()xx, 文件路径3:function()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的包管理机制

相关文章
相关标签/搜索