成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

24 個(gè)實(shí)例入門并掌握「Webpack4」(三)

mindwind / 1946人閱讀

摘要:繼個(gè)實(shí)例入門并掌握二后續(xù)配置配置配置使用加快打包速度多頁面打包配置編寫編寫編寫十七配置源碼地址本節(jié)使用的代碼為基礎(chǔ)我們來模擬平時(shí)開發(fā)中,將打包完的代碼防止到服務(wù)器上的操作,首先打包代碼然后安裝一個(gè)插件在中配置一個(gè)命令運(yùn)

繼 24 個(gè)實(shí)例入門并掌握「Webpack4」(二) 后續(xù):

PWA 配置

TypeScript 配置

Eslint 配置

使用 DLLPlugin 加快打包速度

多頁面打包配置

編寫 loader

編寫 plugin

編寫 Bundle

十七、PWA 配置

demo17 源碼地址

本節(jié)使用 demo15 的代碼為基礎(chǔ)

我們來模擬平時(shí)開發(fā)中,將打包完的代碼防止到服務(wù)器上的操作,首先打包代碼 npm run build

然后安裝一個(gè)插件 npm i http-server -D

在 package.json 中配置一個(gè) script 命令

{
  "scripts": {
    "start": "http-server dist",
    "dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
    "build": "webpack --config ./build/webpack.prod.conf.js"
  }
}

運(yùn)行 npm run start

現(xiàn)在就起了一個(gè)服務(wù),端口是 8080,現(xiàn)在訪問 http://127.0.0.1:8080 就能看到效果了

如果你有在跑別的項(xiàng)目,端口也是 8080,端口就沖突,記得先關(guān)閉其他項(xiàng)目的 8080 端口,再 npm run start

我們按 ctrl + c 關(guān)閉 http-server 來模擬服務(wù)器掛了的場景,再訪問 http://127.0.0.1:8080 就會(huì)是這樣

頁面訪問不到了,因?yàn)槲覀兎?wù)器掛了,PWA 是什么技術(shù)呢,它可以在你第一次訪問成功的時(shí)候,做一個(gè)緩存,當(dāng)服務(wù)器掛了之后,你依然能夠訪問這個(gè)網(wǎng)頁

首先安裝一個(gè)插件:workbox-webpack-plugin

npm i workbox-webpack-plugin -D

只有要上線的代碼,才需要做 PWA 的處理,打開 webpack.prod.conf.js

const WorkboxPlugin = require("workbox-webpack-plugin") // 引入 PWA 插件

const prodConfig = {
  plugins: [
    // 配置 PWA
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}

重新打包,在 dist 目錄下會(huì)多出 service-worker.jsprecache-manifest.js 兩個(gè)文件,通過這兩個(gè)文件就能使我們的網(wǎng)頁支持 PWA 技術(shù),service-worker.js 可以理解為另類的緩存

還需要去業(yè)務(wù)代碼中使用 service-worker

在 app.js 中加上以下代碼

// 判斷該瀏覽器支不支持 serviceWorker
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker
      .register("/service-worker.js")
      .then(registration => {
        console.log("service-worker registed")
      })
      .catch(error => {
        console.log("service-worker registed error")
      })
  })
}

重新打包,然后運(yùn)行 npm run start 來模擬服務(wù)器上的操作,最好用無痕模式打開 http://127.0.0.1:8080 ,打開控制臺(tái)

現(xiàn)在文件已經(jīng)被緩存住了,再按 ctrl + c 關(guān)閉服務(wù),再次刷新頁面也還是能顯示的

TypeScript配置

demo18 源碼地址

TypeScript 是 JavaScript 類型的超集,它可以編譯成純 JavaScript

新建文件夾,npm init -ynpm i webpack webpack-cli -D,新建 src 目錄,創(chuàng)建 index.ts 文件,這段代碼在瀏覽器上是運(yùn)行不了的,需要我們打包編譯,轉(zhuǎn)成 js

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return "Hello, " + this.greeting
  }
}

let greeter = new Greeter("world")

alert(greeter.greet())
npm i ts-loader typescript -D

新建 webpack.config.js 并配置

const path = require("path")

module.exports = {
  mode: "production",
  entry: "./src/index.ts",
  module: {
    rules: [
      {
        test: /.ts?$/,
        use: "ts-loader",
        exclude: /node_modules/
      }
    ]
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist")
  }
}

在 package.json 中配置 script

{
  "scripts": {
    "build": "webpack"
  }
}

運(yùn)行 npm ruh build,報(bào)錯(cuò)了,缺少 tsconfig.json 文件

當(dāng)打包 typescript 文件的時(shí)候,需要在項(xiàng)目的根目錄下創(chuàng)建一個(gè) tsconfig.json 文件

以下為簡單配置,更多詳情看官網(wǎng)

{
  "compileerOptions": {
    "outDir": "./dist", // 寫不寫都行
    "module": "es6", // 用 es6 模塊引入 import
    "target": "es5", // 打包成 es5
    "allowJs": true // 允許在 ts 中也能引入 js 的文件
  }
}

再次打包,打開 bundle.js 文件,將代碼全部拷貝到瀏覽器控制臺(tái)上,使用這段代碼,可以看到彈窗出現(xiàn) Hello,world,說明 ts 編譯打包成功

引入第三方庫
npm i lodash
import _ from "lodash"

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return _.join()
  }
}

let greeter = new Greeter("world")

alert(greeter.greet())

lodash 的 join 方法需要我們傳遞參數(shù),但是現(xiàn)在我們什么都沒傳,也沒有報(bào)錯(cuò),我們使用 typescript 就是為了類型檢查,在引入第三方庫的時(shí)候也能如此,可是現(xiàn)在缺并沒有報(bào)錯(cuò)或者提示

我們還要安裝一個(gè) lodash 的 typescript 插件,這樣就能識(shí)別 lodash 方法中的參數(shù),一旦使用的不對就會(huì)報(bào)錯(cuò)出來

npm i @types/lodash -D

安裝完以后可以發(fā)現(xiàn)下劃線 _ 報(bào)錯(cuò)了

需要改成 import * as _ from "lodash",將 join 方法傳遞的參數(shù)刪除,還可以發(fā)現(xiàn) join 方法的報(bào)錯(cuò),這就體現(xiàn)了 typescript 的優(yōu)勢,同理,引入 jQuery 也要引入一個(gè) jQuery 對應(yīng)的類型插件

如何知道使用的庫需要安裝對應(yīng)的類型插件呢?

打開TypeSearch,在這里對應(yīng)的去搜索你想用的庫有沒有類型插件,如果有只需要 npm i @types/jquery -D 即可

十九、Eslint 配置

demo19 源碼地址

