设计模式
2020-04-16 11:10:11 0 举报
AI智能生成
张容铭《JavaScript 设计模式》阅读笔记
作者其他创作
大纲/内容
设计原则
开放-封闭
在面向对象的程序设计中,开放-封闭原则(OCP)是最重要的一条原则
软件实体(类、模块、函数)等应该是可以扩展、但是不可修改的。
在不修改原始代码的情况下满足新需求例如:给 window.onload 函数添加新的功能
修改之前的代码,很可能会导致“改好一个 bug,引发其他 bug”
使用装饰者模式
window.onload = function() {\u00A0 console.log(\"之前的window.onload回调\
通过动态装饰函数的方式,我们完全不用理会从前 window.onload 函数的内部实现,无论它的实现优雅或是丑陋。就算我们作为维护者,拿到的是一份混淆压缩过的代码也没有关系。只要它从前是个稳定运行的函数,那么以后也不会因为我们的新增需求而产生错误。新增的代码和原有的代码可以井水不犯河水。
找出变化的地方
把程序中不变的部分隔离出来,然后把可变的部分封装起来,这样一来程序就具有了可扩展性。
方式
用对象的多态性消除条件分支对象的多态性不好理解,这里就理解成将一组类似对象可变的地方封装到函数中
过多的条件分支语句是造成程序违反开放-封闭原则的一个常见原因。每当需要增加一个新 的 if 语句时,都要被迫改动原函数。实际上,每当我们看到一大片的 if 或者 swtich-case 语句时,第一时间就应该考虑,能否利用对象的多态性来重构它们。利用对象的多态性来让程序遵守开放-封闭原则,是一个常用的技巧。
// 不变部分隔离var makeSound = function( animal ){ animal.sound();};var Duck = function(){};// 可变部分封装Duck.prototype.sound = function(){ console.log( '嘎嘎嘎' );};var Chicken = function(){};Chicken.prototype.sound = function(){ console.log( '咯咯咯' );};makeSound( new Duck() ); // 嘎嘎嘎makeSound( new Chicken() ); // 咯咯咯/********* 增加动物狗,不用改动原有的 makeSound 函数 ****************/var Dog = function(){}; Dog.prototype.sound = function(){console.log( '汪汪汪' ); };makeSound( new Dog() ); // 汪汪汪
放置挂钩
放置挂钩(hook)也是分离变化的一种方式。我们在程序有可能发生变化的地方放置一个挂钩,挂钩的返回结果决定了程序的下一步走向。这样一来,原本的代码执行路径上就出现了一个 分叉路口,程序未来的执行方向被预埋下多种可能性。在模版方式模式中,由于子类的数量是无限制的,总会有一些“个性化”的子类迫使我们不得不去改变已经封装好的算法骨架。于是我们可以在父类中的某个容易变化的地方放置挂钩,挂钩的返回结果由具体子类决定。这样一来,程序就拥有了变化的可能。
使用回调函数
回调函数是一种特殊的挂钩。我们可以把一部分易于变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。当回调函数被执行的时候,程序就可以因为回 调函数的内部逻辑不同,而产生不同的结果。
比如,我们通过 ajax 异步请求用户信息之后要做一些事情,请求用户信息的过程是不变的, 而获取到用户信息之后要做什么事情,则是可能变化的: var getUserInfo = function( font color=\"#924517\
行为型
用于不同对象之间职责划分或算法抽象。不仅涉及类和对象,还涉及类或对象之间的交流模式并加以实现。
模版方法
模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类在不改变算法架构的情况下,重新定义算法中的某些步骤。
弹窗示例
模版类
class Alert { constructor(data) { if (!data) return; this.content = data.content; this.panel = document.createElement(\"div\"); //... } init() {} bindEvent() {} show() {} hide() {}}
子类
class RightAlert extends Alert { constructor(data) { super(data); this.confirmBtn.className += \" right\"; }}class TitleAlert extends Alert { constructor(data) { super(data); this.title = data.title; }}let c = new TitleAlert({ title: \"hh\
class CancelAlert extends TitleAlert{ constructor(data){ super(data) this.cancel=data.cancel this.cancelBtn=document.createElement('span') //... } // 修改 init 方法 init(){// 先调用父类 init 方法 super.init() this.panel.appendChild(this.cancelBtn) }}
钩子方法
在模板方法模式中,我们在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但对于一些特别“个性”的子类,框架可能就不适用了。比如我们在饮料类 Beverage 中封装了 饮料的冲泡顺序:(1) 把水煮沸(2) 用沸水冲泡饮料 (3) 把饮料倒进杯子 (4) 加调料这 4 个冲泡饮料的步骤适用于咖啡和茶,在我们的饮料店里,根据这 4 个步骤制作出来的咖 啡和茶,一直顺利地提供给绝大部分客人享用。但有一些客人喝咖啡是不加调料(糖和牛奶)的。 既然 Beverage 作为父类,已经规定好了冲泡饮料的 4 个步骤,那么有什么办法可以让子类不受这个约束呢?钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向。
模板类
class Beverage { boilWater() { console.log(\"把水煮沸\"); } brew() { throw new Error(\"子类必须重写 brew 方法\"); } pourInCup() { throw new Error(\"子类必须重写 pourInCup 方法\"); } addCondiments() { throw new Error(\"子类必须重写 addCondiments 方法\"); } customerWantsCondiments() { return true; // 默认需要调料 } init() { this.boilWater(); this.brew(); this.pourInCup(); if (this.customerWantsCondiments()) { this.addCondiments(); } }}
class Coffee extends Beverage { brew() { console.log(\"用沸水冲泡咖啡\"); } pourInCup() { console.log(\"把咖啡倒进咖啡杯\"); } addCondiments() { console.log(\"加糖和牛奶\"); } customerWantsCondiments() { return window.confirm(\"请问需要调料吗?\"); }}let coffee=new Coffee()coffee.init()
模版方法 vs 策略
发布订阅
测试:let event = new Event();// event.$on(\"event1\
发布订阅 vs 观察者
在观察者模式中,被观察者 维护着一个 它的观察者列表;在发布订阅模式中,发布者和订阅者不知道对方的存在,它们只能通过消息代理进行通信。观察者模式是强耦合的,而发布订阅模式是松散耦合的。
参考
https://blog.csdn.net/Firvana_Mutex/article/details/82696406
状态模式
类的行为是基于它的状态改变的
状态模式的关键是区分事物内部的状态
最终目的是简化分支判断流程
将每个分支转化为一种状态独立出来,方便每种状态的管理又不至于每次执行时遍历所有分支
超级玛丽示例
var m=new superMary()font color=\"#924517\
// 将 if else 判断变成对象上的状态function superMary(action){ this.state=[]font color=\"#924517\
策略模式
将定义的一组算法封装起来,使其相互之间可以替换
表单验证示例
const InputStrategy = (function() { let Strategy = { notNull(val) { // \\s 匹配一个空白字符,包括空格、制表符、换页符和换行符。 return /\\s+/.test(val) ? \"请输入内容\" : \"\
策略 vs 模版方法
模板方法模式基于继承的思想,而策略模式则偏重于组合和委托
职责链模式
解决请求的发送者与接受者之间的耦合。通过职责链上多个对象分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。
步骤
分解需求流程每个对象只做分内的事无关的事情传到下一个对象中做,直到需求完成
优点
即便项目经理某一模块需求不确定,也不影响开发其他模块
方便进行单元测试
请求发送者只需要知道链中的第一个节点,从而弱化了发送者和接收者之间的强联系。
封装 axios,方便给请求前请求后增加功能
var a = new myAxios({ baseURL: \"xxx\"});a.send({ url: \"xxx\
function font color=\"#16884a\
function axiosBuild(config) { const service = axios.create(config); return service;}function beforeSend(config) { this.axios.get(\"xxxurl\" + config.url); return font color=\"#924517\
命令模式
对命令进行封装,将发出命令的责任和执行命令的责任分割开
canvas 绘图命令示例
使用 canvas 绘图时需要不停使用 canvas 元素的上下文引用,这在多人项目中耦合度比较高。通常会将上下文引用对象安全地封装在一个命令对象内部,如果他人想绘图,就通过命令对象书写一条命令。
CanvasCommand.excute([ { command: \"fillStyle\
const CanvasCommand = (function() { let canvas = document.getElementById(\"canvas\"); let ctx = canvas.getContext(\"2d\");font color=\"#c41230\
访问者模式
它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作
不常用,书中的例子目前看着没有用处
中介者模式
解除对象与对象之间的紧耦合关系,降低多个对象和类之间的通信复杂性
增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用。当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方
示例
泡泡堂游戏
玩家可以组队pk玩家可以换组玩家可能会死亡一队人全部失败时其所在的组失败...
中介者 playerDirector 对象
玩家加入、换组、死亡等时只用通知中介者,中介者来负责做通知其他人等的操作
可以用发布-订阅模式实现中介者
将 playerDirector 实现为订阅者,各 player 作为发布者;一旦 player 的状态发生改变,便推送消息给 playerDirector,playerDirector 处理消息后将反馈发送给其他 player。
缺点
系统中会新增一个中介者对象
中介者对象要占去一部分内存
它了解整个链条中的所有关系,中介者对象自身往往是一个难以维护的对象
备忘录模式
保存一个对象的某个状态,以便在适当的时候恢复对象
JS 编程中常用此模式来对服务端数据做缓存备份
重复数据反复请求不仅增加了服务器端的压力,请求数据的等待过程也会影响客户端的用户体验
在备忘录模式中,数据常常存储在备忘录对象的缓存器中,对于数据的读取要通过调用备忘录提供的方法,因此备忘录对象也是数据缓存器的一次保护性封装,防止外界的直接访问。
迭代器模式
在不暴露对象内部结构的同时,可以顺序访问聚合对象内的元素
就像银行里的点钞机,可以降低点钞成本,安全而可靠
在开发中极大简化了代码中的循环语句,使代码结构清晰紧凑;这些简化了的循环语句实质上隐形地移到了迭代器中。
使用迭代器时,不用关心对象的内部结构,解决了对象的使用者和对象内部结构之间的耦合
提供了操作对象的统一接口
示例:font color=\"#16884a\
getVmVal('a')getVmVal('a.b')getVmVal('a.b.v')
function getVmVal(expr) { if(!expr) return let keys = expr.split(\".\
setVmVal(\"k\
解释器模式
提供了评估语言的语法或表达式的方式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
获取元素的 XPath(元素在页面中所处的位置路径)
技巧型
通过一些特定的技巧来解决组件某方面的问题,一般通过实践经验总结得到
链式调用模式
JS 中链式调用是通过在对象每个方法调用执行完毕后返回当前对象 this 来实现的
委托模式
多个对象接收并处理同一请求时,将请求委托给另一个对象统一处理
将子元素的事件委托给父元素
// 这里可以使用font color=\"#924517\
优化页面中事件的数量可以给未来的子元素间接绑定事件
数据访问对象(Data access object DAO)模式
抽象和封装对数据源的访问与存储
localStorage
使用:const LJ = new BaseLocalStorage(\"LJ_\");LJ.set(\"name\
MongoDB
config.js
module.exports = { DB: { name: \"demo\
db.js
var mongodb = require(\"mongodb\");var config = require(\"./config\
节流模式
涉及技巧:高阶函数(这里的返回值是函数)闭包
字符串模板模式
用正则匹配方式去格式化字符串拼凑出视图避免创建试图时的大量节点操作
主要包括三部分
字符串模版库
格式化方法
字符串拼接操作
结合策略模式
A.init({ type: \"listPart\
const A = (function() {// 这里的代码移到了左边👈 const Strategy = { listPart(data) { let $container = document.createElement(\"div\
const root = document.getElementById(\"container\");// 字符串格式化方法functionfont color=\"#16884a\
惰性模式
通过对对象重定义来屏蔽原对象中的分支判断,去掉每次代码执行时重复的特性判断
两种实现方式
在文件加载进来时通过闭包执行该方法对其进行重新定义
页面加载时会占用一定资源
let A={}font color=\"#f1753f\
在函数第一次调用的时候对其重定义
减少文件加载时的资源消耗
等待者模式
Promise.all()
MyPromise.all = function(values) { // font color=\"#f1753f\
font color=\"#c41230\
let fs =require('fs')function read(url){font color=\"#f1753f\
MyPromise.defer = MyPromise.deferred = function() { font color=\"#f1753f\
架构型
模块化
模块管理器
创建
调度
同步模块调度
异步模块调度
同步模块
异步模块
这一章的代码没有跑通,如果需要了解,考虑去看实现了 AMD 规范的 require.js 的源码分析
require.js 源码分析
define
require
1、检查依赖的模块,根据配置文件,获取js文件的实际路径2、根据js文件实际路径,在dom中插入script节点,并绑定onload事件来获取该模块加载完成的通知3、依赖script全部加载完成后,调用回调函数
学源码流程
源码如何组织->debug理清调用链->参考别人分析->demo校验->总结
https://blog.csdn.net/zqjflash/article/details/43373957
MV*
设计模式
合适模式的意义
好代码
不同的需求用最适合其的模式去解决时,开发成本会减少很多
功能
灵活高效可扩展
结构
结构清晰
技术准备
为函数自身/函数原型添加方法
函数式调用
methods.checkEmail().checkName();
类式调用
var m=new Methods()m.checkEmail().checkName()
注意事项
链式调用,注意返回自身
使用类继承时将方法定义在父类的 prototype 上;如果放在函数内部通过 this 定义,每 new 一次就要复制一下它自己本身的方法,会造成浪费
实例化类的安全模式(忘记使用 new 创建时抛出错误或者处理成正确的结果)
es6
const book=Book('js') // es6 语法会自动检测并报错// const book=new Book('js') console.log(book)
class Book { constructor(title) { this.title = title; }}
es5
var book=Book('js')// var book=new Book('js')console.log(book)
var Book=function(title){ if(this instanceof Book){ this.title=title }else{ return new Book(title) }}
创建型
处理对象创建的设计模式,通过某种方式控制对象的创建 来避免 基本对象创建时可能导致的设计上问题或复杂度的增加
简单工厂
又叫静态工厂方法,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
百科定义
通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类
像一个魔术师,告诉他要什么就能变什么
const userNameAlert = createPop(\"alert\
工厂方法
定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类当中
let uiObj = new Factory(\"UI\
用 es6 的 class 语法,在父类的 constructor 里调子类,子类里又调 super() 会导致栈溢出
抽象工厂
一般用它作为父类来创建一些子类
建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
相比之前的工厂模式,建造者模式在创建对象时更加复杂一些,更关心创建这个对象的整个过程,甚至每个细节。
在建造过程中,通常将创建对象的类模块化,使得每一个模块都可以得到灵活的运用与高质量的复用。
class Human { constructor(name) { this.name = name; } getName() { return this.name; }}
class Work { constructor(work) { this.work = work; this.addDes(); } addDes() { switch (this.work) { case \"code\": this.title = \"工程师\"; this.des = \"沉醉于编程\"; } } changeWork(work){ this.work=work }}
原型模式
让多个对象分享同一个原型对象的属性与方法
将可复用、共享、耗时大的方法与属性放在原型中,子类继承此原型,将子类中需要重写的方法进行重写
// 上下滑动切换class SlideLoopImg extends LoopImages{ changeImage(i){ console.log(\"SlideLoopImg changeImage function\"); }}// 渐隐切换class FadeLoopImg extends LoopImages{// 重写方法 changeImage(i){ console.log(i) console.log(\"FadeLoopImg changeImage function\"); }}
单例模式
只允许实例化一次的对象类
单例对象的延迟创建
let a = lazySingle();let b = lazySingle();console.log(a === b); //true
涉及到的技巧点:立即执行函数高阶函数(这里立即执行函数的返回值是函数)闭包
结构型
关注如何将类或对象组合成更大、更复杂的结构以简化设计
外观模式
向现有的系统添加一个接口,来隐藏系统的复杂性
接口的二次封装一般就是外观模式
dom 事件兼容
// 获取事件对象function getEvent(e) { // IE 下为 window.event return e || window.e;}function getTarget(e) { let e = getEvent(e); // IE 下为 event.srcElement return e.target || e.srcElement;}function preventDefault(e) { let e = getEvent(e); if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; }}
可以使用惰性模式,去掉每次执行函数时的特性检测
适配器
将一个类的接口适配成用户所期待的
代理模式
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
虚拟代理实现图片预加载
常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里
var myImage = (function(){var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode );return { setSrc: function( src ){ imgNode.src = src; }} })();var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage.setSrc( this.src ); // 这里的this指 img 对象}return {setSrc: function( src ){ myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' ); img.src = src; }} })();proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
装饰者模式
在不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法),使原有对象可以满足用户更加复杂的需求
AOP 面向切片编程Aspect Oriented Programming
主要作用
把一些和核心业务逻辑模块无关的功能抽离出来,给函数增加一层,不用管内部实现
一般会将原始方法保存起来,在运行原始方法的前后进行一些其他操作
给原始函数加方法
Function.prototype.before = function(beforeFunc) { // 这里的 this 是调用 before 的函数 // 在 prototype 上添加方法,可以通过 this 获取原始函数, // 就不用保存原始函数在一个变量里了 // 剩余参数,将所有参数合成一个数组 return (...args) => { beforeFunc(); // 展开运算符 this(...args) };};function say(who) { //todo console.log(who+\"说话\");}let newFn = say.before(function() { console.log(\"说话前\");});newFn('我');
Vue 2.0 函数劫持重写原生方法
// 这里要font color=\"#924517\
源码
react 事务(这里需要了解 wrapper 的内部结构,在其结构中间插入其他功能,就像给饼干加了夹心,所以这是AOP,但不属于装饰者模式)
桥接模式
用于将抽象化与实现化解耦分离,使得二者可以独立变化。通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
组合模式
将对象组合成树形结构以表示\"部分-整体\"的层次结构
用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。
作用/何时使用
用来表示树形结构/部分-整体结构
组合模式可以方便地构造一棵树来表示对象的部分-整体结构。在开发期间不确定这棵树到底存在多少层次的时候组合模式尤其适用。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则。
通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性
客户希望统一对待树中的所有对象时可以考虑使用此模式。组合模式使客户可以忽略组合对象和叶对象的区别, 客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情, 这是组合模式最重要的能力。
请求在树中传递的过程
请求从树最顶端的对象往下传递;如果当前处理请求的对象是叶对象,叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象, 组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。
组合对象可以拥有子节点,叶对象下面就没有子节点。因为用户对单个对象和组合对象的使用具有一致性,所以我们也许会发生一些误操作, 比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加 add 方法,并且在调用这个方法时,抛出一个异常来及时提醒客户
示例:扫描文件夹
文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树
组合模式在文件夹的应用中的好处
我在同事的移动硬盘里找到了一些电子书,想把它们复制到 F 盘中的学习资料文 件夹。在复制这些电子书的时候,我并不需要考虑这批文件的类型,不管它们是单独的 电子书还是被放在了文件夹中。组合模式让 Ctrl+V、Ctrl+C 成为了一个统一的操作。
当我用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式 使得我们只需要操作最外层的文件夹进行扫描。
Folder 类
class Folder { constructor(name) { this.name = name; this.files = []; } add(file) { this.files.push(file); } scan() { console.log(\"开始扫描文件夹: \
File 类
class File { constructor(name) { this.name = name; } add() { throw new Error(\"文件夹下面不能再添加文件\"); } scan() { console.log(\"开始扫描文件: \" + this.name); }}
创建一些文件夹和文件对象, 并且让它们组合成一棵树
let folder = new Folder(\"学习资料\");let folder1 = new Folder(\"JavaScript\");let folder2 = new Folder(\"jQuery\");let file1 = new File(\"JavaScript 设计模式与开发实践\");let file2 = new File(\"精通 jQuery\");let file3 = new File(\"重构与模式\");folder1.add(file1);folder2.add(file2);folder.add(folder1);folder.add(folder2);folder.add(file3);
把移动硬盘里的文件和文件夹都复制到这棵树
移动硬盘中的文件对象:
let folder3 = new Folder(\"Nodejs\");let file4 = new File(\"深入浅出 Node.js\");folder3.add(file4);let file5 = new File(\"JavaScript 语言精髓与编程实践\");
把这些文件添加到原有树中:
folder.add( folder3 ); folder.add( file5 );
在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作。
运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,我们只需要操作树的最顶端对象
folder.scan();
⚠️ 注意
组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键是拥有相同的接口。如文件夹例子里的 add 和 scan 接口。
享元模式
运用共享技术有效支持大量细粒度的对象,避免对象间拥有相同内容造成多余的开销
该模式中会将可以共享的单元缓存起来,以后要用到此类单元时,先检查是缓存中否有已经建立好的单元,有就直接返回缓存中的单元,没有就新建此特定类型的单元并缓存
翻页优化
const shareUnit = (function() { const created = []; let num=5; function create() { let dom = document.createElement(\"div\"); document.getElementById(\"container\").appendChild(dom); created.push(dom); return dom; } return { getDiv: function(i) { if (created.length < num) { return create(); } else { return created[i] } } };})();
// 初始化列表const articles = [ \"11\
// 翻页监听document.getElementById(\"next_page\").addEventListener(\"click\
JavaScript 设计模式 张容铭
JavaScript 设计模式与开发实践 曾探
0 条评论
回复 删除
下一页