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>

        手寫(xiě)webpack核心原理,再也不怕面試官問(wèn)我webpack原理

        共 14686字,需瀏覽 30分鐘

         ·

        2021-04-06 11:44

        作者:培歌行

        來(lái)源:SegmentFault 思否社區(qū)





        手寫(xiě)webpack核心原理


        [toc]


        一、核心打包原理


        1.1 打包的主要流程如下


        1. 需要讀到入口文件里面的內(nèi)容。
        2. 分析入口文件,遞歸的去讀取模塊所依賴(lài)的文件內(nèi)容,生成AST語(yǔ)法樹(shù)。
        3. 根據(jù)AST語(yǔ)法樹(shù),生成瀏覽器能夠運(yùn)行的代碼


        1.2 具體細(xì)節(jié)


        1. 獲取主模塊內(nèi)容
        2. 分析模塊
          • 安裝@babel/parser包(轉(zhuǎn)AST)
        3. 對(duì)模塊內(nèi)容進(jìn)行處理
          • 安裝@babel/traverse包(遍歷AST收集依賴(lài))
          • 安裝@babel/core和@babel/preset-env包 (es6轉(zhuǎn)ES5)
        4. 遞歸所有模塊
        5. 生成最終代碼





        二、基本準(zhǔn)備工作


        我們先建一個(gè)項(xiàng)目


        項(xiàng)目目錄暫時(shí)如下:



        已經(jīng)把項(xiàng)目放到 

        github:https://github.com/Sunny-lucking/howToBuildMyWebpack

         

        我們創(chuàng)建了add.js文件和minus.js文件,然后 在index.js中引入,再將index.js文件引入index.html。


        代碼如下:


        add.js


        export default (a,b)=>{  return a+b;}


        minus.js


        export const minus = (a,b)=>{    return a-b}


        index.js


        import add from "./add"import {minus} from "./minus";
        const sum = add(1,2);const division = minus(2,1);
        console.log(sum);console.log(division);


        index.html


        <!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Title</title></head><body><script src="./src/index.js"></script></body></html>


        現(xiàn)在我們打開(kāi)index.html。你猜會(huì)發(fā)生什么???顯然會(huì)報(bào)錯(cuò),因?yàn)闉g覽器還不能識(shí)別import語(yǔ)法



        不過(guò)沒(méi)關(guān)系,因?yàn)槲覀儽緛?lái)就是要來(lái)解決這些問(wèn)題的。





        三、獲取模塊內(nèi)容


        好了,現(xiàn)在我們開(kāi)始根據(jù)上面核心打包原理的思路來(lái)實(shí)踐一下,第一步就是 實(shí)現(xiàn)獲取模塊內(nèi)容。


        我們來(lái)創(chuàng)建一個(gè)bundle.js文件。


        // 獲取主入口文件const fs = require('fs')const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    console.log(body);}getModuleInfo("./src/index.js")


        目前項(xiàng)目目錄如下



        我們來(lái)執(zhí)行一下bundle.js,看看時(shí)候成功獲得入口文件內(nèi)容



        哇塞,不出所料的成功。一切盡在掌握之中。好了,已經(jīng)實(shí)現(xiàn)第一步了,且讓我看看第二步是要干嘛。


        哦?是分析模塊了





        四、分析模塊


        分析模塊的主要任務(wù)是 將獲取到的模塊內(nèi)容 解析成AST語(yǔ)法樹(shù),這個(gè)需要用到一個(gè)依賴(lài)包@babel/parser


        npm install @babel/parser


        ok,安裝完成我們將@babel/parser引入bundle.js,


        // 獲取主入口文件const fs = require('fs')const parser = require('@babel/parser')const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });    console.log(ast);}getModuleInfo("./src/index.js")


        我們?nèi)タ聪翤babel/parser的文檔:




        可見(jiàn)提供了三個(gè)API,而我們目前用到的是parse這個(gè)API。


        它的主要作用是 parses the provided code as an entire ECMAScript program,也就是將我們提供的代碼解析成完整的ECMAScript代碼。


        再看看該API提供的參數(shù)




        我們暫時(shí)用到的是sourceType,也就是用來(lái)指明我們要解析的代碼是什么模塊。


        好了,現(xiàn)在我們來(lái)執(zhí)行一下 bundle.js,看看AST是否成功生成。



        成功。又是不出所料的成功。


        不過(guò),我們需要知道的是,當(dāng)前我們解析出來(lái)的不單單是index.js文件里的內(nèi)容,它也包括了文件的其他信息。


        而它的內(nèi)容其實(shí)是它的屬性program里的body里。如圖所示



        我們可以改成打印ast.program.body看看


        // 獲取主入口文件const fs = require('fs')const parser = require('@babel/parser')const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });    console.log(ast.program.body);}getModuleInfo("./src/index.js"


        執(zhí)行



        看,現(xiàn)在打印出來(lái)的就是 index.js文件里的內(nèi)容(也就是我們?cè)賗ndex.js里寫(xiě)的代碼啦).





        五、收集依賴(lài)


        現(xiàn)在我們需要 遍歷AST,將用到的依賴(lài)收集起來(lái)。什么意思呢?其實(shí)就是將用import語(yǔ)句引入的文件路徑收集起來(lái)。我們將收集起來(lái)的路徑放到deps里。


        前面我們提到過(guò),遍歷AST要用到@babel/traverse依賴(lài)包


        npm install @babel/traverse


        現(xiàn)在,我們引入。


        const fs = require('fs')const path = require('path')const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });
        // 新增代碼 const deps = {} traverse(ast,{ ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = './' + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) console.log(deps);

        }getModuleInfo("./src/index.js")


        我們來(lái)看下官方文檔對(duì)@babel/traverse的描述




        好吧,如此簡(jiǎn)略


        不過(guò)我們不難看出,第一個(gè)參數(shù)就是AST。第二個(gè)參數(shù)就是配置對(duì)象


        我們看看我們寫(xiě)的代碼


        traverse(ast,{    ImportDeclaration({node}){        const dirname = path.dirname(file)        const abspath = './' + path.join(dirname,node.source.value)        deps[node.source.value] = abspath    }})


        配置對(duì)象里,我們配置了ImportDeclaration方法,這是什么意思呢?
        我們看看之前打印出來(lái)的AST。



        ImportDeclaration方法代表的是對(duì)type類(lèi)型為ImportDeclaration的節(jié)點(diǎn)的處理。


        這里我們獲得了該節(jié)點(diǎn)中source的value,也就是node.source.value,


        這里的value指的是什么意思呢?其實(shí)就是import的值,可以看我們的index.js的代碼。


        import add from "./add"import {minus} from "./minus";
        const sum = add(1,2);const division = minus(2,1);
        console.log(sum);console.log(division);


        可見(jiàn),value指的就是import后面的 './add' 和 './minus'


        然后我們將file目錄路徑跟獲得的value值拼接起來(lái)保存到deps里,美其名曰:收集依賴(lài)。


        ok,這個(gè)操作就結(jié)束了,執(zhí)行看看收集成功了沒(méi)?



        oh my god。又成功了。





        六、ES6轉(zhuǎn)成ES5(AST)


        現(xiàn)在我們需要把獲得的ES6的AST轉(zhuǎn)化成ES5的AST,前面講到過(guò),執(zhí)行這一步需要兩個(gè)依賴(lài)包


        npm install @babel/core @babel/preset-env


        我們現(xiàn)在將依賴(lài)引入并使用


        const fs = require('fs')const path = require('path')const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst babel = require('@babel/core')const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });    const deps = {}    traverse(ast,{        ImportDeclaration({node}){            const dirname = path.dirname(file)            const abspath = "./" + path.join(dirname,node.source.value)            deps[node.source.value] = abspath        }    })
        新增代碼 const {code} = babel.transformFromAst(ast,null,{ presets:["@babel/preset-env"] }) console.log(code);
        }getModuleInfo("./src/index.js")


        我們看看官網(wǎng)文檔對(duì)@babel/core 的transformFromAst的介紹


        害,又是一如既往的簡(jiǎn)略。。。


        簡(jiǎn)單說(shuō)一下,其實(shí)就是將我們傳入的AST轉(zhuǎn)化成我們?cè)诘谌齻€(gè)參數(shù)里配置的模塊類(lèi)型。


        好了,現(xiàn)在我們來(lái)執(zhí)行一下,看看結(jié)果



        我的天,一如既往的成功??梢?jiàn) 它將我們寫(xiě)const 轉(zhuǎn)化成var了。


        好了,這一步到此結(jié)束,咦,你可能會(huì)有疑問(wèn),上一步的收集依賴(lài)在這里怎么沒(méi)啥關(guān)系啊,確實(shí)如此。收集依賴(lài)是為了下面進(jìn)行的遞歸操作。





        七、遞歸獲取所有依賴(lài)


        經(jīng)過(guò)上面的過(guò)程,現(xiàn)在我們知道getModuleInfo是用來(lái)獲取一個(gè)模塊的內(nèi)容,不過(guò)我們還沒(méi)把獲取的內(nèi)容return出來(lái),因此,更改下getModuleInfo方法


        const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });    const deps = {}    traverse(ast,{        ImportDeclaration({node}){            const dirname = path.dirname(file)            const abspath = "./" + path.join(dirname,node.source.value)            deps[node.source.value] = abspath        }    })    const {code} = babel.transformFromAst(ast,null,{        presets:["@babel/preset-env"]    })    // 新增代碼    const moduleInfo = {file,deps,code}    return moduleInfo}


        我們返回了一個(gè)對(duì)象 ,這個(gè)對(duì)象包括該模塊的路徑(file),該模塊的依賴(lài)(deps),該模塊轉(zhuǎn)化成es5的代碼


        該方法只能獲取一個(gè)模塊的的信息,但是我們要怎么獲取一個(gè)模塊里面的依賴(lài)模塊的信息呢?


        沒(méi)錯(cuò),看標(biāo)題,,你應(yīng)該想到了就算遞歸。


        現(xiàn)在我們來(lái)寫(xiě)一個(gè)遞歸方法,遞歸獲取依賴(lài)


        const parseModules = (file) =>{    const entry =  getModuleInfo(file)    const temp = [entry]    for (let i = 0;i<temp.length;i++){        const deps = temp[i].deps        if (deps){            for (const key in deps){                if (deps.hasOwnProperty(key)){                    temp.push(getModuleInfo(deps[key]))                }            }        }    }    console.log(temp)}


        講解下parseModules方法:

        1. 我們首先傳入主模塊路徑
        2. 將獲得的模塊信息放到temp數(shù)組里。
        3. 外面的循壞遍歷temp數(shù)組,此時(shí)的temp數(shù)組只有主模塊
        4. 里面再獲得主模塊的依賴(lài)deps
        5. 遍歷deps,通過(guò)調(diào)用getModuleInfo將獲得的依賴(lài)模塊信息push到temp數(shù)組里。


        目前bundle.js文件:


        const fs = require('fs')const path = require('path')const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst babel = require('@babel/core')const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });    const deps = {}    traverse(ast,{        ImportDeclaration({node}){            const dirname = path.dirname(file)            const abspath = "./" + path.join(dirname,node.source.value)            deps[node.source.value] = abspath        }    })    const {code} = babel.transformFromAst(ast,null,{        presets:["@babel/preset-env"]    })    const moduleInfo = {file,deps,code}    return moduleInfo}
        // 新增代碼const parseModules = (file) =>{ const entry = getModuleInfo(file) const temp = [entry] for (let i = 0;i<temp.length;i++){ const deps = temp[i].deps if (deps){ for (const key in deps){ if (deps.hasOwnProperty(key)){ temp.push(getModuleInfo(deps[key])) } } } } console.log(temp)}parseModules("./src/index.js")


        按照目前我們的項(xiàng)目來(lái)說(shuō)執(zhí)行完,應(yīng)當(dāng)是temp 應(yīng)當(dāng)是存放了index.js,add.js,minus.js三個(gè)模塊。


        ,執(zhí)行看看。



        牛逼?。?!確實(shí)如此。


        不過(guò)現(xiàn)在的temp數(shù)組里的對(duì)象格式不利于后面的操作,我們希望是以文件的路徑為key,{code,deps}為值的形式存儲(chǔ)。因此,我們創(chuàng)建一個(gè)新的對(duì)象depsGraph。


        const parseModules = (file) =>{    const entry =  getModuleInfo(file)    const temp = [entry]     const depsGraph = {} //新增代碼    for (let i = 0;i<temp.length;i++){        const deps = temp[i].deps        if (deps){            for (const key in deps){                if (deps.hasOwnProperty(key)){                    temp.push(getModuleInfo(deps[key]))                }            }        }    }    // 新增代碼    temp.forEach(moduleInfo=>{        depsGraph[moduleInfo.file] = {            deps:moduleInfo.deps,            code:moduleInfo.code        }    })    console.log(depsGraph)    return depsGraph}


        ok,現(xiàn)在存儲(chǔ)的就是這種格式啦






        八、處理兩個(gè)關(guān)鍵字


        我們現(xiàn)在的目的就是要生成一個(gè)bundle.js文件,也就是打包后的一個(gè)文件。其實(shí)思路很簡(jiǎn)單,就是把index.js的內(nèi)容和它的依賴(lài)模塊整合起來(lái)。然后把代碼寫(xiě)到一個(gè)新建的js文件。



        我們把這段代碼格式化一下


        // index.js"use strict"var _add = _interopRequireDefault(require("./add.js"));var _minus = require("./minus.js");function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }var sum = (0, _add["default"])(1, 2);var division = (0, _minus.minus)(2, 1);console.log(sum); console.log(division);


        // add.js"use strict";Object.defineProperty(exports, "__esModule", {  value: true});exports["default"] = void 0;var _default = function _default(a, b) {  return a + b;};exports["default"] = _default;


        但是我們現(xiàn)在是不能執(zhí)行index.js這段代碼的,因?yàn)闉g覽器不會(huì)識(shí)別執(zhí)行require和exports。


        不能識(shí)別是為什么?不就是因?yàn)闆](méi)有定義這require函數(shù),和exports對(duì)象。那我們可以自己定義。


        我們創(chuàng)建一個(gè)函數(shù)


        const bundle = (file) =>{    const depsGraph = JSON.stringify(parseModules(file))
        }


        我們將上一步獲得的depsGraph保存起來(lái)。


        現(xiàn)在返回一個(gè)整合完整的字符串代碼。


        怎么返回呢?更改下bundle函數(shù)


        const bundle = (file) =>{    const depsGraph = JSON.stringify(parseModules(file))    return `(function (graph) {                function require(file) {                    (function (code) {                        eval(code)                    })(graph[file].code)                }                require(file)            })(depsGraph)`
        }


        我們看下返回的這段代碼

         (function (graph) {        function require(file) {            (function (code) {                eval(code)            })(graph[file].code)        }        require(file)    })(depsGraph)


        其實(shí)就是

        1. 把保存下來(lái)的depsGraph,傳入一個(gè)立即執(zhí)行函數(shù)。
        2. 將主文件路徑傳入require函數(shù)執(zhí)行
        3. 執(zhí)行reuire函數(shù)的時(shí)候,又立即執(zhí)行一個(gè)立即執(zhí)行函數(shù),這里是把code的值傳進(jìn)去了
        4. 執(zhí)行eval(code)。也就是執(zhí)行code這段代碼


        我們?cè)賮?lái)看下code的值


        // index.js"use strict"var _add = _interopRequireDefault(require("./add.js"));var _minus = require("./minus.js");function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }var sum = (0, _add["default"])(1, 2);var division = (0, _minus.minus)(2, 1);console.log(sum); console.log(division);


        沒(méi)錯(cuò)執(zhí)行這段代碼的時(shí)候,又會(huì)用到require函數(shù)。此時(shí)require的參數(shù)為add.js的路徑,哎,不是絕對(duì)路徑,需要轉(zhuǎn)化成絕對(duì)路徑。因此寫(xiě)一個(gè)函數(shù)absRequire來(lái)轉(zhuǎn)化。怎么實(shí)現(xiàn)呢?我們來(lái)看下代碼


        (function (graph) {    function require(file) {        function absRequire(relPath) {            return require(graph[file].deps[relPath])        }        (function (require,code) {            eval(code)        })(absRequire,graph[file].code)    }    require(file)})(depsGraph)


        實(shí)際上是實(shí)現(xiàn)了一層攔截。


        1. 執(zhí)行require('./src/index.js')函數(shù)
        2. 執(zhí)行了


        (function (require,code) {    eval(code)})(absRequire,graph[file].code)
        1. 執(zhí)行eval,也就是執(zhí)行了index.js的代碼。

        2. 執(zhí)行過(guò)程會(huì)執(zhí)行到require函數(shù)。

        3. 這時(shí)會(huì)調(diào)用這個(gè)require,也就是我們傳入的absRequire



        1. 而執(zhí)行absRequire就執(zhí)行了return require(graph[file].deps[relPath])這段代碼,也就是執(zhí)行了外面這個(gè)require



        在這里return require(graph[file].deps[relPath]),我們已經(jīng)對(duì)路徑轉(zhuǎn)化成絕對(duì)路徑了。因此執(zhí)行外面的require的時(shí)候就是傳入絕對(duì)路徑。


        1. 而執(zhí)行require("./src/add.js")之后,又會(huì)執(zhí)行eval,也就是執(zhí)行add.js文件的代碼。


        是不是有點(diǎn)繞?其實(shí)是個(gè)遞歸。


        這樣就將代碼整合起來(lái)了,但是有個(gè)問(wèn)題,就是在執(zhí)行add.js的code時(shí)候,會(huì)遇到exports這個(gè)還沒(méi)定義的問(wèn)題。如下所示


        // add.js"use strict";Object.defineProperty(exports, "__esModule", {  value: true});exports["default"] = void 0;var _default = function _default(a, b) {  return a + b;};exports["default"] = _default;


        我們發(fā)現(xiàn) 這里它把exports當(dāng)作一個(gè)對(duì)象來(lái)使用了,但是這個(gè)對(duì)象還沒(méi)定義,因此我們可以自己定義一個(gè)exports對(duì)象。


        (function (graph) {    function require(file) {        function absRequire(relPath) {            return require(graph[file].deps[relPath])        }        var exports = {}        (function (require,exports,code) {            eval(code)        })(absRequire,exports,graph[file].code)        return exports    }    require(file)})(depsGraph)


        我們?cè)鎏砹艘粋€(gè)空對(duì)象 exports,執(zhí)行add.js代碼的時(shí)候,會(huì)往這個(gè)空對(duì)象上增加一些屬性,

        // add.js"use strict";Object.defineProperty(exports, "__esModule", {  value: true});exports["default"] = void 0;var _default = function _default(a, b) {  return a + b;};exports["default"] = _default;


        比如,執(zhí)行完這段代碼后

        exports = {  __esModule:{  value: true},  default:function _default(a, b) {  return a + b;}}


        然后我們把exports對(duì)象return出去。


        var _add = _interopRequireDefault(require("./add.js"));


        可見(jiàn),return出去的值,被_interopRequireDefault接收,_interopRequireDefault再返回default這個(gè)屬性給_add,因此_add = function _default(a, b) { return a + b;}


        現(xiàn)在明白了,為什么ES6模塊 引入的是一個(gè)對(duì)象引用了吧,因?yàn)閑xports就是一個(gè)對(duì)象。


        至此,處理;兩個(gè)關(guān)鍵詞的功能就完整了。

        const fs = require('fs')const path = require('path')const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst babel = require('@babel/core')const getModuleInfo = (file)=>{    const body = fs.readFileSync(file,'utf-8')    const ast = parser.parse(body,{        sourceType:'module' //表示我們要解析的是ES模塊    });    const deps = {}    traverse(ast,{        ImportDeclaration({node}){            const dirname = path.dirname(file)            const abspath = "./" + path.join(dirname,node.source.value)            deps[node.source.value] = abspath        }    })    const {code} = babel.transformFromAst(ast,null,{        presets:["@babel/preset-env"]    })    const moduleInfo = {file,deps,code}    return moduleInfo}const parseModules = (file) =>{    const entry =  getModuleInfo(file)    const temp = [entry]    const depsGraph = {}    for (let i = 0;i<temp.length;i++){        const deps = temp[i].deps        if (deps){            for (const key in deps){                if (deps.hasOwnProperty(key)){                    temp.push(getModuleInfo(deps[key]))                }            }        }    }    temp.forEach(moduleInfo=>{        depsGraph[moduleInfo.file] = {            deps:moduleInfo.deps,            code:moduleInfo.code        }    })    return depsGraph}// 新增代碼const bundle = (file) =>{    const depsGraph = JSON.stringify(parseModules(file))    return `(function (graph) {        function require(file) {            function absRequire(relPath) {                return require(graph[file].deps[relPath])            }            var exports = {}            (function (require,exports,code) {                eval(code)            })(absRequire,exports,graph[file].code)            return exports        }        require('${file}')    })(${depsGraph})`
        }const content = bundle('./src/index.js')
        console.log(content);


        來(lái)執(zhí)行下,看看效果




        確實(shí)執(zhí)行成功。接下來(lái),把返回的這段代碼寫(xiě)入新創(chuàng)建的文件中


        //寫(xiě)入到我們的dist目錄下fs.mkdirSync('./dist');fs.writeFileSync('./dist/bundle.js',content)


        至次,我們的手寫(xiě)webpack核心原理就到此結(jié)束了。


        我們參觀(guān)下生成的bundle.js文件



        發(fā)現(xiàn)其實(shí)就是將我們?cè)缙谑占乃幸蕾?lài)作為參數(shù)傳入到立即執(zhí)行函數(shù)當(dāng)中,然后通過(guò)eval來(lái)遞歸地執(zhí)行每個(gè)依賴(lài)的code。


        現(xiàn)在我們將bundle.js文件引入index.html看看能不能執(zhí)行



        成功。。。。。驚喜。。


        感謝您也恭喜您看到這里,我可以卑微的求個(gè)star嗎?。。?/span>




        點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開(kāi)更多互動(dòng)和交流。

        - END -

        瀏覽 36
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        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>
            国产成人做爰A片免费胖人 | 欧洲成人在线视频 | 免费无遮挡无码永久在线观看视频 | 青娱乐在线视频国产 | 午夜AV天堂 | 影音先锋成人网站 | 女性裸体不遮胸平台 | 日韩AV一级片 | 国产一极毛片 | 我要看日逼|