├── .gitignore ├── default_index.ejs ├── lib ├── errors.js ├── loader.js ├── readDir.js ├── readDirAsync.js ├── chunksorter.js └── compiler.js ├── LICENSE ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## node_modules 2 | node_modules/* 3 | 4 | ## test 5 | test/* 6 | 7 | ## webpackconfig 8 | webpack.config.js -------------------------------------------------------------------------------- /default_index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | default 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var PrettyError = require('pretty-error'); 3 | var prettyError = new PrettyError(); 4 | prettyError.withoutColors(); 5 | prettyError.skipPackage(['html-plugin-evaluation']); 6 | prettyError.skipNodeFiles(); 7 | prettyError.skip(function (traceLine) { 8 | return traceLine.path === 'html-plugin-evaluation'; 9 | }); 10 | 11 | module.exports = function (err, context) { 12 | return { 13 | toHtml: function () { 14 | return 'Html Webpack Plugin:\n
\n' + this.toString() + '
'; 15 | }, 16 | toJsonHtml: function () { 17 | return JSON.stringify(this.toHtml()); 18 | }, 19 | toString: function () { 20 | return prettyError.render(err).replace(/webpack:\/\/\/\./g, context); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 小虫巨蟹 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muti-html-webpack-plugin", 3 | "version": "1.0.5", 4 | "description": "a webpack plugin to due html", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zengwenfu/muti-html-webpack-plugin.git" 12 | }, 13 | "keywords": [ 14 | "webpack", 15 | "plugin", 16 | "muti", 17 | "html" 18 | ], 19 | "dependencies": { 20 | "async": "^2.0.1", 21 | "bluebird": "^3.4.1", 22 | "html-minifier": "^2.1.6", 23 | "loader-utils": "^0.2.15", 24 | "lodash": "^4.13.1", 25 | "pretty-error": "^2.0.0", 26 | "toposort": "^1.0.0" 27 | }, 28 | "author": "zengwenfu", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/zengwenfu/muti-html-webpack-plugin/issues" 32 | }, 33 | "homepage": "https://github.com/zengwenfu/muti-html-webpack-plugin#readme", 34 | "devDependencies": { 35 | "css-loader": "^0.25.0", 36 | "extract-text-webpack-plugin": "^1.0.1", 37 | "html-loader": "^0.4.4", 38 | "style-loader": "^0.13.1", 39 | "url-loader": "^0.5.7", 40 | "webpack": "^1.13.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | /* This loader renders the template with underscore if no other loader was found */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | var loaderUtils = require('loader-utils'); 6 | 7 | module.exports = function (source) { 8 | if (this.cacheable) { 9 | this.cacheable(); 10 | } 11 | var allLoadersButThisOne = this.loaders.filter(function (loader) { 12 | // Loader API changed from `loader.module` to `loader.normal` in Webpack 2. 13 | return (loader.module || loader.normal) !== module.exports; 14 | }); 15 | // This loader shouldn't kick in if there is any other loader 16 | if (allLoadersButThisOne.length > 0) { 17 | return source; 18 | } 19 | // Skip .js files 20 | if (/\.js$/.test(this.request)) { 21 | return source; 22 | } 23 | 24 | // The following part renders the tempalte with lodash as aminimalistic loader 25 | // 26 | // Get templating options 27 | var options = loaderUtils.parseQuery(this.query); 28 | // Webpack 2 does not allow with() statements, which lodash templates use to unwrap 29 | // the parameters passed to the compiled template inside the scope. We therefore 30 | // need to unwrap them ourselves here. This is essentially what lodash does internally 31 | // To tell lodash it should not use with we set a variable 32 | var template = _.template(source, _.defaults(options, { variable: 'data' })); 33 | // All templateVariables which should be available 34 | // @see HtmlWebpackPlugin.prototype.executeTemplate 35 | var templateVariables = [ 36 | 'compilation', 37 | 'webpack', 38 | 'webpackConfig', 39 | 'htmlWebpackPlugin' 40 | ]; 41 | return 'var _ = require(' + loaderUtils.stringifyRequest(this, require.resolve('lodash')) + ');' + 42 | 'module.exports = function (templateParams) {' + 43 | // Declare the template variables in the outer scope of the 44 | // lodash template to unwrap them 45 | templateVariables.map(function (variableName) { 46 | return 'var ' + variableName + ' = templateParams.' + variableName; 47 | }).join(';') + ';' + 48 | // Execute the lodash template 49 | 'return (' + template.source + ')();' + 50 | '}'; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/readDir.js: -------------------------------------------------------------------------------- 1 | /** 2 | * create by zengwenfu 3 | */ 4 | 'use strict'; 5 | 6 | var fs = require('fs'); 7 | 8 | module.exports = function(path, suffix, outputPath) { 9 | return new Promise(function(resolve, reject) { 10 | var dirs = []; 11 | var over_dir = []; 12 | var over = {}; 13 | dirs.push(path) 14 | 15 | function forDir(dirs) { 16 | function forFiles(files, file_path) { 17 | var fixdir = []; 18 | files.forEach(function(e, i) { 19 | var e = file_path + '/' + e; 20 | fs.stat(e, function(err, stat) { 21 | if (err) { 22 | console.log(err); 23 | reject(err); 24 | } 25 | if (stat.isDirectory()) { 26 | fixdir.push(e) 27 | over_dir.push(e) 28 | } else { 29 | if(!suffix || e.indexOf(suffix) >=0 ) { 30 | var key = e.substring(path.length + 1); 31 | if(!suffix && key.indexOf('.html')>=0) {//默认过滤掉.html 32 | key = key.substring(0, key.indexOf('.html')); 33 | } else if(!!suffix){ 34 | key = key.substring(0, key.indexOf(suffix)); 35 | } 36 | if(!!outputPath) { 37 | key = outputPath + '/' + key; 38 | } 39 | over[key] = e; 40 | } 41 | } 42 | if (i == files.length - 1) { 43 | if (file_path == over_dir[over_dir.length - 1]) { 44 | resolve(over); 45 | } 46 | if (fixdir.length > 0) { 47 | forDir(fixdir) 48 | } 49 | } 50 | }) 51 | }) 52 | } 53 | 54 | dirs.forEach(function(e, i) { 55 | fs.readdir(e, function(err, files) { 56 | if(err) { 57 | console.log(err); 58 | reject(err); 59 | } 60 | forFiles(files, e) 61 | }) 62 | }); 63 | } 64 | 65 | forDir(dirs); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /lib/readDirAsync.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var async = require('async'); 4 | 5 | 6 | //分文件夹输出 7 | function getFileList(dirname, suffix, outputPath, ignore, callback, error) { 8 | var fileJson = {}; 9 | if(ignore && typeof ignore === 'object') { 10 | ignore = ignore.join(','); 11 | } 12 | 13 | var a = (function listFiles(rootPath, fn, error) { 14 | var files; 15 | try { 16 | files = fs.readdirSync(rootPath); 17 | } catch(e) { 18 | console.log(e); 19 | error(e); 20 | return ; 21 | } 22 | async.eachSeries(files, function(item, next) { 23 | var filePath = path.normalize(rootPath + '/' + item); 24 | var info = fs.statSync(filePath); 25 | if (info.isDirectory()) { 26 | listFiles(filePath, function() { 27 | next(); 28 | }); 29 | } else { 30 | if (!suffix || filePath.indexOf(suffix) >= 0) { 31 | var key = filePath.substring(dirname.length + 1); 32 | if(!ignore || ignore.indexOf(key) < 0) { 33 | if (!suffix && key.indexOf('.html') >= 0) { //默认过滤掉.html 34 | key = key.substring(0, key.indexOf('.html')); 35 | } else if (!!suffix) { 36 | key = key.substring(0, key.indexOf(suffix)); 37 | } 38 | if (!!outputPath) { 39 | key = outputPath + '/' + key; 40 | } 41 | fileJson[key] = filePath; 42 | } 43 | } 44 | 45 | next(); 46 | } 47 | }, function(err) { 48 | if (!err) { 49 | fn && fn(); 50 | } else { 51 | console.error("遍历模板路径失败:" + err); 52 | error && error(err); 53 | } 54 | }); 55 | })(dirname, function() { 56 | callback(fileJson); 57 | }, function(err) { 58 | console.log(err); 59 | error(err); 60 | }); 61 | }; 62 | 63 | 64 | module.exports = function(path, suffix, outputPath, ignore) { 65 | var suffix = suffix || false; 66 | var outputPath = outputPath || false; 67 | var ignore = ignore || false; 68 | return new Promise(function(resolve, reject) { 69 | getFileList(path, suffix, outputPath, ignore, function(files) { 70 | resolve(files); 71 | }, function(err) { 72 | console.log(err); 73 | reject(err); 74 | }); 75 | }); 76 | } 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /lib/chunksorter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var toposort = require('toposort'); 4 | 5 | /* 6 | Sorts dependencies between chunks by their "parents" attribute. 7 | 8 | This function sorts chunks based on their dependencies with each other. 9 | The parent relation between chunks as generated by Webpack for each chunk 10 | is used to define a directed (and hopefully acyclic) graph, which is then 11 | topologically sorted in order to retrieve the correct order in which 12 | chunks need to be embedded into HTML. A directed edge in this graph is 13 | describing a "is parent of" relationship from a chunk to another (distinct) 14 | chunk. Thus topological sorting orders chunks from bottom-layer chunks to 15 | highest level chunks that use the lower-level chunks. 16 | 17 | @param {Array} chunks an array of chunks as generated by the html-webpack-plugin. 18 | It is assumed that each entry contains at least the properties "id" 19 | (containing the chunk id) and "parents" (array containing the ids of the 20 | parent chunks). 21 | 22 | @return {Array} A topologically sorted version of the input chunks 23 | */ 24 | module.exports.dependency = function (chunks) { 25 | if (!chunks) { 26 | return chunks; 27 | } 28 | 29 | // We build a map (chunk-id -> chunk) for faster access during graph building. 30 | var nodeMap = {}; 31 | 32 | chunks.forEach(function (chunk) { 33 | nodeMap[chunk.id] = chunk; 34 | }); 35 | 36 | // Next, we add an edge for each parent relationship into the graph 37 | var edges = []; 38 | 39 | chunks.forEach(function (chunk) { 40 | if (chunk.parents) { 41 | // Add an edge for each parent (parent -> child) 42 | chunk.parents.forEach(function (parentId) { 43 | var parentChunk = nodeMap[parentId]; 44 | // If the parent chunk does not exist (e.g. because of an excluded chunk) 45 | // we ignore that parent 46 | if (parentChunk) { 47 | edges.push([parentChunk, chunk]); 48 | } 49 | }); 50 | } 51 | }); 52 | // We now perform a topological sorting on the input chunks and built edges 53 | return toposort.array(chunks, edges); 54 | }; 55 | 56 | /** 57 | * Sorts the chunks based on the chunk id. 58 | * 59 | * @param {Array} chunks the list of chunks to sort 60 | * @return {Array} The sorted list of chunks 61 | */ 62 | module.exports.id = function (chunks) { 63 | return chunks.sort(function orderEntryLast (a, b) { 64 | if (a.entry !== b.entry) { 65 | return b.entry ? 1 : -1; 66 | } else { 67 | return b.id - a.id; 68 | } 69 | }); 70 | }; 71 | 72 | /** 73 | * Performs identity mapping (no-sort). 74 | * @param {Array} chunks the chunks to sort 75 | * @return {Array} The sorted chunks 76 | */ 77 | module.exports.none = function (chunks) { 78 | return chunks; 79 | }; 80 | 81 | /** 82 | * Defines the default sorter. 83 | */ 84 | module.exports.auto = module.exports.id; 85 | 86 | // In webpack 2 the ids have been flipped. 87 | // Therefore the id sort doesn't work the same way as it did for webpack 1 88 | // Luckily the dependency sort is working as expected 89 | if (require('webpack/package.json').version.split('.')[0] === '2') { 90 | module.exports.auto = module.exports.dependency; 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # muti-html-webpack-plugin 2 | > webpack不适合多页面应用么?那是因为你写的插件不够 3 | > 改造自html-webpack-plugin,可以解析多个html页面模板,将html也纳入到webpack的管理当中 4 | 5 | ## 安装使用 6 | 1. npm install muti-html-webpack-plugin 7 | 2. 在webpack.config.js中引用之: 8 | `var MutiHtmlWebpackPlugin = require('muti-html-webpack-plugin');` 9 | 3. 配置插件 10 | ``` 11 | plugins: [ 12 | extractCSS, 13 | new MutiHtmlWebpackPlugin({ 14 | templatePath: 'views', 15 | loader: 'html?attrs=img:src img:data-src!compile-nunjucks', 16 | templateSuffix: '.html', 17 | path: '/views', 18 | ignore: ['demo.html', 'abc/abc.html'] 19 | }) 20 | ] 21 | ``` 22 | 23 | ## 参数说明 24 | 1. templatePath: 需要解析的html模板根路径,插件将在这里寻找你的模板 25 | 2. loader: 用于解析模板的loader,配置方法同module.loaders配置,两处配置的loaders同时起作用,所以要注意避免配置重复的loader 26 | ``` 27 | module: { 28 | loaders: [{ 29 | test: /\.css$/, 30 | loader: 'style!css!change-rem', 31 | loader: extractCSS.extract('style', 'css') 32 | }, { 33 | //图片加载器,雷同file-loader,更适合图片,可以将较小的图片转成base64,减少http请求 34 | //如下配置,将小于8192byte的图片转成base64码 35 | test: /\.(png|jpg|gif)$/, 36 | loader: 'url-loader?limit=8192&context=client&name=./img/[name].[ext]?[hash:8]' 37 | // loader: [ 38 | // // url-loader更好用,小于10KB的图片会自动转成base64 dataUrl, 39 | // // 否则则调用file-loader,参数直接传入 40 | // 'url-loader?limit=8192&context=client&name=./img/[name].[ext]?[hash:8]', 41 | // 'image?{bypassOnDebug:true, progressive:true,optimizationLevel:3,pngquant:{quality:"65-80",speed:4}}' 42 | // ] 43 | }, { 44 | //html模板加载器,可以处理引用的静态资源,默认配置参数attrs=img:src,处理图片的src引用的资源 45 | //比如配置,attrs=img:src img:data-src就可以一并处理data-src引用的资源了,就像下面这样 46 | test: /\.html$/, 47 | // include: 'test/css', 48 | //这里的插件对所有.html都起作用 49 | loader: 'html?attrs=img:src img:data-src' 50 | }] 51 | }, 52 | plugins: [ 53 | extractCSS, 54 | new MutiHtmlWebpackPlugin({ 55 | templatePath: 'test/views/', 56 | //这里的loader只对插件要处理的模板起作用 57 | loader: 'html?attrs=img:src img:data-src!compile-nunjucks' 58 | templateSuffix: '.html', 59 | path: '/views', 60 | ignore: ['demo.html', 'abc/abc.html'] 61 | }) 62 | ] 63 | ``` 64 | 示例中,module.loaders对所有.html的文件都起作用 65 | 插件中的loaders对插件配置的的模板文件起作用,模板后缀是.html 66 | 所以相当于对模板文件使用了'html!compile-nunjucks!html'三个解析器,导致解析失败。 67 | 解决办法,module.loaders中的配置增加include过滤 68 | 69 | 3. templateSuffix: 模板后缀,用于过滤非模板文件,不传则解析templatePath下的所有文件 70 | 4. ignore: 过滤掉的模板文件,单个文件可以传string,多个文件传数组。场景:公共的头部模板,只是用于引用,我们是不需要解析成单独的文件的 71 | 72 | ## 使用注意 73 | html中引用webpack打包出来的js和css,可以使用##entry.[name].js/css## 74 | ``` 75 | 76 | 77 | 78 | demo 79 | 80 | 81 | 82 | 83 | HELLO WORLD !!! 84 | 85 | 86 | 87 | ``` 88 | 对应于webpack.config.js中的entry配置 89 | ``` 90 | entry: { 91 | demo: ['./test/js/demo.js'], 92 | demo1: ['./test/js/demo1.js'] 93 | } 94 | ``` 95 | 其中entry.[name].css对应于css文件(当然,不使用ExtractTextPlugin插件你是拿不到独立的css文件的), 96 | entry.[name].js对应于js的bundle 97 | 如果匹配不到,那么插件不会做任何替换,页面没有拿到正确的引用,只能只求多福了,阿弥陀佛 98 | 99 | ![](https://nodei.co/npm/muti-html-webpack-plugin.png) 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file uses webpack to compile a template with a child compiler. 3 | * 4 | * [TEMPLATE] -> [JAVASCRIPT] 5 | * 6 | */ 7 | 'use strict'; 8 | var Promise = require('bluebird'); 9 | var _ = require('lodash'); 10 | var path = require('path'); 11 | var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); 12 | var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); 13 | var LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin'); 14 | var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin'); 15 | var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); 16 | 17 | /** 18 | * Compiles the template into a nodejs factory, adds its to the compilation.assets 19 | * and returns a promise of the result asset object. 20 | * 21 | * @param template relative path to the template file 22 | * @param context path context 23 | * @param outputFilename the file name 24 | * @param compilation The webpack compilation object 25 | * 26 | * Returns an object: 27 | * { 28 | * hash: {String} - Base64 hash of the file 29 | * content: {String} - Javascript executable code of the template 30 | * } 31 | * 32 | */ 33 | module.exports.compileTemplate = function compileTemplate (template, context, outputFilename, compilation) { 34 | // The entry file is just an empty helper as the dynamic template 35 | // require is added in "loader.js" 36 | var outputOptions = { 37 | filename: '[name].html', 38 | publicPath: compilation.outputOptions.publicPath 39 | }; 40 | 41 | // Store the result of the parent compilation before we start the child compilation 42 | var assetsBeforeCompilation = _.assign({}, compilation.assets[outputOptions.filename]); 43 | // Create an additional child compiler which takes the template 44 | // and turns it into an Node.JS html factory. 45 | // This allows us to use loaders during the compilation 46 | var compilerName = getCompilerName(context, outputFilename); 47 | var childCompiler = compilation.createChildCompiler(compilerName, outputOptions); 48 | childCompiler.context = context; 49 | childCompiler.apply( 50 | new NodeTemplatePlugin(outputOptions), 51 | new NodeTargetPlugin(), 52 | new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var'), 53 | // new SingleEntryPlugin(this.context, template), 54 | new LoaderTargetPlugin('node') 55 | ); 56 | 57 | function itemToPlugin(item, name) { 58 | if(Array.isArray(item)) 59 | return new MultiEntryPlugin(context, item, name); 60 | else 61 | return new SingleEntryPlugin(context, item, name); 62 | } 63 | 64 | Object.keys(template).forEach(function(name) { 65 | childCompiler.apply(itemToPlugin(template[name], name)); 66 | }); 67 | 68 | // Fix for "Uncaught TypeError: __webpack_require__(...) is not a function" 69 | // Hot module replacement requires that every child compiler has its own 70 | // cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179 71 | childCompiler.plugin('compilation', function (compilation) { 72 | if (compilation.cache) { 73 | if (!compilation.cache[compilerName]) { 74 | compilation.cache[compilerName] = {}; 75 | } 76 | compilation.cache = compilation.cache[compilerName]; 77 | } 78 | }); 79 | 80 | // Compile and return a promise 81 | return new Promise(function (resolve, reject) { 82 | childCompiler.runAsChild(function (err, entries, childCompilation) { 83 | // Resolve / reject the promise 84 | if (childCompilation && childCompilation.errors && childCompilation.errors.length) { 85 | var errorDetails = childCompilation.errors.map(function (error) { 86 | return error.message + (error.error ? ':\n' + error.error : ''); 87 | }).join('\n'); 88 | reject(new Error('Child compilation failed:\n' + errorDetails)); 89 | } else if (err) { 90 | reject(err); 91 | } else { 92 | 93 | var items = []; 94 | for(var i=0 ; i < entries.length; i++) { 95 | var outputName = compilation.mainTemplate.applyPluginsWaterfall('asset-path', entries[i].name + '.html', { 96 | hash: childCompilation.hash, 97 | chunk: entries[i] 98 | }); 99 | 100 | compilation.assets[outputName] = assetsBeforeCompilation[outputName]; 101 | 102 | 103 | if (assetsBeforeCompilation[outputName] === undefined) { 104 | // If it wasn't there - delete it 105 | delete compilation.assets[outputName]; 106 | } 107 | items.push({ 108 | // Hash of the template entry point 109 | hash: entries[i].hash, 110 | // Output name 111 | outputName: outputName, 112 | // Compiled code 113 | content: childCompilation.assets[outputName].source() 114 | }); 115 | } 116 | resolve(items); 117 | 118 | } 119 | }); 120 | }); 121 | }; 122 | 123 | /** 124 | * Returns the child compiler name e.g. 'html-webpack-plugin for "index.html"' 125 | */ 126 | function getCompilerName (context, filename) { 127 | var absolutePath = path.resolve(context, filename); 128 | var relativePath = path.relative(context, absolutePath); 129 | return 'html-webpack-plugin for "' + (absolutePath.length < relativePath.length ? absolutePath : relativePath) + '"'; 130 | } 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var vm = require('vm'); 3 | var fs = require('fs'); 4 | var _ = require('lodash'); 5 | var Promise = require('bluebird'); 6 | var path = require('path'); 7 | var childCompiler = require('./lib/compiler.js'); 8 | var prettyError = require('./lib/errors.js'); 9 | var chunkSorter = require('./lib/chunksorter.js'); 10 | var readDir = require('./lib/readDirAsync.js'); 11 | Promise.promisifyAll(fs); 12 | 13 | function HtmlWebpackPlugin(options) { 14 | // Default options 15 | this.options = _.extend({ 16 | template: path.join(__dirname, 'default_index.ejs'), 17 | templatePath: false, 18 | loader: false, 19 | filename: 'index.html', 20 | hash: false, 21 | inject: true, 22 | compile: true, 23 | favicon: false, 24 | minify: false, 25 | cache: true, 26 | showErrors: true, 27 | chunks: 'all', 28 | excludeChunks: [], 29 | title: 'Webpack App', 30 | xhtml: false, 31 | templateSuffix: false, 32 | path: false, 33 | ignore: false 34 | }, options); 35 | } 36 | 37 | HtmlWebpackPlugin.prototype.apply = function(compiler) { 38 | var self = this; 39 | var isCompilationCached = false; 40 | var compilationPromise; 41 | 42 | 43 | self.outputPath = compiler.options.output.path; 44 | 45 | // this.options.template = this.getFullTemplatePath(this.options.template, compiler.context); 46 | 47 | // convert absolute filename into relative so that webpack can 48 | // generate it at correct location 49 | var filename = this.options.filename; 50 | if (path.resolve(filename) === path.normalize(filename)) { 51 | this.options.filename = path.relative(compiler.options.output.path, filename); 52 | } 53 | 54 | compiler.plugin('make', function(compilation, callback) { 55 | //读取dirs 56 | readDir(path.resolve(compiler.context, self.options.templatePath), self.options.templateSuffix, self.options.path, self.options.ignore).then(function(template) { 57 | 58 | template = self.getFullTemplatePath(template); 59 | compilationPromise = childCompiler.compileTemplate(template, compiler.context, self.options.filename, compilation); 60 | return compilationPromise; 61 | }).catch(function(err) { 62 | compilation.errors.push(prettyError(err, compiler.context).toString()); 63 | return { 64 | content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR', 65 | outputName: self.options.filename 66 | }; 67 | }).then(function(compilationResult) { 68 | // If the compilation change didnt change the cache is valid 69 | isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash; 70 | self.childCompilerHash = compilationResult.hash; 71 | self.childCompilationOutputName = compilationResult.outputName; 72 | callback(); 73 | return compilationResult.content; 74 | }); 75 | 76 | // Compile the template (queued) 77 | // compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation) 78 | // .catch(function(err) { 79 | // compilation.errors.push(prettyError(err, compiler.context).toString()); 80 | // return { 81 | // content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR', 82 | // outputName: self.options.filename 83 | // }; 84 | // }) 85 | // .then(function(compilationResult) { 86 | // // If the compilation change didnt change the cache is valid 87 | // isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash; 88 | // self.childCompilerHash = compilationResult.hash; 89 | // self.childCompilationOutputName = compilationResult.outputName; 90 | // callback(); 91 | // return compilationResult.content; 92 | // }); 93 | }); 94 | 95 | compiler.plugin('emit', function(compilation, callback) { 96 | var applyPluginsAsyncWaterfall = Promise.promisify(compilation.applyPluginsAsyncWaterfall, { context: compilation }); 97 | // Get all chunks 98 | var chunks = self.filterChunks(compilation.getStats().toJson(), self.options.chunks, self.options.excludeChunks); 99 | // Sort chunks 100 | chunks = self.sortChunks(chunks, self.options.chunksSortMode); 101 | // Let plugins alter the chunks and the chunk sorting 102 | chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self }); 103 | // Get assets 104 | var assets = self.htmlWebpackPluginAssets(compilation, chunks); 105 | 106 | // If this is a hot update compilation, move on! 107 | // This solves a problem where an `index.html` file is generated for hot-update js files 108 | // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle 109 | if (self.isHotUpdateCompilation(assets)) { 110 | return callback(); 111 | } 112 | 113 | // If the template and the assets did not change we don't have to emit the html 114 | var assetJson = JSON.stringify(self.getAssetFiles(assets)); 115 | if (isCompilationCached && self.options.cache && assetJson === self.assetJson) { 116 | return callback(); 117 | } else { 118 | self.assetJson = assetJson; 119 | } 120 | 121 | Promise.resolve() 122 | // Favicon 123 | .then(function() { 124 | if (self.options.favicon) { 125 | return self.addFileToAssets(self.options.favicon, compilation) 126 | .then(function(faviconBasename) { 127 | var publicPath = compilation.options.output.publicPath || ''; 128 | if (publicPath && publicPath.substr(-1) !== '/') { 129 | publicPath += '/'; 130 | } 131 | assets.favicon = publicPath + faviconBasename; 132 | }); 133 | } 134 | }) 135 | // Wait for the compilation to finish 136 | .then(function() { 137 | return compilationPromise; 138 | }) 139 | .then(function(compiledTemplate) { 140 | 141 | // Allow to use a custom function / string instead 142 | if (self.options.templateContent) { 143 | return self.options.templateContent; 144 | } 145 | // Once everything is compiled evaluate the html factory 146 | // and replace it with its content 147 | return self.evaluateCompilationResult(compilation, compiledTemplate); 148 | }) 149 | // Allow plugins to make changes to the assets before invoking the template 150 | // This only makes sense to use if `inject` is `false` 151 | .then(function(compilationResult) { 152 | return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', { 153 | assets: assets, 154 | outputName: self.childCompilationOutputName, 155 | plugin: self 156 | }) 157 | .then(function() { 158 | return compilationResult; 159 | }); 160 | }) 161 | // Execute the template 162 | .then(function(compilationResult) { 163 | // If the loader result is a function execute it to retrieve the html 164 | // otherwise use the returned html 165 | return compilationResult; 166 | }) 167 | // Allow plugins to change the html before assets are injected 168 | .then(function(html) { 169 | 170 | var pluginArgs = { html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName }; 171 | return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', pluginArgs) 172 | .then(function() { 173 | return pluginArgs.html; 174 | }); 175 | }) 176 | .then(function(html) { 177 | // html = html[0]; 178 | // Prepare script and link tags 179 | 180 | var assetTags = self.generateAssetTags(assets); 181 | var pluginArgs = { head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName }; 182 | // Allow plugins to change the assetTag definitions 183 | return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', pluginArgs) 184 | .then(function() { 185 | // Add the stylesheets, scripts and so on to the resulting html 186 | return self.postProcessHtml(html, assets, { body: pluginArgs.body, head: pluginArgs.head }); 187 | }); 188 | }) 189 | // Allow plugins to change the html after assets are injected 190 | .then(function(html) { 191 | 192 | var pluginArgs = { html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName }; 193 | return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', pluginArgs) 194 | .then(function() { 195 | return pluginArgs.html; 196 | }); 197 | }) 198 | .catch(function(err) { 199 | // In case anything went wrong the promise is resolved 200 | // with the error message and an error is logged 201 | compilation.errors.push(prettyError(err, compiler.context).toString()); 202 | // Prevent caching 203 | self.hash = null; 204 | return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR'; 205 | }) 206 | .then(function(html) { 207 | var addToAssets = function(source, outputName) { 208 | compilation.assets[outputName] = { 209 | source: function() { 210 | return source; 211 | }, 212 | size: function() { 213 | return source.length; 214 | } 215 | }; 216 | } 217 | // Replace the compilation result with the evaluated html code 218 | for(var i=0; i]*>)/i; 595 | var headRegExp = /(<\/head>)/i; 596 | var bodyRegExp = /(<\/body>)/i; 597 | var body = assetTags.body.map(this.createHtmlTag); 598 | var head = assetTags.head.map(this.createHtmlTag); 599 | 600 | if (body.length) { 601 | if (bodyRegExp.test(html)) { 602 | // Append assets to body element 603 | html = html.replace(bodyRegExp, function(match) { 604 | return body.join('') + match; 605 | }); 606 | } else { 607 | // Append scripts to the end of the file if no element exists: 608 | html += body.join(''); 609 | } 610 | } 611 | 612 | if (head.length) { 613 | // Create a head tag if none exists 614 | if (!headRegExp.test(html)) { 615 | if (!htmlRegExp.test(html)) { 616 | html = '' + html; 617 | } else { 618 | html = html.replace(htmlRegExp, function(match) { 619 | return match + ''; 620 | }); 621 | } 622 | } 623 | 624 | // Append assets to head element 625 | html = html.replace(headRegExp, function(match) { 626 | return head.join('') + match; 627 | }); 628 | } 629 | 630 | // Inject manifest into the opening html tag 631 | if (assets.manifest) { 632 | html = html.replace(/(]*)(>)/i, function(match, start, end) { 633 | // Append the manifest only if no manifest was specified 634 | if (/\smanifest\s*=/.test(match)) { 635 | return match; 636 | } 637 | return start + ' manifest="' + assets.manifest + '"' + end; 638 | }); 639 | } 640 | return html; 641 | }; 642 | 643 | /** 644 | * Appends a cache busting hash 645 | */ 646 | HtmlWebpackPlugin.prototype.appendHash = function(url, hash) { 647 | if (!url) { 648 | return url; 649 | } 650 | return url + (url.indexOf('?') === -1 ? '?' : '&') + hash; 651 | }; 652 | 653 | /** 654 | * Turn a tag definition into a html string 655 | */ 656 | HtmlWebpackPlugin.prototype.createHtmlTag = function(tagDefinition) { 657 | var attributes = Object.keys(tagDefinition.attributes || {}).map(function(attributeName) { 658 | return attributeName + '="' + tagDefinition.attributes[attributeName] + '"'; 659 | }); 660 | return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (tagDefinition.selfClosingTag ? '/' : '') + '>' + 661 | (tagDefinition.innerHTML || '') + 662 | (tagDefinition.closeTag ? '' : ''); 663 | }; 664 | 665 | /** 666 | * Helper to return the absolute template path with a fallback loader 667 | */ 668 | HtmlWebpackPlugin.prototype.getFullTemplatePath = function(template) { 669 | var obj = template; 670 | var self = this; 671 | if(self.options.loader) { 672 | Object.keys(template).forEach(function(name) { 673 | obj[name] = self.options.loader + "!" + template[name]; 674 | }); 675 | } 676 | return obj; 677 | }; 678 | 679 | /** 680 | * Helper to return the absolute template path with a fallback loader 681 | */ 682 | // HtmlWebpackPlugin.prototype.getFullTemplatePath = function(template, context) { 683 | // // If the template doesn't use a loader use the lodash template loader 684 | // if (template.indexOf('!') === -1) { 685 | // /** 686 | // * create by zengwenfu 687 | // * @type {[type]} 688 | // */ 689 | // template = require.resolve('html-loader') + '!' + path.resolve(context, template); 690 | // } 691 | 692 | // // Resolve template path 693 | // template = template.replace( 694 | // /([!])([^\/\\][^!\?]+|[^\/\\!?])($|\?.+$)/, 695 | // function(match, prefix, filepath, postfix) { 696 | 697 | // return prefix + path.resolve(filepath) + postfix; 698 | // }); 699 | 700 | 701 | // return template; 702 | // }; 703 | 704 | /** 705 | * Helper to return a sorted unique array of all asset files out of the 706 | * asset object 707 | */ 708 | HtmlWebpackPlugin.prototype.getAssetFiles = function(assets) { 709 | var files = _.uniq(Object.keys(assets).filter(function(assetType) { 710 | return assetType !== 'chunks' && assets[assetType]; 711 | }).reduce(function(files, assetType) { 712 | return files.concat(assets[assetType]); 713 | }, [])); 714 | files.sort(); 715 | return files; 716 | }; 717 | 718 | module.exports = HtmlWebpackPlugin; 719 | --------------------------------------------------------------------------------