1.0.3 • Published 2 years ago

generator-sophonfrontend v1.0.3

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
2 years ago

从零开始创建一个 React App - Webpack@5, TypeScript@4+, React@17+

简介

作为React开发者一定很熟悉 CRA (Create React App) 它使开发者可以讯速构建出React应用并开始书写业务代码. 然而 CRA 也有许多缺点. 如下:

  • 为多个环境构建多个输出包变得团难. 比如, 你可以在应用代码中添加一些代码以便读写一些特定环境的文件, 但如果想在build流程中控制它,那么起码需要弹出(eject)应用配置文件或利用配置重载dependencies

  • CRA 使用较早的依赖包,并不第一时间使用最新版本的特性 (在结稿时, CRA 仍然在使用 webpack 4.41.2 其实webpack 5 早已发布一年有余.)

  • 无法使用路径别名, 比如 import { ITruck, ICar, IVehicle } from '@models' . 无法在 tsconfig, eslint plugin 以及 webpack plugin 3个上下文中一同使用 (type script design time, webpack compile time, eslint design time.) 所以要做到所有的上下文件用统一的路径别名这样层度的控制, 还是要弹出(eject)应用配置.

  • 使用CRA而过于依赖CRA的话,一旦被迫弹出(eject)基于CRA的应用程序,就不一定知道如何修复问题. 了解 CRA 底层的工具对于自己编写 react app有很多价值. 例如,遇到依赖关系中的错误,可以利用 yarn 临时应用补丁,再在插件作者修复该错误时继续.

值得注意的是, webpack 5 对 webpack 4 进行了重大更改包含了breaking change, 这也是CRA 迟迟不升级级的原因, 如果想使用最新的功能, 这也是退出CRA 的一大原因. 如果想知道 Webpack 5 中有什么新东西以及为什么会要使用它,你可以参考 webpack.js.org 的发行说明在这里: https://webpack.js.org/blog/2020-10-10-webpack-5-release/

除了使用 CRA 的缺点之外,也许您只是想更好地了解您正在使用的工具及其工作原理. 在本文中,我将向您展示如何从一个空命令行开始,使用 VSCode 和 Node.js 在 Typescript 中创建一个 Hello World React 应用程序,同时从头开始配置它. 同时将给出涵盖每个依赖项资源的链接.

前置条件,您已经安装了 vscode 和 node.js 并且知道如何操作 VSCode. 安装了最新版本的 npm, 如果不没有请执行以下语句升级 npm install -g npm@latest 因为尽管安装了新版本的node, 但如果您在非常旧的 npm 版本上运行,这可能会导致问题.


Git Resource:

You can clone the git repository here to already have all these steps: https://github.com/rmannjbs/WP5ReactTSFromScratch.git

cd somePath
git clone https://github.com/rmannjbs/WP5ReactTSFromScratch.git ./

If you want to just examine the repo you can install yarn globally and just run "yarn install" on the project to install all the depedencies.

要从头开始使用本指南,请继续.

初始化项目

执行的首条命令行是:

npm init 

此命令会初始化项目,它会生成一个 package.json 和一些默认设置.您可以在跳出的提示问题中选择您认为适合设置的任何内容,一般情况下只需采用默认值.


Installing Yarn

现在让我们安装 yarn,因为从现在开始,我们将使用它进行所有进一步的包依赖安装. yarn的相关文件链接在这: https://classic.yarnpkg.com/en/docs

注意:在使用 yarn 时,不要再使用 NPM(全局包除外)很重要,因为它会创建一个与 yarn.lock.json 冲突的 package.lock.json。 所以你要么只使用 NPM,要么只使用 Yarn。 对于这个指南,我将从这里开始使用yarn

执行以下命令:

npm install -g yarn 

Installing SCSS and PostCss

我们将在本教程中使用 SCSS,自动前缀处理使用 post css. 首先我们需要安装一些依赖...

执行以下命令:

yarn add -D postcss sass@1.32 postcss-preset-env autoprefixer

请注意,我在这里使用 sass@1.32 是因为最新版本的 sass 已经弃用了警告 scss 作者准备 sass@2.0.0 中的破坏性更改,并且一些像 font-awesome 这样的库还没有更新他们对 /(除法 ) 运算符到新的数学库. 所以为了不收到弃用警告的垃圾邮件,在这个项目中,我使用添加警告之前的最后一个 sass 版本. 如果您不需要 SASS,这无关紧要. 如果您的依赖项都没有在其 scss 文件中使用 / 运算符进行除法,那么您可以使用最新版本而不会收到来自库的过时警告的垃圾邮件.

在项目根目录添加 postcss.config.js 文件:

module.exports = {
    plugins: [
        [
            "postcss-preset-env",
            {
                // Options
            },
        ],
        require('autoprefixer'),
    ],
};

