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>

        手寫webpack核心原理,再也不怕面試官問我webpack原理

        共 12744字,需瀏覽 26分鐘

         ·

        2020-08-13 20:23

        一、核心打包原理

        1.1 打包的主要流程如下

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

        1.2 具體細節(jié)

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

        二、基本準備工作

        我們先建一個項目

        項目目錄暫時如下:

        ?

        已經(jīng)把項目放到 「github」:https://github.com/Sunny-lucking/howToBuildMyWebpack??梢员拔⒌匾獋€star嗎

        ?

        我們創(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


        "en">

        ????"UTF-8">
        ????Title





        現(xiàn)在我們打開index.html。你猜會發(fā)生什么???顯然會報錯,因為瀏覽器還不能識別import語法

        不過沒關(guān)系,因為我們本來就是要來解決這些問題的。

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

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

        我們來創(chuàng)建一個bundle.js文件。

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

        目前項目目錄如下

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

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

        哦?是分析模塊了

        四、分析模塊

        分析模塊的主要任務(wù)是 將獲取到的模塊內(nèi)容 解析成AST語法樹,這個需要用到一個依賴包@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的文檔:

        可見提供了三個API,而我們目前用到的是parse這個API。

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

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

        我們暫時用到的是sourceType,也就是用來指明我們要解析的代碼是什么模塊。

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

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

        不過,我們需要知道的是,當前我們解析出來的不單單是index.js文件里的內(nèi)容,它也包括了文件的其他信息。而它的內(nèi)容其實是它的屬性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)在打印出來的就是 index.js文件里的內(nèi)容(也就是我們在index.js里寫的代碼啦).

        五、收集依賴

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

        前面我們提到過,遍歷AST要用到@babel/traverse依賴包

        npm?install?@babel/traverse

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

        const?fs?=?require('fs')
        const?path?=?require('path')
        const?parser?=?require('@babel/parser')
        const?traverse?=?require('@babel/traverse').default
        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
        ????????}
        ????})
        ????console.log(deps);


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

        我們來看下官方文檔對@babel/traverse的描述

        好吧,如此簡略

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

        我們看看我們寫的代碼

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

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

        ImportDeclaration 方法代表的是對type類型為ImportDeclaration的節(jié)點的處理。

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

        這里的value指的是什么意思呢?其實就是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);

        可見,value指的就是import后面的 './add' 和 './minus'

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

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

        oh my god。又成功了。

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

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

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

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

        const?fs?=?require('fs')
        const?path?=?require('path')
        const?parser?=?require('@babel/parser')
        const?traverse?=?require('@babel/traverse').default
        const?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)文檔對@babel/core 的transformFromAst的介紹

        害,又是一如既往的簡略。。。

        簡單說一下,其實就是將我們傳入的AST轉(zhuǎn)化成我們在第三個參數(shù)里配置的模塊類型。

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

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

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

        七、遞歸獲取所有依賴

        經(jīng)過上面的過程,現(xiàn)在我們知道getModuleInfo是用來獲取一個模塊的內(nèi)容,不過我們還沒把獲取的內(nèi)容return出來,因此,更改下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
        }

        我們返回了一個對象 ,這個對象包括「該模塊的路徑(file)」,「該模塊的依賴(deps)」,「該模塊轉(zhuǎn)化成es5的代碼」

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

        沒錯,看標題,,你應(yīng)該想到了就算遞歸。

        現(xiàn)在我們來寫一個遞歸方法,遞歸獲取依賴

        const?parseModules?=?(file)?=>{
        ????const?entry?=??getModuleInfo(file)
        ????const?temp?=?[entry]
        ????for?(let?i?=?0;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. 外面的循環(huán)遍歷temp數(shù)組,此時的temp數(shù)組只有主模塊
        4. 里面再獲得主模塊的依賴deps
        5. 遍歷deps,通過調(diào)用getModuleInfo將獲得的依賴模塊信息push到temp數(shù)組里。

        目前bundle.js文件:

        const?fs?=?require('fs')
        const?path?=?require('path')
        const?parser?=?require('@babel/parser')
        const?traverse?=?require('@babel/traverse').default
        const?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????????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")

        按照目前我們的項目來說執(zhí)行完,應(yīng)當是temp 應(yīng)當是存放了index.js,add.js,minus.js三個模塊。,執(zhí)行看看。

        牛逼!??!確實如此。

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

        const?parseModules?=?(file)?=>{
        ????const?entry?=??getModuleInfo(file)
        ????const?temp?=?[entry]?
        ????const?depsGraph?=?{}?//新增代碼
        ????for?(let?i?=?0;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)在存儲的就是這種格式啦

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

        我們現(xiàn)在的目的就是要生成一個bundle.js文件,也就是打包后的一個文件。其實思路很簡單,就是把index.js的內(nèi)容和它的依賴模塊整合起來。然后把代碼寫到一個新建的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這段代碼的,因為瀏覽器不會識別執(zhí)行require和exports。

        不能識別是為什么?不就是因為沒有定義這require函數(shù),和exports對象。那我們可以自己定義。

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

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

        我們將上一步獲得的depsGraph保存起來。

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

        怎么返回呢?更改下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)

        其實就是

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

        我們再來看下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);

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

        (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)

        實際上是實現(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í)行過程會執(zhí)行到require函數(shù)。
        3. 這時會調(diào)用這個require,也就是我們傳入的absRequire
        4. 而執(zhí)行absRequire就執(zhí)行了return require(graph[file].deps[relPath])這段代碼,也就是執(zhí)行了外面這個require

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

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

        是不是有點繞?其實是個遞歸。

        這樣就將代碼整合起來了,但是有個問題,就是在執(zhí)行add.js的code時候,會遇到exports這個還沒定義的問題。如下所示

        //?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當作一個對象來使用了,但是這個對象還沒定義,因此我們可以自己定義一個exports對象。

        (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)

        我們增添了一個空對象 exports,執(zhí)行add.js代碼的時候,會往這個空對象上增加一些屬性,

        //?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},
        ??defaultfunction?_default(a,?b)?{??return?a?+?b;}
        }

        然后我們把exports對象return出去。

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

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

        現(xiàn)在明白了,為什么ES6模塊 引入的是一個對象引用了吧,因為exports就是一個對象。

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

        const?fs?=?require('fs')
        const?path?=?require('path')
        const?parser?=?require('@babel/parser')
        const?traverse?=?require('@babel/traverse').default
        const?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????????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);

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

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

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

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

        我們參觀下生成的bundle.js文件

        發(fā)現(xiàn)其實就是將我們早期收集的所有依賴作為參數(shù)傳入到立即執(zhí)行函數(shù)當中,然后通過eval來遞歸地執(zhí)行每個依賴的code。

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

        成功。。。。。驚喜。。

        感謝您也恭喜您看到這里,我可以卑微的求個star嗎!?。?/p>

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

        ?

        作者:陽光是sunny

        原文地址:https://juejin.im/post/6854573217336541192

        ?
        • 分享前端好文,點亮?在看?

        瀏覽 90
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        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在线 | free性欧美69hd | 黃色一级A一片人与 | 婷婷五月丁香六月 | 精品国产AⅤ一区二区三区四川人 | 国精产品秘 成人一区二视频 | 韩国三级中文字幕 | 黄色一级片在线 | 男生操女生下面 | 深夜办公室老板揉我胸摸下边 |