一.什么是Vite?

法语Vite(轻量,轻快)vite 是一个基于 Vue3单文件组件的非打包开发服务器,它做到了本地疾速开发启动、实现按需编译、不再期待整个利用编译实现的性能作用。

对于Vite的形容:针对Vue单页面组件的无打包开发服务器,能够间接在浏览器运行申请的vue文件。

面向古代浏览器,Vite基于原生模块零碎 ESModule 实现了按需编译,而在webpack的开发环境却很慢,是因为其开发时须要将进行的编译放到内存中,打包所有文件。

Vite有如此多的长处,那么它是如何实现的呢?

二.Vite的实现原理

咱们先来总结下Vite的实现原理:

  • Vite在浏览器端应用的是 export import 形式导入和导出的模块;
  • vite同时实现了按需加载;
  • Vite高度依赖module script个性。

实现过程如下:

  • koa 中间件中获取申请 body;
  • 通过 es-module-lexer 解析资源 ast 并拿到 import 内容;
  • 判断 import 的资源是否是 npm 模块;
  • 返回解决后的资源门路:"vue" => "/@modules/vue"

将要解决的template,script,style等所需依赖以http申请的模式、通过query参数的模式辨别,并加载SFC(vue单文件)文件各个模块内容。

接下来将本人手写一个Vite来实现雷同的性能:

三.手把手实现Vite

1.装置依赖

实现Vite的环境须要es-module-lexerkoakoa-staticmagic-string模块搭建:

npm install es-module-lexer koa koa-static magic-string

这些模块的性能是:

  • koakoa-staticvite外部应用的服务框架;
  • es-module-lexer 用于剖析ES6import语法;
  • magic-string 用来实现重写字符串内容。

2.根本构造搭建

Vite须要搭建一个koa服务:

const Koa = require('koa');function createServer() {    const app = new Koa();    const root = process.cwd();    // 构建上下文对象    const context = {        app,        root    }    app.use((ctx, next) => {        // 扩大ctx属性        Object.assign(ctx, context);        return next();    });    const resolvedPlugins = [    ];    // 顺次注册所有插件    resolvedPlugins.forEach(plugin => plugin(context));    return app;}createServer().listen(4000);

3.Koa动态服务配置

用于解决我的项目中的动态资源:

const {serveStaticPlugin} = require('./serverPluginServeStatic');const resolvedPlugins = [ serveStaticPlugin];
const path = require('path');function serveStaticPlugin({app,root}){    // 以以后根目录作为动态目录    app.use(require('koa-static')(root));    // 以public目录作为根目录    app.use(require('koa-static')(path.join(root,'public')))}exports.serveStaticPlugin = serveStaticPlugin;
目标是让当前目录下的文件和public目录下的文件能够间接被拜访

4.重写模块门路

const {moduleRewritePlugin} = require('./serverPluginModuleRewrite');const resolvedPlugins = [    moduleRewritePlugin,    serveStaticPlugin];
const { readBody } = require("./utils");const { parse } = require('es-module-lexer');const MagicString = require('magic-string');function rewriteImports(source) {    let imports = parse(source)[0];    const magicString = new MagicString(source);    if (imports.length) {        for (let i = 0; i < imports.length; i++) {            const { s, e } = imports[i];            let id = source.substring(s, e);            if (/^[^\/\.]/.test(id)) {                id = `/@modules/${id}`;                // 批改门路减少 /@modules 前缀                magicString.overwrite(s, e, id);            }        }    }    return magicString.toString();}function moduleRewritePlugin({ app, root }) {    app.use(async (ctx, next) => {        await next();        // 对类型是js的文件进行拦挡        if (ctx.body && ctx.response.is('js')) {            // 读取文件中的内容            const content = await readBody(ctx.body);            // 重写import中无奈辨认的门路            const r = rewriteImports(content);            ctx.body = r;        }    });}exports.moduleRewritePlugin = moduleRewritePlugin;
js文件中的 import 语法进行门路的重写,改写后的门路会再次向服务器拦挡申请

读取文件内容:

const { Readable } = require('stream')async function readBody(stream) {    if (stream instanceof Readable) { //         return new Promise((resolve, reject) => {            let res = '';            stream                .on('data', (chunk) => res += chunk)                .on('end', () => resolve(res));        })    }else{        return stream.toString()    }}exports.readBody = readBody

5.解析 /@modules 文件

const {moduleResolvePlugin} = require('./serverPluginModuleResolve');const resolvedPlugins = [    moduleRewritePlugin,    moduleResolvePlugin,    serveStaticPlugin];
const fs = require('fs').promises;const path = require('path');const { resolve } = require('path');const moduleRE = /^\/@modules\//; const {resolveVue} = require('./utils')function moduleResolvePlugin({ app, root }) {    const vueResolved = resolveVue(root)    app.use(async (ctx, next) => {        // 对 /@modules 结尾的门路进行映射        if(!moduleRE.test(ctx.path)){             return next();        }        // 去掉 /@modules/门路        const id = ctx.path.replace(moduleRE,'');        ctx.type = 'js';        const content = await fs.readFile(vueResolved[id],'utf8');        ctx.body = content    });}exports.moduleResolvePlugin = moduleResolvePlugin;
将/@modules 结尾的门路解析成对应的实在文件,并返回给浏览器
const path = require('path');function resolveVue(root) {    const compilerPkgPath = path.resolve(root, 'node_modules', '@vue/compiler-sfc/package.json');    const compilerPkg = require(compilerPkgPath);    // 编译模块的门路  node中编译    const compilerPath = path.join(path.dirname(compilerPkgPath), compilerPkg.main);    const resolvePath = (name) => path.resolve(root, 'node_modules', `@vue/${name}/dist/${name}.esm-bundler.js`);    // dom运行    const runtimeDomPath = resolvePath('runtime-dom')    // 外围运行    const runtimeCorePath = resolvePath('runtime-core')    // 响应式模块    const reactivityPath = resolvePath('reactivity')    // 共享模块    const sharedPath = resolvePath('shared')    return {        vue: runtimeDomPath,        '@vue/runtime-dom': runtimeDomPath,        '@vue/runtime-core': runtimeCorePath,        '@vue/reactivity': reactivityPath,        '@vue/shared': sharedPath,        compiler: compilerPath,    }}
编译的模块应用commonjs标准,其余文件均应用es6模块

6.解决process的问题

浏览器中并没有process变量,所以咱们须要在html中注入process变量

const {htmlRewritePlugin} = require('./serverPluginHtml');const resolvedPlugins = [    htmlRewritePlugin,    moduleRewritePlugin,    moduleResolvePlugin,    serveStaticPlugin];
const { readBody } = require("./utils");function htmlRewritePlugin({root,app}){    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>/,`const { readBody } = require("./utils");function htmlRewritePlugin({root,app}){    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>/,`$&${devInjection}`)        }    })}exports.htmlRewritePlugin = htmlRewritePluginamp;${devInjection}`)        }    })}exports.htmlRewritePlugin = htmlRewritePlugin
html的head标签中注入脚本

7.解决.vue后缀文件

const {vuePlugin} = require('./serverPluginVue')const resolvedPlugins = [    htmlRewritePlugin,    moduleRewritePlugin,    moduleResolvePlugin,    vuePlugin,    serveStaticPlugin];
const path = require('path');const fs = require('fs').promises;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();        }        // vue文件解决        const filePath = path.join(root, ctx.path);        const content = await fs.readFile(filePath, 'utf8');        // 获取文件内容        let { parse, compileTemplate } = require(resolveVue(root).compiler);        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) {                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') {            ctx.type = 'js';            let content = descriptor.template.content;            const { code } = compileTemplate({ source: content });            ctx.body = code;        }    })}exports.vuePlugin = vuePlugin;
在后端将.vue文件进行解析成如下后果
import {reactive} from '/@modules/vue';const __script = {  setup() {    let state = reactive({count:0});    function click(){      state.count+= 1    }    return {      state,      click    }  }}import { render as __render } from "/src/App.vue?type=template"__script.render = __renderexport default __script
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "/@modules/vue"export function render(_ctx, _cache) {  return (_openBlock(), _createBlock(_Fragment, null, [    _createVNode("div", null, "计数器:" + _toDisplayString(_ctx.state.count), 1 /* TEXT */),    _createVNode("button", {      onClick: _cache[1] || (_cache[1] = $event => (_ctx.click($event)))    }, "+")  ], 64 /* STABLE_FRAGMENT */))}
解析后的后果能够间接在createApp办法中进行应用

8.小结

到这里,根本的一个Vite就实现了。总结一下就是:通过Koa服务,实现了按需读取文件,省掉了打包步骤,以此来晋升我的项目启动速度,这两头蕴含了一系列的解决,诸如解析代码内容、动态文件读取、浏览器新个性实际等等。

其实Vite的内容远不止于此,这里咱们实现了非打包开发服务器,那它是如何做到热更新的呢,下次将手把手实现Vite热更新原理~

本文应用 mdnice 排版