简介: 本篇文章主要讲解如何从一个空目录开始,建立起一个基于webpack + react + typescript的标准化前端应用。

作者 | 刘皇逊(恪语)
来源 | 阿里开发者公众号

前言

本篇文章主要讲解如何从一个空目录开始,建立起一个基于webpack + react + typescript的标准化前端应用。

  • 技术栈: webpack5 + React18 + TS
  • 工程化: eslint + prettier + husky + git hooks
  • 支持图片、less、sass、fonts、数据资源(JSON、csv、tsv等)、Antd按需加载以及主题
  • 支持热更新、资源压缩、代码分离(动态导入、懒加载等)、缓存、devServer

背景

在项目开发中,我们可以使用create-react-app或者飞冰等脚手架工具,那么,为什么我们要自己来搭建一个标准化项目?

原因

  • 当我们使用优秀的脚手架工具开发项目时,当然会提升很多便利,他们的功能更全面、性能更强大,但是在这些值得学习的榜样面前,我们需要从零开始,动手去实现每一个细节和功能,看的再多都不如自己动手实现一个demo更有效果。并且动手实践也可以帮助我们理解项目打包和编译的原理,进而提升自己的技术熟练度,扩展我们的知识面。Webpack 实现工程化方方面面的功能,自然不是 all in one code实现的。从 Webpack 的设计理念和实现原理中,我们能接触到工程化方面的知识:架构扩展、插件化、缓存机制。学习Webpack也代表着学习前端的发展趋势:例如在webpack的竟对Vite上,我们可以学到bundleless的理念,跳过了传统的打包这个概念,并且其他先进理念都是我们需要去学习的地方。
  • 开发中,我们发现使用def、aone等生成一个成熟的前端项目模版,不难会发现,项目中的babel、weback、prettier、loader等配置文件缺失,而且难以修改现成的脚手架配置,可扩展能力较弱。导致在性能优化方面能做的工作有限,使得开发受到限制。

项目结构

目录

├── dist                                // 默认的 build 输出目录
├── .husky // pre-commit hook
├── webpack.config.js // 全局配置文件及webpack配置文件
├── test // 测试目录
└── src // 源码目录
├── assets // 公共的文件(如image、css、font等)
├── components // 项目组件
├── constants // 常量/接口地址等
├── routes // 路由
├── utils // 工具库
├── pages // 页面模块
├── Home // Home模块,建议组件统一大写开头
├── ...
├── App.tsx // react顶层文件
├── typing // ts类型文件
├── .editorconfig // IDE格式规范
├── .eslintignore // eslint忽略
├── .eslintrc // eslint配置文件
├── .gitignore // git忽略
├── .prettierrc // prettierc配置文件
├── .babelrc // babel配置文件
├── LICENSE.md // LICENSE
├── package.json // package
├── README.md // README
├── tsconfig.json // typescript配置文件