请注意,这告诉 postcss 对最后两个主要浏览器版本使用 autoprefixer,您可能需要编辑浏览器目标以满足您的需要.

更多详情可参考 post css 文件: https://github.com/postcss/postcss

Installing WebPack

现在让我们开始深入研究使用 WebPack 5+ 版本构建 React 应用程序的第一个依赖项. 有关 webpack 的参考,您可以访问他们的网站: https://webpack.js.org/concepts/

yarn add -D webpack@"^5.0.0" webpack-cli

安装本指南中用到的webpack 插件

Plugin NameUsage
eslint-webpack-pluginused to enable eslint in the webpack build process and Hot Module Replacement
tsconfig-paths-webpack-pluginused to resolve imports from tsconfig path aliases
sass-loaderused to process Syntactically Awesome Style Sheets
postcss-loaderused to post process css files to apply auto prefixer
css-loaderused to process css to be output
style-loaderused to inject css to the page
terser-webpack-pluginused to minify assets
html-webpack-pluginused to tell webpack to generate an index.html for our project from a template html file
webpack-dev-serverused to run our react app locally with hot module reloading
source-map-loaderlets webpack load source maps for files

下一步: 装插件使用如下命令:

yarn add -D eslint-webpack-plugin tsconfig-paths-webpack-plugin sass-loader postcss-loader css-loader style-loader terser-webpack-plugin html-webpack-plugin webpack-dev-server source-map-loader

支持 CSS Modules, 支持引入 .png, .svg 等格类静态资源

-> src 目录中添加 types.d.ts

/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />

declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV: 'development' | 'production' | 'test';
    readonly PUBLIC_URL: string;
  }
}

declare module '*.avif' {
  const src: string;
  export default src;
}

declare module '*.bmp' {
  const src: string;
  export default src;
}

declare module '*.gif' {
  const src: string;
  export default src;
}

declare module '*.jpg' {
  const src: string;
  export default src;
}

declare module '*.jpeg' {
  const src: string;
  export default src;
}

declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.webp' {
    const src: string;
    export default src;
}

declare module '*.svg' {
  import * as React from 'react';

  export const ReactComponent: React.FunctionComponent<React.SVGProps<
    SVGSVGElement
  > & { title?: string }>;

  const src: string;
  export default src;
}

declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.module.scss' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.module.sass' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.module.less' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

Installing Babel

接下来我们将添加所有 babel 依赖, 我们使用 babel 7. 您可以在他们的网站上查看 babel:https://babeljs.io/docs/en/ babel的简要说明:它是一个 javascript 转译器,将为我们编译typescript 并处理 polyfill 用于目标浏览器,并确保编译后的 JS 与这些浏览器目标兼容.

执行以下命令安装 babel 相关的依赖:

yarn add -D @babel/core@"^7.0.0" babel-loader@"^8.0.0" @babel/register@"^7.0.0" @babel/preset-env@"^7.0.0" @babel/preset-typescript@"^7.0.0" @babel/preset-react@"^7.0.0" core-js@"^3.0.0"

安装的都是些什么?

...

Babel 是 JS 的转译器, 所以你刚刚安装了 babel core, babel register, preset-env, 以及 core-js version 3+. Babel Register 允许babel在 node 的require解析时通过.babe.js为后缀的node.js 脚本注入钩子的方式进行babel转译. 这使得可以在npm scripts中使用 exports/imports 等语法. Preset-env 是一组关于如何借助最新的 core-js 通过 babel 转译 JS 的默认设置(是插件的集合, 它使我们用最新的ES特性写的代码也能运行在目标浏览器上). Core-JS 包含大量 polyfills 和其他标准,以确保您的 JS 将在您的目标浏览器上运行. 比如IE. 如果目标浏览器是 IE11 并编写了不支持 IE11 的 Javascript,Preset-env 将为您处理从 core-js 中提取最合适的polyfills,并以在 Internet Explorer 11 上运行的方式转换 Javascript.

现在添加babel配置到 package.json 中 (用babel.config.json替代也可以):

  "babel": {
    "presets": [
      [
        "@babel/preset-env",
        {
          "targets": {
            "node": "current"      
          },
          "corejs": "3",
          "useBuiltIns": "usage"
        }
      ]
    ]
  }

注意,这主要是设置 babel在 node.js 脚本中使用方式, 比如我们的构建文件. 之后我们将在webpack配置中定义babel在 typescript loader 中的工作方式.

Installing Typescript

下一步安装 typescript Run the following command:

yarn add -D typescript@"^4.0.0"

现在我们需要创建 tsconfig.json 到项目根目录, 并定义一些编译选项... 内容如下:

{
    "compilerOptions": {
        "target": "ES2019",
        "lib": [   // 要包含在类型检查过程中的标准类型
            "dom",
            "dom.iterable",
            "esnext"
        ],
        "allowJs": true,
        "skipLibCheck": true,     // 类型检查时跳过类型声明文件,比如两个库以不一致的方式定义相同类型的两个副本, TypeScript 不会对所有 d.ts 文件进行类型较准,而是会对您在应用程序源代码中专门引用的代码进行类型检查
        "allowSyntheticDefaultImports": true,  // 允许 import React from "react"; 的写法,否则只能写成 import * as React from "react";
        "esModuleInterop": true, // 兼空了Commonjs/AMD/UMD模块, 的命名空空间导出和默认导出语句, 它为true 时 allowSyntheticDefaultImports也得是true
        "strict": true, // 启用此选项等效于启用所有严格模式系列选项, 然后,您可以根据需要关闭个别严格模式系列检查。
        "alwaysStrict": true, // 确保您的文件在 ECMAScript 严格模式下解析,每个源文件都启用“use strict”
        "forceConsistentCasingInFileNames": true,  // 兼容大小写敏感的系统与大小写不敏感的系统
        "moduleResolution": "node",  //模块解析策略, 采用node.js 的CommonJS
        "resolveJsonModule": true,   //允许从Json文件中引入模块
        "isolatedModules": true, //在出现容易引发运行时错误的代码时给出警告, 分别的1.导出值不确定的标识符; 2.非模块文件; 3.操作常量enum的数值引用
        "noEmit": true,  //不生成文件, 在启用babel转义而非tsc转义时需要设置为true
        "jsx": "react", //tsx文件转译为js文件内所调用的React的方法, "react"调用React.createElement函数, React17+可以用React-jsx,它调用_jsx函数
        "rootDir": "./src", // TS源码所在位置
        "baseUrl": "./src", // 别名引入模块搜索的路径起点, 如果设置成"./",那么搜索路径将起点将是tsconfig.json文件所在的目录
        "paths": {  // 添加路径别名
            "@routes/*": [
                "components/routes/*"
            ]

        }
    },
    "include": [  //只编译某些目录
        "src"
    ]
}

此 tsconfig.js 文件定义了 typescript 在项目中的行为, 你也可以让它更严格. 有关选项的完整列表,请查看此处的文档: https://www.typescriptlang.org/tsconfig...

目前我使用 ES2019 作为编译目标, allowJs 为true,它允许您编写 js... 但这里最重要的是 noEmit 为 true. 所以我们不会在这里输出任何文件(.js或.d.ts都没有). 相反,我们使用 typescript 作为 babel 管道中的中间人, babel 会为我们转译 typescript. 此外,我已将根目录和 baseUrl 设置为 ./src 并为@routes 设置了一个路径别名,我将在本文档后面介绍.

Installing ESLint

现在开始安装 eslint for the linter 和 typescript 以及 react plugin for eslint 执行以下命令

yarn add -D eslint eslint-plugin-import typescript-eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-import-resolver-typescript eslint-plugin-react eslint-plugin-react-hooks

Note* 在这一步我们还会获取使eslint 与 typescript 及 react 协同工作相关插件, 之后会设置它们.

配置 ESLint

ESLint 文档地址: https://eslint.org/docs/user-guide/configuring/

接下来我们将设置eslint使用typescript的配置文件并添加规则. 您可以添加更多想要的规则, 这里有完整的 ESLint 可用规则列表: https://eslint.org/docs/rules/

TypeScript ESLint 可用规则列表 https://typescript-eslint.io/rules/

linter 的目的是在您编写typescript 和 javascript 实时为您提供有用的警告或错误. 您可以使用 eslint 来强制执行代码标准和以特定方式编写代码.

eslint 共享tsconfig.json中设置的alias 路径可能参考: https://stackoverflow.com/questions/57032522/eslint-complains-about-typescripts-path-aliasing

根目录添加.eslintrc.js

