在现代前端开发中,建立一个高效的开发环境和部署流程对于团队的工作效率以及产品质量至关重要。本篇博客将深入介绍如何利用Webpack以及其他工具来优化前端开发环境部署流程,并将内容分为以下几个方面。
介绍如何创建一个基本的项目结构
在本项目中,我们将使用pnpm
来进行包管理。pnpm
相比于npm
和yarn
有以下优势:
1. 更快的安装和更新时间
2. 更少的磁盘空间使用
3. 更好地支持 monorepos
4. 更好地支持对等依赖
5. 更清晰的依赖树
初始化 package.json
文件 pnpm init
在根目录新建基本的项目结构:
├── build
| ├── webpack.base.ts # 公共配置
| ├── webpack.dev.ts # 开发环境配置
| └── webpack.prod.ts # 打包环境配置
├── public
│ └── index.html # html模板
├── src
| ├── App.tsx
| ├── App.css
│ └── index.tsx # react应用入口页面
└── package.json
引入React和TypeScript确保代码质量
为了引入 React,需要安装依赖:pnpm i react react-dom
为了确保代码质量,我们将使用 TypeScript。TypeScript有如下优点:
安装 TypeScript 相关依赖
pnpm i @types/react @types/react-dom -D
pnpm i babel-loader ts-node @babel/core @babel/preset-react @babel/presettypescript @babel/preset-env core-js -D
初始化 tsconfig.json :npx tsc --init
分别介绍基础配置、开发环境配置和生产环境配置
着重讨论Webpack配置文件(webpack.base.ts、webpack.dev.ts、webpack.prod.ts)
安装依赖:pnpm i webpack webpack-cli -D
配置 webpack.base.ts 文件:
pnpm i @types/node -D
pnpm i style-loader css-loader html-webpack-plugin -D
module.exports = {
// 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
presets: [
[
"@babel/preset-env",
{
// 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
// "targets": {
// "chrome": 35,
// "ie": 9
// },
targets: { browsers: ["> 1%", "last 2 versions", "not ie <= 8"] },
useBuiltIns: "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入
polyfill按需添加
corejs: 3, // 配置使用core-js使用的版本
loose: true,
},
],
// 如果您使用的是 Babel 和 React 17,您可能需要将 "runtime": "automatic" 添加到配置中。
// 否则可能会出现错误:Uncaught ReferenceError: React is not defined
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
};
// ...
module: {
rules: [
{
test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
use: "babel-loader"
},
// ...
],
},
// ...
我们需要通过 webpack-dev-server 来启动我们的项目,所以需要安装相关的依赖:
pnpm i webpack-dev-server webpack-merge -D
配置开发环境配置: webpack.dev.ts
import path from "path";
import { merge } from "webpack-merge";
import { Configuration as WebpackConfiguration } from "webpack";
import { Configuration as WebpackDevServerConfiguration } from "webpack-devserver";
import baseConfig from "./webpack.base";
interface Configuration extends WebpackConfiguration {
devServer?: WebpackDevServerConfiguration;
}
const host = "127.0.0.1";
const port = "8082";
// 合并公共配置,并添加开发环境配置
const devConfig: Configuration = merge(baseConfig, {
mode: "development", // 开发模式,打包更加快速,省了代码优化步骤
devtool: "eval-cheap-module-source-map",
devServer: {
host,
port,
open: true, // 是否自动打开
compress: false, // gzip压缩,开发环境不开启,提升热更新速度
hot: true, // 开启热更新
historyApiFallback: true, // 解决history路由404问题
setupExitSignals: true, // 允许在 SIGINT 和 SIGTERM 信号时关闭开发服务器和退出进程。
static: {
directory: path.join(__dirname, "../public"), // 托管静态资源public文件夹
},
headers: { "Access-Control-Allow-Origin": "*" }, // HTTP响应头设置,允许任何来源进行跨域请求
},
});
export default devConfig;
然后再 package.json 中添加启动脚本:
"scripts": {
"dev": "webpack serve -c build/webpack.dev.ts"
},
需要在 tsconfig.json 中加入一行 "jsx": "react-jsx"
import { Configuration } from "webpack";
import { merge } from "webpack-merge";
import baseConfig from "./webpack.base";
const prodConfig: Configuration = merge(baseConfig, {
mode: "production", // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
});
export default prodConfig;
在 package.json 中添加:
"scripts": {
// ...
"build": "webpack -c build/webpack.prod.ts"
},
resolve: {
extensions : [".ts", ".tsx", ".js", ".jsx", ".less", ".css"],
// 别名需要配置两个地方,这里和 tsconfig.json
alias: {
"@": path.join(__dirname, "../src")
},
},
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
},
}
在React中使用CSS Modules预处理器(Less、Sass、Stylus)具有多种优点:
基本用法
pnpm i less less-loader sass-loader sass stylus stylus-loader -D
const cssRegex = /\.css$/;
const sassRegex = /\.(scss|sass)$/;
const lessRegex = /\.less$/;
const stylRegex = /\.styl$/;
const styleLoadersArray = [
"style-loader",
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[path][name]__[local]--[hash:5]",
},
},
},
];
const baseConfig: Configuration = {
// ... other configurations
module: {
rules: [
// ... other rules
{
test: cssRegex, // 匹配css文件
use: styleLoadersArray,
},
{
test: lessRegex,
use: [
...styleLoadersArray,
{
loader: 'less-loader',
options: {
lessOptions: {
// 如果要在less中写js的语法,需要加这一配置
javascriptEnabled: true
}
}
}
]
},
{
test: sassRegex,
use: [
...styleLoadersArray,
{
loader: 'sass-loader',
options: {
implementation: require('sass') // 使用dart-sass代替node-sass
}
}
]
},
{
test: stylRegex,
use: [
...styleLoadersArray,
'stylus-loader'
]
}
],
},
// ... other configurations
};
export default baseConfig;
{
output: {
// ... 这里自定义输出文件名的方式是,将某些资源发送到指定目录
assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
// ...
{
test: /\.(png|jpe?g|gif|svg)$/i, // 匹配图片文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64
}
},
generator:{
filename:'static/images/[hash][ext][query]', // 文件输出目录和命名
},
},
]
}
}
// ...
/* IMAGES */
declare module '*.svg' {
const ref: string;
export default ref;
}
declare module '*.bmp' {
const ref: string;
export default ref;
}
declare module '*.gif' {
const ref: string;
export default ref;
}
declare module '*.jpg' {
const ref: string;
export default ref;
}
declare module '*.jpeg' {
const ref: string;
export default ref;
}
declare module '*.png' {
const ref: string;
export default ref;
}
import React, { PureComponent } from "react";
// 装饰器为,组件添加age属性
function addAge(Target: Function) {
Target.prototype.age = 111
}
// 使用装饰器
@addAge
class Cls extends PureComponent {
age?: number
render() {
return (
<h2>我是类组件---{this.age}</h2>
)
}
}
export default Cls
// tsconfig.json
{
"compilerOptions": {
// ...
// 开启装饰器使用
"experimentalDecorators": true
}
}
pnpm i @babel/plugin-proposal-decorators -D
const isDEV = process.env.NODE_ENV === "development"; // 是否是开发模式
module.exports = {
// 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
presets: [
[
"@babel/preset-env",
{
// 设置兼容目标浏览器版本,也可以在根目录配置.browserslistrc文件,babel-loader会自动寻找上面配置好的文件.browserlistrc
// "targets": {
// "chrome": 35,
// "ie": 9
// },
targets: { browsers: ["> 1%", "last 2 versions", "not ie <= 8"] },
useBuiltIns: "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
corejs: 3, // 配置使用core-js使用的版本
loose: true,
},
],
// 如果您使用的是 Babel 和 React 17,您可能需要将 "runtime": "automatic" 添加到配置中。
// 否则可能会出现错误:Uncaught ReferenceError: React is not defined
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
].filter(Boolean), // 过滤空值
};
pnpm i webpackbar -D
// ...
import WebpackBar from 'webpackbar';
// ...
const baseConfig: Configuration = {
// ...
// plugins 的配置
plugins: [
// ...
new WebpackBar({
color: "#85d", // 默认green,进度条颜色支持HEX
basic: false, // 默认true,启用一个简单的日志报告器
profile:false, // 默认false,启用探查器。
})
],
};
export default baseConfig;
pnpm i speed-measure-webpack-plugin -D
import { Configuration } from 'webpack'
import prodConfig from './webpack.prod' // 引入打包配置
import { merge } from 'webpack-merge' // 引入合并webpack配置方法
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); // 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件
// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
const analyConfig: Configuration = smp.wrap(merge(prodConfig, {
}))
export default analyConfig
{
// ...
"scripts": {
// ...
"build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -cbuild/webpack.analy.ts"
}
// ...
}
// webpack.base.ts
// ...
module.exports = {
// ...
cache: {
type: 'filesystem', // 使用文件缓存
},
}
运行在 Node.js 之上的 webpack 是单线程模式的,也就是说, webpack 打包只能逐个文件处理,当webpack 需要打包大量文件时,打包时间就会比较漫长。
多进程/多实例构建的方案比较知名的有以下三种:
安装依赖:pnpm i thread-loader -D
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的worker 池中运行。修改 webpack.base.ts:
module: {
rules: [
{
test: /\.(ts|tsx)$/, // 匹配ts和tsx文件
use: [
// 开启多进程打包。
// 进程启动大概为600ms,进程通信也有开销。
// 只有工作消耗时间比较长,才需要多进程打包
{
loader: 'thread-loader',
options: {
wokers: 4 // 进程数
}
},
'babel-loader']
},
]
}
module: {
rules: [
{
test: /\.(ts|tsx)$/, // 匹配ts和tsx文件
exclude : /node_modules/,
use: [
// 开启多进程打包。
// 进程启动大概为600ms,进程通信也有开销。
// 只有工作消耗时间比较长,才需要多进程打包
{
loader: 'thread-loader',
options: {
wokers: 4 // 进程数
}
},
'babel-loader']
},
]
}
^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
// webpack.dev.ts
module.exports = {
// ...
devtool: 'eval-cheap-module-source-map'
}
pnpm i webpack-bundle-analyzer -D
import { Configuration } from "webpack";
import { merge } from "webpack-merge";
import prodConfig from "./webpack.prod";
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
// 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin();
// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
const analyConfig: Configuration = smp.wrap(merge(prodConfig, {
plugins: [
new BundleAnalyzerPlugin() // 配置分析打包结果插件
]
}))
export default analyConfig;
pnpm i mini-css-extract-plugin -D
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const prodConfig: Configuration = merge(baseConfig, {
// ...
plugins: [
// ...
new MiniCssExtractPlugin({
filename: 'static/css/[name].css' // 抽离css的输出目录和名称
}),
],
});
export default prodConfig;
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
const styleLoadersArray = [
isDev ? "style-loader" : MiniCssExtractPlugin.loader, // 开发环境使用stylelooader,打包模式抽离css
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[path][name]__[local]--[hash:5]",
},
},
},
'postcss-loader'
];
pnpm i css-minimizer-webpack-plugin -D
// ...
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
module.exports = {
// ...
optimization: {
minimizer: [
new CssMinimizerPlugin(), // 压缩css
],
},
}
pnpm i terser-webpack-plugin compression-webpack-plugin -D
import path from "path";
import { Configuration } from "webpack";
import { merge } from "webpack-merge";
import CopyPlugin from "copy-webpack-plugin";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
import TerserPlugin from "terser-webpack-plugin";
import CompressionPlugin from "compression-webpack-plugin";
import baseConfig from "./webpack.base";
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const prodConfig: Configuration = merge(baseConfig, {
mode: "production", // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
/**
* 打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none')
* ● none话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。
* ● 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在。
*/
plugins: [
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"), // 复制public下文件
to: path.resolve(__dirname, "../dist"), // 复制到dist目录中
filter: (source) => !source.includes("index.html"), // 忽略index.html
},
],
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].css", // 抽离css的输出目录和名称
}),
// 打包时生成gzip文件
new CompressionPlugin({
test: /\.(js|css)$/, // 只生成css,js压缩文件
filename: "[path][base].gz", // 文件命名
algorithm: "gzip", // 压缩格式,默认是gzip
threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
minRatio: 0.8, // 压缩率,默认值是 0.8
}),
],
optimization: {
// splitChunks: {
// chunks: "all",
// },
runtimeChunk: { name: "mainifels" },
minimize: true,
minimizer: [
new CssMinimizerPlugin(), // 压缩css
new TerserPlugin({
parallel: true, // 开启多线程压缩
terserOptions: {
compress: {
pure_funcs: ["console.log"], // 删除console.log
},
},
}),
],
},
performance: {
// 配置与性能相关的选项的对象
hints: false, // 设置为false将关闭性能提示。默认情况下,Webpack会显示有关入口点和资产大小的警告和错误消息。将hints设置为false可以禁用这些消息。
maxAssetSize: 4000000, // 设置一个整数,表示以字节为单位的单个资源文件的最大允许大小。如果任何资源的大小超过这个限制,Webpack将发出性能警告。在你提供的配置中,这个值被设置为4000000字节(约4MB)。
maxEntrypointSize: 5000000, // 设置一个整数,表示以字节为单位的入口点文件的最大允许大小。入口点是Webpack构建产生的主要JS文件,通常是应用程序的主要代码。如果入口点的大小超过这个限制,Webpack将发出性能警告。在你提供的配置中,这个值被设置为5000000字节(约5MB)。
},
});
export default prodConfig;
// webpack.base.ts
// ...
const baseConfig: Configuration = {
// 打包文件出口
output: {
filename: 'static/js/[name].[chunkhash:8].js', // 加上[chunkhash:8]
// ...
},
module: {
rules: [
// ...
{
test: mediaRegex, // 匹配媒体文件
// ...
generator: {
filename: 'static/media/[name].[contenthash:8][ext]' // 文件输出目录和命名
}
},
{test: fontRegex, // 匹配字体图标文件
// ...
generator: {
filename: 'static/json/[name].[contenthash:8][ext]' // 文件输出目录和命名
}
},
{
test: imageRegex, // 匹配图片文件
// ...
generator:{
filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
},
},
{
// 匹配json文件
test: jsonRegex,
type: 'json', // 模块资源类型为json模块
generator: {
filename: 'static/json/[name].[hash][ext][query]', // 专门针对json文件的处理
}
}
]
// ...
}
// webpack.prod.ts
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8]
}),
// ...
],
// ...
})
module.exports = {
// ...
optimization: {
// ...
splitChunks: { // 分隔代码
cacheGroups: {
vendors: { // 提取node_modules代码
test: /node_modules/, // 只匹配node_modules里面的模块
name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
minChunks: 1, // 只要使用一次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
priority: 1, // 提取优先级为1
},
commons: { // 提取页面公共代码
name: 'commons', // 提取文件命名为commons
minChunks: 2, // 只要使用两次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
}
}
}
}
}
资源懒加载
const LazyComponent = React.lazy(() => import('./LazyComponent'));
资源预加载
import(/* webpackPrefetch: true */ './SomeComponent');
import(/* webpackPreload: true */ './AnotherComponent');
const CompressionPlugin = require('compression-webpack-plugin');
const webpackConfig = {
// ... other configurations
plugins: [
new CompressionPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),
],
// ... other configurations
};
{ "recommendations": ["editorconfig.editorconfig"] }
# https://editorconfig.org
root = true # 设置为true表示根目录,控制配置文件 .editorconfig 是否生效的字段 [*] # 匹配全部文件,匹配除了 `/` 路径分隔符之外的任意字符串
charset = utf-8 # 设置字符编码,取值为 latin1,utf-8,utf-8-bom,
utf-16be 和 utf-16le,当然 utf-8-bom 不推荐使用
end_of_line = lf # 设置使用的换行符,取值为 lf,cr 或者 crlf
indent_size = 2 # 设置缩进的大小,即缩进的列数,当 indexstyle 取值 tab时,indentsize 会使用 tab_width 的值
indent_style = space # 缩进风格,可选space|tab
insert_final_newline = true # 设为true表示使文件以一个空白行结尾
trim_trailing_whitespace = true # 删除一行中的前后空格
[*.md] # 匹配全部 .md 文件
trim_trailing_whitespace = false
pnpm i prettier -D
// .prettierrc.js
module.exports = {
tabWidth: 2, // 一个tab代表几个空格数,默认就是2
useTabs: false, // 是否启用tab取代空格符缩进,.editorconfig设置空格缩进,所以设置为false
printWidth: 100, // 一行的字符数,如果超过会进行换行
semi: false, // 行尾是否使用分号,默认为true
singleQuote: true, // 字符串是否使用单引号
trailingComma: 'all', // 对象或数组末尾是否添加逗号 none| es5| all
jsxSingleQuote: true, // 在jsx里是否使用单引号,你看着办
bracketSpacing: true, // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
arrowParens: 'avoid' // 箭头函数如果只有一个参数则省略括号
}
// .prettierignore
node_modules
dist
env
.gitignore
pnpm-lock.yaml
README.md
src/assets/*
.vscode
public
.github
.husky
{
"search.exclude" : {
"/node_modules": true,
"dist": true,
"pnpm-lock.yaml": true
},
"files.autoSave": "onFocusChange",
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"javascript.validate.enable": false,
}
pnpm i stylelint stylelint-config-css-modules stylelint-config-prettier stylelint-config-standard stylelint-order -D
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-prettier'
// "stylelint-config-css-modules",
// "stylelint-config-recess-order" // 配置stylelint css属性书写顺序插件,
],
plugins: [
"stylelint-scss",
"stylelint-order",
"stylelint-declaration-strict-value",
"stylelint-no-unsupported-browser-features"
],
rules: {
"string-quotes": "single", // 指定字符串引号为单引号
"at-rule-name-case": "lower", // @规则名称使用小写
"number-leading-zero": "always", // 小数前面加零
"block-opening-brace-space-before": "always", // 块级元素大括号之前需要空格
"indentation": 2, // 使用两个空格作为缩进
"linebreaks": "unix", // 使用Unix风格的换行符
"at-rule-name-newline-after": "always", // @规则后需要换行
"at-rule-name-space-after": "always", // @规则后需要空格
/**
* indentation: null, // 指定缩进空格
"no-descending-specificity": null, // 禁止在具有较高优先级的选择器后出现被其覆盖的
较低优先级的选择器
"function-url-quotes": "always", // 要求或禁止 URL 的引号 "always(必须加上引
号)"|"never(没有引号)"
"string-quotes": "double", // 指定字符串使用单引号或双引号
"unit-case": null, // 指定单位的大小写 "lower(全小写)"|"upper(全大写)"
"color-hex-case": "lower", // 指定 16 进制颜色的大小写 "lower(全小写)"|"upper(全
大写)"
"color-hex-length": "long", // 指定 16 进制颜色的简写或扩写 "short(16进制简
写)"|"long(16进制扩写)"
"rule-empty-line-before": "never", // 要求或禁止在规则之前的空行 "always(规则之前
必须始终有一个空行)"|"never(规则前绝不能有空行)"|"always-multi-line(多行规则之前必须始终有
一个空行)"|"never-multi-line(多行规则之前绝不能有空行。)"
"font-family-no-missing-generic-family-keyword": null, // 禁止在字体族名称列表
中缺少通用字体族关键字
"block-opening-brace-space-before": "always", // 要求在块的开大括号之前必须有一
个空格或不能有空白符 "always(大括号前必须始终有一个空格)"|"never(左大括号之前绝不能有空
格)"|"always-single-line(在单行块中的左大括号之前必须始终有一个空格)"|"never-single-
line(在单行块中的左大括号之前绝不能有空格)"|"always-multi-line(在多行块中,左大括号之前必须
始终有一个空格)"|"never-multi-line(多行块中的左大括号之前绝不能有空格)"
"property-no-unknown": null, // 禁止未知的属性(true 为不允许)
"no-empty-source": null, // 禁止空源码
"declaration-block-trailing-semicolon": null, // 要求或不允许在声明块中使用尾随
分号 string:"always(必须始终有一个尾随分号)"|"never(不得有尾随分号)"
"selector-class-pattern": null, // 强制选择器类名的格式
"value-no-vendor-prefix": null, // 关闭 vendor-prefix(为了解决多行省略 -
webkit-box)
"at-rule-no-unknown": null,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["global", "v-deep", "deep"]
}
]
}
*/
'selector-class-pattern': [
// 命名规范 -
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
{
message: 'Expected class selector to be kebab-case'
}
],
'string-quotes': 'double', // 单引号
'at-rule-empty-line-before': null,
'at-rule-no-unknown': null,
'at-rule-name-case': 'lower', // 指定@规则名的大小写
'length-zero-no-unit': true, // 禁止零长度的单位(可自动修复)
'shorthand-property-no-redundant-values': true, // 简写属性
'number-leading-zero': 'always', // 小数不带0
'declaration-block-no-duplicate-properties': true, // 禁止声明快重复属性
'no-descending-specificity': true, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器。 'selector-max-id': null, // 限制一个选择器中 ID 选择器的数量
'max-nesting-depth': 10,
'declaration-block-single-line-max-declarations': 1,
'block-opening-brace-space-before': 'always',
// 'selector-max-type': [0, { ignore: ['child', 'descendant', 'compounded']}],
indentation: [
2,
{
// 指定缩进 warning 提醒
severity: 'warning'
}
],
'order/order': ['custom-properties', 'dollar-variables', 'declarations',
'rules', 'at-rules'
],
'order/properties-order': [
// 规则顺序
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'float',
'width',
'height',
'max-width',
'max-height',
'min-width',
'min-height',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-collapse',
'margin-top-collapse',
'margin-right-collapse',
'margin-bottom-collapse',
'margin-left-collapse',
'overflow',
'overflow-x',
'overflow-y',
'clip',
'clear',
'font',
'font-family',
'font-size',
'font-smoothing',
'osx-font-smoothing',
'font-style',
'font-weight',
'line-height',
'letter-spacing',
'word-spacing',
'color',
'text-align',
'text-decoration',
'text-indent',
'text-overflow',
'text-rendering',
'text-size-adjust',
'text-shadow',
'text-transform',
'word-break',
'word-wrap',
'white-space',
'vertical-align',
'list-style',
'list-style-type',
'list-style-position',
'list-style-image',
'pointer-events',
'cursor',
'background',
'background-color',
'border',
'border-radius',
'content',
'outline',
'outline-offset',
'opacity',
'filter',
'visibility',
'size',
'transform'
]
}
}
*.js
*.tsx
*.ts
*.json
*.png
*.eot
*.ttf
.woff
src/styles/antd-overrides.less
node_modules
dist
env
.gitignore
pnpm-lock.yaml
README.md
src/assets/
.vscode
public
.github
.husky
{
// ...
"editor.codeActionsOnSave": {
"source.fixAll.stylelint" : true
},
"stylelint.validate": [
"css",
"less",
"sass",
"stylus",
"postcss"
]
}
*.tsx
*.ts
*.json
*.png
*.eot
*.ttf
*.woff
src/styles/antd-overrides.less
node_modules
dist
env
pnpm i eslint eslint-config-airbnb eslint-config-standard eslint-friendlyformatter eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-node eslintplugin-promise eslint-plugin-react-hooks eslint-plugin-react @typescripteslint/eslint-plugin @typescript-eslint/parser eslint-plugin-prettier eslintconfig-prettier -D
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'airbnb-base',
'eslint:recommended',
'plugin:import/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint'],
rules: {
// 最大警告数设置为 5
// 'max-warnings': ['error', 5],
// eslint (http://eslint.cn/docs/rules)
'react/jsx-filename-extension': [
'error',
{
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
],
'class-methods-use-this': 'off',
'no-param-reassign': 'off',
'no-unused-expressions': 'off',
'no-plusplus': 0,
'no-restricted-syntax': 0,
'consistent-return': 0,
'@typescript-eslint/ban-types': 'off',
// "import/no-extraneous-dependencies": "off",
'@typescript-eslint/no-non-null-assertion': 'off',
'import/no-unresolved': 'off',
'import/prefer-default-export': 'off', // 关闭默认使用 export default 方式导出
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
},
],
'@typescript-eslint/no-use-before-define': 0,
'no-use-before-define': 0,
'@typescript-eslint/no-var-requires': 0,
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
'no-shadow': 'off',
// "@typescript-eslint/no-var-requires": "off"
'import/extensions': [
'error',
'ignorePackages',
{
'': 'never',
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
// "no-var": "error", // 要求使用 let 或 const 而不是 var
// "no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行
// "no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们
// "prefer-const": "off", // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
// "no-irregular-whitespace": "off", // 禁止不规则的空白
// // typeScript (https://typescript-eslint.io/rules)
// "@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量
// "@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长
// "@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。
// "@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型
// "@typescript-eslint/ban-ts-ignore": "off", // 禁止使用 @ts-ignore
// "@typescript-eslint/ban-types": "off", // 禁止使用特定类型
// "@typescript-eslint/explicit-function-return-type": "off", // 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明
// "@typescript-eslint/no-var-requires": "off", // 不允许在 import 语句中使用 require 语句
// "@typescript-eslint/no-empty-function": "off", // 禁止空函数
// "@typescript-eslint/no-use-before-define": "off", // 禁止在变量定义之前使用它们
// "@typescript-eslint/ban-ts-comment": "off", // 禁止 @ts- 使用注释或要求在指令后进行描述
// "@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!)
// "@typescript-eslint/explicit-module-boundary-types": "off", // 要求导出函数和类的公共类方法的显式返回和参数类型
// // react (https://github.com/jsx-eslint/eslint-plugin-react)
// "react-hooks/rules-of-hooks": "error",
// "react-hooks/exhaustive-deps": "off"
},
settings: {
react: {
version: 'detect',
},
'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
node: {
paths: ['src'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
moduleDirectory: ['node_modules', 'src/'],
},
},
},
}
node_modules
dist
env
.gitignore
pnpm-lock.yaml
README.md
src/assets/*
.vscode
public
.github
.husky
"lint:eslint": "eslint --fix --ext .js,.ts,.tsx ./src",
pnpm i eslint-config-prettier eslint-plugin-prettier -D
{
// ...
"eslint.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
},
}
- .eslintrc.js 的 extends 中加入
module.exports = {
// ...
extends: [
// ...
'plugin:prettier/recommended', // <==== 增加一行
],
// ...
}
安装 Husky:
pnpm i husky -D
npx husky install
配置 Husky 的 pre-commit 钩子:
git init
)npx husky add .husky/pre-commit "pnpm run pre-check"
pnpm run pre-check
命令。这里的 pre-check
是自定义的用于代码检查的脚本。测试提交代码:
提交代码进行测试,你会发现 pre-commit 钩子执行了 pnpm run pre-check
,使用 ESLint 检测了 git 暂存区的文件,并且如果发现问题,则会阻止代码提交到本地仓库。
安装 Commitlint:
首先安装 Commitlint:
pnpm i @commitlint/cli -D
使用 Commitlint :
安装完成后,配置 commitlint.config.js:
module.exports = {
rules: {
'header-min-length': [2, 'always', 10],
}
};
然后执行 commitlint:
echo 'foo' | npx commitlint
当 message 信息为 “foo” 时,由于长度只有 3,Commitlint 会视为违规并输出错误提示。
配置规则包:
为了节省配置规则的时间,可以使用预先配置的规则包来设定多项规则。安装并在配置中指定使用规则包,如 @commitlint/config-conventional。
使用 Husky 为 Commitlint 注册 Git Hooks:
使用 husky add 将指令加入 Git hooks,并且重新注册 Git hooks,这样当执行 git commit 时,Commitlint 将会检查 commit message。
Commitizen:
为了避免写出不符合规范的 commit message 而提交失败,可以使用 Commitizen 来启动设定的 adapter,通过问答的方式编写符合规范的 commit message。
cz-git:
指定提交文字规范,一款工程性更强、高度自定义、标准输出格式的 commitizen 适配器。
配置 package.json :
在 package.json 中配置 commitizen 的 path,指定使用的 adapter路径。
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
配置 commitlint.config.js 文件:
// @see: https://cz-git.qbenben.com/zh/guide
/** @type {import('cz-git').UserConfig} */
module.exports = {
ignores: [commit => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
// @see: https://commitlint.js.org/#/reference-rules
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release',
],
],
},
prompt: {
messages: {
type: "Select the type of change that you're committing:",
scope: 'Denote the SCOPE of this change (optional):',
customScope: 'Denote the SCOPE of this change:',
subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n',
body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
footerPrefixsSelect: 'Select the ISSUES type of changeList by this change (optional):',
customFooterPrefixs: 'Input ISSUES prefix:',
footer: 'List any ISSUES by this change. E.g.: #31, #34:\n',
confirmCommit: 'Are you sure you want to proceed with the commit above?',
// 中文版
// type: '选择你要提交的类型 :',
// scope: '选择一个提交范围(可选):',
// customScope: '请输入自定义的提交范围 :',
// subject: '填写简短精炼的变更描述 :\n',
// body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: '选择关联issue前缀(可选):',
// customFooterPrefixs: '输入自定义issue前缀 :',
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
// confirmCommit: '是否提交或修改commit ?',
},
types: [
{
value: 'feat',
name: 'feat: A new feature',
emoji: '',
},
{
value: 'fix',
name: 'fix: A bug fix',
emoji: '',
},
{
value: 'docs',
name: 'docs: Documentation only changes',
emoji: '',
},
{
value: 'style',
name: 'style: Changes that do not affect the meaning of the code',
emoji: '',
},
{
value: 'refactor',
name: 'refactor: ♻ A code change that neither fixes a bug nor adds a feature',
emoji: '♻ ',
},
{
value: 'perf',
name: 'perf: ⚡ A code change that improves performance',
emoji: '⚡ ',
},
{
value: 'test',
name: 'test: ✅ Adding missing tests or correcting existing tests',
emoji: '✅',
},
{
value: 'build',
name: 'build: Changes that affect the build system or external dependencies',
emoji: ' ',
},
{
value: 'ci',
name: 'ci: Changes to our CI configuration files and scripts',
emoji: '',
},
{
value: 'chore',
name: "chore: Other changes that don't modify src or test files",
emoji: '',
},
{
value: 'revert',
name: 'revert: ⏪ Reverts a previous commit',
emoji: '⏪ ',
},
// 中文版
// { value: '特性', name: '特性: 新增功能', emoji: '' },
// { value: '修复', name: '修复: 修复缺陷', emoji: '' },
// { value: '文档', name: '文档: 文档变更', emoji: '' },
// {
// value: '格式',
// name: '格式: 代码格式(不影响功能,例如空格、分号等格式修正)',
// emoji: '',
// },
// { value: '重构', name: '重构: ♻ 代码重构(不包括 bug 修复、功能新增)', emoji: '♻ ' },
// { value: '性能', name: '性能: ⚡ 性能优化', emoji: '⚡ ' },
// { value: '测试', name: '测试: ✅ 添加疏漏测试或已有测试改动', emoji: '✅' },
// {
// value: '构建',
// name: '构建: 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)',
// emoji: ' ',
// },
// { value: '集成', name: '集成: 修改 CI 配置、脚本', emoji: '' },
// { value: '回退', name: '回退: ⏪ 回滚 commit', emoji: '⏪ ' },
// {
// value: '其他',
// name: '其他: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)',
// emoji: '',
// },
],
useEmoji: true,
themeColorCode: '',
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: 'bottom',
customScopesAlias: 'custom',
emptyScopesAlias: 'empty',
upperCaseSubject: false,
allowBreakingChanges: ['feat', 'fix'],
breaklineNumber: 100,
breaklineChar: '|',
skipQuestions: [],
issuePrefixs: [
{
value: 'closed',
name: 'closed: ISSUES has been processed',
},
],
customIssuePrefixsAlign: 'top',
emptyIssuePrefixsAlias: 'skip',
customIssuePrefixsAlias: 'custom',
allowCustomIssuePrefixs: true,
allowEmptyIssuePrefixs: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: '',
defaultIssues: '',
defaultScope: '',
defaultSubject: '',
},
}
"scripts": {
// ...
"commit": "git pull && git add -A && git-cz && git push",
}
通过学习Webpack配置,我掌握了创建基本项目结构并成功引入React和TypeScript的方法。在Webpack配置方面,我深入了解了webpack.base.ts、webpack.dev.ts和webpack.prod.ts这些配置文件的编写方式,以及如何处理静态资源、配置环境变量和设置文件别名。此外,我还学会了引入Less、Sass(Scss)和Stylus,并处理CSS3前缀兼容以及其他常用资源的方法。在JavaScript方面,我了解了Babel如何处理非标准语法,以及如何优化Webpack的构建速度和产物。另外,在代码规范工具方面,我学习并掌握了使用EditorConfig、Prettier、stylelint和eslint来统一代码格式、规范提交行为,并检测代码质量。通过Husky和lint-staged,我成功优化了eslint检测速度,同时充分利用Commitlint和Commitizen规范提交信息,提高团队协作效率。这些收获使我更加熟练地应用Webpack进行项目构建和优化,同时能够确保项目代码的一致性和高质量,为团队协作和项目持续健康发展提供了重要支持。