前后端分離之路 - Vue2項目多入口模板改造方案
做前后端分離也有一段時間了,業務一直在用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限制太嚴格
我:嗯,確實嚴格,習慣就好
某:能不能改成只有提示,但是不影響編譯流程的
我:看看文檔
- eslint-loader: github.com/MoOx/eslint-loader
- eslint:
- en: eslint.org/docs
- zh: eslint.cn/docs
研究了一下倆文檔,理論上可以有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