1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        【總結】1086- 如何搭建適合自己團隊的構建部署平臺

        共 23634字,需瀏覽 48分鐘

         ·

        2021-09-23 15:18

        前端業(yè)界現(xiàn)有的構建部署方案,常用的應該是 Jenkins,Docker,GitHub Actions 這些,而恰巧,我們公司現(xiàn)在就并存了前兩種方案。既然已經(jīng)有了穩(wěn)定的構建部署方式,為什么還要自己做一套前端自己的構建平臺呢?當然不是為了好玩啊,原因聽我慢慢分析。

        前端構建使用的時候可能會碰到各種各樣問題,比如:

        • Eslint 跳過校驗——公司里面的前端項目,隨著時間的推移,不同階段,通過新老腳手架創(chuàng)建出來的項目可能風格各異,并且校驗規(guī)則可能也不一定統(tǒng)一,雖然項目本身可以有著各種的 Eslint,Stylelint 等校驗攔截,但阻止不了開發(fā)者跳過這些代碼校驗。
        • npm 版本升級不兼容——對于依賴的 npm 版本必須的一些兼容性校驗,如果某些 npm 插件突然升級了不兼容的一些版本,代碼上線后就會報錯出錯,典型的就是各類 IE 兼容。
        • 無法自由添加自己想要的功能——想要優(yōu)化前端構建的流程,或者方便前端使用的功能優(yōu)化,但因為依賴運維平臺的構建應用,想加點自己的功能需要等別人排期。

        而這些問題,如果有了自己的構建平臺,這都將不是問題,所以也就有了現(xiàn)在的——云長。

        為何起名叫“云長“呢,當然是希望這個平臺能像”關云長“一樣,一夫當關萬夫莫開。那云長又能給我們提供什么樣的一些能力呢?

        云長能力

        構建部署

        這當然是必備的基本能力了,云長提供了公司不同前端項目類型,例如 Pampas、React、Vue、Uniapp 等的構建能力。整個流程其實也并不復雜,開始構建后,云長的服務端,獲取到要構建的項目名,分支,要部署的環(huán)境等信息后,開始進行項目的代碼更新,依賴安裝,之后代碼打包,最后將生成的代碼再打包成鏡像文件,然后將這份鏡像上傳到鏡像倉庫后,并且將項目的一些資源靜態(tài)文件都可以上傳 CDN,方便前端之后的調用,最后調用 K8S 的鏡像部署服務,進行鏡像按環(huán)境的部署,一個線上構建部署的流程也就完成了。

        可插拔的構建流程

        如果是使用別人的構建平臺, 很多前端自己想加入的腳本功能就依賴別人的服務來實現(xiàn),而如果走云長,則可以提供開放型的接口,讓前端可以自由定制自己的插件式服務。

        比如這個線上構建打包的過程當中,就可以處理一些前文提到過的問題,痛點,例如:

        • 代碼的各類 Eslint、Tslint 等合規(guī)性校驗,再也不怕被人跳過檢驗步驟。
        • 項目構建前還可以做 npm 包版本的檢測,防止代碼上線后的兼容性報錯等等。
        • 代碼打包后也能做一些全局性質的前端資源注入,例如埋點,錯誤監(jiān)控,消息推送等等類型。

        審核發(fā)布流程

        公司現(xiàn)有的平臺發(fā)布流程管控靠的是運維的名單維護,每個項目都會管理一個可發(fā)布人的名單,所以基本項目發(fā)版都需要發(fā)布人當晚跟隨進行發(fā)布,而云長為了解決這個問題,提供了一個審核流的概念。

        也就是當項目在預發(fā)環(huán)境測試完成之后,代碼開發(fā)者可以提起一個真線的發(fā)布申請單,之后這個項目的可發(fā)布人會通過釘釘收到一個需要審核的申請單,可以通過網(wǎng)頁端,或者釘釘消息直接操作,同意或者拒絕這次發(fā)布申請,在申請經(jīng)過同意后,代碼開發(fā)者到了可發(fā)布時間后,就能自己部署項目發(fā)布真線,發(fā)布真線后,后續(xù)會為這個項目創(chuàng)建一個代碼的 Merge Request 請求,方便后續(xù)代碼的歸檔整理。

        這么做的好處呢,一方面可以由前端來進行項目構建發(fā)布的權限管控,讓發(fā)布權限可以進行收攏,另一方面也可以解放了項目發(fā)布者,讓開發(fā)者可以更方便的進行代碼上線,而又開放了項目的發(fā)布。

        能力對外輸出

        云長可以對外輸出一些構建更新的能力,也就讓第三方插件接入構建流程成為了可能,我們貼心的為開發(fā)者提供了 VsCode 插件,讓你在開發(fā)過程中可以進行自由的代碼更新,省去打開網(wǎng)頁進行構建的時間,足不出戶,在編輯器中進行代碼的構建更新,常用環(huán)境更是提供了一鍵更新的快捷方式,進一步省去中間這些操作時間,這個時候多寫兩行代碼不是更開心嗎。

        我們的 VsCode 插件不僅僅提供了云長的一些構建能力,還有小程序構建,路由查找,等等功能,期待這個插件分享的話,請期待我們后續(xù)的文章哦。

        云長架構

        上面講過云長的構建流程,云長是依賴于 K8S 提供的一個部署鏡像的能力,云長的客戶端與服務端都是跑在 Docker 中的服務,所以云長是采用了Docker In Docker 的設計方案,也就是由 Docker 中的服務來進行一個 Docker 鏡像的打包。

        針對代碼的構建,云長服務端部分引入了進程池的處理,每個在云長中構建的項目都是進程池中的一個獨立的實例,都有獨立的打包進程,而打包過程的進度跟進則是靠 Redis 的定時任務查詢來進行,也就實現(xiàn)了云長多實例并行構建的架構。

        云長客戶端與服務端的接口通信則是正常的 HTTP 請求和 Websocket 請求,客戶端發(fā)起請求后,服務端則通過 MySQL 數(shù)據(jù)存儲一些應用,用戶,構建信息等數(shù)據(jù)。

        外部的資源交互則是,構建的過程中也會上傳一些靜態(tài)資源還有打包的鏡像到 cdn 和鏡像倉庫,最后則是會調用 K8S 的部署接口進行項目的部署操作。

        前端構建的 0-1

        上面看過了“云長”的一些功能介紹,以及“云長”的架構設計,相信很多朋友也想自己做一個類似于“云長”的前端構建發(fā)布平臺,那需要怎么做呢,隨我來看看前端構建平臺主要模塊的設計思路吧。

        構建流程

        前端構建平臺的主要核心模塊肯定是構建打包,構建部署流程可以分為以下幾個步驟:

        • 每一次構建開始后,需要保存本次構建的一些信息數(shù)據(jù),所以需要創(chuàng)建構建發(fā)布記錄,發(fā)布記錄會存儲本次發(fā)布的發(fā)布信息,例如發(fā)布項目的名稱,分支,commitId,commit 信息,操作人數(shù)據(jù),需要更新的發(fā)布環(huán)境等,這時我們會需要一張構建發(fā)布記錄表,而如果你需要項目以及操作人的一些數(shù)據(jù),你就又需要應用表以及用戶表來存儲相關數(shù)據(jù)進行關聯(lián)。
        • 構建發(fā)布記錄創(chuàng)建以后,開始了前端構建流程,構建流程可以 pipeline 的流程來進行,流程可以參考以下例子
          // 構建的流程
          async run() {
            const app = this.app;
            const processData = {};
            const pipeline = [{
              handler: context => app.fetchUpdate(context), // Git 更新代碼
              name: 'codeUpdate',
              progress: 10 // 這里是當前構建的進度
            }, {
              handler: context => app.installDependency(context), // npm install 安裝依賴
              name: 'dependency',
              progress: 30
            }, {
              handler: context => app.check(context), // 構建的前置校驗(非必須):代碼檢測,eslint,package.json 版本等
              name: 'check',
              progress: 40
            }, {
              handler: context => app.pack(context), // npm run build 的打包邏輯,如果有其他的項目類型,例如 gulp 之類,也可以在這一步進行處理
              name: 'pack'
              progress: 70
            }, {
              handler: context => app.injectScript(context), // 構建的后置步驟(非必須):打包后的資源注入
              name: 'injectRes',
              progress: 80
            }, { // docker image build
              handler: context => app.buildImage(context), // 生成 docker 鏡像文件,鏡像上傳倉庫,以及之后調用 K8S 能力進行部署
              name: 'buildImage',
              progress: 90
            }];
            // 循環(huán)執(zhí)行每一步構建流程
            for (let i = 0; i < pipeline.length; i++) {
              const task = pipeline[i];
              const [ err, response ] = await to(this.execProcess({
                ...task,
                step: i
              }));
              if (response) {
                processData[task.name] = response;
              }
            }
            return Promise.resolve(processData);
          }
          // 執(zhí)行構建中的 handler 操作
          async execProcess(task) {
            this.step(task.name, { status: 'start' });
            const result = await task.handler(this.buildContext);
            this.progress(task.progress);
            this.step(task.name, { status: 'end', taskMeta: result });
            return result;
          }
        • 構建的步驟,上面構建的一些流程,相比大家也想知道在服務端如何跑構建流程當中的一些腳本,其實思路就是通過 node 的child_process 模塊執(zhí)行 shell 腳本,下面是代碼的一些示例:
        import { spawn } from 'child_process';
        // git clone 
        execCmd(`git clone ${url} ${dir}`, {
          cwd: this.root,
          verbose: this.verbose
        });
        // npm run build
        const cmd = ['npm run build', cmdOption].filter(Boolean).join(' ');
        execCmd(cmd, options);
        // 執(zhí)行 shell 命令
        function execCmd(cmd: string, options:any = {}): Promise<any{
          const [ shell, ...args ] = cmd.split(' ').filter(Boolean);
          const { verbose, ...others } = options;
          return new Promise((resolve, reject) => {
            let child: any = spawn(shell, args, others);
            let stdout = '';
            let stderr = '';
            child.stdout && child.stdout.on('data'(buf: Buffer) => {
              stdout = `${stdout}${buf}`;
              if (verbose) {
                logger.info(`${buf}`);
              }
            });
            child.stderr && child.stderr.on('data'(buf: Buffer) => {
              stderr = `${stderr}${buf}`;
              if (verbose) {
                logger.error(`${buf}`);
              }
            });
            child.on('exit'(code: number) => {
              if (code !== 0) {
                const reason = stderr || 'some unknown error';
                reject(`exited with code ${code} due to ${reason}`);
              } else {
                resolve({stdout,  stderr});
              }
              child.kill();
              child = null;
            });
            child.on('error'err => {
              reject(err.message);
              child.kill();
              child = null;
            });
          });
        };
        • 而例如我們想在構建前想加入 Eslint 校驗操作,也可以在構建流程中加入,也就可以在線上構建的環(huán)節(jié)中加入攔截型的校驗,控制上線構建代碼質量。
        import { CLIEngine } from 'eslint';
        export function lintOnFiles(context{
          const { root } = context;
          const [ err ] = createPluginSymLink(root);
          if (err) {
            return [ err ];
          }
          const linter = new CLIEngine({
            envs: [ 'browser' ],
            useEslintrc: true,
            cwd: root,
            configFile: path.join(__dirname, 'LintConfig.js'),
            ignorePattern: ['**/router-config.js']
          });
          let report = linter.executeOnFiles(['src']);
          const errorReport = CLIEngine.getErrorResults(report.results);
          const errorList = errorReport.map(item => {
            const file = path.relative(root, item.filePath);
            return {
              file,
              errorCount: item.errorCount,
              warningCount: item.warningCount,
              messages: item.messages
            };
          });
          const result = {
            errorList,
            errorCount: report.errorCount,
            warningCount: report.warningCount
          }
          return [ null, result ];
        };
        • 構建部署完成后,可根據(jù)構建情況,來更新這條構建記錄的更新狀態(tài)信息,本次構建生成的 Docker 鏡像,上傳鏡像倉庫后,也需要信息記錄,方便后期可用之前構建的鏡像再次進行更新或者回滾操作,所以需要添加一張鏡像表,下面為 Docker 鏡像生成的一些實例代碼。
        import Docker = require('dockerode');
        // 保證服務端中有一個基本的 dockerfile 鏡像文件
        const docker = new Docker({ socketPath: '/var/run/docker.sock' });
        const image = '鏡像打包名稱'
        let buildStream;
        [ err, buildStream ] = await to(
          docker.buildImage({
            context: outputDir
          }, { t: image })
        );
        let pushStream;
        // authconfig 鏡像倉庫的一些驗證信息
        const authconfig = {
          serveraddress: "鏡像倉庫地址"
        };
        // 向遠端私有倉庫推送鏡像
        const dockerImage = docker.getImage(image);
        [ err, pushStream ] = await to(dockerImage.push({
          authconfig,
          tag
        }));
        // 3s 打印一次進度信息
        const progressLog = _.throttle((msg) => logger.info(msg), 3000); 
        const pushPromise = new Promise((resolve, reject) => {
          docker.modem.followProgress(pushStream, (err, res) => {
            err ? reject(err) : resolve(res);
          }, e => {
            if (e.error) {
              reject(e.error);
            } else {
              const { id, status, progressDetail } = e;
              if (progressDetail && !_.isEmpty(progressDetail)) {
                const { current, total } = progressDetail;
                const percent = Math.floor(current / total * 100);
                progressLog(`${id} : pushing progress ${percent}%`);
                if (percent === 100) { // 進度完成
                  progressLog.flush();
                }
              } else if (id && status) {
                logger.info(`${id} : ${status}`);
              }
            }
          });
        });
        await to(pushPromise);
        • 每一次的構建需要保存一些構建進度,日志等信息,可以再加一張日志表來進行日志的保存。

        多個構建實例的運行

        到這里一個項目的構建流程就已經(jīng)成功跑通了,但一個構建平臺肯定不能每次只能構建更新一個項目啊,所以這時候可以引入一個進程池,讓你的構建平臺可以同時構建多個項目。

        Node 是單線程模型,當需要執(zhí)行多個獨立且耗時任務的時候,只能通過 child_process 來分發(fā)任務,提高處理速度,所以也需要實現(xiàn)一個進程池,用來控制多構建進程運行的問題,進程池思路是主進程創(chuàng)建任務隊列,控制子進程數(shù)量,當子進程完成任務后,通過進程的任務隊列,來繼續(xù)添加新的子進程,以此來控制并發(fā)進程的運行,流程實現(xiàn)如下。

        ProcessPool.ts 以下是進程池的部分代碼,主要展示思路。

        import * as child_process from 'child_process';
        import { cpus } from 'os';
        import { EventEmitter } from 'events';
        import TaskQueue from './TaskQueue';
        import TaskMap from './TaskMap';
        import { to } from '../util/tool';
        export default class ProcessPool extends EventEmitter {
          private jobQueue: TaskQueue;
          private depth: number;
          private processorFile: string;
          private workerPath: string;
          private runningJobMap: TaskMap;
          private idlePool: Array<number>;
          private workPool: Map<anyany>;
          constructor(options: any = {}) {
            super();
            this.jobQueue = new TaskQueue('fap_pack_task_queue');
            this.runningJobMap = new TaskMap('fap_running_pack_task');
            this.depth = options.depth || cpus().length; // 最大的實例進程數(shù)量
            this.workerPath = options.workerPath;
            this.idlePool = []; // 工作進程  pid 數(shù)組
            this.workPool = new Map();  // 工作實例進程池
            this.init();
          }
          /**
           * @func init 初始化進程,
           */

          init() {
            while (this.workPool.size < this.depth) {
              this.forkProcess();
            }
          }
          /**
           * @func forkProcess fork 子進程,創(chuàng)建任務實例
           */

          forkProcess() {
            let worker: any = child_process.fork(this.workerPath);
            const pid = worker.pid;
            this.workPool.set(pid, worker);
            worker.on('message'async (data) => {
              const { cmd } = data;
              // 根據(jù) cmd 狀態(tài) 返回日志狀態(tài)或者結束后清理掉任務隊列
              if (cmd === 'log') {
              }
              if (cmd === 'finish' || cmd === 'fail') {
                this.killProcess();//結束后清除任務
              }
            });
            worker.on('exit'() => {
              // 結束后,清理實例隊列,開啟下一個任務
              this.workPool.delete(pid);
              worker = null;
              this.forkProcess();
              this.startNextJob();
            });
            return worker;
          }
          // 根據(jù)任務隊列,獲取下一個要進行的實例,開始任務
          async startNextJob() {
            this.run();
          }
          /**
           * @func add 添加構建任務
           * @param task 運行的構建程序
           */

          async add(task) {
            const inJobQueue = await this.jobQueue.isInQueue(task.appId); // 任務隊列
            const isRunningTask = await this.runningJobMap.has(task.appId); // 正在運行的任務
            const existed = inJobQueue || isRunningTask;
            if (!existed) {
              const len = await this.jobQueue.enqueue(task, task.appId);
              // 執(zhí)行任務
              const [err] = await to(this.run());
              if (err) {
                return Promise.reject(err);
              }
            } else {
              return Promise.reject(new Error('DuplicateTask'));
            }
          }
          /**
           * @func initChild 開始構建任務
           * @param child 子進程引用
           * @param processFile 運行的構建程序文件
           */

          initChild(child, processFile) {
            return new Promise(resolve => {
              child.send({ cmd: 'init', value: processFile }, resolve);
            });
          }
          /**
           * @func startChild 開始構建任務
           * @param child 子進程引用
           * @param task 構建任務
           */

          startChild(child, task) {
            child.send({ cmd: 'start', task });
          }
          /**
           * @func run 開始隊列任務運行
           */

          async run() {
            const jobQueue = this.jobQueue;
            const isEmpty = await jobQueue.isEmpty();
            // 有空閑資源并且任務隊列不為空
            if (this.idlePool.length > 0 && !isEmpty) {
              // 獲取空閑構建子進程實例
              const taskProcess = this.getFreeProcess();
              await this.initChild(taskProcess, this.processorFile);
              const task = await jobQueue.dequeue();
              if (task) {
                await this.runningJobMap.set(task.appId, task);
                this.startChild(taskProcess, task);
                return task;
              }
            } else {
              return Promise.reject(new Error('NoIdleResource'));
            }
          }
          /**
           * @func getFreeProcess 獲取空閑構建子進程
           */

          getFreeProcess() {
            if (this.idlePool.length) {
              const pid = this.idlePool.shift();
              return this.workPool.get(pid);
            }
            return null;
          }
          
          /**
           * @func killProcess 殺死某個子進程,原因:釋放構建運行時占用的內存
           * @param pid 進程 pid
           */

          killProcess(pid) {
            let child = this.workPool.get(pid);
            child.disconnect();
            child && child.kill();
            this.workPool.delete(pid);
            child = null;
          }
        }

        Build.ts

        import ProcessPool from './ProcessPool';
        import TaskMap from './TaskMap';
        import * as path from 'path';
        // 日志存儲
        const runningPackTaskLog = new TaskMap('fap_running_pack_task_log');
        //初始化進程池
        const packQueue = new ProcessPool({
          workerPath: path.join(__dirname, '../../task/func/worker'),
          depth: 3
        });
        // 初始化構建文件
        packQueue.process(path.join(__dirname, '../../task/func/server-build'));
        let key: string;
        packQueue.on('message'async data => {
          // 根據(jù)項目 id,部署記錄 id,以及用戶 id 來設定 redis 緩存的 key 值,之后進行日志存儲
          key = `${appId}_${deployId}_${deployer.userId}`;
          const { cmd, value } = data;
          if(cmd === 'log') { // 構建任務日志
            runningPackTaskLog.set(key,value);
          } else if (cmd === 'finish') { // 構建完成
            runningPackTaskLog.delete(key);
            // 后續(xù)日志可以進行數(shù)據(jù)庫存儲
          } else if (cmd === 'fail') { // 構建失敗
            runningPackTaskLog.delete(key);
            // 后續(xù)日志可以進行數(shù)據(jù)庫存儲
          }
          // 可以通過 websocket 將進度同步給前臺展示
        });
        //添加新的構建任務
        let [ err ] = await to(packQueue.add({
          ...appAttrs, // 構建所需信息
        }));

        有了進程池處理了多進程構建之后,如何記錄每個進程構建進度呢,我這邊選擇用了 Redis 數(shù)據(jù)庫進行構建進度狀態(tài)的緩存,同時通過Websocket 同步前臺的進度展示,在構建完成后,進行日志的本地存儲。上面代碼簡單介紹了進程池的實現(xiàn)以及使用,當然具體的應用還要看自己設計思路了,有了進程池的幫助下,剩下的思路其實就是具體代碼實現(xiàn)了。

        前端構建的未來

        最后來聊聊我們對于前端構建未來的一些想法吧,首先前端構建必須保證的是更加穩(wěn)定的構建,在穩(wěn)定的前提下,來達到更快的構建,對于 CI/CD 方向,比如更加完整的構建流程,在更新完生成線上環(huán)境以后,自動處理代碼的歸檔,歸檔后最新的 Master 代碼重新合入各個開發(fā)分支,再更新全部的測試環(huán)境等等。

        而對于服務端性能方面,我們考慮過能不能將云端構建的能力來靠每臺開發(fā)的電腦來完成,實現(xiàn)本地構建,云端部署的離岸云端構建,將服務器壓力分散到各自的電腦上,這樣也能減輕服務端構建的壓力,服務端只做最后的部署服務即可。

        還有比如我們的開發(fā)同學很想要項目按組的維度進行打包發(fā)布的功能,一次發(fā)布的版本中,選定好要一起更新發(fā)布的項目以及版本分支,統(tǒng)一發(fā)布更新。

        小結

        所以有了自己的構建發(fā)布平臺,自己想要的功能都可以自己操作起來,可以做前端自己想要的各類功能,豈不是美滋滋。我猜很多同學可能會對我們做的 VsCode 插件感興趣吧,除了構建項目,當然還有一些其他的功能,比如公司測試賬號的管理,小程序的快速構建等等輔助開發(fā)的功能,是不是想進一步了解這個插件的功能呢,請期待我們之后的分享吧。

        參考文檔

        node child_process 文檔 (http://nodejs.cn/api/child_process.html#child_process_child_process_fork_modulepath_args_options)

        深入理解Node.js 進程與線程 (https://blog.csdn.net/xgangzai/article/details/98919412)

        淺析 Node 進程與線程 (https://juejin.cn/post/6844904033640169486?utm_source=weibo&utm_campaign=admin)

        瀏覽 35
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            亚洲男人av| 俺去啦在线| 亚洲精品久久久久avwww潮水| 黄色毛片在线| 久久久久久久久黄色| 免费看V片| av在线资源网站| 日本爱爱视频| 黄色片在线| 亚洲www.| 亚洲国产精品久久人人爱| 337p西西人体大胆瓣开下部 | 2025av中文字幕| 伊人毛片| 成人免费无码婬片在线| www.99爱| 久大香蕉| 国产精品93333333| 久久在线免费视频| 日韩一级片免费看| 国产手机拍视频推荐2023| 中文在线A∨在线| 亚洲av黄片| 天天做天天爱夜夜爽| 无码精品人妻一区二区欧美| 女人自慰在线观看| 三洞齐开Av在线免费观看| 91成人免费视频| 国产又粗又长| 久久精品视频18| 欧美视频中文字幕| 中文字幕11页| 亚洲无码高清视频在线| 久久极品| 国产黄色免费电影| 日本中文视频| 成人电影综合网| 亚洲AV无码国产精品久久不卡 | 天堂成人网站| 成人在线视频一区| 中文原创麻豆传媒md0052| 天天天天天天天天干| 天天天日天天天操| 国产在线不卡年轻点的| 国产成人AV| 91av视频在线观看| 色老板在线观看| 高清无码视频网站| 无码精品ThePorn| 日产久久视频| 安徽妇女BBBWBBBwm| 日韩人妻一区二区三区| 中文字幕成人免费视频| 99自拍视频| 人人狠狠综合婷婷| 日日撸视频| 国产精品女人777777| 99久久精品国产一区二区成人 | 国产精品白浆| 国产动态图| 中文字幕无码Av在线看| 69久久久久久久久久| 四虎影院污| 91无码人妻一区二区成人AⅤ| 国产高清无码免费| 在线看黄网| 日韩三级网| 丁香五月AV| 国产日本在线视频| 大蕉网| 中文字幕在线观| 国产毛片一照区| 一级a免一级a做免费线看内裤| 日本天堂在线| 四虎永久在线精品无码| 亚洲天堂电影网| 亚洲区成人777777精品| 大鸡吧在线| 青草久久久久| 无码人妻精品一区二区三区蜜臀百度| 免费一区二区三区四区| 男人午夜网站| 国产精品a久久久久| 亚洲成人在线| 九九九av| 91精品网站| 69视频在线免费观看| 尹人在线视频| 亚洲高清在线视频| 操人在线观看| 中文字幕一区二区三区四区五区六区 | 成人五区| 五月综合久久| 性欧美丰满熟妇XXXX性久久久| 午夜福利成人网站| 97色综合| 91人妻中文字幕| 宅男视频| 一区二区三区精品| 人人人人人人操| 婷婷五月天综合网| 夜夜天天人人| AV东方在线| 天天天做夜夜夜夜爽无码| 国产伦精品一区二区三区视频女 | 久久婷婷国产综合| 天天爽日日澡AAAA片| 99久免费视频| 极品无码| 夜夜撸夜夜| 日本色色网站| 久久久久久99| 免费黄色视频网址| 午夜操一操一级| 欧美国产视频| 一区二区三区四区在线| 亚洲黄片在线| 成人免费在线观看| 亚洲中文字幕日本| 亚欧洲精品在线视频免费观看| 国产成人无码毛片| 国产9熟妇视频网站| 精品一区二区三区蜜桃臀www | 男女啪啪啪网站| 91免费小视频| 国产亚洲网| 无码A∨| 国产免费观看AV| 亚洲区视频| 欧一美一婬一伦一区?| 日韩高清一区二区| 免费欧美成人网站| www.狠狠撸| 亚洲一区亚洲二区| 欧美一级黄色电影| 亲子伦一区二区三区观看方式| 国产第一页在线观看| 亚洲AV秘无码不卡在线观看 | 夜夜爽夜夜高潮夜夜爽| 中文字幕在线视频免费观看| 在线观看免费欧美操逼视频| 日韩一区二区在线视频| 99精品一区二区三区| 久久久久亚洲AV成人网人人软件 | 日韩熟妇视频| 影音先锋av在线资源站| 神马影院午夜福利| 色久悠悠综合网| 久草超碰在线| 成人黃色A片免费看| 久久久91| 在线aaa| 日韩精品一区二区三区四区 | 影音先锋男人网| 韩国高清无码| 97天天干| 亚洲高清无码在线观看视频| 自拍偷拍一区| 免费观看黄色AV| 国产三级网站| 欧洲第一无人区观看| 亚洲无码1| 日韩无码三级| 国产xxxx视频| 亚洲在线免费观看| 蜜臀精品一区二区三区| 亚洲av综合在线| 91视频一区二区三区| 天天舔天天干| 亚洲国产一区二区三区四区| 亚洲内射视频| 日本无码成人片在线播放| 国产三级毛片| 国产男女AV| 99精品一区二区| 3D动漫精品啪啪一区二区免费| 大香蕉a片| 香蕉av在线| 国产精品九九| 欧美久久免费| 狠狠躁夜夜躁人人爽视频| 国产无套内射在线观看| 久操国产视频| 成人黄色网| 九九热国产视频| 18禁看网站| 大香蕉av一区二区三区在线观看| 伊人天天操| 北条麻妃99精品青青久久| 影音先锋av在线资源站| 老熟女-ThePorn| 作爱网站| 中国人妻HDbute熟睡| 黄网站免费看| 亚洲国产熟妇无码日韩| 亚洲日韩在线看| 四虎最新视频| 国产福利精品视频| 天堂婷婷| 水果派解说AV无码一区| 亚洲精品影院| 亚洲精品熟女| 免费视频99| 久久成人在线视频| 日本三级在线| 午夜电影福利| 乱伦乱码| 国产三级片视频在线观看| 嫩BBB嫩BBB嫩BBB| 麻豆亚洲AV成人无码久久精品| 超碰在线人人爱| 国产欧美视频在线| 在线视频91| 另类老妇奶BBBBwBB| 国产一级a毛一级a毛观看视频网站www.jn| 国产三级日本三级国产三级| 在线观看高清无码视频| 五月天婷婷丁香综合视频| 精品精品精品| 黄色片视频日本| 波多野结衣无码高清| 99色网站| 亚l洲视频在线观看| 视频一区中文字幕| 黄色A片网| 91视频在线观看| 男人天堂V| 蜜臀av在线观看| 一区二区三区日本| 成人一区二区电影| 国产性交网站| 欧美日韩a| 亚洲高清无码中文字幕| 黑人猛躁白人BBBBBBBBB| 日韩一区二区高清无码| 草av| 在线免费观看国产| 中文字幕无码AV| 成人午夜毛片| 久久超碰精品| 欧美性区| 亚洲日韩在线视频观看| 午夜大黄片| 空姐白洁| 亚洲午夜AV久久乱码| 七十路の高齡熟妇无码| 视频一区18| 玖玖在线播放| 亚洲一线视频| 爱爱中文字幕| 国产乱国产乱老熟300视频| 在线免费观看黄片| 秋霞午夜福利| 人人射人人操| 男女做爱视频网站| 99re免费视频| 日韩精品免费在线观看| 国产一区二区三区免费观看| 99热精品2| 国产精品尤物| 国产无遮挡又黄又爽在线观看| 淫秽视频免费看| 日韩人妻精品中文字幕免费 | 性做久久久久久| 午夜黄片| www黄片| 成人无码www在线看免费| 蜜臀999| 亚洲精品一区二区三区在线观看| 国产无码中文字幕| 日韩精品人妻无码| 婷婷国产精品| 欧美一级久久| 色欲欲www成人网站| 亚洲精品久久久久毛片A级绿茶| 欧美日韩在线观看中文字幕| 特级西西444www大胆高清图片| 五月天国产视频| 亚洲欧美在线一区| 欧美成人中文字幕| 欧美在线观看网站18| 肏逼网站在线观看| 成人免费内射视频| 国产三级AV在线观看| 99re2| 在线看操逼| 人人爽网站| 就去色色五月天| 欧美亚洲综合在线观看| 欧美久草蜜桃视频| 超碰自拍99| 在线高清无码不卡| 国产寡妇亲子伦一区二区三区四区 | 大鸡巴视频在线观看| 日韩亚洲在线| 嫩草国产| 秋霞A片| 先锋影音在线资源| 99热8| 18禁黄网站|