├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── console.js ├── dist ├── index.html ├── index_absolute.html ├── index_inline.html └── static │ ├── merge.css │ ├── merge.js │ ├── merge2.css │ └── merge2.js ├── index.js ├── package.json └── test ├── assets └── js │ ├── a.js │ └── b.js ├── css ├── a.css └── b.css ├── gulpfile.js ├── index.html ├── index_absolute.html └── index_inline.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /*.log 3 | .idea/ 4 | *~ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g gulp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 jason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 推荐 [node-project-template](https://github.com/dnxbf321/node-project-template) 2 | gulp-htmloptimize 已非常陈旧,npm scripts 当道,node-project-template 基于 npm scripts 提供整套的前端开发流程 3 | 4 | # gulp-htmloptimize 5 | 构建 html 的 gulp 模块 6 | 7 | ## 用法 8 | 9 | ```js 10 | var htmloptimize = require('gulp-htmloptimize'); 11 | gulp.task('htmloptimize', function() { 12 | // 自定义配置 13 | var opts = {}; 14 | return gulp.src('./**/*.html') 15 | .pipe(htmloptimize(opts)) 16 | .pipe(gulp.dest('./dist')); 17 | }); 18 | ``` 19 | 20 | ## 优化哪些方面 21 | 1. 在合适的地方加上特定格式的html注释就能编译css、 js 文件 22 | 2. 编译css文件: 23 | * 支持 @import url(file.css) 24 | * 自动加浏览器厂商前缀,解决兼容性问题,使用 autoprefixer-core 库 25 | * 压缩、优化css,使用 clean-css 库 26 | 3. 编译js文件 27 | * 压缩混淆 js 代码,使用 uglify-js 库 28 | 4. 行内样式、js优化 29 | 5. 自定义配置 30 | 31 | 32 | ## options 33 | ``` 34 | opts = mergeObj({ 35 | debug: false, 36 | root: process.cwd().replace(/\\/g, '/'), 37 | assetsDir: '', 38 | uglifyJs: { 39 | mangle: { 40 | except: ['require', 'exports', 'module'] 41 | }, 42 | code: { 43 | indent_level: 2 44 | } 45 | }, 46 | autoprefixer: { 47 | safe: true, 48 | browsers: ['> 5%', 'last 3 versions', 'Firefox ESR', 'iOS >= 6', 'Android >= 4.0', 'ExplorerMobile >= 10'] 49 | }, 50 | cleanCss: { 51 | advanced: false, 52 | keepSpecialComments: 0, 53 | processImport: false, 54 | rebase: false 55 | } 56 | }, yourOpts); 57 | ``` 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
debug 什么都不做,directly pipe
root web项目根目录,大部分情况应当与package.json同级(tips:硬盘路径,使用path.resolve(process.cwd(), '../yourproject')获得)
assetsDir 外联js、css文件存放目录,当html使用绝对路径引入js、css文件时,对应的真实文件路径是path.join(opts.root, opts.assetsDir, 'static_file_href')
uglifyJs 见 [uglifyJs的配置](https://github.com/mishoo/UglifyJS)
autoprefixer 见 [autoprefixer-core的配置](https://github.com/postcss/autoprefixer-core)
cleanCss 见 [clean-css的配置](https://github.com/jakubpawlowicz/clean-css)
84 | 85 | ## 文档 86 | [wiki](https://github.com/dnxbf321/gulp-htmloptimize/wiki) 87 | 88 | ## LICENSE 89 | 90 | [MIT License](https://github.com/dnxbf321/gulp-htmloptimize/blob/master/LICENSE) 91 | -------------------------------------------------------------------------------- /console.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var pathjoin = path.join('/a/b.js', 'c.js'); 4 | 5 | console.log(pathjoin); -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 9 | test body. 10 | 11 | 12 | -------------------------------------------------------------------------------- /dist/index_absolute.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 9 | test body. 10 | 11 | 12 | -------------------------------------------------------------------------------- /dist/index_inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 9 | test body. 10 | 11 | code between two scripts 12 | 13 | 14 | -------------------------------------------------------------------------------- /dist/static/merge.css: -------------------------------------------------------------------------------- 1 | html{padding:30px}body{height:100px;background:#000;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)} -------------------------------------------------------------------------------- /dist/static/merge.js: -------------------------------------------------------------------------------- 1 | (function(e,t){var n=/mobile/i.test(t),r={ios:/iphone|ipod|ipad/i.test(t),android:/android/i.test(t),wechat:/micromessenger/i.test(t),weibo:/weibo/i.test(t)};e.browser=r})(window,navigator.userAgent.toLowerCase()),console.log("hello world!") -------------------------------------------------------------------------------- /dist/static/merge2.css: -------------------------------------------------------------------------------- 1 | body{height:100px;background:#000;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}html{padding:30px} -------------------------------------------------------------------------------- /dist/static/merge2.js: -------------------------------------------------------------------------------- 1 | (function(e,t){var n=/mobile/i.test(t),r={ios:/iphone|ipod|ipad/i.test(t),android:/android/i.test(t),wechat:/micromessenger/i.test(t),weibo:/weibo/i.test(t)};e.browser=r})(window,navigator.userAgent.toLowerCase()),console.log("hello world!") -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(opts) { 2 | var through = require('through2'), 3 | gutil = require('gulp-util'), 4 | glob = require('glob'), 5 | CleanCSS = require('clean-css'), 6 | autoprefixer = require('autoprefixer-core'), 7 | uglifyJs = require('uglify-js'), 8 | uglify = uglifyJs.uglify, 9 | parser = uglifyJs.parser, 10 | path = require('path'), 11 | fs = require('fs'); 12 | 13 | var START_REG = //gim, 14 | END_REG = //gim, 15 | JSFILE_REG = /<\/script>/gi, 16 | CSSFILE_REG = //gi, 17 | IMPORT_REG = /@import\s*url\((?:'|")?([^\s]+)(?:'|")?\)[\s\;]?/gi, 18 | INLINE_START_REG = /(\<(?:script|style)\>)/gim, 19 | INLINE_END_REG = /(\<\/(?:script|style)\>)/gim; 20 | 21 | opts = mergeObj({ 22 | root: process.cwd().replace(/\\/g, '/'), 23 | assetsDir: '', 24 | uglifyJs: { 25 | mangle: { 26 | except: ['require', 'exports', 'module'] 27 | }, 28 | code: { 29 | indent_level: 2 30 | } 31 | }, 32 | autoprefixer: { 33 | safe: true, 34 | browsers: ['> 5%', 'last 3 versions', 'Firefox ESR', 'iOS >= 6', 'Android >= 4.0', 'ExplorerMobile >= 10'] 35 | }, 36 | cleanCss: { 37 | advanced: false, 38 | keepSpecialComments: 0, 39 | processImport: false, 40 | rebase: false 41 | } 42 | }, opts); 43 | 44 | function mergeObj(source1, source2) { 45 | var target = {}; 46 | for (var key1 in source1) { 47 | target[key1] = source1[key1]; 48 | } 49 | for (var key2 in source2) { 50 | target[key2] = source2[key2]; 51 | } 52 | return target; 53 | } 54 | 55 | function resolveSrc(p, relativeDir) { 56 | switch (true) { 57 | case !path.isAbsolute(p): 58 | p = path.join(relativeDir, p); 59 | break; 60 | case p[0] === '/': 61 | p = path.join(opts.root, opts.assetsDir, p); 62 | break; 63 | } 64 | return path.resolve(p).replace(/\\/g, '/'); 65 | } 66 | 67 | function relativeDest(p, base) { 68 | return p.replace(base, '').replace(opts.root, '').replace(/^\//i, ''); 69 | } 70 | 71 | function getNewImports(filepath, content, included) { 72 | var newPaths = []; 73 | content.replace(IMPORT_REG, function(g, p) { 74 | p = resolveSrc(p, path.dirname(filepath)); 75 | if (included.indexOf(p) === -1) { 76 | newPaths.push(p); 77 | } 78 | }); 79 | return newPaths; 80 | } 81 | 82 | function concatSource(paths) { 83 | var source = ''; 84 | for (var i = 0; i < paths.length; i) { 85 | var p = paths[i]; 86 | 87 | if (glob.sync(p)[0] === undefined) { 88 | throw new gutil.PluginError('gulp-htmlbuild', 'file ' + p + ' not found\n'); 89 | } 90 | 91 | var _source = fs.readFileSync(p).toString(); 92 | 93 | // 支持 @import url(import.css) 94 | if (path.extname(p) === '.css') { 95 | var newPaths = getNewImports(p, _source, paths); 96 | 97 | // 推入paths数组,位置紧邻 i 前面 98 | if (newPaths.length) { 99 | Array.prototype.splice.apply(paths, [i, 0].concat(newPaths)); 100 | continue; 101 | } 102 | _source = _source.replace(IMPORT_REG, ''); 103 | } 104 | source += _source; 105 | i += 1; 106 | } 107 | return source; 108 | } 109 | 110 | /* 111 | * do autoprefixer, clean-css 112 | */ 113 | function optimizeCss(source) { 114 | var ret = ''; 115 | // autoprefixer 116 | ret = autoprefixer.process(source, opts.autoprefixer).css; 117 | // minify 118 | ret = new CleanCSS(opts.cleanCss).minify(ret).styles; 119 | 120 | return ret; 121 | } 122 | 123 | /* 124 | * do uglify 125 | */ 126 | function optimizeJs(source) { 127 | var ret = ''; 128 | ret = parser.parse(source); 129 | ret = uglify.ast_mangle(ret, opts.uglifyJs.mangle); 130 | ret = uglify.ast_lift_variables(ret); 131 | ret = uglify.ast_squeeze(ret); 132 | ret = uglify.gen_code(ret, opts.uglifyJs.code); 133 | return ret; 134 | } 135 | 136 | /* 137 | * 行内css、js优化 138 | * 注:不支持内嵌样式, 只支持 139 | */ 140 | function inlineOptimze(source) { 141 | var blocks = []; 142 | var sections = source.split(INLINE_START_REG); 143 | sections.forEach(function(s) { 144 | if (s.match(INLINE_END_REG) && !s.match(JSFILE_REG)) { 145 | // s[0] css rules or js code 146 | // s[1] or 147 | // s[2] other html code after 148 | s = s.split(INLINE_END_REG); 149 | switch (s[1]) { 150 | case '': 151 | s[0] = optimizeCss(s[0]); 152 | break; 153 | case '': 154 | s[0] = optimizeJs(s[0]); 155 | break; 156 | } 157 | blocks.push(s.join('')); 158 | } else { 159 | blocks.push(s); 160 | } 161 | }); 162 | return blocks.join(''); 163 | } 164 | 165 | function Builder(file, push) { 166 | this.push = push; 167 | 168 | this.filebase = file.base.replace(/\\/g, '/'); 169 | this.filepath = file.path.replace(/\\/g, '/'); 170 | this.filedir = path.dirname(this.filepath).replace(/\\/g, '/'); 171 | 172 | var result = this.analysis(file); 173 | this.pushHtmlFile(result.blocks).pushStaticFile(result.files); 174 | } 175 | 176 | Builder.prototype = { 177 | /* 178 | * 分析 html 代码 179 | * return {blocks: ['html代码块'], files: [{type: 'css', paths: ['static file path']}]} 180 | */ 181 | analysis: function(file) { 182 | var sections = file.contents.toString().split(END_REG); 183 | 184 | var blocks = [], 185 | files = []; 186 | 187 | var self = this; 188 | 189 | sections.forEach(function(s) { 190 | if (s.match(START_REG)) { 191 | // s[0] html code 192 | // s[1] string 'css' or 'js' 193 | // s[2] string dest url 194 | // s[3] tags of link or script 195 | s = s.split(START_REG); 196 | blocks.push(s[0]); 197 | switch (s[1]) { 198 | case 'css': 199 | blocks.push(''); 200 | break; 201 | case 'js': 202 | blocks.push(''); 203 | break; 204 | default: 205 | throw new gutil.PluginError('gulp-htmlbuild', 'only build:css build:js accept\n'); 206 | } 207 | var paths = self.matchContentPaths(s[1] === 'css' ? CSSFILE_REG : JSFILE_REG, s[3]); 208 | var source = concatSource(paths); 209 | files.push({ 210 | type: s[1], 211 | dest: resolveSrc(s[2], self.filedir), 212 | source: source 213 | }); 214 | } else { 215 | blocks.push(s); 216 | } 217 | }); 218 | 219 | return { 220 | blocks: blocks, 221 | files: files 222 | } 223 | }, 224 | matchContentPaths: function(reg, content) { 225 | var self = this; 226 | var paths = []; 227 | content.replace(reg, function(g, p) { 228 | p = resolveSrc(p, self.filedir); 229 | paths.push(p); 230 | }); 231 | return paths; 232 | }, 233 | pushHtmlFile: function(blocks) { 234 | var dest = relativeDest(this.filepath, this.filebase); 235 | var optimized = inlineOptimze(blocks.join('')); 236 | var htmlFile = new gutil.File({ 237 | path: dest, 238 | contents: new Buffer(optimized) 239 | }); 240 | this.push(htmlFile); 241 | return this; 242 | }, 243 | pushStaticFile: function(groups) { 244 | var self = this; 245 | groups.forEach(function(group) { 246 | var dest = relativeDest(group.dest, self.filebase); 247 | 248 | var optimizeFun = group.type === 'css' ? optimizeCss : optimizeJs; 249 | var optimized = optimizeFun(group.source); 250 | 251 | self.push(new gutil.File({ 252 | path: dest, 253 | contents: new Buffer(optimized) 254 | })); 255 | }); 256 | return this; 257 | } 258 | }; 259 | 260 | return through.obj(function(file, enc, cb) { 261 | switch (true) { 262 | case opts.debug: 263 | this.push(file); 264 | break; 265 | case file.isStream(): 266 | this.emit('error', new gutil.PluginError('gulp-htmlbuild', 'Streams are not supported!\n')); 267 | break; 268 | case file.isNull(): 269 | cb(null, file); 270 | return; 271 | break; 272 | default: 273 | try { 274 | new Builder(file, this.push.bind(this)); 275 | } catch (e) { 276 | this.emit('error', e); 277 | } 278 | } 279 | cb(); 280 | }); 281 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-htmloptimize", 3 | "description": "gulp html files", 4 | "version": "0.0.3", 5 | "homepage": "https://github.com/dnxbf321/gulp-htmloptimize", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index.js", 9 | "test": "node test/gulpfile.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/dnxbf321/gulp-htmloptimize.git" 14 | }, 15 | "keywords": [ 16 | "html", 17 | "gulp", 18 | "optimize" 19 | ], 20 | "author": { 21 | "name": "dnxbf321", 22 | "email": "dnxbf321@gamil.com", 23 | "url": "https://github.com/dnxbf321" 24 | }, 25 | "license": "MIT", 26 | "dependencies": { 27 | "through2": "0.6.3", 28 | "glob": "5.0.3", 29 | "gulp": "3.8.11", 30 | "gulp-util": "3.0.4", 31 | "clean-css": "3.1.*", 32 | "autoprefixer-core": "5.1.*", 33 | "uglify-js": "1.3.5" 34 | } 35 | } -------------------------------------------------------------------------------- /test/assets/js/a.js: -------------------------------------------------------------------------------- 1 | (function(global, userAgent) { 2 | var isMobile = /mobile/i.test(userAgent); 3 | var browser = { 4 | ios: /iphone|ipod|ipad/i.test(userAgent), 5 | android: /android/i.test(userAgent), 6 | wechat: /micromessenger/i.test(userAgent), 7 | weibo: /weibo/i.test(userAgent) 8 | }; 9 | global.browser = browser; 10 | }(window, navigator.userAgent.toLowerCase())); -------------------------------------------------------------------------------- /test/assets/js/b.js: -------------------------------------------------------------------------------- 1 | console.log('hello world!'); -------------------------------------------------------------------------------- /test/css/a.css: -------------------------------------------------------------------------------- 1 | @import url(b.css); 2 | 3 | body { 4 | height: 100px; 5 | 6 | background: #000; 7 | 8 | transform: scale(0.5); 9 | } -------------------------------------------------------------------------------- /test/css/b.css: -------------------------------------------------------------------------------- 1 | html { 2 | padding: 30px; 3 | } -------------------------------------------------------------------------------- /test/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | htmlbuild = require('../index.js'), 3 | path = require('path'); 4 | gulp.src('test/**/*.html') 5 | .pipe(htmlbuild({})) 6 | .pipe(gulp.dest('./dist')); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | test 9 | 10 | 11 | test body. 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/index_absolute.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | test 10 | 11 | 12 | test body. 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/index_inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | test 14 | 15 | 16 | test body. 17 | 22 | code between two scripts 23 | 28 | 29 | --------------------------------------------------------------------------------