「超详细React项目搭建教程二」集成 Webpack5/React17

3,162 阅读6分钟

「超详细React项目搭建教程一」企业级前端开发规范如何搭建🛠中我们已经使用 TypeScript/ESlint/Prettier/EditorConfig/stylelint,搭建好了前端规范的基础设施。

这篇文章将会在前文的基础上配置 Webpack5 和 React17。

在我们的 Webpack 构建的过程中将会包含

  • 使用 Typescript 进行类型检查
  • 使用 Eslint 进行代码规范检查

因为这些能帮助我们提高代码质量。我们还会

  • 配置 Webpack 热更新
  • 配置 Webpack 以区分开发/生产环境

以便提高我们的开发体验。接下来让我们开始吧!!!

创建一个基础项目

接下来我们创建以下文件目录

└── config/                                            // webpack配置文件
    ├── webpack.dev.js
    ├── webpack.pro.js
    ├── webpack.common.js
└── public/
    ├── index.html/                                   // html模板文件
└── src/
    ├── index.tsx                                    // 项目入口文件
├── package.json

index.html 中添加如下代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>react-app</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

这个 HTML 文件是 Webpack 构建过程中的模板文件。目的是告诉 Webpack 将 React 代码注入到 id="root"的 div 元素中,并在 HTML 中自动引入打包好的 JavaScript 和 CSS。

添加 React

安装 React 及其对应的类型库

yarn add react react-dom


yarn add   @types/react @types/react-dom --dev

添加 React 根组件

创建一个 src/index.tsx 来编写 React 组件,此代码将会被展示到index.html 文件id="root"的 div 元素下

import React from "react";
import ReactDOM from "react-dom";

const App = () => <h1>My React and TypeScript App!</h1>;

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

在上面的代码中,我们使用React.StrictMode 创建组件并插入到id="root"的 div 元素下

添加 Babel

在项目中,我们需要使用 Babel 将 React 和 TypeScript 代码转换为 JavaScript。接下来我们安装一些 Babel 工具

 yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime @babel/runtime   --dev

以下是一些 Babel 依赖的解释

  • @babel/core:Babel 核心库
  • @babel/preset-env:让我们可以在不支持 JavaScript 最新特性的浏览器中使用 ES6+语法
  • @babel/preset-react:将 React 代码转换为 JavaScript
  • @babel/preset-typescript:将 TypeScript 代码转换为 JavaScript
  • @babel/plugin-transform-runtime@babel/runtime:支持在低版本浏览器使用 ES6+语法,如 async/await

Babel 配置

我们通过.babelrc文件来进行 Babel 配置,在根目录创建此文件并加入以下内容

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "regenerator": true
      }
    ]
  ]
}

上面的配置是告诉 Babel 使用哪些插件

添加 Webpack

Webpack 是目前最流行的前端模块打包工具。接下来我们开始安装 Webpack 依赖

yarn add webpack webpack-cli @types/webpack --dev

在开发环境中,我们还要使用 Webpack 为我们提供的 web server功能

yarn add webpack-dev-server @types/webpack-dev-server  --dev

安装babel-loader-通知 Babel 将 React 和 TypeScript 代码转换为 JavaScript

yarn add babel-loader  --dev

安装 html-webpack-plugin-用来生成 HTML 模板

yarn add html-webpack-plugin  --dev

开发环境配置

我们需要为 Webpack 添加几个配置文件

  • 公共配置文件
  • 开发环境配置文件
  • 生产环境配置文件

注: Webpack 配置文件的代码需要符合 CommonJs规范。

让我们首先配置开发环境文件,在根目录创建 config/webpack.dev.js并加入以下内容

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

const config = {
  mode: "development",
  entry: {
    main: path.resolve(__dirname, "../src/index.tsx"),
  },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "../build"),
  },
  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/i,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
              "@babel/preset-typescript",
            ],
            plugins: [
              [
                "@babel/plugin-transform-runtime",
                {
                  regenerator: true,
                },
              ],
            ],
          },
        },
      },
    ],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "../src"),
    },
    extensions: [".tsx", ".ts", ".jsx", ".js"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
    }),
    new webpack.HotModuleReplacementPlugin(),
  ],
  devtool: "inline-source-map",
  devServer: {
    contentBase: path.join(__dirname, "../build"),
    historyApiFallback: true,
    port: 4000,
    hot: true,
  },
};

module.exports = config;

