webpack與SPA實踐之開發環境搭建

熊建剛發表於2017-05-31

目前,公司前端專案開發技術棧基本改造為使用gulp進行自動化構建,webpack進行專案模組化依賴管理,Vue+ Vuex + Vue-Router作為專案元件化開發框架,為了更深入的理解、使用當前技術棧並與讀者分享、交流,計劃推出一系列相關學習與實踐文章。本篇為開篇,主要講述如何使用webpack搭建開發環境。

歡迎訪問我的個人部落格

專案初始化

以你喜歡的任意方式,建立專案根目錄,如:


    mkdir vue-hello複製程式碼

初始化包模組管理檔案

進入專案根目錄,初始化專案包模組管理檔案package.json:


    npm init複製程式碼

命令臺會提示你輸入一堆資訊,如果你想簡單一點,可以新增-y引數,跳過輸入步驟,生成預設資訊:


    npm init -y複製程式碼

webpack與SPA實踐之開發環境搭建
初始化專案package.json

初始化原始碼目錄

在專案根目錄下建立原始碼目錄結構,通常原始碼目錄是src或app,個人喜好是使用src:

webpack與SPA實踐之開發環境搭建

webpack簡述及使用

雖然在本篇文章我們不會對webpack做太過詳細的介紹,但是依然希望能幫助讀者對webpack擁有更清晰的瞭解,webpack是什麼?

webpack is a tool to build JavaScript modules in your application

webpack是一個幫助你的應用構建JavaScript模組的工具。

接下來,我們介紹幾個知識點幫助理解webpack:模組化,webpack原理及其與grunt和gulp的比較。

模組化

模組化 指解決一個複雜問題時自頂向下逐層把系統劃分成若干模組的過程。

模組是一個可組合、分解和更換的單元,將一個系統分解成若干模組,單元;大家遵循一定的規範,各司其職,各自開發不同模組;之後可以較低成本的將模組組合起來,構成一個完整的系統,極大方便了團隊成員之間的協助開發,產出效率得到有效提升。

webpack原理

webpack是一個幫助你的應用構建JavaScript模組的工具,其本質只能處理JavaScript,那你會疑惑了,不是說使用webpack,可以很方便的在JavaScript程式碼中引用圖片,CSS等資源嘛?是的,這正是webpack的優勢,那怎麼實現的呢?這就要涉及到webpack中的一個概念:載入器(loader)

載入器 是作用於應用資原始檔的轉換器,它們是一系列JavaScript函式,接受資原始檔的內容做引數,然後返回新的資源(以一個JavaScript模組的形式返回)。

所以,對於webpack,我們明確三點:

  1. 模組:webpack中一切資原始檔(JavaScript, 樣式, 圖片資源等)皆視為模組;
  2. 載入器:webpack通過載入器(JavaScript函式)將其他資源處理(構建)成JavaScript模組;
  3. 管理依賴:webpack在編譯模組時,就能分析查詢該模組內的依賴,可以很好的處理不同模組間的依賴;

webpack對比grunt/gulp

  • grunt

    開啟grunt官網,你可以看到最醒目的介紹:The JavaScript Task Runner,還有一個關鍵字automation - 自動化,其定位是一個JavaScript的自動化構建任務處理器,幫助開發者自動化處理專案的構建流程;

  • gulp

    gulp官網的定義是:Automate and enhance your workflow,自動強化專案構建流程,其與grunt的目標一致,都是幫助開發者自動化處理專案的構建流程,不同的是gulp實現方式是基於流的,即以流的方式處理檔案,而grunt是以二進位制方式處理檔案,gulp使用效能是要強於grunt的;

  • 總結

    1. webpack定位是一個模組化管理工具,而grunt/gulp都是自動化任務流程構建工具;
    2. grunt基於二進位制處理檔案,gulp基於流式處理檔案,效率比grunt更高一些;
    3. webpack強大特性,使得其新增諸多外掛可以替代grunt/gulp,但是目前的實踐專案中,通常webpack結合gulp或grunt使用(各自處理各自專長的任務);

安裝

首先安裝webpack,npm或yarn都可以,無甚區別:


    npm install --save-dev webpack複製程式碼

關於此處的--save-dev引數做簡要說明:

  1. --save是宣告將安裝依賴新增入package.json檔案;
  2. 預設地,使用npm安裝包模組依賴時,依賴關係儲存在在"dependencies"屬性物件內,表示專案依賴;
  3. webpack是作為開發環境依賴的,不是作為原始碼直接呼叫,所以新增-dev引數,以宣告其是開發環境依賴;

webpack配置介紹

實踐專案使用webpack完成自動化構建,本地服務除錯與熱載入,首先在根目錄下建立webpack的配置入口檔案webpack.config.js,基本內容結構如下:


    var path = require('path');

    module.exports = {
        context: path.resolve(__dirname, 'src/'),
        entry: {
            app: './scripts/app.js'
        },
        output: {
            filename: '[name].js',
            path: path.resolve(__dirname, 'dist/scripts/')
        },
        module: {
            loaders: []
        },
        resolve: {
            modules:[],
            alias: {},
            extensions: []
        },
        plugins: [],
        devServer: {}
    };複製程式碼

如上,webpack配置檔案使用module.exports方式匯出配置物件,webpack執行時會預設讀取專案根目錄下webpack.config.js檔案,當然可以手動配置指定一個檔案作為配置檔案,我們不討論,可以參考webpack文件,接下來對webpack配置內容做簡要介紹,如果你對webpack使用比較熟悉,可以跳過此節。

檔案處理配置

和webpack檔案處理相關的幾個配置屬性主要有三個:目錄上下文資訊(context), 處理檔案入口資訊(entry), 檔案輸出資訊(output)。

目錄上下文資訊(context)

設定解析處理檔案入口的相對目錄,值為一個絕對目錄路徑,預設為當前執行目錄,通常即專案根目錄,在Node中其值與process.cwd()相同,如:


    context: path.resolve(__dirname, 'src/'),複製程式碼

如上即解析為專案根目錄下的src目錄。

處理檔案入口資訊(entry)

處理檔案入口,值可以是字串,或陣列,或物件,值為字串或陣列時,即輸出單檔案,值為物件,可以輸出多檔案,輸出檔名稱等資訊參考檔案輸出資訊(output)。

檔案輸出資訊(output)

此配置宣告webpack編譯輸出檔案的檔名等資訊,如:


    filename: '[name].js',複製程式碼

宣告檔名就是模組(chunk)名,對應在entry中定義的入口,你可能還見過[id],[hash],[chunkhash]這些用法,現做簡單介紹:

  • [id]:該值表示webpack編譯模組(chunk)的id,通常是一個數字;
  • [name]:該值表示webpack編譯模組(chunk)名,對應entry中定義的入口名或檔名;
  • [chunkhash]:該值表示webpack編譯模組(chunk)hash值,根據模組內容計算出的一個md5值;
  • [hash]:該值表示webapck編譯物件hash值,根據編譯物件計算出的md5值;

編譯物件,即webpack執行時讀取配置後生成的一個編譯配置物件,包含模組,待編譯檔案,相對於上次編譯的變更檔案等諸多資訊,需要注意的是該物件在webpack啟動讀取配置檔案後形成,在此次編譯過程保持不變。

  • output.path:定義輸出檔案所在目錄;
  • output.publicPath:定義輸出檔案在瀏覽器訪問時的基礎相對路徑,可以與後文webpack-dev-server一起使用。

webpack載入器與模組

在介紹載入器配置之前,先看看載入器是什麼:

Loaders are transformations that are applied on a resource file of your app. They are functions (running in node.js) that take the source of a resource file as the parameter and return the new source.

載入器是作用於應用資原始檔的轉換器,它們是一系列函式,接受資原始檔的內容做引數,然後返回新的資源(以一個模組的形式返回)。

webpack

webpack解析檔案時使用的載入器都宣告在module屬性的loaders陣列中,可以設定一個或多個載入器。

  • module.noParse:指定某些檔案不需要使用解析,支援傳入檔案路徑或正規表示式;
  • module.loaders:指定解析檔案的載入器模組及各模組解析規則,可以設定以下屬性:
    1. test: 待解析檔案需匹配的規則,通常以檔案字尾名稱匹配檔案;
    2. include:待解析檔案所處目錄;
    3. exclude: 過濾掉的目錄;
    4. loader:載入器模組名稱;
    5. loaders多個載入器;

模組解析規則配置

webpack支援在resolve屬性物件中配置模組解析規則,主要有rootmodules, aliasextensions屬性。

resolve.root與resolve.modules

該屬性設定的是在開發程式碼中使用requireimport載入某模組時,webpack查詢該模組所遵循的查詢目錄範圍,如在原始碼中存在:


    var utils = require('utils/utils.js');複製程式碼

root配置如下:


    resolve: {
          root: [
            path.resolve('./src/'),
            'node_modules'
          ]
    }複製程式碼

webpack編譯時將自動在專案根目錄下的src目錄內的utils目錄下查詢utils.js,若存在,則返回,否則進入node_modules目錄內查詢utils.js模組。

當然,若未設定resolve.root屬性值,則webpack預設先從node_modules查詢模組,然後在根目錄下查詢。

注意:resolve.root在webpack v1版本中使用,而在webpack v2 中,使用modules代替,建議使用v2版本,後文也將統一使用modules屬性。

resolve.alias

