Node.js学习第一天笔记

1 服务器创建

  • 创建服务器
    • 利用require引入http模块:var http=require("http")
    • 利用http模块创建server服务器;
      • 创建服务器:var server=http.createServer(function(req,res){})
      • req:为形参,指的是前端向后台发送的请求;
      • res:为形参,指的是后台向前端响应的结果;
  • 服务器监听端口号的设置
    • 通过listen监听端口号:server.listen(8080),端口号可以自定义设置;
  • 浏览器发送请求
    • 在网址栏中设置localhost:8080,加载页面;发送请求,每刷新一次,请求一次;端口号必须与设置的一致;
    • 网址栏中的地址可以是本地IP加端口号;访问页面;
  • 服务器的运行
    • 利用webstorm中右键中的run,在里面运行服务器;
    • 可以在文件夹中利用命令行node 01http-server.js,开启服务器;ctrl+c关闭服务器;

2 服务器中的知识

  • const:定义不变的常量;
  • require引入模块:const http=require("http");
  • req.url:指请求的地址;
  • console.log("这是打印的内容");:指在服务器中打印显示的内容;不在页面的console中显示;
  • res.writeHeader(200,{"Content-Type":"text/html;charset=utf-8"});:指通过writeHeader来设置后台响应的状态码和需要渲染的文件类型及字符集编码;
    • 200:为状态码;
    • content-type:指的是响应的类型;
      • html页面:text/html;加上字符集编码格式,否则会出现乱码;
      • css文件:text/css;
      • JS文件:application/javascript;
      • 图片:image/png或image/x-icon;
  • res.write("这是页面呈现的内容");指在页面上显示的内容;
  • res.end():指的是响应结束,不然在页面中会持续加载;
  • 服务器内容在修改之后,必须重新启动服务器;
  • 注意:chrome浏览器在页面发送一次请求后,会响应两次,其中包含默认响应一次/favicon.ico小图标的响应;所以需要对其进行判断,阻止其响应;
    • 利用req.url来判断请求地址,进行条件判断;小图标的请求地址为/favicon.ico;
     if(req.url==="/favicon.ico"){
         return;//阻止程序执行;
      }
    
  • 基本的http-server服务器的创建代码
     //创建服务器,http模块的运用;
      const http=require("http");
      const server=http.createServer(function (req,res) {
          //request:前端向后台发起请求;请求页面和数据等;
          //response:后台给前端响应;
          if(req.url==="/favicon.ico"){
              return;//阻止程序执行;
          }
          console.log("这是打印的内容");
          //通过writeHeader来设置后台响应的状态码和需要渲染的文件类型及字符集编码;
          res.writeHeader("200",{"Content-Type":"text/html;charset=utf-8"});
          res.write("这是页面呈现的内容");
          res.end();//响应结束
      
      });
      //端口号监听
      server.listen(8080);
    

3 文件系统fs(file System)模块

  • 引入fs模块
    • 代码:const fs=require("fs")

