我正在参加「掘金·启航计划」
之前断断续续学过一写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
由于没有配置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
我们需要一下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
我们看到了最终的打包结果
我们执行npm start
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>
此时页面
好了,现在完成了一个基本的配置了
配置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
可以看到现在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 中
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 被单独打到一个包
default: {
minChunks: 2,
priority: -30,
reuseExistingChunk: true,
},
默认配置是当模块被引用两次以上就会被单独打到一个包里,比如我们平常写代码的公共方法和组件
另外optimization中还有一个配置
optimization: {
runtimeChunk: true,
}
runtimeChunk 为每个仅含有 runtime 的入口起点添加一个额外 chunk。
我们的入口文件被单独打包到runtime~main.bundle.js
我们都知道路由懒加载,那是为什么呢?
我们可以改下页面里的组件引用方式
src/Layouts/index.js
// import List from '../pages/List'
const List = () => import(/* webpackChunkName: "list" */ '../pages/List')
再次打包看下结果,list单独打包到一个文件(前提设置了 runtimeChunk:true)
我们这个时候看的都是打包的状态,那开发环境中我们发现本地网页打开,页面没有渲染, 我们还需要做以下配置
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(),
],
})
这样就可以正常启动项目了
方便后面测试 把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目录下是我们生成的打包文件
同时,把splitChunks 的dll注释
// dll: {
// test: /[\\/]antd[\\/]/,
// priority: -10,
// },
再看看打包前后对比 dll 前
dll 配置后
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'),
},
文件名现在包含了hash值
我们改变src/List/index.js 文件内容(随便更改)
import React from 'react'
export default function List() {
return <div>Listchange</div>
}
可以看到只有main bundle 改变了,其他名称都没有变化
那我们再更改下main
总结
这只是一个初步的学习,不是很完善,只是对webpack有了一个初步的印象,万事开头难,💪🏻💪🏻