前端新工具--vite從入門到實戰(一)

前段時間尤大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

支持html和js

先不廢話了,咱們先用樸實無話的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)
複製代碼

不出意外 報錯了 咱們要解決兩個問題

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 後面 引號中間的內容摳出來 驗證如下看看是否是加前綴便可,思路明確,代碼就呼之欲出了

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裏找 大概邏輯

  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/',''))
    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_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就大概知道原理了

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

其餘的就思路相似了 好比支持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的現場代碼

正在剪輯中,歡迎關注我,視頻出來我儘快發出來

預告

也歡迎關注公衆號 嘿嘿 一塊兒摸魚

相關文章
相關標籤/搜索