🌈React的优点

学习简单

学完ES6+或TS基本语法再过一遍React官方文档即可进入简单的开发

语法简洁

React对TS有着近乎完美的支持度和Eslint的编辑器友好.与Vue需要记一大堆API不同的是React几乎没有API也没有类似Vue3中的setup等啰嗦的东西,更加极客,适合治疗代码强迫症.

生态丰富

无论国内外大厂,React的采样率都是三大框架中最高的,所以其生态也是非常强大的,与Vue的生态基本不相上下,远超Angular,比如支持小程序开发的Taro,支持移动端开发的React Native等,基本开发中要用到的东西都有开源实现的方案

社区庞大

因为是三大框架中全球用户最多的,所以社区庞大,爬坑最容易,一般找生态,查问题,基本谷歌一下,各种stackoverflow,github issue,medium,dev.to等各种社区会报出最准确的解决方案.

🌧React缺点

React最大的缺点在于过于灵活,一大堆杂七杂八的状态管理,有时候会导致选择障碍,路由也是组件实现,需要配置式必须要自己封装,并且工程化团队开发困难,NG虽然工程化比较好,但生态的落差基本直接弃.React还有一个缺点就是移动端好用的UI库并不多,而上述问题Vue都解决的不错,比如拥有官方的vuex和配置式的vue router以及类似Vant这种现象级别的移动组件库.所以站长建议最好有时间把两者都学习一下,那么在开发的时候就可以灵活选择.

✍️目标学者

本教程适合以下朋友学习

👉 前端入门者: 已经学习过TS和ES6的基础知识,需要快速学习一个前端框架

👉 React初学者: 已经学会了各种React知识希望有一套能进行项目实战练习的教程

👉 Vue开发者: 觉得Vue3太啰嗦不够Geek或者对TS和编辑器支持度有强迫症想尝试一下新东西

👉 Angular开发者: 想开发一下小程序或者移动APP

👉 Jquery为主的传统MVC开发者: Jquery+PHP/ROR写腻了? 那么就来追一下潮流哈

👉 Gopher,Javaer等职业后端: 想成为全栈开发者或者至少不再与前端争论对错和扯皮,自己能看懂前端代码,那么本教程正适合你

👀教程内容

本教程是一套从React入门到项目实战的全解析教程.内容涉及React的几乎所有知识以及Spa后台和SSR网站两个实战案例的开发等,非常全面地讲解React+TS项目开发中的方方面面,使大家能全面深入的掌握React及其周边的生态.

Spa应用的脚手架选择更好更快的vitejs,SSR应用的脚手架选择next.js.为了教程更轻量易懂,我们直接略过传统的Class组件,而直接全部使用函数组件讲解,后续所有React系列的教程也全部使用函数组件.并且为了与本站其它教程相对应,本站所有的前端/Node教程全部使用Typescript讲解.

🌒知识点

  • React+TS+Vitejs的开发环境构建
  • 掌握React开发的绝大部分技术
  • 灵活的使用Hooks以及编写自定义的Hooks
  • React Router的深度配置化封装以及懒加载实现
  • 学会使用Reducer+Context封装轻量级的状态管理
  • 掌握Redux与Mobx两种模式来进行全局的状态管理
  • 全面掌握React Router的使用和配置化封装
  • 学会css in js等多种css模式的使用
  • 使用Antd Components快速构建布局和表单
  • 使用localforage构建本地化数据缓存
  • 使用umi request进行与后端的数据互交
  • 动态暗黑模式和实时皮肤切换实现
  • 登录手机认证,社会化认证和验证码实现
  • 动态权限路由与菜单的生成
  • 图片上传以及区块拖动等常用组件的使用
  • Antd Charts实现数据可视化
  • TailWindCss与Material ui的使用
  • SSR原理以及React手动构建SSR应用
  • 完全掌握Nextjs框架的使用
  • 初步学会使用Mobx进行状态管理
  • 学会使用swr.js来和后端进行数据互交
  • Monorepo和同构架构的实现

🔥后续可期待

后面我们还会围绕着《精通React》这个系列制作其它教程,大概有以下几个内容

  • React+Electron桌面应用开发
  • Taro+RN开发跨端应用
  • React原理与实现

希望大家喜欢

[]

基本使用

安装react-router

~ pnpm add react-router-dom
~ pnpm add @types/react-router-dom -D

路由页面

// src/pages/Home.tsx
const Home: FC = () => <div>Home Page</div>;
export default Home;
// src/pages/About.tsx
const About: FC = () => <div>About Page</div>;
export default About;

