持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
概述
本文旨在通过从0到1搭建一套完整的React开发框架来掌握如Webpack
、React
、Typescript
、 CSS preprocessor
等各部分是如何协同进行编译开发的,进而能去了解如creat-react-app
之类的框架都做了哪些事情。
同时本文还提供了业务层面的如路由react-router
、状态管理器redux-toolkit
的配置和使用方式。
TIPS: 本文中所有包版本均采用当前最新版本,注意区别于如webpack、react、react-router等模块的旧版本使用方式。
一、初始化项目
yarn init
yarn init或npm init都是OK的
二、构建核心打包环境
-
添加依赖
yarn add webpack webpack-cli webpack-dev-server -D
-
依赖模块说明
模块名 描述 版本 webpack 模块化打包工具,打包代码时的核心依赖 ^5.72.1 webpack-cli 支持在命令行中执行webpack的工具 ^4.9.2 webpack-dev-server 本地开发服务器,便于开发 ^4.9.0 webpack-merge 用于合并webpack-config文件 ^5.8.0 -
webpack.config.js 配置文件
webpack.config.js是webpack的默认配置文件,也可以在命令行中通过--config指定配置文件。
一般在项目中会区分production、development环境的配置文件,这里我们设计配置文件为:公共配置文件,开发环境配置文件,产品环境配置文件,目录结构如下
config -- webpack.config.base.js -- webpack.config.dev.js -- webpack.config.prod.js
webpack.config.base.js 公共配置文件
const path = require("path"); /** * @method resolve * @description 从根路径开始查找文件 */ const resolve = (targetPath) => { return path.resolve(__dirname, "..", targetPath); }; module.exports = { target: "web", // 入口文件 entry: { main: resolve("./src/index.js"), }, // 输出 output: { // 文件名称 filename: "[name].[contenthash].js", // 输出目录 path: resolve("./dist"), // 每次编译输出的时候,清空dist目录 - 这里就不需要clean-webpack-plugin了 clean: true, // 所有URL访问的前缀路径 publicPath: "/", }, resolve: { // 定义了扩展名之后,在import文件时就可以不用写后缀名了,会按循序依次查找 extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".css", ".less"], // 设置链接 alias: { // 注意resolve方法开始的查找的路径是/ "@": resolve("./src"), }, }, };
webpack.config.dev.js 开发环境配置文件
// merge,合并两个或多个webpack配置文件 const { merge } = require("webpack-merge"); // 导入公共配置文件 const webpackConfigBase = require("./webpack.config.base"); // dev环境下相关配置 module.exports = merge(webpackConfigBase, { // 指定环境 mode: "development", // 输出source-map的方式,增加调试。eval是默认推荐的选择,build fast and rebuild fast! devtool: "eval", // 本地服务器配置 devServer: { // 启动GZIP压缩 compress: true, // 设置端口号 port: 3000, // 代理请求设置 proxy: { "/api": { // 目标域名 target: "http://xxxx.com:8080", // 允许跨域了 changeOrigin: true, // 重写路径 - 根据自己的实际需要处理,不需要直接忽略该项设置即可 pathRewrite: { // 该处理是代码中使用/api开头的请求,如/api/userinfo,实际转发对应服务器的路径是/userinfo "^/api": "", }, // https服务的地址,忽略证书相关 secure: false, }, }, }, });
weback.config.build.js 线上产品环境配置文件
const { merge } = require("webpack-merge"); const webpackConfigBase = require("./webpack.config.base"); module.exports = merge(webpackConfigBase, { // 指定打包环境 mode: "production", });
-
package.json命令行
在package.json的scripts中添加一下命令:
scripts: { "dev": "webpack serve --config config/webpack.config.dev", "build": "webpack build --config config/webpack.config.prod" }
开发环境运行:
yarn dev
生产环境运行:yarn build
三、添加HTML模板文件
-
添加依赖
yarn add html-webpack-plugin -D
-
创建模板文件
目录结构如下
public -- index.html -- logo.ico
-
更新webpack配置
在webpack.config.base.js中添加如下配置:
const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // ... plugins: [ new HtmlWebpackPlugin({ // HTML模板文件 template: resolve("./public/index.html"), // 收藏夹图标 favicon: resolve("./public/logo.ico"), }), ] // ... }
四、解析React
-
添加依赖
# 添加react、react-dom yarn add react react-dom
# 添加babel等loader yarn add babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react core-js@3 -D
-
依赖模块说明
模块名 描述 版本 react 还用说是啥吗? 核心代码 ^18.1.0 react-dom 还用说是啥吗? 浏览器端实现 ^18.1.0 babel-loader 识别ES6语法,编译js ^8.2.5 @babel/core babel处理的核心逻辑 ^7.18.2 @babel/preset-env 根据一些预设的目标值转换js语法,会打包一些polyfill ^7.18.2 @babel/preset-react 帮助识别jsx语法,解析react ^7.17.12 @babel/plugin-transform-runtime 优化解决preset-env打包的polyfill会污染全局的问题 ^7.18.2 core-js polyfill的核心实现,选择3版本 3 -
添加webpack的loader配置
loader是webpack的文件处理器,让webpack能够处理其他类型的文件,并转为有效的模块,以供程序使用,以及被添加到依赖图中。
webpack.config.base.js文件
module.exports = { // ... module: { rules: [ { // 匹配js/jsx test: /\.jsx?$/, // 排除node_modules exclude: /node_modules/, use: { // 确定使用的loader loader: "babel-loader", // 参数配置 options: { presets: [ [ // 预设polyfill "@babel/preset-env", { // polyfill 只加载使用的部分 useBuiltIns: "usage", // 使用corejs解析,模块化 corejs: "3", }, ], // 解析react "@babel/preset-react", ], // 使用transform-runtime,避免全局污染,注入helper plugins: ["@babel/plugin-transform-runtime"], }, }, } ] } // ... }
-
创建React组件
创建App.jsx文件
import React, { useState } from "react"; export default function App () { return <div className="app"> <h1>Hello Webpack-React</h1> </div>; }
入口文件src/index.js
import React from "react"; // 注意这里最新版的ReactDOM是从client中导出的 import ReactDOM from "react-dom/client"; // 因为设置了extensions,所以可以不加扩展名 import App from './App'; // 创建app根节点 const appEl = document.createElement("div"); // 设置id appEl.id = "app"; // 追加节点到body中 document.body.appendChild(appEl); // 最新版本使用的是ReactDOM.createRoot // 如果使用ReactDOM.render()控制台会报warnning错误 const root = ReactDOM.createRoot(appEl); // 渲染 root.render(<App />);
页面效果图:
效果都不用说,肯定是
五、解析CSS以及CSS预处理器
CSS在webpack中也是作为一个资源被识别的,需要配置相关的loader。CSS预处理器如less/sass/stylus/postcss都可以被相关的loader识别。
-
添加依赖
yarn add css-loader less less-loader style-loader postcss postcss-loader mini-css-extract-plugin cross-env autoprefixer css-minimizer-webpack-plugin -D
-
依赖模块说明
模块名 描述 版本 css-loader 解析css ^6.7.1 less 支持less语法 ^4.1.2 less-loader 解析less ^11.0.0 style-loader 将解析的css内容追加到head中 ^3.3.1 postcss 好用好玩的css插件,压缩、自动补全 ^8.4.14 postcss-loader 解析postcss设置 ^7.0.0 mini-css-extract-plugin 分离CSS ^2.6.0 cross-env 好用的环境配置 ^7.0.3 autoprefixer 自动补全css属性前缀 ^10.4.7 css-minimizer-webpack-plugin 生产环境,压缩css ^4.0.0 -
webpack相关配置
webpack.config.base.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const isProd = process.env.NODE_ENV === "prod"; module.exports = { // ... plugins: [ new MiniCssExtractPlugin({ // 输出的每个css文件名称 filename: isProd ? "[name].[contenthash].css" : "[name].css", // 非入口的chunk文件名 - 通过import()加载异步组件中样式 chunkFilename: isProd ? "[id].[contenthash].css" : "[id].css", }), ], module: { rules: [ { test: /\.(css|less)$/, use: [ // 生产环境下直接分离打包css isProd ? MiniCssExtractPlugin.loader : "style-loader", { loader: "css-loader", }, "less-loader", { loader: "postcss-loader", options: { postcssOptions: { // 浏览器前缀自动补全 plugins: ["autoprefixer"], }, }, }, ], }, ] } // ... }
在webpack的loader中,加载顺序是从右向左依次处理,css/less文件的处理顺序是:postcss-loader -> less-loader -> css-loader -> (style-loader | MiniCssExtractPlugin.loader)
webpack.config.prod.js
在生产环境中,将打包的css文件进行压缩
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = merge(webpackConfigBase, { // ... optimization: { minimizer: [ new CssMinimizerPlugin() ] } })
-
package.json更新scripts,增加环境变量
通过cross-env配置环境变量参数,跨平台好用!
"scripts": { "dev": "cross-env NODE_ENV=dev webpack serve --config config/webpack.config.dev", "build": "cross-env NODE_ENV=prod webpack build --config config/webpack.config.prod" },
传递参数NODE_ENV,在webpack的配置文件中可以使用
process.env.NODE_ENV
获取值。判断是否是生产环境
const isProd = process.env.NODE_ENV === 'prod';
六、解析图片、JSON等其他文件
图片文件也是我们经常使用的资源,在webpack5之前,一般都使用raw-loader、url-loader、file-loader来进行处理,webpack5对此进行了一些升级,提供了一种资源模块类型(asset module type)
,替换了这些loader。
资源模块类型 | 描述 |
---|---|
asset/resource | 发送一个单独的文件,并导出URL,替代file-loader |
asset/inline | 导出一个资源的data URI,替代url-loader |
asset/source | 导出资源源代码 |
asset | 相当于自动选择asset/resource或asset/inline,替换url-loader中的limit |
-
webpack.config.base.js添加配置
module.exports { // ... output: { // ... // 指定asset或asset/resource类型文件存储时的命名规则 // 注意这里设置的是所有的公共的命名处理逻辑 assetModuleFilename: 'image/[hash][ext][query]' // ... }, module: { rules: [ { // 匹配图片文件 test: /\.(png|jpg|jpeg|gif)$/i, // 设置资源处理的类型为asset type: "asset", parser: { // 转为inline dataUrl的条件 dataUrlCondition: { // 默认限制为8kb,现在调整限制为10kb,大文件直接作为asset/resource类型文件输出 maxSize: 10 * 1024, }, }, }, { // 匹配json文件 test: /\.json$/, // 将json文件视为文件类型 type: "asset/resource", // 路径中包含animations的 include: /animations/, generator: { // 这里专门针对json文件的处理 filename: "static/[name].[hash][ext][query]", }, } ] } // ... }
-
组件中图片文件引入
// 引入的rocket会被webpack对应的的rule解析规则转为data URL或者是图片地址路径 import rocket from './assets/rocket.gif'; export default function App () { return <div> {/* 将变量rocket赋值给src */} <img src={rocket} alt="火箭"> </div> }
-
组件中json文件引入
默认情况,在组件中引入json文件会被视作JS资源,返回的是一个JSON对象,可供直接使用的。在webpack中配置处理json文件的规则,将应用于路径中包含
animations
的json文件上,因为类型type=asset/resource
,所以返回的是json文件的字符串地址。// 这里是常规的JSON对象 import kittyLoadingJsonObject from './assets/kitty-loading.json'; // 这里返回的是路径 import kittyLoadingPath from './animations/kitty-loading.json'; console.log(kittyLoadingJsonObject); // JSON对象 console.log(kittyLoadingPath); // /static/kitty-loading.32e5eb3647c98eaa8045.json
在这里举个实际应用的例子,在我的上一篇文章《都2022年了,一个还不知道Lottie动画的前端已经OUT啦!》中,配置
lottie
动画的json文件有两种方式,一种是path
,一种是animationData
。import lottie from 'lottie-web'; // 这里是常规的JSON对象 import kittyLoadingJsonObject from './assets/kitty-loading.json'; // 这里返回的是路径 import kittyLoadingPath from './animations/kitty-loading.json'; // 两种不同的调用方式 // path调用 lottie.loadAnimation({ // 这里是渲染动画的容器 container: animationEle.current, loop: true, autoplay: true, renderer: 'svg', // kittyLoadingPath返回的是打包后,可以访问的http路径 path: kittyLoadingPath }) // animationData调用 lottie.loadAnimation({ // 这里是渲染动画的容器 container: animationEle.current, loop: true, autoplay: true, renderer: 'svg', // 这里配置JSON Object animationData: kittyLoadingJsonObject })
七、解析TypeScript
TS是前端开发中的一大“利器“,不用纠结要不上TypeScript,直接干就完了,使用TS的快乐你想象不到!
TypeScript是JavaScript类型的超集,可以编译成纯JavaScript。
-
添加依赖
yarn add typescript @babel/perset-typescript @types/react @types/react-dom -D
我们还是使用babel-loader来编译ts,所以就不用安装ts-loader了
-
依赖模块说明
模块名 描述 版本 typescript 你说这是啥 ^4.7.2 @babel/preset-typescript Babel解析typescript语法 ^7.17.12 @types/react react类型文件 ^18.0.9 @types/react-dom react-dom类型文件 ^18.0.5 -
webpack.config.base.js文件更新
配置解析
.ts
、.tsx
文件的loadermodule.exports = { // ... { // 匹配ts/js/tsx/jsx文件 test: /\.(ts|js)x?$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: "3", }, ], // 解析typescript语法 "@babel/preset-typescript", "@babel/preset-react", ], plugins: ["@babel/plugin-transform-runtime"], }, }, }, // ... }
-
配置tsconfig.json文件
tsconfig.json文件是提供给TypeScript的解析规则,简单配置如下,可以根据自己项目需要自行调整。完整规则地址传送门
// tsconfig.json { "compilerOptions": { // 重定向输出目录 "outDir": "./dist", // 在表达式和声明上有隐含的any类型时报错,false表示关闭该规则 "noImplicitAny": false, // 指定生成哪个模块系统下的代码 "module": "ESNext", // 指定ECMAScript目标版本 "target": "ESNext", // 在.tsx文件里支持react "jsx": "react", // 允许编辑JS文件 "allowJs": true, // 检查JS文件中的错误并报告,需要allowJs配合使用 "checkJs": true, // 模块解析,查找模块的方式 "moduleResolution": "node", // 允许从没有默认导出的模块中默认导入 "allowSyntheticDefaultImports": true, // "esModuleInterop": true, "resolveJsonModule": true, // 模块名基于baseUrl的路径映射列表 "paths": { // 与webpack中声明的@对应,若不声明会导致报查询不到对应模块的错误 "@": ["./src"], // 这个位置与上面区分下,不是一个意思,表示/*表示src下的所有文件 "@/*": ["./src/*"] } }, // 包含编译目录 "include": ["src/*", "images.d.ts"], // 排除编译目录 "exclude": ["node_modules", "dist"] }
-
ts中图片等文件类型声明
在项目中我们引入图片文件
import smileGif from './smile.gif';
时,vscode会提示ts 2307 找不到模块“./smile.gif”或其相应的类型声明。
,配置images.d.ts
文件解决这个问题。declare module "*.css"; declare module "*.less"; declare module "*.png"; declare module "*.jpg"; declare module "*.gif"; declare module "*.txt"; declare module "*.svg" { export function ReactComponent( props: React.SVGProps<SVGSVGElement> ): React.ReactElement; const url: string; export default url; }
-
文件名调整
从现在开始所有的
.jsx
文件调整为.tsx
文件,如果tsconfig.json文件声明中没有配置allowJs: true
,所有的.js
文件都要调整成.ts
扩展名,否则不被编译。
从现在开始就开心的使用TypeScript来开发吧,TS的快乐,谁用谁知道~