美文网首页面试
前端常见问题总结

前端常见问题总结

作者: 小本YuDL | 来源:发表于2019-12-02 21:15 被阅读0次

    补:get请求传参长度的误区

    • 实际上HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度。

    • 不同的浏览器和WEB服务器,限制的最大长度不一样要支持IE,则最大长度为2083byte,若只支持Chrome,则最大长度 8182byte

    补:get和post请求在缓存方面的区别

    • get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存
    • post不同,post做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存。

    1.三次握手and四次挥手

    • 三次握手
      详细描述:
      客户端发送连接请求报文,服务器接受连接后回复ACK报文,并为这次连接分配资源。客户端接收到ACK报文后也向服务器发生ACK报文,并分配资源,这样TCP连接就建立了。
      简单的理解:

      • 客户端看到服务器,打声招呼(发送syn);
      • 服务器收到客户端的招呼,也向客户端打招呼,表示他看到了(发送syn+ack
      • 客户端看到服务器的回应,相当建立沟通(发送ack),表示很开心

      详细过程:

      • 1)第一次握手:A的TCP客户进程也是首先创建传输控制块TCB,然后向B发出连接请求报文段(首部的同步位SYN=1,初始序号seq=x)
      • 2)第二次握手:B收到连接请求报文段后,如同意建立连接,则向A发送确认,在确认报文段中(SYN=1,ACK=1)
      • 3)第三次握手:TCP客户进程收到B的确认后,要向B给出确认报文段(ACK=1)
    image.png
    • 四次挥手
      TCP断开链接的过程和建立链接的过程比较类似,只不过中间的两部并不总是会合成一步走,所以它分成了4个动作。
      简单理解:
      • 客户端挥手(fin)
      • 服务器伤感地微笑(ack)
      • 服务器挥手(fin)
      • 客户端伤感地微笑(ack)。
        详细描述:
      • 1)客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,客户端进入FIN-WAIT-1(终止等待1)状态
      • 2)服务器收到连接释放报文,发出确认报文,ACK=1,服务端就进入了CLOSE-WAIT(关闭等待)状态
      • 3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态。
        服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1。服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
      • 4) 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1。此时,客户端就进入了TIME-WAIT(时间等待)状态。
        注意此时TCP连接还没有释放,必须经过2个MSL(最长报文段寿命)的时间后(即两分钟),当客户端撤销相应的TCB后,才进入CLOSED状态
        服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
    • 总的说就是:
      客户端要断开,告诉服务器,服务器同意断开连接。
      服务器发送完最后数据,服务器要断开,告诉客户端,客户端同意断开连接。
      客户端同意断开连接,服务器立马close。但是客户端还要等待两分钟。
    image.png
    • 为什么连接的时候是三次握手,关闭的时候却是四次握手?

    答:中间的两个动作没有合并,是因为tcp存在「半关闭」状态,也就是单向关闭。
    因为当Server端收到Client端的syn连接请求报文后,可以直接发送syn+ack报文。其中ack报文是用来应答的,syn报文是用来同步的。但是关闭连接时,当Server端收到fin报文时,很可能并不会立即关闭,所以只能先回复一个ack报文,告诉Client端,"你发的fin报文我收到了"。只有等到我Server端所有的数据报文都发送完了,我才能发送fin报文,因此不能一起发送。故需要四步握手。

    • 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

    答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。


    2.url到页面渲染完成的经过

    大致分为三步:

    • 1. 域名解析
      • 浏览器会将输入的域名解析成相应的ip地址
        1. 查看浏览器内部缓存
        1. 查看本机的host文件,会查看本机的host文件下,是否存了对应的ip地址
        1. 本地路由器的DNS解析
        1. 查看网络服务DNS
        1. 查询到ip地址后,开始建立TCP三次握手,与服务器建立连接
        1. 通过协议(http)向目标主机发送请求
    • 2. 服务器接收请求并返回数据
        1. 服务器接收到了浏览器发送的请求后,根据某个协议,通过web-server把浏览器发送的数据进行打包(包含请求头,ip地址,请求路径和查询参数等)
        1. web-server把数据打包后,发送给网站代码(比如django、flask、node.js等后端服务)
        1. 后端服务软件会根据路径和查询参数进行相应处理,返回给浏览器对应的数据包(包括http协议组成的代码。里面包含页面的布局、文字。数据也可能是图片、脚本程序,反应头,反应数据,请求头等)
    • 3. 浏览器接收数据并渲染页面
        1. 浏览器接收到返回的数据包,根据浏览器的渲染机制对相应的数据进行渲染。
        1. 渲染后的数据,进行相应的页面呈现和脚步的交互。

    3.js引擎的执行机制

    (1) JS是单线程语言
    (2) JS的 Event Loop是JS的执行机制。

    • js为什么是单线程的呢?
      因为,如果是多线程,若几个线程同时操作dom的话,浏览器该怎么执行呢。

    • js为什么需要异步呢?
      js中不存在异步,是从上而下顺序执行的,但是这样很容易阻塞,若某一句代码解析执行时间很长,那用户就需要等待很长时间,所以需要异步执行。

    • js怎么实现异步呢?
      就是通过本节的核心事件循环(Event Loop)了,那事件循环具体是什么呢?
      比如:

    cosole.log(1);
    setTimeOut(function(){
        cosole.log(2);
    },0);
    cosole.log(3);
    执行的输出顺序是: 1  3  2
    

    js是顺序从上到下执行,但是setTimeOut是最后才执行的,就证明了异步的存在。js也就将任务分为:同步任务和异步任务。

    • 那事件循环具体怎么循环?
      • 1.js判断是同步事件还是异步事件,同步就进入执行栈,异步事件被挂起
        1. 异步事件返回结果后,就进入消息队列
        1. 同步任务进入执行栈后一直执行,直到执行栈为空时,才会去消息队列中查看是否有可执行的异步任务,如果有就推入执行栈中

    循环执行上述三步,直到执行栈为空,就是事件循环了

    所以上面例子的执行顺序分析是怎样的呢?

    console.log(1) 是同步任务,放入主线程(执行栈)里
    setTimeout() 是 异步任务,被挂起, 0秒之后被推入消息队列里
    console.log(3 是同步任务,放到主线程(执行栈)里
    
    当 1、 3输出后,主线程去消息队列(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数,输出2
    

    以上就是event loop 的简单分析了。但是只是很浅的一部分,因为还有下面这样情况:

    setTimeout(function(){
         console.log('定时器')
     });
     
     new Promise(function(resolve){
         console.log('开始for循环');
         for(var i = 0; i < 10000; i++){
             i == 99 && resolve();
         }
     }).then(function(){
         console.log('执行then')
     });
      console.log('执行结束');
    
    

    对于这样多个异步的事件,按照之前的分析应该输出:开始for循环 --> 执行结束 --> 定时器 --> 执行then
    但是实际的输出却是: 开始for循环 --> 执行结束 --> 执行then --> 定时器

    会发现 是 先执行promise 再执行的setTimeOut ,那难道是异步任务的执行顺序,不是前后顺序,而是另有规定? 事实上,按照异步和同步的划分方式,并不准确。

    而准确的划分方式是:

    • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
    • micro-task(微任务):Promise,process.nextTick

    按照这种分类方式:JS的执行机制

    • 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里

    • 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完

    重复以上2步骤,再结合前面的事件循环,就是更为准确的JS执行机制了。

    所以上面例子的执行顺序分析是怎样的呢?

    先执行script宏任务
    遇到 setTimeOut 是宏任务,将其放入宏任务队列
    遇到 new Promise直接执行,打印  "开始for循环"
    遇到 then 是微任务,将其放入微任务队列
    打印 "执行结束"
    本轮宏任务(script)执行完毕,检查微任务队列,遇到then,执行输出 " 执行then",就只有这一个微任务,所以执行结束
    本轮 event loop 执行结束
    进入下一轮 
    执行宏任务 setTimeOut,打印 "定时器"
    再查看微任务队列,没有微任务
    执行完毕
    

    4.hash与history的区别

    • hash模式

      • hash就是指url尾巴后的#号以及后面的字符,hash值变化不会导致浏览器向服务器发出请求,而且hash改变会触发hashchange事件,浏览器的进后退也能对其进行控制,所以人们在html5的history出现前,基本都是使用hash来实现前端路由的。
      • hash出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面
      • hash 本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。其次,hash的传参是基于url的,如果要传递复杂的数据,会有体积的限制
    • history模式

      • history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。
        history——— 利用了HTML5 History Interface 中新增的 pushState()和replaceState()方法。
        (需要特定浏览器的支持)history不能运用与IE8一下
    • pushState()和 replaceState()的区别:

      • pushState 是创建新的历史纪录
      • replaceState是修改当前历史纪录
    window.history.pushState(state,title,url)
      state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
      title:标题,基本没用,一般传null
      url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。
      如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成       https://www.baidu.com/a/qq/,
      执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
    
    window.history.replaceState(state,title,url)
      与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录
    
    window.addEventListener("pospstate",function(){
       监听浏览器前进后退事件,pushState与replaceState方法不会触发
    })
    window.history.back()   后退
    window.history.forward()   前进
    window.history.go(1)   前进一部,-2回退两不,window.history.lengthk可以查看当前历史堆栈中页面的数量
    

    这两个方法应用于浏览器的历史纪录站,在当前已有的back、forward、go 的基础之上,他们提供了对历史纪录进行修改的功能,只是当他们执行修改使,虽然改变了当前的url,但你的浏览器不会立即像后端发送请求

    • 404错误

    1、hash模式下,仅hash符号之前的内容会被包含在请求中,如 http://www.abc.com 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误;
    2、history模式下,前端的url必须和实际后端发起请求的url一致,如http://www.abc.com/book/id 。如果后端缺少对/book/id 的路由处理,将返回404错误。


    5.vue钩子函数

    (1)与生命周期有关的生命周期函数: beforeCreatecreatedbeforeMountedmountedbeforeUpdateupdatedbeforeDestorydestoryed
    (2)computedwatchfilter
    (3)自定义指令directive的钩子函数

    • bind : 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作
    • inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document)
    • update: 被绑定元素所在的模板更新时调用,而不论绑定值是否
    • componentUpdated: 被绑定元素所在模板完成一次更新周期时调
    • unbind: 只调用一次,指令与元素解绑时

    常用参考链接:https://www.jianshu.com/p/8314ccd03fa9


    6.vue常用指令

    • v-for v-for="字段名 in(of) 数组json" 循环数组或json
    • v-model 数据的双向绑定
    • v-if 显示与隐藏 ,是创建和删除元素
    • v-else-if 必须和v-if连用
    • v-else 必须和v-if连用 不能单独使用 否则报错 模板编译错误
      • v-show 显示内容,只是切换display值
      • v-hidden 隐藏内容
    • v-bind 动态绑定
    • v-bind:class 3种绑定方法
      • 1.对象型 {red:isred}
      • 2.三元型 isred?"red":"blue"
      • 3.数组型 [{red:"isred"},{blue:"isblue"}]
    • v-on 监听dom事件,可以缩写为@,例如绑定一个点击函数 ,函数必须写在methods里面
    • v-text 解析文本
    • v-html 解析html标签
    • v-once 进入页面时 ,只渲染一次,不在进行渲染

    常用参考链接:https://blog.csdn.net/dz13271116886/article/details/99708315


    7.vue常用修饰符

    • 事件修饰符(5个)
      • .stop:阻止事件冒泡
      • .prevent :阻止默认事件
      • .once :只执行一次
      • .capture :捕获事件,与冒泡相反
      • .self :只触发自身事件
    • 键盘修饰符(9个)
      • .enter:回车键
      • .tab:制表键
      • .delete:含delete和backspace键
      • .esc:返回键
      • .space: 空格键
      • .up:向上键
      • .down:向下键
      • .left:向左键
      • .right:向右键
    • v-modle修饰符(3个)
      • .number:将输出字符串转为Number类型
      • .lazy:在改变后才触发(也就是说只有光标离开input输入框的时候值才会改变)
      • .trim:自动过滤用户输入的首尾空格

    详细参考链接:https://blog.csdn.net/qq_42238554/article/details/86592295


    8.vue常用组件

    • vue-cli : 项目构建工具
    • vue-router:路由
    • vuex:状态管理
    • axios:http请求
    • 组件库的组件 eg:Element-ui 、iview

    详细参考链接:sohu.com/a/328202078_120047065


    9.vue 过滤器

    在vue中提供了Vue.filter('filterName',fn)来定义一个过滤器。
    过滤器可以在HTML代码中使用,对动态拿到的数据进行过滤
    filter不会修改原始数据,它的作用是过滤数据。
    通过|管道符来过滤前面数据

    • 过滤器参数:
      • 第一个参数 fliterName:是过滤器的名字
      • 第二个参数 fn :是过滤器功能函数(两个参数)
    • 过滤功能函数参数:
      • 第一个参数是传入的要过滤数据,即原数据。
      • 第二个参数开始就是html调用过滤器的时候传入的参数。

    用法参考链接:https://blog.csdn.net/badmoonc/article/details/81485803


    10.MVVM的理解

    MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

    响应式原理

    观察者-订阅者(数据劫持):

    • vueObserver 数据监听器,把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用Object.defineProperty()方法把这些属性全部转成setter、getter方法。当data中的某个属性被访问时,则会调用getter方法,当data中的属性被改变时,则会调用setter方法。
    • Compile指令解析器,它的作用对每个元素节点的指令进行解析,替换模板数据,并绑定对应的更新函数,初始化相应的订阅。
    • Watcher 订阅者,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。
    • Dep 消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发notify 函数,再调用订阅者的 update 方法

    实现方法:

    • 1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;
    • 2.实现Observe,监听所有的数据,并对变化数据发布通知;
    • 3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。
    • 4.结合上述方法,向外暴露mvvm方法。


      执行过程

    过程描述:
    (1) 当创建一个vue对象时,先进入初始化阶段:(两部分工作)
    一方面:vue 会遍历data的所有属性,通过object.defineproprety()方法,将所有属性变成setter和getter。 另一方面:vue的指令编译器Complie会解析每个元素节点,初始化视图,然后由watcher(订阅者)更新视图,此时watcher会将自身添加到消息订阅器(Dep)中,初始化完毕。

    (2) 当数据变化时:会触发 observer数据监听器中的setter方法,setter 会调用Dep中的方法,此时Dep会去遍历所有的订阅者,然后去调用订阅者的update方法,通知订阅者进行视图更新。

    参考:https://segmentfault.com/a/1190000018399478


    11.vue生命周期

    Vue实例有一个完整的生命周期,也就是说从开始创建、初始化数据、编译模板、挂在DOM、渲染-更新-渲染、卸载等一系列过程

    • beforeCreate :创建之前,在实例初始化之后,数据观测和事件配置之前被调用。
    • created: 创建完成,实例已完成以下配置:数据观测、属性和方法的运算,watch/event事件回调,完成了data 数据的初始化,el没有。此时dom还没渲染,可以在此处进行ajax请求。
    • beforeMount : 挂载之前,相关的render函数首次被调用(虚拟DOM),实例已完成以下的配置: 编译模板,把data里面的数据和模板生成html,完成了el和data 初始化,注意此时还没有挂在html到页面上。
    • mounted:挂载完成,此时dom已渲染完成,可以访问dom元素,只在挂载到vue 对象上执行一次,而后每次更新执行的都是update
    • beforeUpdate:在数据更新之前被调用
    • updated:数据更新之后,该钩子在服务器端渲染期间不被调用
    • beforeDestroy:销毁之前,此时vue实例依然可以使用
    • destroyed:销毁,所有的事件监听器会被移出,所有的子实例也会被销毁,该钩子在服务器端渲染期间不被调用

    详细参考连接:https://www.jianshu.com/p/672e967e201c


    12.vue动态路由

    在vue项目中,使用vue-router如果进行不传递参数的路由模式,则称为静态路由;
    如果能够传递参数,对应的路由数量是不确定的,此时的路由称为动态路由。

    比如在写商品详情页面的时候,页面结构都一样,只是商品id的不同,所以这个时候就可以用动态路由动态。

    冒号后面就是动态的参数
    路由配置:
    const router = new VueRouter({
        routers:[
          {
            path:'/home:id'
            name:'home'
            component:home
          }
        ]
    });
    
    使用:
    <template>
      <div>
        <router-link to="/home/10">衣服</router-link>
        <router-link to="/home/11">麻辣火锅</router-link>
        <router-link to="/home/12">肉夹馍</router-link>
      </div>
    </template>
    

    实现参数传递的方法:

    1. 使用query传参,name属性为要跳转的组件所对应的name,query为要携带的参数
    <router-link :to="{name:'main','query':{data:'allData'}}"></router-link>
    
    1. 使用params传参,name属性为要跳转的组件所对应的name,params为要携带的参数
    <router-link :to="{name:'main','params':{data:'allData'}}"></router-link>
    
    • 使用params传参时,url中不会出现参数,页面刷新后参数会消失
    • 使用query传参时,url中会出现参数,页面刷新后参数不会消失

    3.设置页面默认的路由参数(query/params):

     this.$router.push( {name: 'main', 'query': {data: 'allData'} } )
    this.$router.push( {path: '/main', 'query': {data: 'allData'} } )
    
    • 在组件中接受参数 : this.$route.query.data || this.$route.params.data

    13.post、get、put、delete

    post、get、put、delete就像对应着数据库的CRUD(增、删、改、查)

        post             /url          创建
        delete           /url/xxx       删除
        put              /url/xxx       更新或创建
        get              /url/xxx       查询
    

    (1) get请求,请求会向数据库发索取数据的请求,从而来获取信息,该请求就像数据库的select操作一样,只是用来查询一下数据,不会修改、增加数据,不会影响资源的内容,即该请求不会产生副作用。无论进行多少次操作,结果都是一样的,具有幂等性。

    (2) put 请求,求是向服务器端发送数据的(与get不同)从而改变信息,该请求就像数据库的update操作一样,用来修改数据的内容,但是不会增加数据的种类等,也就是说无论进行多少次put操作,其结果并没有不同,具有幂等性。

    (3) post 请求,与put请求类似。都是向服务器端发送数据求会改变数据的种类等资源,就像数据库的insert操作一样,会创建新的内容。几乎目前所有的提交操作都是用POST请求的。不具有幂等性。

    (4) delete 请求,用来删除某一资源,该请求就像数据库的delete操作。

    put 与post 的共同点及区别?

    • put和post 都是向服务器发送数据
    • post 主要是在一个集合资源之上(url),put 主要作用在一个具体的资源之上(url/xxx)
    • put 通常指定了资源的存放位置,而post没有。post的数据存放位置由服务器自己决定,如果url可以在客户端确定,那么可使用put,否则用post
    • put 有等幂性,而post没有。
      幂等性:幂等意味着对同一个URL的多次请求会返回一样的结果

    14.跨域方法

    • jsonp
    • CORS
    • webSocket
    • postMessage

    详情链接:http://www.imooc.com/article/40074


    15.vue插件(图表,excel)


    16.callback、promise、async-await

    请参考链接


    17.map、reduce、filter、forEach

    • map、filter、reduce会返回新数组,返回值是新数组或结果
    • forEach会改变原来数组,forEach没有返回值
    • map:用来迭代对数组进行统一的操作(运算),返回一个新数组
    • reduce: 用来迭代一个数组,并且把它累积到一个值中
    • filter:用来迭代一个数组,并且按给出的条件过滤出符合的元素

    18.for、forEach、 for-in 、 for-of

    • for循环
      遍历数组

    • forEach循环
      遍历数组,对象(不包括原型上的属性)
      循环不能中途退出,不能使用break,return

    • for-in
      这个循环是特别针对遍历对象属性的。
      会遍历对象的所有属性,包括原型上的属性和自定义属性
      对象的属性是没有顺序的,所以for-in遍历属性输出也是没有顺序的
      若对象是null或undefined有可能会报错

    • for-of
      这个循环是最棒的,不仅支持数组,还支持遍历类数组对象和其他可迭代对象。
      可以使用 break、continue、return
      for-of循环也支持字符串遍历,将字符串视为一系列的Unicode字符来进行遍历
      for-of也支持Map和Set遍历。
      for-of不遍历普通对象。

    小总结:

    • for-in循环的每次迭代操作会同时搜索实例或者原型属性,for-in循环的每次迭代会产生很多开销。除非明确要迭代一个属性数量未知的对象,否则应该避免使用。 for-in 并不适合用来遍历数组中的元素,其更适合遍历对象中的属性。
    • forEach循环不会遍历原型链上的属性,不能break和return。
    • for-of 循环这是最直接、最简洁的遍历数组的方法。这个方法避开了for-in循环的所有缺陷
    • forEach 的速度不如 for
    • for in循环出的是key,for of循环出的是value
      for-in 、for-of遍历普通对象
    let  arr = {
      name:'aaa',
      age:23,
      sex:'女'
    }
    普通对象要加可枚举的属性Object.keys(),不然报错
    for(let i of Object.keys(arr)){
      console.log(i);
    }  //name age sex
    
    for(let i in arr){
      console.log(i);
    }  //name age sex
    
    

    for-of 遍历 Map ,初始是一个二维数组,对应的键值匹配

    let test = new Map([['name','aaa'],['age',12],['sex','女']]);
    for (var [key, value] of test) {
      console.log(key +" is "+ value);
    }
    //name is aaa
    //age is 12
    //sex is 女
    

    for-of 遍历 Set ,遍历同时会进行数组的去重

    var test = new Set([1,1,2,3,4,5]);
    for (var i of test) {
      console.log(i);
    }
    //1,2,3,4,5
    

    相关文章

      网友评论

        本文标题:前端常见问题总结

        本文链接:https://www.haomeiwen.com/subject/hyliictx.html