【翻譯向】webpack2 指南(中)

原文發佈與抹橋的博客-【翻譯向】webpack2 指南(中)javascript

動態模塊替換(Hot Module Repalcement -React)

就像以前 理念頁面 中解析的細節那樣,動態模塊替換(HMR)會在應用運行時動態的替換、添加或者刪除模塊而不用從新刷新頁面。 HMR 很是有用,當應用只有一個狀態樹(single state tree)時。css

下面介紹的方法描述中使用了 Babel 和 React ,但這並非使用 HRM 所必須的工具。html

項目配置

這裏會指導你如何用 Babel, React 和 PostCss 一塊兒使用 HMR 去演示一個項目。爲了可以跟着下面走下去,須要把這些依賴添加到 package.json 中去。java

爲了使用 HMR,你須要以下這些依賴:node

npm install --save-dev babel@6.5.2 babel-core@6.13.2 babel-loader@6.2.4 babel-preset-es2015@6.13.2 babel-preset-react@6.11.1 babel-preset-stage-2@6.13.0 css-loader@0.23.1 postcss-loader@0.9.1 react-hot-loader@3.0.0-beta.6 style-loader@0.13.1 webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.0

同時,爲了達到咱們演示的目的,還須要:react

npm install --save react@15.3.0 react-dom@15.3.0

Babel Config

.babelrc 文件應該以下:webpack

{
  "presets": [
    ["es2015", {"modules": false}],
    // webpack understands the native import syntax, and uses it for tree shaking

    "stage-2",
    // Specifies what level of language features to activate.
    // Stage 2 is "draft", 4 is finished, 0 is strawman.
    // See https://tc39.github.io/process-document/

    "react"
    // Transpile React components to JavaScript
  ],
  "plugins": [
    "react-hot-loader/babel"
    // Enables React code to work with HMR.
  ]
}

Webpack Config

const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
  entry: [
    'react-hot-loader/patch',
    // activate HMR for React

    'webpack-dev-server/client?http://localhost:8080',
    // bundle the client for webpack-dev-server
    // and connect to the provided endpoint

    'webpack/hot/only-dev-server',
    // bundle the client for hot reloading
    // only- means to only hot reload for successful updates


    './index.js'
    // the entry point of our app
  ],
  output: {
    filename: 'bundle.js',
    // the output bundle

    path: resolve(__dirname, 'dist'),

    publicPath: '/'
    // necessary for HMR to know where to load the hot update chunks
  },

  context: resolve(__dirname, 'src'),

  devtool: 'inline-source-map',

  devServer: {
    hot: true,
    // enable HMR on the server

    contentBase: resolve(__dirname, 'dist'),
    // match the output path

    publicPath: '/'
    // match the output `publicPath`
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'babel-loader',
        ],
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader?modules',
          'postcss-loader',
        ],
      },
    ],
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // enable HMR globally

    new webpack.NamedModulesPlugin(),
    // prints more readable module names in the browser console on HMR updates
  ],
};

上面有不少配置,但不是全部都和 HMR 有關。能夠經過查閱 webpack-dev-server options 和concept pages 來加深理解。git

咱們基礎設想是這樣的,你的 JavaScript 入口文件在 ./src/index.js 且你使用 CSS Module 來編寫樣式文件。github

配置文件中須要重點關注的是 devServerentry key. HotModueReplacementPlugin 一樣須要被包含在 plugins key 中。web

爲了達到目的,咱們引入了兩個模塊:

  • react-hot-loader 添加到了入口中, 是爲了可以使 React 支持 HMR

  • 爲了更好的理解 HMR 每次更新的時候作了哪些事情,咱們添加了 NamedModulePlugin

Code

// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';

import { AppContainer } from 'react-hot-loader';
// AppContainer is a necessary wrapper component for HMR

import App from './components/App';

const render = (Component) => {
  ReactDOM.render(
  <AppContainer>
  <Component/>
  </AppContainer>,
    document.getElementById('root')
  );
};

