灏天阁

使用 React18 + Vite + TypeScript 完成公司项目经验总结

· Yin灏

当前找工作环境恶劣,很多求职者表示招聘不仅要求会 vue,还要求会 react。

刚好这段时间我用 React18 + Vite + TS 为公司从 0 到 1 开发了一个项目,就顺便总结了一些前端开发知识和技巧,帮助我记忆这些知识的同时,希望能对你也有所启发。欢迎评论区交流。

我会把代码(当然不是真实项目的。。)上传到 GitHub 上,文末可以取链接。最后还请多多评论、点赞支持!废话不多说,下面开始进入正题:

创建项目

选择 React + TypeScript 模板。不要选用 React + TypeScript + SWC。因为 SWC 不支持 babel 插件,不能转换 es6 语法和特性。这个后面也会说到。

pnpm create vite

image.png

添加 eslint 支持

这里有一个 eslint 常用配置、插件的清单,可以收藏看看。github.com/dustinspeck…

如何配置

根据官网的说明,只需要简单的执行下面的命令,然后根据你的项目进行选择配置。

pnpm create @eslint/config

image.png

让 eslint 检查缩进

如果我想让所有代码的索引为 2 个空格,否则就报错误,那么如何配置呢?

在 .eslintrc.cjs 文件中。

module.exports = {
  rules: {
    // 缩进必须为 2 个空格 https://eslint.org/docs/latest/rules/indent#rule-details
    indent: ["error", 2],
    // 禁止所有 tab https://eslint.org/docs/latest/rules/no-tabs#rule-details
    "no-tabs": "error",
  },
};

禁用 tab 表示缩进使用 spaces,而不是 tab。可以在 vscode 右下角查看配置。

image.png

使用 eslint-plugin-react-hooks 插件

这个插件是在上面那个清单里看到的。可以检查我们写的 react hook 是否规范。

add eslint-plugin-react-hooks --dev

然后扩展 eslint 配置

{
  "extends": [
    // ...
    "plugin:react-hooks/recommended"
  ]
}

TypeScript 技巧

怎么定义全局类型

比如我想在全局都能使用以下几个类型

type pageview = "pageview";
type click = "click";
type blockview = "blockview";
type elementview = "elementview";

就在 global.d.ts 文件里进行定义,然后就可以在全局里使用啦。但是要确保 global.d.ts 文件包含在 tsconfig.json 文件的 include 选项中。

declare global {
  declare type pageview = "pageview";
  declare type click = "click";
  declare type blockview = "blockview";
  declare type elementview = "elementview";
}

如何获取数组类型的元素类型

type ListType = { a: number; b: string }[];
const list = [{}] as ListType;
// 法一
type ArrayItem<T> = T extends Array<infer R> ? R : never;
type Item = ArrayItem<ListType>;
// 法二
TaskList["task"][number];
type Item = ListType[number];

setState 传入回调函数场景

假设 age 为 42,这个方法将会调用 setAge(age + 1) 三次。

function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
}

然而,触发 handleClick 方法后,age 还是 43,不是 45。因为 set 方法不会更新 age 状态变量在当前正在运行的代码。所以每次 setAge(age + 1) 都变成 setAge(43)。

为了解决这个问题,你可以传一个函数类型参数给 setAge,获取到下一会的状态。

function handleClick() {
  setAge((a) => a + 1); // setAge(42 => 43)
  setAge((a) => a + 1); // setAge(43 => 44)
  setAge((a) => a + 1); // setAge(44 => 45)
}

a => a + 1 是你的更新函数。它接收一个待改变的 state,然后在函数体计算返回下一个 state。

React 把更新函数放入一个队列中。然后在下一次 render 时,更新函数将会以相同的顺序被调用。

  1. a => a + 1 将接收 42 作为待改变的状态,然后返回 43 作为下一次的状态。
  2. a => a + 1 将接收 43 作为待改变的状态,然后返回 44 作为下一次的状态。
  3. a => a + 1 将接收 44 作为待改变的状态,然后返回 45 作为下一次的状态。
  4. 没有其它队列需要更新, 最后 React 将会存储 45 作为最后的状态。

在开发模式下,React 可能会调用两次你的更新函数,目的是确保他们是纯的没有副作用。

