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是一个基于V8引擎的开源、跨平台的JavaScript运行环境,用于执行JavaScript代码。它允许开发者使用JavaScript编写... 自动化脚本:Node.js可以用于编写自动化脚本,如任务调度、文件处理等。 物联网应用:Nod
Gulp: 后台服务器使用,自动化构建工具。 Webpack: Vue-cli自带的。需要配置本地代理proxyTable,以及配置SCSS的相关loader。 MySQL: 数据保存。 superagent: 比Node.js原生http模块更好用的客户端请求代理模块。 ...
茉莉花节点jsdom-extjs-testing-tool 使用 jasmine-node 和 jsdom 的功能性前端 Ext.JS 测试自动化工具如果您已经安装了节点包模块( ),安装将为您获取所需的库。Ext.JS 设置使用 Ext.JS 包并遵循 Sencha cmd 企业...
在技术选型上以 JavaScript & Node.js 为主要开发语言,前端使用 Vue.js 全家桶,后端主要使用 Node.js 与 TypeScript 实现,采用 Web 服务框架 Express 与 MySQL 数据库构建后台应用。 # 项目运行 请预先安装 ...
前端自动化构建环境可以把这些重复工作一次配置,多次重复执行,极大的提高开发效率。 目前最知名的构建工具: Gulp、Grunt、NPM + Webpack; grunt是前端工程化的先驱 gulp更自然基于流的方式连接任务 ...
使用jQuery和Bootsrap完成网站前端JS脚本和样式处理; 前后端的数据请求交互通过Ajax完成; 引入了Moment.js格式化前端页面显示时间; 2、项目后端搭建: 使用NodeJs的express框架完成电影网站后端搭建; 使用mongodb...
借助Webpack这个前端自动化构建工具 可以完美实现资源的合并 打包 压缩 混淆等诸多功能 示意图: 官网:https://webpack.github.io 二、安装 在新版本中 需要分开安装webpack和webpack-cli 安装web
Node催生了一批自动化工具,像Bower,Yeoman,Grunt等。 gulp和grunt的异同点 易于使用:采用代码优于配置策略,Gulp让简单的事情继续简单,复杂的任务变得可管理。 高效:通过利用Node.js强大的流,不需要往磁盘写...
在前端开发中,会涉及到一些重要的前端技术栈,如vue.js、react.js、node.js、前端安全、react-native等。同时,也需要借助一些工具,如代码编辑工具(WebStorm、VS Code)、代码版本控制工具(Git、SVN)、代码包...
使用D2Server完全可以替换Apache的开发环境(),构建一个基于静态文件和测试数据的前端开发环境。Features使用项目配置文件管理项目,项目信息简单明了针对团队协作,可每人设置独立的项目配置文件,由D2Server来...
我们使用了(简称gulp)作为前端自动化工具,gulp是构建在平台上的一整套前端自动化工具平台,有大量的插件可供我们使用。因此,第一次启动此项目前我们需要搭建好自动化环境。 2. gulp环境搭建 gulp是node.js平台上的...
基于PostgreSQL数据库的自动化CRUD资源(使用pg和co-postgres-queries ) 最先进的API健壮性和安全性(JWT,速率限制,安全标头,基于helmet ) 管理员的单独API,具有不同的安全设置(和登录表单) 内置数据库...
在应用程序文件夹的源代码文件夹中运行: $ npm install -g retire$ retireGrunt插件在应用程序的构建例程或某些其他自动化工作流程中 。Gulp任务一个Gulp任务的示例,可以在gulpfile中使用它来自动监视和扫描项目...
是基于Node.js 实现web前端自动化构建的工具,它可以自动化高效的构建我们工作中的一些任务, 在 Web 前端开发工作中有很多“重复工作”,比如压缩CSS/JS文件、es6编译成es5,而这些工作都是有规律的。找到这些规律,...
测试自动化示例 包括前端代码构建运行 样本用户模块准备开始项目发展这是Node / Swagger JuthorAPI Admin Application项目的存储库包括节点表示昂首阔步运行服务器(节点) npm installnpm start生产或阶段npm run ...
本项目适合IT相关专业各种计算机技术的源代码和项目资料,如计科、人工智能、通信工程、自动化和电子信息等的在校学生、老师或者企业员工下载使用。 也适合小白学习进阶,可以用作比赛项目、可以进行项目复刻去参加...
随着前端技术的飞速发展,前端开发也从原始的刀耕火种,向着工程化效率化的方向发展。在各种开发框架之外,打包编译等技术也是层出不穷,开发体验也是越来越好。例如HMR,让我们的更新可以即时可见,告别了手动F5的...
gulp.js 是一种基于流的,代码优于配置的新一代构建工具。 Gulp 和 Grunt 类似。但相比于 Grunt 的频繁的 IO 操作,Gulp 的流操作,能更快地完成构建。 易于使用 通过代码优于配置的策略,Gulp让简单的任务简单,...
自nodeJS登上前端舞台,自动化构建变得越来越流行。目前最流行的当属grunt和gulp,这两个光看名字挺像,功能也差不多,不过gulp能在grunt这位大哥如日中天的境况下开辟出自己的一片天地,有着她独到的优点。 易用 ...