您的位置:首页 > 产品设计 > UI/UE

require.js+backbone 使用r.js 在本地与生产环境 一键压缩的实现方式

2014-07-11 17:29 507 查看
requie.js 和backbone.js 这里就不说了,可以去看官方文档,都很详细!

但是使用require.js 默认带的压缩方式感觉不是很方便,所以本文主要讲 利用r.js压缩,来实现本地不压缩,生产环境压缩

r.js 是运行在node上的,默认使用UglifyJS。UglifyJS真的很好用,那为什么说默认的方式 不是很方便呢?

r.js 单独压缩一个文件也很好用的,但在实际项目中,总不能一个一个压吧!因此r.js提供了一种多文件的压缩方式

,使用一个叫bulid.js 的配置文件来配置模块,这样可以压缩多个模块。

但是,问题有几个:

1.要维护一个配置文件,模块越多,越不好维护。当然也可写个自动生成配置文件的脚本,个人感觉也不好用,因为第二个问题。

2.压缩后,会生成整个文件夹压缩后的完整副本,这样你就要提交两个js的文件夹到你的代码库里了。而且压缩后的文件夹里存在代码的冗余,因为所有的代码都会根据层层依赖关系被压缩的一个入口文件中,加载是只需加载入口文件就行了,但其他的文件也被压缩了,被复制到了新的文件夹内。

3.压缩时每次都全部压缩,效率很低!可能也能实现部分压缩,不过我没找到合适的方法。

4.本地使用未压缩的,压缩后提交,不能保证100%的压缩正确(配置里的依赖万一出错了),这样需要提交到测试环境才能发现。

问题说完了,有能解决的欢迎回复。下面说说我的实现方式。window开发环境&&node环境&&apache需要开启rewrite和eTag。

首先大概说下原理:

总共分两步,1是合并;2是压缩;

1.利用apche的.htaccess文件将请求的js文件(/js/dist/home.js) 重定向到一个php脚本里(本地环境里),并将/js/src/main/home.js作为参数传入。在这个脚本里来判断是否需要合并(这里说的只是合并,不是压缩),如果需要,利用r.js在/js/dist目录下合并成一个home.dev.js ,然后根据所依赖的文件修改时间来生成eTag的token,设置eTag,并将内容输出。如果不需要合并(通过eTag来判断),则直接返回304,去读之前的缓存。.这样本地加载的即为合并但未压缩的js文件,便于调试.

2.这个home.dev.js 并不需要提交的你的代码库,只保留在本地就行。这是还需要另一个php脚本,通过一个批处理来调用。脚本的作用是把home.dev.js再相同的目录压缩出一个home.js,这里也需要根居home.js是否存在和home.js 与home.dev.js 的文件修改时间做比较,来判断是否压缩。压缩好的home.js
即使要提交到代码库里的(从home.dev.js 到home.js 相当于是单个文件压缩,没有依赖关系,这样出错的概率就很小很小了.很好的解决了上述提出的问题)。

下面来说下具体的实现方式:

1.目录结构是这样的:







简单解释下

左边是js的目录结构图,lib是放核心框架的,如jquery等;plugin是放插件的;common是放入自己写的公用模块。src是应用的源代码,main是入口文件。dist是放置合并过和压缩过的,文件名和main里的相同。其他的就是backbone的目录结构了,可能还有些不全面,这里先不考虑。

右边是压缩脚本的目录,r.js 即require.js 提供的压缩js脚本,compile.bat 调用压缩脚本的批处理文件,combine_js.php 合并代码的PHP脚本,conplile.php 是压缩代码的脚本,notmodified.php 是判断是否需要合并,原理是利用所依赖的文件修改时间生成Etag的token,combine_css.php 是合并css的脚本,讲完js压缩后再说。

2.了解了目录的结构和基本原理,剩下的就是贴代码了。

html的的引入:

<script data-main="/js/dist/home" src="/js/lib/require-2.1.14.min.js"></script>
程序的入口是js/dist/home,这个文件是由js/scr/mian/home.js经过合并和压缩的到的,即在生产环境使用的。

在本地环境的话,就要靠.htaccess这个文件了

.htaccess

# 将js/dist/home.js 重定向到combine_js.php这个脚本里,并将dist替换为main,
# 即js/src/main/home.js 这个真正的入口文件路径作为参数传过去
rewriteCond %{REQUEST_FILENAME} ^(.*)dist(.*\.js)$
rewriteCond %1src/main%2 -f
RewriteRule ^(.*)dist(.*\.js)$ build/combine_js.php?f=$1src/main$2 [B,L]
             
