书接上回《react 基础工程(react-redux & react-router)》,由于最近项目上的新实践新体验,所以记录一下。
依然,代码仓库:github。
一、用 @reduxjs/toolkit 创建 reducer 和 api
其实工程里已经用到这个工具,但是当时还没完全发现它的优势,还以为不过是官方推荐的比较好用的三方库而已。才知道确实简化了处理以及还有别的封装功能。
1、简化写法
以前老式的 redux 写法:
import { cloneDeep } from 'lodash';
export const addStringAction = payload => ({
type: 'ADD_STRING',
payload: payload,
});
export const minusStringAction = payload => ({
type: 'MINUS_STRING',
payload: payload,
});
const defaultState = {
b: 'abc',
};
export default function stringReducer(state = defaultState, action) {
const { type, payload } = action;
state = cloneDeep(state);
switch (type) {
case 'ADD_STRING':
state.b += payload;
return state;
case 'MINUS_STRING':
state.b = '';
return state;
default:
return state;
}
};
用 @reduxjs/toolkit 的写法:
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
a. 不需要再额外声明 action 了,不需要定义那些 type 了。reducers 字段下的每个方法就是个 action。用的时候直接
dispatch(increment(123))
即可。b. 而且好像可以直接改 state 上的字段,不需要返回一个完全新的 state 了?其实也是里面做了处理。看官方介绍,在注释里:
Redux Toolkit allows us to write “mutating” logic in reducers. It doesn’t actually mutate the state because it uses the Immer library, which detects changes to a “draft state” and produces a brand new immutable state based off those changes
2、用 createApi 创建接口请求
相似于 react-query,对一些请求做了封装,有一些状态参数什么的。这里有官方对比。
给一段代码示例:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const axiosBaseQuery = ({ baseUrl }) =>
async ({ url, method, data, params }) => {
try {
const result = await Promise.resolve({ a: 1, b: 2, baseUrl, url, method, data, params });
return { data: result };
} catch (error) {
return {
error: {
status: error?.status,
data: error?.data || error.message,
},
};
}
};
// Define a service using a base URL and expected endpoints
export const digimonApi = createApi({
reducerPath: 'digimonApi',
// 可以用默认的 fetchBaseQuery({ baseUrl: '' }),也可以使用自己封装的请求方法
baseQuery: axiosBaseQuery({ baseUrl: '' }),
endpoints: (builder) => ({
getDigimonByName: builder.query({
query: (name) => `digimon/${name}`,
async onQueryStarted(args, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
// `onSuccess` side-effect
console.info('onQueryStarted', args, data);
} catch (error) {
// `onError` side-effect
console.error('onQueryStarted', error);
}
},
}),
saveDigimon: builder.mutation({
query: (body) => ({
url: `digimon/save`,
method: 'POST',
body,
}),
async onQueryStarted(args, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
// `onSuccess` side-effect
console.info('onQueryStarted', args, data);
} catch (error) {
// `onError` side-effect
console.error('onQueryStarted', error);
}
},
}),
}),
});
export default digimonApi;
用就是这么个用法,主要几个点介绍下:
- a. 记得要在全局
store
注册一下
import { configureStore } from '@reduxjs/toolkit'
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
})
- b. 可以用
query/mutation
方式,用法差别就是那个 builder 后面跟的参数。至于目的差别就是如语义所示,一个是获取值一个是修改值吧。然后以上面代码为例,会生成两个 hook,useGetDigimonByNameQuery
和useSaveDigimonMutation
。用法也不太一样。
const getDigimonByName = digimonApi.useGetDigimonByNameQuery('agumon', { skip: false });
const [saveDigimonFunc, saveDigimonStatus] = digimonApi.useSaveDigimonMutation();
query
的请求是直接请求,返回对象包含了返回值 data 和 各类的状态,isLoading,isSuccess 这些。当然有时不想它马上就请求,但 hook 又不能写在条件里。所以官方提供了个 skip 参数,可以用来控制是否触发请求。谓之:条件获取。mutation
返回对象是个数组,元素一是请求的触发方法,用 useEffect 什么的去触发一下。元素二就是各类的状态,isLoading,isSuccess 这些。
- c. 请求完成钩子 onQueryStarted,无论失败与否都会触发。第一个参数是我们写的请求入参。第二是个复合对象,有两个字段(当然不只这两个):
dispatch
,就是 store.dispatch,也就是请求完毕就可以顺手触发一些修改 store 的操作。queryFulfilled
,一个 promise 函数,执行成功会返回请求结果 data。失败了也就是请求失败,则触发 promise error 失败事件。
二、用 @craco/craco 修改 react 原生打包配置
上篇文章提到,react 把打包配置藏了起来,所以自己另起了 webpack.config.js 对其进行处理。现在知道还可以用 @craco/craco 挟持修改其默认配置的方法。用法也很简单。
- 把命令改成:
{
"scripts": {
"dev": "craco start",
"build": "craco build",
}
}
- 项目根路径配置一份
craco.config.js
:
const { resolve } = require('path');
const { DefinePlugin } = require('webpack');
const { getPlugin, pluginByName } = require('@craco/craco');
module.exports = {
webpack: {
configure: (webpackConfig) => {
// 默认配置没有处理 PUBLIC_URL,会导致处理 index.html 模板里的变量出错
const { isFound, match: htmlWebpackPlugin } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin'),
);
if(isFound) {
const templateParameters = htmlWebpackPlugin.userOptions?.templateParameters || {};
htmlWebpackPlugin.userOptions.templateParameters =
Object.assign(templateParameters, { 'PUBLIC_URL': '' });
}
return webpackConfig;
},
alias: {
'@': resolve(process.cwd(), 'src'),
},
plugins: [
new DefinePlugin({
'NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
],
},
};
多半改的都是 webpack 配置吧,更多更具体可看《官网文档》。
三、用 vite 打包
vite 已经很成熟了,用于 vue 工程当仁不让,react 工程也很可以了。话不多说直接上操作。
- 项目根目录新建
vite.config.js
:
import fs from 'fs';
import path from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(process.cwd(), '/src'),
},
],
},
define: {
'NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
plugins: [
react(),
],
server: {
port: 3000,
open: 'http://localhost:3000', // 或 true
// proxy: {
// '/api': {
// target: 'http://jsonplaceholder.typicode.com',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// },
// },
},
// 解决在 js 文件里写 jsx
// How to use .js instead of .jsx https://github.com/vitejs/vite/discussions/3448
esbuild: {
loader: 'jsx',
include: /src\/.*\.jsx?$/,
// loader: 'tsx',
// include: /src\/.*\.[tj]sx?$/,
exclude: [],
},
optimizeDeps: {
esbuildOptions: {
plugins: [
{
name: 'load-js-files-as-jsx',
setup(build) {
build.onLoad({ filter: /src\/.*\.js$/ }, async (args) => ({
loader: 'jsx',
contents: fs.readFileSync(args.path, 'utf8'),
}));
},
},
],
},
},
});
- 项目根目录新建
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ReactZz App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>
项目根目录的 index.html 是 vite 的默认入口,当然也可以改,不过要自行配置了,可参考《多页面应用模式》。
这个和 @craco/craco 只是修改部分参数不同,是完全新搭建了打包配置。实际上还会有其他相关的要配置,比如,css 文件处理,自定义压缩处理,ts 配置等等。这里只是记录个基础使用。