gulp + webpack 構建多頁面前端項目

jopen 9年前發布 | 413K 次閱讀 webpack gulp

 

之前在使用gulp和webpack對項目進行構建的時候遇到了一些問題,最終算是搭建了一套比較完整的解決方案,接下來這篇文章以一個實際項目為例子,講解多頁面項目中如何利用gulp和webpack進行工程化構建。本文是自己的實踐經驗,所以有些解決方案并不是最優的,仍在探索優化中。所以有什么錯誤疏漏請隨時指出。

使用gulp過程中的一些問題,我已經在另外一篇文章講到了 grunt or gulp

前言

現在為什么又整了一個webpack進來呢?

我們知道webpack近來都比較火,那他火的原因是什么,有什么特別屌的功能嗎?帶著這些疑問,繼續看下去。

在使用gulp進行項目構建的時候,我們一開始的策略是將所有js打包為一個文件,所有css打包為一個文件。然后每個頁面都將只加載一個js 和一個css,也就是我們通常所說的 ==all in one== 打包模式。這樣做的目的就是減少http請求。這個方案對于簡單的前端項目來說的是一個萬金油。因為通常頁面依賴的js,css并不會太大,通過壓縮和 gzip等方法更加減小了文件的體積。在項目最開始的一段時間內(幾個月甚至更長),一個前端團隊都能通過這種辦法達到以不變應萬變的效果。

然而,作為一個有追求(愛折騰)的前端,難道就滿足于此嗎?

媽媽說我不僅要請求合并,還要按需加載,我要模塊化開發,還要自動監聽文件更新,支持圖片自動合并....

等等!你真的需要這些功能嗎?是項目真的遇到了性能問題?不然你整這些干嘛?

對于pc端應用來說,性能往往不是最突出的問題,因為pc端的網速,瀏覽器性能都有比較好,所以很長一段時間我們要考慮的是開發效率的問題而不是性能問題,得在前端框架的選型上下功夫。至于加載文件的大小或文件個數,都難以形成性能瓶頸。

對于wap端來說,限制于手機的慢網速(仍然有很多用不上4g,wifi的人),對網站的性能要求就比較苛刻了,這時候就不僅僅要考慮開發效率的問題了。(移動網絡的性能問題可參考《web性能權威指南》)

在《高性能網站建設進階指南》中也講到:不要過早地考慮網站的性能問題。

這點我有不一樣的看法。如果我們在項目搭建的時候就能考慮得多一點,把基本能做的先做了。所花的成本絕對比以后去重構代碼的成本要低很多,而且我們能夠同時保證開發效率和網站性能,何樂而不為呢。

問題

竟然要做,那要做到什么程度呢,往往“度”是最難把握的東西。

以前在做wap網站的時候,遇到的最大的問題按需加載和請求合并的權衡。

通過純前端的方法不能同時滿足請求合并和按需加載,這里面的原理和難點已經有大牛講得很清楚了 前端工程與模塊化框架

實現的方法歸納起來主要有以下步驟:

  1. 通過工具分析出前端靜態文件依賴表

  2. 頁面通過模塊化工具加載入口文件,并將所依賴的所有文件合并為combo請求。

  3. 后端返回combo文件,瀏覽器將模塊緩存起來,跳頁面的時候執行步驟2,只請求沒有緩存過的文件。

如此通過依賴分析和后端combo實現了按需加載和請求合并。

這種實現方式的缺陷就是需要后端的支持,如果前端團隊本身不是自己實現的后端路由層,需要后端同學加以配合,就需要更多溝通成本。

在沒有后端支持的情況下,怎么比較好地實現按需加載和請求合并,我們用webpack做了嘗試。

webpack的使用

webpack可以說是一個大而全的前端構建工具。它實現了模塊化開發和靜態文件處理兩大問題。

以往我們要在項目中支持模塊化開發,需要引入requirejs,seajs等模塊加載框架。而webpack天生支持 AMD,CommonJS, ES6 module等模塊規范。不用思考加載器的選型,可以直接像寫nodejs一樣寫模塊。而webpack這種萬物皆模塊的思想好像就是為React而生的,在React組件中可以直接引入css或圖片,而做到這一切只需要一個require語句和loader的配置。

