Skip to content

(施工中...👷)上海交通大学课程“互联网应用开发技术(SE2321)” 的前端 demo 项目,供同学们学习参考。

Notifications You must be signed in to change notification settings

Okabe-Rintarou-0/BookStore-Frontend

Repository files navigation

电子书城前端

该 repo 为上海交通大学课程“互联网应用开发技术(SE2321)” 的前端 demo 项目,供同学们学习参考。 欢迎点亮✨,发表你的 issue 或 pr,为课程建设贡献一份力。如果你想参与问题、技术讨论,欢迎使用本 repo 的讨论模块

UI 设计上,在不删减基本功能的前提下,保证尽可能的简洁明了。项目主要使用 Ant Design 框架,请参考 Ant Design 5.0 学习各类组件使用方法。

各类文档 👉 文档索引课程要求

⚠️注意

为了便于同学们专注开发前端,我们提供了后端服务。但是由于你们本地的前端(localhost)到我们的远程后端属于是跨站调用,这就会引发跨站传递 Cookies 的问题:Cookie Samesite简析Chrome 浏览器在不需要更改偏好设置的情况下就能正常使用,而 EdgeSafariSameSite 识别解析有一定问题,所以我们推荐使用 Chrome

部分浏览器不支持部分SameSite=none。IOS 12 的 Safari 以及老版本的一些 Chrome 会把 SameSite=none 识别成 SameSite=Strict,所以服务端必须在下发 Set-Cookie 响应头时进行 User-Agent 检测,对这些浏览器不下发 SameSite=none 属性。

上面的问题你暂时看不懂也不要紧。请注意,后续你们在本地进行开发的时候没有任何(主流的)浏览器限制,只不过每一种浏览器 UI 会有些许变化。你们本地开发前后端也不会有跨站问题(localhost:3000 -> localhost:8080)。

什么是前端

一个最简单的前后端架构图:

前端一词是指用户可以直接与之交互的图形用户界面(GUI),例如导航菜单、设计元素、按钮、图像和图表。采用技术术语,用户看到的带有多个 UI 组件的页面或屏幕称为文档对象模型(DOM)。—— 应用程序开发中的前端和后端之间有什么区别?

项目结构

├── ...
├── Dockerfile          Docker 镜像构建文件
├── package-lock.json   记录当前安装的 npm 包的确切版本
├── package.json        包含项目的元数据和依赖项信息
├── public              存放静态资源,如 HTML 文件、图片等
└── src                 包含项目的源代码
    ├── App.js          项目的根组件
    ├── components      React 组件
    ├── css             css 样式文件
    ├── index.js        项目的入口文件
    ├── lib             工具函数、工具类
    ├── page            包含不同页面的组件,如首页、购物车页面
    ├── service         后端交互的服务或 API 请求的代码
    └── utils           通用的工具函数或帮助函数

IDE 选择

一般而言开发前端没有什么“非它不可”的 IDE。我个人推荐 vscode(配合一些插件, 如 EsLint, Es7+ 就已经可以很方便地开发了)。如果你对 jetbrains 公司的产品有执念,那么用 webstorm 之类的也可以。

环境配置

请下载最新版本的 nodejs(21.x)。如果你使用的是 macOS,建议使用 Homebrew 安装。 如果你学过 python 的话,你应该清楚 pip 是一个功能强大的包管理器。与其类似,你可以选择 npm 或者 yarn 作为 node 的包管理器。

通过以下命令,你可以轻松启动前端项目(推荐):

# 使用 npm
npm install && npm start

# 使用 yarn
yarn install && yarn start

对于 npm 下载速度慢的问题,可以考虑使用淘宝源、腾讯源或者 SJTUG 镜像源,具体换源命令请自行检索,推荐使用 nrm 工具管理源。

npm install 下载项目所需的依赖(只需执行一次即可,后续无需再执行);npm start 则会启动前端项目。与 Makefile 类似,你也可以在 package.json 中定义自己的指令(你可以由此发现 npm start 是如何启动项目的):

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
},

请确保进入 package.json 文件所在的目录后再执行上述命令。如果一切顺利,你会发现当前目录下生成了一个名为 node_modules 的目录,这里面是下载的第三方库(项目依赖)。启动后,你可以在 http://localhost:3000 访问前端。

或者,使用 Docker 一键部署(仅测试用):

docker run -itd -e REACT_APP_BASE_URL=后端服务器URL -p 3000:3000 --name bookstore-frontend 923048992/frontend

如果你使用的是 macOS,请使用 923048992/frontend:mac 镜像。

