Tutorials
Introduction
Shuvi.js is a front-end application development solution that integrates modern tool chains, focuses on improving development experience and efficiency.
Create your first app
We are going to make a simple blog demo through a series of operations.
We will learn the following:
- init project
- use page route
- data fetching
- use dynamic route
- use layout route
- use css modules
- how to deploy
System Requirements
Create a project
Initialize a new Shuvi project.
npm init shuvi@latest
We'll call it "blog-tutorial" but you can call it something else if you'd like.
✔ What is your project named? blog-tutorial
Let's start the dev server:
npm run dev
Create a route
notice: In the convention routing specification, all our routing endpoint files are placed in the src/routes directory.
We want to create a component handle to the /posts
path.
Create the src/routes/posts
directory.
mkdir -p src/routes/posts
Create the page.js
file in the src/routes/posts
directory.
The file content of page.js is:
export default function PostsPage() {
return <div>posts</div>;
}
Now visit http://localhost:3000
and you will see the PostsPage
component.
Use loader data
Create the posts-mock-data.js
file in the src
directory.
export const postsMockData = [
{
id: "1",
title: "First-post",
content: "First-post-content",
},
{
id: "2",
title: "Second-post",
content: "Second-post-content",
},
];
Modify the content of the src/routes/posts/page.js
file to:
Notice: Loaders can be synchronous or asynchronous
import { useLoaderData, Link } from "@shuvi/runtime";
import { postsMockData } from "../../posts-mock-data";
export default function PostsPage() {
const data = useLoaderData();
return (
<div>
<ul>
{data.posts.map((post) => {
return (
<li key={post.id}>
<Link>{post.title}</Link>
</li>
);
})}
</ul>
</div>
);
}
export const loader = () => {
return new Promise((res) => {
setTimeout(() => {
res({
posts: postsMockData,
});
}, 1000);
});
};
Dynamic route
Next, we need to access the details of a post.
Create the page.js
file in the blog/src/routes/posts/[id]
directory.
import { useLoaderData } from "@shuvi/runtime";
import { postsMockData } from "../../../posts-mock-data";
export default function PostPage() {
const { id, title, content } = useLoaderData();
return (
<div>
<h1>{title}</h1>
<h2>ID:{id}</h2>
<p>{content}</p>
</div>
);
}
export const loader = (ctx) => {
const { id } = ctx.params;
return postsMockData.find((item) => item.id === id);
};
Notice: When
[id]
is used as a folder name, it will become a dynamic path rule.
When we visit /posts/1 or /posts/2, we will get the correct post content.
Layout route
We also need a component to control nested routes as a common layout component to avoid repeated rendering.
Change src/routes/post/page.js to src/routes/post.layout.js, and modify the file content as:
import { useLoaderData, Link, RouterView } from "@shuvi/runtime";
import { postsMockData } from "../../posts-mock-data";
export default function PostsLayout() {
const data = useLoaderData();
return (
<div>
<ul>
{data.posts?.map((post) => {
return (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
);
})}
</ul>
<div>
<RouterView />
</div>
</div>
);
}
export const loader = () => {
return new Promise((res) => {
setTimeout(() => {
res({
posts: postsMockData,
});
}, 1000);
});
};
Assets
Create the src/assets directory and place two image files at will.
Modify the content of the src/post-mock-data.js
file to:
import img1 from "./assets/img1.png";
import img2 from "./assets/img2.png";
export const postsMockData = [
{
id: "1",
title: "First-post",
content: "First-post-content",
img: img1,
},
{
id: "2",
title: "Second-post",
content: "Second-post-content",
img: img2,
},
];
Modify the content of the src/routes/posts/[id]/page.js
file to:
import { useLoaderData } from "@shuvi/runtime";
import { postsMockData } from "../../../posts-mock-data";
export default function PostPage() {
const { id, title, content, img } = useLoaderData();
return (
<div>
<h1>{title}</h1>
<h2>ID:{id}</h2>
<p>{content}</p>
<p>
<img src={img} alt="" />
</p>
</div>
);
}
export const loader = (ctx) => {
const { id } = ctx.params;
return postsMockData.find((item) => item.id === id);
};
Revisit the post page and you can see that the image has been loaded.
CSS modules
Create the style.css
file in the src/routes/posts
directory.
* {
padding: 0;
margin: 0;
}
.headerNav {
display: flex;
color: yellow;
border-bottom: 1px solid #f1f1f1;
}
.headerNav li {
list-style: none;
cursor: pointer;
padding: 12px;
border-right: 1px solid #f1f1f1;
}
.headerNav li a {
text-decoration: none;
color: #00a4db;
}
.headerNav li:hover a {
color: #2d66c3;
}
.mainContent {
margin: 20px 40px;
padding: 20px;
}
.mainContent h1,
.mainContent h2 {
text-align: center;
font-weight: 400;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 20px;
padding-bottom: 12px;
}
.mainContent p:first-of-type img {
width: 100%;
object-fit: cover;
height: 200px;
}
.mainContent p:last-of-type {
padding: 20px;
border: 1px solid #f1f1f1;
font-size: 14px;
font-weight: 200;
}
Modify the content of the src/routes/posts/layout.js
file to:
import { useLoaderData, Link, RouterView } from "@shuvi/runtime";
import { postsMockData } from "../../posts-mock-data";
import styles from "./style.css";
export default function PostsLayout() {
const data = useLoaderData();
return (
<div>
<ul className={styles.headerNav}>
{data.posts?.map((post) => {
return (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
);
})}
</ul>
<div className={styles.mainContent}>
<RouterView />
</div>
</div>
);
}
export const loader = () => {
return new Promise((res) => {
setTimeout(() => {
res({
posts: postsMockData,
});
}, 1000);
});
};
Deploy
npm build
npm serve