可能想要玩的技术栈:
- 主技术栈:webpack5 + React18 + TS4.x + React-Router6.x
- 代码风格和规范:eslint + prettier + stylelint + husky + pre-commmit + commitlint
- 样式与处理器:
- 状态管理:TanStack Query、RTK/RTK-Query、mobx、recoil、jotai、zustand......
- UI:Antd5.x、Arco design -(按需加载、
dark theme
、I18n
) - 单测:Jest、React testing library,Chai、Mocha、Enzyme's......
- 性能优化
- 工具层面:热更新、资源压缩、代码分离(动态导入、懒加载、预加载等)、缓存……
- 代码层面:大组件拆分、全局状态管理、组件封装、
re-render
- 静态资源:图片、
fonts
、Media
、数据资源(JSON
、csv
、tsv
、excel
)...... React-Admin
、可视化大屏、响应式、代码生成、低代码- 权限:
React-Admin(RBAC) + Go
、React-Admin(RBAC) + Node
- 接口
mock
CI/CD
、nginx
、jekins
、Docker 部署
- ......
1、依赖管理
这里将使用 pnpm
,至于为什么,一个字:快、省、狠!
pnpm
、npm
和 yarn
都是 Node.js
和 JavaScript
项目的包管理器。每种都有自己的优点和缺点,选择使用哪一种最终取决于你的具体需求和偏好。 以下是 pnpm
与 npm
和 yarn
相比的一些优势:
- 更快的安装和更新时间:
pnpm
使用独特的方法来安装包,避免重复包,从而加快安装和更新时间。特别是,当安装共享依赖的多个包时,pnpm
只会为每个依赖安装一次,而npm
和yarn
会为每个包分别安装每个依赖。 - 更少的磁盘空间使用:由于
pnpm
避免了包重复,它使用的磁盘空间比npm
和yarn
更少。如果你的磁盘空间有限,或者你正在处理具有许多依赖项的大型项目,那么这一点尤其重要。 - 更好地支持
monorepos
:pnpm
旨在与monorepo
项目很好地配合使用,这些项目是在单个存储库中包含多个包或模块的项目。pnpm
独特的包管理方法可以更轻松地管理monorepo
中多个包和模块的依赖关系。 - 更好地支持对等依赖:
pnpm
比npm
和yarn
对对等依赖有更好的支持。对等依赖项是包所需的依赖项,但不应与包一起安装,因为它们已由应用程序或其他依赖项提供。pnpm
可以比npm
和yarn
更高效、更准确地处理对等依赖。 - 更清晰的依赖树:
pnpm
生成比npm
和yarn
更清晰的依赖树。这是因为pnpm
对依赖项使用平面目录结构,而npm
和yarn
使用嵌套目录。扁平结构使得依赖树更容易理解和调试。
总的来说,对于具有大量依赖关系或使用
monorepo
结构的项目,pnpm
是一个不错的选择。但是,值得注意的是,pnpm
不像npm
和yarn
那样广泛使用,因此也可能会遇到某些包或工具的兼容性问题。
详细信息可以看 这里,因为本系列不会涉及到更高阶的功能,这里只介绍基本使用:
Commond | Meaning |
pnpm add sax (-S) | 安装到 dependencies |
pnpm add sax -D | 安装到 devDependencies |
pnpm add sax -g | 安装到全局 |
# 我的 pnpm 版本 % pnpm -v 7.18.2 # 初始化package.json文件 pnpm init
会在根目录生成一个package.json
文件:
{ "name": "fe", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
2、基本项目结构
在根目录新建基本的项目结构:
├── build | ├── webpack.base.ts # 公共配置 | ├── webpack.dev.ts # 开发环境配置 | └── webpack.prod.ts # 打包环境配置 ├── public │ └── index.html # html模板 ├── src | ├── App.tsx | ├── App.css │ └── index.tsx # react应用入口页面 └── 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>
3、引入React
安装依赖:
pnpm add react react-dom # 声明依赖 pnpm add @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.getElementById('root'); 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
4、引入TypeScript
为什么要使用 typescript
?
当使用 JavaScript
编写大型项目时,可能会面临许多挑战,例如代码管理,可扩展性,协作和维护等。TypeScript
则是一种解决这些挑战的工具,它有如下的一些优点:
- 更好的代码质量:
TypeScript
的静态类型系统可以帮助开发人员在编写代码时捕获错误,这可以提高代码质量和稳定性。 - 更好的可读性和可维护性:
TypeScript
的静态类型系统和类可以提高代码的可读性和可维护性,特别是在大型项目中。这可以使代码更易于理解和修改。 - 更好的IDE支持:
TypeScript
具有出色的IDE支持,包括自动完成,语法突出显示和类型检查。这可以提高开发人员的生产力和准确性。 - 更好的可扩展性:
TypeScript
支持面向对象编程,可以帮助开发人员创建复杂的数据类型和接口,并使代码更易于扩展和维护。 - 更好的协作:
TypeScript
可以帮助开发团队更好地协作,因为代码的结构和类型是显式声明的。这可以减少在协作开发中出现的潜在问题和错误。 - 更好的性能:由于
TypeScript
可以在编译时捕获错误,因此可以减少运行时错误并提高性能。
总的来说,
TypeScript
可以提高代码质量,可读性,可维护性,可扩展性和协作,并且可以提高性能。这些优点使得TypeScript
成为编写大型项目的优秀选择。
接下来,我们在项目中引入 typescript
,先安装依赖:
pnpm add typescript -D pnpm add babel-loader ts-node @babel/core @babel/preset-react @babel/preset-typescript @babel/preset-env core
- 由于
webpack
默认只能识别js
文件,不能识别jsx
语法,需要配置loader
的预设预设@babel/preset-typescript
来先将ts
语法转换为js
语法,再借助预设@babel/preset-react
来识别jsx
语法。ts-node
:编译ts
文件,它可以立即编译并执行指定的TypeScript
文件,因此不需要单独的编译步骤。babel-loader
: 使用babel
加载最新js
代码并将其转换为ES5
(上面已经安装过)@babel/corer
:babel
编译的核心包@babel/preset-env
:babel
编译的预设,可以转换目前最新的js
标准语法core-js
: 使用低版本js
语法模拟高版本的库,也就是垫片现在
js
不断新增很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。需要把最新的标准语法转换为低版本语法,把非标准语法转换为标准语法才能让浏览器识别解析,而babel
就是来做这件事的,这里只讲配置,更详细的可以看Babel 那些事儿。
初始化tsconfig.json
:
./node_modules/typescript/bin/tsc --init # 如果全局安装了typescript,也可以通过下面的命令创建 tsc --init
就会在根目录生成一个tsconfig.json
文件:
{ "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "commonjs", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outDir": "./", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ } }
5、webpack配置
既然都使用了
typescript
,那就尽可能都使用ts
编写,所以,webpack
配置文件,我们也将会用ts来写~
安装依赖:
pnpm add webpack webpack-cli -D
5.1 webpack.base.ts
配置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: [] };
PS:path.resolve 与 path.join 的区别
将会出现以下问题:
需要我们安装 @types/node
这个依赖:
pnpm add @types/node -D
错误消失~ 错误虽然消失了,但是这个库是干嘛用的呢?看官方npm
包的介绍:
这是由于typescript自身的机制,需要一份xx.d.ts
声明文件,来说明模块对外公开的方法和属性的类型以及内容。对于内建模块,安装一个@types/node
模块可以整体解决模块的声明文件问题。
让我们回到TypeScript
的基本理念。TypeScript
希望所有全局使用的代码都是类型化的,当你的项目有一个合理的配置时,它对你自己的代码也是如此。TypeScript
库本身只包含TypeScript
包的代码类型。你可以为一个库编写自己的类型,但这几乎是不需要的 —— 因为TypeScript
社区已经为我们做了这个工作。
与npm
一样,TypeScript
世界也在庆祝开源代码。社区很活跃,不断对常用的npm
包的更新和变化做出反应。你几乎总能找到npm包的类型,所以你不必单独为你的成千上万的依赖创建类型。
通常,现有软件包的类型可以从npm
内部的 @types
组织中找到,你可以通过安装一个带有@types/
前缀的软件包名称的npm
包将相关类型添加到你的项目中。比如说npm install --save-dev @types/react @types/express @types/lodash @types/jest @types/mongoose
等等,等等。 @types/*
由Definitely typed维护,这是一个社区项目,目的是在一个地方维护所有的类型。
有时,一个npm
包也可以在代码中包含它的类型,在这种情况下,安装相应的 @types/*
就没有必要。
- 由于类型只在编译前使用,所以在生产构建中不需要类型,它们应该放在
package.json
的devDependencies
中。- 比如后面我们会用到的全局变量:
process
,是由Node本身定义的,我们从包@types/node
中获得其类型。- 从
10.0
版本开始,ts-node
已经将@types/node
定义为一个对等依赖。如果你使用的是npm,npm的版本至少是7.0,那么一个项目的对等依赖就会自动被npm安装。如果你有一个更老的npm,同行依赖必须明确安装。关于TypeScript的类型声明,可以阅读这位同学写的入门指南:TypeScript类型声明完全指南
另外因为我们在App.tsx
中引入了css
文件,所以还需要安装相关的loader
:
pnpm add style-loader css-loader -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,再处理jsx 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"], }, // plugins plugins: [ new HtmlWebpackPlugin({ // 复制 'index.html' 文件,并自动引入打包输出的所有资源(js/css) template: path.join(__dirname, "../public/index.html"), // 压缩html资源 minify: { collapseWhitespace: true, //去空格 removeComments: true, // 去注释 }, }), ], };
因为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" }, // ... ], }, // ...
5.2 webpack.dev.ts
接下来,我们需要通过webpack-dev-server
来启动我们的项目,所以需要安装相关的依赖:
pnpm add webpack-dev-server html-webpack-plugin 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-dev-server"; 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": "*" }, }, }); export default devConfig;
开发环境推荐:eval-cheap-module-source-map
- 本地开发首次打包慢点没关系,因为 eval 缓存的原因,热更新会很快
- 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
- 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
然后再 package.json
中添加启动脚本:
"scripts": { "dev": "webpack serve -c build/webpack.dev.ts" },
正当我们准备启动项目的时候,发现还有一个错误:
只需要在tsconfig.json
中加入一行"jsx": "react-jsx"
即可:
{ "compilerOptions": { "target": "es2016", "esModuleInterop": true, "module": "commonjs", "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "jsx": "react-jsx" // 这里改成react-jsx,就不需要在tsx文件中手动引入React了 }, "include": ["./src"] }
回到App.tsx
,可以发现React
的import
变灰了:
从React v17
开始,我们就不需要再显式import React from 'react'
了。
运行 pnpm run dev
脚本启动项目,就可以看到页面跑出来了!
5.3 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;
打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none')
none
话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。- 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在
在package.json
中添加:
"scripts": { // ... "build": "webpack -c build/webpack.prod.ts" },
运行pnpm run build
,如果想看打包结果,可以通过一个小工具来查看:
# 如果之前使用npm,最简单的方法就是使用如下命令 npm i serve -g # 如果是首次使用pnpm安装全局依赖,通过如下命令 pnpm setup source ~/.zshrc pnpm add serve -g
然后通过serve -S dist
命令,启动一个服务来查看打包结果,如果不出意外,打开控制台启动的服务,就能看到页面了!
5.4 copy 静态资源
一般public
文件夹都会放一些静态资源,可以直接根据绝对路径引入,比如图片、css
、js
文件等,不需要webpack
进行解析,只需要打包的时候把public
下内容复制到构建出口文件夹中,可以借助 copy-webpack-plugin 插件,安装依赖:
pnpm add 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样式元素和样式属性 }, nodeModules: path.resolve(__dirname, "../node_modules"), }), 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), }), ], }; 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
图标文件(找不到的去我 GitHub repo download),在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:dev
打包,就可以看到public
下的favicon.ico
图标文件被复制到dist
文件中了。
有同学可能会遇到favicon
不显示的问题,提供以下思路进行处理:
- 在head添加了
favicon
没生效,但是新开标签页直接访问图片可以访问到
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
- 改图片尺寸格式依然没显示,直接复制其他网站正常
favaicon.ico
,排除格式的原因。 - 语法问题,尝试各种写法。
<link rel="icon" href="/favicon.ico""> <link rel="shortcut" href="/favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
- 添加版本号,favicon正常显示,排除语法原因。
<link rel="shortcut icon" href="/favicon.ico?v=1.0" type="image/x-icon">
- 继续深入查找问题,
F12
查看html
和network
,发现favicon.ico
的请求没有。 - 没有
favicon
的请求,不会显示图片,怀疑是浏览器缓存问题,清除缓存后依然没有请求。 - 打开其它浏览器,发现
favicon
正常显示,进一步确定是浏览器缓存。 - 关闭标签页,重新打开浏览器,最后
favicon.ico
正常显示。 chrome
浏览器,前端资源经常会产生缓存问题,清空或禁用缓存也不一定有效。可以尝试重启浏览器,或者重启电脑。
6、配置环境变量
6.1 corss-env + DefinePlugin
环境变量按作用分为两种:
- 区分是开发模式还是打包构建模式
- 区分项目业务环境,开发/测试/预测/正式环境
区分开发模式还是打包构建模式可以用
process.env.NODE_ENV
,因为很多第三方包里面判断都是采用的这个环境变量。区分项目接口环境可以自定义一个环境变量
process.env.BASE_ENV
,设置环境变量可以借助 cross-env 和 webpack.DefinePlugin 来设置。
cross-env
:运行跨平台设置和使用环境变量的脚本,兼容各系统的设置环境变量的包webpack.DefinePlugin
:webpack
内置的插件,可以为业务代码注入环境变量
cross-env
解决的问题:当您使用NODE_ENV =production
,来设置环境变量时,大多数Windows
命令提示将会阻塞(报错)。 (异常是Windows
上的Bash
,它使用本机Bash
。)同样,Windows
和POSIX
命令如何使用环境变量也有区别。 使用POSIX
,您可以使用:$ ENV_VAR
和使用%ENV_VAR%
的Windows
。
安装 cross-env
:
shell
复制代码
pnpm add 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" },
process.env.NODE_ENV
环境变量webpack
会自动根据设置的mode
字段来给业务代码注入对应的development
和prodction
,这里在命令中再次设置环境变量NODE_ENV
是为了在webpack
和babel
的配置文件中访问到。
在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
const webpack = require('webpack') module.export = { // ... plugins: [ // ... new webpack.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", "esModuleInterop": true, "module": "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
打印一下两个环境变量:
需要注意的是,业务环境要能访问
process
,需要安装:pnpm add @types/node -D
// 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
当前是开发模式,业务环境是测试环境。
6.2 配置多环境运行配置
安装依赖:
pnpm add 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), };
业务代码中使用:
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
,就可以在控制台
还可以验证一下环境配置是否正确,启动打包:pnpm run build:prod
,通过serve -s dist
,启动项目:
多运行环境配置成功!
7、文件别名
先在webpack.base.ts
中配置:
resolve: { extensions: [".ts", ".tsx", ".js", ".jsx", ".less", ".css"], // 别名需要配置两个地方,这里和 tsconfig.json alias: { "@": path.join(__dirname, "../src") }, modules: [path.resolve(__dirname, "../node_modules")], // 查找第三方模块只在本项目的node_modules中查找 },
然后还需要在tsconfig.json
中配置:
{ "compilerOptions": { // ... "baseUrl": ".", "paths": { "@/*": ["src/*"] }, }, }
然后就可以在项目中使用了~
import '@/App.css' function App() { return <h2>webpack5-react-ts</h2> } export default App
8、重启项目时在同一个浏览器Tab中打开页面
我们发现,每次运行pnpm run dev:*
命令都会在当前浏览器打开新的Tab
,虽然也不影响项目开发,但是很影响开发体验,可以参考Create-React-App
的处理方式:
参考:create-react-app 的启动方式
复制出这两个文件源码,将其放置在build
下的util
中:
build - util - openBrowser.js - openChrome.applescript
修改webpack.dev.ts
:
import path from "path"; import { merge } from "webpack-merge"; import webpack, { Configuration as WebpackConfiguration } from "webpack"; import WebpackDevServer from "webpack-dev-server"; import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server"; import baseConfig from "./webpack.base"; // 运行命令的时候重启一次打开一个tab 页很烦,所以呢优化一下 // 参考:create-react-app 的启动方式 // https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openChrome.applescript // 记得关闭webpack-dev-server的配置中的自动打开 open: false 或者注释 const openBrowser = require("./util/openBrowser"); interface Configuration extends WebpackConfiguration { devServer?: WebpackDevServerConfiguration; } const host = "127.0.0.1"; const port = "8082"; // 合并公共配置,并添加开发环境配置 const devConfig: Configuration = merge(baseConfig, { mode: "development", // 开发模式,打包更加快速,省了代码优化步骤 /** 开发环境推荐:eval-cheap-module-source-map - 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快 - 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap - 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module */ devtool: "eval-cheap-module-source-map", }); const devServer = new WebpackDevServer( { host, // 地址 port, // 端口 open: false, // 是否自动打开,关闭 setupExitSignals: true, // 允许在 SIGINT 和 SIGTERM 信号时关闭开发服务器和退出进程。 compress: false, // gzip压缩,开发环境不开启,提升热更新速度 hot: true, // 开启热更新,后面会讲react模块热替换具体配置 historyApiFallback: true, // 解决history路由404问题 static: { directory: path.join(__dirname, "../public"), // 托管静态资源public文件夹 }, headers: { "Access-Control-Allow-Origin": "*" }, }, webpack(devConfig) ); devServer.start().then(() => { // 启动界面 openBrowser(`http://${host}:${port}`); }); export default devConfig;
重启项目,搞定~
项目的初始化就完成了,接下来,第二篇会引入一些基本的配置,敬请期待~