module.exports = {
    "env": {
        "browser": true,
        "node": true
    },
    "globals": {
        "fetch": true
    },
    "root": true,
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": 2019,
        "sourceType": "module",
        "ecmaFeatures": {
            "jsx": true
        }      
    },
    "plugins": [
        "@typescript-eslint",
        "react",
        "react-hooks",
        "import"
    ],
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/eslint-recommended",
        "plugin:@typescript-eslint/recommended",
        "plugin:react/recommended",
        "plugin:react-hooks/recommended"
    ],
    "ignorePatterns": [".eslintrc.js"],
    "rules": {
        "import/no-unresolved": "warn",
        "prefer-rest-params": "off",
        "prefer-spread": "off",
        "eol-last": 2,
        "no-undef": 2,
        "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
        "@typescript-eslint/interface-name-prefix": "off",
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/member-delimiter-style": "off",
        "@typescript-eslint/no-non-null-assertion": "off",
        "@typescript-eslint/no-inferrable-types": "off",
        "@typescript-eslint/no-use-before-define": "off",
        "@typescript-eslint/camelcase": "off",
        "@typescript-eslint/ban-ts-ignore": "off",
        "@typescript-eslint/no-empty-interface": "off",
        "@typescript-eslint/no-empty-function": "off",
        "@typescript-eslint/no-var-requires": "off",
        "@typescript-eslint/no-this-alias": "off",
        "@typescript-eslint/semi": 2,
        "@typescript-eslint/triple-slash-reference": 0,
        "@typescript-eslint/no-unused-vars": ["warn"],
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "react/display-name": "off",
        "react/prop-types": "warn",
        "react/no-string-refs": 0,
        "react/no-children-prop": 0,
        "react/jsx-no-target-blank": 0,
        "react/no-render-return-value": 0,
        "react/no-direct-mutation-state": "off",
        "react/no-unescaped-entities": "off",
        "react/no-find-dom-node": "off",
        "react/no-deprecated": "off",
        "react-hooks/rules-of-hooks": "error",
        "react-hooks/exhaustive-deps": "warn",
        "jsx-quotes": [2, "prefer-double"]
    },
    "settings": {
        "react": {
            "pragma": "React",
            "version": "detect"
        },
        "import/parsers": {
            "@typescript-eslint/parser": [
                ".ts",
                ".tsx"
            ]
        },
        "import/resolver": {
            "node": {
                "extensions": [
                    ".ts",
                    ".tsx"
                ]
            },
            "typescript": {
                "extensions": [
                    ".ts",
                    ".tsx",
                    ".js",
                    ".jsx"
                ],
                "alwaysTryTypes": true
            }
        }
    }
};

ESLint 配置中要注意 : 我们已添加了react 的插件和 react hooks的规则, 所以在源码中的hooks 调用出错时会有提示. 你可以添加更多的React的规则,它们可以在这里找到: https://github.com/

yannickcr/eslint-plugin-react

Installing StyleLint

现在开始安装 stylelint 及其插件 stylelint plugin, stylelint-config-standard等 执行以下命令

yarn add -D stylelint stylelint-webpack-plugin stylelint-config-standard stylelint-config-recommended-less stylelint-config-recommended-scss stylelint-less stylelint-scss stylelint-order

配置 StyleLint

Stylelint 文档地址L:https://stylelint.io/user-guide/get-started 可用规则列表:https://stylelint.io/user-guide/rules/list

根目录添加stylelint.config.js

module.exports = {
    "defaultSeverity": "error",
    "extends": [
        "stylelint-config-standard",
        "stylelint-config-recommended-scss",
        "stylelint-config-recommended-less"
    ],
    "plugins": [
        "stylelint-scss",
        "stylelint-less",
        "stylelint-order"
    ],
    "rules": {
        "max-nesting-depth": null,
        "no-empty-source": null,
        "no-descending-specificity": null,
        "property-no-vendor-prefix": null,
        "selector-max-compound-selectors": null,
        "scss/at-import-partial-extension-blacklist": null,
        "scss/at-import-no-partial-leading-underscore": null,
        "value-no-vendor-prefix": null,
        "color-hex-case": "upper",
        "order/properties-alphabetical-order": true,
        "no-missing-end-of-source-newline": null,
        "at-rule-no-unknown": null,
        "selector-pseudo-class-no-unknown": [
            true,
            {
                "ignorePseudoClasses": ["global"]
            }
        ],
        "unit-no-unknown": null
    }
};

StyleLint 配置中要注意: 我们不是检查css, 面是.scss, .less, 因此需要使用PostCss语法, 建议扩展一个共享配置,其中包含您首选语言或库的推荐的语法. 所以我们将 stylelint-config-standard-scss 共享配置扩展用于 lint SCSS, stylelint-config-recommended-less共享配置扩展用于 lint LESS.

创建项目的目录结构

现在让我们在我们的项目中创建文件夹结构来支持我们的 webpack build. 我在这里使用了一个非常基本的示例,但其中一个要点是您的文件夹结构可以是您想要的任何内容.

添加以下目录与文件:

注: React 组件文件名应该以大写字母开头,因为 React jsx 组件必须以大写字母开头. 所以在这个例子中,所有组件的文件名都以大写字母开头,以与文件中导出的组件名一致.

  • config
    • paths
      • index.js
    • webpack
      • webpack.common.config.js
      • webpack.dev.babel.js
    • public
      • index.html
  • dist
  • src
    • assets
      • scss
        • themes
          • _metroTheme.scss
        • variables
          • _vendorVariables.scss
        • app.scss
        • vendor.scss
    • components
      • routes
        • home
          • HomeRoute.tsx
          • index.tsx
        • index.tsx
      • App.tsx
  • index.tsx

填写配置

config\paths\index.js

首先,让我们在 config\paths\index.js 文件中创建一个帮助函数来计算我们的路径. 它看起来长这样:

FileName: config\paths\index.js:

import path from 'path';

