【教程】webpack从零搭建TS版React开发环境

361 阅读8分钟

前言

本文初衷:平时的项目大多用create-react-app开发,从开发到打包都只是在敲命令而见不到webpack.config.js配置文件,简直是面向黑箱编程,遇到问题之后即使解决了也不知道怎么回事,这种感觉非常不好。

学习 webpack 的重要性不言而喻,即使市面上已经有如此众多的成熟脚手架,比如普通项目可以用 CRA,SPA 管理系统可以用 antdpro,打包组件库可以用 tsdx 等等,但如果不懂这些打包工具的原理甚至基础用法,总有一天你会遇到奇葩问题而不知道如何解决。

本文将以问题导向的形式,在实际搭建过程中逐个剖析webpack重要配置,深浅适宜,整体内容较基础,适合初入坑 webpack 的小伙伴们参考。

本示例 webpack 版本为5.xwebpack-cli版本为4.x

话不多说,马上开始吧~

正文

1.项目初始化

mkdir webapck-ts-react
cd webapck-ts-react
yarn init 
yarn add webpack webpack-cli -D

空项目中初始化为以下结构:

image.png

🤔 问题1:webpack是什么?

👉 展开查看答案 - webpack是一个打包工具;将符合`ES Module`和`CommonJS`模块化规范的工程文件打包成一个静态资源(可部署到服务器)

image.png

一张图讲清楚webpack的作用

此时直接执行npx webpack命令试试看吧:

webpack

神奇的事情发生了,会发现多出了个文件夹dist,里面是打包编译好的文件main.js,如果我们没有在webpack.config.js中配置任何内容,则默认按照相应出入口进行打包,默认命令类似:

// webpack.config.js

const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

再次命令行执行npx webpack结果一样,验证默认配置就是上面这样的~

配置项中entry为入口,可以配置为相对路径; output为出口,path属性必须设置为绝对路径;

为什么输出路径要求绝对路径?

以上差异原因在于项目中入口一般可以确定为本项目中,但是出口理论上可以是磁盘上任意值,所以output的path必须为绝对路径。

outputfilename在单入口项目中可以写任意固定值,在多入口项目中不能写固定值,[name]为变量占位符表示不固定的值;

🤔 问题2:为什么要npx webpack而不是直接webpack?

👉 展开查看答案 webpack打包命令默认有两种方式:全局和本地(局部); 如果直接执行webpack则用的是全局webpack编译,结果一样的嗷; 如果使用npx webpack则会在当前项目中寻找webpack指令执行,查找路径为/node_modules/bin/webpack

image.png

🤔 问题3:全局有webpack命令不就够了吗?为啥本地还要安装webpack?

👉 展开查看答案 全局安装的都是固定版本(比如最新的5.x),有些年代久远的项目需要需要使用更早期的webpack版本(比如4.x),为了防止版本冲突,所以开发中一般都是用项目本地版本

不过每次都要npx webpack未免太麻烦了,所以我们可以在package.json中做如下配置:

// package.json
...
  "scripts": {
    "build": "webpack"
  },
...

之后直接执行yarn build就和执行npx webpack效果一样啦~

2.处理图片loader

接着我们发挥下webpack模块化打包的特性,新建一个模块专门在页面上加载图片:

// src/loadImg.js

import Img from './images/picture.jpg'

const Image = document.createElement('img')
Image.src = Img

document.body.appendChild(Image)

index.js中引入

require('./loadImg')

function sum (a, b) {
  return a + b
}

console.log(sum(1, 2))

执行yarn build发现报错:

image.png

提示得很清楚啦,由于webpack默认只认识.js.json文件,对于图片文件的识别是需要借助loader的;

🤔 问题4:loader是什么?

👉 展开查看答案 webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。

在webpack4.x版本中处理图片需要用到file-loader,url-loader或raw-loader,但是在webpack5.x中不需要了,对于图片和字体文件等,可以通过type: asset声明直接处理文件。

