前端新工具--vite从入门到实战(一)

前段时间尤大B站直播,介绍了一款新的前端开发工具,利用了浏览器自带的import机制,无论多大的项目,都是秒开,听起来很诱人,火速看了源码,并且最近做了《前端会客厅》后,经过尤大亲自讲解了设计思路,又有了新感悟,写个文章总结以下

能和尤大当面交流vue3的设计思路 收获真的很大,最近也成为了vue3的contributor,希望下半年能给vue生态贡献更多的代码

实战

这个没啥,github走起把,贼简单

github.com/vitejs/vite

$ 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机制,

<!DOCTYPE html>

<htmllang="en">

<head>

<metacharset="UTF-8">

<linkrel="icon"href="/favicon.ico" />

<title>Vite App</title>

</head>

<body>

<divid="app"></div>

<scripttype="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清空以下

import {log} from'./util.js'

log('xx')

目录新建util.js

exportfunctionlog(msg){

console.log(msg)

}

但是现在会有一个小报错

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 服务,来拦截这些请求,返回合适的结果,就欧克了,下面我们一步步来,为了方便演示,代码简单粗暴

支持html和js

先不废话了,我们先用朴实无话的if else试下这个demo的功能

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')

}elseif(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)

不出意外 报错了 我们要解决两个问题

1. 不是合法的相对路径,浏览器报错

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 后面 引号中间的内容抠出来 验证以下看看是不是加前缀即可,思路明确,代码就呼之欲出了

functionrewriteImport(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)

支持/@module/

解析的url的时候,加一个判断即可,主要就是要去node_module里找 大概逻辑

  1. url开头是/@module/ 就把剩下的路径扣下来
  2. 去node_module里找到这个库,把package.json读出来
  3. 我们用的import语法,所以把package.json里的Module字段读出来,就是项目的入口 替换回来即可

思路清楚了,代码就呼之欲出了

---- 孟德鸠斯

注意node_module里的文件,也是有import 别的npm 包的,所以记得返回也要用rewriteImport包以下

if(url.startsWith('/@modules/')){

// 这是一个node_module里的东西

const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))

constmodule = 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 折腾了半天,终于支持了第一行

.vue组件

然后我们把代码补全 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'

exportdefault {

setup(){

const count = ref(0)

functionadd(){

count.value++

}

const double = computed(()=>count.value*2)

return {count,add,double}

}

}

</script>

ok不出所料的报错了 毕竟我们node环境还没支持单文件组件,大家其实看下vite项目的network就大概知道原理了

  1. 发起.vue的请求后,先把script解析出来,然后里面加上请求template和css的import语句
  2. 把template解析成render函数,返回拼成一个组件
  3. 还是那句话,思路通了,代码就呼之欲出了,当时看到这里,觉得尤大真是优秀啊

看到app.vue的返回结果没,这就是我们的目标,核心就是

const __script = {

setup() {

...

}

}

import {render as __render} from"/src/App.vue?type=template&t=1592389791757"

__script.render = __render

exportdefault __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

其他的就思路类似了 比如支持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,方便后续做热更新

支持typescript

其实支持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

}

client代码

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的现场代码

正在剪辑中,欢迎关注我,视频出来我尽快发出来

预告

也欢迎关注公众号 嘿嘿 一起摸鱼

以上是 前端新工具--vite从入门到实战(一) 的全部内容, 来源链接: utcz.com/a/25672.html

回到顶部