render(App);

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept('./components/App', () => {
    const NewApp = require('./components/App').default
    render(NewApp)
  });
}
// ./src/components/App.js
import React from 'react';
import styles from './App.css';

const App = () => (
  <div className={styles.app}>
    <h2>Hello, </h2>
  </div>
);

export default App;
.app {
    text-size-adjust: none;
    font-family: helvetica, arial, sans-serif;
    line-height: 200%;
    padding: 6px 20px 30px;
}

一個須要特別注意的是 module 的引用:

  1. Webpack 會暴露出 module.hot 給咱們的代碼,當咱們設置 devServer: { hot: true } 時;

  2. 這樣咱們可使用 module.hot 來給特定的資源棄用 HMR (這裏是 App.js). 這裏有一個很是重要的 API module.hot.accept ,用來決定如何處理這些特定的依賴。

  3. 須要注意的是,webpack2 內建支持 ES2015 模塊語法,你不須要在 module.hot.accept 中從新引用跟組件。爲了達到這個目的,須要在 .babelrc 配置 Babel ES2015 的預先設置:

    ["es2015", {"modules": false}]

    就像咱們在以前 Babel Config 中配置的那樣。須要注意,禁用 Babel 的模塊功能 不只僅是爲了啓用 HMR。若是你不關掉這個配置,那麼你會碰到須要問題。

  4. 若是你在 webpack2 的配置文件中使用 ES6 模塊,而且你按照 #3 修改了 .babelrc,那麼你須要使用 require 語法來建立兩個 .babelrc 文件:

    1. 一個放在根目錄下面並配置爲 "presets: ["es2015"]"

    2. 一個放在 webpack 要編譯的文件夾下,好比在這個例子中,就是 src/
      因此在這個案例中,module.hot.accept 會執行 render 方法不管 src/compoents/App.js 或者其它的依賴文件變化的時候 ——這意味着當 App.css 被引入到 App.js 中之後,即便是 App.css 被修改,

    render 方法一樣會被執行。

Index.html

入口頁面須要被放在頁面 dist 下面,webpack-dev-server 的運行須要這個文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Example Index</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>

Package.json

最後,咱們須要啓動 webpack-dev-server 來打包咱們的代碼而且看看 HMR 是如何工做的。咱們可使用以下的 package.json 入口:

{
  "scripts" : {
    "start" : "webpack-dev-server"
  }
}

執行 npm start, 打開瀏覽器輸入 http://localhost:8080, 應該能夠看到下面這些項展現在 console.log中:

dev-server.js:49[HMR] Waiting for update signal from WDS…
only-dev-server.js:74[HMR] Waiting for update signal from WDS…
client?c7c8:24 [WDS] Hot Module Replacement enabled.

而後編輯而且修改 App.js 文件,你會在 console.log 中看到相似以下的日誌:

[WDS] App updated. Recompiling…
client?c7c8:91 [WDS] App hot update…
dev-server.js:45 [HMR] Checking for updates on the server…
log-apply-result.js:20 [HMR] Updated modules:
log-apply-result.js:22 [HMR]  - ./components/App.js
dev-server.js:27 [HMR] App is up to date.

注意 HMR 指出了更新模塊的路徑。這是由於咱們使用了 NamedModulesPlugin.

開發環境(Development)

這個章節介紹在開發過程當中可使用的一些工具。

須要注意,不能在生產環境使用

Source Map

當 JS 發生異常的時候,咱們須要指導是哪個文件的哪一行出錯了。可是當文件都被 webpack 打包之後,找問題會變得很麻煩。
Source Map 就是爲了解決這個問題的。它有不少不一樣的選項,每一種都有的好處和不足。在一開始,咱們使用:

devtool: "cheap-eval-source-map"

選擇一個工具(Choosing a Tool)

