webpack實戰

webpack實戰

查看全部文檔頁面: 全棧開發,獲取更多信息。

馬不停蹄,加班加點,終於把這個文檔整理出來了,順便深刻地學習一番,鞏固知識,就是太累人,影響睡眠時間和質量。極客就是想要把事情作到極致,開始了就必須到達終點。css

原文連接:webpack實戰,原文廣告模態框遮擋,閱讀體驗很差,因此整理成本文,方便查找。html

本章教你如何用 Webpack 去解決實際項目中常見的場景。前端

按照不一樣場景劃分紅如下幾類:vue

  • 使用新語言來開發項目:node

    • 使用 ES6 語言
    • 使用 TypeScript 語言
    • 使用 Flow 檢查器
    • 使用 SCSS 語言
    • 使用 PostCSS
  • 使用新框架來開發項目:react

    • 使用 React 框架
    • 使用 Vue 框架
    • 使用 Angular2 框架
  • 用 Webpack 構建單頁應用:webpack

    • 爲單頁應用生成 HTML
    • 管理多個單頁應用
  • 用 Webpack 構建不一樣運行環境的項目:git

    • 構建同構應用
    • 構建 Electron 應用
    • 構建 Npm 模塊
    • 構建離線應用
  • Webpack 結合其它工具搭配使用,各取所長:github

    • 搭配 Npm Script
    • 檢查代碼
    • 經過 Node.js API 啓動 Webpack
    • 使用 Webpack Dev Middleware
  • 用 Webpack 加載特殊類型的資源:web

    • 加載圖片
    • 加載SVG
    • 加載 Source Map

使用 TypeScript 語言

因爲本文不推薦使用TypeScript,ES6就足夠完成大部分任務。原文連接:使用 TypeScript 語言

使用 Angular2 框架

Angular2不在個人技術棧範圍,因此這一章不加入,有興趣的查看原文:使用 Angular2 框架

使用ES6語言

一般咱們須要把採用 ES6 編寫的代碼轉換成目前已經支持良好的 ES5 代碼,這包含2件事:

  1. 把新的 ES6 語法用 ES5 實現,例如 ES6 的 class 語法用 ES5 的 prototype 實現。
  2. 給新的 API 注入 polyfill ,例如使用新的 fetch API 時注入對應的 polyfill 後才能讓低端瀏覽器正常運行。

Babel

Babel 能夠方便的完成以上2件事。

Babel 是一個 JavaScript 編譯器,能將 ES6 代碼轉爲 ES5 代碼,讓你使用最新的語言特性而不用擔憂兼容性問題,而且能夠經過插件機制根據需求靈活的擴展。

在 Babel 執行編譯的過程當中,會從項目根目錄下的 .babelrc 文件讀取配置。.babelrc 是一個 JSON 格式的文件,內容大體以下:

{
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false
      }
    ]
   ],
  "presets": [
    [
      "es2015",
      {
        "modules": false
      }
    ],
    "stage-2",
    "react"
  ]
}

Plugins

plugins 屬性告訴 Babel 要使用哪些插件,插件能夠控制如何轉換代碼。

以上配置文件裏的 transform-runtime 對應的插件全名叫作 babel-plugin-transform-runtime,即在前面加上了 babel-plugin-,要讓 Babel 正常運行咱們必須先安裝它:

npm i -D babel-plugin-transform-runtime

babel-plugin-transform-runtime 是 Babel 官方提供的一個插件,做用是減小冗餘代碼。

Babel 在把 ES6 代碼轉換成 ES5 代碼時一般須要一些 ES5 寫的輔助函數來完成新語法的實現,例如在轉換 class extent 語法時會在轉換後的 ES5 代碼裏注入 _extent 輔助函數用於實現繼承:

function _extent(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i];
    for (var key in source) {
      if (Object.prototype.hasOwnProperty.call(source, key)) {
        target[key] = source[key];
      }
    }
  }
  return target;
}

這會致使每一個使用了 class extent 語法的文件都被注入重複的 _extent 輔助函數代碼,babel-plugin-transform-runtime 的做用在於不把輔助函數內容注入到文件裏,而是注入一條導入語句:

var _extent = require('babel-runtime/helpers/_extent');

這樣能減少 Babel 編譯出來的代碼的文件大小。

同時須要注意的是因爲 babel-plugin-transform-runtime 注入了 require('babel-runtime/helpers/_extent') 語句到編譯後的代碼裏,須要安裝 babel-runtime 依賴到你的項目後,代碼才能正常運行。 也就是說 babel-plugin-transform-runtimebabel-runtime 須要配套使用,使用了 babel-plugin-transform-runtime 後必定須要 babel-runtime

Presets

presets 屬性告訴 Babel 要轉換的源碼使用了哪些新的語法特性,一個 Presets 對一組新語法特性提供支持,多個 Presets 能夠疊加。

Presets 實際上是一組 Plugins 的集合,每個 Plugin 完成一個新語法的轉換工做。Presets 是按照 ECMAScript 草案來組織的,一般能夠分爲如下三大類:

  1. 已經被寫入 ECMAScript 標準裏的特性,因爲以前每一年都有新特性被加入到標準裏;

    • env 包含當前全部 ECMAScript 標準裏的最新特性。
  2. 被社區提出來的但還未被寫入 ECMAScript 標準裏特性,這其中又分爲如下四種:

    • stage0 只是一個美好激進的想法,有 Babel 插件實現了對這些特性的支持,可是不肯定是否會被定爲標準;
    • stage1 值得被歸入標準的特性;
    • stage2 該特性規範已經被起草,將會被歸入標準裏;
    • stage3 該特性規範已經定稿,各大瀏覽器廠商和 Node.js 社區開始着手實現;
    • stage4 在接下來的一年將會加入到標準裏去。
  3. 爲了支持一些特定應用場景下的語法,和 ECMAScript 標準沒有關係,例如 babel-preset-react 是爲了支持 React 開發中的 JSX 語法。

在實際應用中,你須要根據項目源碼所使用的語法去安裝對應的 Plugins 或 Presets。

接入 Babel

因爲 Babel 所作的事情是轉換代碼,因此應該經過 Loader 去接入 Babel,Webpack 配置以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
      },
    ]
  },
  // 輸出 source-map 方便直接調試 ES6 源碼
  devtool: 'source-map'
};

配置命中了項目目錄下全部的 JavaScript 文件,經過 babel-loader 去調用 Babel 完成轉換工做。 在從新執行構建前,須要先安裝新引入的依賴:

# Webpack 接入 Babel 必須依賴的模塊
npm i -D babel-core babel-loader 
# 根據你的需求選擇不一樣的 Plugins 或 Presets
npm i -D babel-preset-env

使用SCSS語言

SCSS 可讓你用更靈活的方式寫 CSS。 它是一種 CSS 預處理器,語法和 CSS 類似,但加入了變量、邏輯等編程元素,代碼相似這樣:

$blue: #1875e7; 

div {
  color: $blue;
}

SCSS 又叫 SASS,區別在於 SASS 語法相似 Ruby,而 SCSS 語法相似 CSS,對於熟悉 CSS 的前端工程師來講會更喜歡 SCSS。

採用 SCSS 去寫 CSS 的好處在於能夠方便地管理代碼,抽離公共的部分,經過邏輯寫出更靈活的代碼。 和 SCSS 相似的 CSS 預處理器還有 LESS 等。

使用 SCSS 能夠提高編碼效率,可是必須把 SCSS 源代碼編譯成能夠直接在瀏覽器環境下運行的 CSS 代碼。

node-sass 核心模塊是由 C++ 編寫,再用 Node.js 封裝了一層,以供給其它 Node.js 調用。 node-sass 還支持經過命令行調用,先安裝它到全局:

npm i -g node-sass

再執行編譯命令:

# 把 main.scss 源文件編譯成 main.css
node-sass main.scss main.css

你就能在源碼同目錄下看到編譯後的 main.css 文件。

接入 Webpack

Webpack 接入 sass-loader 相關配置以下:

module.exports = {
  module: {
    rules: [
      {
        // 增長對 SCSS 文件的支持
        test: /\.scss/,
        // SCSS 文件的處理順序爲先 sass-loader 再 css-loader 再 style-loader
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ]
  },
};

以上配置經過正則 /\.scss/ 匹配全部以 .scss 爲後綴的 SCSS 文件,再分別使用3個 Loader 去處理。具體處理流程以下:

  1. 經過 sass-loader 把 SCSS 源碼轉換爲 CSS 代碼,再把 CSS 代碼交給 css-loader 去處理。
  2. css-loader 會找出 CSS 代碼中的 @importurl() 這樣的導入語句,告訴 Webpack 依賴這些資源。同時還支持 CSS Modules、壓縮 CSS 等功能。處理完後再把結果交給 style-loader 去處理。
  3. style-loader 會把 CSS 代碼轉換成字符串後,注入到 JavaScript 代碼中去,經過 JavaScript 去給 DOM 增長樣式。若是你想把 CSS 代碼提取到一個單獨的文件而不是和 JavaScript 混在一塊兒,可使用1-5 使用Plugin 中介紹過的 ExtractTextPlugin。

因爲接入 sass-loader,項目須要安裝這些新的依賴:

# 安裝 Webpack Loader 依賴
npm i -D  sass-loader css-loader style-loader
# sass-loader 依賴 node-sass
npm i -D node-sass

使用Flow檢查器

Flow 是一個 Facebook 開源的 JavaScript 靜態類型檢測器,它是 JavaScript 語言的超集。

你所須要作的就是在須要的地方加上類型檢查,例如在兩個由不一樣人開發的模塊對接的接口出加上靜態類型檢查,能在編譯階段就指出部分模塊使用不當的問題。 同時 Flow 也能經過類型推斷檢查出 JavaScript 代碼中潛在的 Bug。

Flow 使用效果以下:

// @flow

// 靜態類型檢查
function square1(n: number): number {
  return n * n;
}
square1('2'); // Error: square1 須要傳入 number 做爲參數

// 類型推斷檢查
function square2(n) {
  return n * n; // Error: 傳入的 string 類型不能作乘法運算
}
square2('2');
須要注意的時代碼中的第一行 // @flow 告訴 Flow 檢查器這個文件須要被檢查。

使用 Flow

Flow 檢測器由高性能跨平臺的 OCaml 語言編寫,它的可執行文件能夠經過:

npm i -D flow-bin

安裝,安裝完成後經過先配置 Npm Script:

"scripts": {
   "flow": "flow"
}

再經過 npm run flow 去調用 Flow 執行代碼檢查。

除此以外你還能夠經過:

npm i -g flow-bin

把 Flow 安裝到全局後,再直接經過 flow 命令去執行代碼檢查。

安裝成功後,在項目根目錄下執行 Flow 後,Flow 會遍歷出全部須要檢查的文件並對其進行檢查,輸出錯誤結果到控制檯。

採用了 Flow 靜態類型語法的 JavaScript 是沒法直接在目前已有的 JavaScript 引擎中運行的,要讓代碼能夠運行須要把這些靜態類型語法去掉。

// 採用 Flow 的源代碼
function foo(one: any, two: number, three?): string {}

// 去掉靜態類型語法後輸出代碼
function foo(one, two, three) {}

有兩種方式能夠作到這點:

  1. flow-remove-types 可單獨使用,速度快。
  2. babel-preset-flow 與 Babel 集成。

集成 Webpack

因爲使用了 Flow 項目通常都會使用 ES6 語法,因此把 Flow 集成到使用 Webpack 構建的項目裏最方便的方法是藉助 Babel。

  1. 安裝 npm i -D babel-preset-flow 依賴到項目。
  2. 修改 .babelrc 配置文件,加入 Flow Preset:

    "presets": [
    ...[],
    "flow"
    ]

往源碼里加入靜態類型後從新構建項目,你會發現採用了 Flow 的源碼仍是能正常在瀏覽器中運行。

要明確構建的目的只是爲了去除源碼中的 Flow 靜態類型語法,而代碼檢查和構建無關。 許多編輯器已經整合 Flow,能夠實時在代碼中高亮指出 Flow 檢查出的問題。

使用PostCSS

PostCSS 是一個 CSS 處理工具,和 SCSS 不一樣的地方在於它經過插件機制能夠靈活的擴展其支持的特性,而不是像 SCSS 那樣語法是固定的。 PostCSS 的用處很是多,包括給 CSS 自動加前綴、使用下一代 CSS 語法等,目前愈來愈多的人開始用它,它極可能會成爲 CSS 預處理器的最終贏家。

PostCSS 和 CSS 的關係就像 Babel 和 JavaScript 的關係,它們解除了語法上的禁錮,經過插件機制來擴展語言自己,用工程化手段給語言帶來了更多的可能性。

PostCSS 和 SCSS 的關係就像 Babel 和 TypeScript 的關係,PostCSS 更加靈活、可擴張性強,而 SCSS 內置了大量功能而不能擴展。

給 CSS 自動加前綴,增長各瀏覽器的兼容性:

/*輸入*/
h1 {
  display: flex;
}

/*輸出*/
h1 {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
}

使用下一代 CSS 語法:

/*輸入*/
:root {
  --red: #d33;
}

h1 {
  color: var(--red);
}


/*輸出*/
h1 { 
  color: #d33;
}

PostCSS 所有采用 JavaScript 編寫,運行在 Node.js 之上,即提供了給 JavaScript 代碼調用的模塊,也提供了可執行的文件。

在 PostCSS 啓動時,會從目錄下的 postcss.config.js 文件中讀取所需配置,因此須要新建該文件,文件內容大體以下:

module.exports = {
  plugins: [
    // 須要使用的插件列表
    require('postcss-cssnext')
  ]
}

其中的 postcss-cssnext 插件可讓你使用下一代 CSS 語法編寫代碼,再經過 PostCSS 轉換成目前的瀏覽器可識別的 CSS,而且該插件還包含給 CSS 自動加前綴的功能。

目前 Chrome 等現代瀏覽器已經能徹底支持 cssnext 中的全部語法,也就是說按照 cssnext 語法寫的 CSS 在不通過轉換的狀況下也能在瀏覽器中直接運行。

接入 Webpack

雖然使用 PostCSS 後文件後綴仍是 .css 但這些文件必須先交給 postcss-loader 處理一遍後再交給 css-loader

接入 PostCSS 相關的 Webpack 配置以下:

module.exports = {
  module: {
    rules: [
      {
        // 使用 PostCSS 處理 CSS 文件
        test: /\.css/,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
    ]
  },
};

接入 PostCSS 給項目帶來了新的依賴須要安裝,以下:

# 安裝 Webpack Loader 依賴
npm i -D postcss-loader css-loader style-loader
# 根據你使用的特性安裝對應的 PostCSS 插件依賴
npm i -D postcss-cssnext

使用React框架

React 語法特徵

使用了 React 項目的代碼特徵有 JSX 和 Class 語法,例如:

class Button extends Component {
  render() {
    return <h1>Hello,Webpack</h1>
  }
}
在使用了 React 的項目裏 JSX 和 Class 語法並非必須的,但使用新語法寫出的代碼看上去更優雅。

其中 JSX 語法是沒法在任何現有的 JavaScript 引擎中運行的,因此在構建過程當中須要把源碼轉換成能夠運行的代碼,例如:

// 原 JSX 語法代碼
return <h1>Hello,Webpack</h1>

// 被轉換成正常的 JavaScript 代碼
return React.createElement('h1', null, 'Hello,Webpack')

React 與 Babel

要在使用 Babel 的項目中接入 React 框架是很簡單的,只須要加入 React 所依賴的 Presets babel-preset-react

經過如下命令:

# 安裝 React 基礎依賴
npm i -D react react-dom
# 安裝 babel 完成語法轉換所需依賴
npm i -D babel-preset-react

安裝新的依賴後,再修改 .babelrc 配置文件加入 React Presets

"presets": [
    "react"
],

就完成了一切準備工做。

再修改 main.js 文件以下:

import * as React from 'react';
import { Component } from 'react';
import { render } from 'react-dom';

class Button extends Component {
  render() {
    return <h1>Hello,Webpack</h1>
  }
}

render(<Button/>, window.document.getElementById('app'));

從新執行構建打開網頁你將會發現由 React 渲染出來的 Hello,Webpack

React 與 TypeScript

TypeScript 相比於 Babel 的優勢在於它原生支持 JSX 語法,你不須要從新安裝新的依賴,只需修改一行配置。 但 TypeScript 的不一樣在於:

  • 使用了 JSX 語法的文件後綴必須是 tsx
  • 因爲 React 不是採用 TypeScript 編寫的,須要安裝 reactreact-dom 對應的 TypeScript 接口描述模塊 @types/react@types/react-dom 後才能經過編譯。

修改 TypeScript 編譯器配置文件 tsconfig.json 增長對 JSX 語法的支持,以下:

{
  "compilerOptions": {
    "jsx": "react" // 開啓 jsx ,支持 React
  }
}

因爲 main.js 文件中存在 JSX 語法,再把 main.js 文件重命名爲 main.tsx,同時修改文件內容爲在上面 React 與 Babel 裏所採用的 React 代碼。 同時爲了讓 Webpack 對項目裏的 tstsx 原文件都採用 awesome-typescript-loader 去轉換, 須要注意的是 Webpack Loader 配置的 test 選項須要匹配到 tsx 類型的文件,而且 extensions 中也要加上 .tsx,配置以下:

module.exports = {
  // TS 執行入口文件
  entry: './main',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist'),
  },
  resolve: {
    // 先嚐試 ts,tsx 後綴的 TypeScript 源碼文件 
    extensions: ['.ts', '.tsx', '.js',] 
  },
  module: {
    rules: [
      {
        // 同時匹配 ts,tsx 後綴的 TypeScript 源碼文件 
        test: /\.tsx?$/,
        loader: 'awesome-typescript-loader'
      }
    ]
  },
  devtool: 'source-map',// 輸出 Source Map 方便在瀏覽器裏調試 TypeScript 代碼
};

經過npm i react react-dom @types/react @types/react-dom安裝新的依賴後重啓構建,從新打開網頁你將會發現由 React 渲染出來的 Hello,Webpack

使用Vue框架

Vue是一個漸進式的 MVVM 框架,相比於 React、Angular 它更靈活輕量。 它不會強制性地內置一些功能和語法,你能夠根據本身的須要一點點地添加功能。 雖然採用 Vue 的項目能用可直接運行在瀏覽器環境裏的代碼編寫,但爲了方便編碼大多數項目都會採用 Vue 官方的單文件組件的寫法去編寫項目。

Vue 的單文件組件經過一個相似 HTML 文件的 .vue 文件就能描述清楚一個組件所需的模版、樣式、邏輯。

main.js 入口文件:

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
});

入口文件建立一個 Vue 的根實例,在 ID 爲 app 的 DOM 節點上渲染出上面定義的 App 組件。

接入 Webpack

目前最成熟和流行的開發 Vue 項目的方式是採用 ES6 加 Babel 轉換,這和基本的採用 ES6 開發的項目很類似,差異在於要解析 .vue 格式的單文件組件。 好在 Vue 官方提供了對應的 vue-loader 能夠很是方便的完成單文件組件的轉換。

修改 Webpack 相關配置以下:

module: {
  rules: [
    {
      test: /\.vue$/,
      use: ['vue-loader'],
    },
  ]
}

安裝新引入的依賴:

# Vue 框架運行須要的庫
npm i -S vue
# 構建所需的依賴
npm i -D vue-loader css-loader vue-template-compiler

在這些依賴中,它們的做用分別是:

  • vue-loader:解析和轉換 .vue 文件,提取出其中的邏輯代碼 script、樣式代碼 style、以及 HTML 模版 template,再分別把它們交給對應的 Loader 去處理。
  • css-loader:加載由 vue-loader 提取出的 CSS 代碼。
  • vue-template-compiler:把 vue-loader 提取出的 HTML 模版編譯成對應的可執行的 JavaScript 代碼,這和 React 中的 JSX 語法被編譯成 JavaScript 代碼相似。預先編譯好 HTML 模版相對於在瀏覽器中再去編譯 HTML 模版的好處在於性能更好。

使用 TypeScript 編寫 Vue 應用

從 Vue 2.5.0+ 版本開始,提供了對 TypeScript 的良好支持,使用 TypeScript 編寫 Vue 是一個很好的選擇,由於 TypeScript 能檢查出一些潛在的錯誤。

新增 tsconfig.json 配置文件,內容以下:

{
  "compilerOptions": {
    // 構建出 ES5 版本的 JavaScript,與 Vue 的瀏覽器支持保持一致
    "target": "es5",
    // 開啓嚴格模式,這能夠對 `this` 上的數據屬性進行更嚴格的推斷
    "strict": true,
    // TypeScript 編譯器輸出的 JavaScript 採用 es2015 模塊化,使 Tree Shaking 生效
    "module": "es2015",
    "moduleResolution": "node"
  }
}

修改 App.vue 腳本部份內容以下:

<!--組件邏輯-->
<script lang="ts">
  import Vue from "vue";

  // 經過 Vue.extend 啓用 TypeScript 類型推斷
  export default Vue.extend({
    data() {
      return {
        msg: 'Hello,Webpack',
      }
    },
  });
</script>

注意 script 標籤中的 lang="ts" 是爲了指明代碼的語法是 TypeScript。

修改 main.ts 執行入口文件爲以下:

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
});

因爲 TypeScript 不認識 .vue 結尾的文件,爲了讓其支持 import App from './App.vue' 導入語句,還須要如下文件 vue-shims.d.ts 去定義 .vue 的類型:

// 告訴 TypeScript 編譯器 .vue 文件實際上是一個 Vue  
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

Webpack 配置須要修改兩個地方,以下:

const path = require('path');

module.exports = {
  resolve: {
    // 增長對 TypeScript 的 .ts 和 .vue 文件的支持
    extensions: ['.ts', '.js', '.vue', '.json'],
  },
  module: {
    rules: [
      // 加載 .ts 文件
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
        options: {
          // 讓 tsc 把 vue 文件當成一個 TypeScript 模塊去處理,以解決 moudle not found 的問題,tsc 自己不會處理 .vue 結尾的文件
          appendTsSuffixTo: [/\.vue$/],
        }
      },
    ]
  },
};

除此以外還須要安裝新引入的依賴:npm i -D ts-loader typescript

爲單頁應用生成HTML

引入問題

在使用 React 框架中,是用最簡單的 Hello,Webpack 做爲例子讓你們理解, 這個例子裏由於只輸出了一個 bundle.js 文件,因此手寫了一個 index.html 文件去引入這個 bundle.js,才能讓應用在瀏覽器中運行起來。

在實際項目中遠比這複雜,一個頁面經常有不少資源要加載。接下來舉一個實戰中的例子,要求以下:

  1. 項目採用 ES6 語言加 React 框架。
  2. 給頁面加入 Google Analytics,這部分代碼須要內嵌進 HEAD 標籤裏去。
  3. 給頁面加入 Disqus 用戶評論,這部分代碼須要異步加載以提高首屏加載速度。
  4. 壓縮和分離 JavaScript 和 CSS 代碼,提高加載速度。

在開始前先來看看該應用最終發佈到線上的代碼

能夠看到部分代碼被內嵌進了 HTML 的 HEAD 標籤中,部分文件的文件名稱被打上根據文件內容算出的 Hash 值,而且加載這些文件的 URL 地址也被正常的注入到了 HTML 中。

解決方案

推薦一個用於方便地解決以上問題的 Webpack 插件 web-webpack-plugin。 該插件已經被社區上許多人使用和驗證,解決了你們的痛點得到了不少好評,下面具體介紹如何用它來解決上面的問題。

首先,修改 Webpack 配置

