这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

每次用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

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--webpack和vite原理的更多相关文章

  1. 如何编写一个WebPack的插件原理及实践

    _ 阅读目录 一:webpack插件的基本原理 二:理解 Compiler对象 和 Compilation 对象 三:插件中常用的API 四:编写插件实战 回到顶部 一:webpack插件的基本原理 ...

  2. webpack与vite的对比

    vite与webpack的打包原理: vite: 基于游览器原生ES Module,利用游览器解析import,服务器端按需编译返回 webpack: 逐级递归识别依赖,构建依赖图谱->转化AS ...

  3. Atitit 分区后的查询  mysql分区记录的流程与原理

    Atitit 分区后的查询  mysql分区记录的流程与原理 1.1.1. ibd是MySQL数据文件.索引文件1 1.2. 已经又数据了,如何分区? 给已有的表加上分区 ]1 1.3. 分成4个区, ...

  4. webpack 的编译原理

    自从接触了react,vue 这两个框架,都会用到webpack这个打包工具.面试的时候,经常被问到知道webpack的编译原理吗? 可以简单的介绍一下.每每这个时候都被问的哑口无言,平时用的时候挺顺 ...

  5. vue-cli3 vue2 保留 webpack 支持 vite 成功实践

    大家好! 文本是为了提升开发效率及体验实践诞生的. 项目背景: 脚手架:vue-cli3,具体为 "@vue/cli-service": "^3.4.1" 库: ...

  6. 一篇文章说清 webpack、vite、vue-cli、create-vue 的区别

    webpack.vite.vue-cli.create-vue 这些都是什么?看着有点晕,不要怕,我们一起来分辨一下. 先看这个表格: 脚手架 vue-cli create-vue 构建项目 vite ...

  7. webpack之webpack和vite的区别

      打包原理 缺点 其他特点 webpack 解析各个模块的依赖关系 使用loader转换文件,使用plugin注入钩子,打包合并模块,最终生成bundle文件,使用express开启本地服务器, 浏 ...

  8. 轻松理解webpack热更新原理

    一.前言 - webpack热更新 Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块.HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间.提升开发 ...

  9. Webpack与Vite热更新差异对比

    随着项目的日渐迭代,项目整体的代码量也会越来越多,从而导致项目体积越来越大:在Webpack时代,很多人会对历史项目(巨型项目)感到头疼,因为往往巨型项目在本地开发调试的时候会因为本地代码的修改触发H ...

  10. .net 温故知新:【9】.NET日志记录 ILogger使用和原理

    日志 日志作为我们程序记录的"黑匣子"是不论什么系统都会使用到的,比如我们经常使用的log4net就是第三方日志记录提供程序.NET 支持使用各种内置和第三方日志记录提供程序的日志 ...

随机推荐

  1. Hive数据导入与导出

    数据导入 ● 本地文件导入 -- 本地文件导入(local) LOAD DATA local INPATH '/home/hadoop/sourceA.txt' INTO TABLE testA PA ...

  2. JS Leetcode 690. 员工的重要性 题解分析

    壹 ❀ 引 本题来自LeetCode690. 员工的重要性,难度简单,题目描述如下: 给定一个保存员工信息的数据结构,它包含了员工 唯一的 id ,重要度 和 直系下属的 id . 比如,员工 1 是 ...

  3. 关于动态抽样(Dynamic Sampling)

    关于动态抽样(Dynamic Sampling) 原文:http://www.oracle.com/technetwork/issue-archive/2009/09-jan/o19asktom-08 ...

  4. 解决:EXP-00003: 未找到段 (0,0) 的存储定义报错

    问题说明 DB服务器oracle版本:11.2.0.4.0 最近在客户端用exp命令导出部分表时报错,如题.搜索了一下,找到以下解决方案: 问题原因 Oracle 11g 有一个参数deferred_ ...

  5. 网站(>???<)

    http://cpeditor.org/ https://csacademy.com/app/graph_editor/ https://www.cnblogs.com/zhangyi1357/p/1 ...

  6. MySQL写入SQL整个执行流程

    innodb存储引擎中一条sql写入的详细流程     第0步:会先去看缓冲区有没有这条数据,如果有就不进行缓存,直接进入第三步.   第1步:会将要修改的那一行数据所在的一整页加载到缓冲池Buffe ...

  7. [BUUCTF][WEB][极客大挑战 2019]Http 1

    打开靶机提供的url 右键查看网页源代码 发现一个链接 (Secret.php),访问看看 返回: It doesn't come from 'https://Sycsecret.buuoj.cn' ...

  8. NamedTuple技巧用法

    PS: 第一眼看到这个代码的时候,就联想到了go中的构造函数,虽然知道go中的构造函数其实就类比于python中的构造函数__init__,但是不得不说,这个太像了 在日常编码中,我们经常需要写一些返 ...

  9. 关于json序列化时报错json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

    1.今天在写客户端与服务端交互的程序的时候,发现了一个问题 客户端代码 #客户端程序主要是发送注册请求/登录请求给服务端,服务端接收响应后回应对应的应答给客户端,客户端接收响应后,然后做一些操作 # ...

  10. 【八股cover#1】MySQL Q&A与知识点

    MySQL Q&A与知识点 1.基础知识 什么是主键? 它用来唯一标识一条记录(一个字段).每个表都必须有且只能有一个主键,主键的取值不允许为空,而且在表中必须是唯一的(当然还可以有复合主键) ...