从零到一配置webpack

本文以react项目为例,从零到一配置webpack,涉及分chunk优化等内容。

项目初始化

在新建的文件夹中打开shell,输入npm init -y进行项目初始化,生成package.json

package.json中的scripts当中添加如下指令,限制安装依赖使用pnpm

"scripts": {
  "preinstall": "npx only-allow pnpm"
},

接着在decDependencies中添加webpackwebpack-cli,一定记得webpack是在开发依赖当中。

"devDependencies": {
  "webpack": "5.89.0",
  "webpack-cli": "5.1.4"
}

这里推荐使用vscode插件Version Lens进行版本提示,安装后点击在屏幕右上角的V字按钮使用:

接着pnpm i安装依赖,然后创建src目录,在src目录下创建index.js,输入以下代码:

const a = 1;
console.log(a);

然后在package.json下的scripts中添加build指令,并且在终端执行pnpm build

"scripts": {
  "preinstall": "npx only-allow pnpm",
  "build": "webpack"
},

可以看到在dist目录下生成了main.js,并且其中代码如下:

console.log(1);

这是因为webpack默认src下的index.js作为入口,去生成dist下的main.js,并且在没有指定mode的情况下,webpack默认打开生产环境,会对代码执行 Tree-shaking。

配置

我们要真正给 webpack 指明入口、出口,以及相关的配置,就需要创建webpack.config.js进行配置。

const path = require('path')

module.exports = {
  mode: "production",
  entry: path.resolve(__dirname, "src", "index.js"),
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "build")
  }
}

其中,mode指明了我们现在使用生产环境,entryoutput分别对应入口和出口,并且我们推荐使用path来对路径进行处理。

我们也可以在打包产物中加入哈希,以便更好地控制缓存。

output: {
  filename: "bundle.[hash].js",
  path: path.resolve(__dirname, "build")
}

生成的产物如下,其中一个用了hash,另一个用了内容哈希contenthash,当然也可以用chunkhash

我们可以在output中指定clean: true使得每次打包都能清除之前的产物。

接下去要介绍 webpack 中的几大概念:

  • entry 入口

  • output 输出

  • module 模块

  • asset 资源

  • chunk 代码块

  • bundle 产物

babel

我们接下来要做的就是配置模块,简单来说就是处理什么样的文件,使用什么样的 loader,这里以 babel-loader 为例,在webpack.config.js中添加module配置,同时也要记得去安装 babel-loader 和 @babel/core。

module: {
  rules: [
    {
      test: /\.js$/,
      use: "babel-loader"
    }
  ]
},

babel 的配置我们建议在独立的文件babel.config.json中配置,而不是在webpack.config.js中。

我们测试下面这个例子,在src/index.js中有如下代码:

const sum = (a, b) => {
  return a + b;
}

const result = sum(20, 30);

console.log(result, sum)

打包后的产物如下:

!function(){const o=(o,n)=>o+n,n=o(20,30);console.log(n,o)}();

可见代码经过了丑化压缩,并且最外层使用了立即执行函数(IIFE)做作用域的隔离。

而我们如果要在输出时对代码进行降级,也就是打polyfill,因为有些低版本浏览器不支持高阶语法,比如箭头函数,我们就要借助编译工具,也就是 babel 的插件集,这里用以上的例子测试下箭头函数转 function。

首先安装@babel/plugin-transform-arrow-functions,然后在babel.config.json中做如下配置:

{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

接着pnpm build打包,产物如下:

!function(){const n=function(n,o){return n+o},o=n(20,30);console.log(o,n)}();

可以看到箭头函数被编译成了普通函数。

那么我们要转换一种语法就要用一种插件,以后项目就会非常复杂,所以 babel 把插件打包成了 preset。

我们将刚刚的插件换成@babel/preset-env,在babel.config.json中作如下配置:

{
  "presets": ["@babel/preset-env"]
}

最终打包结果和使用箭头函数插件的结果一样。

Typescript

再往后我们要去做 typescript 编译相关的配置。

首先我们需要安装 babel 中 ts 的预设@babel/preset-typescript

然后要在babel.config.json中作如下配置:

{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"]
}

并且webpack.config.js中,babel-loader处理的文件类型也要进行相应的改变:

module.exports = {
  mode: "production",
  entry: path.resolve(__dirname, "src", "index.ts"),
  module: {
    rules: [
      {
        test: /\.(j|t)s$/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  output: {
    filename: "bundle.[hash].js",
    path: path.resolve(__dirname, "build"),
    clean: true
  }
}

最后我们打包如下index.ts

const myName: string = "musio";
const age: number = 18;

console.log(myName, age);

产物如下:

console.log("musio",18);

如果这个过程中有哪里报错就去检查有没有什么地方没有改成 ts。

React

接下去我们基于 webpack 自己配置一个 react 项目。

我们首先要安装@babel/preset-reactreact,其中react要安装到dependencies当中。

"dependencies": {
  "react": "18.2.0"
},
"devDependencies": {
  "@babel/core": "^7.23.7",
  "@babel/preset-env": "^7.23.8",
  "@babel/preset-react": "^7.23.3",
  "@babel/preset-typescript": "^7.23.3",
  "babel-loader": "^9.1.3",
  "webpack": "5.89.0",
  "webpack-cli": "5.1.4"
}

其次我们要在babel.config.json的preset中加入@babel/preset-react

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript",
    "@babel/preset-react"
  ]
}

接下去要修改webpack.config.js中识别文件的类型:

module: {
  rules: [
    {
      test: /\.(js|ts|tsx)$/,
      use: {
        loader: "babel-loader"
      }
    }
  ]
},

此外,为了简化导入时的名称,无需'xxx.tsx'而是'xxx',我们要在webpack.config.js中指定后缀名。

resolve: {
  extensions: [".js", ".ts", ".tsx"]
}

接下去我们创建基本的组件示例Button.tsx

import React from 'react'

const Button = () => {
  return <div>I am Button</div>
}

export default Button;

运行pnpm build,最终产物包括了 react 的一些 hooks 以及最关键的:

return e.createElement("div",null,"I am Button")

devServer

这里是 webpack 很重要的一个能力,也就是 devServer 和热更新的能力。

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

devServer: {
  // 项目构建后的路径
  static: path.resolve(__dirname, "build"),
  // 启动 gzip 压缩
  compress: true,
  // 端口号
  port: 3000,
  // 自动打开浏览器
  open: true,
  // 开启 HMR 功能
  hot: true
}

同时我们要安装webpack-dev-server,并且此时,再叫build就不合理了,应该改成:

"scripts": {
  "preinstall": "npx only-allow pnpm",
  "dev": "webpack-dev-server",
  "build": "webpack --mode production"
},

或者也可以用webpack --config webpack.dev.js,通过独立的文件来配置开发环境。

运行pnpm dev,可以看到自动打开了网页,并且更改 src 中的代码,项目实时更新。

我们更改 App 和 Button 组件,来写点 react 基本语法:

import React from "react"
import { useState } from "react"
import Button from './Button'

const App = () => {
  const [count, setCount] = useState(0)
  return (
    <>
      <div>App {count}</div>
      <Button onClick={() => setCount((c) => c + 1)}>+</Button>
    </>
  )
}

export default App
import React from 'react'

interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({ onClick, children }) => {
  return <button onClick={onClick}>{children}</button>
}

export default Button;

可以看到点击按钮,可以使 count 值增加。

但是代码会有报错提示,这是因为我们没有 tsconfig 导致的,所以我们接下去去配置tsconfig.json

tsconfig.json

tsconfig.json配置如下:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ES6",
    "jsx": "react",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}

我们发现配置了tsconfig.json之后还是会有许多报错,这是因为我们要安装@types/react@types/react-dom这两个依赖。

安装后发现现在只有createRoot报错,我们改成以下的方式:

import React from "react";
import ReactDOM from "react-dom"

import App from './App'

ReactDOM.render(<App />, document.getElementById("root"));

优化

优化以前是通过 webpack 插件去实现的,现在是在 webpack 中通过配置optimization去实现,要记得优化一定是在生产模式中。

我们使用webpack官网推荐的配置:

optimization: {
  usedExports: false,
  splitChunks: {
    chunks: 'async',
    minSize: 20000,
    minRemainingSize: 0,
    minChunks: 1,
    maxAsyncRequests: 30,
    maxInitialRequests: 30,
    enforceSizeThreshold: 50000,
    cacheGroups: {
      defaultVendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
        reuseExistingChunk: true,
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true,
      },
    },
  },
},

我们也可以测试一下 react 的 lazyComponent,在App.tsx中:

import React from "react"
import { Suspense, useState } from "react"
// import Button from './Button'
const AsyncButton = React.lazy(() => 
  import('./Button')
)

const App = () => {
  const [count, setCount] = useState(0)
  return (
    <>
      <div>App {count}</div>
      <Suspense fallback={<div>loading...</div>}>
        <AsyncButton onClick={() => setCount((c) => c + 1)}>+</AsyncButton>
      </Suspense>
      
    </>
  )
}

export default App

可以看到生成了另一部分代码,内容就是 button。

为什么要分chunk?

因为首屏加载优化,我们要让第一次加载的代码量足够小。

我们可以安装webpack-bundle-analyzer来分析代码体积,然后在webpack.config.js中使用:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  ...
  plugins: [
    ...
    new BundleAnalyzerPlugin()
  ],
  ...
}

此时重新运行 build:

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值