閱讀632 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Webpack 愛與恨

關於標題,為什麼是“愛與恨”?

因為在 webpack 剛出來的時候,我並不是堅定的支持者,有很多地方用起來不方便,api 設計不合理。隨著 webpack 和 react 生態的越發完善,加上 webpack2.0 的發布,它的功能也越來越強大,讓我又重新認識它。

內容提要

webpack 構建方案

  • webpack 生態
  • 需求是什麼
  • 對比其他方案

webpack vs gulp

  • webpack
  • gulp
  • 什麼時候用

webpack 構建方案

webpack 生態

網上有好多介紹 webpack 的文章,本文隻簡單介紹兩個基本概念。

  • Loaders
  • Plugins

Loaders

webpack 可以使用 loader 來處理文件,允許你打包除 JavaScript 之外的任何靜態資源。

文件
  • raw-loader 加載文件原始內容
  • file-loader 將文件發送到輸出文件夾,並返回 URL
  • url-loader 像 file loader 一樣工作,但如果文件小於限製,可以返回 data URL
編譯
  • babel-loader 加載 ES2015+ 代碼,然後使用 Babel 轉譯為 ES5
  • traceur-loader 加載 ES2015+ 代碼,然後使用 Traceur 轉譯為 ES5
  • ts-loader 像 JavaScript 一樣加載 TypeScript 2.0+
  • coffee-loader 像 JavaScript 一樣加載 CoffeeScript
模板
  • html-loader 導出 HTML 為字符串,需要引用靜態資源
  • markdown-loader 將 Markdown 轉譯為 HTML
  • handlebars-loader 將 Handlebars 轉譯為 HTML
樣式
  • css-loader 解析 CSS 文件後,使用 import 加載,並且返回 CSS 代碼
  • less-loader 加載和轉譯 LESS 文件
  • sass-loader 加載和轉譯 SASS/SCSS 文件
  • postcss-loader 使用 PostCSS 加載和轉譯 CSS/SSS 文件

Plugins

  • CommonsChunkPlugin
    • 將多個入口起點之間共享的公共模塊,生成為一些 chunk,並且分離到單獨的 bundle 中,例如,vendor.bundle.js 和 app.bundle.js
  • DefinePlugin, EnvironmentPlugin
    • 允許在編譯時(compile time)配置的全局常量,用於允許「開發/發布」構建之間的不同行為
  • ExtractTextWebpackPlugin
    • 從 bundle 中提取 CSS 文本到獨立的文件
  • HtmlWebpackPlugin
    • 用於簡化 HTML 文件(index.html)的創建,提供訪問 bundle 的服務。
  • I18nWebpackPlugin
    • 為 bundle 增加國際化支持
  • NamedModulesPlugin
    • 保留編譯結果的模塊名,便於調試

需求是什麼

技術問題還是要從需求出發,我們團隊的實際需求是什麼。

  • 區分兩套環境
  • 多頁麵多入口
  • mock 接口數據
  • iconfont 字體打包

1. 區分兩套環境

  • develop
  • production

/build 文件夾編譯結果如下:

/build

  - /develop
    - /pageA
      - index.js
      - index.css
      - index.html
    - /pageB
    - common.js
    - common.css

  - /production
    - /1.0.0
      - /pageA
        - index.js
        - index.css
        - index.html
      - /pageB
      - common.js
      - common.css
    - /1.0.1
      - /pageA
        - index.js
        - index.css
        - index.html
      - /pageB
      - common.js
      - common.css

其中開發環境的 html 編譯結果為:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>首頁</title>
        <link href="/common.css" rel="stylesheet">
        <link href="/home/index.css" rel="stylesheet">
    </head>
    <body>
        <div ></div>
        <script src="/common.js" type="text/javascript"></script>
        <script src="/home/index.js" type="text/javascript"></script>
    </body>
</html>

其中生產環境的 html 編譯結果為:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>首頁</title>
        <link href="/0.1.0/common.css" rel="stylesheet">
        <link href="/0.1.0/home/index.css" rel="stylesheet">
    </head>
    <body>
        <div ></div>
        <script src="/0.1.0/common.js" type="text/javascript"></script>
        <script src="/0.1.0/home/index.js" type="text/javascript"></script>
    </body>
</html>

生產環境的最終頁麵上靜態資源路徑是

<script src="/{version}/path/file.js"></script>

業界還有一種做法是使用 /path/file.{hash}.js 形式的路徑,都是為了配合 http 緩存策略,做到前端資源的緩存和無縫發布。

  • 緩存策略:在 http 響應頭中,設置 Cache-ControlExpiresLast-Modified 控製靜態資源緩存。用戶隻有首次訪問需要下載全部靜態資源,以後的訪問都直接使用緩存資源。

    cache-control:max-age=31536000
    expires:Fri, 06 Apr 2018 08:32:17 GMT
    last-modified:Thu, 06 Apr 2017 06:54:04 GMT
    

    對用戶和客戶端來說,每次發布更新代碼,隻需要下載新的資源,而無需清除緩存

  • 無縫發布:在團隊合作開發中,往往會遇到新老版本兼容和發布順序的問題。例如原來的 a.js 添加了新功能變成了 a'.js,為了避免前端先發布上線的代碼影響線上業務,保證發布上去的代碼兼容舊版本,需要在代碼中添加如下的兼容語句:

    if(newVersion) {
    // new feature
    } else {
    // old feature
    }
    

    而使用增量發布的方式不需要這樣的額外處理,由於每次都是生成新的 url,那麼隻要後端引用的路徑沒有變化,就始終引用舊資源,一旦後端發布完成就自動引用新資源。

多頁麵多入口

雖然 React 天然是開發 SPA 的利器,它的生態中的配套工具也是以解決 SPA 問題為主,例如 React-Redux, React-Saga 等,但是我們的項目中頁麵非常簡單,沒有太多的用戶交互,沒有太多的組件間的消息傳遞,如果強行引入這些概念,反而把整個項目搞得非常複雜,因此選用多頁麵多入口的方案。使用前麵提到的 HtmlWebpackPlugin 插件進行多頁麵的構建。

// 編譯 html,多頁麵多入口
Object.keys(webpackConfig.entry).forEach(name => {
  webpackConfig.plugins.push(new HtmlWebpackPlugin({
    template: `./src/template.ejs`,
    filename: `${name}.html`,
    chunks: ['common', name]
  }));
});

mock 接口數據

webpack-dev-server 提供了 proxy 代理解決方案,但是沒有解決 mock 接口數據的問題。我們利用 Koa 開發了一個簡版的 mock 服務器,可以加載本地文件中的模擬測試數據。

首先,在 package.json 中添加 mockEnable 字段,當 mockEnable 為 true 時,則開啟 mock 服務。

// package.json
{
  "mockEnable": true,
  "proxy": {
    "/api/**": {
      "target": "https://example.com"
    }
  },
}

其次,在 webpack.config.js 中,將 package.proxy 的 target 指向 mock 服務器

// webpack.config.js
const pkg = require('./package.json');
const MockServer = require('./mock/server.js');

// 將 https://example.com/api/path 的接口,轉發到 https://localhost:8088/api/path
if(pkg.mockEnable) {
  let port = 8088;
  MockServer.start(port);

  Object.keys(pkg.proxy).forEach(filter => {
    let proxy = pkg.proxy[filter];
    proxy.target = `https://localhost:${port}`; // mock server
  });
}

最後,在 mock 服務器中處理請求,mock server 的邏輯很簡單,隻有不到 50 行代碼:

// mock/server.js
const fs = require('fs');
const path = require('path');
const color = require('colorful');

const Koa = require('koa');
const router = require('koa-router')();

let app = new Koa();

/* 訪問日誌,logger */
app.use(async function (ctx, next) {
  console.log(color.green('Mock Server'), (new Date()).toLocaleString(), 'url:', ctx.url);
  await next();
});

/* 路由,router */
router.all('*', async (ctx) => {
  let filepath = path.resolve(__dirname, 'data', `./${ctx.url}.json`);
  let methodFilepath = path.resolve(__dirname, 'data', `./${ctx.url}.${ctx.method}.json`);
  if(fs.existsSync(filepath)) {
    ctx.body = require(filepath);
  } else if(fs.existsSync(methodFilepath)) {
    ctx.body = require(methodFilepath);
  } else {
    ctx.status = 404;
    ctx.body = 'Mock data not found';
  }
}); 
app.use(router.routes()).use(router.allowedMethods());

/* 啟動 Mock Server */
exports.start = function(port) {
  app.listen(port, () => {
    console.log('Mock Server started on', color.green(`https://127.0.0.1:${port}/`));
  });
  return app;
}

最終的效果是,隻要開啟了 mockEnable,當用戶在頁麵上發起請求時,自動返回本地文件中的模擬數據。例如請求的是GET https://localhost/api/path/users則返回/mock/data/api/path/users.json
中的數據。如果同一個 url 既有 GET 請求又有 POST 請求,則可以通過如下方式避免衝突

/mock/data/api/path/users.get.json
/mock/data/api/path/users.post.json

iconfont 字體打包

我們的項目是基於 Ant Design 做二次開發,由於 ant design 的 iconfont 會依賴 https://at.alicdn.com/ 的字體資源,而公司內網不能訪問外部資源,所以需要將字體打包到自己的應用裏。利用 less-loader 的 modifyVars 屬性,修改 antd 裏的 @icon-url 變量。

先設置 @icon-url 的變量值。

// webpack.config.js
let cssVars = {
  '@icon-url': '"/assets/iconfont/iconfont"',
}

然後在 less-loader 的參數中設置 modifyVars。

// webpack.config.js
{
  test: /\.less$/,
  use: ExtractTextPlugin.extract([
    'css-loader', 
    { loader:'less-loader', options: { modifyVars:cssVars } }
  ])
}

對比其他方案

  • atool-build
    • 預設了適用於 antd 的 webpack 構建腳本
    • 配置非常靈活,幾乎支持所有場景的定製
    • 對插件的修改略麻煩
    • 版本升級兼容性不太好
  • roadhog
    • 使用非常簡單,不用關心那麼多概念
    • 自定義的靈活性受局限

