# Node.js葵花寶典

Node.js葵花寶典

前言

​ 欲練此功,必先自宮;javascript

​ 沒必要自宮,亦可練成;html

​ 兄臺仍是好好修煉此功吧!保持一個清醒的頭腦,你將駕馭這匹野馬!!!前端

—— 致讀者vue

​ 知識就像海洋同樣,永遠也學不完,可是不斷精益求精是一種態度,是對新事物的一種持續保持瞻望的態度,希望你在學習的樂園裏不斷完善己身,不斷修煉,等待破繭成蝶。java

文檔風格

  • 書名採用 #
  • 大標題採用##
  • 單元小結標題採用####

​ —— 致開發者 node

​ 好的書寫風格可以讓讀者的思路清晰,一樣能讓人有繼續閱讀的興趣,希望你能按照此風格繼續完善本書籍。git

第一篇 爲什麼偏心Node.js

1.1 引子

  • 前端職責範圍變大,爲了統一流程開發體驗
  • 在處理高併發,I/O密集型場景性能優點足夠明顯

1.2 CPU密集 ==VS== IO密集

  • CPU密集:壓縮、解壓、加密、解密
  • I/O密集:文件操做、網絡操做、數據庫

1.3 Web常見的場景

  • 靜態資源獲取
  • ......

1.4 高併發對應之道

  • 增長物理機的個數
  • 增長每臺機器的CPU數------多核

1.5 關於進程線程那些事兒

  • 進程:用一句比較接地氣的一句話敘述就是,執行中的程序就叫作進程。
  • 多進程:啓動多個進程處理一個任務。
  • 線程:進程內一個相對獨立、能夠調度的執行單元,與同屬一個進程的線程共享進程資源。

1.6 再來談談Node.js的單線程

  • 單線程只是針對主進程,I/O操做系統底層進行多線程調度。也就是它僅僅起一個監聽做用。es6

    ###  舉個栗子叭
         場景:飯店
         情節:人流量高併發
         BOSS的策略:僱傭多名廚師,只僱傭一個服務員,當多名顧客點餐的時候,服務員告訴廚師,作菜,上菜。
  • 單線程並非只開一個進程。github

    ###  Node.js中的cluster(集羣)模塊
         官方介紹:單個 Node.js 實例運行在單個線程中。 爲了充分利用多核系統,有時須要啓用一組 Node.js 進程去處理負載任務。
         Demo:
    const cluster = require('cluster');
    const http = require('http');
    const numCPUs = require('os').cpus().length;
    
    if (cluster.isMaster) {
      console.log(`主進程 ${process.pid} 正在運行`);
    
      // 衍生工做進程。
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
      }
    
      cluster.on('exit', (worker, code, signal) => {
        console.log(`工做進程 ${worker.process.pid} 已退出`);
      });
    } else {
      // 工做進程能夠共享任何 TCP 鏈接。
      // 在本例子中,共享的是 HTTP 服務器。
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end('你好世界\n');
      }).listen(8000);
    
      console.log(`工做進程 ${process.pid} 已啓動`);
    }
  • 友情連接Cluster

1.7 Node.js的經常使用場景

  • Web server
  • 本地代碼構建

第二篇 環境&調試

2.1 環境安裝

2.2 環境必須

  • CommonJSweb

    • 每一個文件都是一個模塊,有本身的做用域。(它會自動包裹函數)
    • 在模塊內部module變量表明模塊自己。
    • module.exports屬性表明模塊對外接口。

      ModuleDemo.js

      console.log('this is a module');
      
      const testVar = 100;
      
      function test () {
          console.log(testVar);
      }
      
      module.exports.testVar = testVar;
      module.exports.testFn = test;
    • 使用模塊之require規則

​ - /表示絕對路徑,./表示相對於當前文件的路徑。

​ - 支持jsjsonnode拓展名

​ - 不寫路徑則認爲是build-in模塊或者各級node-modules內的第三方模塊

CommonJS Use Module

const modu = require('ModuleDemo.js');

console.log(modu.testVar);

