前端环境搭建

在当今的前端开发领域,构建现代化的前端开发环境对于项目的成功至关重要。本篇博客将介绍如何结合 React 18、TypeScript 和 Webpack 5 来打造高效、现代化的前端开发环境

一、项目创建

 本篇博客使用pnpm包管理器

运行 pnpm -v 命令查看本机pnpm版本

运行 pnpm i 命令,它会在根目录下生成一个package.json文件

二、基本项目结构

在根目录下新建基本的项目结构

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>webpack5-react-ts</title>
</head>
<body>
    <!-- 容器节点 -->
    <div id="root"></div>
</body>
</html>

三、引入react

首先安装依赖

pnpm i react react-dom

声明依赖

pnpm i @types/react @types/react-dom -D

然后将入口文件写好(src/index.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = document.querySelector('#root')

if(root) {
 createRoot(root).render(<App />)
}

App.css中写入内容

h2 {
    color: red;
}

最后在App.tsx中写入内容

import React from 'react'
import './App.css'
function App() {
    return <h2>Hello East_White</h2>
}
export default App

四、引入typescript

运行以下命令安装typescript依赖

pnpm i typescript -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

运行了npx tsc --init后,会在根目录下自动生成一个tsconfig.json文件

五、webpack配置

1、安装webpack依赖

pnpm i webpack webpack-cli -D

2、配置webpack.base.ts文件

import { Configuration } from 'webpack';
const path = require("path");
const baseConfig: Configuration = {
entry: path.join(__dirname, "../src/index.tsx"), // 入口文件
// 打包出口文件
output: {
    filename: "static/js/[name].js", // 每个输出js的名称
    path: path.join(__dirname, "../dist"), // 打包结果输出路径
    clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
    publicPath: "/", // 打包后文件的公共前缀路径
},
// loader 配置
module: {
    rules: [],
},
resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js"],
},
// plugins 的配置
plugins: []
};
以上代码会出现报错,原因是由于 typescript 自身的机制,需要一份 xx.d.ts 声明文件,来说明模块对外公开的方法和属性的类型以及内容。对于内建模块,安装一个 @types/node 模块可以整体解决模块的声明文件问 题。

那咱们就安装一下@types/node依赖

pnpm i @types/node -D
另外因为我们在 App.tsx 中引入了 css 文件,所以还需要安装相关的 loader
pnpm i style-loader css-loader html-webpack-plugin -D

然后完善 webpack.base.ts

import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const path = require("path");
const baseConfig: Configuration = {
  entry: path.join(__dirname, "../src/index.tsx"), // 入口文件
  // 打包出口文件
  output: {
    filename: "static/js/[name].js", // 每个输出js的名称
    path: path.join(__dirname, "../dist"), // 打包结果输出路径
    clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
    publicPath: "/", // 打包后文件的公共前缀路径
  },
  // loader 配置
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
        use: {
          loader: "babel-loader",
          options: {
            // 预设执行顺序由右往左,所以先处理ts,再处理tsx
            presets: [
              [
                "@babel/preset-env",
                {
                  // 设置兼容目标浏览器版本,也可以在根目录配置.browserslistrc文件,babel-loader会自动寻找上面配置好的文件.browserslistrc
                  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",
            ],
          },
        },
      },
      {
        test: /.css$/, //匹配 css 文件
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js"],
  },
}
因为 webpack.base.ts 文件承载了基本的配置,随着 webpack 做的事情越来越多,会逐渐变得很庞大,我们可以将其中的 babel-loader 相关的配置抽离出来进行管理。在根目录新建babel.config.js
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",
  ],
};
然后在 webpack.base.ts 文件中,就可以将 babel-loader 配置简化成以下代码
module: {
    rules: [
        {
            test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
            use: "babel-loader"
        },
    ],
},

3、webpack.dev.ts

接下来,我们需要通过 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" 即可

{
},
"include": ["./src"
"compilerOptions": {
"target": "es2016",
"esModuleInterop": true,
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx" // 这里改成react-jsx,就不需要在tsx文件中手动引入React了
]
}
运行 pnpm run dev 脚本启动项目,就可以看到页面跑出来了!

4、webpack.prod.ts

配置 webpack.prod.ts
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"
},
运行 pnpm run build ,如果想看打包结果,可以通过一个小工具来查看
# 如果之前使用npm,最简单的方法就是使用如下命令
npm i serve -g
# 如果是首次使用pnpm安装全局依赖,通过如下命令
pnpm setup
source ~/.zshrc
pnpm i serve -g
然后通过 serve -S dist 命令,启动一个服务来查看打包结果,如果不出意外,打开控制台启动的服
务,就能看到页面了

5、copy静态资源

一般 public 文件夹都会放一些静态资源 , 可以直接根据绝对路径引入,比如图片、 css js 文件等,不 需要 webpack 进行解析,只需要打包的时候把 public 下内容复制到构建出口文件夹中,可以借助 copy-webpack-plugin 插件,安装依赖
pnpm i copy-webpack-plugin -D

然后修改 webpack.base.ts