这里我们采用5.x的方式处理图片:

const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  },
+  module: {
+    rules: [
+      {
+        test: /\.(png|jpg|jpeg|gif|webp)$/,
+        type: 'asset'
+      }
+    ]
+  }
}

打包成功:

image.png

新建HTML文件,引入打包后的main.js文件测试,注意script标签一定要加defer属性:

dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script defer src="./main.js"></script>
</head>
<body>
  
</body>
</html>

🤔 机智如你已经发现了,直接在dist文件夹中新建额外文件的操作不对劲吧,别急,后面会有plugin帮我们自动处理的。

打开dist/index.html预览,一切正常:

image.png

3.处理css文件loader

让我们新建一个css文件

// src/css/index.css

body {
  background-color: burlywood;
  color: blueviolet;
}

引入

src/index.js
require('./loadImg')
+ import './css/index.css'

function sum (a, b) {
  return a + b
}

console.log(sum(1, 2))

不出所料,还是同样内容的报错:缺少合适的loader,因为上面我们已经知道了,webpack默认只能识别js文件和JSON文件,其他格式文件都需要loader帮助识别处理。 安装处理css的loader

yarn add style-loader css-loader -D

配置文件中指定.css文件的解析所用loader

...
module.exports = {
...
  module: {
    rules: [
      {
        test: /\.(png|jpg|jpeg|gif|webp)$/,
        type: 'asset'
      },
+      {
+        test: /\.css$/,
+        use: ['style-loader', 'css-loader']
+      }
    ]
  }
}

再次yarn build,无报错而且样式生效

image.png

🤔 问题5:css-loader我猜是解析css的,那么style-loader是干啥的?

👉 展开查看答案 css-loader仅能识别并打包css文件,而style-loader将打包出来的css样式插入到HTML的head中,使其在页面上生效

4.打包模式mode

接下来解决打包模式警告问题:

image.png

只需要在webpack.config.js中指定mode配置项即可

image.png

mode参数有两种:developmentproduction,默认为production,这两种模式各有一套默认配置:

Mode: development

// webpack.development.config.js

module.exports = {
 mode: 'development'
 devtool: 'eval',
 cache: true,
 performance: {
   hints: false
 },
 output: {
   pathinfo: true
 },
 optimization: {
   moduleIds: 'named',
   chunkIds: 'named',
   mangleExports: false,
   nodeEnv: 'development',
   flagIncludedChunks: false,
   occurrenceOrder: false,
   concatenateModules: false,
   splitChunks: {
     hidePathInfo: false,
     minSize: 10000,
     maxAsyncRequests: Infinity,
     maxInitialRequests: Infinity,
   },
   emitOnErrors: true,
   checkWasmTypes: false,
   minimize: false,
   removeAvailableModules: false
 },
 plugins: [
   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
 ]
}

Mode: production

// webpack.production.config.js
module.exports = {
  mode: 'production',
 performance: {
   hints: 'warning'
 },
 output: {
   pathinfo: false
 },
 optimization: {
   moduleIds: 'deterministic',
   chunkIds: 'deterministic',
   mangleExports: 'deterministic',
   nodeEnv: 'production',
   flagIncludedChunks: true,
   occurrenceOrder: true,
   concatenateModules: true,
   splitChunks: {
     hidePathInfo: true,
     minSize: 30000,
     maxAsyncRequests: 5,
     maxInitialRequests: 3,
   },
   emitOnErrors: false,
   checkWasmTypes: true,
   minimize: true,
 },
 plugins: [
   new TerserPlugin(/* ... */),
   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
   new webpack.optimize.ModuleConcatenationPlugin(),
   new webpack.NoEmitOnErrorsPlugin()
 ]
}

5.借助babel打包react项目

当前的webpack配置已经能够打包js和css以及图片文件了,接下来我们让它支持react项目的打包; 众所周知,打包react项目的核心工作就是转化其jsx语法,这就不得不提到babel了。

🤔 问题6:什么是babel?

