• vite + react + ts 手摸手做项目系列二 (实战篇)


     

    传送门

    前言

    • 这篇实战篇文章,我改了很多遍,本来加了很多复杂的封装,但是对于初学 react+ts 的同学很不友好,因为不好看懂 所以我删删减减,尽量用写的大家都能看的懂,尽量用简洁的语言表达出我们想做什么功能,先易后难

    • 复杂功能的封装都会再后面的系列文章中详细讲解,一步一步深入学习, 项目中绝大部分的代码都有详细的注释,希望大家看的不懵逼,因为我自己在初学的时候再 github 上找开源项目,很多 项目封装的很好,但是对小白就很懵了,得看好久,所以我这个教程,就会尽力的讲详细,按照文档来,项目能跑通,注释写的详尽,再配合直播,讲明白说清楚。

    • ts 大家不要觉得很难,去官网看一看基础用法,就能干了,碰到不懂的语法直接查就好,用几次就会了。

    • react 同理,切勿眼高手低,要大量代码实践。不懂就问,不会就查,一来二去自然会了。

    • 这一篇主要写的是

      • 假数据 json-server 的使用

      • 请求接口的简单使用

      • 路由的简单使用

      • 一些简单的全局配置数据

      • 国际化的使用

      • 简单公共组件的封装

    json-server mock 数据

    • yarn add json-server -D

    • 在终端

      mkdir mock
      cd mock
      touch db.json
      复制代码
    • 在 package.json 中的 scripts 中添加

      "mock": "json-server mock/db.json --port 3008"
      复制代码
    • 然后运行命令 yarn mock 就可以在控制台成功访问到我们在 db.json 中配置的接口数据了

    请求封装

    注意事项:process.env 要替换成 import.meta.env

    • 全局的公共配置文件都会放在根目录下的 config.ts 文件中,目前项目刚开始只有少量配置信息
     /**
     * 当前环境变量
     */
    // process.env 在vite中不能用
    // export const whyEnv = import.meta.env.VITE_REACT_URL || "";
    /**
     * 接口地址
     * @description env 可为主要环境或自定义地址
     */
    export const apiAddress = "http://localhost:3008/";
    
    /**
     * 开发代理前缀
     */
    export const proxyApi = "/api";
    
    /**
     * 接口前缀
     * 判断环境,是否需要使用前缀
     * 生产环境不需要代理,同时本地配置的代理在生产环境也是不能用的
     */
    export const urlPrefix = process.env.NODE_ENV === "development" ? proxyApi : "";
    
    复制代码
    • 项目中用的 umi-request 这个库,目前我给配置的很少的东西,错误处理,中间件处理等等我的给删减了,刚开始不搞这么复杂
       // utils/request.ts
    
     /**
     * request 网络请求工具
     * 更详细的 api 文档: https://github.com/umijs/umi-request
     */
    import umiRequest, { extend } from "umi-request";
    import { urlPrefix } from "../config";
    
    // 使用前缀,配合本地代理
    export const whyRequest = extend({
      prefix: `${urlPrefix}`,
    });
    
    export default umiRequest;
    复制代码
    • 定义接口:要提前和后段沟通好入参数,出参数的格式,结合 ts 的类型提示,在其他地方调用的时候就可以直接看到接口定义的属性了,非常方便
    /**
    * 登陆请求数据类型
    */
    export interface ILogin {
      userName: string;
      pwd: string;
    }
    
    /**
    * 返回数据类型
    * 要提前和后段定义好类型,等接口写完直接替换地址就好了
    *
    */
    export interface ILoginData {
    code: number;
    message: string;
    token: string;
    }
    
    /**
    * 登陆接口
    * @param params
      */
      export const loginApp = (params: ILogin): Promise<ILoginData> => {
      return whyRequest.get("/login", params);
      };
    复制代码
    • 使用就很简单,直接调用,之后我们会配合,ahooks 中的 useRequest()使用
     loginApp({ userName: "why", pwd: "123" }).then((res) => {
         if (res.code === 200) {
           history.push("/home");
         } else {
           message.error("用户名或密码错误!");
         }
       });
    复制代码

    国际化配置

    • yarn add react-intl -D
    • 国际化我们使用 react-intl 同时也要兼容 antd,的之类插件的中英文,我们在切换语言的时候插件库也要直接进行切换到对应的语言,配置起来也很方便,
    • 我们直接上代码
    // 引入创建语言,国际化容器,暂时我们只需要用这两个就可以实现的我们目前的功能
    import { createIntl, IntlProvider } from "react-intl";
    // 我们需要引入antd 的国际化的配置
    import antdEnUS from "antd/lib/locale/en_US";
    import antdZhCN from "antd/lib/locale/zh_CN";
    // 这是我们项目中中英文的配置,
    import enLn from "./components/ln-en";
    import zhLn from "./components/ln-zh-cn";
    ···核心代码
    /**
    * 包裹了默认 locale 的 Provider
    * LocaleProvider 需要在App.tx使用,包装整个项目
    * @param props
    * @returns
      */
      export const LocaleProvider: React.FC = (props) => {
      return <IntlProvider locale={getLocale()}>{props.children}</IntlProvider>;
      };
    /**
     * 获取当前的 intl 对象,可以在 node 中使用
     * @param locale 需要切换的语言类型
     * @param changeIntl 是否不使用 g_intl
     * @returns IntlShape
     */
      const getIntl = (locale?: string, changeIntl?: boolean) => {
    
      // 如果全局的 g_intl 存在,且不是 setIntl 调用
      if (gIntl && !changeIntl && !locale) {
      return gIntl;
      }
      // 如果存在于 localeInfo 中
      if (locale && localeInfo[locale]) {
      return createIntl(localeInfo[locale]);
      }
    
    // 使用默认语言
    if (localeInfo[defaultLanguage])
    return createIntl(localeInfo[defaultLanguage]);
    // 使用 zh-CN
    if (localeInfo["zh-cn"]) return createIntl(localeInfo["zh-cn"]);
      // 抛错
    if (!locale || !!localeInfo[locale]) {
      throw new Error(
      "The current popular language does not exist, please check the locales folder!"
      );
      }
    // 如果还没有,返回一个空的
    return createIntl({
    locale: "zh-cn",
    messages: {},
    });
    };
    /**
    * 语言转换
    * @param descriptor
    * @param values
      */
      export const formatMessage = (
      descriptor: MessageDescriptor,
      values?: Record<string, any>
      ) => {
      if (!gIntl) {
      setIntl(getLocale());
      }
      return gIntl.formatMessage(descriptor, values);
      };
    复制代码
    • 页面中使用

      1,我们要在对应的 ts 文件中配置中英文对照

      // 在locale 文件下配置中文对照
      export default {
      frontEnd: "Work hard on the front end",
      switchLan: "Chinese-English shift",
      switchToEn: "switch to chinese",
      switchToCh: " switch to english",
      localLan: "The internationalization of this project is   based on",
      };
      // 配置英文对照
      export default {
      frontEnd: "前端要努力",
      switchLan: "中英文切换",
      switchToEn: "切换到中文",
      switchToCh: "切换到英文",
      localLan: "本项目国际化基于",
      };
      复制代码

      2,在页面中我们直接调用 formatMessage() 这个方法就好了

    /**
     * 国际化页面
     * @constructor
     */
    const LocalePage: React.FC = () => {
      // 这使用的是useState,其实这里是完全不需要的
      const [value, setValue] = React.useState(
        localStorage.getItem("why__locale") || "zh-cn"
      );
      // 切换多语言
      const onChange = (e: RadioChangeEvent) => {
        setValue(e.target.value); //在这里是没有作用的代码
        setLocale(e.target.value); // 调用切换多语言方法,然后刷新页面
      };
      return (
        <Card title={formatMessage({ id: "switchLan" })} style={{  "500px" }}>
          <Radio.Group onChange={onChange} value={value}>
            <Radio value={"zh-cn"}>{formatMessage({ id: "switchToEn" })}</Radio>
            <Radio value={"en"}>{formatMessage({ id: "switchToCh" })}</Radio>
          </Radio.Group>
          <div className={styles.localLan}>
            {formatMessage({ id: "localLan" })}react-intl
          </div>
        </Card>
      );
    };
    复制代码
    • 国际化页面

    image.png

    路由

    • react 路由看这个

    • react 路由系统和 vue 大有不同,没有路由导航前钩子,配置登陆鉴权就要自己配置下,结合 token,

    • 我们项目中路由的目的就是支持动态路由,路由权限,配置抽离,目前就是最简单的,裸的

    image.png

    公共组件封装

    • 我们如何封装一个公共组件?

      1, 项目中需要多处使用的组件

      2, 不和业务耦合的组件,业务耦合的公共组件

      3, 所有状态都可以在外部控制,通过传入的props来控制其行为而不是暴露其内部结构。

      封装良好的组件隐藏其内部结构,并提供一组属性来控制其行为。
      
      隐藏内部结构是必要的。其他组件没必要知道或也不依赖组件的内部  结构或实现细节
      复制代码
    • 我们的项目中统一目录,主要为了看起来舒服

    • 目录:image.png

      • index.tsx为主入口文件

      • index.md为组件使用样例,必要的代码注释,要清楚的告诉别人怎么使用这个公共组件

      • image.png

    如何使用iconfont的字体图标

    • 封装 icon,主要配合 antd createFromIconfontCN 直接引入 iconfont 中的字体图标,非常方便
    • 如下图所示直接登陆到iconfont网站生成对应js文件,在项目中直接用就好,很简单

      // 简单来说
    
      // 这里可以根据各属性动态添加,如果属性值为true则为其添加该类名,
    
      // 如果值为false,则不添加。这样达到了动态添加class的目的
    
       <FontIcon
          className={classNames(
            {
              [styles.large]: size === "large", // 返回为true使用css .large,下方同理
              [styles.normal]: size === "normal",
              [styles.small]: size === "small",
              [styles.disabled]: disabled,
            },
            className
          )}
          {...restProps}
        />
    复制代码
    • React.FC<>的使用 1.React.FC 是函数式组件,是在 TypeScript 使用的一个泛型,FC 就是 FunctionComponent 的缩写,事实上 React.FC 可以写成 React.FunctionComponent:
    const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
      <div>{message}</div>
    );
    复制代码

    2.React.FC 包含了 PropsWithChildren 的泛型,不用显式的声明 props.children 的类型。React.FC<> 对于返回类型是显式的,而普通函数版本是隐式的(否则需要附加注释)。

    3.React.FC 提供了类型检查和自动完成的静态属性:displayName,propTypes 和 defaultProps(注意:defaultProps 与 React.FC 结合使用会存在一些问题)。

    4.我们使用 React.FC 来写 React 组件的时候,是不能用 setState 的,取而代之的是 useState()、useEffect 等 Hook API。

    封装icon公共组件

      // IconType继承React.HTMLAttributes的属性,然后IconType,就拥有了其可被外界访问的属性
      export interface IconType extends React.HTMLAttributes<any> {
      // type 必有属性,如果使用的时候没有静态检查是,会提示错误,类型不匹配,使用ts的好处,静态类型检查非常nice
      // 报错如下:TS2741: Property 'type' is missing in type '{}' but required in type 'IconType'.  index.tsx(7, 3): 'type' is declared here.
      type: string;
      // 图标尺寸,默认 normal
      size?: "small" | "normal" | "large" | null; // 可选属性,size后面加上?
      // 是否禁用
      disabled?: boolean;
    }
    // createFromIconfontCN 返回一个组件
    const FontIcon = createFromIconfontCN({
      // 请给新图标一个合适的驼峰命名,并保证单词正确
      scriptUrl: "//at.alicdn.com/t/font_955172_ymhvgyhjk.js",
    });
    
    const Icon: React.FC<IconType> = ({
      className,
      size = "normal",
      disabled,
      ...restProps
    }) => {
      // 我们使用classNames 这个插件动态渲染icon的状态,size,disabled等等
      return (
        <FontIcon
          className={classNames(
            {
              [styles.large]: size === "large",
              [styles.normal]: size === "normal",
              [styles.small]: size === "small",
              [styles.disabled]: disabled,
            },
            className
          )}
          {...restProps}
        />
      );
    };
    // 思考题:这个地方需要用,react.memo吗?
    export default React.memo(Icon);
    
    复制代码
    • 使用(截图中有iconSelect公共组件,此篇不做讲解)

    image.png

    结语

    • vite + react + ts 手摸手做项目系列一 (项目配置篇)
    • github地址(github的更新速度比文档要快,文档要追加大量注释)github地址
    • 有问题可以提issue 或者加博主:lisawhy0706
    • 下一篇会主要讲解一些复杂组件
      • 完善项目中的功能
      • 公共配置的处理
      • 自定义hooks的封装
      • 如何设计项目的权限包括路由权限,按钮级别权限
  • 相关阅读:
    javaScript表单焦点自动切换
    JavaScript禁止用户多次提交方法
    javaScript事件机制兼容【整理】
    DOM元素尺寸和位置(clientwidth ,scrollwidth , offsetwidth.......)
    javaScript给元素添加多个class
    javaScript增加样式规则(新增样式)
    javaScript动态添加样式
    工作中遇到的各种jar包说明
    springboot—Jpa原生sql使用
    Des3EncryptionUtil加密与解密
  • 原文地址:https://www.cnblogs.com/onesea/p/15431884.html
Copyright © 2020-2023  润新知