likes
comments
collection
share

从零搭建React脚手架

作者站长头像
站长
· 阅读数 22

关于本文

本文通过webpack5的基础配置手动搭建了一个基础React脚手架,读完本文你将会对webpack的一些基础配置有所了解,必要时请结合官方文档进行解读。 概念 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)

概念

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

webpack仅能编译 JS 中的 ES Module 语法,生产模式还能压缩 JS 和HTML代码;其本身的能力非常有限,这就不得不借助预置器,插件来扩展其能力。

初始化

// 创建文件夹:命令创建或手动创建
// 命令创建:
mkdir about-react
cd about-react
// 加了-y能跳过手动确认配置,若需要自定义的参数,生成pack.json后可修改
npm初始化:npm init -y 

执行完以上命令,就会生成一个pack.json文件 以下配置的项目结构均以这个为例

从零搭建React脚手架

开发环境配置

在项目根目录创建文件config/webpack.dev.js

基本配置

webpack五大核心基本配置:entry、output、loader(module.rules)、plugins、mode

module.exports = {
  // 入口
  entry: "",
  // 输出
  output: {},
  // 加载器
  module: {
    rules: [],
  },
  // 插件
  plugins: [],
  // 模式
  mode: "development", // 生产模式:production
};

配置webpack进行编译打包的入口entry

// src文件夹是项目源码,config文件夹和src文件夹同级,示例以main.js作为入口文件,可根据自身项目做调整
// 相对路径写法
entry: "../src/main.js", 
// 绝对路径写法
entry:path.resolve(__dirname,'../src/main.js'),

配置output

output:{
    path:path.resolve(__dirname,'../dist'),
    // path:undefined, // 或开发环境不配置打包后的输出路径
    filename:static/js/main.js, // 这里的地址是相对于path路径来配置
    clean: true, // 清空上次打包资源
},

配置loader

Webpack 支持使用 loader 对文件进行预处理。你可以构建包括 JavaScript 在内的任何静态资源

接下来引用大致分三小点:处理样式loader、处理图片loder、处理其他资源loader

处理样式

现在基本是使用预处理器,这里以sass为例

 // 先下载 npm i -D style-loader css-loader sass-loader sass postcss-loader postcss postcss-preset-env
 module: {
    rules: [
     {
        test: /.s[ac]ss$/, // 匹配.s[ac]ss 结尾的文件
        use: ["style-loader", "css-loader",  // use 数组里面Loader执行顺序是从右到左,顺序不能写错
            {
              loader: "postcss-loader",
                options: {
                  postcssOptions: {
                    plugins: [
                      "postcss-preset-env", // 能解决大多数样式兼容性问题
                    ],
                  },
                },
              },
        "sass-loader"]
      }
    ]
  }
  • sass-loader:加载 Sass/SCSS 文件并将他们编译为 CSS
  • postcss-loader:使用众多插件来处理css,为css属性达到更高的兼容性
  • css-loader:对@import 和 url() 进行处理,就像 js 解析 import/require() 一样
  • style-loader:在html文件中生成style标签,并把最终生成的css属性添加到style标签中

兼容样式还需要在package.json文件中配置browerslist,详情见browserslist/browserslist: 🦔 Share target browsers between different front-end tools, like Autoprefixer, Stylelint and babel-preset-env (github.com)

"browerslist": [  
    "last 2 version", // 匹配最后两个版本
    "> 1%", // 全球使用占比大于1%
    "not dead" // 未停用的
] 

处理js资源

