91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

webpack打包原理及如何實現loader和plugin

發布時間:2020-07-28 15:35:51 來源:億速云 閱讀:357 作者:小豬 欄目:web開發

這篇文章主要講解了webpack打包原理及如何實現loader和plugin,內容清晰明了,對此有興趣的小伙伴可以學習一下,相信大家閱讀完之后會有幫助。

1. webpack打包基本原理

webpack的一個核心功能就是把我們寫的模塊化的代碼,打包之后,生成可以在瀏覽器中運行的代碼,我們這里也是從簡單開始,一步步探索webpack的打包原理

1.1 一個簡單的需求

我們首先建立一個空的項目,使用 npm init -y 快速初始化一個 package.json ,然后安裝 webpack webpack-cli

接下來,在根目錄下創建 src 目錄, src 目錄下創建 index.jsadd.jsminus.js ,根目錄下創建 index.html ,其中 index.html 引入 index.js ,在 index.js 引入 add.jsminus.js

目錄結構如下:

webpack打包原理及如何實現loader和plugin

文件內容如下:

// 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.js'
import { minus } from './minus.js'

const sum = add(1, 2)
const division = minus(2, 1)
console.log('sum>>>>>', sum)
console.log('division>>>>>', division)
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>demo</title>
</head>
<body>
 <script src="./src/index.js"></script>
</body>
</html>

這樣直接在 index.html 引入 index.js 的代碼,在瀏覽器中顯然是不能運行的,你會看到這樣的錯誤

Uncaught SyntaxError: Cannot use import statement outside a module

是的,我們不能在 script 引入的 js 文件里,使用 es6 模塊化語法

1.2 實現webpack打包核心功能

我們首先在項目根目錄下再建立一個bundle.js,這個文件用來對我們剛剛寫的模塊化 js 代碼文件進行打包

我們首先來看webpack官網對于其打包流程的描述:

it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack會在內部構建一個 依賴圖(dependency graph),此依賴圖會映射項目所需的每個模塊,并生成一個或多個 bundle)

在正式開始之前,結合上面 webpack 官網說明進行分析,明確我們進行打包工作的基本流程如下:

首先,我們需要讀到入口文件里的內容(也就是index.js的內容) 其次,分析入口文件,遞歸的去讀取模塊所依賴的文件內容,生成依賴圖 最后,根據依賴圖,生成瀏覽器能夠運行的最終代碼 1. 處理單個模塊(以入口為例) 1.1 獲取模塊內容

既然要讀取文件內容,我們需要用到 node.js 的核心模塊 fs ,我們首先來看讀到的內容是什么:

// bundle.js
const fs = require('fs')
const getModuleInfo = file => {
 const body = fs.readFileSync(file, 'utf-8')
 console.log(body)
}
getModuleInfo('./src/index.js')

我們定義了一個方法 getModuleInfo ,這個方法里我們讀出文件內容,打印出來,輸出的結果如下圖:

webpack打包原理及如何實現loader和plugin

我們可以看到,入口文件 index.js 的所有內容都以字符串形式輸出了,我們接下來可以用正則表達式或者其它一些方法,從中提取到 import 以及 export 的內容以及相應的路徑文件名,來對入口文件內容進行分析,獲取有用的信息。但是如果 importexport 的內容非常多,這會是一個很麻煩的過程,這里我們借助 babel

提供的功能,來完成入口文件的分析

1.2 分析模塊內容

我們安裝 @babel/parser ,演示時安裝的版本號為 ^7.9.6

這個babel模塊的作用,就是把我們js文件的代碼內容,轉換成js對象的形式,這種形式的js對象,稱做 抽象語法樹(Abstract Syntax Tree, 以下簡稱AST)

// 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, {
  // 表示我們要解析的是es6模塊
  sourceType: 'module' 
 })
 console.log(ast)
 console.log(ast.program.body)
}
getModuleInfo('./src/index.js')

使用 @babel/parserparse 方法把入口文件轉化稱為了 AST ,我們打印出了 ast ,注意文件內容是在 ast.program.body 中,如下圖所示:

webpack打包原理及如何實現loader和plugin

入口文件內容被放到一個數組中,總共有六個 Node 節點,我們可以看到,每個節點有一個 type 屬性,其中前兩個的 type 屬性是 ImportDeclaration ,這對應了我們入口文件的兩條 import 語句,并且,每一個 type 屬性是 ImportDeclaration 的節點,其 source.value

屬性是引入這個模塊的相對路徑,這樣我們就得到了入口文件中對打包有用的重要信息了。

接下來要對得到的ast做處理,返回一份結構化的數據,方便后續使用。

1.3 對模塊內容做處理

