前段时间尤大B站直播,介绍了一款新的前端开发工具,利用了浏览器自带的import机制,不管多大的项目,都是秒开,听起来很诱人,火速看了源码,而且最近作了《前端会客厅》后,通过尤大亲自讲解了设计思路,又有了新感悟,写个文章总结如下javascript
能和尤大当面交流vue3的设计思路 收获真的很大,最近也成为了vue3的contributor,但愿下半年能给vue生态贡献更多的代码css
这个没啥,github走起把,贼简单 github.com/vitejs/vitehtml
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
复制代码
而后咱们看下大概的代码 一如既往的精简前端
➜ vite-app tree
.
├── index.html
├── package.json
├── public
│ └── favicon.ico
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── index.css
└── main.js
复制代码
看下index和main, 就是利用了浏览器自带的import机制,vue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
复制代码
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
复制代码
当浏览器识别type="module"
引入js文件的时候,内部的import 就会发起一个网络请求,尝试去获取这个文件,咱们先整个简单的,把main.js清空如下java
import {log} from './util.js'
log('xx')
复制代码
目录新建util.jsnode
export function log(msg){
console.log(msg)
}
复制代码
可是如今会有一个小报错webpack
Access to script at 'file:///src/main.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
main.js:1 Failed to load resource: net::ERR_FAILED
/favicon.ico:1 Failed to load resource: net::ERR_FILE_NOT_FOUND
复制代码
vite的任务,就是用koa起一个http 服务,来拦截这些请求,返回合适的结果,就欧克了,下面咱们一步步来,为了方便演示,代码简单粗暴git
先不废话了,咱们先用朴实无话的if else试下这个demo的功能github
npm install koa --save
复制代码
拦截路由/ 和xx.js结尾的请求,代码呼之欲出
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const app = new Koa()
app.use(async ctx=>{
const {request:{url} } = ctx
// 首页
if(url=='/'){n
ctx.type="text/html"
ctx.body = fs.readFileSync('./index.html','utf-8')
}else if(url.endsWith('.js')){
// js文件
const p = path.resolve(__dirname,url.slice(1))
ctx.type = 'application/javascript'
const content = fs.readFileSync(p,'utf-8')
ctx.body = content
}
})
app.listen(3001, ()=>{
console.log('听我口令,3001端口,起~~')
})
复制代码
访问locaohost:3001 看下console和network 搞定第一步 支持了import 本底的js文件
看到这里,你应该大概对vite为何快,有一个初步的认识,这就是天生的按需加载呀,告别冗长的webpack打包
咱们不能知足于此,毕竟不可能全部模块都本身写,好比咱们用到的vue 就是从npm 引入的,准确的来讲,是从node_module引入的 改一下main.js
import { createApp } from 'vue'
console.log(createApp)
复制代码
不出意外 报错了 咱们要解决两个问题
Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../".
复制代码
大概意思就是"/", "./", or "../"
开头的路径,才是合法的,这个其实也好说,咱们对main.js里返回的内容作个重写就能够,咱们作个规定,把import from 后面,不是上面仨符号开头的,加一个/@module/
前缀
// 替换前
import { createApp } from 'vue'
// 替换后
import { createApp } from '/@module/vue'
复制代码
咱们新建一个函数,其实vite是用的es-module-lexer来解析成ast拿到import的地址,咱们既然是乞丐版,整个土鳖的正则把
// 单引号双引号都支持 我真是个小机灵
/from ['"]([^'"]+)['"]/g
复制代码
大概就是from 后面 引号中间的内容抠出来 验证如下看看是否是加前缀便可,思路明确,代码就呼之欲出了
function rewriteImport(content){
return content.replace(/from ['"]([^'"]+)['"]/g, function(s0,s1){
// . ../ /开头的,都是相对路径
if(s1[0]!=='.'&& s1[1]!=='/'){
return `from '/@modules/${s1}'`
}else{
return s0
}
})
}
if(url.endsWith('.js')){
// js文件
const p = path.resolve(__dirname,url.slice(1))
ctx.type = 'application/javascript'
const content = fs.readFileSync(p,'utf-8')
ctx.body = rewriteImport(content)
}
复制代码
在刷新,报了另一个错 说明模块重写完毕,下面咱们须要支持@module的前缀
GET http://localhost:3001/@modules/vue net::ERR_ABORTED 404 (Not Found)
复制代码
解析的url的时候,加一个判断便可,主要就是要去node_module里找 大概逻辑
思路清楚了,代码就呼之欲出了
---- 孟德鸠斯
复制代码
注意node_module里的文件,也是有import 别的npm 包的,因此记得返回也要用rewriteImport包如下
if(url.startsWith('/@modules/')){
// 这是一个node_module里的东西
const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))
const module = require(prefix+'/package.json').module
const p = path.resolve(prefix,module)
const ret = fs.readFileSync(p,'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
复制代码
而后报了一个小错 就是vue源码里有用process.ENV判断环境的,咱们浏览器client里设置如下便可
Uncaught ReferenceError: process is not defined
at shared:442
复制代码
咱们注入一个全局变量 ,vite的作法是解析html以后,经过plugin的方式注入,逼格很高,我这乞丐版,凑和replace一下把
if(url=='/'){
ctx.type="text/html"
let content = fs.readFileSync('./index.html','utf-8')
content = content.replace('<script ',` <script> window.process = {env:{ NODE_ENV:'dev'}} </script> <script `)
ctx.body = content
}
复制代码
打开console yeah 折腾了半天,终于支持了第一行
而后咱们把代码补全 main.js
import { createApp } from 'vue' // node_module
import App from './App.vue'
// import './index.css'
createApp(App).mount('#app')
复制代码
App.vue
<template>
<h1>你们好 kkb欢迎你</h1>
<h2>
<span>count is {{count}}</span>
<button @click="count++">戳我</button>
</h2>
</template>
<script> import {ref,computed} from 'vue' export default { setup(){ const count = ref(0) function add(){ count.value++ } const double = computed(()=>count.value*2) return {count,add,double} } } </script>
复制代码
ok不出所料的报错了 毕竟咱们node环境还没支持单文件组件,你们其实看下vite项目的network就大概知道原理了
const __script = {
setup() {
...
}
}
import {render as __render} from "/src/App.vue?type=template&t=1592389791757"
__script.render = __render
export default __script
复制代码
好了 写代码 拼呗
咱们就不考虑缓存了,直接解析,咱们直接用vue官方的@vue/compiler-sfc来整单文件,用@vue/compiler-dom
来把template解析成 ,这块核心逻辑都是这里vue核心包的,咱们反而没作啥,思路通了写代码
if(url.indexOf('.vue')>-1){
// vue单文件组件
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8'))
if(!query.type){
ctx.type = 'application/javascript'
// 借用vue自导的compile框架 解析单文件组件,其实至关于vue-loader作的事情
ctx.body = ` // option组件 ${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))} import { render as __render } from "${url}?type=template" __script.render = __render export default __script `
}
}
复制代码
看下结果 完美下一步搞定type=template的解析就能够,
直接@vue/compiler-dom
把html解析城render就能够, 能够在线体验一波
if(request.query.type==='template'){
// 模板内容
const template = descriptor.template
// 要在server端吧compiler作了
const render = compilerDom.compile(template.content, {mode:"module"}).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render)
}
复制代码
体验一下
其余的就思路相似了 好比支持css
import { createApp } from 'vue' // node_module
import App from './App.vue' // 解析成额外的 ?type=template请求
import './index.css'
createApp(App).mount('#app')
复制代码
代码直接呼
if(url.endsWith('.css')){
const p = path.resolve(__dirname,url.slice(1))
const file = fs.readFileSync(p,'utf-8')
const content = `const css = "${file.replace(/\n/g,'')}" let link = document.createElement('style') link.setAttribute('type', 'text/css') document.head.appendChild(link) link.innerHTML = css export default css `
ctx.type = 'application/javascript'
ctx.body = content
}
复制代码
其实内部设置css的逻辑,应该再client端注入,最好每一个link加一个id,方便后续作热更新
其实支持less啥的逻辑都是相似的,vite用了esbuild来解析typescript, 比官方的tsc快了几十倍,快去体验一波 vite的实现 ifelse太多了,不不献丑了,下次在写 其实支持less sass都是相似的逻辑
以上逻辑其实你们直接去看vite的import解析源码更合适 ,我只是但愿能讲明白思路 代码略丑 请轻喷 就是经过拦截import的http请求,来实现无需打包,自带按需加载的工具
下一次来说一下热更新怎么作的,其实核心逻辑就是注入socket.io ,后端数据变了,通知前端便可,大概类型以下 在线代码
// 不一样的更新方式
interface HMRPayload {
type:
| 'js-update'
| 'vue-reload'
| 'vue-rerender'
| 'style-update'
| 'style-remove'
| 'full-reload'
| 'sw-bust-cache'
| 'custom'
timestamp: number
path?: string
changeSrcPath?: string
id?: string
index?: number
customData?: any
}
复制代码
switch (type) {
case 'vue-reload': Vue组件更新
case 'vue-rerender': Vue-template更新
case 'style-update': css更新
case 'style-remove': css删除
case 'js-update': js更新
case 'full-reload': 全量重载更新
复制代码
到此为止基本上vite咱们就入门了,下篇文章写一下如何作的热更新 欢迎关注 ,敬请期待
github.com/shengxinjin… 下面的vite-mini文件夹 其实这个代码仓库是咱们开课吧搞得一个节目《前端会客厅》,由我、winter还有尤大搞得一次聊vue的现场代码
正在剪辑中,欢迎关注我,视频出来我尽快发出来
也欢迎关注公众号 嘿嘿 一块儿摸鱼