Webpack 可被用於監視模式(watch mode)。這種模式下, webpack 會監視你的文件,當它們有變更的時候就會重編譯。Webpack-dev-server 提供了一個很方便使用的開發環境的服務,而且支持自動刷新功能。若是你已經有了一個開發環境的服務,而且但願可以擁有更好的適應性,那麼 webpack-dev-middleware 能夠被用做一箇中間件來達到這個目的。

Webpack-dev-server 和 webpack-dev-middleware 實在內存中進行編譯的,這意味着打包後的代碼包並不會保存到本地磁盤中。這回使打包變得很快,同時不會產生不少臨時文件來污染你的本地文件系統。

大多數狀況下,你都會想要去使用 webpack-dev-server, 由於它使用起來很方便,並且提供了許多開箱即用的功能。

Webpack 監視模式(wtach mode)

Webpack 的監視模式會檢測文件的變更。只要變更被檢測到,它就會從新進行一次編譯。咱們但願它的編譯過程能有一個很好的進度展現。那麼就執行如下命令:

webpack --progress --watch

隨便修改一個文件而後保存,你就會看到從新編譯的過程。

監視模式沒有考慮任何和服務有關的問題,因此你須要本身提供一個服務。一個簡單的服務就是 [server](https://github.com/tj/serve). 當安裝好後(npm i server -g),在你打包後的文件目錄下運行:

server

當每次從新編譯後,你都須要手動的去刷新瀏覽器。

webpack-dev-server

webpack-dev-server 提供一個支持自動刷新的服務。

首先,確認你 index.html 頁面裏面已經引用了你的代碼包。咱們先假設 output.filename 設置爲 bundle.js:

<script src="/bundle.js"></srcipt>

從 npm 安裝 webpack-dev-server

npm install webpack-dev-server --save-dev

而後就能夠執行 webpack-dev-server 的命令了:

webpack-dev-server --open

上面的命令會自動打開你的瀏覽器並指定到 http://localhost:8080.

修改一下你的文件並保存。你會發現代碼被從新打包了,當打包完成的時候,頁面會自動刷新。若是沒有如願達到效果,那麼你須要調整 watchOptions(https://webpack.js.org/configuration/dev-server#devserver-watchoptions-).

如今你有了一個能夠自動刷新的服務,接下來咱們看如何啓用動態模塊替換(Hot Module Replacement)。這是一個能夠提供不刷新頁面替換模塊的接口,查看這裏瞭解更多 。

webpack-dev-server 能夠作不少的事情,好比代理請求到你的後端服務。想了解更多的配置項,那就查看 devServer 的文檔吧

webpack-dev-middleware

webpack-dev-middleware 適用於基於中間件的連接堆棧(好難翻譯)。當你已經有一個 Node.js 服務或者你想要徹底的控制服務的時候會頗有用。

這個中間件會讓文件編譯在內存中進行。當一個編譯在進行過程當中,它會延遲一個文件請求,直到它編譯完成。

首先從 npm 上安裝:

npm install express webpack-dev-server --save-dev

做爲一個例子,咱們能夠這樣使用中間件:

var express = require("express");
var webpackDevMiddleware = require("webpack-dev-middleware");
var webpack = require("webpack");
var webpackConfig = require("./webpack.config");

var app = express();
var compiler = webpack(webpackConfig);

app.use(webpackDevMiddleware(compiler, {
  publicPath: "/" // Same as `output.publicPath` in most cases.
}));

app.listen(3000, function () {
  console.log("Listening on port 3000!");
});

根據你在 output.publicPathoutput.filename 中的配置,你打包的代碼應該能夠經過 http://localhost:3000/bundle.js 訪問。

默認狀況下使用的是監視模式。它還支持懶模式(lazy mode),只有在有請求進來的時候纔會從新編譯。

配置以下:

app.use(webpackDevMiddleware(compiler, {
  lazy: true,
  filename: "bundle.js" // Same as `output.filename` in most cases.
}));

還有許多其它有用的選項,詳細內容能夠查看 文檔.

爲生產環境構建(Building for Production)

本章介紹如何用 webpack 來作生產環境的構建。

一條自動化的方式

執行 webpack -p(等同於 webpack --optimize--minimize --define process.env.NODE_ENV="production").
這會執行如下幾個步驟:

  • 使用 UglifyJsPlugin 壓縮文件

  • 執行了 LoaderOptionsPlugin, 查看文檔

  • 設置 Node 的環境變量

源碼壓縮

webpack 使用 UglifyJsPlugin 來壓縮源碼,經過執行 UglifyJs 來達到壓縮輸出代碼的目的。這個插件支持全部 UgilfyJs 的功能。在命令行裏輸入 --optimize-minimize ,那麼至關與在配置文件中添加了如下配置:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  /*...*/
  plugins:[
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)
    })
  ]
};

