关于学习webpack这件事

134 阅读7分钟

我正在参加「掘金·启航计划」

之前断断续续学过一写webpack,但是没有整体学过,所以准备自己重新学习下

通过从0搭建一个项目来学习,采用react框架 通过create-react-app 创建的项目,让我远离了基本的webpack配置,不方便拓展

创建项目开始参考了这篇文章 medium.com/@JedaiSabot…

创建项目

mkdir react-demo
cd react-demo
npm init -y
git init
touch .gitignore
mkdir public
mkdir src

.gitignore

/node_modules

/dist

public 里添加index.html

<!-- sourced from https://raw.githubusercontent.com/reactjs/reactjs.org/master/static/html/single-file-example.html -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>React Starter</title>
</head>

<body>
  <div id="root"></div>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <script src="main.bundle.js"></script> //webpack output 定义的输出文件
</body>

</html>

配置babel

npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react

这是我安装的版本号

 "@babel/cli": "^7.17.6",
    "@babel/core": "^7.17.8",
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-react": "^7.16.7",

配置 babel.config.json

看babel 官网 推荐用 babel.config.json

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

配置页面

npm install react react-dom

在src下创建index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App.js";
ReactDOM.render(<App />, document.getElementById("root"));

在src下创建App.js

import React, { Component} from "react";
import "./App.css";

class App extends Component{
  render(){
    return(
      <div className="App">
        <h1> Hello, Wo! </h1>
      </div>
    );
  }
}

export default App;

在 src 下创建App.css

.App {
    margin: 1rem;
    font-family: Arial, Helvetica, sans-serif;
  }

配置webpack

安装 webpack4 (这是我想学习的版本,webpack5之后再学吧)

npm install --save-dev webpack@4.46.0 webpack-cli@4.9.2 webpack-dev-server@4.7.4

我创建了多个webpack 配置文件,方便区分不同环境(里面有些基本的配置,想必大家都能看懂)

安装webpack-merge来合并配置

webpack.base.js

const path = require('path')


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

webpack.dev.js

const { merge } = require('webpack-merge')
const base = require('./webpack.base.js')

module.exports = merge(base, {
  mode: 'development',
  devServer: {
    port: 3000,
  },
})

webpack.prod.js

下载 html-webpack-plugin@4.4.1(在dist 生成html) clean-webpack-plugin(清理dist)

const {merge} = require('webpack-merge')
const base = require('./webpack.base.js')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = merge(base,{
    mode:'production',
     plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'demo' })],
})

配置packge.json

 "start": "webpack-dev-server --config webpack.dev.js",
  "build": "webpack --config webpack.prod.js"

这个时候可以 运行 npm start npm build

image.png

由于没有配置bable-loader 安装 babel-loader 我们添加一下配置

const path = require('path')

module.exports = {
  entry: './src/index.js',
  module: {
    rules: [
       {
         test: /\.(js|jsx)$/,
         exclude: /(node_modules|bower_components)/,
         loader: 'babel-loader',
         options: { presets: ['@babel/env'] },
      },
    
    ],
  },
  resolve: { extensions: ['*', '.js', '.jsx'] },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
}

继续执行 npm run build

image.png

我们需要一下loader告诉webpack加载css style-loader@2.0.0 css-loader@5.1.4

const path = require('path')

module.exports = {
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel-loader',
        options: { presets: ['@babel/env'] },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  resolve: { extensions: ['*', '.js', '.jsx'] },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
}


最后再试下npm run build

我们看到了最终的打包结果

image.png

我们执行npm start

image.png

webappack

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

入口只有一个,没有指定名字,[name].bundle.js name默认为main

因此在public/index.html 我们引入的是<script src="main.bundle.js"></script>

此时页面

image.png

好了,现在完成了一个基本的配置了

配置eslint

项目必须配置eslint 统一代码风格

我是参考这个篇文章配置的 juejin.cn/post/684490…

先下载eslint

npm install eslint --save-dev

运行以下命令会自动生成配置文件,可以按自己需要进行选择配置

./node_modules/.bin/eslint --init

下载一些插件

npm install -D eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y eslint-plugin-prettier prettier-eslint

.eslint-json

具体配置含义: juejin.cn/post/685929…

{
    "env": {
        "browser": true,
        "es2021": true,
        "node": true
    },
    "extends": [ // 继承一下规则
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:import/recommended",
        "plugin:prettier/recommended"
    ],
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [ // 其他规则插件,需要在rules里开启才有作用
        "react",
        "prettier",
        "jsx-a11y"
    ],
    "rules": {
    }
}

.prettierrc.js

{
    "env": {
        "browser": true,
        "es2021": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:import/recommended",
        "plugin:prettier/recommended"
    ],
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "prettier",
        "jsx-a11y"
    ],
    "rules": {
    }
}

