vue3 简陋的实现vite

vue3 最大的优点: 编译时的优化

vite 是一个基于 Vue3 单文件组件的非打包开发服务器,它做到了本地快速开发启动:

  1. 快速的冷启动,不需要等待打包操作;
  2. 即时的热模块更新,替换性能和模块数量的解耦让更新飞起;
  3. 真正的按需编译,不再等待整个应用编译完成,这是一个巨大的改变。

由于现代浏览器都支持es6的import;
import XX from './a.js' 时 浏览器会发出一个网络请求
vite会拦截这个请求,去做vue相关的编译、解析等,这样就实现了按需加载的能力
快的原因 是不用 打包

vite有啥用

  1. vue3配套的工具, 下一代脚手架工具
  2. 掌握vue3 代码编译的流程(使用层面)

原理:

在html中 script链接上要增加type="module"


然后对比 main.js
代码

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

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

然后查看浏览器 看返回 main.js

import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app')
  1. 将vue引用转化为/@modules/vue.js
  2. 将./App.vue转换为/src/App.vue
  3. 将./index.css转化为/src/index.css?import

/@modules/vue.js会新发起一个网络请求 http://localhost:3000/@modules/vue.js

image.png

直接返回内容;

实现vite效果

创建一个server.js

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

const app = new Koa()

app.use(ctx => {
  const { request: { url, query } } = ctx
  // 访问根目录 渲染index.html
  if (url === '/') {
    // 读取文件
    let content = fs.readFileSync('./index.html', 'utf-8')
    ctx.type = "text/html"
    ctx.body = content
  }
})

app.listen(9092, () => {
  console.log('listen 9092');
})

然后把html中 alert试一下,启动服务器




  
  
  
  Vite App


  

nodemon server.js , 打开http://localhost:9092/
页面弹出了alert, 怎么启动成功

下面我们把 alert(2)去掉,把注释的main.js放开,可以看到页面报错了


image.png

处理一下 server.js 中处理 js文件

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

const app = new Koa()

app.use(ctx => {
  const { request: { url, query } } = ctx
  // 访问根目录 渲染index.html
  if (url === '/') {
    // 读取文件
    let content = fs.readFileSync('./index.html', 'utf-8')
    ctx.type = "text/html"
    ctx.body = content


  }
  // 处理js 文件 
  else if (url.endsWith('.js')) {
    // 把 / 干掉
    const _path = path.resolve(__dirname, url.slice(1))
    ctx.type = "application/javascript"
    const content = fs.readFileSync(_path, 'utf-8')
    ctx.body = content
  }
})

app.listen(9092, () => {
  console.log('listen 9092');
})
image.png

现在main.js 已经处理过来了,但是页面有报错提示, 用import时 来源必须用 "/". "./", "../"。
而在 main.js中引入 import { createApp } from 'vue'没有用这种方式。

目前我们还要处理几种情况

  1. 支持 npm包的import
  2. 支持.vue当文件组件的解析
  3. 支持import css

处理npm 包 的import

这里我们就规定 : 不是以 / ./ ../ 开头的,那么就来着 node_modules
首先我们把main.js中的
import { createApp } from 'vue' 中vue 替换为 '/@modules/vue.js'
我们再server.js中增加一个函数 处理

//  目的是把 不是 / ./ ../开头的import 改造成 /@modules/开头
function rewriteImport(content){
  return content.replace(/ from ['|"]([^'"]+)['|"]/g , function(s0,s1){
    console.log('rewriteImport', s0, s1);
    if(s1[0] !== '.' && s1[0] !== '/'){
      return ` from '/@modules/${s1}'`
    } else {
      return s0
    }
  })
}

然后再处理 js文件时 把content内容调用函数处理

  else if (url.endsWith('.js')) {
    // 把 / 干掉
    const _path = path.resolve(__dirname, url.slice(1))
    ctx.type = "application/javascript"
    const content = fs.readFileSync(_path, 'utf-8')
    ctx.body = rewriteImport(content)
  }

再看一下浏览器控制台


image.png

说明我们处理成功了,但是发起请求 时404.
此时, koa监听得到 /@modules开头的网络请求时,我们就去node_modules里面去查找

这里要明白 在 node_modules里面我们要找的是什么。 比如


image.png

这里我们就是去 找的 对应文件下的package.json中 module对应的路径(main 对应的是 require的, module对应的es6 import)

好了,我们继续再server.js中增加判断

  // 处理 /@modules/开头 
  // 这个模板 不是本地文件,而是 node_modules中
  else if (url.startsWith('/@modules/')) {
    // 拿到文件 路径的前缀
    const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))

    // 拿到 文件下 package.json中 module 对应的路径
    const module = require(prefix + '/package.json').module

    // 获取完整路径
    const p = path.resolve(prefix, module)

    const ret = fs.readFileSync(p, 'utf-8')

    ctx.type = 'application/x-javascript'

    ctx.body = rewriteImport(ret)
  }
image.png

(main.js中 先只引入 import { createApp } from 'vue')

