一个小型项目的前端构建方案

首先,给大家说明一下这次项目的背景,是针对于一个用smarty模板引擎写的开放平台的UI改版工作,需求比较急。原始架构是基于php yii框架,前后端代码杂糅在一起,逻辑也较为混乱,以下是其基本目录结构:

基本的业务代码是在websites文件目录下,assets文件夹目录下存储了一些静态文件资源。鉴于以上几个原因,最后定的方案是,不进行html代码的重构,只进行css代码的样式覆盖并采用less语言进行开发从而提高效率。

方案确定好了,接下来就是确定前端的一个基本的构建方案,要解决的问题有这么几个:

1、less文件的代码组织方式。

2、less文件的编译打包,你自然不能把一堆less文件扔到线上让less.min.js去进行编译解析。

3、缓存问题,为了最大程度的提升性能,我们需把每次上线的新版本文件加戳,从而不读取缓存获取最新的代码。关于缓存这方面的知识可以参考https://my.oschina.net/jathon/blog/404968 一文。

针对于第一个问题,最终采用的方案是根据less本身函数注入、导入、变量等特性而采取组件化开发的思想,对于整个项目进行公用部分的拆分,针对性的对每一个组件单独抽离出相应的less文件样式。最终的目录结构如下:(图一所示)

       

               图一                                                  图二                                                                              图三

在d_theme_main.less文件中是对各组件文件的引用,如图二所示。本次UI改版的样式文件引用在lib.tpl文件下,如图三所示。

针对于第二、三个问题我们放在一起进行解决。

与其他项目不同的是,本次项目开发没有开发、生产环境之分,不存在输出目录的概念。我们要做的就是:

1、实现对于less文件的编译打包压缩,对于各组件的less文件将最终编译打包到一个css文件里。

2、实现对lib.tpl文件中对css文件引用的加戳,从而解决缓存问题。

我们用的是gulp前端自动化工具,将在项目根目录下创建gulpfile.js文件,并安装相应的项目依赖。

第一个问题相对来说好解决,gulpfile.js代码如下:

//导入工具包 require('node_modules里对应模块')
var gulp = require('gulp'), //本地安装gulp所用到的地方
    less = require('gulp-less'),
    rev = require('gulp-rev'),
    nodepath = require('path'),
    through = require('through2'),
    gutil = require('gulp-util'),
    inject = require('gulp-inject'),
    cleanCSS = require('gulp-clean-css'),
    assetRev = require('gulp-asset-rev'),
    revCollector = require('gulp-rev-collector'),
    revReplace = require("gulp-rev-replace");

var path = {
        app: process.cwd(), //当前app的目录路径
        basePath: './datawork/websites/visual', //基础目录
        lessPath: './datawork/websites/visual/assets/css/theme/', //需打包编译的less路径和产出css文件的目录

    }
//定义一个testLess任务(自定义任务名称)
gulp.task('testLess', function() {
    gulp.src(path.lessPath + 'd_theme_main.less') //该任务针对的文件
        .pipe(less()) //该任务调用的模块
        .pipe(cleanCSS())
        .pipe(gulp.dest(path.lessPath)); //将会在src/css下生成相应的css文件
});

经过编译测试发现结果如预期,对less文件进行编译打包压缩,并在当前目录下生成相应的css文件。

接下来,编写加戳功能和自动编译的代码,代码如下所示:

//css文件生成hash编码并生成rev-manifest.json文件名对照映射
gulp.task('revCss',['testLess'], function() {
    return gulp.src(path.lessPath + 'd_theme_main.css')
        .pipe(rev())  //针对相应的css文件生成md5戳
        .pipe(rev.manifest()) //生成rev-manifest.json文件
        .pipe(gulp.dest('./'))  //rev-manifest.json文件的生成位置
})
//参照rev-manifest.json文件,替换相应目录下的lib.tpl文件对于css文件的引用
gulp.task('revhtml',['revCss'], function() {
    return gulp.src(['./rev-manifest.json', path.basePath + '/protected/views/layouts/lib.tpl'])
        .pipe(revCollector())
        .pipe(gulp.dest(path.basePath + '/protected/views/layouts/'));
})


gulp.task('watch', function() {
    gulp.watch(path.lessPath + '*.less', ['revhtml']);
})
gulp.task('default', ['revhtml', 'watch']); //定义默认任务 

执行gulp命令,得到的结果如下:

根目录下生成了manifest.json对应文件
{
  "d_theme_main.css": "d_theme_main-803a7fe4ae.css"
}

<link rel="stylesheet" href="/assets/css/theme/d_theme_main-803a7fe4ae.css">

这显然不符合我们的预期,我们并不需要在相应的目录下生成d_theme_main-803a7fe4ae.css文件同时改变tpl文件中的引用,我们只需要在相应的引用后面加戳即可。

更改gulp-rev和gulp-rev-collector

打开node_modules\gulp-rev\index.js
第144行 manifest[originalFile] = revisionedFile;
更新为: manifest[originalFile] = originalFile + '?v=' + file.revHash;
打开nodemodules\gulp-rev\nodemodules\rev-path\index.js
10行 return filename + '-' + hash + ext;
更新为: return filename + ext;
打开node_modules\gulp-rev-collector\index.js
31行 if ( !_.isString(json[key]) || path.basename(json[key]).replace(new RegExp( opts.revSuffix ), '' ) !==  path.basename(key) ) {
更新为: if ( !_.isString(json[key]) || path.basename(json[key]).split('?')[0] !== path.basename(key) ) {
打开node_modules\gulp-assets-rev\index.js
78行 var verStr = (options.verConnecter || "-") + md5;
更新为:var verStr = (options.verConnecter || "") + md5;
80行 src = src.replace(verStr, '').replace(/(\.[^\.]+)$/, verStr + "$1");
更新为:src=src+"?v="+verStr;

再执行gulp命令,得到的结果如下(效果正确):

<link rel="stylesheet" href="/assets/css/theme/d_theme_main.css?v=afd12860ab">

但是假如我们更改了css和js后,再执行gulp命令,得到的结果会如下:

<link rel="stylesheet" href="/assets/css/theme/d_theme_main.css?v=afd12860ab?v=803a7fe4ae">

有没有发现,会在版本号后面再添加一个版本号,因为gulp只替换了原来文件名,这样又不符合预期效果了,所以我们想到,还需要修改插件的替换正则表达式。

4、继续更改gulp-rev-collector

打开node_modules\gulp-rev-collector\index.js
第107行 regexp: new RegExp( '([\/\\\\\'"])' + pattern, 'g' ),
更新为: regexp: new RegExp( '([\/\\\\\'"])' + pattern+'(\\?v=\\w{10})?', 'g' ),

现在你不管执行多少遍gulp命令,得到的html效果都是

<link rel="stylesheet" href="/assets/css/theme/d_theme_main.css?v=86f020d88a">

至此,一个较为完善的一个方案就诞生了,但是还有优化的地方吗,答案自然自然是有的,假如这时又有其他人介入此项目的开发,你总不能让他们根据你的文档去修改相应插件的源码吧,于是进行以下修改:

//导入工具包 require('node_modules里对应模块')
var gulp = require('gulp'), //本地安装gulp所用到的地方
    less = require('gulp-less'),
    rev = require('gulp-rev'),
    nodepath = require('path'),
    through = require('through2'),
    gutil = require('gulp-util'),
    inject = require('gulp-inject'),
    cleanCSS = require('gulp-clean-css'),
    revReplace = require("gulp-rev-replace");

var path = {
        app: process.cwd(), //当前app的目录路径
        basePath: './datawork/websites/visual', //基础目录
        lessPath: './datawork/websites/visual/assets/css/theme/', //需打包编译的less路径和产出css文件的目录

    }
    //定义一个testLess任务(自定义任务名称)
gulp.task('testLess', function() {
    gulp.src(path.lessPath + 'd_theme_main.less') //该任务针对的文件
        .pipe(less()) //该任务调用的模块
        .pipe(cleanCSS())
        .pipe(gulp.dest(path.lessPath)); //将会在src/css下生成index.css
});

gulp.task('rev', ['testLess'], function() {
    return gulp.src([path.lessPath + 'd_theme_main.css'])
        .pipe(rev())
        .pipe(function() {
            var hashes = {},
                oPath = '';
            var collect = function(file, enc, cb) {
                if (file.revHash) {
                    var filename = nodepath.basename(file.revOrigPath);
                    hashes[filename] = filename + '?v=' + file.revHash;
                    oPath = file.base;
                }
                return cb();
            }

            var emit = function(cb) {
                if (oPath) {
                    var file = new gutil.File({
                        base: oPath,
                        cwd: process.cwd(),
                        path: nodepath.resolve(oPath, 'rev-manifest.json')
                    });
                    file.contents = new Buffer(JSON.stringify(hashes, null, 4));
                    this.push(file);
                }
                return cb();
            }
            return through.obj(collect, emit);
        }())
        .pipe(gulp.dest(path.app))
        .pipe(rev.manifest())
})

gulp.task('revCollector', ['rev'], function() {
    var manifest = gulp.src([nodepath.resolve(path.app, 'rev-manifest.json')]);
    var cssPath = nodepath.resolve(path.lessPath, 'd_theme_main.css');

    return gulp.src([path.basePath + '/protected/views/layouts/lib.tpl'])
        .pipe(inject(gulp.src(cssPath, {read: false}), {
            transform: function (filepath, file, i, length) {
                var fpath = ['/assets', filepath.split('assets')[1]].join('');
                return '<link rel="stylesheet" href="'+fpath+'">';
            }
        }))
        .pipe(revReplace({ 
            manifest: manifest,
            replaceInExtensions: ['.tpl']
        }))
        .pipe(gulp.dest(path.basePath + '/protected/views/layouts/'));
})

gulp.task('watch', function() {
    gulp.watch(path.lessPath + '*.less', ['revCollector']);
})


gulp.task('default', ['revCollector', 'watch']); //定义默认任务


//gulp.task(name[, deps], fn) 定义任务  name:任务名称 deps:依赖任务名称 fn:回调函数
//gulp.src(globs[, options]) 执行任务处理的文件  globs:处理的文件路径(字符串或者字符串数组) 
//gulp.dest(path[, options]) 处理完后文件生成路径

至此,一个更为完善的前端构建方案就诞生了。

 

posted @ 2017-04-04 15:47  强强、  阅读(3626)  评论(0编辑  收藏  举报