最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vite 特性和部分源码解析

    正文概述 掘金(政采云前端团队)   2021-07-03   429

    本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战! Vite 特性和部分源码解析

    Vite 特性和部分源码解析

    Vite 的特性

    Vite 的主要特性就是 Bundleless。基于浏览器开始原生的支持 JavaScript 模块功能,JavaScript 模块依赖于 importexport 的特性,目前主流浏览器基本都支持;

    想要查看具体支持的版本可以点击这里;

    那这有什么优势呢?

    去掉打包步骤

    打包是开发者利用打包工具将应用各个模块集合在一起形成 bundle,以一定规则读取模块的代码,以便在不支持模块化的浏览器里使用,并且可以减少 http 请求的数量。但其实在本地开发过程中打包反而增加了我们排查问题的难度,增加了响应时长,Vite 在本地开发命令中去除了打包步骤,从而缩短构建时长。

    按需加载

    为了减少 bundle 大小,一般会想要按需加载,主要有两种方式:

    1. 使用动态引入 import() 的方式异步的加载模块,被引入模块依然需要提前编译打包;
    2. 使用 tree shaking 等方式尽力的去掉未引用的模块;

    而 Vite 的方式更为直接,它只在某个模块被 import 的时候动态的加载它,实现了真正的按需加载,减少了加载文件的体积,缩短了时长;

    Vite开发环境主体流程

    下图是 Vite 在开发环境运行时加载文件的主体流程。

    Vite 特性和部分源码解析

    Vite 部分源码解析

    总体目录结构

    |-CHANGELOG.md
    |-LICENSE.md
    |-README.md
    |-bin
    |  |-openChrome.applescript
    |  |-vite.js
    |-client.d.ts
    |-package.json
    |-rollup.config.js #打包配置文件
    |-scripts
    |  |-patchTypes.js
    |-src
    |  |-client #客户端
    |  |  |-client.ts
    |  |  |-env.ts
    |  |  |-overlay.ts
    |  |  |-tsconfig.json
    |  |-node #服务端
    |  |  |-build.ts
    |  |  |-cli.ts #命令入口文件
    |  |  |-config.ts
    |  |  |-constants.ts #常量
    |  |  |-importGlob.ts
    |  |  |-index.ts
    |  |  |-logger.ts
    |  |  |-optimizer
    |  |  |  |-esbuildDepPlugin.ts
    |  |  |  |-index.ts
    |  |  |  |-registerMissing.ts
    |  |  |  |-scan.ts
    |  |  |-plugin.ts #rollup 插件
    |  |  |-plugins   #插件相关文件
    |  |  |  |-asset.ts
    |  |  |  |-clientInjections.ts
    |  |  |  |-css.ts
    |  |  |  |-esbuild.ts
    |  |  |  |-html.ts
    |  |  |  |-index.ts 
    |  |  |  |-...
    |  |  |-preview.ts
    |  |  |-server
    |  |  |  |-hmr.ts #热更新
    |  |  |  |-http.ts
    |  |  |  |-index.ts
    |  |  |  |-middlewares #中间件
    |  |  |  |  |-...
    |  |  |  |-moduleGraph.ts #模块间关系组装(树形)
    |  |  |  |-openBrowser.ts #打开浏览器
    |  |  |  |-pluginContainer.ts
    |  |  |  |-send.ts
    |  |  |  |-sourcemap.ts
    |  |  |  |-transformRequest.ts
    |  |  |  |-ws.ts
    |  |  |-ssr
    |  |  |  |-__tests__
    |  |  |  |  |-ssrTransform.spec.ts
    |  |  |  |-ssrExternal.ts
    |  |  |  |-ssrManifestPlugin.ts
    |  |  |  |-ssrModuleLoader.ts
    |  |  |  |-ssrStacktrace.ts
    |  |  |  |-ssrTransform.ts
    |  |  |-tsconfig.json
    |  |  |-utils.ts
    |-tsconfig.base.json
    |-types
    |  |-...                  
    

    server 核心方法

    从入口文件 cli.ts,可以看到三个命令对应了 3 个核心的文件&方法;

    1. dev 命令

    文件路径:./server/index.ts;

    主要方法:createServer;

    主要功能:项目的本地开发命令,基于 httpServer 启动服务,Vite 通过对请求路径的劫持获取资源的内容返回给浏览器,服务端将文件路径进行了重写。例如:

    项目源码如下:

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

    经服务端重写后,node_modules 文件夹下的三方包代码路径也会被拼接完整。

    import __vite__cjsImport0_vue from "/node_modules/.vite/vue.js?v=ed69bae0"; 
    const createApp = __vite__cjsImport0_vue["createApp"];
    import App from '/src/pages/back-sky/index.vue';
    

    2.build 命令 文件路径:./build.ts ;

    主要方法:build;

    主要功能:使用 rollup 打包编译

    3.optimize 命令

    文件路径:./optimizer/index.ts;

    主要方法:optimizeDeps;

    主要功能:主要针对第三方包,Vite 在执行 runOptimize 的时候中会使用 rollup 对三方包重新编译,将编译成符合 esm 模块规范的新的包放入 node_modules 下的 .vite 中,然后配合 resolver 对三方包的导入进行处理:使用编译后的包内容代替原来包的内容,这样就解决了 Vite 中不能使用 cjs 包的问题。

    下面是 .vite 文件夹中的 _metadata.json 文件,它在预编译的过程中生成,罗列了所有被预编译完成的文件及其路径。例如:

    {
      "hash": "31d458ff",
      "browserHash": "ed69bae0",
      "optimized": {
        "element-plus/lib/utils/dom": {
          "file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus_lib_utils_dom.js",
          "src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/utils/dom.js",
          "needsInterop": true
        },
        "element-plus": {
          "file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus.js",
          "src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/index.esm.js",
          "needsInterop": false
        },
        "vue": {
          "file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js",
          "src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/vue/dist/vue.runtime.esm-bundler.js",
          "needsInterop": true
        },
        ......
        }
      }
    }
    

    模块解析

    预构建是用来提升页面重载速度,它将 CommonJS、UMD 等转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包程序都要快得多。

    Vite 特性和部分源码解析

    为什么 ESbuild 会更快?

    1. 使用 Go 语言
    2. 重度并行,使用 CPU
    3. 高效使用内存
    4. Scratch 编写,减少使用三方库,避免导致性能不可控

    重写导入为合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便浏览器能够正确导入它们

    热更新

    热更新主体流程如下:

    1. 服务端基于 watcher 监听文件改动,根据类型判断更新方式,并编译资源
    2. 客户端通过 WebSocket 监听到一些更新的消息类型
    3. 客户端收到资源信息,根据消息类型执行热更新逻辑

    Vite 特性和部分源码解析

    下面是服务端热更新的核心 hmr.ts 中的部分判断逻辑;

    如果配置文件或者环境文件发生修改时,会触发服务重启,才能让配置生效。

    if (file === config.configFile || file.endsWith('.env')) {
      // auto restart server 配置&环境文件修改则自动重启服务
      debugHmr(`[config change] ${chalk.dim(shortFile)}`)
      config.logger.info(
        chalk.green('config or .env file changed, restarting server...'),
        { clear: true, timestamp: true }
      )
      await restartServer(server)
      return
    }
    

    html 文件更新时,将会触发页面的重新加载。

    if (file.endsWith('.html')) { // html 文件更新
      config.logger.info(chalk.green(`page reload `) +         chalk.dim(shortFile), {
        clear: true,
        timestamp: true
      })
      ws.send({
        type: 'full-reload',
        path: config.server.middlewareMode
        ? '*'
        : '/' + normalizePath(path.relative(config.root, file))
      })
    } else {
      // loaded but not in the module graph, probably not js
      debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`)
    }
    

    Vue 等文件更新时,都会进入 updateModules 方法,正常情况下只会触发 update,实现热更新,热替换;

    function updateModules(
      file: string,
      modules: ModuleNode[],
      timestamp: number,
      { config, ws }: ViteDevServer
    ) {
      const updates: Update[] = []
      const invalidatedModules = new Set<ModuleNode>()
    	// 遍历插件数组,关联下面的片段
      for (const mod of modules) {
        const boundaries = new Set<{
          boundary: ModuleNode
          acceptedVia: ModuleNode
        }>()
        // 设置时间戳
        invalidate(mod, timestamp, invalidatedModules)
        // 查找引用模块,判断是否需要重载页面
        const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
        // 找不到引用者则会发起刷新
        if (hasDeadEnd) {
          config.logger.info(chalk.green(`page reload `) + chalk.dim(file), {
            clear: true,
            timestamp: true
          })
          ws.send({
            type: 'full-reload'
          })
          return
        }
        updates.push(
          ...[...boundaries].map(({ boundary, acceptedVia }) => ({
            type: `${boundary.type}-update` as Update['type'],
            timestamp,
            path: boundary.url,
            acceptedPath: acceptedVia.url
          }))
        )
      }
      // 日志输出
      config.logger.info(
        updates
          .map(({ path }) => chalk.green(`hmr update `) + chalk.dim(path))
          .join('\n'),
        { clear: true, timestamp: true }
      )
      // 向客户端发送消息,进行热更新操作
      ws.send({
        type: 'update',
        updates
      })
    }
    

    上面代码中的 modules 是热更新时需要执行的各个插件

    for (const plugin of config.plugins) {
      if (plugin.handleHotUpdate) {
        const filteredModules = await plugin.handleHotUpdate(hmrContext)
        if (filteredModules) {
          hmrContext.modules = filteredModules
        }
      }
    }
    

    Vite 会把模块的依赖关系组合成 moduleGraph,它的结构类似树形,热更新中判断哪些文件需要更新也会依赖 moduleGraph;它的文件内容大致如下:

    // moduleGraph 返回的 ModuleNode 大致结构
     ModuleNode {
      id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
      file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
      importers: Set {},
      importedModules: Set {
        ModuleNode {
          id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js?v=32cfd30c',
          file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js',
          ......
          lastHMRTimestamp: 0,
          url: '/node_modules/.vite/vue.js?v=32cfd30c',
          type: 'js'
        },
        ModuleNode {
          id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
          file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
          ......
          url: '/src/pages/back-sky/index.vue',
          type: 'js'
        },
        ModuleNode {
          id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
          file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
          importers: [Set],
          importedModules: Set {},
          acceptedHmrDeps: Set {},
          isSelfAccepting: true,
          transformResult: [Object],
          ssrTransformResult: null,
          ssrModule: null,
          lastHMRTimestamp: 0,
          url: '/node_modules/element-plus/lib/theme-chalk/index.css',
          type: 'js'
        },
        ......
      },
      acceptedHmrDeps: Set {},
      isSelfAccepting: false,
      transformResult: {
        code: 'import __vite__cjsImport0_vue from ' +
          '"/node_modules/.vite/vue.js?v=32cfd30c"; const createApp = ' +
          '__vite__cjsImport0_vue["createApp"];\nimport App from ' +
          "'/src/pages/back-sky/index.vue';\nimport " +
          "'/node_modules/element-plus/lib/theme-chalk/index.css';\n\nconst app = " +
          'createApp(App);\n\nimport { addHistoryMethod } from ' +
          "'/src/pages/back-sky/api/index.js';\nimport {\n  ElButton,\n  ElDropdown,\n  " +
          'ElDropdownMenu,\n  ElDropdownItem,\n  ElMenu,\n  ElSubmenu,\n  ElMenuItem,\n  ' +
          'ElMenuItemGroup,\n  ElPopover,\n  ElDialog,\n  ElRow,\n  ElInput,\n  ' +
          "ElLoading,\n} from '/node_modules/.vite/element-plus.js?v=32cfd30c';\n\n" +
          'app.use(ElButton);\napp.use(ElLoading);\napp.use(ElDropdown);\n' +
          'app.use(ElDropdownMenu);\napp.use(ElDropdownItem);\napp.use(ElMenu);\n' +
          'app.use(ElSubmenu);\napp.use(ElMenuItem);\napp.use(ElMenuItemGroup);\n' +
          'app.use(ElPopover);\napp.use(ElDialog);\napp.use(ElRow);\napp.use(ElInput);\n' +
          "\nconst f = ()=>{\n  return app.mount('#app');\n};\n\nconst $backsky = " +
          "document.getElementById('back-sky');\nif($backsky) {\n  $backsky.innerHTML " +
          "= '';\n  $backsky.appendChild(f().$el);\n} else {\n  window.onload = " +
          "function(){\n    document.getElementById('back-sky') && " +
          "document.getElementById('back-sky').appendChild(f().$el);\n  };\n}\n\n" +
          "window.addHistoryListener = addHistoryMethod('historychange');\n" +
          "history.pushState =  addHistoryMethod('pushState');\nhistory.replaceState " +
          "=  addHistoryMethod('replaceState');\n\n// 监听hash路由变化,不与onhashchange互相覆盖\n" +
          'addHashChange(()=>{\n  setTimeout(() => {\n    const $backsky = ' +
          "document.getElementById('back-sky');\n    if($backsky && " +
          "$backsky.innerHTML === '') {\n      $backsky.appendChild(f().$el);\n    }\n " +
          " },0);\n});\n\nfunction addHashChange(callback) {\n  if('onhashchange' in " +
          'window === false){//浏览器不支持\n    return false;\n  }\n  ' +
          'if(window.addEventListener) {\n    ' +
          "window.addEventListener('hashchange',function(e) {\n      callback && " +
          'callback(e);\n    },false);\n  }else if(window.attachEvent) {//IE 8 及更早 IE ' +
          "版本浏览器\n    window.attachEvent('onhashchange',function(e) {\n      callback " +
          '&& callback(e);\n    });\n  }\n  ' +
          "window.addHistoryListener('history',function(e){\n    callback && " +
          'callback(e);\n  });\n}\n\n\n',
        map: null,
        etag: 'W/"846-Qa424gJKl3YCqHDWXXsM1mFHRqg"'
      },
      ssrTransformResult: null,
      ssrModule: null,
      lastHMRTimestamp: 0,
      url: '/src/pages/back-sky/index.js',
      type: 'js'
    }
    

    原有项目切换

    最后我们来看下如何使用 Vite 去打包一个旧的 Vue 项目;

    首先我们需要升级 Vue3

    npm install vue@next
    

    并为项目添加 vite 配置文件,在根目录下创建 vite.config.js,并为它添加一些基础的配置。

    // vite.config.js
    // vite2.1.5
    const path = require('path');
    import vue from '@vitejs/plugin-vue';
    
    export default {
      // 配置选项
      resolve: {
        alias: {
          '@utils': path.resolve(__dirname, './src/utils')
        },
      },
      plugins: [vue()],
    };
    

    引用的第三方组件库可能也会需要升级,例如:升 element-ui 至 element-plus

    npm install element-plus
    

    Vue3 在 import 时,需使用 createApp 方法进行初始化

    import { createApp } from 'vue';
    import App from './index.vue';
    const app = createApp(App);
    import {
      ElInput,
      ElLoading,
    } from 'element-plus';
    
    app.use(ElButton);
    app.use(ElLoading);
    ......
    

    到这里就可以将项目运行起来了。 注意:Vite 官方不允许省略 .vue 后缀,否则就会报错;

    [plugin:vite:import-analysis] Failed to resolve import "./todoList" from "src/pages/back-sky/components/header/index.vue". Does the file exist?
    /components/header/index.vue:2:23
    1  |  
    2  |  import todoList from './todoList';
    import todoList from './todoList.vue';
    

    最后我们来对比一下该项目两种构建方式时间的对比;

    Webpack 冷启动,耗时 7513ms:

    ⚠ 「wdm」: Hash: 1ad1dd54289cfad8ecbe
    Version: webpack 4.46.0
    Time: 7513ms
    Built at: 2021-05-24 13:59:35
    

    相同项目 Vite 冷启动,耗时 924ms:

    > vite
    Pre-bundling dependencies:
      vue
      element-plus
      @zcy/zcy-request
      element-plus/lib/utils/dom
    (this will be run only when your dependencies or config have changed)
      vite v2.3.3 dev server running at:
      > Local: http://localhost:3000/
      > Network: use `--host` to expose
      ready in 924ms.
    

    二次启动(预编译的依赖已存在),耗时 407ms;

    > vite
      vite v2.3.3 dev server running at:
      > Local: http://localhost:3000/
      > Network: use `--host` to expose
      ready in 407ms.
    

    总结

    使用 Vite 进行本地服务启动和热更新都会有明显的提效,至于编译打包环节的差异点有哪些?效果如何?你们还踩过哪些坑?留言告诉我吧。

    推荐阅读:

    What are CJS, AMD, UMD, and ESM in Javascript?

    推荐阅读

    我在工作中是如何使用 git 的

    15 分钟学会 Immutable

    开源作品

    • 政采云前端小报

    开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)

    招贤纳士

    政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

    如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

    Vite 特性和部分源码解析


    起源地下载网 » Vite 特性和部分源码解析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元