爲何我要構建這個腳手架

本文不是什麼技術性介紹文章,準確地說算是本身的成長記錄吧。剛參加工做時,組裏使用的腳手架是由 leader 使用 webpack, gulp 搭建的多頁面應用腳手架 fex。當時只需知道怎麼使用就好了,不過爲了能更好地工做,對 fex 怎麼構建一直很好奇,也一直關注相關的技術棧。通過一年多磨練後,對 fex 怎麼搭建的有了個大概地認識。常言道:"沒有對比就沒有傷害"。css

在使用 vue-cli 構建第一個 vue 項目後,對腳手架構建有了個全新的認識。發現 fex 存在不少不足:html

  • 在打包時,只對 JavaScript 和 CSS 腳本文件進行打包壓縮處理。不能對資源文件(如 img,字體等)進行依賴處理。致使在打包時:前端

    • 不能按需打包(即實際用到資源,纔將其進行打包)
    • 不能進行 MD5 處理
    • 不能輸出壓縮版的 html
  • 手動注入 JavaScript 和 CSS 腳本文件,若是須要作優化,會很不方便,特別在多頁面狀況下。
  • dev 與 build 使用不一樣的技術方案,增長定製的成本。
  • 基於 nodemon 對開發目錄進行 watch,當執行修改操做時,會重啓整個服務。會存在重啓服務耗時比較長的狀況,致使刷新頁面出現空頁面的狀況,開發體驗不是很好。
  • 缺乏 code-splitting、HMR、端口檢測、Babel 等功能。

固然,fex 也有本身的優勢。基於自建服務提供先後端複用模板功能。前端後端使用相同的模板語言,前端拼接的模板能夠直接輸出給後端使用。vue

第二年年初,組裏項目不是太多,恰好有時間折騰一下,因而決定構建一個全新的腳手架 fes。爲了嘗試一些新東西,在技術棧上,都使用了當時最新的技術框架 webpack四、koa二、babel6 來搭建。爲了瞭解 webpack 如何工做,對 webpack 就作了 8 次調試,才稍微對 webpack 整個架構有個初步認識。node

singsong: 在真正去了解 webpack 時,才知道它有多複雜。固然也參考了網上一些大神分享關於 webpack 源碼分析的文章。反正整個過程仍是挺熬心的🙂

同時,還對 koa二、babel6 作了相關的研究。附一張 koa2 分析圖吧😝:
koa2react

爲了提升 fes 開發體驗,除了繼承 fex 的模板複用功能外,還集成了 vue-cli 中不錯的功能。jquery

  • 兼容 macOS、windows、Linux 等操做系統,同時兼容主流瀏覽器及 IE 低版本。
  • ES六、SASS
  • js-code-splitting、css-code-splitting
  • 多頁面開發環境
  • proxy
  • css autoprefixer
  • css/svg sprite
  • 支持更靈活定製,如是否自動打開瀏覽器、熱加載等配置。
  • 自動監聽 port,若是被佔用,提示性切換。
  • 打包優化分析
  • 模板輸出(便於後端複用模板)
  • 基於mockjs 模擬 api。

在搭建 fes 過程當中,本身對前端代碼規範化的重要性有了本身的一些思考:webpack

前端開發的規範如 JavaScript 弱類型特性同樣,沒有統一規範。每一個人都有本身的一套編碼風格。這對團隊來講並非一件什麼好事。就拿咱們團隊來講吧。大多數的項目(前端)都是由我的來維護,不多有團隊合做的項目。由於每一個人編碼風格不一樣,致使下個接手維護的人須要從新習慣這種編碼風格。這就存在必定的學習成本,並且效率不高。可能原維護人只需花幾分鐘解決的事,接手人須要花幾個小時,甚至更多的時間和精力。對團隊合做項目來講,統一的編碼風格顯得更爲重要。由於不一樣的編碼風格會讓團體開發進度大打折扣,維護起來也很費力。另外,開發人員會對彼此編碼習慣存在不一樣程度的排斥現象。git

項目規範化的輔助性工具:github

  • eslint:規範 js 代碼
  • stylelint:規範 css 代碼
  • editorconfig:規範 IDE
  • husky 和 lint-staged:在 pre-commit 時 eslint、stylelint,確保風格一致、高質量的代碼輸出。