ast.program.body 部分數據的獲取和處理,本質上就是對這個數組的遍歷,在循環中做數據處理,這里同樣引入一個babel的模塊 @babel/traverse 來完成這項工作。

安裝 @babel/traverse ,演示時安裝的版本號為 ^7.9.6

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' 
 })
 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')

創建一個對象 deps ,用來收集模塊自身引入的依賴,使用 traverse 遍歷 ast ,我們只需要對 ImportDeclaration 的節點做處理,注意我們做的處理實際上就是把相對路徑轉化為絕對路徑,這里我使用的是 Mac 系統,如果是 windows 系統,注意斜杠的區別

獲取依賴之后,我們需要對 ast 做語法轉換,把 es6 的語法轉化為 es5 的語法,使用 babel 核心模塊 @babel/core 以及 @babel/preset-env 完成

安裝 @babel/core @babel/preset-env ,演示時安裝的版本號均為 ^7.9.6

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' 
 })
 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 }
 console.log(moduleInfo)
 return moduleInfo
}
getModuleInfo('./src/index.js')

如下圖所示,我們最終把一個模塊的代碼,轉化為一個對象形式的信息,這個對象包含文件的絕對路徑,文件所依賴模塊的信息,以及模塊內部經過 babel 轉化后的代碼

webpack打包原理及如何實現loader和plugin

2. 遞歸的獲取所有模塊的信息

這個過程,也就是獲取 依賴圖(dependency graph) 的過程,這個過程就是從入口模塊開始,對每個模塊以及模塊的依賴模塊都調用 getModuleInfo 方法就行分析,最終返回一個包含所有模塊信息的對象

const parseModules = file => {
 // 定義依賴圖
 const depsGraph = {}
 // 首先獲取入口的信息
 const entry = getModuleInfo(file)
 const temp = [entry]
 for (let i = 0; i < temp.length; i++) {
  const item = temp[i]
  const deps = item.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
}
parseModules('./src/index.js')

獲得的depsGraph對象如下圖:

webpack打包原理及如何實現loader和plugin

我們最終得到的模塊分析數據如上圖所示,接下來,我們就要根據這里獲得的模塊分析數據,來生產最終瀏覽器運行的代碼。

3. 生成最終代碼

在我們實現之前,觀察上一節最終得到的依賴圖,可以看到,最終的code里包含exports以及require這樣的語法,所以,我們在生成最終代碼時,要對exports和require做一定的實現和處理

我們首先調用之前說的parseModules方法,獲得整個應用的依賴圖對象:

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

接下來我們應該把依賴圖對象中的內容,轉換成能夠執行的代碼,以字符串形式輸出。 我們把整個代碼放在自執行函數中,參數是依賴圖對象

const bundle = file => {
 const depsGraph = JSON.stringify(parseModules(file))
 return `(function(graph){
  function require(file) {
   var exports = {};
   return exports
  }
  require('${file}')
 })(${depsGraph})`
}

接下來內容其實很簡單,就是我們取得入口文件的code信息,去執行它就好了,使用eval函數執行,初步寫出代碼如下:

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

上面的寫法是有問題的,我們需要對file做絕對路徑轉化,否則 graph[file].code 是獲取不到的,定義adsRequire方法做相對路徑轉化為絕對路徑

const bundle = file => {
 const depsGraph = JSON.stringify(parseModules(file))
 return `(function(graph){
  function require(file) {
   var exports = {};
   function absRequire(relPath){
    return require(graph[file].deps[relPath])
   }
   (function(require, exports, code){
    eval(code)
   })(absRequire, exports, graph[file].code)
   return exports
  }
  require('${file}')
 })(${depsGraph})`
}

接下來,我們只需要執行bundle方法,然后把生成的內容寫入一個JavaScript文件即可

const content = bundle('./src/index.js')
// 寫入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)

最后,我們在index.html引入這個 ./dist/bundle.js 文件,我們可以看到控制臺正確輸出了我們想要的結果

webpack打包原理及如何實現loader和plugin

4. 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')
 console.log(body)
 const ast = parser.parse(body, {
  sourceType: 'module' 
 })
 // console.log(ast.program.body)
 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 depsGraph = {}
 // 首先獲取入口的信息
 const entry = getModuleInfo(file)
 const temp = [entry]
 for (let i = 0; i < temp.length; i++) {
  const item = temp[i]
  const deps = item.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
}


// 生成最終可以在瀏覽器運行的代碼
const bundle = file => {
 const depsGraph = JSON.stringify(parseModules(file))
 return `(function(graph){
  function require(file) {
   var exports = {};
   function absRequire(relPath){
    return require(graph[file].deps[relPath])
   }
   (function(require, exports, code){
    eval(code)
   })(absRequire, exports, graph[file].code)
   return exports
  }
  require('${file}')
 })(${depsGraph})`
}


const build = file => {
 const content = bundle(file)
 // 寫入到dist/bundle.js
 fs.mkdirSync('./dist')
 fs.writeFileSync('./dist/bundle.js', content)
}

build('./src/index.js')

2. 手寫 loader 和 plugin

2.1 如何自己實現一個 loader

loader本質上就是一個函數,這個函數會在我們在我們加載一些文件時執行

2.1.1 如何實現一個同步 loader

首先我們初始化一個項目,項目結構如圖所示:

webpack打包原理及如何實現loader和plugin

其中index.js和webpack.config.js的文件內容如下:

// index.js
console.log('我要學好前端,因為學好前端可以: ')

// webpack.config.js
const path = require('path')
module.exports = {
 mode: 'development',
 entry: {
  main: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].js'
 }
}