依赖

 "dependencies": {
"antd": "^4.22.4", // 懂得都懂
"react": "^18.2.0", // 懂得都懂
"react-dom": "^18.2.0" // 懂得都懂
},
"devDependencies": {
// babel全家桶
"@babel/core": "^7.18.10",
"@babel/plugin-proposal-class-properties": "^7.18.6", // React class支持
"@babel/plugin-transform-runtime": "^7.18.10", // 抽离提取 Babel的注入代码,防止重复加载,减小体积
"@babel/preset-env": "^7.18.10", // 提供的预设,允许我们使用最新的JavaScript
"@babel/preset-react": "^7.18.6", // react支持 // ts类型检查
"@types/node": "^18.6.4",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
// @types 开头的是对应包的 TypeScript 类型声明
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0", // webpack loader:解析对应文件
"csv-loader": "^3.0.5",
"sass-loader": "^13.0.2",
"xml-loader": "^1.2.1",
"ts-loader": "^9.3.1",
"less-loader": "^11.0.0", // eslint全家桶
"eslint": "^8.21.0",
"eslint-config-ali": "^14.0.1", // ali前端规约
"eslint-config-prettier": "^8.5.0", // 关闭所有不必要或可能与[Prettier]冲突的规则
"eslint-import-resolver-typescript": "^3.4.0", // 添加 ts 语法支持 eslint-plugin-import
"eslint-plugin-import": "^2.26.0", // ES6+ import/export 语法支持
"eslint-plugin-prettier": "^4.2.1", // prettier语法支持
"eslint-plugin-react": "^7.30.1", // react语法支持
"eslint-plugin-react-hooks": "^4.6.0", // hooks语法支持
"eslint-webpack-plugin": "^3.2.0", // webpack plugin
"fork-ts-checker-webpack-plugin": "^7.2.13", // 避免webpack中检测ts类型
"html-webpack-plugin": "^5.5.0", // 简化HTML文件的创建 ,配合webpack包含hash的bundle使用
"mini-css-extract-plugin": "^2.6.1", // css拆分
"optimize-css-assets-webpack-plugin": "^6.0.1", // css压缩
"terser-webpack-plugin": "^5.3.3", // 使用 terser 压缩 js (terser 是一个管理和压缩 ES6+ 的工具)
"webpack-bundle-analyzer": "^4.5.0", // webpack打包体积可视化分析
"webpack-cli": "^4.10.0", // 提供脚手架命令
"webpack": "^5.74.0", // webpack引擎
"webpack-dev-server": "^4.9.3", // 开发环境的live server // 工具
"husky": "^8.0.1", // 自动配置 Git hooks 钩子
"less": "^4.1.3", // css类型
"sass": "^1.54.3", // css类型
"typescript": "^4.7.4", // ts
"lint-staged": "^13.0.3", // 对暂存的git文件运行linter // prettier 格式化
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3", // 在更改的文件上运行 prettier
}

实现过程

项目初始化

首先从一个空目录开始,对项目初始化:

mkdir demo
cd demo
git init
npm init

React和Babel引入

对于一个React项目,我们首先要安装React,写一个Hello World!

安装我们主要的项目依赖:

tnpm i -S react react-dom

由于我们的浏览器不支持最新的ECMAScript语法,所以我们需要Babel来转义为ES5或者ES6。

安装我们的Babel来提高兼容性:

tnpm i -D @babel/core babel-preset-env babel-preset-react @babel/plugin-proposal-class-properties
  • @babel/core: babel转码的核心引擎
  • babel-preset-env: 添加对ES5、ES6的支持
  • babel-preset-react: 添加对JSX的支持
  • @babel/plugin-proposal-class-properties: 对React中class的支持

Webpack引入

tnpm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
  • webpack: weback插件的核心依赖
  • webpack-cli: 为插件提供命令行工具
  • webpack-dev-server: 帮助启动live server
  • html-webpack-plugin: 帮助创建HTML模版

Babel配置

.babelrc中添加基本配置:

{
"presets": ["@babel/react", "@babel/env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
Babel Plugin

Babel是代码转换器,借助Babel,我们可以使用最流行的js写法,而plugin就是实现Babel功能的核心。

这里的配置是为了支持react中class的写法。

Babel Preset

Babel的Plugin一般拆成尽可能小的粒度,开发者可以按需引进,例如ES6到ES5的功能,官方提供了20+插件,这样可以提高性能和扩展性,但是很多时候逐个引入就很让人头大,而Babel Preset就是为此而生,可以视为Presets是相关Plugins的集合。

  • @babel/react: 支持了React所有的转码需求
  • @babel/env: 不夸张滴讲,仅需要它自己内部的配置项,就可以完成现代JS工程几乎所有的转码需求

Webpack基本配置

新建一个webpack.config.js文件。

//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, '/dist'),
filename: 'bundle.js'
},
devServer: {
port: 8080
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
]
},
plugins:[
new HtmlWebpackPlugin({
template: path.join(__dirname,'/src/index.html')
})
]
}
  • entry: 入口,开始打包的起点
  • output: 打包文件的地址
  • devServer: live server配置
  • test: 使用loader的文件类型
  • loader: 将要使用的loader

