2021 年,从 Vue3 到 Vite

2,193 阅读6分钟

前言

2020 年已经过去,Vue3 在社区内的生态已经慢慢丰富起来,Element Plus 和 antd-Vue 已经发布 vue3 版本;

Evan You 发布了一个新的名词 Vite 法语中意味 fast, 发音为 /vit/, 默认处理创建 vue3 的 app 工程,也可支持 react 的工程项目。

2021/01/01,Evan You 发布了 Vite 2.0.0 版本,把 vite 重新定位为一个纯粹的建构工具。但是 2.0 版本还处于 beta 阶段, 和 1.0 想比 Vite 的核心概念不变,在配置层面提供了更加丰富和规范的标准和思路。

那么今天我们还是从 1.X 开始出发结合 vue3 踏出新年的第一步吧~

项目初始化

首先我们使用 Vite 新建一个项目:大致如下,

// 1. 初始化项目
╭─~/vite
╰─➤  npm init vite-app vite1.x-demo
Scaffolding project in /Users/leiliao/vite/vite1.x-demo...

Done. Now run:

  cd vite1.x-demo
  npm install (or `yarn`)
  npm run dev (or `yarn dev`)

// 2、安装其他依赖
╭─~/vite
╰─➤  cd vite1.x-demo

╭─~/vite/vite1.x-demo
╰─➤  npm i @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier koa-compose@^4.1.0 prettier typescript vue-eslint-parser eslint-plugin-prettier -D

+ koa-compose@4.2.0
+ eslint-config-prettier@7.1.0
+ prettier@2.2.1
+ eslint-plugin-prettier@3.3.0
+ vue-eslint-parser@7.3.0
+ eslint@7.17.0
+ @typescript-eslint/parser@4.11.1
+ @typescript-eslint/eslint-plugin@4.11.1
+ typescript@4.1.3

╭─~/vite/vite1.x-demo
╰─➤  npm i vuex@next vue-router@next

+ vuex@4.0.0-rc.2
+ vue-router@4.0.2

// 3、 添加配置文件

// tsconfig.json
{
  "include": ["./**/*.ts"],
  "exclude": [
    "node_modules",
    "dist",
    "./**/*.js"
  ],
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "baseUrl": ".",
    "paths":{ "/@/*": ["src/*"]},
    "jsx": "preserve",
    "sourceMap": true /* Generates corresponding '.map' file. */,
    "outDir": "./dist" /* Redirect output structure to the directory. */,
    "strict": true /* Enable all strict type-checking options. */,
    "noUnusedLocals": true /* Report errors on unused locals. */,
    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
    "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
  }
}

// vite.config.ts
import {} from 'vite';

import path from 'path';
export default {
  alias: {
    '/@/': path.resolve(__dirname, './src'),
  },
};

// .prettierrc.js
module.exports = {
  semi: true,
  trailingComma: 'all',
  singleQuote: true,
  printWidth: 100,
  tabWidth: 2,
  endOfLine: 'auto',
};


// .eslintrc.js
module.exports = {
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser', // Specifies the ESLint parser
    ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
    ecmaFeatures: {
      tsx: true, // Allows for the parsing of JSX
    },
  },
  plugins: ['@typescript-eslint'],
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
    'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
    'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
  ],
  rules: {
    'vue/html-self-closing': 'off',
    'vue/max-attributes-per-line': 'off',
  },
  env: {
    browser: true,
    node: true,
  },
};

4、修改入口文件

  • main.js -> main.ts
  • index.html
    • <script type="module" src="/src/main.js"></script> -> <script type="module" src="/src/main.ts"></script>

5、添加声明文件

// src/shim.d.ts
declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

declare module '*.scss' {
  const content: any;
  export default content;
}

// src/source.d.ts
declare const React: string;
declare module '*.json';
declare module '*.png';
declare module '*.jpg';

Vue3

vue3 最重要的就是 Proxy 和 setup 函数,今天主要看一下这两个方面

  1. Object.defineProperty 看起