我們在根目錄下創建 syncLoader.js ,用來實現一個同步的loader,注意這個函數必須返回一個 buffer 或者 string

// syncloader.ja
module.exports = function (source) {
 console.log('source>>>>', source)
 return source
}

同時,我們在 webpack.config.js 中使用這個 loader ,我們這里使用 resolveLoader 配置項,指定 loader 查找文件路徑,這樣我們使用 loader 時候可以直接指定 loader 的名字

const path = require('path')
module.exports = {
 mode: 'development',
 entry: {
  main: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].js'
 },
 resolveLoader: {
  // loader路徑查找順序從左往右
  modules: ['node_modules', './']
 },
 module: {
  rules: [
   {
    test: /\.js$/,
    use: 'syncLoader'
   }
  ]
 }
}

接下來我們運行打包命令,可以看到命令行輸出了source內容,也就是loader作用文件的內容。

webpack打包原理及如何實現loader和plugin

接著我們改造我們的loader:

module.exports = function (source) {
 source += '升值加薪'
 return source
}

我們再次運行打包命令,去觀察打包后的代碼:

webpack打包原理及如何實現loader和plugin

這樣,我們就實現了一個簡單的loader,為我們的文件增加一條信息。 我們可以嘗試在 loader 的函數里打印 this ,發現輸出結果是非常長的一串內容, this 上有很多我們可以在 loader 中使用的有用信息,所以,對于 loader 的編寫,一定不要使用箭頭函數,那樣會改變 this

的指向。

一般來說,我們會去使用官方推薦的 loader-utils 包去完成更加復雜的 loader 的編寫

我們繼續安裝 loader-utils ,版本是 ^2.0.0

我們首先改造 webpack.config.js

const path = require('path')

module.exports = {
 mode: 'development',
 entry: {
  main: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].js'
 },
 resolveLoader: {
  // loader路徑查找順序從左往右
  modules: ['node_modules', './']
 },
 module: {
  rules: [
   {
    test: /\.js$/,
    use: {
     loader: 'syncLoader',
     options: {
      message: '升值加薪'
     }
    }
   }
  ]
 }
}

注意到,我們為我們的 loader 增加了 options 配置項,接下來在loader函數里使用loader-utils獲取配置項內容,拼接內容,我們依然可以得到與之前一樣的打包結果

// syncLoader.js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
 const options = loaderUtils.getOptions(this)
 console.log(options)
 source += options.message
 // 可以傳遞更詳細的信息
 this.callback(null, source)
}

webpack打包原理及如何實現loader和plugin

webpack打包原理及如何實現loader和plugin

這樣,我們就完成了一個簡單的同步 loader 的編寫

2.1.2 如何實現一個異步 loader

和同步loader的編寫方式非常相似,我們在根目錄下建立一個asyncLoader.js的文件,內容如下:

const loaderUtils = require('loader-utils')
module.exports = function (source) {
 const options = loaderUtils.getOptions(this)
 const asyncfunc = this.async()
 setTimeout(() => {
  source += '走上人生顛覆'
  asyncfunc(null, res)
 }, 200)
}

注意這里的 this.async() ,用官方的話來說就是 Tells the loader-runner that the loader intends to call back asynchronously. Returns this.callback. 也就是讓webpack知道這個loader是異步運行,返回的是和同步使用時一致的 this.callback

接下來我們修改webpack.config.js

const path = require('path')
module.exports = {
 mode: 'development',
 entry: {
  main: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].js'
 },
 resolveLoader: {
  // loader路徑查找順序從左往右
  modules: ['node_modules', './']
 },
 module: {
  rules: [
   {
    test: /\.js$/,
    use: [
     {
      loader: 'syncLoader',
      options: {
       message: '走上人生巔峰'
      }
     },
     {
      loader: 'asyncLoader'
     }
    ]
   }
  ]
 }
}

