当前位置:   article > 正文

【一】尤大神都说Vite香,让我来手把手分析Vite原理

vite 原理 语言

戳蓝字"

前端优选

"

关注我们哦

一.什么是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服务:

  1. const Koa = require('koa');
  2. function createServer() {
  3.     const app = new Koa();
  4.     const root = process.cwd();
  5.     // 构建上下文对象
  6.     const context = {
  7.         app,
  8.         root
  9.     }
  10.     app.use((ctx, next) => {
  11.         // 扩展ctx属性
  12.         Object.assign(ctx, context);
  13.         return next();
  14.     });
  15.     const resolvedPlugins = [
  16.     ];
  17.     // 依次注册所有插件
  18.     resolvedPlugins.forEach(plugin => plugin(context));
  19.     return app;
  20. }
  21. createServer().listen(4000);

3.Koa静态服务配置

用于处理项目中的静态资源:

  1. const {serveStaticPlugin} = require('./serverPluginServeStatic');
  2. const resolvedPlugins = [
  3.  serveStaticPlugin
  4. ];
  1. const path = require('path');
  2. function serveStaticPlugin({app,root}){
  3.     // 以当前根目录作为静态目录
  4.     app.use(require('koa-static')(root));
  5.     // 以public目录作为根目录
  6.     app.use(require('koa-static')(path.join(root,'public')))
  7. }
  8. exports.serveStaticPlugin = serveStaticPlugin;

目的是让当前目录下的文件和public目录下的文件可以直接被访问

4.重写模块路径

  1. const {moduleRewritePlugin} = require('./serverPluginModuleRewrite');
  2. const resolvedPlugins = [
  3.     moduleRewritePlugin,
  4.     serveStaticPlugin
  5. ];
  1. const { readBody } = require("./utils");
  2. const { parse } = require('es-module-lexer');
  3. const MagicString = require('magic-string');
  4. function rewriteImports(source) {
  5.     let imports = parse(source)[0];
  6.     const magicString = new MagicString(source);
  7.     if (imports.length) {
  8.         for (let i = 0; i < imports.length; i++) {
  9.             const { s, e } = imports[i];
  10.             let id = source.substring(s, e);
  11.             if (/^[^\/\.]/.test(id)) {
  12.                 id = `/@modules/${id}`;
  13.                 // 修改路径增加 /@modules 前缀
  14.                 magicString.overwrite(s, e, id);
  15.             }
  16.         }
  17.     }
  18.     return magicString.toString();
  19. }
  20. function moduleRewritePlugin({ app, root }) {
  21.     app.use(async (ctx, next) => {
  22.         await next();
  23.         // 对类型是js的文件进行拦截
  24.         if (ctx.body && ctx.response.is('js')) {
  25.             // 读取文件中的内容
  26.             const content = await readBody(ctx.body);
  27.             // 重写import中无法识别的路径
  28.             const r = rewriteImports(content);
  29.             ctx.body = r;
  30.         }
  31.     });
  32. }
  33. exports.moduleRewritePlugin = moduleRewritePlugin;

js文件中的 import 语法进行路径的重写,改写后的路径会再次向服务器拦截请求

读取文件内容:

  1. const { Readable } = require('stream')
  2. async function readBody(stream) {
  3.     if (stream instanceof Readable) { // 
  4.         return new Promise((resolve, reject) => {
  5.             let res = '';
  6.             stream
  7.                 .on('data', (chunk) => res += chunk)
  8.                 .on('end', () => resolve(res));
  9.         })
  10.     }else{
  11.         return stream.toString()
  12.     }
  13. }
  14. exports.readBody = readBody

5.解析 /@modules 文件

  1. const {moduleResolvePlugin} = require('./serverPluginModuleResolve');
  2. const resolvedPlugins = [
  3.     moduleRewritePlugin,
  4.     moduleResolvePlugin,
  5.     serveStaticPlugin
  6. ];
  1. const fs = require('fs').promises;
  2. const path = require('path');
  3. const { resolve } = require('path');
  4. const moduleRE = /^\/@modules\//; 
  5. const {resolveVue} = require('./utils')
  6. function moduleResolvePlugin({ app, root }) {
  7.     const vueResolved = resolveVue(root)
  8.     app.use(async (ctx, next) => {
  9.         // 对 /@modules 开头的路径进行映射
  10.         if(!moduleRE.test(ctx.path)){ 
  11.             return next();
  12.         }
  13.         // 去掉 /@modules/路径
  14.         const id = ctx.path.replace(moduleRE,'');
  15.         ctx.type = 'js';
  16.         const content = await fs.readFile(vueResolved[id],'utf8');
  17.         ctx.body = content
  18.     });
  19. }
  20. exports.moduleResolvePlugin = moduleResolvePlugin;

