使用react全家桶制作博客后台管理系统
前面的话
笔者在做一个完整的博客上线项目,包括前台、后台、后端接口和服务器配置。本文将详细介绍使用react全家桶制作的博客后台管理系统
概述
该项目是基于react全家桶(React、React-router-dom、redux、styled-components)开发的一套博客后台管理系统,用于前端小站的管理,主要功能包括游客浏览、文章管理、类别管理、评论通知、推荐设置和用户管理
【访问地址】
域名:https://admin.xiaohuochai.cc
Github: https://github.com/littlematch0123/blog-admin
或者可以直接扫描二维码访问
采用移动优先的响应式布局,移动端、桌面端均可适配;字体大小使用em单位,桌面端的文字相应变大;移动端大量使用滑屏操作,桌面端通过光标设置、自定义滚动条、回车确定等,提升交互体验
根据HTML标签内容模型,使用语义化标签,尽量减少标签层级,尽量使用React.Fragment来代替div
采用统一的色调处理,除了黑白两色外,所有页面共使用了8种颜色,保证了页面颜色素雅、统一
使用service worker实现了离线缓存,配置了robots,禁止搜索引擎抓取页面
使用styled-components插件,实现css in JS。所有图标资源均采用svg格式,并存储到common/BaseImg组件中,方便管理,图片资源均上传到七牛云图床,使用外链访问。最终,html、css、image都使用js管理
没有引用第三方组件库,如bootstrap或蚂蚁设计,而是自己开发了项目中所需的公共组件。在common目录下,封装了头像、筛选框、全屏、loading、遮罩、搜索框、滑屏、联动选择等组件,方便开发
功能组件按照功能(Post、Comment...)而不是角色(controllers、models、views)分类,将展示组件component和容器组件container整合为一个文件
状态管理借鉴了vuex的管理模式,action-types、action、reducer、selecter、state整合到每个模块目录的module.js文件下。为了方便扩展,所有的state都设置了filter字段
使用配置数据,实现了数据和应用分离,配置数据包括API调用地址和颜色值,以常量的形式存储在constants目录下
使用esLint规范JS代码,代码风格参照airbnb规范,所有命名采用驼峰写法,公共组件以Base为前缀,函数大多以get或set为前缀,事件函数以on为前缀,异步函数以async为后缀,布尔值基本以do或is为前缀
使用styleLint规范CSS代码,按照布局类属性、盒模型属性、文本类属性、修饰类属性的顺序编写代码,并使用order插件进行校验
使用react最新版本的方法,包括createRef()、getDerivedStateFromProps生命周期、 React.Fragment
语法糖等
进行了代码优化,包括减少请求数量(文件合并 、小图片使用Base64、使用301而不是302重定向、静态资源使用强缓存、接口资源使用协商缓存、使用离线缓存、长缓存优化、CSS内联),减小资源大小(文件压缩、andriod下使用webp格式图片、开启gzip),优化网络连接(使用DNS预解析、使用keep-alive持久连接、使用HTTP2管道化连接),优化资源加载(优化资源加载位置、图片懒加载),减少重绘回流(减少兄弟选择器、动画元素硬件渲染、使用函数节流、及时清理环境)
该项目的一个隐藏彩蛋是摇一摇功能,可以直接摇到前台页面,当然也可以再摇回来
最终优化评分如下所示
功能演示
功能主要包括游客浏览、评论通知、用户管理、推荐设置、文章管理和类别管理
【游客浏览】
在没有管理员帐号的情况下,可以点击游客浏览进入后台。但是,游客只有浏览权限,没有操作权限
【评论通知】
有新评论未查看时,右上角快捷菜单上会出现评论通知的按钮。查看评论后,通知按钮消失
【用户管理】
用户管理包括查看所有用户信息、查看用户点赞情况、查看用户评论情况、按用户名拼音排序、按点赞数排序、按评论数排序以及设置用户状态
【推荐管理】
推荐管理包括文章推荐和专题推荐两类
1、文章推荐
文章推荐的功能包括更改推荐文章、更改背景图和更改次序
2、专题推荐
专题推荐的功能包括更改推荐专题、更改专题说明和更改次序
【文章管理】
文章管理包括文章筛选、文章搜索、新建文章、编辑文章、删除文章、设置配图、查看点赞等功能
1、文章筛选
初始页显示全部文章,设置类别后,只显示筛选后的文章,文章查阅完成后,可返回文章筛选页
2、文章搜索
初始页只显示搜索框,设置搜索词后,显示出相关文章,但每次只显示16篇,下拉刷新后,可继续显示。文章查阅完成后,可返回文章搜索页
3、新建文章
4、编辑文章
5、设置配图
6、查看点赞和评论并删除文章
【类别管理】
类别管理包括查看类别、添加类别、编辑类别、删除类别
目录结构
src目录下,包括assets(静态资源)、common(公共组件)、components(功能组件)、constants(常量配置)、store(redux)和utils(工具方法)这6个目录
- assets // 存放静态资源,包括通用CSS和图片
global.css // 全局CSS
login_bg.jpg // 登录框背景图
- common // 存放公共组件
BaseArticle.js // 文章组件
BaseAvatar.js // 头像组件
...
- components // 存放功能组件
Category // 类别组件
AddCategory.js // 类别添加组件
DeleteCategory.js // 类别删除组件
UpdateCategory.js // 类别更新组件
Category.js // 类别路由组件
CategoryForm.js // 类别基础组件
CategoryItem.js // 类别项组件
CategoryItemList.js // 类别列表组件
CategoryRootList.js // 类别根列表组件
module.js //类别状态管理
...
- constants // 存放常量配置
API.js // 存放API调用地址
Colors.js // 存放颜色值
- store // 存放redux
index.js
- utils // 存放工具方法
async.js // fetch方法
history.js // 路由方法
util.js // 其他工具方法
【公共组件】
没有引用第三方组件库,如bootstrap或蚂蚁设计,而是自己开发了项目中所需的公共组件
封装了文章组件、头像组件、返回组件、徽章组件、按钮组件、卡片组件、筛选框组件、全屏组件、图片组件、输入框组件、loading组件、遮罩组件、搜索框组件、滑屏组件、多行输入框组件、标题组件、面包屑组件、按钮组组件、反色按钮组件、自适应按钮组件、密码框组件和联动选择组件
BaseArticle.js // 文章组件
BaseAvatar.js // 头像组件
BaseBack.js // 返回组件
BaseBadge.js // 徽章组件
BaseButton.js // 按钮组件
BaseCard.js // 卡片组件
BaseFilterList.js // 筛选框组件
BaseFullScreen.js // 全屏组件
BaseImg.js // 图片组件
BaseInput.js // 输入框组件
BaseLoading.js // loading组件
BaseMask.js // 遮罩组件
BaseSearchBox.js // 搜索框组件
BaseSwipeItem.js // 滑屏组件
BaseTextArea.js // 多行输入框组件
BaseTitle.js // 标题组件
BreadCrumb.js // 面包屑组件
ButtonBox.js // 按钮组组件
ButtonInverted.js // 反色按钮组件
ButtonWithAutoWidth.js // 自适应按钮组件
InputPassword.js // 密码框组件
LinkageSelector.js // 联动选择组件
【功能组件】
按照功能来设置目录,如下所示
弹出框(Alert)
登录框(Auth)
类别管理(Category)
评论管理(Comment)
主页(Home)
点赞管理(Like)
文章管理(Post)
七牛传图(Qiniu)
推荐设置(Recommend)
页面尺寸(Size)
用户管理(User)
整体思路
【全屏布局】
使用设置高度的全屏布局方式,主要通过calc来实现
<section style={{ height: `${wrapHeight}px` }}>
<HomeHeader />
<Inner>
...
</Inner>
<HomeNav />
</section>
const Header = styled.header`
height: 50px;
`
const Inner = styled.main`
height: calc(% - 100px);
background: ${PRIMARY_BG_COLOR};
`
const List = styled.nav`
height: 50px;
`
【层级管理】
项目的层级z-index,只使用0-3
全屏的弹出框优化级最高,设置为3;侧边栏设置为2;页面元素默认为0,如有需要,要设置为1
【全局弹出层】
在入口文件app.js中设置全局的弹出层和loading,所有组件都可以共用
// app.js
render() {
const { doShowLoading, alertText, hideAlertText } = this.props
return (
<React.Fragment>
{ doShowLoading && <AlertWithLoading /> }
{ !!alertText && <AlertWithText text={alertText} onExit={hideAlertText} />}
<Router history={history} >
...
</Router>
</React.Fragment>
)
}
【路由管理】
react-router-dom第四版采用了动态路由,在组件目录内,以组件同名文件保存该组件内的路由
// category.js
const Category = () =>
(
<Switch>
<Route exact path="/categories" component={CategoryRootList} />
<Route exact path="/categories/:id" component={CategoryItemList} />
<Route path="/categories/:id/add" component={AddCategory} />
<Route path="/categories/:id/update" component={UpdateCategory} />
<Route path="/categories/:id/delete" component={DeleteCategory} />
</Switch>
)
【状态管理】
参照vuex的状态管理方式,将每个组件的状态管理命名为module.js,保存在当前组件目录下
import auth from '@/components/Auth/module'
import size from '@/components/Size/module'
import alert from '@/components/Alert/module'
import categories from '@/components/Category/module'
import posts from '@/components/Post/PostsModule'
import post from '@/components/Post/PostModule'
import comments from '@/components/Comment/module'
import likes from '@/components/Like/module'
import qiniu from '@/components/Qiniu/module'
import users from '@/components/User/module' const rootReducer = combineReducers({
auth, size, alert, categories, posts, post, comments, likes, qiniu, users
})
每个模块的状态都设置有filter字段,方便扩展
// action-types
export const SET_COMMENTS_FILTER = 'SET_COMMENTS_FILTER' // state
const initialState = {
filter: null,
docs: []
} // action
export const setCommentsFilter = filter => dispatch => new Promise(resolve => {
resolve()
dispatch({ type: SET_COMMENTS_FILTER, filter })
}) // reducer
const comments = (state = initialState, action) => {
switch (action.type) {
case SET_COMMENTS_FILTER:
return { ...state, filter: action.filter } }
export default comments // selector
export const getCommentsFilter = state => state.comments.filter
【数据传递】
组件间的数据传递方式一般有三种,一种是使用react中的函数传参,另一种是使用路由的location属性,还有一种是通过redux
1、函数传参
// PostRecommendItem
<BaseSearchBox
searchText={title}
datas={posts}
onInput={this.onInput}
onBack={() => { this.setState({ doShowSearchBox: false }) }}
/> onInput = data => {
this.setState({ doShowSearchBox: false })
const { updatePostAsync, showAlertText } = this.props
const { prevData, datas } = this.statethis.setState({
datas: datas.map(t => {
if (t.number === data.number) return data
return t
})
})
...
} // BaseSearchBox
<List innerRef={this.scrollRef}>
{resultDatas.map(t =>
<Item key={t._id} onClick={() => { onInput && onInput(t) }}>{t.title}</Item>)}
{resultDatas.length >= limitNumber && !doNeedMoreDatas &&
<ExtendedItem>已经到底了...</ExtendedItem>}
</List>
2、location传递state
// CommentForm
constructor(props) {
super(props)
const { operate, location } = props
if (operate === 'update' && location.state) {
const { content } = location.state.comment
this.state = { content }
} else {
this.state = { content: '' }
}
} // CommentList
history.push({ pathname: `${BasePostUrl}/comments/${t._id}/update`, state: { comment: t } })
3、使用redux
//CategoryForm.js
componentDidMount() {
const { operate, match, setCategoriesFilter } = this.props
setCategoriesFilter(Number(match.params.id)).then(() => {
if (operate === 'update') {
const { category } = this.props
const { name, description } = category
if (name) {
this.setState({ name, description })
} else {
history.push(`/categories/${getParentNumber(Number(match.params.id))}`)
}
}
})
}
const mapStateToProps = state => ({
category: getCategoryByFilter(state)
})
export default connect(mapStateToProps, { setCategoriesFilter })(CategoryForm)
项目优化
【子页面刷新】
子页面刷新时,可能会出现得不到从父级传递过来的数据的情况,笔者的处理是跳转到父级页面
componentDidMount() {
const { operate, location, match } = this.props
if (operate === 'update' && !location.state) {
history.push(`/posts/${match.params.postId}/comments`)
}
}
【reselect】
通过reselect来保存状态,减少状态查询,提升性能
export const getRecommendedCategories = createSelector(getCategories,
datas => datas.filter(t => t.recommend).sort((a, b) => a.index - b.index))
【promise】
为action添加Promise,方便状态改变后的处理
export const setCategoriesFilter = filter => dispatch => new Promise(resolve => {
resolve()
dispatch({ type: SET_CATEGORIES_FILTER, filter })
})
【组件共用】
由于编辑和新建组件用到的元素是一样的,只不过,新建组件时内容为空,编辑组件时需要添加内容,这时就可以复用组件
const AddCategory = ({ match }) => <CategoryForm match={match} operate="add" />
const UpdateCategory = ({ match }) => <CategoryForm match={match} operate="update" />
【清理环境】
如果使用addEventListener绑定了事件处理函数,在组件销毁的时候,要及时清理环境
componentDidMount() {
this.scrollRef.current.addEventListener('scroll', throttle(this.onScroll))
}
componentWillUnmount() {
this.scrollRef.current.removeEventListener('scroll', throttle(this.onScroll))
}
【生命周期函数】
1、使用getDerivedStateFromProps生命周期函数时,如果不设置constructor,会有如下警告
Did not properly initialize state during construction. Expected state to be an object, but it was undefined.
添加空state即可解决
constructor(props) {
super(props)
this.state = {}
}
2、使用componentDidMount生命周期函数时,如果在该函数中直接使用this.setState(),会有如下警告
Do not use setState in componentDidMount react/no-did-mount-set-state
将state设置转移到then方法,或者另一个函数中即可
componentDidMount() {
this.test()
}
test() {
this.setState({ name: '' })
}
【应用和数据分离】
使用配置数据,实现数据和应用分离,配置数据包括API调用地址和颜色值,以常量的形式存储在constants目录下
// API.js
let API_HOSTNAME
if (process.env.NODE_ENV === 'development') {
API_HOSTNAME = '/local'
} else {
API_HOSTNAME = '/api'
} export const BASE_AUTH_URL = `${API_HOSTNAME}/auth/admin`
export const BASE_USER_URL = `${API_HOSTNAME}/users`
export const BASE_POST_URL = `${API_HOSTNAME}/posts`
export const BASE_TOPIC_URL = `${API_HOSTNAME}/topics`
export const BASE_CATEGORY_URL = `${API_HOSTNAME}/categories`
export const BASE_LIKE_URL = `${API_HOSTNAME}/likes`
export const BASE_COMMENT_URL = `${API_HOSTNAME}/comments`
export const BASE_RECOMMEND_URL = `${API_HOSTNAME}/recommends`
export const BASE_QINIU_URL = `${API_HOSTNAME}/qiniu`
export const STATIC = 'https://static.xiaohuochai.site'
export const CLIENT_URL = 'https://www.xiaohuochai.cc' // Colors.js
export const PRIMARY_COLOR = '#00a8e5'
export const DARK_COLOR = '#0066cc'
export const ERROR_COLOR = '#f67280'
export const PRIMARY_BG_COLOR = '#fafafa'
export const TRANSPARENT_BG_COLOR = 'rgba(7, 17, 27, .4)'
export const DARK_BG_COLOR = '#f5f5f5'
export const PRIMARY_LINE_COLOR = '#eee'
export const DARK_LINE_COLOR = '#ebedf0'
【函数节流】
为触发频率较高的函数使用函数节流
/**
* 函数节流
* @param {fn} function test(){}
* @return {fn} function test(){}
*/
export const throttle = (fn, wait = ) => function func(...args) {
if (fn.timer) return
fn.timer = setTimeout(() => {
fn.apply(this, args)
fn.timer = null
}, wait)
}
功能实现
【登录设置】
将用户信息保存到sessionStorage中并检测,如果不存在,则跳转到登录页面
<Router history={history} >
<Switch>
<Route path="/login" component={AuthLogin} />
<Route
path="/"
render={props => {
if (sessionStorage.getItem('token') && sessionStorage.getItem('user')) {
return <Home {...props} />
}
return <Redirect to="/login" />
}}
/>
</Switch>
</Router>
【全角空格占位】
使用全角空格占位,从而使文字对齐
<Label htmlFor="username">用户名:</Label>
<Label htmlFor="password"> 密码:</Label>
【一像素边框】
将伪元素高度设置为1px,然后用 transform缩小到原来的一半
div {
position: relative;
&::after {
position: absolute;
left: ;
right: ;
height: 1px;
transform: scaleY(.);
content: '';
}
`
【缓动弹出层】
过渡弹出层有两种实现方式,包括transition和animation,该项目使用transition的方式实现
<StyledMask className={doShowMenuList ? 'mask-show' : ''} />
<StyledList className={doShowMenuList ? 'transform-show' : ''} />
const StyledList = styled(HomeMenuList)`
transform: translateY(-100%);
transition: .2s;
`
const StyledMask = styled(BaseMask)`
z-index: 2;
display: none;
`
const MenuBox = styled.div`
cursor: pointer;
& .transform-show {
transform: translateY(0);
}
& .mask-show {
display: block;
}
`
【图标管理】
所有的图标都使用SVG格式,存储在common/BaseImg.js文件中
// BaseImg.js
...
export const Home = props => (
<svg height={} viewBox="0 0 24 24" width={} {...props}>
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
)
【搜索实现】
处理搜索功能时,需要特别处理正则表达式中的元字符
static getReg(searchText) {
return new RegExp(searchText.replace(/[[(){}^$|?*+.\\-]/g, '\\$&'), 'ig')
}
如果将间隔符-放在中间,大写字母,如V会被匹配为/V
return new RegExp(searchText.replace(/[[(){}^$|?*+.-\\]/g, '\\$&'), 'ig')
此时的-被识别为范围间隔符,相当于.到\之间的字符,正好包括了所有的大写字母,所以。一定要把-放在最后
【滑屏实现】
滑屏主要通过touch事件来实现,一般地,有两种形式。一种是当前元素滑动,另一种是其他元素滑动。该项目采用较简单的第二种
static checkSwipe(absMove, duration) {
const THRESHOLD =
const SHORTESTTIME =
// 距离大于10,且时间小于300ms,才算做一次滑动
return Boolean(absMove > THRESHOLD && duration < SHORTESTTIME)
}
onTouchStart = e => {
this.startTime = new Date().getTime()
this.startX = e.targetTouches[].pageX
this.startY = e.targetTouches[].pageY
}
onTouchEnd = e => {
const { pageX, pageY } = e.changedTouches[]
// 如果y轴移动距离大于元素高度,说明手指已经移出元素本身,则取消滑动
if (pageY - this.startY > this.clientHeight) {
return false
}
const moveX = pageX - this.startX
const duration = new Date().getTime() - this.startTime
// 如果符合滑动要求,且向左滑动,则控制条滑出
if (BaseSwipeItem.checkSwipe(Math.abs(moveX), duration) && moveX < ) {
this.setState({ doShowControlBox: true })
} else {
this.setState({ doShowControlBox: false })
}
return true
}
【密码框实现】
密码框的右侧一般都有一个小图标用于显示密码
<Wrap className={className} {...rest} >
<StyledInput
id="password"
textIndent={textIndent}
color={color}
value={value}
onChange={onChange}
type={doShowPassword ? 'password' : 'text'}
/>
{ doShowPassword ?
<Visibility onClick={onChangeStatus} />
: <VisibilityOff onClick={onChangeStatus} />
}
</Wrap>
【fetch函数封装】
该项目是基于create-react-app构建的,自带fetch功能。封装fetch函数到utils目录下的async.js文件中,将loading组件、alert组件整合到fetch函数的整个数据获取过程中
import { showLoading, hideLoading, showAlertText, hideAlertText } from '@/components/Alert/module'
import { logout } from '@/components/Auth/module' const async = ({ dispatch, url, method, data, headers, success, fail, doHideAlert }) => {
// 显示loading
dispatch(showLoading())
let fetchObj = {}
if (method) {
fetchObj = {
method,
body: JSON.stringify(data),
headers: new Headers({ ...headers, 'Content-Type': 'application/json' })
}
}
fetch(url, fetchObj).then(res => {
// 关闭loading
dispatch(hideLoading())
return res.json()
}).then(json => {
// 成功
if (json.code === ) {
!doHideAlert && dispatch(showAlertText(json.message))
setTimeout(() => {
dispatch(hideAlertText())
}, )
success && success(json.result)
// 自定义错误
} else if (json.code === ) {
dispatch(showAlertText(json.message))
// 系统错误
} else if (json.code === ) {
dispatch(showAlertText(json.message))
fail && fail(json.err)
// 认证失败
} else if (json.code === ) {
dispatch(showAlertText(json.message))
dispatch(logout)
// 权限不足
} else if (json.code === ) {
dispatch(showAlertText(json.message))
}
}).catch(() => {
dispatch(showAlertText('服务器故障'))
})
} export default async
【组件内路由】
如果要在组件内使用路由功能,可封装utils/history.js文件
// utils/history.js
import createBrowserHistory from 'history/createBrowserHistory'
const customHistory = createBrowserHistory()
export default customHistory
Router中使用history={history},而不是BrowserRouter
// app.js
import history from '@/utils/history'
<Router history={history} >
<Switch>
<Route path="/login" component={AuthLogin} />
<Route
path="/"
render={props => {
if (sessionStorage.getItem('token') && sessionStorage.getItem('user')) {
return <Home {...props} />
}
return <Redirect to="/login" />
}}
/>
</Switch>
</Router>
然后,在组件中引用即可
import history from '@/utils/history'
// 跳转到首页
history.push('/')
兼容处理
【虚拟键盘】
andriod下,虚拟键盘会影响可视区域的高度;而IOS下,不会影响
可视区域高度 = document.documentElement.clientHeight - 虚拟键盘的高度;
bug重现如下:
所以,要将包含input域的页面高度设为固定
在页面初始化时,获取页面高度
// app.js
componentDidMount() {
const { setWrapSize } = this.props
const { clientHeight, clientWidth } = document.documentElement
setWrapSize({ clientHeight, clientWidth })
window.addEventListener('orientationchange', this.setSize)
}
然后通过行间样式,将此高度设置到包含input域的页面上
// BaseFullScreen
<Wrap className={className} style={{ height: `${wrapHeight}px` }} {...rest}>{children}</Wrap>
【取消自动大写】
IOS下,input域会自动大写首字母,设置autoCapitallize="off"即可
const BaseInput = ({ value, onChange, ...rest }) =>
<Input {...rest} value={value} onChange={onChange} autoComplete="off" autoCapitalize="off" />
【光标颜色】
默认情况下,光标颜色与字体颜色color相同,但也可以通过caret-color属性来单独设置
但是,IOS的光标不支持caret-color,与字体颜色无关,默认为紫蓝色。所以,尽量不要设置蓝色或紫色背景,否则光标看不清楚
【页面放大】
IOS下,input获取焦点时会放大,meta设置user-scalable=no,可取消放大效果
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, shrink-to-fit=no">
【圆角】
IOS下,input域只显示底边框时,会出现底边圆角效果,设置border-radius:0即可
border-radius:
【轮廓outline】
android浏览器下,input域处于焦点状态时,默认会有一圈淡黄色的轮廓outline效果
通过设置outline:none可将其去除
outline: none
【点击背景】
在移动端,点击可点击元素时,android下会出现淡蓝色背景,IOS下会出现灰色背景
bug重现如下:
可以通过-webkt-tap-hightlight-color属性的设置,取消点击时出现的背景效果
* {
-webkit-tap-highlight-color: rgba(, , , );
}
【局部不滚动】
IOS下,可能会出现局部滚动不流畅,甚至局部不滚动的bug
通过在该元素上设置overflow-scrolling属性为touch即可解决
div {
-webkit-overflow-scrolling: touch;
}
【高度无效】
在IOS下,设置height:100%,如果父级的flex值为1,而没有设置具体高度,则100%高度设置无效
处理方法是,在父级通过计算来设置具体高度height,如height: calc(100% - 100px)
使用react全家桶制作博客后台管理系统的更多相关文章
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
- 转载: 使用vue全家桶制作博客网站 HTML5 移动网站制作的好教程
使用vue全家桶制作博客网站 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用vue全家桶制作的博客网站 概述 该项目是基于vue全家桶(vue. ...
- 使用vue全家桶制作博客网站
前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用vue全家桶制作的博客网站 概述 该项目是基于vue全家桶(vue.vue-router.vuex.v ...
- React全家桶+Material-ui构建的后台管理系统
一.简介 一个使用React全家桶(react-router-dom,redux,redux-actions,redux-saga,reselect)+Material-ui构建的后来管理中心. 二. ...
- React全家桶打造共享单车后台管理系统项目_第1篇_项目环境搭建_首页编写
1.项目介绍 项目github地址:https://github.com/replaceroot/React-manageSystem 项目整体架构: 课程大纲: 第一章:React基础知识 ...
- React全家桶+AntD 共享单车后台管理系统开发
第1章 课程导学对课程整体进行介绍,并且说明学习的必要性.第2章 React基础知识React基础知识以及生命周期的介绍,并使用React官方脚手架初始化基础项目,同时介绍了新一代打包工具Yarn.第 ...
- Vue实战狗尾草博客后台管理系统第三章
Vue实现狗尾草博客后台管理系统第三章 本章节,咱们开发管理系统侧边栏及面包屑功能. 先上一张效果图 样式呢,作者前端初审,关于设计上毫无美感可言,大家可根据自己情况设计更好看的哦~ 侧边栏 这里我们 ...
- Vue实战狗尾草博客后台管理系统第七章
Vue实战狗尾草博客后台管理平台第七章 本章内容为借助模块化来阐述Vuex的进阶使用. 在复杂项目的架构中,对于数据的处理是一个非常头疼的问题.处理不当,不仅对维护增加相当的工作负担,也给开发增加巨大 ...
- 基于Laravel开发博客应用系列 —— 构建博客后台管理系统
一个完整的博客应用不能没有后台管理系统.所以在本节中我们将继续完善博客应用 —— 开发后台管理系统. 1.创建路由 在上一节十分钟创建博客项目中,已经设置过了 app/Http/routes.php, ...
随机推荐
- XSS 绕过技术
XSS Cross-Site Scripting(XSS)是一类出现在 web 应用程序上的安全弱点,攻击者可以通过 XSS 插入一 些代码,使得访问页面的其他用户都可以看到,XSS 通常是可以被看作 ...
- 【Javascript】JS遍历数组的三种方法:map、forEach、filter
前言 近一段时间,因为项目原因,会经常在前端对数组进行遍历.处理,JS自带的遍历方法有很多种,往往不加留意,就可能导致知识混乱的现象,并且其中还存在一些坑.前端时间在ediary中总结了js原生自带的 ...
- Android SDK 开发——发布使用踩坑之路
前言 在 Android 开发过程中,有些功能是通用的,或者是多个业务方都需要使用的. 为了统一功能逻辑及避免重复开发,因此将该功能开发成一个 SDK 是相当有必要的. 背景 刚好最近自己遇到了类似需 ...
- List去重的实现
List<T> 当T为值类型的时候 去重比较简单,当T为引用类型时,一般根据业务需要,根据T的中几个属性来确定是否重复,从而去重. 查看System.Linq下的Enumerable存在一 ...
- Android Material Design控件使用(四)——下拉刷新 SwipeRefreshLayout
使用下拉刷新SwipeRefreshLayout 说明 SwipeRefreshLayout是Android官方的一个下拉刷新控件,一般我们使用此布局和一个RecyclerView嵌套使用 使用 xm ...
- java--基本数据类型的转换(强制转换)
强制类型的转换 规则: 1.执行算术运算时,低类型(短字节)可以转换为高类型(长字节):例如: int型转换成double型,char型转换成int型等等. 就是用强制类型来实现. 3.强制类型转换语 ...
- 升级WIN10 (9879)后IE无响应的解决办法
身为程序猿,当然有了新系统就要尝尝鲜,有WIN8时,哥是朋友圈第一个用的,有WIN8.1时哥也是第一个升级的. 现在WIN10来了,当然也得赶紧尝尝鲜.直接下载了 9879版的预览版本安装. 要说WI ...
- Dynamics 365-CRM又报看不懂的错误了
在CRM上执行各种操作,时不时会碰到各种问题,尤其是CRM环境里包含越来越多定制的时候.有的问题在CRM弹出的错误提示框,一目了然:而有的,可能就是简单的提示:SQL Error. 这个时候我们可能都 ...
- 三星5.0以上设备最完美激活XPOSED框架的经验
对于喜欢钻研手机的小伙伴来说,常常会接触到Xposed框架以及种类繁多功能强大的模块,对于5.0以下的系统版本,只要手机能获得Root权限,安装和激活Xposed框架是异常简易的,但随着系统版本的不断 ...
- appium+python搭建自动化测试框架_Appium元素定位(二)
Appium元素定位: 工具:Android\android-sdk\tools uiautomatorviewer.bat 1. id定位: self.driver.find_element_ ...