以上配置中,大多數都是按照前面已經講過的內容增長的配置,例如:

  • 增長對 CSS 文件的支持,提取出 Chunk 中的 CSS 代碼到單獨的文件中,壓縮 CSS 文件;
  • 定義 NODE_ENV 環境變量爲 production,以去除源碼中只有開發時才須要的部分;
  • 給輸出的文件名稱加上 Hash 值;
  • 壓縮輸出的 JavaScript 代碼。

但最核心的部分在於 plugins 裏的:

new WebPlugin({
  template: './template.html', // HTML 模版文件所在的文件路徑
  filename: 'index.html' // 輸出的 HTML 的文件名稱
})

其中 template: './template.html' 所指的模版文件 template.html 的內容是:

<head>
  <meta charset="UTF-8">
  <!--注入 Chunk app 中的 CSS-->
  <link rel="stylesheet" href="app?_inline">
  <!--注入 google_analytics 中的 JavaScript 代碼-->
  <script src="./google_analytics.js?_inline"></script>
  <!--異步加載 Disqus 評論-->
  <script src="https://dive-into-webpack.disqus.com/embed.js" async></script>
</head>
<body>
<div id="app"></div>
<!--導入 Chunk app 中的 JS-->
<script src="app"></script>
<!--Disqus 評論容器-->
<div id="disqus_thread"></div>
</body>

該文件描述了哪些資源須要被以何種方式加入到輸出的 HTML 文件中。

<link rel="stylesheet" href="app?_inline"> 爲例,按照正常引入 CSS 文件同樣的語法來引入 Webpack 生產的代碼。href 屬性中的 app?_inline 能夠分爲兩部分,前面的 app 表示 CSS 代碼來自名叫 app 的 Chunk 中,後面的 _inline 表示這些代碼須要被內嵌到這個標籤所在的位置。

一樣的 <script src="./google_analytics.js?_inline"></script> 表示 JavaScript 代碼來自相對於當前模版文件 template.html 的本地文件 ./google_analytics.js, 並且文件中的 JavaScript 代碼也須要被內嵌到這個標籤所在的位置。

也就是說資源連接 URL 字符串裏問號前面的部分表示資源內容來自哪裏,後面的 querystring 表示這些資源注入的方式。

除了 _inline 表示內嵌外,還支持如下屬性:

  • _dist 只有在生產環境下才引入該資源;
  • _dev 只有在開發環境下才引入該資源;
  • _ie 只有IE瀏覽器才須要引入的資源,經過 [if IE]>resource<![endif] 註釋實現。

這些屬性之間能夠搭配使用,互不衝突。例如 app?_inline&_dist 表示只在生產環境下才引入該資源,而且須要內嵌到 HTML 裏去。

WebPlugin 插件還支持一些其它更高級的用法,詳情能夠訪問該項目主頁閱讀文檔。

管理多個單頁應用

引入問題

在開始前先來看看該應用最終發佈到線上的代碼。

<html>
<head>
<meta charset="UTF-8">
<!--從多個頁面中抽離出的公共 CSS 代碼-->
<link rel="stylesheet" href="common_7cc98ad0.css">
<!--只有這個頁面須要的 CSS 代碼-->
<link rel="stylesheet" href="login_e31e214b.css">
<!--注入 google_analytics 中的 JS 代碼-->
<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');</script>
<!--異步加載 Disqus 評論-->
<script async="" src="https://dive-into-webpack.disqus.com/embed.js"></script>
</head>
<body>
<div id="app"></div>
<!--從多個頁面中抽離出的公共 JavaScript 代碼-->
<script src="common_a1d9142f.js"></script>
<!--只有這個頁面須要的 JavaScript 代碼-->
<script src="login_f926c4e6.js"></script>
<!--Disqus 評論容器-->
<div id="disqus_thread"></div>
</body>
</html>

構建出的目錄結構爲:

dist
├── common_029086ff.js
├── common_7cc98ad0.css
├── index.html
├── index_04c08fbf.css
├── index_b3d3761c.js
├── login.html
├── login_0a3feca9.js
└── login_e31e214b.css

若是按照上節的思路,可能須要爲每一個單頁應用配置一段以下代碼:

new WebPlugin({
  template: './template.html', // HTML 模版文件所在的文件路徑
  filename: 'login.html' // 輸出的 HTML 的文件名稱
})

而且把頁面對應的入口加入到 enrty 配置項中,就像這樣:

entry: {
  index: './pages/index/index.js',// 頁面 index.html 的入口文件
  login: './pages/login/index.js',// 頁面 login.html 的入口文件
}

當有新頁面加入時就須要修改 Webpack 配置文件,新插入一段以上代碼,這會致使構建代碼難以維護並且易錯。

解決方案

項目源碼目錄結構以下:

├── pages
│   ├── index
│   │   ├── index.css // 該頁面單獨須要的 CSS 樣式
│   │   └── index.js // 該頁面的入口文件
│   └── login
│       ├── index.css
│       └── index.js
├── common.css // 全部頁面都須要的公共 CSS 樣式
├── google_analytics.js
├── template.html
└── webpack.config.js

從目錄結構中能夠當作出下幾點要求:

  • 全部單頁應用的代碼都須要放到一個目錄下,例如都放在 pages 目錄下;
  • 一個單頁應用一個單獨的文件夾,例如最後生成的 index.html 相關的代碼都在 index 目錄下,login.html 同理;
  • 每一個單頁應用的目錄下都有一個 index.js 文件做爲入口執行文件。
雖然 AutoWebPlugin 強制性的規定了項目部分的目錄結構,但從實戰經驗來看這是一種優雅的目錄規範,合理的拆分了代碼,又能讓新人快速的看懂項目結構,也方便往後的維護。

Webpack 配置文件修改以下:

<p data-height="465" data-theme-id="0" data-slug-hash="gzJWwB" data-default-tab="js,result" data-user="whjin" data-embed-version="2" data-pen-title="webpack管理多個單頁應用" class="codepen">See the Pen webpack管理多個單頁應用 by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>

AutoWebPlugin 會找出 pages 目錄下的2個文件夾 indexlogin,把這兩個文件夾當作兩個單頁應用。 而且分別爲每一個單頁應用生成一個 Chunk 配置和 WebPlugin 配置。 每一個單頁應用的 Chunk 名稱就等於文件夾的名稱,也就是說 autoWebPlugin.entry() 方法返回的內容實際上是:

{
  "index":["./pages/index/index.js","./common.css"],
  "login":["./pages/login/index.js","./common.css"]
}

但這些事情 AutoWebPlugin 都會自動爲你完成,你不用操心,明白大體原理便可。

template.html 模版文件以下:

<html>
<head>
  <meta charset="UTF-8">
  <!--在這注入該頁面所依賴但沒有手動導入的 CSS-->
  <!--STYLE-->
  <!--注入 google_analytics 中的 JS 代碼-->
  <script src="./google_analytics.js?_inline"></script>
  <!--異步加載 Disqus 評論-->
  <script src="https://dive-into-webpack.disqus.com/embed.js" async></script>
</head>
<body>
<div id="app"></div>
<!--在這注入該頁面所依賴但沒有手動導入的 JavaScript-->
<!--SCRIPT-->
<!--Disqus 評論容器-->
<div id="disqus_thread"></div>
</body>
</html>

因爲這個模版文件被看成項目中全部單頁應用的模版,就不能再像上一節中直接寫 Chunk 的名稱去引入資源,由於須要被注入到當前頁面的 Chunk 名稱是不定的,每一個單頁應用都會有本身的名稱。 <!--STYLE--><!--SCRIPT--> 的做用在於保證該頁面所依賴的資源都會被注入到生成的 HTML 模版裏去。

web-webpack-plugin 能分析出每一個頁面依賴哪些資源,例如對於 login.html 來講,插件能夠肯定該頁面依賴如下資源:

  • 全部頁面都依賴的公共 CSS 代碼 common.css
  • 全部頁面都依賴的公共 JavaScrip 代碼 common.js
  • 只有這個頁面依賴的 CSS 代碼 login.css
  • 只有這個頁面依賴的 JavaScrip 代碼 login.css

因爲模版文件 template.html 裏沒有指出引入這些依賴資源的 HTML 語句,插件會自動將沒有手動導入但頁面依賴的資源按照不一樣類型注入到 <!--STYLE--><!--SCRIPT--> 所在的位置。

  • CSS 類型的文件注入到 <!--STYLE--> 所在的位置,若是 <!--STYLE--> 不存在就注入到 HTML HEAD 標籤的最後;
  • JavaScrip 類型的文件注入到 <!--SCRIPT--> 所在的位置,若是 <!--SCRIPT--> 不存在就注入到 HTML BODY 標籤的最後。

若是後續有新的頁面須要開發,只須要在 pages 目錄下新建一個目錄,目錄名稱取爲輸出 HTML 文件的名稱,目錄下放這個頁面相關的代碼便可,無需改動構建代碼。

因爲 AutoWebPlugin 是間接的經過上一節提到的 WebPlugin 實現的,WebPlugin 支持的功能 AutoWebPlugin 都支持。

構建同構應用

同構應用是指寫一份代碼但可同時在瀏覽器和服務器中運行的應用。

認識同構應用

如今大多數單頁應用的視圖都是經過 JavaScript 代碼在瀏覽器端渲染出來的,但在瀏覽器端渲染的壞處有:

  • 搜索引擎沒法收錄你的網頁,由於展現出的數據都是在瀏覽器端異步渲染出來的,大部分爬蟲沒法獲取到這些數據。
  • 對於複雜的單頁應用,渲染過程計算量大,對低端移動設備來講可能會有性能問題,用戶能明顯感知到首屏的渲染延遲。

爲了解決以上問題,有人提出可否將本來只運行在瀏覽器中的 JavaScript 渲染代碼也在服務器端運行,在服務器端渲染出帶內容的 HTML 後再返回。 這樣就能讓搜索引擎爬蟲直接抓取到帶數據的 HTML,同時也能下降首屏渲染時間。 因爲 Node.js 的流行和成熟,以及虛擬 DOM 提出與實現,使這個假設成爲可能。

實際上如今主流的前端框架都支持同構,包括 React、Vue二、Angular2,其中最早支持也是最成熟的同構方案是 React。 因爲 React 使用者更多,它們之間又很類似,本節只介紹如何用 Webpack 構建 React 同構應用。

同構應用運行原理的核心在於虛擬 DOM,虛擬 DOM 的意思是不直接操做 DOM 而是經過 JavaScript Object 去描述本來的 DOM 結構。 在須要更新 DOM 時不直接操做 DOM 樹,而是經過更新 JavaScript Object 後再映射成 DOM 操做。

虛擬 DOM 的優勢在於:

  • 由於操做 DOM 樹是高耗時的操做,儘可能減小 DOM 樹操做能優化網頁性能。而 DOM Diff 算法能找出2個不一樣 Object 的最小差別,得出最小 DOM 操做;
  • 虛擬 DOM 的在渲染的時候不只僅能夠經過操做 DOM 樹來表示出結果,也能有其它的表示方式,例如把虛擬 DOM 渲染成字符串(服務器端渲染),或者渲染成手機 App 原生的 UI 組件( React Native)。

以 React 爲例,核心模塊 react 負責管理 React 組件的生命週期,而具體的渲染工做能夠交給 react-dom 模塊來負責。

react-dom 在渲染虛擬 DOM 樹時有2中方式可選:

  • 經過 render() 函數去操做瀏覽器 DOM 樹來展現出結果。
  • 經過 renderToString() 計算出表示虛擬 DOM 的 HTML 形式的字符串。