理解 useRef、useMemo、useCallback

useRef 可用来存储一个引用值(不会受 re-render 影响),也可用来获取 dom 节点。

useMemo 用来缓存一个值。当依赖项为空数组时,缓存值永远不会变。当有依赖性时,每次 re-render 如果依赖改变,那么将重新执行函数,将新的函数返回值作为缓存的数据。

useCallback 是 useMemo 的语法糖,相当于返回一个函数。

const fn1 = useCallback(() => {
  console.log(123);
}, []);
const fn2 = useMemo(
  () => () => {
    console.log(123);
  },
  []
);

react 不会缓存组件状态的解决方案

react 不像 vue 能使用 keep-alive。

当 react 跳到另外一个页面,再返回到上一个页面。上一个页面会重新渲染。

由于水平有限,所以决定将接口数据进行缓存。第一次没有缓存会请求。第二次执行方法时判断是否有缓存,有就直接返回缓存的数据。

我觉得要理解这个 hook,要明白模块只会被加载一次。还要明白每次 re-render 有些方法是会被重新执行,有些方法是会被重新定义。

useCache hook

interface Cb {
  (...arg: unknown[]): unknown;
}
const cacheMap = new Map();
export default (key: string, callback: Cb) => {
  return async (cache = true) => {
    const result = cacheMap.get(key);
    if (cache && result) {
      return result;
    }
    const res = await callback();
    cacheMap.set(key, res);
    return res;
  };
};

使用

function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    fn();
  });
  async function fn() {
    const res = await getStatus();
    console.log("===>", res);
  }
  const getStatus = useCache("getStatus", () => {
    const arr = [1, 2, 3, 4];
    const res = [] as number[];
    arr.forEach((num) => {
      console.log(num);
      res.push(num);
    });
    return res;
  });
  function add() {
    setCount((c) => c + 1);
  }
  return (
    <>
      <h2>==</h2>
      <br />
      {count}
      <br />
      <button onClick={add}>add</button>
      <h2>==</h2>
    </>
  );
}

react-router-dom 简单使用

页面跳转管理 使用 react-router-dom

2023-03-01-17-13-37.gif

下载依赖包

pnpm install react-router-dom

创建路由表,懒加载组件

/src/router/index.tsx

ts

复制代码

import React, { lazy } from "react";
import { createHashRouter, Navigate } from "react-router-dom";
import HomeC from "../pages/home";
// 引入方法一
// const Home = lazy(() => import('../pages/home'))
// 引入方法二
// const Home = lazy(async () => {
//   const res = await new Promise<any>((resolve) => {
//     setTimeout(() => {
//       resolve(HomeC)
//     }, 2000)
//   })
//   return {default: res}
// })
// 引入方法三
const Home = lazy(async () => {
  return (
    new Promise() <
    any >
    ((resolve) => {
      setTimeout(() => {
        resolve({
          default: HomeC,
        });
      }, 2000);
    })
  );
});
const Detail = lazy(() => import("../pages/detail"));
const Record = lazy(() => import("../pages/record"));
export default createHashRouter([
  {
    path: "/",
    element: <Navigate replace to="/home" />,
  },
  {
    path: "/home",
    element: <Home />,
  },
  {
    path: "/detail",
    element: <Detail />,
  },
  {
    path: "/record",
    element: <Record />,
  },
]);

改造根组件

App.tsx

import React, { Suspense } from "react";
import { RouterProvider } from "react-router-dom";
import Loading from "./pages/loading";
import router from "./router";
import "./App.css";
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <RouterProvider router={router}></RouterProvider>
    </Suspense>
  );
}
export default App;

路由跳转

Home 页

import React from "react";
import { useNavigate } from "react-router-dom";
const Index = () => {
  // 路由跳转
  const router = useNavigate();
  function toDetail() {
    router("/detail");
  }
  function toRecord() {
    router("/record");
  }
  return (
    <>
      <div>home </div>
      <br />
      <button onClick={toDetail}> detail</button>
      <br />
      <br />
      <button onClick={toRecord}> record</button>
    </>
  );
};
export default Index;

定义组件 props 类型,并且声明默认值

普通情况

使用 defaultProps。

import React, { memo } from "react";
interface Props {
  name?: string;
}
// function Index(props: Props){
//   return (
//     <button>name: {props.name}</button>
//   )
// }
const Index = (props: Props) => {
  return <button>name: {props.name}</button>;
};
// const 的作用域不会提升
Index.defaultProps = {
  name: "小王",
} as Props;
export default memo(Index);

复杂情况:当使用了 forwardRef

forwardRef 用来获取子组件内的 dom 元素。

如果使用了 forwardRef,那么定义默认值需要这么写:

import React, { forwardRef } from "react";
interface Props {
  btnClick?: (item: any) => void;
  totalScore?: number;
}
const Index = forwardRef(function HeadReward(props: Props, ref: any) {
  // ...
});
Index.defaultProps = {
  btnClick: () => {
    //
  },
  totalScore: 0,
} as Props;
export default Index;

React 性能优化注意事项

由于全部使用的函数式组件,每次 setState 的时候,都会导致 re-render。

而 React 是父组件更新,子组件也会跟着更新,所以需要做一些优化。

1 子组件使用 memo 包裹

父组件

function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>add</button>
      <Foo />
    </>
  );
}

子组件

import { memo } from "react";
const Index = () => {
  console.log("re-render");

  return <div>子组件</div>;
};
export default memo(Index);

2 传递给子组件的函数使用 useCallback 包裹

如果不传递给子组件,可以不使用 useCallback 包裹。因为 re-render 时只是重新定义了一遍,函数内部并没有执行。

为什么要用 useCallback 包裹,因为 re-render 时前后创建的两个函数引用地址并不一样,Object.is 比较是否相同时会返回 false。

父组件

import { useCallback, useState } from "react";
import Foo from "@/components/foo";
import "./App.css";
function App() {
  const request = useCallback(() => {
    setTimeout(() => {
      console.log("请求");
    }, 10);
  }, []);
  // const request = () => {
  //   setTimeout(() => {
  //     console.log('请求');
  //   }, 10)
  // }
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <Foo request={request} />
    </>
  );
}
export default App;

子组件

import { memo } from "react";
interface Props {
  request: () => void;
}
const Index = (props: Props) => {
  console.log("re-render");

  return <button onClick={props.request}>按钮</button>;
};
export default memo(Index);

3 传递给子组件的引用数据类型需要使用 useMemo 包裹。其实和上面说的传递函数给组件,函数要用 useCallback 包裹起来是同一个道理。函数也是引用数据类型,是一个特殊的对象!

父组件

import { useCallback, useMemo, useState } from "react";
import Foo from "@/components/foo";
import "./App.css";
function App() {
  const obj = useMemo(
    () => ({
      name: "xiaowang",
      age: 19,
    }),
    []
  );
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <Foo obj={obj} />
    </>
  );
}
export default App;

子组件

import { memo } from "react";
interface Props {
  obj: {
    name: string,
    age: number,
  };
}
const Index = (props: Props) => {
  console.log("re-render");

  return <button>按钮</button>;
};
export default memo(Index);

4 hook 使用 useMemo 包裹

为什么 hook 要使用 useMemo 包裹?因为我们使用 hook 本质上是在调用一个函数执行的计算结果。

每次 re-render 的时候,都去执行一下 hook 函数的话,可能会造成性能上的损失。所以可以使用 useMemo 将函数执行的计算结果缓存起来。

组件

function App() {
  const list = usePow([1, 2, 3, 4]);
  console.log(list);
  const list2 = usePow([1, 2]);
  console.log(list2);

  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <Foo />
    </>
  );
}

hook

import { useMemo } from "react";
export default (list: number[]) => {
  return useMemo(
    () =>
      list.map((num) => {
        console.log("hook 执行了");
        return Math.pow(num, 2);
      }),
    []
  );
};

在函数式组件中使用 ref

获取当前组件内的 dom 节点

import { useRef, useEffect, useState } from "react";
import Foo from "@/components/foo";
import "./App.css";
function App() {
  const button = useRef(null);
  useEffect(() => {
    console.log(button.current);
  }, []);
  return (
    <>
      <h2>==</h2>
      <button ref={button}>按钮</button>
      <h2>==</h2>
    </>
  );
}
export default App;

获取子组件的 dom 节点

使用 forwardRef 转发。