#css的合并,和上面的一样,只不过对应处理的脚本不同
rewriteCond %{REQUEST_FILENAME} ^(.*)dist(.*\.css)$
rewriteCond %1config%2 -f
RewriteRule ^(.*)dist(.*\.css)$ build/combine_css.php?f=$1config$2 [L]

#如果xxx.(js||css)有对应的xxx.dev.(js||css) 则将重定向到.dev.js或.dev.css
#这个是给已经合并和已经压缩好的js或css 来用的
#比如lib/jquery.js,你在本地可以下载对应的debug版,修改文件名为jquery.dev.js ,这样本地也可以调试jquery了
rewriteCond %{REQUEST_FILENAME} ^(.*)\.(js|css)$
rewriteCond %1.dev.%2 -f
RewriteRule ^(.*)\.(js|css)$ $1.dev.$2 [L]


下面就是关键combine_js.php了

<?php
include 'notmodified.php';
define('BASE', dirname(__FILE__));
$file_main = BASE.'/../'.$_GET['f'];
$file_config = BASE.'/../js/config.js';
$comment_reg_exp = '/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/m';
$js_require_reg_exp = '/[^.]\s*require\s*\(\s*[\"\']([^\'\"\s]+)[\"\']\s*\)/';
$exclude_reg_exp = '/\s*exclude\s*:\s*(\[.*?\])/s';
$alias_reg_exp = '/\s*paths\s*:\s*(\{.*?\})/s';

$data_dep_file = array($file_main, $file_config);  //所依赖文件的数组,包括自身和config文件
$data_ex_dep_file = array();                       //不需压缩的依赖文件数组
$data_alias_file = '';                             //有别名的的文件字符串
if(file_exists($file_config)){
    $text_config = file_get_contents($file_config);
    $text_config = preg_replace($comment_reg_exp, '', $text_config);  //去掉注释
    preg_match_all($exclude_reg_exp, $text_config, $matches);
    if(isset($matches[1][0])){  //取出不需压缩的依赖文件配置
        $data_ex_dep_file = json_decode(str_replace("\n",'',str_replace(" ", "", str_replace("'", '"', $matches[1][0]))), true);
    }
    preg_match_all($alias_reg_exp, $text_config, $matches);
    if(isset($matches[1][0])){  //取出有别名的真正文件配置
        $data_alias_file = str_replace("\n",'',str_replace(" ", "", str_replace("'", '"', $matches[1][0])));
    }
   
}
function get_true_path($alias){  //取出有别名的的真正文件名
    global $data_alias_file;
    $alias_escape = str_replace('.','\.', str_replace('/','\/',$alias));
    $regExp ='/'.$alias_escape.':"(.*?)"/';
    preg_match_all($regExp, $data_alias_file, $matches);
    if(isset($matches[1][0])){
        return $matches[1][0];
    }
    return $alias;
}

function get_dep_files($file_name){
    global $js_require_reg_exp, $data_dep_file, $data_ex_dep_file, $comment_reg_exp;
    if(file_exists($file_name)){
        $text_main = file_get_contents($file_name);
        $text_main = preg_replace($comment_reg_exp, '', $text_main);
        preg_match_all($js_require_reg_exp, $text_main, $matches);
        if(isset($matches[1]) && is_array($matches[1]) && count($matches[1]) > 0){  //取出依赖文件
            foreach($matches[1] as $v){
                $v = trim($v);
                $v_true = get_true_path($v);
                $v_path = BASE.'/../js/'.$v_true.(strrchr($v, '.') == '.js' ? '' : '.js');  //所依赖的文件的完整路径
                if(!in_array($v, $data_ex_dep_file) && !in_array($v_path, $data_dep_file)){
                    $data_dep_file[] = $v_path;
                    get_dep_files($v_path); //递归取出依赖文件
                }
            }
        }
    }
}
get_dep_files($file_main);
$ext = strrchr($file_main, '.');
$file_name = basename($file_main, $ext);
$file_source = 'src/main/'.$file_name;
$file_output = BASE.'/../js/dist/'.$file_name.'.dev.js';
if(file_exists($file_output) && notmodified($data_dep_file)){  //根据所依赖文件修改时间生成eTag,来判断是否需要压缩
    exit;
}

$output = array();
$error = array();
exec('node r.js -o mainConfigFile='.BASE.'/../js/config.js baseUrl='.BASE.'/../js/ name='.$file_source.' out='.$file_output.' optimize=none', $output);  //使用node 压缩,生成dev文件,保存输出结果。

