1.前言
最近项目组打算从Apache+PHP环境迁移到Node上,正好刚看完入门资料,想借此练练手,也方便整合先前基于Grunt的压缩合并任务,于是,大幕拉开……
首先,说明下需要完成的任务,也即使用Node所能够带来的好处:
-
熟悉的JS操作,从函数的使用,到JSON的处理,以及事件、异步编程,乃是前端所擅长的,移植到Node后,可以减少对原有后端语言(PHP、Java等)的依赖,组内成员容易上手;
-
整合打包任务,也即项目构建,包括:Less编译CSS、合并、压缩、JSLint、打包等,后续还包括JSDoc、JSUnit等,借助Node的异步特性,可方便移植到Web平台上,实现自动化构建,从而无需由前端组专门构建;
-
结合WebSocket,除项目构建外,也可以打造消息平台,从IM客户端移植到Web上;
-
……
Node能实现的远超原先的想象,其所带来的性能也超乎想象,从易用性,到功能完整性,无不略胜一筹。在讲求敏捷开发的时代,不失为优秀的平台。
2.缘由
项目最早的构建是基于Ant,每次变更,都需要上传SVN后,登录SecureCRT手动执行命令更新到静态服务器上;然后后期有了压缩和合并,因为是基于SeaJS,整个合并的过程并没有那么简单,讨论的方案是使用Grunt进行模块的合并和压缩处理。于是,构建过程变成三步曲:执行合并压缩脚本、上传、部署脚本。烦不胜烦!而且在维护多个分支时,容易遗漏或忘记,在每次发测时已发生过不止一次版本不匹配情况。
3.开始
在确定移植到Node后,基于对Node的了解,突然想到Node的机制非常适合将构建过程移到Web上,并且可任务化、图形化、实时化,何乐而不为?做成了,对项目组乃至整个前端组都是大功一件!>_<
于是,立马动工……
入门的过程忽略,主要在于对各种Grunt模块的了解,以及Node的文件操作等。
3.1.导出SVN
因为有现成的工具和命令行可以直接执行SVN更新、导出等操作,所以首选命令行。首先采用spawn直接尝试运行输出,发现能运行ipconfig等命令,却不能执行cd等基本命令,各种搜索后,给出的答案是调用cmd,也因此而了解了调用子进程的四种方式之间的区别(spawn、exec、fork、execFile)。确认能实现后,搜索相关的Grunt模块,组员推荐的是grunt-shell,但有乱码问题,而且无法(至少目前没有找到)解决,然后找到grunt-shell-spawn,但出现无法输出的问题,对比grunt-shell源代码后,想起在grunt官网资料上看到的一段话:
Chances are this is happening because you have forgotten to call the this.async method to tell Grunt that your task is asynchronous. For simplicity's sake, Grunt uses a synchronous coding style, which can be switched to asynchronous by calling this.async() within the task body.
Note that passing false to the done() function tells Grunt that the task has failed.
于是查看源代码,才恍然大悟,问题出在async配置上。
解决输出问题后,同样出现乱码问题,但现象和直接采用spawn类似,便借鉴后者的解决方法移植到模块配置上,并修改了模块源代码。因此顺带了解了iconv-lite模块。
看到屏幕上的输出,甚是欢喜,万事开头难,解决了第一步,已然胜利了大半。
导出SVN任务:
shell: { exportSvn: { command: 'svn export "<%= config.svn.repoUrl %>" <%= config.path.tmp.svn %> --username <%= config.svn.user %> --password <%= config.svn.password %>', options: { async: false, stdout: function(data) { process.stdout.write(iconv.decode(data, 'gb2312')); }, stderr: function(data) { process.stderr.write(iconv.decode(data, 'gb2312')); } } } }
3.2.文件替换
因为项目原先采用的是PHP和SHTML的语法,需要对文件包含等语句进行替换,以符合ejs的语法(也可不替换,但和注释语法混淆),同时,因为Node本身有中间件支持Less的解析,因此可以去除页面上解析Less的js包含语句,于是,采用replace模块实现如下:
replace: { shtml: { src: ['<%= config.path.dest.views %>/*.shtml'], overwrite: true, replacements: [{ from: /<!--#include\s*file="([\w\-\.]+)"\s*-->/ig, to: '{{ include $1 }}' }] }, less: { src: ['<%= config.path.dest.views %>/*.php'], overwrite: true, replacements: [{ from: 'stylesheet/less', to: 'stylesheet' }, { from: /"less\/([\w\-]+)\.less"/ig, to: '"less/$1.css"' }, { from: /(<script\stype="text\/javascript"\ssrc="js\/lib\/less\/[\w\-\\\/\.]+\.js"><\/script>)/ig, to: '<!--$1-->' }] } }
3.3.文件复制
项目原结构为JS、LESS、Img、Mockup和SHTML文件平级,移植到Node后,前三者在public下,和mockup平级,shtml、php等文件在views下(含二级目录),因此需要分别复制,使用copy实现如下:
copy: { views: { files: [{ expand: true, cwd: '<%= config.path.tmp.svn %>', src: ['*.*', '<%= config.path.src.views %>'], dest: '<%= config.path.dest.views %>' }] }, public: { files: [{ expand: true, cwd: '<%= config.path.tmp.svn %>', src: ['<%= config.path.src.public %>'], dest: '<%= config.path.dest.public %>' }] }, mockup: { files: [{ expand: true, cwd: '<%= config.path.tmp.svn %>', src: ['<%= config.path.src.data %>'], dest: '<%= config.path.dest.data %>' }] } }
3.4.Less解析
项目采用Less编译生成css文件(个人觉得,方便编写、调试,但生成的目标文件太过庞大,也多少会影响性能),于是加入Less的编译任务(分前台和后台):
less: { front: { options: { compress: true, cleancss: true, report: 'gzip' }, files: [{ cwd: '<%= config.path.dest.public %>', expand: true, src: 'less/all.less', dest: '<%= config.path.dest.public %>', rename: function(dest, src) { return dest + src.replace('.less', '.css'); } }] }, admin: { options: { compress: true, cleancss: true, report: 'gzip' }, files: [{ cwd: '<%= config.path.dest.public %>', expand: true, src: 'less/all-admin.less', dest: '<%= config.path.dest.public %>', rename: function(dest, src) { return dest + src.replace('.less', '.css'); } }, { cwd: '<%= config.path.dest.public %>', expand: true, src: 'less/all-new-admin.less', dest: '<%= config.path.dest.public %>', rename: function(dest, src) { return dest + src.replace('.less', '.css'); } }] } }
3.5.合并压缩
改动较大,基于grunt-cmd-transport、grunt-cmd-concat、grunt-contrib-uglify改造而来,主要思想是递归合并require包含的模块文件,并提供配置以排除不需要合并的文件,以及提供压缩和非压缩之间的切换(URL+Cookie实现)。任务配置略,见完整Gruntfile.js配置。
3.6.JSLint
组员推荐JSHint,在于配置的灵活性,但个人认为,JSLint也可以通过声明等形式进行个性配置,更重要的在于已经有现成的可视化工具提供页面形式的输出,相比JSHint的控制台或文件输出,显然前者更为人性化,且可以结合Node,以Web形式访问,而且全组可查看校验结果。
任务配置如下:
shell: { jslint: { command: '<%= config.path.jslint.disk %>&cd <%= config.path.jslint.bin %>&run.bat', options: { async: false, stdout: function(data) { process.stdout.write(iconv.decode(data, 'gb2312')); }, stderr: function(data) { process.stderr.write(iconv.decode(data, 'gb2312')); } } } }
主要有个问题,因为JSLint的配置文件和JS源代码文件、结果输出文件不在同一目录,因此,在其结果输出文件名中便包含“..”,从而导致res.render和res.sendfile调用失败,返回404或403。网上查找资料,没有找到相关解决信息,于是显式地增加了两条路由,控制台调试输出显示路由匹配成功,但仍然返回404。于是继续尝试更改文件名的形式,增加了rename的任务,无奈rename同样失败,而且rename任务无法配置src和dest为正则形式。只好另寻其他方案,尝试调用fs.exists,返回true,说明file调用能够操作此文件,便试着通过readFile的形式输出,一举成功!兴奋!
3.7.压缩打包
此任务用于提供给后端进行部署,剔除页面文件,只包含js、图片、样式等,如下:
zip: { publish: { cwd: '<%= config.path.dest.public %>', src: ['<%= config.path.src.zipfiles %>'], dest: '<%= config.path.publish %>/<%= config.info.name + "-" + config.info.version + "-" + grunt.template.today("yyyymmddHHMMss") + ".zip" %>' } }
4.TODO
至此,预先制定的构建任务已全部完成,剩余的便是任务执行的定制化(以避免全部执行)、不同版本分支的构建、页面排版优化、消息机制引入(构建时通知全组)、构建结果邮件通知、代码量计算等等。
5.结束语
Grunt使用下来,相比Ant,确实前者更为容易配置、执行,而且结合丰富的各种模块和Web,几乎能实现各种功能。试想,若是用PHP或Java和Ant构建一套Web构建平台,不知要做多少工作?……
附1:完整构建任务配置
/*jslint es5:true*/ /*global process*/ /** * 构建任务配置 * @author Fuyun * @version 1.0.0(2014-04-06) * @since 1.0.0(2014-04-03) */ module.exports = function(grunt) { //@formatter:off 'use strict'; //@formatter:on var iconv = require('iconv-lite'); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), config: grunt.file.readJSON('config.json'), shell: { exportSvn: { command: 'svn export "<%= config.svn.repoUrl %>" <%= config.path.tmp.svn %> --username <%= config.svn.user %> --password <%= config.svn.password %>', options: { async: false, stdout: function(data) { process.stdout.write(iconv.decode(data, 'gb2312')); }, stderr: function(data) { process.stderr.write(iconv.decode(data, 'gb2312')); } } }, jslint: { command: '<%= config.path.jslint.disk %>&cd <%= config.path.jslint.bin %>&run.bat', options: { async: false, stdout: function(data) { process.stdout.write(iconv.decode(data, 'gb2312')); }, stderr: function(data) { process.stderr.write(iconv.decode(data, 'gb2312')); } } } }, replace: { shtml: { src: ['<%= config.path.dest.views %>/*.shtml'], overwrite: true, replacements: [{ from: /<!--#include\s*file="([\w\-\.]+)"\s*-->/ig, to: '{{ include $1 }}' }] }, less: { src: ['<%= config.path.dest.views %>/*.php'], overwrite: true, replacements: [{ from: 'stylesheet/less', to: 'stylesheet' }, { from: /"less\/([\w\-]+)\.less"/ig, to: '"less/$1.css"' }, { from: /(<script\stype="text\/javascript"\ssrc="js\/lib\/less\/[\w\-\\\/\.]+\.js"><\/script>)/ig, to: '<!--$1-->' }] } }, copy: { views: { files: [{ expand: true, cwd: '<%= config.path.tmp.svn %>', src: ['*.*', '<%= config.path.src.views %>'], dest: '<%= config.path.dest.views %>' }] }, public: { files: [{ expand: true, cwd: '<%= config.path.tmp.svn %>', src: ['<%= config.path.src.public %>'], dest: '<%= config.path.dest.public %>' }] }, mockup: { files: [{ expand: true, cwd: '<%= config.path.tmp.svn %>', src: ['<%= config.path.src.data %>'], dest: '<%= config.path.dest.data %>' }] } }, less: { front: { options: { compress: true, cleancss: true, report: 'gzip' }, files: [{ cwd: '<%= config.path.dest.public %>', expand: true, src: 'less/all.less', dest: '<%= config.path.dest.public %>', rename: function(dest, src) { return dest + src.replace('.less', '.css'); } }] }, admin: { options: { compress: true, cleancss: true, report: 'gzip' }, files: [{ cwd: '<%= config.path.dest.public %>', expand: true, src: 'less/all-admin.less', dest: '<%= config.path.dest.public %>', rename: function(dest, src) { return dest + src.replace('.less', '.css'); } }, { cwd: '<%= config.path.dest.public %>', expand: true, src: 'less/all-new-admin.less', dest: '<%= config.path.dest.public %>', rename: function(dest, src) { return dest + src.replace('.less', '.css'); } }] } }, zip: { publish: { cwd: '<%= config.path.dest.public %>', src: ['<%= config.path.src.zipfiles %>'], dest: '<%= config.path.publish %>/<%= config.info.name + "-" + config.info.version + "-" + grunt.template.today("yyyymmddHHMMss") + ".zip" %>' } }, clean: { beforeExport: ['<%= config.path.tmp.svn %>'], beforeTransport: ['../public/js/pages-min', '../public/js/pages-admin-min'], afterUglify: ['../public/.transport', '../public/.concat', '../public/js/pages-min/global.js', '../public/js/pages-admin-min/global.js'] }, transport: { options: { paths: ['../public/js'], debug: false, alias: grunt.file.readJSON('alias.json') }, // 前台页面模块的转换 frontPages: { files: [{ expand: true, cwd: '../public/js/pages', src: '*.js', dest: '../public/.transport/pages' }] }, // 后台页面模块的转换 adminPages: { files: [{ expand: true, cwd: '../public/js/pages-admin', src: '*.js', dest: '../public/.transport/pages-admin' }] }, // 辅助库模块的转换 lib: { files: [{ expand: true, cwd: '../public/js', src: 'lib/**/*.js', dest: '../public/.transport/' }] } }, concat: { // 前台页面模块的合并 frontPages: { options: { paths: '../public/.transport/', include: 'relative', // 把global模块合并到每个模块中 globalModule: 'pages/global.js', footer: 'seajs.use(["global"]);', logPath: 'concat.log' }, files: [{ expand: true, cwd: '../public/.transport/pages', src: '*.js', dest: '../public/.concat/pages' }] }, // 后台页面模块的合并 adminPages: { options: { paths: '../public/.transport/', include: 'relative', // 把global模块合并到每个模块中 globalModule: 'pages-admin/global.js', footer: 'seajs.use(["global"]);', logPath: 'concat.log' }, files: [{ expand: true, cwd: '../public/.transport/pages-admin', src: '*.js', dest: '../public/.concat/pages-admin' }] } }, uglify: { options: { banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd HH:MM:ss") %> */\r\n', }, frontPages: { files: [{ expand: true, cwd: '../public/.concat/pages', src: '*.js', dest: '../public/js/pages-min' }] }, adminPages: { files: [{ expand: true, cwd: '../public/.concat/pages-admin', src: '*.js', dest: '../public/js/pages-admin-min' }] } } }); grunt.loadNpmTasks('grunt-shell-spawn'); grunt.loadNpmTasks('grunt-text-replace'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-cmd-transport'); grunt.loadNpmTasks('grunt-cmd-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-zip'); grunt.registerTask('default', ['clean:beforeExport', 'shell:exportSvn', 'copy', 'replace', 'shell:jslint', 'less', 'clean:beforeTransport', 'transport', 'concat', 'uglify', 'clean:afterUglify', 'zip']); };
附2:Socket.io脚本
$(function () { var socket = io.connect('http://127.0.0.1'), $log = $('#buildLog'); socket.on('ready', function(data){ $log.html($log.html() + data); }); socket.on('log', function(data){ data = data.replace(/\[\d{1,2}m/ig, ' '); data = data.replace(/\n/ig, '<br/>'); $log.html($log.html() + data); }); socket.on('error', function(data){ $log.html($log.html() + '<br/>Error: ' + data); }); socket.on('finish', function(data){ $log.html($log.html() + data); socket.disconnect(); }); $('#doBuild').click(function(e){ $log.html(''); socket.socket.reconnect(); socket.emit('doBuild', ''); }); });
附3:页面截图
首页:移植到Node后,根据文件名转为树状形式输出:
构建执行结果输出(不含更新SVN):
构建完成后,生成打包文件,提供页面供对历史打包归档文件的列表访问以及下载:
相关推荐
Node.js 与大前端的结合体现在它可以作为构建工具,例如使用 Gulp 或 Webpack 进行自动化构建流程,包括文件的合并、压缩、编译等。此外,Node.js 可以作为API服务器,配合前端框架(如React、Vue或Angular)进行...
Node.js 是一个开源、跨平台的JavaScript运行环境,它允许开发者在服务器端执行JavaScript代码,极大地拓宽了JavaScript的应用领域。10.16.3-x64 版本是 Node.js 的一个稳定版本,适用于64位操作系统。下面将详细...
在本项目中,“微信小程序+Node.js 构建的失物招领平台源码.zip”是一个包含使用微信小程序和Node.js技术开发的失物招领系统完整源代码的压缩包。这个平台旨在方便用户报告丢失或找到的物品,并促进双方之间的沟通与...
在前端开发中,Node.js 被广泛用于构建工具链,如 Webpack、Gulp 和 Grunt 等,这些工具可以帮助开发者自动化构建过程,包括代码编译、压缩、合并、格式化等。此外,通过 Express 框架,Node.js 可以快速搭建后端...
Node.js是一种开源、跨平台的JavaScript运行环境,它允许开发者在服务器端运行JavaScript代码,极大地扩展了JavaScript的应用范围。Node.js基于Chrome V8引擎,因此它具有高性能和高效率的特点。这个压缩包文件包含...
Node.js是一个基于V8引擎的开源、跨平台的JavaScript运行环境,用于执行JavaScript代码。它允许开发者使用JavaScript编写... 自动化脚本:Node.js可以用于编写自动化脚本,如任务调度、文件处理等。 物联网应用:Nod
**Node.js介绍** Node.js是一个基于Chrome V8引擎的...以上就是关于“Node.js-一个使用node.js构建无服务器平台的实验”的详细解析,涵盖了从理论知识到实践操作的多个层面,帮助理解Node.js在无服务器架构中的应用。
开发者可以使用Mocha和Chai进行单元测试,而Jenkins或Travis CI则用于自动化构建和测试流程。 总的来说,《新时期的Node.js入门》是一本全面介绍Node.js基础与实践的书籍,通过李锴精心编写的实例代码,读者不仅能...
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,它让开发者能够在服务器端使用 JavaScript 进行...前端开发者可以通过这些材料学习如何运用 Node.js 实现前后端同构、构建 CLI 工具、部署自动化流程等。
Node.js in Action 第二版 第一章的知识点主要包括Node.js的基础介绍、编写Node.js程序的基本原理、以及网络爬虫自动化网络数据处理的相关内容。 首先,Node.js是一种基于Chrome V8引擎的JavaScript运行时环境,它...
- **构建工具**:如Gulp、Webpack等自动化构建流程工具。 **6. 不适用场景** - CPU密集型计算:Node.js的单线程模型不适合大量计算任务。 - 需要多线程并行处理的应用:Node.js的非并行处理可能导致瓶颈。 - 非...
Vue.js 是一个轻量级的前端JavaScript框架,它以其易用性、灵活性和组件化特性而闻名。Vue.js的核心理念是通过声明式渲染,使得开发者可以专注于描述视图应该显示什么,而无需关注如何实现这一过程。在Vue.js中,你...
- **持续集成/持续部署 (CI/CD)**:建立自动化构建和部署流程,提高开发效率。 #### 总结 《Learning.Node.js.for.Mobile.Application.Development.2015.10.pdf》这本书为希望使用Node.js进行移动应用开发的学习...
- **Gulp** 是一个自动化任务运行器,可以自动化构建过程,如 CSS 预处理器编译、压缩文件、浏览器同步等。 - **Stylus** 是一个 CSS 预处理器,它允许开发者使用变量、嵌套规则、函数等编程特性编写 CSS。 - **...
例如,前端构建工具(如 Grunt、Gulp、Webpack)多采用 Node.js 编写,此外,Node.js 还可以用于自动化测试、代码质量检查、静态文件服务器等前端开发环节。 综上所述,"node-v9.11.2-win-x64-安装文件.zip" 提供了...
5. **构建工具**:Gulp、Grunt等基于Node.js的构建工具,用于自动化项目构建和测试。 **总结** Node.js以其独特的事件驱动、非阻塞I/O模型,以及高效的V8引擎,改变了服务器端编程的格局,使得JavaScript成为全栈...
本项目是一个基于Node.js的机房收费管理系统,它旨在为大学机房提供一种自动化计费解决方案,从而提高管理效率。Node.js是一个流行的开源JavaScript运行环境,它允许开发人员使用JavaScript进行服务器端编程。这个...
1. **PM2**:学习使用PM2管理Node.js应用,实现进程监控、负载均衡和自动重启。 2. **Nginx反向代理**:了解如何配置Nginx作为Node.js应用的反向代理,提升性能和稳定性。 3. **Docker化部署**:将Node.js应用容器化...
Node.js还常被用作构建CLI工具,如自动化脚本,因为其强大的文件系统操作能力和处理流的能力。 总之,Node.js是一个功能强大的平台,为开发人员提供了一种统一的语言环境,用于构建从前端到后端的全栈Web应用。通过...