有了 vite,还须要 webpack 么?

原创声明:本文首发于公众号:前端琐话(qianduansuohua),欢迎关注css

前言

前两天尤大在 vue 3.0 beta 直播中提到了一个 vite 的工具,并且还发推表示再也回不去 webpack 了, 还引来了 webpack 核心开发人员肖恩的搞笑回复, 那就让咱们一块儿来看一下 vite 到底有啥魔力?html

什么是 Vite?

github:github.com/vitejs/vite前端

Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。vue

它主要具备如下特色:node

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译

那废话少说,咱们先直接来试用一下。webpack

$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
复制代码

咱们来看下生成的代码, 由于 vite 尝试尽量多地镜像 vue-cli 中的默认配置, 因此咱们会发现看上去和 vue-cli 生成的代码没有太大区别。git

├── index.html
├── package.json
├── public
│   └── favicon.ico
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    ├── index.css
    └── main.js
复制代码

那咱们看下入口 index.html 和 main.jsgithub

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

// main.js
// 只是引用的是最新的 vue3 语法,其他没有啥不一样
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')
复制代码

发现主要的不一样在于多了这么个东西web

<script type="module" src="/src/main.js"></script>
复制代码

那下面咱们就来看下这是个啥?vue-cli

原理

ESM

script module 是 ES 模块在浏览器端的实现,目前主流的浏览器都已经支持

其最大的特色是在浏览器端使用 exportimport 的方式导入和导出模块,在 script 标签里设置 type="module"

<script type="module"> import { createApp } from './main.js‘; createApp(); </script>
复制代码

浏览器会识别添加 type="module"<script> 元素,浏览器会把这段内联 script 或者外链 script 认为是 ECMAScript 模块,浏览器将对其内部的 import 引用发起 http 请求获取模块内容。 在 main.js 里,咱们用 named export 导出 createApp 函数,在上面的 script 中能获取到该函数

// main.js
export function createApp(){
    console.log('create app!');
};
复制代码

其实到这里,咱们基本能够理解 vite 宣称的几个特性了。

  • webpack 之类的打包工具为了在浏览器里加载各模块,会借助胶水代码用来组装各模块,好比 webpack 使用 map 存放模块 id 和路径,使用 webpack_require 方法获取模块导出,vite 利用浏览器原生支持模块化导入这一特性,省略了对模块的组装,也就不须要生成 bundle,因此 冷启动是很是快的
  • 打包工具会将各模块提早打包进 bundle 里,但打包的过程是静态的——无论某个模块的代码是否执行到,这个模块都要打包到 bundle 里,这样的坏处就是随着项目愈来愈大打包后的 bundle 也愈来愈大。而 ESM 天生就是按需加载的,只有 import 的时候才会去按需加载

看到这里是否是会好奇那 vite 到底作了什么,咱们直接用浏览器的 ESM 不就行了,那咱们就来试试。

Vite 运行

提供 web server

咱们在刚才生成的代码库里,不经过 npm run dev 来启动项目,直接经过浏览器打开 index.html, 会看到下面一个报错

在浏览器里使用 ES module 是使用 http 请求拿到模块,因此 vite 的一个任务就是启动一个 web server 去代理这些模块,vite 里是借用了 koa 来启动了一个服务

export function createServer(config: ServerConfig): Server {
  // ...
  const app = new Koa<State, Context>()
  const server = resolveServer(config, app.callback())
  
  // ...
  const listen = server.listen.bind(server)
  server.listen = (async (...args: any[]) => { if (optimizeDeps.auto !== false) { await require('../optimizer').optimizeDeps(config) } return listen(...args) }) as any return server } 复制代码

模块解析

那咱们就在本地起一个静态服务,再来打开一下 index.html 来看下

大概意思是说,找不到模块 vue,"/", "./", or "../"开头的 import 路径,才是合法的。

import vue from 'vue'
复制代码