3.1 读取文件

  • 代码:fs.readFile(path,callback)
    • path:文件路径;
    • callback:回调函数;
      • 代码:function(err,data){}
      • err:指路径错误时,执行的代码;if(err){ res.end("404") }
      • data:指路径下的文件中的数据内容;
        • 在浏览器中通过设置字符集编码来呈现数据原来的内容;
        • 在服务器中打印的data数据,为buffer文件,即数据转化为二进制的代码串;通过data.toString()可以拿到正常的数据内容;
  • 路径
    • 定义:指的是在端口后面的部分,自身包含斜杠;
    • 如网址地址localhost:8080/1.txt,里面的请求路径为/1.txt;通过req.url获取;
    • 在服务器中定义变量pathname,设置不同的路径;var pathname="./static"+req.url;
    • 当浏览器请求不同的路径时,服务器就会根据不同的路径读取不同的数据,然后响应不同的数据;
  • 知识点:
    • 创建服务器后,首先要先处理chrome浏览器中小图标icon自动响应的问题;
    • 路径变量的创建;
    • 代码res.end("404")为二合一的写法;指的是res.write("404")res.end()的组合写法;
    • 响应的数据在服务器中打印的内容为buffer,是二进制的编码;在浏览器中的数据为乱码,需要设置字符集编码;在服务器中打印data内容为实际的内容,不是buffer文件,则使用toString()方法;即代码:console.log(data.toString());
  • 注意:
    • node.js不会自动根据路由的不同,渲染不同的内容,它需要我们自己去判断路径的不同,来手动返回不同的内容;
    • 如果不想反复重启服务器,而又想输入不同内容,渲染不容页面的话,可以使用读取文件和处理路径的方式来解决;即设置变量pathname;
  • 读取文件步骤
    • 前端浏览器向服务器发送请求;
    • 服务器接到请求后,去磁盘中读取文件;
    • 把读到的该文件返回给前台;
  • 代码:
     const http=require("http");
     const fs=require("fs");//文件系统模块
     //创建服务器
     const server=http.createServer(function (req,res) {
         //处理icon自动响应的问题;
         if(req.url==="/favicon.ico"){
             return;
         }
         //根据路径不同,返回不同的文件;路径为地址中端口号后面的部分;路径地址中带斜杠;
         var pathname="./static"+req.url;//其中req.url里面是带斜杠的
         //fs.readFile读取文件
         fs.readFile(pathname,function (err,data) {
             if(err){
                 res.end("404");//二合一:res.write("404")和res.end()的组合;
             }
             //设置响应数据在浏览器中的渲染类型和字符集编码
             res.writeHeader(200,{"Content-Type":"text/html;charset=UTF-8"});
             res.end(data);
             console.log(data);//在服务器中打印data数据,为buffer文件,即二进制的一串代码;不同的数据对应不同的二进制代码;
             //打印的数据:<Buffer e5 a4 a9 e7 a9 ba e5 88 86 e5 a4 96 e8 93 9d ef bc 8c e5 8a a0 e6 b2 b9 ef bc 8c e6 98 8e e5 a4 a9 e4 bc 9a e6 9b b4 e5 a5 bd ef bc 81>
         })
     });
     //监听
     server.listen(8080);
    

3.2 写入文件

  • 代码格式:fs.writeFile(path,data,callback);
    • path:为修改的文件的路径;
    • data:修改的数据内容,可以是字符串;
    • callback:回调函数,只有一个形参为err;
    • 代码:
     const http=require("http");
     const fs=require("fs");
     const server=http.createServer((req,res)=>{
         fs.writeFile("./static/2.txt","111这是写入的文件",(err)=>{
             if(err){
                 res.end("404");
             }
             res.end();
         })
     });
     server.listen(8080);
    

