首页 > 代码库 > 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 被清除了!
解决办法:这个问题其实不用管,如果是没修改的被重新压缩了,不提交就可以了,提交了也没太大关系!
目前只能先忍着,如果有好的方法,欢迎指导。