前言
React组件化开发也学习了有一段时间了,前段时间在掘金上发布的一篇文章:手把手教你做:快手最新爆款“一甜相机”- 新手react开发必备项目 正是我react开发项目的一个试水篇章
一甜相机的模板页面还是个雏形,现在我结合这段时间所学的redux,将项目中的数据交给redux管理,优化了页面,同时新增了一些功能。本篇文章将围绕上一篇文章进行优化,如果看不明白的小伙伴可以移步上一篇哦~
页面优化部分
这个项目起步时,我的学习还不够到位,还有不少地方都比较粗糙,这篇文章的优化部分我将从页面自适应,路由,页面等方面进行。
优化项目
- 自适应页面:上次项目开发时,使用单位均为px,在电脑上的移动端效果显示一切正常,但用到
gitPage
在手机端展示的时候,布局有一些混乱,增加页面自适应以解决这个问题 - 路由的懒加载:按需加载
- 导航栏优化:先前的页面导航栏切换均为tab切换,写起来过于繁琐,二级路由的使用能减少代码的繁琐
- redux数据管理:项目中的大部分数据状态改为
redux
接管,方便管理 - 搜索栏跳转页面优化:切入切出动画效果
- 图片懒加载的实现:在学习了神三元的云音乐项目后,图片的懒加载被提上了日程,在图片还未加载成功时用一张默认图片代替,可以提高用户体验感
自适应页面
移动端的适应性与电脑端不同,使用px在电脑端布局看着是没有问题,但一但到了真正的移动端,由于手机型号的多变,页面局部也需要进行调整,px是绝对单位,无法根据页面大小进行相应调整,为了优化用户体验感,我采用
rem
来进行自适应布局。
在项目目录下新增一个public
文件夹,里面js
文件中的adapter.js
来写页面自适应布局
rem :是一个相对单位,是指相对于根元素的字体大小的单位。这样就意味着,我们只需要在根元素确定一个px字号,则可以来算出元素的宽高。对于页面自适应来说,rem这个相对单位有很大的作用
页面自适应代码如下:这是一个通用样式,html 的font-size 被设为了16px,经过尝试,这个大小可以刚好使页面修改量为最少(单指我这个项目)。
var init = function () {
var clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
if (clientWidth >= 640) {
clientWidth = 640;
}
//设计稿为750px ,css则需/2,为375px
var fontSize = 16 / 375 * clientWidth;
document.documentElement.style.fontSize = fontSize + "px";
}
init();
window.addEventListener("resize", init);
//如果页面宽度超过640px,那么页面中的宽度恒为640px
//否则页面中html 的font-size的大小为 16 *(当前宽度/375)
路由的懒加载
缺点: 上篇文章中与路由相关的组件都是直接导入的,整个网页打开时就默认加载所有网页,这样会拖延首屏加载时间,导致用户体验感不强。路由懒加载就是来解决这个问题的!
优化: 新增的路由懒加载lazy,顾名思义,就是只加载你当前点击的那个模块。按需去加载路由对应的资源,提高首屏加载速度
(tip:首页不用设置懒加载,而且一个页面加载过后再次访问不会重复加载)。
import { useState, lazy} from 'react'
import { Routes, Route, Link,Navigate } from 'react-router-dom'
import Tem from '../pages/Tem' //首屏不需要懒加载
const Vedio = lazy(() => import('../pages/Vedio'))
const Pic = lazy(() => import('../pages/Pic'))
const Kd = lazy(() => import('../pages/Kd'))
const Searchk = lazy(() => import('../pages/Searchk'))
const Tpmb = lazy(() => import('../pages/Tem/Tpmb'))
const Spmb= lazy(() => import('../pages/Tem/Spmb'))
const Login = lazy(() => import('../components/Login'))
const Shop = lazy(() => import('../components/Shop'))
const Geren = lazy(() => import('../components/Geren'))
const Tpmbdetail = lazy(() => import('../pages/Tem/Tpmbdetail'))
const Spmbdetail = lazy(() => import('@/pages/Tem/Spmbdetail'))
const RouteConfig =() =>{
return (
<Routes>
<Route path="/" element={<Navigate to ='/temp'/>} />
<Route path="/temp" element={<Tem/>}>
{/* 二级路由 */}
<Route path="/temp/tpmb" element={<Tpmb/>}></Route>
<Route path="/temp/spmb" element={<Spmb/>}></Route>
</Route>
<Route path="/pz" element={<Pic/>}></Route>
<Route path="/vedio" element={<Vedio/>}></Route>
<Route path="/kd" element={<Kd/>}></Route>
<Route path="/select" element={<Searchk/>} />
<Route path="/login" element={<Login/>} />
<Route path="/geren" element={<Geren/>} />
<Route path="/shop" element={<Shop/>} />
<Route path="/tpmbdetail/:id" element={<Tpmbdetail/>} />
<Route path="/spmbdetail/:id" element={<Spmbdetail/>} />
</Routes>
)
}
export default RouteConfig
导航栏优化
1. 第一层导航栏:二级路由
优化: 双层Tab键切换还是显得有些繁琐,于是第一层导航栏则换成了页面的二级路由,整个页面由路由包裹着,使用路由进行页面跳转十分方便快捷
操作: 在模板页面中用<TpNav/>
占位,将需要用到的二级路由写进一个数组,再用map
方法和Swiper
将二级路由展现出来,二级路由下的页面则由<Outlet/>
输出
代码仅展示部分:
增加二级路由
<Route path="/temp" element={<Tem/>}>
{/* 二级路由 */}
<Route path="/temp/tpmb" element={<Tpmb/>}></Route>
<Route path="/temp/spmb" element={<Spmb/>}></Route>
</Route>
实现二级路由
// 将二级路由写入数组中
let TpNavs = [
{ id: 1, desc: '图片模板', path: 'tpmb'},
{ id: 2, desc: '视频模板', path: 'spmb'}
]
// 使用map 和 Swiper 将二级路由展现出来
<div className="navbar swiper-container">
<div className="nav-box swiper-wrapper">
{
TpNavs.map((item, index) => {
return (
<NavLink
index={index}
to={`/temp/${item.path}`}
key={item.id}
className="nav-item swiper-slide"
>
{item.desc}
</NavLink>
)
})
}
</div>
</div>
2. 第二层导航:超长自动滑动导航切换
上次项目中导航栏的滑动主要是由css 样式实现的,且无法进行自动滑动,滑动过程中还有滑动条出现,体验感还有待加强。
优化:使用anted-mobile
组件库中CapsuleTabs
组件,实现超长自动滑动,具体实现还需修改
ps:antd-mobile是由蚂蚁金融团队推出的一个开源的react组件库,相当于Weui 组件库,能提供许多便捷功能的组件,打造react项目方便快捷当属它!大家开发项目的时候可以去找找有没有便捷的组件~
要在
CapsuleTabs
组件中实现tab键的切换,需要另外设置一个类名。用classnames
设置active
时的tab键值,就能实现tab键的切换了
注意事项--修改antd-mobile样式
antd-mobile样式有点难改,经过我多次尝试和查询网络,现在最有效的方法是使用antd-mobile中的组件都需要另建一个css文件,在页面源码中找到需要修改元素的类名,所有包含在组件中的样式都需要在css文件中写样式
<CapsuleTabs defaultActiveKey='2'>
<CapsuleTabs.Tab className={classnames({active:tab1 == "hot"},'cap')} title={<a onClick={()=> changeTab1("hot")}>热门</a>} key='1'>
</CapsuleTabs.Tab>
<CapsuleTabs.Tab className={classnames({active:tab1 == "new"})} title={<a onClick={()=>changeTab1("new")}>最新</a>} key='2'>
</CapsuleTabs.Tab>
<CapsuleTabs.Tab className={classnames({active:tab1 == "biye"})} title={<a onClick={()=>changeTab1("biye")}>毕业季</a>} key='3'>
</CapsuleTabs.Tab>
<CapsuleTabs.Tab className={classnames({active:tab1 == "summy"})} title={<a onClick={()=>changeTab1("summy")}>夏日</a>} key='4'>
</CapsuleTabs.Tab>
<CapsuleTabs.Tab className={classnames({active:tab1 == "kuai"})} title={<a onClick={()=>changeTab1("kuai")}>快手爆款</a>} key='5'>
</CapsuleTabs.Tab>
<CapsuleTabs.Tab className={classnames({active:tab1 == "hbizhi"})} title={<a onClick={()=>changeTab1("bizhi")}>壁纸</a>} key='6'>
</CapsuleTabs.Tab>
<CapsuleTabs.Tab className={classnames({active:tab1 == "pai"},'cap')} title={<a onClick={()=>changeTab1("pai")}>拍立得</a>} key='7'>
</CapsuleTabs.Tab>
</CapsuleTabs>
修改样式后实现效果:
redux数据管理
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
随着项目页面的增加,状态变得越来越复杂,
状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,页面的变换也有可能会引起状态的变化;
当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪 ,而redux可以解决这个问题——redux能够 集中式管理(读/写) react 应用中多个组件共享状态
Redux 可以用这三个基本原则来描述:
单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。 State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象 。
使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
- store
store 就是保存数据的地方,它相当是一个容器。整个应用只能有一个 store。store 就是将 state、action 与 reducer 联系在一起的一个对象,在这个项目中,在一个总store 下还有两个子store掌管着数据状态
- 创建
store
import { createStore,compose,applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer,
composeEnhancers(
applyMiddleware(thunk)
)
)
export default store
- redux-thunk 是一个比较流行的 redux 异步 action 中间件,比如 action 中有通过
axios
通用远程 API 这些场景,那么就应该使用 redux-thunk 了。redux-thunk 帮助你统一了异步和同步 action 的调用方式,把异步过程放在 action 级别解决,对 component 没有影响。
2.汇总reducer
这个项目中有两个页面需要进行数据渲染,为了方便管理,我分别创建了两个子store,在reducer文件中将两个子store的reducer引入合并成总的reduce文件
import { combineReducers } from 'redux'
import { reducer as TemReducer} from '@/pages/Tem/store/index'
import { reducer as GerenReducer } from '@/components/Geren/store/index'
export default combineReducers({
Tem:TemReducer,
Geren:GerenReducer
})
- actionCreators
所有的数据都需要通过派发(dispatch)action
来更新,action 也是一个普通的 js 对象,这个对象包含两部分:更新的 type
和data
,拉取数据需要在获取数据后dispatch (action)
仅展示部分代码:
export const changeBannerList = (data) =>({
type:actionTypes.CHANGE_BANNER,
data
})
export const getBannerList = () =>{
return (dispatch) => {
getBanners()
// console.log(data)
.then(data => {
const action = changeBannerList(data)
dispatch(action)
dispatch(changeEnterLoading(false))
})
}
}
- reducer
将 state 和 action 联系在一起,也就是根据旧的 state 和 action, 产生新的state 的纯函数
const reducer= (state = defaultState,action)=>{
switch (action.type){
case actionTypes.CHANGE_BANNER:
return {
...state,
bannerList:action.data
}
...
default:
return state
}
}
- constants
本文件用于管理所有action
的type
,方便维护
export const CHANGE_BANNER = 'CHANGE_BANNER'
export const CHANGE_TPMB_LIST = 'CHANGE_TPMB_LIST'
export const CHANGE_SPMB_LIST = 'CHANGE_SPMB_LIST'
export const CHANGE_ENTERLOADING = 'CHANGE_ENTERLOADING'
export const CHANGE_ALBUM = 'CHANGE_ALBUM'
export const CHANGE_TPMBLIST_ID = 'CHANGE_TPMBLIST_ID'
export const CHANGE_TPMB_STAR = 'CHANGE_TPMB_STAR'
export const CHANGE_SPMB_STAR = 'CHANGE_SPMB_STAR'
搜索栏跳转页面优化
通过学习神三元的云音乐项目,我get到了搜索栏切入切出的动画效果,于是,我将它运用到了我的搜索栏中,优化页面跳转
具体实现效果如下:
操作: 点击搜索栏,搜索页面会从右侧平移过来,点击取消,动画消失
代码实现:
安装依赖-- React过渡动画
在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验。 React 可以为我们提供
react-transition-group
。
这个库可以帮助我们方便的实现组件的 入场
和 离场
动画,使用时需要进行额外的安装:
npm i react-transition-group
引入CSSTransition--- 在前端开发中,通常使用CSSTransition来完成过渡动画效果
import { CSSTransition } from 'react-transition-group'
<CSSTransition
in={show} //用于判断是否出现的状态
timeout={300} // 动画持续时间
appear={true}
classNames="fly" // classNames值,防止重复
unmountOnExit //元素退场时,自动将DOM删除
onExited={() =>{
navigate(-1)
}}
>
...搜索页面内容
<CSSTransition />
CSSTransition的动画效果主要由css样式来实现
- 它们有三种状态,需要定义对应的CSS样式:
- 第一类,开始状态:对于的类是-appear、-enter、exit;
- 第二类:执行动画:对应的类是-appear-active、-enter-active、-exit-active;
- 第三类:执行结束:对应的类是-appear-done、-enter-done、-exit-done;
//搜索页面实现动画效果的css
export const Container = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
z-index:100;
overflow: hidden;
background: #f2f3f4;
transform-origin:right bottom;
&.fly-enter,&.fly-appear {
opacity:0;
transform:translate3d(100%,0,0) ;
}
&.fly-enter-active,&.fly-apply-active {
opacity:1;
transition:all .3s;
transform: translate3d(0,0,0);
}
&.fly-exit {
opacity: 1;
transform: translate3d(0,0,0);
}
&.fly-exit-active {
opacity: 0;
transition: all .3s;
transform: translate3d(100%,0,0);
}
`
图片懒加载的实现
在学习了神三元的云音乐项目后,我又get到了一项新技能——图片的懒加载 ,利用
Scroll
组件和react-lazyload
中的Lazyload
组件实现图片下滑后的懒加载
onScroll
事件:在页面滚动时触发此事件,forceCheck
是从react-Lazyload
中解构出来的函数,实现图片移动到视口时进行加载,<Lazyload/>
组件为数据提供了一个默认图片,还未到达视口的图片由默认图片替代,等图片滚动到视口时再进行加载,优化了用户体验感。
// Scroll
<Scroll onScroll ={forceCheck}>
<TpmbList tpmbda={tpmbda}/>
</Scroll>
//Lazyload
<ListWrapper>
{tpmbda.map(item =>
<Link to ={`/tpmbdetail/${item.id}`} key = {item.id}>
<List key = {item.id}>
<div className="img_wrapper">
<Lazyload placeholder={<img
width="100%" height="100%"
src={tp}/>
}>
<img src={item.img} alt="" />
</Lazyload>
</div>
<p className="get" onClick={() => {
Toast.show({
icon: 'loading',
content: '加载中…',
})
}}>Get 同款</p>
<p className='title'>{item.title}</p>
</List></Link>)}
</ListWrapper>
具体实现效果为:
页面新增功能
- 简易版登录功能:实现登录页面跳转用户页面
- 图片导航实现相册选取
- 图片细节页面的实现:模板的下滑,用户的关注收藏等
- 视频模板细节页面的实现:视频的自动播放,收藏数等
简易版登录功能
实现效果:
由于没有后端,登录功能只能做一个简易版的,主要是使用变量和
localstorage
实现
- 登录功能,在默认头像上添加点击事件,引入antd-mobile中的
Mask
组件,制作登录弹窗,通过visible变量来控制弹窗的出现和消失
{/* 遮罩层 */}
<Mask visible={visible} >
<div className="content">
<p>登录一甜相机</p>
<p> get专属于你的美颜相机!</p>
<div className='btns'>
<span onClick={() => setVisible(false)}>取消</span>
<span className='login' onClick={() => (setShowlogo(false))}><Link to="/login" > 登录 </Link></span>
</div>
</div>
</Mask>
- 点击登录则跳转登录页面,下面的复选框
check值
由变量控制,在登录按钮中添加点击事件,通过判断check的值来判断页面是否能成功登录,check为false
则跳出弹窗提示进行勾选,若为true
则跳转页面
//判断复选框是否被勾选
const setSelect = () =>{
if(check){
navigate('/temp/tpmb')
}else{
Modal.alert({
content: '请勾选同意后再进行登录'
})
}
}
- 实现默认头像图片和用户头像转换:在登录页面中添加一个
localstorage
变量
window.localStorage.showuser = 'showuser'
点击登录,在页面跳转成功的页面上添加一个判断函数 :
if(!window.localStorage.getItem('showuser')){
window.localStorage.setItem('showuser','');
}else{}
{ showuser && user.map(item => {
return (
<span className="im" key={item.id}><Link to="/geren"><img src={item.img}/></Link></span>
)
}) }
通过判断showuser
的值来实现用户头像的替换,由于localstorage
存储的值均为string
类型,要使showuser
展示出boolean
类型,为false则设为空值,即解决了头像无法转换的问题
图片导航实现相册选取
和上面登录功能一样,照片拉取也是关于后端的功能,于是我就做了一个双层弹窗转换相册的功能
具体实现效果:
图片导航采取的是双层嵌套弹出层的组件,也是在antd-mobile组件库中引入的
双层弹出层
<Popup
visible={visible7}
showCloseButton
onClose={() => {
setVisible7(false)
}}
bodyStyle={{
borderTopLeftRadius: '1px',
borderTopRightRadius: '1px',
minHeight: '100vh',
}}
>
{mockContent}
</Popup>
</li>
<Popup
visible={visible}
bodyStyle={{ height: '90vh' }}
>
<div style={{ padding: '24px' }} className="album" >
{renderAlbums()}
</div>
</Popup>
相册选取功能
使用函数将相册列表渲染上去,点击列表选择相册则转至指定相册
<span>{albumName?albumName:''}</span>
const renderAlbums = () => {
return albumList.map(({id,nm,img}) => {
return <Link
className="album_name"
to={{
search:`name= ${nm}` // ? 后面的参数 0
}}
onClick={() => {
setVisible(false)
}}
key= {id}>
<img src={img} />
{nm}
</Link>
})
}
图片细节页面实现
这个功能实现的是由首页的图片模板点进去的细节页面,这个页面分为三个步骤去实现:
- 使用
useParams
获取这个页面图片的id值,使用slice() 这个api
将图片列表数据从此id开始截取 ,并使用map函数将图片模板渲染至页面上
const { id } = useParams() //获取页面id值
setData1(tpmbList.slice(id-1)) // id从0开始,即id-1为本页面id值
const renderTpmbdetail =() => {
return data1.map(item =>{
return (
<List key = {item.id}>
<NavBar>
<CloseOutline onClick={() => navigate(-1)} className="close"/>
{item.artist}<span className='gz' onClick={() =>setAttention(item.id)}>{ (item.attention)?'已关注':'关注'}</span>
{/* { console.log(item.attention)} */}
<SendOutline className='share' onClick={() => {
setVisible5(true)
}}/>
</NavBar>
<div className='dw'>
<img src={item.img} alt="" />
<p>
<span className='title'>{item.title}</span>
<span className='sc'>收藏{item.star1 ? item.star : item.star-1}</span>
</p>
</div>
<div className='caozuo'>
<span className='star'><Rate className='star_a' count ={1}
style={{
'--active-color': '#ea84ae',
'--inactive-color':'#fec7df'
}}
onChange={() =>setStar(item.id)}
/></span>
<span className='zi'>Get同款</span>
</div>
</List>
)
})
}
具体效果为:
2. 实现关注作者功能 :关注功能实现要将id传进数据列表中,将attention
的值改为相反值,再将数据重传,重新渲染
- 在函数中触发点击事件,将id值作为参数传入 ,在函数中运行
dispatch
函数后再判断
{item.artist}<span className='gz' onClick={() =>setAttention(item.id)}>{ (item.attention)?'已关注':'关注'}</span>
const setAttention = (id) =>{
changeTpmbListByIdDispatch(id)
setData1(tpmbList.slice(id-1))
}
- 在页面上
dispatch
一下修改关注对象,将id值作为data
传入
const mapDispatchToProps =( dispatch ) =>{
return {
changeTpmbListByIdDispatch(data) {
dispatch(actionCreators.changeTpmbListById(data))
}
}
}
- 在
reducer
里面进行数据修改
仅展示部分代码:
// 根据id修改attention
const changeTpmbById = (list,id) =>{
console.log(list)
console.log(id)
let index = list.findIndex(data => id == data.id);
list[index].attention = !list[index].attention;
return list;
}
// 根据action的值返回数据
case actionTypes.CHANGE_TPMBLIST_ID:
// console.log(action.data) 通过id改变attention状态
return {
...state,
tpmbList:changeTpmbById(Object.assign([],state.tpmbList),action.data)
}
具体实现:
3.实现收藏功能: 与关注相仿,收藏功能也需要将id传入数据列表中进行查找到相应的了列表项,将star1的值改为true,再重新渲染数据
- 在Rate中触发
onChange
事件,将id值作为参数传入,在函数中运行dispatch
函数
const setStar = (id) => {
changeTpmbListStar(id)
// changetStarDispatch(id)
setData1(tpmbList.slice(id-1))
}
//onChange事件
<Rate className='star_a' count ={1}
style={{
'--active-color': '#ea84ae',
'--inactive-color':'#fec7df'
}}
onChange={() =>setStar(item.id)}
/></span>
- 在
mapDispatchToProps
中创建一个有参数的dispatch
函数
const mapDispatchToProps =( dispatch ) =>{
return {
changeTpmbListStar(data){
dispatch(actionCreators.changeTpmbListStar(data))
}
}
- 在
reducer
中进行数据的查询修改
// 修改函数
const changeTpmbStar =(list,id) => {
let ind = list.findIndex(data => id == data.id);
list[ind].star1 = !list[ind].star1;
return list
}
//根据action的值调用修改函数
case actionTypes.CHANGE_TPMB_STAR:
return {
...state,
tpmbList:changeTpmbStar(Object.assign([],state.tpmbList),action.data)
}
具体实现为:
视频细节页面实现
上篇文章中这样页面没有用真正的视频,仅仅用图片进行了占位,这次我准备好视频了,并且实现了视频的播放,暂停等功能,视频细节页面也与图片细节页面相仿,能从本视频id开始一直向下滑,实现收藏功能
- 实现视频的占位和播放
// 点击实现视频的暂停播放
const videoRef = useRef(null)
const [play,setPlay] = useState(false)
const onVideo = () =>{
if(play) {
videoRef.current.pause();
setPlay(false)
}else{
videoRef.current.play();
setPlay(true)
}
}
//放置视频
<video
controls="controls" //进度条
src={item.videos}
onClick={onVideo}
ref={videoRef}
loop
autoPlay="autoplay" //自动播放
muted="true" //静音
/>
- 实现收藏功能
过程与图片模板收藏相似,也是将id传入数据列表,在进行修改数据后返回
- 在
mapDispatchToProps
中dispatch
一下action
const mapDispatchToProps =( dispatch ) =>{
return {
changeSpmbListStar(data){
dispatch(actionCreators.changeSpmbListStar(data))
}
}
}
- 在reducer中根据id进行数据的修改
// 修改收藏star1的值
const changeSpmbStar =(list,id) => {
let ind = list.findIndex(data => id == data.id);
list[ind].star2 = !list[ind].star2;
return list
}
// 调用函数
case actionTypes.CHANGE_SPMB_STAR:
return {
...state,
spmbList:changeSpmbStar(Object.assign([],state.spmbList),action.data) //浅拷贝
}
具体效果为:
由于和这个相机相关的视频链接因为跨域容易被禁止,所有这个项目的视频都是下载至本地来进行展示,也因为这个原因导致gitPage不能实现,我就给大家录了个较为完整的视频,大家将就看一下吧~
结束语
经过这段时间的修改和完善,项目的完善度也是更近一步了!还是有很多不足,和功能还未实现,我将继续努力,继续完善这个项目,大家有什么建议也可以在评论区告诉我,我们下期再见~
(ps:本篇文章图源网络,如有侵权,请联系我删除)