eslint-webpack-plugin

我们需要在启动项目时,控制台能告诉我们eslint报错,规范我们团的的开发,需要配置eslint 插件

安装 eslint-webpack-plugin

const { merge } = require('webpack-merge')
const base = require('./webpack.base.js')
const ESLintWebpackPlugin = require('eslint-webpack-plugin')

module.exports = merge(base, {
  mode: 'development',
  devServer: {
    port: 3000,
  },
  plugins: [new ESLintWebpackPlugin()], // eslint-loader已经废弃,eslint-webpack-plugin 在控制台显示eslint 报错
})

这样配置后,有eslint语法错误,控制台我们可以看到

配置页面

我们可以看到上边打包后的文件很小,因为我们还没引入各种插件,接下来引入antd,看看打包后的体积

先配置下页面

创建目录 Layouts compontents pages

安装antd 安装 react-router-dom

在pages创建 DataSet 和 List 两个page ,内容随便写就行

Layouts下创建index.js 引入两个page

import React from 'react'
import { Layout, Menu, Breadcrumb } from 'antd'
import { Routes, Route, Link } from 'react-router-dom'

const { Header, Content, Footer } = Layout

import List from '../pages/List'
import DataSet from '../pages/DataSet'



export default function Layouts() {
  return (
    <Layout className="layout">
      <Header>
        <div className="logo" />
        <Menu theme="dark" mode="horizontal" defaultSelectedKeys={['2']}>
          <Menu.Item key="1">
            <Link to="/">列表页面</Link>
          </Menu.Item>
          <Menu.Item key="2">
            <Link to="/dataSet">数据页面</Link>
          </Menu.Item>
        </Menu>
      </Header>
      <Content style={{ padding: '0 50px' }}>
        <Breadcrumb style={{ margin: '16px 0' }}>
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div className="site-layout-content">
          <Routes>
            <Route path="/" element={<List />} exact />
            <Route path="/dataSet/" element={<DataSet />} />
          </Routes>
        </div>
      </Content>
      <Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
    </Layout>
  )
}

Appjs 引入

import React, { Component } from 'react'
import './App.css'
import Layout from './Layouts'
import 'antd/dist/antd.css'
class App extends Component {
  render() {
    return (
      <div className="App">
        <Layout></Layout>
      </div>
    )
  }
}

export default App

修改 /src/index

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'
import 'antd/dist/antd.css'
import { BrowserRouter } from 'react-router-dom'

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
)

现在src目录是这样滴

├── App.css
├── App.js
├── index.js
├── Layouts
│   ├── Routes.js
│   └── index.js
├── components
└── pages
    ├── DataSet
    │   └── index.js
    └── List
        └── index.js

SplitChunksPlugin

npm run build

image.png

可以看到现在bundle就很大了,因为antd等插件都打在一个包里,现在我们需要去优化它,webpack官网推荐使用SplitChunksPlugin优化项目

我们先看看他默认的配置 webpack.base.js

const path = require('path')

module.exports = {
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel-loader',
        options: { presets: ['@babel/env'] },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  resolve: { extensions: ['*', '.js', '.jsx'] },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  
  // 默认配置
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
}

因为是默认配置,打包后的结果和之前不会有变化

分别来看看这些参数都是什么意思

splitChunks: {
      // 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
      chunks: 'async',
      // 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
      minSize: 30000,
      // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
      minChunks: 1,
      // 表示按需加载文件时,并行请求的最大数目。默认为5。
      maxAsyncRequests: 5,
      // 表示加载入口文件时,并行请求的最大数目。默认为3。
      maxInitialRequests: 3,
      // 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
      automaticNameDelimiter: '~',
      // 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
      name: true,
      // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        //
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },

      },
    },

我们修改一下 chunks 改为all

可以看到 node_module里的插件被单独打到一个包里,名称是 vendors~main 中

image.png


 splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -20,
        },
        dll: {
          test: /[\\/]antd[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -30,
          reuseExistingChunk: true,
        },
      },
    },

可以看到,antd 被单独打到一个包

image.png

 default: {
          minChunks: 2, 
          priority: -30,
          reuseExistingChunk: true,
        },

默认配置是当模块被引用两次以上就会被单独打到一个包里,比如我们平常写代码的公共方法和组件

另外optimization中还有一个配置

optimization: {
    runtimeChunk: true,
}

runtimeChunk 为每个仅含有 runtime 的入口起点添加一个额外 chunk。

我们的入口文件被单独打包到runtime~main.bundle.js

image.png

我们都知道路由懒加载,那是为什么呢?

我们可以改下页面里的组件引用方式

src/Layouts/index.js

// import List from '../pages/List'
const List = () => import(/* webpackChunkName: "list" */ '../pages/List')