如果你希望快速新建一个 React 项目,请参考:create-react-app 或者 vite

后端 API

我们开源了完整的前端项目,但是为了防止同学们照抄后端代码,所以只提供了后端 API 及其文档(你可以在启动前端后在 http://localhost:3000/api-docs 查看文档)。后端 API 采用 RESTful 形式。请在 .env 文件中修改环境变量 REACT_APP_BASE_URL 的值为我们提供的后端服务器的 URL。

如果你想调试 API,请查看后端 API 使用指南。如果你想在前端中调用后端 API,请参考:Sending a request with credentials included 等文档。

如果你使用 vite 来创建自己的项目,需要注意项目源代码之中

export const BASEURL = process.env.REACT_APP_BASE_URL ?? 'http://localhost:8080';

是设置了使用npm启动时的环境变量,你需要将其修改成:

export const BASEURL = import.meta.env.VITE_REACT_APP_BASE_URL ?? "http://localhost:8080";

的形式,并将所有在 .env 之中定义的环境变量加上 VITE_ 前缀以向 vite 标识。

请注意,如果你已经启动了前端项目,请先 CTRL + C 强制终止前端进程,然后再次重启,修改的环境变量才会生效。在校外的同学可能需要开启 SJTUvpn 才能正常访问后端。

我们暂时不提供注册功能,你可以使用我们给定的账号密码登录,并按照如下方式修改密码:

学习指南

本课程主要学习 React 框架,学有余力的同学也可以尝试一下 Vue。如果你感兴趣,这边也提供了 Vue 版本以供参考:BookStore-Frontend-Vue

函数式组件

React 使用 JSX 来定义组件,支持两种方式:类组件和函数式组件。由于便利性,现在大家倾向于使用后者进行开发。函数式组件最令人头疼的就是各种 hooks(钩子函数)。所以学会各种 hooks 的用法是学好 React 的关键。

  • useState:

    State 是 React 中最核心的理念。当一个组件的状态发生改变的时候,React 会对其进行重新渲染。当我们希望进行交互,或者是渲染动态组件,我们就会用到状态。useState 会返回一个元组,第一个元素是状态变量。第二个则是设置该状态变量的函数(详见链接)。

    常见误区:

    • Q:为什么我修改了状态,但是页面没有重新渲染?

      const [count, setCount] = useState(1);
      return <>
          <button onClick={()=>{count+=1;}}>Increase</button>
          <p>Count: {count}</p>
      </>

      A:只有通过 setCount 函数修改变量才会触发重新渲染。

    • Q:我明明用了 set 函数,为什么依然没有渲染?

      const [tags, setTags] = useState([]);
      return <>
          <button onClick={()=>{
              tags.push("tag");
              setTags(tags);
          }}>Add tag</button>
          {tags.map(tag => (<p>{tag}</p>))}
      </>

      A:tags 的地址没有改变(数组和对象是引用类型),所以 React 认为它没有被修改。 正确做法:

      setTags([...tags, "tag"]);
  • useEffect:提供生命周期管理、在依赖项修改时触发。

    如果你希望在组件挂载之后做一些事情(例如初始化工作),你可以将第二个参数依赖项设置为空数组,这样就等同于类组件的 componentDidMount 生命周期函数。

    useEffect(()=>{
        // do something here
    }, []);

    If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.

    如果你希望监视某些值的变化,那就加入到依赖项中(这一功能非常重要,你可以从链接中的例子看到这一点):

    useEffect(() => {
        // do something when someVar changes
    }, [someVar]);

    你可以返回一个函数用于进行组件 unmount 时的额外处理。

    useEffect(() => {
        return () => {
            // do some cleanup here
        };
    });

    常见误区:

    • 死循环: 以下代码将产生死循环,请注意依赖是否正确使用。
      useEffect(() => {
          setSomeVar(newValue);
      }, [someVar]);
      在没有依赖情况下,如果你在 useEffect 中修改状态,就会触发再次渲染,useEffect 又会被调用,形成死循环。
      useEffect(() => {
          setSomeVar(newValue);
      });
  • useRef:帮助引用一个不需要渲染的值,见链接介绍。

  • useMemo:在每次重新渲染的时候能够缓存计算的结果,见链接介绍。

  • useLocation:获取前端当前 URL.

  • useSearchParams:获取 URL query 参数。

  • useNavigate:用于进行页面跳转。

  • useParams:获取 URL 路径参数(例如 "/path/:id",你可以通过以下方式获取该参数)

    const {id} = useParams(); 

异步函数

异步函数也是一大重点,建议大家学习好两种方式:回调函数await 方式,本项目均采用后者,避免过多回调函数产生不美观和“回调地狱”。 可以参考:

闭包陷阱

当你在箭头函数中发现一个变量怎么样都拿不到最新的值的时候,很可能是因为你遇到了“闭包陷阱”。 请参考:一文讲透 React Hooks 闭包陷阱

解决方案:使用 useRef

避免 CSS 冲突

请参考:react 中 css modules-基本使用

依赖添加

依赖添加指南

养成良好的习惯

Tricks

  • 字符串格式化

    const num = 123;
    const str = `number is ${num}`;

    这将产生字符串"number is 123"。

  • 简洁的条件渲染

    {shouldShow && <SomeComponent/>}

    shouldShow 是一个 bool 类型的变量,当且仅当其为真,渲染后面的组件。这种写法最为简洁,也不失可读性。

  • bool 类型的组件属性可以简写

    // case 1
    <Modal open />
    // equals to
    <Modal open={true} />
    
    // case 2
    <Modal />
    // equals to
    <Modal open={false} />
  • 便捷的函数式编程

    基本上函数式都会支持 mapfilterreduce 操作。下面的代码将产生一系列组件。

    function ArrayComponents({ array }) {
        return array.map(item => <div>{item}</div>);
    }    
    <ArrayComponents array={[1, 2, 3]}/>

    这三种函数非常常用,尤其是 map,请熟练掌握其用法,增加编程效率。

  • 箭头函数

    基本上可以完美代替 function,编码方便(还有一个好处是不容易出现this指针相关的问题)。注意返回类型如果是如下形式(返回一个对象,因为JSX语法对{}的解释有函数体和对象的二义性)请加括号以标识返回为对象:

    let func = book => ({
        id: book.id,
        title: book.title
    });

    正如上述例子所示,只有一个参数的情况下参数可以不加括号。

  • Javascript 中的 ? 运算符

    const func = (obj) => {
        console.log(obj?.id);
    };
    
    func({id: 1});   // 输出 1
    func(null);      // 输出 undefined
    func([]);        // 输出 undefined
    func(undefined); // 输出 undefined
    
    // 调用函数
    let handleFunc = null;
    handleFunc();    // 报错:Uncaught TypeError: handleFunc is not a function
    handleFunc?.();  // 无报错且无任何效果
    
    handleFunc = () => {
        console.log("HELLO WORLD!");
    };
    handleFunc?.(); // 无报错且函数被执行,输出 HELLO WORLD!
    
    // 支持链式
    let someAttr = obj?.nested?.attr;
  • Javascript 中的 ?? 运算符

    作用:

    • 当左侧的操作数为 nullundefined 时,返回右侧的操作数。
    • 否则,返回左侧的操作数。

    请明确其与 && 效果的差异:

    • 当左侧和右侧的操作数都为真时,返回右侧的操作数。
    • 否则,返回左侧的操作数。
    let NULL = null;
    let UNDEFINED = undefined;
    let STRING = "Hello World!";
    let STRING2 = "I Like React!";
    
    console.log(NULL ?? STRING);      // 输出 Hello World!
    console.log(UNDEFINED ?? STRING); // 输出 Hello World!
    console.log(STRING ?? STRING2);   // 输出 Hello World!
    console.log(STRING2 ?? STRING);   // 输出 I Like React!
    
    
    console.log(NULL && STRING);      // 输出 null
    console.log(UNDEFINED && STRING); // 输出 undefined
    console.log(STRING && STRING2);   // 输出 I Like React!
    console.log(STRING2 && STRING);   // 输出 Hello World!
  • Javascript 中的 ... 运算符

    作用在数组上就是将数组展开:

    let array = [1, 2, 3];
    let newArray = [...array, 4]; // [1, 2, 3, 4]

    作用在对象上:

    let obj = {
        id: "1"
    };
    let newObj = {
        ...obj,
        name: "object"
    };

    等价于:

    let newObj = {
        id: "1",
        name: "object"
    };

    顺带一提,如果变量和对象的 key 名字相同,则可以简化赋值表达:

    let book = {
        title: "chiikawa"
    };
    let title = "chiikawa2";
    book = {
        title
    };

插件推荐

  • React1s: 点击页面元素跳转到编辑器。

一些示例

进阶

致谢

感谢以下用户为本仓库做出的贡献:

Ayanami1314
Ayanami1314

Star History

Star History Chart

About

(施工中...👷)上海交通大学课程“互联网应用开发技术(SE2321)” 的前端 demo 项目,供同学们学习参考。

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages