一、前言
在項目的前端開發中,對於絕大多數的小伙伴來說,當然,也包括我,不可避免的需要在項目中使用到一些第三方的組件包。這時,團隊中的小伙伴是選擇直接去組件的官網上下載,還是圖省事直接在網上搜索,然后從一些來源不明的地方下載,我們就無法管控了。同時,我們添加的組件間可能存在各種依賴關系,如果我們沒有正確下載引用的話,到最后可能還是無法正常使用。
因此,如何從可信的源下載組件包,以及如何輕松的解決各個組件間的依賴關系就成了我們需要解決的問題,那么,有沒有一種工具可以幫我們解決這一問題?你好,有的,npm 了解一下。
代碼倉儲:https://github.com/Lanesra712/ingos-common/tree/master/sample/aspnetcore/aspnetcore-npm-tutorial
二、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 去執行一些移動文件、壓縮文件的任務。隨着這些年前端的發展,前端的開發越來越規范化,也越來越朝后端靠攏了,我們作為傳統意義上的后端程序猿,在涉及到前端的開發時,如果可以用到這些可以規范化我們的前端項目的特性,還是極好的。因為自己水平也很菜,很多東西並沒有很詳細的涉及到,可能還需要你在實際使用中進行進一步的探究,畢竟,實踐出真知。