創(chuàng)建一個(gè)空文件夾,npm init -y,npm webpack webpack-cli -D 起手式,之后安裝 eslint 依賴

npm i eslint -D

使用 npx 運(yùn)行此項(xiàng)目中的 eslint 來初始化配置,npx eslint --init

這里會(huì)有選擇是 React/Vue/JavaScript,我們統(tǒng)一都先選擇 JavaScript。選完后會(huì)在項(xiàng)目的根目錄下新建一個(gè) .eslintrc.js 配置文件

module.exports = {
  env: {
    browser: true,
    es6: true
  },
  extends: "eslint:recommended",
  globals: {
    Atomics: "readonly",
    SharedArrayBuffer: "readonly"
  },
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: "module"
  },
  rules: {}
}

里面就是 eslint 的一些規(guī)范,也可以定義一些規(guī)則,具體看 eslint 配置規(guī)則

在 index.js 中隨便寫點(diǎn)代碼來測試一下 eslint

eslint 報(bào)錯(cuò)提示,變量定義后卻沒有使用,如果在編輯器里沒出現(xiàn)報(bào)錯(cuò)提示,需要在 vscode 里先安裝一個(gè) eslint 擴(kuò)展,它會(huì)根據(jù)你當(dāng)前目錄的下的 .eslintrc.js 文件來做作為校驗(yàn)的規(guī)則

也可以通過命令行的形式,讓 eslint 校驗(yàn)整個(gè) src 目錄下的文件

如果你覺得某個(gè)規(guī)則很麻煩,想屏蔽掉某個(gè)規(guī)則的時(shí)候,可以這樣,根據(jù) eslint 的報(bào)錯(cuò)提示,比如上面的 no-unused-vars,將這條規(guī)則復(fù)制一下,在 .eslintrc.js 中的 rules 里配置一下,"no-unused-vars": 0,0 表示禁用,保存后,就不會(huì)報(bào)錯(cuò)了,但是這種方式是適用于全局的配置,如果你只想在某一行代碼上屏蔽掉 eslint 校驗(yàn),可以這樣做

/* eslint-disable no-unused-vars */
let a = "1"

這個(gè) eslint 的 vscode 擴(kuò)展和 webpack 是沒有什么關(guān)聯(lián)的,我們現(xiàn)在要講的是如何在 webpack 里使用 eslint,首先安裝一個(gè)插件

npm i eslint-loader -D

在 webpack.config.js 中進(jìn)行配置

/* eslint-disable no-undef */
// eslint-disable-next-line no-undef
const path = require("path")

module.exports = {
  mode: "production",
  entry: {
    app: "./src/index.js" // 需要打包的文件入口
  },
  module: {
    rules: [
      {
        test: /.js$/, // 使用正則來匹配 js 文件
        exclude: /nodes_modules/, // 排除依賴包文件夾
        use: {
          loader: "eslint-loader" // 使用 eslint-loader
        }
      }
    ]
  },
  output: {
    // eslint-disable-next-line no-undef
    publicPath: __dirname + "/dist/", // js 引用的路徑或者 CDN 地址
    // eslint-disable-next-line no-undef
    path: path.resolve(__dirname, "dist"), // 打包文件的輸出目錄
    filename: "bundle.js" // 打包后生產(chǎn)的 js 文件
  }
}

由于 webpack 配置文件也會(huì)被 eslint 校驗(yàn),這里我先寫上注釋,關(guān)閉校驗(yàn)

如果你有使用 babel-loader 來轉(zhuǎn)譯,則 loader 應(yīng)該這么寫

loader: ["babel-loader", "eslint-loader"]

rules 的執(zhí)行順序是從右往左,從下往上的,先經(jīng)過 eslint 校驗(yàn)判斷代碼是否符合規(guī)范,然后再通過 babel 來做轉(zhuǎn)移

配置完 webpack.config.js,我們將 index.js 還原回之前報(bào)錯(cuò)的狀態(tài),不要使用注釋關(guān)閉校驗(yàn),然后運(yùn)行打包命令,記得去 package.json 配置 script

會(huì)在打包的時(shí)候,提示代碼不合格,不僅僅是生產(chǎn)環(huán)境,開發(fā)環(huán)境也可以配置,可以將 eslint-loader 配置到 webpack 的公共模塊中,這樣更有利于我們檢查代碼規(guī)范

如:設(shè)置 fix 為 true,它會(huì)幫你自動(dòng)修復(fù)一些錯(cuò)誤,不能自動(dòng)修復(fù)的,還是需要你自己手動(dòng)修復(fù)

{
 loader: "eslint-loader", // 使用 eslint-loader
  options: {
    fix: true
  }
}

關(guān)于 eslint-loader,webpack 的官網(wǎng)也給出了配置,感興趣的朋友自己去看一看

二十、使用 DLLPlugin 加快打包速度

demo20 源碼地址

本節(jié)使用 demo15 的代碼為基礎(chǔ)

我們先安裝一個(gè) lodash 插件 npm i lodash,并在 app.js 文件中寫入

import _ from "lodash"
console.log(_.join(["hello", "world"], "-"))

在 build 文件夾下新建 webpack.dll.js 文件

const path = require("path")

module.exports = {
  mode: "production",
  entry: {
    vendors: ["lodash", "jquery"]
  },
  output: {
    filename: "[name].dll.js",
    path: path.resolve(__dirname, "../dll"),
    library: "[name]"
  }
}

這里使用 library,忘記的朋友可以回顧一下第十六節(jié),自定義函數(shù)庫里的內(nèi)容,定義了 library 就相當(dāng)于掛載了這個(gè)全局變量,只要在控制臺(tái)輸入全局變量的名稱就可以顯示里面的內(nèi)容,比如這里我們是 library: "[name]" 對應(yīng)的 name 就是我們在 entry 里定義的 vendors

在 package.json 中的 script 再新增一個(gè)命令

{
  "scripts": {
    "dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
    "build": "webpack --config ./build/webpack.prod.conf.js",
    "build:dll": "webpack --config ./build/webpack.dll.js"
  }
}

運(yùn)行 npm run build:dll,會(huì)生成 dll 文件夾,并且文件為 vendors.dll.js

打開文件可以發(fā)現(xiàn) lodash 已經(jīng)被打包到了 dll 文件中

那我們要如何使用這個(gè) vendors.dll.js 文件呢

需要再安裝一個(gè)依賴 npm i add-asset-html-webpack-plugin,它會(huì)將我們打包后的 dll.js 文件注入到我們生成的 index.html 中

在 webpack.base.conf.js 文件中引入

const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin")

module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, "../dll/vendors.dll.js") // 對應(yīng)的 dll 文件路徑
    })
  ]
}

使用 npm run dev 來打開網(wǎng)頁

現(xiàn)在我們已經(jīng)把第三方模塊多帶帶打包成了 dll 文件,并使用

但是現(xiàn)在使用第三方模塊的時(shí)候,要用 dll 文件,而不是使用 /node_modules/ 中的庫,繼續(xù)來修改 webpack.dll.js 配置

const path = require("path")
const webpack = require("webpack")

module.exports = {
  mode: "production",
  entry: {
    vendors: ["lodash", "jquery"]
  },
  output: {
    filename: "[name].dll.js",
    path: path.resolve(__dirname, "../dll"),
    library: "[name]"
  },
  plugins: [
    new webpack.DllPlugin({
      name: "[name]",
      // 用這個(gè)插件來分析打包后的這個(gè)庫,把庫里的第三方映射關(guān)系放在了這個(gè) json 的文件下,這個(gè)文件在 dll 目錄下
      path: path.resolve(__dirname, "../dll/[name].manifest.json")
    })
  ]
}

保存后重新打包 dll,npm run build:dll

修改 webpack.base.conf.js 文件,添加 webpack.DllReferencePlugin 插件

module.exports = {
  plugins: [
    // 引入我們打包后的映射文件
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, "../dll/vendors.manifest.json")
    })
  ]
}

之后再 webpack 打包的時(shí)候,就可以結(jié)合之前的全局變量 vendors 和 這個(gè)新生成的 vendors.manifest.json 映射文件,然后來對我們的源代碼進(jìn)行分析,一旦分析出使用第三方庫是在 vendors.dll.js 里,就會(huì)去使用 vendors.dll.js,不會(huì)去使用 /node_modules/ 里的第三方庫了

再次打包 npm run build,可以把 webpack.DllReferencePlugin 模塊注釋后再打包對比一下

注釋前 4000ms 左右,注釋后 4300ms 左右,雖然只是快了 300ms,但是我們目前只是實(shí)驗(yàn)性的 demo,實(shí)際項(xiàng)目中,比如拿 vue 來說,vue,vue-router,vuex,element-ui,axios 等第三方庫都可以打包到 dll.js 里,那個(gè)時(shí)候的打包速度就能提升很多了

還可以繼續(xù)拆分,修改 webpack.dll.js 文件

const path = require("path")
const webpack = require("webpack")

module.exports = {
  mode: "production",
  entry: {
    lodash: ["lodash"],
    jquery: ["jquery"]
  },
  output: {
    filename: "[name].dll.js",
    path: path.resolve(__dirname, "../dll"),
    library: "[name]"
  },
  plugins: [
    new webpack.DllPlugin({
      name: "[name]",
      path: path.resolve(__dirname, "../dll/[name].manifest.json") // 用這個(gè)插件來分析打包后的這個(gè)庫,把庫里的第三方映射關(guān)系放在了這個(gè) json 的文件下,這個(gè)文件在 dll 目錄下
    })
  ]
}

運(yùn)行 npm run build:dll

可以把之前打包的 vendors.dll.jsvendors.manifest.json 映射文件給刪除掉

然后再修改 webpack.base.conf.js

module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, "../dll/lodash.dll.js")
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, "../dll/jquery.dll.js")
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, "../dll/lodash.manifest.json")
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, "../dll/jquery.manifest.json")
    })
  ]
}

保存后運(yùn)行 npm run dev,看看能不能成功運(yùn)行

這還只是拆分了兩個(gè)第三方模塊,就要一個(gè)個(gè)配置過去,有沒有什么辦法能簡便一點(diǎn)呢? 有!

這里使用 node 的 api,fs 模塊來讀取文件夾里的內(nèi)容,創(chuàng)建一個(gè) plugins 數(shù)組用來存放公共的插件

const fs = require("fs")

const plugins = [
  // 開發(fā)環(huán)境和生產(chǎn)環(huán)境二者均需要的插件
  new HtmlWebpackPlugin({
    title: "webpack4 實(shí)戰(zhàn)",
    filename: "index.html",
    template: path.resolve(__dirname, "..", "index.html"),
    minify: {
      collapseWhitespace: true
    }
  }),
  new webpack.ProvidePlugin({ $: "jquery" })
]

const files = fs.readdirSync(path.resolve(__dirname, "../dll"))
console.log(files)

寫完可以先輸出一下,把 plugins 給注釋掉,npm run build 打包看看輸出的內(nèi)容,可以看到文件夾中的內(nèi)容以數(shù)組的形式被打印出來了,之后我們對這個(gè)數(shù)組做一些循環(huán)操作就行了

完整代碼:

const path = require("path")
const fs = require("fs")
const webpack = require("webpack")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin")

// 存放公共插件
const plugins = [
  // 開發(fā)環(huán)境和生產(chǎn)環(huán)境二者均需要的插件
  new HtmlWebpackPlugin({
    title: "webpack4 實(shí)戰(zhàn)",
    filename: "index.html",
    template: path.resolve(__dirname, "..", "index.html"),
    minify: {
      collapseWhitespace: true
    }
  }),
  new webpack.ProvidePlugin({ $: "jquery" })
]

// 自動(dòng)引入 dll 中的文件
const files = fs.readdirSync(path.resolve(__dirname, "../dll"))
files.forEach(file => {
  if (/.*.dll.js/.test(file)) {
    plugins.push(
      new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, "../dll", file)
      })
    )
  }
  if (/.*.manifest.json/.test(file)) {
    plugins.push(
      new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, "../dll", file)
      })
    )
  }
})

module.exports = {
  entry: {
    app: "./src/app.js"
  },
  output: {
    path: path.resolve(__dirname, "..", "dist")
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader"
          }
        ]
      },
      {
        test: /.(png|jpg|jpeg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name]-[hash:5].min.[ext]",
              limit: 1000, // size <= 1KB
              outputPath: "images/"
            }
          },
          // img-loader for zip img
          {
            loader: "image-webpack-loader",
            options: {
              // 壓縮 jpg/jpeg 圖片
              mozjpeg: {
                progressive: true,
                quality: 65 // 壓縮率
              },
              // 壓縮 png 圖片
              pngquant: {
                quality: "65-90",
                speed: 4
              }
            }
          }
        ]
      },
      {
        test: /.(eot|ttf|svg)$/,
        use: {
          loader: "url-loader",
          options: {
            name: "[name]-[hash:5].min.[ext]",
            limit: 5000, // fonts file size <= 5KB, use "base64"; else, output svg file
            publicPath: "fonts/",
            outputPath: "fonts/"
          }
        }
      }
    ]
  },
  plugins,
  performance: false
}

使用 npm run dev 打開網(wǎng)頁也沒有問題了,這樣自動(dòng)注入 dll 文件也搞定了,之后還要再打包第三方庫只要添加到 webpack.dll.js 里面的 entry 屬性中就可以了

二十一、多頁面打包配置

demo21 源碼地址

本節(jié)使用 demo20 的代碼為基礎(chǔ)

在 src 目錄下新建 list.js 文件,里面寫 console.log("這里是 list 頁面")

在 webpack.base.conf.js 中配置 entry,配置兩個(gè)入口

module.exports = {
  entry: {
    app: "./src/app.js",
    list: "./src/list.js"
  }
}

如果現(xiàn)在我們直接 npm run build 打包,在打包自動(dòng)生成的 index.html 文件中會(huì)發(fā)現(xiàn) list.js 也被引入了,說明多入口打包成功,但并沒有實(shí)現(xiàn)多個(gè)頁面的打包,我想打包出 index.htmllist.html 兩個(gè)頁面,并且在 index.html 中引入 app.js,在 list.html 中引入 list.js,該怎么做?

為了方便演示,先將 webpack.prod.conf.jscacheGroups 新增一個(gè) default 屬性,自定義 name

optimization: {
  splitChunks: {
    chunks: "all",
    cacheGroups: {
      jquery: {
        name: "jquery", // 多帶帶將 jquery 拆包
        priority: 15,
        test: /[/]node_modules[/]jquery[/]/
      },
      vendors: {
        test: /[/]node_modules[/]/,
        name: "vendors"
      },
      default: {
        name: "code-segment"
      }
    }
  }
}

打開 webpack.base.conf.js 文件,將 HtmlWebpackPlugin 拷貝一份,使用 chunks 屬性,將需要打包的模塊對應(yīng)寫入

// 存放公共插件
const plugins = [
  new HtmlWebpackPlugin({
    title: "webpack4 實(shí)戰(zhàn)",
    filename: "index.html",
    template: path.resolve(__dirname, "..", "index.html"),
    chunks: ["app", "vendors", "code-segment", "jquery", "lodash"]
  }),
  new HtmlWebpackPlugin({
    title: "多頁面打包",
    filename: "list.html",
    template: path.resolve(__dirname, "..", "index.html"),
    chunks: ["list", "vendors", "code-segment", "jquery", "lodash"]
  }),
  new CleanWebpackPlugin(),
  new webpack.ProvidePlugin({ $: "jquery" })
]

打包后的 dist 目錄下生成了兩個(gè) html

打開 index.html 可以看到引入的是 app.js,而 list.html 引入的是 list.js,這就是 HtmlWebpackPlugin 插件的 chunks 屬性,自定義引入的 js

如果要打包三個(gè)頁面,再去 copy HtmlWebpackPlugin,通過在 entry 中配置,如果有四個(gè),五個(gè),這樣手動(dòng)的復(fù)制就比較麻煩了,可以寫個(gè)方法自動(dòng)生成 HtmlWebpackPlugin 配置

修改 webpack.base.conf.js

const path = require("path")
const fs = require("fs")
const webpack = require("webpack")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin")
const CleanWebpackPlugin = require("clean-webpack-plugin")

const makePlugins = configs => {
  // 基礎(chǔ)插件
  const plugins = [
    new CleanWebpackPlugin(),
    new webpack.ProvidePlugin({ $: "jquery" })
  ]

  // 根據(jù) entry 自動(dòng)生成 HtmlWebpackPlugin 配置,配置多頁面
  Object.keys(configs.entry).forEach(item => {
    plugins.push(
      new HtmlWebpackPlugin({
        title: "多頁面配置",
        template: path.resolve(__dirname, "..", "index.html"),
        filename: `${item}.html`,
        chunks: [item, "vendors", "code-segment", "jquery", "lodash"]
      })
    )
  })

  // 自動(dòng)引入 dll 中的文件
  const files = fs.readdirSync(path.resolve(__dirname, "../dll"))
  files.forEach(file => {
    if (/.*.dll.js/.test(file)) {
      plugins.push(
        new AddAssetHtmlWebpackPlugin({
          filepath: path.resolve(__dirname, "../dll", file)
        })
      )
    }
    if (/.*.manifest.json/.test(file)) {
      plugins.push(
        new webpack.DllReferencePlugin({
          manifest: path.resolve(__dirname, "../dll", file)
        })
      )
    }
  })

  return plugins
}

const configs = {
  entry: {
    index: "./src/app.js",
    list: "./src/list.js"
  },
  output: {
    path: path.resolve(__dirname, "..", "dist")
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader"
          }
        ]
      },
      {
        test: /.(png|jpg|jpeg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name]-[hash:5].min.[ext]",
              limit: 1000, // size <= 1KB
              outputPath: "images/"
            }
          },
          // img-loader for zip img
          {
            loader: "image-webpack-loader",
            options: {
              // 壓縮 jpg/jpeg 圖片
              mozjpeg: {
                progressive: true,
                quality: 65 // 壓縮率
              },
              // 壓縮 png 圖片
              pngquant: {
                quality: "65-90",
                speed: 4
              }
            }
          }
        ]
      },
      {
        test: /.(eot|ttf|svg)$/,
        use: {
          loader: "url-loader",
          options: {
            name: "[name]-[hash:5].min.[ext]",
            limit: 5000, // fonts file size <= 5KB, use "base64"; else, output svg file
            publicPath: "fonts/",
            outputPath: "fonts/"
          }
        }
      }
    ]
  },
  performance: false
}

makePlugins(configs)

configs.plugins = makePlugins(configs)

module.exports = configs

再次打包后效果相同,如果還要增加頁面,只要在 entry 中再引入一個(gè) js 文件作為入口即可

多頁面配置其實(shí)就是定義多個(gè) entry,配合 htmlWebpackPlugin 生成多個(gè) html 頁面
二十二、編寫 loader

demo22 源碼地址

新建文件夾,npm init -ynpm i webpack webpack-cli -D,新建 src/index.js,寫入 console.log("hello world")

新建 loaders/replaceLoader.js 文件

module.exports = function(source) {
  return source.replace("world", "loader")
}

source 參數(shù)就是我們的源代碼,這里是將源碼中的 world 替換成 loader

新建 webpack.config.js

const path = require("path")

module.exports = {
  mode: "development",
  entry: {
    main: "./src/index.js"
  },
  module: {
    rules: [
      {
        test: /.js/,
        use: [path.resolve(__dirname, "./loaders/replaceLoader.js")] // 引入自定義 loader
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js"
  }
}

目錄結(jié)構(gòu):

打包后打開 dist/main.js 文件,在最底部可以看到 world 已經(jīng)被改為了 loader,一個(gè)最簡單的 loader 就寫完了

添加 optiions 屬性

const path = require("path")

module.exports = {
  mode: "development",
  entry: {
    main: "./src/index.js"
  },
  module: {
    rules: [
      {
        test: /.js/,
        use: [
          {
            loader: path.resolve(__dirname, "./loaders/replaceLoader.js"),
            options: {
              name: "xh"
            }
          }
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js"
  }
}

修改 replaceLoader.js 文件,保存后打包,輸出看看效果

module.exports = function(source) {
  console.log(this.query)
  return source.replace("world", this.query.name)
}

打包后生成的文件也改為了 options 中定義的 name

更多的配置見官網(wǎng) API,找到 Loader Interface,里面有個(gè) this.query

如果你的 options 不是一個(gè)對象,而是按字符串形式寫的話,可能會(huì)有一些問題,這里官方推薦使用 loader-utils 來獲取 options 中的內(nèi)容

安裝 npm i loader-utils -D,修改 replaceLoader.js

const loaderUtils = require("loader-utils")

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  console.log(options)
  return source.replace("world", options.name)
}

console.log(options)console.log(this.query) 輸出內(nèi)容一致

如果你想傳遞額外的信息出去,return 就不好用了,官網(wǎng)給我們提供了 this.callback API,用法如下

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
)

修改 replaceLoader.js

const loaderUtils = require("loader-utils")

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  const result = source.replace("world", options.name)

  this.callback(null, result)
}

目前沒有用到 sourceMap(必須是此模塊可解析的源映射)、meta(可以是任何內(nèi)容(例如一些元數(shù)據(jù))) 這兩個(gè)可選參數(shù),只將 result 返回回去,保存重新打包后,效果和 return 是一樣的

如果在 loader 中寫異步代碼,會(huì)怎么樣

const loaderUtils = require("loader-utils")

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)

  setTimeout(() => {
    const result = source.replace("world", options.name)
    return result
  }, 1000)
}

報(bào)錯(cuò) loader 沒有返回,這里使用 this.async 來寫異步代碼

const loaderUtils = require("loader-utils")

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)

  const callback = this.async()

  setTimeout(() => {
    const result = source.replace("world", options.name)
    callback(null, result)
  }, 1000)
}

模擬一個(gè)同步 loader 和一個(gè)異步 loader

新建一個(gè) replaceLoaderAsync.js 文件,將之前寫的異步代碼放入,修改 replaceLoader.js 為同步代碼

// replaceLoaderAsync.js

const loaderUtils = require("loader-utils")
module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace("world", options.name)
    callback(null, result)
  }, 1000)
}

// replaceLoader.js
module.exports = function(source) {
  return source.replace("xh", "world")
}

修改 webpack.config.js,loader 的執(zhí)行順序是從下到上,先執(zhí)行異步代碼,將 world 改為 xh,再執(zhí)行同步代碼,將 xh 改為 world

module: {
  rules: [
    {
      test: /.js/,
      use: [
        {
          loader: path.resolve(__dirname, "./loaders/replaceLoader.js")
        },
        {
          loader: path.resolve(__dirname, "./loaders/replaceLoaderAsync.js"),
          options: {
            name: "xh"
          }
        }
      ]
    }
  ]
}

保存后打包,在 mian.js 中可以看到已經(jīng)改為了 hello world,使用多個(gè) loader 也完成了

如果有多個(gè)自定義 loader,每次都通過 path.resolve(__dirname, xxx) 這種方式去寫,有沒有更好的方法?

使用 resolveLoader,定義 modules,當(dāng)你使用 loader 的時(shí)候,會(huì)先去 node_modules 中去找,如果沒找到就會(huì)去 ./loaders 中找

const path = require("path")

module.exports = {
  mode: "development",
  entry: {
    main: "./src/index.js"
  },
  resolveLoader: {
    modules: ["node_modules", "./loaders"]
  },
  module: {
    rules: [
      {
        test: /.js/,
        use: [
          {
            loader: "replaceLoader.js"
          },
          {
            loader: "replaceLoaderAsync.js",
            options: {
              name: "xh"
            }
          }
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js"
  }
}
二十三、編寫 plugin

demo23 源碼地址

首先新建一個(gè)文件夾,npm 起手式操作一番,具體的在前幾節(jié)已經(jīng)說了,不再贅述

在根目錄下新建 plugins 文件夾,新建 copyright-webpack-plugin.js,一般我們用的都是 xxx-webpack-plugin,所以我們命名也按這樣來,plugin 的定義是一個(gè)類

class CopyrightWebpackPlugin {
  constructor() {
    console.log("插件被使用了")
  }
  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin

在 webpack.config.js 中使用,所以每次使用 plugin 都要使用 new,因?yàn)楸举|(zhì)上 plugin 是一個(gè)類

const path = require("path")
const CopyrightWebpackPlugin = require("./plugins/copyright-webpack-plugin")

module.exports = {
  mode: "development",
  entry: {
    main: "./src/index.js"
  },
  plugins: [new CopyrightWebpackPlugin()],
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js"
  }
}

保存后打包,插件被使用了,只不過我們什么都沒干

如果我們要傳遞參數(shù),可以這樣

new CopyrightWebpackPlugin({
  name: "xh"
})

同時(shí)在 copyright-webpack-plugin.js 中接收

class CopyrightWebpackPlugin {
  constructor(options) {
    console.log("插件被使用了")
    console.log("options = ", options)
  }
  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin

我們先把 constructor 注釋掉,在即將要把打包的結(jié)果,放入 dist 目錄之前的這個(gè)時(shí)刻,我們來做一些操作

apply(compiler) {} compiler 可以看作是 webpack 的實(shí)例,具體見官網(wǎng) compiler-hooks

hooks 是鉤子,像 vue、react 的生命周期一樣,找到 emit 這個(gè)時(shí)刻,將打包結(jié)果放入 dist 目錄前執(zhí)行,這里是個(gè) AsyncSeriesHook 異步方法

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        console.log(11)
        cb()
      }
    )
  }
}

module.exports = CopyrightWebpackPlugin

因?yàn)?emit異步的,可以通過 tapAsync 來寫,當(dāng)要把代碼放入到 dist 目錄之前,就會(huì)觸發(fā)這個(gè)鉤子,走到我們定義的函數(shù)里,如果你用 tapAsync 函數(shù),記得最后要用 cb() ,tapAsync 要傳遞兩個(gè)參數(shù),第一個(gè)參數(shù)傳遞我們定義的插件名稱

保存后再次打包,我們寫的內(nèi)容也輸出了

compilation 這個(gè)參數(shù)里存放了這次打包的所有內(nèi)容,可以輸出一下 compilation.assets 看一下

返回結(jié)果是一個(gè)對象,main.js 是 key,也就是打包后生成的文件名及文件后綴,我們可以來仿照一下

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        // 生成一個(gè) copyright.txt 文件
        compilation.assets["copyright.txt"] = {
          source: function() {
            return "copyright by xh"
          },
          size: function() {
            return 15 // 上面 source 返回的字符長度
          }
        }
        console.log("compilation.assets = ", compilation.assets)
        cb()
      }
    )
  }
}

module.exports = CopyrightWebpackPlugin

在 dist 目錄下生成了 copyright.txt 文件

之前介紹的是異步鉤子,現(xiàn)在使用同步鉤子

class CopyrightWebpackPlugin {
  apply(compiler) {
    // 同步鉤子
    compiler.hooks.compile.tap("CopyrightWebpackPlugin", compilation => {
      console.log("compile")
    })

    // 異步鉤子
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        compilation.assets["copyright.txt"] = {
          source: function() {
            return "copyright by xh"
          },
          size: function() {
            return 15 // 字符長度
          }
        }
        console.log("compilation.assets = ", compilation.assets)
        cb()
      }
    )
  }
}

module.exports = CopyrightWebpackPlugin
二十四、編寫 Bundle

demo24 源碼地址

模塊分析

在 src 目錄下新建三個(gè)文件 word.js、message.js、index.js,對應(yīng)的代碼:

// word.js
export const word = "hello"

// message.js
import { word } from "./word.js"

const message = `say ${word}`

export default message

// index.js
import message from "./message.js"

console.log(message)

新建 bundle.js

const fs = require("fs")

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  console.log(content)
}

moduleAnalyser("./src/index.js")

使用 node 的 fs 模塊,讀取文件信息,并在控制臺(tái)輸出,這里全局安裝一個(gè)插件,來顯示代碼高亮,npm i cli-highlight -g,運(yùn)行 node bundle.js | highlight

index.js 中的代碼已經(jīng)被輸出到控制臺(tái)上,而且代碼有高亮,方便閱讀,讀取入口文件信息就完成了

現(xiàn)在我們要讀取 index.js 文件中使用的 message.js 依賴,import message from "./message.js"

安裝一個(gè)第三方插件 npm i @babel/parser

@babel/parser 是 Babel 中使用的 JavaScript 解析器。

官網(wǎng)也提供了相應(yīng)的示例代碼,根據(jù)示例代碼來仿照,修改我們的文件

const fs = require("fs")
const parser = require("@babel/parser")

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  console.log(
    parser.parse(content, {
      sourceType: "module"
    })
  )
}

moduleAnalyser("./src/index.js")

我們使用的是 es6 的 module 語法,所以 sourceType: "module"

保存后運(yùn)行,輸出了 AST (抽象語法樹),里面有一個(gè) body 字段,我們輸出這個(gè)字段

const fs = require("fs")
const parser = require("@babel/parser")

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  const ast = parser.parse(content, {
    sourceType: "module"
  })
  console.log(ast.program.body)
}

moduleAnalyser("./src/index.js")

打印出了兩個(gè) Node 節(jié)點(diǎn),第一個(gè)節(jié)點(diǎn)的 type 是 ImportDeclaration(引入的聲明),對照我們在 index.js 中寫的 import message from "./message.js",第二個(gè)節(jié)點(diǎn)的 type 是 ExpressionStatement (表達(dá)式的聲明),對照我們寫的 console.log(message)

使用 babel 來幫我們生成抽象語法樹,我們再導(dǎo)入 import message1 from "./message1.js" 再運(yùn)行

抽象語法樹將我們的 js 代碼轉(zhuǎn)成了對象的形式,現(xiàn)在就可以遍歷抽象語法樹生成的節(jié)點(diǎn)對象中的 type,是否為 ImportDeclaration,就能找到代碼中引入的依賴了

再借助一個(gè)工具 npm i @babel/traverse

const fs = require("fs")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  const ast = parser.parse(content, {
    sourceType: "module"
  })
  traverse(ast, {
    ImportDeclaration({ node }) {
      console.log(node)
    }
  })
}

moduleAnalyser("./src/index.js")

只打印了兩個(gè) ImportDeclaration,遍歷結(jié)束,我們只需要取到依賴的文件名,在打印的內(nèi)容中,每個(gè)節(jié)點(diǎn)都有個(gè) source 屬性,里面有個(gè) value 字段,表示的就是文件路徑及文件名

const fs = require("fs")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  const ast = parser.parse(content, {
    sourceType: "module"
  })
  const dependencise = []
  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencise.push(node.source.value)
    }
  })
  console.log(dependencise)
}

moduleAnalyser("./src/index.js")

保存完重新運(yùn)行,輸出結(jié)果:

["./message.js", "./message1.js"]

這樣就對入口文件的依賴分析就分析出來了,現(xiàn)在把 index.js 中引入的 message1.js 的依賴給刪除,這里有個(gè)注意點(diǎn),打印出來的文件路徑是相對路徑,相對于 src/index.js 文件,但是我們打包的時(shí)候不能是入口文件(index.js)的相對路徑,而應(yīng)該是根目錄的相對路徑(或者說是絕對路徑),借助 node 的 api,引入一個(gè) path

const fs = require("fs")
const path = require("path")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  const ast = parser.parse(content, {
    sourceType: "module"
  })
  const dependencise = []
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      console.log(dirname)
      dependencise.push(node.source.value)
    }
  })
  // console.log(dependencise)
}

moduleAnalyser("./src/index.js")

輸出為 ./src,繼續(xù)修改

ImportDeclaration({ node }) {
  const dirname = path.dirname(filename)
  const newFile = path.join(dirname, node.source.value)
  console.log(newFile)
  dependencise.push(node.source.value)
}

輸出為 srcmessage.js

windows 和 類 Unix(linux/mac),路徑是有區(qū)別的。windows 是用反斜杠  分割目錄或者文件的,而在類 Unix 的系統(tǒng)中是用的 /。

由于我是 windows 系統(tǒng),所以這里輸出為 srcmessage.js,而類 Unix 輸出的為 src/message.js

.srcmessage.js 這個(gè)路徑是我們真正打包時(shí)要用到的路徑

newFile .srcmessage.js
[ ".srcmessage.js" ]

既存一個(gè)相對路徑,又存一個(gè)絕對路徑

const fs = require("fs")
const path = require("path")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  const ast = parser.parse(content, {
    sourceType: "module"
  })
  const dependencise = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = "." + path.join(dirname, node.source.value)
      console.log("newFile", newFile)
      dependencise[node.source.value] = newFile
    }
  })
  console.log(dependencise)
  return {
    filename,
    dependencise
  }
}

moduleAnalyser("./src/index.js")
newFile .srcmessage.js
{ "./message.js": ".srcmessage.js" }

因?yàn)槲覀儗懙拇a是 es6,瀏覽器無法識(shí)別,還是需要 babel 來做轉(zhuǎn)換

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

"use strict"

var _message = _interopRequireDefault(require("./message.js"))

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj }
}

console.log(_message.default)
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 moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, "utf-8")
  const ast = parser.parse(content, {
    sourceType: "module"
  })
  const dependencise = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = "." + path.join(dirname, node.source.value)
      dependencise[node.source.value] = newFile
    }
  })
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })
  return {
    filename,
    dependencise,
    code
  }
}

const moduleInfo = moduleAnalyser("./src/index.js")
console.log(moduleInfo)

分析的結(jié)果就在控制臺(tái)上打印了

{ filename: "./src/index.js",
  dependencise: { "./message.js": ".srcmessage.js" },
  code:
   ""use strict";

var _message = _interopRequireDefault(require("./message.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

console.log(_message.default);" }

目前我們只對一個(gè)模塊進(jìn)行分析,接下來要對整個(gè)項(xiàng)目進(jìn)行分析,所以我們先分析了入口文件,再分析入口文件中所使用的依賴

依賴圖譜

創(chuàng)建一個(gè)函數(shù)來循環(huán)依賴并生成圖譜

// 依賴圖譜
const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [entryModule]
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencise } = item
    // 如果入口文件有依賴就去做循環(huán)依賴,對每一個(gè)依賴做分析
    if (dependencise) {
      for (const j in dependencise) {
        if (dependencise.hasOwnProperty(j)) {
          graphArray.push(moduleAnalyser(dependencise[j]))
        }
      }
    }
  }
  console.log("graphArray = ", graphArray)
}

將入口的依賴,依賴中的依賴全部都分析完放到 graphArray 中,控制臺(tái)輸出的打印結(jié)果

可以看到 graphArray 中一共有三個(gè)對象,就是我們在項(xiàng)目中引入的三個(gè)文件,全部被分析出來了,為了方便閱讀,我們創(chuàng)建一個(gè) graph 對象,將分析的結(jié)果依次放入

// 依賴圖譜
const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [entryModule]
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencise } = item
    // 如果入口文件有依賴就去做循環(huán)依賴,對每一個(gè)依賴做分析
    if (dependencise) {
      for (const j in dependencise) {
        if (dependencise.hasOwnProperty(j)) {
          graphArray.push(moduleAnalyser(dependencise[j]))
        }
      }
    }
  }
  // console.log("graphArray = ", graphArray)

  // 創(chuàng)建一個(gè)對象,將分析后的結(jié)果放入
  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencise: item.dependencise,
      code: item.code
    }
  })
  console.log("graph = ", graph)
  return graph
}

輸出的 graph 為:

最后在 makeDependenciesGraph 函數(shù)中將 graph 返回,賦值給 graphInfo,輸出的結(jié)果和 graph 是一樣的

const graghInfo = makeDependenciesGraph("./src/index.js")
console.log(graghInfo)
生成代碼

現(xiàn)在已經(jīng)拿到了所有代碼生成的結(jié)果,現(xiàn)在我們借助 DependenciesGraph(依賴圖譜) 來生成真正能在瀏覽器上運(yùn)行的代碼

最好放在一個(gè)大的閉包中來執(zhí)行,避免污染全局環(huán)境

const generateCode = entry => {
  // makeDependenciesGraph 返回的是一個(gè)對象,需要轉(zhuǎn)換成字符串
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return `
    (function (graph) {

    })(${graph})
  `
}

const code = generateCode("./src/index.js")
console.log(code)

我這里先把輸出的 graph 代碼格式化了一下,可以發(fā)現(xiàn)在 index.js 用到了 require 方法,message.js 中不僅用了 require 方法,還用 exports 對象,但是在瀏覽器中,這些都是不存在的,如果我們直接去執(zhí)行,是會(huì)報(bào)錯(cuò)的

let graph = {
  "./src/index.js": {
    dependencise: { "./message.js": ".srcmessage.js" },
    code: `
      "use strict";


       var _message = _interopRequireDefault(require("./message.js"));


       function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } 


       console.log(_message.default);
      `
  },
  ".srcmessage.js": {
    dependencise: { "./word.js": ".srcword.js" },
    code:
      ""use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _word = require("./word.js");

var message = "say ".concat(_word.word);
var _default = message;
exports.default = _default;"
  },
  ".srcword.js": {
    dependencise: {},
    code:
      ""use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.word = void 0;
var word = "hello";
exports.word = word;"
  }
}

接下來要去構(gòu)造 require 方法和 exports 對象

const generateCode = entry => {
  console.log(makeDependenciesGraph(entry))
  // makeDependenciesGraph 返回的是一個(gè)對象,需要轉(zhuǎn)換成字符串
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {

      };
      require("${entry}")
    })(${graph})
  `
}

const code = generateCode("./src/index.js")
console.log(code)

graph 是依賴圖譜,拿到 entry 后去執(zhí)行 ./src/index.js 中的 code,也就是下面高亮部分的代碼,為了直觀我把前面輸出的 graph 代碼拿下來參考:

let graph = {
  "./src/index.js": {
    dependencise: { "./message.js": ".srcmessage.js" },
    code: `
      "use strict";


       var _message = _interopRequireDefault(require("./message.js"));


       function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } 


       console.log(_message.default);
      `
  }
}

為了讓 code 中的代碼執(zhí)行,這里再使用一個(gè)閉包,讓每一個(gè)模塊里的代碼放到閉包里來執(zhí)行,這樣模塊的變量就不會(huì)影響到外部的變量

return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {
        (function (code) {
          eval(code)
        })(graph[module].code)
      };
      require("${entry}")
    })(${graph})
  `

閉包里傳遞的是 graph[module].code,現(xiàn)在 entry 也就是 ./src/index.js 這個(gè)文件,會(huì)傳給 require 中的 module 變量,實(shí)際上去找依賴圖譜中 ./src/index.js 對應(yīng)的對象,然后再去找到 code 中對應(yīng)的代碼,也就是下面這段代碼,被我格式化過,為了演示效果

"use strict"
var _message = _interopRequireDefault(require("./message.js"))
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_message.default)

但是我們會(huì)發(fā)現(xiàn),這里 _interopRequireDefault(require("./message.js")) 引入的是 ./message.js 相對路徑,等到第二次執(zhí)行的時(shí)候,require(module) 這里的 module 對應(yīng)的就是 ./message.js

它會(huì)到 graph 中去找 ./message.js 下對應(yīng)的 code,可是我們在 graph 中存的是 ".srcmessage.js" 絕對路徑,這樣就會(huì)找不到對象

因?yàn)槲覀冎皩懘a的時(shí)候引入的是相對路徑,現(xiàn)在我們要把相對路徑轉(zhuǎn)換成絕對路徑才能正確執(zhí)行,定義一個(gè) localRequire 方法,這樣當(dāng)下次去找的時(shí)候就會(huì)走我們自己定義的 localRequire,其實(shí)就是一個(gè)相對路徑轉(zhuǎn)換的方法