webpack的功能之多和繁雜的配置項會讓初學者感到眼花繚亂,網上的很多資料也是只介紹功能不教人實用技巧。這里有一篇文章就講解了 webpack開發的workflow , 雖然該教程是基于React的,但是比較完整地講了webpack的開發流程。下面我也用一個實例講解使用中遇到的問題和解決方案。

我們的項目是一個多頁面項目,即每個頁面為一個html,訪問不同的頁面需要跳轉鏈接。項目目錄結構大概是這樣的,app放html文件,css為樣式文件,images存放圖片,js下有不同的文件夾,里面的子文件夾為一些核心文件和一些庫文件,ui組件。js的根目錄為頁面入口文件。

├── app
│   ├── header.inc
│   ├── help-charge.inc
│   ├── index.html
│   ├── news-detail.html
│   └── news-list.html
├── css
│   ├── icon.less
│   └── slider.css
├── images
└── js
  ├── core
  ├── lib
  ├── ui
  ├── news-detail.js
  ├── news-list.js
  └── main.js

該項目中我們只用webpack處理js文件的合并壓縮。其他任務交給gulp。關于多頁面項目和單頁面項目中js處理的差異請看 這里

配置文件如下:

module.exports = {
  devtool: "source-map",    //生成sourcemap,便于開發調試
  entry: getEntry(),         //獲取項目入口js文件
  output: {
    path: path.join(__dirname, "dist/js/"), //文件輸出目錄
    publicPath: "dist/js/",     //用于配置文件發布路徑,如CDN或本地服務器
    filename: "[name].js",      //根據入口文件輸出的對應多個文件名
  },
  module: {
    //各種加載器,即讓各種文件格式可用require引用
    loaders: [
      // { test: /\.css$/, loader: "style-loader!css-loader"},
      // { test: /\.less$/, loader: "style-loader!csss-loader!less-loader"}
    ]
  },
  resolve: {
    //配置別名,在項目中可縮減引用路徑
    alias: {
      jquery: srcDir + "/js/lib/jquery.min.js",
      core: srcDir + "/js/core",
      ui: srcDir + "/js/ui"
    }
  },
  plugins: [
    //提供全局的變量,在模塊中使用無需用require引入
    new webpack.ProvidePlugin({
      jQuery: "jquery",
      $: "jquery",
      // nie: "nie"
    }),
    //將公共代碼抽離出來合并為一個文件
    new CommonsChunkPlugin('common.js'),
    //js文件的壓縮
    new uglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
};

配置項參考文檔

打包思路:

該配置方案的思路是每個頁面一個入口文件,文件中可以通過require引入其他模塊,而這些模塊webpack會自動跟入口文件合并為一個文件。通過getEntry獲取入口文件:

function getEntry() {
  var jsPath = path.resolve(srcDir, 'js');
  var dirs = fs.readdirSync(jsPath);
  var matchs = [], files = {};
  dirs.forEach(function (item) {
    matchs = item.match(/(.+)\.js$/);
    if (matchs) {
      files[matchs[1]] = path.resolve(srcDir, 'js', item);
    }
  });
  return files;
}

該方法將生成文件名到文件絕對路徑的map, 比如

entry{ news-detail: /../Document/project/.../news-detail.js
}

然后output就會在output.path路徑下生成[name].js,即news-detail.js,文件名保持相同。

module的作用是添加loaders, 那loaders有什么作用呢?

如果我們想要在js文件中通過require引入模塊,比如css或image,那么就需要在這里配置加載器,這一點對于React來說相當方便,因為可以在組件中使用模塊化CSS。而一般的項目中可以不用到這個加載器。

resolve中的alias可以用于定義別名,用過seajs等模塊工具的都知道alias的作用,比如我們在這里定義了ui這個別名,那么在模塊中想引用ui目錄下的文件,就可以直接這樣寫

require('ui/dialog.js');

不用加上前面的更長的文件路徑。

plugin用于引入一些插件,常見的有 這些

我們這里使用了CommonsChunkPlugin用于生成公用代碼,不只可以生成一個,還能根據不同頁面的文件關系,自由生成多個,例如:

該方法將生成文件名到文件絕對路徑的map, 比如

entry{ news-detail: /../Document/project/.../news-detail.js
}

然后output就會在output.path路徑下生成[name].js,即news-detail.js,文件名保持相同。

module的作用是添加loaders, 那loaders有什么作用呢?

如果我們想要在js文件中通過require引入模塊,比如css或image,那么就需要在這里配置加載器,這一點對于React來說相當方便,因為可以在組件中使用模塊化CSS。而一般的項目中可以不用到這個加載器。

resolve中的alias可以用于定義別名,用過seajs等模塊工具的都知道alias的作用,比如我們在這里定義了ui這個別名,那么在模塊中想引用ui目錄下的文件,就可以直接這樣寫

require('ui/dialog.js');

不用加上前面的更長的文件路徑。

plugin用于引入一些插件,常見的有 這些

我們這里使用了CommonsChunkPlugin用于生成公用代碼,不只可以生成一個,還能根據不同頁面的文件關系,自由生成多個,例如:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
  entry: {
    p1: "./page1",
    p2: "./page2",
    p3: "./page3",
    ap1: "./admin/page1",
    ap2: "./admin/page2"
  },
  output: {
    filename: "[name].js"
  },
  plugins: [
    new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
    new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
  ]
};
// 在不同頁面用<script>標簽引入如下js:
// page1.html: commons.js, p1.js
// page2.html: commons.js, p2.js
// page3.html: p3.js
// admin-page1.html: commons.js, admin-commons.js, ap1.js
// admin-page2.html: commons.js, admin-commons.js, ap2.js

