本文不是什麼技術性介紹文章,準確地說算是本身的成長記錄吧。剛參加工做時,組裏使用的腳手架是由 leader 使用 webpack, gulp 搭建的多頁面應用腳手架 fex。當時只需知道怎麼使用就好了,不過爲了能更好地工做,對 fex 怎麼構建一直很好奇,也一直關注相關的技術棧。通過一年多磨練後,對 fex 怎麼搭建的有了個大概地認識。常言道:"沒有對比就沒有傷害"。css
在使用 vue-cli 構建第一個 vue 項目後,對腳手架構建有了個全新的認識。發現 fex 存在不少不足:html
在打包時,只對 JavaScript 和 CSS 腳本文件進行打包壓縮處理。不能對資源文件(如 img,字體等)進行依賴處理。致使在打包時:前端
固然,fex 也有本身的優勢。基於自建服務提供先後端複用模板功能。前端後端使用相同的模板語言,前端拼接的模板能夠直接輸出給後端使用。vue
第二年年初,組裏項目不是太多,恰好有時間折騰一下,因而決定構建一個全新的腳手架 fes。爲了嘗試一些新東西,在技術棧上,都使用了當時最新的技術框架 webpack四、koa二、babel6 來搭建。爲了瞭解 webpack 如何工做,對 webpack 就作了 8 次調試,才稍微對 webpack 整個架構有個初步認識。node
singsong: 在真正去了解 webpack 時,才知道它有多複雜。固然也參考了網上一些大神分享關於 webpack 源碼分析的文章。反正整個過程仍是挺熬心的🙂
同時,還對 koa二、babel6 作了相關的研究。附一張 koa2 分析圖吧😝:
react
爲了提升 fes 開發體驗,除了繼承 fex 的模板複用功能外,還集成了 vue-cli 中不錯的功能。jquery
在搭建 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 搭建過程當中也並非一路順風的,途中也碰見一些坑:
在開發模式下,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 的插件。
向來 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 進行重構。並整理了一些優化點:
不過在重構過程當中,在是否將 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', ], }, ], }, };
爲優化,webpack loader 在輸出時,通常都是一個 js runtime 字符串。而 js runtime 對html-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 的 gitter 和 Stack 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
功能,來提升編譯速度。只需指定新增的頁面的文件名便可,同時支持多個文件名的指定。
在 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
變量代替。
該模式是針對一些簡單項目而新增的功能,在打包時會將全部的文件打包到一個文件,即最後輸出結果一個js、一個css。
該模式主要用於幫助調試打包代碼。在開啓 debug 模式,build 出的代碼不會被壓縮,同時生成source map。方便開發調試。
到此 fes 構建之旅也告一段落,接下就不斷地完善。
這就是本身構建 fes 的過程。構建過程當中存在不少挑戰,特別碰見一個花數日也不能解決的問題,對本身的積極性、自信心打擊仍是挺大的。途中真有想放棄的念頭,欣慰的是本身最終仍是堅持了下來😀。雖然在搭建完後,成就感並不如想象中那麼大。由於感受就那麼一回事,以爲任何人只要花點時間也能完成。不過整個過程下來本身也收穫很多。不管是在技術知識上、或對問題處理上等都獲得了很大地提高。同時對本身也有了一個新的認識。人嘛就得對本身狠點,否則你真不知道本身有多大的潛能🤣。
好了,文章到此就結束。重構的 fes 已放置 github: create-fes。畢竟團隊業務場景存在侷限性,加上我的能力有限。爲了讓 fes 變得更好,最好的選擇就是將其放置github。
喜歡的小夥伴請隨意 github: create-fes 拉代碼體驗。歡迎使用🙂歡迎使用🙂歡迎使用🙂