嵌套路由

// src/pages/User.tsx
type UserParams = { username: string };
const User: FC = () => {
    const { username } = useParams<UserParams>();
    return <div>current user is {username}</div>;
};

const UsersIndex: FC = () => {
    const match = useRouteMatch();
    return (
        <div>
            <ul>
                <li>
                    <Link to={`{match.url}`}>User List</Link>
                </li>
                <li>
                    <Link to={`{match.url}/pincman`}>Pincman</Link>
                </li>
            </ul>
            <Switch>
                <Route path={`${match.path}/:username`}>
                    <User />
                </Route>
                <Route path={match.path}>
                    <h3>Please select a user.</h3>
                </Route>
            </Switch>
        </div>
    );
};
export default UsersIndex;

链接菜单

// src/components/Menu.tsx
const Menu: FC = () => (
    <div>
        <ul>
            <li>
                <Link to="/">Home</Link>
            </li>
            <li>
                <Link to="/about">About</Link>
            </li>
            <li>
                <Link to="/users">Users</Link>
            </li>
        </ul>
    </div>
);
export default Menu;

构建路由

// src/App.tsx
...
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const App: FC = () => {
    return (
        <Router>
            <div className="app flex items-center bg-white dark:bg-gray-800 flex-col place-content-between dark:text-white">
                <Menu />
                <div className="flex justify-center">
                    <Switch>
                        <Route path="/about">
                            <About />
                        </Route>
                        <Route path="/users">
                            <UsersIndex />
                        </Route>
                        <Route path="/">
                            <Home />
                        </Route>
                    </Switch>
                </div>
                <footer>footer</footer>
            </div>
        </Router>
    );
};
export default App;

路由模式

路由模式有真实地址hash两种,真实地址在生产环境下需要服务器软件URL重写(如: nginx)的支持,其生成的urlexample.com/some/path这样的格式.而hash则不需要,只要有CDN的地方扔上去就可以,其url看上去是这样example.com/#/your/page

真实地址通过BrowserRouter组件生成,hash模式通过HashRouter生成,他们可以通过使用Router这个底层组件自由切换,如下

安装history

history版本必须为4.x.x,因为react-router v5版本还不支持history v5

~ pnpm add history@"<5.0.0"
// src/App.tsx
const App: FC<{ hash?: boolean }> = ({ hash }) => {
    const historyCreator = hash ? createHashHistory() : createBrowserHistory();
    return (
        <Router history={historyCreator}>
        ...
        </Router>
    );
};

现在如果需要hash模式,可以这样

// src/main.tsx
ReactDOM.render(
    <React.StrictMode>
        <App hash />
    </React.StrictMode>,
    document.getElementById('root'),
);

当然也可以把hash选项放到状态管理(比如我们前面讲得context+userReducer或者后续将会讲到的redux,[mobx][]等)中手动更换,这样会更灵活方便,甚至还可以把这个配置放到localstorage中储存!

精确匹配

Route组件用于根据URL路径来匹配路由,Switch组件则用于匹配第一个路由,如果在Route列表外部不包装Switch则会匹配所有符合条件的路由,试一下把App.tsx中的Switch组件去除,然后访问/about,这时Home页面也会呈现,因为/about同时匹配了/

由于Switch总是匹配第一个符合的路由,那么如果我们把Home放到最前面会发生什么?这将导致我们访问任何其它路由都只会显示Home页面,这时候就需要为/路由添加添加一个exact参数来设置精确匹配了

// src/App.tsx
const App: FC<{ hash?: boolean }> = ({ hash }) => {
  ...
                <div className="flex justify-center">
                    <Switch>
                        <Route exact path="/">
                            <Home />
                        </Route>
                        <Route path="/about">
                            <About />
                        </Route>
                        <Route path="/users">
                            <UsersIndex />
                        </Route>
                    </Switch>
                </div>
};

这时访问/则只会匹配Home页面了

路由导航

导航组件

一般导航用Link就可以了,但是在需要对匹配的链接添加一个CSS类的时候可以使用NavLink(如导航栏),具体使用如下

<NavLink to="/react" activeClassName="actived">
  React
</NavLink>

路由跳转

Redirect组件用于设置跳转路由,如下

// src/App.tsx
<Switch>
  ...
    <Route path="/users">
        <UsersIndex />
    </Route>
    <Redirect path="/test" to="/" />
</Switch>

在导航链接上添加/test

// src/components/Menu.tsx
...
<ul>
    <li>
        <Link to="/test">redirect to home</Link>
    </li>
