公司的统一登录项目之前部署在私有云上采用的是 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

  1. # 在项目的根目录
  2. npx create-react-app my-app --typescript

这样就在根目录新建了一个项目,这里可以先把子项目 /my-app/node_modules 添加到 .gitignore.

后面安装依赖:

  1. {
  2. "@reduxjs/toolkit": "^1.6.1",
  3. "react-redux": "^7.2.4",
  4. "react-router-dom": "^5.2.1",
  5. "redux": "^4.1.1",
  6. "@types/react-redux": "^7.1.18",
  7. "@types/react-router-dom": "^5.1.8",
  8. }

react-router-dom

由于之前的 next.js 是约定式路由,改成使用 react-router-dom, 加之公司其他的项目都是使用的配置式的路由,所以需要对其进行改造,经过研究,在项目的 index.tsx引入路由,在 App.tsx中配置路由表。

  1. npm i -S react-router-dom
  2. npm i -D @types/react-router-dom
  1. import { HashRouter as Router } from 'react-router-dom';
  2. ReactDOM.render(
  3. <React.StrictMode>
  4. <Router>
  5. <App />
  6. </Router>
  7. </React.StrictMode>
  8. ,
  9. document.getElementById('root')
  10. );
  1. import { Route, Switch } from 'react-router-dom';
  2. export default function App() {
  3. return (
  4. <div className="App">
  5. <Switch>
  6. <Route path="/login" component={Login} />
  7. <Route path="/oauth" component={Oauth} />
  8. <Route path="/dashboard" component={Dashboard} />
  9. </Switch>
  10. </div>
  11. );
  12. }

这里的路由是直接写在里面,也可以配置一个 路由表 然后渲染成组件,这样更加解耦

  1. const configRoute = [
  2. {
  3. path:'/login', component: Oauth,
  4. ...
  5. }
  6. ];
  7. return (
  8. <Switch>
  9. {configRoute.map(route)=>(
  10. <Route path={route.path}, component={route.component}></Route>
  11. )}
  12. </Switch>
  13. );

对于 next/router 还有一个 useRouter, 可以使用 useHistory , useLocation来代替:

  1. import { useHistory, useLocation } from 'react-router-dom';
  2. function App() {
  3. const history = useHistory();
  4. // 获取搜索栏的地址
  5. const { search } = useLocation();
  6. useEffect(() => {
  7. if (search) {
  8. history.push(`/oauth${search}`);
  9. } else {
  10. history.push('/dashboard');
  11. }
  12. }, [history, search]);
  13. return (
  14. ...
  15. );
  16. }

这样一来 next/router 的功能就被代替了,下面配置 react-redux 进行状态管理。

react-redux

  1. npm i -S @reduxjs/toolkit react-redux redux
  2. npm i -D @types/react-redux

首先是 index.tsx

  1. import { Provider } from 'react-redux';
  2. import store from './store/store';
  3. ReactDOM.render(
  4. <Provider store={store}>
  5. <App />
  6. </Provider>,
  7. document.getElementById('root')
  8. );

配置的 store :

  1. // store.ts
  2. import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
  3. // 按照模块划分需要保存的状态
  4. import userReducer from './modules/userSlice'
  5. export function makeStore() {
  6. return configureStore({
  7. reducer: { user: userReducer },
  8. })
  9. }
  10. const store = makeStore()
  11. // 导出类型
  12. export type AppState = ReturnType<typeof store.getState>
  13. export type AppDispatch = typeof store.dispatch
  14. export type AppThunk<ReturnType = void> = ThunkAction<
  15. ReturnType,
  16. AppState,
  17. unknown,
  18. Action<string>
  19. >
  20. export default store

user 模块:

  1. import { createSlice, PayloadAction } from '@reduxjs/toolkit';
  2. import type { AppState } from '@/store/store';
  3. export interface UserState {
  4. userName: string;
  5. }
  6. // 创建一个初始的状态
  7. const initialState: UserState = {
  8. userName: '',
  9. };
  10. export const userSlice = createSlice({
  11. name: 'user',
  12. initialState,
  13. reducers: {
  14. // 类似于 vuex mutations
  15. getUserInfo: (state, { payload }: PayloadAction<UserState>) => {
  16. return payload;
  17. },
  18. },
  19. });
  20. export const { getUserInfo } = userSlice.actions;
  21. export const selectUserName = (state: AppState) => state.user.userName;
  22. export default userSlice.reducer;

另外 react-redux 还提供了几个hook 用于使用:

  1. import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
  2. import type { AppDispatch, AppState } from './store';
  3. // Use throughout your app instead of plain `useDispatch` and `useSelector`
  4. export const useAppDispatch = () => useDispatch<AppDispatch>();
  5. export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;

使用方法可以看这个例子:

  1. import { useAppDispatch } from '@/store/hooks';
  2. import { getUserInfo } from '@/store/modules/userSlice';
  3. const App = ()=>{
  4. const dispatch = useAppDispatch();
  5. const handleClick = (data)=>{
  6. dispatch(getUserInfo({ userInfo: data }));
  7. }
  8. }

对于 异步 actions 回头再研究研究

eject? customize-cra?

上面的配置完成之后,对于之前使用的一些 alias, proxy 等还需要继续配置,这里有两种情况

  1. 运行 npm run eject 弹出隐藏的 webpack 配置,在其中配置参数;
  2. 使用 customize-cra + react-app-rewired 进行个性化配置

本次迁移中一开始选择使用第二种方法,customize-cra + react-app-rewired

  1. const { useBabelRc, override } = require('customize-cra');
  2. const { alias, configPaths } = require('react-app-rewire-alias');
  3. const aliasMap = configPaths('./tsconfig.path.json')
  4. console.log(__dirname);
  5. const config = override(
  6. // eslint-disable-next-line
  7. useBabelRc(),
  8. alias(aliasMap)
  9. );
  10. module.exports = config;

但是后面发现每次配置的时候都需要去查对应的封装的包,有点麻烦于是索性 eject ,自由配置 webpack . eject 之后主要配置项就在 /config 目录下了,这里的配置大同小异,不会的小朋友可以去看看 《深入浅出webpack》.

eject还带来了一个目录 /scripts 里面写了打包编译的脚本文件,一般不用动,有时间可以看下,在启动项目和打包的时候 create-react-app到底做了什么工作。

去除 next.js 依赖

脚手架安装完成之后就是对项目进行迁移,并把next.js相关的 类似于 next/linknext/router等依赖切换成对应的 react-router-dom的方法和包。

配置 create-react-app 成为 react 开发环境

  1. .env 文件里 以 REACT_APP_ 开头配置地址等文件
  2. 创建 /src/types/index.d.ts 声明一些静态文件的类型
  1. declare module '*.svg';
  2. declare module '*.png';
  3. declare module '*.jpg';
  4. declare module '*.jpeg';
  5. declare module '*.gif';
  1. 设置别名和 baseUrl
    • 一个是在webpack里面设置,用于打包的时候,不过这里create-react-app的配置已经处理了,会读取项目中的t/jsconfig.json 文件里面的配置。
    • 还有一个是在 tsconfig.json 设置,用于开发的时候在 ide 里面解析
      1. "baseUrl": "./",
      2. "paths": {
      3. "@/*": ["./src/*"]
      4. }

后记

这个文章告诉我们的道理是,技术选型首先要慎重,根据项目的场景选择最合适的技术栈;其次是要选择熟悉的技术,否则后面的维护会受到影响;还有就是一旦遇到问题,当发现技术确实与现有的业务不匹配的时候,抓紧时间进行切换,减少沉默成本。