規範化的好處:

  • 規範化團隊的編碼風格,便於團隊內項目的維護。
  • 規範化可以讓開發規避一些常見的錯誤。如未使用的變量;文件命名錯誤,未能成功導入等。
  • 規範化對新人有很好的指導做用,好的開始很重要。由於這些規範都是行業內一些最佳實踐,可以讓新人成長得更加專業化。

爲了促進團隊的代碼規範化,本身也將 eslint、stylelint、prettier、husky、lint-staged 集成到 fes 中。

固然整個 fes 搭建過程當中也並非一路順風的,途中也碰見一些坑:

  • koa2 與 html-webpack-plugin

    在開發模式下,fes 是基於 html-webpack-plugin 插件自動生成 HTML 文件,而 html-webpack-plugin 插件合成的 html 緩存於內存中,爲了配合 koa2 輸出合成的 html 文件,須要將 html 文件寫入磁盤中。而要將 html-webpack-plugin 合成的 html 文件輸出到磁盤中,須要藉助 html-webpack-harddisk-plugin 插件。html-webpack-harddisk-plugin 是個基於 html-webpack-plugin 的插件。

  • html 中 img 的解析

    向來 webpack 對 html 的解析不是很友好。雖然 webpack 提供了 html-loader 來解析 html 中的 img。但 html-loader 是基於字符正則匹配來解析,即解析的是 html。但 fes 使用的是模板文件,這就須要對應模板 loader 來將其轉換爲 html。而 webpack 對 loader 的實現制定了相關的規範,爲了提升編譯性能,loader 通常返回的是一個 runtime 字符串,而不是最終編譯後的輸出。這樣不只有效地避免每次從新生成,也方便共享。因此爲了能讓 html-loader 解析模板文件,須要對模板 loader 作些定製,將其輸出由 runtime 變爲最終輸出編譯結果。

  • 對 twig 模板 include 文件修改,重編譯不生效

    開啓 twig.cache(false),也不能解決這個問題。經查閱 twig.js 源碼後,須要經過twig.extend擴展,對緩存對象進行初始化,來禁掉緩存。

    // 去掉緩存
    Twig.extend(T => {
      if (T.Templates && T.Templates.registry) {
        T.Templates.registry = {};
      }
    });
  • postcss-sprites 不支持 webpack 的 alias

    由於 postcss-sprites 是 postcss 的插件,獨立於 webpack。要讓 postcss-sprites 支持 alias,只能擴展 postcss-sprites 讓其支持與 webpack 同樣的 alias 配置項。須要在遍歷樣式節點時,根據 alias 配置項替換,換成真實數據。

    const replaceAlias = image => {
      const {alias} = opts;
      let {url, originalUrl} = image;
      const tempUrl = url;
      if (/^~/.test(url)) {
        Object.keys(alias).forEach(item => {
          url = url.replace(RegExp('^~' + item), alias[item]);
          if (url !== tempUrl) {
            originalUrl = path.relative(path.parse(styleFilePath).dir, url);
            url = originalUrl;
            // 替換源碼
            rule.replaceValues(tempUrl, {fast: tempUrl}, s => url);
          }
        });
      }
      image.url = url;
      image.originalUrl = originalUrl;
      return image;
    };
  • 模板複用

    fes 是基於 webpack-html-plugin 插件自動生成合成的 html 文件。但爲了提供工做效率,業務中存在對模板複用的需求,因此須要從新定製輸出。

    思路:經過 webpack-manifest-plugin 輸出資源清單 manifest,再根據 manifest 將資源注入到模板中。另外,爲了方便替換 html 中的圖片資源,還須要將 html-loader 解析結果做爲依賴替換。
    {
      "commonScripts": {
        "common.js": "/static/js/common.486cb059.chunk.js",
        "vendors.js": "/static/js/vendors.11aa87af.chunk.js"
      },
      "commonCss": {
        "common.css": "/static/media/common.6094b30a.css"
      },
      "scriptFiles": {
        "index.js": "/static/js/index.bc043de1.js",
        "home.js": "/static/js/home.d8768213.js",
        "about.js": "/static/js/about.a3e6551a.js"
      },
      "cssFiles": {},
      "assets": {
        "static/media/logo.jpg": "/static/media/logo.da5595d8.jpg",
        "static/media/ant2.png": "/static/media/ant2.89ca7b1b.png",
        "static/media/ant1.png": "/static/media/ant1.ed485ba9.png",
      },
      "htmlFiles": {
        "about.html": "/about.html",
        "home.html": "/home.html",
        "index.html": "/index.html"
      }
    }

大概經歷一個半月的時間,fes 也如期而至。因而就在組裏推廣使用,本身也使用開發了幾個項目。與 fex 相比,fes 在開發效率、體驗上都獲得很大的提高。但同時也暴露一些問題,其中最頭疼的問題是:因爲沒有將核心代碼提取做爲依賴包。致使在使用過程當中升級維護不是很方便。通常若發現問題都是現場解決,而後再同步到代碼庫中。但這樣不能很好地將代碼同步其餘已使用項目中。

在通過半年的沉澱後,決定對 fes 進行重構。並整理了一些優化點:

  • 優化熱加載。
  • 支持模板語言 loader 的配置。
  • 支持 css 預處理器 loader 的配置。
  • 引入 common.js,方便添加公用代碼,避免每一個 js 文件重複引用。
  • 優化某些頁面沒有對應的 js 文件。
  • 去掉 jquery 中爲默認內置。
  • 支持路由配置。
  • 支持多級目錄結構。
  • 將 media.json 放入 gitignore。
  • sprite 合成會引發一次編譯,大多數狀況此次編譯是多餘的。
  • 能夠將 start、tmpl、preview 腳本進行優化,提出共有邏輯,增長複用性,和可維護性。
  • 支持 CSS-Modules。
  • 支持 typeScript。
  • 優化編譯,打包時間。
  • 增長 service worker。
  • babel6 升級到 babel7。

不過在重構過程當中,在是否將 Babel 內置於 fes 中有了一些新的思考 🤔。

在搭建 fes 第一版時,只要以爲功能不錯都會集成於 fes 中。但並非全部的項目都須要全部功能,並且這樣會致使 fes 變得臃腫。也就是說有些可選功能,不必做爲內置功能。如 babel、typeScript、stylelint、eslint、precommit 等。其實 fes 只需內置基礎架構便可,其餘可選功能能夠經過配置來定製。這樣不只可以讓 fes 變得靈活輕巧,並且也方便擴展。

爲了將 fes 的核心代碼提取做爲依賴包,參考了 create-react-app 構建。畢竟 create-react-app 是個明星項目,技術也相對穩定成熟。加上以前也使用 create-react-app 構建幾個 react 項目,對其也算有點了解,不過只停留在使用上。若是要重構 fes 還須要對 create-react-app 源碼深刻研究一番。

整個 fes 的構建徹底基於 create-react-app。代碼結構也由兩個 packages 組成:create-fes 和 fes-scripts。但對於如何維護這兩個 packages 是一個很棘手的問題。若是獨立分開管理,開發起來不是很方便,後期維護成本也高(如版本號維護)。因而查看了 create-react-app 源碼,發如今其源碼中有一個 lerna.json 文件。好奇這個文件是作什麼的,就瞭解一番。經查閱瞭解到 Lerna 能夠用來管理項目中多個 packages。這正是本身所須要的,爲此本身也專門寫了一篇 Lerna 文章:monorepos by lerna

在此次重構中本身也作了一些優化,讓 fes 的體驗獲得很大地提高。

  • 動態響應mock api(模擬服務請求接口)

    在 fes 第一版時,對 mock api 的修改,須要重啓服務才能生效。這樣體驗在開發中不是很友好的。因而就開始折騰,有木有什麼方法能讓 mock api 的修改不用重啓就能生效。一開始想到的解決方案是基於 nodemon,可是這樣只要對 mock api 文件作修改,就會從新服務。若是頻繁地修改,會不停地重啓服務,影響到正常的開發服務,不是很理想。那另開一個服務來專門服務於 mock api,再基於 nodemon 監聽變化,這樣就不會影響正常的開發服務。但這樣整個架構就變得有點重了。看來基於nodemon的思路是走不通了,只能換一個思路🤔。對請求響應着手,在響應請求時,去掉緩存,確保每次響應都是最新的數據,這樣問題不就迎刃而解麼😝。但須要過濾掉靜態資源的請求,否則會影響頁面的響應時間。

    // mockApi 中間件
    const mockApi = async (ctx, next) => {
      if (!ctx.path.includes('/static')) { // 過濾掉靜態資源
        // avoid loading static resources with delay
        const mockContext = {
          mock(path) {
            const url = join(paths.appApis, path);
            delete require.cache[url];// 刪除緩存
            return Mock.mock(require(url)); // eslint-disable-line
          },
        };
        delete require.cache[join(paths.appApis, 'index.js')];// 刪除緩存
        const api = require(paths.appApis); // eslint-disable-line
        const mockData = api.call(mockContext);
        const responseBody = mockData[ctx.url];
    
        if (responseBody) {
          ctx.body = responseBody;
          if (typeof responseBody === 'function') {
            try {
              const responseBodyFromFun = responseBody.call(mockContext);
              ctx.body = responseBodyFromFun.data;
              if (typeof responseBodyFromFun.others === 'function') {
                responseBodyFromFun.others(ctx);
              }
            } catch (error) {
              console.log(`${chalk.bold.red('Error: ')}`, error);
            }
            console.log(`${chalk.black.bgYellow('MOCK-APIs')}   ${chalk.bold.green(ctx.method)}  ${chalk.gray('--->')}  ${chalk.dim(ctx.url)}`);
          }
        }
      }
      await next();
    };
  • css 熱加載

    以前 webpack 支持 css 熱加載,一直由 sytle-loader 來完成。而 style-loader 是基於 js 將待更新的 css 注入到 DOM 中,這樣會致使 FOUC(flash of unstyled content) 問題。爲了不 FOUC,可使用 mini-css-extract-plugin,尷尬的是 mini-css-extract-plugin 不支持 hmr。不過能夠配合 css-hot-loader 讓其支持 hmr。而 css-hot-loader 工做原理是將 mini-css-extract-plugin 提取的 css 中注入熱加載相關代碼來實現熱更新的。

    好消息是在重構過程當中 mini-css-extract-plugin 在 0.0.6 版本開始支持 hmr🎉🎉🎉。

    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    module.exports = {
      plugins: [
        new MiniCssExtractPlugin({
          // Options similar to the same options in webpackOptions.output
          // both options are optional
          filename: '[name].css',
          chunkFilename: '[id].css',
        }),
      ],
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              {
                loader: MiniCssExtractPlugin.loader,
                options: {
                  // only enable hot in development
                  hmr: process.env.NODE_ENV === 'development',
                  // if hmr does not work, this is a forceful method.
                  reloadAll: true,
                },
              },
              'css-loader',
            ],
          },
        ],
      },
    };
  • [template]-loader

    爲優化,webpack loader 在輸出時,通常都是一個 js runtime 字符串。而 js runtimehtml-loader不是很友好,特別對一些結構徹底與html結構不類似的 template engine,如pug。若是不使用html-loader能夠忽略。但在實際開發過程,在 html 中直接插入圖片或其餘資源仍是個很經常使用的需求。

    要解決這個問題,須要對 loader 作一些定製。確保 loader 的輸出是編譯好的 html,這樣對下游html-loader 處理就很友好了。同時,這樣也方便對 mock 數據的處理。

    另外,在編寫 template loader 時,須要確保支持視圖模板文件 base 目錄的指定,即支持絕對路徑引用。由於後端常使用絕對引用方式。爲了方便先後端模板複用,最好與後端的引用方式保持一致。

  • 去掉 postcss-modules,使用 css-loader 的 css-modules

    以前使用 postcss-moudles,主要解決將通過css-modules 編譯後的類名對象與模板變量數據合併做爲模板渲染數據。這種模式對當前的開發場景來講不如將在js中使用靈活。

    嘗試:將 css-modules 與模板數據結合在一塊兒也算一種新嘗試。能夠制定必定規範,只要全部模板都遵循這一套規範也是一種不錯的開發方式。
  • 將 runtime chunk 合併到 vendors 中。

    在對打包進行優化時,爲了使用瀏覽器緩存,使用 runtimeChunk: 'single' 提出 runtime chunk。同時也使用splitChunks生成一個 commons chunk 和 vendor chunk。由於 runtime chunk 和 vendor chunk 屬於不常改變的代碼,能夠將二者打包到一個 chunk 中。
    查閱文檔,也沒有提供相關的方法。因而就去了 webpack 的 gitterStack Overflow 提問。可是沒有人鳥我 😣(也許本身的英語太差,沒有表述清楚🤣或這個問題 too easy)。

    module.exports = {
    entry: {
        pageA: "./pageA",
        pageB: "./pageB",
        pageC: "./pageC"
    },
    mode: 'development',
    
    optimization: {
        runtimeChunk: 'single',
        splitChunks: {
            cacheGroups: {
                commons: {
                    chunks: "initial",
                    minChunks: 2,
                    maxInitialRequests: 5, 
                    minSize: 0
                },
                vendor: {
                    test: /node_modules/,
                    chunks: "all",
                    name: "vendor",
                    priority: 10,
                    enforce: true
                }
            }
        }
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].js"
      }
    };

    後續,在幫同事將老項目遷移到 fes 中時,由於存在歷史包袱,須要從新定製打包方式。在此過程當中無心中發現一種解決方案:

    module.exports = {
      entry: {
          pageA: "./pageA",
          pageB: "./pageB",
          pageC: "./pageC"
      },
      mode: 'development',
    
      optimization: {
          runtimeChunk: { name: 'vendor'},
          splitChunks: {
              cacheGroups: {
                  commons: {
                      chunks: "initial",
                      minChunks: 2,
                      maxInitialRequests: 5,
                      minSize: 0
                  },
                  vendor: {
                      test: /node_modules/,
                      chunks: "all",
                      name: "vendor",
                      priority: 10,
                      enforce: true
                  }
              }
          }
       },
       output: {
           path: path.join(__dirname, "dist"),
           filename: "[name].js"
         }
       };

    runtimeChunk: { name: 'vendor'}name 設置與 cacheGroups['vendor']['name']: "vendor"相同便可,就這麼簡單🙂。

  • focus,提升編譯速度

    在進行項目迭代時,有時須要新增頁面。若是項目已存在頁面不少。這樣每次編譯都須要從新編譯一遍,會致使整個編譯速度變得很慢。而在開發時,其實只需關注新增的頁面,其餘的頁面是不必編譯的。全部就新增這個 focus 功能,來提升編譯速度。只需指定新增的頁面的文件名便可,同時支持多個文件名的指定。

  • 減小讀磁盤操做

    在 fes 第一版的開發模式下,fes 是基於 html-webpack-plugin 插件自動生成 HTML 文件並緩存在內存中,爲了配合 koa2 輸出合成的 html 文件,須要藉助 html-webpack-harddisk-plugin 插件將 html 文件寫入磁盤中。

    爲了減小讀磁盤操做,基於 global 將 html 數據存儲在 global.__fes_bind_views_data__ 變量中。另外,還將 html-loader 解析的結果由以前的 media.json 文件轉由 shareData 變量代替。

  • foolMode

    該模式是針對一些簡單項目而新增的功能,在打包時會將全部的文件打包到一個文件,即最後輸出結果一個js、一個css。

  • debug

    該模式主要用於幫助調試打包代碼。在開啓 debug 模式,build 出的代碼不會被壓縮,同時生成source map。方便開發調試。

到此 fes 構建之旅也告一段落,接下就不斷地完善。

這就是本身構建 fes 的過程。構建過程當中存在不少挑戰,特別碰見一個花數日也不能解決的問題,對本身的積極性、自信心打擊仍是挺大的。途中真有想放棄的念頭,欣慰的是本身最終仍是堅持了下來😀。雖然在搭建完後,成就感並不如想象中那麼大。由於感受就那麼一回事,以爲任何人只要花點時間也能完成。不過整個過程下來本身也收穫很多。不管是在技術知識上、或對問題處理上等都獲得了很大地提高。同時對本身也有了一個新的認識。人嘛就得對本身狠點,否則你真不知道本身有多大的潛能🤣。

好了,文章到此就結束。重構的 fes 已放置 github: create-fes。畢竟團隊業務場景存在侷限性,加上我的能力有限。爲了讓 fes 變得更好,最好的選擇就是將其放置github。

喜歡的小夥伴請隨意 github: create-fes 拉代碼體驗。歡迎使用🙂歡迎使用🙂歡迎使用🙂

相關文章
相關標籤/搜索