👉 展开查看答案 Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。除此之外还能为你做的事情有:
  1. 语法转换

  2. 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)

  3. 源码转换 (codemods)

babel的使用方法: 一个核心包@babel/core必须安装的,其余功能可以通过配置插件plugins或预设presets实现,这里我们要转化jsx语法,可以直接使用@babel/preset-react这个预设(预设就是一堆插件的合集方案),考虑到在webpack中使用babel,所以还要用到babel-loader

yanr add @babel/core @babel/preset-react babel-loader -D

当然react和react-dom也需要安装到生产依赖中

yarn add react react-dom

新建index.jsx文件写入react代码

// src/index.jsx

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(<div>React组件测试</div>, document.getElementById('root'))

配置文件中更改打包入口并增加jsx解析规则:

// webpack.config.js

const path = require('path')

module.exports = {
-  // entry: './src/inde.jsx',
+  entry: './src/index.jsx',
  mode: 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|jpeg|gif|webp)$/,
        type: 'asset'
      },
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader']
      },
+      {
+        test: /\.jsx?/,
+        use: [
+          {
+            loader: 'babel-loader',
+            options: {
+              presets: ['@babel/preset-react']
+            }
+          }
+        ]
+      }
    ]
  }
}

执行yarn build打包; 更改dist/index.html文件新增id为root的节点

...
</head>
<body>
  <div id="root"></div>
</body>
</html>

可以发现编译成功:

image.png

6.配置plugin

(1)html-webpack-plugin

之前的操作中我们多次手动修改dist文件夹下的内容,这种操作肯定是不被允许的,所以我们需要配置模板,借助html-webpack-plugin自动生成这个测试用的HTML文件

yarn add html-webpack-plugin -D

src目录先新建index.html模板文件

// src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Webpack搭建TS版React开发环境</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

修改配置项,增加插件

// webpack.config.js

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

module.exports = {
  entry: './src/index.jsx',
  mode: 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
      ...
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

此时yarn build打包,发现dist文件夹下已经自动生成了模板文件,并且自动引入了main.js打包文件

(2)clean-webpack-plugin

见名知意,这个插件作用很简单,就是在每次打包生成新的打包文件之前自动删除所有老的打包文件

yarn add clean-webpak-plugin -D
// webapck.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
...

module.exports = {
  entry: './src/index.jsx',
  mode: 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[chuankhash].[name].js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  module: {
      ...
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CleanWebpackPlugin()
  ]
}

7.支持TS版React项目编译

如何让webpack支持ts呢,其实这个问题和如何支持jsx语法一样性质,对于代码转化工作都是要loader去做。

以下提供两种方案用来支持React组件的TS写法,无论哪种都要先在本地安装typescript

yarn add typescript -D

生成tsconfig.json配置文件

yarn tsc --init

方案一:babel-loader的@babel/preset-typescript

一种方法就是沿用babel-loader,通过增加预设preset来支持ts解析:

yarn add @babel/preset-typescript -D

src/index.jsx改名为src/index.tsx,同时打包配置文件中的entry也要改为entry: './src/index.tsx'babel-loader的presets数组增加预设:@babel/preset-typescript

// webpack.config.js

const path = require('path')

module.exports = {
  entry: './src/index.tsx',
  mode: 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[chuankhash].[name].js'
  },
  module: {
    rules: [
        ....
      {
        test: /\.tsx?/,
        use: [
          {
            loader: 'babel-loader',
            options: {
+              presets: ['@babel/preset-react', '@babel/preset-typescript']
            }
          }
        ]
      }
    ]
  },
}

执行yarn build可以成功打包;

但是这种方案下,很多typescript语法是不被支持的,比如我们新建一个Comp组件故意写出错误的类型定义:

const Comp = () => {
  const list: number[] = ['1', 'abc']
  let peekValue: string
  peekValue = list.pop()
  return (<>
    <div>这是COMP组件{peekValue}</div>
  </>)
}

export default Comp

执行yarn build可以看到:

image.png

说明这种方案虽然能够打包TS,但是无法在打包过程中对TS错误语法进行校验,如果既想打包又想校验怎么办呢?这是就要用到另一个loader了:

方案二:ts-loader

yarn add ts-loader -D

配置文件中移除@babel/preset-typescript预设并增加ts-loader后执行打包:

image.png

可以看到一下子出了16个error,可见ts-loader能在打包过程中对不符合规则的ts语法做校验的。

解决报错的过程分别为

  1. tsconfig.json配置"jsx": "react"

  2. yarn add @types/react @types/react-dom

  3. 解决具体语法报错

8.优化开发体验webpack-dev-server

目前每次重新打包之后都要手动查看HTML文件变更,太不“自动化”了,其实webpack允许我们开启一个本地服务监听打包过程自动更新页面,而且还能热更新。

yarn add webpack-dev-server -D

开启打包服务,在4.x版本中需要修改命令为:webpack-dev-server; 而在5.x版本中只要:webpack serve

// package.json

{
  ...
  "scripts": {
    "build": "webpack",
    "dev": "webpack serve"
  },
  ...
}

此时执行yarn dev即可观察到已经开启打包监听,devSer的具体配置项如下:

// webpack.config.js

module.exports = {
    ...
    devServer: {
      contentBase: path.join(__dirname, "dist"), // * 服务启动根目录(除了main.js所在目录之外的静态服务目录)
      compress: true, // * 为每个静态文件开启 gzip compression
      open: true, // * 是否自动打开浏览器,默认false不打开
      port: 8081, // * 自定义服务端口,默认为8080
      hot: true, // * 是否开启模块热更新,默认为false不开启
      proxy: { // * 本地正向代理(常用于非同源请求)
        "/api": {
          target: "http://localhost:3000",
          pathRewrite: {
            "^/api": "",
          },
        },
      },
    },
    ...
}

那么至此,一个ts版的react开发环境就搭建好了,剩下一些自定义配置完全根据各自公司项目需要了,比如我们项目习惯用sass module模式开发。

9.支持sass module开发模式

安装sass-loader和node-sass

yarn add sass-loader node-sass -D
// src/comp.module.scss

.wrap {
  .head {
    font-size: 20px;
    color: blueviolet;
  }
  .body {
    font-size: 14px;
    color: yellowgreen;
  }
}
// src/Comp.tsx

import React from 'react'
import styles from './comp.module.scss'

const Comp = () => {
  const list: string[] = ['1', 'abc']
  let peekValue: string
  peekValue = list.pop() as string
  return (<div className={styles.wrap}>
    <div className={styles.head}>这是COMP组件</div>
    <div className={styles.body}>测试使用</div>
  </div>)
}

export default Comp

配置文件中增加一个解析规则:

image.png

为了配合一下TS,还要新建个类型声明文件

// typed-css.d.ts

// scss模块声明
declare module '*.scss' {
  const content: {[key: string]: any}
  export = content
}
// less模块声明
declare module '*.less' {
  const content: { [key: string]: any }
  export default content
}

10.实现react模块热替换(HMR)

yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh -D
// webpack.config.js

const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
...
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new CleanWebpackPlugin(),
+    new ReactRefreshPlugin()
  ],
...

11.配置路径别名

一定要照着下面的配

// webpack.config.js
module.exports = {
  ...
  resolve: {
    extensions: [".js", ".json", ".ts", ".tsx"],
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  ...
}
// tsconfig.json

{
  "compilerOptions": {

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

image.png

image.png

结语

至此,一款工作中能用的TS版React开发环境已经搭建完毕~

现已具备功能:

  • typescript语法
  • sass module
  • 模块热替换
  • 路径别名
  • 解析图片和CSS
  • source-map 后期可支持项:
  • 第三方包优化,treeshaking,cdn等
  • 生产环境配置文件分离
  • 生产环境包体积和chunkname优化

文中项目源码:webpack-ts-react-lead