基於gulp+webpack的"約定大於配置"的構建方案探討

這不到半年的時間,玩了不少東西。有些以爲不錯的新技術,直接拿到公司的項目裏去挖坑。感受進步很大,可是看看工程,啥都有。單看模塊管理,從遺留的requirejs,到我過來改用的browserify,以及如今的es6 module,都有,亂糟糟的感受。而後有天老大發現:如今發佈,前端的構建時間比後端還長。從新作構建方案已經變成了一個本身想作,又有可能升值加薪的事~~css

示例代碼

demo在github上,自取。html

假設與前提

我很是推崇分治的開發方式。改了一個頁面,對其餘頁面最好不要產生任何影響。開發環境也能夠單獨針對某個頁面,不須要去編譯打包其餘頁面的東西。這就要求,除了一些基本不會改變的公用js框架,js庫,公用樣式,以及logo圖片之類的東西,其餘的代碼,每一個頁面的代碼徹底獨立,最好從文件夾層面的分離。這樣的話,好比有個頁面,若是再也不須要,直接把文件夾刪就ok了。前端

demo的目錄結構是這樣的node

dir

兩個頁面,一個index,一個contact。各自須要的全部代碼,資源文件,全在本身的目錄。公用的東西在lib裏。react

因此這裏假設的構建方案是:jquery

  1. 多個頁面,每一個頁面相互獨立,若是頁面不要了,直接刪了文件夾就ok。webpack

  2. 開發時,只構建本身的東西,由於若是項目有20,30個頁面,我如今只開發index,打包、watch其餘頁面的代碼,會影響個人開發效率。nginx

  3. 發佈的時候,全量構建.git

  4. 構建的文件路徑映射,給出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引入依賴(注意reactreact-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
],

都是些經常使用的配置和插件,有幾點須要的注意的地方

  1. output的filename要有[chunkhash],使用[hash]的話,同一次構建,不一樣的entry文件,會是同一個hash.緣由看文檔

  2. 使用了assets-webpack-plugin生成文件的路徑映射。

  3. 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的處理

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].jslib-[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-map.json

這裏有個細節注意一下,assets-webpack-plugin這個插件,默認是把json文件覆蓋掉的。對於本demo,lib和其餘是分開,lib先執行,因此默認lib相關的路徑映射會被覆蓋。不覆蓋有兩個條件

  1. 設置屬性{update:true}

  2. 同一個插件實例

代碼以下:

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'))

運行demo

說了半天,到如今沒有任何能夠看的效果。其實最大的效果,都在assets/dist裏。不過爲了你們看效果,多寫點,可以運行。運行方式:

github上clone下來,而後

npm i
gulp
node index.js

瀏覽器打開

http://localhost:3333/contact.html
http://localhost:3333/index.html

效果很簡單(簡直是粗糙),可是構建的不少方面都有涉及了。

看一下assets/dist裏的index.htmlcontact.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.htmlcontact.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。
另外沒有就開發流程進行說明。還有單頁的應用怎麼辦。
都再起一篇吧。不過大致思路,就是本篇的東西,你們能夠根據本身的狀況,去修改。

第二篇看這裏,總結了實踐中幾個小問題,公共模塊的抽取和性能優化等。

相關文章
相關標籤/搜索