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

Posted lytwajue

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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文件将请求的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,非常easy

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

再看compile.php,也非常easy

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上的。

以上是关于require.js+backbone 使用r.js 在本地与生产环境 一键压缩的实现方式的主要内容,如果未能解决你的问题,请参考以下文章

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

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

寻找有关在Backbone.js中使用Parceljs的建议

require.js的简单使用

加载Dropzone.js与Require.js

在backbone.js项目中导入和使用javascript库