首页 > 其他分享 >记录--webpack和vite原理

记录--webpack和vite原理

时间:2023-08-17 17:44:06浏览次数:50  
标签:vue const -- require js 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

代码如下:

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

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

 

标签:vue,const,--,require,js,webpack,打包,vite
From: https://www.cnblogs.com/smileZAZ/p/17638336.html

相关文章

  • 【华秋干货铺】DDR电路的PCB布局布线要求
    上期和大家聊的电源PCB设计的重要性,那本篇内容小编则给大家讲讲存储器的PCB设计建议,同样还是以大家最为熟悉的RK3588为例,详细介绍一下DDR模块电路的PCB设计要如何布局布线。由于RK3588DDR接口速率最高达4266Mbps,PCB设计难度大,所以强烈建议使用瑞芯微原厂提供的DDR模板和对应的DD......
  • SQL基础知识
     (一)DDL数据定义语言作用:用于检索或修改数据命令:SELECT:用于检索数据INSERT:用于添加数据到数据库UPDATE:用于修改数据库数据DELETE:用于删除数据库数据(二)DML数据操作语言作用:用于定义数据结构,比如:创建、修改或删除数据库对象,包括:用于创建用户和重建数据库对象命令:CR......
  • 关于 n+1 个点可以唯一确定一个 n 次函数的证明
    设原函数\(f(x)=\sum\limits_{i=0}^{n}{a_ix^i}\)。定义一个范德蒙矩阵\(V\)为:\[V=\begin{bmatrix}1&x_0&x_0^2&\cdots&x_0^n\\1&x_1&x_1^2&\cdots&x_1^n\\1&x_2&x_2^2&\cdots&x_2^n......
  • 【收藏】Stable Diffusion 制作光影文字效果
    作者:Jessie大家对于最近StableDiffusion不断出新的视觉“整活”印象都很深刻,很多人对最近比较流行的制作光影文字很感兴趣,制作光影文字可以作为进阶StableDiffusion的必备一课,本文将详细讲解基于函数计算部署StableDiffusion实现光影文字效果,观看文章需要5分钟,看完......
  • 为什么企业需要内部威胁检测软件?
    在数字时代,企业不仅需要抵御外部威胁,还必须密切关注内部威胁,因为内部因素可能对数据安全造成严重威胁。作为一款强大的内部威胁监测工具,ADAuditPlus在这一领域发挥着关键作用。本文将深入探讨ADAuditPlus在内部威胁监测中的重要性。 一、内部威胁的隐患 内部威胁是......
  • 【狂神说Java】Java零基础学习笔记-JavaSE总结
    【狂神说Java】Java零基础学习笔记-JavaSE总结JavaSE总结:......
  • 在ubuntu中半自动安装搜狗输入法的shell脚本
    总而言之就是用脚本实现了一遍搜狗官网教程中的内容,脚本仅仅在虚拟机环境的ubuntu22中对sogoupinyin_4.2.1.145_amd64.deb测试过,不保证在其他地方还能不能正常运行。脚本启动需要用sudo命令以管理员权限执行,脚本执行完毕并重启电脑后,按Ctrl+空格键调出输入法,按Shift键切换中......
  • 计算机程序员 考 试 级 别 及 其 编 码
     考试级别及其编码  领域计算机软件计算机网络计算机应用技术信息系统信息服务高级资格信息系统项目管理师01系统分析师02系统架构设计师03网络规划设计师04系统规划与管理师05中级资格软件评测师14软件设计师1......
  • HCIP学习(2)-- 路由引入
    一.路由引入的基本概念路由引入指的是将路由信息从一种路由协议发布到另一种路由协议的操作。通过路由引入,可以实现路由信息在不同路由协议间传递;执行路由引入时,还可以部署路由控制,从而实现对业务流量的灵活把控。二.实例讲解设备编号端口IP配置......
  • /proc/PID/maps 文件及示例说明
    文件及字段说明这个文件中的内容描述了进程的虚拟内存空间中的不同区域,包括代码段、数据段、堆、栈以及共享库等。每一行都代表了一个内存区域,并包含以下列:起始地址和结束地址:内存区域在虚拟内存空间中的起始地址和结束地址。权限:内存区域的访问权限,如读、写、执行等。偏移量......