赞
踩
戳蓝字"
前端优选
"
关注我们哦
!
法语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
实现Vite
的环境需要es-module-lexer
、koa
、koa-static
、magic-string
模块搭建:
npm install es-module-lexer koa koa-static magic-string
这些模块的功能是:
koa
、koa-static
是vite
内部使用的服务框架;
es-module-lexer
用于分析ES6import
语法;
magic-string
用来实现重写字符串内容。
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);

用于处理项目中的静态资源:
- 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目录下的文件可以直接被访问
- 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
/@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
模块
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标签中注入脚本
.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
方法中进行使用
到这里,基本的一个Vite
就实现了。总结一下就是:通过Koa服务,实现了按需读取文件,省掉了打包步骤,以此来提升项目启动速度,这中间包含了一系列的处理,诸如解析代码内容、静态文件读取、浏览器新特性实践等等。
其实Vite
的内容远不止于此,这里我们实现了非打包开发服务器,那它是如何做到热更新的呢,下次将手把手实现Vite
热更新原理~
历史好文推荐:
点个在看,大家都看
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。