公司前端開發架構改造

要看更多的文章,歡迎訪問個人我的博客: 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開始,到如今爲止,產品/開發/項目管理等都在逐漸完善。走專業化道路,是我一直以來的目標,有興趣的能夠加我一塊兒交流!

問題總結

先來總結一下改造前前端開發存在的問題:

  1. 同時存在多端,形成開發效率不高

  2. 項目沒有模塊化,組件化的概念,代碼複用率低

  3. 部署困難,沒有自動生成版本號,每次都要手動修改js的版本號

  4. 麪條式的代碼,開發任務重,沒有作很好的規劃

改進目標

有問題,那就想辦法去解決它:

  1. 解決多端統一的問題,一處修改,多端同時生效

  2. 模塊化開發,使代碼邏輯更加清晰,更好維護

  3. 組件化開發,加強擴展性

  4. 按需打包,以及自動構建

  5. 自動更新js版本號,實現線上自動更新緩存資源

  6. 緊跟發展趨勢,使用ES6進行開發

在改進的過程當中,會用到2個工具: GulpWebpack。用這2個工具,也是有緣由的。

原本我想在Grunt的基礎上利用Browserify進行模塊化打包,可是發現打包的速度太慢,個人Linux系統編譯要4s以上,美女前端的Widnows系統通常都要7s以上,這簡直不能忍受。在試用Gulp以後發現速度槓槓的,不用想了,馬上替換Grunt。至於Webpack,是由於用browserify打包多個入口的文件配置比較麻煩,在試用了Webpack以後,發現Webpack的功能比browserify強大不少,因而就有了這2個工具的組合。Webpack的配置比較靈活,可是帶來的結果就是比較複雜,在項目中,我也僅僅用到了它的模塊化打包。

因而,最終初步實現前端構建的方案是:

Gulp進行JS/CSS壓縮,代碼合併,代碼檢查,sass編譯,js版本替換等,Webpack只用來進行ES6的模塊化打包。

webpack

如今前端的操做很簡單:

開發的時候,執行如下命令,監聽文件,自動編譯:

$ 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.jspay.js

webpack.config.js文件,能夠經過getEntry函數來統一處理入口,並獲得entry配置對象。若是你是多頁面多入口的項目,建議你使用統一的命名規則,好比頁面叫index.html,那麼你的js和css入口文件也應該叫index.jsindex.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:devbuild: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-revgulp-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文件,最後執行版本替換就好了。

一些要點

使用externals

項目通常會用到第三方庫,好比咱們會用到zeptojsart-templatecrypto-js等。

單獨把這些庫打包成一個文件lib.js,在頁面上用script標籤引入。

這能夠經過在webpack中配置externals來處理。

sourcemap

在開發環境下,能夠設置webpack的sourcemap,方便調試。 可是webpack的sourcemap模式很是多,哪一個比較好,還沒什麼時間去細看。 能夠參考官方文檔

最後

至此,前端項目的第一階段的改造算是完成了。 我不是前端開發,gulpwebpack都是第一次接觸而後使用,中間踩了很多的坑,爲了解決各類問題,差很少把google都翻爛了。不過慶幸的是,如今前端開發能夠比較順暢地去寫代碼了。整個結構看起來比之前賞心悅目了很多。

我以爲此次改造最大的變化不是使用了2個工具使到開發更自動化,而是整個開發的思想與模式都從根本上發生了變化。將來還會繼續去作更多的探索與改進。

各位看官對前端開發有更好的建議或者作法,歡迎隨時跟我交流。

相關文章
相關標籤/搜索