1. 程式人生 > 其它 >資料分析基本概念

資料分析基本概念

webpack

#可以做的事情

程式碼轉換、檔案優化、程式碼分割、模組合併、自動重新整理、程式碼校驗、自動釋出

#配套視訊

#最終目的

  • webpack的基本配置
  • webpack的高階配置
  • webpack的優化策略
  • ast抽象語法樹
  • webpackTapable
  • 掌握webpack的流程 手寫webpack
  • 手寫webpack中常見的loader
  • 手寫webpack中常見的plugin

#1. 安裝webpack

  • webpack:提供了內建的東西 express plugin
  • webpack-cli: npx webpack
  • 服務:webpack-dev-server:啟動服務 proxy beforeapp
    • 不會真正的打包檔案, 只會在記憶體中打包 執行命令npx webpack-dev-server

#2.配置檔案

let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {//webpack 是node中的一個模組 CommonJs
  devServer: {//靜態伺服器的配置
    port: 3000,
    progress: true,//進度提哦啊
    contentBase: "./dist",//靜態資源路徑
    compress:true//是否壓縮Gzip
  },
  mode: "production",//環境
  entry: "./src/index.js",
  output: {
    filename: "bundle[hash:8].js",//設定hash之後會解決瀏覽器快取問題
    path: path.resolve(__dirname, "dist")//解析 會把相對路徑解析成絕對路徑
  },
  plugins: [
    new HtmlWebpackPlugin({//打包的時候 自動把html打包到dist目錄
      template: "./src/index.html",
      filename: "index.html",
      minify:{
        removeAttributeQuotes:true,//去除雙引號
        collapseWhitespace:true//單行壓縮
      },
      hash:true//是否加hash字尾
    })
  ]
};

  • 思考1: 如何壓縮html檔案
  • 思考2: 如何實現命名的hash串
 plugins:[
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true
      },
      hash: true
    })
  ]

#2.1 修改樣式

#2.2.1 loader配置

如果直接插入css

檔案會報一個這樣的錯誤

解決: 下載兩個loader

  module: {//模組
    rules: [//規則
      {
        test: /\.css$/,
        use: [{
          loader: 'style-loader',//將css插入到head中
          options: {
            insert: 'top'//head/top foot
          }
        }, 'css-loader']
      },
      {
        test: /\.scss$/,
        use: ['style-loader','css-loader', 'sass-loader']
      }
    ],
  },

#2.1.1 分離css

但是 此時 我們打包後發現css是插入在js裡面的

為了解決這個問題 接下來我們引入mini-css-extract-plugin這個外掛

let MiniCssExtractPlugin require('mini-css-extract-plugin')

 rules: [
      {
        test: /\.css$/,
        use: [{
          loader: MiniCssExtractPlugin.loader,
        }, 'css-loader']//loader順序的規律
      },
      {
        test: /\.(sc|sa)ss$/,
        use: [{
          loader: MiniCssExtractPlugin.loader,
        }, 'css-loader', 'sass-loader']//loader順序的規律
      }
    ]

當我們加入css3之後 新的問題出現了 沒有字首

#2.1.3 引入字首

此時 我們需要下載一個包autoprefixer以及一個loader檔案postcss-loader

{
        test: /\.css$/,
        use: [{
          loader: MiniCssExtractPlugin.loader,
        }, 'css-loader','postcss-loader']//loader順序的規律
      },
  1. 建立一個配置檔案postcss.config.js
module.exports = {
  plugins: [require('autoprefixer')]
};

再次打包

需要注意的是 此設定項只能用早生產環境

mode: 'production',

#2.1.4 壓縮css檔案

如何壓縮檔案呢

其中有個包optimize-css-assets-webpack-plugin

此包主要是用來壓縮css的 但是 引入這個包後出現了js沒被壓縮的問題

怎麼解決呢

按照官網配置需要使用TerserJSPlugin

https://www.npmjs.com/package/mini-css-extract-plugin

optimization: {//webpack4.0之後新出的優化項配置
  minimizer: [new TerserJSPlugin({}), new OptimizeCssAssetsPlugin({})]
},

TerserJSPlugin具體引數檢視這個

interface TerserPluginOptions {
        test?: string | RegExp | Array<string | RegExp>;
        include?: string | RegExp | Array<string | RegExp>;
        exclude?: string | RegExp | Array<string | RegExp>;
        chunkFilter?: (chunk: webpack.compilation.Chunk) => boolean;
        cache?: boolean | string;
        cacheKeys?: (defaultCacheKeys: any, file: any) => object;
        parallel?: boolean | number;
        sourceMap?: boolean;
        minify?: (file: any, sourceMap: any) => MinifyResult;
        terserOptions?: MinifyOptions;
        extractComments?: boolean
        | string
        | RegExp
        | ExtractCommentFn
        | ExtractCommentOptions;
        warningsFilter?: (warning: any, source: any) => boolean;
    }

#2.2 處理js檔案

#2.2.1 babel核心模組

當我們嘗試對寫了es6語法的程式碼進行打包時候

並沒有變成es5

接下來執行命令babel

yarn add babel-loader @babel/core @babel/preset-env

  • babel-loader:babel載入器
  • @babel/core:babel的核心模組
  • @babel/preset-env: 將es6轉換成es5
  • @babel/plugin-transform-runtime
  • @babel/runtime
  • @babel/polyfill
{
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {//預設
                  presets: ['@babel/preset-env']
            }
          }
        ]
 }

接下來 就是見證奇蹟的時刻

#2.2.2 處理箭頭函式

@babel/preset-env

#2.2.3 處理裝飾器

當我們新增裝飾器 會有如下提示

具體可以檢視官網 https://babeljs.io/docs/en/babel-plugin-proposal-decorators

 {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {//預設
              presets: ['@babel/preset-env'],
              plugins:[
                ["@babel/plugin-proposal-decorators", { "legacy": true }],
                ["@babel/plugin-proposal-class-properties", { "loose" : true }]
              ]
            }
          }
        ]
      },

index.js

@log
class A {
  a = 1;//es7 的語法(es6的變種語法)  // let a = new A() a.a = 1
}
function log(target) {
  console.log(target,'21');
}

#2.2.4 處理es7語法

{
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {//預設
              presets: ['@babel/preset-env'],
              plugins:['@babel/plugin-proposal-class-properties']
            }
          }
        ]
      }

a.js

class B {

}

function* fnB() {
  yield 1;
}

console.log(fnB().next());

module.exports = 'a';

接下來打包發現 每個檔案都會打包一個_classCallCheck

寫了generator執行也會報錯

出現以上問題的原因是

  1. webpack執行時不會自動檢測哪些方法重用了

    1. 一些es6的高階語法 比如generator和promise不會轉換成es5

根據官方文件https://babeljs.io/docs/en/babel-plugin-transform-runtime#docsNav

需要下載兩個包

yarn add @babel/plugin-transform-runtime @babel/runtime -D

執行npx webpack但是 報了一些警告

 {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {//預設
              presets: ['@babel/preset-env'],
              plugins: [
                ["@babel/plugin-proposal-decorators", {"legacy": true}],
                ["@babel/plugin-proposal-class-properties", {"loose": true}],
                "@babel/plugin-transform-runtime"
              ]
            }
          }
        ],
        include: path.resolve(__dirname, 'src'),
        exclude: /node_modules/

      },

#2.2.5 處理全域性變數的問題

方法一 : 外接loader

require('expose-loader?$!jquery');
1

方法二 : 內建loader在每個模組都注入$

// rules:

{//內建loader
        test: require.resolve('jquery'),
        use: 'expose-loader?$'
      },
          
// plugins:

    //提供者
    new webpack.ProvidePlugin({
      "$": "jquery"
    })

優化:

如果在html引入cdn路徑並且在頁面也import $ from jquery這就壞了, 即使引入cdn也會打包

 //排除之外  加入 在cdn引入了這個包 就不會打包這個包
  externals: {
    'jquery': '$
  }

#2.3 處理圖片檔案

#2.3.1 處理js中的圖片

index.js

import logo from './logo.png';
<img src=logo/>
    
    
webpack.config.js:
{
    test: /\.(png|jpg|gif)$/,
      use: [{
        loader: 'file-loader',
        options: {
        	esModule: false, 
      },
    }
}

#2.3.2 處理css中圖片檔案

因為css-loader中已經對圖片做loader處理了 所以 只需要引入相應路徑就行了

#2.3.3 處理html中的圖片

//1. 下載依賴
yarn add html-withimg-plugin -D

//2. 配置
{
    test:/\.html$/,
    use:['html-withimg-plugin']
    
}

#2.4 多入口多出口

#2.5 webpack小外掛

  • clean-webpack-plugin
let {CleanWebpackPlugin} = require('clean-webpack-plugin');

//使用:
plugins:[
	new CleanWebpackPlugin()
]
  • copy-webpack-plugin
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  plugins: [
    new CopyPlugin([
      { from: 'source', to: 'dest' },
      { from: 'other', to: 'public' },
    ]),
  ],
};

#2.6 resolve、分離

#2.6.1 resolve

resolve:{
  modules:[path.resolve(__dirname,'node_modules')],//只從當前這個node_modules查詢相應的包
  alise:{//別名
  "bootstrapcss":"bootstrap/dist/css/bootstrap.css"
	},
	extensions:['js','jsx','vue','json','css']
}

#2.6.2 分離檔案 dev、 prod、base

let {smart} = require('webpack-merge')
let base = require('./webpack.config.js')

module.exports = smart(base,{
  mode:'production'
})

#2.7 分離打包檔案

#2.8 跨域

  • 方式一:在devServer中配置
devServer: {
  port: 8080,
  host: '0.0.0.0',
  quiet: true,
  proxy: {
    // '/api': 'http://127.0.0.1:3000',
    '/api': {
      target: 'http://127.0.0.1:3000',
      pathRewrite:{
        '^/api': ''
      }
    },

  },
  before(app) {
    //app就是express物件
    app.get('/list', function (req, res) {
      res.send({code: 1, msg: 'hello'});
    });
  }
},
  • 方式二 : 在服務端配置(node/express)
//1: npm i webpack-dev-middleware
let middleDevWebpack = require('webpack-dev-middleware')
let config = require('./webpack.config.js')

app.use(middleDevWebpack(config))

#2.9 懶載入和熱更新實時監聽

  • 熱更新

    devServer:{
      hot:true,
      quite:true//安靜啟動
    }
    
  • 實時監聽

    watch:true,
    wathcOptions:{
      poll:1000,
        aggregateTimeout:500,
        ignore:/note_modules/
    }
    

#3. webpack優化

打包優化,可以從幾個出發點點

  • 打包體積

  • 載入速度

  • 打包速度

  • webpack自帶優化

    • tree-sharking : import 把沒用的程式碼自動刪除掉
    • scope-hoisting : 作用域提升
  • 優化網路解析時長和執行時長

    • 新增DNS預解析
    • 延時執行影響頁面渲染的程式碼
  • 優化webpack產出

    • 優化程式碼重複打包
    • 去掉不必要的import
    • babel-preset-env 和 autoprefix 配置優化
    • webpack runtime檔案inline
    • 去除不必要的async語句
    • 優化第三方依賴
    • lodash按需引入
  • webpack 知識點

    • hash、contenthash、chunkhash的區別
    • splitChunks詳解
  • 必殺技--動態連結庫

  • 多程序打包之HappyPack

  • 提取公共程式碼

#3.1 webpack自帶優化

  • tree-sharking
  • scope-hoisting

#3.2 多執行緒打包

需要用到happypack實現多執行緒打包

注意: 如果體積較小會使打包時間更長

#第一步:下載

npm install happypack --save-dev
1
const HappyPack = require('happypack');

module.exports = {
    ...
}

#第二步: 將常用的loader替換為happypack/loader

const HappyPack = require('happypack');

module.exports = {
    ...
    module: {
        rules: [
            test: /\.js$/,
            // use: ['babel-loader?cacheDirectory'] 之前是使用這種方式直接使用 loader
            // 現在用下面的方式替換成 happypack/loader,並使用 id 指定建立的 HappyPack 外掛
            use: ['happypack/loader?id=babel'],
            // 排除 node_modules 目錄下的檔案
            exclude: /node_modules/
        ]
    }
}

#三、建立 HappyPack 外掛

module.exports = {
    ...
    module: {
        rules: [
            test: /\.js$/,
            // use: ['babel-loader?cacheDirectory'] 之前是使用這種方式直接使用 loader
            // 現在用下面的方式替換成 happypack/loader,並使用 id 指定建立的 HappyPack 外掛
            use: ['happypack/loader?id=babel'],
            // 排除 node_modules 目錄下的檔案
            exclude: /node_modules/
        ]
    },
    plugins: [
        ...,
        new HappyPack({
            /*
             * 必須配置
             */
            // id 識別符號,要和 rules 中指定的 id 對應起來
            id: 'babel',
            // 需要使用的 loader,用法和 rules 中 Loader 配置一樣
            // 可以直接是字串,也可以是物件形式
            loaders: ['babel-loader?cacheDirectory']
        })
    ]
}

#3.3 關於語言包的打包

有些包自帶語言包,有時候不需要把所有的語言包跟著打包比如moment,那麼我們就需要把這個包特殊對待,

主要是通過webpack自導的IgnorePlugin

src下某.js

import moment from 'moment';
import 'moment/locale/zh-cn';

moment.locale('zh-cn');

let r = moment().endOf('day').fromNow();
console.log(r);

webpack.config.js

plugins: [
  ...
	new webpack.IgnorePlugin(/\.\/locale/,/moment/),
]

#3.3 不打包某個檔案

有些檔案我們不希望打包,比如已經在cdn中引入了的檔案,此時要用externals進行配置

modules:{
  noParse:/jquery/,
 	...
}

plugins: [
  ...
	new webpack.ProvidePlugin({
      '$': 'jquery'
  }),
] 

//忽略打包的檔案
externals:{
  'jquery': '$'
}

#3.4 關於css字首的處理

#3.5 關於js新語法的處理

#3.6 關於檔案拆分的處理

#3.7 關於別名和副檔名的處理

#3.8 webpack必殺技 : 動態連結庫

  • 什麼是動態連結庫: 用dll連結的方式提取固定的js檔案,並連結這個js檔案

    當我們引入一個js檔案的時候,這個js檔案比較大,那我們是否可以單獨打包,釋出到cdn上,直接引用

  • 比如 當我們想要把react打包的時候,希望將react和reactdom放到一個js檔案打包的時候 不打包這兩個檔案,而是直接引用js的cdn路徑

新建一個webpack的js配置檔案

webpack.react.js

var path = require('path');
let webpack = require("webpack");

module.exports = {
  mode: 'development',
  entry: {
    react: ['react', 'react-dom']
  },
  output:{
    filename: '_dll_[name].js',
    path: path.resolve(__dirname, 'dist'),
    library: '_dll_[name]',
    //  "var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
    // libraryTarget: 'commonjs2'//預設 var
  },
  plugins: [
    new webpack.DllPlugin({
      name: '_dll_[name]',
      path: path.resolve(__dirname, 'dist', 'manifest.json')
    })

  ]
};
npx webpack --config webpack.react.js
1

此時就會生成一個manifest.json檔案

最後 在webpack.prod.config.js線上配置檔案中引入外掛

    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, 'dist', 'manifest.json')
    })

#3.9 抽離公共程式碼塊

optimization: {//webpack4.0之後出現的優化項
    minimizer: [new TerserPlugin({}), new OptimizeCssAssetsWebpackPlugin({})],//壓縮css
    //缺陷 可以壓縮css 但是 js壓縮又出現了問題

    splitChunks:{//分割程式碼塊
      cacheGroups:{//快取組
        common:{//公共的邏輯
          chunks: 'initial',//從入口檔案開始查詢
          minSize: 0,//最小分包體積
          minChunks: 2,//
        },
        vendor:{
          priority: 1,
          test:/node_modules/,
          chunks: 'initial',
          minSize: 0,
          minChunks: 2
        }
      }
    }
  },

#4. webpack打包原理

#webpack 構建流程

Webpack 的執行流程是一個序列的過程,從啟動到結束會依次執行以下流程 :

  1. 初始化引數:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數。
  2. 開始編譯:用上一步得到的引數初始化 Compiler 物件,載入所有配置的外掛,執行物件的 run 方法開始執行編譯。
  3. 確定入口:根據配置中的 entry 找出所有的入口檔案。
  4. 編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理。
  5. 完成模組編譯:在經過第 4 步使用 Loader 翻譯完所有模組後,得到了每個模組被翻譯後的最終內容以及它們之間的依賴關係。
  6. 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再把每個 Chunk 轉換成一個單獨的檔案加入到輸出列表,這步是可以修改輸出內容的最後機會。
  7. 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,把檔案內容寫入到檔案系統。

在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫 Webpack 提供的 API 改變 Webpack 的執行結果。

#實踐加深理解,擼一個簡易 webpack

#1. 定義 Compiler 類

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模組
    this.modules = []
  }
  // 構建啟動
  run() {}
  // 重寫 require函式,輸出bundle
  generate() {}
}

#2. 解析入口檔案,獲取 AST

我們這裡使用@babel/parser,這是 babel7 的工具,來幫助我們分析內部的語法,包括 es6,返回一個 AST 抽象語法樹。

// webpack.config.js

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  }
}
//

const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
  getAst: path => {
    // 讀取入口檔案
    const content = fs.readFileSync(path, 'utf-8')
    // 將檔案內容轉為AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模組
    this.modules = []
  }
  // 構建啟動
  run() {
    const ast = Parser.getAst(this.entry)
  }
  // 重寫 require函式,輸出bundle
  generate() {}
}

new Compiler(options).run()

#3. 找出所有依賴模組

Babel 提供了@babel/traverse(遍歷)方法維護這 AST 樹的整體狀態,我們這裡使用它來幫我們找出依賴模組。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
  getAst: path => {
    // 讀取入口檔案
    const content = fs.readFileSync(path, 'utf-8')
    // 將檔案內容轉為AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模組,存入dependecies
    traverse(ast, {
      // 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 儲存依賴模組路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模組
    this.modules = []
  }
  // 構建啟動
  run() {
    const { getAst, getDependecies } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
  }
  // 重寫 require函式,輸出bundle
  generate() {}
}

new Compiler(options).run()

#4. AST 轉換為 code

將 AST 語法樹轉換為瀏覽器可執行程式碼,我們這裡使用@babel/core 和 @babel/preset-env。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 讀取入口檔案
    const content = fs.readFileSync(path, 'utf-8')
    // 將檔案內容轉為AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模組,存入dependecies
    traverse(ast, {
      // 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 儲存依賴模組路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST轉換為code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模組
    this.modules = []
  }
  // 構建啟動
  run() {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
    const code = getCode(ast)
  }
  // 重寫 require函式,輸出bundle
  generate() {}
}

new Compiler(options).run()

#5. 遞迴解析所有依賴項,生成依賴關係圖

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 讀取入口檔案
    const content = fs.readFileSync(path, 'utf-8')
    // 將檔案內容轉為AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模組,存入dependecies
    traverse(ast, {
      // 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 儲存依賴模組路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST轉換為code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模組
    this.modules = []
  }
  // 構建啟動
  run() {
    // 解析入口檔案
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判斷有依賴物件,遞迴解析所有依賴項
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依賴關係圖
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用檔案路徑作為每個模組的唯一識別符號,儲存對應模組的依賴物件和檔案內容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 檔案路徑,可以作為每個模組的唯一識別符號
      filename,
      // 依賴物件,儲存著依賴模組路徑
      dependecies,
      // 檔案內容
      code
    }
  }
  // 重寫 require函式,輸出bundle
  generate() {}
}

