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:
{"name": "vite-cli","version": "0.0.0","description": "> TODO: description","author": "Your Name <you@example.com>","homepage": "","license": "MIT","main": "lib/vite-cli.js","directories": {"lib": "lib","test": "__tests__"},"files": ["lib"],"bin": {"vite-cli": "./bin/vite.js"},
./bin/vite.js:
#!/usr/bin/env nodefunction start() {require('../lib/cli');}start();
项目目录:
14.vite-source是lerna父项目,packages是各个子项目
进入/14.vite-source/packages/vite-cli 目录下,执行npm link,这样就会把当前项目链接到全局node_modules下,我们使用npm root -g就可以看到多出来了vite-cli这样一个目录,这个目录将会链接到我们的项目目录中:
这时在任意目录下执行vite-cli,就会去/AppData/Roaming/npm下找vite-cli.cmd文件,然后执行这个cmd命令,这个cmd命令内容如下:
可以看到它将会在当前目录下找到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文件
安装依赖:
cd packages/vite-projectyarn workspace vite-project add vitecd packages/vite-cliyarn 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个插件,执行方式如下:
每个插件以next的执行为分界,next()之前是进入该插件时执行的逻辑,next()之后是离开该插件时执行的逻辑
在serverPluginModuleRewrite这个插件中,我们只有在next()后面有东西,所以进入这个插件时会直接跳过,执行下一个,而出来的时候再执行next()后面的逻辑
rewriteImports方法会拿到所有import语句,再调用magicString.overwrite将其重写,最后返回
但实际上,服务器中并没有/node_modules/.vite/vue.js这个路径,所以这个路径其实是个逻辑上的路径而不是物理路径,所以我们还需要写一个插件去对这个逻辑路径进行解析——serverPluginModuleResolve,这个插件要放在serverPluginModuleRewrite和serverPluginServeStatic中间:
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个请求在中间件中的处理如下图所示:
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端口上`);
