使用webpack从零开始手动搭建React项目

2,364 阅读9分钟

自从接触前端以来,每次创建一个新的项目时,为了图方便几乎都是使用create-react-app脚手架来搭建的。到了工作中时,发现公司的项目都是大佬们已经搭建配置好的,根本不需要我去关心项目如何搭建的,以及内部的一些配置细节。虽然,现在市面上一些热门的打包工具(例如webpack、vite)都有看过,但总是看了就忘,没有亲身实践,总无法理解其中真正的意义。所以本文便借助网上资料自己手动搭建一个React项目。

一、初始化项目

首先创建一个项目文件夹,本文将其命名为 zero-react-app,用于存放项目文件,如下图:

image.png

然后进入 cmd 终端界面,切换到该文件夹的路径下:

image.png

然后在cmd中输入命令 npm init 初始化一个项目:

npm init

运行后界面如下所示:

image.png

需要输入一些项目相关信息:

  • package name: 项目名称
  • version:项目版本
  • description:项目描述
  • entry point:入口文件
  • test command:测试命令
  • git repository:Git仓库地址
  • keywords:关键字
  • license:版本信息

这些信息可以直接回车使用默认值,也可自定义设置。

npm init命令运行成功后,项目文件夹中会创建一个 package.json 文件,里面存放的就是创建项目时指定的基础信息:

image.png

package.json文件是什么?

package.json 位于项目根目录,是项目的配置文件,例如配置项目启动、打包命令,声明依赖包等。
具体配置项可见npm docGithub上的介绍。

二、安装webpack

首先安装webpack

npm install webpack webpack-cli -D

安装好后,项目文件夹如下所示,多了 node_modules 文件夹和 package-lock.json 文件。

image.png

node_modules文件夹中存放着所有安装的依赖包。

package.json 和 package-lock.json的区别

package.json:运行 npm init 时生成,主要作用是描述项目以及记录项目依赖的模块信息(模块名称、大版本信息等)

package-lock.json:运行 npm install 时生成,用于记录所有模块的详细信息,包括版本信息、模块来源以及依赖的小版本信息等。

之所以引入 package.json 文件,主要用途在于:

  • 描述依赖关系树的单一表示,以便保证团队成员、部署和持续集成安装完全相同的依赖关系。(即锁定所有依赖的版本号)
  • npm install 重新安装全部依赖时,可以通过 package-lock.json 文件指定的下载地址和相关依赖,相对加快下载速度。

有关package-lock.json文件中具体配置,可以见官方说明

那为什么要安装 webpack 呢?

按官方定义,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 >webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后>将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

安装好webpack后,需要在根目录下创建一个webpack.config.js文件,这是webpack的配置文件。该文件配置主要包含以下几部分:

  • 入口(entry):指示 webpack 应该使用哪个模块来作为构建其内部依赖图的开始。默认值是 ./src/index.js。具体配置可见官方文档
module.exports = {
  entry: './path/to/my/entry/file.js',
};
  • 输出(output):告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    // bundle生成位置
    path: path.resolve(__dirname, 'dist'),
    // bundle文件名
    filename: 'my-first-webpack.bundle.js',
  },
};
  • loader:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。loader主要用于转换源代码,可以使你在import 或 "load(加载)" 模块时预处理文件。通常有两个属性:
    • test 属性,识别出哪些文件会被转换。
    • use 属性,定义出在进行转换时,应该使用哪个 loader

module.rules 允许你在 webpack 配置中指定多个 loader。
loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // [style-loader](/loaders/style-loader)
          { loader: 'style-loader' },
          // [css-loader](/loaders/css-loader)
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          },
          // [sass-loader](/loaders/sass-loader)
          { loader: 'sass-loader' }
        ]
      }
    ]
  }
};
  • 插件(plugin):loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。 webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 访问内置的插件
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    filename: 'my-first-webpack.bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        use: 'babel-loader',
      },
    ],
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};
  • 模式(mode):主要有开发模式(development)和生产模式(production

三、创建项目目录结构

  • 在项目根目录下创建 src文件夹。src文件夹用于存放项目代码文件.
    • 在其中创建一个index.js文件作为项目的入口文件。
  • 在项目根目录下创建public文件夹。
    • public文件夹下创建一个index.html文件,作为生成HTML文件的模板。

初始项目目录结构如下图:

image.png

紧接着向 webpack.config.js 中写入一些基本的webpack配置:

const path = require('path');

module.exports = {
    mode: 'development',  // 开发模式
    entry: './src/index.js', // 入口文件
    output: {
        // 必须是绝对路径
        path: path.resolve(__dirname, 'dist'), //打包后文件的输出位置
        filename: 'main.js',  // 打包后的文件名
        clean: true  // 打包之前清理dist文件夹
    },
}

我们在入口文件 ./src/index.js 文件中写入一段 JS 代码,测试是否能够打包成功:

image.png

然后运行 npx webpack --config webpack.config.js进行打包,可以看到打包后输出到了./dist/main.js

image.png

注意:

npx webpack --config webpack.config.js 可以简写为 npx webpack,因为当存在webpack.config.js文件时webpack会默认选择该文件,--config参数表明可以传递任何名称的配置文件。

四、安装 HtmlWebpackPlugin 插件

该插件将自动生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。

npm install -D html-webpack-plugin

安装好后,我们可以在webpack.config.js文件中进行如下配置。

const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    ...
    plugins: [
        new htmlWebpackPlugin({
            template: './public/index.html',  // 生成HTML文件的模板文件
            filename: 'index.html',  // 生成的HTML文件名
            inject: 'body'  // <script>标签插入的地方
        })
    ],
    ...
}

