前言
前面介绍了 React前端 项目的构建,这这里实战一下小项目,分别是 javascript 和 typescript 版本的各一个,就只讲解一个 typescript 的版本,JavaScript 相信很容易看明白
这里的项目实战介绍,用的是 typescript 测试案例,和 javascript的测试案例一样,javascript的仅仅比 typescript的少了类型代码
自己根据需要选择使用,为了项目的易读性,且避免一些编译器就能排除的错误,推荐使用typescript编写项目
另外,typescript 就是类型化的 javascript,很容易上手,不要担心太多
注:本篇文章适会 flex布局,会React,但是没项目开发过的人入门开发项目参考用的
项目实战
先看一下效果图(以前做的一个小项目删减改编,有点丑😂)
这篇文章中用了 React-Component 和 React-hooks两种方式创建组件,以便与自己选择,通常交互比较多,需要用到组件多个生命周期的,直接使用 React-Component 即可,其他情况两者都行,不过个人感觉 React-hooks 平时使用要简单方便一些
这里主要是项目展示,介绍比较粗糙,主要是给一个案例代码,推荐下载下来查看
首页
从上面的图可以看到,首页布局分为 顶部、左侧、右侧三部分,由于顶部比较简单,就写到首页布局中了,而左右两侧,分别写到了布局组件中,且分别以 React-component 和 React-hooks的方式辨别
首页的布局如下所示
import { useEffect, useState } from 'react'; //react中自带的 hooks
import {toggleFull} from 'be-full'; //切换屏幕框架
import LeftView from './leftView';
import RightView from './rightVIew';
import { useNavigate } from 'react-router-dom'; //react-router-dom路由框架,可查看前言链接
//这个是声明的list的内部item类型,js中并不需要
export interface listType {
id: number
title: string
text: string
number: number
unit: string
status: number
}
function HomeView() {
//前面两个分别是获取和设置属性的方法,可以用更新UI和更新数据
//useState后面的是变量类型以及赋初值
const [isLoading, setIsLoading] = useState<boolean>(true);
const [list, setList] = useState<Array<listType>>([{
id: 0,
title: '斗罗大陆',
text: '累计观看:',
number: 70,
unit: '亿次',
status: 1, //0下架 1热播中 2等待上映
}]);
//这个是声明的路由,用于跳转路由
const navigate = useNavigate()
//这个相当于 componentDidMount,后面参数得设置空集合
useEffect(() => {
setTimeout(() => {
setIsLoading(false)
}, 1500);
}, []) //这个集合中,如果放了某一个状态变量,当变量改变时会回调这个useEffect
//进入详情页,通过路由,跳转到指定path
const enterDetail = () => {
navigate('/home/detail')
}
//react UI 节点布局
return (<div>{
isLoading ? (
//loading gif加载中的代码,外层是flex布局,为了让gif图片窗口居中
<div
style={{
position: 'absolute',
width: '100vw',
height: '100vh',
display: `${isLoading ? 'flex' : 'none'}`,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
background: '#000000'
}}
//加载gif图片
<img alt='' src="../currency_loading.gif" style={{ width: '50%' }} />
</div>
) : (
<div style={{userSelect: 'none', pointerEvents: 'none'}}>
//左侧布局组件,后面是props传参
<LeftView list={list} />
//右侧布局组件,其中的参数是React中的属性
<RightView
list={list}
updateListCallback={(list) => {
setList(list)
}}
enterDetailCallback={enterDetail}
/>
//顶部布局文件,也是一个绝对布局 + flex
<div
onDoubleClick={() => {
toggleFull() //切换全屏或者恢复,使用了 be-full框架
}}
style={{
width: '100%',
position: 'absolute',
backgroundImage: 'linear-gradient(to bottom, #000000DD, #000000BB)',
height: 60,
top: 0,
left: 0,
pointerEvents: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<div style={{color: '#FFF'}}>奇葩电影控制台</div>
</div>
</div>)
}
</div>)
}
export default HomeView
设置背景图片
底部空白,需要设置图片的话可以通过设置背景图片的方式来解决
//url 布局位置上下、左右 固定方式默认fixed 覆盖方式cover最短方向填充满 no-repeat不重复
background: `url(${process.env.PUBLIC_URL}/home_bkg.png) center center / cover no-repeat`
左侧组件Component
左侧信息栏使用的是常见的 React-component组件,拥有 React 组件的正常生命周期,如下所示
//设置接口 props传递参数的类型
interface LeftProps {
list: Array<listType> //如果想加入回调,直接放一个Function类型的callback属性接口即可
}
//后面需要实现LeftProps接口,以便于访问,跟其他语言的很像
export default class LeftView extends React.Component<LeftProps> {
state = {
list: this.props.list
}
//组件加载完毕时
componentDidMount() {
console.log('组件加载完毕了,在这里可以做组件自身内容')
}
//组件更新时
componentDidUpdate(prevProps: Readonly<LeftProps>, prevState: Readonly<{}>, snapshot?: any) {
if (prevProps.list === this.props.list) return
//通过setState方法,更新 state 状态机中的参数,可以触发list所在节点重新渲染
this.setState({
list: this.props.list
})
}
//组件将要卸载时
componentWillUnmount() {
console.log('组件即将被释放')
}
//下面是一个常见的flex布局
render() {
const {list} = this.props
return (
<div style={{
position: 'absolute',
backgroundColor: "#000000BB",
top: 60,
left: 0,
display: 'flex',
flexDirection: 'column',
height: "100%",
minWidth: 370,
userSelect: 'none',
pointerEvents: 'auto' //设置点击事件,如果父节点关闭,子节点需要开启此属性可响应点击事件
}}>
<div
style={{
marginTop: 20,
marginLeft: 24,
marginRight: 30,
display: 'flex',
height: "90%",
overflowY: 'auto'
}}>
<div style={{
display: 'flex',
minHeight: 940,
alignItems: 'flex-end'
}}>
//布局列表,看了布局可能会觉得,一个集合横着罗列不就可以了么,确实可以
//这里只是当时方便解决右侧数字文字过大,向上偏移的问题,有些电脑没事,这里有改动
//也间接说明了,同样的效果,有多种布局方式,或者解决方案
<div style={{ display: 'flex', flexDirection: 'column' }}>
{
list && list.length > 0 &&
list.map((item, index) => {
return (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
marginBottom: 14,
height: 80,
}}>
<div style={{ fontSize: 24, color: '#A7FAFF' }}>
{item.title}</div>
<div style={{ fontSize: 17, color: '#FFF',
marginTop: 13 , marginBottom: 8}}>
{item.text}</div>
</div>
)
})
}
</div>
//这里是右侧的数字和单位文字
<div style={{ display: 'flex', flexDirection: 'column',
minWidth: 180, alignItems: 'flex-end' }}>
{
list && list.length > 0 &&
list.map((item, index) => {
return (
<div
key={index}
style={{
marginLeft: 4,
display: 'flex',
alignItems: 'flex-end',
marginBottom: 13,
height: 80
}}>
<div style={{ color: '#50E7F9',
fontSize: 64, fontFamily: 'LcdD' }}>
{item.number}</div>
<div style={{ color: '#FFF', fontSize: 17,
marginLeft: 6, marginBottom: 8}}>
{item.unit}</div>
</div>
)
})
}
</div>
</div>
</div>
</div>
)
}
}
右侧组件Hooks
右侧组件采用了 React-hooks,组件代码比较长,贴出一部分介绍,推荐下载下来代码观看
使用了 hooks 之后,会发现里面不使用 this了,相当于访问的都是自己函数内部的方法,使用很方便
//设置属性,和component一样,这里同时设置了回调
interface RightProps {
list: Array<listType>
updateListCallback?: (list: Array<listType>) => any
enterDetailCallback?: Function //可以理解任意类型的函数
}
//传递属性的方式如下所示,和component不一样了
const RightView = (props: RightProps) => {
//通过 useState来触发组件渲染
const [list, setList] = useState<Array<listType>>(props.list)
const [isShowSimulate, setIsShowSimulate] = useState<boolean>(true)
//react-router-dom中跳转路由使用
const navigate = useNavigate()
useEffect(() => {
console.log('组件加载完毕了,在这里可以做组件自身内容')
}, [])
//当 props发生改变时,更新内容
useEffect(() => {
//更新list
setList(props.list)
}, [props.list])
//这里只介绍UI是哪里的
return (
//外层布局,控制整体位置
<div
style={{
position: 'absolute',
background: "#000000BB",
top: 60,
right: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
height: "100%",
width: 360,
userSelect: 'none',
pointerEvents: 'auto',
}}>
//设置内部滚动效果
<div
style={{
height: "90%",
overflowY: 'auto'
}}>
<div style={{
marginRight: 24,
display: 'flex',
alignItems: 'flex-end',
flexDirection: 'column',
}}>
//设备控制模块代码
<div style={{
display: 'flex',
width: 300,
height: 40,
marginTop: 20,
alignItems: 'center',
backgroundImage: 'linear-gradient(to right, rgba(28, 83, 112, 1), rgba(28, 83, 112, 0))'
}}>
<div style={{ fontSize: 24, fontWeight: 'bold', color: '#FFF', marginLeft: 16 }}>设备控制</div>
</div>
<div style={{
display: "flex",
marginTop: 20
}}>
//下架按钮
<div
onClick={() => {
let newList = list.map(item => {
item.status = 0
return item
})
setList(newList)
props.updateListCallback && props.updateListCallback(newList)
}}
style={{
cursor: "pointer",
width: 114,
height: 40,
marginRight: 16,
backgroundImage: 'linear-gradient(to bottom, #A7FAFF, #0EBBBE)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{ fontSize: 20, color: '#0B1017' }}>一键下架</div>
</div>
//上架按钮
<div
onClick={() => {
let newList = list.map(item => {
item.status = 1
return item
})
setList(newList)
props.updateListCallback && props.updateListCallback(newList)
}}
style={{
width: 114,
height: 40,
cursor: "pointer",
backgroundImage: 'linear-gradient(to bottom, #A7FAFF, #0EBBBE)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{ fontSize: 20, color: '#0B1017' }}>一键上架</div>
</div>
</div>
//开关顶部说明代码
<div style={{
display: 'flex',
marginTop: 20,
marginBottom: 6
}}>
<div style={{
display: 'flex',
marginRight: 40,
justifyContent: 'flex-end',
alignItems: 'center'
}}>
<div style={{ color: '#FFF', fontSize: 20,
fontWeight: 'bold' }}>机器名称</div>
</div>
<div style={{
display: 'flex',
width: 160,
height: 40,
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{ color: '#FFF', fontSize: 20,
fontWeight: 'bold' }}>运行开关</div>
</div>
</div>
//开关代码,通过集合纵向展开
//可以发现使用 hooks 渲染不需要this.state了
{
list && list.length > 0 &&
list.map((item, index) => {
return (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
}}>
<div style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
height: 44,
marginRight: 40,
}}>
{
item.status > 1 &&
<div style={{
display: 'flex',
flexDirection: 'row',
marginRight: 9,
}}>
{
item.status > 1 &&
<img
alt=''
src={`../icon_${item.status === 0?'warning':'error'}.png`}
style={{
width: 22,
height: 22
}} />
}
{
item.status === 2 &&
<div
onClick={() => {
const opItem = list[index]
opItem.status = 1
let newList = list.map(item => item) //更新一次才能自动更新list
setList(newList)
props.updateListCallback && props.updateListCallback(newList)
}}
style={{
cursor: "pointer",
color: '#FF4646',
fontSize: 18,
height: 28,
marginLeft: 6,
marginBottom: -4,
borderBottom: '1px solid #FF4646'
}}></div>
}
</div>
}
<div
style={{
color: item.status === 2 ? '#FF4646' : item.status === 3 ? '#FFC600' : '#FFF',
fontSize: 18,
marginRight: 10
}}>{item.title}</div>
</div>
<div
onClick={() => {
const opItem = list[index]
if (opItem.status === 2) return //等待上映无法操作
opItem.status = opItem.status === 1 ? 0 : 1
let newList = list.map(item => item) //更新一次才能自动更新list
setList(newList)
props.updateListCallback && props.updateListCallback(newList)
}}
style={{
cursor: "pointer",
border: '2px solid #526283',
borderRadius: '4px',
display: 'flex',
width: 160,
height: 32,
}}>
<div style={{
display: 'flex',
marginTop: -2,
marginLeft: -2,
border: item.status === 1 ? '2px solid #54E9FC' : '',
borderRadius: '4px 0 0 4px',
width: 80,
height: 32,
justifyContent: 'center',
alignItems: 'center',
}}>
<div style={{color: item.status === 1 ? '#54E9FC' : '#8394B6', fontSize: 18}}>上架</div>
</div>
<div style={{
marginLeft: -2,
marginTop: -2,
marginRight: -2,
display: 'flex',
border: item.status === 1 ? '' : '2px solid #54E9FC',
borderRadius: '0 4px 4px 0',
width: 80,
height: 32,
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{color: item.status === 1 ? '#8394B6' : '#54E9FC', fontSize: 18}}>下架</div>
</div>
</div>
</div>
)
})
}
//测试操控菜单
<div style={{
display: 'flex',
width: 300,
height: 40,
marginTop: 10,
alignItems: 'center',
justifyContent: 'space-between',
backgroundImage: 'linear-gradient(to right, rgba(28, 83, 112, 1), rgba(28, 83, 112, 0))'
}}>
<div style={{ fontSize: 24, fontWeight: 'bold', color: '#FFF', marginLeft: 16 }}>模拟测试</div>
<img
alt=''
onClick={() => {
setIsShowSimulate(!isShowSimulate)
}}
src={`../arrow_${isShowSimulate ? 'up' : 'down'}.png`}
style={{
cursor: "pointer",
width: 24,
height: 24,
marginRight: 10
}} />
</div>
//通过isShowSimulate控制底部是否显示,为false则释放底部view
{
isShowSimulate &&
<div style={{
display: "flex",
flexDirection: 'column',
marginTop: 10
}}>
<div
onClick={() => {
props.enterDetailCallback && props.enterDetailCallback()
}}
style={{
cursor: "pointer",
width: 250,
height: 40,
marginRight: 25,
backgroundImage: 'linear-gradient(to bottom, #A7FAFF, #0EBBBE)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{ fontSize: 20, color: '#0B1017' }}>进入详情页</div>
</div>
<div
onClick={() => {
navigate('/other/mine')
// navigate('other/mine') //如果不加'/'.则是默认在当前页后面拼接这个路径
}}
style={{
cursor: "pointer",
width: 250,
height: 40,
marginTop: 10,
marginRight: 25,
backgroundImage: 'linear-gradient(to bottom, #A7FAFF, #0EBBBE)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{ fontSize: 20, color: '#0B1017' }}>跳转到个人页</div>
</div>
<div
onClick={() => {
toggleFull()
}}
style={{
cursor: "pointer",
width: 250,
height: 40,
marginRight: 25,
marginTop: 10,
backgroundImage: 'linear-gradient(to bottom, #A7FAFF, #0EBBBE)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<div style={{ fontSize: 20, color: '#0B1017' }}>切换全屏</div>
</div>
</div>
}
</div>
</div>
</div>
)
}
export default RightView
APP页面react-router-dom
一个项目的入口则是这个 App 页面,这里通过路由 Router 的方式控制整个项目的跳转以及显示
APP的代码,路由的设置如下所示
import HomeView from './home/homeView';
import DetailView from './detail/detailView';
import MineView from './other/mine';
import HomeDetailView from './home/homeDetailVIew';
//Browser与浏览器互动,在网址栏的地方显示路径,MemoryRouter不显示路径
import { BrowserRouter, MemoryRouter, Routes, Route } from "react-router-dom"
function App() {
return (
// <MemoryRouter>
<BrowserRouter>
<Routes>
{/* 默认进入页面 */}
<Route path="/" element={<HomeView />} />
//设置父子路由节点,进入detail即 /home/detail 节点
<Route path="/home" element={<HomeView />} >
<Route path="detail" element={<HomeDetailView />} />
</Route>
<Route path="/detail" element={<DetailView />} />
<Route path="/other" >
<Route path="mine" element={<MineView />} />
</Route>
{/* 可以匹配全路径,找不到的时候就走着一个,可以用于匹配404 */}
{/* <Route path='*' element={NotFound}></Route> */}
</Routes>
</BrowserRouter>
// </MemoryRouter>
)
}
export default App;
路由跳转
路由的跳转 navigate
mport { Link, useNavigate } from "react-router-dom"
const MineView = () => {
const navigate = useNavigate()
return (
<div
style={{
background: "#000000BB",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
height: '100',
userSelect: 'none',
pointerEvents: 'auto',
}}>
<div style={{fontSize: 48, marginTop: 20}}>我是个人页的标题</div>
<Link to='/home'>通过Link跳转到home</Link>
<Link to='/home/detail'>通过Link跳转到通用detail</Link>
<div
onClick={() => {
navigate('/home')
//使用全名进入指定路由,路径相对于域名,推荐使用全名
//navigate('/home')
//如果不加'/'.则是默认在当前页后面拼接这个路径,即相当于子路径页面
// navigate('home')执行这句相当与进入 .../other/mine/home,路径就错了
//如果在 home 页面跳转 detail,则没问题,相当于 .../home/detail
}}
style={{
fontSize: 18,
marginTop: 20
}}
>通过navigate,我也可以跳转到home页</div>
</div>
)
}
const navigate = useNavigate()
传递值与导航路由带参
方式如下所示
//只需要在后面拼接?id=1即可,key=value,多个与get请求一样,用&隔开 ?key1=value1&key2=value2
http://localhost:3000/detail?id=1
如果使用的是react
,接收路由带参路径传递过来的值
query-string框架
//获取参数
let obj = queryString.parse(window.location.search)
obj?.id //即id
如果使用的是hooks
const params = useSearchParams()[0]
params?.id //即id
监听路由变化
我们需要引出 history 就可以监听了
//创建历史对象
let history = createBrowserHistory()
//声明 pathname 用于更新页面
const [pathname, setPathname] = useState<any>()
//初次进入更新pathname,并且监听 pathname 变化,以便于路由和页面效果相匹配
setPathname(window.location.pathname)
history.listen((res) => {
setPathname(res.location.pathname)
})
//页面点击跳转后,在 replace 我们的历史,即可实现保存历史,点击返回箭头就可以监听到了
setPathname(item.pathname)
navigate(item.pathname)
history.replace(item.pathname) //不要用push
umi-request的使用
使用案例如下所示,很简单,不多介绍
import { extend } from "umi-request"
//一般不使用url,都是使用代理,实际部署服务时,后台或者运维会处理好
export const requestUrl = 'http://192.168.1.2/editor/api'
// export const requestUrl = '/editor/api' //测试代理需要,正式解决跨域,应用上面的,并删除setupProxy文件或者注释即可
const request = extend({
prefix: requestUrl,
timeout: 10000,
requestType: 'form', //默认form形式,可以设置成 json
credentials: 'include' //跨域时使用,否则不用写这个参数
})
/** 创建项目 POST,修改 PUT,删除 DELETE ,获取项目列表GET*/
//获取列表GET
export async function requestByProjectInfo(
params?: API.pojectListParams, //参数类型
options?: { [key: string]: any }
) {
//这里是返回值类型
return request<API.projectListResponse>('/project/list', {
method: 'GET',
params: params ? params : {},
...(options || {}),
})
}
//创建POST
export async function requestByCreateProject(
body?: API.createProjectParams,
params?: {},
options?: { [key: string]: any }
) {
return request<API.projectResponse>('/project', {
method: 'POST',
params,
data: body,
...(options || {}),
})
}
//修改 PUT
export async function requestByModifyProject(
body?: API.createProjectParams,
params?: {},
options?: { [key: string]: any }
) {
return request<API.projectResponse>('/project', {
method: 'PUT',
params,
data: body,
...(options || {}),
})
}
//删除 DELETE
export async function requestByDeleteProject(
body?: API.deleteProjectParams,
params?: {},
options?: { [key: string]: any }
) {
return request<API.projectResponse>('/project', {
method: 'DELETE',
params,
data: body,
...(options || {}),
})
}
上传富文本对象时,data直接传递 FormData类型对象即可
let formdata = new FormData()
formdata.append('key', "value可以是String或者二进制Blob")
formdata.append('key2', new Blob())
request('/upload', {
method: 'POST',
params,
data: formdata,
options: {}
})
声明类型代码如下所示
declare namespace API {
//获取项目列表参数
type pojectListParams = {
name?: string
}
//创建修改项目参数
type createProjectParams = {
name: string,
description: string,
project_type: number
}
//删除项目
type deleteProjectParams = {
object_id: number
}
//返回列表类型
type projectListResponse = {
/** 标识代码;200表示成功,非200表示出错 */
code: number;
/** 返回的数据 */
data: Required<ProjectData>;
/** 结果提示信息 */
message: string;
}
//返回项目类型
type projectResponse = {
/** 标识代码;200表示成功,非200表示出错 */
code: number;
/** 返回的数据 */
data: Required;
/** 结果提示信息 */
message: string;
}
//项目信息
type projectData = {
object_id: string,
create_time: string,
update_time: string,
project_type: number,
name: string,
description: string
}
}
禁止拖拽
有时候发现拖拽行为非常讨厌,可以直接禁止掉,某些组件不禁止则可以直接重新 ondragstart 方法
window.ondragstart = () => false
最后
可以模仿理解尝试哈,这篇文章仅仅是对懂得布局,会React,但是不会写项目的参考,快来试试吧