3.3 获取文件夹下的所有文件,并筛选

  • 思路:
    • 创建server服务器
    • 通过代码fs.readdir(path,(err,files)=>{});来读取文件夹内容;其中files为文件下所有文件内容组成的数组;
    • 遍历数组,分别判断每个文件的类型;
      • 使用代码fs.stat(path,(err,stats)=>{});来判断每个文件的类型;path为文件地址;
      • 使用代码:stats.isDirectory(),判断是否为文件夹,如果是文件夹,返回true,反之,返回false;
    • 新建空数组,将符合条件的文件夹插入到数组中;
  • 知识:
    • 地址路径的拼接:"./"+files[i],其中files[i]为文件名称,为字符串,通过字符串拼接建成新的路径字符串;
    • 代码stats.isFile()指的是判断文件时候为纯文件,如果为文件夹,返回false,其他文件返回true;
  • 问题:
    • 获得files数组后,遍历数组时会出现两个问题;
      • 利用for循环时,i值会出错,由于fs.stat()为异步,函数中获取的i值为最大值;会出错;
        • 解决方法:利用闭包或let创建变量;或不是用for循环,使用迭代函数递归;
      • 获取最终的ary数组时,会出现同异步问题;直接打印ary获取的是空数组;同步优先执行;
        • 解决方法:将获取数据ary放在异步的函数中,通过判断i值,来判断遍历结束;获取最终的ary数组;
  • 注意:
    • 利用for循环遍历数组时,要注意,for循环中是先判断条件,然后再执行语句,所以在语句执行中i的最大值为数组长度减一;不会等于数组长度;所以最终判断i值是否为数组长度减一,来判断数组遍历完;经测试后发现if(stats.isDirectory()){ary.push(files[i]);}此行代码执行为同步执行;所以需要将判断条件放在其后面,这样才能先执行上面代码,再判断下面条件,停止运行;
      • 如果在for循环中,条件判断中,存在异步操作,可以将i<=ary.length,然后再判断i是否等于ary.length,如果等于就直接return阻止程序执行;条件判断放在最开始;这样就可以解决,在i==ary.length-1时,同步先执行,异步后执行的问题;
    • 利用迭代函数递归执行自己,然后通过在开始判断i的值,来进行输出数据并阻断;相当于将异步运行转化为同步运行;
  • 代码:
     //需求:获取dady1下的所有文件,筛选其中的文件夹放入数组中;
     const http=require("http");
     const fs=require("fs");
     //创建服务器
     const server=http.createServer((req,res)=>{
         if(req.url==="/favicon.ico"){
             return;
         }
         //读取dady1的文件夹下的所有文件;
         fs.readdir("./",(err,files)=>{
             //files获取是一个数组,数组元素为文件夹下的文件;
             //判断数组中各元素是否为文件夹,获取其中的文件夹插入到新的数组中;
             var ary=[];
             //1.通过for循环遍历数组,里面的异步运行里面的i值为错值,而且ary打印为空数组,同步先运行;
             /*var ary=[];
             for(var i=0; i<files.length; i++){
                 fs.stat("./"+files[i],(err,stats)=>{
                     if(stats.isDirectory()){
                         ary.push(files[i]);//此时的i值会出错;为最大值10;
                     }
                 })
             }
             console.log(ary);//此时打印出来的为空数组;由于其为同步,先执行;*/
             //2.1利用let定义变量,来解决i值问题;将打印ary放在异步中,解决ary的同步问题;
             /*for(let i=0; i<files.length; i++){
                 //利用let形成空间,相当于闭包;
                 fs.stat("./"+files[i],(err,stats)=>{
                     if(stats.isDirectory()){//将测试得出,此判断为同步
                         ary.push(files[i]);
                     }
                     if(i===files.length-1){
                         console.log(ary);
                         return;
                     }
                 });
             }*/
             //2.2利用闭包解决i值问题
             /*for(var i=0; i<files.length; i++){
                 //利用let形成空间,相当于闭包;
                 (function (i) {
                     fs.stat("./"+files[i],(err,stats)=>{
                         if(stats.isDirectory()){//将测试得出,此判断为同步
                             ary.push(files[i]);
                         }
                         if(i===files.length-1){
                             console.log(ary);
                             return;
                         }
                     });
                 })(i);
             }*/
             //3 利用闭包和递归函数
             //利用闭包函数执行,利用迭代函数解决数组遍历问题;将迭代函数放在异步中,相当于变成同步,待异步执行完后,在执行下一步;
             (function Iterator(i) {
                 if(i===files.length){
                     console.log(ary);
                     return;
                 }
                 fs.stat("./"+files[i],(err,stats)=>{
                     if(stats.isDirectory()){
                         ary.push(files[i]);
                     }
                     //利用递归函数,迭代函数执行
                     Iterator(++i);//执行自己;
                 })
             })(0)
         });
         res.end();
     });
     //监听服务器
     server.listen(8080);
    