</ul>

编程式导航

通过useHistory这个hook可以获得history对象,通过这个对象可以实现push,replace等方式的手动跳转

const Menu: FC = () => {
    const history = useHistory();
    return (
        <div>
            <Button onClick={() => history.push('/about')}>跳转到About</Button>
            ...
        </div>
    );
};
export default Menu;

代码分割

动态导入

通过动态导入的方式实现代码分割,可以使每个组件和页面只在需要的时候加载

动态导入需要安装@loadable/component这个库

~ pnpm add @loadable/component
~ pnpm add @types/loadable__component -D

现在可以尝试一下动态导入About页面

// src/App.tsx
const About = loadable(() => import('./pages/About'));

当我们从http://localhost:4000导航到/about时,开发者工具->networks中会显示一个异步加载的About.tsx页面,表示已经异步加载

对于每个页面都进行一次loadable的HOC包装是一件非常繁琐的事情,为了简单,可以一次性把所有需要的页面进行代码分割出去,并通过页面名称的字符串来动态加载页面,在vitejs下使用glob的方式实现,而cra或者webpack等可以直接使用import(./${props.page})的方式

vitejs通过const pages = import.meta.glob('../../pages/**/*.{tsx,jsx}')这样的语法可加载pages目录下的所有页面并最终生成如下代码

const modules = {
  './dir/foo.js': () => import('./dir/foo.js'),
  './dir/bar.js': () => import('./dir/bar.js')
}

我们在添加一个函数,作用是通过正则去除前缀路径,比如../../pages/和后缀,比如.tsx,.jsx等,就获得最终的动态导入对象

// src/components/Router/view.tsx
const getAsyncImports = (imports: Record<string, () => Promise<any>>, reg: RegExp) => {
    return Object.keys(imports)
        .map((key) => {
            const names = reg.exec(key);
            return Array.isArray(names) && names.length >= 2
                ? { [names[1]]: imports[key] }
                : undefined;
        })
        .filter((m) => !!m)
        .reduce((o, n) => ({ ...o, ...n }), []) as unknown as Record<string, () => Promise<any>>;
};
const pages = getAsyncImports(
    import.meta.glob('../../pages/**/*.{tsx,jsx}'),
    /..\/..\/pages\/([\w+.?/?]+).tsx|.jsx/i,
);

最终pages对象会像这样

如果生产环境下,后缀会是jsx

const modules = {
  Home: () => import('../../pages/Home.tsx')
  About: () => import('../../pages/About.tsx'),
  'users/Index': () => import('../../pages/users/Index.tsx')
}

现在loadable('Home')就会异步加载Home页面了

export const AsyncPage = ({ page }: { page: string }) => {
    const View = loadable(pages[page], {
        cacheKey: () => page.replaceAll('/', '-'),
    });
    return <View />;
};

修改App.tsx试一下

<Switch>
<Route exact path="/">
    <AsyncPage page="Home" />
</Route>
<Route path="/about">
    <AsyncPage page="About" />
</Route>
<Route path="/users">
    <AsyncPage page="users/Index" />
</Route>
    <Redirect path="/test" to="/" />
</Switch>

添加"加载中"组件

为了保证不会因为loadding组件的每次计算而导致页面总是被重新渲染,使用useMemo缓存

const Loading: FC = () => (
    <div className="fixed w-full h-full top-0 left-0 dark:bg-white bg-gray-800 bg-opacity-25 flex items-center justify-center">
        <span>加载中</span>
    </div>
);

export const AsyncPage: FC<{ page: string; loading?: JSX.Element }> = ({ page, loading }) => {
    const fallback = useMemo(() => loading ?? <Loading />, []);
    const View = loadable(pages[page], {
        cacheKey: () => page.replaceAll('/', '-'),
        fallback,
    });
    return <View />;
};

以上代码看不出效果,可以使用p-min-delay来做一下延迟测试(请注意: 生产环境中不要添加延迟),并且需要设置一下超时

~ pnpm add p-min-delay promise-timeout
~ pnpm add @types/promise-timeout -D

代码

const ViewHoc = loadable(() => timeout(pMinDelay(pages[page](), 500), 12000), {
    cacheKey: () => page.replaceAll('/', '-'),
    fallback,
});

render函数

[react router][]加载组件页面有三种方案

  • component加载,如<Route path="/home" component={Home} />
  • children方式加载,如<Route path="/home"><Home /></Route>
  • render加载,如<Route path="/home" render={Home} />

