前言
在此之前对 Rax.js 官方文档 进行 学习阅读 对 Rax 框架大致了解,之后进行实战项目的练习和熟悉,下是整个项目的源码仓库:
背景
因在线上实在找不到有关于 Rax.js 框架的实战项目教程或视频(收费除外),因基于 React.js 封装,故个人寻找了较为合适的 React 实战项目视频《通过React, Gatsby, GraphQL构建响应式旅游网站【中英字幕,分P】》,进行 Rax.js + Ts + eslint 简单重构,并且对每个页面功能点进行了详细的注释。
在期间同步进行着 React 官网文档的阅读,确实触类旁通。
项目总览
PC 端
移动端
主要目录
对项目主要目录进行解读
.
├── README.md
├── package.json
├── src // 主目录
│ ├── app.json
│ ├── app.ts
│ ├── assets // 静态文件目录
│ │ ├── images // 图片
│ │ │ ├── 1.jpg
│ │ │ ├── 2.jpg
│ │ │ ├── 21062901.jpeg
│ │ │ ├── 21062902.jpeg
│ │ │ ├── 21063001.jpeg
│ │ │ ├── 3.jpg
│ │ │ └── 4.jpg
│ │ └── svg // 导航栏 bar
│ │ └── bar.svg
│ ├── components // 头和尾组件
│ │ ├── Footer
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ └── Header
│ │ ├── index.module.css
│ │ └── index.tsx
│ ├── config.ts
│ ├── global.css // 全局样式
│ ├── pages // 页面目录
│ │ ├── Demo // 练手的小 Demo,与本项目无关
│ │ │ ├── components
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ ├── Email
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ ├── Hero
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ ├── Home // 主页面
│ │ │ └── index.tsx
│ │ ├── Stats
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ ├── Testimonials
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ └── Trips
│ │ ├── index.module.css
│ │ └── index.tsx
│ ├── typings.d.ts // ts 配置文件
│ └── utils // 工具方法(就一个嘿嘿)
│ └── index.ts
└── tsconfig.json
项目实战
进行项目实战之前,首先需要将 Rax.js 基础框架搭建好噢。
关于内置的配置个人选择的是
App (Build universal application)Web 单页面应用TypeScript
配置
app.json
应用配置路由信息
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
}
],
"window": {
"title": "柃木🎈"
}
}
global.css
全局样式配置
/* 全局样式 */
* {
font-family: 'Roboto', sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
a {
color: red;
}
p {
color: #000;
}
.button {
white-space: nowrap;
font-weight: bold;
color: #fff;
outline: none;
border: none;
min-width: 100px;
cursor: pointer;
text-decoration: none;
transition: 0.3s !important;
}
.button:hover {
transform: translateY(-2px);
}
.buttonPrimary {
background: #f26a2e;
}
.buttonPrimary:hover {
background: #077bf1;
}
.buttonNoPrimary {
background: #077bf1;
}
.buttonNoPrimary:hover {
background: #f26a2e;
}
.buttonBig {
padding: 16px 40px;
font-size: 20px;
}
.buttonNoBig {
padding: 10px 32px;
font-size: 16px;
}
.buttonRound {
border-radius: 50px;
}
.buttonNoRound {
border-radius: 4px;
}
.icon {
font-size: 1.4rem !important;
}
.color1 {
color: #047bf1;
}
.color2 {
color: #f3a82e;
}
.color3 {
color: #f34f2e;
}
.color4 {
color: #3af576;
}
typings.d.ts
用于 ts 声明文件类型(如图片类型)
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
// 配置 TS 文件后缀
declare module '*.jpg';
declare module '*.jpeg';
页面
Home
整个项目的容器
src/pages/Home/
index.tsx
import { createElement } from 'rax';
import View from 'rax-view';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import Hero from '../Hero';
import Trips from '../Trips';
import Testimonials from '../Testimonials';
import State from '../Stats';
import Email from '../Email';
function Home() {
// 项目容器
return (
<View id="home">
<Header />
<main>
<Hero />
<Trips />
<Testimonials />
<State />
<Email />
</main>
<Footer />
</View>
);
}
export default Home;
Header
项目头部导航栏
src/components/Header/
index.tsx
import { createElement, useEffect, useState } from 'rax';
import styles from './index.module.css';
import { routerLink } from '@/utils/index';
function Header() {
// 导航
const menuData: {
title: string;
link: string;
}[] = [
{
title: 'Trips',
link: 'trips',
},
{
title: 'About',
link: 'about',
},
{
title: 'Careers',
link: 'careers',
},
{
title: 'Contact',
link: 'contact',
},
];
// 监听事件
const [hasVerticalScrolled, setState] = useState(false);
useEffect(() => {
window.addEventListener('scroll', bindHandleScroll);
return () => {
window.removeEventListener('scroll', bindHandleScroll);
};
});
const bindHandleScroll = (event) => {
// 滚动的高度
const scrollTop =
(event.srcElement ? event.srcElement.documentElement.scrollTop : false) ||
window.pageYOffset ||
(event.srcElement ? event.srcElement.body.scrollTop : 0);
window.console.log(scrollTop, scrollTop > 100);
setState(() => scrollTop > 100);
};
return (
// 头部栏
<div className={`${styles.navContainer} ${hasVerticalScrolled ? styles.navContainer2 : ''}`}>
{/* 标签 */}
<div className={styles.navLink} onClick={() => routerLink('home')}>
EXPLORIX
</div>
{/* 响应式后 bar 图标 */}
<div className={styles.navBar} />
{/* navMenu */}
<div className={styles.navMenu}>
{menuData.map((item) => {
return (
<div className={styles.navLink} key={item.link} onClick={() => routerLink(item.link)}>
{item.title}
</div>
);
})}
</div>
{/* navBtn */}
<div className={styles.navBtn}>
<div className="button buttonPrimary buttonNoBig buttonRound" onClick={() => routerLink('trips')}>
Book a Flight
</div>
</div>
</div>
);
}
export default Header;
index.module.css
.navContainer {
background: transparent;
height: 80rpx;
display: flex;
justify-content: space-evenly;
padding: 0.5rem calc((100vw - 1300rpx) / 2);
z-index: 100;
position: relative;
}
.navContainer2 {
background: rgba(0, 0, 0, 0.4);
position: sticky;
top: 0;
}
.navLink {
color: #fff;
display: flex;
font-size: 20px;
align-items: center;
text-decoration: none;
padding: 0 2rem;
height: 100%;
cursor: pointer;
}
.navBar {
background: url('../../assets/svg/bar.svg') no-repeat;
background-size: 1.8rem;
display: block;
color: #fff;
}
@media screen and (max-width: 768px) {
.navBar {
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
right: 0;
transform: translate(90%, 40%);
}
}
.navMenu {
display: flex;
align-items: center;
margin-right: 48rpx;
}
@media screen and (max-width: 768px) {
.navMenu {
display: none;
}
}
.navBtn {
display: flex;
align-items: center;
margin-right: 24px;
}
@media screen and (max-width: 768px) {
.navBtn {
display: none;
}
}
src/utils/
index.ts
改变 hash 路由的跳转,个人重构为原生锚点跳转
/**
* @description 跳转锚点
* @param url
*/
export const routerLink = (url: string) => {
if (url) {
// 找到锚点
const anchorElement = document.getElementById(url);
// 如果对应id的锚点存在,就跳转到锚点
if (anchorElement) {
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' });
}
}
};
src/assets/svg/
bar.svg
Hero
主页的巨幕视频及项目介绍
src/pages/Hero/
index.tsx
import { createElement } from 'rax';
import { history } from 'rax-app';
import View from 'rax-view';
import styles from './index.module.css';
function Hero() {
return (
// 主页巨幕
<View id="hero">
<div className={styles.HeroContainer}>
{/* 背景视频 */}
<div className={styles.HeroBg}>
<video src="https://mp4.vjshi.com/2020-11-19/34b1f460a8a9fcc283c4edc8fe43b32f.mp4" type="video/mp4" autoPlay loop muted playsInline className={styles.VideoBg} />
</div>
{/* 内容 */}
<div className={styles.HeroContent}>
<div className={styles.HeroItems}>
<h1 className={styles.HeroH1}>前往梦想的道路上</h1>
<p className={styles.HeroP}>Out of this world!</p>
<span onClick={() => history.push('/trips')} className="button buttonPrimary buttonBig buttonNoRound">开始旅行</span>
</div>
</div>
</div>
</View>
);
}
export default Hero;
尝试引入本地背景视频,引入失败了。询问了对应的社区,建议是采用线上链接、CDN 等形式引入
index.module.css
.HeroContainer {
background: #0c0c0c;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
padding: 0 1rem;
position: relative;
margin-top: -80rpx;
color: #fff;
}
.HeroContainer::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 2;
background: linear-gradient(180deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.6) 100%),
linear-gradient(180deg, rgba(0,0,0,0.2) 0%, transparent 100%);
}
.HeroBg {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.VideoBg {
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
}
.HeroContent {
z-index: 3;
height: calc(100vh - 80rpx);
max-width: 100%;
padding: 0 calc((100vw - 1300rpx) / 2);
}
.HeroItems {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
height: 100vh;
max-height: 100%;
padding: 0;
color: beige;
line-height: 1.1;
font-weight: bold;
}
.HeroH1 {
font-size: clamp(1.5rem, 6vw, 4rem);
margin-bottom: 1.5rem;
letter-spacing: 3px;
padding: 0 1rem;
}
.HeroP {
font-size: clamp(1rem, 3vw, 3rem);
margin-bottom: 2rem;
color: #fff;
font-weight: 400;
}
Trips
旅游页面
src/pages/Trips/
index.tsx
import { createElement } from 'rax';
import View from 'rax-view';
import Image from 'rax-image';
import styles from './index.module.css';
import { Icon } from '@alifd/meet';
import img1 from '@/assets/images/1.jpg';
import img2 from '@/assets/images/2.jpg';
import img3 from '@/assets/images/3.jpg';
import img4 from '@/assets/images/4.jpg';
function Trips() {
// 景点
const trips: {
id: number;
img: any;
alt: string;
name: string;
button: string;
}[] = [
{
id: 1,
img: img1,
alt: 'Snow Mountain「雪山」',
name: 'Snow Mountain「雪山」',
button: 'View Destination',
},
{
id: 2,
img: img2,
alt: 'Snow Mountain「雪山」',
name: 'Snow Mountain「雪山」',
button: 'View Destination',
},
{
id: 3,
img: img3,
alt: 'BeiJi「极光」',
name: 'BeiJi「极光」',
button: 'View Destination',
},
{
id: 4,
img: img4,
alt: 'Church「教堂」',
name: 'Church「教堂」',
button: 'View Destination',
},
];
return (
<View id="trips">
<div className={styles.productsContainer}>
{/* 标题 */}
<div className={styles.productsHeading}>Our Favorite Destinations</div>
{/* 图片卡片 */}
<div className={styles.productsWrapper}>
{trips.map((item) => (
<div key={item.id} className={styles.productCard}>
{/* 图片 */}
<Image className={styles.productsImg} source={{ uri: item.img }} alt={item.alt} />
{/* 内容 */}
<div className={styles.productInfo}>
<div className={styles.textWrap}>
{/* icon */}
<Icon className="icon" type="heart-filling" />
<div className={styles.productTitle}>
{item.name}
</div>
</div>
{/* 按钮 */}
<span className={`${styles.tripBtn} button buttonPrimary buttonNoBig buttonRound`}>
{item.button}
</span>
</div>
</div>
))}
</div>
</div>
</View>
);
}
export default Trips;
不能在 jsx 中直接引入图片,使用 import 引入图片文件,并且因 ts 需设置声明图片文件
index.module.css
.productsContainer {
min-height: 100vh;
padding: 5rem calc((100vw - 1300rpx) / 2);
color: #fff;
}
.productsHeading {
font-size: clamp(1.2rem, 5vw, 3rem);
text-align: center;
margin-bottom: 5rem;
color: #000;
}
.productsWrapper {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 10px;
justify-items: center;
padding: 0 3rem;
}
@media screen and (max-width: 1200px) {
.productsWrapper {
grid-template-columns: 1fr 1fr;
}
}
@media screen and (max-width: 868px) {
.productsWrapper {
display: grid;
grid-template-columns: 1fr;
}
}
.productCard {
line-height: 2;
display: flex;
justify-content: center;
width: 100%;
height: 70vh;
position: relative;
border-radius: 10px;
transition: 0.2s ease;
}
.productsImg {
height: 100%;
max-width: 100%;
position: absolute;
border-radius: 10px;
filter: brightness(70%);
transition: 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.productsImg:hover {
filter: brightness(100%);
}
.productInfo {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 3rem;
}
@media screen and (max-width: 280px) {
.productInfo {
padding: 0 1.5rem;
}
}
.textWrap {
display: flex;
align-items: center;
position: absolute;
top: 360px;
}
.productTitle {
font-weight: 400;
font-size: 1rem;
margin-left: 0.5rem;
}
.tripBtn {
position: absolute;
top: 420px;
font-size: 14px;
}
src/assets/images/
旅游图片
Testimonials
客户评价推荐页面
src/pages/Testimonials/
index.tsx
import { createElement } from 'rax';
import View from 'rax-view';
import styles from './index.module.css';
import { Icon } from '@alifd/meet';
import Image from 'rax-image';
import img1 from '@/assets/images/21062901.jpeg';
import img2 from '@/assets/images/21062902.jpeg';
function Testimonials() {
// 图片
const images: {
id: number;
img: any;
}[] = [
{
id: 1,
img: img1,
},
{
id: 2,
img: img2,
},
];
return (
// 关于我们
<View id="about">
<div className={styles.testimonialsContainer}>
{/* 标题 */}
<div className={styles.topLine}>Testimonials</div>
<div className={styles.description}>What People are Saying</div>
{/* 容器 */}
<div className={styles.contentWrapper}>
<div className={styles.columnOne}>
{/* 描述1 */}
<div className={styles.testimonial}>
<Icon className={`${styles.color1} ${styles.icon}`} type="success" />
<h3>Ling Mu</h3>
<p>
You have unique gifts and talents that no one else in this world has. Sometimes we feel that we needto
be someone else in order to fit in be a better mother or wife or portray an image that we believele else
will love. No matter how hard you try to be someone else you will never be good enoughYou will do the
best and be the happiest only if you st op living by someone else's standards and startusing your
unique potential to shine like a light in this world
</p>
</div>
{/* 描述2 */}
<div className={styles.testimonial}>
<Icon className={`${styles.color2} ${styles.icon}`} type="favorites-filling" />
<h3>Qi Qiu</h3>
<p>
Sooner or later, the time comes when we all must become responsible adults and learn to give up what we
want, so we can choose to do what is right. Of course, a lifetime of responsibility isn't always
easy. And as the years go on, it's a burden that can become too heavy for some to bear. But still
we try to do what is best, what is good. Not only for ourselves, but for those we love. Yes, sooner or
later we must all become responsible adults. No one knows this better than the young.
</p>
</div>
</div>
{/* 图片 */}
<div className={styles.columnTwo}>
{images.map((item) => (
<div key={item.id}>
<Image className={styles.Images} source={{ uri: item.img }} />
</div>
))}
</div>
</div>
</div>
</View>
);
}
export default Testimonials;
不能在 jsx 中直接引入图片,使用 import 引入图片文件,并且因 ts 需设置声明图片文件
index.module.css
.testimonialsContainer {
width: 100%;
background: #fcfcfc;
color: #000;
padding: 5rem calc((100vw - 1300rpx) / 2);
height: 100%;
}
.topLine {
color: #077bf1;
font-size: 2rem;
padding-left: 6rem;
margin-bottom: 0.75rem;
}
.description {
text-align: start;
padding-left: 6rem;
margin-bottom: 4rem;
font-size: clamp(1.5rem, 5vw, 2rem);
font-weight: bold;
}
.contentWrapper {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 6rem;
}
@media screen and (max-width: 1200px) {
.contentWrapper {
grid-template-columns: 1fr;
}
}
.columnOne {
display: grid;
grid-template-rows: 1fr 1fr;
}
.testimonial {
padding-top: 1rem;
padding-right: 2rem;
}
h3 {
margin-bottom: 1rem;
font-size: 2rem;
font-style: italic;
}
p {
color: #3b3b3b;
font-size: 1.2rem;
}
.columnTwo {
display: grid;
grid-template-columns: 1fr 1fr;
margin-top: 2rem;
grid-gap: 10rpx;
}
@media screen and (max-width: 1200px) {
.columnTwo {
grid-template-columns: 1fr;
}
}
.Images {
border-radius: 10px;
height: 100%;
width: 100%;
}
.icon {
font-size: 2rem;
margin-bottom: 1rem;
}
.color1 {
color: #3fffa8;
}
.color2 {
color: #f9b19b;
}
src/assets/images/
State
特点页面
src/pages/State/
index.tsx
import styles from './index.module.css';
import { createElement } from 'rax';
import View from 'rax-view';
import { Icon } from '@alifd/meet';
function State() {
// Icon 图标
const statsData: {
id: number;
icon: string;
title: string;
desc: string;
}[] = [
{
id: 1,
icon: 'camera',
title: 'Over 100 Destinations',
desc: 'Travel to over 100 unique places',
},
{
id: 2,
icon: 'exit',
title: '1 Million Trips Made',
desc: 'Over 1 million trips completed last year',
},
{
id: 3,
icon: 'dashboard',
title: 'Fastest Support',
desc: 'Access our support team 24/7',
},
{
id: 4,
icon: 'smile',
title: 'Best Deals',
desc: 'We offer the best prices',
},
];
return (
// 特点
<View id="careers">
<div className={styles.statsContainer}>
{/* 描述 */}
<h1 className={styles.heading}>Why choose us ?</h1>
{/* Icon 容器 */}
<div className={styles.statsWrapper}>
{statsData.map((item, index) => {
return (
<div className={styles.statsBox} key={item.id}>
<Icon type={item.icon} className={`${styles.icon} color${index + 1}`} />
<p className={styles.title}>{item.title}</p>
<p className={styles.desc}>{item.desc}</p>
</div>
);
})}
</div>
</div>
</View>
);
}
export default State;
主要利用了 Icon
index.module.css
.testimonialsContainer {
width: 100%;
background: #fcfcfc;
color: #000;
padding: 5rem calc((100vw - 1300rpx) / 2);
height: 100%;
}
.topLine {
color: #077bf1;
font-size: 2rem;
padding-left: 6rem;
margin-bottom: 0.75rem;
}
.description {
text-align: start;
padding-left: 6rem;
margin-bottom: 4rem;
font-size: clamp(1.5rem, 5vw, 2rem);
font-weight: bold;
}
.contentWrapper {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 6rem;
}
@media screen and (max-width: 1200px) {
.contentWrapper {
grid-template-columns: 1fr;
}
}
.columnOne {
display: grid;
grid-template-rows: 1fr 1fr;
}
.testimonial {
padding-top: 1rem;
padding-right: 2rem;
}
h3 {
margin-bottom: 1rem;
font-size: 2rem;
font-style: italic;
}
p {
color: #3b3b3b;
font-size: 1.2rem;
}
.columnTwo {
display: grid;
grid-template-columns: 1fr 1fr;
margin-top: 2rem;
grid-gap: 10rpx;
}
@media screen and (max-width: 1200px) {
.columnTwo {
grid-template-columns: 1fr;
}
}
.Images {
border-radius: 10px;
height: 100%;
width: 100%;
}
.icon {
font-size: 2rem;
margin-bottom: 1rem;
}
.color1 {
color: #3fffa8;
}
.color2 {
color: #f9b19b;
}
src/assets/images/
邮箱页面
src/pages/Email/
index.tsx
import { createElement } from 'rax';
import View from 'rax-view';
import styles from './index.module.css';
function Email() {
return (
// 邮件
<View>
{/* 容器 + 图片 */}
<div className={styles.emailContainer}>
<div className={styles.emailContent}>
{/* 描述 */}
<h1 className={styles.emailH1}>Get Access to Exclusive Offers</h1>
<p className={styles.emailP}>Sign up for your newsletter below to get $100 off your first trip!</p>
{/* form 表单 */}
<div className={styles.formWrap}>
<label htmlFor="email">
<input type="email" placeholder="请输入您的邮箱" id="email" />
</label>
<span className="button buttonPrimary buttonBig buttonRound">注册</span>
</div>
</div>
</div>
</View>
);
}
export default Email;
index.module.css
.emailContainer {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.5) 35%,
rgba(0, 0, 0, 0.1) 100%
),
url('../../assets/images/21063001.jpeg') no-repeat center;
background-size: cover;
height: 650px;
width: 100%;
padding: 5rem calc((100vw - 1300px) / 2);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.emailContent {
display: flex;
flex-direction: column;
align-items: center;
}
.emailH1 {
text-align: center;
margin-bottom: 1rem;
font-size: clamp(1rem, 5vw, 3rem);
padding: 0 1rem;
}
.emailP {
text-align: center;
font-size: clamp(1rem, 2.5vw, 1.5rem);
padding: 0 1rem;
margin-bottom: 2rem;
color: #fff;
}
form {
z-index: 10;
}
input {
padding: 1rem 1.5rem;
outline: none;
width: 450px;
height: 60px;
border-radius: 50px;
border: none;
margin-right: 1rem;
}
@media screen and (max-width: 768px) {
.formWrap {
display: flex;
flex-direction: column;
padding: 0 1rem;
}
input {
margin-bottom: 1rem;
width: 100%;
margin-right: 0;
}
}
@media screen and (max-width: 768px) {
.button {
width: 100%;
min-width: 450px;
}
}
src/assets/images/
邮箱图片
Footer
项目底部栏
src/components/Footer/
index.tsx
import { createElement } from 'rax';
import View from 'rax-view';
import styles from './index.module.css';
function Footer() {
return (
// 底部栏
<View id="contact">
<div className={styles.footerContainer}>
{/* 第一栏 */}
<div className={styles.footerLinkWrapper}>
{/* 标签 */}
<div className={styles.footerDesc}>
<h1>Expel</h1>
<p>We strive to create the best experiences for our customers</p>
</div>
<div className={styles.footerLinkItems}>
<h2 className={styles.footerLinkTitle}>联系我们</h2>
<div className={styles.footerLink}>
Contact
</div>
<div className={styles.footerLink}>
Support
</div>
<div className={styles.footerLink}>
Destinations
</div>
<div className={styles.footerLink}>
Sponsorships
</div>
</div>
</div>
{/* 第二栏 */}
<div className={styles.footerLinkWrapper}>
<div className={styles.footerLinkItems}>
<h2 className={styles.footerLinkTitle}>音频</h2>
<div className={styles.footerLink}>
Submit Video
</div>
<div className={styles.footerLink}>
Ambassadors
</div>
<div className={styles.footerLink}>
Agency
</div>
<div className={styles.footerLink}>
Influencer
</div>
</div>
<div className={styles.footerLinkItems}>
<h2 className={styles.footerLinkTitle}>社区内容</h2>
<div className={styles.footerLink}>
博客
</div>
<div className={styles.footerLink}>
掘金
</div>
<div className={styles.footerLink}>
知乎
</div>
<div className={styles.footerLink}>
WeChat
</div>
</div>
</div>
</div>
</View>
);
}
export default Footer;
index.module.css
.footerContainer {
padding: 5rem calc((100vw - 1100px) / 2);
display: grid;
grid-template-columns: repeat(2, 1fr);
color: #000;
background: #fafafb;
font-size: 14px;
}
.footerDesc {
padding: 0 2rem;
}
.footerDesc h1 {
margin-bottom: 3rem;
color: #f26a2e;
}
@media screen and (max-width: 400px) {
.footerDesc {
padding: 1rem;
}
}
.footerLinkWrapper {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 820px) {
.footerLinkWrapper {
grid-template-columns: 1fr;
}
}
.footerLinkItems {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 1rem 2rem;
}
@media screen and (max-width: 400px) {
.footerLinkItems {
padding: 1rem;
}
}
.footerLinkTitle {
font-size: 18px;
margin-bottom: 16px;
}
.footerLink {
text-decoration: none;
margin-bottom: 0.5rem;
font-size: 16px;
color: #3d3d4e;
}
.footerLink:hover {
color: #f26a2e;
transition: 0.3s ease-out;
}
后言
最终项目
当以上内容完成后,再进行一次项目的最终展示效果:
PC
移动
视频教程技术栈是 React, Gatsby, GraphQL ,在期间用了一些技巧,对这个项目进行 Rax 的重构,以及一些 React 的周边插件,也都是用 Rax 官网文档例子进行对应的替换。
在期间主要熟悉了 JSX 语法,部分 Hooks 使用,ts 语法以及 Rax 的基础内容。但薄弱的地方在于几个方面:
- 各组件的通信,数据流等
- Rax 的后端网络通信
- 测试(自己对这一块十分欠缺)
- 构建部署
个人认为后续可以优化的地方:
- css 转为 sass
- 锚点的过渡应使用 tranform 等,尝试过,但没有很好地实现出来
- Home 应转为一个真正地 layout 布局
- 进一步组件模块化
- 打包构建(薄弱)
最后感想对于学好 Rax 需要打好 React 基础,虽然有许多触类旁通的地方,但还是需要自己一丝不苟地学习,学完还得要在适当的场景下会用才行。