此时我们可以拿到vue,并且通过vue拿到vue依赖的其他文件。但是现在有个问题 浏览器中是没有process存在的。
我们简单粗暴的 在server.js中处理一下, 在window对象上挂载一个process 变量, 在url === '/' 中增加一个script

 if (url === '/') {
    // 读取文件
    let content = fs.readFileSync('./index.html', 'utf-8')

    content = content.replace('
        window.process = {
          env: {
            NODE_ENV: 'dev'
          }
        }
      
      

解析单文件 单文件组件, 需要官方的库 npm i @vue/complie-sfc -D
然后再server.js中引入

const complierSfc = require('@vue/compiler-sfc')

然后我们处理.vue文件

  // 处理 .vue文件
  else if (url.indexOf('.vue') > -1) {
    // import  xx from 'xx.vue'
    // 处理类似 可能有 "/src/App.vue?type=template"
    const p = path.resolve(__dirname, url.split('?')[0].slice(1))

    // 解析单文件组件, 需要官方的库 npm i @vue/complie-sfc -D
    const { descriptor } = complierSfc.parse(fs.readFileSync(p, 'utf-8'))
    console.log('descriptor', descriptor);
  }

我们可以看到 descriptor 返回有模板信息


image.png

以及 script 信息


image.png

现在我们处理 js

else if (url.indexOf('.vue') > -1) {
    // import  xx from 'xx.vue'
    // 处理类似 "/src/App.vue?type=template"
    const p = path.resolve(__dirname, url.split('?')[0].slice(1))

    // 解析单文件组件, 需要官方的库 npm i @vue/complie-sfc -D
    const { descriptor } = complierSfc.parse(fs.readFileSync(p, 'utf-8'))
    // js内容
    if(!query.type){
      ctx.type = 'application/x-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
      `
    }
  }
image.png

单文件的js处理好了。接下来处理 template
处理 template我们需要 npm i @vue/compiler-dom -D

const complierDom = require('@vue/compiler-dom')

然后再处理vue单文件时增加 对template的处理

else if (url.indexOf('.vue') > -1) {
    // import  xx from 'xx.vue'
    // 处理类似 "/src/App.vue?type=template"
    const p = path.resolve(__dirname, url.split('?')[0].slice(1))

    // 解析单文件组件, 需要官方的库 npm i @vue/complie-sfc -D
    const { descriptor } = complierSfc.parse(fs.readFileSync(p, 'utf-8'))
    // js内容
    if(!query.type){
      ctx.type = 'application/x-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'){
      // 解析我的template 变成 render 函数
      const template = descriptor.template
      const render = complierDom.compile(template.content, {mode: 'module'}).code
      ctx.type = 'application/x-javascript'

      ctx.body = rewriteImport(render)
    }
  }

image.png

现在 对应vue文件的 template也可以处理了。其中图片没有显示出来,这里简单处理
app.vue文件中 图片的引入改为绝对引用


 else if(url.endsWith('.png')){

    const _path = path.resolve(__dirname, url.slice(1))

    const file = fs.readFileSync(_path)
    ctx.type = "image/png"
    ctx.body = file
  }
image.png

支持import css

//  这里还可以支持 .scss .less .stylus .ts等 
  else if(url.endsWith('.css')){
    const  p = path.resolve(__dirname, url.slice(1))

    const file = fs.readFileSync(p, 'utf-8')

    // 处理换行
    const content = `
      const css = "${file.replace(/\n/g, '')}"
      const 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

处理.vue 文件中的style

// 处理 .vue文件
  else if (url.indexOf('.vue') > -1) {
    // import  xx from 'xx.vue'
    // 处理类似 "/src/App.vue?type=template"
    const p = path.resolve(__dirname, url.split('?')[0].slice(1))

    // 解析单文件组件, 需要官方的库 npm i @vue/complie-sfc -D
    const { descriptor } = complierSfc.parse(fs.readFileSync(p, 'utf-8'))
    // js内容
    if (!query.type) {
      // 有js
      if (descriptor.script) {
        ctx.type = 'application/javascript'
        ctx.body += `
          ${rewriteImport(descriptor.script.content.replace('export default ', 'const __script = '))}
          
        `
      }
      // 有style
      if (descriptor.styles) {
        descriptor.styles.forEach((s, i) => {
          const styleRequest = url + `?type=style&index=${i}`;
          ctx.body += `\nimport ${JSON.stringify(styleRequest)}`;
        })
      }
      // 有模板
      if (descriptor.template) {
        const templateRequest = url + `?type=template`;
        ctx.body += `\nimport { render as __render } from ${JSON.stringify(templateRequest)}`;
        ctx.body += `\n__script.render = __render`;
      }

      ctx.body += `
      export default __script
      `
    }

然后我们把 vue?type=style 处理一下

    //  处理 .vue文件 type 为 style
    else if (query.type === 'style') {
      const index = Number(query.index);
      const styleBlock = descriptor.styles[index];

      const content = `
      var style = document.createElement('style'); 
      style.type = 'text/css'; 
      style.innerHTML= ${JSON.stringify(styleBlock.content.replace(/\n/g, ''))}; 
      document.head.appendChild(style)
    `

      ctx.type = 'application/javascript'
      ctx.body = content
    }
  } 

完整的server.js

const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const complierSfc = require('@vue/compiler-sfc')
const complierDom = require('@vue/compiler-dom')

const app = new Koa()

//  目的是把 不是 / ./ ../开头的import 改造成 /@modules/开头
function rewriteImport (content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function (s0, s1) {

    if (s1[0] !== '.' && s1[0] !== '/') {
      return ` from '/@modules/${s1}'`
    } else {
      return s0
    }
  })
}


// 处理 img 中 的路径
function replaceSrc (fileContent) {
  // Vue logo
  fileContent = fileContent.replace(/ {
  const { request: { url, query } } = ctx
  // 访问根目录 渲染index.html
  if (url === '/') {
    // 读取文件
    let content = fs.readFileSync('./index.html', 'utf-8')

    content = content.replace('
        window.process = {
          env: {
            NODE_ENV: 'dev'
          }
        }
      
      
                    
                    

你可能感兴趣的:(vue3 简陋的实现vite)