// 先下载 npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime
{
  test: /.(jsx|js)$/,
  include: path.resolve(__dirname, "../src"), // 只匹配编写源码
  loader: "babel-loader",
  options: {
          cacheDirectory: true, // 开启babel编译缓存
          cacheCompression: false, // 缓存文件不压缩
          plugins: [
            "react-refresh/babel", // 开启js的HMR功能
          ],
           
          /** presets: [ 这里的配置可以放在babel.config.js文件中
            "@babel/preset-env"
            "@babel/preset-react", 
            "@babel/preset-typescript"
          ],
         ** /
        },
},

 // babel.config.js
module.exports = {
// 先处理ts,再处理jsx,最后babel转换为低版本语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      { useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
    ],
     "@babel/preset-react", 
     "@babel/preset-typescript"
  ],
};

处理图片

Webpack4时,处理图片资源通过 file-loader 和 url-loader 进行处理 而Webpack5 已经将这两个 Loader 功能内置了,只需要配置asset资源处理即可,详情见资源模块 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)

 module: {
    rules: [
       {
        test: /.(png|jpe?g|gif|webp)$/,
        type: "asset",
        parser: {
            dataUrlCondition: {
            maxSize: 8 * 1024 // 小于8kb的图片会被base64处理
          }
        },
      }
    ]
  }

处理其他资源

以引入阿里巴巴字体图标为例,正确引入iconfont并使用字体图标后

 module: {
    rules: [
       {
       // |map4|map3|avi,一般用不到,这些资源一般是放在资源服务器上
        test: /.(ttf|woff2?|map4|map3|avi)$/, 
        type: "asset/resource", // 直接导出资源,不做处理
        generator: {
        // 将图片文件输出到 static/images 目录中
          // [contenthash:10]: hash值取10位
          // [ext]: 使用之前的文件扩展名
          // [query]: 添加之前的query参数
          filename: "static/media/[contenthash:10][ext][query]",
        }
      }
    ]
  }

资源模块类型(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,并且配置资源体积限制实现。

配置plugins

plugins可以扩展webpack的能力,我们可以通过自定义配置plugin来实现更符合自己需求的脚手架

eslint-webpack-plugin

  1. 先下载 npm i eslint-webpack-plugin eslint -D
  2. 定义 Eslint 配置文件。详情查看官网配置文件(新) - ESLint 中文网 (nodejs.cn)
// .eslintrc.js
module.exports = {
  extends: ["react-app"], // 继承 react 官方规则
  // 若想覆盖掉react-app的规则,直接配置即可
  rules: {
    eqeqeq: ["warn", "smart"],
  },
  // 预设配置
  parserOptions: {
    babelOptions: {
      presets: [
        // 解决页面报错问题,支持import动态导入写法
        ["babel-preset-react-app", false],
        "babel-preset-react-app/prod",
      ],
    },
  },
};
//  webpack.config.js
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "src"),  // 指定检查文件的根目录
      exclude: "node_modules", // 默认值
      cache: true, // 开启eslint检查缓存
      cacheLocation: path.resolve( __dirname,"../node_modules/.cache/.eslintcache"), // 缓存目录
    }),
  ],

处理html资源

// 先下载:npm i html-webpack-plugin -D
const HtmlWebpackPlugin = require("html-webpack-plugin");
plugins: [
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 打包后的html=原html+自动引入打包生成的js等资源
      template: path.resolve(__dirname, "public/index.html"),
    }),
]

提取css成单独文件并压缩css

提一嘴,生产模式默认开启html 压缩和 js 压缩,所以不需要额外配置

// 先下载:npm i mini-css-extract-plugin  css-minimizer-webpack-plugin -D
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 提取css成单独文件
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 压缩css
/**由于前面我们使用了style-loader处理样式,
这个loader会直接在html文件中添加style标签并一股脑把处理好的css添加到style标签中
所以我们要提取css成单独的文件需要用 MiniCssExtractPlugin.loader代替style-loader**/
 module: {
    rules: [
     {
        test: /.s[ac]ss$/, // 匹配 .s[ac]ss 结尾的文件
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"] // use 数组里面 Loader 执行顺序是从右到左
      }
    ]
  }
plugins: [
    new MiniCssExtractPlugin({
      filename: "static/css/[name].css", // 定义输出文件名和目录
      chunkFilename: "static/css/[name].chunk.css",
    }),
    new CssMinimizerPlugin(),
]

配置开发服务器

// 先下载 npm i webpack-dev-server -D
// 开发服务器
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot:true
  },

一些优化点

1.SourceMap(源代码映射)

{
    devtool:"cheap-module-source-map" // 开启打包后的语句和行源码的映射,有利于定位问题
}

2.将静态内容直接复制到打包后的文件夹中

比如复制public文件夹下的图标文件等

const CopyPlugin = require("copy-webpack-plugin");
new CopyPlugin({
  patterns: [
    {
      from: path.resolve(__dirname, "../public"),
      to: path.resolve(__dirname, "../dist"),
      toType: "dir",
      noErrorOnMissing: true, // 不生成错误
      globOptions: {
      // 忽略文件,html-webpack-plugin已经复制过这个文件了,所以在这里要忽略
        ignore: ["**/index.html"],
      }
    },
  ],
}),

3.code split 按需导入并提取重复代码

// 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割,其他内容用默认配置即可
    },