new Compiler(options).run()

#6. 重寫 require 函式,輸出 bundle

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 讀取入口檔案
    const content = fs.readFileSync(path, 'utf-8')
    // 將檔案內容轉為AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模組,存入dependecies
    traverse(ast, {
      // 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 儲存依賴模組路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST轉換為code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模組
    this.modules = []
  }
  // 構建啟動
  run() {
    // 解析入口檔案
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判斷有依賴物件,遞迴解析所有依賴項
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依賴關係圖
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用檔案路徑作為每個模組的唯一識別符號,儲存對應模組的依賴物件和檔案內容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
    this.generate(dependencyGraph)
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 檔案路徑,可以作為每個模組的唯一識別符號
      filename,
      // 依賴物件,儲存著依賴模組路徑
      dependecies,
      // 檔案內容
      code
    }
  }
  // 重寫 require函式 (瀏覽器不能識別commonjs語法),輸出bundle
  generate(code) {
    // 輸出檔案路徑
    const filePath = path.join(this.output.path, this.output.filename)
    // 懵逼了嗎? 沒事,下一節我們捋一捋
    const bundle = `(function(graph){
      function require(module){
        function localRequire(relativePath){
          return require(graph[module].dependecies[relativePath])
        }
        var exports = {};
        (function(require,exports,code){
          eval(code)
        })(localRequire,exports,graph[module].code);
        return exports;
      }
      require('${this.entry}')
    })(${JSON.stringify(code)})`

    // 把檔案內容寫入到檔案系統
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}

new Compiler(options).run()

#7. 看完這節,徹底搞懂 bundle 實現

我們通過下面的例子來進行講解,先死亡凝視 30 秒

;(function(graph) {
  function require(moduleId) {
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath])
    }
    var exports = {}
    ;(function(require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    return exports
  }
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

#step 1 : 從入口檔案開始執行

// 定義一個立即執行函式,傳入生成的依賴關係圖
;(function(graph) {
  // 重寫require函式
  function require(moduleId) {
    console.log(moduleId) // ./src/index.js
  }
  // 從入口檔案開始執行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

#step 2 : 使用 eval 執行程式碼

// 定義一個立即執行函式,傳入生成的依賴關係圖
;(function(graph) {
  // 重寫require函式
  function require(moduleId) {
    ;(function(code) {
      console.log(code) // "use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));
      eval(code) // Uncaught TypeError: Cannot read property 'code' of undefined
    })(graph[moduleId].code)
  }
  // 從入口檔案開始執行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

可以看到,我們在執行"./src/index.js"檔案程式碼的時候報錯了,這是因為 index.js 裡引用依賴 hello.js,而我們沒有對依賴進行處理,接下來我們對依賴引用進行處理。

#step 3 : 依賴物件定址對映,獲取 exports 物件

// 定義一個立即執行函式,傳入生成的依賴關係圖
;(function(graph) {
  // 重寫require函式
  function require(moduleId) {
    // 找到對應moduleId的依賴物件,呼叫require函式,eval執行,拿到exports物件
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
    }
    // 定義exports物件
    var exports = {}
    ;(function(require, exports, code) {
      // commonjs語法使用module.exports暴露實現,我們傳入的exports物件會捕獲依賴物件(hello.js)暴露的實現(exports.say = say)並寫入
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    // 暴露exports物件,即暴露依賴物件對應的實現
    return exports
  }
  // 從入口檔案開始執行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

這下應該明白了吧 ~ 可以直接複製上面程式碼到控制檯輸出哦~

完整程式碼地址戳我