手写vite

372 阅读2分钟

Vite是一个基于浏览器原生 ES Modules 开发的服务器。利用浏览器去解析模块,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。

vite运行原理

通过此流程图可得: vite是借助了node的koa模块,启动一个静态服务器,通过Koa的中间件, 完成了设置静态资源文件, 解析import语句,解析@module模块,解析vue文件等功能, 本文讲会带领大家手写一个toyVite, 完成上述类似的功能。

2.创建一个koa服务器

const Koa = require('Koa')
const { serverStaticPlugin } = require('./plugins/serverStaticPlugin')
const { moduleRewritePlugin } = require('./plugins/moduleRewritePlugin')
const { moduleResolvePlugin } = require('./plugins/moduleResolvePlugin')
const { htmlRewritePlugin } = require('./plugins/htmlRewritePlugin')
const { vuePlugin } = require('./plugins/vuePlugin')
function createServer() {    
    const app = new Koa() 
    // 进程当前工作路径    
    const root = process.cwd() 
    const context = { 
       app, // koa实例 
       root,
    }    
     const resolvedPlugins = [
        htmlRewritePlugin, // Html文件重写 
        moduleRewritePlugin, // 导入模块重写       
        moduleResolvePlugin, // 模块解析
        vuePlugin, // .vue文件解析
        serverStaticPlugin, // 静态服务器启动
    ]
    resolvedPlugins.forEach(fn => fn && fn(context)) // 遍历执行中间件
    return app
}
module.exports = createServer

3. serverStaticPlugin 设置静态服务器

const static = require("koa-static");
const path = require("path");
function serverStaticPlugin({ app, root }) {
  app.use(static(root));
  app.use(static(path.join(root, "public")));
 }
exports.serverStaticPlugin = serverStaticPlugin;

通过koa-static设置当前目录下和当前目录下public文件作为静态服务。

4.moduleRewritePlugin 模块解析

先看看两张对比图, 图一是项目文件中我们手写的main.js, 经过vite的模块重写, 

import { createApp } from 'vue' 就变成了 import { createApp } from '/@modules/vue'

由此我们可以得出,此模块的主要功能就是重写 import 导入, 把非项目内的导入重新成以/@modules为开头的导入,具体代码如下。

const { readBody } = require("./utils");
const MagicString = require("magic-string");
const { parse } = require("es-module-lexer");
// 重写import
function rewriteImport(source) {
  let imports = parse(source)[0];  // 获取字符串里面的import内容
  let magicString = new MagicString(source); // 字符串转换成魔法字符串
  if (imports.length) {
    for (let i = 0; i < imports.length; i++) {
      let { s, e } = imports[i];  // 如果 import a from 'bc'; {s: 起始位置, e: 结束位置}
      let id = source.substring(s, e);
      if (/^[^\/\.]/.test(id)) { // 如果非/或者非.开头
        id = `/@modules/${id}`;
        magicString.overwrite(s, e, id);
      }
    }
  }
  return magicString.toString();
}
// 模块重写插件
function moduleRewritePlugin({ app, root }) {
  app.use(async (ctx, next) => {
    await next();
    if (ctx.body && ctx.response.is("js")) {
      let content = await readBody(ctx.body);
      const result = rewriteImport(content);
      ctx.body = result;
    }
  });
}
exports.moduleRewritePlugin = moduleRewritePlugin;

async function readBody(stream) {
  if (stream instanceof Stream) {
    return new Promise((resolve, reject) => {
      let res = "";
      stream.on("data", (data) => (res += data));
      stream.on("end", () => resolve(res));
    });
  }
  else {
    return stream.toString();
  }
}

此中间件判断如果是js文件的话,获取content字符串, 然后通过rewriteImport方法重写字符, 主要使用了es-module-lexer获取js文件中import的具体位置,通过magic-string重写字符串。vite官方也是使用这两个库。