alias,有別名的意思,這裡的作用是設定其他模組或路徑的別名,webpack在解析模組時,將使用配置值替換該別名,如,在未設定alias屬性內容時,即resolve.alias預設是空物件{}時,我們在程式碼中引用模組:


    var Vue = require('vue/dist/vue.js');
    var TopHeader = require('components/header.js');複製程式碼

webpack在編譯程式碼時,按resolve.modules宣告的順序依次查詢對應模組,如按照上一節定義的resolve.modules


    resolve: {
          root: [
            path.resolve('./src/'),
            'node_modules'
          ]
    }複製程式碼

查詢模組時,將首先在src/vue/dist/目錄下尋找vue模組,在src/components/目錄下查詢header.js模組,查詢到後返回,否則進入node_modules目錄查詢。

現在,我們還可以通過resolve.alias為模組或路徑宣告一個別名,更方便的宣告載入模組:


    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.js'
            components: path.resolve(__dirname, 'src/components/')
        }
    }複製程式碼

我可以使用如下方式載入模組:


    var Vue = require('vue');
    var TopHeader = require('components/header.js');複製程式碼
  • $符號

關於alias的詳細使用介紹,請參見文件,本文並不是要介紹webpack文件,在這裡介紹一下宣告的vue$別名中的$符號:

這裡的$符合是正則的文末匹配符,即只有當vue是最後一級路徑時,webpack才會將該值解析成別名,進行別名與對應值替換,如vue/test.js中的vue是不會當作別名解析的,而components/header.js中的components則會按照宣告的components別名進行解析,其結果是src/components/header.js

resolve.extensions

該值定義解析模組時的查詢檔案的字尾順序,如["", ".js", ".json"],會優先返回js檔案,其次json檔案,然後是其他檔案,注意,這裡陣列傳入了一個空字串,他的作用是在未找到配置中所有列舉的型別檔案時,支援webpack返回其他型別檔案。

webpack外掛配置

webpack還支援多種多樣的外掛擴充,你可以使用它們對你的專案webpack模組構建過程進行額外處理,如程式碼壓縮,圖片等資源提交壓縮,構建異常捕獲和提升,構建流程時間消耗比,等等,而關於這些外掛使用的配置在plugins屬性陣列內,將在後續進行介紹。

專案本地除錯與開發

為了方便開發和除錯,通常都需要在本地主機開啟服務,提供區域網內多終端訪問,並且在檔案變更時,實時重新整理頁面,正如grunt和gulp中Browsersync外掛提供的功能一樣,webpack可以使用webpack-dev-server模組實現。

webpack-dev-server是一個Node.js的express伺服器,以webpack開發中介軟體的形式為webpack包提供服務,當監聽到原始碼檔案變更時,會自動重新打包,並且支援配置自動重新整理瀏覽器,重新載入資源。

安裝

由於該外掛只用於開發模式,所以通過以下npm指令安裝:


    npm install webpack-dev-server --save-dev複製程式碼

配置

啟用webpack-dev-server時,其預設開啟8080埠,訪問localhost:8080返回當前目錄下的index.html,即執行指令當前目錄下的靜態資源,當然可以通過指令傳遞引數或在配置檔案進行配置指定靜態資源目錄。

另外需要注意的是,開啟webpack-dev-server後,變更檔案重新打包後,並不會實際輸出到配置的output目錄,而是在publicPath屬性宣告的相對路徑所在的記憶體中讀取。

靜態資源目錄配置(content base)


    webpack-dev-server --content-base src/複製程式碼

執行以上指令開啟服務後,webpack-dev-server將預設在src/目錄返回靜態資源,當然也可以在webpack.config.js配置檔案中指定:


    devServer: {
        contentBase: './src'
    }複製程式碼

訪問http://localhost:8080http://localhost:8080/index.html效果一樣,均返回src目錄下的index.html檔案。

publicPath與輸出檔案訪問

在前文webpack配置一節中提到output.publicPath屬性值表示,在瀏覽器訪問webpack output輸出的檔案時的基礎相對路徑,如設定:


    output: {
        path: 'dist/scripts',
        filename: 'app.js',
        publicPath: 'assets/'
    }複製程式碼

則在html檔案中引用該app.js檔案的方式如下:


    <script src="assets/app.js"></script>複製程式碼

在瀏覽器中訪問app.js的方式為:http://localhost:8080/assets/app.js

自動重新整理(Automatic Refresh)

前面說到webpack-dev-server支援檔案變更時,自動重新整理瀏覽器,這也是開發者急需的功能,webpack-dev-server支援兩種方式實現:

  • iframe模式(iframe mode):頁面通過iframe視窗插入然後變更時自動重新載入;
  • inline模式(inline mode):通過sock.js(比如websocket協議,輪詢等方式)在頁面嵌入一個小型客戶端與webpack-dev-server伺服器建立連線,當發生變更重新打包時,通過此連線通知頁面重新載入;
