记一次全局错误捕获打版本号 + 工程化插入js脚本(下)

149 阅读4分钟

记一次全局错误捕获打版本号 + 工程化插入js脚本(下)

上集已经成功编写了一段全局捕获错误脚本,之后压缩混淆之后,上传静态服务器获取到了脚本URL:

frontlog.error.cn/js/xxxxxxEr…

上集还有说需要一个meta标签标示的版本号也需要动态添加

<meta http-equiv="version" content="1.0.0">

现在就是想办法吧这两个字符串插入到head标签的最前面,为什么是最前而不是最后呢?因为咱们要检测资源加载错误啊,如果放到css后边就检查不出css是否有错误的加载了

要一个一个页面的查找插入文本肯定不是现在前端该干的事情,我们需要引入gulp和webpack工程化来完成烦人的手工修改

对于老的项目还处于jq时代,响应快、体验差、多页面、松散管理,项目目录还是js、css、html三大鼻祖呢,这个我们就采用gulp来构建,相对于webpack来说轻巧灵便biuibiu快

上gulp代码

// 因为是老项目没有构建前后一说,所以构建完的代码都是替换原来的代码,当然也可以新起一个目录存放源代码,由于怕之后两份代码不好维护所以还是直接替换
const process = require('process')
const { src, dest, series } = require('gulp')
// 采用gulp-cheerio插件,可以和jq一样查找修改dom元素
const cheerio = require('gulp-cheerio')
// 保存一下捕获错误js的内容名称 用来检索head中是否已经有了该js 如果有则不做操作 如果没有再引入该文件
const ERROR_JS_TEXT_DEV = 'http://frontlog.dev.cn/js/xxxxxxErrorHandle.js'
const ERROR_JS_TEXT_PROD = 'http://frontlog.prod.cn/js/xxxxxxErrorHandle.js'

// 项目中引入cross-env来设置命令行变量,有更好的兼容性
// 根据命令输入的变量来判断是否是生产环境
const IS_PRODUCTION = process.env.NODE_ENV === 'development' ? false : true

// 特定的需要增加捕获错误js和版本号的文件及目录
/*
  由于老项目中内容繁杂不好规整统一,有的需要插入脚本有的则不需要,
  所以需要单独给个别目录单独构建 采用一个数组来存储相关目录,以后扩展直接维护该数组即可,而不用动其他代码
*/

/*
  由于在gulp中没有找到可以动态让输出文件目录和输入文件目录一样的方法所以只能一个文件夹一个文件夹的构建替换
  由于他们的工作内容都一样 并且 cheerioGmDjwx 是一个函数,我就想到了复用函数,这样代码量可以几何倍的减少
  采用目录数组reduce遍历,为每个目录提供一定单独的 cheerioGmDjwx 任务,
  拿到任务数组后 用扩展运算符传递给series 
  由于 cheerioGmDjwx 任务并不是立即执行的函数,所以用bind方法传参并且返回一个待执行的函数
  然后当默认任务导出 
*/
const CompilerHTMLUrl = [
  {
    originUrl: './gsdjwx/**/*.html',
    targetUrl: './gsdjwx'
  },
  {
    originUrl: './gswegame/**/*.html',
    targetUrl: './gswegame'
  },
  {
    originUrl: './gswx/**/*.html',
    targetUrl: './gswx'
  },
  {
    originUrl: ['./html/**/*.html','./html/**/*.shtml'],
    targetUrl: './html'
  },
  {
    originUrl: './abxhtml/**/*.html',
    targetUrl: './abxhtml'
  },
]
// 输入旧的版本号自增1后返回新版本号
function versionIncrement(v){
  let newVersion = v.split('.').join('')
  newVersion++
  newVersion = String(newVersion).split('').join('.')
  return newVersion
}

