這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 每次用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尾碼的文件的,這就需要引入loader
及vue編譯插件
了!裝它!
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語法樹
- 安裝 @babel/parser 轉AST樹)
- 根據
AST語法樹
,生成
瀏覽器可以運行的代碼
(遍歷AST樹)- 安裝 @babel/traverse 做依賴收集
- 安裝 @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 }) }