父组件

import { useRef, useEffect } from "react";
import Foo from "@/components/foo";
import "./App.css";
function App() {
  const button = useRef(null);
  useEffect(() => {
    console.log(button.current);
  }, []);
  return (
    <>
      <h2>==</h2>
      <Foo ref={button} />
      <h2>==</h2>
    </>
  );
}
export default App;

子组件

import React, { forwardRef } from "react";
interface Props {
  name?: string;
}
const Index = (props: Props, ref: any) => {
  return <button ref={ref}>name: {props.name}</button>;
};
export default forwardRef(Index);

memo 配合 forwardRef 一起使用

用 memo 包裹 forwardRef 转发后的组件,防止子组件不必要的 re-render。

import React, { forwardRef, memo } from "react";
interface Props {
  name?: string;
}
const Index = (props: Props, ref: any) => {
  // console.log(123);

  return <button ref={ref}>name: {props.name}</button>;
};
export default memo(forwardRef(Index));

memo 配合 forwardRef 和 defaultProps 一起使用

在 forwardRef 转发后返回的组件中添加 defaultProps 属性。以便当组件没有接收到父组件传来的 prop,使用默认值。

注意这里只能在 forwardRef 转发后返回的组件中添加 defaultProps 属性,否则会报错。

import React, { forwardRef, memo } from "react";
interface Props {
  name?: string;
}
const Index = forwardRef((props: Props, ref: any) => {
  return <button ref={ref}>name: {props.name}</button>;
});
// 注意这里只能在 forwardRef 转发后返回的组件中添加 defaultProps 属性
// 否则会报错。
Index.defaultProps = {
  name: "小王",
} as Props;
export default memo(Index);

获取循环列表的 dom 节点

列表在当前组件内

给 ref 传入一个 函数

import { useRef, useEffect, useState } from "react";
import Foo from "@/components/foo";
import "./App.css";
function App() {
  const buttonList = useRef<Element[]>([]);
  function getRef(dom: Element | null) {
    if (!dom) return;
    buttonList.current.push(dom);
  }
  return (
    <>
      {[1, 2, 3, 4, 5].map((num) => {
        return (
          <button ref={getRef} key={num}>
            name: {num}
          </button>
        );
      })}
    </>
  );
}
export default App;

列表在子组件内

父组件

import { useRef, useEffect, useState } from "react";
import Foo from "@/components/foo";
import "./App.css";
function App() {
  const buttonList = useRef(null);
  useEffect(() => {
    console.log(buttonList.current);
  }, []);
  return (
    <>
      <h2>==</h2>
      <Foo ref={buttonList} />
      <h2>==</h2>
    </>
  );
}
export default App;

子组件通过 useImperativeHandle 自定义向父组件暴露属性、方法等。

import React, { forwardRef, memo, useRef, useImperativeHandle } from "react";
interface Props {
  name?: string;
}
const Index = forwardRef((props: Props, ref: any) => {
  const buttonList = [] as (Element | null)[];
  function getRef(dom: Element | null) {
    if (!dom) return;
    buttonList.push(dom);
  }
  useImperativeHandle(ref, () => ({
    buttonList,
  }));
  return (
    <>
      {[1, 2, 3, 4, 5].map((item) => {
        return (
          <button ref={getRef} key={item}>
            name: {props.name}
          </button>
        );
      })}
    </>
  );
});
Index.defaultProps = {
  name: "小王",
} as Props;
export default memo(Index);

禁止用户手动缩放屏幕

在 index.html 添加如下脚本

<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>

react 样式隔离

可以约定命名规范,每个页面或组件的根标签的 css 类名都不能一样。

但如何做到像 vue 一样在 <style>标签添加一个 scoped 属性,就可以达到样式隔离的效果呢? 可以使用 styled-jsx 这个库。

下面简单介绍一下它的使用。

下载依赖

pnpm install --save styled-jsx

下一步,在你的 babel 配置文件中配置 plugins

{
  "plugins": [
+   "styled-jsx/babel"
  ],
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
  ],
  "targets": "chrome 70"
}

然后使用

const Index = () => {
  // 路由跳转
  const router = useNavigate();

  function back() {
    router(-1);
  }
  return (
    <>
      <div className="abc">record </div>
      <button onClick={back}>返回</button>
      <style jsx>{`
        .abc {
          color: red;
        }
      `}</style>
    </>
  );
};