const obj = {}
Object.defineProperty(obj, "a", {
  value : 1,
  writable : false, // 是否可写 
  configurable : false, // 是否可配置
  enumerable : false // 是否可枚举
})

// 上面给了三个false, 下面的相关操作就很容易理解了
obj.a = 2 // 无效
delete obj.a // 无效
for(key in obj){
  console.log(key) // 无效 
}

obj.b = 3 // 3
obj // {b: 3, a: 1}
delete obj.b // true

从上面的例子已经看出 Object.defineProperty 只能对已经存在的属性进行拦截,如果是新增的属性,没有办法再次进行拦截。所以 vue2 提供了vm.$set 让开发者可以把对象某些属性进行重新拦截。

但是我们使用 proxy 可以解决这个问题。


var obj = new Proxy({}, {
  deleteProperty: function(target, prop) {
    return false;
  }
});

obj.b = 1

delete obj.b // false

proxy 是返回一个新的对象,只有新对象才拥有 proxy 的特性,无需手动对新增属性进行拦截,可撤销,可对数组等属性进行拦截。

  1. setup() setup 函数是 vue3 的精髓:
// HelloWorkd.tsx
import { defineComponent, ref, reactive, computed } from 'vue';

export default defineComponent({
  name: 'App',
  props: {
    modelValue: {
      type: Number,
      default: '',
    },
  },
  emits: ['update:modelValue', 'change'],
  setup(props, { attrs, slots, emit }) {
    const count = ref(0);

    const title = computed(() => `hello vue3-${count.value}`);

    const info = reactive({ title, version: 'vue3', count });

    function handleClick() {
      info.count++;
      emit('update:modelValue', info.count);
    }

    return () => (
      <>
        <button onClick={handleClick}>count is: {count.value}</button>

        <p>title.value: {title.value}</p>
        <p>info.title: {info.title}</p>
        <p>info.version: {info.version}</p>
      </>
    );
  },
});

// app.vue
<template>
  <img alt="Vue logo" src="/@/assets/logo.png" />
  <HelloWorld v-model="counts" />

  <div>counts :{{ counts }}</div>
</template>

<script>
import { ref } from 'vue';

import HelloWorld from '/@/components/HelloWorld';

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
  setup() {
    const counts = ref(0);
    return {
      counts,
    };
  },
};
</script>
  • tsx 中在 setup 函数中直接返回 jsx templete 代码
  • vue 文件中需要返回所有 ref, reactive , function ,才能在 templete 中使用
  • templete 可以对 ref 自动进行展平,但是在 tsx 只用不可以
  • tsx 版本无法解析 v-model 等语法糖
//App.tsx
port { defineComponent, ref } from 'vue';
// import { RouterLink, RouterView } from 'vue-router';
import HelloWorld from './components/HelloWorld';

export default defineComponent({
  name: 'App',
  setup() {
    const counts = ref(0);
    return () => (
      <>
        <img alt="Vue logo" src="/@/assets/logo.png" />
        <HelloWorld v-model={counts.value} />

        <div>counts :{counts.value}</div>
      </>
    );
  },
});

在 webpack 时代,可以借用 babel-plugin 对 jsx v-model 进行转译和支持

  • 有关 setup 的其他语法可自行阅读文档

Vite

Evan You 在发布了 vite 之后发出一句感叹:“我好像再也不会回到 webpack 了”。

这引发了我的兴趣,这也是为什么本文开头会使用 vite 作为搭建 vue3 的工具。

es module

在浏览器专辑的 浏览器渲染原理 中有比较过 srcript 标签 module 引用其他 js 的情况,它会不阻塞地像同域中的其他 js 发出请求, es module 大多数浏览器已经支持。

esbuild

An extremely fast JavaScript bundler

对比其他包工具简直可以根据打包工具的速度分为两种:esbuild 和 其他打包工具。。。

koa

基于洋葱模型的 nodejs 下一代 web 框架, 利用中间件可以快速搭建定制化的 web 服务