// 用cheerio来构建相关文件
function cheerioGmDjwx({originUrl,targetUrl}) {
  return src(originUrl)
    .pipe(cheerio({
      run: ($) => {
        const head = $('head')
        // 获取head元素
        if (head) {
          let headText = head.html()
          // 获取head元素的html内容
          if (headText) {
            // 根据部署环境变量判断是测试或生产环境
            let nowJsText = IS_PRODUCTION ? ERROR_JS_TEXT_PROD : ERROR_JS_TEXT_DEV
            // 如果head的html中既没有测试js脚本也没有生产js脚本,再引入捕获错误js
            if (!headText.includes(ERROR_JS_TEXT_DEV) && !headText.includes(ERROR_JS_TEXT_PROD)) {
              head.prepend(`<script src='${nowJsText}'></script>\n<meta http-equiv="version" content="1.0.0">`)
            } else {
              // 获取当前版本号后更新替换
              const versionMeta = $('meta[http-equiv="version"]')
              const currentVersion = versionMeta.attr('content')
              if (currentVersion) {
                const newVersion = versionIncrement(currentVersion)
                versionMeta.attr('content',newVersion)
              }
              // 根据部署环境(生产或者测试)来更新插入的js路径
              const versionScript = $(`script[src="${nowJsText}"]`)
              // 如果已经有了即将要插入的部署环境的js就不用替换
              // 如果没有即将要插入的部署环境的js则说明现在是有一个相反的js脚本 需要手动替换src
              if (!versionScript.attr('src')) {
                // 即将要插入的部署环境的js取反,就是现在html中的js,用它来获取元素后替换成nowJsText
                let preJsText = IS_PRODUCTION ? ERROR_JS_TEXT_DEV : ERROR_JS_TEXT_PROD
                const preScript = $(`script[src="${preJsText}"]`)
                preScript.attr('src',nowJsText)
              }
            }
          }
        }
      },
      parserOptions: {
        // 空格正常化
        normalizeWhitespace: false,
        // 防止中文或者其他字符被转义
        decodeEntities: false
      }
    }))
    .pipe(dest(targetUrl))
}
// 将CompilerHTMLUrl和cheerioGmDjwx 融合到一个数组中最后调用
// 由于 cheerioGmDjwx 任务并不是立即执行的函数,所以用bind方法传参并且返回一个待执行的函数
const serverArr = CompilerHTMLUrl.reduce((pre,cur)=>{
  pre.push(cheerioGmDjwx.bind(null,cur))
  return pre
},[])


exports.default = series(...serverArr)

/*
  package.json中配置scripts:
  "build:sit": "cross-env NODE_ENV=development gulp",
  "build:prod": "cross-env NODE_ENV=production gulp"
*/

// 这样,每次构建 运行npm run build:sit 或者 运行npm run build:prod 即可 

npm run build:sit 一下子 888 个文件被修改。

上webpack代码

@1 由于在html-webpack-plugin中没有找到在head中随意添加文本的方法 所以参考了其他文章,先自己动手丰衣足食 写了一个小插件

PrependCodeToHead.js


// 增加一个webpack插件,在html-webpack-plugin修改了html之后添加新代码
// options用来接收参数(自定义的插入代码片段)
function PrependCodeToHead (options) {
  this.options = options
}

PrependCodeToHead.prototype.apply = function (compiler) {
  // 接受自己的option参数
  const code = this.options.code
  compiler.plugin('compilation',function (compilation) {
    compilation.plugin('html-webpack-plugin-after-html-processing',function (htmlPluginData, callback) {
      // 替换html文本中head为新的增加了新代码的head,这个方式比较low(比起cheerio),以后有机会改进
      htmlPluginData.html = htmlPluginData.html.replace('<head>',`<head>${code}`)
      callback(null, htmlPluginData)
    })
  })
}

module.exports = PrependCodeToHead

webpack.base.conf.js

// 增加一个webpack插件,在html-webpack-plugin修改了html之后添加新代码
var PrependCodeToHead = require('./PrependCodeToHead')
// ...

plugins: [
  new PrependCodeToHead({
    code: '<script src="http://frontlog.prod.cn/js/xxxxErrorHandle.js"></script><meta http-equiv="version" content="1.0.0">'
  }),
]

@2 发现一个好用的添加tag的插件 html-webpack-tags-plugin

// webpack.config.js
const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin')
// ....
new HtmlWebpackTagsPlugin({
  // 设置公共路径为空 防止output中配置的publicPath 影响到这里,会给绝对路径增加相对路径标识 如:./http://xxxxxx.cn/js/xxxErrorHandle.js
  publicPath: '', 
  append: false,
  metas: [
    {
      attributes: {
        'http-equiv': 'version',
        content: 'v1.0.0'
      }
    }
  ],
  scripts: ['http://xxxx.cn/js/xxxxxErrorHandle.js'],
}),

// 唯一的问题是meta标签是在js/css之后添加,这样如果css加载错误的时候按理应该是获取不到version的, 实际情况中是暂时没有这个问题

总结

运用gulp和webpack来工程化构建动态插入相关的代码片段

这些东西可能对于我这个菜鸟已经是目前能考虑到能做到的极限了,希望以后还可以写出更多有内容有知识点的东西!!!