前端新工具—vite从入门到实践

作者:蜗牛老师
转发链接:https://zhuanlan.zhihu.com/p/149033579

前言

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

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

#TOC

  1. 入门使用
  2. 自己实现支持html
  3. 支持js
  4. 支持第三方模块
  5. 支持.vue组件
  6. 支持import css
  7. 预告

补充

  1. vite开发环境利用浏览器的import机制,打包右内置的rollup,所以已经可以直接用了

如果对vite还不太了解,请看这一篇《一个由 Vue 作者尤雨溪开发的 web 开发工具—vite

实战

这个没啥,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机制,

<!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清空以下

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 ['"]([^'"]+)['"]/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请求,来实现无需打包,自带按需加载的工具

下一次来讲一下热更新怎么做的,其实核心逻辑就是注入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我们就入门了,下篇文章写一下如何做的热更新 欢迎关注 ,敬请期待

推荐Vue学习资料文章:

一文带你搞懂Vue3 底层源码

9个优秀的 VUE 开源项目

细聊Single-Spa + Vue Cli 微前端落地指南「实践」

通俗易懂的Vue异步更新策略及 nextTick 原理

通俗易懂的Vue响应式原理以及依赖收集

原生JS +Vue实现框选功能

Vue.js轮播库热门精选

一文带你搞懂vue/react应用中实现ssr(服务端渲染)

Vue+CSS3 实现图片滑块效果

教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(上)

教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(下)

vue实现一个6个输入框的验证码输入组件

一用惊人的Vue实践技巧「值得推荐」

Vue常见的面试知识点汇总(上)「附答案」

Vue常见的面试知识点汇总(下)「附答案」

Kbone原理详解与小程序技术选型

为什么我不再用Vue,改用React?

让Jenkins自动部署你的Vue项目「实践」

20个免费的设计资源 UI套件背景图标CSS框架

Deno将停止使用TypeScript,并公布五项具体理由

前端骨架屏都是如何生成的

Vue原来可以这样写开发效率杠杠的

用vue简单写一个音乐播放组件「附源码」

为什么Vue3.0不再使用defineProperty实现数据监听?

「干货」学会这些Vue小技巧,可以早点下班和女神约会

探索 Vue-Multiselect

细品30张脑图带你从零开始学Vue

Vue后台项目中遇到的技术难点以及解决方案

手把手教你Electron + Vue实战教程(五)

手把手教你Electron + Vue实战教程(四)

手把手教你Electron + Vue实战教程(三)

手把手教你Electron + Vue实战教程(二)

手把手教你Electron + Vue实战教程(一)

收集22种开源Vue模板和主题框架「干货」

如何写出优秀后台管理系统?11个经典模版拿去不谢「干货」

手把手教你实现一个Vue自定义指令懒加载

基于 Vue 和高德地图实现地图组件「实践」

一个由 Vue 作者尤雨溪开发的 web 开发工具—vite

是什么让我爱上了Vue.js

1.1万字深入细品Vue3.0源码响应式系统笔记「上」

1.1万字深入细品Vue3.0源码响应式系统笔记「下」

「实践」Vue 数据更新7 种情况汇总及延伸解决总结

尤大大细说Vue3 的诞生之路「译」

提高10倍打包速度工具Snowpack 2.0正式发布,再也不需要打包器

大厂Code Review总结Vue开发规范经验「值得学习」

Vue3 插件开发详解尝鲜版「值得收藏」

带你五步学会Vue SSR

记一次Vue3.0技术干货分享会

Vue 3.x 如何有惊无险地快速入门「进阶篇」

「干货」微信支付前后端流程整理(Vue+Node)

带你了解 vue-next(Vue 3.0)之 炉火纯青「实践」

「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分

「干货」Vue+Element前端导入导出Excel

「实践」Deno bytes 模块全解析

细品pdf.js实践解决含水印、电子签章问题「Vue篇」

基于vue + element的后台管理系统解决方案

Vue仿蘑菇街商城项目(vue+koa+mongodb)

基于 electron-vue 开发的音乐播放器「实践」

「实践」Vue项目中标配编辑器插件Vue-Quill-Editor

基于 Vue 技术栈的微前端方案实践

消息队列助你成为高薪 Node.js 工程师

Node.js 中的 stream 模块详解

「干货」Deno TCP Echo Server 是怎么运行的?

「干货」了不起的 Deno 实战教程

「干货」通俗易懂的Deno 入门教程

Deno 正式发布,彻底弄明白和 node 的区别

「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台

「实践」深入对比 Vue 3.0 Composition API 和 React Hooks

前端网红框架的插件机制全梳理(axios、koa、redux、vuex)

深入Vue 必学高阶组件 HOC「进阶篇」

深入学习Vue的data、computed、watch来实现最精简响应式系统

10个实例小练习,快速入门熟练 Vue3 核心新特性(一)

10个实例小练习,快速入门熟练 Vue3 核心新特性(二)

教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」

2020前端就业Vue框架篇「实践」

详解Vue3中 router 带来了哪些变化?

Vue项目部署及性能优化指导篇「实践」

Vue高性能渲染大数据Tree组件「实践」

尤大大细品VuePress搭建技术网站与个人博客「实践」

10个Vue开发技巧「实践」

是什么导致尤大大选择放弃Webpack?【vite 原理解析】

带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】

带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】

实践Vue 3.0做JSX(TSX)风格的组件开发

一篇文章教你并列比较React.js和Vue.js的语法【实践】

手拉手带你开启Vue3世界的鬼斧神工【实践】

深入浅出通过vue-cli3构建一个SSR应用程序【实践】

怎样为你的 Vue.js 单页应用提速

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

【新消息】Vue 3.0 Beta 版本发布,你还学的动么?

Vue真是太好了 壹万多字的Vue知识点 超详细!

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

手把手教你深入浅出vue-cli3升级vue-cli4的方法

Vue 3.0 Beta 和React 开发者分别杠上了

手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件

Vue3 尝鲜

总结Vue组件的通信

Vue 开源项目 TOP45

2020 年,Vue 受欢迎程度是否会超过 React?

尤雨溪:Vue 3.0的设计原则

使用vue实现HTML页面生成图片

实现全栈收银系统(Node+Vue)(上)

实现全栈收银系统(Node+Vue)(下)

vue引入原生高德地图

Vue合理配置WebSocket并实现群聊

多年vue项目实战经验汇总

vue之将echart封装为组件

基于 Vue 的两层吸顶踩坑总结

Vue插件总结【前端开发必备】

Vue 开发必须知道的 36 个技巧【近1W字】

构建大型 Vue.js 项目的10条建议

深入理解vue中的slot与slot-scope

手把手教你Vue解析pdf(base64)转图片【实践】

使用vue+node搭建前端异常监控系统

推荐 8 个漂亮的 vue.js 进度条组件

基于Vue实现拖拽升级(九宫格拖拽)

手摸手,带你用vue撸后台 系列二(登录权限篇)

手摸手,带你用vue撸后台 系列三(实战篇)

前端框架用vue还是react?清晰对比两者差异

Vue组件间通信几种方式,你用哪种?【实践】

浅析 React / Vue 跨端渲染原理与实现

10个Vue开发技巧助力成为更好的工程师

手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】

1W字长文+多图,带你了解vue的双向数据绑定源码实现

深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现

手把手教你D3.js 实现数据可视化极速上手到Vue应用

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】

Vue3.0权限管理实现流程【实践】

后台管理系统,前端Vue根据角色动态设置菜单栏和路由

作者:蜗牛老师

转发链接:https://zhuanlan.zhihu.com/p/149033579

原文链接:,转发请注明来源!