下面是一些相关配置的解释

  • mode : 构建开发环境代码还是生产环境代码。在上面的配置中我们使用development. Webpack 会自动将 process.env.NODE_ENV设置为development
  • output.public:构建的根路径是什么。
  • entry :模块构建的入口文件.在我们的项目中,入口是 src/index.tsx
  • module: 用于处理不同的资源模块.在我们的项目中,用babel-loader来处理.js,.jsx,.js,.tsx 后缀的文件
  • resolve.alias: 可以让我们在引入模块路径时使用别名
  • resolve.extensions告诉 Webpack 在模块解析期间要按顺序查找哪些文件的后缀,以方便我们在在引入模块文件时不带后缀名。
  • HtmlWebpackPlugin:用来创建 HTML 文件.在上面的配置中,我们告诉此插件使用public/index.html 作为文件模板
  • HotModuleReplacementPlugin/devServer.hot:修改业务代码后界面可以自动局部刷新,而不是整体刷新
  • devtool: 使用inline-source-map,可以在让我们在谷歌开发工具中调试源代码
  • devServer: 启动 Webpack 开发服务器,我们告诉 Webpack web 服务的根路径是 build目录,并且在4000端口上启动服务. historyApiFallback 对于多页面应用是比较有用的。最后,使用open在服务启动后自动打开浏览器

为开发环境添加 NPM 脚本

为了方便以开发模式启动应用,可以利用 npm 脚本-将以下内容添加到package.json

  ...
  "scripts": {
    "start": "webpack serve --config config/webpack.dev.js",
  }
  ...

以上脚本会启动一个 Webpack 下的开发环境服务器.并且使用 config 选项来引用开发环境配置文件

启动应用

yarn start

N 秒后,Webpack development server 将会启动,然后我们在浏览器中访问http://localhost:4000

yarn start

注意:Webpack 并没有在 build 目录生成任何文件,这是因为 Webpack 服务启动后文件都在内存中

现在,我们修改 React 代码内容并观察变化.当我们保存代码后,浏览器会自动刷新

更新后的效果

在 Webpack 中手动配置热更新插件

可能因为各种各样的原因导致 webpack 的 HMR 不生效。我们还可以手动配置热更新插件!

安装 React 热更新插件react-refresh-webpack-plugin

yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh

修改config/webpack.dev.js并加入以下内容

const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");

const config = {
  // 指定target为 web
  target: "web",
  module: {
    rules: [
      {
        test: /\.(t|j)sx?$/i,
        include: path.resolve(__dirname, "../src"),
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
              "@babel/preset-typescript",
            ],
            plugins: [
              [
                "@babel/plugin-transform-runtime",
                {
                  regenerator: true,
                },
              ],
              // 热更新加载器
              "react-refresh/babel",
            ],
          },
        },
      },
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // 热更新插件
    new ReactRefreshWebpackPlugin({
      exclude: [/node_modules/],
    }),
  ],
};

如果热更新配置遇到问题,可以参考以下 issue

在 webpack 构建过程中添加类型检查

目前, Webpack 构建过程没有做任何类型检查,我们可以使用fork-ts-checker-webpack-plugin 让 Webpack 构建过程支持类型检查。这意味着 Webpack 会通知我们任何类型相关的错误。 接下来我们安装相关依赖

yarn add fork-ts-checker-webpack-plugin @types/fork-ts-checker-webpack-plugin --dev

webpack.dev.js添加如下配置

...
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

const config = {
  ...,
  plugins: [
    ...,
    new ForkTsCheckerWebpackPlugin({
      async: false
    }),
  ],
};

我们使用 async 标志来告诉 Webpack 等待代码的类型检查结束,然后才提交代码进行编译

修改后,我们需要重新启动应用

让我们在 src/index.tsx 中做如下修改

...
const App = () => <h1>My React and TypeScript App!! {today}</h1>;
...

当然,控制台报错了,因为使用了一个未定义的变量today。Webpack 将在终端中显示此类型错误

Webpack进行类型检查

现在,可以修改为类似的如下代码来解决此问题

const App = () => (
  <h1>My React and TypeScript App!! {new Date().toLocaleDateString()}</h1>
);

控制台的类型错误消失了,刷新浏览器界面后显示为正确的内容

 today’s date

在 webpack 构建过程中添加代码规范校验

目前,Webpack 构建流程不会执行代码规范校验。 我们可以使用ESLintPlugin来使 Webpack 构建过程能够使用 ESLint 进行代码规范校验。 这意味着 Webpack 会通知我们任何代码规范校验的错误。 让我们安装这个依赖

yarn add eslint-webpack-plugin --dev

webpack.dev.js 修改如下内容

...
const ESLintPlugin = require('eslint-webpack-plugin')

const config = {
  ...,
  plugins: [
    ...,
    new ESLintPlugin({
      extensions: ["js", "jsx", "ts", "tsx"],
    }),
  ],
};

src/index.tsx 中,添加一个未使用的变量

const unused = "something";

Webpack 将会在控制台出现如下的代码校验警告

Webpack进行代码规范校验

生产环境配置

Webpack 的生产环境配置与开发环境有些不同-我们需要项目代码被打包到文件目录中,并且做一定的优化

  • 不需要热更新/代码规范校验等功能
  • 为打包的文件名生成 hash 串
  • 清空打build目录
  • 压缩代码
  • ......

