vite实现原理是什么?
当声明一个 script
标签类型为 module
时
如: <script type="module" src="/src/main.js"></script>
浏览器就会像服务器发起一个GET
http://localhost:3000/src/main.js
请求main.js文件
// /src/main.js: import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
浏览器请求到了main.js
文件,检测到内部含有import
引入的包,又会对其内部的 import
引用发起 HTTP
请求获取模块的内容文件
如: GET
http://localhost:3000/@modules/vue.js
如: GET
http://localhost:3000/src/App.vue
其Vite
的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器渲染页面,vite
整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack
开发编译速度快出许多!
vite做了哪些事?
1. 重写引入模块路径前面加上/@modules/, 重写后浏览器会再次发送请求
原main.js文件:
通过vite构建后请求的main.js文件:
2. 拦截含有/@modules/的请求, 去node_modules引入对应的模块并返回
3. 解析.vue文件
如app.vue文件如下:
<template> <HelloWorld msg="Hello Vue 3.0 + Vite" /> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } } </script>
被解析成render函数返回给浏览器渲染页面:
请求:http://localhost:3000/src/App.vue
vue文件时,koa中间件检测到请求的是vue模板文件,则会在请求后面添加一个type=template
参数
如: http://localhost:3000/src/App.vue?type=template
koa通过这个参数来判断是请求vue模板文件,并编译成js文件返回给浏览器
4. 静态服务插件 实现可以返回静态文件的功能
app.use(static(root)) app.use(static(path.resolve(root, 'public')))
手写vite代码 实现以上4种功能:
新建一个vite项目:
npm instal -g create-vite-app //全局安装最新vite构建工具 (默认最新) create-vite-app my-vite-vue3 //创建一个名为myvitevue3的项目 cd my-vite-vue3 //进入项目 yarn install //安装项目依赖 yarn dev //启动项目
下面我们在根目录新建vite/index.js
文件
通过运行node vite/index.js
代替yarn dev
启动项目
使用自实现的vite来模拟vite
的这4个功能
如图所述则是使用自写vite渲染的页面:
//vite/index.js const fs = require('fs').promises const Koa = require('koa') const path = require('path') const chalk = require('chalk') const static = require('koa-static') const { parse } = require('es-module-lexer') const MagicString = require('magic-string') const { Readable } = require('stream') //读取body方法 async function readBody(stream) { if (stream instanceof Readable) { return new Promise((resolve) => { let res = '' stream.on('data', function (chunk) { res += chunk }); stream.on('end', function () { resolve(res) }) }) } else { return stream; } } //koa中间件 const resolvePlugin = [ // 1. 重写引入模块路径前面加上/@modules/vue, 重写后浏览器会再次发送请求 ({ app, root }) => { function rewriteImports(source) { let imports = parse(source)[0]; let ms = new MagicString(source); if (imports.length > 0) { for (let i = 0; i < imports.length; i++) { let { s, e } = imports[i]; let id = source.slice(s, e); // 应用的标识 vue ./App.vue // 不是./ 或者 / if (/^[^\/\.]/.test(id)) { id = `/@modules/${id}`; ms.overwrite(s, e, id) } } } return ms.toString(); } app.use(async (ctx, next) => { await next(); // 静态服务 // 默认会先执行 静态服务中间件 会将结果放到 ctx.body // 需要将流转换成字符串 , 只需要处理js中的引用问题 if (ctx.body && ctx.response.is('js')) { let r = await readBody(ctx.body); // vue => /@modules const result = rewriteImports(r); ctx.body = result; } }) }, // 2. 拦截含有/@modules/vue的请求, 去node_modules引入对应的模块并返回 ({ app, root }) => { const reg = /^\/@modules\// app.use(async (ctx, next) => { // 如果没有匹配到 /@modules/vue 就往下执行即可 if (!reg.test(ctx.path)) { return next(); } const id = ctx.path.replace(reg, ''); let mapping = { vue: path.resolve(root, 'node_modules', '@vue/runtime-dom/dist/runtime-dom.esm-browser.js'), } const content = await fs.readFile(mapping[id], 'utf8'); ctx.type = 'js'; // 返回的文件是js ctx.body = content; }) }, // 3. 解析.vue文件 ({ app, root }) => { app.use(async (ctx, next) => { if (!ctx.path.endsWith('.vue')) { return next(); } const filePath = path.join(root, ctx.path); const content = await fs.readFile(filePath, 'utf8'); // 引入.vue文件解析模板 const { compileTemplate, parse } = require(path.resolve(root, 'node_modules', '@vue/compiler-sfc/dist/compiler-sfc.cjs')) let { descriptor } = parse(content); if (!ctx.query.type) { //App.vue let code = '' if (descriptor.script) { let content = descriptor.script.content; code += content.replace(/((?:^|\n|;)\s*)export default/, '$1const __script='); } if (descriptor.template) { const requestPath = ctx.path + `?type=template`; code += `\nimport { render as __render } from "${requestPath}"`; code += `\n__script.render = __render` } code += `\nexport default __script` ctx.type = 'js'; ctx.body = code } if (ctx.query.type == 'template') { ctx.type = 'js'; let content = descriptor.template.content const { code } = compileTemplate({ source: content }); // 将app.vue中的模板 转换成render函数 ctx.body = code; } }) }, // 4. 静态服务插件 实现可以返回文件的功能 ({ app, root }) => { app.use(static(root)) app.use(static(path.resolve(root, 'public'))) } ] function createServer() { let app = new Koa() const context = { // 直接创建一个上下文 来给不同的插件共享功能 app, root: process.cwd() // C:\Users\...\my-vite-vue3 } // 运行中间件 resolvePlugin.forEach(plugin => plugin(context)) return app } createServer().listen(4000, () => { console.log(' Dev server running at:') console.log(` > Local: ${chalk.cyan('http://localhost:4000/')}`) })
图片和css文件我们还没有处理,所以除去app.vue
引入的图片与main.js
内引入的css即可实现对应的功能
一.什么是Vite?
法语Vite(轻量,轻快)vite
是一个基于 Vue3
单文件组件的非打包开发服务器,它做到了本地快速开发启动, 实现按需编译,不再等待整个应用编译完成
面向现代浏览器,基于原生模块系统
ESModule
实现。webpack
的开发环境很慢(开发时需要进行编译放到内存中)
#二.vite
的实现原理
我们先来总结下Vite的实现原理,vite
在浏览器端使用 export import 的方式导入和导出模块,同时实现了按需加载。vite
高度依赖module script特性
过程如下:
- 在
koa
中间件里获取请求 body - 通过 es-module-lexer 解析资源
ast
拿到 import 的内容 - 判断 import 的资源是否是
npm
模块 - 返回处理后的资源路径:
"vue" => "/@modules/vue"
将处理的template,script,style等所需的依赖以http
请求的形式,通过query参数形式区分并加载SFC
文件各个模块内容。
#三.手把手实现vite
#1.安装依赖
npm install es-module-lexer koa koa-static magic-string koa、koa-static vite内部使用koa进行编写 es-module-lexer 分析ES6import语法 magic-string 实现重写字符串内容
#2.基本结构搭建
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.静态服务配置
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>/,`$&${devInjection}`) } }) } exports.htmlRewritePlugin = htmlRewritePlugin
在
htm
l的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 = __render export 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常用配置