webpack(v5.7)+React(v18.0)+react-router(v6.3)+Mobx(v6.5)+TS(v4.6)从零开始构建
万丈高楼平地起
先找块地,挖个坑
- 进入项目根目录,执行
npm init -y
生成初始的package.json文件,并将其配置项的入口main
删除,添加private:true
。 - 执行
yarn add -D webpack webpack-cli webpack-merge
安装webpack相关的东东。 - 在根目录下创建
.gitignore
文件,其内用来忽略一些我们不需要提交到远程的目录或文件。
webpack的配置
坑已挖好,开始打地基
坑里边打钢筋柱子
- 在根目录下创建
build
目录,用来存放我们后期用于打包的一些配置文件。 - 在
build
目录下创建webpack.config.js
文件,写入基本的配置,如下: - 在根目录下创建
src
目录,在其内创建index.html模板文件。同时创建index.tsx入口文件
const path = require('path');
module.exports = {
entry: {
app: path.join(__dirname, '../src/index.tsx')
},
output: {
path: path.join(__dirname, '../dist'),
filename: '[name].[chunkhash:8].bundle.js',
// publicPath: "/",
chunkFilename: 'chunk/[name].[chunkhash:8].js',
},
resolve: {
// 解析这些文件
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
},
}
配置babel-loader解析resolve.extensions中的多种类型文件
- 安装
yarn add -D @babel/core bable-loader @babel/preset-env
- Babel 的核心功能包含在 @babel/core 模块中,必须安装。
插件和预设
- 代码转换功能以插件的形式出现,插件是小型的 JavaScript 程序,用于指导 Babel 如何对代码进行转换。
@babel/preset-env
可以让你使用最新的javaScript。@babel/preset-react
用来转换JSX片段。bable-loader
使用 Babel 加载 ES2015+ 代码并将其转换为 ES5。babel
会在每个文件都插入辅助代码,这就会使得代码体积过大。可以使用Babel runtime
作为一个独立模块,来避免重复引入一些不需要的代码。你必须执行yarn add -D @babel/plugin-transform-runtime
来把它包含到你的项目中,然后使用yarn add @babel/runtime
把@babel/runtime
安装为一个依赖。
module.exports = {
module: {
// 将缺失的导出提示成错误而不是警告
strictExportPresence: true,
// loaders
rules: [{
test: /\.(js|mjs|jsx)$/,
include: path.join(__dirname, 'src'),
use: {
// 当有设置cacheDirectory时,指定的目录将用来缓存 loader 的执行结果。
loader: 'babel-loader?cacheDirectory',
options: {
presets: ['@babel/preset-env', "@babel/preset-react"],
// 'transform-runtime' 插件告诉 Babel,要引用runtime来代替注入
// 引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。
plugins: ['@babel/plugin-transform-runtime'],
}
}
},{
test: /\.(ts|tsx)$/,
include: path.join(__dirname, 'src'),
use: {
loader: 'ts-loader',
}
}]
},
}
添加TS支持
npm install --save typescript awesome-typescript-loader source-map-loader
awesome-typescript-loader
可以让Webpack使用TypeScript的标准配置文件 tsconfig.json编译TypeScript代码。- source-map-loader使用TypeScript输出的sourcemap文件来告诉webpack何时生成 自己的sourcemaps。方便我们调试。
- 在根目录下创建
tsconfig.json
文件。配置如下:
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
// 允许export=导出,由import from 导入
"esModuleInterop": true,
// 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
"allowSyntheticDefaultImports": true,
"removeComments": false, // 删除注释
"alwaysStrict": true, // 在代码中注入'use strict'
// 禁止对同一个文件的不一致的引用
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
"importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
"experimentalDecorators": true, // 启用实验性的ES装饰器。
"resolveJsonModule": true, //是否允许把json文件当做模块进行解析
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src",
"*.d.ts"//配置的.d.ts文件,用于定义一些 declare
],
}
添加符合ts的路径别名
- 在
build
目录下,创建alias.js
文件,用于书写我们使用到的路径别名。
const path = require('path')
module.exports = {
"@img": path.resolve(__dirname, '../src/assets/images'),
"@svg": path.resolve(__dirname, '../src/assets/svg'),
"@pages": path.resolve(__dirname, '../src/pages'),
"@store": path.resolve(__dirname, '../src/store'),
"@components": path.resolve(__dirname, '../src/components'),
"@routes": path.resolve(__dirname, '../src/routes'),
}
- 然后将其配置在webpack的resolve选项下的alias属性中。
- 接着,在项目根目录下创建
paths.json
文件,用于配置ts的路径别名。内容如下:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"*"
],
"@src/*": [
"src/*"
],
"@pages/*": [
"src/pages/*"
],
"@components/*": [
"src/components/*"
],
"@routes/*": [
"src/routes/*"
],
"@assets/*": [
"src/assets/*"
],
"@store/*": [
"src/store/*"
]
}
}
}
- 在
tsconfig.json
文件中使用extends
配置项继承上面的paths.json
文件。 - 大功告成。
注意: 后续如果要添加新的路径的别名的话,必须要在
alias.js
和paths.json
两个文件中分别添加!!!
配置html模板
- 安装
yarn add -D html-webpack-plugin
- 该插件将为你生成一个 HTML5 文件, 在 body 中使用
script
标签引入你所有 webpack 生成的 bundle。 只需添加该插件到你的 webpack 配置中。如下: - 在根目录下创建
src
目录,在其内创建index.html模板文件。同时创建index.tsx入口文件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: `${path.join(__dirname, "../src")}/index.html`
})
],
}
配置本地开发服务以及热更新
- 安装
yarn add -D webpack-dev-server
- 利用
webpack-dev-server
搭建本地的web server,并启用模块热更新
module.exports = {
mode: 'development',
devServer: {
client: {
// 当出现编译错误或警告时,在浏览器中显示全屏覆盖。
overlay: true,
// 在浏览器中以百分比显示编译进度。
progress: false,
// 告诉 dev-server 它应该尝试重新连接客户端的次数。当为 true 时,它将无限次尝试重新连接。
reconnect: true,
},
// 项目使用了react-router-dom的BrowserRouter,所以此项必须设置为true。否则会导致配置的路由404
// 也就是说,当使用h5的history API时,必须将 historyApiFallback设置为true。
historyApiFallback: true,
// 启用gzip 压缩
compress: true,
port: 9000,
// 启用webpack的模块热替换
hot: true,
// 告诉 dev-server 在服务器已经启动后打开默认的浏览器
open: true,
// 代理---处理跨域转发
proxy: {
}
},
}
- 在
package.json
文件的scripts
选项中添加"dev": "webpack-dev-server --config build/webpack.config.js"
- 接着我们执行
yarn dev
命令,在服务启动之后就可以看到会自动打开一个浏览器窗口,显示我们index.tsx文件中的内容。
配置样式loader
yarn add -D less less-loader style-loader css-loader postcss-loader postcss
- 再安装一些postcss需要的插件
yarn add -D postcss-flexbugs-fixes postcss-preset-env postcss-normalize
配置如下:(后面再做公共代码的提取)
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const lessRegex = /\.(less)$/;
const lessModuleRegex = /\.module\.(less)$/;
module.exports = {
// ...
module: {
// ...
rules: [
// ...
// 配置css-loader
{
test: cssRegex,
exclude: cssModuleRegex,
use: [
// style-loader将css插入到DOM中, 推荐将 style-loader 与 css-loader 一起使用
require.resolve('style-loader'),
// css-loader 会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样。
{
loader: 'css-loader',
options: {
// 启用/禁用@import规则进行处理,控制@import的解析,默认值为true
import: true,
// importLoaders 选项允许你配置在 css-loader 之前有多少 loader 应用于 @imported 资源与 CSS 模块/ICSS 导入。
importLoaders: 1,
// 启用/禁用css模块或者icss及其配置
modules: {
mode: 'icss',
// 允许配置生成的本地标识符(ident)
// 建议:开发环境使用 '[path][name]__[local]' 生产环境使用 '[hash:base64]'
localIdentName: "[path][name]__[local]--[hash:base64:5]",
// 允许为本地标识符名称重新定义基本的 loader 上下文。
localIdentContext: path.resolve(__dirname, "src"),
},
sourceMap: true,
},
},
// 自动添加css浏览器前缀
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
ident: 'postcss',
config: false,
plugins: [
'postcss-flexbugs-fixes', // 此插件用来修复flexbug的问题
[
'postcss-preset-env', // 包含了autoprefixer
{
autoprefixer: {
flexbox: 'no-2009',
},
// 规定按照哪个阶段的css来实现polyfill
stage: 3, // 默认启用阶段2的功能
},
],
'postcss-normalize', // PostCSS归一化,可让您使用formantize.css或Sanitize.css的各个部分。
]
},
sourceMap: true,
},
},
],
// 注意,所有导入文件都会受到 tree shaking 的影响。
// 这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,
// 则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
sideEffects: true,
},
// 处理.module.css文件
{
test: cssModuleRegex,
use: [
require.resolve('style-loader'),
{
loader: 'css-loader',
options: {
// 启用/禁用@import规则进行处理,控制@import的解析,默认值为true
import: true,
// importLoaders 选项允许你配置在 css-loader 之前有多少 loader 应用于 @imported 资源与 CSS 模块/ICSS 导入。
importLoaders: 1,
// 启用/禁用css模块或者icss及其配置
modules: {
mode: 'local',
// 允许配置生成的本地标识符(ident)
// 建议:开发环境使用 '[path][name]__[local]' 生产环境使用 '[hash:base64]'
localIdentName: "[path][name]__[local]--[hash:base64:5]",
// 允许为本地标识符名称重新定义基本的 loader 上下文。
localIdentContext: path.resolve(__dirname, "src"),
},
sourceMap: true,
},
},
// 自动添加css浏览器前缀
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
ident: 'postcss',
config: false,
plugins: [
'postcss-flexbugs-fixes', // 此插件用来修复flexbug的问题
[
'postcss-preset-env', // 包含了autoprefixer
{
autoprefixer: {
flexbox: 'no-2009',
},
// 规定按照哪个阶段的css来实现polyfill
stage: 3, // 默认启用阶段2的功能
},
],
'postcss-normalize', // PostCSS归一化,可让您使用formantize.css或Sanitize.css的各个部分。
]
},
sourceMap: true,
},
},
]
},
// 配置支持less
{
test: lessRegex,
exclude: lessModuleRegex,
use: [
// style-loader将css插入到DOM中, 推荐将 style-loader 与 css-loader 一起使用
require.resolve('style-loader'),
// css-loader 会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样。
{
loader: 'css-loader',
options: {
// 启用/禁用@import规则进行处理,控制@import的解析,默认值为true
import: true,
// importLoaders 选项允许你配置在 css-loader 之前有多少 loader 应用于 @imported 资源与 CSS 模块/ICSS 导入。
importLoaders: 3,
// 启用/禁用css模块或者icss及其配置
modules: {
mode: 'icss',
// 允许配置生成的本地标识符(ident)
// 建议:开发环境使用 '[path][name]__[local]' 生产环境使用 '[hash:base64]'
localIdentName: "[path][name]__[local]--[hash:base64:5]",
// 允许为本地标识符名称重新定义基本的 loader 上下文。
localIdentContext: path.resolve(__dirname, "src"),
},
sourceMap: true,
},
},
// 自动添加css浏览器前缀
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
ident: 'postcss',
config: false,
plugins: [
'postcss-flexbugs-fixes', // 此插件用来修复flexbug的问题
[
'postcss-preset-env', // 包含了autoprefixer
{
autoprefixer: {
flexbox: 'no-2009',
},
// 规定按照哪个阶段的css来实现polyfill
stage: 3, // 默认启用阶段2的功能
},
],
'postcss-normalize', // PostCSS归一化,可让您使用formantize.css或Sanitize.css的各个部分。
]
},
sourceMap: true,
},
},
'less-loader',
],
sideEffects: true,
},
// 支持.module.less文件
{
test: lessModuleRegex,
use: [
require.resolve('style-loader'),
{
loader: 'css-loader',
options: {
// 启用/禁用@import规则进行处理,控制@import的解析,默认值为true
import: true,
// importLoaders 选项允许你配置在 css-loader 之前有多少 loader 应用于 @imported 资源与 CSS 模块/ICSS 导入。
importLoaders: 3,
// 启用/禁用css模块或者icss及其配置
modules: {
mode: 'local',
// 允许配置生成的本地标识符(ident)
// 建议:开发环境使用 '[path][name]__[local]' 生产环境使用 '[hash:base64]'
localIdentName: "[path][name]__[local]--[hash:base64:5]",
// 允许为本地标识符名称重新定义基本的 loader 上下文。
localIdentContext: path.resolve(__dirname, "src"),
},
sourceMap: true,
},
},
// 自动添加css浏览器前缀
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
ident: 'postcss',
config: false,
plugins: [
'postcss-flexbugs-fixes', // 此插件用来修复flexbug的问题
[
'postcss-preset-env', // 包含了autoprefixer
{
autoprefixer: {
flexbox: 'no-2009',
},
// 规定按照哪个阶段的css来实现polyfill
stage: 3, // 默认启用阶段2的功能
},
],
'postcss-normalize', // PostCSS归一化,可让您使用formantize.css或Sanitize.css的各个部分。
]
},
sourceMap: true,
},
},
'less-loader'
],
}
]
}
}
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性
抽取css文件(MiniCssExtractPlugin)建议用于生产环境
- MiniCssExtractPlugin插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
- 本插件基于 webpack v5 的新特性构建,并且需要 webpack 5 才能正常工作。
- 与 extract-text-webpack-plugin 相比:
- 异步加载
- 没有重复的编译(性能)
- 更容易使用
- 特别针对 CSS 开发
- 建议
mini-css-extract-plugin
与css-loader
一起使用。
提取公共的样式loader处理
- 在
build
目录下新建rules
目录同时创建子文件styleRules.js
,用于存放webpack配置项module
中的一些rules
规则配置。 - 同时我们将之前的对js、ts等文件的处理loader也抽取到名为
jsRules.js
的文件中。
处理图片、字体文件等资源
- 资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
- 在 webpack 5 之前,通常使用:
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录
- 资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用file-loader
实现。asset/inline
导出一个资源的 data URI。之前通过使用url-loader
实现。asset/source
导出资源的源代码。之前通过使用raw-loader
实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现。 此外,当在 webpack 5 中使用旧的 assets loader(如file-loader
/url-loader
/raw-loader
等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为'javascript/auto'
来解决。 此处,我们使用type模块类型为asset,让其自动处理:
module: {
rules: [
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
},
{
test: /\.svg$/,
use: [
{
// @svgr/webpack允许将svg作为组件使用
loader: require.resolve('@svgr/webpack'),
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash].[ext]',
},
},
],
// 一个条件,用来与被发出的 request 对应的模块项匹配。
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
]
}
区分webpack打包环境
我们先将webpack分为开发环境、生产环境。
- 首先我们在build目录下创建
constants.js
文件用于存放一些我们经常用到的变量。
// 存放一些用到的常量
const IS_DEV = process.env.NODE_ENV === "development"
const APP_ENV = process.env.APP_ENV || "prod"
const FILE_EXTENSIONS = [".ts", ".tsx", ".js", "jsx", '.json']
module.exports = {
IS_DEV,
APP_ENV,
FILE_EXTENSIONS
}
yarn add -D webpack-merge
使用webpack-merge来对一些公共的配置和不同环境下的配置进行自动合并。- 在build目录下创建
webpack.dev.config.js
。 - 我们将之前写好的
webpack.config.js
修改其内部的代码,区分开发环境和生产环境的配置。 - 在
webpack.dev.config.js
中添加devServer选项。
打包优化
- 在build目录下创建
optimization.js
文件,用于书写打包优化的配置。 - webpack v5 自带了
terser-webpack-plugin
,如果想自定义配置,则仍需要安装该插件。 terser-webpack-plugin
插件使用terser来压缩js- 使用
css-minimizer-webpack-plugin
插件来压缩css,该插件内部使用了cssnano来优化和压缩css。 详情配置查看
代码规范配置
手摸手教你使用最新版husky(v7.0.1)让代码更优雅规范
配置antd库 按需引入
yarn add antd
并根据官方文档配置即可
配置react路由管理
yarn add react-router-dom
此时会安装v6.3.0版本。import { Outlet,useParams,NavLink, useSearchParams, useLocation } from "react-router-dom"
Outlet保证了路由切换时,父级路由页面的内容会一直存在。<Route path="*"/>
仅在路由不存在时才会匹配该路由。useParams
用来获取路由上的参数。NavLink
用来改变路由按钮激活时的颜色。查看useSearchParams
用来获取和设置路由上的?后边的参数。const [searchParams, setSearchParams] = useSearchParams();
查看useLocation
自定义路由行为,该钩子返回一路由数据的对象。具体查看 具体的react-router-dom v6版本的详细介绍 请看 或者 查阅官方文档
配置全局的store
yarn add mobx mobx-react-lite
- 在src目录下创建store目录,其内创建index.tsx文件。内容如下:
import User from './modules/user'
export default {
userStore: new User(),
}
- 在store目录下新建modules目录,其下用于存放各个模块的单独store,最终在index.tsx文件中统一导出。
- 例如:modules下的user.tsx文件用来创建我们上述代码中的User类,最终实例化得到了userStore。代码如下:
import { makeAutoObservable } from 'mobx'
export default class User {
/**
* this上绑定的变量需要在此处声明,否则ts检查会报错
*/
state: { token: string }
constructor() {
/**
* state
*/
this.state = {
token: 'aaa',
}
// 自动将已经存在的对象属性并且使得它们可观察
makeAutoObservable(this)
}
/**
* computed
*/
get isLogin() {
return this.state.token;
}
/**
* action
*/
setToken(val) {
this.state.token = val || ''
}
}
- 接着在src目录下,新建contexts目录,其内用于存放一些需要数据共享的Context对象。此时我们先创建一个
storeContext.tsx
文件用来将store数据创建成Context对象,利用Context对象的Provider最终将全局的store传递到各个组件中去。 storeContext.tsx
代码如下:
import React from "react";
import store from '@store/index';
// 使用react的context向下级传递数据,从而共享全局的可观察数据
const StoreContext = React.createContext(store);
export default StoreContext;
- 此时已经有了全局共享的Context对象了,那么在hooks中如何使用呢?
- 紧接着,我们在src目录下创建hooks目录,用来存放一些自定义的hook。此时我们先创建一个名为storeHook.tsx的文件,用来处理store,代码如下:
/**
* 创建store状态管理的hook
*/
import { useContext } from 'react'
// observer该方法也不可以不在此处导出
import { observer } from 'mobx-react-lite'
import StoreContext from '../contexts/storeContext'
function useStore() {
const store = useContext(StoreContext)
return store;
}
export {
observer,
useStore
}
- 到这一步,已经是万事具备了!!!只需要我们在入口文件下注入即可。代码如下:
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
// 重置浏览器样式
import "normalize.css";
// 引入文件
import App from "./App";
import store from '@store/index'
import StoreContext from './contexts/storeContext';
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<StoreContext.Provider value={store} >
<App />
</StoreContext.Provider>
</BrowserRouter>
);
- 验证:在子组件中这么用
import { useRoutes } from "react-router-dom";
import routes from "@router/index";
import { useStore } from '@hooks/storeHook'
import { toJS } from 'mobx'
function App() {
const { userStore } = useStore();
userStore.setToken('ttt')
console.log(toJS(userStore), 'store-=-=-=', userStore)
return useRoutes(routes);
}
export default App;
console的输出如下图所示:
可以看到,userStore中的state对象下的token变量已经从'aaa'变成了'ttt'。
二次封装axios
mock数据
npm install -g json-server
- 然后在你需要的任意位置创建一个名为
db.json
的文件,用来存放mock的数据。 - 比如我们在项目根目录下创建
__json_server_mock__
文件夹,其内创建db.json
文件。 - 在
package.json
文件的scripts
选项下新增该命令:"json-server": "json-server --watch __json_server_mock__/db.json"
- 然后我们执行
yarn json-server
启动json-server,会在控制台看到如下所示:
- 可以看到我们的资源地址为
http://localhost:3000/users
- 将此地址复制到 postman中 并执行。
- 上图中,我们发送了get请求,得到了一个空数组。是因为我们文件中就是一个空数组。
- 接下来,我们执行post请求,添加一些数据:
- 可以看到,我们文件中的json数据会自动的更新为对应的结果。
- 其他的请求方式,可以自行尝试。
- json-server可以很快速的配置出一个
REST API
Server。 官网详情查看
写在最后
本篇实战型文章,会不定时持续的更新,直到项目框架搭建完毕。 欢迎一键三连!!! 文中,有不对的地方,或者有更好的写法的话,欢迎各位在评论区留言指出!!!
项目github地址:github.com/clr1235/rea…
有兴趣的可以fork或者给个star!!!
转载自:https://juejin.cn/post/7094900059493367845