4.利用contentHash根据内容缓存文件

  • chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。若A 和 B是同一个引入,会共享一个 hash 值。
  • hash:根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的
output:{
    filename: "static/js/[name].[contenthash:10].js", // 入口文件打包输出资源命名方式
    chunkFilename: "static/js/[name].[contenthash:10].chunk.js", // 动态导入输出资源命名方式
},
plugins:[
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:10].css",  
      chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
    }),
]只只

5.将contentHash值单独保管在一个 runtime 文件中

第4点配置缓存后,还会存在一个问题:

假如A引用了B,当B的内容发生变化,其contentHash变化,缓存失效,webpack会重新生成新的文件替代ChunkB文件。但是由于A引用了B,A的contentHash也会变,那么A也需要重新进行各种检测编译压缩打包流程。如果项目中所有的修改都是这个样子,缓存将失去实际意义,这不是我们想要的效果。

想要解决也很简单,生成一个runtime文件,runtime文件只保存文件的contentHash值和对应文件的关系,当B变化时,只需要重新编译打包B和runtime文件即可。runtime文件体积就比较小,所以变化重新请求的开销很小

只需要简单配置,如下:

// 提取runtime文件
runtimeChunk: {
  name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},

生产环境配置

生产环境的配置基本可以复制开发环境的,不同之处有几点

  1. js压缩:terser-webpack-plugin
  2. 把压缩的配置放在optimization中
  3. mode改成生产模式的
  4. sourceMap换成source-map模式
  5. 添加输出路径
  6. 整体包压缩compression-webpack-plugin
  7. splitChunks分割出react react-dom react-router-dom 一起打包成一个js文件
output: { 
    filename: "statics/js/[name].[contenthash:10].js", 
    assetModuleFilename: "statics/assets/[hash][ext][query]", 
    path: path.join(__dirname, "../dist"), 
    clean: true,
},
 optimization: {
    // 压缩的操作
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserWebpackPlugin(),
      new CompressionPlugin()
    ],
    splitChunks: { 
        chunks: "all", 
        cacheGroups: { 
            // react react-dom react-router-dom 打包成一个js文件 
            react: { 
                test: /[\\/]node_modules[\\/]react(.*)?[\\/]/, 
                name: "chunk-react", 
                priority: 40, 
            },
            // 剩下node_modules单独打包 
            libs: { 
                test: /[\\/]node_modules[\\/]/, 
                name: "chunk-libs", 
                priority: 20, 
            }, 
         },
     },
mode: "production",
devtool: "source-map",

提取开发环境和生产环境共有配置,并合并

生产和开发有相同的配置,我们可以提取出共用部分,使用webpack-merge给不同环境配置 那么最终会有一个webpack.common.js文件

