我是这样搭建React+TS的通用webpack脚手架的(阶段一)

694 阅读4分钟

前言

目前公司前端体系还不太完善的原因,一般写项目用的都是使用 create-react-app 搭建的一套通用的脚手架。这个脚手架虽然非常的通用、基础,但是在搭配了 tailwindcss、postcss 后,每次的编译和打包消耗的时间实在是太久了,非常影响工作效率。那么,为了不加班,手动来搭建一套通用的 webpack 脚手架,就成为了当前的一个目标。

正文

项目基本配置

项目初始化

mkdir webpack-demo-1
cd ./webpack-demo-1
yarn init -y

首先需要创建项目的入口文件和 webpack 的配置文件,此时项目目录如下:

image.png

然后,需要安装 webpack 依赖

yarn add --dev  webpack webpack-cli

这里用到的 webpack 相关版本如下:

"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"

写入内容

index.js 中随便写点东西进去:

class Hello {
  constructor() {
      console.log('hello webpack!')
  }
}

const test = new Hello()

然后,我们运行 webpack 来体验一下具体是什么效果

npx webpack

可以看到,在运行完以后,在根目录下会增加一个 dist 目录,该目录下会新增一个 main.js 文件

image.png

其中的内容:

new class{constructor(){console.log('hello webpack!')}};

那这样肯定是不 OK 的呀,需要使用 babel 把 ES6 转换成 ES5 的代码。下面就来安装一些 babel 的一些相关依赖。

配置 babel

安装依赖:

yarn add --dev babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime  @babel/plugin-proposal-decorators  @babel/plugin-proposal-class-properties @babel/plugin-proposal-private-methods

yarn add @babel/runtime @babel/runtime-corejs3

修改 webpack.config.js 文件:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.[contenthash:8].js',
  },
  module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
    ]
  }
}

在根目录下创建 .babelrc 文件:

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    ["@babel/plugin-transform-runtime", {"corejs": 3}],
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
    ["@babel/plugin-proposal-private-methods", { "loose": true }]
  ]
}

让我们再执行一次 npx webpack 来看看输出结果。此时的项目目录:

image.png

来查看一下这个 dist 目录下的 bundle.xxxx.js 文件的内容:

(()=>{"use strict";new function n(){!function(n,o){if(!(n instanceof o))throw new TypeError("Cannot call a class as a function")}(this,n),console.log("hello webpack!")}})();

这样应该就没什么问题了。接下来来让项目运行在浏览器中吧!

运行在浏览器中

这里直接使用 webpack 生态圈里一个非常知名的插件 html-webpack-plugin,这个插件可以让我们的构建产物使用我们指定的 html 文件作为模板来使用。

yarn add --dev html-webpack-plugin

在根目录下,创建一个 public 文件夹,并放入一个 index.html 文件,并给他写入最基本的 html 的内容

image.png

wbepack.config.js文件中使用 html-webpack-plugin 插件

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

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './public/index.html'),
      inject: 'body',
      scriptLoading: 'blocking',
    }),
  ]
}

此时,在运行 npx webpack 查看打包结果

image.png

打开 dist 文件夹目录,直接用浏览器打开这个 index.html 文件

image.png

此时可以看到我们输入的 "hello webpack!" 已经显示在控制台中了。

目前存在的问题

以上就算是跑通了一个最基本的流程,但当前还面临几个最大的问题:

  1. 需要热更新。不可能每次更新内容后都重新 build 一次,去使用打包后的文件来做调试;
  2. 在每次打包前需要清除上一次打包的内容。
  3. 环境拆分 那么接下来,先来解决这两个问题吧!

热更新

这里我们根据官网提示,使用 webpack-dev-server

yarn add --dev webpack-dev-server

然后需要在 webpack.config.js 中添加上相关的配置内容:

module.exports = {
  // ...
  devServer: {
    port: '8080', // 开启的端口号,一般是 8080
    hot: true, // 是否启用 webpack 的 Hot Module Replacement 功能,也就是模块热替换
    stats: 'errors-only', // 终端仅打印 error
    compress: true, // 是否启用 gzip 压缩
  },
}

之后我们需要在 package.json 中添加一个 ``scripts 命令

{
    // ...
    "scripts": {
        "start": "webpack serve  --open"
    }
}

