lerna

lerna是一个管理多个 npm 模块的工具,优化维护多包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题
使用:
npm i lerna -g
lerna init

yarn workspace

每个项目中都要安装node_modules,就会出现npm包需要复用的问题
yarn workspace允许我们使用 monorepo 的形式来管理项目
在安装 node_modules 的时候它不会安装到每个子项目的 node_modules 里面,而是直接安装到根目录下面,这样每个子项目都可以读取到根目录的 node_modules
整个项目只有根目录下面会有一份 yarn.lock 文件。子项目也会被 link 到 node_modules 里面,这样就允许我们就可以直接用 import 导入对应的项目
yarn.lock文件是自动生成的,也完全Yarn来处理.yarn.lock锁定你安装的每个依赖项的版本,这可以确保你不会意外获得不良依赖

环境搭建

用lerna创建两个项目:vite-cli vite-project,分别作为我们自己实现的vite、测试我们实现的vite的运行
在vite-cli项目中新建 ./bin/vite.js文件,作为我们的入口,并在该项目的package.json中添加bin属性:
package.json:

  1. {
  2. "name": "vite-cli",
  3. "version": "0.0.0",
  4. "description": "> TODO: description",
  5. "author": "Your Name <you@example.com>",
  6. "homepage": "",
  7. "license": "MIT",
  8. "main": "lib/vite-cli.js",
  9. "directories": {
  10. "lib": "lib",
  11. "test": "__tests__"
  12. },
  13. "files": [
  14. "lib"
  15. ],
  16. "bin": {
  17. "vite-cli": "./bin/vite.js"
  18. },

./bin/vite.js:

  1. #!/usr/bin/env node
  2. function start() {
  3. require('../lib/cli');
  4. }
  5. start();

项目目录:
14.vite-source是lerna父项目,packages是各个子项目
image.png
进入/14.vite-source/packages/vite-cli 目录下,执行npm link,这样就会把当前项目链接到全局node_modules下,我们使用npm root -g就可以看到多出来了vite-cli这样一个目录,这个目录将会链接到我们的项目目录中:
image.png
这时在任意目录下执行vite-cli,就会去/AppData/Roaming/npm下找vite-cli.cmd文件,然后执行这个cmd命令,这个cmd命令内容如下:
image.png
可以看到它将会在当前目录下找到node_modules/vite-cli/bin/vite.js再执行
这里的vite-cli目录实际上是被链到我们的14.vite-source/packages/vite-cli下了,所以最终会执行到我们自己写的vite.js
在这个vite.js文件中,第一行很重要,不可以省略:
#!/usr/bin/env node
因为有了这行代码,在npm link执行的时候生成的cmd文件才会去/usr/bin/env这个目录中寻找node这个二进制可执行文件来执行我们写的vite.js文件

安装依赖:

  1. cd packages/vite-project
  2. yarn workspace vite-project add vite
  3. cd packages/vite-cli
  4. yarn workspace vite-cli add es-module-lexer koa koa-static magic-string

14.vite-source/packages/vite-cli/bin/vite.js引入的../lib/cli.js内容:

const Koa = require('koa');
const serverPluginServeStatic = require('./serverPluginServeStatic');
function createServer() {
    //获取app
    const app = new Koa();
    //获取当前的工作目录 packages\vite-project
    const root = process.cwd();
    //上下文对象
    const context = {
        app,
        root
    }
    const plugins = [
        serverPluginServeStatic
    ]
    plugins.forEach(plugin => plugin(context));
    return app;
}
createServer().listen(5000, () => `vite server已经启动在5000端口上`);

可以看到,在此处我们启动了一个服务器,然后执行了一堆plugins插件,接下来我们分析一下这些插件做了什么事:
serverPluginServeStatic:

const static = require('koa-static');
function serverPluginServeStatic({ app, root }) {
    //给koa添加一个静态文件中间件,以root,也就是当前命令所有的根目录为静态文件根目录
    app.use(static(root));
}
module.exports = serverPluginServeStatic;

这个插件作用是添加静态目录,需要注意,我们是从vite-project中去执行vite-cli/bin/vite.js命令run起来这个服务器的,即
在 14.vite-source/packages/vite-project目录下执行
nodemon ../vite-cli/bin/vite.js
所以服务器跑起来以后,process.cwd()其实是14.vite-source/packages/vite-project这个目录,所以我们是将这个目录传给了serverPluginServeStatic方法,将其作为静态目录
之后访问localhost:5000,会访问到vite-project下的index.html,在解析这个文件时遇到

<script type="module" src="/src/main.js"></script>

会再次发起请求,从14.vite-source/packages/vite-project这个目录下寻找src/main.js返回给客户端
vite
vite在解析js时,例如如下文件:

import { createApp } from 'vue';
console.log(createApp)

会将import语句转换为:
import { createApp } from ‘/node_modules/.vite/vue.js?v=c2d05e98’;
这个转换,我们需要通过serverPluginModuleRewrite插件完成:

const Koa = require('koa');
const serverPluginServeStatic = require('./serverPluginServeStatic');
const serverPluginModuleRewrite = require('./serverPluginModuleRewrite');
function createServer() {
    //获取app
    const app = new Koa();
    //获取当前的工作目录 packages\vite-project
    const root = process.cwd();
    //上下文对象
    const context = {
        app,
        root
    }
    const plugins = [
          serverPluginModuleRewrite,
        serverPluginServeStatic
    ]
    plugins.forEach(plugin => plugin(context));
    return app;
}
createServer().listen(5000, () => `vite server已经启动在5000端口上`);

./serverPluginModuleRewrite

const { readBody } = require('./utils');
const MagicString = require('magic-string');
const { parse } = require('es-module-lexer');
async function rewriteImports(responseBody) {
    let magicString = new MagicString(responseBody);
    let imports = await parse(responseBody);
    if (imports && imports.length > 0 && imports[0] && imports[0].length) {
        for (let i = 0; i < imports[0].length; i++) {
            const { n, s, e } = imports[0][i];
            //如果引入模块名不是.也不是/开头的话,就要替换
            if (/^[^\/\.]/.test(n)) {
                magicString.overwrite(s, e, `/node_modules/.vite/${n}.js`);
            }
        }
    }
    return magicString.toString();
}
function serverPluginModuleRewrite({ app, root }) {
    app.use(async (ctx, next) => {
        //一上来就先向后执行下一个中间件
        await next();
        //执行完内嵌的中间件之后就会有响应体
        if (ctx.body && ctx.response.is('js')) {
            const responseBody = await readBody(ctx.body);
            const result = await rewriteImports(responseBody);
            ctx.body = result;
        }
    });
}

module.exports = serverPluginModuleRewrite;

./utils:

const { Readable } = require('stream');
const Module = require('module');
async function readBody(responseBody) {
    if (responseBody instanceof Readable) {
        return new Promise(resolve => {
            let buffers = [];
            responseBody.on('data', (chunk) => {
                buffers.push(chunk);
            }).on('end', () => {
                let body = Buffer.concat(buffers).toString();
                resolve(body);
            });
        });
    } else {
        return responseBody.toString();
    }
}

exports.readBody = readBody;

此时已经有2个插件,执行方式如下:
image.png
每个插件以next的执行为分界,next()之前是进入该插件时执行的逻辑,next()之后是离开该插件时执行的逻辑
在serverPluginModuleRewrite这个插件中,我们只有在next()后面有东西,所以进入这个插件时会直接跳过,执行下一个,而出来的时候再执行next()后面的逻辑

rewriteImports方法会拿到所有import语句,再调用magicString.overwrite将其重写,最后返回

但实际上,服务器中并没有/node_modules/.vite/vue.js这个路径,所以这个路径其实是个逻辑上的路径而不是物理路径,所以我们还需要写一个插件去对这个逻辑路径进行解析——serverPluginModuleResolve,这个插件要放在serverPluginModuleRewrite和serverPluginServeStatic中间:
image.png
serverPluginModuleResolve就负责将逻辑路径转换为真正的资源并返回

const fs = require('fs').promises;
const moduleRegex = /\/node_modules\/\.vite\/(.+?)\.js/;
const { resolveVue } = require('./utils');
async function serverPluginModuleResolve({ app, root }) {
    app.use(async (ctx, next) => {
        let result = ctx.path.match(moduleRegex);
        //如果不是第三方模块,则直接向后执行
        if (!result) {
            return await next();
        }
        //如果是第三方模块,继续解析处理
        let moduleName = result[1];//vue

        let vueResolved = resolveVue(root);
        ctx.type = 'js';
        const responseBody = await fs.readFile(vueResolved[moduleName], 'utf8');
        ctx.body = responseBody;

    });
}
module.exports = serverPluginModuleResolve;

./utils

function resolveVue(root) {
    //创建一个自定义require方法。因为如果你在这里直接 使用require,它会在当前目录找文件
    let require = Module.createRequire(root);
    const resolvePath = (moduleName) => require.resolve(`@vue/${moduleName}/dist/${moduleName}.esm-bundler.js`);
    //返回一个映射对象 key是模块名 值是此模块在硬盘上绝对路径
    return {
        "vue": resolvePath('runtime-dom'),
        "@vue/shared": resolvePath('shared'),
        "@vue/reactivity": resolvePath('reactivity'),
        "@vue/runtime-core": resolvePath('runtime-core'),
    }
}

exports.resolveVue = resolveVue;

然后后面的plugins都不会走,直接就将response返回给客户端了
到目前为止,总共有index.html main.js /node_modules/.vite/vue.js 3个请求,这3个请求在中间件中的处理如下图所示:
image.png

const Koa = require('koa');
const serverPluginServeStatic = require('./serverPluginServeStatic');
const serverPluginModuleRewrite = require('./serverPluginModuleRewrite');
const serverPluginModuleResolve = require('./serverPluginModuleResolve');
function createServer() {
    //获取app
    const app = new Koa();
    //获取当前的工作目录 packages\vite-project
    const root = process.cwd();
    //上下文对象
    const context = {
        app,
        root
    }
    const plugins = [
          serverPluginModuleRewrite,
        serverPluginModuleResolve,
        serverPluginServeStatic
    ]
    plugins.forEach(plugin => plugin(context));
    return app;
}
createServer().listen(5000, () => `vite server已经启动在5000端口上`);

vue的源码中有用到process这个变量,我们需要再写一个plugin处理一下——serverPluginInjectProcess

const { readBody } = require("./utils");

function serverPluginInjectProcess({ app, root }) {
    const devInjection = `
        <script>
          window.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>/, `<head>${devInjection}`);
        }
    });
}
module.exports = serverPluginInjectProcess;
const Koa = require('koa');
const serverPluginServeStatic = require('./serverPluginServeStatic');
const serverPluginModuleRewrite = require('./serverPluginModuleRewrite');
const serverPluginModuleResolve = require('./serverPluginModuleResolve');
const serverPluginInjectProcess = require('./serverPluginInjectProcess');
function createServer() {
    //获取app
    const app = new Koa();
    //获取当前的工作目录 packages\vite-project
    const root = process.cwd();
    //上下文对象
    const context = {
        app,
        root
    }
    const plugins = [
        serverPluginInjectProcess,
          serverPluginModuleRewrite,
        serverPluginModuleResolve,
        serverPluginServeStatic
    ]
    plugins.forEach(plugin => plugin(context));
    return app;
}
createServer().listen(5000, () => `vite server已经启动在5000端口上`);

接下来,就可以写业务逻辑相关的代码,即定义App.vue 了
src/main.js:

import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');

./App.vue

<template>
 <h1>App</h1>
</template>

<script>
export default {
    name:'App'
}
</script>

我们在这里添加了import App from ‘./App.vue’;这样一条语句,自然就会再多发一个请求,去请求其内容,这个内容是经过vite编译处理的,我们先看下它最终处理的结果:
下图为简化版:

const _sfc_main = {
    name: 'App'
}

import { openBlock, createElementBlock } from "vue"

function render() {
    return (openBlock(),
        createElementBlock("h1", null, "App"))
}

_sfc_main.render = render;
export default _sfc_main;

我们还需要写一个插件(serverPluginVue),将App.vue这种vue文件的源码转换为上述编译后的浏览器可执行代码


const path = require('path');
const fs = require('fs').promises;
const { parse, compileTemplate } = require('@vue/compiler-sfc');
const exportDefaultRegexp = /export default/;
function serverPluginVue({ root, app }) {
    app.use(async (ctx, next) => {
        if (!ctx.path.endsWith('.vue')) {
            return await next();
        }
        //获取vue组件文件的绝对路径 C:\vite-project\src\App.vue
        const vueSFCPath = path.join(root, ctx.path);
        const content = await fs.readFile(vueSFCPath, 'utf8');
        const { descriptor } = parse(content);
        const { script, template } = descriptor;
        let targetCode = '';
        if (script) {
            let scriptContent = script.content;
            scriptContent = scriptContent.replace(exportDefaultRegexp, 'const _sfc_main=');
            targetCode += scriptContent;
        }
        if (template) {
            let templateContent = template.content;
            const { code } = compileTemplate({ source: templateContent });
            targetCode += code;
        }
        targetCode += `\n_sfc_main.render = render`;
        targetCode += `\nexport default _sfc_main`;
        ctx.type = 'js';
        ctx.body = targetCode;
    });
}
module.exports = serverPluginVue;

./lib/cli.js

const Koa = require('koa');
const serverPluginServeStatic = require('./serverPluginServeStatic');
const serverPluginModuleRewrite = require('./serverPluginModuleRewrite');
const serverPluginModuleResolve = require('./serverPluginModuleResolve');
const serverPluginInjectProcess = require('./serverPluginInjectProcess');
const serverPluginVue = require('./serverPluginVue');
function createServer() {
    //获取app
    const app = new Koa();
    //获取当前的工作目录 packages\vite-project
    const root = process.cwd();
    //上下文对象
    const context = {
        app,
        root
    }
    const plugins = [
        serverPluginInjectProcess,
        serverPluginModuleRewrite,
        serverPluginModuleResolve,
        serverPluginVue,
        serverPluginServeStatic
    ]
    plugins.forEach(plugin => plugin(context));
    return app;
}
createServer().listen(5000, () => `vite server已经启动在5000端口上`);