在 ASP.NET Core 项目中使用 npm 管理你的前端组件包
一、前言
在项目的前端开发中,对于绝大多数的小伙伴来说,当然,也包括我,不可避免的需要在项目中使用到一些第三方的组件包。这时,团队中的小伙伴是选择直接去组件的官网上下载,还是图省事直接在网上搜索,然后从一些来源不明的地方下载,我们就无法管控了。同时,我们添加的组件间可能存在各种依赖关系,如果我们没有正确下载引用的话,到最后可能还是无法正常使用。
因此,如何从可信的源下载组件包,以及如何轻松的解决各个组件间的依赖关系就成了我们需要解决的问题,那么,有没有一种工具可以帮我们解决这一问题?你好,有的,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 去执行一些移动文件、压缩文件的任务。随着这些年前端的发展,前端的开发越来越规范化,也越来越朝后端靠拢了,我们作为传统意义上的后端程序猿,在涉及到前端的开发时,如果可以用到这些可以规范化我们的前端项目的特性,还是极好的。因为自己水平也很菜,很多东西并没有很详细的涉及到,可能还需要你在实际使用中进行进一步的探究,毕竟,实践出真知。