洞悉 Vite 原理,创建自己的 Vite 并发布到npm

121 阅读2分钟

代码已上传 github 和 npm

github地址 npm地址

简述

基于 esModule 的 Vite 现在是很火的技术,我们来探究下它的实现原理

使用方法

下载

可全局安装

npm install cool-vite

启动

npx cool-vite

流程梳理

Vite 本质上是一个本地服务器
因为 浏览器已原生支持 ES 模块,所以根据浏览器的请求进行处理,返回处理后的文件,就能实现一个简单的 Vite

具体流程

  1. 使用 koa 搭建本地服务
  2. 拦截请求 使用不同文件对应的 plugin 进行处理
  3. 返回 plugin 处理后的文件

代码步骤

项目结构

1666683858515-image.png

1. 使用 KOA 搭建文件服务器

index.js

#!/usr/bin/env node
const Koa = require('koa');
const app = new Koa();

app.use(async (ctx) => {
  const { url } = ctx.request;
  const content = url;
  ctx.body = content;
});

app.listen(3000, () => {
  console.log('start at 3000');
});

2. 插件入口

为了解耦,使用职责链模式,将所有插件放到数组里,规则配置使用插件,规则不匹配跳过插件

lib/plugin.js

const plugins = [];

function addPlugin(...fn) {
  plugins.push(...fn);
}

function execute(url) {
  const first = plugins[0];
  let index = 0;
  function next () {
    index ++;
    if (index === plugins.length) return {};
    return plugins[index](url, next);
  }
  return first(url, next);
}

module.exports = {
  addPlugin,
  execute,
};

插件返回格式为 { content: '', type: '' } content 为模块内容, type 为 格式 默认为 'application/javascript'

3. 插件执行

index.js

#!/usr/bin/env node
const Koa = require('koa');
const app = new Koa();

const { addPlugin, execute } = require('./lib/plugins');
addPlugin(
  require('./lib/plugin/htmlPlugin'),
  require('./lib/plugin/jsPlugin'),
  require('./lib/plugin/modulePlugin'),
  require('./lib/plugin/vuePlugin'),
  require('./lib/plugin/cssPlugin'),
);

app.use(async (ctx) => {
  const { url } = ctx.request;
  const { content, type } = execute(url);
  ctx.type = type ? type : 'application/javascript';
  ctx.body = content;
});

app.listen(3000, () => {
  console.log('start at 3000');
});
我们只需要返回插件返回的结果就行了,执行交给插件

4. html插件

lib/plugin/htmlPlugin.js

const fs = require('fs');
const path = require('path');

module.exports = function(url, next) {
  if (!(url === '/' || url.endsWith('.html'))) {
    return next();
  }
  
  if (url === '/') url = '/src/index.html';

  let content = fs.readFileSync(path.resolve() + url, 'utf-8');
  content = content.replace(
    '<script ', 
    `<script>window.process = { env: { NODE_ENV: 'dev' }}</script><script `
  );

  return {
    content: content,
    type: 'text/html'
  };
}

其实就是简单的根据请求路径读取文件,然后返回,中间有块逻辑:

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

后续引入的库会有 读 process 变量,先处理下

5. js插件

lib/plugin/jsPlugin.js

const fs = require('fs');
const path = require('path');
const { rewriteImport } = require('../utils');

module.exports = function(url, next) {
  if (!url.endsWith('.js')) return next();
  return {
    content: rewriteImport(fs.readFileSync(path.resolve() + url, 'utf-8'))
  };
}

rewriteImport 方法

lib/utils.js

function rewriteImport(content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) {
    if (s1[0] !== '.' && s1[0] !== '/') {
      return ` from '/@modules/${s1}'`;
    } else {
      return s0;
    }
  });
}

module.exports = {
  rewriteImport,
};

对引入 node_modules 的库进行处理,加前缀,方便我们写插件拦截

6. vue 插件

lib/plugin/vuePlugin.js

思路

  • 使用 @vue/compiler-sfc 把 .vue 文件解析,让我们可以获取 template script css 三部分
  • 为了解耦,把一个对 .vue文件的请求分为三部分
    • .vue script 部分
    • .vue?type=template template 部分 也就是 render 函数
    • .vue?type=css css 部分
  • 对于 script 部分 我们对源文件进行重写,添加对template css 的引用
  • 对于 template css 部分 我们使用 vue 提供的 compiler 库进行处理并返回
const fs = require('fs');
const path = require('path');
const qs = require('qs');
const compilerSFC = require('@vue/compiler-sfc');
const compilerDOM = require('@vue/compiler-dom');
const { rewriteImport } = require('../utils');

module.exports = function (url, next) {
  if (url.indexOf('.vue') === -1) return next();
  const [fileName, arg] = url.split('?');
  const p = path.resolve() + '/' + fileName.slice(1);
  const { descriptor } = compilerSFC.parse(fs.readFileSync(p, 'utf-8'));
  if (arg) {
    const obj = qs.parse(arg);
    if (obj.type === 'template') {
      const template = descriptor.template;
      const render = compilerDOM.compile(template.content, { mode: 'module' }).code;
      return {
        content: rewriteImport(render),
      };
    }
  }
  return {
    content: `${rewriteImport(descriptor.script.content).replace('export default ', 'const __script = ')}
      import { render as __render } from '${url}?type=template';
      __script.render = __render;
      export default __script;
    `
  };
}

7. css 插件

lib/plugin/cssPlugin.js

就是包装成js模块 执行这个模块 可以把 style appendbody

const fs = require('fs');
const path = require('path');

module.exports = function (url, next) {
  if (!url.endsWith('.css')) return next();
  const p = path.resolve() + '/' + url;
  const content = fs.readFileSync(p, 'utf-8');
  const res = `
    const css = '${content.replace(/\n/g, "")}';
    const link = document.createElement('style');
    link.setAttribute('type', 'text/css');
    document.head.appendChild(link);
    link.innerHTML = css;
    export default css;
  `;
  return { content: res };
}

项目发布

package.json 设置

设置 bin 用于命令行执行

{
  "name": "cool-vite",
  "version": "1.1.3",
  "description": "类vite的本地静态服务器",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "install": "node lib/init.js",
    "dev": "node index.js"
  },
  "bin": "./index.js",
  "keywords": [
    "vue"
  ],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@vue/compiler-sfc": "^3.2.41",
    "koa": "^2.13.4",
    "qs": "^6.11.0",
    "vue": "^3.2.36"
  }
}

可执行程序入口设置

index.js 入口文件顶部添加

#!/usr/bin/env node

登录

npm login

上传

npm publish