iframe模式

使用預設的iframe模式時,不需要進行任何配置,可以直接訪問:http://localhost:8080/webpack-dev-server/index.html即可,如圖,通過iframe插入我們的頁面:

webpack與SPA實踐之開發環境搭建
iframe模式(iframe mode)

注意:

  • 在應用頁面上方會有狀態列顯示當前應用資訊;
  • 應用URL的變更發生在iframe內部,不會反映在瀏覽器位址列;
inline模式

要開啟inline模式,只需要指定--inline命令列引數或在配置檔案中指定inline: true


    devServer: {
        contentBase: './src',
        inline: true
    }複製程式碼

在此模式下,直接訪問http://localhost:8080/index.htmlhttp://localhost:8080即可,此模式與iframe模式的區別在於:

  • 訪問URL不同;
  • 需指定inline配置引數;
  • 應用資訊在控制檯輸出;
  • 應用URL的變更直接反映在瀏覽器位址列;

其實inline模式還可以配合Node.Js服務執行,之後再介紹,這裡只說明瞭其在HTML文件中的使用。

熱替換(Hot Module Replacement)

除了自動重新整理,webpack-dev-server的另一大賣點是模組熱替換,那麼熱替換到底是什麼?

熱替換,即檔案發生變更時,webpack包只替換髮生變更的模組,而不需要全部替換;瀏覽器不需要完全重新載入,而可以直接將變更的模組注入到執行中的應用。

開啟熱替換功能需要指定--hot指令引數,或在配置檔案中新增:


    devServer: {
        contentBase: './src',
        inline: true,
        hot: true
    }複製程式碼

注意到以上程式碼,熱替換需要與inline模式一起使用,另外需要指定output.publicPath值。

HotModuleReplacementPlugin

配置好後還需要使用webpack.HotModuleReplacementPlugin外掛,才能真正啟用熱替換功能:


    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]複製程式碼

同樣的,熱替換可以搭配Node.js服務一起使用,之後介紹。

如圖,為每個檔案(app.js, test.js)都實現了HMR熱替換,當發生變更時,只更新變更模組,而不是重新載入所有檔案:

webpack與SPA實踐之開發環境搭建
熱替換

擁抱ES6

ES6推出以來,廣受Jser們青睞,其確實有很多新特性和新規範,值得我們深入學習並使用,可以參考ECMAScript 6入門,未來所有的JavaScript應用都應該擁抱ES6,不過,目前各大瀏覽器都在推進ES6的實現,在相容實現之前,我們還需要過渡性的將其轉換成ES5語法,最通用的方式就是使用babel轉換。

使用babel載入器

首先,為了能使用webpack和babel轉換js程式碼,需要使用babel-loader載入器,另外還需要安裝babel轉換js的轉換規則外掛,如babel-preset-es2015定義了轉換規則,安裝方式如下:


    npm install --save-dev babel-loader babel-preset-es2015複製程式碼

然後在根目錄下建立.babelrc檔案,內容:


    {
        "presets": ["es2015"]
    }複製程式碼

在webpack.config.js配置檔案中,新增相關loader配置:


    module: {
        loaders: [
            {
                test: /\.(js|vue)$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
            }
        ]
    }複製程式碼

其中,test匹配需要轉換的檔案,exclude匹配不需要轉換的檔案或目錄。

babel-polyfill

我們需要明白的一點是babel-preset-es2015能做的,只是轉換ES6程式碼成ES5,使得瀏覽器可以解析執行,但是對於ES6新提出的API,如Promise,Generator等無法簡單的轉換成ES5程式碼,這時就需要babel-polyfill了,babel-polyfill是一個墊片,它可以模擬提供所有的ES6功能和特性,可以看作是提供了一個模擬的全域性ES6環境。

  • 安裝

安裝依然很簡單,由於是墊片,是需要在原始碼中使用的,所有指定--save引數:


    npm install --save babel-polyfill複製程式碼
  • 使用

不同於babel-preset-es2015babel-polyfill需要直接打包進原始碼,而且需要在使用ES6程式碼前引入一次:


    import 'babel-polyfill'複製程式碼

只需要引入一次,因為該墊片提供的是一個模擬的全域性ES6環境,而不是模組內部的。

或者直接在webpack.config.js中配置打包入口檔案時加入該墊片:


    entry: {
        app: ['babel-polyfill', './scripts/app.js']
    },複製程式碼

現在你可以在你的程式碼中使用任何你想用的ES6 API了。

到此,基本介紹瞭如何使用webpack搭建開發環境,下一篇將開始介紹如何使用webpack處理CSS及如何使用Vue進行元件化開發。

歡迎訪問我的個人部落格

相關文章