将/@modules 开头的路径解析成对应的真实文件,并返回给浏览器

  1. const path = require('path');
  2. function resolveVue(root) {
  3.     const compilerPkgPath = path.resolve(root, 'node_modules''@vue/compiler-sfc/package.json');
  4.     const compilerPkg = require(compilerPkgPath);
  5.     // 编译模块的路径  node中编译
  6.     const compilerPath = path.join(path.dirname(compilerPkgPath), compilerPkg.main);
  7.     const resolvePath = (name) => path.resolve(root, 'node_modules'`@vue/${name}/dist/${name}.esm-bundler.js`);
  8.     // dom运行
  9.     const runtimeDomPath = resolvePath('runtime-dom')
  10.     // 核心运行
  11.     const runtimeCorePath = resolvePath('runtime-core')
  12.     // 响应式模块
  13.     const reactivityPath = resolvePath('reactivity')
  14.     // 共享模块
  15.     const sharedPath = resolvePath('shared')
  16.     return {
  17.         vue: runtimeDomPath,
  18.         '@vue/runtime-dom': runtimeDomPath,
  19.         '@vue/runtime-core': runtimeCorePath,
  20.         '@vue/reactivity': reactivityPath,
  21.         '@vue/shared': sharedPath,
  22.         compiler: compilerPath,
  23.     }
  24. }

编译的模块使用commonjs规范,其他文件均使用es6模块

6.处理process的问题

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

  1. const {htmlRewritePlugin} = require('./serverPluginHtml');
  2. const resolvedPlugins = [
  3.     htmlRewritePlugin,
  4.     moduleRewritePlugin,
  5.     moduleResolvePlugin,
  6.     serveStaticPlugin
  7. ];
  1. const { readBody } = require("./utils");
  2. function htmlRewritePlugin({root,app}){
  3.     const devInjection = `
  4.     <script>
  5.         window.process = {env:{NODE_ENV:'development'}}
  6.     </script>
  7.     `
  8.     app.use(async(ctx,next)=>{
  9.         await next();
  10.         if(ctx.response.is('html')){
  11.             const html = await readBody(ctx.body);
  12.             ctx.body = html.replace(/<head>/,`$&${devInjection}`)
  13.         }
  14.     })
  15. }
  16. exports.htmlRewritePlugin = htmlRewritePlugin

html的head标签中注入脚本

7.处理.vue后缀文件

  1. const {vuePlugin} = require('./serverPluginVue')
  2. const resolvedPlugins = [
  3.     htmlRewritePlugin,
  4.     moduleRewritePlugin,
  5.     moduleResolvePlugin,
  6.     vuePlugin,
  7.     serveStaticPlugin
  8. ];
  1. const path = require('path');
  2. const fs = require('fs').promises;
  3. const { resolveVue } = require('./utils');
  4. const defaultExportRE = /((?:^|\n|;)\s*)export default/
  5. function vuePlugin({ app, root }) {
  6.     app.use(async (ctx, next) => {
  7.         if (!ctx.path.endsWith('.vue')) {
  8.             return next();
  9.         }
  10.         // vue文件处理
  11.         const filePath = path.join(root, ctx.path);
  12.         const content = await fs.readFile(filePath, 'utf8');
  13.         // 获取文件内容
  14.         let { parse, compileTemplate } = require(resolveVue(root).compiler);
  15.         let { descriptor } = parse(content); // 解析文件内容
  16.         if (!ctx.query.type) {
  17.             let code = ``;
  18.             if (descriptor.script) {
  19.                 let content = descriptor.script.content;
  20.                 let replaced = content.replace(defaultExportRE, '$1const __script =');
  21.                 code += replaced;
  22.             }
  23.             if (descriptor.template) {
  24.                 const templateRequest = ctx.path + `?type=template`
  25.                 code += `\nimport { render as __render } from ${JSON.stringify(
  26.                     templateRequest
  27.                 )}`;
  28.                 code += `\n__script.render = __render`
  29.             }
  30.             ctx.type = 'js'
  31.             code += `\nexport default __script`;
  32.             ctx.body = code;
  33.         }
  34.         if (ctx.query.type == 'template') {
  35.             ctx.type = 'js';
  36.             let content = descriptor.template.content;
  37.             const { code } = compileTemplate({ source: content });
  38.             ctx.body = code;
  39.         }
  40.     })
  41. }
  42. exports.vuePlugin = vuePlugin;

在后端将.vue文件进行解析成如下结果

  1. import {reactive} from '/@modules/vue';
  2. const __script = {
  3.   setup() {
  4.     let state = reactive({count:0});
  5.     function click(){
  6.       state.count+= 1
  7.     }
  8.     return {
  9.       state,
  10.       click
  11.     }
  12.   }
  13. }
  14. import { render as __render } from "/src/App.vue?type=template"
  15. __script.render = __render
  16. export default __script
  1. import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "/@modules/vue"
  2. export function render(_ctx, _cache) {
  3.   return (_openBlock(), _createBlock(_Fragment, null, [
  4.     _createVNode("div", null, "计数器:" + _toDisplayString(_ctx.state.count), 1 /* TEXT */),
  5.     _createVNode("button", {
  6.       onClick: _cache[1] || (_cache[1] = $event => (_ctx.click($event)))
  7.     }, "+")
  8.   ], 64 /* STABLE_FRAGMENT */))
  9. }

解析后的结果可以直接在createApp方法中进行使用

8.小结

到这里,基本的一个Vite就实现了。总结一下就是:通过Koa服务,实现了按需读取文件,省掉了打包步骤,以此来提升项目启动速度,这中间包含了一系列的处理,诸如解析代码内容、静态文件读取、浏览器新特性实践等等。

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

历史好文推荐:

1、Vue3之——和Vite不得不说的事                        

2、大厂面试算法之斐波那契数列                        

3、2020字节跳动面试题一面解析                          

点个在看,大家都看 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/970881
推荐阅读
相关标签
  

闽ICP备14008679号