webpack5搭建 React cli

295 阅读4分钟

react cli (开发模式配置)

开篇

废话不多说,我们直接上代码,使用webpack5搭建react 和 vue 脚手架,了解官方脚手架中做了什么事情,以及前端为什么需要打包工具。

入口

yarn init -y

安装webpack

yarn add webpack webpack-cli webpack-dev-server -D

安装 dependencies依赖

yarn add react react-dom react-router-dom antd

eslint

yarn add eslint
npx eslint --init

image.png

yarn add eslint-webpack-plugin -D

作用:让eslint和webpack结合起来,在 dev 环境和 build 环境都可以给出错误提示。

该插件使用 eslint 来查找和修复 JavaScript 代码中的问题。

在没有使用该插件之前,我们看到vscode编辑器里面会报错,但是我们dev启动的项目并没有报错,

image.png

当我们加上这个插件之后,再来看看效果。页面,编辑器,还有命令行都会有报错,所以用起来更加的安全了。

image.png

出口

mode

处理js、jsx

yarn add @babel/core @babel/cli @babel/preset-env babel-loader -D

编写loader处理.js|.jsx结尾的文件

到这,我们先来看看我们现在的文件目录结构。

image.png

命令行运行,就可以在浏览器中看见效果了。

image.png

image.png

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    commonjs: true,
    es2021: true,
  },
  extends: [
    'plugin:react/recommended',
    'airbnb',
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
  },
  plugins: [
    'react',
  ],
  rules: {
    'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
    'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
  },
};

config/webpack.dev.js

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

module.exports = {
  entry: path.resolve(__dirname, '../src/main.js'),
  module: {
    rules: [
      {
        oneOf: [  // 表示当匹配到一个规则的时候,就不在匹配其他的了
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            include: path.resolve(__dirname, '../src'),
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开缓存
              cacheCompression: false, // 关闭缓存压缩
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  mode: 'development',
  devServer: {
    open: true,
    host: 'localhost',
    port: 3000,
    hot: true,
    compress: true,
    historyApiFallback: true,
  },
};

.vscode/settings.json

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "dbaeumer.vscode-eslint",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
  },
  "eslint.format.enable": true,
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
  "prettier.enable": false
}

babel.config.js

module.exports = {
  presets: ['@babel/preset-react'],
};

package.json,如果在运行的时候有包没安装的话,可在这个文件中找到,不用慌。

{
  "name": "react-cli-study1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "webpack server --config ./config/webpack.dev.js"
  },
  "devDependencies": {
    "@babel/cli": "^7.17.10",
    "@babel/core": "^7.18.2",
    "@babel/preset-env": "^7.18.2",
    "@babel/preset-react": "^7.17.12",
    "babel-loader": "^8.2.5",
    "eslint": "^7.32.0 || ^8.2.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-plugin-import": "^2.25.3",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-react": "^7.28.0",
    "eslint-plugin-react-hooks": "^4.3.0",
    "html-webpack-plugin": "^5.5.0",
    "webpack": "^5.72.1",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.9.0"
  },
  "dependencies": {
    "antd": "^4.20.6",
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "react-router-dom": "^6.3.0"
  }
}

src/main.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('app'));
root.render(<App />);

src/App.jsx

import React from 'react';

export default function App() {
  return (
    <h1>hello react</h1>
  );
}
  1. eslint-webpack-plugin
yarn add cross-env -D

处理html

yarn add html-webpack-plugin -D

处理 css

yarn add postcss-loader postcss-preset-env css-loader style-loader less-loader sass-loader stylus-loader stylus less sass -D

config/webpack.dev.js

const getStyleLoaders = (preProcessor) => [
  'style-loader',
  'css-loader',
  {
    // 处理css兼容性问题
    // 配合 browserslist 文件
    loader: 'postcss-loader',
    options: {
      postcssOptions: {
        plugins: ['postcss-preset-env'],
      },
    },
  },
  preProcessor,
].filter(Boolean);


 {
    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)$/,
    use: 'asset',
    parser: {
      dataUrlCondition: {
        maxSize: 10 * 1024, // // 小于10kb的图片会被base64处理
      },
    },
  },
  // 处理字体
  {
    test: /\.(ttf|woff2?)$/,
    type: 'asset/resource',
  },

browserslist

根据提供的目标浏览器的环境来,智能添加css前缀,js的polyfill垫片,来兼容旧版本浏览器。避免不必要的兼容代码,以提高代码的编译质量。

共享使用browserslist的组件们:

组件名功能vv
Autoprefixerpostcss添加css前缀组件
bable-preset-env编译预设环境 智能添加polyfill垫片代码
postcss-normalize 
等等……