構建同構應用的最終目的是從一份項目源碼中構建出2份 JavaScript 代碼,一份用於在瀏覽器端運行,一份用於在 Node.js 環境中運行渲染出 HTML。 其中用於在 Node.js 環境中運行的 JavaScript 代碼須要注意如下幾點:

  • 不能包含瀏覽器環境提供的 API,例如使用 document 進行 DOM 操做,由於 Node.js 不支持這些 API;
  • 不能包含 CSS 代碼,由於服務端渲染的目的是渲染出 HTML 內容,渲染出 CSS 代碼會增長額外的計算量,影響服務端渲染性能;
  • 不能像用於瀏覽器環境的輸出代碼那樣把 node_modules 裏的第三方模塊和 Node.js 原生模塊(例如 fs 模塊)打包進去,而是須要經過 CommonJS 規範去引入這些模塊。
  • 須要經過 CommonJS 規範導出一個渲染函數,以用於在 HTTP 服務器中去執行這個渲染函數,渲染出 HTML 內容返回。

解決方案

用於構建瀏覽器環境代碼的 webpack.config.js 配置文件保留不變,新建一個專門用於構建服務端渲染代碼的配置文件 webpack_server.config.js,內容以下:

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  // JS 執行入口文件
  entry: './main_server.js',
  // 爲了避免把 Node.js 內置的模塊打包進輸出文件中,例如 fs net 模塊等
  target: 'node',
  // 爲了避免把 node_modules 目錄下的第三方模塊打包進輸出文件中
  externals: [nodeExternals()],
  output: {
    // 爲了以 CommonJS2 規範導出渲染函數,以給採用 Node.js 編寫的 HTTP 服務調用
    libraryTarget: 'commonjs2',
    // 把最終可在 Node.js 中運行的代碼輸出到一個 bundle_server.js 文件
    filename: 'bundle_server.js',
    // 輸出文件都放到 dist 目錄下
    path: path.resolve(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // CSS 代碼不能被打包進用於服務端的代碼中去,忽略掉 CSS 文件
        test: /\.css/,
        use: ['ignore-loader'],
      },
    ]
  },
  devtool: 'source-map' // 輸出 source-map 方便直接調試 ES6 源碼
};

以上代碼有幾個關鍵的地方,分別是:

  • target: 'node' 因爲輸出代碼的運行環境是 Node.js,源碼中依賴的 Node.js 原生模塊不必打包進去;
  • externals: [nodeExternals()] webpack-node-externals 的目的是爲了防止 node_modules 目錄下的第三方模塊被打包進去,由於 Node.js 默認會去 node_modules 目錄下尋找和使用第三方模塊;
  • {test: /\.css/, use: ['ignore-loader']} 忽略掉依賴的 CSS 文件,CSS 會影響服務端渲染性能,又是作服務端渲不重要的部分;
  • libraryTarget: 'commonjs2' 以 CommonJS2 規範導出渲染函數,以供給採用 Node.js 編寫的 HTTP 服務器代碼調用。

爲了最大限度的複用代碼,須要調整下目錄結構:

把頁面的根組件放到一個單獨的文件 AppComponent.js,該文件只能包含根組件的代碼,不能包含渲染入口的代碼,並且須要導出根組件以供給渲染入口調用,AppComponent.js 內容以下:

import React, { Component } from 'react';
import './main.css';

export class AppComponent extends Component {
  render() {
    return <h1>Hello,Webpack</h1>
  }
}

分別爲不一樣環境的渲染入口寫兩份不一樣的文件,分別是用於瀏覽器端渲染 DOM 的 main_browser.js 文件,和用於服務端渲染 HTML 字符串的 main_server.js 文件。

main_browser.js 文件內容以下:

import React from 'react';
import { render } from 'react-dom';
import { AppComponent } from './AppComponent';

// 把根組件渲染到 DOM 樹上
render(<AppComponent/>, window.document.getElementById('app'));

main_server.js 文件內容以下:

爲了能把渲染的完整 HTML 文件經過 HTTP 服務返回給請求端,還須要經過用 Node.js 編寫一個 HTTP 服務器。 因爲本節不專一於將 HTTP 服務器的實現,就採用了 ExpressJS 來實現,http_server.js 文件內容以下:

const express = require('express');
const { render } = require('./dist/bundle_server');
const app = express();

// 調用構建出的 bundle_server.js 中暴露出的渲染函數,再拼接下 HTML 模版,造成完整的 HTML 文件
app.get('/', function (req, res) {
  res.send(`
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<div id="app">${render()}</div>
<!--導入 Webpack 輸出的用於瀏覽器端渲染的 JS 文件-->
<script src="./dist/bundle_browser.js"></script>
</body>
</html>
  `);
});

// 其它請求路徑返回對應的本地文件
app.use(express.static('.'));

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

再安裝新引入的第三方依賴:

# 安裝 Webpack 構建依賴
npm i -D css-loader style-loader ignore-loader webpack-node-externals
# 安裝 HTTP 服務器依賴
npm i -S express

以上全部準備工做已經完成,接下來執行構建,編譯出目標文件:

  • 執行命令 webpack --config webpack_server.config.js 構建出用於服務端渲染的 ./dist/bundle_server.js 文件。
  • 執行命令 webpack 構建出用於瀏覽器環境運行的 ./dist/bundle_browser.js 文件,默認的配置文件爲 webpack.config.js

構建執行完成後,執行 node ./http_server.js 啓動 HTTP 服務器後,再用瀏覽器去訪問 http://localhost:3000 就能看到 Hello,Webpack 了。 可是爲了驗證服務端渲染的結果,你須要打開瀏覽器的開發工具中的網絡抓包一欄,再從新刷新瀏覽器後,就能抓到請求 HTML 的包了,抓包效果圖以下:

能夠看到服務器返回的是渲染出內容後的 HTML 而不是 HTML 模版,這說明同構應用的改造完成。

本實例提供 項目完整代碼

構建Electron應用

Electron 是 Node.js 和 Chromium 瀏覽器的結合體,用 Chromium 瀏覽器顯示出的 Web 頁面做爲應用的 GUI,經過 Node.js 去和操做系統交互。 當你在 Electron 應用中的一個窗口操做時,其實是在操做一個網頁。當你的操做須要經過操做系統去完成時,網頁會經過 Node.js 去和操做系統交互。

採用這種方式開發桌面端應用的優勢有:

  • 下降開發門檻,只需掌握網頁開發技術和 Node.js 便可,大量的 Web 開發技術和現成庫能夠複用於 Electron;
  • 因爲 Chromium 瀏覽器和 Node.js 都是跨平臺的,Electron 能作到寫一份代碼在不一樣的操做系統運行。

在運行 Electron 應用時,會從啓動一個主進程開始。主進程的啓動是經過 Node.js 去執行一個入口 JavaScript 文件實現的,這個入口文件 main.js 內容以下:

<p data-height="565" data-theme-id="0" data-slug-hash="vjweQv" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="Electron-main.js" class="codepen">See the Pen Electron-main.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>

主進程啓動後會一直駐留在後臺運行,你眼睛所看得的和操做的窗口並非主進程,而是由主進程新啓動的窗口子進程。

應用從啓動到退出有一系列生命週期事件,經過 electron.app.on() 函數去監聽生命週期事件,在特定的時刻作出反應。 例如在 app.on('ready') 事件中經過 BrowserWindow 去展現應用的主窗口。

啓動的窗口實際上是一個網頁,啓動時會去加載在 loadURL 中傳入的網頁地址。 每一個窗口都是一個單獨的網頁進程,窗口之間的通訊須要藉助主進程傳遞消息。

整體來講開發 Electron 應用和開發 Web 應用很類似,區別在於 Electron 的運行環境同時內置了瀏覽器和 Node.js 的 API,在開發網頁時除了可使用瀏覽器提供的 API 外,還可使用 Node.js 提供的 API。

接入 Webpack

接下來作一個簡單的 Electron 應用,要求爲應用啓動後顯示一個主窗口,在主窗口裏有一個按鈕,點擊這個按鈕後新顯示一個窗口,且使用 React 開發網頁。

因爲 Electron 應用中的每個窗口對應一個網頁,因此須要開發2個網頁,分別是主窗口的 index.html 和新打開的窗口 login.html

須要改動的地方以下:

  • 在項目根目錄下新建主進程的入口文件 main.js,內容和上面提到的一致;
  • 主窗口網頁的代碼以下:
import React, { Component } from 'react';
import { render } from 'react-dom';
import { remote } from 'electron';
import path from 'path';
import './index.css';

class App extends Component {

  // 在按鈕被點擊時
  handleBtnClick() {
    // 新窗口對應的頁面的 URI 地址
    const modalPath = path.join('file://', remote.app.getAppPath(), 'dist/login.html');
    // 新窗口的大小
    let win = new remote.BrowserWindow({ width: 400, height: 320 })
    win.on('close', function () {
      // 窗口被關閉時清空資源
      win = null
    })
    // 加載網頁
    win.loadURL(modalPath)
    // 顯示窗口
    win.show()
  }
  
  render() {
    return (
      <div>
        <h1>Page Index</h1>
        <button onClick={this.handleBtnClick}>Open Page Login</button>
      </div>
    )
  }
}

render(<App/>, window.document.getElementById('app'));

其中最關鍵的部分在於在按鈕點擊事件裏經過 electron 庫裏提供的 API 去新打開一個窗口,並加載網頁文件所在的地址。

頁面部分的代碼已經修改完成,接下來修改構建方面的代碼。 這裏構建須要作到如下幾點:

  • 構建出2個可在瀏覽器裏運行的網頁,分別對應2個窗口的界面;
  • 因爲在網頁的 JavaScript 代碼裏可能會有調用 Node.js 原生模塊或者 electron 模塊,也就是輸出的代碼依賴這些模塊。但因爲這些模塊都是內置支持的,構建出的代碼不能把這些模塊打包進去。

要完成以上要求很是簡單,由於 Webpack 內置了對 Electron 的支持。 只須要給 Webpack 配置文件加上一行代碼便可,以下:

target: 'electron-renderer',

以上修改都完成後從新執行 Webpack 構建,對應的網頁須要的代碼都輸出到了項目根目錄下的 dist 目錄裏。

爲了以 Electron 應用的形式運行,還須要安裝新依賴:

# 安裝 Electron 執行環境到項目中
npm i -D electron

構建Npm模塊

發佈到 Npm 倉庫的模塊有如下幾個特色:

  • 每一個模塊根目錄下都必須有一個描述該模塊的 package.json 文件。該文件描述了模塊的入口文件是哪一個,該模塊又依賴哪些模塊等。
  • 模塊中的文件以 JavaScript 文件爲主,但不限於 JavaScript 文件。例如一個 UI 組件模塊可能同時須要 JavaScript、CSS、圖片文件等。
  • 模塊中的代碼大多采用模塊化規範,由於你的這個模塊可能依賴其它模塊,並且別的模塊又可能依賴你的這個模塊。由於目前支持比較普遍的是 CommonJS 模塊化規範,上傳到 Npm 倉庫的代碼最好遵照該規範。

拋出問題

Webpack 不只可用於構建運行的應用,也可用於構建上傳到 Npm 的模塊。 接下來用教你們如何用 Webpack 構建一個可上傳的 Npm 倉庫的 React 組件,具體要求以下:

  1. 源代碼採用 ES6 寫,但發佈到 Npm 倉庫的須要是 ES5 的,而且遵照 CommonJS 模塊化規範。若是發佈到 Npm 上去的 ES5 代碼是通過轉換的,請同時提供 Source Map 以方便調試。
  2. 該 UI 組件依賴的其它資源文件例如 CSS 文件也須要包含在發佈的模塊裏。
  3. 儘可能減小冗餘代碼,減小發布出去的組件的代碼文件大小。
  4. 發佈出去的組件的代碼中不能含有其依賴的模塊的代碼,而是讓用戶可選擇性的去安裝。例如不能內嵌 React 庫的代碼,這樣作的目的是在其它組件也依賴 React 庫時,防止 React 庫的代碼被重複打包。

在開始前先看下最終發佈到 Npm 倉庫的模塊的目錄結構:

node_modules/hello-webpack
├── lib
│   ├── index.css (組件全部依賴的 CSS 都在這個文件中)
│   ├── index.css.map
│   ├── index.js (符合 CommonJS 模塊化規範的 ES5 代碼)
│   └── index.js.map
├── src (ES6 源碼)
│   ├── index.css
│   └── index.js
└── package.json (模塊描述文件)

