vue3复习-源码-迷你版vite

16,918 阅读4分钟

vite的优势

  • 由于webpack每次打包都是根据整个依赖图谱,重新打包成对应的bunlde文件,当项目大时,打包效率比较低。虽然也有更新但效果不明显。
  • vite使用的是es6的import方式,运行时按需引入对应的代码。所以只需要加载一部分文件的数据,效率高。

只要设置type="module"声明了script的js,就可以使用import的方式导入其他js

下面main.js 就可以正常的使用import

<script type="module" src="/src/main.js"></script>

main.js

import { createApp } from "vue"; //这里可以用import 写法
import App from './App.vue'//这里可以用import 写法

createApp(App).mount('#app') 

迷你vite

原理

通过起一个服务器,把部分代码转化成单独的请求。 如

  • 遇到vue 改变成读当前node_moudles里面的vue
  • 遇到css 转化成新一个js请求,并动态生成style插入到html
  • 遇到template ,转化成新一个js请求,并在服务端实时转化成render方法

实现

代码结构

  • index.html
  • main.js
  • App.vue
  • index.css

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>xxxx</title>
</head>
<body>
  <h1>xxxxx</h1>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
const app = createApp(App)
app.mount('#app')

App.vue

<template>
 <p>--{{val}}--</p>
<button @click="add">测试+1</button>
</template> 

<script>

import {ref} from 'vue';  
 
export default {
  name: 'HelloWorld', 
  data() {
    return {
        val: 0
    }
  },
  methods: {
    add() {
      this.val++
    }
  }
}
</script>
<style> 
p {
    color: green;
}
</style>
 

index.css

html {
    color : red;
}

我们看到主要要解决的是

image.png

  • import的路径问题:识别from 'vue' 里面的vue 具体是从哪里来的
  • vue文件识别问题:App.vue 文件的解析,js不能识别和加载
  • css文件识别问题:解析index.css ,js不能直接识别和加载

import的路径问题

启动一个koa服务,用于转化当前请求的文件地址

const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const app = new Koa()
app.use(async ctx=>{
  const {request:{url,query} } = ctx
if(url=='/'){
    ctx.type="text/html"
    let content = fs.readFileSync('./index.html','utf-8')
    
    ctx.body = content
  }
})
app.listen(10086, ()=>{
  console.log('服务启动')
})

我们一般启动一个vue服务后,请求http://127.0.0.1:10086返回index.html的内容

image.png 这里看到 main.js报错

修改koa代码 ,让他能正常返回 main.js 和 vue

const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const app = new Koa()

app.use(async ctx=>{
  const {request:{url,query} } = ctx
  if(url=='/'){ // 返回首页html内容
      ctx.type="text/html"
      let content = fs.readFileSync('./index.html','utf-8')
      
      ctx.body = content
  }else if(url.endsWith('.js')){ //返回js路径 既访问的文件是: <script type="module" src="/src/main.js"></script>的 main.js
    // js文件
    const p = path.resolve(__dirname,url.slice(1))
    ctx.type = 'application/javascript'
    const content = fs.readFileSync(p,'utf-8')
    ctx.body = rewriteImport(content) // 替换路径
  }else if(url.startsWith('/@modules/')){ // 处理node_module 
    const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))
    const module = require(prefix+'/package.json').module
    const p = path.resolve(prefix,module)
    const ret = fs.readFileSync(p,'utf-8')
    ctx.type = 'application/javascript'
    ctx.body = rewriteImport(ret) // 递归调用
}
})


function rewriteImport(content){ //此方法用于 替换请求文件的路径 改成/@modules下面
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0,s1){
    // . ../ /开头的,都是相对路径
    if(s1[0]!=='.'&& s1[1]!=='/'){
      return ` from '/@modules/${s1}'`
    }else{
      return s0
    }
  })
}



app.listen(10086, ()=>{
  console.log('服务启动')
})

请求的index.html image.png

请求的main.js image.png

请求的vue image.png

vue3又会根据自己的import继续引入其他vue3运行所需要的框架

  • runtime-dom
  • runtime-core
  • shared
  • reactivity

image.png

vue文件识别

