花十分钟来了解下Vite原理吧

Vite是一个面向现代浏览器的一个更轻,更快的web应用开发工具,他基于ECMAScript标准原生模块系统ES Module实现。javascript

他的出现是为了解决webpack冷启动时间过长,另外Webpack HMR热更新反应速度慢的问题。css

使用Vite建立的项目就是一个普通的Vue3应用,相比基于Vue-cli建立的应用少了不少配置文件和依赖。Vite建立的项目开发依赖很是简单,只有Vite和@vue/compiler-sfc, Vite是一个运行工具,compiler-sfc是为了编译.vue结尾的单文件组件。html

Vite目前默认仅支持Vue3.0的版本,在建立项目的时候经过制定不一样的模板也支持使用其余框架好比React,Vite提供了两个子命令。vue

# 开启服务器
vite serve
# 打包
vite build
复制代码

开启服务的时候不须要打包,因此启动速度特别快。在生产环境打包和webpack相似会将全部文件进行编译打包到一块儿。对于代码切割的需求Vite采用的是原生的动态导入来实现的,因此打包结果只能支持现代浏览器,若是老版本浏览器须要使用能够引入Polyfill。java

以前咱们使用Webpack打包是由于浏览器环境并不支持模块化,还有就是模块文件会产生大量的http请求。在现代浏览器模块化已经被支持了,http2也解决了多文件请求的问题。固然若是你的应用须要支持IE浏览器,那么仍是须要打包的。由于IE并不支持ES Module。node

Vite建立的项目几乎不须要额外的配置,默认支持TS、Less, Sass,Stylus,postcss等,可是须要单独安装对应的编译器。同时还支持jsx和web assembly。webpack

Vite带来的好处是提高开发者在开发过程当中的体验,web开发服务器不须要等待能够当即启动,模块热更新几乎是实时的,所需的文件按需编译,避免编译用不到的文件,开箱即用,避免loader及plugins的配置。web

Vite的核心功能包括开启一个静态的web服务器,而且可以编译单文件组件,而且提供HMR功能。npm

当启动vite的时候首先会将当前项目目录做为静态服务器的根目录,静态服务器会拦截部分请求,当请求单文件的时候会实时编译,以及处理其余浏览器不能识别的模块,经过websocket实现hmr。json

咱们本身来实现一下这个功能从而来学习其实现原理。

搭建静态测试服务器

咱们首先实现一个可以开启静态web服务器的命令行工具。vite内部使用的是KOA来实现静态服务器。(ps:node命令行工具能够查看我以前的文章,这里就不介绍了,直接贴代码)。

npm init
npm install koa koa-send -D
复制代码

工具bin的入口文件设置为本地的index.js

#!/usr/bin/env node

const Koa = require('koa')
const send = require('koa-send')

const app = new Koa()

// 开启静态文件服务器
app.use(async (ctx, next) => {
    // 加载静态文件
    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'})
    await next()
})

app.listen(5000)

console.log('服务器已经启动 http://localhost:5000')
复制代码

这样就编写好了一个node静态服务器的工具。

处理第三方模块

咱们的作法是当代码中使用了第三方模块,咱们能够经过修改第三方模块的路径,给他一个标识,而后再服务器中拿到这个标识来处理这个模块。

首先咱们须要修改第三方模块的路径,这里咱们须要一个新的中间件来实现。

须要判断一下当前返回给浏览器的文件是不是javascript,只须要看响应头中的content-type。

若是是javascript,须要找到这个文件中引入的模块路径。ctx.body就是返回给浏览器的内容文件。这里的数据是一个stream,须要转换成字符串来处理。

const stream2string = (stream) => {
    return new Promise((resolve, reject) => {
        const chunks = [];
        stream.on('data', chunk => {chunks.push(chunk)})
        stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))})
        stream.on('error', reject)
    })
}

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,从新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改成from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/');
    }
})
复制代码

接着咱们开始加载第三方模块, 这里一样须要一个中间件,判断请求路径是不是咱们修改过的@module开头,若是是的话就去node_modules里面加载对应的模块返回给浏览器。

这个中间件要放在静态服务器以前。

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
    }
})
复制代码

拿到模块名称以后须要获取模块的入口文件,这里要获取的是ES Module模块的入口文件,须要先找到这个模块的package.json而后再获取这个package.json中的module字段的值也就是入口文件。

// 找到模块路径
const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
const pkg = require(pkgPath);
// 从新给ctx.path赋值,须要从新设置一个存在的路径,由于以前的路径是不存在的
ctx.path = path.join('/node_modules', moduleName, pkg.module);
// 执行下一个中间件
awiat next();
复制代码

这样浏览器请求进来的时候虽然是@modules路径,可是咱们在加载以前将path路径修改成了node_modules中的路径,这样在加载的时候就回去node_modules中获取文件,将加载的内容响应给浏览器。

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
        // 找到模块路径
        const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
        const pkg = require(pkgPath);
        // 从新给ctx.path赋值,须要从新设置一个存在的路径,由于以前的路径是不存在的
        ctx.path = path.join('/node_modules', moduleName, pkg.module);
        // 执行下一个中间件
        awiat next();
    }
})
复制代码

单文件组件处理

以前咱们说过浏览器是没办法处理.vue资源的, 浏览器只能识别js,css等经常使用资源,因此其余类型的资源都须要在服务端处理。当请求单文件组件的时候须要在服务器将单文件组件编译成js模块返回给浏览器。

当浏览器第一次请求文件(App.vue)的时候,服务器会把单文件组件编译成一个对象,先加载这个组件,而后再建立一个对象。

import Hello from './src/components/Hello.vue'
const __script = {
    name: "App",
    components: {
        Hello
    }
}
复制代码