vite 原理概述

const resolvedPlugins = [
    // rewrite and source map plugins take highest priority and should be run
    // after all other middlewares have finished
    sourceMapPlugin,
    moduleRewritePlugin,
    htmlRewritePlugin,
    // user plugins
    ...toArray(configureServer),
    envPlugin,
    moduleResolvePlugin,
    proxyPlugin,
    clientPlugin,
    hmrPlugin,
    ...(transforms.length || Object.keys(vueCustomBlockTransforms).length
      ? [
          createServerTransformPlugin(
            transforms,
            vueCustomBlockTransforms,
            resolver
          )
        ]
      : []),
    vuePlugin,
    cssPlugin,
    enableEsbuild ? esbuildPlugin : null,
    jsonPlugin,
    assetPathPlugin,
    webWorkerPlugin,
    wasmPlugin,
    serveStaticPlugin
  ]

Vite 简单来说就是一个由中间件包裹的本地 web service 服务,中间件提供了一些编译和热更新的功能

  • serveStaticPlugin 由 koa 创建一个静态服务器默认返回 index.html
  • 利用 <script src='main.ts' module ></script> 请求同域(服务器文件夹下)的 main.ts 文件,从 main.ts 出发请求其他其他资源 。
  • webWorkerPlugin wasmPlugin 处理 wasm 、webWorker 文件。
  • 处理 js 中请求的 Asset 文件。
  • hmrPlugin 提供热更新的能力
  • 处理用户自定的 plugin。
  • moduleRewritePlugin 标记请求代码中的 node_module 模块引用(因为浏览器中没有 node_module)为 /@modules/
  • moduleResolvePlugin 处理请求中的 /@modules/ 的模块查找 node_module 中正确的引用
  • vuePlugin 对 vue 文件进行语法分析和编译, 处理和解析 vue 文件 , 监听 wacth 及时通知 hmr。(css t)
  • esbuildPlugin 对请求为 js/jsx 类型的文件进行编译
    • 正因为 esbuild 的快速可以在 es module 在动态请求的过程中对文件进行编译,使得 vite 达到了秒开的速度

以上是大致中间件执行的顺序(可能会有错误)。

由于洋葱模型的原因,导致中间件和代码执行的顺序会和数组中不太一致

rollup

由于业界很多轮子暂时还不支持 esm 引用,vite 提供选项可以在配置指定某些包在服务初始化时利用 rollup 进行重新的打包

server.listen = (async (port: number, ...args: any[]) => {
    if (optimizeDeps.auto !== false) {
      await require('../optimizer').optimizeDeps(config)
    }
    return listen(port, ...args)
  }) as any

optimizer 模块就是 rollup 的处理模块

调试 vite

其实介绍了这么多可能我们的想法还在表面,大家可以跟着后面的方法自己深入源码的探讨。

  • 第一步根据文首进行项目搭建(现在版本已经是 2.0 具体参照官方命令)
  • 第二步下载对应版本的 vite 代码
$ cd vite && yarn && yarn dev
$ yarn link # 新开另一个终端窗口执行 yarn link 命令
$ cd vite_xxxx && yarn remove vite && yarn link vite # 切换到第一步搭建的项目 link vite
$ yarn dev --debug

总结

前端的开发环境和编译由于浏览器的差异导致需要各种工具来适配,带来的体验就是开发起来各种蛋疼和配置,我的梦想呢就是在开发的时候直接写最新语法而不用再讨论兼容、代码版本。

vite 的出现感觉又进一步拉近了我的这个梦想。

各种新技术来袭仿佛天花乱坠,但是只要分析其本质其实都是一些细小的模块和只是拼凑而来,而学习新技术总是会让你在这其中找到自己的缺点和漏洞。

那么我们就不要排斥,不要抱怨,从 2021 开始,从 vue3 开始, 从 vite 开始,一起进步。

内容粗略,分析不到位,很多 plugin 其实我也没有仔细去看里面的原理,如有错误请及时指出。