5.moduleResolvePlugin - 解析@modules开头的文件

  1. 正则匹配拿到**@modules/后面具体需要的文件类型id****。**

  2. 拿到node_modules中全都的解析模块的集合

  3. 根据文件类型去解析集合中找到对应的模块都去并返回文件内容

    const moduleREG = /^/@modules//; const fs = require("fs"); const { resolveVue } = require("./utils") // function moduleResolvePlugin({ app, root }) { const vueResolved = resolveVue(root); app.use(async (ctx, next) => { if (!moduleREG.test(ctx.path)) { return next(); } const id = ctx.path.replace(moduleREG, ""); ctx.type = "js"; const content = fs.readFileSync(vueResolved[id], "utf8"); ctx.body = content; }); }

    exports.moduleResolvePlugin = moduleResolvePlugin;

    function resolveVue(root) { const compilerPkgPath = path.join( root, "node_modules", "@vue/compiler-sfc/package.json" ); const compilerPkg = require(compilerPkgPath); const compilerPath = path.join( path.dirname(compilerPkgPath), compilerPkg.main ); const resolvePath = (name) => path.resolve( root, "node_modules", @vue/${name}/dist/${name}.esm-bundler.js ); const runtimeDomPath = resolvePath("runtime-dom"); const runtimeCorePath = resolvePath("runtime-core"); const reactivityPath = resolvePath("reactivity"); const sharedPath = resolvePath("shared"); return { compiler: compilerPath, "@vue/runtime-dom": runtimeDomPath, "@vue/runtime-core": runtimeCorePath, "@vue/reactivity": reactivityPath, "@vue/shared": sharedPath, vue: runtimeDomPath, }; }

6. vuePlugins 

const fs = require("fs");
const { resolveVue } = require("./utils");
const defaultExportRE = /((?:^|\n|;)\s*)export default/;
function vuePlugin({ app, root }) {
  app.use(async (ctx, next) => {
    if (!ctx.path.endsWith(".vue")) {
      return next();   
    }    
    const filePath = path.join(root, ctx.path);
    const content = fs.readFileSync(filePath, "utf8");
    let { parse, compileTemplate } = require(resolveVue(root).compiler); //获取vue的解析和模版编译
    let { descriptor } = parse(content);
    if (!ctx.query.type) {
      let code = "";
      if (descriptor.script) {
        let content = descriptor.script.content;
        let replaced = content.replace(defaultExportRE, '$1const __script =')
        code += replaced
      }
      if (descriptor.template) { 将路径加上type=template参数,再次发起请求
        const templateRequest = ctx.path + '?type=template'
        code += `\nimport {render as __render} from ${JSON.stringify(templateRequest)}`
        code += `\n__script.render = __render`
      }
      ctx.type = 'js'
      code += `\nexport default __script`
      ctx.body = code
    }
    if (ctx.query.type == 'template') { type=template时利用compileTemplate将模版解析成render函数
      ctx.type = 'js'
      let content = descriptor.template.content
      const { code } = compileTemplate({ source: content })
      ctx.body = code
    }
  });
}
exports.vuePlugin = vuePlugin;

7. htmlRewritePlugin

const { readBody } = require('./utils')
function htmlRewritePlugin({ app, root }) {
  const inject = `
    <script>
      window.process = {}
      process.env = {
        NODE_ENV: "development"
      }
    </script>  `
  app.use(async (ctx, next) => {
    await next()
    if (ctx.response.is('html')) {
      const html = await readBody(ctx.body)
      ctx.body = html.replace(/<head>/, `$&${inject}`)
    }
  })
}
exports.htmlRewritePlugin = htmlRewritePlugin

此模块主要是增加环境变量, 然后vite源码中HRM热更新也是在此处替换进去。热更新主要就是借助websocket通过监听源文件的变动,通过不同的命令通知浏览器进行对应的改变。

8. 执行文件

/bin/index.js

#!/usr/bin/env node //设置执行环境

const createServer = require('../index')

createServer().listen(4000, () => {
    console.log('cx-vite: start')
})

然后在package.json里面增加一条命令

"bin": {
    "cx-vite": './bin/index.js'
}

在通过yarn link 关联到全局,这样你就可以在你的vite-app通过此cx-vite 命令启动。