記錄--webpack和vite原理

来源:https://www.cnblogs.com/smileZAZ/archive/2023/08/17/17638336.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 每次用vite創建項目秒建好,前幾天用vue-cli創建了一個項目,足足等了我一分鐘,那為什麼用 vite 比 webpack 要快呢,這篇文章帶你梳理清楚它們的原理及不同之處!文章有一點長,看完絕對有收穫! 正文 一、webpac ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

前言

每次用vite創建項目秒建好,前幾天用vue-cli創建了一個項目,足足等了我一分鐘,那為什麼用 vite 比 webpack 要快呢,這篇文章帶你梳理清楚它們的原理及不同之處!文章有一點長,看完絕對有收穫!

正文

一、webpack基本使用

webpack 的出現主要是解決瀏覽器里的 javascript 沒有一個很好的方式去引入其它的文件這個問題的。 話說肯定有小伙伴不記得 webpack 打包是咋使用的(清楚的話可以跳過這一小節),那麼我以一個小 demo 來實現一下:

1. 搭建基本目錄結構

  • 我們在vue項目中初始化全局安裝 webpack 和 webpack-cli :

yarn add webpack webpack-cli -g

  • 創建vue所需的目錄文件,以及webpack配置文件

目錄結構如下:

2. webpack.config.js配置文件編寫

不清楚webpack配置項的朋友可以進官方文檔瞅一眼:webpack 中文文檔

看完之後,我們知道webpack主要包含的幾個概念就開始編寫配置文件了!

(1)打包main.js

代碼如下:

const path = require('path')
module.exports = {
  mode: 'development',  //設置開發模式
  entry: path.resolve(__dirname, './src/main.js'),   //打包入口
  output: {   //打包到哪裡去
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js',  //預設文件名main.js
  }
}

為了方便我們運行,我們去package.json中配置命令,只需yarn dev就能運行了:

"dev": "webpack server --progress --config ./webpack.config.js"

運行後我們發現根目錄多出了一個dist文件夾,我們進到main.js中查看發現打包成功了!

(2)打包index.html

問題❓:我們知道vue項目中是有一個index.html文件的,我們如果要打包這個html文件咋辦呢?
我們就需要藉助plugin插件去擴展webpack的能力,去裝它:

yarn add html-webpack-plugin -D

引入並使用它:

const HtmlWebpackPlugin = require('html-webpack-plugin')

  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),    //需要被打包的html
      filename: 'index.html',  //文件打包名
      title: '手動搭建vue' //html傳進去的變數
    }),
  ]

index.html 代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= htmlWebpackPlugin.options.title%></title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

好啦,我們再次運行打包命令,發現dist目錄下多出index.html文件,打包成功

(3)打包vue文件

首先,我們需要去安裝vue的源碼:

yarn add vue

新建一個App.vue:

<template>
  <div>
    vue項目測試
  </div>
</template>

<script setup>
</script>

<style lang="css" scoped>
</style>

main.js中寫入:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

我們再去打包,發現報錯了,根據提示,我們可以推斷webpack是不能處理不能編譯.vue尾碼的文件的,這就需要引入loadervue編譯插件了!裝它!

yarn add vue-loader@next
yarn add vue-template-compiler -D

繼續在配置文件中引入並使用:

const { VueLoaderPlugin } = require('vue-loader')

  module: {
    rules: [
      {
        test: /\.vue$/,  //.vue尾碼的文件
        use: ['vue-loader']  //啟用vue-loader
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]

再次打包,打包成功!我們可以測試一下,用live server運行打包後的index.html看看,會發現寫在vue中的文字在頁面成功展示!

 

(4)打包css

那麼我們如果要在vue中寫css樣式呢?顯然webpack是識別不了的,還得loader來幫忙:

yarn add css-loader style-loader -D

配置文件中加入新的一條css規則:

module: {
    rules: [
      {
        test: /\.css$/,  //.css尾碼的文件
        use: ['style-loader', 'css-loader']
      }
    ]
  }
去vue文件中把字體樣式改為紅色後,打包並測試一下,成功!:

(5)配置babel

為了防止 webpack 識別不了高版本的 js 代碼,我們去裝 babel

yarn add @babel/core @babel/cdpreset-env babel-loader -D

webpack.config.js 配置文件添加新的一條 js 規則:

module: {
    rules: [
      {
        test: /\.js$/,  //.js尾碼的文件
        exclude: /node_modules/, //不包含node_modules
        use: ['babel-loader']
      }
    ]
  }
babel.config.js 配置文件代碼如下:
module.exports={
  presets:[
    ['@babel/preset-env',{
      'targets':{
        'browsers':['last 2 versions']
      }
    }]
  ]
}

3. webpack熱重載

熱重載它是webpack的一個超級nice的插件,讓你不用每次都去執行打包命令,裝它:

yarn add webpack-dev-server -D

之後,我們去webpack.config.js中配置:

devServer: { 
    static:{
      directory: path.resolve(__dirname, './dist')
    },
    port:8080,  //埠
    hot: true, //自動打包
    host:'localhost', 
    open:true //自動跳到瀏覽器
  }

此時還需要將package.json中的命令改改:

"dev": "webpack server --progress --config ./webpack.config.js"

我們使用yarn dev再次運行,熟悉的一幕來了!自動跳轉到瀏覽器且將vue文件的內容展示在頁面上,修改vue內容也會自動打包!

好了!到這裡,基本的打包流程你已經get清楚了,下麵我們來研究研究它的打包原理吧!

二、webpack打包原理

實現一個webpack的思路主要有三步:

  • 讀取入口文件內容(使用 fs
  • 分析入口文件,遞歸的方式去讀取模塊所依賴的文件並且生成AST語法樹
    1. 安裝 @babel/parser 轉AST樹)
  • 根據AST語法樹生成瀏覽器可以運行的代碼(遍歷AST樹)
    1. 安裝 @babel/traverse 做依賴收集
    2. 安裝 @babel/core@babel/preset-env 讓es6轉es5

我們去新建一個目錄,結構如下(其中 add.js 和 minus.js 定義了兩個值相加減的函數並將其拋出,index.js中引入這兩個函數並列印結果,代碼就不附上了,比較簡單):

 bundle.js 是我們用來打造 webpack 的文件,代碼如下:

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) => {
  //1 讀文件
  const body = fs.readFileSync(file, 'utf8');  //讀到路徑下的文件內容

  //2 分析文件轉AST樹
  const ast = parser.parse(body, {   //body為需要解析的代碼
    sourceType: 'module' //以es6的模塊化語法解析
  })
  // console.log(ast.program.body);  //[{},{},{}...]

  //3 依賴收集 
  const deps = {}
  traverse(ast, { //遍歷ast
    ImportDeclaration({ node }) {  //把import類型的對象找出來
      const dirname = path.dirname(file)  //拿到index.js所在文件夾路徑
      const abspath = './' + path.posix.join(dirname, node.source.value) //add.js文件的絕對路徑
      deps[node.source.value] = abspath //key:'./add.js'  value:'xxx/add.js'
    }
  })

  //4.把ast->code
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  })
  console.log(code);

  const moduleInfo = { file, deps, code }
  return moduleInfo
}