這種用法有點像gulp或grunt中手動將多個js合并為common, 但是在webpack里,這個過程是全自動生成的,不用我們自己分析代碼的依賴關系。另外一個插件是uglifyJsPlugin,用于壓縮js代碼。

我們還用到一個字段是 devtool , 用于配置開發工具。‘source-map’就是在生成的代碼中加入sourceMap的支持。能夠直接定位到出錯代碼的具體位置,對sourcemap的使用和原理還不了解的可以看下 這篇文章

另外,devtool的配置參數使用在 這里

如何加載第三方庫?

在pc開發中我們通常會用到jQuery庫。如何很好地處理這類文件呢?這里有兩種辦法。

方法一是在html中用script標簽引入js文件,如

<script src="https://code.jquery.com/jquery-git2.min.js"></script>

然后再配置文件中添加externals

externals: { jquery: "jQuery" }

該字段的作用是將加jQuery全局變量變為模塊可引入。然后在各個模塊中,就可以如下使用:

var $ = require("jquery");

我個人覺得既然已經將加jQuery通過script引入了,那么就直接使用$標簽就行了。不必再將其轉化為模塊。

方法二是將jQuery代碼保存到本地,在配置文件中添加:

resolve: { alias: { jquery: "/path/to/jquery-git2.min.js" } }

即為jquery添加了別名,然后在模塊中也是這樣使用:

var $ = require("jquery");

還可以配合使用ProvidePlugin,其作用是提供全局變量給每個模塊,這樣就不需要在模塊中通過require引入,例如:使用前:

var _ = require("underscore");
_.size(...);

使用后:

plugins: [
  new webpack.ProvidePlugin({
    "_": "underscore"
  })
]

// If you use "_", underscore is automatically required
_.size(...)

總的來說,如果文件來自CDN,那么使用方法一,如果文件在本地,則用方法二。

如何啟動服務器?

首先肯定要安裝webpack-dev-server,安裝方法請自行腦補。

接著在webpack.config.js中添加配置

entry: [
    'webpack-dev-server/client?http://0.0.0.0:9090',//資源服務器地址
    'webpack/hot/only-dev-server',
    './static/js/entry.js'
]

output的發布路徑改為本地服務器

output: {
    publicPath: "http://127.0.0.1:9090/static/dist/",
    path: './static/dist/',
    filename: "bundle.js"
}

在plugin中添加

new webpack.HotModuleReplacementPlugin()

html中通過資源服務器的絕對路徑引入js

<script src="http://127.0.0.1:9090/static/dist/bundle.js"></script>

最后通過命令行啟動

$ webpack-dev-server --hot --inline

配置參數的解釋在 這里

由于webpack服務器配置比較繁瑣,所以我們的項目還是采用gulp來啟動本地服務器...

gulp足夠優秀

目前來說,我們只利用webpack進行了js方面的打包,其他功能用gulp就足夠了。gulp主要做了下面幾個工作:

  • css轉化合并壓縮

  • 圖片的雪碧圖合并和base64

  • 文件md5計算與替換

  • 熱啟動,瀏覽器自動刷新

下列是依賴的npm模塊:

  "devDependencies": {
  "gulp": "^3.8.10",
  "gulp-clean": "0.3.1",
  "gulp-concat": "2.6.0",
  "gulp-connect": "2.2.0",
  "gulp-css-base64": "^1.3.2",
  "gulp-css-spriter": "^0.3.3",
  "gulp-cssmin": "0.1.7",
  "gulp-file-include": "0.13.7",
  "gulp-less": "3.0.3",
  "gulp-md5-plus": "0.1.8",
  "gulp-open": "1.0.0",
  "gulp-uglify": "1.4.2",
  "gulp-util": "~2.2.9",
  "gulp-watch": "4.1.0",
  "webpack": "~1.0.0-beta6"
},

支持雪碧圖合并和base64

我對gulp-css-spriter和gulp-css-base64的源碼做了一點修改,使其支持下面的語法:

.icon_corner_new{ background-image: url(../images/new-ico.png?__sprite); }

如果在url的后面加上__sprite后綴,則插件將會把該圖片合并到雪碧圖里。可以支持一個css文件合并為一個雪碧圖,也可以整站合并。

.icon_corner_new{
    background-image: url(../images/new-ico.png?__inline);
}

如果加上后綴__inline,則會將圖片轉化為base64,直接添加到css文件中,對于幾k的小文件可以直接使用inline操作。具體配置代碼如下:
gulp.task('sprite', function (done) {
  var timestamp = +new Date();
  gulp.src('dist/css/style.min.css')
    .pipe(spriter({
      spriteSheet: 'dist/images/spritesheet' + timestamp + '.png',
      pathToSpriteSheetFromCSS: '../images/spritesheet' + timestamp + '.png',
      spritesmithOptions: {
        padding: 10
      }
    }))
    .pipe(base64())
    // .pipe(cssmin())
    .pipe(gulp.dest('dist/css'))
    .on('end', done);
});

src為需要處理的css文件,spriteSheet為雪碧圖生成的目標文件夾,pathToSpriteSheetFromCSS為css文件中url的替換字符串,spritesmithOptions是生成雪碧圖的間隙。

文件加md5, 實現發布更新

發版本的時候為了避免瀏覽器讀取了舊的緩存文件,需要為其添加md5戳。這里采用了gulp-md5-plus

gulp.task('md5:js', function (done) {
    gulp.src('dist/js/*.js')
        .pipe(md5(10, 'dist/app/*.html'))
        .pipe(gulp.dest('dist/js'))
        .on('end', done);
});

該代碼會將dist/js下面所有的js計算md5戳,并將dist/app/下的html中script中的src引用文件名替換為加了md5的文件名,再將md5文件替換到目標目錄dist/js。css的md5操作跟js無異。

關于服務器啟動和代碼轉換的功能點,這里就不展開講了。

總結

該方案總結下來做了下面幾件事:

  1. 將css直接合并為一個文件,在head中通過link標簽引入,提高網頁渲染速度。

  2. 將js打包為不同的入口文件,并自動合并依賴關系。將跨頁面的公用代碼抽離為獨立文件,益于瀏覽器緩存。

  3. 增加圖片雪碧圖,base64的支持,開發者可以手動配置__sprite和__inline,靈活性較高。

  4. 靜態文件md5打包,并自動更改html引用路徑,方便發布。

  5. 提供開發調試所需要的環境,包括熱啟動,瀏覽器自動刷新,sourceMap。

該方案之所以針對多頁面應用,區別在于對js和css的處理方式。在單頁面應用中,通過哈希跳轉來實現靜態文件的異步加載,打包策略又有所不同。但webpack中已經提供了處理異步加載的接口require.ensure,可以發揮無窮的力量。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!