此时,就可以使用 yarn start 命令,在浏览器中打开 http://localhost:8080/页面,并可以使用热更新来进行调试,真是太方便了!

清除旧的打包产物

这个问题很好解决,直接使用 clean-webpack-plugin 插件即可。但是,要注意的是,webpack 5.20 版本后,webpackoutput已经支持在每次打包前清除构建产物,仅需在 webpack.config.js 中,在 output 字段中添加 clean: true 即可实现与 clean-webpack-plugin 相同的效果

// ...

module.exports = {
  ...
  output: {
      // ...
      clean: true
  }
}

环境拆分

一般来说,一个项目会分为 开发预发生产环境,这里的话主要还是分为开发生产环境

更新目录结构,在根目录下创建 build 文件夹,将原来根目录下的 wbepack.config.js 删除,在 build 目录下创建 webpack.base.config.js(公共部分)、webpack.dev.config.js(开发部分)、webpack.prod.config.js(生产部分) 三个文件。

webpack.base.config.js

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

const rootDir = process.cwd();

module.exports = {
  entry: path.resolve(rootDir, 'src/index.js'),
  output: {
    path: path.resolve(rootDir, 'dist'),
    filename: 'bundle.[contenthash:8].js',
  },
  module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        use: 'babel-loader',
        include: path.resolve(rootDir, 'src'),
        exclude: /node_modules/,
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(rootDir, 'public/index.html'),
      inject: 'body',
      scriptLoading: 'blocking',
    }),
  ],
}

在配置生产和开发环境时,需要安装一个 webpack-merge 的插件,用于合并配置。

yarn add --dev webpack-merge

webpack.dev.config.js

const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'development',
  devServer: {
    port: '8080', // 默认是 8080
    hot: true,
    stats: 'errors-only', // 终端仅打印 error
    compress: true, // 是否启用 gzip 压缩
  },
});

webpack.prod.config,js

const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'production',
});

最后,要注意,更新一下 package.json 文件中的 scripts 命令:

"scripts": {
    "start": "webpack serve --config build/webpack.dev.config.js --open",
    "build": "npx webpack --config build/webpack.prod.config.js"
}

继续完善功能

支持 sass 和 css

首先,在 src 目录下添加一个 index.scss 文件,并在 index.js 文件中引入,此时在执行 yarn start 后,会发现,webpack 是无法在没有安装对应 loader 的情况下,识别 scss 和 css 文件的内容。

接下来让我们来安装 loader。

yarn add --dev sass dart-sass sass-loader css-loader style-loader

注意:安装 sass 一般需要配合 node-sass 来使用,但根据官方文档,会发现其实他们更推荐使用的是 dart-sass,如果使用过 creat-react-app 来配置 sass 的同学会发现,create-react-app 是不支持 dart-sass 的。不过在这里我们自己手动配置,那使用 dart-sass 自然是没有问题的。

然后,修改 webpack.base.config.js 文件

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(s[ac]ss|css)$/i,
        exclude: /node_modules/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      },
    ]
  },
  // ...
}

此时,再使用 yarn start 运行项目,就可以看到 css 在项目中已经可以展示啦!

添加 postcss

接下来,我们来加入 postcss

yarn add --dev autoprefixer postcss postcss-loader

更新 webpack.base.config.js 文件

const autoprefixer = require('autoprefixer');

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(s[ac]ss|css)$/i,
        exclude: /node_modules/,
        use: [
          'style-loader',
          'css-loader',
          'sass-loader',
          {
            loader: "postcss-loader",
            options: {
              postcssOption: {
                plugins: [
                  ["autoprefixer"]
                ]
              }
            }
          }
        ]
      },
    ]
  },
  // ...
}

打包后抽离 css 文件

首先安装 mini-css-extract-plugin 插件

yarn add --dev mini-css-extract-plugin

更新 webpack.base.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(s[ac]ss|css)$/i,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  ["autoprefixer"]
                ]
              }
            }
          }
        ]
      },
    ]
  },
  plugins: [
    // 省略...
    new MiniCssExtractPlugin({
      filename: 'css/[name].css',
    }),
  ],
}

此时,再运行 yarn build 来查看一下

image.png

可以看到,css 文件已经被抽离到指定的目录下了。

把静态资源复制到打包目录