注意loader執行順序是從下網上的,所以首先為文本寫入‘升值加薪',然后寫入‘走上人生巔峰'

webpack打包原理及如何實現loader和plugin

到此,我們簡單介紹了如何手寫一個 loader ,在實際項目中,可以考慮一部分公共的簡單邏輯,可以通過編寫一個 loader 來完成(比如國際化文本替換)

2.2 如何自己實現一個 plugin

plugin 通常是在 webpack 在打包的某個時間節點做一些操作,我們使用 plugin 的時候,一般都是 new Plugin() 這種形式使用,所以,首先應該明確的是, plugin 應該是一個類。

我們初始化一個與上一接實現loader時候一樣的項目,根目錄下創建一個 demo-webpack-plugin.js 的文件,我們首先在 webpack.config.js 中使用它

const path = require('path')
const DemoWebpackPlugin = require('./plugins/demo-webpack-plugin')
module.exports = {
 mode: 'development',
 entry: {
  main: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].js'
 },
 plugins: [
  new DemoWebpackPlugin()
 ]
}

再來看 demo-webpack-plugin.js 的實現

class DemoWebpackPlugin {
 constructor () {
  console.log('plugin init')
 }
 apply (compiler) {

 }
}

module.exports = DemoWebpackPlugin

我們在 DemoWebpackPlugin 的構造函數打印一條信息,當我們執行打包命令時,這條信息就會輸出, plugin 類里面需要實現一個 apply 方法, webpack 打包時候,會調用 pluginaplly 方法來執行 plugin 的邏輯,這個方法接受一個 compiler 作為參數,這個 compilerwebpack 實例

plugin的核心在于,apply方法執行時,可以操作webpack本次打包的各個時間節點(hooks,也就是生命周期勾子),在不同的時間節點做一些操作

關于webpack編譯過程的各個生命周期勾子,可以參考 Compiler Hooks

同樣,這些hooks也有同步和異步之分,下面演示 compiler hooks 的寫法,一些重點內容可以參考注釋:

class DemoWebpackPlugin {
 constructor () {
  console.log('plugin init')
 }
 // compiler是webpack實例
 apply (compiler) {
  // 一個新的編譯(compilation)創建之后(同步)
  // compilation代表每一次執行打包,獨立的編譯
  compiler.hooks.compile.tap('DemoWebpackPlugin', compilation => {
   console.log(compilation)
  })
  // 生成資源到 output 目錄之前(異步)
  compiler.hooks.emit.tapAsync('DemoWebpackPlugin', (compilation, fn) => {
   console.log(compilation)
   compilation.assets['index.md'] = {
    // 文件內容
    source: function () {
     return 'this is a demo for plugin'
    },
    // 文件尺寸
    size: function () {
     return 25
    }
   }
   fn()
  })
 }
}

module.exports = DemoWebpackPlugin

我們的這個 plugin 的作用就是,打包時候自動生成一個 md 文檔,文檔內容是很簡單的一句話

上述異步hooks的寫法也可以是以下兩種:

// 第二種寫法(promise)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', (compilation) => {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   resolve()
  }, 1000)
 }).then(() => {
  console.log(compilation.assets)
  compilation.assets['index.md'] = {
   // 文件內容
   source: function () {
    return 'this is a demo for plugin'
   },
   // 文件尺寸
   size: function () {
    return 25
   }
  }
 })
})
// 第三種寫法(async await)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', async (compilation) => {
 await new Promise((resolve, reject) => {
  setTimeout(() => {
   resolve()
  }, 1000)
 })
 console.log(compilation.assets)
 compilation.assets['index.md'] = {
  // 文件內容
  source: function () {
   return 'this is a demo for plugin'
  },
  // 文件尺寸
  size: function () {
   return 25
  }
 }
})

最終的輸出結果都是一樣的,在每次打包時候生成一個md文檔

webpack打包原理及如何實現loader和plugin

看完上述內容,是不是對webpack打包原理及如何實現loader和plugin有進一步的了解,如果還想學習更多內容,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

丰宁| 开江县| 广丰县| 黎平县| 张家界市| 乌鲁木齐市| 大兴区| 高密市| 桓仁| 盐源县| 陇西县| 滦南县| 合作市| 玉山县| 阜康市| 衡阳市| 定襄县| 绵阳市| 江阴市| 阿克苏市| 乌兰浩特市| 恭城| 星子县| 许昌市| 荥阳市| 新田县| 姚安县| 海丰县| 瑞安市| 常州市| 尉犁县| 子洲县| 宁远县| 通州区| 苍溪县| 于都县| 通渭县| 湛江市| 克拉玛依市| 彝良县| 敖汉旗|