手写vite

195 阅读3分钟

项目代码

装包

npm i vue koa @vue/compiler-dom @vue/compiler-sfc @vue/composition-api

    "@vue/compiler-dom": "^3.2.47",
    "@vue/compiler-sfc": "^3.2.47",
    "@vue/composition-api": "^1.7.1",
    "koa": "^2.14.1",
    "vue": "^3.2.47"

整体流程:

  • 启动koa服务,vite就是一个http服务,解析各种资源文件

  • 利用现代浏览器,支持script标签解析 tpe=module,核心就是利用现代浏览器能直接处理esm规范文件

  • 全局注入process.end.NODE_ENV变量,要啥补啥,在index.html中写入

  • 项目源代码main.js文件代码内容,项目源代码App.vue文件内容

  • 处理 /@module/vue 包引入

  • 处理 .css 文件,就是加载css文件内容,用js创建style标签,设置innerHTML

  • 处理 .vue 文件,SFC格式,要通过 @vue/compiler 编译,取出 template,script,style各个部分处理成js导出函数

  • 继续处理 type=template ,使用@vue/compiler-dom包编译template,注意参数mode: module

  • 处理 type=css,直接读取css内容,继续变为js创建style节点

启动koa服务,vite就是一个http服务,解析各种资源文件

const Koa = require('koa')
const app = new Koa()
app.use(context => {
        context.type = 'application/javascript'
        context.body = 'hello world'
})
app.listen(3000, () => {
    console.log('koa start at 3000')
})

利用现代浏览器,支持script标签解析 tpe=module,核心就是利用现代浏览器能直接处理esm规范文件

// index.html文件中直接引入main.js
<script type="module" src="main.js"></script>

全局注入process.end.NODE_ENV变量,要啥补啥,在index.html中写入

<script>
      window.process = {
        env: {
          NODE_ENV: 'development'
        }
      }
      window.__VUE_OPTIONS_API__ = true
      window.__VUE_PROD_DEVTOOLS__ = true
</script>

项目源代码main.js文件代码内容

import { createApp, h } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')

项目源代码App.vue文件内容

<template>
  <div>
    <h1>
      {{ count }}
    </h1>
    <button @click="add">点击</button>
  </div>
</template>
<script>
import { ref } from 'vue'
import { defineComponent } from '@vue/composition-api'

export default defineComponent({
  setup() {
    const count = ref(0)
    function add() {
      count.value++
    }
    return {
      count,
      add
    }
  }
})
</script>

<style>
h1 {
  color: red;
}
</style>

首先让koa服务,能解析index.html文件,返回html内容

const Koa = require('koa')
const path = require('path')
const fs = require('fs')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
const app = new Koa()
app.use(context => {
    const { request: { url, query } } = context
    const routePath = url.split('?')[0]
    
    // 处理 index.html 内容返回
    if (routePath == '/') {
        const content = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')
        context.type = 'text/html'
        context.body = content
    }
})
app.listen(3000, () => {
    console.log('koa start at 3000')
})

此时,可以看到index.html内容已经返回到页面上,但是报错 main.js 是 404

image.png

处理 main.js 这类js文件 404 的载入问题,后端处理js文件,找到路径并返回

添加 .js 文件处理

else if (routePath.endsWith('.js')) {
        const content = fs.readFileSync(path.resolve(__dirname, routePath.slice(1)), 'utf-8')
        context.type = 'application/javascript'
        context.body = replaceImportModule(content)
    }

此时看到,main.js内容返回了,但是页面报错

image.png

function replaceImportModule(code) {
    return code.replace(/([import|export]) (.*) from ['|"]([^'"\.]+)['|"]/g, function (a, b, c, d) {
        return `${b} ${c} from '/@module/${d}'`
    })
}

原因是,main.js 中引入包 这句import { createApp, h } from 'vue',浏览器无法识别,这里我们要改为 import { createApp, h } from '/@module/vue' ,让浏览器再次请求 /@module/vue 这个文件,后端再次处理

