還在困惑項目腳手架代碼爲何那麼寫?那這篇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的包管理機制

相關文章
相關標籤/搜索