這不到半年的時間,玩了不少東西。有些以爲不錯的新技術,直接拿到公司的項目裏去挖坑。感受進步很大,可是看看工程,啥都有。單看模塊管理,從遺留的requirejs,到我過來改用的browserify,以及如今的es6 module,都有,亂糟糟的感受。而後有天老大發現:如今發佈,前端的構建時間比後端還長。從新作構建方案已經變成了一個本身想作,又有可能升值加薪的事~~css
demo在github上,自取。html
我很是推崇分治
的開發方式。改了一個頁面,對其餘頁面最好不要產生任何影響。開發環境也能夠單獨針對某個頁面,不須要去編譯打包其餘頁面的東西。這就要求,除了一些基本不會改變的公用js框架,js庫,公用樣式,以及logo圖片之類的東西,其餘的代碼,每一個頁面的代碼徹底獨立,最好從文件夾層面的分離。這樣的話,好比有個頁面,若是再也不須要,直接把文件夾刪就ok了。前端
demo
的目錄結構是這樣的node
兩個頁面,一個index,一個contact。各自須要的全部代碼,資源文件,全在本身的目錄。公用的東西在lib裏。react
因此這裏假設的構建方案
是:jquery
多個頁面,每一個頁面相互獨立,若是頁面不要了,直接刪了文件夾就ok。webpack
開發時,只構建本身的東西,由於若是項目有20,30個頁面,我如今只開發index,打包、watch其餘頁面的代碼,會影響個人開發效率。nginx
發佈的時候,全量構建.git
構建的文件路徑映射,給出map.json
(我命名爲assets-map.json)文件,供路徑解析用。es6
有使用後端開發框架的同窗,應該都知道這個說法。只要按照必定的約定去寫代碼,框架會幫你作一個自動的處理。好比以文件名以controller
結尾的,是控制器,而後對應的路由會自動生成等。
很早以前我就在想,能不能前端也有約定大於配置
的構建方案。我以爲各大公司確定有相應的方案,可是我沒見到。我但願一套方案,直接拿過去,npm一下,按照相應的約定去寫,構建自動完成。
這裏託webpack
的福,能比較容易的作出我滿意的方案。webpack
以模塊爲設計出發點,全部資源都當成模塊,css,js,圖片,模版文件等等。
// webpack is a module bundler.
// This means webpack takes modules with dependencies
// and emits static assets representing those modules.
因此實際上,我須要知道每一個頁面的入口文件,就能自動構建每一個頁面的全部代碼和資源。(這麼一說,我好像什麼也不用作-_-!)。而後配合gulp,去動態微調一些配置。gulp + webpack
的基本玩法就是 配置一個基礎的webpackConfig,gulp的task裏,根據須要,動態微調基本的webpackConfig。
首先看代碼怎麼寫。既然分治
了,那麼先只看index
文件夾。目錄結構說明以下:
index img -- 文件夾。是人都知道這個文件夾幹嗎 js -- 文件夾。因此 less -- 文件夾。就不侮辱你們的智商 test -- 一些測試,這裏偷懶,裏面啥也沒有 tools -- 開發環境工具 index.entry.js -- 入口文件
規定因此入口文件都是*.entry.js
,這就是惟一的約定(後面構建時,會找出因此這樣命名規則的文件)。固然主要是webpack作了太多的工做。
看一下index.entry.js
的代碼
import ReactDom from 'react-dom' import IndexComponent from './js/IndexComponent.js' import './less/index.less' ReactDom.render( ( <div> <IndexComponent/> <div className='avatar'/> </div> ), document.getElementById('mount-dom') ) setTimeout(function(){ require.ensure([],function(){ require('./js/async.js') }) },1000)
使用es6
語法,先各類import
引入依賴(注意react
和react-dom
會放到lib
裏,後面說)。包括js,less,setTimeout
模擬按需異步加載js文件。其中index.less
裏有樣式引用img裏的圖片
//index.less的代碼 .avatar{ background:url(../img/touxiang.jpg) no-repeat; height: 100px; width: 100px; background-size: 100%; }
執行完構建後,assets\dist下
,你會看到
//[hash]爲文件的hash,這裏寫成佔位符。 index.entry-[hash].js index.entry-[hash].css img/[hash].jpg
在assets\assets-map.json
,有路徑的映射。(這裏用的file-loader處理圖片,實際url-loader更好,不過用法上,通常就加一個limit,這裏就不贅述了)
對webpack熟悉的同窗,應該會以爲這很普通。相似下面的配置:
entry: {'/index.entry':"./assets/src/index/index.entry.js"}, output: { filename: "[name]-[chunkhash].js", chunkFilename:'[name].js', path: __dirname + "/dist", libraryTarget:'umd', publicPath:'' }, externals:{ 'react': { root: 'React', commonjs2: 'react', commonjs: 'react', amd: 'react' }, 'jquery': { root: 'jQuery', commonjs2: 'jquery', commonjs: 'jquery', amd: 'jquery' } } module: { loaders: [ { test: /[\.jsx|\.js ]$/, exclude: /node_modules/, loader: "babel-loader?stage=0&optional[]=runtime" }, { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') }, { test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader') }, { test: /\.(png|jpg|gif)$/, loader: 'file-loader?name=img/[hash].[ext]' } ] }, devtool:'source-map', plugins: [ new ExtractTextPlugin("[name].css"), new webpack.optimize.UglifyJsPlugin({ mangle: { except: ['$', 'exports', 'require'] } }), assetsPluginInstance ],
都是些經常使用的配置和插件,有幾點須要的注意的地方
output的filename
要有[chunkhash]
,使用[hash]
的話,同一次構建,不一樣的entry文件,會是同一個hash
.緣由看文檔
使用了assets-webpack-plugin
生成文件的路徑映射。
externals
把公用的庫排除掉。公用庫會去生成lib.js
,lib.css
那多個頁面,怎麼去實現一塊兒構建呢。
上面的entry
裏配置項裏只有一個index.entry
,若是有兩個固然就生成兩個頁面的代碼和資源。相似這樣
{ '/contact.entry': './assets/src/contact/contact.entry.js', '/index.entry': './assets/src/index/index.entry.js' }
還記得咱們的約定嗎(說着有點怪。。),選出來全部的*.entry.js
文件,稍做處理就行了。
var entries = {} var entryFiles = glob.sync('assets/src/**/*.entry.js'); for(var i = 0;i<entryFiles.length;i++){ var filePath = entryFiles[i]; key = filePath.substring(filePath.lastIndexOf(path.sep),filePath.lastIndexOf('.')) entries[key] = path.join(__dirname,filePath); } var config = _.merge({},webpackConfig) config.entry=entries
上面的代碼就是生成一個鍵值對(key-value pair),key
形如 /*.entry
,value是入口文件的路徑。生成完了,設置給config.entry
.
lib實際上就是把上面exteranls
裏的東西,統一打個包。
看gulpfile.js 裏的lib
task,就是把external
設成{}
.
lib.js
的代碼
import React from 'react' import jQuery from 'jquery' import ReactDOM from 'react-dom' import './reset.less' window.React = React window.jQuery = jQuery window.$ = jQuery window.ReactDOM = ReactDOM
就是把以前排除掉公共的東西,都import進來,另外加點全局的樣式。
爲啥不用CommonsChunkPlugin
?由於這些東西很明顯是屬於lib的,不用每次都去構建不須要構建的代碼。
運行 gulp lib
後,dist下,就會生成lib-[hash].js
和lib-[hash].css
====
實際上,基本的構建已經完成了。對照下上面說的4點
1.多個頁面,每一個頁面相互獨立,若是頁面不要了,直接刪了文件夾就ok。
index 和 contact的全部東西都是獨立的。這點沒問題。
2.開發時,只構建本身的東西,由於若是項目有20,30個頁面,我如今只開發index,打包、watch其餘頁面的代碼,會影響個人開發效率。
開發環境後面說
3.發佈的時候,全量構建.
發佈包括:發佈lib.js,發佈全部頁面的靜態文件。gulp的default
task先執行lib
task,而後本身打包全部頁面的資源。
4.構建的文件路徑映射,給出
map.json
(我命名爲assets-map.json)文件,供路徑解析用。
已經有了,在/assets/assets-map.json裏。
這裏有個細節注意一下,assets-webpack-plugin
這個插件,默認是把json文件覆蓋掉的。對於本demo,lib
和其餘是分開,lib
先執行,因此默認lib
相關的路徑映射會被覆蓋。不覆蓋有兩個條件
設置屬性{update:true}
同一個插件實例
代碼以下:
var AssetsPlugin = require('assets-webpack-plugin'); var assetsPluginInstance = new AssetsPlugin({filename:'assets/assets-map.json',update: true,prettyPrint: true}) //而後配置裏,plugins加入assetsPluginInstance,這樣gulp lib task 和 default task裏的assetsPluginInstance是同一個對象。
有了這個映射文件,就能夠自動生成路徑了。
//getStatic.js 能夠直接執行node getStatic.js看結果。 //執行gulp後,生成assets/assets-map.json後,執行下面的命令 var fs =require('fs') var path = require('path') var fileContent = fs.readFileSync(path.join(__dirname,'assets/assets-map.json')) var assetsJson = JSON.parse(fileContent); function getStatic(resourcePath){ var lastIndex = resourcePath.lastIndexOf('.') var name = resourcePath.substr(0,lastIndex), suffix = resourcePath.substr(lastIndex+1); if(name in assetsJson){ return assetsJson[name][suffix] }else{ return resourcePath; } } console.log(getStatic('/lib.js')) console.log(getStatic('/index.entry.css'))
以express + jade 爲例
app.locals.getStatic = function(path){ if(isProdction){ return getStatic(path) }else{ //開發環境,return localPath.. } }
而後模板裏這樣使用。
script(src=getStatic('/lib.js'))
說了半天,到如今沒有任何能夠看的效果。其實最大的效果,都在assets/dist
裏。不過爲了你們看效果,多寫點,可以運行。運行方式:
github上clone下來,而後
npm i gulp node index.js
瀏覽器打開
http://localhost:3333/contact.html http://localhost:3333/index.html
效果很簡單(簡直是粗糙),可是構建的不少方面都有涉及了。
看一下assets/dist
裏的index.html
和contact.html
,是徹底靜態的頁面。若是不須要首屏數據,都是經過ajax生成的話,這就是一個徹底靜態化的方案。只須要nginx 指向dist
文件夾下,就發佈好了。這裏爲了你們運行方便,就express 去作靜態文件服務器,道理是同樣的。
構建徹底靜態化的東西,難點只有一個,就是路徑的問題。找webpack
的插件吧。這裏使用:html-webpack-plugin
.它會根據模板,自動在head裏插入chunks
的css,在body底部插入chunks
的js。剛開始的時候,我使用模板文件(路徑./assets/webpack-tpl.html')去生成。
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>webpack coc</title> <!--lib是全部頁面公用的。--> <!--須要自動生成一下--> <link href="/lib.css" rel="stylesheet"> </head> <body> <div id="mount-dom"></div> <script src="/lib.js"></script> </body> </html>
生成後,index.html
和 contact.html
會插入相應模塊的js和css。可是lib呢,怎麼把hash
加上?我寫了個簡單的插件。webpack的插件,最簡單的,就是一個function
//幫助函數 function getTplContent(libJs,libCss) { var str = ` <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>webpack coc</title> <link href="${libCss}" rel="stylesheet"> </head> <body> <div id="mount-dom"></div> <script src="${libJs}"></script> </body> </html> `; return str } //插件,只在執行lib時插入。 function libPathPlugin(){ this.plugin("done", function(stats) { var stats = stats.toJson() var chunkFiles = stats.chunks[0].files var libJs ='',libCss=''; for(var i in chunkFiles){ var fileName = chunkFiles[i] if(fileName.endsWith('.js')){ libJs = fileName } if(fileName.endsWith('.css')){ libCss = fileName } } globalTplContent = getTplContent(libJs,libCss) }); }
this.plugin 的第一個參數是構建的階段,有after-emit
,emit
,done
等。第二個就是負責執行構建邏輯的函數了。關鍵是這個stats
參數,裏面有大量豐富的信息,建議你們把它打印處理,好好看看。這裏只須要知道最終生成了文件名是什麼。stats.toJson().chunks
裏有。這裏只有lib
一個模塊,因此簡單處理一下,就能獲得html-webpack-plugin
須要的模板內容。
另外,一個HtmlWebpackPlugin,只能生成一個html,咱們有多entry,有多個HtmlWebpackPlugin。相關的配置都有說明,另外能夠看文檔。
for(var i in entries){ config.plugins.push(new HtmlWebpackPlugin({ filename:(i +'.html').replace('entry.',''),//index.entry => index.html //template:'./assets/webpack-tpl.html' templateContent:globalTplContent inject: true, chunks:[i] //只注入當前的chunk,index.html注入index.entry })) }
光有一個項目的方案還不行,實際上,咱們如今已經有多個相對獨立的前端項目.繼續分治
.
我的以爲,先後分離,大致上有兩種方案:
1.模板 + ajax
。
首屏有loading
提醒的狀況下,用戶體驗尚可。並且理論上,甚至能夠作到徹底靜態化,既html也是靜態的。這樣開完完成後,nginx直接指到相關目錄,就ok了。我寫的demo爲了簡單,就是全靜態化的。
2.node作api中間層
最簡單的狀況就是node作個api代理,而後順即可以簡單的套個首屏頁面。固然加這一層會給前端幾乎無限的可能性。你能夠實現本身的緩存策略,對感興趣的數據進行統計(由於api轉接,因此用戶請求的數據以及返回的數據,都能拿到。)等。就是工做量略有上升,另外要肩負node運維的職責。node掛了怎麼辦;升級怎麼保證不間斷服務等。
經過上圖能看出來,大體的架構是這樣的:
前端分紅多個小項目,都依賴於api server
=> 每個前端,都使用上面的 gulp+ webpack
的方案,把頁面分開。這與分治思想
相契合.對於一個工程,重寫一個頁面,代價過高;徹底從寫某一個項目,通常也能夠接受。
這裏沒有對公用
的東西進行說明,已經被團隊的同窗pk了。準備在寫一篇,講講commonChunk。
另外沒有就開發流程進行說明。還有單頁的應用怎麼辦。
都再起一篇吧。不過大致思路,就是本篇的東西,你們能夠根據本身的狀況,去修改。
第二篇看這裏,總結了實踐中幾個小問題,公共模塊的抽取和性能優化等。