一、前言
每次用 vite 创立我的项目秒建好,用 vue-cli 创立了一个我的项目,却有点久,那为什么用 vite 比 webpack 要快呢,这篇文章带你梳理分明它们的原理及不同之处!文章有一点长,看完相对有播种!
目录:
webpack 根本应用
webpack 打包原理
vite 工作原理
小结
二、webpack 根本应用
webpack 的呈现次要是解决浏览器里的 javascript 没有一个很好的形式去引入其它的文件这个问题的。话说必定有小伙伴不记得 webpack 打包是咋应用的(分明的话能够跳过这一大节),那么我以一个小 demo 来实现一下:
- 搭建根本目录构造
咱们在 vue 我的项目中初始化后全局装置 webpack 和 webpack-cli:
yarn add webpack webpack-cli -g
创立 vue 所需的目录文件,以及 webpack 配置文件。
目录构造如下:
编辑
- 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’]
}
}]
]
}
- 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 内容也会主动打包!
三、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 树的生成和代码转换,升高服务启动的工夫和我的项目复杂度的耦合,晋升了开发者的体验。
- 须要解决的问题
那么,要打包一个 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 了!
- 打造 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’);
})
- 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
})
}
文章转载于:zt_ever
https://juejin.cn/post/7267791228872671247
五、小结
webpack 能够说是把所有模块的依赖关系打包成一个大文件,速度比较慢。
Vite 利用古代浏览器中原生 ES 模块的个性,将开发和构建过程拆分为更小的单位,通过浏览器运行时发送的 http 申请实现了来实现文件的按需加载。疾速的冷启动和实时的模块热更新。Vite 提供了更高效的开发体验和更快的开发环境速度。
vite 开发环境应用 esbuild 做 esm 转换,不做打包解决,生产用 rollup 作为打包工具。尽管 vite 也有一些小瑕疵(首屏,懒加载),不过和 webpack 相比体验感的确晋升了不少。
- ‘” ↩