一、前言
在專案的前端開發中,對於絕大多數的小夥伴來說,當然,也包括我,不可避免的需要在專案中使用到一些第三方的元件包。這時,團隊中的小夥伴是選擇直接去元件的官網上下載,還是圖省事直接在網上搜尋,然後從一些來源不明的地方下載,我們就無法管控了。同時,我們新增的元件間可能存在各種依賴關係,如果我們沒有正確下載引用的話,到最後可能還是無法正常使用。
因此,如何從可信的源下載元件包,以及如何輕鬆的解決各個元件間的依賴關係就成了我們需要解決的問題,那麼,有沒有一種工具可以幫我們解決這一問題?你好,有的,npm 瞭解一下。
程式碼倉儲:https://github.com/Lanesra712/grapefruit-common
二、Step by Step
在 .NET Framework 的專案中,我們可以在專案中通過 Nuget 下載安裝前端的元件包。但是 Nuget 更多的是作為 .NET 後端專案中的包管理器,在這裡管理前端的元件包顯得有些不太合適。
於是,在 .NET Core 的時代到來後,伴隨著前端的發展,微軟在建立的示例專案中開始推薦我們使用 bower 來管理我們專案中的前端元件包,然後,bower is dead。。。。
所以這裡,我採用 npm 作為我們的 ASP.NET Core 專案中的前端包管理器。
1、安裝 Node 環境
Node.js 是一個能夠在服務端執行 Javascript 的執行環境,也就是說,Javascript 不僅可以用於前端,也可以構建後端服務了。而 npm 則是 Node.js 官方提供的包管理工具,所以在使用 npm 之前,需要在我們的電腦上安裝 Node.js 環境。
當然,如果你之前有開發過 Vue、Angular 這類的前端專案,你肯定已經安裝好了。如果沒有,開啟 Node.js 的官網(https://nodejs.org/en/download),根據你正在使用的作業系統資訊,選擇安裝包下載就可以了。
如果你使用的是 window 系統,很簡單,下載 msi 安裝包,一路 next 即可。在最新版本的 Node.js 安裝包中,npm 是隨著 Node.js 的安裝一起完成的。我們可以使用下面的命令進行驗證,當可以列印出你安裝的版本資訊,則說明安裝已經完成了。
//1、node.js 版本 node -v //2、npm 版本 npm -v
2、使用 npm 安裝包
這篇文章的示例專案,我採用的是 ASP.NET Core 2.2 預設生成的 MVC 專案,因為在寫文章的過程中有過更換解決方案,所以文章中的截圖可能會出現名稱前後不對應的情況,還請見諒。
當示例專案建立完成後,會自動在專案中引用 bootstrap 和 jquery,所以,我們就在這個專案的基礎上,嘗試採用 npm 來管理我們的前端元件包。
右擊我們的專案,新增一個 package.json 配置檔案。在這個 json 檔案中定義了這個專案所需要的各種前端模組,以及專案的配置資訊(比如名稱、版本、許可證等等)。當我們從別處拷貝這個專案後,通過執行 npm install 命令,就會根據這個配置檔案,自動下載專案中所需要引用的前端元件包。
開啟 package.json 檔案,如果你選擇使用 VS 進行編輯的話,可以看到 VS 會自動幫我們出現程式碼補齊提示。這裡我新增了一個 dependencies 節點,它與 devDependencies 節點都代表我們專案中需要安裝的外掛。不同的是,devDependencies 裡面的外掛只用於開發環境,不用於生產環境,而 dependencies 中引用的則是需要釋出到生產環境中的。
例如,這裡我們需要在專案中新增 bootstrap 和 jquery,因為在正式釋出時如果缺少這兩個元件,就會導致我們的程式報錯,所以這裡我們需要新增到 dependencies 節點下,而像後面我們使用到的 gulp 的一系列外掛,只有在我們進行專案開發時才會使用到,所以我們只需要新增到 devDependencies 即可。
這裡我推薦使用命令列的方式新增元件,可以更好地展示出我們新增的元件需要新增哪些依賴。右鍵選中我們的示例專案,選擇 Open Command Line,開啟控制檯,輸入下列的命令,將 bootstrap 新增到我們的專案中。
在 install 命令中我們新增了 --save 修飾,表示需要將 bootstrap 新增到 dependencies 節點下面。如果,你需要將引用到的 package 安裝到 devDependencies 節點下,則需要使用 --save-dev 修飾。
npm install bootstrap --save
可以看到,安裝完成後,npm 提示我們 bootstrap 依賴於 jquery 和 popper.js,所以這裡我們手動新增上這兩個依賴的元件。
當我們安裝 jquery 的 1.9.1 版本後,因為之前的 jquery 版本存在一些安全隱患,所以 npm 會提示我們執行 npm audit 命令來檢視當前專案中可能存在的安全隱患,以及對於如何解決這些隱患的建議。
這裡我進行了版本升級,你可以根據自己的需求進行操作。請特別注意,當你在完成專案的基礎包載入後,後續對於包版本的升級一定要謹慎、謹慎、再謹慎。升級完成後的 package.json 檔案如下所示。
{ "version": "1.0.0", "name": "aspnetcore.npm.tutorial", "private": true, "devDependencies": {}, "dependencies": { "bootstrap": "^4.3.1", "jquery": "^3.4.1", "popper.js": "^1.14.7" } }
在我們第一次執行 npm install 命令時,系統自動為我們建立了 package-lock.json 這個檔案,用來記錄當前狀態下實際安裝的各個 npm package 的具體來源和版本號,當前專案下的 package-lock.json 檔案如下。
{ "name": "aspnetcore.npm.tutorial", "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "bootstrap": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" }, "jquery": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" }, "popper.js": { "version": "1.14.7", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.7.tgz", "integrity": "sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ==" } } }
那麼 package-lock.json 這個檔案到底有什麼用呢?
因為我們在 npm 上下載的包遵循了大版本.次要版本.小版本的版本定義。例如,在上面的示例中,我們使用 npm install 命令安裝的 bootstrap 版本為 4.3.1,而在安裝外掛包的時候,package.json 一般指定的是包的範圍,即只對外掛包的大版本進行限定。因此,當別人拷貝了你的程式碼,準備還原引用的包時,可能此時的 bootstrap 已經有 4.4.4 版本的了,這時,如果你使用了某些 4.3.1 版本中的特性,而在 4.4.4 版本中已經被移除的話,毫無疑問,你的程式碼就會出 bug。
而當專案中存在了 package-lock.json 檔案之後,因為專案中引用的元件包版本和來源資訊已經鎖定在了這個檔案中了,此時,當別人拷貝了程式碼,準備還原時,就可以準確的載入到你開發時使用的元件版本。當然,如果你修改了引用的包資訊,當執行 npm install 命令時,package-lock.json 檔案會同步更新。
對於包的版本限定條件如下所示。
指定版本:比如此例中 bootstrap 的版本為 4.3.1,當重新安裝時只安裝指定的 4.3.1 版本。
波浪號(tilde) + 指定版本:比如 ~1.2.2,表示安裝1.2.x 的最新版本(不低於1.2.2),但是不安裝 1.3.x,也就是說安裝時不改變大版本號和次要版本號。
插入號(caret) + 指定版本:比如 ˆ1.2.2,表示安裝1.x.x 的最新版本(不低於1.2.2),但是不安裝 2.x.x,也就是說安裝時不改變大版本號。需要注意的是,如果大版本號為0,則插入號的行為與波浪號相同。
latest:始終安裝包的最新版本。
3、gulp 配置
當我們通過 npm 新增好需要使用的元件包後,就需要考慮如何在專案中使用。
我們知道,在 ASP.NET Core 專案中,對於 web 專案中的靜態檔案的獲取,通常是使用 StaticFileMiddleware 這個中介軟體。而 “{contentroot}/wwwroot” 這個目錄是對外發布專案中的靜態檔案預設使用的根目錄,也就是說,我們需要將使用到的 npm 包移動到 wwwroot 檔案下。
手動複製?em,工作量似乎有點大。
不過,既然這裡我們使用到了 node.js,那麼這裡就可以使用 gulp.js 這個自動化任務執行器來幫我們實現這一功能,當然,你也可以根據自己的習慣使用別的工具。
通過使用 gulp.js,我們就可以自動的執行移動檔案,打包壓縮 js、css、image、刪除檔案等等,幫我們省了再通過 bundle 去打包壓縮 css 和 js 檔案的過程。
在專案中使用 gulp.js 的前提,需要我們作為專案的開發依賴(devDependencies)安裝 gulp 和一些用到的 gulp 外掛,因為會下載很多的東西,整個安裝的過程長短依據你的網路情況而定,嗯,請坐和放寬。
在這個專案中使用到的 gulp 外掛如下所示,如果你需要拷貝下面的命令列的話,在執行時請刪除註釋內容。
//gulp.js npm install gulp --save-dev //壓縮 css npm install gulp-clean-css --save-dev //合併檔案 npm install gulp-concat --save-dev //壓縮 js npm install gulp-uglify --save-dev //重新命名 npm install gulp-rename --save-dev //刪除檔案、資料夾 npm install rimraf --save-dev //監聽檔案變化 npm install gulp-changed --save-dev
安裝完成後的 package.json 檔案如下所示。
{ "version": "1.0.0", "name": "aspnetcore.npm.tutorial", "private": true, "devDependencies": { "gulp": "^4.0.1", "gulp-changed": "^3.2.0", "gulp-clean-css": "^4.2.0", "gulp-concat": "^2.6.1", "gulp-rename": "^1.4.0", "gulp-uglify": "^3.0.2", "rimraf": "^2.6.3" }, "dependencies": { "bootstrap": "^4.3.1", "jquery": "^3.4.1", "popper.js": "^1.14.7" } }
當我們安裝好所有的 gulp 元件包之後,在我們的專案根路徑下建立一個 gulpfile.js 檔案,檔案的內容如下所示。
/// <binding BeforeBuild='min' Clean='clean' ProjectOpened='auto' /> "use strict"; //載入使用到的 gulp 外掛 const gulp = require("gulp"), rimraf = require("rimraf"), concat = require("gulp-concat"), cssmin = require("gulp-clean-css"), rename = require("gulp-rename"), uglify = require("gulp-uglify"), changed = require("gulp-changed"); //定義 wwwroot 下的各檔案存放路徑 const paths = { root: "./wwwroot/", css: './wwwroot/css/', js: './wwwroot/js/', lib: './wwwroot/lib/' }; //css paths.cssDist = paths.css + "**/*.css";//匹配所有 css 的檔案所在路徑 paths.minCssDist = paths.css + "**/*.min.css";//匹配所有 css 對應壓縮後的檔案所在路徑 paths.concatCssDist = paths.css + "app.min.css";//將所有的 css 壓縮到一個 css 檔案後的路徑 //js paths.jsDist = paths.js + "**/*.js";//匹配所有 js 的檔案所在路徑 paths.minJsDist = paths.js + "**/*.min.js";//匹配所有 js 對應壓縮後的檔案所在路徑 paths.concatJsDist = paths.js + "app.min.js";//將所有的 js 壓縮到一個 js 檔案後的路徑 //使用 npm 下載的前端元件包 const libs = [ { name: "jquery", dist: "./node_modules/jquery/dist/**/*.*" }, { name: "popper", dist: "./node_modules/popper.js/dist/**/*.*" }, { name: "bootstrap", dist: "./node_modules/bootstrap/dist/**/*.*" }, ]; //清除壓縮後的檔案 gulp.task("clean:css", done => rimraf(paths.minCssDist, done)); gulp.task("clean:js", done => rimraf(paths.minJsDist, done)); gulp.task("clean", gulp.series(["clean:js", "clean:css"])); //移動 npm 下載的前端元件包到 wwwroot 路徑下 gulp.task("move", done => { libs.forEach(function (item) { gulp.src(item.dist) .pipe(gulp.dest(paths.lib + item.name + "/dist")); }); done() }); //每一個 css 檔案壓縮到對應的 min.css gulp.task("min:css", () => { return gulp.src([paths.cssDist, "!" + paths.minCssDist], { base: "." }) .pipe(rename({ suffix: '.min' })) .pipe(changed('.')) .pipe(cssmin()) .pipe(gulp.dest('.')); }); //將所有的 css 檔案合併打包壓縮到 app.min.css 中 gulp.task("concatmin:css", () => { return gulp.src([paths.cssDist, "!" + paths.minCssDist], { base: "." }) .pipe(concat(paths.concatCssDist)) .pipe(changed('.')) .pipe(cssmin()) .pipe(gulp.dest(".")); }); //每一個 js 檔案壓縮到對應的 min.js gulp.task("min:js", () => { return gulp.src([paths.jsDist, "!" + paths.minJsDist], { base: "." }) .pipe(rename({ suffix: '.min' })) .pipe(changed('.')) .pipe(uglify()) .pipe(gulp.dest('.')); }); //將所有的 js 檔案合併打包壓縮到 app.min.js 中 gulp.task("concatmin:js", () => { return gulp.src([paths.jsDist, "!" + paths.minJsDist], { base: "." }) .pipe(concat(paths.concatJsDist)) .pipe(changed('.')) .pipe(uglify()) .pipe(gulp.dest(".")); }); gulp.task("min", gulp.series(["min:js", "min:css"])); gulp.task("concatmin", gulp.series(["concatmin:js", "concatmin:css"])); //監聽檔案變化後自動執行 gulp.task("auto", () => { gulp.watch(paths.css, gulp.series(["min:css", "concatmin:css"])); gulp.watch(paths.js, gulp.series(["min:js", "concatmin:js"])); });
在 gulp.js 中主要有四個 API,就像我們專案中的 gulpfile 更多的是對於第三方外掛的使用,而我們只需要通過 pipe 將任務中的每一步操作新增到任務佇列中即可。完整的 API 文件,大家可以去官網去詳細檢視 => https://gulpjs.com/docs/en/api/concepts
gulp.src:根據匹配、或是路徑載入檔案;
gulp.dest:輸出檔案到指定路徑;
gulp.task:定義一個任務;
gulp.watch:監聽檔案變化。
當我們建立好任務後,刪除 wwwroot 路徑下的引用的第三方元件包,執行我們的示例專案,毫無疑問,整個頁面的樣式都已經丟失了。
選中 gulpfile.js,右鍵開啟任務執行程式資源管理器。可以看到,系統會自動顯示出我們定義的所有任務,這時,我們可以滑鼠右鍵點選任務,選中執行,即可執行我們的任務。
然而,我們手動去執行似乎有些不智慧,我們能不能自動執行某些任務呢?答案當然是可以,同樣是滑鼠右鍵點選任務,點選繫結選單選項,我們就將定義好的任務繫結事件上。
例如,在我的 gulpfile 中,我繫結了三個事件:生成解決方案前執行 min task,清理解決方案時執行 clean task,開啟專案時執行 auto task,而 VS 也自動幫我們生成了如下的繫結指令碼到我們的 gulpfile 上。
/// <binding BeforeBuild='min' Clean='clean' ProjectOpened='auto' />
通過將繫結事件與 gulp API 進行結合,就可以很好的實現我們的需求。就像這裡,我在專案開啟時繫結了自動監聽檔案變化的任務,這時,只要我修改了 css、js 檔案,gulp 就會自動幫我們實現對於檔案的壓縮。
PS:如果你將任務繫結到專案開啟的事件上,則是需要下一次開啟專案時才能自動執行。
三、總結
這一章主要是介紹瞭如何在我們的 ASP.NET Core 專案中通過 npm 管理我們的前端元件包,同時,使用 gulp 去執行一些移動檔案、壓縮檔案的任務。隨著這些年前端的發展,前端的開發越來越規範化,也越來越朝後端靠攏了,我們作為傳統意義上的後端程式猿,在涉及到前端的開發時,如果可以用到這些可以規範化我們的前端專案的特性,還是極好的。因為自己水平也很菜,很多東西並沒有很詳細的涉及到,可能還需要你在實際使用中進行進一步的探究,畢竟,實踐出真知。