Package.json基本配置

"start": "webpack serve --mode development --open --hot",
"build": "webpack --mode production"
  • mode: process.env.NODE_ENV --> development, 为modules和chunks启用有意义的名称
  • open: 告诉server在服务启动后打开默认浏览器
  • hot: 开启热更新

写一个React Demo

目前的项目结构如下图所示:

js和html文件如下图所示:

最后,只要start一下,项目就会启动在8080端口。

TypeScript配置

tnpm install -D typescript ts-loader @types/node @types/react @types/react-dom
  • typescript: TypeScript的主要引擎
  • ts-loader: 转义.ts --> .js 并打包
  • @types/node @types/react @types/react-dom: 对node、react、react dom类型的定义

同时在根目录加入tsconfig.json来对ts编译进行配置:

//_tsconfig.json_

{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node"
}
}

最后在webpack中添加对ts的支持。

添加ts-loader:
//_webpack.config.js_
...
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader'
}
...
设置resolve属性,来指定文件如何被解析:
//_webpack.config.js_
...
resolve:
{
extensions: [ '.tsx', '.ts', '.js' ],
}
...
rename入口:
//_webpack.config.js_
...
entry: "./src/index.tsx",
...

最后启动一下server来看一下ts配置是否正确。

上述我们的配置其实相当于执行了一次:

npx create-react-app my-app --template typescript

在这种流程下很是麻烦,将 *.ts 提供给 TypeScript,然后将运行的结果提供给 Babel,而且还要借助很多loader。

那么我们能不能简化一下这样的流程,因为Babel7中提供的babel-loader就可以完美进行编译ts,答案是可以的,这种方式直接简化了过程。

module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: ['babel-loader']
}
]
},

并且在.babelrc中也只多了一行@babel/preset-typescript,这种配置更简单,而且打包速度更快一点,逻辑更加清晰。

那么为什么还要在项目中使用ts-loader呢?

  • ts-loader 在内部是调用了 TypeScript 的官方编译器 -- tsc。所以,ts-loader 和 tsc 是共享 tsconfig.json,所以会提供完整的报错信息,ts-loader也与 vscode 提供的语法校验表现一致
  • 而@babel/preset-typescript有的时候会无法提供完整的报错信息和类型提示

管理资源

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效的模块中。

loader中,test属性可以识别出哪些文件会被转换;use属性可以定义出转换时,应该是用哪个loader。

CSS、Less、Sass

安装loader:

tnpm i -D less less-loader style-loader css-loader sass sass-loader

webpack配置:

//_webpack.config.js_
...
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
{
test: /\.(less|css)$/,
exclude: /\.module\.less$/,
use: [
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: !!DEV,
},
},
{
loader: 'less-loader',
options: {
sourceMap: !!DEV,
},
},
],
},
{
test: /\.(sass|scss)$/,
use: [
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: !!DEV,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: !!DEV,
},
},
],
},
...
图片、JSON资源

对于图片和字体,我们可以使用内置的Assets Modules来轻松地把这些内容加到我们的系统中,对于类型,我们可以选择:

  • asset/resource 发送一个单独的文件并导出 URL。
  • asset/inline 导出一个资源的 data URI。
  • asset/source 导出资源的源代码。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。
//_webpack.config.js_
...
module: {
rules: [{
test: /\.png/,
type: 'asset/resource'
}]
},
...

对于其他类型资源,我们需要安装csv-loader、xml-loader等:

//_webpack.config.js_
...
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
...

搭建开发环境

目前,我们的应用已经可以正常运行tsx文件,并且在本地进行调试和开发,那么我们来看看如何设置一个开发环境,来使开发变得更加轻松。

//_webpack.config.js_
...
const { DEV, DEBUG } = process.env;
process.env.BABEL_ENV = DEV ? 'development' : 'production';
process.env.NODE_ENV = DEV ? 'development' : 'production';
...
mode: DEV ? 'development' : 'production',
devtool: DEV && 'source-map',
...

我们可以从process.env中获取环境变量来区分开发环境和生产环境。

当webpack在本地打包代码时,我们可以使用inline-source-map,可以将编译后的代码映射回原始源代码,这样在报错的时候,错误就会被定为到确切的文件和行数。当然,在生产环境中,为了保护隐私,最好把这个设置动态关掉。
在开发环境中,webpack-dev-server会为你提供一个基本的web server,并且具有实时重新加载功能。

完善打包配置与缓存

我们希望每次打包都把上次的打包文件删除,可以使用CleanWebpackPlugin:

//_webpack.config.js_
...
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = {
plugins: [
new CleanWebpackPlugin(),
]
}
...

并且,在我们生产环境,我们希望改动后的新版本可以丢弃缓存,并且没有改动的版本可以保留缓存;但是在开发环境,我们不希望有缓存,而是每次都是拿到最新的资源。所以,需要对webpack config做一次拆分:分成

  • webpack.prod.js 生产环境打包配置
  • webpack.dev.js 开发环境打包配置

里面的区别主要在于打包后的文件名称、sourceMap等。

生产环境

contenthash:只有模块的内容改变,才会改变hash值:

output: {
filename: 'js/[name].[contenthash:8].js', // contenthash:只有模块的内容改变,才会改变hash值
},
开发环境
output: {
filename: 'js/[name].[hash:8].js',
}

性能优化

打包分析工具

可以使用webpack-bundle-analyzer来分析我们打包资源的大小:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
plugins: [
DEBUG && new BundleAnalyzerPlugin(),
]

同时设置package.json的启动项

资源压缩

OptimizeCSSAssetsPlugin主要用来优化css文件的输出,包括摈弃重复的样式定义、砍掉样式规则中多余的参数、移除不需要的浏览器前缀等。

TerserPlugin主要用来优化js体积,包括重命名变量,甚至是删除整个的访问不到的代码块。

//_webpack.config.js_
...
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
...
optimization: {
minimizer: [
new TerserPlugin({
parallel: false,
terserOptions: {
output: {
comments: false,
},
},
}),
new OptimizeCSSAssetsPlugin({}),
],
minimize: !DEV,
splitChunks: {
minSize: 500000,
cacheGroups: {
vendors: false,
},
},
},
...
代码分离

资源分离

1)多入口

webpack内置的特性能够把代码分离到不同的bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

//_webpack.config.js_
...
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js'
...

2)Tree Shaking

Webpack5在生产环境已经集成了Tree Shaking功能,不用的代码会被shaking掉:

// _webpack.config.js_
module.exports = {
// ...
mode: 'production',
};

但是在开发环境中需要手动配置(Not Recommend):

// _webpack.config.js_
module.exports = {
// ...
mode: 'development',
optimization: {
usedExports: true,
}
};

处于好奇,webpack是如何完美的避开没有使用的代码的呢?

很简单:就是 Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那是未引用代码(或叫做“死代码”—— dead-code ),并会对其进行 tree-shaking 。死代码并不总是那么明确的。下面是一些例子:

// _test.js_
// 这会被看作“活”代码,不会做 tree-shaking
import { add } from './math'
console.log(add(5, 6))
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import { add, minus } from './math'
console.log('hello webpack')
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import { add, minus } from './math' // 死的
import 'lodash' // 活的
console.log('hello webpack')
所以对于这种三方库我们可以使用下面的Shimming方法。

注意 Webpack 不能百分百安全地进行 tree-shaking。有些模块导入,只要被引入,就会对应用程序产生重要影响。一个很好的例子就是全局样式表,或者设置全局配置的JavaScript 文件。Webpack 认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用。比较好的告诉Webpack你的代码有副作用的方法就是在package.json里面设置sideEffects。

{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

3)Shimming预置依赖

对于上面的lodash库无法被shaking,我们可以使用细粒度shimming预置的方法来优化,首先引入ProvidePlugin插件,把应用程序中的模块依赖,改为一个全局变量依赖,让我们先移除 lodash 的 import语句,改为通过插件提供它,并且提取出join方法来全局使用它:

// _src/index.tsx
console.log(join(['hello', 'webpack'], ' '))
// _webpack.config.js_
plugins: [
new webpack.ProvidePlugin({
//_: 'lodash'
// 如果没注释的话,需要这样引用console.log(_.join(['hello', 'webpack'], ' '))
join: ['lodash', 'join'],
})
]

细粒度Shimming

一些遗留的模块依赖的this指向的window对象,我们可以使用import-loaders,它对依赖 window 对象下的全局变量(比如 $ 或 this )的第三方模块非常有用。

CommonJS 上下文中,这将会变成一个问题,也就是说此时的 this指向的是 module.exports。在这种情况下,你可以通过使用 imports-loader覆盖 this 指向:

// _webpack.config.js_
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
]
},

4)公共部分提取

防止重复可以使用splitChunk,提取出代码中的公共部分:

//_webpack.config.js_
...
minimize: !DEV,
splitChunks: {
minSize: 500000,
cacheGroups: {
vendors: false,
},
},
...
  • minSize:形成一个新代码块最小的体积
  • cacheGroups:这里开始设置缓存的 chunks

5)按需分离

在React项目中,代码按需分离可以使用如下方法,webpack 把 import() 作为一个分离点(split-point),并把引入的模块作为一个单独的 chunk。import() 将模块名字作为参数并返回一个 Promoise 对象,即 import(name) -> Promise。

//_index.tsx_
...
const WdAndDxEntry = lazy(() => import(/* webpackChunkName: "wd-and-dx" */ '../../old-code/component/wd-and-dx/entry'));
const WdAndDxFallback = () => ()
const SSRCompatibleSuspense = (props: Parameters< typeof Suspense>['0']) => {
const isMounted = useMounted(); if (isMounted) {
return < Suspense {...props} />;
}
return < >{props.fallback}< />;
}
...
return (
< SSRCompatibleSuspense fallback={< WdAndDxFallback />}>
< WdAndDxEntry
className=""
data={data}
style={{
height: 150,
}}
/>
< /SSRCompatibleSuspense>
);

6)分离三方库

配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块:

//_webpack.config.js_
...
module.exports = {
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
}
}
...

CSS分离

该插件MiniCssExtractPlugin将CSS提取到单独的文件中。它为每个包含CSS的JS文件创建一个CSS文件。

//_webpack.config.js_
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
...
{
test: /\.(sass|scss)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: !!DEV,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: !!DEV,
},
},
],
},
...
DEBUG && new BundleAnalyzerPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css',
}),
...
提高构建速度

当项目体积增大时,编译时间也随之增加。其中时间大头就是ts的类型检测耗时。ts-loader 提供了一个 transpileOnly 选项,它默认为 false,我们可以把它设置为 true,这样项目编译时就不会进行类型检查,也不会输出声明文件。

//_webpack.config.js_
...
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
}
]
}
...

可以看一下开关这个选项后的前后对比:

开启检查前

$ webpack --mode=production --config ./build/webpack.config.js
Hash: 36308e3786425ccd2e9d
Version: webpack 4.41.0
Time: 2482ms
Built at: 12/20/2019 4:52:43 PM
Asset Size Chunks Chunk Names
app.js 932 bytes 0 [emitted] main
index.html 338 bytes [emitted]
Entrypoint main = app.js
[0] ./src/index.ts 14 bytes {0} [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./index.html 489 bytes {0} [built]
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 1 hidden module
Done in 4.88s.

关闭检查后

$ webpack --mode=production --config ./build/webpack.config.js
Hash: e5a133a9510259e1f027
Version: webpack 4.41.0
Time: 726ms
Built at: 12/20/2019 4:54:20 PM
Asset Size Chunks Chunk Names
app.js 932 bytes 0 [emitted] main
index.html 338 bytes [emitted]
Entrypoint main = app.js
[0] ./src/index.ts 14 bytes {0} [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./index.html 489 bytes {0} [built]
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 1 hidden module
Done in 2.40s.

From 4.88s --> 2.4s,但是缺少了类型检查。

这里官方推荐了一个解决方案,使用fork-ts-checker-webpack-plugin,它在一个单独的进程上运行类型检查器,此插件使用 TypeScript 而不是 webpack 的模块解析,有了 TypeScript 的模块解析,我们不必等待webpack 编译。可以极大加快编译速度。

//_webpack.config.js_
...
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin()
]
...

用editorconfig统一编辑器规范

在根目录新建.editorconfig即可,注意不要与已有的lint规则冲突:

// __.editorconfig__
# http://editorconfig.org
root = true [*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true [*.md]
trim_trailing_whitespace = false [makefile]
indent_style = tab
indent_size = 4

Antd配置

babel中配置按需加载:

{
"presets": ["@babel/react", "@babel/env"],
"plugins": [
"@babel/plugin-proposal-class-properties",
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": true // or 'css'
},
"antd"
]
]
}