這樣,基於 devtools option ,在打包的時候會生成 Source Map.

資源映射(Source Map)

咱們推薦在開發環境啓用 Source Map. 由於在 debug 或者測試的時候頗有用。Webpack 能夠生成包含在代碼包或者分離文件中的 inline Source Map.

在配置文件中,經過修改 devtools 配置來設置 Source Map 類型。目前咱們支持七種不一樣類型的 Source Map. 能夠在具體文檔中找到更加詳細的介紹。

一個比較好好的選擇是使用 cheap-module-source-map,能夠將源映射簡化爲每行映射(simplifies the Source Maps to a single mapping per line)。

Node 環境變量

執行 webpack -p( --define process.env.NODE_EMV="production") 會經過以下的配置調用 DefinePlugin:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  /*...*/
  plugins:[
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

DefindPlugin 會在源碼中進行查找和替換的工做。全部找到的 process.env.NODE_ENV 都會被替換爲 production. 這樣,相似與 if(process.env.NODE_ENV !=='procution') console.log(…) 這樣的代碼就會被 UnglifyJs 認爲等同於 if(false) console.log(…) .

一個手動的方式:爲 webpack 配置不一樣環境變量下的配置文件

一個最簡單的方式來爲 webpack 配置不一樣環境變量下的配置文件的方法就是創建多個配置文件。好比:

dev.js

// 此處官網文檔有語法錯誤,我改了一下
module.exports = function (env) {
  return {
    devtool: 'cheap-module-source-map',
    output: {
      path: path.join(__dirname, '/../dist/assets'),
      filename: '[name].bundle.js',
      publicPath: publicPath,
      sourceMapFilename: '[name].map'
    },
    devServer: {
      port: 7777,
      host: 'localhost',
      historyApiFallback: true,
      noInfo: false,
      stats: 'minimal',
      publicPath: publicPath
    }
  }
}

prod.js

module.exports = function (env) {
  return {
    output: {
      path: path.join(__dirname, '/../dist/assets'),
      filename: '[name].bundle.js',
      publicPath: publicPath,
      sourceMapFilename: '[name].map'
    },
    plugins: [
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false
      }),
      new UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true
        },
        compress: {
          screw_ie8: true
        },
        comments: false
      })
    ]
  }
}

而後把咱們的 webpack.config.js 的內容改爲下面這樣:

function buildConfig(env) {
  return require('./config/' + env + '.js')({ env: env })
}

module.exports = buildConfig(env);

最後,在 package.json 中添加以下命令:

"build:dev": "webpack --env=dev --progress --profile --colors",
 "build:dist": "webpack --env=prod --progress --profile --colors",

能夠看到,咱們把環境變量傳給了 webpack.config.js 文件。從這裏咱們使用一個簡單的方式經過傳遞環境變量來決定使用正確的配置文件。

一個更加高級的途徑是咱們有一個基礎配置文件,裏面有全部共通的功能,而後在不一樣環境變量下的不一樣功能經過指定特定的文件,而後使用 webpack-merge 來合併成一個完整的配置。這樣能夠避免寫不少
重複的代碼。好比,相似與解析 js,ts,png,jpeg 等都是共通的功能,須要放在基礎配置文件裏面:

base.js