4 前台向后台发送请求的形式

  • form表单提交:
    • action:服务器;设置协议和主机名和端口号;如:http://localhost:8080;
    • method:请求方式;
    • submit:提交按钮;
    • name:指的是input中的name设置,必须设置,在网址中作为参数传递,其值为key的值;
    • value:指的是input中的value设置,可以不设置,设置value后,文本框中就默认填入文件,如果不设置,文本框内输入的内容就作为value值来上传,在网址中作为参数传递,其值为value的值;
    • 最终发送请求后的地址:http://localhost:8080/?userr=dd&passs=3333;
    • 代码:
     <!doctype html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <title>表单提交请求形式</title>
     </head>
     <body>
     <form action="http://localhost:8080" method="get">
         <label for="user">
             用户名:<input type="text" name="userr" id="user">
         </label>
         <label for="pass">
             密码:<input type="password" name="passs" id="pass">
         </label>
         <input type="submit" value="提交按钮">
     </form>
     </body>
     </html>
    
  • ajax:
    • {url:"xxx",type:"get/post",dataType:"json",data:"参数",fnLoading:fn,fnComplete:fn,success:fn(data){},error:function(){},timeout:300}
    • 1)jQuery中ajax提交请求代码:
     <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <title>jQuery中ajax提交请求</title>
     </head>
     <body>
     <script src="../JS/jquery.js"></script>
     <script>
         //https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=zhouxingchi&cb=xxx
         //分析:问号之前的为请求地址;wd等号后为搜索的内容;cd后面为调用函数的函数名;
         $.ajax({
             url:"http://localhost:8080",//请求地址;后台接口
             data:{"wd":"扶摇"},//前端传给后台的数据,可有可无的;
             type:"get",//请求方式,post,get,jsonp可有可无,如果写了jsonp,没写默认的都是get;
             dataType: "jsonp",//返回的数据类型,可有可无的,如果传了,拿到的数据就是josn对象,如果没有,拿到的是字符串
             jsonp:"cb",//可有可无,没有,默认是callback;但在百度服务器中搜索必须设置为cb
             fnLoading:function(){//等待加载
                 console.log("数据正在加载中,请耐心等待");
             },
             complete:function(){//数据请求成功
                 console.log("数据请求结束")
             },
             success:function(data){//数据响应成功
                 console.log(data);
             },
             error:function (data) {//数据响应失败
                 //数据请求失败的处理;
             },
             timeout:2000//请求超时,可有可无,没传默认3000;
         })
     </script>
     </body>
     </html>
    
    • 2)自己封装的ajax库提交请求代码:
     <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <title>自己封装的ajax提交请求</title>
     </head>
     <body>
     <script src="../JS/ajax.js"></script>
     <script>
         myAjax({
             url:"http://localhost:8080",
             data:{wd:"zhouxingchi"},
             type:"get",
             dataType:"json",
             jsonp:"cb",//此处必须设置jsonp为cb,才能使用
             fnLoading:function(){//等待加载
                 console.log("数据正在加载中,请耐心等待");
             },
             complete:function(){//数据请求成功
                 console.log("数据请求结束")
             },
             success:function(data){//数据响应成功
                 console.log(data);
             },
             error:function (data) {//数据响应失败
                 //数据请求失败的处理;
             },
             timeout:2000//请求超时,可有可无,没传默认3000;
         });
     </script>
     </body>
     </html>
    
  • jsonp:三步走
    • 1)创建一个有名字的全局函数;
    • 2)通过script发送请求;
    • 3)在函数中接受别人调用你全局函数传来的数据;
    • 发送请求html代码:
     <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <title>jsonp提交请求</title>
     </head>
     <body>
     <script>
         window["guo"]=function (data) {
             console.log(data);
         }
     </script>
     <script src="http://localhost:8080?wd=zhouxingchi&cb=guo"></script>
     </body>
     </html>
    
  • 以上三种形式下,服务器的创建代码
    • 代码:
     const http=require("http");
     const url=require("url");
     http.createServer((req,res)=>{
         if(req.url==="/favicon.ico") return;
         console.log(req.url);
         var obj=url.parse(req.url,true);//不添加true,则query属性值为字符串;添加第二个参数后,query属性值为对象;
         var que=obj.query;
         console.log(que);//此时为对象;
         res.end("ok");
     }).listen(8080);
    

5 发送请求的方式

  • 后台不在乎前端用什么来请求数据,后台在乎的是前端的请求方式method;
  • 常见的请求方式:
    • get:用来取数据;
    • post:用来提交数据;