.vue中的三种类型

  • <template>
  • <scirpt>
  • <style>

原理就是把请求vue文件的时候,变成多个不同的import请求,通过url?type=xxx 区分

  • 如:url?type=template,就做template解析处理
  • 如:url?type=style,就做css解析处理

我们使用@vue/compiler-sfc 来处理vue文件。

compiler-dom 处理<template>的转化

const compilerSfc = require('@vue/compiler-sfc') // .vue
const compilerDom = require('@vue/compiler-dom') // 模板<template>处理

...
else if(url.indexOf('.vue')>-1){ //发现是.vue单文件组件,判断 1.无参数生成:template + 当前script输出 2.有参数template 把template转成render函数 
      const p = path.resolve(__dirname, url.split('?')[0].slice(1))
       // 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
      const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8')) 
      let body = ''
      if(!query.type){ // 1.无参数生成:template + 当前script输出 , 此版本只支持传统的options 写法 即 export default {  data() {},methods: {} } 
        ctx.type = 'application/javascript'  
 
        //template + 当前script输出
        body = body + `
            ${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
            import { render as __render } from "${url}?type=template"
            __script.render = __render
            export default __script;
        `  
       
        ctx.body = body 
      }else if(query.type==='template'){ // 2.有参数template 把template转成render函数  
        const template = descriptor.template 
        const render = compilerDom.compile(template.content, {mode:"module"}).code
        ctx.type = 'application/javascript'
        ctx.body = rewriteImport(render) // 继续递归处理import内容
      } 
    } 
    

使用sfc转化后的App.vue,里面多了一个template的转化后的/App.vue?type=template请求

请求App.vue返还内容 image.png

请求/App.vue?type=template返还内容 image.png

css文件识别

识别template中的style
  • 修改.vue的文件逻辑处理,新增style的处理,满足条件加入${url}?type=style
  • 并通过query.type==='style' 判断style的请求场景
else if(url.indexOf('.vue')>-1){ //发现是.vue单文件组件,判断 1.无参数生成:template + 当前script输出 2.有参数template 把template转成render函数 
      const p = path.resolve(__dirname, url.split('?')[0].slice(1))
       // 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
      const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8')) 
      let body = ''
      if(!query.type){ // 1.无参数生成:template + 当前script输出 , 此版本只支持传统的options 写法 即 export default {  data() {},methods: {} } 
        ctx.type = 'application/javascript'  

        if(descriptor.styles && descriptor.styles.length > 0) { //如果有style ,也输出 import url?type=style
            body = body + ` import  "${url}?type=style"` 
        }
        //template + 当前script输出
        body = body + `
            ${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
            import { render as __render } from "${url}?type=template"
            __script.render = __render
            export default __script;
        `  
       
        ctx.body = body 
      }else if(query.type==='template'){ // 2.有参数template 把template转成render函数  
        const template = descriptor.template 
        const render = compilerDom.compile(template.content, {mode:"module"}).code
        ctx.type = 'application/javascript'
        ctx.body = rewriteImport(render) // 继续递归处理import内容
      }else if(query.type==='style'){ // 3.有参数style 把template转成render函数  
        body = body + `
            let link = document.createElement('style')
            link.setAttribute('type', 'text/css')
            document.head.appendChild(link)
            let css2 = \`${descriptor.styles.map(o => o.content).join('\n')}\`
            link.innerHTML = css2 
            `
        ctx.type = 'application/javascript'
        ctx.body = body 
      }
    } 

image.png

识别.css文件

识别.css后缀,然后通过创建一个<style>标签插入css到html里面

else if(url.endsWith('.css')){
        // console.log("css come in")
        const p = path.resolve(__dirname,url.slice(1))
        const file = fs.readFileSync(p,'utf-8')
        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
      }

image.png

完整的请求逻辑


const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const compilerSfc = require('@vue/compiler-sfc') // .vue
const compilerDom = require('@vue/compiler-dom') // 模板<template>处理

const app = new Koa()

