前言
前段時間尤大大在B站直播,介紹了一款新的前端開發工具,利用了瀏覽器自帶的import機制,無論多大的專案,都是秒開,聽起來很誘人,火速看了原始碼,並且最近做了《前端會客廳》後,經過尤大親自講解了設計思路,又有了新感悟,寫個文章總結一下
能和尤大大當面交流vue3的設計思路 收穫真的很大,最近也成為了vue3的contributor,希望下半年能給vue生態貢獻更多的程式碼
#TOC
實戰這個沒啥,github走起吧,賊簡單 https://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機制,
import {log} from './util.js'log('xx')
目錄新建util.js
export function log(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') }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)
不出意外 報錯了 我們要解決兩個問題
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 後面 引號中間的內容摳出來 驗證以下看看是不是加字首即可,思路明確,程式碼就呼之欲出了
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)
支援/@module/
解析的url的時候,加一個判斷即可,主要就是要去node_module裡找 大概邏輯
url開頭是/@module/ 就把剩下的路徑扣下來去node_module裡找到這個庫,把package.json讀出來我們用的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/','')) 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 折騰了半天,終於支援了第一行
.vue元件然後我們把程式碼補全 main.js
import { createApp } from 'vue' // node_moduleimport 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就大概知道原理了
看到app.vue的返回結果沒,這就是我們的目標,核心就是
const __script = { setup() { ... }}import {render as __render} from "/src/App.vue?type=template&t=1592389791757"__script.render = __renderexport 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
其他的就思路類似了 比如支援css
import { createApp } from 'vue' // node_moduleimport 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都是類似的邏輯
下一次來講一下熱更新怎麼做的,其實核心邏輯就是注入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我們就入門了,下篇文章寫一下如何做的熱更新 歡迎關注 ,敬請期待