const baseConfig: Configuration = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: "webpack5-react-ts",
      filename: "index.html",
      // 复制 'index.html' 文件,并自动引入打包输出的所有资源(js/css)
      template: path.join(__dirname, "../public/index.html"),
      inject: true, // 自动注入静态资源
      hash: true,
      cache: false,
      // 压缩html资源
      minify: {
        removeAttributeQuotes: true,
        collapseWhitespace: true, //去空格
        removeComments: true, // 去注释
        minifyJS: true, // 在脚本元素和事件属性中缩小JavaScript(使用UglifyJS)
        minifyCSS: true, // 缩小CSS样式元素和样式属性
      }
    })
  ],
};
export default baseConfig;
开发环境已经在 devServer 中配置了 static 托管了 public 文件夹,在开发环境使用绝对路径可以访问到 public 下的文件,但打包构建时不做处理会访问不到,所以现在需要在打包配置文件
webpack.prod.ts 中新增 copy 插件配置
import path from "path";
import { Configuration } from "webpack";
import { merge } from "webpack-merge";
import CopyPlugin from "copy-webpack-plugin";
import baseConfig from "./webpack.base";
const prodConfig: Configuration = merge(baseConfig, {
  mode: "production", // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"), // 复制public下文件
          to: path.resolve(__dirname, "../dist"), // 复制到dist目录中
          filter: (source) => !source.includes("index.html"), // 忽略
          index.html
        },
      ],
    }),
  ],
});
export default prodConfig;
测试一下,在 public 中新增一个 favicon.ico 图标文件,在 index.html 中引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 绝对路径引入图标文件 -->
<link data-n-head="ssr" rel="icon" type="image/x-icon"
href="/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack5-react-ts</title>
</head>
<body>
<!-- 容器节点 -->
<div id="root"></div>
</body>
</html>
再执行 pnpm run build 打包,就可以看到 public 下的 favicon.ico 图标文件被复制到 dist 文件中

六、配置环境变量

1、corss-env + DefinePlugin

安装 cross-env

pnpm i cross-env -D
修改 package.json scripts
"scripts": {
"dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack
serve -c build/webpack.dev.ts",
"dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack serve
-c build/webpack.dev.ts",
"dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack serve -c
build/webpack.dev.ts",
"dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack
serve -c build/webpack.dev.ts",
"build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack
-c build/webpack.prod.ts",
"build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack -c
build/webpack.prod.ts",
"build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack -c
build/webpack.prod.ts",
"build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack
-c build/webpack.prod.ts"
},
webpack.base.ts 中打印一下设置的环境变量
console.log('NODE_ENV', process.env.NODE_ENV)
console.log('BASE_ENV', process.env.BASE_ENV)
执行 pnpm run build:dev ,就可以在控制台打印出
// NODE_ENV production
// BASE_ENV development
当前是打包模式,业务环境是开发环境,这里需要把 process.env.BASE_ENV 注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和其他数据,要借助 webpack.DefinePlugin 插件。
修改 webpack.base.ts
import { DefinePlugin } from 'webpack'
module.export = {
  // ...
  plugins: [
    // ...
    new DefinePlugin({
      'process.env': JSON.stringify(process.env)
    })
  ]
}
在根目录下新建 typings/global.d.ts 文件
declare module 'process' {
  global {
    namespace NodeJS {
      export interface ProcessEnv {
        BASE_ENV: 'development' | 'test' | 'pre' | 'production'
        NODE_ENV: 'development' | 'production'
      }
    }
  }
}
并在 tsconfig.json 中配置
{
"compilerOptions": {
"target": "es2016", // 编译输出的JavaScript版本为ES2016
"esModuleInterop": true, // 允许更好的兼容性与ECMAScript模块导入
"module": "commonjs", // 指定生成哪个模块系统代码,这里是CommonJS
"forceConsistentCasingInFileNames": true, // 确保文件名大小写一致,有助于跨平台开发
"strict": true, // 启用所有严格的类型检查选项
"skipLibCheck": true, // 跳过声明文件的类型检查,可以提高编译速度
"typeRoots": ["./typings/*.d.ts", "node_modules/@types"], // 指定类型声明文件的路径
"jsx": "react-jsx" // react18这里改成react-jsx,就不需要在tsx文件中手动引入React了
},
"include": ["./src", "./typings/*.d.ts"] // 指定哪些文件或目录应该被包含在编译范围内
}
配置后会把值注入到业务代码里面去, webpack 解析代码匹配到 process.env.BASE_ENV ,就会设置到对应的值。测试一下,在 src/index.tsx 打印一下两个环境变量
console.log('NODE_ENV', process.env.NODE_ENV)
console.log('BASE_ENV', process.env.BASE_ENV)
执行 pnpm run dev:test ,可以在浏览器控制台看到打印的信息
// NODE_ENV development
// BASE_ENV test

2、配置多环境运行配置

安装依赖

pnpm i dotenv
在根目录下新建一个多文件配置文件夹 env
├── env
├── .env.development # 开发环境
├── .env.test # 测试环境
├── .env.pre # 预发布环境
└── .env.production # 生产环境
文件中可以配置任意我们需要的变量
// env/.env.development
REACT_APP_API_URL=https://api-dev.com
// env/.env.test
REACT_APP_API_URL=https://api-test.com
// env/.env.pre
REACT_APP_API_URL=https://api-pre.com
// env/.env.production
REACT_APP_API_URL=https://api-prod.com
然后再 webpack.base.ts 中引入,然后解析对应环境配置,最后通过 DefinePlugin 进行注入
import path from "path";
import { Configuration, DefinePlugin } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import * as dotenv from "dotenv";
// 加载配置文件
const envConfig = dotenv.config({
  path: path.resolve(__dirname, "../env/.env." + process.env.BASE_ENV),
});
// console.log("process.env", process.env);
// console.log("NODE_ENV", process.env.BASE_ENV);
// console.log("REACT_APP_API_URL", process.env.REACT_APP_API_URL);
const baseConfig: Configuration = {
  // ...
  plugins: [
    // 注入到业务
    new DefinePlugin({
      "process.env": JSON.stringify(envConfig.parsed),
      "process.env.BASE_ENV": JSON.stringify(process.env.BASE_ENV),
      "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
    }),
  ].filter(Boolean), // // 去除数组中的假值(例如,如果某些插件的条件不满足而导致未定义)
};
export default baseConfig;
业务代码中使用
import { createRoot } from 'react-dom/client';
import App from './App';
// const root = document.getElementById('root');
const root = document.querySelector('#root')
console.log('NODE_ENV', process.env.NODE_ENV)
console.log('BASE_ENV', process.env.BASE_ENV)
console.log("process.env", process.env);
if(root) {
    createRoot(root).render(<App />)
}
然后重启项目: pnpm run dev:dev

七、文件别名

先在 webpack.base.ts 中配置

resolve: {
  extensions: [".ts", ".tsx", ".js", ".jsx", ".less", ".css"],
    // 别名需要配置两个地方,这里和 tsconfig.json
    alias: {
    "@": path.join(__dirname, "../src")
  },
},
然后还需要在 tsconfig.json 中配置
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
},
}
然后就可以在项目中使用了
import '@/App.css'
function App() {
  return <h2>webpack5 - react - ts < /h2>
}
export default App

八、引入 lesssassscss)、stylus

先安装相关的依赖
pnpm i less less-loader sass-loader sass stylus stylus-loader -D
webpack.base.ts 添加相关的 loader
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 = {
  // ...
  module: {
    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'
        ]
      }
    ],
  },
  // ...
};
export default baseConfig;
然后就可以在业务中使用了
/* src/app.less */
@color: red;
.lessBox {
.box {
    color: @color;
    background - color: lightblue;
    transform: translateX(100);
&:before{
      @arr: 'hello', 'world';
      content: `@{arr}.join(' ').toUpperCase()`;
    }
  }
}
/* src/app.scss */
$blue: #1875e7;
$side: left;
.scssBox {
  margin: 20px 0;
.box {
    color: $blue;
    background - color: rgb(226, 223, 223);
    border: 1px solid grey;
    margin -#{ $side }: 20px;
    padding: (20px / 2);
  }
}
/* src/app.styl */
.stylBox
  .box
color: red;
background - color: yellow;
在 App.tsx 中引入:
import '@/App.css'
import lessStyles from './app.less'
import scssStyles from './app.scss'
import stylStyles from './app.styl'
function App() {
  return <div>
    <h2>webpack5 - react - ts < /h2>
    < div className = { lessStyles['lessBox']} >
      <div className={ lessStyles['box'] }> lessBox < /div>
        < /div>
        < div className = { scssStyles['scssBox']} >
          <div className={ scssStyles['box'] }> scssBox < /div>
            < /div>
            < div className = { stylStyles['stylBox']} >
              <div className={ stylStyles['box'] }> stylBox < /div>
                < /div>
                < /div>
}
export default App
重启项目,就会发现生成了带有 hash 值的 class 类名,且里面包含了我们自定义的类名,方便日后调试用
同时在验证打包是否成功,运行 pnpm run build:dev ,然后通过 serve -S dist 查看。 当然,如果
你不希望每次写 less 的时候,都在文件名上加一个 .module ,可以在 less-loader 中添加如下配置
module: {
  rules: [
    // ...
    {
      test: lessRegex,
      use: [
        ...styleLoadersArray,
        {
          loader: "less-loader",
          options: {
            lessOptions: {
              importLoaders: 2,
              // 可以加入modules: true,这样就不需要在less文件名加module了
              modules: true,
              // 如果要在less中写类型js的语法,需要加这一个配置
              javascriptEnabled: true
            },
          },
        },
      ],
    },
    // ...
  ],
  },
  // ...
  };
export default baseConfig
至此,我们就完成了 less sass scss stylus 的引入
虽然我们在样式文件名上加一个 .module 的后缀,可以明确这是 css modules ,但也带
来了额外的码字开销。可以在 global.d.ts 加入样式文件的声明,就可以避免写 .module
// src/typings/global.d.ts
/* CSS MODULES */
declare module '*.css' {
    const classes: { [key: string]: string };
    export default classes;
}
declare module '*.scss' {
    const classes: { [key: string]: string };
    export default classes;
}
declare module '*.sass' {
    const classes: { [key: string]: string };
    export default classes;
}
declare module '*.less' {const classes: { [key: string]: string };
export default classes;
}
declare module '*.styl' {
    const classes: { [key: string]: string };
    export default classes;
}
然后就可以删掉样式文件中的 .module 后缀了

九、处理其他常用资源

1、处理图片