webpack中定制主题:

module: {
rules: [
// 处理 .css
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
// 处理 .less
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
// less-loader
{
loader: 'less-loader',
options: {
lessOptions: {
// 替换antd的变量,去掉 @ 符号即可
// https://ant.design/docs/react/customize-theme-cn
modifyVars: {
'primary-color': '#1DA57A',
},
javascriptEnabled: true, // 支持js
},
},
},
],
},
]
}

注意样式必须加载 less 格式,一个常见的问题就是引入了多份样式,less 的样式被 css 的样式覆盖了。

ESlint配置

ESlint主要功能包含代码格式和代码质量的校验,并且可以配置pre-commit来规范代码的提交。

tnpm install -D eslint eslint-webpack-plugin @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react
  • eslint: eslint主要引擎
  • eslint-webpack-plugin: webpack loader
  • @typescript-eslint/parser: 帮助ESlint lint ts代码
  • @typescript-eslint/eslint-plugin: 包含TS扩展规则的插件
  • eslint-plugin-react: 包含React扩展规则的插件

ESlint配置文件

// _eslintrc_
module.exports = {
parser: '@typescript-eslint/parser', // ESlint Parser
extends: [
'plugin:react/recommended', // 从@eslint-plugin-react中选择推荐的规则
'plugin:@typescript-eslint/recommended', // 从@typescript-eslint/eslint-plugin选择推荐的规则
],
parserOptions: {
ecmaVersion: 2018, // 帮助转化最先进的ECMAScript功能
sourceType: 'module', // 允许imports的用法
ecmaFeatures: {
jsx: true, // JSX兼容
},
},
rules: {
},
settings: {
react: {
version: 'detect', // 告诉eslint-plugin-react自动检测最新版本的react
},
},
};

Prettier配置

虽然 ESLint 也可以校验代码格式,但 Prettier 更擅长,所以项目中一般会搭配一起使用。为了避免二者的冲突,一般的解决思路是禁掉 ESLint 中与 Prettier 冲突的规则,然后使用 Prettier 做格式化, ESLint 做代码校验。

prettier配置文件

{
"arrowParens": "avoid",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": true,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": true,
"vueIndentScriptAndStyle": false
}

代码提交规范

prettier 只是保证了在通过编辑器(vs code)进行格式化代码的时候,格式化成需要的格式(当然可以通过配置 onSave 在代码保存时自动格式化),但是无法保证所有人都会主动进行。
因此进行自动格式化显得非常重要,而自动格式化的是时机选择 pre-commit 最恰当,通过 git hook ,能够在 commit 之前格式化好代码(如果已经 commit,会将暂存转为提交,生成提交记录,需要回滚才会撤销)。

tnpm i -D pretty-quick prettier husky 
  • pretty-quick: 配合git-hooks进行代码检测,并且fix
  • husky: 可以通过配置的方式来使用git-hooks,避免手动修改

package.json设置

"pretty": "./node_modules/.bin/pretty-quick --staged"
...
"husky": {
"hooks": {
"pre-commit": "tnpm run pretty"
}
},

Webpack完整配置