也就是说浏览器中的 ESM 是获取不到导入的模块内容的,平时咱们写代码,若是不是引用相对路径的模块,而是引用 node_modules 的模块,都是直接 import xxx from 'xxx',由 Webpack 等工具来帮咱们找这个模块的具体路径进行打包。可是浏览器不知道你项目里有 node_modules,它只能经过相对路径或者绝对路径去寻找模块。

那这就引出了 vite 的一个实现核心 - 拦截浏览器对模块的请求并返回处理后的结果

咱们来看下 vite 是怎么处理的?

/@module/前缀

经过工程下的 main.js 和开发环境下的实际加载的 main.js 对比,发现 main.js 内容发生了改变,由

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')
复制代码

变成了

import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app')
复制代码

为了解决 import xxx from 'xxx' 报错的问题,vite 对这种资源路径作了一个统一的处理,加一个/@module/前缀。 咱们在 src/node/server/serverPluginModuleRewrite.ts 源码这个 koa 中间件里能够看到 vite 对 import 都作了一层处理,其过程以下:

  • 在 koa 中间件里获取请求 body
  • 经过 es-module-lexer 解析资源 ast 拿到 import 的内容
  • 判断 import 的资源是不是绝对路径,绝对视为 npm 模块
  • 返回处理后的资源路径:"vue" => "/@modules/vue"

支持 /@module/

/src/node/server/serverPluginModuleResolve.ts 里能够看到大概的处理逻辑是

  • 在 koa 中间件里获取请求 body
  • 判断路径是否以 /@module/ 开头,若是是取出包名
  • 去node_module里找到这个库,基于 package.json 返回对应的内容

文件编译

上面咱们提到的是对普通 js module 的处理,那对于其余文件,好比 vuecssts等是如何处理的呢?

咱们以 vue 文件为例来看一下,在 webpack 里咱们是使用的 vue-loader 对单文件组件进行编译,实际上 vite 一样的是拦截了对模块的请求并执行了一个实时编译。

经过工程下的 App.vue 和开发环境下的实际加载的 App.vue 对比,发现内容发生了改变

本来的 App.vue

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script> import HelloWorld from './components/HelloWorld.vue'; export default { name: 'App', components: { HelloWorld, }, }; </script>
<style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
复制代码

变成了

import HelloWorld from '/src/components/HelloWorld.vue';

const __script = {
    name: 'App',
    components: {
        HelloWorld,
    },
};

import "/src/App.vue?type=style&index=0&t=1592811240845"
import {render as __render} from "/src/App.vue?type=template&t=1592811240845"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue"
export default __script
复制代码

这样就把本来一个 .vue 的文件拆成了三个请求(分别对应 script、style 和template) ,浏览器会先收到包含 script 逻辑的 App.vue 的响应,而后解析到 template 和 style 的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。

// App.vue?type=style
import { updateStyle } from "/vite/hmr"
const css = "\n#app {\n font-family: Avenir, Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n"
updateStyle("7ac74a55-0", css)
export default css
复制代码
// App.vue?type=template
import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = /*#__PURE__*/
_createVNode("img", {
    alt: "Vue logo",
    src: "/src/assets/logo.png"
}, null, -1 /* HOISTED */
)

export function render(_ctx, _cache) {
    const _component_HelloWorld = _resolveComponent("HelloWorld")

    return (_openBlock(),
    _createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, {
        msg: "Hello Vue 3.0 + Vite"
    })], 64 /* STABLE_FRAGMENT */
    ))
}
复制代码

实际上在看到这个思路以后,对于其余的类型文件的处理几乎都是相似的逻辑,根据请求的不一样文件类型,作出不一样的编译处理。

实际上 vite 就是在按需加载的基础上经过拦截请求实现了实时按需编译

后语

到这里咱们实际上就基本了解了 vite 的原理,虽然在目前的生态下,彻底替代 webpack 还不可能,但毕竟是一种的新的解决方案的探索。 而实际上,除了 vite, 社区里相似的方案还有 snowpack, 有兴趣的能够去了解一下。