对于图片文件, webpack4 使用 file-loader url-loader 来处理的,但 webpack5 不使用这两个 loader 了 , 而是采用自带的 asset-module 来处理,修改 webpack.base.ts ,添加图片解析配置
{
  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]', // 文件输出目录和命名
        },
      },
    ]
  }
}
由于我们希望通过 ES6 的新语法 ESModule 的方式导入资源,为了使 TypeScript 可以识别图片模
块,需要在 src/typings/global.d.ts 中加入声明
/* 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;
}
然后在 App.tsx 中引入图片资源
import '@/App.css'
import lessStyles from '@/app.less'
import scssStyles from '@/app.scss'
import stylStyles from '@/app.styl'
import smallImg from '@/assets/images/5kb_img.jpeg'
import bigImg from '@/assets/images/10kb_img.png'
function App() {
return <div>
<h2>webpack5-react-ts</h2>
<div className={lessStyles['lessBox']}>
<div className={lessStyles['box']}>lessBox
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
<div className={lessStyles['smallImg']}>小图片背景</div>
<div className={lessStyles['bigImg']}>大图片背景</div>
</div>
</div>
<div className={scssStyles['scssBox']}>
<div className={scssStyles['box']}>scssBox</div>
</div>
<div className={stylStyles['stylBox']}>
<div className={stylStyles['box']}>stylBox</div>
</div>
</div>
}
export default App
app.less 文件中加入背景图片
@color: red;
.lessBox {
    .box {
        color: @color;
        background-color: lightblue;
        transform: translateX(100);
        &:before{
            @arr: 'hello', 'world';
            content: `@{arr}.join(' ').toUpperCase()`;
        }
    }
    .smallImg {
        width: 69px;
        height: 75px;
        background: url('@/assets/imgs/5kb_img.jpeg') no-repeat;
    }
    .bigImg {
        width: 232px;
        height: 154px;
        background: url('@/assets/imgs/10kb_img.png') no-repeat;
    }
}
重启项目,可以看到图片被正确地展示出来了,图片被打包进了我们指定的 static/images 文件里面

2、处理字体和媒体

字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改 webpack.base.ts 文件
module: {
  rules: [
    // ...
    {
      test: /.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
      type: "asset", // type选择asset
      parser: {
        dataUrlCondition: {
          maxSize: 10 * 1024, // 小于10kb转base64
        }
      },
      generator: {
        filename: 'static/fonts/[hash][ext][query]', // 文件输出目录和命名
      },
    },
    {
      test: /.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
      type: "asset", // type选择asset
      parser: {
        dataUrlCondition: {
          maxSize: 10 * 1024, // 小于10kb转base64
        }
      },
      generator: {
        filename: 'static/media/[hash][ext][query]', // 文件输出目录和命名
      },
    },
  ]
}
对于 fant-family ,可以去 google font 找一个字体,放入 assets/fonts ,然后通过下面的方式引入
@color: red;
@font-face {
font-family: "GoldmanBold";
src: local("GoldmanBold"),
url("@/assets/fonts/Phudu-Bold.ttf") format("truetype");
font-weight: bold;
}
.lessBox {
.box {
color: @color;
background-color: lightblue;
transform: translateX(100);
font-family: "GoldmanBold";
&:before{
@arr: 'hello', 'world';
content: `@{arr}.join(' ').toUpperCase()`;
}
}
}
就可以发现,页面的字体变成了我们设置的字体样式

3、处理json资源

// ...
{
  // 匹配json文件
  test: /\.json$/,
    type: "json", // 模块资源类型为json模块
      generator: {
    // 这里专门针对json文件的处理
    filename: "static/json/[name].[hash][ext][query]",
  },
},
// ...

src下增加一个json文件test.json

[
  {
    "name": "ian1",
    "age": 18
  },
  {
    "name": "ian2",
    "age": 22
  },
  {
    "name": "ian3",
    "age": 23
  }
]
然后在 App.tsx 中引入
import memberList from './test.json'
这里可能会出现一个报错
Cannot find module './test.json'. Consider using '--resolveJsonModule' to import
module with '.json' extension.
需要在 tsconfig.json 中加入一个配置
{
  "compilerOptions": {
    // ...
    "resolveJsonModule": true,
  // ...
  },
  // ...
}
就可以在控制台打印出我们通过模块导入的 json 文件了
4、babel处理js非标准语法
现在 react 主流开发都是函数组件和 react-hooks ,但有时也会用类组件,可以用装饰器简化代码。新增 src/components/Cls.tsx 组件,在 App.tsx 中引入该组件使用
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
需要开启一下 ts 装饰器支持,修改 tsconfig.json 文件
// tsconfig.json
{
  "compilerOptions": {
    // ...
    // 开启装饰器使用
    "experimentalDecorators": true
  }
}
上面 Cls 组件代码中使用了装饰器目前 js 标准语法是不支持的,现在运行或者打包会报错,不识别装饰器语法,需要借助 babel-loader 插件,安装依赖
pnpm i @babel/plugin-proposal-decorators -D
babel.config.js 中添加插件
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), // 过滤空值
};
现在项目就支持装饰器了。重启项目即可

十、webpack构建速度优化

1、webpack进度条

webpackbar 这是一款个人感觉是个十分美观优雅的进度条,很多成名框架都用过他。而且使用起来也极其方便,也可以支持多个并发构建是个十分强大的进度插件。
pnpm i webpackbar -D
最常用的属性配置其实就是这些,注释里也写的很清楚了,我们在 webpack.base.ts 中引入
// ...
import WebpackBar from 'webpackbar';
// ...
const baseConfig: Configuration = {
  // ...
  // plugins 的配置
  plugins: [
    // ...
    new WebpackBar({
      color: "#85d", // 默认green,进度条颜色支持HEX
      basic: false, // 默认true,启用一个简单的日志报告器
      profile: false, // 默认false,启用探查器。
    })
  ],
};
export default baseConfig;
当然里面还有一个属性就是 reporters 还没有写上,可以在里面注册事件,也可以理解为各种钩子函数。如下
{ // 注册一个自定义数组
  start(context) {
    // 在(重新)编译开始时调用
    const { start, progress, message, details, request, hasErrors } = context
  },
  change(context) {
    // 在 watch 模式下文件更改时调用
  },
  update(context) {
    // 在每次进度更新后调用
  },
  done(context) {
    // 编译完成时调用
  },
  progress(context) {
    // 构建进度更新时调用
  },
  allDone(context) {
    // 当编译完成时调用
  },
  beforeAllDone(context) {
    // 当编译完成前调用
  },
  afterAllDone(context) {
    // 当编译完成后调用
  },
}

2、构建耗时

当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而 speed-measure-webpack-plugin 插 件可以帮我们做到,安装依赖
pnpm i speed-measure-webpack-plugin -D
使用的时候为了不影响到正常的开发 / 打包模式,我们选择新建一个配置文件,新增 webpack 构建分析配置文件 build/webpack.analy.ts
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
修改 package.json 添加启动 webpack 打包分析脚本命令,在 scripts 新增:
{
  // ...
  "scripts": {
  // ...
    "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.ts"
  }
  // ...
}
执行 pnpm build:analy 命令,就 可以看到各 plugin loader 的耗时时间 , 现在因为项目内容比较少,所以耗时都比较少,在真正 的项目中可以通过这个来分析打包时间花费在什么地方,然后来针对性的优化

3、开启持久化存储缓存

webpack5 之前做缓存是使用 babel-loader 缓存解决 js 的解析结果, cache-loader 缓存 css 等资源的解析结果,还有模块缓存插件 hard-source-webpack-plugin ,配置好缓存后第二次打包,通过对文件做哈希对比来验证文件前后是否一致,如果一致则采用上一次的缓存,可以极大地节省时间。
// webpack.base.ts
// ...
module.exports = {
  // ...
  cache: {
    type: 'filesystem', // 使用文件缓存
  },
}
通过开启 webpack5 持久化存储缓存,极大缩短了启动和打包的时间。缓存的存储位置在 node_modules/.cache/webpack ,里面又区分了 development production 缓存。

4、开启多线程loader

运行在 Node.js 之上的 webpack 是单线程模式的,也就是说, webpack 打包只能逐个文件处理,当webpack 需要打包大量文件时,打包时间就会比较漫长。
多进程 / 多实例构建的方案比较知名的有以下三种:
thread-loader
parallel-webpack
HappyPack
webpack loader 默认在单线程执行,现代电脑一般都有多核 cpu ,可以借助多核 cpu 开启多线程loader 解析,可以极大地提升 loader 解析的速度, thread-loader 就是用来开启多进程解析 loader
的,安装依赖
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']
    },
  ]
}
由于 thread-loader 不支持抽离 css 插件 MiniCssExtractPlugin.loader ( 下面会讲 ) ,所以这里只配
置了多进程解析 ts

5、缩小构建目标

一般第三库都是已经处理好的 , 不需要再次使用 loader 去解析,可以按照实际情况合理配置 loader 的作用范围,来减少不必要的 loader 解析,节省时间,通过使用 include exclude 两个配置项,可以实现这个功能
修改 webpack.base.ts
module: {
  rules: [
    {
      test: /\.(ts|tsx)$/, // 匹配ts和tsx文件
      exclude: /node_modules/,
      use: [
        // 开启多进程打包。
        // 进程启动大概为600ms,进程通信也有开销。
        // 只有工作消耗时间比较长,才需要多进程打包
        {
          loader: 'thread-loader',
          options: {
            wokers: 4 // 进程数
          }
        },
        'babel-loader']
    },
  ]
}
其他 loader 也是相同的配置方式,如果除 src 文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入 antd css ,可以把 antd 的文件目录路径添加解析 css 规则到 include 里面

6、devtools配置

开发过程中或者打包后的代码都是 webpack 处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, source map 就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度, devtool 选项就是 webpack 提供的选择源码映射方式的配置
修改 webpack.dev.ts
// webpack.dev.ts
module.exports = {
  // ...
  devtool: 'eval-cheap-module-source-map'
}
打包环境推荐: none( 就是不配置 devtool 选项了,不是配置 devtool: 'none')
// webpack.prod.ts
module.exports = {
  // ...
  // devtool: '', // 不用配置devtool此项
}
none 配置在调试的时候,只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。只
是不方便线上排查问题,但一般都可以根据报错信息在本地环境很快找出问题所在。

十一、webpack构建产物优化

1、bundle 体积分析工具

webpack-bundle-analyzer 是分析 webpack 打包后文件的插件,使用交互式可缩放树形图可视化
webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖
pnpm i webpack-bundle-analyzer -D
修改 webpack.analy.ts
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 run build:analy 命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小

2、样式提取

在开发环境我们希望 css 嵌入在 style 标签里面,方便样式热替换,但打包时我们希望把 css 单独抽离出来, 方便配置缓存策略。而插件 mini-css-extract-plugin 就是来帮我们做这件事的,安装依赖
pnpm i mini-css-extract-plugin -D

 修改 webpack.base.ts ,根据环境变量设置开发环境使用 style-looader ,打包模式抽离 css

// ...
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'
];
再修改 webpack.prod.ts ,打包时添加抽离 css 插件
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const prodConfig: Configuration = merge(baseConfig, {
  // ...
  plugins: [
    // ...
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].css' // 抽离css的输出目录和名称
    }),
  ],
});
export default prodConfig;
配置完成后,在开发模式 css 会嵌入到 style 标签里面,方便样式热替换,打包时会把 css 抽离成单独的 css 文件

3、样式压缩

可以看到,上面配置了打包时把 css 抽离为单独 css 文件的配置,打开打包后的文件查看,可以看到默认 css 是没有压缩的,需要手动配置一下压缩 css 的插件。
可以借助 css-minimizer-webpack-plugin 来压缩 css ,安装依赖
pnpm i css-minimizer-webpack-plugin -D
修改 webpack.prod.ts 文件, 需要在优化项 optimization 下的 minimizer 属性中配置
// ...
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
module.exports = {
  // ...
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(), // 压缩css
    ],
  },
}
再次执行打包就可以看到 css 已经被压缩了

4、js压缩

设置 mode production ,webpack 会使用内置插件 terser-webpack-plugin 压缩 js 文件 , 该插件默认支持多线程压缩, 但是上面配置 optimization.minimizer 压缩 css ,js 压缩就失效了 , 需要手动再添加一
,webpack 内部安装了该插件 , 由于 pnpm 解决了幽灵依赖问题 , 如果用的 pnpm 的话 , 需要手动再安装一下依赖
pnpm i terser-webpack-plugin compression-webpack-plugin -D
修改 webpack.prod.ts 文件:
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;
配置完成后再打包, css js 就都可以被压缩了

5、文件指纹

项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而 hash 就是浏览器缓存策略很重要的一部分。 webpack 打包的 hash 分三种:
hash :跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并
且全部文件都共用相同的 hash
chunkhash :不同的入口文件进行依赖文件解析、构建对应的 chunk ,生成对应的哈希值,文件本身修改或者依赖文件修改, chunkhash 值会变化
contenthash :每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash
hash 是在输出文件时配置的,格式是 filename: "[name].[chunkhash:8][ext]" [xx] 格式是 webpack 提供的占位符, :8 是生成 hash 的长度
修改 webpack.base.ts ,把 js 输出的文件名称格式加上 chunkhash ,把 css 和图片媒体资源输出格式加上 contenthash
// 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 ,修改抽离 css 文件名称格式
// 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]
    }),
    // ...
  ],
  // ...
})
再次打包就可以看到文件后面的 hash

6、代码分隔

一般第三方包的代码变化频率比较小,可以单独把 node_modules 中的代码单独打包,当第三包代码没变化时,对应 chunkhash 值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, webpack 提供了代码分隔功能,需要我们手动在优化项 optimization 中手动配置下代码分割 splitChunks 规则。
修改 webpack.prod.ts
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就提取出来
        }
      }
    }
  }
}
配置完成后执行打包,可以看到 node_modules 里面的模块被抽离到 verdors.xxx.js 中,业务代码
main.xxx.js

7、gzip压缩

前端代码在浏览器运行,需要从服务器把 html css js 资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用 gzip 压缩,现在大部分浏览器和服务器都支持 gzip ,可以有效减少静态资源文件大小,压缩率在 70% 左右。
nginx 可以配置 gzip: on 来开启压缩,但是只在 nginx 层面开启,会在每次请求资源时都对资源进
行压缩,压缩文件会需要时间和占用服务器 cpu 资源,更好的方式是前端在打包的时候直接生成 gzip资源,服务器接收到请求,可以直接把对应压缩好的 gzip 文件返回给浏览器,节省时间和 cpu
webpack 可以借助 compression-webpack-plugin 插件在打包时生成 gzip 文章,安装依赖
pnpm i compression-webpack-plugin -D
添加配置,修改 webpack.prod.ts
const glob = require('glob')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
  // ...
  plugins: [
    // ...
    new CompressionPlugin({
      test: /.(js|css)$/, // 只生成css,js压缩文件
      filename: '[path][base].gz', // 文件命名
      algorithm: 'gzip', // 压缩格式,默认是gzip
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    })
  ]
}
配置完成后再打包,可以看到打包后 js 的目录下多了一个 .gz 结尾的文件
因为只有 verdors.js 的大小超过了 10k, 所以只有它生成了 gzip 压缩文件 , 借助 serve -s dist 启动 dist, 查看 verdors.js加载情况

十二、代码提交工具

1. Commitizen Commitizen 是一个 Git 提交信息格式化工具,可以通过命令行交互式地创建符合规范的 Git 提交信息。 Commitizen 支持自定义提交信息的格式,并且可以与其他规范工具(如
ESLint Prettier )集成使用。
2. Conventional Commits Conventional Commits 是一个 Git 提交信息规范标准,规定了 Git
提交信息的格式和内容,包括提交类型、作用域、描述、消息体和消息页脚等部分。
Conventional Commits 规范可以帮助团队协作中的开发人员更好地理解和管理代码变更历史。
3. Husky Commitlint Husky Commitlint 是两个工具,可以结合使用来规范 Git 提交信
息。 Husky 可以在 Git 提交前执行一些预定义的钩子(如 pre-commit pre-push ),而
Commitlint 可以检查 Git 提交信息是否符合规范。
4. Git Hooks Git Hooks Git 自带的一个钩子系统,可以在 Git 的各个生命周期(如 pre
commit post-commit )执行自定义的脚本。开发人员可以使用 Git Hooks 来规范化 Git 提交
信息,例如在 pre-commit 钩子中执行提交信息的格式检查。
代码 Git 提交规范工具是非常有用的,可以帮助开发人员更好地管理代码变更历史,提高代码可维护性和协作效率。开发人员可以根据自己的需求和团队的规范选择合适的工具和标准来使用。

1、安装EditorConfig for VS Code

目前主流的编辑器或者 IDE 基本上都有对应的 EditorConfig 插件,有的是内置支持的(比如,
WebStorm 不需要独立安装 EditorConfig 的插件),有的需要独立安装
然后在插件的介绍页上点击 设置的齿轮 ,并且选择 Add to Workspace Recommendations ,就可以将 其加入清单。也可以直接开启 .vscode/extensions.json 进行编辑
{
    "recommendations": ["editorconfig.editorconfig"]
}

2、新建 .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
上面的配置可以规范本项目中文件的缩进风格,和缩进空格数等,会覆盖 vscode 的配置,来达到不同编辑器中代码默认行为一致的作用。

3、prettier

每个人写代码的风格习惯不一样,比如代码换行,结尾是否带分号,单双引号,缩进等,而且不能只靠口头规范来约束,项目紧急的时候可能会不太注意代码格式,这时候需要有工具来帮我们自动格式化代码,而prettier 就是帮我们做这件事的
安装vs code插件
再安装prettier
pnpm i prettier -D

4、新建.prettierrc.js

在根目录下新建 .prettierrc.js .prettierignore 文件
// .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

5、配置 .vscode/settings.json

配置前两步后,虽然已经配置 prettier 格式化规则,但还需要让 vscode 来支持保存后触发格式化,在项目根目录新建 .vscode 文件夹,内部新建 settings.json 文件配置文件,代码如下
{
"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,
}

6、脚本命令检查和修复格式

package.json "scripts" 中加入以下脚本命令
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.
{js,ts,json,tsx,css,less,scss,stylus,html,md}\""
这段代码是一个脚本命令,用于运行 Prettier 工具来格式化指定目录下的文件
现在可以测试一下,将 .prettierrc.js 中的 tabWidth 值修改为 4 (缩进为 4 个空格),然后运行
pnpm run lint:prettier ,随便打开一个 src 下的文件,你就会发现缩进从 2 变成 4

7、stylelint

7.1、安装stylelint插件和依赖

在vs code中找到stylelint插件并安装

然后再安装以下依赖

pnpm i stylelint stylelint-config-css-modules stylelint-config-prettier
stylelint-config-standard stylelint-order -D
7.2、新建 .stylelintrc.js .stylelintignore
// @see: https://stylelint.io
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-prettier'
// "stylelint-config-css-modules",
// "stylelint-config-recess-order" // 配置stylelint css属性书写顺序插件,
],
plugins: ['stylelint-order'],
rules: {
/**
* 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-singleline(在单行块中的左大括号之前绝不能有空格)"|"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'
]
}
}
.stylelintignore 文件
*.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
7.3、配置 .vscode/settings.json
最后记得在 .vscode/settings.json 中加入
{
// ...
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
},
"stylelint.validate": [
"css",
"less",
"sass",
"stylus",
"postcss"
]
}

8、eslint

ESLint 是什么呢?一个开源的 JavaScript linting 工具,是一个在 JavaScript 代码中通过规
则模式匹配作代码识别和报告的插件化的检测工具,它的目的是保证代码规范的一致性和及时发现代码问题、提前避免错误发生。它使用 espree JavaScript 代码解析成抽象语法树 (AST) ,然后通过 AST 来分析我们代码,从而给予我们两种提示:
1. 代码质量问题 :使用方式有可能有问题, eslint 可以发现代码中存在的可能错误,如 使用未声明变
声明而未使用的变量 修改 const 变量 代码中使用 debugger 等等
2. 代码风格问题 :风格不符合一定规则, eslint 也可以用来统一团队的代码风格,比如 加不加分号
使用 tab 还是空格 字符串使用单引号 等等
ESLint 的关注点是代码质量,检查代码风格并且会提示不符合风格规范的代码。除此之外 ESLint 也具有一部分代码格式化的功能。
8.1、安装eslint插件和包

在vs code中找到eslint插件并安装

安装 eslint依赖包
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
8.2、新建.eslintrc.js
在根目录新建一个 .eslintrc.js 文件
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: {
    // 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-<directive> 使用注释或要求在指令后进行描述
    // "@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: {
    '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/']
      }
    },
    react: {
      version: 'detect'
    }
  }
}
有时候,我们需要在代码中忽略 ESLint 的某些规则检查,此时我们可以通过加入代码注释的方式解
决:可以指定整个文件、某一行、某一区块开启 / 关闭 某些或全部规则检查;
/* eslint-disable */ --禁用全部规则 放在文件顶部则整个文件范围都不检查
/* eslint-disable no-alert, no-console */ --禁用某些规则
// eslint-disable-line --当前行上禁用规则
// eslint-disable-next-line --下一行上禁用规则
8.3 新建 .eslintignore
在根目录新建一个 .eslintignore 文件
node_modules
dist
env
.gitignore
pnpm-lock.yaml
README.md
src/assets/*
.vscode
public
.github
.husky
.eslintignore 用于指定 ESLint 工具在检查代码时要忽略的文件和目录。具体来说, .eslintignore
文件中列出的文件和目录将被 ESLint 忽略,不会对其进行代码检查和报告。这个文件中的每一行都是一个文件或目录的模式,支持使用通配符( * )和正则表达式来匹配多个文件或目录。
8.4 添加eslint语法检测脚本
前面的 eslint 报错和警告都是我们用眼睛看到的,有时候需要通过脚本执行能检测出来,在
package.json scripts 中新增
// --fix:此项指示 ESLint 尝试修复尽可能多的问题。这些修复是对实际文件本身进行的,只有剩余的未
修复的问题才会被输出。
"lint:eslint": "eslint --fix --ext .js,.ts,.tsx ./src",
除此之外再解决一个问题就是 eslint 报的警告
React version not specified in eslint-plugin-react settings
需要告诉 eslint 使用的 react 版本 ,在 .eslintrc.js rules 平级添加 settings 配置,让 eslint 自己检测 react 版本,对应 issuse
settings: {
  "react": {
    "version": "detect"
  }
}
再执行 pnpm run lint:eslint 就不会报这个未设置 react 版本的警告了

8.6 eslint prettier 冲突
安装两个依赖:
pnpm i eslint-config-prettier eslint-plugin-prettier -D
.eslintrc.js extends 中加入:
module.exports = {
  // ...
  extends: [
    // ...
    'plugin:prettier/recommended', // <==== 增加一行
  ],
  // ...
}
最后再配置一下 .vscode/settings.json
{
// ...
"eslint.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
},
}

9、husky + lint-statged

9.1 使用lint-staged优化eslint检测速度
在上面配置的 eslint 会检测 src 文件下所有的 .ts, .tsx 文件,虽然功能可以实现,但是当项目文件
多的时候,检测的文件会很多,需要的时间也会越来越长,但其实只需要检测提交到暂存区,就是 git add 添加的文件,不在暂存区的文件不用再次检测,而 lint-staged 就是来帮我们做这件事情的。
package.json 添加 lint-staged 配置
"lint-staged": {
"src/**/*.{ts,tsx}": [
"pnpm run lint:eslint",
"pnpm run lint:prettier"
]
}
因为要检测 git 暂存区代码,所以如果你的项目还没有使用 git 来做版本控制,需要执行 git init
初始化一下 git
git init
初始化 git 完成后就可以进行测试了,先提交一下没有语法问题的 App.tsx
git add src/App.tsx
src/App.tsx 提交到暂存区后,执行 npx lint-staged ,会顺利通过检测。

