公司的统一登录项目之前部署在私有云上采用的是 next.js
,虽然存在一些问题但是还能使用。现在统一部署到公司自建的 devops 平台,由于平台只有通用的 react
流水线,部署之后是客户端渲染的类型(CSR
),导致之前的服务端渲染部署上去存在很多问题,调整成 SSG
模式部署上去也存在很多问题,例如 redux
状态管理问题以及 router
跳转问题,针对这些问题,最终决定从 next.js
框架切换到 create- react-app
的客户端渲染模式。这个需求还是很奇葩的,网上搜了一圈也没这个先例,于是就写了此文记录一下迁移以及都 CRA
的一些配置。
迁移目标
原有的项目基于 next.js 使用了 next.js 的路由以及一些 [getStaticProps](https://www.nextjs.cn/docs/basic-features/data-fetching#getstaticprops-static-generation)
(Static Generation) 方法,要对其进行重写,首先是安装 CRA 脚手架,将页面迁移过去,然后再配置 react-router-dom
, react-redux-toolkit
等,最后在配置一下 typescript
的开发环境。
安装脚手架 create- react-app
# 在项目的根目录
npx create-react-app my-app --typescript
这样就在根目录新建了一个项目,这里可以先把子项目 /my-app/node_modules
添加到 .gitignore
.
后面安装依赖:
{
"@reduxjs/toolkit": "^1.6.1",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.1",
"redux": "^4.1.1",
"@types/react-redux": "^7.1.18",
"@types/react-router-dom": "^5.1.8",
}
react-router-dom
由于之前的 next.js
是约定式路由,改成使用 react-router-dom
, 加之公司其他的项目都是使用的配置式的路由,所以需要对其进行改造,经过研究,在项目的 index.tsx
引入路由,在 App.tsx
中配置路由表。
npm i -S react-router-dom
npm i -D @types/react-router-dom
import { HashRouter as Router } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
,
document.getElementById('root')
);
import { Route, Switch } from 'react-router-dom';
export default function App() {
return (
<div className="App">
<Switch>
<Route path="/login" component={Login} />
<Route path="/oauth" component={Oauth} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</div>
);
}
这里的路由是直接写在里面,也可以配置一个 路由表 然后渲染成组件,这样更加解耦
const configRoute = [
{
path:'/login', component: Oauth,
...
}
];
return (
<Switch>
{configRoute.map(route)=>(
<Route path={route.path}, component={route.component}></Route>
)}
</Switch>
);
对于 next/router
还有一个 useRouter
, 可以使用 useHistory
, useLocation
来代替:
import { useHistory, useLocation } from 'react-router-dom';
function App() {
const history = useHistory();
// 获取搜索栏的地址
const { search } = useLocation();
useEffect(() => {
if (search) {
history.push(`/oauth${search}`);
} else {
history.push('/dashboard');
}
}, [history, search]);
return (
...
);
}
这样一来 next/router
的功能就被代替了,下面配置 react-redux
进行状态管理。
react-redux
npm i -S @reduxjs/toolkit react-redux redux
npm i -D @types/react-redux
首先是 index.tsx
import { Provider } from 'react-redux';
import store from './store/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
配置的 store :
// store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
// 按照模块划分需要保存的状态
import userReducer from './modules/userSlice'
export function makeStore() {
return configureStore({
reducer: { user: userReducer },
})
}
const store = makeStore()
// 导出类型
export type AppState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppState,
unknown,
Action<string>
>
export default store
user 模块:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { AppState } from '@/store/store';
export interface UserState {
userName: string;
}
// 创建一个初始的状态
const initialState: UserState = {
userName: '',
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
// 类似于 vuex mutations
getUserInfo: (state, { payload }: PayloadAction<UserState>) => {
return payload;
},
},
});
export const { getUserInfo } = userSlice.actions;
export const selectUserName = (state: AppState) => state.user.userName;
export default userSlice.reducer;
另外 react-redux
还提供了几个hook 用于使用:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, AppState } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
使用方法可以看这个例子:
import { useAppDispatch } from '@/store/hooks';
import { getUserInfo } from '@/store/modules/userSlice';
const App = ()=>{
const dispatch = useAppDispatch();
const handleClick = (data)=>{
dispatch(getUserInfo({ userInfo: data }));
}
}
对于 异步 actions 回头再研究研究
eject? customize-cra?
上面的配置完成之后,对于之前使用的一些 alias, proxy 等还需要继续配置,这里有两种情况
- 运行
npm run eject
弹出隐藏的webpack
配置,在其中配置参数; - 使用
customize-cra
+react-app-rewired
进行个性化配置
本次迁移中一开始选择使用第二种方法,customize-cra
+ react-app-rewired
,
const { useBabelRc, override } = require('customize-cra');
const { alias, configPaths } = require('react-app-rewire-alias');
const aliasMap = configPaths('./tsconfig.path.json')
console.log(__dirname);
const config = override(
// eslint-disable-next-line
useBabelRc(),
alias(aliasMap)
);
module.exports = config;
但是后面发现每次配置的时候都需要去查对应的封装的包,有点麻烦于是索性 eject
,自由配置 webpack
. eject
之后主要配置项就在 /config
目录下了,这里的配置大同小异,不会的小朋友可以去看看 《深入浅出webpack》.
eject
还带来了一个目录 /scripts
里面写了打包编译的脚本文件,一般不用动,有时间可以看下,在启动项目和打包的时候 create-react-app
到底做了什么工作。
去除 next.js 依赖
脚手架安装完成之后就是对项目进行迁移,并把next.js
相关的 类似于 next/link
,next/router
等依赖切换成对应的 react-router-dom
的方法和包。
配置 create-react-app 成为 react 开发环境
- 在
.env
文件里 以REACT_APP_
开头配置地址等文件 - 创建
/src/types/index.d.ts
声明一些静态文件的类型
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
- 设置别名和
baseUrl
- 一个是在
webpack
里面设置,用于打包的时候,不过这里create-react-app
的配置已经处理了,会读取项目中的t/jsconfig.json
文件里面的配置。 - 还有一个是在
tsconfig.json
设置,用于开发的时候在 ide 里面解析"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
- 一个是在
后记
这个文章告诉我们的道理是,技术选型首先要慎重,根据项目的场景选择最合适的技术栈;其次是要选择熟悉的技术,否则后面的维护会受到影响;还有就是一旦遇到问题,当发现技术确实与现有的业务不匹配的时候,抓紧时间进行切换,减少沉默成本。