// webpack.common.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 CopyPlugin = require("copy-webpack-plugin");
const isProduction = process.env.NODE_ENV === "production";
module.exports = {
  entry:path.resolve(__dirname,'../src/main.js'),
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /.s[ac]ss$/,
            use: {
            isProduction ? MiniCssExtractPlugin.loader : "style-loader",
            "css-loader",
            {
              loader: "postcss-loader",
              options: {
                postcssOptions: {
                  plugins: [
                    "postcss-preset-env",
                  ],
                },
              },
            },
            "sass-loader",
          },
          {
            test: /.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024, 
              },
            },
          },
          {
            test: /.(ttf|woff2?)$/,
            type: "asset/resource",
          },
        ],
      },
    ],
  },
  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 CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"),
          to: path.resolve(__dirname, "../dist"),
          toType: "dir",
          noErrorOnMissing: true,
          globOptions: {
            ignore: ["**/index.html"],
          }
        },
      ],
    }),
  ],
  optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
  resolve: {
    extensions: [".jsx", ".js", ".json"], // 自动补全文件扩展名,让jsx可以使用
  }
};

webpack.dev.js

const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
  mode: "development",
  devtool: "cheap-module-source-map",
  output: {
    path: undefined,
    filename: "static/js/[name].js",
    chunkFilename: "static/js/[name].chunk.js",
    assetModuleFilename: "static/js/[contenthash:10][ext][query]",
  },
  module:{
       rules:[
       {
            test: /.(jsx|js)$/,
            include: path.resolve(__dirname, "../src"),
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
              cacheCompression: false,
              plugins: [
                "react-refresh/babel", // 开启js的HMR功能
              ],
            },
          },
       ]
  },
  devServer: {
    static: {
      directory: path.join(__dirname, "../dist"),
    },
    compress: true,
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: true,
  },
});

webpack.prod.js

const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");

module.exports = merge(common, {
  mode: "production",
  output: {
      path: path.join(__dirname, "../dist"),
      filename: "static/js/[name].[contenthash:10].js", // 入口文件打包输出资源命名方式
      chunkFilename: "static/js/[name].[contenthash:10].chunk.js", // 动态导入输出资源命名方式
      clean: true,
  },
  module:{
      rules:[
        {
            test: /.(jsx|js)$/,
            include: path.resolve(__dirname, "../src"),
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
              cacheCompression: false,
            },
          },
      ]
  },
 optimization:{
    splitChunks: {
        chunks: "all",
        cacheGroups: {
        // react react-dom react-router-dom 打包成一个js文件
        react: {
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          name: "chunk-react",
          priority: 40,
        },
        // 剩下node_modules单独打包
        libs: {
          test: /[\\/]node_modules[\\/]/,
          name: "chunk-libs",
          priority: 20,
        },
      },
    },
    minimizer:[
        new CssMinimizerWebpackPlugin(),
        new TerserWebpackPlugin(),
        new CompressionPlugin()
    ],
   },
   devtool:"source-map",
   performance: false, // 关闭性能分析,提升打包速度
});

package.json配置运行指令

"scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js",
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
 },

.eslintrc.js配置

module.exports = {
  extends: ["react-app"],
  parserOptions: {
    babelOptions: {
      presets: [
        ["babel-preset-react-app", false],
        "babel-preset-react-app/prod",
      ],
    },
  },
};

babel.config.js配置

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

总结

开发配置

  • 从五大主要因素开始,配置了入口文件
  • 配置了输出文件的路径
  • 样式处理:我们通过style-loader、css-loader、sass-loader、postcss-loader、postcss-preset-env等loader和插件处理
  • 我们通过webpack内置的资源处理,配置asset处理图片资源
  • 通过babel-loader,还有"@babel/preset-env" "@babel/preset-react", "@babel/preset-typescript"预制处理js
  • 通过eslint-webpack-plugin规范我们的代码
  • 通过html-webpack-plugin将源代码的html复制到指定文件夹下 -最后配置开发服务器

接着还进行了一些优化

  • 源代码映射
  • 将静态内容直接复制到打包后的文件夹中
  • code split 按需导入并提取重复代码
  • 利用contentHash根据内容缓存文件
  • 将contentHash值单独保管在一个 runtime 文件中

合并

  • 最后我们抽取出开发和生产的共同配置并应用webpack-merge将他们合并
  • 提取后,配置文件需要获取通过cross-env获取环境变量
  • 配置package.json将对应的环境变量传给webpack配置文件内部,通过process.env.NODE_ENV获取
转载自:https://juejin.cn/post/7276837017231638528
评论
请登录