module.exports = function() {
  return {
    entry: {
      'polyfills': './src/polyfills.ts',
      'vendor': './src/vendor.ts',
      'main': './src/main.ts'

    },
    output: {
      path: path.join(__dirname, '/../dist/assets'),
      filename: '[name].bundle.js',
      publicPath: publicPath,
      sourceMapFilename: '[name].map'
    },
    resolve: {
      extensions: ['', '.ts', '.js', '.json'],
      modules: [path.join(__dirname, 'src'), 'node_modules']

    },
    module: {
      loaders: [{
        test: /\.ts$/,
        loaders: [
          'awesome-typescript-loader',
          'angular2-template-loader'
        ],
        exclude: [/\.(spec|e2e)\.ts$/]
      }, {
        test: /\.css$/,
        loaders: ['to-string-loader', 'css-loader']
      }, {
        test: /\.(jpg|png|gif)$/,
        loader: 'file'
      }, {
        test: /\.(woff|woff2|eot|ttf|svg)$/,
        loader: 'url-loader?limit=100000'
      }],
    },
    plugins: [
      new ForkCheckerPlugin(),

      new webpack.optimize.CommonsChunkPlugin({
        name: ['polyfills', 'vendor'].reverse()
      }),
      new HtmlWebpackPlugin({
        template: 'src/index.html',
        chunksSortMode: 'dependency'
      })
    ],
  };
}

而後使用 webpack-merge 來合併特定環境變量下指定的配置文件。來看一個合併生產環境下特定配置的例子(和上面 prod.js 對比如下):

prod.js(updated)

const webpackMerge = require('webpack-merge');

const commonConfig = require('./base.js');

module.exports = function(env) {
  return webpackMerge(commonConfig(), {
    plugins: [
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false
      }),
      new webpack.DefinePlugin({
        'process.env': {
          'NODE_ENV': JSON.stringify('prod')
        }
      }),
      new webpack.optimize.UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true
        },
        compress: {
          screw_ie8: true
        },
        comments: false
      })
    ]
  })
}

能夠注意到,在 'prod.js' 中主要有三處更新,分別是:

• 經過 'webpack-meger' 合併了 `base.js`
• 把 `output` 屬性移到了 `base.js` 中。咱們只要關心在 `base.js` 中之外的不一樣的配置就能夠了
• 經過 `DefinePlugin` 把 `process.env.NODE_ENV` 設置爲 `prod`. 這樣,整個應用代碼中的 `process.env.NODE_ENV` 都有一個爲 `prod` 的值了。

哪些須要在不一樣的環境變量下保持一致都由你來決定。這裏只是經過一個 DEMO 來典型的說明一下如何在不一樣的環境變量下保持部分配置的統一。

能夠看到,webpack-merge 是多麼強大,可讓咱們避免寫不少重複的代碼(外國人話真多)。

React 懶加載(Lazy Loading - React)

經過使用高階函數可使一個組件懶加載它的依賴而不須要它的消費者知道,或者使用一個接收函數或者模塊的組件,可使一個消費者能夠懶加載它的子組件而不須要它的子組件知道。

組件懶加載

先看一個消費者選擇去懶加載一些組件。importLazy 是一個返回 defualt 屬性的函數,這是爲了能和 Babel/ES2015 互通。若是你不須要,能夠忽略掉 importLazy 方法。importLazy 只是簡單的返回了經過 export default 暴露出的模塊。

<LazilyLoad modules={{
  TodoHandler: () => importLazy(import('./components/TodoHandler')),
  TodoMenuHandler: () => importLazy(import('./components/TodoMenuHandler')),
  TodoMenu: () => importLazy(import('./components/TodoMenu')),
}}>
  {({TodoHandler, TodoMenuHandler, TodoMenu}) => (
    <TodoHandler>
      <TodoMenuHandler>
        <TodoMenu />
      </TodoMenuHandler>
    </TodoHandler>
  )}
</LazilyLoad>

高階組件(Higher Order Component)