Webpack vs Gulp

先看看 webpack 和 gulp 各自的官方說明:

  • Webpack
    • webpack is a module bundler for modern JavaScript applications.
  • Gulp
    • gulp is a toolkit for automating painful or time-consuming tasks in your development workflow, so you can stop messing around and build something.

從上述官方描述可以看出,webpack 的核心概念是 module bundler,而 gulp 的核心概念是 toolkit for tasks。一個側重模塊打包,一個側重自動化任務處理。

webpack - 一切皆是模塊

圖片描述

從官方網站上的圖片可以看出,一切皆是模塊,不管是 js 模塊,還是 css、sass 樣式,還是模板(hds)、圖片(jpg/png)、字體等資源,都以被 webpack 當做互相依賴的模塊(modules with dependencies)處理。經過 loader 的解析,最終處理成頁麵上可用的靜態資源(static assets)。由於模塊間需要有互相依賴關係,因此需要在 js 裏 require 樣式和圖片等資源。

蛋疼的 api,webpack loader 的參數形式簡直反人類,這種字符串拚接的方式不直觀且不方便擴展

 require("file-loader?name=js/[hash].script.[ext]!./javascript.js");

 require("file-loader?name=html-[hash:6].html!./page.html");

 require("file-loader?name=[hash]!./flash.txt");

 require("file-loader?name=[sha512:hash:base64:7].[ext]!./image.png");

 require("file-loader?name=img-[sha512:hash:base64:7].[ext]!./image.jpg");

 require("file-loader?name=picture.png!./myself.png");

 require("file-loader?name=[path][name].[ext]?[hash]!./dir/file.png")

以下是一個簡單的 webpack 的例子:

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

let webpackConfig = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
    publicPath: './'
  },
  module: {
    rules: [{
      test: /\.jsx?$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader'
      }
    }, {
      test: /\.less$/,
      use: ExtractTextPlugin.extract([
        'css-loader', 
        'less-loader'
      ])
    }, {
      test: /\.(png|jpg|jpeg|gif)$/i,
      use: { 
        loader:'file-loader', 
        options: {
          name: 'static/images/[name].[ext]'
        }
      }
    }, {
      test: /\.(woff2?|ttf|eot|svg)$/,
      use: { 
        loader:'file-loader', 
        options: {
          name: 'static/fonts/[name].[ext]'
        }
      }
    }]
  },
  plugins: [
    new ExtractTextPlugin('[name].css'),
    new UglifyJSPlugin(),
    new CleanWebpackPlugin(['./build']),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index'],
      title: '首頁',
      message: 'webpack 測試頁麵'
    })
  ]
};

module.exports = webpackConfig;

gulp - 一切基於任務

gulp 是作為一個 task runner 存在的,最核心的功能是自動化任務執行,複雜任務組織,基於文件 stream 的構建,加上完善的插件體係,處理各種類型的任務執行流程。用戶可以預先定義好一係列的 task,定義好這些 task 分別做些什麼,然後定義好執行順序,最後由 gulp 來執行這些 task。所以 gulp 可以做到幾乎所有 node 能做到的事情,不僅僅是用來打包 js。

下麵是一個簡單的 gulp 的例子:

// gulpfile.js
const gulp = require('gulp');
const clean = require('del');
const ejs = require('gulp-ejs');
const less = require('gulp-less');
const jsmin  = require('gulp-jsmin');
const minifyCSS = require('gulp-csso');

gulp.task('html', function(){
  return gulp.src('src/*.html')
    .pipe(ejs({
      title: '首頁',
      message: 'gulp 測試頁麵'
    }))
    .pipe(gulp.dest('build'))
});

gulp.task('css', function(){
  return gulp.src('src/*.less')
    .pipe(less())
    .pipe(minifyCSS())
    .pipe(gulp.dest('build'))
});

gulp.task('js', function () {
    gulp.src(['src/*.js'])
        .pipe(jsmin())
        .pipe(gulp.dest('build'))
});

gulp.task('clean', function () {
    clean(['build']);
});

gulp.task('clone', function () {
    gulp.src(['static/**'])
        .pipe(gulp.dest('build/static/'))
});

gulp.task('default', ['clean', 'clone', 'html', 'css', 'js' ]);

webpack vs gulp

下麵對比 webpack 和 gulp 各自適合的場景

  • webpack

    • 基於模塊依賴的打包構建
    • 模塊切割
    • 公共模塊提取
  • gulp

    • 與模塊化無關的構建過程
    • js / css 批量壓縮
    • 圖片壓縮
    • 批量文本替換
    • 複雜的任務處理
    • 項目發布任務
    • 啟動服務器

最後更新:2017-07-26 09:32:57

  上一篇:go  CVPR論文解讀 | 剁手有了新方法,明星同款邊看邊買
  下一篇:go  首次曝光!在線視頻衣物精確檢索技術,開啟刷劇敗明星同款時代