//5. 遞歸獲取所有依賴
const parseModules = (file) => {
  const entry = getModuleInfo(file)
  const temp = [entry]
  const depsGraph = {}

  for (let i = 0; i < temp.length; i++) {
    const deps = temp[i].deps //{ './add.js': './src/add.js', './minus.js': './src/minus.js' }
    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(temp);
  return depsGraph
}

//打包
const bundle = (file) => {
  const depsGraph = JSON.stringify(parseModules(file));
  //手寫一個require 藉助eval
  return `(function(grash) {

    function require(file) {
      function absRequire(relPath) {
        return require(grash[file].deps[relPath])
      }

      var exports = {};

      (function(require, code) {
        eval(code)
      })(absRequire, grash[file].code)

      return exports
    }

    require('${file}')

  })(${depsGraph})`

}

const result=bundle('./src/index.js')
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', result)

我們使用node去運行這個文件,去到 html 頁面上,發現控制台能輸出加減法對應的結果,說明打包成功

但是,webpack有一個缺點,如果在這個文件中需要改動一點點再保存,webpack的熱重載又會重新自動打包一次,這對於大型項目是極不友好的,這時間估計等的花都要謝了。那麼vite出現了!

三、vite打包原理

我們知道,當聲明一個 script 標簽類型為 module 時,瀏覽器會對其內部的 import 引用發起 HTTP 請求獲取模塊內容。那麼,vite 會劫持這些請求併進行相應處理。因為瀏覽器只會對用到的模塊發送http請求,所以vite不用對項目中所有文件都打包,而是按需載入,大大減少了AST樹的生成和代碼轉換,降低服務啟動的時間和項目複雜度的耦合,提升了開發者的體驗。

1. 需要解決的問題

那麼,要打包一個vue項目,它的入口文件是main.js,瀏覽器會遇到三個問題:

import { createApp } from 'vue' //瀏覽器無法識別vue路徑
import App from './App.vue' //瀏覽器無法解析.vue文件
import './index.css' //index.css不是一個合法的js文件,因為import只能引入js文件

const app = createApp(App)
app.mount('#app')

知道怎麼解決這幾個問題,我們就能打造一個vite了!

2. 打造vite

我們使用 koa 去搭建一個本地服務讓其可以運行,新建一個 server.js 文件用來打造 vite ,代碼如下:

//用node啟一個服務
const Koa = require('koa');
const app = new Koa()
const fs = require('fs')
const path = require('path')
const compilerDom = require('@vue/compiler-dom')  //引入vue源碼  能識別template中的代碼
const compilerSfc = require('@vue/compiler-sfc')  // 能識別script中的代碼

function rewriteImport(content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, (s0, s1) => {
    //若以 ./  ../  / 開頭的相對路徑
    console.log(s0, s1);
    if (s1[0] !== '.' && s1[0] !== '/') {   //'vue
      return ` from '/@modules/${s1}'`   //去http://localhost:5173/@modules/vue
    } else {
      return s0
    }
  })
}

app.use((ctx) => {
  const { request: { url, query } } = ctx
  if (url === '/') {
    //讀index.html
    ctx.type = 'text/html'  //設置類型
    let content = fs.readFileSync('./index.html', 'utf8')  //讀文件
    // console.log(content);

    ctx.body = content//content輸出給前端
  }
  else if (url.endsWith('.js')) {  //js文件  /src/main.js
    const p = path.resolve(__dirname, url.slice(1))  //   src/main.js  拿到文件的絕對路徑
    ctx.type = 'application/javascript'
    const content = fs.readFileSync(p, 'utf8')
    ctx.body = rewriteImport(content)
  }
  else if (url.startsWith('/@modules')) { //  '/@modules/vue'
    const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))  // 'vue'
    const module = require(prefix + '/package.json').module //讀取package.json中的module欄位   拿到vue的模塊源碼地址
    const p = path.resolve(prefix, module)  // 拿到vue的模塊源碼的終極地址
    const ret = fs.readFileSync(p, 'utf8')  //讀取文件
    ctx.type = 'application/javascript'
    ctx.body = rewriteImport(ret)  //遞歸 防止vue源碼又用到了其它模塊
  }
  else if (url.indexOf('.vue') > -1) {
    const p = path.resolve(__dirname, url.split('?')[0].slice(1)) // src/App.vue
    const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
    
    console.log(descriptor);
    if (!query.type) { // 返回.vue文件的js部分
      ctx.type = 'application/javascript'
      ctx.body = `
        ${rewriteImport(descriptor.script.content.replace('export default ', 'const __script = '))}
        import { render as __render } from "${url}?type=template"
        __script.render = __render
        export default __script
      `
    } else if (query.type === 'template') { // 返回.vue文件的html部分
      const template = descriptor.template
      const render = compilerDom.compile(template.content, {mode: 'module'}).code
      ctx.type = 'application/javascript'
      ctx.body = rewriteImport(render)
    }

  }
  else if (url.endsWith('.css')) {
    const p = path.resolve(__dirname, url.slice(1))
    const file = fs.readFileSync(p, 'utf8')
    const content = `
      const css="${file.replace(/\n/g, '')}"
      let link=document.createElement('style')
      link.setAttribute('type','text/css')
      document.head.appendChild(link)
      link.innerHTML = css
      export default css
    `
    ctx.type = "application/javascript"
    ctx.body = content
  }
})

