本文學習使用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'); });
二: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
這個請求,因此就不會跨域了。如下所示: