前后端分離之路 - Vue2項目多入口模板改造方案

yushenhw 7年前發布 | 87K 次閱讀 Vue.js ESLint Vue.js開發 webpack

做前后端分離也有一段時間了,業務一直在用Vue@1.x的多入口方案,也一直懶癌發作沒搞2.x的版本。適逢最近在等某寶小程序的構建,由于遲遲不定技術方案,只好暫緩先捯飭一下Vue@2.x項目多入口的構建方案。

關于項目模板

項目模板沒有選擇重新開發,而是直接選用了vue官方模板 vuejs-templates/webpack 。熟悉的開發者應該都了解,這是一個SPA模板,只有一個入口,而現在我們需要把它改成多入口,并且修改添加一些開發功能,以配合Koa-grace時的開發流程。

可用的改造方案已經發布,有興趣的同學可以 先體驗基于Koa-grace的多入口項目方案

Grace-vue-webpack-boilerplate --- 基于Koa-grace的多入口Vue@2.x項目構建方案 :rocket:

關于官方模板,就不再分享學習的過程,下面僅討論改造過程中 遇到的問題實施方案的理由

關于目錄結構

由于模板是為了基于Koa-grace的項目而設計,所以在目錄結構上需保持基本的 Koa-grace項目結構 。

以下列舉了項目中關鍵文件夾及文件的結構關系,僅供參考。

.
├── package.json        // 項目依賴
├── node_modules
│
├── mock                // mock數據文件
├── controller          // node層路由目錄
│   ├── defaultCtrl.js
│   └── home.js
│
├── views               // 靜態模板源碼目錄
├── static              // 靜態資源源碼目錄
│   ├── image
│   ├── fonts
│   ├── css
│   └── js
│
├── build               // 編譯腳本目錄,本次重點改造
│   └── config          // 項目配置,供編譯過程
└── vues                // 源碼目錄,下屬文件夾將視作獨立的頁面入口,‘_’開頭的文件夾將被忽略
    ├── _components     // 組件庫,‘_’開頭時將不會被作為入口
    ├── demo            // /demo頁入口
    └── home            // /home頁入口
        ├── index.js
        ├── index.vue
        └── router.js

關于多入口(Multiple Entry)

調整完文件結構,下面要解決的事情就是,在原有模板的基礎上改造多入口方案。

其實這一步很簡單,只需拓展webpack配置文件中entry的獲取方式即可:

// build/webpack.base.conf.js
/**
 * [entries 入口合成器]
 * @param  {object} opt 指定的入口,優先級高于自動抓取入口
 * @return {object}     返回合成的入口對象
 */
function entries (opt) {
  var ens = exec('cd ./vues && ls').split('\n').map(function(item) {
    var obj = {}
    // 將忽略所有以下劃線“_”開頭的文件夾
    if (!/^_[\w-]+$/.test(item)) {
      obj[item] = './vues/'+item+'/'
    }
    return obj
  })
  return Object.assign.apply(null, [].concat(ens, opt))
}

module.exports = {
  entry: entries({
    // 此處可以手動指定其他需打包的頁面/文件入口
    common: [
      './static/css/common/reset.less',
      './static/css/common/index.less',
      './static/js/common/hello.js',
    ]
  }),
  ...
}

以上代碼效果相當于:

module.exports = {
  entry: {
    home: './vues/home/index.js',
    demo: './vues/demo/index.js',
    common: [
      './static/css/common/reset.less',
      './static/css/common/index.less',
      './static/js/common/hello.js',
    ]
  },
  ...
}

對于多入口項目, 避免了手工更新入口的麻煩,在源碼目錄中創建入口文件夾并添加源碼文件 即可。

以上就完成了入口的改造,但這樣還無法將文件按需要的目錄結構產出,所以還需要調整產出配置。

關于文件產出(Files Output)

產出規則及需求