src/index.js 文件,內容以下:

import React, { Component } from 'react';
import './index.css';

// 導出該組件供給其它模塊使用
export default class HelloWebpack extends Component {
  render() {
    return <h1 className="hello-component">Hello,Webpack</h1>
  }
}

要使用該模塊時只須要這樣:

// 經過 ES6 語法導入
import HelloWebpack from 'hello-webpack';
import 'hello-webpack/lib/index.css';

// 或者經過 ES5 語法導入
var HelloWebpack = require('hello-webpack');
require('hello-webpack/lib/index.css');

// 使用 react-dom 渲染
render(<HelloWebpack/>);

使用 Webpack 構建 Npm 模塊

對於要求1,能夠這樣作到:

  • 使用 babel-loader 把 ES6 代碼轉換成 ES5 的代碼。
  • 經過開啓 devtool: 'source-map' 輸出 Source Map 以發佈調試。
  • 設置 output.libraryTarget='commonjs2' 使輸出的代碼符合CommonJS2 模塊化規範,以供給其它模塊導入使用。

相關的 Webpack 配置代碼以下:

module.exports = {
  output: {
    // 輸出的代碼符合 CommonJS 模塊化規範,以供給其它模塊導入使用。
    libraryTarget: 'commonjs2',
  },
  // 輸出 Source Map
  devtool: 'source-map',
};

對於要求2,須要經過 css-loaderextract-text-webpack-plugin 實現,相關的 Webpack 配置代碼以下:

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        // 增長對 CSS 文件的支持
        test: /\.css/,
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader']
        }),
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      // 輸出的 CSS 文件名稱
      filename: 'index.css',
    }),
  ],
};

此步引入了3個新依賴:

# 安裝 Webpack 構建所須要的新依賴
npm i -D style-loader css-loader extract-text-webpack-plugin

對於要求3,須要注意的是 Babel 在把 ES6 代碼轉換成 ES5 代碼時會注入一些輔助函數。

例以下面這段 ES6 代碼:

class HelloWebpack extends Component{
}

在被轉換成能正常運行的 ES5 代碼時須要如下2個輔助函數:

  • babel-runtime/helpers/createClass 用於實現 class 語法
  • babel-runtime/helpers/inherits 用於實現 extends 語法

默認的狀況下 Babel 會在每一個輸出文件中內嵌這些依賴的輔助函數的代碼,若是多個源代碼文件都依賴這些輔助函數,那麼這些輔助函數的代碼將會重複的出現不少次,形成代碼冗餘。

爲了避免讓這些輔助函數的代重複出現,能夠在依賴它們的時候經過 require('babel-runtime/helpers/createClass') 的方式去導入,這樣就能作到只讓它們出現一次。 babel-plugin-transform-runtime 插件就是用來作這個事情的。

修改 .babelrc 文件,爲其加入 transform-runtime 插件:

{
  "plugins": [
    [
      "transform-runtime",
      {
        // transform-runtime 默認會自動的爲你使用的 ES6 API 注入 polyfill
        // 假如你在源碼中使用了 Promise,輸出的代碼將會自動注入 require('babel-runtime/core-js/Promise') 語句
        // polyfill 的注入應該交給模塊使用者,由於使用者可能在其它地方已經注入了其它的 Promise polyfill 庫
        // 因此關閉該功能
        "polyfill": false
      }
    ]
  ]
}

因爲加入 babel-plugin-transform-runtime 後生成的代碼中會大量出現相似 require('babel-runtime/helpers/createClass') 這樣的語句,因此輸出的代碼將依賴 babel-runtime 模塊。

此步引入了3個新依賴:

# 安裝 Webpack 構建所須要的新依賴
npm i -D babel-plugin-transform-runtime
# 安裝輸出代碼運行時所需的新依賴
npm i -S babel-runtime

對於要求4,須要經過在 其它配置項 中介紹過的 Externals 來實現。

Externals 用來告訴 Webpack 要構建的代碼中使用了哪些不用被打包的模塊,也就是說這些模版是外部環境提供的,Webpack 在打包時能夠忽略它們。

相關的 Webpack 配置代碼以下:

module.exports = {
  // 經過正則命中全部以 react 或者 babel-runtime 開頭的模塊
  // 這些模塊經過註冊在運行環境中的全局變量訪問,不用被重複打包進輸出的代碼裏
  externals: /^(react|babel-runtime)/,
};

開啓以上配置後,輸出的代碼中會存在導入 react 或者 babel-runtime 模塊的代碼,可是它們的 react 或者 babel-runtime 的內容不會被包含進去,以下:

[
    (function (module, exports) {
        module.exports = require("babel-runtime/helpers/inherits");
    }),
    (function (module, exports) {
        module.exports = require("react");
    })
]

這樣就作到了在保持代碼正確性的狀況下,輸出文件不存放 react 或者 babel-runtime 模塊的代碼。

實際上當你在開發 Npm 模塊時,不僅須要對 react 和 babel-runtime 模塊作這樣的處理,而是須要對全部正在開發的模塊所依賴的模塊進行這樣的處理。 由於正在開發的模塊所依賴的模塊也可能被其它模塊所依賴。 當一個項目中一個模塊被依賴屢次時,Webpack 只會將其打包一次。

完成以上4步後最終的 Webpack 完整配置代碼以下:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  // 模塊的入口文件
  entry: './src/index.js',
  output: {
    // 輸出文件的名稱
    filename: 'index.js',
    // 輸出文件的存放目錄
    path: path.resolve(__dirname, 'lib'),
    // 輸出的代碼符合 CommonJS 模塊化規範,以供給其它模塊導入使用。
    libraryTarget: 'commonjs2',
  },
  // 經過正則命中全部以 react 或者 babel-runtime 開頭的模塊,
  // 這些模塊使用外部的,不能被打包進輸出的代碼裏,防止它們出現屢次。
  externals: /^(react|babel-runtime)/,
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        // 排除 node_modules 目錄下的文件,
        // node_modules 目錄下的文件都是採用的 ES5 語法,不必再經過 Babel 去轉換。
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // 增長對 CSS 文件的支持
        test: /\.css/,
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader']
        }),
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      // 輸出的 CSS 文件名稱
      filename: 'index.css',
    }),
  ],
  // 輸出 Source Map
  devtool: 'source-map',
};

從新執行構建後,你將會在項目目錄下看到一個新目錄 lib,裏面放着要發佈到 Npm 倉庫的最終代碼。

發佈到 Npm

在把構建出的代碼發佈到 Npm 倉庫前,還須要確保你的模塊描述文件 package.json 是正確配置的。

因爲構建出的代碼的入口文件是 ./lib/index.js,須要修改 package.json 中的 main 字段以下:

{
  "main": "lib/index.js",
  "jsnext:main": "src/index.js"
}

其中 jsnext:main 字段用於指出採用 ES6 編寫的模塊入口文件所在的位置。

修改完畢後在項目目錄下執行 npm publish 就能把構建出的代碼發佈到 Npm 倉庫中(確保已經 npm login 過)。

若是你想讓發佈到 Npm 上去的代碼保持和源碼的目錄結構一致,那麼用 Webpack 將不在適合。 由於源碼是一個個分割的模塊化文件,而 Webpack 會把這些模塊組合在一塊兒。 雖然 Webpack 輸出的文件也能夠是採用 CommonJS 模塊化語法的,但在有些場景下把全部模塊打包成一個文件發佈到 Npm 是不適合的。 例如像 Lodash 這樣的工具函數庫在項目中可能只用到了其中幾個工具函數,若是全部工具函數打包在一個文件中,那麼全部工具函數都會被打包進去,而保持模塊文件的獨立能作到只打包進使用到的。 還有就是像 UI 組件庫這樣由大量獨立組件組成的庫也和 Lodash 相似。
因此 Webpack 適合於構建完整不可分割的 Npm 模塊。

構建離線應用

離線應用的核心是離線緩存技術,歷史上曾前後出現2種離線離線緩存技術,它們分別是:

  1. AppCache 又叫 Application Cache,目前已經從 Web 標準中刪除,請儘可能不要使用它。
  2. Service Workers 是目前最新的離線緩存技術,是 Web Worker 的一部分。 它經過攔截網絡請求實現離線緩存,比 AppCache 更加靈活。它也是構建 PWA 應用的關鍵技術之一。

認識 Service Workers

Service Workers 是一個在瀏覽器後臺運行的腳本,它生命週期徹底獨立於網頁。它沒法直接訪問 DOM,但能夠經過 postMessage 接口發送消息來和 UI 進程通訊。 攔截網絡請求是 Service Workers 的一個重要功能,經過它能完成離線緩存、編輯響應、過濾響應等功能。

Service Workers 兼容性

目前 Chrome、Firefox、Opera 都已經全面支持 Service Workers,但對於移動端瀏覽器就不太樂觀了,只有高版本的 Android 支持。 因爲 Service Workers 沒法經過注入 polyfill 去實現兼容,因此在你打算使用它前請先調查清楚你的網頁的運行場景。

判斷瀏覽器是否支持 Service Workers 的最簡單的方法是經過如下代碼:

// 若是 navigator 對象上存在 serviceWorker 對象,就表示支持
if (navigator.serviceWorker) {
  // 經過 navigator.serviceWorker 使用
}

註冊 Service Workers

要給網頁接入 Service Workers,須要在網頁加載後註冊一個描述 Service Workers 邏輯的腳本。 代碼以下:

if (navigator.serviceWorker) {
  window.addEventListener('DOMContentLoaded',function() {
    // 調用 serviceWorker.register 註冊,參數 /sw.js 爲腳本文件所在的 URL 路徑
      navigator.serviceWorker.register('/sw.js');
  });
}

一旦這個腳本文件被加載,Service Workers 的安裝就開始了。這個腳本被安裝到瀏覽器中後,就算用戶關閉了當前網頁,它仍會存在。 也就是說第一次打開該網頁時 Service Workers 的邏輯不會生效,由於腳本尚未被加載和註冊,可是之後再次打開該網頁時腳本里的邏輯將會生效。

在 Chrome 中能夠經過打開網址 chrome://inspect/#service-workers 來查看當前瀏覽器中全部註冊了的 Service Workers。

使用 Service Workers 實現離線緩存

Service Workers 在註冊成功後會在其生命週期中派發出一些事件,經過監聽對應的事件在特色的時間節點上作一些事情。

在 Service Workers 腳本中,引入了新的關鍵字 self 表明當前的 Service Workers 實例。

在 Service Workers 安裝成功後會派發出 install 事件,須要在這個事件中執行緩存資源的邏輯,實現代碼以下:

// 當前緩存版本的惟一標識符,用當前時間代替
var cacheKey = new Date().toISOString();

// 須要被緩存的文件的 URL 列表
var cacheFileList = [
  '/index.html',
  '/app.js',
  '/app.css'
];

// 監聽 install 事件
self.addEventListener('install', function (event) {
  // 等待全部資源緩存完成時,才能夠進行下一步
  event.waitUntil(
    caches.open(cacheKey).then(function (cache) {
      // 要緩存的文件 URL 列表
      return cache.addAll(cacheFileList);
    })
  );
});

接下來須要監聽網絡請求事件去攔截請求,複用緩存,代碼以下:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // 去緩存中查詢對應的請求
    caches.match(event.request).then(function(response) {
        // 若是命中本地緩存,就直接返回本地的資源
        if (response) {
          return response;
        }
        // 不然就去用 fetch 下載資源
        return fetch(event.request);
      }
    )
  );
});

以上就實現了離線緩存。

更新緩存

線上的代碼有時須要更新和從新發布,若是這個文件被離線緩存了,那就須要 Service Workers 腳本中有對應的邏輯去更新緩存。 這能夠經過更新 Service Workers 腳本文件作到。

