近幾年,前端各種框架工具層出不窮,從兩三年前還是一個jQuery搞定全站,到之后requirejs/seajs,node,gulp/webpack,Angular/React/Vue,RN/weex的不斷涌現,完全顛覆了原來的前端開發模式。
那么這些框架和工具給我們到底帶來了什么好處呢?其實我認為最核心莫過於這兩點:模塊化開發、自動化工程。而本次前端重構所圍繞的核心問題就是自動化工程,將原有的gulp版本的項目利用webpack徹底改造,順利消滅了既繁瑣又易錯的人工操作。
gulp版本的痛點
我們先來看下奇貨商城之前的開發流程:
從上圖可以看出,我們奇貨前端開發之前存在的一些痛點:
- 前端在后端項目里面修改vm文件聯調;
- 開發聯調需要上傳靜態資源到測試域名CDN;
- 不同目錄下的資源還需要在CDN上傳網站上一級一級目錄的點開再上傳;
- 上線前需要人工去替換vm文件里的CDN路徑;
- 上線前還需要人工去上傳靜態資源到正式域名CDN;
- 開發模式不支持es6轉義,導致低端安卓機無法在本地進行前端調試;
- gulp-babel不完全支持es6轉es5,導致部分低端安卓機出現各種莫名其妙的問題;
以上這些痛點,造成的重復性無用功,既浪費精力又着實讓人蛋疼,而經過這次的框架重構,只需一鍵操作,就可完成聯調和發布的部署。省心省力還不會出錯。
如何利用webpack做自動化
先看一下改版后,奇貨商城的開發流程:
從上圖可以看到,我們經過改版后做到了:
- vm文件自動生成
- 開發聯調直接讀取本地靜態資源
- 打包后所有資源在同一級目錄,一次性拖拽上傳(下個版本將實現前靜態資源自動上傳)
- 只需一行配置項,自動生成對應的線上CDN路徑
- 完美的babel-loader,es6語法也可在低端安卓機上輕松本地調試;
下面我們看看如何實現。
項目結構
下面是部分主要目錄結構:
├── build (所有的webpack配置項)
│ ├── build.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js (★入口配置,生成文件配置,vm生成都靠這個文件)
│ ├── webpack.base.conf.js (基礎配置)
│ ├── webpack.dev.conf.js (開發模式配置)
│ └── webpack.prod.conf.js (生成環境配置)
├── config (node環境變量,入口文件的配置)
│ ├── dev.env.js
│ ├── entry.js (頁面文件列表)
│ ├── index.js (★主配置文件)
│ ├── prod.env.js
│ └── skinEntry.js (皮膚文件列表)
├── dist (打包后生成的文件夾,已全部轉成vm)
│ ├── goods
│ │ ├── detail.vm
│ ├── index.vm
│ └── static (打包后-靜態資源文件)
│ ├── css
│ ├── js
│ └── skins (打包后皮膚文件夾)
│ ├── default
│ │ ├── default.1184b4d7.js
│ │ ├── default.f07ae9df.css
│ │ └── default.html
│ ├── huotu
│ └── pay
├── mock
├── package.json
├── routes
├── src (源文件)
│ ├── js
│ │ ├── components
│ │ ├── goods
│ │ │ ├── detail.js
│ │ │ └── skins
│ │ │ ├── default.js
│ │ │ ├── huotu.js
│ │ ├── index.js
│ ├── less
│ │ ├── components
│ │ ├── goods
│ │ │ ├── detail.less
│ │ │ └── skins
│ │ │ ├── default.less
│ │ │ ├── huotu.less
│ ├── index.less
│ └── pages
│ ├── components
│ ├── goods
│ │ ├── detail.html
│ │ └── skins
│ │ ├── default.html
│ │ ├── huotu.html
│ └── index.html
├── static
│ └── images
└── unit (公共庫)
├── common (業務組件)
│ ├── js
│ └── less
├── layout (公共頁面)
│ ├── footer.html
│ └── header.html
└── lib (第三方組件)
以上是我們奇商城的前端目錄結構。
webpack的一些必用的loader和plugin,例如less-loader, style-loader, file-loader, html-loader, 還有UglifyJsPlugin, ExtractTextPlugin, OptimizeCSSPlugin
等等,在這里就不詳細展開了。
我們重點說說以下幾點核心:
node腳本調用webpack
通過node腳本來調用webpack,而不是直接在命令行啟動webpack,會有這么幾個用處:
- 通過node啟express做本地mock數據;
- 開發環境和生產環境的公共配置項,通過
webpack-merge
模塊做抽離,方便維護; - 可以設置node環境變量,以區分不同環境中的打包配置,這點在后面還有一個大招;
HtmlWebpackPlugin
這貨可以說是整個構建過程里,核心中的核心了。
自動生成vm、開發環境調用本地資源,以及皮膚文件的管理都有這個插件的功。部分代碼:
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'production' ? path + name + '.vm' : path + name + '.html',
template: template,
inject: false,
chunks: [pathBuild + name, 'vendor', 'manifest']
})
通過判斷node環境變量,決定生成vm還是本地html;
CommonsChunkPlugin
通過這個插件實現了js模塊打包,公共模塊提取,客戶端緩存&增量發布,皮膚文件生成。部分代碼:
for (let i = 0; i < entry.length; i++) {
let item = entry[i]
let path = item.path
let name = item.name
let pathBuild = path.replace(/\//g, '-');
result[pathBuild + name] = './src/js/' + path + name + '.js'
}
for (let i = 0; i < skinEntry.length; i++) {
let item = skinEntry[i]
let path = item.path
let name = item.name
if (process.env.NODE_ENV === 'production') {
result['../skins/' + path + name] = './src/js/goods/skins/' + name + '.js'
} else {
result['skins/' + path + name] = './src/js/goods/skins/' + name + '.js'
}
}
Object.assign(result, {
vendor: ['@unit/common/js/base', '@unit/common/js/util']
})
// 公共文件提取
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', // 注意不要.js后綴
chunks: utils.computeChunks(entryConfig, '')
})
// 避免修改業務代碼導致vendor的md5改變,保留文件緩存
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
自動化部署
自動化部署是在打包服務器通過腳本實現的,先通過npm命令打包前端工程,然后將代碼copy到后端工程中,最后打包后端項目,再發布。
遇到的困難
公共文件的引入
webpack官方文檔並沒有如何引入公共html文件的說明,這一點是在翻了N多資料后才發現的,最終的方案是:
去掉webpack.config.js文件中配置的全局html-loader,這樣html模版文件就不會被html-loader解析,我們可以使用ejs語法嵌入其他html頁面和圖片資源。因為沒了全局的html-loader解析html文件,使用ejs語法嵌入的資源返回的是ejs代碼,還需要使用html-loader來解析成html代碼。
(html-loader!)表示引用html-loader這個加載器來解析
<%= require('html-loader!../layout/header.html') %>
但是這樣將全局html-loader去掉后,又碰到了下面的問題。
jsp變量的引入
vm中有時需要直接引用后端的變量,如${cssUrl}
,就像這樣:
這時候webpack打包居然就報錯了,報錯了:
原因排查
出現這個問題的原因應該是由於HtmlWebpackPlugin這個插件引用的模版默認是ejs,當不使用全局html-loader的時候,模板文件其實是以ejs解析的,而${cssUrl}
在ejs中也識別為一個變量,當然就報錯了。
解決方法
這過程中,整個周末都在想這個問題,甚至已經開始考慮用gulp+webpack的方案了。。
又翻了很多資料,突然想到既然是ejs模板,可以嘗試了一些ejs去寫,而不是非要把這個模板以html的方式loader進來,然后就有了如下方法:
<link href="<%= '${cssUrl}' %>" rel="stylesheet">
這時候就被識別為一個字符串了!成功解決。
進一步探索,巧用node環境變量
上面的方法解決的其實也是挺丑的,因為本地開發的時候需要引用本地文件的,上線的時候又得傻乎乎地去一個個地方去替換:
<!-- <link href="<%= skinCss %>" rel="stylesheet"> -->
<link href="/skins/pay/pay.css" rel="stylesheet">
然后馬上試了下,在模板文件中用ejs去讀node環境變量process.env.NODE_ENV
,果然能取到值,就有了下面這個相對完美的方案:
<%
if (process.env.NODE_ENV === 'production') {
skinCss = '${cssUrl}';
} else {
skinCss = '/skins/pay/pay.css';
}
%>
<link href="<%= skinCss %>" rel="stylesheet">
其中production
就是利用node啟動webpack時配置的,在這里派上了大用場。
End
到這里,我們奇貨商城已經實現了前端工程自動化,再也不用一遍又一遍地去vm里修改路徑,人工去記着改了哪些文件,要上傳哪些靜態資源。更加不用擔心漏傳什么資源文件而導致線上bug辣。😃