抛开脚手架,徒手搭建 react 项目(一)

1,627 阅读4分钟

《抛开脚手架,徒手搭建 react 项目(一)》]我们用webpack搭建了一个react项目,引入了typescript,用Babel编译,还用webpack-merge拆分了我们的配置文件。

《抛开脚手架,徒手搭建 react 项目(二)》我们在项目里面引入了eslint,还介绍了打开服务的五种方法。尤其重要的是 nginx,要你在本地体验一把线上奔跑的感觉。

《抛开脚手架,徒手搭建 react 项目(三)》我们在项目里面引入了 stylelint、还有husky相关工具,轻松实现git commit的时候,对代码格式和提交信息做具体的检测。

《抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(四)》亲手测试并整理了六点减少打包时间的手段。

抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(五) 亲手测试并整理了六点能够缩减打包体积的手段

本篇文章主要讲解如何从一个空目录开始,建立起一个基于webpack + react + typescript的标准化前端应用。

  • 技术栈: webpack5 + React18 + TS
  • 工程化: eslint + prettier + husky + git hooks
  • 支持图片、less、sass、fonts、数据资源(JSON、csv、tsv等)、Antd按需加载以及主题
  • 支持热更新、资源压缩、代码分离(动态导入、懒加载等)、缓存、devServer

初始化项目

创建一个项目,然后用npm初始化一下

mkdir demo
cd demo
git init
npm init -y

他会生成package.json

加入webpack

npm i webpack webpack-cli webpack-dev-server -D

配置文件webpack.config.js:

import path from 'path';
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));

export default {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js'
  },
};

在package.json里面加入命令

    "dev": "webpack --mode development ",
    "build": "webpack --mode production "

创建文件src/index.js,然后测试,看看有没有生成dist

image.png

加入热启动

webpack.config.js里面先加入插件,再在webpack-dev-server上标注

image.png

引入react 和 babel

npm react react-dom -S
npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D

配置webpack.config.js

import path from 'path';
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import HtmlWebpackPlugin from 'html-webpack-plugin'

const __dirname = dirname(fileURLToPath(import.meta.url));

export default {
  entry: './src/index.jsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: path.resolve(__dirname, 'src'),
        exclude: path.resolve(__dirname, 'node_modules'),
        loader:'babel-loader',
        options:{
            presets: [
                "@babel/preset-env",
                "@babel/preset-react" 
            ]
        }
      }
    ]
  },
  resolve: {
    extensions: [ '.jsx', '.js'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html', // 指定一个HTML模板文件
      filename: './index.html', // 输出的HTML文件名,默认是index.html
      inject: true, // 允许插件修改哪些内容,true将脚本添加到body元素的末尾
    })
  ],
  mode: "development", // 开发模式
};

上面babel的配置,你可以单独放在babel.config.js里面,也可以像我一样配置。

添加react组件,先写 index.jsx

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

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

再写App.jsx

import React from 'react'

function App() {
  return (
    <div className="App">
      hello,React!
    </div>
  );
}

export default App;

然后写index.html容器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script defer src="index.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

配置webpack.config.js

import path from 'path';
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import HtmlWebpackPlugin from 'html-webpack-plugin'

const __dirname = dirname(fileURLToPath(import.meta.url));

export default {
  entry: './src/index.jsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: path.resolve(__dirname, 'src'),
        exclude: path.resolve(__dirname, 'node_modules'),
        loader:'babel-loader',
        options:{
            presets: [
                "@babel/preset-env",
                "@babel/preset-react" 
            ]
        }
      }
    ]
  },
  resolve: {
    extensions: [ '.jsx', '.js'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html', // 指定一个HTML模板文件
      filename: './index.html', // 输出的HTML文件名,默认是index.html
      inject: true, // 允许插件修改哪些内容,true将脚本添加到body元素的末尾
    })
  ],
  mode: "development", // 开发模式
};

配置打包命令

    "dev": "webpack --mode development ",
    "build": "webpack --mode production "

加入ts

说明

@babel/preset-typescript 是一个 Babel 插件的集合,它可以将 TypeScript 代码转换为 JavaScript。如果你想要使用 Babel 来转换 TypeScript 代码,而不需要使用 TypeScript 编译器本身,那么你可以使用 @babel/preset-typescript

所以说什么typescript,什么ts-loader都不需要了,就@babel/preset-typescript即可。

如果你想手动编译ts文件用命令

npx babel your-typescript-file.ts --out-file compiled-js-file.js

npx babel --watch src --out-dir dist --presets @babel/preset-env,@babel/preset-typescript

安装

npm i @types/react @types/react-dom @babel/preset-typescript -D

tsconfig.json

