Vite2 + React 原理与移动端项目实战

1,531 阅读9分钟

前言

本文会从传统的bundle模式和基于ESM的构建模式引出vite,之后详谈vite内部的工作方式,一步步带领大家边分析边剖析原理,最后通过react+vite2的移动端的项目实战帮助大家实操入手,大家是否已经迫不及待,摩拳擦掌了~闲话少说,让我们进入正文

首先我们来看一下官方文档的封面,有一行字深深抓住了我的眼球:下一代前端开发与构建工具

image-20210909144105017

下一代?这么猛~那上一代大家最多用的是什么呢,恐怕就是webpack了。想当初学习webpack真的耗费了不少的精力呢!!

让大家一个词来形容vite,估计大家首先想到就是快!其次呢?就是香!

那到底香在哪里快在哪里呢,它的特性都有什么呢?系好安全带,我们发车了~

一、传统的bundle模式(webpack)和基于ESM的构建模式(vite)

我们在使用webpack启动文件的时候,会从入口的entry文件去索引整个项目的文件,然后去编译成一个或多个单独的js文件,即使我们运用了一些例如代码拆分的方法,但是也会一次生成所有路由下的编译后的文件。这也是我们常被调侃的npm run dev启动服务时间越来越长,而且启动时间也会随项目越来越复杂呈现指数级的形式增长。

那我们再来看看vite基于ESM的构建模式是怎么解决这个问题滴?

按照图的形式,很容易理解,就是在浏览器请求对应 URL 的时候,再提供对应的文件。就是仅提供对应路由下的模块的编译文件,而没有索引全部代码的这一过程,所以项目启动是非常快滴~

是不是有的老铁们还是不是很理解,没关系,我再整体详细描述一遍~

二、详谈vite内部工作方式

还是根据上面的基于ESM的构建模式图片为基础,这里我们结合react进行原理举例剖析

1.type="module"

vite首先会在本地帮你启动一个服务器,当浏览器读取到index.html这个宿主页时,会发现里面会用type="module"的方式去加载文件。

<script type="module" src="/src/pages/m.tsx"></script>

我设置成type="module"有什么好处呢,就是因为Vite别出心裁的利用了浏览器原生ES Module的支持,就会将这个脚本视为 ES 标准模块,并以模块的方式去加载、执行。

2.依赖预构建 ESModule

那么有些同学会问了,我们的依赖管理是采用 npm 的,而 npm 包大部分是采用 CommonJS 标准而未兼容 ES 标准的呀,这点不用担心哈,vite官方文档也写了,在开发阶段中,会首选采用依赖预构建的过程。那依赖预构建又是什么,大家可以看下文档,这里我给大家截图把具体内容呈现出来了~

其中有一点,就是Vite 的开发服务器会将所有代码视为原生 ES 模块,会将作为 CommonJSUMD发布的依赖项转换为 ESM。这样的话在m.tsx文件中可以以es模块的方式来进行组织和编写,且不需要打包。

1)首先加载m.tsx文件:

import React from 'react'
import ReactDOM from 'react-dom'
import GoToMUse from 'src/pages/m/GoToMUse'

ReactDOM.render(
  <React.StrictMode>
    <GoToMUse />
  </React.StrictMode>,
  document.getElementById('root')
)

2)再依次发送加载react.jsreact-dom.jsGoToMUse.tsx文件

这些都加载过来之后,把它创建应用程序在页面中显示。这样做的好处有两点:

  • 我在开发阶段不需要打包,不管我的项目有多大,我的速度都是最快的。
  • 我这是按需加载的,我用到这个页面,我就加载这个页面相关的文件,不相关的绝不加载,这样的话开发的速度就更好了。

3.react模块和react-dom裸模块解析

那接下来深层次的东西我们需要知道,m.tsx这个文件加载过来之后,浏览器只支持相对地址,也就是说根据index地址,根据相对地址加载./GoToMUse.tsx或者等等都可以,但是你让我加载reactreact-dom这个模块,你让我去哪加载呢,我不能安装一个npm吧。那怎么办呢?