最后贴一下完整的配置,因为Aone发布自动更新版本号,所以不用拆分config文件来根据环境设置缓存,并且配置已经尽可能简化,拆分反而会增加维护成本。

//_webpack.config.js_
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const ESLintPlugin = require('eslint-webpack-plugin'); const { DEV, DEBUG } = process.env; process.env.BABEL_ENV = DEV ? 'development' : 'production';
process.env.NODE_ENV = DEV ? 'development' : 'production'; module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, '/dist'),
filename: 'bundle.js',
clean: true,
},
devServer: {
port: 8080,
},
mode: DEV ? 'development' : 'production',
devtool: DEV && 'source-map',
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
// 处理 .less
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
// less-loader
{
loader: 'less-loader',
options: {
lessOptions: {
// 替换antd的变量,去掉 @ 符号即可
// https://ant.design/docs/react/customize-theme-cn
modifyVars: {
'primary-color': '#1DA57A',
'border-color-base': '#d9d9d9', // 边框色
'text-color': '#d9d9d9'
},
javascriptEnabled: true, // 支持js
},
},
},
],
},
{
test: /\.(sass|scss)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: !!DEV,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: !!DEV,
},
},
],
},
{
test: /\.png/,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: false,
terserOptions: {
output: {
comments: false,
},
},
}),
new OptimizeCSSAssetsPlugin({}),
],
minimize: !DEV,
splitChunks: {
minSize: 500000,
cacheGroups: {
vendors: false,
},
},
},
resolve: {
modules: ['node_modules'],
extensions: ['.json', '.js', '.jsx', '.ts', '.tsx', '.less', 'scss'],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, '/src/index.html'),
filename: 'app.html',
inject: 'body',
}),
DEBUG && new BundleAnalyzerPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css',
}),
new ESLintPlugin(),
new ForkTsCheckerWebpackPlugin(),
].filter(Boolean),
};

总结

这篇文章主要记录了开发过程中从项目初始化开始,再到一个标准化前端项目的搭建路程。涉及相关代码规范、开发环境搭建、生产环境优化等,旨在打造出一个可快速使用的现代Webpack5.x+React18.x+Typescript+Antd4.x模板,以供在以后的实际业务场景需求中零成本使用。

推荐阅读

1.研发效能的思考总结

2.关于技术能力的思考和总结

3. 如何结构化和清晰地进行表达


重磅来袭!2022上半年阿里云社区最热电子书榜单!

千万阅读量、百万下载量、上百本电子书,近200位阿里专家参与编写。多元化选择、全领域覆盖,汇聚阿里巴巴技术实践精华,读、学、练一键三连。开发者藏经阁,开发者的工作伴侣~

点击这里,查看详情。

原文链接:https://click.aliyun.com/m/1000357087/

本文为阿里云原创内容,未经允许不得转载。

