要看更多的文章,歡迎訪問個人我的博客: http://oldli.netjavascript
如今的前端早已不是幾年前的前端,不再是jQuery加一個插件就能解決問題的時代。php
最近對公司前端的開發進行了一系列的改造,初步達到了我想要的效果,可是將來還須要更多的改進。最終要完美的實現目標:工程化,模塊化,組件化。css
這是一個艱難的,持續的,不斷進化的過程!html
先說下我司前端改造前的狀況:前端
開始的時候,只有微信公衆號開發,以及在APP中嵌入的Web頁面,只須要考慮微信端的問題,以及跟原生APP的交互處理,一切都好像很完美。java
幾個月後,要開發手機網頁版,接着百度直達號也來了。原來微信端的功能已經比較多,不可能針對這2個端再從新開發,因而把微信端的代碼拷貝2份,而後修改一下就上線了(初創公司功能要快速上線,有時不會考慮太多的技術架構,都懂的)。node
這樣就出現了6個不一樣的項目文件夾,爲何會是6個呢?由於也分別拷貝出了各自的測試目錄:webpack
/wap /waptest /m /mtest /baidu /baidutest
因而,問題就來了,開發的時候,都在其中一個test目錄下開發,好比waptest
,開發測試沒問題了,就拷貝修改的代碼到其它的目錄。這樣開發的痛苦,可想而知了。git
不只僅是各個端的js,css,images文件是分別存放,還有各個端的頁面模板也是在各自的目錄下。 github
另外,一直以來,公司的前端美女會使用grunt
作一些簡單的前端構建,好比sass編譯,css合併等,但離我想要的前端自動化/工程化仍是有點遠。
爲了提升前端的工做效率,最近終於有一點時間騰出手來處理這些問題。
PS:咱們團隊組建一年多,項目也從0開始,到如今爲止,產品/開發/項目管理等都在逐漸完善。走專業化道路,是我一直以來的目標,有興趣的能夠加我一塊兒交流!
先來總結一下改造前前端開發存在的問題:
同時存在多端,形成開發效率不高
項目沒有模塊化,組件化的概念,代碼複用率低
部署困難,沒有自動生成版本號,每次都要手動修改js的版本號
麪條式的代碼,開發任務重,沒有作很好的規劃
有問題,那就想辦法去解決它:
解決多端統一的問題,一處修改,多端同時生效
模塊化開發,使代碼邏輯更加清晰,更好維護
組件化開發,加強擴展性
按需打包,以及自動構建
自動更新js版本號,實現線上自動更新緩存資源
緊跟發展趨勢,使用ES6進行開發
在改進的過程當中,會用到2個工具: Gulp
和Webpack
。用這2個工具,也是有緣由的。
原本我想在Grunt
的基礎上利用Browserify
進行模塊化打包,可是發現打包的速度太慢,個人Linux系統編譯要4s以上,美女前端的Widnows系統通常都要7s以上,這簡直不能忍受。在試用Gulp
以後發現速度槓槓的,不用想了,馬上替換Grunt。至於Webpack,是由於用browserify打包多個入口的文件配置比較麻煩,在試用了Webpack以後,發現Webpack的功能比browserify強大不少,因而就有了這2個工具的組合。Webpack的配置比較靈活,可是帶來的結果就是比較複雜,在項目中,我也僅僅用到了它的模塊化打包。
因而,最終初步實現前端構建的方案是:
Gulp進行JS/CSS壓縮,代碼合併,代碼檢查,sass編譯,js版本替換等,Webpack只用來進行ES6的模塊化打包。
如今前端的操做很簡單:
開發的時候,執行如下命令,監聽文件,自動編譯:
$ gulp build:dev
開發測試完成,執行如下命令,進行編譯打包,壓縮,js版本替換等:
$ gulp build:prod
今後,前端開發能夠專心地去寫代碼了!
整個項目是基於Yii2
這個框架,相關的目錄結構以下:
common/ pages/ user/ index/ cart/ wap/ modules/ user/ index/ cart/ web/ dev/ index/ user/ cart/ common/ lib/ dist/ logs/ gulp/ tasks/ utils/ config.js config.rb node_modules/ index.php package.json gulpfile.js webpack.config.js .eslintrc
common/pages
存放公共模板,各個端統一調用
web/dev
是開發的源碼,包含了js代碼,css代碼,圖片文件,sass代碼
web/dist
是編譯打包的輸出目錄
因爲多端的存在,致使開發一個功能,要開發人員去手動拷貝代碼到不一樣的目錄,同時還要針對不一樣的端再作修改。
js文件,css文件,圖片文件,還有相關的控制器文件,模板文件都分散在不一樣的目錄,要拷貝,耗時間不說,並且容易出錯遺漏。
要解決這個問題,有2種方法:
全部端調用公共的文件
在某個端開發,開發完成以後,用工具自動拷貝文件,而且自動替換相關調用
在綜合考慮了以後,這2種方法同時使用,模板文件多端公共調用,其它的文件,經過命令自動拷貝到其它端的目錄。
公共模板放到目錄common/pages
,按模塊進行劃分,重寫了下Yii2的View類,各個端均可以指定是否調用公共模板。
public function actionIndex() { $this->layout = '/main-new'; return $this->render('index', [ '_common' => true, // 經過該值的設置,調用公共模板 ]); }
模板一處修改,多端生效。
另外,其它文件經過gulp去拷貝到不一樣的目錄,例如:
$ gulp copy:dist -f waptest -t wap
這裏有一個前提就是,全部編譯打包出來的文件都是在dist文件夾,包含了js代碼,css代碼,圖片文件等。
這個只能說是將來努力的一個目標。現階段還沒能很好地實現。這裏單獨列出這一點,是但願給你們一點啓發,或者有哪路高手給我一點建議。
看了網上諸路大神的言論,總結了下前端的組件化開發思想:
頁面上的每一個獨立的可視/可交互區域視爲一個組件
每一個組件對應一個工程目錄,組件所需的各類資源都在這個目錄下就近維護
組件與組件之間能夠 自由組合
頁面只不過是組件的容器,負責組合組件造成功能完整的界面
當不須要某個組件,或者想要替換組件時,能夠整個目錄刪除/替換
其中,各個組件單獨的目錄,包含了js代碼,樣式,頁面結構。這樣就把各個功能單元獨立出來了。不只維護方便,並且通用性高。
最終,整個Web應用自上而下是這樣的結構:
前端開發的代碼從開始到如今,經歷了3個階段:
第一階段,麪條式代碼,在每一個模板頁面寫上一堆js代碼,執行的代碼跟函數代碼交替出現,多重複代碼
第二階段,進化到了Object,每一個模板頁面的js都用Object進行封裝一次
第三階段,引入ES6的模塊化寫法
在以前,前端都按下面的目錄存放文件:
js/ goods-order.js package-order.js index.js user.js zepto.js crypto-js.js images/ goods.jpg logo.png footer.png css/ header.css footer.css goods-order-index.css
這樣會致使一個目錄下會有不少文件,找起來很是不方便,並且結構不清晰,比較雜亂。
如今,在目錄web/dev
分開不一樣的目錄存放各個模塊的代碼以及相關文件,web/dist
是編譯打包出來的文件存放的目錄。如:
dev/ lib/ zepto.js crypto-js.js common/ js/ request.js url.js scss/ head.scss order/ goods-order/ index.js index.scss a.png package-order/ index.js index.scss dist/ lib/ lib.js order/ goods-order/ index.50a80dxf.js a.png
其中,有2個重要的目錄:
lib/
目錄存放第三方庫,編譯的時候合併並壓縮爲一個文件,在頁面上直接引入,服務端開啓gzip以及緩存
common/
目錄存放公共的模塊,包括js的,還有sass等,其它模塊目錄的js,sass能夠調用這些公共的模塊
其它的目錄都是單獨的一個模塊目錄,根據業務的狀況劃分,每一個模塊目錄把js,sass,圖片文件都放一塊兒。
這樣的結構清晰明瞭,極大地提升了可維護性。
至於JS代碼的模塊化,恰好去年發佈了ES6,並且各大框架/類庫/工具等都支持ES6的特性,顯然這是將來的一種趨勢,相比之前的CMD
/AMD
/CommonJS
規範,選擇ES6會更加的符合時代的發展。
ES6支持Class
以及繼承,以及使用import
來引入其餘的模塊,使用起來很簡單。
至於CSS的模塊化,以前是使用Compass
來寫CSS,在本次改造中,還沒作太多的處理,只是由原來的grunt編譯該爲用gulp編譯。可是compass已經好久沒有更新了,並且不建議使用它。之後會逐漸替換掉。
因爲使用了ES6的模塊化寫法,須要引入Webpack進行編譯打包,我是gulp與webpack配合使用。
var gulp = require('gulp'); var webpack = require('webpack-stream'); var changed = require('gulp-changed'); var handleError = require('../utils/handleError'); var config = require('../config'); gulp.task('webpack', function() { return gulp.src(config.paths.js.src) .pipe(changed(config.paths.js.dest)) .pipe(webpack( require('./../../webpack.config.js') )) .on('error', handleError) .pipe(gulp.dest(config.paths.js.dest)); });
webpack的配置以下:
var webpack = require('webpack'); var path = require('path'); var fs = require('fs'); var assetsPlugin = require('assets-webpack-plugin'); var config = require('./gulp/config'); var webpackOpts = config.webpack; var assetsPluginInstance = new assetsPlugin({ filename: 'assets.json', path: path.join(__dirname, '', 'logs'), prettyPrint: true }); var node_modules_dir = path.resolve(__dirname, 'node_modules'); var DEV_PATH = config.app.src; // 模塊代碼路徑,開發的時候改這裏的文件 var BUILD_PATH = config.app.dest; // webpack打包生成文件的目錄 /** * get entry files for webpack */ function getEntry() { var entryFiles = {}; readFile(DEV_PATH, entryFiles); return entryFiles; } function readFile(filePath, fileList) { var dirs = fs.readdirSync(filePath); var matchs = []; dirs.forEach(function (item) { if(fs.statSync(filePath+'/'+item).isDirectory()){ readFile(filePath+'/'+item, fileList); }else{ matchs = item.match(/(.+)\.js$/); if (matchs) { key = filePath.replace(DEV_PATH+'/', '').replace(item, ''); if(!key.match(/^lib(.*)/) && !key.match(/^common(.*)/)){ fileList[key+'/'+matchs[1]] = path.resolve(filePath, '', item); } } } }); } var webpackConfig = { cache: true, node: { fs: "empty" }, entry: getEntry(), output: { path: BUILD_PATH, filename: '', // publicPath: '/static/assets/', }, externals : webpackOpts.externals, resolve: { extensions: ["", ".js"], modulesDirectories: ['node_modules'], alias: webpackOpts.alias, }, plugins: [ assetsPluginInstance, new webpack.ProvidePlugin(webpackOpts.ProvidePlugin), ], module: { noParse: webpackOpts.noParse, loaders: [ { test: /\.js$/, loader: 'babel', exclude: [node_modules_dir], query: { presets: ['es2015'], } }, ] } }; if(process.env.BUILD_ENV=='prod'){ webpackConfig.output.filename = '[name].[chunkhash:8].js'; }else{ webpackConfig.output.filename = '[name].js'; webpackConfig.devtool = "cheap-module-eval-source-map"; } module.exports = webpackConfig;
項目的入口文件都放在/web/dev
下面,根據業務特色來命名,好比:index.js
,pay.js
。
在webpack.config.js
文件,能夠經過getEntry
函數來統一處理入口,並獲得entry配置對象。若是你是多頁面多入口的項目,建議你使用統一的命名規則,好比頁面叫index.html
,那麼你的js和css入口文件也應該叫index.js
和index.css
。
因爲編譯出來的文件是帶有版本號的,如select-car.b9cdba5e.js
,每次更改JS發佈,都必需要替換模板頁面的script包含的js文件名。
我用到了assets-webpack-plugin
這個插件,webpack在編譯的時候,會生成一個assets.json
文件,裏邊記錄了全部編譯的文件編譯先後的關聯。如:
{ "store/select-store": { "js": "store/select-store.54caf1d3.js" }, "user/annual2/index": { "js": "user/annual2/index.2ff2c11d.js" }, "user/user-car/select-car": { "js": "user/user-car/select-car.cd0f5f41.js" } }
這個插件只是生成映射文件,還須要用這個文件去執行js版本替換。看下面的自動更新緩存。
在開發的時候,編譯打包的文件跟發佈編譯打包出來的文件確定不同,具體能夠參考構建優先的原則。
在gulp的build:dev
和build:prod
命令裏邊,會設置一個環境變量:
gulp.task('build:dev', function(cb){ // 設置當前應用環境爲開發環境 process.env.BUILD_ENV = 'dev'; //... ... }); gulp.task('build:prod', function(cb){ // 設置當前應用環境爲生產環境 process.env.BUILD_ENV = 'prod'; //... ... });
而後在webpack裏邊根據不一樣的環境變量,來進行不一樣的配置:
if(process.env.BUILD_ENV=='prod'){ webpackConfig.output.filename = '[name].[chunkhash:8].js'; }else{ webpackConfig.output.filename = '[name].js'; webpackConfig.devtool = "cheap-module-eval-source-map"; }
一直以來,咱們修改js提交發布的時候,都須要手動去修改一下版本號,如:
當前線上版本:
<script src="/js/user-car.js?v=1.0.1"></script>
待發布版本:
<script src="/js/user-car.js?v=1.1.0"></script>
這樣如今看起來好像沒有什麼問題,惟有的問題就是每次都要手動改版本號。
可是,若是之後要對靜態資源進行CDN部署的時候,就會有問題。通常動態頁面會部署在咱們的服務器,靜態資源好比js,css,圖片等會使用CDN,那這時候是先發布頁面呢,仍是先發布靜態資源到CDN呢?不管哪一個前後,都會有個時間間隔,會致使用戶訪問的時候拿到的靜態資源會跟頁面有不一致的狀況。
以上這種是覆蓋式更新靜態資源帶來的問題,要解決這個問題,可使用非覆蓋式更新,也就是每次發佈的文件都是一個新的文件,新舊文件同時存在。這時能夠先發布靜態資源,再發布動態頁面。能夠完美地解決這個問題。
那麼,咱們要實現的就是每次開發修改js文件,都會打包出一個新的js,而且帶上版本號。
webpack中能夠經過配置output的filename來生成不一樣的版本號:
webpackConfig.output.filename = '[name].[chunkhash:8].js';
有了帶版本號的js,同時也生成了資源映射記錄,那就能夠執行版本替換了。
在網上看了下別人的解決方案,基本上都說是用到webpack的html-webpack-plugin
這個插件來處理,或者用gulp的gulp-rev
和gulp-rev-collector
這2個插件處理。可是我感受都不是很符合咱們項目的狀況,並且這個應該不難,就本身寫了一個版本替換的代碼去處理。這些插件後續有時間再研究研究。
在頁面模板上,咱們經過下面的方式來註冊當前頁面的使用的js文件到頁面底部:
<?php $this->registerJsFile('/dist/annual2/index/index.js'); ?>
每次用gulp執行版本替換的時候, 會先讀取資源映射文件assets.json
,拿到全部js的映射記錄。
var assetMap = config.app.root + '/logs/assets.json'; var fileContent = fs.readFileSync(assetMap); var assetsJson = JSON.parse(fileContent); function getStaticMap(suffix){ var map = {}; for(var item in assetsJson){ map[item] = assetsJson[item][suffix]; } return map; } var mapList = getStaticMap('js');
而後再讀取模板文件,用正則分析出上面註冊的js文件,最後執行版本替換就好了。
項目通常會用到第三方庫,好比咱們會用到zeptojs
,art-template
,crypto-js
等。
單獨把這些庫打包成一個文件lib.js
,在頁面上用script
標籤引入。
這能夠經過在webpack中配置externals
來處理。
在開發環境下,能夠設置webpack的sourcemap
,方便調試。 可是webpack的sourcemap模式很是多,哪一個比較好,還沒什麼時間去細看。 能夠參考官方文檔
至此,前端項目的第一階段的改造算是完成了。 我不是前端開發,gulp
與webpack
都是第一次接觸而後使用,中間踩了很多的坑,爲了解決各類問題,差很少把google
都翻爛了。不過慶幸的是,如今前端開發能夠比較順暢地去寫代碼了。整個結構看起來比之前賞心悅目了很多。
我以爲此次改造最大的變化不是使用了2個工具使到開發更自動化,而是整個開發的思想與模式都從根本上發生了變化。將來還會繼續去作更多的探索與改進。
各位看官對前端開發有更好的建議或者作法,歡迎隨時跟我交流。