或者使用分离的 css 文件,而是不是将样式内联。

// 导入 css(?raw 是将文件内容以字符串的形式引入)
// https://vitejs.dev/guide/assets.html#importing-asset-as-string
import styled from "./index.scss?raw";
const Index = () => {
  // 路由跳转
  const router = useNavigate();

  function back() {
    router(-1);
  }
  return (
    <>
      <div className="abc">record </div>
      <button className="back" onClick={back}>
        返回
      </button>
      <div id="div">
        <div className="btn"></div>
      </div>
      <style jsx>{styled}</style>
    </>
  );
};
export default Index;

px 转 rem,实现页面自适应宽度

点击这里了解更多!我猜很多人只是会配置,但不明白 postcss-pxtorem 中的 rootValue 配置项是什么意思

使用 amfe-flexible、postcss,将 px 转为 rem。使用 ui 库 antd-mobile。

pnpm add postcss-pxtorem postcss amfe-flexible -S

在入口文件处引入

// 可伸缩布局库
import "amfe-flexible";

在根目录创建 postcss.config.cjs

// eslint-disable-next-line no-undef
module.exports = {
  plugins: {
    "postcss-pxtorem": {
      rootValue: 37.5, // 设计稿元素尺寸/10
      unitPrecision: 5,
      propList: ["*"], // 是一个存储哪些将被转换的属性列表,这里设置为['*']全部,假设需要仅对边框进行设置,可以写['*', '!border*']
      selectorBlackList: [], // 则是一个对css选择器进行过滤的数组,比如你设置为['el-'],那所有el-类名里面有关px的样式将不被转换,这里也支持正则写法。
      replace: true,
      mediaQuery: false, // 媒体查询( @media screen 之类的)中不生效
      minPixelValue: 0, // px 绝对值小于 0 的不会被转换
      exclude: /node_modules/i,
    },
  },
};

grid 布局的应用

实现如下样式:

image.png

代码:

<div className="week">
  {[1, 2, 3, 4, 5, 6, 7].map((item, index) => (
    <div
      ref={getRef}
      className={`day day${index + 1}`}
      key={`${item}`}
      onClick={() => gather(index)}
    >
      <div className="day-num">{item}</div>
      <div className="top">
        <img className="img" src={star} />
      </div>
      <div className="bottom">5积分</div>
    </div>
  ))}
</div>

关键 css 如下

& > .week {
  display: grid;
  grid-template-columns: repeat(4, auto);
  grid-template-rows: repeat(2, 84px);
  grid-row-gap: 8px;
  grid-column-gap: 8px;
  margin-top: 14px;
  & > .day {
    // ...
  }
  > .day7 {
    grid-column-start: span 2;
  }
}

收集积分动效

2023-03-02-11-00-59.gif

思路:

1 获取动画目的地节点,获取七个元素的节点放到一个数组里。

2 点击某一天时,获取当前点击的元素节点信息,初始化动画开始位置,然后设置到达目标位置的动画

具体是怎么实现的呢?

1 获取动画目的地的 dom 节点

获取动画结束位置的 dom 节点信息。

const destinationDom = useRef < HTMLDivElement > null;
<div ref={destinationDom} className="destination"></div>

2 获取七个元素的节点

const refList = [] as HTMLDivElement[];
function getRef(dom: HTMLDivElement | null) {
  if (dom) refList.push(dom);
}
<div className="week">
  {[1, 2, 3, 4, 5, 6, 7].map((item, index) => (
    <div
      ref={getRef}
      className={`day day${index + 1}`}
      key={`${item}`}
      onClick={() => gather(index)}
    >
      <div className="day-num">{item}</div>
      <div className="top">
        <img className="img" src={star} />
      </div>
      <div className="bottom">5积分</div>
    </div>
  ))}
</div>

3 设置动画元素的 css

我在写的时候在想,可不可以使用 immer 来优化下这里的代码,让它更优雅。不然每次 setState 都要写这一大坨对象。有兴趣的同学可以试试。