从0到1使用Webpack5 + React + TS构建标准化应用的更多相关文章

  1. react+ts封装AntdUI的日期选择框之月份选择器DatePicker.month

    需求:由于在项目开发中,当需要使用该组件时都需要对该组件进行大量的代码输出,为了方便代码统一管理,减少冗余代码,所以将此组件进行二次封装. 其他成员在使用中只需将自己的设置通过对应的参数传递到该组件, ...

  2. typescript使用入门及react+ts实战

    ts介绍 TypeScript是一种由微软开发的自由和开源的编程语言.它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程. 与js关系 ts与js区 ...

  3. react-native init的时候出现问题:npm WARN React-native@0.35.0 requires a peer of react@~15.3.1 but none was

    react-native init的时候出现问题:npm WARN React-native@0.35.0 requires a peer of react@~15.3.1 but none was ...

  4. react+redux构建淘票票首页

    react+redux构建淘票票首页 描述 在之前的项目中都是单纯的用react,并没有结合redux.对于中小项目仅仅使用react是可以的:但当项目变得更加复杂,仅仅使用react是远远不够的,我 ...

  5. vue.js2.0实战(1):搭建开发环境及构建项目

    Vue.js学习系列: vue.js2.0实战(1):搭建开发环境及构建项目 https://my.oschina.net/brillantzhao/blog/1541638 vue.js2.0实战( ...

  6. React + Ts 实现三子棋小游戏

    在这里阅读效果更佳 还记得当年和同桌在草稿纸上下三子棋的时光吗 今天我们就用代码来重温一下年少(假设你有react基础,没有也行,只要你会三大框架的任意一种,上手react不难) 游戏规则 双方各执一 ...

  7. webpack搭建react+ts+eslint项目

    [初始化项目] mkdir react_ts_eslint cd react_ts_eslint npm init [生成ts配置文件] tsc --init [安装相关依赖] npm install ...

  8. 从零搭建react+ts组件库(封装antd)

    为什么会有这样一篇文章?因为网上的教程/示例只说了怎么做,没有系统详细的介绍引入这些依赖.为什么要这样配置,甚至有些文章还是错的!迫于技术洁癖,我希望更多的开发小伙伴能够真正的理解一个项目搭建各个方面 ...

  9. npm WARN React-native@0.35.0 requires a peer of react@~15.3.1 but none was installed.

    解决方案: 方法一: npm install -save react@~15.3.1 方法二:在package.json中可以添加依赖 "dependencies": { &quo ...

  10. npm WARN react-native-maps@0.14.0 requires a peer of react@>=15.4.0 but none was installed

    install  the  react-native     here comes a  questions :: npm WARN react-native@0.41.2 requires a pe ...

随机推荐

  1. labeImg 遇到闪退问题,TypeError: setValue(self, a0: int): argument 1 has unexpected type 'float'

    将python 环境安装为python3.9,然后重新安装labelImg

  2. 【开源库推荐】#5 Android高亮引导库

    原文:[开源库推荐]#5 Android高亮引导库 - Stars-One的杂货小窝 本文介绍2个高亮引导库HighLightPro和Curtain hyy920109/HighLightPro: A ...

  3. Markdown 使用diff高亮代码区某行数据

    使用diff标明代码区即可 如: ```diff fun main(){ + say("") return "" } fun main(){ - say(&qu ...

  4. 35_音视频播放器_seek&暂停

    目录 一.实现seek功能 二.解决点击seek操作时会出现画面快速闪过 三.实现暂停功能 3.1.音频暂停 3.2.视频暂停 一.实现seek功能 我们主要是使用ffmpeg的av_seek_fra ...

  5. 【leetcode 春季比赛3题 二叉搜索树染色】广度搜索

    暴力: import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import ja ...

  6. 不要升级!不要升级!MacOS 14.4 引发Java 应用崩溃

    如果最近您收到了MacOS 14.4的升级提醒,那么建议你暂时先不要升级! 在x上,Java开发领域的一些大v们,也发现了这个问题,并提醒大家不要升级. 根据Java官方发布的文章了解到,该问题主要是 ...

  7. View事件机制分析

    目录介绍 01.Android中事件分发顺序 1.1 事件分发的对象是谁 1.2 事件分发的本质 1.3 事件在哪些对象间进行传递 1.4 事件分发过程涉及方法 1.5 Android中事件分发顺序 ...

  8. 【LeetCode刷题】239.滑动窗口最大值

    239.滑动窗口最大值(点击跳转LeetCode) 给你一个整数数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧.你只可以看到在滑动窗口内的k个数字.滑动窗口每次只向右移动一位. ...

  9. 关于FTP文件传输协议说明,带你了解更详情的文件传输协议

    Internet和其他网络上的人与设备之间的通信使用协议进行.您可以说协议定义了对话规则:谁必须在何时发送哪些信息?如果数据没有到达接收者,会发生什么?您如何保护转帐免受错误和犯规?每当我们使用Int ...

  10. 简单c++构建第一人称

    本文内容为UE4.27的文档教程 GameMode确定 新建的项目会自动生成GameMode,如果有更改,而不是使用默认的GameMode类,就需要在引擎的设置中更改 角色的实现 前后左右移动 //前 ...