万字长文 | 从0到1搭建完整React开发框架:webpack@5.x,react@18.x - 持续更新中

2,094 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

概述

本文旨在通过从0到1搭建一套完整的React开发框架来掌握如WebpackReactTypescript CSS preprocessor等各部分是如何协同进行编译开发的,进而能去了解如creat-react-app之类的框架都做了哪些事情。

同时本文还提供了业务层面的如路由react-router、状态管理器redux-toolkit的配置和使用方式。

TIPS: 本文中所有包版本均采用当前最新版本,注意区别于如webpack、react、react-router等模块的旧版本使用方式。

一、初始化项目

yarn init

yarn init或npm init都是OK的

二、构建核心打包环境

  1. 添加依赖

    yarn add webpack webpack-cli webpack-dev-server -D
    
  2. 依赖模块说明

    模块名描述版本
    webpack模块化打包工具,打包代码时的核心依赖^5.72.1
    webpack-cli支持在命令行中执行webpack的工具^4.9.2
    webpack-dev-server本地开发服务器,便于开发^4.9.0
    webpack-merge用于合并webpack-config文件^5.8.0
  3. webpack.config.js 配置文件

    webpack.config.js是webpack的默认配置文件,也可以在命令行中通过--config指定配置文件。

    一般在项目中会区分production、development环境的配置文件,这里我们设计配置文件为:公共配置文件,开发环境配置文件,产品环境配置文件,目录结构如下

    config
      -- webpack.config.base.js
      -- webpack.config.dev.js
      -- webpack.config.prod.js
    

    webpack.config.base.js 公共配置文件

    const path = require("path");
    
    /**
     * @method resolve
     * @description 从根路径开始查找文件
     */
    const resolve = (targetPath) => {
      return path.resolve(__dirname, "..", targetPath);
    };
    
    module.exports = {
      target: "web",
      // 入口文件
      entry: {
        main: resolve("./src/index.js"),
      },
      // 输出
      output: {
        // 文件名称
        filename: "[name].[contenthash].js",
        // 输出目录
        path: resolve("./dist"),
        // 每次编译输出的时候,清空dist目录 - 这里就不需要clean-webpack-plugin了
        clean: true,
        // 所有URL访问的前缀路径
        publicPath: "/",
      },
      resolve: {
        // 定义了扩展名之后,在import文件时就可以不用写后缀名了,会按循序依次查找
        extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".css", ".less"],
        // 设置链接
        alias: {
          // 注意resolve方法开始的查找的路径是/
          "@": resolve("./src"),
        },
      },
    };
    

    webpack.config.dev.js 开发环境配置文件

    // merge,合并两个或多个webpack配置文件
    const { merge } = require("webpack-merge");
    
    // 导入公共配置文件
    const webpackConfigBase = require("./webpack.config.base");
    
    // dev环境下相关配置
    module.exports = merge(webpackConfigBase, {
      // 指定环境
      mode: "development",
      // 输出source-map的方式,增加调试。eval是默认推荐的选择,build fast and rebuild fast!
      devtool: "eval",
      // 本地服务器配置
      devServer: {
        // 启动GZIP压缩
        compress: true,
        // 设置端口号
        port: 3000,
        // 代理请求设置
        proxy: {
          "/api": {
            // 目标域名
            target: "http://xxxx.com:8080",
            // 允许跨域了
            changeOrigin: true,
            // 重写路径 - 根据自己的实际需要处理,不需要直接忽略该项设置即可
            pathRewrite: {
              // 该处理是代码中使用/api开头的请求,如/api/userinfo,实际转发对应服务器的路径是/userinfo
              "^/api": "",
            },
            // https服务的地址,忽略证书相关
            secure: false,
          },
        },
      },
    });
    

    weback.config.build.js 线上产品环境配置文件

    const { merge } = require("webpack-merge");
    const webpackConfigBase = require("./webpack.config.base");
    
    module.exports = merge(webpackConfigBase, {
      // 指定打包环境
      mode: "production",
    });
    
  4. package.json命令行

    在package.json的scripts中添加一下命令:

    scripts: {
      "dev": "webpack serve --config config/webpack.config.dev",
      "build": "webpack build --config config/webpack.config.prod"
    }
    

    开发环境运行:yarn dev 生产环境运行:yarn build

三、添加HTML模板文件

  1. 添加依赖

    yarn add html-webpack-plugin -D
    
  2. 创建模板文件

    目录结构如下

    public
      -- index.html
      -- logo.ico
    
  3. 更新webpack配置

    在webpack.config.base.js中添加如下配置:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      // ...
      plugins: [
        new HtmlWebpackPlugin({
          // HTML模板文件
          template: resolve("./public/index.html"),
          // 收藏夹图标
          favicon: resolve("./public/logo.ico"),
        }),
      ]
      // ...
    }
    

四、解析React

  1. 添加依赖

    # 添加react、react-dom
    yarn add react react-dom
    
    # 添加babel等loader
    yarn add babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react core-js@3 -D
    
  2. 依赖模块说明

    模块名描述版本
    react还用说是啥吗? 核心代码^18.1.0
    react-dom还用说是啥吗? 浏览器端实现^18.1.0
    babel-loader识别ES6语法,编译js^8.2.5
    @babel/corebabel处理的核心逻辑^7.18.2
    @babel/preset-env根据一些预设的目标值转换js语法,会打包一些polyfill^7.18.2
    @babel/preset-react帮助识别jsx语法,解析react^7.17.12
    @babel/plugin-transform-runtime优化解决preset-env打包的polyfill会污染全局的问题^7.18.2
    core-jspolyfill的核心实现,选择3版本3
  3. 添加webpack的loader配置

    loader是webpack的文件处理器,让webpack能够处理其他类型的文件,并转为有效的模块,以供程序使用,以及被添加到依赖图中。

    webpack.config.base.js文件

    module.exports = {
      // ...
      module: {
        rules: [
          {
            // 匹配js/jsx
            test: /\.jsx?$/,
            // 排除node_modules
            exclude: /node_modules/,
            use: {
              // 确定使用的loader
              loader: "babel-loader",
              // 参数配置
              options: {
                presets: [
                  [
                    // 预设polyfill
                    "@babel/preset-env",
                    {
                      // polyfill 只加载使用的部分
                      useBuiltIns: "usage",
                      // 使用corejs解析,模块化
                      corejs: "3",
                    },
                  ],
                  // 解析react
                  "@babel/preset-react",
                ],
                // 使用transform-runtime,避免全局污染,注入helper
                plugins: ["@babel/plugin-transform-runtime"],
              },
            },
          }
        ]
      }
      // ...
    }
    
  4. 创建React组件

    创建App.jsx文件

    import React, { useState } from "react";
    
    export default function App () {
      
      return <div className="app">
        <h1>Hello Webpack-React</h1>
      </div>;
    }
    

    入口文件src/index.js

    import React from "react";
    // 注意这里最新版的ReactDOM是从client中导出的
    import ReactDOM from "react-dom/client";
    // 因为设置了extensions,所以可以不加扩展名
    import App from './App';
    
    // 创建app根节点
    const appEl = document.createElement("div");
    // 设置id
    appEl.id = "app";
    // 追加节点到body中
    document.body.appendChild(appEl);
    
    // 最新版本使用的是ReactDOM.createRoot
    // 如果使用ReactDOM.render()控制台会报warnning错误
    const root = ReactDOM.createRoot(appEl);
    
    // 渲染
    root.render(<App />);
    

    页面效果图: 页面效果图.png

    效果都不用说,肯定是 完美.png

五、解析CSS以及CSS预处理器