console.log(modu.test);
    • require特性

      • module被加載的時候執行,加載後緩存。(後邊這一句的意義就是,只加載一次,加載完緩存)【注:能夠作一個小test,在一個test文件中,同時加載兩次模塊,你就會發現其中的奧祕了。】
      • 一旦出現某個模塊被循環加載,就只會輸出已經執行的部分,還未執行的部分不會輸出。
    • Global
    • Process

    2.3 Node.js引用系統模塊與第三方模塊

    • 引用系統模塊

      const fs = require('fs');
      
      const result = fs.readFile('fs.js',( err, data) => {
          if (err) {
              return err;
          }
          
          console.log(data);
      });
      
      console.log(result);
    • 引用第三方模塊

      npm i chalk -S
      const chalk = require('chalk');

    2.4 exports與module.exports

    {
        function(exports,require,module,__filename,__dirname) {
            // code
        }
    }
    • 簡而言之,exports就是module.exports的一個簡寫,也就是一個引用,別名。

      exports = {
          a: 1,
          b: 2,
          test: 123
      }
      //這樣是錯誤的
      exports.test = 100;
      //只能添加屬性,可是不能修改其指向
      
      //固然
      module.exports = {
          a:1,
          b:2,
          test:123,
      }
      //這樣是沒問題的

    2.5 Global對象

    • CommonJS
    • Buffer、process、console
    • timer
    global.test = 200;

    2.6 process模塊

    /**
     *  argv
     *  argv0 
     *  execArgv
     *  execPath
     */
    
    const {argv, argv0, execArgv, execPath} = require('process');
    
    argv.forEach( item => {
        console.log(item);
    })
    
    //打印當前工做進程的路徑
    
    console.log(process.cwd());
    
    //setImmediate(fn),不須要任什麼時候間參數,執行最慢
    //process.nextTick(fn)
    //兩者的區別就是後者的執行會先於前者
    • 簡單說明一下,就是process.nextTick()會把任務放在當前事件循環隊列的隊尾,而setImmediate()會把任務放在下一個隊列的隊首,而setTimeout()會把任務放在它倆中間。

    2.7 Debug

    第三篇 Node.js-API

    3.1 path

    和路徑有關的內置模塊

    • path.basename()取得一個路徑的最後一部分文件名
    • path.normalize()幫助修正路徑
    • path.join()用於路徑拼接(參數爲多個路徑參數)
    • path.resolve()將一個相對路徑解析爲絕對路徑
    • {basename, dirname, extname}

      • basename 完整名
      • dirname 上級路徑名
      • extname 後綴名
    • {parse, format}

      • parse用於解析當前路徑爲一個json格式的數據
      • format至關於格式化json數據爲一個字符串

    說明:__dirname__filename老是返回文件的絕對路徑

    process.cwd()老是返回node命令所在文件夾

    3.2 Buffer

    三個要點:

    • Buffer用於處理二進制數據流
    • 實例相似整數數組,大小固定
    • C++代碼在V8堆外分配物理內存

    Buffer經常使用的方法

    • byteLength統計buffer所佔字節數
    • isBuffer用來判斷目標數據是否是一個Buffer
    • concat合併鏈接Buffer
    • from將目標數據轉換爲Buffer
    • toString用來轉換Buffer爲字符串

    3.3 events

    • eventEmitter.on('eventName',callback())用於註冊監聽器
    • eventEmitter.emit('eventName')用於觸發事件
    const EventEmitter = require('events');
    
    class CustomEvent extends EventEmitter {
        
    }
    
    const ce = new CustomEvent();
    
    ce.on('eventName',callback);
    
    ce.emit('eventName','your msg to eventEmit',....);
    
    //有一個通用參數就叫error
    
    ce.on('error',fn);
    
    //Example
    ce.on('error',(err, item) => {
        console.log(err);
        console.log(item);
    });
    
    ce.emit('error', new Error('出錯了'), Date().now);

    針對事件只須要響應一次:

    ce.once('test', ()=> {
        console.log(test);
    });

    針對事件須要移除的話:

    ce.removeListener('eventName',fn);
    
    //or
    
    ce.removeAllListeners('test');

    3.4 fs

    ​ 首先須要注意的就是Node.js的設計模型就是錯誤優先的模式。

    fs.readFile('fileUrl', 'utf8', (err, data) => {
        if(err) throw err;
        
        console.log(data);
    })
    • stat()查看文件的詳細信息

      const fs = require('fs');
      
      fs.stat('fileUrl', (err, data) => {
          if (err) {
              throw err;//這裏能夠判斷文件是否存在
          }
          
          console.log(data);
      });
    • rename()更改文件名

      fs.rename('./text.txt','hahah.ttx');
    • unlink刪除文件
    fs.unlink('fileName', err => err);
    • readdir()讀取文件夾
    • mkdir()建立文件夾
    • rmdir()刪除文件夾
    • watch()監視文件或目錄的變化
    fs.watch('fileUrl', {
        recursive:true //是否監視子文件夾
    }, (eventType, fileName) => {
        console.log(eventType, fileName);
    })
    • readStream()讀取流
    const rs = fs.createReadStream('urlPath');
    
    rs.pipe(process.stdout);//導出文件到控制檯
    • writeStream()寫入流
    • pipe()管道,導通流文件

      const ws  = fscreateWriteStream('urlPath');
      
      ws.write('some content');
      
      ws.end();
      
      ws.on('finish',()=>{
         console.log('done!!!'); 
      });

    第四篇 項目Init

    4.1 .gitignore

    • 匹配模式前/表明項目根目錄
    • 匹配模式最後加\表明是目錄
    • 匹配模式前加!表明取反
    • *表明任意一個字符
    • ?匹配任意字符
    • **匹配任意目錄

    4.2 ESLint

    完整配置.eslintrc.js

    module.exports = {
        env: {
            es6: true,
            mocha: true,
            node: true
        },
        extends: ['eslint:recommended', 'plugin:sort-class-members/recommended'],
        parser: 'babel-eslint',
        plugins: ['babel', 'sort-class-members'],
        root: true,
        rules: {
            'accessor-pairs': 'error', // 強制 getter 和 setter 在對象中成對出現
            'array-bracket-spacing': 'off', // 強制數組方括號中使用一致的空格
            'arrow-parens': 'off', // 要求箭頭函數的參數使用圓括號
            'arrow-spacing': 'error', // 強制箭頭函數的箭頭先後使用一致的空格
            'babel/arrow-parens': ['error', 'as-needed'],
            'babel/generator-star-spacing': ['error', 'before'],
            'block-scoped-var': 'error', // 強制把變量的使用限制在其定義的做用域範圍內
            'block-spacing': 'off', // 強制在單行代碼塊中使用一致的空格
            'brace-style': 'off', // 強制在代碼塊中使用一致的大括號風格
            camelcase: 'off', // 強制使用駱駝拼寫法命名約定
            'comma-dangle': 'off', // 要求或禁止末尾逗號
            'comma-spacing': 'off', // 強制在逗號先後使用一致的空格
            'comma-style': 'off', // 強制使用一致的逗號風格
            complexity: 'off', // 指定程序中容許的最大環路複雜度
            'computed-property-spacing': 'error', // 強制在計算的屬性的方括號中使用一致的空格
            'consistent-return': 'off', // 要求 return 語句要麼老是指定返回的值,要麼不指定
            'consistent-this': 'error', // 當獲取當前執行環境的上下文時,強制使用一致的命名
            'constructor-super': 'error', // 要求在構造函數中有 super() 的調用
            curly: 'off', // 強制全部控制語句使用一致的括號風格
            'default-case': 'error', // 要求 switch 語句中有 default 分支
            'dot-location': ['error', 'property'], // 強制在點號以前和以後一致的換行
            'dot-notation': 'off', // 強制在任何容許的時候使用點號
            'eol-last': 'off', // 強制文件末尾至少保留一行空行
            eqeqeq: ['error', 'smart'], // 要求使用 === 和 !==
            'func-names': 'off', // 強制使用命名的 function 表達式
            'func-style': ['error', 'declaration', { // 強制一致地使用函數聲明或函數表達式
                allowArrowFunctions: true
            }],
            'generator-star-spacing': 'off', // 強制 generator 函數中 * 號周圍使用一致的空格
            'id-length': ['error', { // 強制標識符的最新和最大長度
                exceptions: ['_', 'e', 'i', '$']
            }],
            indent: ['error', 4, { // 強制使用一致的縮進
                SwitchCase: 1
            }],
            'key-spacing': 'off', // 強制在對象字面量的屬性中鍵和值之間使用一致的間距
            'keyword-spacing': ['off', { // 強制在關鍵字先後使用一致的空格
                overrides: {
                    case: {
                        after: true
                    },
                    return: {
                        after: true
                    },
                    throw: {
                        after: true
                    }
                }
            }],
            'linebreak-style': 'off',
            'lines-around-comment': 'off',
            'max-depth': 'error', // 強制可嵌套的塊的最大深度
            'max-nested-callbacks': 'off',
            'max-params': ['error', 4],
            'new-cap': 'off',
            'new-parens': 'error', // 要求調用無參構造函數時有圓括號
            'newline-after-var': 'off',
            'no-alert': 'error', // 禁用 alert、confirm 和 prompt
            'no-array-constructor': 'error', // 禁止使用 Array 構造函數
            'no-bitwise': 'error', // 禁用按位運算符
            'no-caller': 'error', // 禁用 arguments.caller 或 arguments.callee
            'no-catch-shadow': 'off',
            'no-class-assign': 'error', // 禁止修改類聲明的變量
            'no-cond-assign': ['error', 'always'], // 禁止條件表達式中出現賦值操做符
            'no-confusing-arrow': 'error', // 不容許箭頭功能,在那裏他們能夠混淆的比較
            "no-console": 0,
            'no-const-assign': 'error', // 禁止修改 const 聲明的變量
            'no-constant-condition': 'error', // 禁止在條件中使用常量表達式
            'no-div-regex': 'error', // 禁止除法操做符顯式的出如今正則表達式開始的位置
            'no-dupe-class-members': 'error', // 禁止類成員中出現重複的名稱
            'no-duplicate-imports': 'off', // disallow duplicate module imports
            'no-else-return': 'error', // 禁止 if 語句中有 return 以後有 else
            'no-empty-label': 'off',
            'no-empty': 'off',
            'no-eq-null': 'error', // 禁止在沒有類型檢查操做符的狀況下與 null 進行比較
            'no-eval': 'error', // 禁用 eval()
            'no-extend-native': 'error', // 禁止擴展原生類型
            'no-extra-bind': 'error', // 禁止沒必要要的 .bind() 調用
            'no-extra-parens': 'error', // 禁止沒必要要的括號
            'no-floating-decimal': 'error', // 禁止數字字面量中使用前導和末尾小數點
            'no-implied-eval': 'error', // 禁止使用相似 eval() 的方法
            'no-inline-comments': 'error', // 禁止在代碼行後使用內聯註釋
            'no-iterator': 'error', // 禁用 __iterator__ 屬性
            'no-label-var': 'off',
            'no-labels': 'off',
            'no-lone-blocks': 'error', // 禁用沒必要要的嵌套塊
            'no-lonely-if': 'off',
            'no-loop-func': 'error', // 禁止在循環中出現 function 聲明和表達式
            'no-mixed-requires': 'error', // 禁止混合常規 var 聲明和 require 調用
            'no-mixed-spaces-and-tabs': 'off',
            'no-multi-spaces': 'off',
            'no-multi-str': 'off',
            'no-native-reassign': 'error', // 禁止對原生對象賦值
            'no-nested-ternary': 'error', // 不容許使用嵌套的三元表達式
            'no-new-func': 'error', // 禁止對 Function 對象使用 new 操做符
            'no-new-object': 'error', // 禁止使用 Object 的構造函數
            'no-new-require': 'error', // 禁止調用 require 時使用 new 操做符
            'no-new-wrappers': 'error', // 禁止對 String,Number 和 Boolean 使用 new 操做符
            'no-new': 'error', // 禁止在非賦值或條件語句中使用 new 操做符
            'no-octal-escape': 'error', // 禁止在字符串中使用八進制轉義序列
            'no-path-concat': 'error', // 禁止對 __dirname 和 __filename進行字符串鏈接
            'no-process-env': 'error', // 禁用 process.env
            'no-process-exit': 'error', // 禁用 process.exit()
            'no-proto': 'error', // 禁用 __proto__ 屬性
            'no-restricted-modules': 'error', // 禁用指定的經過 require 加載的模塊
            'no-return-assign': 'error', // 禁止在 return 語句中使用賦值語句
            'no-script-url': 'error', // 禁止使用 javascript: url
            'no-self-compare': 'error', // 禁止自身比較
            'no-sequences': 'error', // 禁用逗號操做符
            'no-shadow-restricted-names': 'error', // 禁止覆蓋受限制的標識符
            'no-shadow': 'off',
            'no-spaced-func': 'off',
            'no-sync': 'off', // 禁用同步方法
            'no-this-before-super': 'error', // 禁止在構造函數中,在調用 super() 以前使用 this 或 super
            'no-throw-literal': 'error', // 禁止拋出非異常字面量
            'no-trailing-spaces': 'error', // 禁用行尾空格
            'no-undef-init': 'error', // 禁止將變量初始化爲 undefined
            'no-undefined': 'off',
            'no-underscore-dangle': 'off',
            'no-unexpected-multiline': 'error', // 禁止出現使人困惑的多行表達式
            'no-unneeded-ternary': 'error', // 禁止能夠在有更簡單的可替代的表達式時使用三元操做符
            'no-unused-expressions': 'error', // 禁止出現未使用過的表達式
            "no-unused-vars": [1, { // 禁止出現未使用過的變量
                "vars": "all",
                "args": "after-used"
            }],
            'no-use-before-define': 'error', // 不容許在變量定義以前使用它們
            'no-useless-call': 'error', // 禁止沒必要要的 .call() 和 .apply()
            'no-useless-concat': 'error', // 禁止沒必要要的字符串字面量或模板字面量的鏈接
            'no-var': 'off',
            'no-void': 'error', // 禁用 void 操做符
            'no-warning-comments': 'off',
            'no-with': 'off',
            'object-curly-spacing': 'off',
            'object-shorthand': 'error', // 要求或禁止對象字面量中方法和屬性使用簡寫語法
            'one-var': 'off',
            'operator-assignment': 'error', // 要求或禁止在可能的狀況下要求使用簡化的賦值操做符
            'operator-linebreak': 'off',
            'padded-blocks': 'off',
            'prefer-arrow-callback': 'off',
            'prefer-const': 'error', // 要求使用 const 聲明那些聲明後再也不被修改的變量
            'prefer-spread': 'error', // 要求使用擴展運算符而非 .apply()
            'prefer-template': 'off', // 要求使用模板字面量而非字符串鏈接
            quotes: 'off',
            'quote-props': 'off',
            radix: 'error', // 強制在parseInt()使用基數參數
            'require-yield': 'error', // 要求generator 函數內有 yield
            "semi": ["error", "always"], // 要求或禁止使用分號
            'semi-spacing': 'off',
            'sort-vars': 'error', // 要求同一個聲明塊中的變量按順序排列
            'space-before-blocks': 'off',
            'space-before-function-paren': 'off',
            'space-in-parens': 'off',
            'space-infix-ops': 'off',
            'space-unary-ops': 'off',
            'spaced-comment': 'off',
            strict: 'off',
            'valid-jsdoc': 'error', // 強制使用有效的 JSDoc 註釋
            'vars-on-top': 'off',
            yoda: 'off',
            'wrap-iife': 'off',
            'wrap-regex': 'error' // 要求正則表達式被括號括起來
        }
    }

    第五篇 靜態資源服務器

    5.1 http模塊

    const http = require('http');
    const chalk = require('chalk');
    const hostname = '127.0.0.1';
    
    const port = '3000';
    const server = http.createServe((req, res) => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Hello World');
    });
    
    server.listen(port, hostname, () => {
        const addr = `Server running at http://${hostname}:${port}/`;
        console.log(`${chalk.green(addr)}`);
    })
    • supervisor監視文件模塊
    • hotnode熱啓動模塊

    完整代碼:請參照

    第六篇 測試

    6.1 斷言測試

    • assert- 斷言調試
    console.assert(a===b,msg);

    經常使用斷言庫:chai.js

    6.2 mocha.js

    //you should install mocha
    
    // $ npm install --save-dev mocha
    
    // you should know the '#math' only is it's description
    describe('#math', ()=> {
        describe('#add', ()=> {
            it('should return 5 when 2 + 3', () => {
                ecpect('your test','targetResult');
            });
        });
    });

    Note :若是你想只執行一個模塊的,你能夠在package.json中配置

    "scripts": {
      "test": "mocha /url/xxx.js
    }

    6.3 istanbul

    你能夠參照)

    6.4 benchmark.js

    • 性能測試框架,具體參見
    //常見測試方法
    console.time('markPoint');
    
    console.timeEnd('markPoint');

    6.5 jsbench.js

    • 在FCC社區聽老外告訴的,本身作了測試,確實是能夠測試兩段代碼的性能差別。

    第七篇 UI測試

    7.1 傳統測試

    ## 傳統測試天然就是人工點擊
    
    ## 弊端就是發現錯誤不健全

    7.2 webdriver

    參照

    第八篇 爬蟲

    8.1 爬蟲與反爬蟲

    ==爬蟲==:按照必定的規則自動抓取網絡信息的程序

    ==反爬蟲==:

    • User-Agent,Referer,驗證碼
    • 單位時間的訪問次數,訪問量
    • 關鍵信息用圖片混淆
    • 異步加載

    8.2 爬蟲思路

    思路一:使用cheerio與http/https

    • cheerio:一個能夠像jQuery那樣操做DOM元素
    • http/https:我就不用說了吧
    //簡單示例
    const cheerio = require('cheerio');
    const https = require('https');
    let html = '';
    const $ = '';
    https.get('url',res => {
        res.on('data',(data) => {
            html += data;
        });
        res.on('finish',()=> {
            $ = cheerio.load(html);
            //some code
        })
    })

    思路二:puppeteer

    • 下邊貼一段爬蟲代碼吧!

    index.js

    const puppeteer = require('puppeteer');
    const https = require('https');
    const fs = require('fs');
    (async () => {
        //跳過安裝  npm i --save puppeteer --ignore-scripts
        const browser = await puppeteer.launch({
            executablePath: 'G:/chrome-win/chrome-win/chrome.exe'
        });
        const page = await browser.newPage();
        //指定瀏覽器去某個頁面
        await page.goto('https://image.baidu.com');
        // await page.cookies('https://image.baidu.com')
        //     .then(data => {
        //         console.info(data)
        //     });
        //調大視口,方便截圖,方便容納更多地圖
        await page.setViewport({
            width:2000,
            height:1000,
        });
        //模擬用哪一個戶輸入
        await page.keyboard.sendCharacter('車模');
        //模擬用戶點擊搜索
        await page.click('.s_search');
        console.info('開始點擊查詢......');
        //await page.screenshot({path: 'example.png'});
         page.on('load', async()=> {
             console.info('頁面已加載完畢,開始爬取');
            await page.screenshot({path: 'example.png'});
              let srcs = await page.evaluate(() => {
                 let img =  document.querySelectorAll('img.main_img');
                 return Array.from(img).map(item => item.src);
             });
             await srcs.forEach((item,index) => {
                      if(/^https:/g.test(item))
                      https.get(item,res =>{
                          res.pipe(fs.createWriteStream(__dirname + `/img/${index}.png`))
                              .on('finish',()=>{
                                  console.info(`已成功爬取${index + 1}條記錄!`);
                              });
                      })
                  })
             await browser.close();
        });
         
    })();

    package.json

    {
      "name": "reptiles",
      "version": "1.0.0",
      "description": "This is a replite",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node index.js"
      },
      "author": "vuejs@vip.qq.com",
      "license": "ISC",
      "dependencies": {
        "puppeteer": "^1.14.0"
      }
    }
    • How To Use?
    ## 初始化你的項目
    
    $ npm init
    
    ## 創建文件
    
    $ cd your project
    $ mkdir index.js
    ## 拷貝個人package.json
    ## 注意,這是重點,由於被牆,因此必須跳過安裝
    $ npm i --save puppeteer --ignore-scripts
    
    ## 啓動項目
    
    $ npm start
    • Note SomeDetails

      • 1.安裝puppeteer,必須跳過Chromium 的鏈接
      • 2.安裝好你的Chromium ,在lanuch的時候必須用絕對路徑,切記不可把chrom.exe單純拷貝到項目目錄
      • 3.不可把瀏覽器視口調節的太大,否則會觸碰反爬蟲協議。
      • 4.若是你調大了也不要緊,咱們能夠page.waitFor(200),讓任務沉睡一下子

    結語

      這篇大長文就算完全結束了,我相信文章裏面還有不少不足或者小錯誤,誠懇的但願你們不要噴我。

    ​    "你能不能,在於你想不想!"

    ​    加油,每一天都是新生。

    ​    對於這篇文章,我也會不斷的完善它。

    相關文章
    相關標籤/搜索