让我们创建webpack.pro.js并加入以下内容

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

const config = {
  mode: "production",

  entry: {
    main: path.resolve(__dirname, "../src/index.tsx"),
  },
  output: {
    filename: "[name].[contenthash].js",
    publicPath: "",
    path: path.resolve(__dirname, "../build"),
    // 打包前清空输出目录
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/i,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
              "@babel/preset-typescript",
            ],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
    }),
  ],
};

module.exports = config;

配置与开发环境很像,但是又有以下不同

  • 我们将mode设置为 production. Webpack 会自动将process.env.NODE_ENV设置为production.这意味着打包后的代码中不会包含 React 开发者工具

  • output告诉 Webpack 将打包后的资源放到哪里.在我们的项目中,是放在build 目录中.

    • 如果项目中做了代码分离(code split).我们使用[name]标志告诉 Webpack 分离后的文件名称
    • 同时将[contenthash]标志加入到文件名称中.以便在代码内容更改后,打包以生成新的文件名称。这就可以避免浏览器缓存旧的文件
    • clean: true用来在每次打包构建前清空build目录,而不需要额外的插件,比如CleanWebpackPlugin

为生产环境添加 NPM 脚本

让我们为生产环境添加 NPM 脚本

  ...,
  "scripts": {
    "build": "webpack --config config/webpack.pro.js",
  },
  ...

该脚本可以启动 Webpack 打包流程。 我们使用config选项来引用我们刚刚创建的生产配置文件。

在终端运行以下命令:

npm run build

N 秒后,Webpack 将会在 build 目录生成打包后的文件

如果我们查看 JavaScript 文件,可以发现它是被压缩过的。因为 Webpack 在生产模式会使用TerserWebpackPlugin来压缩代码。

打包后的 JavaScript 文件也包含了我们应用程序中的所有代码以及 reactreact-dom 依赖包中的代码。

如果我们查看 html 文件,会发现所有空格/换行都已被删除。 如果仔细观察,我们会看到一个 script 元素,该元素是通过HtmlWebpackPlugin自动插入的,以便引用打包后的 JavaScript 文件。

打包后的HTML

抽离 Webpack 的公共配置

虽然,我们将 生产环境 和 开发环境 做了区分,但是我们还是应该遵循不重复原则(Don't repeat yourself - DRY),保留一个 "common(通用)" 配置。为了将这些配置合并在一起,我们使用一个名为 webpack-merge 的工具。此工具会引用 "common" 配置,因此我们不必再在环境特定(environment-specific)的配置中编写重复代码。 参考文档

我们先从安装 webpack-merge 开始

yarn add  webpack-merge --dev

添加 config/webpack.common.js 文件并加入以下配置

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

module.exports = {
  entry: {
    main: path.resolve(__dirname, "../src/index.tsx"),
  },

  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/i,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
              "@babel/preset-typescript",
            ],
            plugins: [
              [
                "@babel/plugin-transform-runtime",
                {
                  regenerator: true,
                },
              ],
            ],
          },
        },
      },
    ],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "../src"),
    },
    extensions: [".tsx", ".ts", ".js"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "React Build",
      template: "public/index.html",
    }),
  ],
};

修改 config/webpack.dev.js 的配置

const path = require("path");
const webpack = require("webpack");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");

const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "development",
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "../build"),
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new ForkTsCheckerWebpackPlugin({
      async: false,
    }),
    new ESLintPlugin({
      extensions: ["js", "jsx", "ts", "tsx"],
    }),
  ],
  devtool: "inline-source-map",
  devServer: {
    contentBase: path.join(__dirname, "../build"),
    historyApiFallback: true,
    port: 4000,
    hot: true,
  },
});

修改config/webpack.prod.js的配置

const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "production",
  output: {
    filename: "[name].[contenthash].js",
    publicPath: "",
    path: path.resolve(__dirname, "../build"),
    // 打包前清空输出目录
    clean: true,
  },
});

webpack.common.js 中,我们设置了 entryoutput 配置,并且在其中引入了开发/生产环境公用的全部插件。

webpack.dev.js 中,我们将 mode 设置为 development,并且为此环境添加了推荐的 devtool(强大的 source map)和简单的 devServer 配置。

webpack.prod.js 中,我们将 mode 设置为 production

随便运行下 NPM 脚本,然后查看输出结果的变化都能按预期所展示


完美! 😊 现在我们的项目已经准备就绪,并可以有效地开发 React 和 TypeScript 应用程序了。通过 build 命令也可以轻松地将项目集成到 CI / CD 流程中

最后

下一篇文章是 「React Build」之三集成 css/less/sass/antd design.敬请期待

如果本文对你有帮助的话,给本文点个赞吧。

参考文档

  1. Webpack5 新特性业务落地实战-字节前端