从 0 到 1 的项目管理系统:脚手架篇 - H5 基础脚手架

5,699 阅读3分钟

前言

H5 基础脚手架:极速构建项目

上一篇讲到了快速构建项目的通用 webpack 构建,此篇将结合业务修改 H5 的脚手架

小声 BB,不是一定适合你的项目,具体项目具体对待,符合自身业务的才是最好的

资源添加版本号

看过之前博客的同学,应该知道在创建版本的时候引入了版本号的概念,在创建分支版本的时候,带上版本号,创建的分支名为 feat/0.0.01,而我们发布的静态资源也是带了版本

之前有让同学关注过 url 上面的版本号哈

改造 Webpack 路径

const branch = childProcess.execSync('git rev-parse --abbrev-ref HEAD').toString().replace(/\s+/, '')
const version = branch.split('/')[1] // 获取分支版本号

output: {
  publicPath: `./${version}`,
}

如上,我们先将分支版本号获取,再修改资源引用路径,即可完成资源版本的处理,如下图所示,h5 链接被修改成常规 url,引用资源带上了版本号

版本号的优势

  1. 可以快速定位 code 版本,针对性的修复
  2. 每个版本资源保存上在 cdn 上,快速回滚只需要刷新 html,不必重新构建发布

Webpack Plugin 开发

直接添加版本的方法是不是蠢出天际,so 我们随便写个插件玩玩好了

const HtmlWebpackPlugin = require('html-webpack-plugin');
const childProcess = require('child_process')
const branch = childProcess.execSync('git rev-parse --abbrev-ref HEAD').toString().replace(/\s+/, '')
const version = branch.split('/')[1]

class HotLoad {
  apply(compiler) { 
    compiler.hooks.beforeRun.tap('UpdateVersion', (compilation) => {
      compilation.options.output.publicPath = `./${version}/`
    })
  }
}

module.exports = HotLoad;

module.exports = {
  plugins: [
    new HotLoad() 
]}

如上,我们创建一个 webpack plugin,通过监听 webpack hooks 在任务执行之前修改对应的资源路径,通用性上升。

高级定制化

CDN 资源引入

此外之前的博客我们还引入了 cdn 的概念,我们可以将上述插件升级,构建的时候引入通用的 cdn 资源,减少构建与加载时间。

const scripts = [
  'https://cdn.bootcss.com/react-dom/16.9.0-rc.0/umd/react-dom.production.min.js',
  'https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js'
]


class HotLoad {
  apply(compiler) { 
    compiler.hooks.beforeRun.tap('UpdateVersion', (compilation) => {
      compilation.options.output.publicPath = `./${version}/`
    })
    
    compiler.hooks.compilation.tap('HotLoadPlugin', (compilation) => {
      HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('HotLoadPlugin', (data, cb) => {
        scripts.forEach(src => [
          data.assetTags.scripts.unshift({
            tagName: 'script',
            voidTag: false,
            attributes: { src }
          })
        ])
        cb(null, data)
      })
    })
  }
}

上述我们借助了 HtmlWebpackPlugin 提供的 alterAssetTags hooks,主动添加了 react 相关的第三方 cdn 链接,这样在生产环境中,同域名下面的项目,可以复用资源

通过缓存解决加载 js

对于长期不会改变的静态资源,可以直接将资源缓存在本地,下次项目打开的时候可以直接从本地加载资源,提高二次开启效率。

首先,我们选择 indexDB 来进行缓存,因为 indexDB 较 stroage 来说,容量会更大,我们本身就需要缓存比较大的静态资源所以需要更大容量的 indexDB 来支持

import scripts from './script.json';
import styles from './css.json';
import xhr from './utils/xhr'
import load from './utils/load'
import storage from './stroage/indexedDb'

const _storage = new storage()
const _load = new load()
const _xhr = new xhr()

class hotLoad {

  constructor(props) {
    this.loadScript(props)
    this.issafariBrowser = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
  }

  async loadScript(props = []) {
    const status = await _storage.init()
    let _scripts = scripts
    const expandScripts = props

    if (status) {
      for (let script of _scripts) {
        const { version, type = 'js', name, url } = script
        if (this.issafariBrowser) {
          await _load.loadJs({ url })
        } else {
          const value = await _storage.getCode({ name, version, type });
          if (!value) {
            const scriptCode = await _xhr.getCode(url || `${host}/${name}/${version}.js`)
            if (scriptCode) {
              await _load.loadJs({ code: scriptCode })
              await _storage.setCode({ scriptName: `${name}_${version}_${type}`, scriptCode: scriptCode })
            }
          } else {
            await _load.loadJs({ code: value })
          }
        }
      }

      for (let style of styles) {
        const { url, name, version, type = 'css' } = style
        if (this.issafariBrowser) {
          await _load.loadCSS({ url })
        } else {
          const value = await _storage.getCode({ name, version, type })
          if (!value) {
            const cssCode = await _xhr.getCode(url || `${host}/${name}/${version}.css`)
            _storage.setCode({ scriptName: `${name}_${version}_${type}`, scriptCode: cssCode })
            _load.loadCSS({ code: cssCode })
          } else {
            _load.loadCSS({ code: value })
          }
        }
      }
    } else {
      for (let script of _scripts) {
        const { version, name } = script
        const scriptCode = await _xhr.getCode(script.url || `${host}/${name}/${version}.js`)
        if (scriptCode) {
          await _load.loadJs({ code: scriptCode })
        }
      }
      for (let style of styles) {
        const { url, name, version } = style
        const cssCode = await _xhr.getCode(url || `${host}/${name}/${version}.css`)
        _load.loadCSS({ code: cssCode })
      }
    }

    for (let script of expandScripts) {
      const { url } = script
      await _load.loadJs({ url })
    }

  }
}

window.hotLoad = hotLoad

上述代码是将第三方资源,通过 xhr 获取之后,使用 Blob + URL.createObjectURL 制造本地链接,使用 js 动态添加到页面中去。

class load {
  constructor() { }
  // 加载js
  loadJs({ url, code, callback }) {
    let oHead = document
      .getElementsByTagName('HEAD')
      .item(0);
    let script = document.createElement('script');
    script.type = "text/javascript";
    return new Promise(resolve => {
      if (url) {
        script.src = url
      } else {
        let blob = new Blob([code], { type: "application/javascript; charset=utf-8" });
        script.src = URL.createObjectURL(blob);
      }
      oHead.appendChild(script)
      if (script.readyState) {
        script.onreadystatechange = () => {
          if (script.readyState == "loaded" || script.readyState == "complete") {
            script.onreadystatechange = null;
            callback && callback();
            resolve(true)
          }
        }
      } else {
        script.onload = () => {
          callback && callback();
          resolve(true)
        }
      }
    })
  }

  // 加载css
  loadCSS({ url, code }) {
    let oHead = document
      .getElementsByTagName('HEAD')
      .item(0);
    let cssLink = document.createElement("link");
    cssLink.rel = "stylesheet"
    return new Promise(resolve => {
      if (url) {
        cssLink.href = url
      } else {
        let blob = new Blob([code], { type: "text/css; charset=utf-8" });
        cssLink.type = "text/css";
        cssLink.rel = "stylesheet";
        cssLink.rev = "stylesheet";
        cssLink.media = "screen";
        cssLink.href = URL.createObjectURL(blob);
      }
      oHead.appendChild(cssLink);
      resolve(true)
    })
  }
}

// 通过 xhr 拉取静态资源
class xhr {
  constructor() {
    this.xhr;
    if (window.XMLHttpRequest) {
      this.xhr = new XMLHttpRequest();
    } else {
      this.xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }
  }

  // 同步请求js
  getCode(url) {
    return new Promise(resolve => {
      this.xhr.open('get', url, true);
      this.xhr.send(null);
      this.xhr.onreadystatechange = () => {
        if (this.xhr.readyState == 4) {
          if (this.xhr.status >= 200 && this.xhr.status < 300 || this.xhr.status == 304) {
            resolve(this.xhr.responseText)
          }
        }
      }
    })
  }
}

弱网环境下的直接加载

弱网环境下的缓存加载

对比上图,可以明显看出,在网络环境波动的情况下,有缓存加持的网页二次开启的速度会明显提效,当然在性能上,由于需要判断第三方静态资源版本以及从本地读取资源,会消耗部分时间,可以针对业务自行取舍

优劣势对比

优势

  1. 统一接管项目的依赖,可以针对性的升级通用资源
  2. 资源有版本依赖概念,缓存在本地的时候,可以快速切换版本
  3. 二次加载速度会上升
  4. 配合 Service Worker 有奇效

劣势

  1. 统一升级的过程,可能有引用项目存在不匹配造成程序崩溃的情况
  2. 其实强缓存所有共用静态 cdn 资源也是 ok 的,干嘛那么费劲呢

上述的插件有没有同学想要用的,需要的留言,我放到 github 上去 🐶

全系列博文目录

后端模块

  1. DevOps - Gitlab Api使用(已完成,点击跳转)
  2. DevOps - 搭建 DevOps 基础平台 基础平台搭建上篇 | 基础平台搭建中篇 | 基础平台搭建下篇
  3. DevOps - Gitlab CI 流水线构建
  4. DevOps - Jenkins 流水线构建
  5. DevOps - Docker 使用
  6. DevOps - 发布任务流程设计
  7. DevOps - 代码审查卡点
  8. DevOps - Node 服务质量监控

前端模块

  1. DevOps - H5 基础脚手架
  2. DevOps - React 项目开发

尾声

此项目是从零开发,后续此系列博客会根据实际开发进度推出(真 TMD 累),项目完成之后,会开放部分源码供各位同学参考。

为什么是开放部分源码,因为有些业务是需要贴合实际项目针对性开发的,开放出去的公共模块我写的认真点

为了写个系列博客,居然真撸完整个系统(不是一般的累),觉得不错的同学麻烦顺手三连(点赞,关注,转发)。