所以添加函数 replaceImportModule 修改包引入这种代码的格式

处理 /@module/vue 包引入

在 /@module/vue 这个路径下,用 @module的表明是要在 node_modules 目录下找到vue包,然后根据 package.json文件的module字段,找到实际esm的文件

else if (routePath.startsWith('/@module')) {
        const moduleName = routePath.replace('/@module/', '')
        const modulePath = path.resolve(__dirname, 'node_modules', moduleName)
        console.log(modulePath)
        const pkg = require(path.join(modulePath, 'package.json'))
        const moduleEsmPath = path.join(modulePath, pkg.module)
        const content = fs.readFileSync(moduleEsmPath, 'utf-8')
        context.type = 'application/javascript'
        context.body = replaceImportModule(content)
 }

可以看到,vue包和其他内部包也找到了

image.png

处理 .css 文件,就是加载css文件内容,用js创建style标签,设置innerHTML


    else if (routePath.endsWith('.css')) {
        const content = fs.readFileSync(path.resolve(__dirname, routePath.slice(1)), 'utf-8')
        context.type = 'application/javascript'
        context.body = `
            const style = document.createElement('style')
            style.setAttribute('type','text/css')
            style.innerHTML = \`${content}\`
            document.body.append(style)
        `
    }

可以看到在main.js中css文件引入也解决了

image.png

处理 .vue 文件,SFC格式,要通过 @vue/compiler 编译,取出 template,script,style各个部分处理成js导出函数

通过 @vue/compiler-sfc 的parse函数,编译后的 descriptor 对象,里面有script,template,styles对象,就表示各个部分,先只能导出 script部分,template和styles都还是使用import的方式,等待二次请求再处理

else if (routePath.indexOf('.vue') > -1) {
            const sfcFileContent = fs.readFileSync(path.resolve(__dirname, routePath.slice(1)), 'utf-8')
            const { descriptor } = compilerSfc.parse(sfcFileContent)
            console.log(descriptor)
            let content = descriptor.script.content.replace('export default defineComponent', `
        import { render } from '${routePath}?type=template'\r\n
        import '${routePath}?type=css'\r\n
        const script = defineComponent`)
            content += `
        script.render = render\r\n
        export default script\r\n`
            context.type = 'application/javascript'
            context.body = replaceImportModule(content)
        }

相当于比如请求 App.vue 文件

经过上面之后返回的内容就是

import { render } from './App.vue?type=template'
import './App.vue?type=css'
const script = {
    setup(){
        ...
    }
}
script.render = render
export default script

继续处理 type=template ,使用@vue/compiler-dom包编译template,注意参数mode: module

else if (query.type == 'template') {
            const sfcFileContent = fs.readFileSync(path.resolve(__dirname, routePath.slice(1)), 'utf-8')
            const { descriptor } = compilerSfc.parse(sfcFileContent)
            const template = compilerDom.compile(descriptor.template.content, { mode: "module" })
            context.type = 'application/javascript'
            context.body = replaceImportModule(template.code)
        }

这个请求就是上面 import { render } from './App.vue?type=template' 触发的,也要注意是使用 replaceImportModule,因为内部也有包引入

处理 type=css,直接读取css内容,继续变为js创建style节点

else if (query.type == 'css') {
           const sfcFileContent = fs.readFileSync(path.resolve(__dirname, routePath.slice(1)), 'utf-8')
           const { descriptor } = compilerSfc.parse(sfcFileContent)
           const content = descriptor.styles.reduce((code, it) => {
               return code + it.content
           }, '')
           context.type = 'application/javascript'
           context.body = `
               const style = document.createElement('style')
               style.innerHTML = \`${content}\`
               document.body.append(style)
           `
       }

效果

image.png

剩余,处理 scss 其他文件格式,处理 jsx ,ts 等,相同编译,另外一个重点是热更新

在实际vite中,有几点不同:

  • 不是if else 这么写是通过中间件处理不同格式

  • vite开发环境通过esbuild的能力处理jsx和ts、js 编译成 es6

  • vite生产环境是rollup打包