介绍
Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。一个基于 Vue3 单文件组件的非打包开发服务器,当遇见import依赖时,会直接发起http请求对应的模块文件。 源码地址 我自己手写的,路过的顺手给个start感激不尽
听说喊大哥的那兄弟是webpack的开发人员....
ESM
script module 是 ES 模块在浏览器端的实现,目前主流的浏览器都已经支持
<script type="module">
import { createApp } from './main.js';
createApp();
</script>
浏览器会识别添加** type="module"**的 元素,浏览器会把这段内联 script 或者外链 script 认为是 ECMAScript 模块,浏览器将对其内部的 import 引用发起 http 请求获取模块内容。 在 main.js 里,我们用 named export 导出 createApp 函数,在上面的 script 中能获取到该函数
// main.js
export function createApp(){
console.log('create app!');
};
其实到这里,我们基本可以理解 vite 宣称的几个特性了。 • webpack 之类的打包工具为了在浏览器里加载各模块,会借助胶水代码用来组装各模块,比如 webpack 使用 map 存放模块 id 和路径,使用** webpack_require **方法获取模块导出,vite 利用浏览器原生支持模块化导入这一特性,省略了对模块的组装,也就不需要生成 bundle,所以 冷启动是非常快的 • 打包工具会将各模块提前打包进 bundle 里,但打包的过程是静态的——不管某个模块的代码是否执行到,这个模块都要打包到 bundle 里,这样的坏处就是随着项目越来越大打包后的 bundle 也越来越大。而 ESM 天生就是按需加载的,只有 import 的时候才会去按需加载
安装使用
安装
npm install create-vite-app
create-vite-app UprojecctName
进入项目
npm install
npm run dev
即启动了。第一次启动会稍微慢一些,因为要配置.vite_opt_cache。之后便不再需要配置。 我们可以看到main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
在浏览器中被解析为
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
createApp(App).mount('#app')
从这里我们可以看出vite对.js文件中没有路径的import做了路径标识。这符合ESM的思想,先将import的路径做标识,之后再来替换,换做http请求真实的文件地址。 下图是比较粗糙的流程:
手写源码
建立一个index.js对各个中间件进行管理
const Koa = require('koa')
const {serveStaticPlugin} = require('./plugins/servePluginServeStaticPlugin')//第一步
const {moduleRewritePlugin} = require('./plugins/servePluginModuleRewirtePlugin')//第二步
const {moduleResolvePlugin} = require('./plugins/servePluginModuleResolvePlugin')//第三步
const {htmlRewritePlugin} = require('./plugins/servePluginHtmlRewritePlugin')//向html中插入NODE_ENV环境变量
const {vuePlugin} = require('./plugins/servePluginVue')//第四步
function createServer(){
const app = new Koa()
const root = process.cwd()//用来获取调用文件的根目录。
const context = {
app,
root
}
const resolvePlugins = [//插件的集合.洋葱模型
htmlRewritePlugin,
moduleRewritePlugin,//解析import,进行重新@modules
moduleResolvePlugin,
//2)根据标识了@modules解析模块
vuePlugin,
serveStaticPlugin,//1)静态服务插件
]
resolvePlugins.forEach(plugin => plugin(context))//一层一层来循环执行中间件
return app
}
module.exports = createServer
一、利用koa-static读取静态文件
const root = process.cwd()
const static = require('koa-static')
app.use(static(root))
二、将js文件中不是以‘.或./或/’开头的import进行标记
app.use(async (ctx,next) => { await next()//需要等待读取完静态文件 if(ctx.body && ctx.response.is('js')){//判断一下从上一个传下来的body是否有内容,且返回的请求为js let content =await transStreamTostring(ctx.body)//把流转为String,异步,需要等待。在Koa中,所有的异步必须用promise,所以需要用promise包装一下transStreamTostring方法 let result = rewriteImports(content)//识别出来,并进行标识 ctx.body = result//将最终的结果赋值给body, } })
const { readBody } = require('./util.js')
const { parse } = require('es-module-lexer')
const MagicString = require('magic-string')
function rewriteImports(source) {
let imports = parse(source)[0]//静态的再第一数组,动态的在第二个数组里
/**
* import xxx from 'xxx'静态
* import()动态
*/
let magicString = new MagicString(source)
if (imports.length) {
for (let i = 0; i < imports.length; i++) {
let { s, e } = imports[i]
let id = source.substring(s, e)
if (/^[^\/\.]/.test(id)) {
id = `/@modules/${id}`//标识模块
magicString.overwrite(s, e, id)
}
}
}
return magicString.toString()//替换后的结果返回。
}
function moduleRewritePlugin({ app, root }) {
app.use(async (ctx, next) => {
await next()
if (ctx.body && ctx.response.is('js')) {
let content = await readBody(ctx.body)
const result = rewriteImports(content)
ctx.body = result//将重写的内容响应到body上面
}
})
}
exports.moduleRewritePlugin = moduleRewritePlugin
三、根据第二步的标记进行解析,引用正确的文件地址
const moduleReg = /^\/@modules\//
const fs = require('fs').promises
const {resolveVue} = require('./util')//这是根据vue3的特性,将各个文件的引用指向dist下的真实目录。
function moduleResolvePlugin({app,root}){
const vueResolved = resolveVue(root)
app.use(async (ctx,next) =>{
if(!moduleReg.test(ctx.path)){
return next()
}
const id = ctx.path.replace(moduleReg,'')
//应该去当前项目下查找vue对应的真实文件
ctx.type = 'js'//设置响应类型,响应的结果是JS类型。
const content = await fs.readFile(vueResolved[id],'utf8')
ctx.body = content
})
}
exports.moduleResolvePlugin = moduleResolvePlugin
四、将.vue文件中的导出模块解析为对象导出,并引用template
判断路径是不是以.vue结束的,如果是的话才走,不然直接next 需要从vue中导出parse和compileTemplate两个方法。parse是解析内容的,compileTemplate是解析模板的。
const path = require('path')
const fs = require('fs').promises
const { resolveVue } = require('./util')
const defaultExportReg = /((?:^|\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')//.vue文件中的内容
//获取文件内容
//拿到模板编译模块进行编译
let { parse, compileTemplate } = require(resolveVue(root).complier);//parse解析内容的,compileTemplate编辑模板的
let { descriptor } = parse(content)//解析文件内容
if (!ctx.query.type) {
let code = ``
if (descriptor.script) {//把export default变为const__script =
let content = descriptor.script.content;
let replaced = content.replace(defaultExportReg, '$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`
console.log(code)
ctx.body = code
}
if (ctx.query.type == 'template') {
ctx.type = 'js'
let content = descriptor.template.content;
const { code } = compileTemplate({ source: content })
console.log(code)
ctx.body = code
}
})
}
exports.vuePlugin = vuePlugin
附
• 在项目中配置本地启动命令(在全局环境可调用手写的vie进行打包)需要用到npm link • #! /usr/bin/env node 当前环境下,用node来执行此文件 • 项目中用到的包及其作用
| 包 | 作用 |
|---|---|
| es-module-lexer | 解析import语法为AST语法树。 |
| magic-string | 重写字符串的,将字符串转为对象再转为字符串 |
| koa,koa-static | 后端服务及静态文件导入 |
总结
大致这几步就可以实现一个简单的vite。可以去我git上拉下来运行跑一下,整体思路就是这样子的,尤大的中间件很多的,而且这个项目git上还在不断更新。在这里还要感谢一下珠峰的公开课,的确还是可以学到些东西的。