const [transitionStyle, setTransitionStyle] = useState({
  display: "none",
  top: 0,
  left: 0,
  transform: "scale(0)",
  transition: "none",
});
const canGather = useRef(true);
function gather(index: number) {
  if (!canGather.current) return;
  canGather.current = false;
  const target = refList[index].getBoundingClientRect();
  const destination = destinationDom.current!.getBoundingClientRect();

  setTransitionStyle({
    display: "block",
    top: target.top,
    left: target.left,
    transform: "scale(1)",
    transition: "none",
  });
  setTimeout(() => {
    setTransitionStyle({
      display: "block",
      top: destination.top,
      left: destination.left,
      transform: "scale(0.5)",
      transition: "all .6s linear",
    });
  }, 0);
  setTimeout(() => {
    setTransitionStyle({
      display: "none",
      top: 0,
      left: 0,
      transform: "scale(1)",
      transition: "none",
    });
    canGather.current = true;
  }, 600);
}

tsx

<div ref="{destinationDom}" className="destination">
  目的地
</div>;

{
  /* 注意样式,position: fixed */
}
<div className="transition-div" style="{transitionStyle}"></div>;

模块只会被加载一次

不同的模块文件有不同的作用域,且只会被执行一次。比如下面的日志只会被打印一次。之后无论在其它文件导入 useRef 多少次,日志都不会再打印了。

let value;

console.log(1);

export function useRef(_value) {
  if (value) {
    return value;
  }

  value = _value;

  return value;
}

组件库

使用 Ant Design Mobile。但说实话,我只使用到了其中很少的组件,Toast 提示、Popup 弹框,其余很多样式、业务组件都需要自己手写。

图标

直接下载使用 ui 切的图片,没有额外引入第三方 icon 图标库。

状态管理

虽然装了 mobx 依赖,但是几乎没用到。

因为项目没有那么复杂,就几个页面。用的更多的是 useState。但是如果是为了学习,可以强迫把页面的数据都放到 mobx 里进行管理。

环境变量和模式

查看文档了解更多 cn.vitejs.dev/guide/env-a…

命令行通过 –mode 选项重写模式。

"scripts": {
 "dev:test": "vite --mode test",
 "dev:pre": "vite --mode pre",
 "dev:prod": "vite --mode prod",
 "build:test": "tsc && vite build --mode test",
 "build:pre": "tsc && vite build --mode pre",
 "build:prod": "tsc && vite build --mode prod"
 },

tsc:编译 TypeScript

vite 会通过 dotenv 读取项目中的下列文件,以添加额外的环境变量。

# 必须以 VITE\_ 开头,否则为 undefined

# .env.test
VITE_SHAREURL='https://test.share.com.cn'
VITE_BASEURL='https://www.test.baidu.com'

# .env.pre
VITE_SHAREURL='https://pre.share.com.cn'
VITE_BASEURL='https://www.pre.baidu.com'

# .env.prod
VITE_SHAREURL='https://prod.share.com.cn'
VITE_BASEURL='https://www.prod.baidu.com'

这样我们就可以根据环境来选择操作了。比如只有在开发模式下引入在线调试功能。

if (import.meta.env.MODE === "test") {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  import("https://cdn.staticfile.org/vConsole/3.3.4/vconsole.min.js").then(
    () => {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      new window.VConsole();
    }
  );
}

在生产环境下去除 log 日志和 debugger。

defineConfig 可以传入一个函数,通过 config 参数可以获取到模式 mode 的值。

cn.vitejs.dev/config/#con…

export default defineConfig((config) => {
  return {
    build: {
      minify: "terser",
      terserOptions: {
        compress: {
          // 默认是 false。移除 console
          drop_console: config.mode === "prod",
          // 默认是 true。移除 debugger
          drop_debugger: config.mode === "prod",
        },
      },
    },
  };
});

打包开启 gzip 压缩

image.png

gzip 压缩可以大大节省服务器的网络带宽。

啥是 gzip ?我理解的就是一个能压缩内容的工具。浏览器请求 url 时,如果在 request header 中设置属性 accept-encoding: gzip,则表明浏览器支持 gzip 压缩。

服务器收到浏览器发送的请求之后,判断浏览器是否支持 gzip。如果支持 gzip,则向浏览器传送 gzip 压缩过的内容,不支持则向浏览器发送未经压缩的内容。一般情况下,浏览器和服务器都支持 gzip。