瀏覽器針對 Service Workers 有以下機制:

  1. 每次打開接入了 Service Workers 的網頁時,瀏覽器都會去從新下載 Service Workers 腳本文件(因此要注意該腳本文件不能太大),若是發現和當前已經註冊過的文件存在字節差別,就將其視爲「新服務工做線程」。
  2. 新 Service Workers 線程將會啓動,且將會觸發其 install 事件。
  3. 當網站上當前打開的頁面關閉時,舊 Service Workers 線程將會被終止,新 Service Workers 線程將會取得控制權。
  4. 新 Service Workers 線程取得控制權後,將會觸發其 activate 事件。

新 Service Workers 線程中的 activate 事件就是最佳的清理舊緩存的時間點,代碼以下:

// 當前緩存白名單,在新腳本的 install 事件裏將使用白名單裏的 key 
var cacheWhitelist = [cacheKey];

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          // 不在白名單的緩存所有清理掉
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // 刪除緩存
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

最終完整的代碼 Service Workers 腳本代碼以下:

// 當前緩存版本的惟一標識符,用當前時間代替
var cacheKey = new Date().toISOString();

// 當前緩存白名單,在新腳本的 install 事件裏將使用白名單裏的 key
var cacheWhitelist = [cacheKey];

// 須要被緩存的文件的 URL 列表
var cacheFileList = [
  '/index.html',
  'app.js',
  'app.css'
];

// 監聽 install 事件
self.addEventListener('install', function (event) {
  // 等待全部資源緩存完成時,才能夠進行下一步
  event.waitUntil(
    caches.open(cacheKey).then(function (cache) {
      // 要緩存的文件 URL 列表
      return cache.addAll(cacheFileList);
    })
  );
});

// 攔截網絡請求
self.addEventListener('fetch', function (event) {
  event.respondWith(
    // 去緩存中查詢對應的請求
    caches.match(event.request).then(function (response) {
        // 若是命中本地緩存,就直接返回本地的資源
        if (response) {
          return response;
        }
        // 不然就去用 fetch 下載資源
        return fetch(event.request);
      }
    )
  );
});

// 新 Service Workers 線程取得控制權後,將會觸發其 activate 事件
self.addEventListener('activate', function (event) {
  event.waitUntil(
    caches.keys().then(function (cacheNames) {
      return Promise.all(
        cacheNames.map(function (cacheName) {
          // 不在白名單的緩存所有清理掉
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // 刪除緩存
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

接入 Webpack

用 Webpack 構建接入 Service Workers 的離線應用要解決的關鍵問題在於如何生成上面提到的 sw.js 文件, 而且sw.js文件中的 cacheFileList 變量,表明須要被緩存文件的 URL 列表,須要根據輸出文件列表所對應的 URL 來決定,而不是像上面那樣寫成靜態值。

假如構建輸出的文件目錄結構爲:

├── app_4c3e186f.js
├── app_7cc98ad0.css
└── index.html

那麼 sw.js 文件中 cacheFileList 的值應該是:

var cacheFileList = [
  '/index.html',
  'app_4c3e186f.js',
  'app_7cc98ad0.css'
];

Webpack 沒有原生功能能完成以上要求,幸虧龐大的社區中已經有人爲咱們作好了一個插件 serviceworker-webpack-plugin 能夠方便的解決以上問題。 使用該插件後的 Webpack 配置以下:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { WebPlugin } = require('web-webpack-plugin');
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin');

module.exports = {
  entry: {
    app: './main.js'// Chunk app 的 JS 執行入口文件
  },
  output: {
    filename: '[name].js',
    publicPath: '',
  },
  module: {
    rules: [
      {
        test: /\.css/,// 增長對 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader'] // 壓縮 CSS 代碼
        }),
      },
    ]
  },
  plugins: [
    // 一個 WebPlugin 對應一個 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路徑
      filename: 'index.html' // 輸出的 HTML 的文件名稱
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,// 給輸出的 CSS 文件名稱加上 Hash 值
    }),
    new ServiceWorkerWebpackPlugin({
      // 自定義的 sw.js 文件所在路徑
      // ServiceWorkerWebpackPlugin 會把文件列表注入到生成的 sw.js 中
      entry: path.join(__dirname, 'sw.js'),
    }),
  ],
  devServer: {
    // Service Workers 依賴 HTTPS,使用 DevServer 提供的 HTTPS 功能。
    https: true,
  }
};

以上配置有2點須要注意:

  • 因爲 Service Workers 必須在 HTTPS 環境下才能攔截網絡請求實現離線緩存,使用在 DevServer https 中提到的方式去實現 HTTPS 服務。
  • serviceworker-webpack-plugin 插件爲了保證靈活性,容許使用者自定義 sw.js,構建輸出的 sw.js 文件中會在頭部注入一個變量 serviceWorkerOption.assets 到全局,裏面存放着全部須要被緩存的文件的 URL 列表。

須要修改上面的 sw.js 文件中寫成了靜態值的 cacheFileList 爲以下:

// 須要被緩存的文件的 URL 列表
var cacheFileList = global.serviceWorkerOption.assets;

以上已經完成全部文件的修改,在從新構建前,先安裝新引入的依賴:

npm i -D serviceworker-webpack-plugin webpack-dev-server

安裝成功後,在項目根目錄下執行 webpack-dev-server 命令後,DevServer 將以 HTTPS 模式啓動。

搭配Npm Script

Npm Script 是一個任務執行者。 Npm 是在安裝 Node.js 時附帶的包管理器,Npm Script 則是 Npm 內置的一個功能,容許在 package.json 文件裏面使用 scripts 字段定義任務:

{
  "scripts": {
    "dev": "node dev.js",
    "pub": "node build.js"
  }
}

裏面的 scripts 字段是一個對象,每個屬性對應一段腳本,以上定義了兩個任務 devpub。 Npm Script 底層實現原理是經過調用 Shell 去運行腳本命令,例如執行 npm run pub 命令等同於執行命令 node build.js

Npm Script 還有一個重要的功能是能運行安裝到項目目錄裏的 node_modules 裏的可執行模塊,例如在經過命令:

npm i -D webpack

將 Webpack 安裝到項目中後,是沒法直接在項目根目錄下經過命令 webpack 去執行 Webpack 構建的,而是要經過命令 ./node_modules/.bin/webpack 去執行。

Npm Script 能方便的解決這個問題,只須要在 scripts 字段裏定義一個任務,例如:

{
  "scripts": {
    "build": "webpack"
  }
}

Npm Script 會先去項目目錄下的 node_modules 中尋找有沒有可執行的 webpack 文件,若是有就使用本地的,若是沒有就使用全局的。 因此如今執行 Webpack 構建只須要經過執行 npm run build 去實現。

Webpack 爲何須要 Npm Script

Webpack 只是一個打包模塊化代碼的工具,並無提供任何任務管理相關的功能。 但在實際場景中一般不會是隻經過執行 webpack 就能完成全部任務的,而是須要多個任務才能完成。

  1. 在開發階段爲了提升開發體驗,使用 DevServer 作開發,而且須要輸出 Source Map 以方便調試,同時還須要開啓自動刷新功能。
  2. 爲了減少發佈到線上的代碼尺寸,在構建出發佈到線上的代碼時,須要壓縮輸出的代碼。
  3. 在構建完發佈到線上的代碼後,須要把構建出的代碼提交給發佈系統。

能夠看出要求1和要求2是相互衝突的,其中任務3又依賴任務2。要知足以上三個要求,須要定義三個不一樣的任務。

接下來經過 Npm Script 來定義上面的3個任務:

"scripts": {
  "dev": "webpack-dev-server --open",
  "dist": "NODE_ENV=production webpack --config webpack_dist.config.js",
  "pub": "npm run dist && rsync dist"
},

含義分別是:

  • dev 表明用於開發時執行的任務,經過 DevServer 去啓動構建。因此在開發項目時只需執行 npm run dev
  • dist 表明構建出用於發佈到線上去的代碼,輸出到 dist 目錄中。其中的 NODE_ENV=production 是爲了在運行任務時注入環境變量。
  • pub 表明先構建出用於發佈到線上去的代碼,再同步 dist 目錄中的文件到發佈系統(如何同步文件需根據你所使用的發佈系統而定)。因此在開發完後須要發佈時只需執行 npm run pub

使用 Npm Script 的好處是把一連串複雜的流程簡化成了一個簡單的命令,須要時只須要執行對應的那個簡短的命令,而不用去手動的重複整個流程。 這會大大的提升咱們的效率和下降出錯率。

檢查代碼

檢查代碼和 Code Review 很類似,都是去審視提交的代碼可能存在的問題。 但 Code Review 通常經過人去執行,而檢查代碼是經過機器去執行一些自動化的檢查。 自動化的檢查代碼成本更低,實施代價更小。

檢查代碼主要檢查如下幾項:

  • 代碼風格:讓項目成員強制遵照統一的代碼風格,例如如何縮緊、如何寫註釋等,保障代碼可讀性,不把時間浪費在爭論如何寫代碼更好看上;
  • 潛在問題:分析出代碼在運行過程當中可能出現的潛在 Bug。

目前已經有成熟的工具能夠檢驗諸如 JavaScript、TypeScript、CSS、SCSS 等經常使用語言。

檢查 JavaScript

目前最經常使用的 JavaScript 檢查工具是 ESlint ,它不只內置了大量經常使用的檢查規則,還能夠經過插件機制作到靈活擴展。

ESlint 的使用很簡單,在經過:npm i -g eslint

按照到全局後,再在項目目錄下執行:eslint init

來新建一個 ESlint 配置文件 .eslintrc,該文件格式爲 JSON。

若是你想覆蓋默認的檢查規則,或者想加入新的檢查規則,你須要修改該文件,例如使用如下配置:

{
    // 從 eslint:recommended 中繼承全部檢查規則
    "extends": "eslint:recommended",
    // 再自定義一些規則     
    "rules": {
        // 須要在每行結尾加 ;        
        "semi": ["error", "always"],
        // 須要使用 "" 包裹字符串         
        "quotes": ["error", "double"]
    }
}

寫好配置文件後,再執行:

eslint yourfile.js

去檢查 yourfile.js 文件,若是你的文件沒有經過檢查,ESlint 會輸出錯誤緣由,例如:

檢查 TypeScript

TSLint 是一個和 ESlint 類似的 TypeScript 代碼檢查工具,區別在於 TSLint 只專一於檢查 TypeScript 代碼。

TSLint 和 ESlint 的使用方法很類似,首先經過:npm i -g tslint

安裝到全局,再去項目根目錄下執行:tslint --init

生成配置文件 tslint.json,在配置好後,再執行:tslint yourfile.ts去檢查 yourfile.ts 文件。

檢查 CSS

stylelint 是目前最成熟的 CSS 檢查工具,內置了大量檢查規則的同時也提供插件機制讓用戶自定義擴展。 stylelint 基於 PostCSS,能檢查任何 PostCSS 能解析的代碼,諸如 SCSS、Less 等。

首先經過npm i -g stylelint

安裝到全局後,去項目根目錄下新建 .stylelintrc 配置文件, 該配置文件格式爲 JSON,其格式和 ESLint 的配置類似,例如:

{
  // 繼承 stylelint-config-standard 中的全部檢查規則
  "extends": "stylelint-config-standard",
  // 再自定義檢查規則  
  "rules": {
    "at-rule-empty-line-before": null
  }
}

配置好後,再執行stylelint "yourfile.css"去檢查 yourfile.css 文件。

結合 Webpack 檢查代碼

結合 ESLint

eslint-loader 能夠方便的把 ESLint 整合到 Webpack 中,使用方法以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // node_modules 目錄的下的代碼不用檢查
        exclude: /node_modules/,
        loader: 'eslint-loader',
        // 把 eslint-loader 的執行順序放到最前面,防止其它 Loader 把處理後的代碼交給 eslint-loader 去檢查
        enforce: 'pre',
      },
    ],
  },
}