5.1 get数据请求方式

  • get数据的获取:
    • get通过浏览器的地址栏来传输数据;
    • get参数:req.url得到的是端口号后面的部分,包含斜线;如:/?userr=dd&passs=3333
    • 接收get参数的方式有三种:
      1)纯手工开发-split切req.url字符串,然后数组遍历形成对象;
      2)querystring模块:把一个 URL 查询字符串 str 解析成一个键值对的对象;
      • 代码:querystring.parse("k1=v1&k2=v2");
        3)url模块;
      • url对象:url.parse(req.url);打印出来为一个对象;里面有许多属性和属性值;
       Url {
         protocol: null,//协议;http
         slashes: null,//斜杠
         auth: null,
         host: null,//主机,包括主机名加端口号
         port: null,//端口号
         hostname: null,//主机名
         hash: null,//哈希值
         search: '?userr=dd&passs=3333',//问号之后的部分不包括hash
         query: 'userr=dd&passs=3333',//参数
         pathname: '/',//问号前的部分,带斜线
         path: '/?userr=dd&passs=3333',//路径
         href: '/?userr=dd&passs=3333' }//所有的地址;
      
      • query属性:默认情况下属性值为字符串;在url.parse(req.url,true)代码中,添加第二个参数为true;则 query属性总会通过 querystring 模块的 parse()方法生成一个对象。如果为 false,则返回的 URL 对象上的query属性会是一个未解析、未解码的字符串。默认为 false。
    • 接收get参数的三种方式代码:
     const http=require("http");
     const querystring=require("querystring");
     const url=require("url");
     http.createServer((req,res)=>{
         if(req.url==="/favicon.ico") return;
         //req.url打印为:/?userr=dd&passs=2222
         //方案1:原始的字符串切成数组,数组遍历形成对象;
         var obj={};
         var strQuery=req.url.split("?")[1];
         var ary1=strQuery.split("&");
         for(var i=0; i<ary1.length; i++){
             var ary2=ary1[i].split("=");
             obj[ary2[0]]=ary2[1];
         }
         console.log(obj);
         //方案2:利用querystring模块,把一个 URL 查询字符串 str 解析成一个键值对的集合。
         var strQuery=req.url.split("?")[1];
         var obj=querystring.parse(strQuery);
         console.log(obj);
         //方案3:利用url模块;
         var obj=url.parse(req.url,true);//不添加true,则query属性值为字符串;添加第二个参数后,query属性值为对象;
         var que=obj.query;
         console.log(que);//此时为对象;
         res.end("ok");
     }).listen(8080);
    

6 cmd中命令行指令

  • 查看node.js的版本命令:node -vnode --version;
  • ls:列出当前文件夹下的所有文件;
  • node+文件:运行该文件;node 01http-server.js:运行服务器;
  • tab键:用来补全内容;
  • ctrl+c:阻断程序执行,即关闭服务器;
  • clear: 清空内容;
  • touch:创建文件;可以创建任何文件;
  • mkdir:创建文件夹,创建多个文件夹,中间用空格连接;如:mkdir aa bb,即创建aa和bb两个文件夹;
  • node:只输入node指令后,会进入一个repl的环境,是JS在服务器端运行的环境;指的是:read(读取) eval(执行) print(打印) loop(循环);
  • cd+文件名:打开本文件夹下的该文件;
  • cd ../:回到父级目录;
  • rm -rf xxx:删除xxx文件;

7 其他知识

  • 前端工程师:JS文件在浏览器中运行;
  • 后台工程师:JS文件在服务器中运行;
  • 网址的部分解读;


    网址解读
    • protocal:协议;设置或返回当前URL的协议;
    • host:主机;设置或返回主机名和当前URL端口号;
      • hostname:主机名;设置或返回主机名;
      • port:端口号;设置或返回端口号;
    • pathname:路线;设置或返回当前URL的路径;包含斜杠
    • search:设置或返回从问号开始的URL部分;包含问号,不包括哈希值;
    • hash:哈希值;设置或返回从#号开始的URL部分;
    • href:设置或返回完整的URL部分;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Address:https://www.zybuluo.com/XiangZhou/note/208532 Exp...
    天蠍蒗漫阅读 11,270评论 2 55
  • github地址,欢迎大家提交更新。 express() express()用来创建一个Express的程序。ex...
    Programmer客栈阅读 2,485评论 0 1
  • 个人入门学习用笔记、不过多作为参考依据。如有错误欢迎斧正 目录 简书好像不支持锚点、复制搜索(反正也是写给我自己看...
    kirito_song阅读 2,434评论 1 37
  • 快乐有三法:舍得、放下、忘记。幸福有四要素:可以改变的去改变,不可改变的去改善,不能改善的去承担,不能承担的就放下...
    白雪飘零阅读 394评论 0 0
  • 我朋友的致密亲人去世了,而我站在他面前却不知道该告慰些什么。在那时,仿佛除了沉默,一切都显得那么不合时宜。我无...
    Vic笔触阅读 450评论 0 1