通过componentrender加载,可像页面传递route props,而component每次都会创建新的渲染实例,所以一般会选择render函数来加载页面

首先定义一下类型

export interface PageOptions {
    page: string;
    loading?: JSX.Element;
}

export type PageProps<
    // 传递给Page的额外参数
    T extends Record<string, any> = Record<string, any>,
    Path extends string = string,
    Params extends { [K: string]: string | undefined } = ExtractRouteParams<Path, string>,
> = PageOptions &
    T & {
        route: RouteComponentProps<Params>;
    };

export interface RouteProps
    extends PageOptions,
        Omit<BaseRouterProps, 'children' | 'component' | 'render'> {
    // 传递给Page的额外参数
    params?: Record<string, any>;

改造AsyncPage

export const AsyncPage: FC<PageProps> = ({ page, loading, ...rest }) => {
    ...
    return <View {...rest} />;
};

定义一个AsyncRoute作为异步路由组件

export const AsyncRoute: FC<RouteProps> = (props) => {
    const { page, loading, params = {}, ...rest } = props;
    return (
        <Route
            {...rest}
            render={(route) => (
                <AsyncPage route={route} page={page} loading={loading} {...params} />
            )}
        />
    );
};

App.tsx中使用

<Switch>
    <AsyncRoute page="Home" path="/" exact />
    <AsyncRoute page="About" path="/about" />
    <AsyncRoute page="users/Index" path="/users" />
    <Redirect path="/test" to="/" />
</Switch>

同时也可以在User/Index页面中使用

提取单个用户详情组件为独立页面

// src/pages/users/Detail.tsx
type UserParams = { username: string };
const UserDetail: FC = () => {
    const { username } = useParams<UserParams>();
    return <div>current user is {username}</div>;
};
export default UserDetail;

提取用户列表组件为独立页面

// src/pages/users/List.tsx
const UserList: FC = () => {
    return <div>User List</div>;
};
export default UserList;

原来的用户页面修改为用户布局页面

// src/pages/users/Index.tsx
const UsersIndex: FC = () => {
    const match = useRouteMatch();
    const [count, setCount] = useState(0);
    return (
        <div>
            <Button onClick={() => setCount(count + 1)}>增数</Button>
            <span>{count}</span>
            <ul>
                <li>
                    <Link to={`{match.url}`}>User List</Link>
                </li>
                <li>
                    <Link to={`{match.url}/pincman`}>Pincman</Link>
                </li>
            </ul>
            <Switch>
                <AsyncRoute path={match.path} page="users/List" exact />
                <AsyncRoute path={`${match.path}/:username`} page="users/Detail" />
            </Switch>
        </div>
    );
};
export default UsersIndex;

缓存布局

运行上面的代码会发现每次在访问子页面时会造成Index重新渲染,表现为点击"增数"按钮count变成1,在点击User list或者Pincman链接,count重置为0,造成以上问题的原因是loadable会在每次访问时生成一个新的实例,解决办法就是上一节讲到的缓存hook-useMemo

单独使用useMemo会导致页面一直缓存无法渲染,所以需要一个判断重新渲染页面时机的变量,此变量可以是match.url,因为切换子页面时,父级布局页的match.url是不会变动的

export const AsyncPage: FC<PageProps> = ({ page, loading, ...rest }) => {
    const { url } = rest.route.match;
    const View = useMemo(
        () =>
            loadable(() => timeout(pMinDelay(pages[page](), 500), 12000), {
                cacheKey: () => page.replaceAll('/', '-'),
                fallback,
            }),
        [url],
    );
    ...
};

现在试试点击"增数"再切换子页面,已经不会再重新渲染父级了

为了后续的配置式路由可以更简便地实现,我们还可以为AsyncRouteAsyncRoute添加一个children

export const AsyncPage: FC<PageProps> = ({ page, loading, children, ...rest }) => {
    return <View {...rest}>{children}</View>;
};


export const AsyncRoute: FC<RouteProps> = (props) => {
...
        render={(route) => (
            <AsyncPage route={route} page={page} loading={loading} {...params}>
                {children}
            </AsyncPage>
        )}
};

const UserIndex: FC = ({ children }) => {
    ...
    return (
        <div>
            <ul>...</ul>
            {children}
        </div>
    );
};
export default UserIndex;

现在可以在App.tsx中添加所有路由配置了

<Switch>
    <AsyncRoute page="Home" path="/" exact />
    <AsyncRoute page="About" path="/about" />
    <AsyncRoute page="users/Index" path="/users">
        <Switch>
            <AsyncRoute path="/users" page="users/List" exact />
            <AsyncRoute path="/users/:username" page="users/Detail" />
        </Switch>
    </AsyncRoute>
    <Redirect path="/test" to="/" />
</Switch>

可用API

这部分介绍一些常用的[react router][]的API

因为本教程是Typescript编码,所以不必把所有的API参数和属性列出来,大家可以点开类型文件自己查看

Hooks及函数

修改render

有了Hooks之后不必要再往render里的组件传递route参数

// src/components/Router/types.ts
export type PageProps<
    // 传递给Page的额外参数
    T extends Record<string, any> = Record<string, any>,
> = PageOptions & T;
export const AsyncPage: FC<PageProps> = ({ page, loading, children, ...rest }) => {
    const { url } = useRouteMatch();
    ...
};

// src/components/Router/view.tsx
export const AsyncRoute: FC<RouteProps> = (props) => {
    const { page, loading, params = {}, children, ...rest } = props;
    ...
                <AsyncPage page={page} loading={loading} {...params}>
                    {children}
                </AsyncPage>
};

useHistory

用于编程化导航,比如history.push("/home");

useLocation

获取当前URL的location对象,比如location.pathname

useRouteMatch

通过path参数与某个Routepath匹配,如果不传入参数,则与当前导航到的Route匹配,匹配后即可获取其被导航后的信息,比如match.url

matchPath

在组件渲染的生命周期之前获取match对象,其结果与渲染后使用useRouteMatch一样,用法如下

matchPath("/users/2", {
  path: "/users/:id",
  exact: true,
  strict: true
});
// 结果
//  {
//    isExact: true
//    params: {
//        id: "2"
//    }
//    path: "/users/:id"
//    url: "/users/2"
//  }

组件

  • Router: 底层路由构建器
  • BrowserRouter: html5 history api路由构建器
  • HashRouter: hash路由构建器
  • MemoryRouter: 不把URL写入浏览器地址栏而把历史记录保存在内存中的路由构建器,适用于测试或[react native][]等
  • Switch: 在路由列表中只渲染第一个与当前地址匹配的路由
  • Route: 定义路由,参数包含path(可以是个数组,对应多个地址),extra(精确匹配),render/component
  • Redirect: 跳转路由,部分参数与Route一样
  • Link: 通过组件模式导航路由
  • NavLink: 比Link多了个可以为当前链接激活时添加的CSS类
  • Prompt: 在用户离开页面之前跳出"是否离开"提示

其它知识

以下知识后续教程会讲解

  • 配置式路由
  • 认证和权限路由
  • 基于context的独立路由组件
  • 路由切换时的转场动画

以下知识自行查看官网示例

  • query: 通过Link组件中的query参数可指定query,如要获取query看[这里][https://reactrouter.com/web/example/query-parameters]
  • 弹出框: 想要点击一个路由可弹出一个modal,看[这里][https://reactrouter.com/web/example/modal-gallery]

因为视频教程的制作耗费站长大量的业余时间,有着巨大的工作量.每一集教程,需要编写代码,编写文档,录制视频,剪辑视频等多个流程,并且还提供了问答服务,如果全部免费的话本站将很难持续发展下去为大家提供更优质的内容.所以不得不收取一定费用,望大家谅解.

订阅本站后可以使用本站的一切服务,包括视频教程的学习,下载,问答以及本站发布的其它任何资源都可以随意使用.本站保证不再对订阅者收取二次费用.需要注意的是,后续推送的站长直播服务只有终身订阅者才可以享受.

本站目前提供各类编程开发相关的技术视频教程以及针对这些视频教程的问答服务和这些技术周边生态的导航,文档的翻译,开源项目的推荐,技巧性文章的发布等等.并且也为订阅者提供QQ群和discord问答服务.对于视频教程中的代码,站长专门搭建了一个代码托管平台方便大家下载.

本站在编程语言方面专注于Javascript/Typescript,Golang,PHP等几种站长擅长以及工作中常用的语言.技术栈涉及React,Vue等前端生态以及,Node.js,各种golang和php技术等后端技能,同时也会涉及一些包括Linux,Devops,Docker,K8S等在内与编程相关的技术,甚至还会讲解一些硬件方面的东西,总之用"杂七杂八"形容最为贴切.

本站绝大多数收费属于原创的虚拟商品,具有可复制性,可传播性,一旦授予,不接受任何形式的退款、换货要求。请您在购买获取或订阅之前确认好 ,避免引起不必要的纠纷