浏览器接收到服务器的响应之后判断内容是否被压缩,如果被压缩则解压缩后才显示页面内容。

如何在 vite 项目中开启 gzip 压缩呢?首先下载插件:

pnpm add rollup-plugin-gzip --save-dev

在 vite.config.ts 中

import { defineConfig } from "vite";
import gzipPlugin from "rollup-plugin-gzip";
export default defineConfig({
  base: "./",
  build: {
    rollupOptions: {
      plugins: [gzipPlugin()],
    },
  },
});

接下来,打包后会发现多了个 .gz 文件,说明开启 gzip 压缩成功。

image.png

接口请求

使用的是公司封装好的请求工具,不清楚内部是怎么实现的。

数据加密

虽然我不用管。但我看了下,大概是引入了一个加密的 js 文件工具方法,有几种加密方式。然后接口请求时会判断使用哪种加密方式,对进行携带的参数进行加密。

记录遇到的问题,以及解决方法

‘Foo’ cannot be used as a JSX component.

这其实是 TypeScript 版本问题。

使用 Vscode 写 TS 时,出现一些奇怪的飘红。可能是 TypeScript 版本的问题。选择使用工作目录下的 TS 版本,而不使用 Vscode 的 TS 版本。

09b7ca218e6913063d049d05d54a48e.png

快捷键 ctrl + shift + p

image.png

然后选择使用 Workspace Version image.png

注意:有可能设置版本后,在 vscode 里还是飘 ts 的红。可以尝试把依赖和 package-lock.json 文件都删了,重新下载依赖。

Type annotations can only be used in TypeScript files - in .tsx file

出现这个问题是因为 Vscode 的 Lanuage mode 出现了问题,重新选择,然后重启就可以了。

image.png

低版本安卓浏览器里报错 globalThis is not defined

项目在低版本的安卓浏览器上会报这个错,在 index.html 里添加如下脚本即可解决。

<script>
  this.globalThis || (this.globalThis = this);
</script>

开发时,低版本浏览器不识别 es6 新语法问题

前提: 在用 vite 创建项目的时候,不要选用 SWC。SWC 不支持下面的配置。

vue 项目可以参考 www.cnblogs.com/ygunoil/p/1…

vite.config.ts 里可以配置 babel 插件。

plugins: [react({
   babel: {
     plugins: [
       "@babel/plugin-proposal-optional-chaining",
       "@babel/plugin-proposal-nullish-coalescing-operator"
     ]
   }
})],

我们直接在根目录下的 .babelrc 里进行配置。使用 babel 的预设 preset-env,免得一个个配插件。

plugins: [
 react({
   babel: {
       // 表示使用根目录下 .babelrc 文件的 babel 配置
       babelrc: true,
   },
 })
],

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
  ],
  "targets": "chrome 70"
}

配置好后 vite 就可以使用可选链和空值合并运算符等 es6 新语法了。

配置 alias 时,提示找不到 path

在 ts + vite 项目里提示 “path” 找不到。

原因分析:path 模块是 node.js 内置的功能,但是 node.js 本身并不支持 typescript,所以直接在 typescript 项目里使用是不行的。

解决办法

npm install @types/node --save-dev

image.png

这样我们就方便配置 alias 了。因为它的路径必须是绝对路径。

export default defineConfig({
  resolve: {
    alias: {
      // 只能是绝对路径
      // '@': fileURLToPath(new URL('./src', import.meta.url)),
      "@": path.resolve("./src"),
    },
  },
});

让打包后的代码支持低版本浏览器

vite 开发和打包的构建逻辑是不一样的。默认打包的目标是支持原生 ESM script 标签支持原生 ESM 动态导入  和  import.meta  的浏览器:

  • Chrome >=87
  • Firefox >=78
  • Safari >=14
  • Edge >=88

为了让打包后的代码支持传统浏览器,可以使用 legacy 插件。否则如果你的代码使用了可选链、控制合并运算符等 es6 新语法新特性,打包运行低版本浏览器是会报错的。

pnpm add -D @vitejs/plugin-legacy

vite.config.ts

plugins: [
 legacy({
   targets: ['> 0.25%', 'last 2 versions and not dead'],
 }),
],

代码之外的东西

常用的脚本命令

推荐把 vscode 的终端改为 git bash,解锁更多操作。

image.png

之所以学点脚本命令,是因为手动复制粘贴文件到另一个地方太麻烦。感觉用命令行的方式更简单快速。

比如说我想要把打包后的 dist 目录移动到桌面,可以执行:

image.png

这里 desktop_path 是我设置的一个电脑全局环境变量,就是我的电脑桌面路径。

mv dist ${desktop_path}/

这里列一下我认为比较有用的脚本命令:

# 在当前目录下,把 文件 a 及以下文件移动到 b 目录下(改名也可以用这个命令)
mv a b

# 创建一个文件夹
mkdir test

# 创建一个文件
touch hello.md

# 查看当前目录下所有东西
ls

# 删除文件或文件夹
rm 文件名

# 删除文件及以下的文件
rm -rf xxx

-r 就是向下递归,不管有多少级目录,一并删除
-f 就是直接强行删除,不作任何提示的意思

# 查看 shell 全局变量
env

# 输出全局变量
echo $HOME or $HOME

# 查看当前文件目录
pwd

# 清空命令行窗口
clear

# printenv 查看某个环境变量,变量前不用加 $ 符号了
printenv HOME

# echo 查看某个环境变量,变量前要加 $ 符号
echo $HOME 或者 echo ${HOME}

在 git bash 中,不管是设置局部变量还是全局变量,退出 shell 就会失效,所以直接在电脑上手动添加环境变量

快速起一个 http 服务

npm i http-server -g

起服务

http-server`

电脑无线调试手机上的网页

有时候我们想要知道,自己写的页面在手机浏览器上被打开是什么样子。

可以将电脑和手机连在同一 wifi 下(有些时候咖啡厅里的 wifi 不可以,可能是防火墙的问题),电脑上启动开发服务器后,在手机浏览器上输入 ip,就可以预览啦。

image.png

你还可以用 usb 数据线连接电脑,在电脑上调试手机上的页面。方便看到打印日志、页面元素、请求发送等。

但有时候感觉用数据线太麻烦了,能不能用电脑无线调试手机上的网页呢?是可以的!

需要使用到 adb(Android Development Bridage)。

详细步骤

  1. 电脑和手机连在同一网络下,手机打开无线调试
  2. 下载最新版的 Android 调试桥 (adb) developer.android.google.cn/studio/rele…
  3. 下载完解压后,在目录地址输入 cmd,打开命令行窗口

image.png

  1. 手机点击无线调试选项,点击使用配对码配对设备

9b4b619b4b65b1218ce7251082496e8.jpg

  1. 在电脑命令行输入上图,也就是配对码弹出弹框里的 ip 地址和端口号。然后按提示输入配对码(736001)。
adb pair ip:port
  1. 配对成功后,输入命令(注意这里的 ip:port 和上一步的不一样)
adb connect ip:port

image.png

  1. 查看链接的设备
adb devices -l 查看链接的设备
  1. 谷歌或 edge 打开调试页,进行调试。(可能需要稍等一会,才搜索得到设备)

chrome://inspect/#devices

edge://inspect/#devices

手机打开 edge 或者谷歌或者 app 内嵌的网页,等电脑搜索到。

image.png

fda0a182e275a1a5409e8e74466dbe4.jpg

image.png

无线调试总结

1、首先下载 adb。
2、手机电脑连在同一 wifi 下
3、手机启用开发者模式,打开无线调试,点击使用配对码配对设备
4、adb pair ip:port。(手机上显示的 ip:port)
5、adb devices -l 查看链接的设备
6、adb connect ip:port

使用下来,限制还是比较多。

首先使用 edge 只能调试在手机上用 edge 浏览器打开的网页。 谷歌能调试手机上用 edge 或谷歌打开的网页。

使用谷歌的话调试的话,如果手机浏览器版本太低又会出现点击 inspect 按钮后出现 HTTP/1.1 404 Not Found 的问题。要想调试,只能降低电脑端的浏览器版本了!

如果这些都不受限制,那就更好了!

参考

www.cnblogs.com/wenxuehai/p…

代码仓库地址

github.com/keer-tea/re…

- Book Lists -