likes
comments
collection
share

PHP与Vite能摩擦出啥样的爱情火花?

作者站长头像
站长
· 阅读数 8

PHP与Vite能摩擦出啥样的爱情火花?

一、背景

前段日子公司里准备要重构一个拥有10年高龄的网站,当时听到这个消息心里无比激动,因为我现在就是这个网站的维护人员😂😂😂,在现代这个前端技术快速发展的年代很难想象我尽然还在写JQ+PHP模板🤣🤣🤣,这都不是最令我不爽的,最不爽的还是我改一段js代码,还要去后端项目去更改时间戳,才能保证他更新,这样换来的结果就是我要同时去更改两个项目,哪怕我只改了一点,css也是一样。

这个项目前端代码和后端代码是分开的,后端主要是PHP模板渲染html,前端使用的是sea.js模块化加载插件以及JQ

二、技术调研

由于是一个10年高龄的网站了,其中了业务逻辑盘根交错、代码分布错综复杂,想要一把梭哈的话我估计我就离失业不远了😂,只能说是一个页面一个页面慢慢重构然后上线观察,基于这种形势我采用了vue3+vite的技术方向,你要问我为啥不选webpack作为打包工具,我只能说当你开过法拉利跑车后你还会去开拖拉机吗?再者vite作为vue3的亲儿子肯定选他啊。

PHP与Vite能摩擦出啥样的爱情火花?

三、搭建基础框架

一开始本来是想直接按照vite文档上与后端集成的那种方式去加载js和css文件的但是,我TM突然想到前端资源和后端模板压根不在一个项目啊,咋办去拉着脸皮求后端帮我们去动态拉取打包号的manifest资源清单?

manifest.json是一个vite打包之后产出的文件资源清单里面记录了当前这个入口依赖了那些js和css,大致就是下面这个结构。

PHP与Vite能摩擦出啥样的爱情火花?

作为一个有理想和抱负的前端自己选的,在困难也要完成,随即我发动聪明的大脑瞬间一个想法诞生,如果我写一个加载器,根据入口文件名去manifest资源清单里面去寻找,入口当前依赖了那些文件并把它全部加载到页面上不就可以了?说干就干。

四、Vite加载器编写

首先我们先编写一个vite 的class类里面有一个use方法用来加载入口名对应的文件依赖,方法里面还要区分是否为dev环境,如果为dev环境就不需要加载资源清单了直接调用getAssetSource方法拿到文件路径去请求入口文件,入口文件里面的依赖会根据esModule的特性浏览器自动发送请求获取。

    class Vite{
    
      private static config: ViteConfig;
      private static manifestJson: ManifestJson
      private static root: string = 'vite/src/'
      private static is_dev: boolean = window.location.origin.indexOf('dev') > -1;
      private static support_module: boolean = window.support_module;
      private static manifest_loading: boolean = false;
        
      static async use(src: string) {
        //開發環境
        if (this.is_dev) {
          src = this.getAssetSource(src, environment.dev);
          this.loadScript(src, 'module')
          this.loadScript(`${this.config.base}@vite/client`, 'module')
          return
        }
        //頁面多個vite.use時,防重複加載衝突
        if(this.manifest_loading){
          setTimeout(() => {
            this.use(src)
          }, 1000);
          return
        }
        //加載清單文件
        if (!this.manifestJson) {
          await this.getManifest()
        }
        //加載清單源文件
        let src_item = this.getAssetSource(src, environment.production);
        this.importCSS(src_item);
        return this.importSrc(src_item.file)
      }
        
    }

现在让我们来看看getAssetSource方法做了啥

 //獲取manifest對應的源字段值
  static getAssetSource(src: string, ev: environment.dev): string;
  static getAssetSource(src: string, ev: environment.debug | environment.production ):   ManifestItem;
  static getAssetSource(src: string, ev: environment): ManifestItem | string {
    if (ev == 'dev') {
      return this.config.base + this.root + src
    }
    //不支持module 則調用legacy
    if(!this.support_module){
      let file_suffix = src.includes('.ts') ? '-legacy.ts' : '-legacy.js';
      src = src.replace(/\.js|\.ts/g, '') + file_suffix;
    }
    //如果是从清单文件中获取的路径则不需要拼接root路径
    if (src.indexOf(this.root) == -1) {
      src = this.root + src; 
    }
    return this.manifestJson[src];
  }

在dev环境下主要是用来拼接文件完整路径的,在production环境下还要区分浏览器是否支持esModule,不支持的话则需要更改文件名,用来加载@vitejs/plugin-legacy插件打包出来的代码做兼容旧版浏览器的操作,最终返回入口文件名依赖的文件对象

这个就是资源清单里兼容的旧版浏览器的对象名 PHP与Vite能摩擦出啥样的爱情火花? 这个是支持esModel的对象名 PHP与Vite能摩擦出啥样的爱情火花?