接入 eslint-loader 後就能在控制檯中看到 ESLint 輸出的錯誤日誌了。

結合 TSLint

tslint-loader 是一個和 eslint-loader 類似的 Webpack Loader, 能方便的把 TSLint 整合到 Webpack,其使用方法以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // node_modules 目錄的下的代碼不用檢查
        exclude: /node_modules/,
        loader: 'tslint-loader',
        // 把 tslint-loader 的執行順序放到最前面,防止其它 Loader 把處理後的代碼交給 tslint-loader 去檢查
        enforce: 'pre',
      },
    ],
  },
}

結合 stylelint

StyleLintPlugin 能把 stylelint 整合到 Webpack,其使用方法很簡單,以下:

const StyleLintPlugin = require('stylelint-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new StyleLintPlugin(),
  ],
}

一些建議

把代碼檢查功能整合到 Webpack 中會致使如下問題:

  • 因爲執行檢查步驟計算量大,整合到 Webpack 中會致使構建變慢;
  • 在整合代碼檢查到 Webpack 後,輸出的錯誤信息是經過行號來定位錯誤的,沒有編輯器集成顯示錯誤直觀;

爲了不以上問題,還能夠這樣作:

  • 使用集成了代碼檢查功能的編輯器,讓編輯器實時直觀地顯示錯誤;
  • 把代碼檢查步驟放到代碼提交時,也就是說在代碼提交前去調用以上檢查工具去檢查代碼,只有在檢查都經過時才提交代碼,這樣就能保證提交到倉庫的代碼都是經過了檢查的。

若是你的項目是使用 Git 管理,Git 提供了 Hook 功能能作到在提交代碼前觸發執行腳本。

husky 能夠方便快速地爲項目接入 Git Hook, 執行npm i -D husky

安裝 husky 時,husky 會經過 Npm Script Hook 自動配置好 Git Hook,你須要作的只是在 package.json 文件中定義幾個腳本,方法以下:

{
  "scripts": {
    // 在執行 git commit 前會執行的腳本  
    "precommit": "npm run lint",
    // 在執行 git push 前會執行的腳本  
    "prepush": "lint",
    // 調用 eslint、stylelint 等工具檢查代碼
    "lint": "eslint && stylelint"
  }
}

precommitprepush 你須要根據本身的狀況選擇一個,無需兩個都設置。

經過 Node.js API 啓動 Webpack

Webpack 除了提供可執行的命令行工具外,還提供可在 Node.js 環境中調用的庫。 經過 Webpack 暴露的 API,可直接在 Node.js 程序中調用 Webpack 執行構建。

經過 API 去調用並執行 Webpack 比直接經過可執行文件啓動更加靈活,可用在一些特殊場景,下面將教你如何使用 Webpack 提供的 API。

Webpack 實際上是一個 Node.js 應用程序,它所有經過 JavaScript 開發完成。 在命令行中執行 webpack 命令其實等價於執行 node ./node_modules/webpack/bin/webpack.js

安裝和使用 Webpack 模塊

在調用 Webpack API 前,須要先安裝它:

npm i -D webpack

安裝成功後,能夠採用如下代碼導入 Webpack 模塊:

const webpack = require('webpack');

// ES6 語法
import webpack from "webpack";

導出的 webpack 實際上是一個函數,使用方法以下:

webpack({
  // Webpack 配置,和 webpack.config.js 文件一致
}, (err, stats) => {
  if (err || stats.hasErrors()) {
    // 構建過程出錯
  }
  // 成功執行完構建
});

若是你的 Webpack 配置寫在 webpack.config.js 文件中,能夠這樣使用:

// 讀取 webpack.config.js 文件中的配置
const config = require('./webpack.config.js');
webpack(config , callback);

以監聽模式運行

以上使用 Webpack API 的方法只能執行一次構建,沒法以監聽模式啓動 Webpack,爲了在使用 API 時以監聽模式啓動,須要獲取 Compiler 實例,方法以下:

// 若是不傳 callback 回調函數,就會返回一個 Compiler 實例,用於讓你去控制啓動,而不是像上面那樣當即啓動
const compiler = webpack(config);

// 調用 compiler.watch 以監聽模式啓動,返回的 watching 用於關閉監聽
const watching = compiler.watch({
  // watchOptions
  aggregateTimeout: 300,
},(err, stats)=>{
  // 每次因文件發生變化而從新執行完構建後
});

// 調用 watching.close 關閉監聽 
watching.close(()=>{
  // 在監聽關閉後
});

使用 Webpack Dev Middleware

DevServer 是一個方便開發的小型 HTTP 服務器, DevServer 實際上是基於 webpack-dev-middleware 和 Expressjs 實現的, 而 webpack-dev-middleware 實際上是 Expressjs 的一箇中間件。

也就是說,實現 DevServer 基本功能的代碼大體以下:

const express = require('express');
const webpack = require('webpack');
const webpackMiddleware = require('webpack-dev-middleware');

// 從 webpack.config.js 文件中讀取 Webpack 配置 
const config = require('./webpack.config.js');
// 實例化一個 Expressjs app
const app = express();

// 用讀取到的 Webpack 配置實例化一個 Compiler
const compiler = webpack(config);
// 給 app 註冊 webpackMiddleware 中間件
app.use(webpackMiddleware(compiler));
// 啓動 HTTP 服務器,服務器監聽在 3000 端口 
app.listen(3000);

從以上代碼能夠看出,從 webpack-dev-middleware 中導出的 webpackMiddleware 是一個函數,該函數須要接收一個 Compiler 實例。Webpack API 導出的 webpack 函數會返回一個Compiler 實例。

webpackMiddleware 函數的返回結果是一個 Expressjs 的中間件,該中間件有如下功能:

  • 接收來自 Webpack Compiler 實例輸出的文件,但不會把文件輸出到硬盤,而是保存在內存中;
  • 往 Expressjs app 上註冊路由,攔截 HTTP 收到的請求,根據請求路徑響應對應的文件內容;

經過 webpack-dev-middleware 可以將 DevServer 集成到你現有的 HTTP 服務器中,讓你現有的 HTTP 服務器能返回 Webpack 構建出的內容,而不是在開發時啓動多個 HTTP 服務器。 這特別適用於後端接口服務採用 Node.js 編寫的項目。

Webpack Dev Middleware 支持的配置項

在 Node.js 中調用 webpack-dev-middleware 提供的 API 時,還能夠給它傳入一些配置項,方法以下:

// webpackMiddleware 函數的第二個參數爲配置項
app.use(webpackMiddleware(compiler, {
    // webpack-dev-middleware 全部支持的配置項
    // 只有 publicPath 屬性爲必填,其它都是選填項

    // Webpack 輸出資源綁定在 HTTP 服務器上的根目錄,
    // 和 Webpack 配置中的 publicPath 含義一致 
    publicPath: '/assets/',

    // 不輸出 info 類型的日誌到控制檯,只輸出 warn 和 error 類型的日誌
    noInfo: false,

    // 不輸出任何類型的日誌到控制檯
    quiet: false,

    // 切換到懶惰模式,這意味着不監聽文件變化,只會在請求到時再去編譯對應的文件,
    // 這適合頁面很是多的項目。
    lazy: true,

    // watchOptions
    // 只在非懶惰模式下才有效
    watchOptions: {
        aggregateTimeout: 300,
        poll: true
    },

    // 默認的 URL 路徑, 默認是 'index.html'.
    index: 'index.html',

    // 自定義 HTTP 頭
    headers: {'X-Custom-Header': 'yes'},

    // 給特定文件後綴的文件添加 HTTP mimeTypes ,做爲文件類型映射表
    mimeTypes: {'text/html': ['phtml']},

    // 統計信息輸出樣式
    stats: {
        colors: true
    },

    // 自定義輸出日誌的展現方法
    reporter: null,

    // 開啓或關閉服務端渲染
    serverSideRender: false,
}));

Webpack Dev Middleware 與模塊熱替換

DevServer 提供了一個方便的功能,能夠作到在監聽到文件發生變化時自動替換網頁中的老模塊,以作到實時預覽。

DevServer 雖然是基於 webpack-dev-middleware 實現的,但 webpack-dev-middleware 並無實現模塊熱替換功能,而是 DevServer 本身實現了該功能。

爲了在使用 webpack-dev-middleware 時也能使用模塊熱替換功能去提高開發效率,須要額外的再接入 webpack-hot-middleware。 須要作如下修改才能實現模塊熱替換。

第1步:修改 webpack.config.js 文件,加入 HotModuleReplacementPlugin 插件,修改以下:

const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

module.exports = {
  entry: [
    // 爲了支持模塊熱替換,注入代理客戶端
    'webpack-hot-middleware/client',
    // JS 執行入口文件
    './src/main.js'
  ],
  output: {
    // 把全部依賴的模塊合併輸出到一個 bundle.js 文件
    filename: 'bundle.js',
  },
  plugins: [
    // 爲了支持模塊熱替換,生成 .hot-update.json 文件
    new HotModuleReplacementPlugin(),
  ],
  devtool: 'source-map',
};

第2步:修改 HTTP 服務器代碼 server.js 文件,接入 webpack-hot-middleware 中間件,修改以下:

const express = require('express');
const webpack = require('webpack');
const webpackMiddleware = require('webpack-dev-middleware');

// 從 webpack.config.js 文件中讀取 Webpack 配置
const config = require('./webpack.config.js');
// 實例化一個 Expressjs app
const app = express();

// 用讀取到的 Webpack 配置實例化一個 Compiler
const compiler = webpack(config);
// 給 app 註冊 webpackMiddleware 中間件
app.use(webpackMiddleware(compiler));
// 爲了支持模塊熱替換,響應用於替換老模塊的資源
app.use(require('webpack-hot-middleware')(compiler));
// 把項目根目錄做爲靜態資源目錄,用於服務 HTML 文件
app.use(express.static('.'));
// 啓動 HTTP 服務器,服務器監聽在 3000 端口
app.listen(3000, () => {
  console.info('成功監聽在 3000');
});

第3步:修改執行入口文件 main.js,加入替換邏輯,在文件末尾加入如下代碼:

if (module.hot) {
  module.hot.accept();
}

第4步:安裝新引入的依賴:

npm i -D webpack-dev-middleware webpack-hot-middleware express

安裝成功後,經過 node ./server.js 就能啓動一個相似於 DevServer 那樣支持模塊熱替換的自定義 HTTP 服務了。

本實例提供 項目完整代碼

加載圖片

在網頁中不可避免的會依賴圖片資源,例如 PNG、JPG、GIF,下面來教你如何用 Webpack 加載圖片資源。

使用 file-loader

file-loader 能夠把 JavaScript 和 CSS 中導入圖片的語句替換成正確的地址,並同時把文件輸出到對應的位置。

例如 CSS 源碼是這樣寫的:

#app {
  background-image: url(./imgs/a.png);
}

file-loader 轉換後輸出的 CSS 會變成這樣:

#app {
  background-image: url(5556e1251a78c5afda9ee7dd06ad109b.png);
}

而且在輸出目錄 dist 中也多出 ./imgs/a.png 對應的圖片文件 hash.png, 輸出的文件名是根據文件內容的計算出的 Hash 值。

同理在 JavaScript 中導入圖片的源碼以下:

import imgB from './imgs/b.png';

window.document.getElementById('app').innerHTML = `
<img src="${imgB}"/>
`;

通過 file-loader 處理後輸出的 JavaScript 代碼以下:

module.exports = __webpack_require__.p + "0bcc1f8d385f78e1271ebfca50668429.png";

也就是說 imgB 的值就是圖片對應的 URL 地址。

在 Webpack 中使用 file-loader 很是簡單,相關配置以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.png$/,
        use: ['file-loader']
      }
    ]
  }
};

使用 url-loader

url-loader 能夠把文件的內容通過 base64 編碼後注入到 JavaScript 或者 CSS 中去。

例如 CSS 源碼是這樣寫的:

#app {
  background-image: url(./imgs/a.png);
}