function paths() {
    this.root = path.resolve(path.join(__dirname), '../../');
    this.src = path.join(this.root, 'src');
    this.srcIndexEntry = path.join(this.src, 'index.tsx');
    this.srcScss = path.join(this.src, 'assets', 'scss');
    this.srcScssEntry = path.join(this.srcScss, 'app.scss');
    this.srcScssVendorEntry = path.join(this.srcScss, 'vendor.scss');
    
    this.dst = path.join(this.root, 'dist');
    
    this.config = path.join(this.root, 'config');
    this.configHtmlTemplates = path.join(this.config, 'public');
    this.configHtmlTemplatesLocalIndex = path.join(this.configHtmlTemplates, 'index.html');

    this.nodemodules = path.join(this.root, 'node_modules');    
}

export default new paths();

注: 您可以根据需要向 path.js 文件添加更多内容. 目标是你应该在这里编辑所有的路径,而不是把它们放在你的 webpack 配置文件中,如果你以后移动文件或文件夹结构,这将修改路径更加容易. (使用 CRA 无法轻松完成的事情).

webpack\webpack.common.config.js

现在来创建 webpack 配置.... 你可以参考这里的文档来更多地了解这个配置文件: https://webpack.js.org/configuration/#options 但基本上 webpack.config 就是告诉 webpack 该做什么的. 这是您定义项目的入口文件(比如 index.js),以及如何输出的地方. 您还可以添加大量插件,例如 HtmlWebpackPlugin,它可以为您生成包含bundle的 index.html,这样您就不必手动更新脚本链接. 随着我们的继续,我们将为 webpack 安装更多插件.

在 config/webpack 目录下添加名为 webpack.common.config.js 的文件:

我们将把 webpack 的大部分逻辑放在这里,以便它可以在基于特定环境的构建中使用,我们将从为本地开发搭建的环境开始.

在你的 webpack.common.config.js 中添加以下内容:

import paths from '../paths'; //*本项目的路径库*
import HtmlWebpackPlugin from 'html-webpack-plugin'; //docs -> https://webpack.js.org/plugins/html-webpack-plugin/
import tsConfigPathPlugin from 'tsconfig-paths-webpack-plugin'; //docs -> https://www.npmjs.com/package/tsconfig-paths-webpack-plugin
import TerserPlugin from 'terser-webpack-plugin'; //docs -> https://github.com/webpack-contrib/terser-webpack-plugin
import sass from 'sass'; //docs -> https://sass-lang.com/install
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; //docs -> https://github.com/TypeStrong/fork-ts-checker-webpack-plugin
import ESLintPlugin from 'eslint-webpack-plugin';

const NODE_ENV = process.env.NODE_ENV === 'production' ? 'production' : 'development';

//根据构建环境获取插件
function getPlugins(env) {
    return [
        new ForkTsCheckerWebpackPlugin({   // 此eslint 只负责js语法检查, 此插件负责做TS类型检查
            async: true
        }),
        new HtmlWebpackPlugin({
            title: '项目名',
            fileName: 'index.html', // 这里可以填任何文件名
            template: paths.configHtmlTemplatesLocalIndex,
            inject: 'body', // scripts 注入的位置, body 意味着执行到body的尾部
            base: '/',     // head中添加标签 <base href="/"> 表示获取静态资源的基准路径,publicPath 就可以设置为'auto'
            publicPath: 'auto',  // 基准路径通常设置为 '/',也就是页面中<script> 引入js前缀路径, 注意:使用module ferderation 要设置为 'auto'
            scriptLoading: 'blocking', // SPA 应用可以设置为block, 也可以使用 defer
            hash: true,
            cache: true,
            showErrors: true
        }),
        new ESLintPlugin({
            context: paths.root,
            extensions: ['js', 'jsx', 'ts', 'tsx'],
        }),
    ];
}

