动态

详情 返回 返回

【從實戰帶你認識gulp】打包前端項目並實現防緩存 - 动态 详情

gulp是什麼?

一個基於node的前端自動化任務構建工具,使用經典回調+鏈式調用的方式實現任務的自動化 (src.pipe(...).pipe),gulp其實和webpack很相似,但是gulp側重點不同,gulp更側重前端流程自動化、任務執行(通過任務使開發提效),就像一條流水線。而webpack則是更側重用於打包前端資源,一切皆可打包成模塊。
官方文檔:https://www.gulpjs.com.cn/

gulp的應用場景?為什麼用gulp?

1.構建前端自動化流程比較好的方案,一些重複的人工操作可以讓gulp去做,並且代碼量不大,提升開發效率(自動化完成前端工作流任務:單元測試、優化、打包)。

2.與其他構建工具相比開發簡單,易上手。基於nodejs 文件系統流,運行速度快。

3.更適合原生前端項目的打包。有助於理解前端工程化。

4.發佈通用型組件或者npm庫的時候可以用gulp來進行打包。

gulp的安裝

在你的項目目錄中執行

npm install -g gulp 

在根目錄下創建gulp的配置文件gulpfile.jsinstall一下文件中的依賴,之後就可以直接在這個文件中定義我們的gulp任務了

var gulp=require("gulp");//引入gulp模塊
gulp.task('default',function(){ 
 console.log('hello world');
});

然後直接終端中進入當前目錄運行 gulp 命令就開始執行任務了。
gulp後面也可以加上要執行的任務名,例如gulp task1,如果沒有指定任務名,則會執行任務名為default的默認任務。
gulp常用的幾個api : task, series, parallel, src, pipe, dest

  • task: 創建一個任務
  • series:順序執行多個任務
  • prallel:並行執行多個任務
  • src:讀取數據源轉換成stream
  • pipe:管道-可以在中間對數據流進行處理
  • dest:輸出數據流到目標路徑

gulp實際應用之原生前端項目打包:

以一個jQuery原生項目為例子,目錄結構:

image.png

路徑表:
因為項目結構有點特殊,資源比較分散,html的js、css保存在對應模塊文件夾中。而公共的js、css卻在html的外面的文件中,所以就設置了2個入口(以js資源為例):一個根目錄入口、一個html入口

  scriptsjs: {//根目錄入口
    src: [
      "./temp/js/**/*.js",//temp/js/之下的所有文件夾下的js
      "!./temp/**/*.min.js", //不匹配壓縮過的js,防止二次壓縮
      "!./temp/js/common.js", 
      "!./temp/mpcc/**/*.js",//這個文件不需要壓縮處理所以不匹配
    ],
    dest: destDir + "/js",//出口
  },
  scriptshtml: {//html入口
    src: [
      "./temp/html/**/*.js",
      "!./temp/html/BasicSet/RoleManage/js/*.js",//不匹配
      "!./temp/html/BasicSet/UserAuthorization/js/*.js",
    ],
    dest: destDir + "/html",
  },

你沒看錯,是不是有點像webpack中的entryoutput* 通配符是代表所有文件夾、是代表不匹配。這裏可以自己選哪些文件不需要進行匹配

gulpfile.js 完整代碼:

var gulp = require("gulp"),
  sourcemaps = require("gulp-sourcemaps");
var babel = require("gulp-babel");
var uglify = require("gulp-uglify");
var del = require("del");
var minifycss = require("gulp-minify-css");
var through = require("through2");
var path = require("path");
var fs = require("fs");
var crypto = require("crypto");
var ramdomHash = function (len) {
  //獲取隨機hash值
  var random = Math.random().toString();
  return crypto
    .createHash("md5")
    .update(new Date().valueOf().toString() + random) //加入時間戳和隨機數保證hash的唯一
    .digest("hex")
    .substr(0, len);
};
var destDir = "workOrder"; //生產包文件目錄
//包路徑表
var paths = {
  stylescss: {
    src: ["./temp/css/**/*.css", "!./temp/**/*.min.css"],
    dest: destDir + "/css",
  },
  styleshtml: {
    src: ["./temp/html/**/*.css", "!./temp/**/*.min.css"],
    dest: destDir + "/html",
  },
  scripts: {
    src: "./temp/**/*.js",
    dest: destDir + "/",
  },
  scriptsjs: {
    src: [
      "./temp/js/**/*.js",
      "!./temp/**/*.min.js",
      "!./temp/js/common.js",
      "!./temp/mpcc/**/*.js",
    ],
    dest: destDir + "/js",
  },
  scriptshtml: {
    src: [
      "./temp/html/**/*.js",
      "!./temp/html/BasicSet/RoleManage/js/*.js",
      "!./temp/html/BasicSet/UserAuthorization/js/*.js",
    ],
    dest: destDir + "/html",
  },
  html: {
    src: "./temp/**/*.html",
    dest: destDir + "/",
  }
};
//刪除生產包
function clean() {
  return del([destDir]);
}
//清除temp文件夾
function revClean() {
  return del(["temp"]);
}
//複製到temp,避免污染src
function revCopy() {
  return gulp
    .src("./workorder_dev/**/*", { base: "./workorder_dev" })
    .pipe(gulp.dest("./temp/"));
}
//html中資源路徑加版本號,更改所有的文件裏的資源路徑,以便接下來的增加版本號工作.
function revHtmlPathReplace() {
  var ASSET_REG = {
    SCRIPT:
      /("|')(.[^('|")]*((\.js)|(\.css)|(\.json)|(\.png)|(\.jpg)|(\.ttf)|(\.eot)|(\.gif)|(\.woff2)|(\.woff)))(\1)/gi,
  };
  return gulp
    .src("./temp/html/**/*.html")
    .pipe(
      (function () { //利用through讀取html文件夾下的所有html文件
        return through.obj(function (file, enc, cb) {
          if (file.isNull()) {
            this.push(file);
            return cb();
          }
          if (file.isStream()) {
            this.emit(
              "error",
              new gutil.PluginError(PLUGIN_NAME, "Streaming not supported")
            );
            return cb();
          }

          var content = file.contents.toString();

          var filePath = path.dirname(file.path);
             for (var type in ASSET_REG) { //獲取html文件內容直接使用replace+正則進行替換
            content = content.replace(
              ASSET_REG[type],
              function (str, tag, src) {
                var _f = str[0];
                src = src.replace(/(^['"]|['"]$)/g, "");

                if (/\.min\./gi.test(src)) {
                  //壓縮文件不加版本號
                  return src;
                }
                var assetPath = path.join(filePath, src);
                if (fs.existsSync(assetPath)) {
                  var buf = fs.readFileSync(assetPath);

                  var md5 = ramdomHash(7); //獲取版本號hash,只需要7位hash不需要太長
                  var verStr = "" + md5;
                  src = src + "?v=" + verStr;
                }
                src = _f + src + _f;
                return src;
              }
            );
          }
          file.contents = new Buffer(content);
          this.push(file);
          cb();
        });
      })()
    )
    .pipe(gulp.dest("./temp/html/"));
}
//css中資源加版本號
function assetRev(options) {
  var ASSET_REG = {
    SCRIPT: /(<script[^>]+src=)['"]([^'"]+)["']/gi,
    STYLESHEET: /(<link[^>]+href=)['"]([^'"]+)["']/gi,
    IMAGE: /(<img[^>]+src=)['"]([^'"]+)["']/gi,
    BACKGROUND: /(url\()(?!data:|about:)([^)]*)/gi,
  };
  return through.obj(function (file, enc, cb) {
    options = options || {};
    if (file.isNull()) {
      this.push(file);
      return cb();
    }

    if (file.isStream()) {
      this.emit(
        "error",
        new gutil.PluginError(PLUGIN_NAME, "Streaming not supported")
      );
      return cb();
    }

    var content = file.contents.toString();
    var filePath = path.dirname(file.path);

    for (var type in ASSET_REG) {
      if (type === "BACKGROUND" && !/\.(css|scss|less)$/.test(file.path)) {
      } else {
        content = content.replace(ASSET_REG[type], function (str, tag, src) {
          src = src.replace(/(^['"]|['"]$)/g, "");
          if (!/\.[^\.]+$/.test(src)) {
            return str;
          }
          if (options.verStr) {
            src += options.verStr;
            return tag + '"' + src + '"';
          }
          // remote resource
          if (/^https?:\/\//.test(src)) {
            return str;
          }
          var assetPath = path.join(filePath, src);
          if (src.indexOf("/") == 0) {
            if (
              options.resolvePath &&
              typeof options.resolvePath === "function"
            ) {
              assetPath = options.resolvePath(src);
            } else {
              assetPath = (options.rootPath || "") + src;
            }
          }

          if (fs.existsSync(assetPath)) {
            var buf = fs.readFileSync(assetPath);
            var md5 = ramdomHash(7);
            var verStr = (options.verConnecter || "") + md5;
            src = src + "?v=" + verStr; //增加版本號
          } else {
            return str;
          }
          return tag + '"' + src + '"';
        });
      }
    }

    file.contents = new Buffer(content);
    this.push(file);
    cb();
  });
}

//為css中引入的圖片/字體等添加hash編碼
function revAssetCsscss() {
  return gulp
    .src(paths.stylescss.src) //該任務針對的文件
    .pipe(assetRev()) //該任務調用的模塊
    .pipe(gulp.dest("./temp/css")); //編譯後的路徑
}
function revAssetCsshtml() {
  return gulp
    .src(paths.styleshtml.src) //該任務針對的文件
    .pipe(assetRev()) //該任務調用的模塊
    .pipe(gulp.dest("./temp/html")); //編譯後的路徑
}
//壓縮css,並添加sourcemap
function stylesMinifyCss() {
  return (
    gulp
      .src(paths.stylescss.src)
      .pipe(sourcemaps.init())
      // .pipe(less())
      // .pipe(cleanCSS())
      // // pass in options to the stream
      // .pipe(rename({
      //   basename: 'main',
      //   suffix: '.min'
      // }))
      .pipe(minifycss())
      .pipe(sourcemaps.write("./maps"))
      .pipe(gulp.dest(paths.stylescss.dest))
  );
}
//把html文件夾下的css進行壓縮
function stylesMinifyHtml() {
  return (
    gulp
      .src(paths.styleshtml.src)
      .pipe(sourcemaps.init())
      .pipe(minifycss())
      .pipe(sourcemaps.write("./maps"))
      .pipe(gulp.dest(paths.styleshtml.dest))
  );
}
//壓縮js,並添加sourcemap
function scriptsjs() {
  return gulp
    .src(paths.scriptsjs.src, { sourcemaps: true })
    .pipe(sourcemaps.init()) //源碼映射便於調試
    .pipe(sourcemaps.identityMap())
    .pipe(babel()) //es6轉換
    .pipe(uglify()) //壓縮
    .pipe(sourcemaps.write("./maps"))
    .pipe(gulp.dest(paths.scriptsjs.dest));
}
//壓縮html文件夾下的js
function scriptshtml() {
  return gulp
    .src(paths.scriptshtml.src, { sourcemaps: true })
    .pipe(sourcemaps.init())
    .pipe(sourcemaps.identityMap())
    .pipe(babel())
    .pipe(uglify())
    .pipe(sourcemaps.write("./maps"))
    .pipe(gulp.dest(paths.scriptshtml.dest));
}
//把臨時文件拷貝到生產目錄
function copy() {
  return gulp
    .src("./temp/**/*", { base: "./temp" })
    .pipe(gulp.dest(destDir + "/"));
}
//創建一個json文件保存標識用於識別當前是否是線上環境
function updateEnv(done) {
  fs.writeFile(
    "./temp/env.json",
    JSON.stringify({ env: "prod" }),
    function (err) {
      if (err) {
        console.error(err);
      }
      done();
      console.log("--------------------updateEnv");
    }
  );
}
var build = gulp.series(//串行任務
  clean,//清除上一次的生產包
  revClean,//刪除temp文件夾
  revCopy,//拷貝開發目錄到temp
  revHtmlPathReplace,//html加版本號
  revAssetCsscss,//給css資源加版本號
  revAssetCsshtml,
   updateEnv,//生成運行環境json
  copy, //copy之後再壓縮
  gulp.parallel( //對兩個入口的資源壓縮、優化的並行任務
    stylesMinifyCss,
    stylesMinifyHtml,
    scriptsjs,
    scriptshtml
  ),
  revClean //刪除temp
);
exports.clean = clean;
exports.stylesMinifyCss = stylesMinifyCss;
exports.stylesMinifyHtml = stylesMinifyHtml;
exports.updateEnv = updateEnv;
exports.scriptsjs = scriptsjs;
exports.scriptshtml = scriptshtml;
exports.default = build;

可以看出每個任務就是一個函數,在最後對定義的任務(函數)按順序進行執行一遍。
打包流程:
其實看最後的gulp.series 就能看出來,串行任務裏面含有一個並行任務,直接運行gulp命令就能直接看到打包的過程。

image.png

所有任務執行的先後順序:

刪除上次生產包 > 刪除temp文件夾 > 把開發目錄拷貝到temp文件夾 > html內容加版本號 > 資源內容加版本號 > 創建json > 把temp文件拷到生產目錄 > 開始並行壓縮 > 最後刪除temp文件夾

gulp實現防緩存

為什麼要防緩存?
如果不防緩存,在原生項目上線後,瀏覽器會把前端的css,js資源緩存在本地,下次打開的時候如果資源不變就會直接使用本地的緩存來加載頁面,這樣會造成用户必須手動清除瀏覽器緩存才能使用新的功能,影響體驗,所以就需要在頁面引入資源的時候給文件加上版本號?v=xxxx,這樣瀏覽器就能識別到資源有變化就會從服務器上重新獲取資源
我們回看一下gulpfile文件中加版本號的任務revHtmlPathReplace

//html中資源路徑加版本號,更改所有的文件裏的資源路徑,以便接下來的增加版本號工作.
function revHtmlPathReplace() {
  var ASSET_REG = {
    SCRIPT:
      /("|')(.[^('|")]*((\.js)|(\.css)|(\.json)|(\.png)|(\.jpg)|(\.ttf)|(\.eot)|(\.gif)|(\.woff2)|(\.woff)))(\1)/gi,
  };
  return gulp
    .src("./temp/html/**/*.html")
    .pipe(
      (function () { //利用through讀取html文件夾下的所有html文件
        return through.obj(function (file, enc, cb) {
          if (file.isNull()) {
            this.push(file);
            return cb();
          }
          if (file.isStream()) {
            this.emit(
              "error",
              new gutil.PluginError(PLUGIN_NAME, "Streaming not supported")
            );
            return cb();
          }
          var content = file.contents.toString();

          var filePath = path.dirname(file.path);
             for (var type in ASSET_REG) { //獲取html文件內容直接使用replace+正則進行替換
            content = content.replace(
              ASSET_REG[type],
              function (str, tag, src) {
                var _f = str[0];
                src = src.replace(/(^['"]|['"]$)/g, "");

                if (/\.min\./gi.test(src)) {
                  //壓縮文件不加版本號
                  return src;
                }
                var assetPath = path.join(filePath, src);
                if (fs.existsSync(assetPath)) {
                  var buf = fs.readFileSync(assetPath);
                  var md5 = ramdomHash(7); //獲取版本號hash,只需要7位hash不需要太長
                  var verStr = "" + md5;
                  src = src + "?v=" + verStr;
                }
                src = _f + src + _f;
                return src;
              }
            );
          }
          file.contents = new Buffer(content);
          this.push(file);
          cb();
        });
      })()
    )
    .pipe(gulp.dest("./temp/html/"));
}

通過遍歷所有html文件時使用through獲取到文件的文本內容,然後利用正則對文本內容中需要加版本的路徑加上hash版本然後替換上去,最後再輸出一個新的文件文件。同時assetRev任務也類似
這個任務的代碼其實是借鑑了gulp-asset-rev/index.js 中的源碼,把原來源碼中的css文件內容的資源路徑加版本拿出來改成了給html文件內容加版本。
加版本後效果:

image.png

順帶説一下gulp另一種比較麻煩的加版本防緩存方案就是使用rev模塊,需要修改多處源碼。這種方案有不好的地方,因為是生成rev-manifest.json對應關係來加版本 ,如果因為某些原因在這個json中沒有對應文件對照,會導致某些特殊路徑如(../../xxx.js)的文件加版本號沒加上 就會漏掉某些文件沒加版本。還有就是node_modules源碼改動後重新安裝就會被覆蓋。

最後:

對打包流程進行一些優化
1.因為每次打包後是都要手動進行壓縮、命名一下再發給後端部署。解放雙手,寫一個打包後對生產包自動進行壓縮的任務distZip:
這個distZip任務要放到最後執行,默認壓縮當前目錄下的dist文件夾

//對生產包自動壓縮成zip
function distZip(done) {
  var archiver = require("archiver");
  var now = new Date();
  var filename = [
    __dirname + "/dist",
    now.getMonth(),
    now.getDate(),
    now.getHours(),
    now.getMinutes(),
    now.getSeconds(),
    ".zip",
  ].join(""); //當前時間拼接
  var output = fs.createWriteStream(filename);
  //設置壓縮格式為zip
  var archive = archiver("zip", {
    zlib: { level: 9 }, // Sets the compression level.
  });  archive.on("error", function (err) {
    throw err;
  }); 
   archive.pipe(output);
  archive.directory("./dist/");
  archive.finalize();
  done();
}

2.在原生項目打包完成時沒有像webpack有process.env來區分是否是生產環境,可以在打包階段寫一個生成當前運行環境配置文件的任務updateEnv,自動生成一個json文件,項目進入時提前加載這個json文件,在代碼中就可以用這個json裏的env標識來判斷當前是否在生產環境。

function updateEnv(done) {
  fs.writeFile(
    "./temp/env.json",
    JSON.stringify({ env: "prod" }),
    function (err) {
      if (err) {
        console.error(err);
      }
      done();
      console.log("--------------------updateEnv");
    }
  );
}

3.以上打包的js、css資源文件沒有做文件合併的任務,如果提前進行js,css文件合併就可以解決了文件分散的問題,就不需要多設置入口了

Add a new 评论

Some HTML is okay.