foreach($output as $v){
    if(preg_match('/Error/', $v)){
        $error[]=json_encode($v); //保存错误信息
    }
}
if(empty($error)){
    header('Content-Type: application/javascript');
    echo file_get_contents($file_output);
    exit;
}
foreach($error as $e){
   echo "console.error($e);"; //输出错误信息
}
exit;


下面是notmodified.php

function notmodified($files = array()){
	$s = '';
	
	if(is_string($files)){
		$files = array($files);
        }
	
	if($files){
		foreach($files as $f){
			$s .= filemtime($f); //拼接所依赖文件修改时间,用来生成eTag的token
                }
	}
	
	$etag = sprintf('%08x', crc32($s));
	header("ETag: \"$etag\"");//输出eTag 
	if(isset($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $etag)){
		header('HTTP/1.1 304 Not Modified');// 如果没有修改过,则返回304,去读缓存
		return true;
	}
	return false;
}


下面看下入口文件main/spc.js

require(["config.js"], function(config){<span style="font-family: Arial, Helvetica, sans-serif;">// 加载公用的配置文件后,再开始定义模块。</span>
    require(['src/main/home']);
});
define(function(require){
    "use strict";
    var home = require("src/controllers/home");
    new home({name:'homeController'}).init();
});


下面看公用配置文件config.js到此已经完成大部分工作了,还剩下最后上生产的压缩工作

/*
 * 默认的config.
 */
requirejs.config({
    baseUrl: typeof(javascriptServer) === 'undefined' ? '' : javascriptServer  + 'js/',
    paths: {
        jquery: 'lib/jquery-1.11.1.min',
        underscore: 'lib/underscore-1.6.min',
        backbone: 'lib/backbone-1.1.2.min',
        cookie: 'plugin/jquery.cookie'
    },
    useStrict: true,
    exclude: ['jquery', 'underscore', 'backbone', 'cookie'], //不需要合并的文件,使用r.js 进行合并或压缩时,读此配置文件:node r.js -o mainConfigFile='.BASE.'/../js/config.js ......
    shim: {
        /*目前backbone和underscore都已支持amd!如果是不支持的版本,则需要下面的配置。
        backbone: {
            deps: ['jquery', 'underscore'],
            exports: 'Backbone'
        },
        underscore: {
            exports: '_'
        }
        */    
    }
});


先看批处理 compile.bat,很简单

@echo off
php compile.php "../js/dist"
php compile.php "../css/dist"
pause

再看compile.php,也很简单

define('BASE', dirname(__FILE__));

if($argc == 1){
	compile(BASE);
}else{
	compile(BASE.'/'.$argv[1]);
}
function compile($dir){
	$h = opendir($dir);
	while(($f = readdir($h)) !== false){
		if($f == '.' || $f == '..'){
			continue;
        }	
		$f = $dir.'/'.$f;

		if(is_dir($f)){
			compile($f);
		}else if(substr($f, -7) == '.dev.js'){ //js
        	$ext = strrchr($f, '.');
			$out = substr($f, 0, -7).'.js';
			if(!file_exists($out) || filemtime($f) > filemtime($out)){
                system('node r.js -o mainConfigFile='.BASE.'/../js/config.js baseUrl='.BASE.'/../js/ name=app/product/'.basename($f, $ext).' out='.$out);
                system('jsl -process ../js/app/product/'.basename($f, $ext).'.js'); //jslint 检查语法  
            }
		}else if(substr($f, -8) == '.dev.css'){  //css
			$out = substr($f, 0, -8).'.css';
			if(!file_exists($out) || filemtime($f) > filemtime($out)){
				system('node r.js -o cssIn='.$f.' out='.$out.' optimizeCss=standard');
            }
		}
	}
	closedir($h);
}


最后要说下,其实这个套路有两个可以容忍小bug,平时需要注意下。

1.如果(往前)修改了本地时间后,再进行压缩,可能会导致压缩失败。

解决办法:把时间调正确后,删除produt/xxx.js ,重新从版本库里更新,再进行压缩。其实注意下压缩时不要修改本地时间就可以了。

2.如果ctrl+F5 或清空缓存的话,即使文件没有修改,也会重新合并,因为eTag 被清除了!

解决办法:这个问题其实不用管,如果是没修改的被重新压缩了,不提交就可以了,提交了也没太大关系!

目前只能先忍着,如果有好的方法,欢迎指导。

等有时间,会把整套的源码整理在github上的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: