webpack4+node合併資源請求, 實現combo功能(二十三)

龍恩0707發表於2018-11-14

本文學習使用nodejs實現css或js資原始檔的合併請求功能,我們都知道在一個複雜的專案當中,可能會使用到很多第三方外掛,雖然目前使用vue開發系統或者h5頁面,vue元件夠用,但是有的專案中會使用到類似於echarts這樣的外掛,或者第三方其他的外掛,比如ztree.js這樣的,那如果我們把所有js都打包到一個js檔案當中,那麼該js可能會變得非常大,或者我們可能會把他們單獨打包一個個js檔案去,然後在頁面中分別一個個使用script標籤去引入他們,但是這樣會導致頁面多個js請求。因此就想使用node來實現類似於combo功能,比如以下的js功能構造:

http://127.0.0.1:3001/jsplugins/??a.js,b.js

如上的js請求,會把a.js和b.js合併到一個請求裡面去, 然後使用node就實現了combo功能。
首先我們來分析下上面的請求,該請求中的 ?? 是一個分隔符,分隔符前面是合併的檔案路徑,後面是合併資原始檔名,多個檔名使用逗號(,)隔開,知道了該請求的基本原理之後,我們需要對該請求進行解析,解析完成後,分別讀取該js檔案內容,然後分別讀取到內容後合併起來輸出到瀏覽器中。

首先看下我們專案簡單的目錄架構如下:

### 目錄結構如下:
demo1                                       # 工程名           
|   |--- node_modules                       # 所有的依賴包
|   |--- jsplugins
|   | |-- a.js
|   | |-- b.js
|   |--- app.js
|   |--- package.json

專案截圖如下:

jsplugins/a.js 內容如下:

function testA() {
  console.log('A.js');  
}

jsplugins/b.js 內容如下:

function testB() {
  console.log('b.js');
}

當我們訪問 http://127.0.0.1:3001/jsplugins/??a.js,b.js 請求後,資原始檔如下:

如何實現呢?

app.js 一部分程式碼如下:

// 引入express模組
const express = require('express');

const fs = require('fs');
const path = require('path');

// 建立app物件
const app = express();

app.use((req, res, next) => {
  const urlInfo = parseURL(__dirname, req.url);
  console.log(urlInfo);
  if (urlInfo) {
    // 合併檔案
    combineFiles(urlInfo.pathnames, (err, data) => {
      if (err) {
        res.writeHead(404);
        res.end(err.message);
      } else {
        res.writeHead(200, {
          'Content-Type': urlInfo.mime
        });
        res.end(data);
      }
    });
  }
});


// 定義伺服器啟動埠 
app.listen(3001, () => {
  console.log('app listening on port 3001');
});

如上程式碼,使用express實現一個簡單的,埠號為3001的伺服器,然後使用 app.use模組擷取請求,比如我們現在在瀏覽器中訪問 http://127.0.0.1:3001/jsplugins/??a.js,b.js 這個請求的時候,會對該請求進行解析,會呼叫 parseURL方法,該方法的程式碼如下:

let MIME = {
  '.css': 'text/css',
  '.js': 'application/javascript'
};

// 解析檔案路徑
function parseURL(root, url) {
  let base, 
    pastnames,
    separator;
  if (url.indexOf('??') > -1) {
    separator = url.split('??');
    base = separator[0];

    pathnames = separator[1].split(',').map((value) => {
      const filepath = path.join(root, base, value);
      return filepath;
    });
    return {
      mime: MIME[path.extname(pathnames[0])] || 'text/plain',
      pathnames: pathnames
    }
  }
  return null;
};

如上程式碼,給parseURL函式傳遞了兩個引數,一個是 __dirname 和 req.url, 其中__dirname就是當前app.js檔案的所在目錄,因此會列印出該目錄下全路徑:/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案, req.url返回的是url中的所有資訊,因此 req.url='/jsplugins/??a.js,b.js', 然後判斷url中是否有 ?? 這樣的,找到的話,就使用 ?? 分割,如下程式碼:

