2023.20 vite源码学习-mini-vite实现(三)

lxf2023-08-08 15:50:01

大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。有想法的同学也可以加我微信进行交流:hp1256003949

写一个mini-vite

上节看了vite是怎么加载文件的,本节使用koa做服务器实现一个mini-vite

实现思路:首先这边所有文件都应该是服务器返回的,我们要针对不同的请求类型进行区别处理,例如:如果请求的是/则返回index.html项目的入口文件,且入口文件的js文件引入需要设置成type="module"以此来让浏览器支持es module语法

如果请求的是js文件,解析js文件,如果import的模块是相对路径,则说明是项目路径下的文件,则请求该文件,如果不是说明是引用的node_modules下的模块,然后需要读取对应的node_modules下的package.json文件,然后获取到入口文件,然后在进行加载(在此过程中如果继续需要了node_modules中的模块,依旧照此进行处理);

如果请求的是vue文件,我们需要在服务端使用vue的模块编译将代码编译成ast,其中ast中可以获取到templatescriptstyle三部分的模块解析并且需要重新发送一个请求以此来处理模块的渲染。针对script模块自定义一个变量并将其导出,针对template模块,直接对代码进行拼接返回,针对style模块,也是理应对应的loader或者模块分析器进行处理,我的例子中只写了对templatescript的处理。

实现服务器搭建

新建一个项目mini-vite,打开目录,对项目进行初始化

npm init一路回车,然后配置scripts


 "scripts": {
    "serve": "node ./vite/index.cjs"
  },

在平级目录下新建vite/index.cjs表明是服务端代码,搭建服务

npm i koa

const Koa = require("koa")
//创建server
function createServer() {
    const app = new Koa()  
    app.use((ctx)=>{
        ctx.body = 'Hello World';
    })
    app.listen(3000)
    console.log("listen:", `http://127.0.0.1:3000`)
}
//创建服务
createServer()

2023.20 vite源码学习-mini-vite实现(三)

说明服务已经启动成功了。

实现文件的访问

直接在服务器处理里面加判断呗

  app.use(async (ctx) => {
    const url = ctx.url
    if (url === "/") {
      //解析html文件获取依赖
      const html = fs.readFileSync(resolvePath(root, "./index.html"))
      ctx.type = "html"
      ctx.body = html
    }
  })
​

因为html文件里面引入了main.js但是我们没有对js文件进行处理

2023.20 vite源码学习-mini-vite实现(三)

处理js文件

  //判断是否是js
  app.use(async (ctx) => {
    const url = ctx.url
​
    if (url.endsWith(".js")) {
      //处理js请求
      const file = resolveFileName(ctx.url)
      let text = ""
      if (url.indexOf("node_modules")) {
        text = fs.readFileSync(resolvePath(root, `./${url}`, "utf-8"))
      } else {
        text = fs.readFileSync(resolvePath(root, `./${file}`, "utf-8"))
      }
      //读取到vue文件时,没有时去node_modules中查找
      ctx.type = "application/javascript"
      ctx.body = text.toString()
    }
  })

2023.20 vite源码学习-mini-vite实现(三)

因为我们main.js中引入了vue,而浏览器只能根据相对路径去读取文件,加载vue文件需要到node_modules中去查找

main.js文件


import { createApp, h } from "vue"createApp({
  render() {
    return h("div", "hello vite")
  },
}).mount("#app")
​
console.log("hello world")
​

处理node_modules模块引入问题rewriteImport方法,将匹配到的import xxx from "xxx"替换为import xxx from "/@modules/xxx"

function rewriteImport(content) {
  //匹配所有 import xxx from "xxx"
  return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) {
    if (s2.startsWith("./") || s2.startsWith("/") || s2.startsWith("../")) {
      //相对路径
      return s1
    } else {
      return ` from '/@modules/${s2}'`
    }
  })
}
​
//处理js的地方,返回内容的时候需要重写一下import 语句
   //判断是否是js
  app.use(async (ctx) => {
    const url = ctx.url
​
    if (url.endsWith(".js")) {
      //处理js请求
      const file = resolveFileName(ctx.url)
      let text = ""
      if (url.indexOf("node_modules")) {
        text = fs.readFileSync(resolvePath(root, `./${url}`, "utf-8"))
      } else {
        text = fs.readFileSync(resolvePath(root, `./${file}`, "utf-8"))
      }
      //读取到vue文件时,没有时去node_modules中查找
      ctx.type = "application/javascript"
      ctx.body = rewriteImport(text.toString())
    }
  })

node_modules中模块替换后,又提示找不到@modules中的资源

2023.20 vite源码学习-mini-vite实现(三)

处理@modules资源

app.use(async (ctx) => {
    const url = ctx.url
    if (url.startsWith("/@modules/")) {
      const moduleName = url.replace("/@modules/", "")
      const prefix = path.join(__dirname, "../node_modules", moduleName)
      // package.json中获取moduels字段
      const module = require(prefix + "/package.json").module
      const filePath = path.join(prefix, module)
      const ret = fs.readFileSync(filePath, "utf-8")
      ctx.type = "application/javascript"
      ctx.body = rewriteImport(ret)
    }
  })

然后就能看到效果了。

2023.20 vite源码学习-mini-vite实现(三)

看下如果main.js中引入.vue文件呢,会发现加载不到App.vue,因为没有处理.vue文件

2023.20 vite源码学习-mini-vite实现(三)

处理.vue文件

 app.use(async (ctx) => {
    const url = ctx.url
    (url.endsWith(".vue")) {
      //需要将vue文件中template转换成ast
      const filePath = resolvePath(root, `./${url}`)
      const content = fs.readFileSync(filePath, "utf-8")
      ctx.type = "text/javascript"
      ctx.body = content
    }
  })

确实是返回App.vue文件了,但是浏览器不识别

2023.20 vite源码学习-mini-vite实现(三)

2023.20 vite源码学习-mini-vite实现(三)

所以需要利用vue的编译模块对读取到的文件内容进行编译,生成一个ast

const {
  parse,
  compileTemplate,
} = require("../node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs")
app.use(async (ctx) => {
    const url = ctx.url
     if (url.endsWith(".vue")) {
      //需要将vue文件中template转换成ast
      const filePath = resolvePath(root, `./${url}`)
      const content = fs.readFileSync(filePath, "utf-8")
      const contentAst = parse(content)
      console.log(contentAst)
      const scriptContent = contentAst.descriptor.script
        ? contentAst.descriptor.script.content
        : null
​
      // const script = scriptContent.replace("export default","const _script = ")
​
      //转换script\template\style
      //script部分放在app.vue中,template部分放在aap.vye?type=template中
​
      let code = ``
​
      if (contentAst.descriptor.script.content) {
        // code += contentAst.descriptor.script.content
        code += contentAst.descriptor.script.content.replace(
          /((?:^|\n|;)\s*)export default/,
          "$1const __script="
        )
      }
      //将content转成render函数
      if (contentAst.descriptor.template.content) {
        // code += `import './App.vue?type=template'`
        const requestPath = ctx.url + `?type=template`
        code += `\nimport {render as __render} from '${requestPath}'`
        code += `\n__script.render=__render`
      }
      code += `\nexport default __script`
      ctx.type = "text/javascript"
      ctx.body = content
    }
  
  })

使用parse生成ast后的内容

2023.20 vite源码学习-mini-vite实现(三)

上面只是生成了ast,我们还需要将模板编译render函数,因此需要在上面单独对type=template类型的文件进行处理,这里type=template是我们自己标记的,用来标识是模板文件

处理type=template文件,compileTemplate用来将模板编译成render函数

 app.use(async (ctx) => {
    const url = ctx.url
    if (ctx.request.query.type === "template") {
      let queryUrl = ctx.url.indexOf("?")
      let fileUrl = ctx.url.slice(0, queryUrl)
      console.log("vue template request")
      const filePath = resolvePath(root, `./${fileUrl}`)
      let content = fs.readFileSync(filePath, "utf-8")
​
      const contentAst = parse(content)
      let { code } = compileTemplate({
        source: contentAst.descriptor.template.content,
        id: "app",
      })
      ctx.type = "text/javascript"
      ctx.body = rewriteImport(`${code}`)
    }
  })

type=template返回内容

2023.20 vite源码学习-mini-vite实现(三)

至此处理.vue文件终于成功了

2023.20 vite源码学习-mini-vite实现(三)

完整代码参考


next 这节用了vueparsecompilerTemplate函数,下节学习一下这两个函数

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!