<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Webpack】1080- Webpack入門到精通(AST、Babel、依賴)

          共 10717字,需瀏覽 22分鐘

           ·

          2021-09-14 13:43

          ?

          讀者投稿:作者:一咻,原文地址:https://juejin.cn/post/6975885302493609991?share_token=68a0b777-70c1-4021-a894-3ed9f8c107e9

          ?

          babel與AST

          初始化項目

          mkdir webpack-study
          cd webpack-study
          yarn init -y

          此時在package.json里面加入以下依賴

          {
            "name""01",
            "version""1.0.0",
            "main""index.js",
            "license""MIT",
            "dependencies": {
              "@babel/core""7.12.3",
              "@babel/generator""7.12.5",
              "@babel/parser""7.12.5",
              "@babel/preset-env""7.12.1",
              "@babel/traverse""7.12.5",
              "ts-node""9.0.0",
              "typescript""4.0.5"
            },
            "devDependencies": {
              "@types/babel__core""7.1.12",
              "@types/babel__generator""7.6.2",
              "@types/babel__parser""7.1.1",
              "@types/babel__preset-env""7.9.1",
              "@types/babel__traverse""7.0.15",
              "@types/node""14.14.6"
            }
          }

          babel以及項目依賴

          對使用到的包進行說明 詳細內容請參考: 理解babel的基本原理和使用方法

          • @babel/core Babel 是一個 JavaScript 編譯器, Babel 是一個工具鏈,主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環(huán)境中。下面列出的是 Babel 能為你做的事情:
          1. 語法轉換
          2. 通過 Polyfill 方式在目標環(huán)境中添加缺失的特性(通過第三方 polyfill 模塊,例如 core-js,實現)
          3. 源碼轉換 (codemods)
          • @babel/generator

            這個過程已經在上面的實例中有所展現,使用的插件是@babel/generator,其作用就是將轉換好的ast重新生成代碼。這樣的代碼就就可以安全的在瀏覽器運行。

          • @babel/parser

            在babel中編譯器插件是@babel/parser,其作用就是將源碼轉換為AST,

          • @babel/preset-env (預設(preset)——babel的插件套裝)

            那么問題來了新語法新特性那么多,難道我們要挨個去加嗎?當然不是,babel已經預設了幾套插件,將最新的語法進行轉換,可以使用在不同的環(huán)境中,如下:

          @babel/preset-env
          @babel/preset-flow
          @babel/preset-react
          @babel/preset-typescript

          從名字上就能看出他們使用的環(huán)境了,需要注意的是env,他的作用是將最新js轉換為es6代碼。預設是babel插件的組合,我們可以看下package.json(截取一部分):

          image.png
          • @babel/traverse

          ast進行遍歷parse

          • ts-node 使用.d.ts文件 既然要開發(fā)一個項目,顯然不會只有這些代碼。肯定要用到內建模塊和第三方模塊。然而,直接導入模塊,在.ts文件中是不行的。例如:

          這是由于typescript自身的機制,需要一份xx.d.ts聲明文件,來說明模塊對外公開的方法和屬性的類型以及內容。感覺有一些麻煩。好在,官方以及社區(qū)已經準備好了方案,來解決這個問題。

          在TypeScript 2.0以上的版本,獲取類型聲明文件只需要使用npm。在項目目錄下執(zhí)行安裝:

          npm install --save-dev @types/node

          就可以獲得有關node.js v6.x的API的類型說明文件。之后,就可以順利的導入需要的模塊了:

          import * as http from 'http';

          小試牛刀

          我們安裝好依賴之后,編寫以下代碼

          touch var_to_let.ts
          import { parse } from '@babel/parser'
          import traverse from '@babel/traverse'
          import generate from '@babel/generator'

          //將一段代碼(字符串)轉換成 AST

          let code = `let a = 'str'; let b = 2`

          const ast = parse(code, { sourceType: 'module' })

          console.log(ast)

          在vscode里面打上斷點

          接著我們在終端運行

          就可以進行斷點調試了,我們可以看到parse()函數把字符串的代碼轉換后的結果

          image.png

          我們在這個ast樹形結構里面找到以下幾個屬性,不難發(fā)現ats就是把一個字符串代碼,表示成一個樹形結構。

          image.png

          把let變成 var

          traverse(ast, {
            //遍歷每一個節(jié)點都會進入的回調函數。
            enteritem => {
              if(item.node.type === 'VariableDeclaration') {
                if(item.node.kind === 'let') {
                  item.node.kind = 'var'
                }
              }
            }
          })

          const result = generate(ast, {}, code)
          console.log(result)

          看下面的結果,我們修改了ats對象里面的屬性的值,最終通過generate函數生成了一個新的字符串代碼片段。成功的將原始代碼里面的let轉化成了var。嗯?Es6轉Es5就這么簡單?我們繼續(xù)

          為什么用AST?

          1. 很難用正則表達式來替換,正則表達式很容易把let a = 'let',替換成var a = 'var'
          2. 在修改的時候需要知道每一個單詞的意思,才能做到只修改用于變量聲明let
          3. 而AST能明確的告訴你每個let的意思

          把代碼轉化為ES5

          //使用@babel/core 和 @babel/preset-env把代碼自動轉化成ES5

          import { parse } from '@babel/parser'
          import * as babel from '@babel/core'

          let code = `let a = 'str'; let b = 2; const c = 100`

          //把字符串轉成ast
          const ast = parse(code, { sourceType'module' })

          //把ats變成字符串
          const result = babel.transformFromAstSync(ast, code, {
            presets: ['@babel/preset-env']
          })

          console.log(result)

          得到以下結果,可以看到constlet都被轉化成了ES5的代碼了。下面我們接著寫,我們把code字符串代碼放在文件里面,把生成的結果寫入到另一個.es5.js結尾的文件中。

          //使用@babel/core 和 @babel/preset-env把代碼自動轉化成ES5

          import { parse } from '@babel/parser'
          import * as babel from '@babel/core'
          import * as fs from 'fs'

          //從文件中讀取源代碼,并轉成字符串
          let code = fs.readFileSync('./test.js').toString()

          //把字符串轉成ast
          const ast = parse(code, { sourceType'module' })

          //把ats變成字符串
          const result = babel.transformFromAstSync(ast, code, {
            presets: ['@babel/preset-env']
          })

          //把生成好的字符串寫入文件里面
          let fileName = 'test.es5.js'
          fs.writeFileSync(fileName, result.code)

          通過以上代碼我們就將一個源文件是ES6js代碼轉換成了ES6的代碼

          image.png

          分析index.js的依賴

          在當前目錄下新建project-01目錄,新建三個文件a.js,b.js,index.js分別寫下以下內容

          a.js

          var a = {
            value100
          }

          export default a

          b.js

          var b = {
            value : 100
          }

          export default b

          index.js

          import a from './a'
          import b from './b'

          var sum = a.value + b.value 

          console.log(sum)

          因為當前我們的環(huán)境是node環(huán)境,為了在node里面讓import生效,我們使用以下方法。

          全局安裝 babel-cli

          npm install babel-cli -g

          安裝 babel-preset-env

          npm install babel-preset-env -D

          然后原來是 node server.js,改為這樣調用:babel-node --presets env server.js

          ?

          需要注意的是如果只是為了 babel-node 這一個命令,安裝 babel-cli 會加載安裝很多資源和模塊,出于性能考慮不推薦在生產環(huán)境使用。自己在開發(fā)調試的時候,可以鼓搗著玩玩

          ?

          下面我們在命令行執(zhí)行以下操作,便可以看到結果。呀是不是有點跑偏了的感覺,我們是來分析index.js文件的依賴項的呀,趕緊回到正題。

          image.png

          在項目下新建deps.ts文件,在文章最后面我會把完整的代碼放上來,一段一段貼代碼,太浪費空間了。

          最終我們得到了想要的結果。

          image.png

          遞歸分析嵌套的依賴

          下面我們再加一點難度,假如我們的a.js又依賴了其他的文件呢?b.js也同樣依賴了其他文件呢?我們又該如何獲取到其內部文件依賴的依賴呢?我們繼續(xù)

           cp -r project-01/ project-02
          cd project-02

          mkdir lib

          a.js

          import a1 from "./lib/a1.js"

          var a = {
            value'a'
          }

          export default a

          b.js

          js
          import b1 from "./lib/b1.js"

          var b = {
            value : 100
          }

          export default b

          在之前的a.jsb.js里面分別把這兩個文件import進去, 這樣就有更深層次的依賴關系了,我們下面只需要在遍歷AST語法樹的時候,當發(fā)現這個節(jié)點是ImportDeclaration的時候,再獲取這個節(jié)點的值,組裝一下真實的文件路徑,再遞歸調用把組裝好的路徑傳入collectCodeAndDeps便可以繼續(xù)分析了。

          什么是循環(huán)依賴?

          index.js

          import a from './a.js'
          import b from './b.js'

          console.log(a)
          let sum = a.value + b.value

          console.log(sum)

          a.js

          import b from './b.js'

          var a = {
            value: b.value + 1
          }

          export default a

          b.js

          js
          import a from './a.js'

          var b = {
            value: a.value + 1
          }

          export default b

          我在node版本為v16.3.0下面測試得到的結果。

          image.png

          如果我們把上面value的值換成一個常量的話,就可以正常執(zhí)行完代碼了。

          靜態(tài)分析循環(huán)依賴

          在遍歷AST的時候如果發(fā)現在之前的記錄里面已經有了,就不再進行遍歷了。

          //分析index.js里面代碼依賴的文件
          import { resolve, relative, dirname } from 'path'
          import { readFileSync } from 'fs'

          import { parse } from '@babel/parser'
          import traverse from '@babel/traverse';


          //設置項目根目錄


          const projectRoot = resolve(__dirname, 'project-02')

          //聲明最終結果的類型
          var result = {
            'index.js': {
              deps: ['a.js''b.js'],
              code"import a from './a'\r\nimport b from './b'\r\n\r\nvar sum = a.value + b.value \r\n\r\nconsole.log(sum)"
            }
          }

          type DepRelation = {
            [key: string]: {
              deps: string[],
              code: string
            }
          }

          interface a {

          }
          //初始化

          const depRelation: DepRelation = {}

          function collectCodeAndDeps(filepath: string{
            let key = getProjectPath(filepath)
            if (Object.keys(depRelation).includes(key)) {
              // 注意,重復依賴不一定是循環(huán)依賴
              return
            }
            //先讀取index文件的內容
            //把字符串代碼轉換成ats
            let code = readFileSync(resolve(filepath)).toString()

            //把入口文件的文件名當做map的key
            depRelation[key] = {
              deps: [],
              code
            }

            let ast = parse(code, {
              sourceType'module'
            })

            //遍歷ast

            traverse(ast, {
              enterpath => {
                //如果發(fā)現當前語句是 import 就把inport的value 寫入到依賴中去
                if (path.node.type === 'ImportDeclaration') {
                  //當前文件的上一級目錄 與獲取到當前文件的依賴文件進行拼接。
                  let depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
                  //獲取當前文件與根目錄的相對路徑
                  const depProjectPath = getProjectPath(depAbsolutePath)
                  // 把依賴寫進 depRelation
                  depRelation[key].deps.push(depProjectPath)
                  //拿到依賴文件的真實路徑進行再一次依賴分析
                  collectCodeAndDeps(depAbsolutePath)
                }
              }
            })
          }

          collectCodeAndDeps(resolve(projectRoot, 'index.js'))

          console.log(depRelation)

          //獲取文件相對跟目錄的相對路徑
          /*
          C: \\Users\\code\\zf\\webpack\\01\\project - 01
          C: \\Users\\code\\zf\\webpack\\01\\project - 01\\index.js

          //得到的結果就是index.js
          */

          function getProjectPath(path: string{
            return relative(projectRoot, path).replace(/\\/g'/')
          }

          總結

          AST相關

          1. parse:把代碼轉換成AST
          2. traverse:遍歷AST,并在需要的時候可以進行修改
          3. generate:把AST再轉換成代碼code2

          工具相關

          1. babel 可以把高級代碼轉換成ES5代碼
          2. @babel/parser
          3. @babel/traverse
          4. @babel/generate
          5. @babel/core
          6. @babel-preset-env 獲取您指定的任何目標環(huán)境并根據其映射檢查它們以編譯插件列表并將其傳遞給 Babel

          代碼技巧

          用哈希表來存儲映射關系 通過檢查哈希表的key來檢測重復

          循環(huán)依賴

          有的循環(huán)依賴可以正常執(zhí)行 有的循環(huán)依賴不可以正常執(zhí)行 但是兩者都可以進行靜態(tài)分析

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設計模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 120+篇原創(chuàng)系列匯總

          回復“加群”與大佬們一起交流學習~

          點擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  草骚逼视频 | 欧美伦理一区二区三区 | 日本大香蕉伊人网 | 久久九九99 | 天天射天天插天天舔天天日天天操天天爽 |