separator = url.split('??');
base = separator[0];

因此 base = '/jsplugins/', separator[1] = a.js,b.js了,然後再進行對 separator[1] 使用逗號(,) 分割變成陣列進行遍歷a.js和b.js了,遍歷完成後,如程式碼 const filepath = path.join(root, base, value); 使用path.join()對路徑進行合併,該方法將多個引數值字串結合為一個路徑字串,path.join基本使用,看我這篇文章
(https://www.cnblogs.com/tugenhua0707/p/9944285.html#_labe1),

root = '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案'
base = '/jsplugins/';
value = 'a.js' 或 value = 'b.js';

因此 pathnames 的值最終變成如下的值:

[ '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案/jsplugins/a.js',
  '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案/jsplugins/b.js' ]

執行完parseURL後返回的是如下物件:

{ 
  mime: 'application/javascript',
  pathnames:
   [ '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案/jsplugins/a.js',
     '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案/jsplugins/b.js' 
   ] 
}

path.extname 的使用可以看如下這篇文章(https://www.cnblogs.com/tugenhua0707/p/9944285.html#_labe4),就是拿到路徑的副檔名,那麼拿到的副檔名就是 .js, 然後 mime = MIME[path.extname(pathnames[0])] || 'text/plain', 因此 mine = 'application/javascript' 了。

返回值後,就會執行如下程式碼:

if (urlInfo) {
  // 合併檔案
  combineFiles(urlInfo.pathnames, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end(err.message);
    } else {
      res.writeHead(200, {
        'Content-Type': urlInfo.mime
      });
      res.end(data);
    }
  });
}

先合併檔案,檔案合併後,再執行回撥,把合併後的js輸出到瀏覽中,先看下 combineFiles 函式的方法程式碼如下:

//合併檔案
function combineFiles(pathnames, callback) {
  const output = [];
  (function nextFunc(l, len){
    if (l < len) {
      fs.readFile(pathnames[l], (err, data) => {
        if (err) {
          callback(err);
        } else {
          output.push(data);
          nextFunc(l+1, len);
        }
      })
    } else {
      const data = Buffer.concat(output);
      callback(null, data);
    }
  })(0, pathnames.length);
}

首先該方法傳了 pathnames 和callback回撥,其中pathnames的值是如下:

[ '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案/jsplugins/a.js',
  '/Users/tugenhua/個人demo/webpack-all-demo2/webpack-all-demo/webpack+node合併js資源請求檔案/jsplugins/b.js' 
]

然後一個使用立即函式先執行,把 0, 和 長度引數傳遞進去,判斷是否小於檔案的長度,如果是的話,就是 fs中的讀取檔案方法 (readFile), 就依次讀取檔案,對 readFile讀取檔案的方法不熟悉的話,可以看這篇文章(https://www.cnblogs.com/tugenhua0707/p/9942886.html#_labe0), 讀取完後使用 Buffer.concat進行拼接。最後把資料傳給callback返回到回撥函式裡面去,執行回撥函式,就把對應的內容輸出到瀏覽器中了。

注意:
1. 使用 fs.readFile 方法,如果沒有設定指定的編碼,它會以位元組的方式讀取的,因此使用Buffer可以進行拼接。
2. 使用Buffer.concat拼接的時候,如果a.js或b.js有中文的話,會出現亂碼,出現的原因是如果js檔案是以預設的gbk儲存的話,那麼我們nodejs預設是utf8讀取的,就會有亂碼存在的,因此js檔案如果是本地的話,儘量以utf8儲存。如果不是utf8儲存的話,出現了亂碼,我們需要解決,下一篇文章就來折騰下 Buffer出現亂碼的情況是如何解決的。

因此整個app.js 程式碼如下:

// 引入express模組
const express = require('express');

const fs = require('fs');
const path = require('path');

// 建立app物件
const app = express();

app.use((req, res, next) => {
  const urlInfo = parseURL(__dirname, req.url);
  console.log(urlInfo);
  if (urlInfo) {
    // 合併檔案
    combineFiles(urlInfo.pathnames, (err, data) => {
      if (err) {
        res.writeHead(404);
        res.end(err.message);
      } else {
        res.writeHead(200, {
          'Content-Type': urlInfo.mime
        });
        res.end(data);
      }
    });
  }
});

let MIME = {
  '.css': 'text/css',
  '.js': 'application/javascript'
};

// 解析檔案路徑
function parseURL(root, url) {
  let base, 
    pastnames,
    separator;
  if (url.indexOf('??') > -1) {
    separator = url.split('??');
    base = separator[0];

    pathnames = separator[1].split(',').map((value) => {
      const filepath = path.join(root, base, value);
      return filepath;
    });
    return {
      mime: MIME[path.extname(pathnames[0])] || 'text/plain',
      pathnames: pathnames
    }
  }
  return null;
};

//合併檔案
function combineFiles(pathnames, callback) {
  const output = [];
  (function nextFunc(l, len){
    if (l < len) {
      fs.readFile(pathnames[l], (err, data) => {
        if (err) {
          callback(err);
        } else {
          output.push(data);
          nextFunc(l+1, len);
        }
      })
    } else {
      const data = Buffer.concat(output);
      callback(null, data);
    }
  })(0, pathnames.length);
}
// 定義伺服器啟動埠 
app.listen(3001, () => {
  console.log('app listening on port 3001');
});

github上的程式碼檢視請點選

二:combo功能合併資原始檔後如何在專案中能實戰呢?

如上使用node實現了資原始檔combo功能後,我們會把該技術使用到專案中去,那麼這個專案還是我們之前的這篇文章的專案--- webpack4+express+mongodb+vue 實現增刪改查。

目錄結構還是和以前一樣的,如下所示:

### 目錄結構如下:
demo1                                       # 工程名
|   |--- dist                               # 打包後生成的目錄檔案             
|   |--- node_modules                       # 所有的依賴包
|   |----database                           # 資料庫相關的檔案目錄
|   | |---db.js                             # mongoose類庫的資料庫連線操作
|   | |---user.js                           # Schema 建立模型
|   | |---addAndDelete.js                   # 增刪改查操作
|   |--- app
|   | |---index
|   | | |-- views                           # 存放所有vue頁面檔案
|   | | | |-- list.vue                      # 列表資料
|   | | | |-- index.vue
|   | | |-- components                      # 存放vue公用的元件
|   | | |-- js                              # 存放js檔案的
|   | | |-- css                             # 存放css檔案
|   | | |-- store                           # store倉庫
|   | | | |--- actions.js
|   | | | |--- mutations.js
|   | | | |--- state.js
|   | | | |--- mutations-types.js
|   | | | |--- index.js
|   | | | |
|   | | |-- app.js                          # vue入口配置檔案
|   | | |-- router.js                       # 路由配置檔案
|   |--- views
|   | |-- index.html                        # html檔案
|   |--- webpack.config.js                  # webpack配置檔案 
|   |--- .gitignore  
|   |--- README.md
|   |--- package.json
|   |--- .babelrc                           # babel轉碼檔案
|   |--- app.js                             # express入口檔案

唯一不同的是,在webpack.dll.config.js 對公用的模組進行打包會把 vue 和 echarts 會打包成二個檔案:

module.exports = {
  // 入口檔案
  entry: {
    // 專案中用到該依賴庫檔案
    vendor: ['vue/dist/vue.esm.js', 'vue', 'vuex', 'vue-router', 'vue-resource'],
    echarts: ['echarts']
  },
  // 輸出檔案
  output: {
    // 檔名稱
    filename: '[name].dll.[chunkhash:8].js',
    // 將輸出的檔案放到dist目錄下
    path: path.resolve(__dirname, './dist/components'),

    /*
     存放相關的dll檔案的全域性變數名稱,比如對於jquery來說的話就是 _dll_jquery, 在前面加 _dll
     是為了防止全域性變數衝突。
    */
    library: '_dll_[name]'
  },
}

因此會在我們專案中 dist/components/ 下生成兩個對應的 vendor.dll.xx.js 和 echarts.dll.xx.js, 如下所示

然後把 剛剛的js程式碼全部複製到我們的 該專案下的 app.js 下:如下程式碼:

// 引入express模組
const express = require('express');

// 建立app物件
const app = express();

const addAndDelete = require('./database/addAndDelete');

const bodyParser = require("body-parser");

const fs = require('fs');
const path = require('path');

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: false }));

// 使用
app.use('/api', addAndDelete);

let MIME = {
  '.css': 'text/css',
  '.js': 'application/javascript'
};

app.use((req, res, next) => {
  const urlInfo = parseURL(__dirname, req.url);
  if (urlInfo) {
    // 合併檔案
    combineFiles(urlInfo.pathnames, (err, data) => {
      if (err) {
        res.writeHead(404);
        res.end(err.message);
      } else {
        res.writeHead(200, {
          'Content-Type': urlInfo.mime
        });
        res.end(data);
      }
    });
  }
});

// 解析檔案路徑
function parseURL(root, url) {
  let base, 
    pastnames,
    separator;
  if (url.indexOf('??') > -1) {
    separator = url.split('??');
    base = separator[0];

    pathnames = separator[1].split(',').map((value) => {
      const filepath = path.join(root, base, value);
      return filepath;
    });
    return {
      mime: MIME[path.extname(pathnames[0])] || 'text/plain',
      pathnames: pathnames
    }
  }
  return null;
};

//合併檔案
function combineFiles(pathnames, callback) {
  const output = [];
  (function nextFunc(l, len){
    if (l < len) {
      fs.readFile(pathnames[l], (err, data) => {
        if (err) {
          callback(err);
        } else {
          output.push(data);
          nextFunc(l+1, len);
        }
      })
    } else {
      const data = Buffer.concat(output);
      callback(null, data);
    }
  })(0, pathnames.length);
}

// 定義伺服器啟動埠 
app.listen(3001, () => {
  console.log('app listening on port 3001');
});

如上完成後,在我們的頁面引入該合併後的js即可:index.html 如下引入方式:

<script src="../combineFile/dist/components/??vendor.dll.afa07023.js,echarts.dll.38cfc51b.js" type="text/javascript"></script>

如上引入,為什麼我們的js前面會使用 combineFile 這個目錄呢,這是為了解決跨域的問題的,因此我們app.js 是在埠號為3001伺服器下的,而我們的webpack4的埠號8081,那頁面直接訪問 http://localhost:8081/#/list 的時候,肯定會存在跨域的情況下,因此前面加了個 combineFile檔案目錄,然後在我們的webpack中的devServer.proxy會代理下實現跨域,如下配置:

module.exports = {
  devServer: {
    port: 8081,
    // host: '0.0.0.0',
    headers: {
      'X-foo': '112233'
    },
    inline: true,
    overlay: true,
    stats: 'errors-only',
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:3001',
        changeOrigin: true  // 是否跨域
      },
      '/combineFile': {
        target: 'http://127.0.0.1:3001',
        changeOrigin: true,  // 是否跨域,
        pathRewrite: {
          '^/combineFile' : ''  // 重寫路徑
        }
      }
    }
  }
}

對請求為 '/combineFile' 會把它代理到 'http://127.0.0.1:3001',下,並且pathRewrite這個引數重寫路徑,以'^/combineFile' : '' 開頭的,會替換成空,因此當我們使用肉眼看到的如下這個請求:
http://127.0.0.1:8081/combineFile/dist/components/??vendor.dll.afa07023.js,echarts.dll.38cfc51b.js

它會被轉義成 :
http://127.0.0.1:3001/dist/components/??vendor.dll.afa07023.js,echarts.dll.38cfc51b.js

這個請求,因此就不會跨域了。如下所示:

github原始碼檢視

相關文章