url-loader 轉換後輸出的 CSS 會變成這樣:

#app {
  background-image: url(data:image/png;base64,iVBORw01afer...); /* 結尾省略了剩下的 base64 編碼後的數據 */
}

同理在 JavaScript 中效果也相似。

從上面的例子中能夠看出 url-loader 會把根據圖片內容計算出的 base64 編碼的字符串直接注入到代碼中,因爲通常的圖片數據量巨大,這會致使 JavaScript、CSS 文件也跟着變大。 因此在使用 url-loader 時必定要注意圖片體積不能太大,否則會致使 JavaScript、CSS 文件過大而帶來的網頁加載緩慢問題。

通常利用 url-loader 把網頁須要用到的小圖片資源注入到代碼中去,以減小加載次數。由於在 HTTP/1 協議中,每加載一個資源都須要創建一次 HTTP 連接, 爲了一個很小的圖片而新建一次 HTTP 鏈接是不划算的。

url-loader 考慮到了以上問題,並提供了一個方便的選擇 limit,該選項用於控制當文件大小小於 limit 時才使用 url-loader,不然使用 fallback 選項中配置的 loader。 相關 Webpack 配置以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.png$/,
        use: [{
          loader: 'url-loader',
          options: {
            // 30KB 如下的文件採用 url-loader
            limit: 1024 * 30,
            // 不然採用 file-loader,默認值就是 file-loader 
            fallback: 'file-loader',
          }
        }]
      }
    ]
  },
};

除此以外,你還能夠作如下優化:

以上加載圖片的方法一樣適用於其它二進制類型的資源,例如 PDF、SWF 等等。

本實例提供 項目完整代碼

加載 SVG

SVG 做爲矢量圖的一種標準格式,已經獲得了各大瀏覽器的支持,它也成爲了 Web 中矢量圖的代名詞。 在網頁中採用 SVG 代替位圖有以下好處:

  1. SVG 相對於位圖更清晰,在任意縮放的狀況下後不會破壞圖形的清晰度,SVG 能方便地解決高分辨率屏幕下圖像顯示不清楚的問題。
  2. 在圖形線條比較簡單的狀況下,SVG 文件的大小要小於位圖,在扁平化 UI 流行的今天,多數狀況下 SVG 會更小。
  3. 圖形相同的 SVG 比對應的高清圖有更好的渲染性能。
  4. SVG 採用和 HTML 一致的 XML 語法描述,靈活性很高。

畫圖工具能導出一個個 .svg 文件,SVG 的導入方法和圖片相似,既能夠像下面這樣在 CSS 中直接使用:

body {
  background-image: url(./svgs/activity.svg);
}

也能夠在 HTML 中使用:

<img src="./svgs/activity.svg"/>

也就是說能夠直接把 SVG 文件當成一張圖片來使用,方法和使用圖片時徹底同樣。

使用 file-loader 和使用 url-loader 對 SVG 來講一樣有效,只須要把 Loader test 配置中的文件後綴改爲 .svg,代碼以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg/,
        use: ['file-loader']
      }
    ]
  },
};

因爲 SVG 是文本格式的文件,除了以上兩種方法外還有其它方法,下面來一一說明。

使用 raw-loader

raw-loader 能夠把文本文件的內容讀取出來,注入到 JavaScript 或 CSS 中去。

例如在 JavaScript 中這樣寫:

import svgContent from './svgs/alert.svg';

通過 raw-loader 處理後輸出的代碼以下:

module.exports = "<svg xmlns=\"http://www.w3.org/2000/svg\"... </svg>" // 末尾省略 SVG 內容

也就是說 svgContent 的內容就等於字符串形式的 SVG,因爲 SVG 自己就是 HTML 元素,在獲取到 SVG 內容後,能夠直接經過如下代碼將 SVG 插入到網頁中:

window.document.getElementById('app').innerHTML = svgContent;

使用 raw-loader 時相關的 Webpack 配置以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        use: ['raw-loader']
      }
    ]
  }
};

因爲 raw-loader 會直接返回 SVG 的文本內容,而且沒法經過 CSS 去展現 SVG 的文本內容,所以採用本方法後沒法在 CSS 中導入 SVG。 也就是說在 CSS 中不能夠出現 background-image: url(./svgs/activity.svg) 這樣的代碼,由於 background-image: url(<svg>...</svg>) 是不合法的。

使用 svg-inline-loader

svg-inline-loader 和上面提到的 raw-loader 很是類似, 不一樣在於 svg-inline-loader 會分析 SVG 的內容,去除其中沒必要要的部分代碼,以減小 SVG 的文件大小。

在使用畫圖工具如 Adobe Illustrator、Sketch 製做 SVG 後,在導出時這些工具會生成對網頁運行來講沒必要要的代碼。 舉個例子,如下是 Sketch 導出的 SVG 的代碼:

<svg class="icon" verison="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
     stroke="#000">
  <circle cx="12" cy="12" r="10"/>
</svg>

svg-inline-loader 處理後會精簡成以下:

<svg viewBox="0 0 24 24" stroke="#000"><circle cx="12" cy="12" r="10"/></svg>

也就是說 svg-inline-loader 增長了對 SVG 的壓縮功能。

使用 svg-inline-loader 時相關的 Webpack 配置以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        use: ['svg-inline-loader']
      }
    ]
  }
};

加載 Source Map

因爲在開發過程當中常常會使用新語言去開發項目,最後會把源碼轉換成能在瀏覽器中直接運行的 JavaScript 代碼。 這樣作雖能提高開發效率,在調試代碼的過程當中你會發現生成的代碼可讀性很是差,這給代碼調試帶來了不便。

Webpack 支持爲轉換生成的代碼輸出對應的 Source Map 文件,以方便在瀏覽器中能經過源碼調試。 控制 Source Map 輸出的 Webpack 配置項是 devtool,它有不少選項,下面來一一詳細介紹。

devtool 含義
不生成 Source Map
eval 每一個 module 會封裝到 eval 裏包裹起來執行,而且會在每一個 eval 語句的末尾追加註釋 //# sourceURL=webpack:///./main.js
source-map 會額外生成一個單獨 Source Map 文件,而且會在 JavaScript 文件末尾追加 //# sourceMappingURL=bundle.js.map
hidden-source-map source-map 相似,但不會在 JavaScript 文件末尾追加 //# sourceMappingURL=bundle.js.map
inline-source-map source-map 相似,但不會額外生成一個單獨 Source Map 文件,而是把 Source Map 轉換成 base64 編碼內嵌到 JavaScript 中
eval-source-map eval 相似,但會把每一個模塊的 Source Map 轉換成 base64 編碼內嵌到 eval 語句的末尾,例如 //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW...
cheap-source-map source-map 相似,但生成的 Source Map 文件中沒有列信息,所以生成速度更快
cheap-module-source-map cheap-source-map 相似,但會包含 Loader 生成的 Source Map

其實以上表格只是列舉了 devtool 可能取值的一部分, 它的取值其實能夠由 source-mapevalinlinehiddencheapmodule 這六個關鍵字隨意組合而成。 這六個關鍵字每一個都表明一種特性,它們的含義分別是:

  • eval:用 eval 語句包裹須要安裝的模塊;
  • source-map:生成獨立的 Source Map 文件;
  • hidden:不在 JavaScript 文件中指出 Source Map 文件所在,這樣瀏覽器就不會自動加載 Source Map;
  • inline:把生成的 Source Map 轉換成 base64 格式內嵌在 JavaScript 文件中;
  • cheap:生成的 Source Map 中不會包含列信息,這樣計算量更小,輸出的 Source Map 文件更小;同時 Loader 輸出的 Source Map 不會被採用;
  • module:來自 Loader 的 Source Map 被簡單處理成每行一個模塊;

該如何選擇

若是你不關心細節和性能,只是想在不出任何差錯的狀況下調試源碼,能夠直接設置成 source-map,但這樣會形成兩個問題:

  • source-map 模式下會輸出質量最高最詳細的 Source Map,這會形成構建速度緩慢,特別是在開發過程須要頻繁修改的時候會增長等待時間;
  • source-map 模式下會把 Source Map 暴露出去,若是構建發佈到線上的代碼的 Source Map 暴露出去就等於源碼被泄露;

爲了解決以上兩個問題,能夠這樣作:

  • 在開發環境下把 devtool 設置成 cheap-module-eval-source-map,由於生成這種 Source Map 的速度最快,能加速構建。因爲在開發環境下不會作代碼壓縮,Source Map 中即便沒有列信息也不會影響斷點調試;
  • 在生產環境下把 devtool 設置成 hidden-source-map,意思是生成最詳細的 Source Map,但不會把 Source Map 暴露出去。因爲在生產環境下會作代碼壓縮,一個 JavaScript 文件只有一行,因此須要列信息。
在生產環境下一般不會把 Source Map 上傳到 HTTP 服務器讓用戶獲取,而是上傳到 JavaScript 錯誤收集系統,在錯誤收集系統上根據 Source Map 和收集到的 JavaScript 運行錯誤堆棧計算出錯誤所在源碼的位置。

不要在生產環境下使用 inline 模式的 Source Map, 由於這會使 JavaScript 文件變得很大,並且會泄露源碼。

加載現有的 Source Map

有些從 Npm 安裝的第三方模塊是採用 ES6 或者 TypeScript 編寫的,它們在發佈時會同時帶上編譯出來的 JavaScript 文件和對應的 Source Map 文件,以方便你在使用它們出問題的時候調試它們;

默認狀況下 Webpack 是不會去加載這些附加的 Source Map 文件的,Webpack 只會在轉換過程當中生成 Source Map。 爲了讓 Webpack 加載這些附加的 Source Map 文件,須要安裝 source-map-loader 。 使用方法以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 只加載你關心的目錄下的 Source Map,以提高構建速度
        include: [path.resolve(root, 'node_modules/some-components/')],
        use: ['source-map-loader'],
        // 要把 source-map-loader 的執行順序放到最前面,若是在 source-map-loader 以前有 Loader 轉換了該 JavaScript 文件,會致使 Source Map 映射錯誤
        enforce: 'pre'
      }
    ]
  }
};
因爲 source-map-loader 在加載 Source Map 時計算量很大,所以要避免讓該 Loader 處理過多的文件,否則會致使構建速度緩慢。 一般會採用 include 去命中只關心的文件。

再安裝新引入的依賴:

npm i -D source-map-loader

重啓 Webpack 後,你就能在瀏覽器中調試 node_modules/some-components/ 目錄下的源碼了。

實戰總結

在實際應用中,會遇到各類各樣的需求,雖然前面的小節中已經給出了大部分場景需求的解決方案,但仍是很難涵蓋全部的可能性。 因此你本身須要有能力去分析遇到的問題,而後去尋找對應的解決方案,你能夠按照如下思路去分析和解決問題:

  1. 對所面臨的問題自己要了解。例如在用 Webpack 去構建 React 應用時你須要先掌握 React 的基礎知識。
  2. 找出現實和目標之間的差別。例如在 React 應用的源碼中用到了 JSX 語法和 ES6 語法,須要把源碼轉換成 ES5。
  3. 找出從現實到目標的可能路徑。例如把新語法轉換成 ES5 可使用 Babel 去轉換源碼。
  4. 搜索社區中有沒有現成的針對可能路徑的 Webpack 集成方案。例如社區中已經有 babel-loader
  5. 若是找不到現成的方案說明你的需求很是特別,這時候你就須要編寫本身的 Loader 或者 Plugin 了。

在解決問題的過程當中有如下2點能力很重要:

  1. 從一個知識你須要儘量多的聯想到其相關連的知識,這有利於打通你的知識體系,從經驗中更快地得出答案。
  2. 善於使用搜索引擎去尋找你所面臨的問題,這有利於藉助他人的經驗更快地得出答案,而不是本身從新探索。

最重要的是你須要多實戰,本身去解決問題,這有利於加深你的影響和理解,而不是隻看不作。

相關文章
相關標籤/搜索