按照項目結構,整理文件產出的需求:

  • js
    • 文件:編譯后文件名應為build.js
    • 路徑:需輸出到static/js下,并存放于入口名稱的文件夾內,如:static/js/home/build.js
  • css
    • 文件:規則同上,如:build.css
    • 路徑:規則同上,如:static/css/home/build.css
  • html
    • 文件:產出文件名應為inde.html
    • 路徑:需輸出到views/下,如:views/home/index.html

改造時直接進行了webpack.prod.conf的全功能配置,webpack.dev.conf進行簡化即可(開發階段無需分離css、壓縮代碼、生成map、抽取公共依賴等步驟)。

通過查看原項目模板的編譯流程可以了解:

  • js:作為entry配置,output直接影響輸出路徑及文件名
  • css:entry中配置的部分同上,由vue文件中抽離的部分則需要插件 ExtractTextPlugin 抽離
  • html:需要插件 HtmlWebpackPlugin 配合入口進行產出,可以使用ejs模板

產出html入口文件

首先,在webpack文檔中可以了解到chunk的概念,它和entry是一一對應的,在多入口項目中尤為重要。

Passing an array of file paths to the entry property creates what is known as a "multi-main entry". This is useful when you would like to inject multiple dependent files together and graph their dependencies into one "chunk" . --- webpack.js.org/entry-points

其次,需要先產出可用的HTML文件,才能調試其他靜態資源的加載。由于多入口化,HTML在產出時需要按chunk進行輸出,才能保證對應入口的編譯文件引用到對應的HTML中。因此需要按entry初始化對應的HtmlWebpackPlugin:

// build/webpack.prod.conf.js
  var webpackConfig = merge(utils.setEntrys(baseWebpackConfig), {
    ...
  }
  //
  // build/util.js
  function setEntrys (conf) {
    ...
    var htmlConfig = {
      // 壓縮HTML選項,dev時不壓縮
      minify: {
        removeComments: isNotDev,
        collapseWhitespace: isNotDev,
        removeAttributeQuotes: isNotDev
      }
    }
    var entries = Object.keys(conf.entry)
    entries.map(function(ent) {
      ...
      // 根據entry添加新插件到plugins中
      conf.plugins.push(new HtmlWebpackPlugin(Object.assign({
        // HTML輸出地址,形如:/Users/thunf/fe/server/app/demo/views/home/index.html
        filename: `${path.resolve(config.base.outputRoot, 'views')}/${ent}/index.html`,
        // 允許引用的chunk,包含本身及公共部分
        chunks: [ent, 'vendor', 'manifest', 'common'],
        // 模板路徑,形如:/Users/thunf/fe/server/app/demo/views/_common/_template.ejs
        template: path.resolve(projectRoot, 'views/_common/_template.ejs'),
        // 自動插入靜態資源,由于結合使用了Koa-grace在Node預渲染HTML的功能,所以關閉該功能
        inject: false,
        // Chunks排序規則
        chunksSortMode: 'dependency'
      }, htmlConfig)))
    });
    return conf;
  }

現在,我們可以通過Koa-grace提供的服務來打開對應的頁面了,但是頁面中的靜態資源鏈接還存在問題。

產出js/css靜態資源

此時需要配置輸出的參數 output ,參考webpack文檔( webpack.js.org/output | webpack-china.org/output ),可以查看output的說明和配置,其中本次需要使用的參數及有效說明如下:

output.filename : This option determines the name of each output bundle.

output.path : The output directory as an absolute path.

output.publicPath : This option specifies the public URL of the output directory when referenced in a browser.

簡單來說,就是:

  • output.filename:決定產出文件的名稱和后綴
  • output.path:影響文件輸出的絕對位置(本機輸出文件夾的絕對路徑)
  • output.publicPath:決定資源在瀏覽器中加載的路徑(比如添加CDN、指定公開URL)

假設我們需要在home頁產出的html中生成 /demo/static/js/home/build.js?v=12345 的資源鏈接,對應以上規則有:

  • output.filename: static/js/home/build.js?v=12345
  • output.path: /Users/thunf/fe/server/app/demo
    • 此路徑為本地產出目錄 絕對地址 ,只影響本地產出目錄,不影響html中輸出的鏈接
  • output.publicPath: /demo/
    • 在html輸出時,會直接 添加到filename前 ,生成完整鏈接
    • 將影響其他所有在html中引用的資源鏈接

于是output應按以上規則配置,實際代碼中 復用了路徑配置及拼合路徑的方法 ,此處就不再進行代碼分析。

文件加戳:Query or fileName

關于文件加戳的問題,為什么我們選擇了 build.js?v=12345 ,而不是 build.12345.js ,是出于以下考慮的:

1、每次改動build都會產出新文件,長久以往會導致線上代碼版本過多

2、上線倉儲將越來越大,并且不易清理

但實際應用上,Query戳對比fileName戳也有問題存在,比如

1、無法有效利用CDN組合加速文件

2、無法存在多版本

但目前階段根據業務需求,暫時足夠使用,若有需要更改加戳方式的需求,只需在上述 output.filename 處將文件命名方式進行調整即可,此處不再累述。

其他資源路徑問題

項目模板中目前還有圖片資源及字體資源未提及,該兩種資源在原模板中已存在loader及引用方式,下面描述碰到的問題及解決方法:

生成圖片引用路徑

由于在處理圖片資源時,并沒有很好的理解 output.publicPath 的作用,沒有使用形如 /demo/ 的 相對于服務模式(server-relative) ,導致始終無法產出期望的文件路徑。后來在webpack文檔中找到具體的說明,才完成配置。

The value of the option is prefixed to every URL created by the runtime or loaders. Because of this the value of this option ends with / in most cases. --- output.publicPath

圖片資源引用方式

正常情況下,在.vue文件中可以使用相對路徑來引用圖片資源,但通過 配置別名 及 ~alias 的引用方式將 更簡潔

<!-- Same effect, but alias write less -->
<img class="logo" src="../../static/image/logo-grace.png">
<img class="logo" src="~image/logo.png">

配置別名alias如下:

// build/webpack.base.conf.js
module.exports = {
  ...
  resolve: {
    ...
    alias: {
      'static': resolve('static'),
      'image': resolve('static/image'),
      'components': resolve('vues/_components')
    }
  },
  ...
}

.vue文件中其他靜態資源引用同理

<!-- JS/Component -->
<script>
import Grace from 'components/grace.vue'
</script>
...
<!-- CSS/LESS -->
<style lang="less">
@import '~static/fonts/iconfont.less';
</style>

關于提升開發效率

考慮實際開發中,有需要進行諸如本地配置、打開瀏覽器、創建新文件等重復性操作的場景。

為了進一步

懶癌發作 提升開發效率

,特別增加了一些增強功能,目前剛開始進行,歡迎拋出寶貴的建議。

自動配置開發環境

由于Koa-grace作為前后端分離框架,允許 同時解析多域名請求到多項目 ,故以往在切換項目開發時,需要手動更改server.json配置及重啟服務(官方吐槽:好煩啊喂)。那么既然項目啟動需要單獨 npm run dev 一次,為何不考慮將項目配置(開發環境)交由項目本身進行呢?

// build/check-server.js
// 匹配grace下配置目錄
function matchServerJson(graceRoot) {
  return glob.sync(path.join(graceRoot, '*/config/main.development.js')) || []
}
...
// 匹配grace目錄
function findServerFolder(graceRoot, scb, ecb) {
  var confMatch = matchServerJson(graceRoot)
  ;(1 === confMatch.length) ? callback(scb)({
    serverRoot: path.resolve(confMatch[0], '../..'),
    serverConf: confMatch[0]
  }) : callback(ecb)(confMatch.length)
}

于是本著盡可能

懶癌發作 讓系統自己找

的思想,通過引入glob進行關鍵文件的路徑匹配,來驗證當前項目是否符合Koa-grace目錄規范,順便解析出當前Koa-grace啟動的文件夾(default: server)名稱,并添加到config。

有了server的路徑,就可以按路徑讀取配置文件信息了,順便也可以驗證并添加本項目的配置:

// build/open-browser.js
function lookForHost(hosts, autoOpenBrowser) {
  return Object.keys(hosts).filter(function(key, value) {
    // if vhost-matched, use the match one
    return hosts[key] === config.base.moduleName
  })[0] || (autoOpenBrowser && writeHostConf({
    // if auto-open & no-vhost-matched, auto set
    "127.0.0.1": config.base.moduleName
  }) || [
    // if no-auto-open & no-vhost-matched, to tip
    '> Maybe you have not set vhost to this app: ' + chalk.cyan(config.base.moduleName),
    '> Please set ' + chalk.magenta('vhost') + ' in ' + chalk.magenta('/server/config/server.json') + ' like this:',
    '  ' + chalk.green( JSON.stringify({
      merge: {vhost: {"127.0.0.1": config.base.moduleName } }
    }, null, 2).replace(/\n/g, '\n    ') ), '', ''
  ])
}

此處代碼實現風格比較詭異,其實做了3種情況的判斷:

  • 如果匹配到本項目的vhost配置,那就使用該host
  • 如果未匹配到,且允許自動打開瀏覽器,就 寫入默認配置 {127.0.0.1: moduleName}
  • 如果未匹配到,且不允許自動打開瀏覽器,就發起提示

自動打開項目首頁

其實這個功能在原項目模板中已經存在,只不過原項目自帶server并且只有一個入口,啟動時只需打開固定的首頁即可。

當變成多入口項目后,就需要面臨新的問題:

  • Koa-grace啟動的host及端口不確定
  • 首頁路徑不確定

第一個問題,通過上述方案已經可以解決。

第二個問題,暫時需在config配置 autoOpenPage ,需在配置文件中添加如下配置,特此說明。

// build/config/index.js
devConf = {
  ...
  autoOpenBrowser: true, // 是否自動打開瀏覽器
  autoOpenDelay: 2000,   // 延遲多少ms打開瀏覽器,koa-grace服務檢測到路由文件變化會自動重啟
  autoOpenPage: 'home',  // 自動打開時的項目入口(路由)
  ...
};

關于ESLint

檢查你的代碼

Code linting is a type of static analysis.

ESLint is designed to have all rules completely pluggable. --- eslint.org

很早之前就了解過這個玩意了,曾經組里也有幾次技術調研涉及這個,一直沒正式投入使用過,也就自己搞一搞。

其實開始使用時也是挺蛋疼的(模板自帶配置使用標準: feross/standard ,感覺要求超級嚴格,比如“ { ”后換行并留一行空格都要error),但耐心看一下提示,就能知道格式哪里不標準,然后逐步就習慣了(避免寫的時候太飄逸)。

This module helps hold our code to a high standard of quality.

This module ensures that new contributors follow some basic style standards. --- feross/standard

這里吐個槽, 如果團隊協作開發一個倉儲,圖快而不加以規范,簡直就是給維護者挖坑 (版本越老坑越大,自己深有體會,靠嘴遁要求統一風格,真心沒用,什么詭異的代碼風格都有)。

翻一翻文檔

某:eslint限制太嚴格

我:嗯,確實嚴格,習慣就好

某:能不能改成只有提示,但是不影響編譯流程的

我:看看文檔

研究了一下倆文檔,理論上可以有2個方案實現 只提示而不影響編譯流程 ,但是似乎又各自有問題,以下是這兩種方案。

配置extends/rules

在ESlint的官方文檔里可以了解到 configuring#extending-configuration-files :

  • extends :
    • a string that specifies a configuration
    • an array of strings: each additional configuration extends the preceding configurations
  • rules : which rules are enabled and at what error level
    • enable additional rules
    • change an inherited rule’s severity without changing its options
    • override options for rules from base configurations

接下來通過查看 feross/standard 的配置文件 eslint-config-standard/eslintrc.json 可以得知,這個標準一言不合就error,而且 全部都是error (沒辦法,就是這么任性)。不過 feross/standard 的作者也這么表態了:

The word "standard" has more meanings than just "web standard" :-)

--- FAQ: But this isn't a real web standard!

那么方案也就顯而易見:

  • 找一份完全符合心意的標準(看起來比較難)
  • 寫個插件,然后 自己定義規則 ( eslint.org/docs/rules | eslint.cn/docs/rules ),添加到extends
    • 比如強制縮進就給error,不關心是否空行就給warning,隨心所欲so easy
  • 在.eslintrc中,通過 添加rules屬性,強行覆蓋 現有extends提供的rules

優缺點也一目了然:

  • 優:配置化并完全可以自定義,完全可拔插,可持續維護;
  • 缺:想找到一個完全符合心意的標準幾乎不可能(若自己寫規則,一般人怕是沒這心情,雖然規則不是很多,哈)

配置failOnWarning

當然先講一句,我其實 不推薦 這種配置,原因在后面說。

首先在eslint-loader文檔里,找到這么一句話:

So even ESLint warnings will fail the build. --- eslint-loader#noerrorsplugin

即使是warning也會阻斷build過程。但有趣的是,option配置可以改變提示行為,默認值都是false:

emitError: Loader will always return errors if this option is set to true.

emitWarning: Loader will always return warnings if option is set to true.

failOnWarning: Loader will cause the module build to fail if there are any eslint warnings.

failOnError: Loader will cause the module build to fail if there are any eslint errors.

以我的理解,eslint-loader應該是在preLoad階段,以報錯的形式影響webpack編譯進程,并顯示提示信息。

所以在 eslint-loader的源碼 里,找到了這么個地方:

// default behavior: emit error only if we have errors
  var emitter = res.errorCount ? webpack.emitError : webpack.emitWarning

  // force emitError or emitWarning if user want this
  if (config.emitError) {
    emitter = webpack.emitError
  }
  else if (config.emitWarning) {
    emitter = webpack.emitWarning
  }

  if (emitter) {
    emitter(messages)
    if (config.failOnError && res.errorCount) {
      throw new Error("Module failed because of a eslint error.\n"
        + messages)
    }
    else if (config.failOnWarning && res.warningCount) {
      throw new Error("Module failed because of a eslint warning.\n"
        + messages)
    }
  }

這段代碼表明,正常流程上的warning和error是調用webpack自帶的方法來emit的,只不過我們可以通過配置:

1.emitError/emitWarning:取消default behavior,使emitter強制變成error/warning類型

2.failOnWarning/failOnError:拋出一個Error來打斷webpack的build過程

那么問題來了,默認的配置 {emitError: false, failOnError: false} ,只是由webpack進行了一次emitError,并沒有實際拋出一個Error,那實際上應該 不會 cause the module build to fail 才對?但實際開發時,比如為什么“ { ”后換行并留一行空格,不只觸發了規則 no-trailing-spaces 的error,還導致webpack沒有實際build出新文件呢?

那么基本上可以斷定這和webpack.emitError有關系了,來看一下webpack的源碼:

// webpack/lib/NormalModule.js
  ...
    emitWarning: function(warning) {
      module.warnings.push(new ModuleWarning(module, warning));
    },
    emitError: function(error) {
      module.errors.push(new ModuleError(module, error));
    },
  ...

這里API功能是收集error和warning,那么理論上還有地方,會根據error和warning數量決定是否可以繼續編譯。

中間調試的過程就不累述,最終在webpack的Compiler.js中先找到了判定emit并return的地方:

// webpack/lib/Compiler.js
  ...
  if(self.compiler.applyPluginsBailResult("should-emit", compilation) === false) {
    return self._done(null, compilation);
  }
  ...

經過尋找 should-emit 在哪里觸發,然后找到在NoEmitOnErrorsPlugin中,處理errors數量的邏輯:

// webpack/lib/NoEmitOnErrorsPlugin.js
  ...
  compiler.plugin("should-emit", (compilation) => {
    if(compilation.errors.length > 0)
      return false;
  });
  ...

此處可以看出,若errors中存在error,NoEmitOnErrorsPlugin會返回false,而對warning沒有處理。這一行為會導致Compiler.js中的compile流程被中斷,當然就不會有文件產出啦。

那么為了驗證這個邏輯,將NoEmitOnErrorsPlugin從項目build/webpack.dev.conf.js中注釋掉之后,帶著已知的格式問題(如多個空行,不影響實際編譯),仍然可以編譯出新文件,有興趣的小伙伴可以自行嘗試。當然這個插件在dev環境開發時還會影響其他loader的表現,不建議砍掉它。

那么回到我們的問題上來,通過強制eslint觸發emitWarning而不是emitError,即在eslint的option中配置 {emitWarning: true} , 格式檢測規則(編譯無關的規則) 就不會終止webpack編譯產出文件了:

...
  {
    test: /\.(js|vue)$/,
    loader: 'eslint-loader',
    ...
    options: {
      ...
      /* ======= begin ====== */ 
      emitWarning: true
      /* =======  end  ====== */ 
    }
  },
  ...

當然如果是會引起編譯的錯誤,在接下來的編譯中該斷還是得斷,還有可能產生多次log(畢竟沒有被eslint在preLoad過程中攔下來,eslint提示完,后面的loader還會繼續提示,比如重復顯示error,調試過程簡直最強大腦有木有)

所以盡管可以實現 只提示而不影響編譯流程 的目標,但又帶來了新問題,雖然簡單快捷但并不優雅,也沒有讓eslint發揮出什么作用(對非強迫癥患者,僅提示還是攔不住的各種詭異風格的)。

這就是不推薦的理由。總之blabla這么多,就是想說下面這句話。

不想用就別硬用

實在不能接受,那就干掉吧,推薦下面的方式。

  • 1、 Fork It And Make Your Own
  • 2、Comment out or delete the codes below (the part between comments):
    // build/webpack.base.conf.js
    ...
    module: {
      rules: [
        /* ==================== eslint =================== */ 
        /* If eslint makes you mad, just delete these code */
        {
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          enforce: "pre",
          include: [resolve('vues'), resolve('test')],
          options: {
            formatter: require('eslint-friendly-formatter')
          }
        },
        /* ================== eslint end ================= */
        ...
      ]
      ...
    }
    ...
  • 3、Enjoy your code : )

其他問題

報錯信息不顯示

有小伙伴反應這一現象,如下圖所示。其實這跟個人使用的terminal配色方案有關(webpack顯示錯誤代碼的顏色,正好跟terminal的背景色相同,就看不到了嘛),。

經過調試,發現這種提示是由 eslint-friendly-formatter 打印出來的,根據源碼顯示:

...
  var parseBoolEnvVar = function(varName) {
    var env = process.env || { };
    return env[varName] === 'true';
  };
  var subtleLog = function(args) {
    return parseBoolEnvVar('EFF_NO_GRAY') ? args : chalk.gray(args);
  };
  ...

avoidTerminalColorGray

只需配置 process.env.EFF_NO_GRAY = true 即可阻止使用chalk.gray顯示文字。

那么這個配置也將 加入項目的配置文件,如下配置 即可

// build/config/index.js
  ...
  devConf = {
    ...
    avoidTerminalColorGray: true
  };
  ...

NEXT

抽取代碼(TODO)

有時間將抽離 提升開發效率 部分的邏輯為插件,以幫助更多其他技術棧的Koa-grace項目更好的提升開發體驗。

自動創建新入口(TODO)

已經有同學提出需要自動創建新入口的腳本及命令,這個近期也會提供(每次添加新入口要創建好幾個文件夾及文件也是麻煩)

熱加載(TODO)

該功能在原模板中存在,但升級多入口后,server將由Koa-grace接管,目前已移除該部分代碼,下一步考慮添加Koa-grace的中間件,來配合完成vue項目的熱加載方案

 

 

來自:http://feclub.cn/post/content/grace-vue-boilerplate

 

 本文由用戶 yushenhw 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!