dev环境下获得完整文件路径后则调用loadScript去加载文件

 //插入script標籤加載js
  static async loadScript(src: string, type?: string) {
    return new Promise((resolve, reject) => {
      let script = document.createElement('script');
      if (type) script.type = type;
      script.src = src;

      document.head.appendChild(script);

      script.addEventListener('load', ev => {
        resolve(ev);
      })
      script.addEventListener('error', ev => {
        reject(ev)
      })
    })
  }

还要去加载vite的热更新包 this.loadScript(${this.config.base}@vite/client, 'module'),这个样子dev环境就能愉快的使用vue3和热更新做开发了爽的一批。

现在我们来处理一下production环境下的文件加载,首先我们先加载打包好的资源清单文件,然后调用getAssetSource方法如果是production环境他会根据你传入的文件名去资源清单里面找出当前这个入口文件名所依赖的文件对象,比如你在项目里面创建了一个warePay.ts的文件里面初始化了vue,然后你吧这个文件当做一个打包入口传入给vite这个时候产出的资源清单manifest里面就会有一个根据你文件根路径+文件名当做key的一个对象这个对象的value就是你这个文件所依赖的所有资源。

以下是加载资源清单的getManifest方法让我们看看

    //獲取編譯後清單文件
  private static async getManifest() {
    this.manifest_loading = true;
    let manifestSrc = this.config.base + `manifest.json?t=${new Date().getTime()}`
    await $.get(manifestSrc, res => {
      this.manifestJson = res;
    }, 'json');

     // 加載兼容墊片
    if(!this.support_module){
      this.manifestJson['vite/src/legacy-polyfills'] = this.manifestJson['\x00vite/legacy-polyfills']
      let legacy_src = this.config.base + this.manifestJson['vite/src/legacy-polyfills'].file
  
      await this.loadScript(legacy_src)
    }
    this.manifest_loading = false;
  }

方法里面首先去加载资源清单文件并加上时间戳这样只要修改代码,打包后就能拿到最新的资源清单文件加载最新的代码就不需要去更改PHP模板了美滋滋,如果不支持esModule的话还要加载兼容墊片,这里有个小坑资源清单里面的兼容垫片的key前面会有一串Unicode编码的空格。

PHP与Vite能摩擦出啥样的爱情火花? 所以我们要转换一下。

拿到依赖对象后就可以加载css和js文件了,首先我们先调用importCSS来加载css文件,如果当前这个对象还有子依赖的css的话就递归去加载其子依赖的css。

      //生產環境引入css
  static importCSS(item: ManifestItem) {
    if (this.support_module && item.css) {
      for(let v of item.css){
        let id = v.split('/')[1].replace(/\./g,'-');
        if(!document.querySelector(`#${id}`)){
          let css = document.createElement("link");
          css.setAttribute('rel', 'stylesheet');
          css.setAttribute('href', this.config.base + v);
          css.setAttribute('id', id);
          document.head.appendChild(css)
        }
      }
    }
    //检测该文件引入的其他模块是否包含css文件有的话导入
    if (item.imports && item.imports.length > 0) {
      for (let css of item.imports) {
        let src_item = this.manifestJson[css]
        if(src_item && src_item.css && src_item.css.length>0)  this.importCSS(src_item)
      }
    }
  }

随后再去调用importSrc去加载js

  //生產環境引入js
  static importSrc(url: string) {
    url = this.config.base + url;
    if(this.support_module){
      this.loadScript(url, 'module')
      return;
    }
    if (window.System) {
     return window.System.import(url)
    }
  }

同样也要区分是否支持esModule如果支持就直接加载主入口文件就可以了,随后浏览器后自动去请求其子依赖, 如果不支持的话,我们就要调用 window.System.import方法去加载入口文件和其子依赖。

window.System对象是我们在getManifest方法里面加载的兼容垫片js里的对象他可以根据主入口去加载其子依赖,因为他的子依赖路径全部在入口文件里面,大概就是这个样子。

 import("data:text/javascript,") } import { J as t, E as e } from "./JqExtension.ae8d9012.js"; import { L as a } from "./LotteryTicketModel.3322a10b.js"; import { R as s } from "./RegistrationModel.d9d02bf8.js"; import { W as i } from "./WinningModel.6aaaa2e1.js"; import "./BaseModel.69145e29.js";

到此为止加载器基本完成,在PHP模板里面就可以通过Vite.use('xxx/xxx/xxx/warePay.ts')去加载文件愉快的使用vue开发了。

这里是全部代码

interface ViteConfig {
  base: string,
}
interface ManifestItem{
  file: string;
  src?: string,
  isEntry?: boolean,
  css?: string;
  imports?:Array<string>
}
interface ManifestJson{
  [fileKey:string]: ManifestItem
}

enum environment{
  dev = 'dev',
  debug = 'debug',
  production = 'production'
}
class Vite {
  private static config: ViteConfig;
  private static manifestJson: ManifestJson
  private static root: string = 'vite/src/'
  private static is_dev: boolean = window.location.origin.indexOf('dev') > -1;
  private static support_module: boolean = window.support_module;
  private static manifest_loading: boolean = false;

  //設置基本屬性
  static setConfig(config: ViteConfig) {
    this.config = config;
    this.config.base = this.config.base + 'vite_dist/';
    if (this.is_dev) this.config.base = `${location.protocol}//www.dev.8591.com.tw/v31/`;
  }

  //插入script標籤加載js
  static async loadScript(src: string, type?: string) {
    return new Promise((resolve, reject) => {
      let script = document.createElement('script');
      if (type) script.type = type;
      script.src = src;

      document.head.appendChild(script);

      script.addEventListener('load', ev => {
        resolve(ev);
      })
      script.addEventListener('error', ev => {
        reject(ev)
      })
    })
  }

  //獲取編譯後清單文件
  private static async getManifest() {
    this.manifest_loading = true;
    let manifestSrc = this.config.base + `manifest.json?t=${new Date().getTime()}`
    await $.get(manifestSrc, res => {
      this.manifestJson = res;
    }, 'json');

    // 加載兼容墊片
    if(!this.support_module){
      this.manifestJson['vite/src/legacy-polyfills'] = this.manifestJson['\x00vite/legacy-polyfills']
      let legacy_src = this.config.base + this.manifestJson['vite/src/legacy-polyfills'].file
  
      await this.loadScript(legacy_src)
    }
    this.manifest_loading = false;
  }

  //獲取manifest對應的源字段值
  static getAssetSource(src: string, ev: environment.dev): string;
  static getAssetSource(src: string, ev: environment.debug | environment.production ): ManifestItem;
  static getAssetSource(src: string, ev: environment): ManifestItem | string {
    if (ev == 'dev') {
      return this.config.base + this.root + src
    }
    //不支持module 則調用legacy
    if(!this.support_module){
      let file_suffix = src.includes('.ts') ? '-legacy.ts' : '-legacy.js';
      src = src.replace(/\.js|\.ts/g, '') + file_suffix;
    }
    //如果是从清单文件中获取的路径则不需要拼接root路径
    if (src.indexOf(this.root) == -1) {
      src = this.root + src; 
    }
    return this.manifestJson[src];
  }
  
  //生產環境引入js
  static importSrc(url: string) {
    url = this.config.base + url;
    if(this.support_module){
      this.loadScript(url, 'module')
      return;
    }
    if (window.System) {
     return window.System.import(url)
    }
  }

  //生產環境引入css
  static importCSS(item: ManifestItem) {
    if (this.support_module && item.css) {
      for(let v of item.css){
        let id = v.split('/')[1].replace(/\./g,'-');
        if(!document.querySelector(`#${id}`)){
          let css = document.createElement("link");
          css.setAttribute('rel', 'stylesheet');
          css.setAttribute('href', this.config.base + v);
          css.setAttribute('id', id);
          document.head.appendChild(css)
        }
      }
    }
    //检测该文件引入的其他模块是否包含css文件有的话导入
    if (item.imports && item.imports.length > 0) {
      for (let css of item.imports) {
        let src_item = this.manifestJson[css]
        if(src_item && src_item.css && src_item.css.length>0)  this.importCSS(src_item)
      }
    }
  }

  //主加載器
  static async use(src: string) {
    //開發環境
    if (this.is_dev) {
      src = this.getAssetSource(src, environment.dev);
      this.loadScript(src, 'module')
      this.loadScript(`${this.config.base}@vite/client`, 'module')
      return
    }
    //頁面多個vite.use時,防重複加載衝突
    if(this.manifest_loading){
      setTimeout(() => {
        this.use(src)
      }, 1000);
      return
    }
    //加載清單文件
    if (!this.manifestJson) {
      await this.getManifest()
    }
    //加載清單源文件
    let src_item = this.getAssetSource(src, environment.production);
    this.importCSS(src_item);
    return this.importSrc(src_item.file)
  }
}



如何去判断浏览器是否支持esMule了,我这边使用了一个比较笨的方法,在全局模板里面加入这段代码,如果家人们有更好的方案可以留言告知谢谢。

<script nomodule>window.support_module = false;</script>

至此还有一个小坑要说说,那就是热更新的域名因为我这边是前端和后端是不同的域名,在后端PHP模板里面加载热更新插件的时候,他默认会取当前的域名作为连接socket的域名导致,一直连接不上热更新服务器,这个时候我们就要修改vite的配置文件指定域名

  server: {
    host: "0.0.0.0",
    port: 3001,
    hmr: {
      host: 'localhost',
      protocol: 'ws'
    }
  },

当我们指定域名后热更新js就要按照我们指定的域名去连接socket。

经此一役我感觉我变得更强了

PHP与Vite能摩擦出啥样的爱情火花?

但是头发也变少了

PHP与Vite能摩擦出啥样的爱情火花?

五、未来的畅想

在用vite+vue3迁移老页面达到一定的规模后,就可以把他在迁移到nuxt上面求这样就能拥有服务端渲染的能力,才能脱离后端自己定义路由和数据,实现真正的前后端分离的目标。

如果想考虑SEO的话建议使用无头浏览器,去把js渲染成html返回给爬虫。

最后感谢家人们的观看,谢谢大家。

PHP与Vite能摩擦出啥样的爱情火花?