{
    "compilerOptions": {
      "target": "ESNext",
      "lib": ["DOM", "DOM.Iterable", "ESNext"],
      "allowJs": false,
      "skipLibCheck": false,
      "esModuleInterop": false,
      "allowSyntheticDefaultImports": true,
      "strict": true,
      "forceConsistentCasingInFileNames": true,
      "module": "ESNext",
      "moduleResolution": "Node",
      "resolveJsonModule": true,
      "isolatedModules": true,
      "noEmit": true,
      "jsx": "react" // react18这里也可以改成react-jsx
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
}

修改webpack.config.js

image.png

修改文件

image.png

测试

image.png

webpack-merge拆分webpack.config.js

说明

  • development(开发环境) 和 production(生产环境) 这两个环境下的构建目标存在着巨大差异 所以webpack的配置写的差距会非常的大

  • 在开发环境中,我们需要:强大的 source map 和一个有着 live reloading(实时重新加载)  或 hot module replacement(热模块替换)  能力的 localhost server

  • 而生产环境目标则转移至其他方面,关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

  • 虽然,以上我们将 生产环境 和 开发环境 做了细微区分,但是,请注意,我们还是会遵循不重复写配置的原则,保留一个 "common( 公共 )" 配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。此工具会引用 “common” 配置,因此我们不必再在环境特定env的配置中编写重复代码。

安装

npm i webpack-merge -D

引入merge

import {merge} from "webpack-merge";

拆分webpack.config.js

image.png

代码webpack.common.js

import path from 'path';
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import HtmlWebpackPlugin from 'html-webpack-plugin'

const __dirname = dirname(fileURLToPath(import.meta.url));

export default {
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
    publicPath: '/' // 打包后文件的公共前缀路径
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        include: path.resolve(__dirname, 'src'),
        exclude: path.resolve(__dirname, 'node_modules'),//不解析的模块
        loader:'babel-loader',
        options:{
            presets: [//他的主席那个顺序时自下往上的。
                "@babel/preset-env",
                "@babel/preset-react",
                "@babel/preset-typescript"
            ]
        }
      }
    ]
  },
  resolve: {
    extensions: ['.js','.jsx', '.tsx', '.ts'],//当文件没有扩展名的时候,默认就找这些扩展名的文件,比如import A from ‘./A’
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html', // 打包的时候需要的模板,你不设置也行,一般都会在public文件夹里面设置下,我们图方便就不分public了
      filename: './index.html', // 打包后的文件名
      inject: true, // 注入静态资源,就是把js打包好以后,把script标签加进去,不然运行起来怎么找到js文件。
    })
  ],
  mode: "development", // 开发模式
};

代码webpack.dev.js

import path from 'path';
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import {merge} from "webpack-merge";
import baseConfig from "./webpack.common.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
 
//开发
export default merge(baseConfig, {
  mode: "development", // 开发模式,打包更加快速,省了代码优化步骤
  devtool: "inline-source-map", // 源码调试模式,后面会讲
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    compress: false, // gzip压缩,开发环境不开启,提升热更新速度
    hot: true, // 开启热更新,后面会讲react模块热替换具体配置
    historyApiFallback: true, // 解决history路由404问题
    static: {
      directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
    },
  },
});

代码webpack.prod.js

import {merge} from "webpack-merge";
import baseConfig from "./webpack.common.js";

export default merge(baseConfig, {
    mode: "production",
  });

命令配置:

  "scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },

添加样式编译

由于webpack默认只加载js文件,所以前端的html和css文件都需要特定的工具来处理才能正确的加载并打包。所以对于css文件而言,我们用的是css-loader

加载css

npm i style-loader css-loader -D

配置webpack.config.js

image.png

测试看看已经生效

image.png

加载less

npm i less-loader -D

配置

image.png

把之前的css文件改成less,并且将样式改成包含结构的,试试看

image.png

启动项目

image.png

加载sass

npm i sass-loader sass -D

配置webpack

image.png

添加文件

image.png

测试

image.png

一般情况下,项目要么使用less,要么使用sass,不会一起使用的,所以咱们就选择一个就好了,我喜欢使用less,所以就把sass删掉了,如果你选择了sass,请把后缀名写成.scss,不是.sass,'.sass'是老版本。

优化配置文件

image.png

别名设置

配置

image.png

image.png

image.png

测试

image.png

webpack.config.js

  resolve: {
    extensions: ['.js','.jsx', '.tsx', '.ts'],//当文件没有扩展名的时候,默认就找这些扩展名的文件,比如import A from ‘./A’
    alias: {
      '@': path.resolve(__dirname, 'src'),
    }
  },

tsconfig.js

      "baseUrl": ".",
      "paths": {
      	"@/*": ["src/*"]
      }

打包图片

webpack5已经支持了对图片的加载,所以不再额外下载loader了,只需要简单的配置就好。

配置

  {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 1 * 1024, // 小于1kb转base64,减少请求次数
          },
        },
        generator: {
          filename: "public/[hash:10][ext][query]", // 指定打包路径和文件名
        },
      },

image.png

测试

image.png

加入字体图标

antd官网

npm i antd -S
npm install @ant-design/icons --save

一般我们用的都是组件库里面的图标,我发现不管配置,还是不配置,webpack都能处理,为了保险期间还是配上吧。

配置

      {
        test: /\.(ttf|woff|woff2?)$/i,
        type: "asset/resource",
        generator: {
          filename: "public/iconfont/[hash:10][ext][query]",
        },
      },

加入视频音频

接下来看看视频

image.png

添加webpack处理

      {
        test: /\.(map3|map4|avi)$/i,
        type: "asset/resource",
        generator: {
          filename: "public/media/[hash:10][ext][query]",
        },
      },

测试

image.png