基础语法: 只要package.json配置了browserslist对象,需要的组件将自动匹配到并使用,也可以配置到具体的组件参数

{   // package.json
  "browserslist": [         // 注意:是一个数组对象
    "> 1%",
    "last 2 versions"
  ] }

或者

.browserslistrc

> 1%
last 2 versions
not dead

last 2 versions:CanIUse.com追踪的IE最新版本为11,向后兼容两个版本即为10、11

设置语法:通过浏览器过滤的思路实现

默认是兼容所有最近版本

支持的插件

Browserslist这个东西单独是没用的,(补充: 在vue官方脚手架中,browserslist字段会被 @babel/preset-envAutoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。2019-7-22)下面的搭配的工具列表:

了解更多请看这个list

或者npx browserslist

处理图片

devtool

devtool: "cheap-module-source-map",

optimization

devserver

  devServer: {
    open: true,
    host: "localhost",
    port: 3000,
    hot: true,
    compress: true,
    historyApiFallback: true, // 解决react-router刷新404问题
  }

resolve

webpack解析模块的配置

react cli (生产环境配置)

去掉dev-server、去掉HMR功能。

开启css js 压缩

开启图片压缩

css压缩提取

yarn add css-minimizer-webpack-plugin mini-css-extract-plugin -D

图片压缩

无损压缩

yarn add image-minimizer-webpack-plugin -D

yarn add imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D

react cli (合并配置)

// 合并配置
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");

// 需要通过 cross-env 定义环境变量
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: [
          {
            // 用来匹配 .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, // 开启babel编译缓存
              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(),
  ].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: {
        react: {
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          name: "chunk-react",
          priority: 40
        },
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: "chunk-antd",
          priority: 30
        },
        libs: {
          test: /[\\/]node_modules[\\/]/,
          name: "chunk-libs",
          priority: 20
        }
      }
    },
    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 ? undefined : "cheap-module-source-map",
  performance: false, // 关闭性能分析,提示速度
};

react cli (优化配置)

antd 自定义主题

类库单独打包

考虑到node_modules下面的代码打成一个包的时候文件体积会太大。所以将其分成几个包。

react react-dom react-router-dom 打成一个包

antd 打成一个包

剩下的打成一个包

image.png

runtimeChunk 的作用

我们先开启看看效果。

optimization: {
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
}

image.png

什么是运行时文件?

搜索文件得知

形如import('abc').then(res=>{})这种异步加载的代码,在webpack中即为运行时代码。

下面我们试一试最熟悉的路由懒加载,快速写下以下代码

main.js

import React from 'react';
import { createRoot } from "react-dom/client";
import './style/css/index.css'
import 'antd/dist/antd.less';
import { BrowserRouter } from 'react-router-dom'


import App from './App'
const root = createRoot(document.getElementById("app"));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

App.jsx

import React, { lazy, Suspense } from "react";
import { Button } from "antd";
import { Routes, Route, Link } from "react-router-dom";
const About = React.lazy(() => import("./pages/About"));
const Home = lazy(() => import("./pages/Home"));

export default function App() {
  return (
    <>
      <h1 className="h1-text-color">hello word 888</h1>
      <Button type="primary">Primary Button</Button>
      <Link to="/about">About</Link>
      <Link to="/home">Home</Link>
      <Routes>
        <Route
          path="/about"
          element={
            <Suspense fallback={<div>loading</div>}>
              <About />
            </Suspense>
          }
        />
        <Route
          path="/home"
          element={
            <Suspense fallback={<div>loading</div>}>
              <Home />
            </Suspense>
          }
        />
      </Routes>
    </>
  );
}

同样先不开启runtimeChunk 、开启runtimeChunk、修改About.jsx文件。我们分别来对比结果

image.png

设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个 runtime~xxx.js的文件。然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。

  • 你以为这就完了?

1、查看下runtime~xxx.js文件内容:

(i.u = function (e) {
  return "static/js/" + e + "." + { 229: "9acdd27ab0", 895: "147b4cdec2" }[e] + ".chunk.js";
}),

发现文件很小,且就是加载chunk的依赖关系的文件。虽然每次构建后app的hash没有改变,但是runtime~xxx.js会变啊。每次重新构建上线后,浏览器每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中。这边我们使用script-ext-html-webpack-plugin来实现。(也可使用html-webpack-inline-source-plugin,其不会删除runtime文件。)

这样重新打包,查看index.html文件

index.html中已经没有对runtime~xxx.js的引用了,而是直接将其代码写入到了index.html中,故不会在请求文件,减少http请求。

总结

runtimeChunk作用是为了线上更新版本时,充分利用浏览器缓存,使用户感知的影响到最低。