接着再去加载入口文件,此次会告诉服务器编译一下这个单文件组件的模板,返回一个render函数。而后将render函数挂载到刚建立的组件选项对象上,最后导出选项对象。

import { render as __render } from '/src/App.vue?type=template'
__script.rener = __render
__script.__hmrId = '/src/App.vue'
export default __script
复制代码

也就是说vite会发送两次请求,第一次请求会编译单文件文件,第二次请求是编译单文件模板返回一个render函数。

  1. 编译单文件选项

咱们首先来实现一下第一次请求单文件的状况。须要把单文件组件编译成一个选项,这里一样用一个中间件来实现。这个功能要在处理静态服务器只有,处理第三方模块路径以前。

咱们首先须要对单文件组件进行编译。这里须要借助compiler-sfc。

// 处理单文件组件
app.use(async (ctx, next) => {
    if (ctx.path.endsWith('.vue')) {
        // 获取响应文件内容,转换成字符串
        const contents = await streamToString(ctx.body);
        // 编译文件内容
        const { descriptor } = compilerSFC.parse(contents);
        // 定义状态码
        let code;
        // 不存在type就是第一次请求
        if (!ctx.query.type) {
            code = descriptor.script.content;
            // 这里的code格式是, 须要改形成咱们前面贴出来的vite中的样子
            // import Hello from './components/Hello.vue'
            // export default {
            // name: 'App',
            // components: {
            // Hello
            // }
            // }
            // 改造code的格式,将export default 替换为const __script =
            code = code.relace(/export\s+default\s+/g, 'const __script = ')
            code += ` import { render as __render } from '${ctx.path}?type=template' __script.rener = __render export default __script `
        }
        // 设置浏览器响应头为js
        ctx.type = 'application/javascript'
        // 将字符串转换成数据流传给下一个中间件。
        ctx.body = stringToStream(code);
    }
    await next()
})

const stringToStream = text => {
    const stream = new Readable();
    stream.push(text);
    stream.push(null);
    return stream;
}
复制代码
npm install @vue/compiler-sfc -D
复制代码

接着咱们再来处理单文件组件的第二次请求,第二次请求url会带上type=template参数,咱们须要将单文件组件模板编译成render函数。

咱们首先要判断当前请求中有没有type=template

if (!ctx.query.type) {
    ...
} else if (ctx.query.type === 'template') {
    // 获取编译后的对象 code就是render函数
    const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
    // 将render函数赋值给code返回给浏览器
    code = templateRender.code
}
复制代码

这里咱们还要处理一下工具中的process.env,由于这些代码会返回到浏览器中运行,若是不处理会默认为node,致使运行失败。能够在修改第三方模块路径的中间件中修改,修改完路径以后再添加一条修改process.env

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,从新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改成from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"');
    }
})

复制代码

至此咱们就实现了一个简版的vite,固然这里咱们只演示了.vue文件,对于css,less,其余资源都没有处理,不过方法都是相似的,感兴趣的同窗能够自行实现。HRM也没有实现。

#!/usr/bin/env node

const path = require('path')
const { Readable } = require('stream) const Koa = require('koa') const send = require('koa-send') const compilerSFC = require('@vue/compiler-sfc') const app = new Koa() const stream2string = (stream) => { return new Promise((resolve, reject) => { const chunks = []; stream.on('data', chunk => {chunks.push(chunk)}) stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))}) stream.on('error', reject) }) } const stringToStream = text => { const stream = new Readable(); stream.push(text); stream.push(null); return stream; } // 加载第三方模块 app.use(async (ctx, next) => { if (ctx.path.startsWith('/@modules/')) { // 截取模块名称 const moduleName = ctx.path.substr(10); // 找到模块路径 const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json'); const pkg = require(pkgPath); // 从新给ctx.path赋值,须要从新设置一个存在的路径,由于以前的路径是不存在的 ctx.path = path.join('/node_modules', moduleName, pkg.module); // 执行下一个中间件 awiat next(); } }) // 开启静态文件服务器 app.use(async (ctx, next) => { // 加载静态文件 await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'}) await next() }) // 处理单文件组件 app.use(async (ctx, next) => { if (ctx.path.endsWith('.vue')) { // 获取响应文件内容,转换成字符串 const contents = await streamToString(ctx.body); // 编译文件内容 const { descriptor } = compilerSFC.parse(contents); // 定义状态码 let code; // 不存在type就是第一次请求 if (!ctx.query.type) { code = descriptor.script.content; // 这里的code格式是, 须要改形成咱们前面贴出来的vite中的样子 // import Hello from './components/Hello.vue' // export default { // name: 'App', // components: { // Hello // } // } // 改造code的格式,将export default 替换为const __script = code = code.relace(/export\s+default\s+/g, 'const __script = ') code += ` import { render as __render } from '${ctx.path}?type=template' __script.rener = __render export default __script ` } else if (ctx.query.type === 'template') { // 获取编译后的对象 code就是render函数 const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }) // 将render函数赋值给code返回给浏览器 code = templateRender.code } // 设置浏览器响应头为js ctx.type = 'application/javascript' // 将字符串转换成数据流传给下一个中间件。 ctx.body = stringToStream(code); } await next() }) // 修改第三方模块路径 app.use(async (ctx, next) => { if (ctx.type === 'application/javascript') { const contents = await stream2string(ctx.body); // 将body中导入的路径修改一下,从新赋值给body返回给浏览器 // import vue from 'vue', 匹配到from '修改成from '@modules/ ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"'); } }) app.listen(5000) console.log('服务器已经启动 http://localhost:5000') 复制代码
相关文章
相关标签/搜索