7、React 应用

作者: Brinnatt 分类: python 道 发布时间: 2023-05-21 09:59

7.1、React 脚手架

7.1.1、开发模式配置

webpack.dev.js

// webpack.dev.js
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

const getStyleLoaders = (preProcessor) => {
  return [
    "style-loader",
    "css-loader",
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            "postcss-preset-env", // 能解决大多数样式兼容性问题
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
    path: undefined,
    filename: "static/js/[name].js",
    chunkFilename: "static/js/[name].chunk.js",
    assetModuleFilename: "static/js/[hash:10][ext][query]",
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: getStyleLoaders(),
          },
          {
            test: /\.less$/,
            use: getStyleLoaders("less-loader"),
          },
          {
            test: /\.s[ac]ss$/,
            use: getStyleLoaders("sass-loader"),
          },
          {
            test: /\.styl$/,
            use: getStyleLoaders("stylus-loader"),
          },
          {
            test: /\.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
              },
            },
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
          },
          {
            test: /\.(jsx|js)$/,
            include: path.resolve(__dirname, "../src"),
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
              cacheCompression: false,
              plugins: [
                // "@babel/plugin-transform-runtime", // presets中包含了
                "react-refresh/babel", // 开启js的HMR功能
              ],
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules",
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    new ReactRefreshWebpackPlugin(), // 解决js的HMR功能运行时全局变量的问题
    // 将public下面的资源复制到dist目录去(除了index.html)
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"),
          to: path.resolve(__dirname, "../dist"),
          toType: "dir",
          noErrorOnMissing: true, // 不生成错误
          globOptions: {
            // 忽略文件
            ignore: ["**/index.html"],
          },
          info: {
            // 跳过terser压缩js
            minimized: true,
          },
        },
      ],
    }),
  ],
  optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
  resolve: {
    extensions: [".jsx", ".js", ".json"], // 自动补全文件扩展名,让jsx可以使用
  },
  devServer: {
    open: true,
    host: "localhost",
    port: 3000,
    hot: true,
    compress: true,
    historyApiFallback: true, // 解决react-router刷新404问题
  },
  mode: "development",
  devtool: "cheap-module-source-map",
};

7.1.2、生产模式配置

webpack.prod.js