然后在 App.tsx 中加入一个未使用的变量:

// ...
const a = 1
//
然后执行:
git add src/App.tsx
npx lint-staged
你就会发现控制台出现了 warning,这就是 lint-staged 的作用

9.2 使用tsc检测类型和报错
需要注意的是,执行 tsc 命令可能会生成一个编译后的产物文件,需想要避免这个问题,需要在
tsconfig.json 中加入 "noEmit": true
{
"compilerOptions": {
"target": "es2016", // 编译输出的ECMAScript目标版本为ES2016
"esModuleInterop": true, // 允许默认导入与CommonJS模块操作
"module": "commonjs", // 使用CommonJS模块系统
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"strict": true, // 启用所有严格的类型检查选项
"skipLibCheck": true, // 跳过所有声明文件(.d.ts)的类型检查
"resolveJsonModule": true, // 允许从.json文件中导入数据
"allowJs": false, // 不允许在项目中包含JavaScript文件
"noEmit": true, // 不生成输出文件(例如.js文件)
"noImplicitAny": false, // 在表达式和声明上有隐含的any类型时,不发出警告
"experimentalDecorators": true, // 启用实验性的装饰器
"baseUrl": ".", // 用于解析非相对模块名称的基目录
"paths": {
"@/*": ["./src/*"] // 将路径别名“@”映射到“./src/”
},
"typeRoots": ["./typings/*.d.ts", "node_modules/@types"], // 指定类型声明文件的查找路径
"jsx": "react-jsx" // react18这里改成react-jsx,就不需要在tsx文件中手动引入React了
},
"include": ["./src/*", "./src/**/*.ts", "./src/**/*.tsx", "./typings/*.d.ts"],
// 指定需要包含的文件和目录
"exclude": ["node_modules", "dist"] // 指定需要排除的文件和目录
}
在项目中使用了 ts ,但一些类型问题,现在配置的 eslint 是检测不出来的,需要使用 ts 提供的 tsc
工具进行检测
index.tsx 定义了函数 hello ,参数 name string 类型,当调用传 number 类型参数时,页面有了明显的ts 报错,但此时提交 index.tsx 文件到暂存区后执行 npx lint-staged
发现没有检测到报错,所以需要配置下 tsc 来检测类型,在 package.json 添加脚本命令
"pre-check": "tsc && npx lint-staged"
执行 pnpm run pre-check ,发现已经可以检测出类型报错了。
 9.3、配置husky
为了避免把不规范的代码提交到远程仓库,一般会在 git 提交代码时对代码语法进行检测,只有检测通过时才能被提交,git 提供了一系列的 githooks ,而我们需要其中的 pre-commit 钩子,它会在 git commit 把代码提交到本地仓库之前执行,可以在这个阶段检测代码,如果检测不通过就退出命令行进程停止 commit
本篇博客介绍了 webpack 的配置使用,但只介绍了其中一部分,更多的使用方法还是得到官方文档查询。

  • 27
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值