然后我们每次使用 webpack 进行打包时,都会自动在dist文件夹中自动生成一个 index.html文件。

五、加载样式文件

项目中存在各种各样的样式文件,我们可以通过loader来加载。通常我们需要安装以下loader

以加载less文件为例;

首先安装相关loader 以及 less:

npm install -D style-loader css-loader less-loader less

然后在webpack.config.js文件中添加如下配置:

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    } 
    ...
}

当 webpack 打包 less 文件时,会依次调用'less-loader', 'css-loader', 'style-loader'对less文件进行解析。['style-loader', 'css-loader', 'less-loader']的书写顺序和调用顺序(从后往前)相反。

抽离和压缩CSS

MiniCssExtractPlugin:会将CSS提取到单独的文件中,为每个包含CSS的JS文件(打包后的)创建一个CSS文件,并且支持CSS和SourceMaps的按需加载

npm install -D mini-css-extract-plugin

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

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
    ...
    plugins: [
        new MiniCssExtractPlugin({
            filename: './styles/[contenthash].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
            }
        ]
    } 
    ...
}

CssMinimizerWebpackPlugin:这个插件使用cssnano优化和压缩CSS

npm install -D css-minimizer-webpack-plugin
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
    ...
    mode: 'production',
    
   optimization: {
       minimizer: new CssMinimizerWebpackPlugin()
   }
    ...
}

六、静态资源打包

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。

在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

例如打包 .png 格式的图片:

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.png$/,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[contenthash][ext]'
                }
            },
        ]
    } 
    ...
}

此外 webpack 也可以打包 .csv .xml 格式的文件,不过需要借助以下 loader:

  • csv-loader:将 csv 文件内容转换成数组
  • xml-loder:将 xml 文件内容转换成对象

这类静态资源如何打包,可根据官方文档以及具体项目需求进行配置。

七、支持 ES6 语法(babel-loader)

对于某些浏览器而言,并不支持 ES6 语法,所以需要使用babel-loader将代码转换成 ES5 语法。假设我们在index.js文件写入ES6的箭头函数:

image.png

然后我们看webpack打包后的文件:

image.png

可以看到,webpack 并未对其做任何转换。然后我们安装 babel-loader

npm install -D babel-loader @babel/core @babel/preset-env
  • babel-loader :webpack中用babel解析ES6的桥梁
  • @babel/core:babel的核心模块
  • @babel/preset-env:babel预设,一组babel插件的集合 然后在webpack.config.js文件中添加如下配置:
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
           }
        ]
    } 
    ...
}

然后再进行打包,可以看到箭头函数转换成了ES中的函数声明:

image.png

此外还需要安装regeneratorRuntime插件,这是webpack打包生成的全局辅助函数,由babel生成,用于兼容async/await语法。所以需要安装和配置插件:

// 该包中含有 regeneratorRuntime
npm install --save @babel/runtime

// 该插件会在需要regeneratorRuntime的地方自动require导入
npm install --save-dev @babel/plugin-transform-runtime

然后将配置文件修改如下:

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                        plugins: [
                            ["@babel/plugin-transform-runtime"]
                        ]
                    }
                }
           }
        ]
    } 
    ...
}

至此,babel相关配置已经完成。当然,实际项目中会根据实际要求进行更复杂的配置,可参考Babel官网

八、集成React

本文目的在于搭建一个 React 项目,上述已经完成了基本配置,紧接着我们将引入React。首先安装React:

npm install -D react react-dom

此外,我们仍然需要安装@babel/preset-react用于将 jsx 转换成 ES5.

npm install -D @babel/preset-react

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

{
    test: /\.(js|jsx)$/,
    exclude: /(node_modules)/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: [
            '@babel/preset-env',
            '@babel/preset-react'
        ],
        plugins: [
            // 主要改变在此处
            ["@babel/plugin-transform-runtime"]
        ]
      }
    }
}

然后我们在index.js中写入如下代码:

image.png

HTML 模板文件也需要进行如下修改:

image.png

然后执行打包命令,打包后的文件在浏览器中显示如下:

image.png

至此,一个简单的React项目就搭建完成了。

九、模块热更新

在实际开发过程中,如果每次修改后都执行一次npx webpack 打包命令,操作过于繁琐。对此,我们引入webpack-dev-server,提供一个本地的web服务,同时具有live reloading(实时重新加载)功能。

npm install -D webpack-dev-server

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

module.exports = {
    ...
    devServer: {
        hot: true,
        static: './dist'
    },
    ...
}

然后在 package.json 文件的script中添加启动命令:
"start": "webpack-dev-server --open-app-name chrome"

然后我们就可以在命令行中执行npm start 启动服务了。

十、引入 TypeScript

目前大多数前端项目开发中使用的都是 TypeScript 。TypeScript 是 JavaScript 的超集,为其增加了类型系统,可以编译为普通 JavaScript 代码。若想在项目中使用 TypeScript 进行编码,首先我们需要安装TypeScript compiler 和 loader:

npm install -D typescript ts-loader

然后在根目录下添加一个tsconfig.json文件,然后输入基本配置:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node"
  }
}

关于 TypeScript 更细致的配置,可查看 TypeScript 官方文档 了解更多关于 tsconfig.json 的配置选项。

到此,一个简单的React项目就搭建好了,当然在实际开发中还有更加复杂的配置,大家可参考 webpack官方文档进行配置。

本文是自己学习手动搭建React项目的过程记录,当中存在错误或者不合理的地方,欢迎大家指正。

参考:

不借助脚手架手动搭建react项目(webpack5 + Antd4 + React18)