CSS在webpack中也是作为一个资源被识别的,需要配置相关的loader。CSS预处理器如less/sass/stylus/postcss都可以被相关的loader识别。

  1. 添加依赖

    yarn add css-loader less less-loader style-loader postcss postcss-loader mini-css-extract-plugin cross-env autoprefixer css-minimizer-webpack-plugin -D
    
  2. 依赖模块说明

    模块名描述版本
    css-loader解析css^6.7.1
    less支持less语法^4.1.2
    less-loader解析less^11.0.0
    style-loader将解析的css内容追加到head中^3.3.1
    postcss好用好玩的css插件,压缩、自动补全^8.4.14
    postcss-loader解析postcss设置^7.0.0
    mini-css-extract-plugin分离CSS^2.6.0
    cross-env好用的环境配置^7.0.3
    autoprefixer自动补全css属性前缀^10.4.7
    css-minimizer-webpack-plugin生产环境,压缩css^4.0.0
  3. webpack相关配置

    webpack.config.base.js

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    const isProd = process.env.NODE_ENV === "prod";
    
    module.exports = {
      // ...
      plugins: [
        new MiniCssExtractPlugin({
          // 输出的每个css文件名称
          filename: isProd ? "[name].[contenthash].css" : "[name].css",
          // 非入口的chunk文件名 - 通过import()加载异步组件中样式
          chunkFilename: isProd ? "[id].[contenthash].css" : "[id].css",
        }),
      ],
      
      module: {
        rules: [
          {
            test: /\.(css|less)$/,
            use: [
              // 生产环境下直接分离打包css
              isProd ? MiniCssExtractPlugin.loader : "style-loader",
              {
                loader: "css-loader",
              },
              "less-loader",
              {
                loader: "postcss-loader",
                options: {
                  postcssOptions: {
                    // 浏览器前缀自动补全
                    plugins: ["autoprefixer"],
                  },
                },
              },
            ],
          },
        ]
      }
    
      // ...
    }
    

    在webpack的loader中,加载顺序是从右向左依次处理,css/less文件的处理顺序是:postcss-loader -> less-loader -> css-loader -> (style-loader | MiniCssExtractPlugin.loader)

    webpack.config.prod.js

    在生产环境中,将打包的css文件进行压缩

    const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
    
    module.exports = merge(webpackConfigBase, {
      // ...
      optimization: {
        minimizer: [
          new CssMinimizerPlugin()
        ]
      }
    })
    
  4. package.json更新scripts,增加环境变量

    通过cross-env配置环境变量参数,跨平台好用!

    "scripts": {
      "dev": "cross-env NODE_ENV=dev webpack serve --config config/webpack.config.dev",
      "build": "cross-env NODE_ENV=prod webpack build --config config/webpack.config.prod"
    },
    

    传递参数NODE_ENV,在webpack的配置文件中可以使用process.env.NODE_ENV获取值。

    判断是否是生产环境

    const isProd = process.env.NODE_ENV === 'prod';
    

六、解析图片、JSON等其他文件

图片文件也是我们经常使用的资源,在webpack5之前,一般都使用raw-loader、url-loader、file-loader来进行处理,webpack5对此进行了一些升级,提供了一种资源模块类型(asset module type),替换了这些loader。

资源模块类型描述
asset/resource发送一个单独的文件,并导出URL,替代file-loader
asset/inline导出一个资源的data URI,替代url-loader
asset/source导出资源源代码
asset相当于自动选择asset/resource或asset/inline,替换url-loader中的limit
  1. webpack.config.base.js添加配置

    module.exports {
     // ...
     output: {
       // ...
       // 指定asset或asset/resource类型文件存储时的命名规则
       // 注意这里设置的是所有的公共的命名处理逻辑
       assetModuleFilename: 'image/[hash][ext][query]'
       // ...
     },
     module: {
       rules: [
         {
           // 匹配图片文件
           test: /\.(png|jpg|jpeg|gif)$/i,
           // 设置资源处理的类型为asset
           type: "asset",
           parser: {
             // 转为inline dataUrl的条件
             dataUrlCondition: {
               // 默认限制为8kb,现在调整限制为10kb,大文件直接作为asset/resource类型文件输出
               maxSize: 10 * 1024,
             },
           },
         },
         {
           // 匹配json文件
           test: /\.json$/,
           // 将json文件视为文件类型
           type: "asset/resource",
           // 路径中包含animations的
           include: /animations/,
           generator: {
             // 这里专门针对json文件的处理
             filename: "static/[name].[hash][ext][query]",
           },
         }
       ]
     }
     // ...
    }
    
  2. 组件中图片文件引入

    // 引入的rocket会被webpack对应的的rule解析规则转为data URL或者是图片地址路径 
    import rocket from './assets/rocket.gif';
    
    export default function App () {
    
      return <div>
          {/* 将变量rocket赋值给src */}
          <img src={rocket} alt="火箭">
      </div>
    }
    
  3. 组件中json文件引入

    默认情况,在组件中引入json文件会被视作JS资源,返回的是一个JSON对象,可供直接使用的。在webpack中配置处理json文件的规则,将应用于路径中包含animations的json文件上,因为类型type=asset/resource,所以返回的是json文件的字符串地址。

    // 这里是常规的JSON对象
    import kittyLoadingJsonObject from './assets/kitty-loading.json';
    // 这里返回的是路径
    import kittyLoadingPath from './animations/kitty-loading.json';
    
    console.log(kittyLoadingJsonObject); // JSON对象
    console.log(kittyLoadingPath); // /static/kitty-loading.32e5eb3647c98eaa8045.json
    

    在这里举个实际应用的例子,在我的上一篇文章《都2022年了,一个还不知道Lottie动画的前端已经OUT啦!》中,配置lottie动画的json文件有两种方式,一种是path,一种是animationData

    import lottie from 'lottie-web';
    // 这里是常规的JSON对象
    import kittyLoadingJsonObject from './assets/kitty-loading.json';
    // 这里返回的是路径
    import kittyLoadingPath from './animations/kitty-loading.json';
    
    // 两种不同的调用方式
    
    // path调用
    lottie.loadAnimation({
      // 这里是渲染动画的容器
      container: animationEle.current,
      loop: true,
      autoplay: true,
      renderer: 'svg',
      // kittyLoadingPath返回的是打包后,可以访问的http路径
      path: kittyLoadingPath
    })
    
    // animationData调用
    lottie.loadAnimation({
      // 这里是渲染动画的容器
      container: animationEle.current,
      loop: true,
      autoplay: true,
      renderer: 'svg',
      // 这里配置JSON Object
      animationData: kittyLoadingJsonObject
    })
    