//根据构建环境生成不同的webpack配置项
export function getBaseWebPackConfig(env, argv) {
    console.log('NODE_ENV', NODE_ENV);
    console.log('evn', env);
    console.log('argv', argv);
    let config = {};
    config.mode = NODE_ENV; //设置模式
    const isLocalDev = argv.env.localdev ? true : false;

    //调用 getPlugins helper 生成插件集合
    config.plugins = getPlugins(env);

    //代码拆分,您可以使用此入口对象将您的应用定义为单独的entry chunk,这些chunk相互依赖以优化 webpack 生成包的方式。
    config.entry = {
        //告诉webpack有两个入口需要被拆分为chunk, 以app为入口chunk需要依赖以vendor 为入口的chunk
        //配置 vendor styles (bootstrap/overrides等 到一个单独的chunk)
        vendor: {
            import: paths.srcScssVendorEntry
        },
        app: {
            import: paths.srcIndexEntry,
            dependOn: 'vendor'
        }
    };

    config.output = {
        filename: '[name].[contenthash].js', //因为不只有一个 chunk, 所以输出的 chunk名基于 chunk名和它的hash值, hash值在内容变化时会改变
        path: paths.dst, //打包输出路径
        clean: true, //目村路径如果存在,则先清除
        publicPath: 'auto',
        assetModuleFilename: 'assets/[name][ext]' //资源文件存入assets/子目录下
    };


    config.resolve = {
        extensions: ['.scss', '.js', '.jsx', '.tsx', '.ts'],
        plugins: [
            new tsConfigPathPlugin() //设置webpack在编辑时使用 tsconfig.json 中的 paths 选项来定义别名,省去了在webpack配置项resolve 中再定义 alias
        ]
    };


    // rules 设置不同的文件类型匹配特定的处理loader
    config.module = {
        rules: [
            {
                test: /\.(js|ts)x?$/i, //这个正则匹配 ts,js,tsx,jsx
                exclude: /[\\/]node_modules[\\/]/, //忽略 node_modules,其中的代码切单独切分到 vendor chunk
                use: [
                    {
                        loader: 'babel-loader', //使用 babel loader, 现已不推荐使用 ts-loader
                        options: {
                            presets: [
                                '@babel/preset-env', //使用三种预置 env, react, typescript
                                '@babel/preset-react',
                                '@babel/preset-typescript'
                            ]
                        }
                    },
                    {
                        loader: 'source-map-loader',
                        options: {

                        }
                    }
                ]
            },
            {
                // 这里是webpack 5 的新特性,原本在webpack 4 只能靠 file-loader, url-loader 来实现将资源文件打包到输出目录
                test: /\.(woff(2)?|ttf|eot|svg|jpg|jpeg|png|gif|pdf)(\?v=\d+\.\d+\.\d+)?$/,
                type: 'asset/resource'
            },
            {
                test: /\.(scss|sass)$/, //处理 scss 和 sass 文件
                include: [
                    paths.src,
                    paths.nodemodules //项目的 node_modules 也可能提供样式文件,所以要把它也包含进来
                ],
                use: [ //处理的 loaders 采用倒序,所以最后的 loader 最先执行
                    {
                        //注意, 在生产环境应该用css 抽取插件来处理,  style-loader 可以用在开发环境, 因为它处理速度会更快
                        loader: 'style-loader', // docs -> https://webpack.js.org/loaders/style-loader/
                        options: {
                            esModule: false,
                            insert: 'head'   // 最后执行
                        }
                    },
                    {
                        loader: 'css-loader', //docs -> https://www.npmjs.com/package/css-loader
                        options: {
                            modules: false, //不使用 styled components in react
                            esModule: false,  //不使用 es module 语法
                            sourceMap: true //是/否生成样式的 source maps 文件
                        }
                    },
                    {
                        loader: 'postcss-loader', //docs -> https://github.com/webpack-contrib/postcss-loader
                        options: {
                            sourceMap: true
                        }
                    },
                    {
                        loader: 'sass-loader',
                        options: {
                            sourceMap: true,
                            implementation: sass
                        }
                    }
                ]
            }
        ]
    };

    // 配置webpack 如何拆分 chunk,并可以为 CSS 创建测试函数,以便 css get 被提取到它自己的chunk中。
    config.optimization = { // docs -> https://webpack.js.org/plugins/split-chunks-plugin/#defaults
        nodeEnv: NODE_ENV,   // nodeEnv的默认值与mode 一致,会利用 webpack.DefinePlugin 自动添加到 process.env.NODE_ENV 中去,所以无需再设置 DefinePlugin 插件
        /**
         *  docs -> https://webpack.js.org/plugins/split-chunks-plugin/#defaults
         *  中文说明 -> https://juejin.cn/post/6992887038093557796
         */
        splitChunks: {
            chunks: 'all',
            minSize: 20000,
            minRemainingSize: 0,
            minChunks: 1,
            maxAsyncRequests: 30,
            maxInitialRequests: 30,
            enforceSizeThreshold: 50000,
            cacheGroups: {
                defaultVendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    reuseExistingChunk: true,
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
    };

    config.performance = {  //增加weback 报资源太大的阈值, production 环境不用加
        hints: 'warning',
        maxAssetSize: 20000000, // 整数类型(以字节为单位)
        maxEntrypointSize: 40000000, // 整数类型(以字节为单位)
        assetFilter: function(assetFilename) {
          // 提供资源文件名的断言函数
          return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
        }
     };


    // 命令行带有 localdev, 就运行本地开发Server
    if (isLocalDev) {
        console.log('DEV SERVER');
        config.devtool = 'source-map';
        config.devServer = {
            host: 'local-ip', // 自动以ip地址(非localhost)打开页面
            historyApiFallback: true,
            hot: true, //打开模块热更新功 !
            port: 9000,
            client: {
                progress: true,
                overlay: true,
                logging: 'info' //在本地开发模式下,提供记录到客户端的所有信息
            },
            static: {
                publicPath: 'auto',
                directory: paths.dst
            }
        };
    } else {
        config.devtool = 'cheap-module-source-map';
    }

    return config;
}

添加本地运行环境的 webpack 配置

现在我们将添加实际的文件,它将成为我们 webpack 的入口点, 本地运行我们使用的叫作 webpack.dev.babel.js 的配置文件.

注意这里以 .babel.js 为后缀, 此前在安装babel时提及过 @babel/register. 这使得 ES6 import/export 等用法在webpack 配置文件中可用,以及用babel来处理 webpack 配置.

继续在 config/webpack 目录下创建名为 webpack.dev.babel.js 的文件, 内容如下:

import { getBaseWebPackConfig } from './webpack.common.config';

function getLocalWebPackConfig(env, argv) {    
    const webPackConfig = getBaseWebPackConfig(env, argv);
    // 这里可以在运行之前更改或覆盖从 getBaseWebPackConfig 取得的 webpack 配置对象
    // 比如可以改变Dev Server, 或路径配置, 以及任务你认为可以不同的设置 
    return webPackConfig;
}

module.exports = getLocalWebPackConfig

新建文件 config/public/index.html 加入以下内容:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="Cache-control" content="no-cache, no-store, must-revalidate">
  <meta http-equiv="Pragma" content="no-cache">
  <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
</head>
<body >
  <div id="root"></div>
</body>
</html>

设置 SCSS

我们将在这里设置一些基本的 SCSS 作为一个粗略的例子.

  1. 新建文件 src/assets/scss/themes/_metroTheme.scss 添加以下内容:
/* 并不完整, 仅作例子以供参考 */
$spacer: 1rem;
$enable-rounded: false;
$enable-shadows: false;
$enable-gradients: false;
$enable-transitions: false;
$enable-prefers-reduced-motion-media-query: true;
$enable-responsive-font-sizes: true;
  1. 新建文件 src/assets/variables/_vendorVariables.scss 添加以下内容:
@import '../themes/metroTheme';
  1. 新建文件 src/assets/scss/app.scss 添加以下内容:
/* reimport variables so we have them in app scss */
@import './themes/metroTheme';
@import '~/bootstrap/scss/_variables';
  1. 新建文件 src/assets/scss/vendor.scss 添加以下内容:
$fa-font-path: '~font-awesome/fonts';
@import './variables/vendorVariables';
@import '~bootstrap/scss/bootstrap';
@import '~font-awesome/scss/font-awesome';

新建文件 src/components/routes/home/HomeRoute.tsx 添加以下内容:

import React, { useState } from 'react';
import { Container, Col, Row } from 'react-bootstrap';

export const HomeRoute = () : React.ReactElement => {
    const [testState] = useState<boolean>(true);
    if (testState) {
        const [oppsState] = useState<boolean>(false); //broken on purpose to show linting errors.
    }
    return (
        <Container fluid className="gx-0">
            <Row>
                <Col xs={12}>
                    <h1 className="bg-primary text-center">Hello World!</h1>
                </Col>
            </Row>
        </Container>
    )
}

新建文件 src/components/routes/home/index.tsx 添加以下内容:

export * from './HomeRoute'; //导出所有 HomeRoute.tsx 所导出的内容

新建文件 src/components/routes/index.tsx 添加以下内容:

export * as Home from './home'; //导出所有 /home 目录下 index.tsx 导出的内容

新建文件 src/components/App.tsx 添加以下内容:

import React from 'react';
import { BrowserRouter, Routes, Route, Outlet, Navigate } from 'react-router-dom';
import { HomeRoute } from '@routes/home'; //之前提及的TSConfig中的别名路径  @routes, 这里就是使用别名路径的例子. 这是一个捷径,因此您不必在整个应用程序中使用 ../../ 等进行文件夹钻孔.

export const App = () => {
    return (
        <BrowserRouter>
            <Routes>                              
                <Route path="/home" element={<Outlet />}>
                    <Route path="/" element={(<HomeRoute />)} />                    
                </Route>                
                <Route path="*" element={<Navigate to="/home" /> } />
            </Routes>
        </BrowserRouter>
    )
}

如果您对索引文件的用途感到困惑... Imports in es6/typescript 自动以不同的后缀名(.js or .ts or .jsx or .tsx)查找 index 文件. 所以当使用 import thing from 'somepath' import语句角析不光查找 "somepath" 文件, 同时也查找 "somepath" 目录下名叫 index 的文件, 然后加载它. 因此,通过从index中导出所有内容,它可以让您从一个路径导入许多内容,从而节省导入语句中的样板,因此您可以执行以下操作:

import { Animal, Dog, Cat, Cow, Horse, Pig } from '@models' // @models 是一个路径别名 
// 在 tsconfig.json 中配置了 @models 指向一个内有index文件的目录, 此index 又导出了所有模块 .

src下, 添加 index.tsx (这是入口文件) 内容如下:

import React from 'react';
import { render } from 'react-dom';
import { App } from './components/App';

function index() {
    return (
        <React.StrictMode>
            <App />
        </React.StrictMode>
    )
}

render(index(), document.getElementById('root'));

在运行 build 之前安装运行时依赖

我们即将从头开始设置一个初始的 hello world 应用程序,但首先我们需要安装我们所有的运行时依赖项,如 React、React-Router、@types、Luxon、Bootstrap 等(或任何您想要的应用程序)

终端中运行以下命令:

//安装主要的依赖库 React, React-dom 等 

//注 - react-router 和 react-bootstrap 用了beta版
yarn add @popperjs/core bootstrap@"^5.0.0" font-awesome history react@"^17.0.0" react-dom@"^17.0.0" react-bootstrap@"^2.0.0-beta.5" react-router-dom@"^6.0.0-beta.2" react-router@"^6.0.0-beta.2"

//安装 typescript types 作为依赖
yarn add @types/react @types/react-bootstrap @types/react-dom @types/react-router @types/react-router-dom

创建 package.json scripts 构建build的脚本:

添加以下脚本到 package.json:

  "scripts": {
    "build": "NODE_ENV=production webpack --config ./config/webpack/webpack.dev.babel.js",
    "start": "NODE_ENV=development webpack serve --open --config ./config/webpack/webpack.dev.babel.jss --env=localdev",
  }

注: --env 标志的是传入 webpack config 的运行环境, 在这里,我们传递了一个名为 localdev 的环境标志

Run the build

现在让我们测试构建的输出,看看它是否按预期运行并生成 lint 错误:

yarn build

Your output should resemble: npm.io

Let's fix the lint errors:

Change App.tsx to the following:

import React from 'react';
import { BrowserRouter, Routes, Route, Outlet, Navigate } from 'react-router-dom';
import { HomeRoute } from '@routes/home'; //earlier I talked about the @routes path alias in the TSConfig, this is an example of using it.  It's a short cut so you don't have to folder drill with ../../ etc all over your app.

export const App = (): React.ReactElement => {
    return (
        <BrowserRouter>
            <Routes>                              
                <Route path="/home" element={<Outlet />}>
                    <Route path="/" element={(<HomeRoute />)} />                    
                </Route>                
                <Route path="*" element={<Navigate to="/home" /> } />
            </Routes>
        </BrowserRouter>
    )
}

Change HomeRoute.tsx to the following

import React from 'react';
import { Container, Col, Row } from 'react-bootstrap';

export const HomeRoute = () : React.ReactElement => {
    return (
        <Container fluid className="gx-0">
            <Row>
                <Col xs={12}>
                    <h1 className="bg-primary text-center">Hello World!</h1>
                </Col>
            </Row>
        </Container>
    )
}

re-run yarn build and your lint errors should go away and you should have a dist folder with output files in it

Run the local server and work on the code locally

Run the following command:

yarn start

dev srever 会运行在 9000 端口.

Wrapping up Part 1

您现在已经完成了安装 yarn 和 react,typescript,scss 项目所需的许多依赖项. 你应该有完整的 eslint 运行. 如果在 VSCode 中没有进行 linting,您可能缺少 ESLint 插件,或者您需要在 VSCode 中配置您的用户设置以启用文件扩展名:

-> settings.json (VSCode)

        "eslint.validate": [ "javascript", "javascriptreact", "html", "typescriptreact" ]

你已经设置了一个带有环境标志的本地 webpack 配置。 如果你想为特定环境添加 webpack 配置,你可以将它们添加到 config/webpack 文件夹并在package.json 中为它们添加新的脚本.

-> 1.添加插件抽取样式生成独立的样式文件

-> 2.添加插件fork-ts-checker-webpack-plugin作类型检查

-> 3.添加less的支持

-> 4.添加sass moduless, less modules 的支持

-> 5.添加stylelint 可对样式进行 autofix

-> 6.将babel, eslint 的全局配置从package.json中分离到单独的配置文件中去

-> 7.添加types(.d.ts)文件以支持在源代码中import引入.png, .svg 等文件,也要支作为静态资源在页面中引入

-> 8.更合理得分离dev与prod 的webpack配置,生产环境需要压缩js, 最后检查不同环境source map的生成情况

-> 9.完善模块文件(包括起更语义化的名字)以支持yeoman-generator生成的项目可以独立运行(需测试build出来的bundle 是否可用)

-> 10.添加dev server 支持https

11.dev server 添加proxy

12.package.json中script脚本兼容windows

13.引入.npmrc到模板文件,gitlab.yml, dockerfile 等公司内部文件

14.结合sophon-cli的配置和项目结构做成兼容的yeoman-generator

15.添加husky