1)解析编译**/src/pages/m.tsx** 文件

这里我们可以看下经过vite编译后的/src/pages/m.tsx 文件:

image-20210909180115518

所以这里就又提到预构建了,预构建除了我们刚才说的那个功能之外,还有一点,就是它会提前进行一个预打包,提前把第三方的模块例如react.jsreact-dom.js预打包放在这个/node_modules/.vite目录下:

可以看到**from "react"**重写为

from "/node_modules/.vite/react.js?v=b460ee5e";

可以看到**from "react-dom"**重写为

from "/node_modules/.vite/react-dom.js?v=b460ee5e"

那么肯定有细心的小伙伴又要提问了,后面的v=xxxx这一串字符串是干嘛的嘞?

2)浏览器缓存

其实不难理解,就是为了控制缓存的:

通过给cache-control属性写为max-age=31536000,immutable来设置为永久的强制缓存,一旦被缓存,这些请求将永远不会再到达开发服务器,永远从本地读取文件,然后向文件名添加hash值来控制版本更新。

所以将依赖文件的缓存判断交给了浏览器,从而减少了vite端的工作量。

3)依赖预构建提高性能

这个时候我们打开react.js?v=b460ee5e文件看一下:

执行预构建和重写为合法url的步骤,这里官方文档对依赖预构建的另一个目的也做了详细的介绍:

官方文档:一些包将它们的 ES 模块构建作为许多单独的文件相互导入。例如,lodash-es 有超过 600 个内置模块!当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。

通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求了!

4.文件编译解析

接下来就加载我们自己的相对地址的源代码,但这有一个问题比如GoToMUse.tsx文件,浏览器又不认识,vite需要额外做些事。

1)特殊的import的资源进行解析转换

把那些特殊的import的资源进行解析转换后,GoToMUse.tsx文件会变成一个js文件,你可以看到GoToMUse.tsx文件的Response里的Content-Type是application/javascript文件:

这个时候我们再打开这个GoToMUse.tsx看下编译后的文件

看到了它的解析文件,说明vite对他进行解析转换了,就好比weback的babel-loader的功能,那么vite是依赖什么进行转换的呢,没错就是esbuild

vite使用esbuild来作为例如( TSX & TypeScript)部分文件类型的解析器,它的解析与编译与webpack是不同的。webpack是提前将所有文件编译为浏览器可以接受的类型,而vite则是在接收浏览器发起的 http 请求之后再去编译对应文件,这个时候有的小伙伴就会提出问题了,每次页面加载都需要编译一次文件,这对页面的加载速度考验极大呀!这里我们通过这张图比较发现,esbuild的构建速度是极快的!

三、移动端项目实战

讲了这么多知识点,看来是时候开启项目实战的关卡,坐稳了~

我们先从项目基本配置(安装、样式、组件库、代理和环境变量)开始:

1.安装

npm init @vitejs/app

然后根据指引一步步选择react + ts

2.css预处理器的配置

Vite 提供了对 .scss, .sass, .less, .styl.stylus 文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖。

这里我就使用了sass:

# .scss and .sass
npm install -D sass

3.Yep-React组件库的引入

这里我们先试着样式全部引入,打包看一次:

import '@jdcfe/yep-react/dist/@jdcfe/yep-react.css';

目前的m.css静态资源的大小为166.85kb,那么下面我们进行一次瘦身,使用按需加载的方法,那么这里我们需要一个插件

npm i vite-plugin-imp -D

然后进行配置:

import reactRefresh from '@vitejs/plugin-react-refresh'
import path from 'path'
import { defineConfig } from 'vite'
import vitePluginImp from 'vite-plugin-imp'
// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  plugins: [
    reactRefresh(),
    vitePluginImp({
      libList: [
        {
          libName: '@jdcfe/yep-react',
          style: (name) => `@jdcfe/yep-react/lib/${name}/style/index.scss`,
        },
      ],
    }),
  ],
})

这个时候我们将import '@jdcfe/yep-react/dist/@jdcfe/yep-react.css'去掉,通过按需加载引入模块的方式:

拓展:这里就不得不提到vite插件的知识了

这里会简单提一嘴,等到后续文章,再给大家详细介绍一下vite插件的知识

Vite插件是一个拥有名称创建钩子(build hook)或生成钩子(output generate hook)的对象。可以这么说,在vite2的全新架构下,你所做的所有事情是靠vite插件来解析的,比如开发vue时关于vue组件的解析等等,以及钩子和插件都有自己的执行顺序,那么后续文章再给大家介绍~

4.proxy代理配置

为了解决本地dev.jd.com访问其他域名导致的跨域问题了,这里选择使用设置proxy代理来解决这个问题

import reactRefresh from '@vitejs/plugin-react-refresh'
import path from 'path'
import { defineConfig } from 'vite'
import vitePluginImp from 'vite-plugin-imp'
// https://vitejs.dev/config/
export default defineConfig({
  server: {
    proxy: {
      '/devServer': {
        target: 'https://xxx.jd.com/',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/devServer/, ''),
      },
    },
  },
})

5.环境变量配置

那么我在上面用proxy代理进行配置时,怎么识别出是开发环境还是线上环境,从而进行devServer路径的替换呢?

这不得不提出通过环境变量来控制了,官方文档有详细介绍

官方文档:vite在一个特殊的import.meta.env对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量

Import.meta.env.MODE 应用运行的模式

这里有一段配置代码,注释已经为大家标注好了,大家可以自行参考一下:

type EnvConfig = {
  baseUrl: string
}

/**
 * 配置编译环境和线上环境之间的切换
 *
 * baseUrl: 域名地址
 */

const config: EnvConfig = {
  baseUrl: '',
}
switch (import.meta.env.MODE) {
  case 'development':
    /*
     * 开发环境    => npm run dev
     */
    config.baseUrl = '/devServer'
    break
  case 'production':
    /*
     * 线上环境 => npm run build
     */
    config.baseUrl = 'https://xxx.jd.com'
    break
}
export default config

再谈谈process.env为什么用import.meta.env来替代呢

1.process.env来自Node.js,在前端中并不实际存在这个概念

process.env不是一个通用的概念,我们现在在写代码时再用它就是其实是给后来代码维护者增加负担。因为现在代码是在后端构建,前端运行。像有些可以在浏览器端直接运行nodejs的各种打包工具,在浏览器端用Service Worker直接构建我们的模块。这个时候,可能2年或者3年后,再也不需要命令行端的构建工具了,我们所有代码都在Web IDE里面去写了,这个时候如果有新人接手项目发现有process.env,不知道是哪里来的,发现前端里没有这个概念。

2.不作限制,容易泄漏敏感的环境变量

process.env在node端能够读取整个操作系统环境里面的环境变量,前端写代码时肯定不能把这个环境变量暴露出去,我们又要做各种限制,这个时候前端的process.env和后端的process.env是两种东西了,前端拿到的是一个有限的环境变量,它其实更像是一种配置而不是环境、这个时候我们拿这个名字存在一种误解。

3.NODE_ENV有各种约定俗成的用法

之前有人有疑问,在staging环境(预发布环境),把NODE_ENV设置成staging会有各种奇怪的问题呢,这是为什么呢?

NODE_ENV约定俗成的用法并不包括staging的这个概念,它是来自于express约定俗成的环境变量,不是说node运行时里面就有这个东西,也不是说所有前端构建工具中里面默认就有这个东西。而且我们依赖的第三方库它也不一定知道我们使用的哪个构建工具来做的,一般来说NODE_ENV只存在于production、development和testing三种可能性,然后如果想把它运用成其他情况的话,就会出现问题。

用process.env时候不会意识到这个问题,我们会觉得环境变量不就是各种配置嘛,各种配置不就是基于我的需求来改的么,那这个时候随便改NODE_ENV就会出现问题,所以我们要把这个概念从前端构建中剥离出去,前端构建我们用NODE_ENV多半是判断我们的环境,到底是打包成一个开发包,测试包还是一个生产包,这个时候我们用import.meta.env可以更好的帮我们了解到它其实是一种环境配置而不是一个环境变量。

接下来就是根据需求,设置自己喜好的个性化和兼容性的配置了:

6.定义别名

别名的使用,主要是为了避免出现大量的相对路径,我通常愿意把src文件夹下的路径配置成@。

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

那么我们就可以在文件中开心的使用了

import GoToMUse from '@/pages/m/GoToMUse'

但是小伙伴们会发现有一个红线,提示我们:

简单来说就是说找不到模块“@/pages/m/GoToMUse”或其相应的类型声明

这个时候我们要看看tsconfig.json的配置文件了,加上一个配置可以完美解决

"paths": {
   "@/*": ["src/*"]
 },

7.m端和pc端不同配置文件解析

1)通过--config 命令行选项指定一个配置文件

"scripts": {
  "dev": "vite",
  "build:m": "vite build --config vite.config.build.ts",
  "build:pc": "vite build --config vite.config.build.ts",
},

2)pc端和m端情景配置,巧用mode模式

我们可以通过传递 --mode 选项标志来覆盖命令使用的默认模式

"scripts": {
  "dev": "vite",
  "build:m": "vite build --mode m --config vite.config.build.ts",
  "build:pc": "vite build --mode pc --config vite.config.build.ts",
},

然后如官网所介绍,如果配置文件需要基于(servebuild)命令或者不同的模式来决定选项,则可以选择导出这样一个函数:

export default ({ mode }) => {
  if (mode === 'pc') {
    return {
      // pc 独有配置
    }
  } else {
    return {
      // m 独有配置
    }
  }
})

3)多页面的应用模式配置

我们目前的结构目录大体如下:

├── .husky
├── node_modules
├── scripts
├── src
    ├── componetnts
    ├── pages
    ├── utils
└── vite.config.ts
└── vite.config.build.ts
└── m.html
└── pc.html
└── tsconfig.json
...

然后我们在构建过程中,只需指定多个 .html 文件作为入口点即可:

// vite.config.build.ts
export default ({ mode }) => {
  return {
    build: {
      rollupOptions: {
        input: {
          [mode]: resolve(__dirname, `${mode}.html`),
        }
      },
    },
  }
}

4)打包后的目录配置

这里打包后文件的配置,就根据自己的喜好来了,想打包成什么样的配置,我们借助rollup的打包配置,这里提供给大家一个思路,大家再根据rollup的文档

自行调配美酒哈~

// vite.config.build.ts
import * as pack from './package.json'
export default ({ mode }) => {
  return {
    build: {
      rollupOptions: {
        input: {
          output: {
            dir: `dist/${mode}/${pack.version}`,
          },
          [mode]: resolve(__dirname, `${mode}.html`),
        }
      },
    },
  }
}

效果如下图哈:

5)浏览器兼容性配置

vite 为打包后的文件提供了传统浏览器兼容性支持,这里用到了一个插件@vitejs/plugin-legacy

// vite.config.js
import legacy from '@vitejs/plugin-legacy'

export default {
  plugins: [
    legacy({
      ignoreBrowserslistConfig: true,
      targets: ['ie >= 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime'] // 当面向IE11时需要使用到regenerator-runtime
    })
  ]
}

打包出来的文件如下,会多出来一些如legacy为文件的名称

html文件中也多出来一些关于nomodule的脚本为了兼容:

四、Vite构建工具参与开发者

image-20210909111838412

从上面的图可以看出,vite这个项目目前发展是十分健康的,每个月都会有新的贡献者参与进来,来帮助分类和处理PR。

五、总结

好了,老铁们,这里是swag~君,本节文章先写到这里,到站下车,喜欢本文章的记得点赞加收藏呦,后续会继续推出关于vite插件以及其他采坑的地方与兄弟萌分享

参考资料

developer.mozilla.org/zh-CN/docs/…

cn.vitejs.dev/guide/dep-p…

discord.com/channels/80…

esbuild.github.io/faq/#benchm…

vue.w3ctech.com/