做爲一個組件,你能夠確保整個組件自己的依賴是懶加載的。當一個組件依賴一個很是大的庫文件的時候會頗有用。假設咱們要寫一個支持代碼高亮的 Todo 組件:

class Todo extends React.Component {
  render() {
    return (
      <div>
        {this.props.isCode ? <Highlight>{content}</Highlight> : content}
      </div>
    );
  }
}

咱們能夠確保只有當咱們須要代碼高亮功能的時候纔去加載這個代價高昂的庫文件:

// Highlight.js
class Highlight extends React.Component {
  render() {
    const {Highlight} = this.props.highlight;
    // highlight js is now on our props for use
  }
}
export LazilyLoadFactory(Highlight, {
  highlight: () => import('highlight'),
});

注意這個 Highlight 組件的消費者是如何在不知情的狀況下被懶加載的。

完整的代碼

LazilyLoad 組件的源碼,暴露了組件接口和高階組件接口。

import React from 'react';

class LazilyLoad extends React.Component {

  constructor() {
    super(...arguments);
    this.state = {
      isLoaded: false,
    };
  }

  componentWillMount() {
    this.load(this.props);
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillReceiveProps(next) {
    if (next.modules === this.props.modules) return null;
    this.load(next);
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  load(props) {
    this.setState({
      isLoaded: false,
    });

    const { modules } = props;
    const keys = Object.keys(modules);

    Promise.all(keys.map((key) => modules[key]()))
      .then((values) => (keys.reduce((agg, key, index) => {
        agg[key] = values[index];
        return agg;
      }, {})))
      .then((result) => {
        if (!this._isMounted) return null;
        this.setState({ modules: result, isLoaded: true });
      });
  }

  render() {
    if (!this.state.isLoaded) return null;
    return React.Children.only(this.props.children(this.state.modules));
  } 
}

LazilyLoad.propTypes = {
  children: React.PropTypes.func.isRequired,
};

export const LazilyLoadFactory = (Component, modules) => {
  return (props) => (
    <LazilyLoad modules={modules}>
      {(mods) => <Component {...mods} {...props} />}
    </LazilyLoad>
  );
};

export const importLazy = (promise) => (
  promise.then((result) => result.default)
);

export default LazilyLoad;

提示

  • 經過使用 bundle loader 能夠語義化命名代碼塊,一次來智能的加載一組代碼

  • 確保你使用了 babel-preset-2015, 而且設置 modules 爲 false,這容許 webpack 去處理 modules

公開路徑?(Public Path)

Webpack 提供了一個很長有用的功能,能夠設置你應用中全部資源引用的基礎路徑。它被稱之爲 publicPath.

使用場景(Use case)

這裏有一些真實應用中的場景,經過這個功能來達到目的。

在構建的時候設置值

在開發模式下,咱們一般會把 assets/ 目錄放在和入口頁同級的目錄下面。這樣沒有問題,可是假如在生產環境下你的靜態資源是存放在 CDN 上那又該怎麼辦呢?

能夠很方便的經過環境變量來解決這個問題。假設咱們有一個變量 ASSET_PATH:

// 這裏看起來好像有問題
import webpack from 'webpack';

// Whatever comes as an environment variable, otherwise use root
const ASSET_PATH = process.env.ASSET_PATH || '/';

export default {
  output: {
    publicPath: ASSET_PATH
  },

  plugins: [
    // This makes it possible for us to safely use env vars on our code
    new webpack.DefinePlugin({
      'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH)
    })
  ]
};

在開發中設置值(Set Value on the fly)

另外一種方式是在開發過程成設置 public 路徑。Webpack 暴露了一個全局變量 __webpack_public_path__ 來讓咱們達到這個目的。因此在你的入口文件中,你能夠這樣作:

__webpack_publick_path__ = process.en.ASSET_PATH;

如何來作都取決於你。當咱們經過 DefinePlugin 進行了配置之後, process.env.ASSET_PATH 在任何地方均可以直接拿來使用。

相關文章
相關標籤/搜索