再次打包看下结果,list单独打包到一个文件(前提设置了 runtimeChunk:true)

image.png

我们这个时候看的都是打包的状态,那开发环境中我们发现本地网页打开,页面没有渲染, 我们还需要做以下配置

webpack.base.js

const { merge } = require('webpack-merge')
const base = require('./webpack.base.js')
const ESLintWebpackPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = merge(base, {
  mode: 'development',
  devServer: {
    port: 3000,
  },
  plugins: [
    new HtmlWebpackPlugin({ title: 'demo', template: './public/index.html' }),
    new ESLintWebpackPlugin(),
  ],
})

这样就可以正常启动项目了

image.png

方便后面测试 把src/Layouts/index.js 修改恢复原来的样子

dll 分割打包 (feature/dll 还需要再次学习)

关于dll,我也是借鉴了其他文章

自己的理解是单独打包第三方文件到一个包或多个,这样我们我们正常打包的时候,不需要将之前dll打过的再打一次,增加构建时间

具体配置如下

新建 webpack.dll.config.js

// 定义常用对象
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

// dll文件存放的目录
const dllPath = 'public/vendor'

module.exports = {
  // 需要提取的库文件
  entry: {
    react: ['react', 'react-dom'],
    antd: ['antd'],
  },
  output: {
    path: path.join(__dirname, dllPath),
    filename: '[name].dll.js',
    // vendor.dll.js中暴露出的全局变量名
    // 保持与 webpack.DllPlugin 中名称一致
    library: '[name]_[hash]',
  },
  plugins: [
    // 清除之前的dll文件
    new CleanWebpackPlugin({
      root: path.join(__dirname, dllPath),
    }),
    // 定义插件
    new webpack.DllPlugin({
      path: path.join(__dirname, dllPath, '[name]-manifest.json'),
      // 保持与 output.library 中名称一致
      name: '[name]_[hash]',
      context: process.cwd(),
    }),
  ],
}

webpack.base.js 添加

 plugins: [
    // // 避免在公共区域重复编译依赖
    new webpack.DllReferencePlugin({
      context: process.cwd(),
      manifest: require(`./public/vendor/react-manifest.json`),
    }),
    new webpack.DllReferencePlugin({
      context: process.cwd(),
      manifest: require(`./public/vendor/antd-manifest.json`),
    }),
  ],

packge.json

"dll": "webpack --config webpack.dll.config.js"

运行npm run dll

public目录下是我们生成的打包文件

image.png

同时,把splitChunks 的dll注释

  // dll: {
        //   test: /[\\/]antd[\\/]/,
        //   priority: -10,
        // },

再看看打包前后对比 dll 前

image.png

dll 配置后

image.png vendors~main.bundle.js 比之前要小了,因为我们dll中把react 单独打包了,

开发环境下我们需要手动引入的public/index.html中

<!-- sourced from https://raw.githubusercontent.com/reactjs/reactjs.org/master/static/html/single-file-example.html -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>React Starter</title>
</head>

<body>
  <div id="root"></div>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <script src="./vendor/react.dll.js"></script>
  <script src="./vendor/antd.dll.js"></script>
</body>

</html>

运行

runtime

runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。

mainifest

在你的应用程序中,形如 index.html 文件、一些 bundle 和各种资源,都必须以某种方式加载和链接到应用程序,一旦被加载到浏览器中。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来……

当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。

webpack-manifest-plugin

我们可以通过 webpack-manifest-plugin 插件来生成mainfest.json文件,这是一份资源清单,通过

npm install webpack-manifest-plugin@2 --save-dev

可以看到打包多了manifest.json文件

{
  "antd~main.js": "antd~main.1bb71d5bdb5330a758fa.js",
  "main.js": "main.bd36c8fddcbd656b45a1.js",
  "runtime~main.js": "runtime~main.02279f12afe42b34928c.js",
  "vendors~main.js": "vendors~main.bd6f717f446b820de1ef.js",
  "index.html": "index.html"
}

提取runtime

参考

在webpack.base.js 新增配置如下

  recordsPath: path.join(__dirname, 'records.json'),

npm run build 后就能看到分离出的runtime records.json

稍微有个概念就行

缓存

浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快

通过配置contenthash 来生成文件名

 output: {
    // filename: '[name].bundle.js',
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },

image.png

文件名现在包含了hash值

我们改变src/List/index.js 文件内容(随便更改)

import React from 'react'

export default function List() {
  return <div>Listchange</div>
}

image.png

可以看到只有main bundle 改变了,其他名称都没有变化

那我们再更改下main

总结

这只是一个初步的学习,不是很完善,只是对webpack有了一个初步的印象,万事开头难,💪🏻💪🏻

参考文章

juejin.cn/post/684490…