首页 > 代码库 > require.js+backbone 使用r.js 来压缩,本地不压缩,生产环境压缩 的实现方式

require.js+backbone 使用r.js 来压缩,本地不压缩,生产环境压缩 的实现方式

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


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


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

1.目录结构是这样的:


简单解释下 

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

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


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

html的的引入:

<script data-main="<?=$jsServer?>js/app/product/spc" src=http://www.mamicode.com/"<?=$jsServer?>js/lib/require-2.1.14.min.js?v=<?=$ver?>"></script>
程序的入口是js/app/product/spc,这个文件是由js/app/mian/spc.js经过合并和压缩的到的,即在生产环境使用的。

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

.htaccess

# 将js/app/product/spc.js 重定向到combine.php这个脚本里,并将product替换为main,
# 即js/app/mian/spc.js 这个真正的入口文件路径作为参数传过去
rewriteCond %{REQUEST_FILENAME} ^(.*)product(.*\.js)$
rewriteCond %1main%2 -f
RewriteRule ^(.*)product(.*\.js)$ autocompile/combine.php?f=$1main$2 [B,L]
             
#css的合并,和上面的一样,只不过对应处理的脚本不同
rewriteCond %{REQUEST_FILENAME} ^(.*)product(.*\.css)$
rewriteCond %1main%2 -f
RewriteRule ^(.*)product(.*\.css)$ autocompile/combine_css.php?f=$1main$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.php了

include 'notmodified.php';
define('BASE', dirname(__FILE__));
$file_main = BASE.'/../'.$_GET['f'];
$file_config = BASE.'/../js/config.js'; //require.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 = 'app/main/'.$file_name;
$file_output = BASE.'/../js/app/product/'.$file_name.'.dev.js';
if(file_exists($file_output) && notmodified($data_dep_file)){  //判断是否需要压缩,如果没有修改过,会返回304,去读缓存
    exit;
}
exec('node r.js -o mainConfigFile='.BASE.'/../js/config.js baseUrl='.BASE.'/../js/ name='.$file_source.' out='.$file_output.' optimize=none');  //使用node 压缩,生成dev文件,optimize=none 是只合并的意思

header('Content-Type: application/javascript');
echo file_get_contents($file_output);
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([javascriptServer+"js/config.js"], function(){// 加载公用的配置文件后,再开始定义模块。javascriptServer是全局变量,放置js的域名,如果 是本域,则不用写
    require(['app/main/spc']);
});
define(function(require){
    "use strict";
    var app = require('app/views/app');
	new app();
});


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

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'
    },
    useStrict: true,
    exclude: ['jquery', 'underscore', 'backbone'],//不需要合并的文件,使用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/app/product"
php compile.php "../css/product"
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);
                     }
		}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 被清除了!
  解决办法:这个问题其实不用管,如果是没修改的被重新压缩了,不提交就可以了,提交了也没太大关系!

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