記前端從「刀耕火種」過渡到到「現代化」的自動構建工具(在ThinkPHP的項目裏使用webpack)

我是14年入的程序員大軍,當時主java兼具前端開發的活兒,在如今看來的一些流開發框架和新興思想,早在node.js開始進入你們視野的時候就流行起來了,只是在那時博主並無關注前端的生態圈(然而java好像也並無關注,逃),因此仍是處在不少人所描述的刀耕火種的階段,前端代碼所有掛載到全局做用域,包括插件導出的變量。那更別提組件化模塊化的編程思想了,甚至代碼都不用壓縮優化就直接上傳到服務器發佈了。javascript

後來換了一家公司,沒有前端開發這個職位,是從javaer轉過去的,由於項目須要,漸漸的也就坐實了這個崗位。項目到如今(2014年8月-2017年7月22日)一共出現了三個階段php

  • 用着十年前的開發(或者叫整合)技術的簡陋期
  • 經歷四、5個月的半模塊化改造的準現代期
  • 到如今能整合全局資源(僅限web靜態資源),隨意整合新技術的現代期(未實施)

爲何要不斷的去折騰,去改造?僅僅是爲了跟上「現代」的步伐嗎?下面我將講述每一個階段是如何無痛改造的,爲何要改造。css

簡陋期準現代期

舉個例子,咱們之前的代碼是這樣的html

html頁面部分

<html>
<head>

<link href="style.css" rel="stylesheet"/>
</head>
<body>

<!-- 通用的代碼 -->
<script src="common.js"></script>
<!-- 第三方的插件代碼 -->
<script src="plugin.js"></script>
<!-- 咱們的主入口 -->
<script src="individual.js"></script>
</body>
</html>

javascript部分

common.js裏,是咱們的定義的通用函數,好比一些特定組件的部分代碼如headerfooter,或者是字符串處理,日期格式化的函數等等,這些函數都以對象或函數的形式暴露在全局做用域裏,很是的冗雜和不安全,隨着代碼量的增長,容易致使覆蓋,出現難以預料的bug,還有一個致命的弱點就是沒法按需加載資源,我哪怕只是用到了其中一個小小的常量,都須要引用整個文件,而後從全局做用域裏拿。前端

// common.js
var Header = {
  var1: '',
  var2: {},
  fn1: function() {
    // some code
  },
  fn2: function() {
    // some code
  }
}

function strReplace() {
  // some code
}
...
// individual.js
// 也許咱們早已有覺悟 使用了自執行匿名函數來防止全局變量的污染
(function() {
  // 這裏咱們須要用到commonjs的函數 常量等
  var afterHandleStr = strReplace(str);

  // 也許咱們忘記strReplace函數已存在全局做用域又或者換了一我的
  // 來維護這個文件可能又會定義一個函數叫strReplace
  function strReplace() {
    // 那麼此時根據javascript特性,原先的函數已經被覆蓋了,
    // 上面的調用邏輯優先從最近的做用域開始找,因而會執行到這裏
  }

  ...
}());

由於項目是迭代開發的,功能一點點疊加上去,考慮到整個項目的生命週期,不至於到後期徹底沒法維護,因此咱們必須以優雅的姿態去重構整個項目的資源引用方式,那就是模塊化,一個模塊作一件事情,並暴露它對外提供的接口以供具象化的頁面來使用。好比headerfooternavsidebarutils等等。前端的模塊化有倆個標準,一個AMD(Asynchronous Module Definition),一個是CMD(Common Module Definition),前者是異步模塊定義,推崇依賴前置,後者是通用模塊定義,推崇依賴就近,AMD的表明框架有requirejs,CMD的表明框架有seajs,都是很優秀的做品,這裏對兩者有詳細的介紹。最後我選擇了requirejs做爲本次重構的基礎,其實就當是的代碼來講,改造起來並無什麼難度,就是須要細心,細心,細心,只須要將common.js這個通用模塊進行拆分就行了,頁面只須要引入一個js文件,以下面這樣java

<html>
<head>

<link href="style.css" rel="stylesheet"/>
</head>
<body>
...
<script type="text/javascript" data-main="/individual.js" src="/require.js"></script>
</body>
</html>

data-main是咱們的代碼主入口,srcrequireJs的源碼。從文件引用來講,至少咱們沒必要再關心每次使用一個插件都要手動來加入一個script標籤了,如何引用呢?我下面會介紹。
假如咱們之前的代碼是這樣的node

// common.js
(function(){

  var exportObj = {
    aa: 'aa',
    bb: 'bb'
    ...
  }

  var utils = {
    replaceStr: function() {

    }
    ...
  }

  // 放到全局做用域
  window.exportObj = exportObj;
  window.utils = utils;
}());



// individual。js
(function(){

  var aa = constants.aa;
  var bb = constants.bb;
  
  var tempStr = utils.replaceStr('tempStr');

}());

上面的代碼使用了兩個全局對象,constantsutils,那麼改造後應該是:webpack

// constants.js
// 若是它不依賴於其餘模塊,就沒必要聲明依賴的數組
define( function() {
  var exportObj = {
    aa: 'aa',
    bb: 'bb'
    ...
  }

  // 返回咱們要暴露出來的對象,不用再放到全局做用域
  return exportObj;
} );

// utils.js
define( function() {

  // 返回咱們要暴露出來的對象,不用再放到全局做用域
  return  {
    replaceStr: function() {

    }
    ...
  };
} );

// individual
define( [
  'constants',
  'utils'
], function( consts, utils ) {
  var aa = consts.aa;
  var bb = consts.bb;
  
  var tempStr = utils.replaceStr('tempStr');
} );
// 或者
define( [
  'constants',
  'utils'
], function() {
  var consts = require('constants');
  var utils = require('utils');

  var aa = consts.aa;
  var bb = consts.bb;
  
  var tempStr = utils.replaceStr('tempStr');
} );

是否是感受毫無挑戰性,對,這就是一個體力活,細心點就行了程序員

咱們沒必要擔憂還須要手動去改動第三方插件,如今的主流插件基本都會UMD方式去適配,也就是兼容了AMDCMD,因此只須要直接引用第三方插件就好了,沒必要再去html文件裏手動引用script標籤了,其餘具體實現細節和必備的配置能夠參照requirejs官網的例子web

等到改造完,也尚未愉快的結束,咱們的準現代期增長了一個優化環節,官方提供了r.js這個優化器來幫咱們打包壓縮代碼(畢竟生產環境過多的請求數仍是不被容許的),此時的改造才真的作到了模塊化能優化,從簡陋期無痛過渡到準現代期。此時的代碼,其實已經具有了進入現代期的要求,那就是規範模塊化。下面是咱們即將進行的改造,順利過渡到現代期,從而擁抱你想使用的新技術

準現代期現代期

其實這個階段,由於對一些新工具新技術的不熟悉,繞了不少彎子,花費了很多的精力,好在弄出來了,基於webpack構建工具,解放鍵盤F5,加入代碼風格和規範的檢查工具,加入ECMAScript 6語法轉換工具等等,爲何要使用這些,歸納爲主要如下幾點:

  • 提高開發效率和代碼質量
  • 新語法新和技術能解決開發上的不少痛點和盲點
  • 強大的整合性和包容性(相對於封閉的r.js優化器終於可定製了)
  • emmmmmm思考中

首先咱們介紹一下webapck是什麼
webpack
這是webpack官方文檔首頁對其的簡單描述(ps: 其實中間的正方體是會旋轉的哦),強大的webpack能整合全部依賴的文件進行處理,如less編譯(依賴less-loader),ES6語法轉換(依賴babel-loader),文件hash添加,自動上傳ftp發佈生產環境等等。還有就是webpack-dev-server這個開發神器,熱替換自動監測文件變化刷新瀏覽器,雖然現代期並無用到實際項目中去,可是到如今(2017年7月22日),我已經能徹底拿出一套方案,使現有項目平滑過分到webpack。(ps:網上的教程大可能是基於單頁單入口的SPA應用,和後端徹底解耦的,咱們的項目是和後端處於半解耦狀態,而且是多頁多入口,因此並不能使用大多數的webpack配置文件,須要進行變通處理)

咱們先來講說處理通常的SPA應用的配置參數

module.exports = {
  entry: {
    // webpack生成的文件名和入口文件
    // 咱們是多頁入口,先以首頁爲例
    index: './Public/dev/page/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
  },
  module: {
    rules: [  {
      // 各類loaders
    } ]
  },
  plugins: [
    new HtmlWebpackPlugin( {
      // 模板生成後的文件名(能夠加上路徑)
      filename: 'index.html',
      // 入口文件的模板(也就是承載你頁面視圖的地方)
      template: 'index.html',
      inject: true
    })
  ]
}

拋開其餘雜七雜八配置不談,上面的配置就是大多數的SPA應用的配置。用在咱們的項目裏,在根目錄運行webpack會發現發生錯誤,並提示缺乏不少的模塊,由於這些咱們自定義的模塊webpack自己並不能識別,因此這裏有相當重要的一步,將現有的requirejs的配置文件裏的paths同步遷移到webpack的配置文件裏

// 在requirejs配置文件裏多是這樣寫的
require.config( {

  paths: {
    header: './modules/header',
  }

} );

// 那麼咱們就應該將此配置交給webpack
resolve: {
  alias: {
    header: 'modules/header' // 路徑可能不必定是這個
  }
}

而後咱們再打包,運行,發現丫的竟然會報錯了?最明顯的錯誤就是define is not defined。讓咱們來翻翻上面咱們準現代期的代碼

// individual
define( [
  'constants',
  'utils'
], function( consts, utils ) {
  var aa = consts.aa;
  var bb = consts.bb;
  
  var tempStr = utils.replaceStr('tempStr');
} );

這裏的define就是報錯的緣由(webpack有時候並不能識別這裏,有時候卻又能正確轉換成能運行的代碼,沒有深究這裏的緣由,雖然webpack2已經支持AMD風格的代碼打包,可是我仍是決定對這裏稍做修改,變成CMD風格,即便是使用CMD風格的seajs依然是須要去掉外面那層包裹的函數的,無論怎樣都得改),因而咱們只須要將上面的代碼調整爲:


2017年7月24日22點50分更新,通過個人嘗試,只要配置依賴都正確,徹底能夠直接打包,不用非得改爲CMD,因而換成webpack更輕鬆了~~?


var consts = require('constants');
var utils = require('utils');

var aa = consts.aa;
var bb = consts.bb;
var tempStr = utils.replaceStr('tempStr');

// 若是這裏有return的話須要將return obj調整爲
module.exports = obj;

至此咱們再打包即可以輕輕鬆鬆合併了(固然若是你要提取公共代碼的話又是另一個插件了,這裏再也不贅述)

打包發佈的問題解決了,最重要的一環開發環境的搭建呢?

其實機智的我早料到這種配置在咱們的項目並不完美,由於HtmlWebpackPlugin這個插件須要的模板是放在硬盤裏的靜態文件模板,它會自動插入構建好的jscss文件,咱們的模板不是靜態的,是從php後端渲染的一段動態的html,仍是做死試了試,果真出現瞭如下狀況

  • 動態引用的header、footer不見了
  • 頁面出現一堆後端模板的語法{$xxx}{$yyy}{$zzz}

其實webpack-dev-server提供了一個代理功能,那這裏的問題解決起來就美滋滋了。單純的我最早的配置是這樣的:

var express = require( 'express' )
var proxyMiddleware = require( 'http-proxy-middleware' )
var app = express();
// 這是代理的
var proxyTable = {
  '/': {
    target: 'http://xxxx.cn/'
  }
}
Object.keys( proxyTable ).forEach( function( context ) {
  var options = proxyTable[ context ]
  if ( typeof options === 'string' ) {
    options = {
      target: options
    }
  }
  // 應用代理地址和代理目標
  app.use( proxyMiddleware( options.filter || context, options ) )
} )

以上代碼將咱們全部的請求路徑一股腦所有代理給後端php服務了,HtmlWebpackPlugin這個插件會自動寫入依賴的腳本文件和樣式表文件,可是此時的文件是webpack-dev-server服務生成的,而且存在於內存裏,因此此時咱們再運行webpack開啓的服務,就會形成頁面出來了(包括任何動態從服務端渲染的數據),可是樣式和js都沒有加載,由於請求被代理到了後端,後端的目錄裏並不存在這些文件(廢話麼),因此咱們須要過濾掉這些特定的請求不讓http-proxy-middleware插件進行代理,爲了區分這些特定的請求,咱們將entry字段裏的文件名都加上一個前綴__webpack或任何獨一無二的與後臺請求開頭不同的字符串,此時proxyTable裏的filter函數就派上用場了,查看官方文檔是這麼描述這個函數的

For full control you can provide a custom function to determine which requests should be proxied or not.

爲了徹底控制你的請求,你能夠定義一個函數來肯定這些請求是否應該被提交

因而我終於拿出一個滿意的代理配置文件,開心得我彷彿升職加薪了同樣?

var proxyTable = {
  '/': {
    target: 'http://xxx.cn/',
    changeOrigin: true,
    filter( pathname, req ) {
      return !new RegExp( `^\/(__webpack|${assetsSubDirectory})` ).test( pathname );
    }
  }
}

讓我來解釋一下上面的代碼:未匹配到以__webpack開頭的請求,都進行代理,這裏添加了一個assetsSubDirectory變量,這個變量實際上是webpack生成的圖片、字體文件、json文件、svg等仍然存在於內存裏的引用的路徑,由於在內存裏隨着咱們的編碼可能實時變更,因此它們仍是不須要作代理,直接過濾掉。

對了,遺漏了一個很重要的配置,代碼以下:

plugins: [
  new HtmlWebpackPlugin( {
    alwaysWriteToDisk: true,
    // php端使用到的模板
    filename: `${ROOT}/Application/Home/View/Index/index.html`,
    // 模板文件
    template: `${ROOT}/Application/Home/View-template/Index/index.html`,
    chunks: [ '__webpack-indexController' ],
    inject: true
  })
]

機智的咱們確定能發現View-template這裏的不一樣,見名知意,這個文件夾裏的html都是對應的後端的模板視圖文件,咱們經過alwaysWriteToDisk這個參數(其實還須要配合另一個插件)以template字段的值爲目標,實時寫入到filename對應的文件裏,而此時,由於瀏覽器訪問的頁面裏由於咱們啓動webpack-dev-server時已經編譯了這個文件,js會主動和webpack服務創建一個eventSource長鏈接(這個鏈接也是排除在代理範圍內的)來監聽文件變化,因此就會自動刷新瀏覽器,從而實現咱們的live-reload

至此,從準現代期現代期的過渡方案就算是完成了,接下面即是尋找一個合適的時間點實施到項目中去。若你要問我那麼多頁面是否是全都一個個得配,固然是,可是爲了方便易維護,能不侵入現有項目去修改文件名,咱們確定須要去手動編寫一個map映射文件,來指明咱們的模板文件對應的入口文件,經過這個map咱們再來動態生成entryHtmlWebpackPlugin須要的模板路徑,固然這裏並非沒有便捷的辦法,咱們能夠寫一個腳本去讀取View-template下面的目錄來自動生成map可是由於咱們童鞋在命名的時候文件夾和對應的入口文件並不能對應上,就得修改,這並非推薦的作法,並且也不方便咱們在改造代碼風格的時候進行單個調試。

上面的示例代碼都不是完整的,由於我並非要提供一個webpack的教程,而是解決後端和前端html耦合的webpack-dev-server配置的問題。


以上內容都轉自我本身的博客原文地址

相關文章
相關標籤/搜索