app.listen(5173, () => {
  console.log('listening on port 5173');
})

3. vite熱更新

那麼,vite 的熱更新怎麼實現呢?

我們可以使用chokidar庫來監聽某個文件夾的變更,只要監聽到有文件變更,就用websocket通知瀏覽器重新發一個請求,瀏覽器就會在代碼每次變更之後立刻重新請求這份資源。

(1) 安裝chokidar庫:

yarn add chokidar -D

(2) 之後去新建一個文件夾chokidar,在其中新建 handleHMRUpdate.js 用於實現監聽:

const chokidar = require('chokidar');

export function watch() {
  const watcher = chokidar.watch('../src', {
    ignored: ['**/node_modules/**', '**/.git/**'],  //不監聽哪些文件
    ignorePermissionErrors: true,
    disableGlobbing: true
  })
  return watcher
}

export function handleHMRupdate(opts) {   //創建websocket連接 客戶端不給服務端發請求,服務端可以通過websocket來發數據
  const { file, ws } = opts
  const shortFile = getShortName(file, appRoot)
  const timestamp = Date.now()
  let updates;
  if (shortFile.endsWith('.css')) {  //css文件的熱更新
    updates = [
      {
        type: 'js-update',
        timestamp,
        path: `${shortFile}`,
        acceptPath: `${shortFile}`
      }
    ]
  }

  ws.send({
    type: 'update',
    updates
  })
}

本文轉載於:

https://juejin.cn/post/7267791228872671247

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ![](https://img2023.cnblogs.com/blog/3076680/202308/3076680-20230816163336851-1607680900.png) # 1. 資料庫 ## 1.1. 一組相關信息 ## 1.2. 電話簿肯定是最為普及且常用的資料庫 # 2. 非 ...
  • # mysql代理、中間件技術 ## 代理簡介 **名詞** DB proxy 資料庫中間件 **功能** 讀寫分離:讀寫分離導致處理速度迅速,一般情況下是主伺服器進行寫操作而從伺服器進行讀操作 負載均衡 支持數據的分片自動路由和聚合 **本文主要圍繞Mycat實現、且在完成MM-SS集群的條件下* ...
  • ![file](https://img2023.cnblogs.com/other/2685289/202308/2685289-20230817174410327-1799604230.png) Apache DolphinScheduler 3.2.0 版本已經呼之欲出,8 月 中下旬,這個大版 ...
  • NineData提供了高效、穩定的MySQL大表遷移能力,解決了傳統遷移方案的問題。通過智能分片、行級併發和動態攢批等核心技術,NineData保證了遷移性能。同時,NineData具備完善的容災能力,提高了大表遷移的成功率。通過數據和結構的對比功能,保障了數據的一致性。使用NineData進行遷移... ...
  • 本文分享自華為雲社區《【手把手帶你玩轉HetuEngine】(一)HetuEngine快速上手》,作者:HetuEngine九級代言。 HetuEngine是什麼 HetuEngine是華為推出的高性能互動式SQL分析及數據虛擬化引擎。與大數據生態無縫融合,實現海量數據秒級互動式查詢;支持跨源跨域統 ...
  • Taier 作為[袋鼠雲](https://www.dtstack.com/?src=szsm)的[開源項目](https://www.dtstack.com/?src=szsm)之一,是一個[分散式可視化的 DAG 任務調度系統](https://www.dtstack.com/?src=szsm ...
  • ##### 3 列表標簽 html中的列表標簽,該類標簽是關於HTML文檔中列表的,包含dl、dt、dd、ol、li、ul等標簽。這裡主要說的是ul和ol標簽。 (1)ol標簽代表HTML的有序列表。ol成對出現,以開始,結束。列表中的每一列使用標簽定義,這一點與無序列表相同。每列使用數字或字母開頭 ...
  • ##### 2 超鏈接標簽 超鏈接是瀏覽者和伺服器的交互的主要手段,也叫超級鏈接或a鏈接,是網頁中指向一個目標的連接關係,這個目標可以是網頁、網頁中的具體位置、圖片、郵件地址、文件、應用程式等。 超鏈接是網頁中最重要的元素之一。一個網站的各個網頁就是通過超鏈接關聯起來的,用戶通過點擊超鏈接可以從一個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...