// webpack.prod.js
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    "css-loader",
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            "postcss-preset-env", // 能解决大多数样式兼容性问题
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "static/js/[name].[contenthash:10].js",
    chunkFilename: "static/js/[name].[contenthash:10].chunk.js",
    assetModuleFilename: "static/js/[hash:10][ext][query]",
    clean: true,
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: getStyleLoaders(),
          },
          {
            test: /\.less$/,
            use: getStyleLoaders("less-loader"),
          },
          {
            test: /\.s[ac]ss$/,
            use: getStyleLoaders("sass-loader"),
          },
          {
            test: /\.styl$/,
            use: getStyleLoaders("stylus-loader"),
          },
          {
            test: /\.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
              },
            },
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
          },
          {
            test: /\.(jsx|js)$/,
            include: path.resolve(__dirname, "../src"),
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
              cacheCompression: false,
              plugins: [
                // "@babel/plugin-transform-runtime" // presets中包含了
              ],
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules",
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:10].css",
      chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
    }),
    // 将public下面的资源复制到dist目录去(除了index.html)
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"),
          to: path.resolve(__dirname, "../dist"),
          toType: "dir",
          noErrorOnMissing: true, // 不生成错误
          globOptions: {
            // 忽略文件
            ignore: ["**/index.html"],
          },
          info: {
            // 跳过terser压缩js
            minimized: true,
          },
        },
      ],
    }),
  ],
  optimization: {
    // 压缩的操作
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserWebpackPlugin(),
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [
              ["gifsicle", { interlaced: true }],
              ["jpegtran", { progressive: true }],
              ["optipng", { optimizationLevel: 5 }],
              [
                "svgo",
                {
                  plugins: [
                    "preset-default",
                    "prefixIds",
                    {
                      name: "sortAttrs",
                      params: {
                        xmlnsOrder: "alphabetical",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
  resolve: {
    extensions: [".jsx", ".js", ".json"],
  },
  mode: "production",
  devtool: "source-map",
};

7.1.3、合并优化

将开发环境和生产环境合并成一个配置,并进行适当优化。所谓优化,就是通过第三方库、插件、loader等工具,解决当前问题或者提高开发生产效率。

7.1.3.1、webpack.config.js

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

const isProduction = process.env.NODE_ENV === "production";

const getStyleLoaders = (preProcessor) => {
  return [
    isProduction ? MiniCssExtractPlugin.loader : "style-loader",
    "css-loader",
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            "postcss-preset-env",
          ],
        },
      },
    },
    preProcessor && {
      loader: preProcessor,
      options:
        preProcessor === "less-loader"
          ? {
              // antd的自定义主题
              lessOptions: {
                modifyVars: {
                  // 其他主题色:https://ant.design/docs/react/customize-theme-cn
                  "@primary-color": "#1DA57A", // 全局主色
                },
                javascriptEnabled: true,
              },
            }
          : {},
    },
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
    path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
    filename: isProduction
      ? "static/js/[name].[contenthash:10].js"
      : "static/js/[name].js",
    chunkFilename: isProduction
      ? "static/js/[name].[contenthash:10].chunk.js"
      : "static/js/[name].chunk.js",
    assetModuleFilename: "static/js/[hash:10][ext][query]",
    clean: true,
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.css$/,
            use: getStyleLoaders(),
          },
          {
            test: /\.less$/,
            use: getStyleLoaders("less-loader"),
          },
          {
            test: /\.s[ac]ss$/,
            use: getStyleLoaders("sass-loader"),
          },
          {
            test: /\.styl$/,
            use: getStyleLoaders("stylus-loader"),
          },
          {
            test: /\.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024,
              },
            },
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
          },
          {
            test: /\.(jsx|js)$/,
            include: path.resolve(__dirname, "../src"),
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
              cacheCompression: false,
              plugins: [
                // "@babel/plugin-transform-runtime",  // presets中包含了
                !isProduction && "react-refresh/babel",
              ].filter(Boolean),
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      extensions: [".js", ".jsx"],
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules",
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    isProduction &&
      new MiniCssExtractPlugin({
        filename: "static/css/[name].[contenthash:10].css",
        chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
      }),
    !isProduction && new ReactRefreshWebpackPlugin(),
    // 将public下面的资源复制到dist目录去(除了index.html)
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"),
          to: path.resolve(__dirname, "../dist"),
          toType: "dir",
          noErrorOnMissing: true, // 不生成错误
          globOptions: {
            // 忽略文件
            ignore: ["**/index.html"],
          },
          info: {
            // 跳过terser压缩js
            minimized: true,
          },
        },
      ],
    }),
  ].filter(Boolean),
  optimization: {
    minimize: isProduction,
    // 压缩的操作
    minimizer: [
      // 压缩css
      new CssMinimizerPlugin(),
      // 压缩js
      new TerserWebpackPlugin(),
      // 压缩图片
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [
              ["gifsicle", { interlaced: true }],
              ["jpegtran", { progressive: true }],
              ["optipng", { optimizationLevel: 5 }],
              [
                "svgo",
                {
                  plugins: [
                    "preset-default",
                    "prefixIds",
                    {
                      name: "sortAttrs",
                      params: {
                        xmlnsOrder: "alphabetical",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
    // 代码分割配置
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        // layouts通常是admin项目的主体布局组件,所有路由组件都要使用的
        // 可以单独打包,从而复用
        // 如果项目中没有,请删除
        layouts: {
          name: "layouts",
          test: path.resolve(__dirname, "../src/layouts"),
          priority: 40,
        },
        // 如果项目中使用antd,此时将所有node_modules打包在一起,那么打包输出文件会比较大。
        // 所以我们将node_modules中比较大的模块单独打包,从而并行加载速度更好
        // 如果项目中没有,请删除
        antd: {
          name: "chunk-antd",
          test: /[\\/]node_modules[\\/]antd(.*)/,
          priority: 30,
        },
        // 将react相关的库单独打包,减少node_modules的chunk体积。
        react: {
          name: "react",
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          chunks: "initial",
          priority: 20,
        },
        libs: {
          name: "chunk-libs",
          test: /[\\/]node_modules[\\/]/,
          priority: 10, // 权重最低,优先考虑前面内容
          chunks: "initial",
        },
      },
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
  resolve: {
    extensions: [".jsx", ".js", ".json"],
  },
  devServer: {
    open: true,
    host: "localhost",
    port: 3000,
    hot: true,
    compress: true,
    historyApiFallback: true,
  },
  mode: isProduction ? "production" : "development",
  devtool: isProduction ? "source-map" : "cheap-module-source-map",
  performance: false, // 关闭性能分析,提示速度
};

7.1.3.2、package.json

{
  "name": "webpackjs",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.17.10",
    "@babel/plugin-proposal-decorators": "^7.21.0",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
    "babel-loader": "^8.2.5",
    "babel-preset-react-app": "^10.0.1",
    "copy-webpack-plugin": "^11.0.0",
    "cross-env": "^7.0.3",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^3.4.1",
    "eslint-config-react-app": "^7.0.1",
    "eslint-webpack-plugin": "^3.1.1",
    "html-webpack-plugin": "^5.5.0",
    "image-minimizer-webpack-plugin": "^3.2.3",
    "imagemin": "^8.0.1",
    "imagemin-gifsicle": "^7.0.0",
    "imagemin-jpegtran": "^7.0.0",
    "imagemin-optipng": "^8.0.0",
    "imagemin-svgo": "^10.0.1",
    "less-loader": "^10.2.0",
    "mini-css-extract-plugin": "^2.6.0",
    "react-refresh": "^0.13.0",
    "sass-loader": "^12.6.0",
    "style-loader": "^3.3.1",
    "stylus-loader": "^6.2.0",
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.9.0"
  },
  "dependencies": {
    "antd": "^5.5.0",
    "axios": "^1.4.0",
    "mobx": "^6.9.0",
    "mobx-react": "^7.6.0",
    "prop-types": "^15.8.1",
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "react-router": "^6.11.1",
    "react-router-dom": "^6.3.0"
  },
  "browserslist": [
    "last 2 version",
    "> 1%",
    "not dead"
  ]
}

devDependencies 开发时依赖,不会打包到目标文件中。对应 npm install xxx --save-dev 。例如 babel 的一些依赖,只是为了帮我们转译代码,没有必要发布到生产环境中。

dependencies 运行时依赖,会打包到项目中。对应 npm install xxx --save 。

版本号指定:

版本号:只安装指定版本号的。

~版本号:例如 ~1.2.3 表示安装 1.2.x 中最新版本,不低于 1.2.3,但不能安装 1.3.x

^版本号:例如 ^2.4.1 表示安装 2.x.x 最新版本不低于 2.4.1

latest:安装最新版本。

babel 转译,因为开发用了很多 ES6 语法。从 6.x 开始 babel 拆分成很多插件,需要什么引入什么。

babel-core 核心。

babel-loader webpack 的 loader,webpack 通过 loader 增强功能。

babel-preset-xxx 预设的转换插件。

antd ant design,基于 react 实现,蚂蚁金服开源的 react 的 UI 库。做中后台管理非常方便。

axios 异步请求支持。

react 开发的主框架

react-dom 支持 DOM

react-router 支持路由

react-router-dom DOM 绑定路由

mobx 状态管理库,透明化。

mobx-react,mobx 和 react 结合的模块。mobx 6 版本以前还有个 mobx-react-devtools 工具,6 版本后就废弃了。

react 和 mobx 是一个强强联合。

其它配置解释参见前面的 webpack 打包工具。

7.1.3.3、.eslintrc.js

module.exports = {
  extends: ["react-app"], // 继承 react 官方规则
  parserOptions: {
    babelOptions: {
      presets: [
        // 解决页面报错问题
        ["babel-preset-react-app", false],
        "babel-preset-react-app/prod",
      ],
    },
  },
};

7.1.3.4、babel.config.js

module.exports = {
  // 使用react官方规则
  presets: ["react-app"],
};

7.1.4、项目构建

在项目根目录下创建合并优化后的 4 个配置文件,然后执行下面的命令,就会自动按照 package.json 的配置安装依赖模块。

$ npm install

安装完成后,会生成一个目录 node_modules ,里面是安装的所有依赖的模块。

安装过程需要科学上网,否则很多依赖无法完成安装。

创建测试文件,最终目录文件结构如下:

webpackjs/
├── babel.config.js
├── config
│   └── webpack.config.js
├── .eslintrc.js
├── package.json
├── package-lock.json
├── public
│   └── index.html
└── src
    ├── App.jsx
    └── main.js

[root@brinnatt ~]# cat webpackjs/public/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>My React Tool</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

[root@brinnatt ~]# cat webpackjs/src/main.js 
import React from "react";
import ReactDOM from "react-dom/client";
import App from './App';

const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<App />);

[root@brinnatt ~]# cat webpackjs/src/App.jsx 
import React from "react";

function App() {
    return <h1>App</h1>
}

export default App;

运行指令:

npm start

以上实验测试通过,注意,该目录结构是对应配置文件中的 path,不能随意修改。另外,插件、loader、三方库版本都有兼容性,如果版本不匹配,请参考官方文档 https://www.npmjs.com/

7.2、React 应用

7.2.1、React 简介

React 是 Facebook 开发并开源的前端框架。

当时他们的团队在市面上没有找到合适的 MVC 框架,就自己写了一个 Js 框架,用来开发大名鼎鼎的 Instagram(图片分享社交网络)。2013 年 React 开源。

React 解决的是前端 MVC 框架中的 View 视图层的问题。

7.2.1.1、Virtual DOM

DOM(文档对象模型 Document Object Model)

react_virtualdom

react_virtualdom1

将网页内所有内容映射到一棵树型结构的层级对象模型上,浏览器提供对 DOM 的支持,用户可以用脚本调用 DOM API 来动态的修改 DOM 结点,从而达到修改网页的目的,这种修改在浏览器中完成,浏览器会根据 DOM 的改变重绘改变的 DOM 结点部分。

修改 DOM 重新渲染代价太高,前端框架为了提高效率,尽量减少 DOM 的重绘,提出了 Virtual DOM,所有的修改都是先在 Virtual DOM 上完成的,通过比较算法,找出浏览器 DOM 之间的差异,使用这个差异操作 DOM,浏览器只需要渲染这部分变化就行了。

React 实现了 DOM Diff 算法可以高效比对 Virtual DOM 和 DOM 的差异。

7.2.1.2、支持 JSX 语法

JSX 是一种 JavaScript 和 XML 混写的语法,是 JavaScript 的扩展。

React.render(
    <div>
        <div>
            <div>content</div>
        </div>
    </div>,
    document.getElementById('example')
);

7.2.1.3、测试程序

替换 ./src/main.js 为下面的代码:

import React from "react";
import ReactDOM from "react-dom/client";
import Root from './App';

const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<Root />);

替换 ./src/App.jsx 为下面的代码:

import React from 'react';

export default class Root extends React.Component {
    render() {
        return <div><h1>React v18.x test page</h1></div>;
    }
}

保存文件后,会自动编译,并重新装载刷新浏览器端页面。

import React from 'react'; 导入 react 模块。

import ReactDOM from "react-dom/client"; 导入 react 的 DOM 模块。

class Root extends React.Component 组件类定义,从 React.Component 类上继承。这个类生成 JSXElement 对象即 React 元素。

render() 渲染函数。返回组件中渲染的内容。注意,只能返回唯一 一个顶级元素回去。

root = ReactDOM.createRoot(document.getElementById("app")); 定位 DOM Element 元素,创建一个根对象。

root.render(<Root />); 将 React JSXElement 元素添加到 DOM 的 Element 元素中并渲染。

增加一个子元素:

import React from 'react';

class SubEle extends React.Component {
    render() {
        return (<div>
            <h2>Second Content</h2>
        </div>);
    }
}

export default class Root extends React.Component {
    render() {
        return (<div>
            <h1>First Content</h1>
            <br />
            <SubEle />
        </div>);
    }
}

注意:

1、React 组件的 render 函数 return,只能是一个顶级元素。
2、JSX 语法是 XML,要求所有元素必须闭合,注意 <br /> 不能写成 <br>

7.2.2、JSX 规范

  • 标签中首字母小写就是 html 标记,首字母大写就是 组件
  • 要求严格的 HTML 标记,要求所有标签都必须闭合。br 也应该写成 <br />/ 前留一个空格。
  • 单行省略小括号,多行请使用小括号。
  • 元素有嵌套,建议多行,注意缩进。
  • JSX 表达式:使用 括起来,如果大括号内使用了引号,会当做字符串处理,例如 <div>{'2>1?true:false'}</div> 里面的表达式成了字符串了。

7.2.2、组件状态 state

每一个 React组件 都有一个状态变量 state,它是一个 JavaScript 对象,可以为它定义属性来保存值。

如果状态变化了,会触发 UI 的重新渲染。使用 setState() 方法可以修改 state 值。

注意:state 是组件自己内部使用的,是组件私有的属性。

依然修改 ./src/App.jsx

import React from 'react';

class SubEle extends React.Component {
    render() {
        return <div>
            <h2>Second Content</h2>
        </div>;
    }
}

export default class Root extends React.Component {
    // 定义一个对象
    state = {
        p1: 'brinnatt',
        p2: '.com'
    };

    render() {
        setTimeout(() => this.setState({ p1: 'www.brinnatt' }), 4000)
        return (
            <div>
                <div>Welcome to {this.state.p1}{this.state.p2}</div>
                <br />
                <SubEle />
            </div>
        );
    }
}

7.2.3、复杂状态例子

先看一个网页:

<html>

<head>
  <script type="text/javascript">
    function getEventTrigger(event) {
      x = event.target; // 从事件中获取元素
      alert("触发的元素的id是:" + x.id);
    }
  </script>
</head>

<body>
  <div id="t1" onmousedown="getEventTrigger(event)">
    点击这句话,会触发一个事件,并弹出一个警示框
  </div>
</body>

</html>

div 的 id 是 t1,鼠标按下事件捆绑了一个函数,只要鼠标按下就会触发调用 getEventTrigger 函数,浏览器会送给它一个参数 event。event 是事件对象,当事件触发时,event 包含触发这个事件的对象。

HTML DOM 的 JavaScript 事件

属性 此事件发生在何时
onabort 图像的加载被中断
onblur 元素失去焦点
onchange 域的内容被改变
onclick 当用户点击某个对象时调用的事件句柄
ondblclick 当用户双击某个对象时调用的事件句柄
onerror 在加载文档或图像时发生错误
onfocus 元素获得焦点
onkeydown 某个键盘按键被按下
onkeypress 某个键盘按键被按下并松开
onkeyup 某个键盘按键被松开
onload 一张页面或一幅图像完成加载
onmousedown 鼠标按钮被按下
onmousemove 鼠标被移动
onmouseout 鼠标从某元素移开
onmouseover 鼠标移到某元素之上
onmouseup 鼠标按键被松开
onreset 重置按钮被点击
onresize 窗口或框架被重新调整大小
onselect 文本被选中
onsubmit 确认按钮被点击
onunload 用户退出页面

使用 React 实现上面的传统的 HTML:

import React from 'react';

class Toggle extends React.Component {
    state = { flag: true };
    handleClick(event) {
        console.log(event.target.id);
        console.log(event.target === this);
        console.log(this);
        console.log(this.state);
        this.setState({ flag: !this.state.flag });
    }
    render() {
        return (
            <div id='t1' onClick={this.handleClick.bind(this)}>
                点击这句话,会触发一个事件。{this.state.flag.toString()}
            </div>
        )
    }
}

export default class Root extends React.Component {
    // 定义一个对象
    state = {
        p1: 'www.brinnatt',
        p2: '.com'
    };

    render() {
        setTimeout(() => this.setState({ p1: 'python.brinnatt' }), 4000)
        return (
            <div>
                <div>Welcome to {this.state.p1}{this.state.p2}</div>
                <br />
                <Toggle />
            </div>
        );
    }
}

分析:

Toggle 类,它有自己的 state 属性。

当 render 完成后,网页上有一个 div 标签,div 标签对象捆绑了一个 click 事件的处理函数,div 标签内有文本内容。

如果通过点击左键,就触发了 click 方法关联的 handleClick 函数,在这个函数里将状态值改变。

状态值 state 的改变将引发 render 重绘。

如果组件自己的 state 变了,只会触发自己的 render 方法重绘。

注意:
this.handleClick.bind(this),不能外加引号。

this.handleClick.bind(this) 一定要绑定 this,否则当触发捆绑的函数时,this 是函数执行的上下文决定的,this 已经不是触发事件的对象了。

console.log(event.target.id),取回产生事件的对象的 id,但是这不是我们封装的组件对象。所以,console.log(event.target===this) 是 false。所以这里一定要用 this,而这个 this 是通过绑定来的。

React 中的事件:

  • 使用小驼峰命名。
  • 使用 JSX 表达式,表达式中指定事件处理函数。
  • 不能使用 return false,如果要阻止事件默认行为,使用 event.preventDefault()。

7.2.4、属性 props

把 React 组件当做标签使用,可以为其增加属性,<Toggle name="school" parent={this} />

为上面的 Toggle 元素增加属性:

1、name = "school",这个属性会作为一个单一的对象传递给组件,加入到组件的 props 属性中。

2、parent = {this},注意这个 this 是在 Root 元素中,指的是 Root 组件本身。

3、在 Root 中使用 JSX 语法为 Toggle 增加子元素,这些子元素也会被加入 Toggle 组件的 props.children 中。

import React from 'react';

class Toggle extends React.Component {
    state = { flag: true }; // 类中定义 state
    handleClick(event) {
        console.log(event.target.id);
        console.log(event.target === this);
        console.log(this);
        console.log(this.state);
        this.setState({ flag: !this.state.flag });
    }
    render() { /* 注意一定要绑定this onClick写成小驼峰 */
        return (
            <div id='t1' onClick={this.handleClick.bind(this)}>
                点击这句话,会触发一个事件。{this.state.flag.toString()}<br />
                显示props<br />
                {this.props.name} : {this.props.parent.state.p1 + this.props.parent.state.p2}<br />
                {this.props.children}
            </div>
        );
    }
}

export default class Root extends React.Component {
    // 定义一个对象
    state = {
        p1: 'www.brinnatt',
        p2: '.com'
    };

    render() {
        setTimeout(() => this.setState({ p1: 'python.brinnatt' }), 4000)
        return (
            <div>
                <div>Welcome to {this.state.p1}{this.state.p2}</div>
                <br />
                <Toggle name="school" parent={this}>{/*自定义2个属性通过props传给Toggle组件对象*/}
                    <hr />{/*子元素通过props.children访问*/}
                    <span>我是Toggle元素的子元素</span>{/*子元素通过props.children访问*/}
                </Toggle>
            </div>
        );
    }
}

尝试修改 props 中的属性值,会抛出: TypeError: Cannot assign to read only property 'name' of object '#<Object>' 异常。

应该说,state 是私有 private 属性,组件外无法直接访问。可以修改 state,但是建议使用 setState 方法。

props 是公有 public 属性,组件外也可以访问,但只读。

7.2.5、构造器 constructor

使用 ES6 的构造器,要提供一个参数 props,并把这个参数使用 super 传递给父类。

import React from 'react';

class Toggle extends React.Component {
    constructor(props) {
        super(props); // 一定要调用super父类构造器,否则报错
        this.state = { flag: true }; // 类中定义 state
    }

    handleClick(event) {
        console.log(event.target.id);
        console.log(event.target === this);
        console.log(this);
        console.log(this.state);
        this.setState({ flag: !this.state.flag });
    }
    render() { /* 注意一定要绑定this onClick写成小驼峰 */
        return (
            <div id='t1' onClick={this.handleClick.bind(this)}>
                点击这句话,会触发一个事件。{this.state.flag.toString()}<br />
                显示props<br />
                {this.props.name} : {this.props.parent.state.p1 + this.props.parent.state.p2}<br />
                {this.props.children}
            </div>
        );
    }
}

export default class Root extends React.Component {
    // 定义一个对象
    constructor(props) {
        super(props); // 一定要调用super父类构造器,否则报错
        this.state = {
            p1: 'www.brinnatt',
            p2: '.com'
        }; // 构造函数中定义state
    }

    render() {
        setTimeout(() => this.setState({ p1: 'python.brinnatt' }), 4000)
        return (
            <div>
                <div>Welcome to {this.state.p1}{this.state.p2}</div>
                <br />
                <Toggle name="schools" parent={this}>{/*自定义2个属性通过props传给Toggle组件对象*/}
                    <hr />{/*子元素通过props.children访问*/}
                    <span>我是Toggle元素的子元素</span>{/*子元素通过props.children访问*/}
                </Toggle>
            </div>
        );
    }
}

7.2.6、组件的生命周期

组件的生命周期可分成三个状态:

  • Mounting:已插入真实 DOM。

  • Updating:正在被重新渲染。

  • Unmounting:已移出真实 DOM。

组件的生命周期状态,说明在不同时机访问组件,组件正处在生命周期的不同状态上。

在不同的生命周期状态访问,就产生不同的方法。

生命周期的方法如下:

  • 装载组件触发

    • componentWillMount 在渲染前调用,在客户端也在服务端。只会在装载之前调用一次。

    • componentDidMount : 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的 DOM 结构,可以通过 this.getDOMNode() 来进行访问。 如果你想和其他 JavaScript 框架一起使用,可以在这个方法中调用 setTimeout, setInterval 或者发送 AJAX 请求等操作(防止异部操作阻塞 UI)。只在装载完成后调用一次,在 render 之后。

  • 更新组件触发。这些方法不会在首次 render 组件的周期调用。

    • componentWillReceiveProps(nextProps) 在组件接收到一个新的prop时被调用。这个方法在初始化 render 时不会被调用。

    • shouldComponentUpdate(nextProps, nextState) 返回一个布尔值。在组件接收到新的 props 或者 state 时被调用。在初始化时或者使用 forceUpdate 时不被调用。

    • 可以在你确认不需要更新组件时使用。

    • 如果设置为 false,就是不允许更新组件,那么 componentWillUpdate、componentDidUpdate 不会执行。

    • componentWillUpdate(nextProps, nextState) 在组件接收到新的 props 或者 state 但还没有 render 时被调用。在初始化时不会被调用。

    • componentDidUpdate(prevProps, prevState) 在组件完成更新后立即调用。在初始化时不会被调用。

  • 卸载组件触发

    • componentWillUnmount 在组件从 DOM 中移除的时候立刻被调用。

react_component

由图可知

constructor 构造器是最早执行的函数。触发 更新生命周期函数,需要更新 state 或 props。

因此,重新编写/src/App.jsx。构造两个组件,在子组件 Sub 中,加入所有生命周期函数。

下面的例子添加是装载、卸载组件的生命周期函数:

import React from 'react';

class Sub extends React.Component {
    constructor(props) {
        console.log('Sub constructor')
        super(props); // 调用父类构造器
        this.state = { count: 0 };
    }
    handleClick(event) {
        this.setState({ count: this.state.count + 1 });
    }
    render() {
        console.log('Sub render');
        return (<div id="sub" onClick={this.handleClick.bind(this)}>
            Sub's count = {this.state.count}
        </div>);
    }
    componentWillMount() {
        // constructor之后,第一次render之前
        console.log('Sub componentWillMount');
    }
    componentDidMount() {
        // 第一次render之后
        console.log('Sub componentDidMount');
    }
    componentWillUnmount() {
        // 清理工作
        console.log('Sub componentWillUnmount');
    }
}

export default class Root extends React.Component {
    constructor(props) {
        console.log('Root Constructor')
        super(props); // 调用父类构造器
        // 定义一个对象
        this.state = {};
    }
    render() {
        return (
            <div>
                <Sub />
            </div>);
    }
}

上面可以看到顺序是:

constructor -> componentWillMount -> render -> componentDidMount ----state或props改变----> render

增加更新组件函数,为了演示 props 的改变,为 Root 元素增加一个 click 事件处理函数:

import React from 'react';

class Sub extends React.Component {
    constructor(props) {
        console.log('Sub constructor')
        super(props); // 调用父类构造器
        this.state = { count: 0 };
    }
    handleClick(event) {
        this.setState({ count: this.state.count + 1 });
    }
    render() {
        console.log('Sub render');
        return (<div style={{ height: 400 + 'px', color: 'yellow', backgroundColor: 'pink' }}>
            <button id="sub" onClick={this.handleClick.bind(this)}>
                Sub's count = {this.state.count}
            </button>
        </div>);
    }
    componentWillMount() {
        // constructor之后,第一次render之前
        console.log('Sub componentWillMount');
    }
    componentDidMount() {
        // 第一次render之后
        console.log('Sub componentDidMount');
    }
    componentWillUnmount() {
        // 清理工作
        console.log('Sub componentWillUnmount');
    }
    componentWillReceiveProps(nextProps) {
        // props变更时,接到新props了,交给shouldComponentUpdate。
        // props组件内只读,只能从外部改变
        console.log(this.props);
        console.log(nextProps);
        console.log('Sub componentWillReceiveProps', this.state.count);
    }
    shouldComponentUpdate(nextProps, nextState) {
        // 是否组件更新,props或state方式改变时,返回布尔值,true才会更新
        console.log('Sub shouldComponentUpdate', this.state.count, nextState);
        return true; // return false将拦截更新
    }
    componentWillUpdate(nextProps, nextState) {
        // 同意更新后,真正更新前,之后调用render
        console.log('Sub componentWillUpdate', this.state.count, nextState);
    }
    componentDidUpdate(prevProps, prevState) {
        // 同意更新后,真正更新后,在render之后调用
        console.log('Sub componentDidUpdate', this.state.count, prevState);
    }
}

export default class Root extends React.Component {
    constructor(props) {
        console.log('Root Constructor')
        super(props); // 调用父类构造器
        // 定义一个对象
        this.state = { flag: true, name: 'root' };
    }
    handleClick(event) {
        this.setState({
            flag: !this.state.flag,
            name: this.state.flag ? this.state.name.toLowerCase() : this.state.name.toUpperCase()
        });
    }
    render() {
        return (
            <div id="root" onClick={this.handleClick.bind(this)}>
                My Name is {this.state.name}
                <hr />
                <Sub /> {/*父组件的render,会引起下一级组件的更新流程,导致props重新发送,即使子组件props没有
改变过*/}
            </div>);
    }
}

componentWillMount 第一次装载,在首次 render 之前。例如控制 state、props。

componentDidMount 第一次装载结束,在首次 render 之后。例如控制 state、props。

componentWillReceiveProps 在组件内部,props 是只读不可变的,但是这个函数可以接收到新的 props,可以对 props 做一些处理,this.props = {name:'roooooot'}; 这就是偷梁换柱。componentWillReceiveProps 触发,也会走 shouldComponentUpdate。

shouldComponentUpdate 判断是否需要组件更新,就是是否 render,精确的控制渲染,提高性能。

componentWillUpdate 在除了首次 render 外,每次 render 前执行,componentDidUpdate 在 render 之后调用。

不过,大多数时候,用不上这些函数,这些钩子函数是为了精确的控制。

7.2.7、无状态组件

React 从15.0 开始支持无状态组件,定义如下:

import React from 'react';

export default function Root(props) {
return <div>{props.schoolName}</div>;
}

无状态组件,也叫 函数式组件

开发中,很多情况下,组件其实很简单,不需要 state 状态,也不需要使用生命周期函数。无状态组件很好的满足了需要。

无状态组件函数应该提供一个参数 props,返回一个 React 元素。

无状态组件函数本身就是 render 函数。

改写上面的代码:

import React from 'react';

let Root = props => <div>{props.schoolName}</div>;

export default Root;

7.2.8、高阶组件

let Root = props => <div>{props.schoolName}</div>;

如果要给上例的 Root 组件增强功能怎么办?例如在 Root 组件的 div 外部再加入其它 div。

let Wrapper = function (Component, props) {
    return (
        <div>
            {props.schoolName}
            <hr />
            <Component />
        </div>
    );
};

let Root = props => <div>{props.schoolName}</div>;

export default Root;

为了以后用的方便,柯里化这个 Wrapper 函数。

App.jsx

import React from "react";

let Wrapper = function (Component) {
    function _wrapper(props) {
        return (
            <div>
                {props.schoolName}
                <hr />
                <Component />
            </div>
        );
    }
    return _wrapper;
};

let Root = props => <div>Root</div>;
let NewComp = Wrapper(Root) // 返回一个包装后的元素

export default NewComp;

main.js

import React from "react";
import ReactDOM from "react-dom";
import NewComp from './App';

const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<NewComp schoolName='brinnatt'/>);

简化 Wrapper:

let Wrapper = function (Component) {
    return (props) => {
        return (
            <div>
                {props.schoolName}
                <hr />
                <Component />
            </div>
        );
    };
};

再次变化:

let Wrapper = function (Component) {
    return props =>
    (
        <div>
            {props.schoolName}
            <hr />
            <Component />
        </div>
    );
};

再次变化:

import React from "react";

let Wrapper = Component => props => (
    <div>
        {props.schoolName}
        <hr />
        <Component />
    </div>
);

let Root = props => <div>Root</div>;
let NewComp = Wrapper(Root) // 返回一个包装后的元素

export default NewComp;

7.2.9、装饰器

新版 ES 2016 中增加了装饰器的支持,因此可以使用装饰器来改造上面的代码。

@Wrapper // 这是不对的,装饰器装饰函数或类
let Root = props => <div>Root</div>;

ES 2016 的装饰器只能装饰类,所以,将 Root 改写成类。

App.jsx

import React from 'react';

let Wrapper = Component => props => (
    <div>
        {props.schoolName}
        <hr />
        <Component />
    </div>
);

@Wrapper
class Root extends React.Component {
    render() {
        return <div>Root</div>;
    }
}

export default Root;

main.js

import React from "react";
import ReactDOM from "react-dom";
import Root from './App';

const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<Root schoolName='brinnatt'/>);

项目运行时会报错,如下:

Failed to compile.
Syntax error: Support for the experimental syntax 'decorators' isn't currently enabled (12:1):
src/App.jsx

  10 |
  11 |
> 12 | @Wrapper
     | ^
  13 | class Root extends React.Component {
  14 |     render() {
  15 |         return <div>Root</div>;

Add @babel/plugin-proposal-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-proposal-decorators) to the 'plugins' section of your Babel config to enable transformation.
If you want to leave it as-is, add @babel/plugin-syntax-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators) to the 'plugins' section to enable parsing.

因为上面用到了装饰器新语法,传统 babel-loader 还不足以支撑该新语法,按照提示,需要使用 @babel/plugin-proposal-decorators 辅助 babel-loader。

实际上看我们的 package.json 文件,"devDependencies" 里面有 "@babel/plugin-proposal-decorators",之前 npm install 会安装 package.json 中所有的依赖包,没有生效是因为还没有增加 babel 配置。

安装 babel 官网 https://babeljs.io/docs/babel-plugin-proposal-decorators 配置如下:

babel.config.json

module.exports = {
    // 使用react官方规则
    presets: ["react-app"],
    "plugins": [
        ["@babel/plugin-proposal-decorators", { "version": "2023-01" }],
    ],
};

遇到另外一个报错:

ERROR in [eslint]
D:\learning\webpackjs\src\App.jsx
  12:0  error  Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (12:0)

✖ 1 problem (1 error, 0 warnings)

ESLint 默认情况下不会识别 Babel 的配置,因此需要进行额外的配置才能使 ESLint 使用 Babel 解析器和插件。

安装依赖包:

npm install @babel/eslint-parser eslint-plugin-babel -D

.eslintrc.js 配置如下:

module.exports = {
    extends: ["react-app"], // 继承 react 官方规则
    parserOptions: {
        babelOptions: {
            presets: [
                // 解决页面报错问题
                ["babel-preset-react-app", false],
                "babel-preset-react-app/prod",
            ],
        },
    },
    "parser": "@babel/eslint-parser",
    "plugins": ["babel"],
    "rules": {
        "babel/semi": "error"
    }
};

思考题?

如何让 Root 也显示出 schoolName?

import React from 'react';

let Wrapper = Component => props => (
    <div>
        {props.schoolName}
        <hr />
        <Component {...props}/>
    </div>
);

@Wrapper
class Root extends React.Component {
    render() {
        return <div>{this.props.schoolName}</div>;
    }
}

export default Root;

使用 <Component {...props} /> 相当于给组件增加了属性。

7.2.10、带参装饰器

想给装饰器函数增加一个 id 参数:

import React from 'react';

// 带参装饰器函数
let Wrapper = id => Component => props => (
    <div id={id}>
        {props.schoolName}
        <hr />
        <Component {...props}/>
    </div>
);

@Wrapper('wrapper') // 带参
class Root extends React.Component {
    render() {
        return <div>{this.props.schoolName}</div>;
    }
}

export default Root;

通过上面的改写,就得到带参装饰器。

如果觉得不好接受,可以先写成原始的形式,熟悉了箭头函数语法后,再改成精简的形式。

标签云