vite

413 阅读2分钟

浏览器解析import

初始化一个项目vite-mini,只有html.index和src/main.js,src/log.js

// html.index
<body>
    <div id="app"></div>
    <script src="/src/main.js"></script>
</body>
// main.js
import log from "./log.js"
log("this is main.js");
// log.js
export default function log(message) {
    console.log("message:", message);
}

使用vscode自带的服务打开index.html 提示不能在模块外部使用import,说明当前script不是一个模块,那么在script标签加上 type="module"

报错消失了,main.js的代码输出到了控制台。说明**type="module"**让script标签变成了一个模块,打开Network

浏览器把import当做了一次网络请求,就像使用node读取一个js文件,把内容返回来。

用koa拦截浏览器请求

现在的页面访问是vscode服务,我们初始化一个koa的项目,启动koa的服务,前端访问koa的接口,让koa读取这个index.html

npm init -y && npm i koa

touch server.js

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

const js_type = "application/javascript"

app.use(async (ctx, next) => {
    const {request: {url}} = ctx
    if (url === "/") {
        let _path = path.resolve(__dirname, "index.html")
        let _content = fs.readFileSync(_path, "utf-8")
        ctx.body = _content
    } else {
        await next()
    }
})
app.use(async (ctx, next) => {
    const {request: {url}} = ctx
    if (url.endsWith(".js")) {
        let _path = path.resolve(__dirname, url.slice(1))
        let _content = fs.readFileSync(_path, "utf-8")
        ctx.type = js_type
        ctx.body = _content
    } else {
        await next()
    }
})
app.listen(9527, () => {
    console.log("koa.9527 ----> start")
})

现在就能正常加载main.js的内容了

实现目标

以上步骤:

  1. 浏览器是将import导入模块,解析成请求资源
  2. 我们可以用koa拦截浏览器的请求,读取资源,返回给前端

使用 vite 初始化一个demo,看下vite都做了什么。 将vite-demo/src/App.vue稍作修改

<template>
    <div>
        <h1>{{num}}, {{db}}</h1>
        <button @click="fnAdd">+</button>
    </div>
</template>

<script>
import {ref, computed} from "vue"
export default {
    setup() {
        const num = ref(0)
        function fnAdd() {
            num.value ++
        }
        const db = computed(() => num.value * 2)
        return {num, fnAdd, db}
    }
}
</script>
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

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

这就是我们的实现目标,将这两段代码贴到vite-mini/src下,能正常运行,就ok了

运行npm run dev打开Network:

右边的代码块,就是使用 import vue from "vue" 时,koa拦截到浏览器对App.vue请求时,返回浏览器的结果 对我们来说,写代码的地方就是.js、.vue、.css、.scss等

  1. 分析import包的来源,是node_modules,就解析成/@modules/
  2. 解析render

先把main.js、App.vue复制到vite-demo

解析node_modules包

在koa再加个中间件,解析vue包

import { createApp } from 'vue' => import { createApp } from '/@modules/vue.js'

其实main.js解析并返回给前端了,只是前端解析不了,先写个辅助函数

function fnReWriteImport(content) {
    return content.replace(/from ['"]([^'"]+)['"]/g, function(s0, s1) {
        // 只解析不带/的
        if (s1[0] !== "." && s1[0] !== "/") {
            return `from '/@modules/${s1}'`
        }
        return s0
    })
}

在此推荐一个正则的可视化工具,挺适合我这种菜鸡用的。还有更多的工具,去大圣的bi站找吧 将解析的js包起来

ctx.body = fnReWriteImport(_content)

先下载vue

npm i vue && vue add vue-next

再加个中间件,将/@modules/指向包在node_modules实际位置

使用import指向的是module,require是main

app.use(async (ctx, next) => {
    const {request: {url}} = ctx
    if (url.startsWith("/@modules/")) {
        let _page = `node_modules/${url.replace("/@modules/", '')}`
        // 读取包在node_modules的路径
        let _path = path.resolve(__dirname, _page)
        let _module = require(`${_path}/package.json`).module
        // 读取包的打包文件在node_modules的路径
        let _module_path = path.resolve(__dirname, _page, _module)
        let _module_content = fs.readFileSync(_module_path, "utf-8")
        ctx.type = js_type
        ctx.body = fnReWriteImport(_module_content)
    } else {
        await next()
    }
})

这个是没有设置node的环境变量,我们在第一个中间件加一段设置环境变量的脚本

_content = _content.replace("<script", `
<script>
    window.process = {env: {NODE_ENV: 'DEV'}}
</script>
<script`)
ctx.body = _content

稍微修改下main.js,测试下node_modules的解析是否成功

import { createApp } from '/@modules/vue'
console.log("createApp", createApp)

node_modules解析成功了

解析.vue文件

添加解析vue单文件的中间件

app.use(async (ctx, next) => {
    const {request: {url}} = ctx
    console.log("url", url)
    if (url.endsWith(".vue")) {
        let _path = path.resolve(__dirname, url.slice(1))
        let _content = fs.readFileSync(_path, "utf-8")
        ctx.body = _content
    } else {
        await next()
    }
})

没有报错,但这并不是我们想要的,需要用到@vue/compiler-sfc帮我来解析这个.vue文件

ctx.body = _content => ctx.body = compilerSfc.parse(_content)

包含了我们平时看到的script、template、styles等,这就是最终要组合的内容,我们先看下 fnReWriteImport(descriptor.script.content) 先用的内容

__script.__hmrId = "/src/App.vue"是热更新,后面补充,接下来组合一下这个返回结果

app.use(async (ctx, next) => {
    const {request: {url}} = ctx
    console.log("url", url)
    if (url.endsWith(".vue")) {
        let _path = path.resolve(__dirname, url.slice(1))
        let _content = fs.readFileSync(_path, "utf-8")
        let {descriptor} = compilerSfc.parse(_content)
        let _parse_content = fnReWriteImport(descriptor.script.content).replace("export default", "const __script =")
        ctx.type = js_type
        ctx.body = `
${_parse_content}
import {render as __render} from "${url}?type=template"
__script.render = __render
export default __script
        `
    } else {
        await next()
    }
})

因为我们处理的是.vue结尾的,所以上边要修改下,加上url?type=template的判断,报错的是这句:

import {render as __render} from "/src/App.vue?type=template"

import这个需要返回的是render函数,我们知道render函数式通过compiler编译来的,引入

npm i @vue/compiler-dom

使用@vue/compiler-dom来编译App.vue的内容

let _compiler_content = compilerDom.compile(descriptor.template.content, {mode: "module"})
ctx.type = js_type
ctx.body = fnReWriteImport(_compiler_content.code)

在前端看下输出:

可以正常使用了

解析.css

添加中间件

app.use(async (ctx, next) => {
    const {request: {url, query}} = ctx
    if (url.endsWith(".css")) {
        const p = path.resolve(__dirname,url.slice(1))
        let css = fs.readFileSync(p,'utf-8')
        css = css.replace(/<\/?.+?>/g,"") 
        css = css.replace(/[\r\n]/g, "")
        ctx.type = js_type
        ctx.body = `let css = "${css}"
let _style = document.createElement("style")
_style.innerHTML = css
document.head.appendChild(_style)`
    } else {
        await next()
    }
})