七、解析TypeScript

TS是前端开发中的一大“利器“,不用纠结要不上TypeScript,直接干就完了,使用TS的快乐你想象不到!

TypeScript是JavaScript类型的超集,可以编译成纯JavaScript。

  1. 添加依赖

    yarn add typescript @babel/perset-typescript @types/react @types/react-dom -D
    

    我们还是使用babel-loader来编译ts,所以就不用安装ts-loader了

  2. 依赖模块说明

    模块名描述版本
    typescript你说这是啥^4.7.2
    @babel/preset-typescriptBabel解析typescript语法^7.17.12
    @types/reactreact类型文件^18.0.9
    @types/react-domreact-dom类型文件^18.0.5
  3. webpack.config.base.js文件更新

    配置解析.ts.tsx文件的loader

    
    module.exports = {
      // ...
      {
        // 匹配ts/js/tsx/jsx文件
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  useBuiltIns: "usage",
                  corejs: "3",
                },
              ],
              // 解析typescript语法
              "@babel/preset-typescript",
              "@babel/preset-react",
            ],
            plugins: ["@babel/plugin-transform-runtime"],
          },
        },
      },
      // ...
    }
    
    
  4. 配置tsconfig.json文件

    tsconfig.json文件是提供给TypeScript的解析规则,简单配置如下,可以根据自己项目需要自行调整。完整规则地址传送门

    // tsconfig.json
    {
      "compilerOptions": {
        // 重定向输出目录
        "outDir": "./dist",
        // 在表达式和声明上有隐含的any类型时报错,false表示关闭该规则
        "noImplicitAny": false,
        // 指定生成哪个模块系统下的代码
        "module": "ESNext",
        // 指定ECMAScript目标版本
        "target": "ESNext",
        // 在.tsx文件里支持react
        "jsx": "react",
        // 允许编辑JS文件
        "allowJs": true,
        // 检查JS文件中的错误并报告,需要allowJs配合使用
        "checkJs": true,
        // 模块解析,查找模块的方式
        "moduleResolution": "node",
        // 允许从没有默认导出的模块中默认导入
        "allowSyntheticDefaultImports": true,
        // "esModuleInterop": true,
        "resolveJsonModule": true,
        // 模块名基于baseUrl的路径映射列表
        "paths": {
          // 与webpack中声明的@对应,若不声明会导致报查询不到对应模块的错误
          "@": ["./src"],
          // 这个位置与上面区分下,不是一个意思,表示/*表示src下的所有文件
          "@/*": ["./src/*"]
        }
      },
      // 包含编译目录
      "include": ["src/*", "images.d.ts"],
      // 排除编译目录
      "exclude": ["node_modules", "dist"]
    }
    
  5. ts中图片等文件类型声明

    在项目中我们引入图片文件 import smileGif from './smile.gif';时,vscode会提示ts 2307 找不到模块“./smile.gif”或其相应的类型声明。,配置images.d.ts文件解决这个问题。

    declare module "*.css";
    declare module "*.less";
    declare module "*.png";
    declare module "*.jpg";
    declare module "*.gif";
    declare module "*.txt";
    
    declare module "*.svg" {
      export function ReactComponent(
        props: React.SVGProps<SVGSVGElement>
      ): React.ReactElement;
      const url: string;
      export default url;
    }
    
    
  6. 文件名调整

    从现在开始所有的.jsx文件调整为.tsx文件,如果tsconfig.json文件声明中没有配置allowJs: true,所有的.js文件都要调整成.ts扩展名,否则不被编译。

从现在开始就开心的使用TypeScript来开发吧,TS的快乐,谁用谁知道~