初始化项目
yarn init -y
安装 webpack
yarn add -D webpack webpack-cli webpack-dev-server
webpack 最好是安装在本项目中,避免与其它项目冲突
创建scripts文件夹,并新建base.config.js
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, '../src/index.js'),
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'static/js/[name].[contenthash:8].js'
}
};
创建src文件夹,并新建index.js
console.log('hello');
修改package.json文件中的打包命令
"scripts": {
"dev": "webpack --config scripts/base.config.js --mode development"
}
添加 react
yarn add react react-dom
修改src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<div>Hello React</div>, document.getElementById('app'));
创建public文件夹, 并新建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>
</head>
<body>
<div id="app"></div>
</body>
</html>
添加 babel
yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader
在根目录下创建babel.config.js
module.exports = {
presets: [['@babel/preset-env', { targets: { ie: 11 } }], '@babel/preset-react'],
plugins: []
};
配置 loader
module: {
rules: [
{
test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/
}
];
}
添加html-webpack-plugin
yarn add -D html-webpack-plugin
添加打包配置
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
filename: 'index.html',
inject: 'body'
})
];
环境配置
开发环境配置
引入webpack-merge
yarn add -D webpack-merge
创建dev.config.js文件
const { merge } = require('webpack-merge');
const baseConfig = require('./base.config');
const devConfig = {
mode: 'development',
devServer: {
port: '9090',
contentBase: path.resolve(__dirname, '../dist'),
hot: true,
open: true
}
};
module.exports = merge(baseConfig, devConfig);
修改打包命令
"dev": "webpack serve --config scripts/dev.config.js"
线上环境配置
创建pro.config.js
const { merge } = require('webpack-merge');
const baseConfig = require('./base.config');
const preConfig = {
mode: 'production'
};
module.exports = merge(baseConfig, preConfig);
修改打包命令
"build": "webpack --config scripts/pro.config.js"
使用 typescript
yarn add -D typescript ts-node @types/node @types/webpack @types/webpack-dev-server @types/react @types/react-dom @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-syntax-dynamic-import
- 添加tsconfig.json
tsc --init
- 修改tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
- 修改base.config.js为 ts 文件
import * as path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { Configuration } from 'webpack';
const BaseConfig: Configuration = {
entry: path.resolve(__dirname, '../src/index.tsx'),
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'static/js/[name].[contenthash:8].js'
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
use: 'babel-loader',
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
filename: 'index.html',
inject: 'body'
})
]
};
export default BaseConfig;
- 修改dev.config.js为 ts 文件
import { merge } from 'webpack-merge';
import * as path from 'path';
import BaseConfig from './base.config';
import { Configuration } from 'webpack';
const devConfig: Configuration = {
mode: 'development',
devServer: {
port: 9000,
contentBase: path.resolve(__dirname, '../dist'),
hot: true,
open: true
}
};
module.exports = merge(BaseConfig, devConfig);
- 修改pro.config.js为 ts 文件
import { merge } from 'webpack-merge';
import BaseConfig from './base.config';
import { Configuration } from 'webpack';
const config: Configuration = {
mode: 'production'
};
const preConfig = merge(BaseConfig, config);
export default preConfig;
- 修改src/index.js为 tsx 文件
- 修改打包命令
"dev": "webpack serve --config scripts/dev.config.ts",
"build": "webpack --config scripts/pro.config.ts"
- 修改babel.config.js
module.exports = {
//...
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }], // 支持装饰器模式
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-syntax-dynamic-import' // 动态导入
]
};
css 打包配置
基础配置
使用 style-loader 和 css-loader
- css-loader 使你能够使用类似@import 和 url()的方法引入 css 文件
- style-loader 将所有的计算后的样式加入页面中
- 二者结合能够把样式表嵌入 webpack 打包后的 js 文件中
yarn add -D style-loader css-loader
修改base.config.ts中打包规则
{
test: /\.css$/,
use: ['style-loader', 'css-loader]
}
引入 sass
使用sass,需要引入sass-loader和node-sass
yarn add -D node-sass sass-loader
- 修改base.config.ts中的打包规则
{
test: /\.(sc|c)ss$/,
include: path.resolve(__dirname, '../src'),
use: ['style-loader', 'css-loader', 'sass-loader']
}
- 配置公共sass属性 在 src 目录下新建 style 文件夹,并新建var.sass文件,写入样式变量
$pink: pink;
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
在index.sass中引入变量
@import './style/var.scss';
div {
color: $pink;
width: 200px;
@include ellipsis;
}
优化引入路径层级
{
test: /\.(sc|c)ss$/,
include: path.resolve(__dirname, '../src'),
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: [path.resolve(__dirname, '../src/style')]
}
}
}
]
}
// @import var.scss
引入 postcss
postcss 是一个 js 工具和插件转换 css 代码的工具
autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些属性自动添加 css 规则添加前缀 postcss preset env 将最新的 css 语法转换成大多数浏览器都能理解的语法
yarn add post-loader postcss-preset-env -D
修改打包规则
...
'css-loader',
'postcss-loader',
...
添加postcss.config.js配置
const presetenv = require('postcss-preset-env');
module.exports = {
plugins: [
presetenv({
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3
})
]
};
加载图片字体媒体文件
使用 url-loader 和 file-loader
yarn add -D url-loader file-loader
修改打包配置
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
name: 'static/img/[name].[hash:8].[ext]'
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: {
limit: 10240,
name: 'static/font/[name].[hash:8].[ext]'
}
}
]
},
{
test: /\.mp3(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: { name: 'static/media/[name].[hash:8].[ext]' }
}
]
}
代码校验
使用 eslint prettier husky 和 lint-staged
yarn add -D eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-prettier prettier eslint-config-prettier
配置编辑器设置
- 创建*.editorConfig*文件
# top-most EditorConfig file
# 表示最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件
root = true
# 匹配除路径分隔符(/)之外的任意字符
[*]
# 设置编码
charset = utf-8
# 设置换行符,值为lf(常用)、cr和crlf
end_of_line = lf
# 设置缩进风格(tab是硬缩进,space为软缩进)
indent_style = space
# 用一个整数定义的列数来设置缩进的宽度,如果indent_style为tab,则此属性默认为tab_width
indent_size = 2
# 设置为true以删除换行符前面的任何空白字符
trim_trailing_whitespace = true
# 设为true表示使文件以一个空白行结尾
insert_final_newline = true
- 创建*.eslintrc.js*
module.exports = {
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module'
},
env: {
node: true,
browser: true,
commonjs: true,
es6: true
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react-hooks', 'prettier'],
globals: {
// 这里填入你的项目需要的全局变量
// 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
// React: false,
// ReactDOM: false
},
settings: {
react: {
pragma: 'React',
version: 'detect'
}
},
rules: {
'prettier/prettier': 'error',
// 这里填入你的项目需要的个性化配置,比如:
'no-console': 'off',
'no-unused-vars': [
'warn',
{
vars: 'all',
args: 'none',
caughtErrors: 'none'
}
],
'max-nested-callbacks': 'off',
'react/no-children-prop': 'off',
'typescript/member-ordering': 'off',
'typescript/member-delimiter-style': 'off',
'react/jsx-indent-props': 'off',
'react/no-did-update-set-state': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
indent: [
'off',
2,
{
SwitchCase: 1,
flatTernaryExpressions: true
}
]
}
};
- 创建 .prettierrc.js
module.exports = {
// 一行最多 100 字符
printWidth: 100,
// 使用 2 个空格缩进
tabWidth: 2,
// 使用缩进符
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾不需要逗号
trailingComma: 'none',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf'
};
- 在package.json中添加一下内容
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write",
"eslint --fix"
]
}
打包优化
用到的依赖
- webpack-merge: 合并webpack配置
- html-webpack-plugin: 生成html文件
- add-asset-html-webpack-plugin: 和html-webpack-plugin配合使用,把资源文件引用到生成的html中
- mini-css-extract-plugin: 把css提取到单独的文件中
- css-minimizer-webpack-plugin: 使用cssnano优化和压缩css
- clean-webpack-plugin: 清理打包文件夹
- webpack.DefinePlugin: 编译时创建一些全局变量
- compression-webpack-plugin: gzip压缩
- webpack.DllPlugin: 将模块预先编译,在第一次编译时将配置好的需要预先编译的模块编译在缓存中。第二次编译的时候,解析到这些模块就直接使用缓存
- webpack.DllReferencePlugin: 将预先编译好的模块关联到当前编译中,当webpack解析到这些模块时,会直接使用预先编译好的模块
- webpack-bundle-analyzer: 依赖分析
公共配置
- clean-webpack-plugin
yarn add -D clean-webpack-plugin
修改base.config.ts文件
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
...
new CleanWebpackPlugin()
...
- 公共变量 打开scripts目录,并新建env.ts文件
const envConfig = {
dev: {
BASEURL: 'devurl'
},
pro: {
BASEURL: 'preurl'
}
};
export default envConfig;
修改开发环境和线上环境打包配置
dev.config.ts添加plugins配置
plugins: [new DefinePlugin(envConfig.dev)]
pro.config.ts添加plugins配置
plugins: [new DefinePlugin(envConfig.pro)]
- css打包配置
使用mini-css-extract-plugin将css从js中分离出来,并支持chunk 使用css-minimizer-webpack-plugin优化和压缩css
yarn add -D mini-css-extract-plugin css-minimizer-webpack-plugin @types/mini-css-extract-plugin @types/css-minimizer-webpack-plugin
修改公共配置base.config.ts, 将style-loader改为MiniCssExtractPlugin.loader
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import Cssminimizer from 'css-minimizer-webpack-plugin';
{
module: {
rules: [
{
// style-loader -> MiniCssExtractPlugin.loader
}
]
},
// ...
plugins: [
// ...
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[id].[contenthash:8].css'
})
],
optimization: {
minimize: true,
minimizer: [new Cssminimizer()]
}
}
分包
webpack.splitChunksPlugin 默认情况下,只会影响到按需加载的chunks。 webpack将根据一下条件自动拆分chunks:
- 新的chunk可以被共享,或者模块来自于node_modules文件夹
- 新的chunk体积大于20kb
- 当按需加载chunks时,并行请求的最大数量小于或等于30
- 当加载初始化页面时,并发请求的最大数量小于或等于30
默认配置
export default const config = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 提取的 chunk 类型,all: 所有,async: 异步,initial: 初始
// minSize: 30000, // 默认值,新 chunk 产生的最小限制 整数类型(以字节为单位)
// maxSize: 0, // 默认值,新 chunk 产生的最大限制,0为无限 整数类型(以字节为单位)
// minChunks: 1, // 默认值,新 chunk 被引用的最少次数
// maxAsyncRequests: 5, // 默认值,按需加载的 chunk,最大数量
// maxInitialRequests: 3, // 默认值,初始加载的 chunk,最大数量
// name: true, // 默认值,控制 chunk 的命名
cacheGroups: {
// 配置缓存组
vendor: {
name: 'vendor',
chunks: 'initial',
priority: 10, // 优先级
reuseExistingChunk: false, // 允许复用已经存在的代码块
test: /node_modules/ // 只打包初始时依赖的第三方
},
common: {
name: 'common',
chunks: 'initial',
// test: resolve("src/components"), // 可自定义拓展你的规则
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
}
gzip
yarn add -D compression-webpack-plugin @types/compression-webpack-plugin
修改pro.config.ts
plugins: [
...
new CompressionPlugin()
...
]
DllPluginh和DllRefrencePlugin
将引入的基础包如react,react-dom打到一个包,只要这些包的版本没升级,以后每次打包就不需要再编译这些模块,提高打包速率。
- 再scripts目录下创建一个webpack.dll.ts文件
import * as path from 'path';
import {} from 'webpack';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import webpack from 'webpack';
const DllConfig = {
mode: 'production',
entry: {
vendor: ['react', 'react-dom']
},
output: {
filename: '[name].dll.[hash:8].js',
path: path.resolve(__dirname, '../dll'),
// 链接库输出方式 默认'var'形式赋给变量
libraryTarget: 'var',
// 全局变量名称 导出库将被以var的形式赋给这个全局变量 通过这个变量获取到里面模块
library: '_dll_[name]_[hash:8]'
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../dll')]
}),
new webpack.DllPlugin({
// path 指定manifest文件的输出路径
path: path.resolve(__dirname, '../dll/[name].manifest.json'),
// 和library 一致,输出的manifest.json中的name值
name: '_dll_[name]_[hash:8]'
})
]
};
export default DllConfig;
- 添加命令,并执行
"dll": "webpack --config scripts/dll.config.ts"
- 使用DllReferencePlugin告诉webpack使用哪些动态链接,然后使用add-asset-html-webpack-plugin将DllPlugin生成的文件引入到生成的html中 修改pro.config.ts
import
plugins: [
...
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendor.manifest.json')
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, '../dll/**/*.js')
})
]
依赖分析
如果要分析打包之后依赖的占比,可以使用webpack-bundle-analyzer
- 引入webpack-bundle-analyzer
yarn add -D webpack-bundle-analyzer @types/webpack-bundle-analyzer
- 在scripts目录下,新建analyzer.config.ts
import { merge } from 'webpack-merge';
import { Configuration } from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import proConfig from './pro.config';
const config: Configuration = {
plugins: [new BundleAnalyzerPlugin()]
};
const AnalyzerConfig = merge(proConfig, config);
export default AnalyzerConfig;
- 在package.json中添加命令
"analyzer": "webpack --config scripts/analyzer.config.ts"