有时候可能会有一份静态的、需要手动下载下来的文件,需要手动加入到项目中。通常情况下,需要把这份资源放入到我们的 public 文件目录中,然后在 index.html 中用 script 导入。 但实际情况是,在 public 目录下添加后,还是无法找到这个文件。

// index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>

<script src="./js/test.js"></script>
</body>
</html>

编译结果:

image.png

那么这时候就需要用到 copy-webpack-plugin 这个插件,在打包构建时,把指定的文件复制到打包的产物中。

yarn add --dev copy-webpack-plugin

更新 webpack.base.js

const CopyWebpackPlugin = require('copy-webpack-plugin');

const rootDir = process.cwd();

module.exports = {
  // ...
  plugins: [
    
    new CopyWebpackPlugin({
      patterns: [
        {
          from: '*.js',
          context: path.resolve(rootDir, "public/js"),
          to: path.resolve(rootDir, 'dist/js'),
        },
      ],
    })
    new MiniCssExtractPlugin({
      filename: 'css/[name].css',
    }),
    new OptimizeCssPlugin(),
  ],
}

再次运行 yarn start

image.png

现在这个静态的 js 文件已经可以成功加载了

加载图片资源

前端项目,自然免不了要引入图片等资源,此时,我们尝试在项目中引入资源。 在 index.js 中引入图片:

import './index.scss'
import imgTets from './assets/1.png'
class Hello {
  constructor() {
    console.log('hello webpack!')
  }

  renderImg() {
    const img = document.createElement('img')
    img.src = imgTets
    document.body.appendChild(img)
  }
}

const test = new Hello()
test.renderImg()

运行项目,会看到一个熟悉的报错:

image.png

正常来说,那就按提示走,缺少什么 loader,那就直接 yarn add xxx-loader 就完事了,那这里就安装 raw-loaderurl-loaderfile-loader,一把梭就行了。

但是,要注意的是,在 webpack 5 中,并不需要再安装这些依赖了,只需在 webpack.base.config.js 的配置中加上:

rules: [
    // ...
    {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
        type: 'asset',
    },
]

重新运行一遍 yarn start,此时再运行,就没有问题啦!

构建阶段的项目优化

缓存

webpack 5 已经为我们做了许多事情,其中就包括了缓存。配置:

// webpack.dev.config.js

module.exports = merge(baseConfig, {
  mode: 'development',
  //...
  cache: {
    type: 'memory'
  },
});
// webpack.prod.config.js

module.exports = merge(baseConfig, {
  mode: 'production',
  // ...
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    },
  },
});

然后我们来尝试运行两次 yarn build,来查看前后的时间差

image.png

image.png

可以看到前后的差距,还是比较大的

代码拆分

分割各个模块代码,提取相同部分代码,好处是减少重复代码的出现频率。从 webpack 4 开始,就开始使用 splitChunks 来代替 CommonsChunksPlugin 做代码的拆分操作。 配置:

// webpack.base.config.js

const webpack = require('webpack');

module.exports = {
  //...
  plugins: [
    new webpack.optimize.SplitChunksPlugin(),
  ],
  optimization: {
    splitChunks: {
      chunks: 'all' // 代码分割类型:all全部模块,async异步模块,initial入口模块
    }
  },
}

多线程打包

项目的打包速度在大型项目里是一个比较令人头疼的点,这里我们利用 thread-loader 来对项目进行多线程打包。

安装依赖:

yarn add --dev thread-loader

更新 webpack.base.config.js

module.exports = {
  entry: path.resolve(rootDir, 'src/index.js'),
  output: {
    path: path.resolve(rootDir, 'dist'),
    filename: 'bundle.[contenthash:8].js',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        use: ['thread-loader', 'babel-loader'],
        include: path.resolve(rootDir, 'src'),
        exclude: /node_modules/,
      },
      {
        test: /\.(s[ac]ss|css)$/i,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader,
          'thread-loader',
          'css-loader',
          'sass-loader',
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  ["autoprefixer"]
                ]
              }
            }
          }
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
        type: 'asset',
      },
    ]
  },
  //...
}

阶段一总结

到这里为止,就是一个比较通用的、基于 webpack 5 的脚手架了。但是,要直接拿来开发,还是需要再做一些操作,因为,react 或者 vue 都还没有安装配置呢!

在阶段二,我需要在这个脚手架的基础上,根据自己的需求,添加 reacttypescripttailwindcss 等功能,以满足日常开发的需要!