记录--webpack和vite原理
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前言
每次用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
代码如下:
1 2 3 4 5 6 7 8 9 | 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
引入并使用它:
1 2 3 4 5 6 7 8 9 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' ) plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html' ), //需要被打包的html filename: 'index.html' , //文件打包名 title: '手动搭建vue' //html传进去的变量 }), ] |
index.html 代码如下:
1 2 3 4 5 6 7 8 9 10 11 | <!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:
1 2 3 4 5 6 7 8 9 10 11 | <template> <div> vue项目测试 </div> </template> <script setup> </script> <style lang= "css" scoped> </style> |
main.js中写入:
1 2 3 4 5 | 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
继续在配置文件中引入并使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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规则
:
1 2 3 4 5 6 7 8 | module: { rules: [ { test: /\.css$/, //.css后缀的文件 use: [ 'style-loader' , 'css-loader' ] } ] } |
红色
后,打包并测试一下,成功!:(5)配置babel
为了防止 webpack 识别不了高版本
的 js 代码,我们去装 babel
:
yarn add @babel/core @babel/cdpreset-env babel-loader -D
webpack.config.js 配置文件添加新的一条 js 规则:
1 2 3 4 5 6 7 8 9 | module: { rules: [ { test: /\.js$/, //.js后缀的文件 exclude: /node_modules/, //不包含node_modules use: [ 'babel-loader' ] } ] } |
1 2 3 4 5 6 7 8 9 | module.exports={ presets:[ [ '@babel/preset-env' ,{ 'targets' :{ 'browsers' :[ 'last 2 versions' ] } }] ] } |
3. webpack热重载
热重载它是webpack的一个超级nice的插件,让你不用每次都去执行打包命令,装它:
yarn add webpack-dev-server -D
之后,我们去webpack.config.js中配置:
1 2 3 4 5 6 7 8 9 | 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 的文件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 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,浏览器会遇到三个问题:
1 2 3 4 5 6 | 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 ,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | //用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 用于实现监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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 }) } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)