return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {
        // 相對路徑轉(zhuǎn)換
        function localRequire(relativePath) {
          return require(graph[module].dependencise[relativePath])
        }
        (function (require, code) {
          eval(code)
        })(localRequire, graph[module].code)
      };
      require("${entry}")
    })(${graph})
  `

我們定義了 localRequire 方法,并把它傳遞到閉包里,當(dāng)執(zhí)行了 eval(code) 時(shí)執(zhí)行了 require 方法,就不是執(zhí)行外部的 require(module) 這個(gè)方法,而是執(zhí)行我們傳遞進(jìn)去的 localRequire 方法

我們在分析出的代碼中是這樣引入 message.js

var _message = _interopRequireDefault(require("./message.js"))

這里調(diào)用了 require("./message.js"),就是我們上面寫的 require 方法,也就是 localRequire(relativePath)

所以 relativePath 就是 "./message.js"

這個(gè)方法返回的是 require(graph[module].dependencise[relativePath])

這里我把參數(shù)帶進(jìn)去,就是這樣:

graph("./src/index.js").dependencise["./message.js"]

let graph = {
  "./src/index.js": {
    dependencise: { "./message.js": ".srcmessage.js" },
    code: `
      "use strict";


       var _message = _interopRequireDefault(require("./message.js"));


       function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } 


       console.log(_message.default);
      `
  }
}

對照著圖譜就能發(fā)現(xiàn)最終返回的就是 ".srcmessage.js" 絕對路徑,返回絕對路徑后,我們再調(diào)用 require(graph("./src/index.js").dependencise["./message.js"]) 就是執(zhí)行外部定義的 require(module) 這個(gè)方法,重新遞歸的去執(zhí)行,光這樣還不夠,這只是實(shí)現(xiàn)了 require 方法,還差 exports 對象,所以我們再定義一個(gè) exports 對象

return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {
        // 相對路徑轉(zhuǎn)換
        function localRequire(relativePath) {
          return require(graph[module].dependencise[relativePath])
        }
        var exports = {};
        (function (require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports
      };
      require("${entry}")
    })(${graph})
  `

最后要記得 return exports 將 exports 導(dǎo)出,這樣下一個(gè)模塊在引入這個(gè)模塊的時(shí)候才能拿到導(dǎo)出的結(jié)果,現(xiàn)在代碼生成的流程就寫完了,最終返回的是一個(gè)大的字符串,保存再次運(yùn)行 node bundle.js | highlight

這里我是 windows 環(huán)境,將輸出完的代碼直接放到瀏覽器里不行,我就把壓縮的代碼格式化成下面這種樣子,再放到瀏覽器里就能輸出成功了

;(function(graph) {
  function require(module) {
    function localRequire(relativePath) {
      return require(graph[module].dependencise[relativePath])
    }
    var exports = {}
    ;(function(require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[module].code)
    return exports
  }
  require("./src/index.js")
})({
  "./src/index.js": {
    dependencise: { "./message.js": ".srcmessage.js" },
    code:
      ""use strict";

var _message = _interopRequireDefault(require("./message.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

console.log(_message.default);"
  },
  ".srcmessage.js": {
    dependencise: { "./word.js": ".srcword.js" },
    code:
      ""use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _word = require("./word.js");

var message = "say ".concat(_word.word);
var _default = message;
exports.default = _default;"
  },
  ".srcword.js": {
    dependencise: {},
    code:
      ""use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.word = void 0;
var word = "hello";
exports.word = word;"
  }
})

將上面代碼放入瀏覽器的控制臺(tái)中,回車就能輸出 say hello

總結(jié)

這就是打包工具打包后的內(nèi)容,期間涉及了 node 知識(shí),使用 babel 來轉(zhuǎn)譯 ast(抽象語法樹),最后的 generateCode 函數(shù)涉及到了遞歸閉包形參實(shí)參,需要大家多看幾遍,加深理解

To Be Continued 個(gè)人博客 24 個(gè)實(shí)例入門并掌握「Webpack4」(一) 24 個(gè)實(shí)例入門并掌握「Webpack4」(二)

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/110207.html

相關(guān)文章

  • 24 個(gè)實(shí)例入門掌握Webpack4」(一)

    摘要:前言此項(xiàng)目總共小節(jié),目錄搭建項(xiàng)目并打包文件生產(chǎn)和開發(fā)模式覆蓋默認(rèn)用轉(zhuǎn)譯自動(dòng)生成文件處理文件圖片處理匯總處理第三方庫開發(fā)模式與開發(fā)模式和生產(chǎn)模式實(shí)戰(zhàn)打包自定義函數(shù)庫配置配置配置使用加快打包速度多頁面打包配置編寫編寫編寫前節(jié)基于漸進(jìn)式教程為 前言 此項(xiàng)目總共 24 小節(jié),目錄: 搭建項(xiàng)目并打包 JS 文件 生產(chǎn)和開發(fā)模式 覆蓋默認(rèn) entry/output 用 Babel 7 轉(zhuǎn)譯 ES...

    Tonny 評論0 收藏0
  • 24 個(gè)實(shí)例入門掌握Webpack4」(二)

    摘要:代碼如下所示按照正常使用習(xí)慣,操作來實(shí)現(xiàn)樣式的添加和卸載,是一貫技術(shù)手段。將幫助我們進(jìn)行操作。 繼 24 個(gè)實(shí)例入門并掌握「Webpack4」(一) 后續(xù): JS Tree Shaking CSS Tree Shaking 圖片處理匯總 字體文件處理 處理第三方 js 庫 開發(fā)模式與 webpack-dev-server 開發(fā)模式和生產(chǎn)模式?實(shí)戰(zhàn) 打包自定義函數(shù)庫 九、JS Tre...

    hlcc 評論0 收藏0
  • 【Cute-Webpack】Webpack4 入門手冊(共 18 章)

    摘要:介紹背景最近和部門老大,一起在研究團(tuán)隊(duì)前端新手村的建設(shè),目的在于幫助新人快速了解和融入公司團(tuán)隊(duì),幫助零基礎(chǔ)新人學(xué)習(xí)和入門前端開發(fā)并且達(dá)到公司業(yè)務(wù)開發(fā)水平。 showImg(https://segmentfault.com/img/remote/1460000020063710?w=1300&h=646); 介紹 1. 背景 最近和部門老大,一起在研究團(tuán)隊(duì)【EFT - 前端新手村】的建設(shè)...

    AlanKeene 評論0 收藏0
  • 關(guān)于Vue2一些值得推薦的文章 -- 五、六月份

    摘要:五六月份推薦集合查看最新的請點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...

    sutaking 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<