app.use(async ctx=>{
  const {request:{url,query} } = ctx
  if(url=='/'){ // 返回首页html内容
      ctx.type="text/html"
      let content = fs.readFileSync('./index.html','utf-8')
      
      ctx.body = content
  }else if(url.endsWith('.js')){ //返回js路径 既访问的文件是: <script type="module" src="/src/main.js"></script>的 main.js
    // js文件
    const p = path.resolve(__dirname,url.slice(1))
    ctx.type = 'application/javascript'
    const content = fs.readFileSync(p,'utf-8')
    ctx.body = rewriteImport(content) // 替换路径
  }else if(url.startsWith('/@modules/')){ // 处理node_module 
    const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))
    const module = require(prefix+'/package.json').module
    const p = path.resolve(prefix,module)
    const ret = fs.readFileSync(p,'utf-8')
    ctx.type = 'application/javascript'
    ctx.body = rewriteImport(ret) // 递归调用
  }else if(url.indexOf('.vue')>-1){ //发现是.vue单文件组件,判断 1.无参数生成:template + 当前script输出 2.有参数template 把template转成render函数 
      const p = path.resolve(__dirname, url.split('?')[0].slice(1))
       // 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
      const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8')) 
      let body = ''
      if(!query.type){ // 1.无参数生成:template + 当前script输出 , 此版本只支持传统的options 写法 即 export default {  data() {},methods: {} } 
        ctx.type = 'application/javascript'  

        if(descriptor.styles && descriptor.styles.length > 0) { //如果有style ,也输出 import url?type=style
            body = body + ` import  "${url}?type=style"` 
        }
        //template + 当前script输出
        body = body + `
            ${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
            import { render as __render } from "${url}?type=template"
            __script.render = __render
            export default __script;
        `  
       
        ctx.body = body 
      }else if(query.type==='template'){ // 2.有参数template 把template转成render函数  
        const template = descriptor.template 
        const render = compilerDom.compile(template.content, {mode:"module"}).code
        ctx.type = 'application/javascript'
        ctx.body = rewriteImport(render) // 继续递归处理import内容
      }else if(query.type==='style'){ // 3.有参数style 把template转成render函数  
        body = body + `
            let link = document.createElement('style')
            link.setAttribute('type', 'text/css')
            document.head.appendChild(link)
            let css2 = \`${descriptor.styles.map(o => o.content).join('\n')}\`
            link.innerHTML = css2 
            `
        ctx.type = 'application/javascript'
        ctx.body = body 
      }
    } else if(url.endsWith('.css')){ // 如果是css 生成style标签插入到html 
        const p = path.resolve(__dirname,url.slice(1))
        const file = fs.readFileSync(p,'utf-8')
        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 
        `
        ctx.type = 'application/javascript'
        ctx.body = content
      }

})
 


function rewriteImport(content){ //此方法用于 替换请求文件的路径 改成/@modules下面
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0,s1){
    // . ../ /开头的,都是相对路径
    if(s1[0]!=='.'&& s1[1]!=='/'){
      return ` from '/@modules/${s1}'`
    }else{
      return s0
    }
  })
}



app.listen(10086, ()=>{
  console.log('服务启动')
})

最终请求效果

Nov-01-2024 14-51-00.gif

扩展-热更新

通过WebSocket 实时通知客户端更新页面数据


const WebSocket = require('ws');
const chokidar = require('chokidar');


....
const server = app.listen(10086, ()=>{
  console.log('服务启动')
})



const wss = new WebSocket.Server({ server });

// 向所有客户端发送刷新通知
function broadcast(message) {
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// 使用 chokidar 监听文件
chokidar.watch(['./*.js', './*.vue'], {
    ignored: /node_modules/,
    persistent: true,
    usePolling: true,
    interval: 100, // 设置轮询间隔,单位是毫秒
  }).on('change', (path) => {
    console.log(`File ${path} has been changed`);
    // 执行重载或其他操作
  });
 

wss.on('connection', ws => {
  ws.on('message', message => console.log('received:', message));
});

main.js添加监听

 const socket = new WebSocket('ws://localhost:10086');
  socket.onmessage = (event) => {
    if (event.data === 'reload') {
      location.reload();
    }
  };

源码地址

github.com/mjsong07/mi…

参考

玩转vue3