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"

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

然后对比 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试一下,启动服务器

<!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>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <!-- <script type="module" src="/src/main.js"></script> -->
  <script>
    alert(2)
  </script>
</body>
</html>

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('<script', `
      <script>
        window.process = {
          env: {
            NODE_ENV: 'dev'
          }
        }
      </script>
      <script
    `)
    
    ctx.type = "text/html"
    ctx.body = content
  }

然后再来页面看一下


image.png

.vue当文件组件的解析

由于.vue文件 浏览器是不认识的,浏览器再import中只认识js

  1. 我们把 .vue文件 拆开为 script ,template (由于app.vue中没有css,这里不处理)
  2. template 转换为 render函数 拼成一个对象
  3. script.render = render
    看一下vite处理的app.vue


    image.png

    app.vue

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

解析单文件 单文件组件, 需要官方的库 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文件中 图片的引入改为绝对引用

<template>
  <img alt="Vue logo" src="/src/assets/logo.png" />
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>
 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) {
  // <img alt="Vue logo" src="./assets/logo.png" />
  fileContent = fileContent.replace(/<img(.*)src=['|"](.+)['|"]/, function (s0, s1, s2) {

    if (s1[0] !== '.') {
      return `<img${s1}src='${path.resolve('./src/', s2)}'`.replace(__dirname, '')
    } else {
      return s0
    }
  })

  return fileContent;
}


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

    content = content.replace('<script', `
      <script>
        window.process = {
          env: {
            NODE_ENV: 'dev'
          }
        }
      </script>
      <script
    `)

    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 = rewriteImport(content)
  }
  // 处理 /@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/javascript'

    ctx.body = rewriteImport(ret)
  }

  // 处理 .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 为 template 
    else if (query.type === 'template') {
      // 解析我的template 变成 render 函数
      const template = descriptor.template
      const render = complierDom.compile(replaceSrc(template.content), { mode: 'module' }).code

      ctx.type = 'application/javascript'

      ctx.body = rewriteImport(render)
    } 
    //  处理 .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
    }
  } 
  //  处理 png 图片
  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
  }
  //  这里还可以支持 .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
  }
})

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



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容