基于webpack的前端工程化開發之多頁站點篇(二)
來自: https://segmentfault.com/a/1190000004516832
這篇,我們要解決上篇留下的兩個問題:
-
webpack如何自動發現entry文件及進行相應的模板配置
-
如何直接處理后端模板的樣式、腳本自動引入問題
目錄結構
以express項目為例,使用express-generator構建一個初始項目,然后再添加需要的目錄,最終的目錄架構如下:
- website- bin #express項目啟動文件 - lib #express項目開發所需的庫 + routes #express項目路由 - src #前端源碼開發目錄 - styles #css目錄,按照頁面(模塊)、通用、第三方三個級別進行組織 + page + common + lib + imgs #圖片資源 - scripts #JS腳本,按照page、components進行組織 + page + components + views #HTML模板 - public #webpack編譯打包輸出目錄的靜態文件,express工程的靜態目錄 + styles + scripts + imgs + views #webpack編譯輸出的模板靜態文件,express工程的視圖模板 + node_modules #所使用的nodejs模塊 package.json #項目配置 webpack.config.js #webpack配置 README.md #項目說明</pre>
你同樣可以根據個人喜好自由設計目錄結構。完整的源碼示例前往 https://github.com/vhtml/webpack-MultiPage 。
安裝開發依賴
package.json里最終的聲明依賴如下:
"devDependencies": { "css-loader": "^0.23.1", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.8.5", "glob": "^7.0.0", "html-loader": "^0.4.3", "html-webpack-plugin": "^2.9.0", "jquery": "^1.12.0", "less": "^2.6.0", "less-loader": "^2.2.2", "style-loader": "^0.13.0", "url-loader": "^0.5.7", "webpack": "^1.12.13", "webpack-dev-server": "^1.14.1" }可以看出,比上篇多了一個glob依賴,它是一個根據模式匹配獲取文件列表的node模塊。有關glob的詳細用法可以在這里看到—— https://github.com/isaacs/node-glob 。利用glob模塊可以很方便的獲取src/scripts/page路徑下的所有js入口文件。同理,可以實現自動的進行與入口文件相對應的模板配置。
webpack配置
最終的webpack配置如下:
var path = require('path'); var glob = require('glob'); var webpack = require('webpack'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;const debug = process.env.NODE_ENV !== 'production';
var entries = getEntry('src/scripts/page/*/.js', 'src/scripts/page/'); var chunks = Object.keys(entries);
var config = { entry: entries, output: { path: path.join(__dirname, 'public'), publicPath: '/static/', filename: 'scripts/[name].js', chunkFilename: 'scripts/[id].chunk.js' }, module: { loaders: [ //加載器 { test: /.css$/, loader: ExtractTextPlugin.extract('style', 'css') }, { test: /.less$/, loader: ExtractTextPlugin.extract('css!less') }, { test: /.html$/, loader: "html" }, { test: /.(woff|woff2|ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, loader: 'file-loader?name=fonts/[name].[ext]' }, { test: /.(png|jpe?g|gif)$/, loader: 'url-loader?limit=8192&name=imgs/[name]-[hash].[ext]' } ] }, plugins: [ new webpack.ProvidePlugin({ //加載jq $: 'jquery' }), new CommonsChunkPlugin({ name: 'vendors', // 將公共模塊提取,生成名為
vendors
的chunk chunks: chunks, minChunks: chunks.length // 提取所有entry共同依賴的模塊 }), new ExtractTextPlugin('styles/[name].css'), //單獨使用link標簽加載css并設置路徑,相對于output配置中的publickPath debug ? function() {} : new UglifyJsPlugin({ //壓縮代碼 compress: { warnings: false }, except: ['$super', '$', 'exports', 'require'] //排除關鍵字 }), new webpack.HotModuleReplacementPlugin() //熱加載 ], devServer: { publicPath:'http://localhost:8080/static/', proxy: { "*": "http://localhost:54999" }, inline: true, hot: true } };var pages = Object.keys(getEntry('src/views/*/.html', 'src/views/')); pages.forEach(function(pathname) { var conf = { filename: '../views/' + pathname + '.html', //生成的html存放路徑,相對于path template: 'src/views/' + pathname + '.html', //html模板路徑 inject: false, //js插入的位置,true/'head'/'body'/false minify: { //壓縮HTML文件 removeComments: true, //移除HTML中的注釋 collapseWhitespace: false //刪除空白符與換行符 } }; if (pathname in config.entry) { conf.inject = 'body'; conf.chunks = ['vendors', pathname]; conf.hash = true; } config.plugins.push(new HtmlWebpackPlugin(conf)); });
module.exports = config;
function getEntry(globPath, pathDir) { var files = glob.sync(globPath); var entries = {}, entry, dirname, basename, pathname, extname;
for (var i = 0; i < files.length; i++) { entry = files[i]; dirname = path.dirname(entry); extname = path.extname(entry); basename = path.basename(entry, extname); pathname = path.join(dirname, basename); pathname = pathDir ? pathname.replace(new RegExp('^' + pathDir), '') : pathname; entries[pathname] = './' + entry; } return entries;
}</pre>
探索之路
這里還要說說如何直接處理后端模板的問題。一開始本菜也是對這個問題進行了苦苦的探索,覺得可能真的實現不了,一度要放棄,并打算采用先純靜態打包再改寫成后端模板的方式(因為貌似還沒有這樣的loader可以很智能的處理模板include的問題以及在非html模板中自動引入css和js)。但是這樣做真的很蛋疼啊有木有!明明是一件事為什么要拆成兩件事去做呢?!
如果你也進行過這樣一番探索,你可能接觸過像jade-loader、ejs-loader、ejs-compiled-loader等這樣的webpack loader。無奈它們統統都不是我要找的,它們只是編譯了模板而沒有保留模板原有的生態,也不能自動地引入css和js。我也曾試過自己寫loader將ejs模板先轉成html模板(只處理include標簽,其余原樣保留)再用html-loader去處理,但又破壞了模板的可復用性,失去了靈活性。
好吧,其實只是想原樣輸出src/views中的模板,然后像上篇中那樣自動引入css和js,僅此而已。沒想到差一點鉆了死胡同,想得過于復雜了。
我們應該先知道一個事實,html-webpack-plugin插件實現自動引入css和js的原理,是在模板中對應的成對head和body標簽中進行解析插入。如果沒有head和body標簽,它會分別在模板頭和尾生成這兩個標簽并插入link和script標簽來引入css和js。而至于你的模板里寫了什么,它是不會關心的。明白了這個原理,要完成“大業”就為期不遠了。我們應該先改一改寫模板的方式,模板結構一定要是類html的,不能是jade這種(還好我并不喜歡用jade)。以artTemplate模板為例,如下:
<!DOCTYPE html> <html> <head> {{include './common/meta'}} </head> <body> {{include './common/header'}} <div class="g-bd"> {{include './common/_content'}} </div> {{include './common/footer'}} </body> </html>是的,沒錯,只要保留完整的head、body結構即可。然后根據上述的webpack配置,將與入口js對應的模板插入link和script標簽并輸出到./views目錄中,其余模板原樣輸出到./views目錄或相應的子目錄下即可。
到此,“大業”完成。
假如你有更好的解決方案,歡迎一起分享。
</div>