【Vue】Vue1.0+Webpack1+Gulp項目升級構建方案的踩坑路

最近半年在維護公司的一個管理後臺項目,搭建之初的技術棧比較混亂,構建方案採用了Gulp中調用Webpack的方式,Gulp負責處理.html文件,Webpack負責加載.vue.js等。而在這一套構建方案中,主要有這些問題:javascript

  1. 沒有實現JS壓縮、CSS兼容等功能。
  2. 在開發模式下,保存代碼,項目會進行徹底的從新打包,持續構建速度不只緩慢,還會產生緩存的現象(構建完成後刷新頁面改動不生效)。
  3. 因爲目前的方案沒有使用http-proxy-middleware這樣的請求代理模塊,致使項目在本地開發時還要部署後端服務,對新接手的開發者不友好,並且常常因爲溝通不及時產生測試環境與本地環境的代碼同步問題。

所以,在熟悉這個項目以後,打算對其構建方案進行升級,主要爲了解決上述的問題。css

1. 原有構建方案描述

原有構建速度

  • npm run build:打包約50s
  • npm run dev:開啓開發模式約50s,保存自動從新編譯需約6s,編譯完成後須要刷新才能看到效果,偶爾因緩存問題須要再次自動從新編譯才能看到效果

原有構建結果

  • ./build/development:存放渲染後的js文件
  • ./build/html:存放渲染後的html文件
  • ./build/rev:保存各個入口文件hash值的json文件

打包代碼解析

/** * 使用gulp-clean插件刪除build目錄下的文件 */
gulp.task('clean', function () {
    if (!stopClean) {
        return gulp.src('build/' + directory, { read: false }).pipe(clean())
    }
})
/** * 使用webpack打包vue與js文件,在clean以後進行 */
gulp.task('webpack', ['clean'], function (callback) {
    deCompiler.run(function (err, stats) {
        if (err) throw new gutil.PluginError('webpack', err)
        gutil.log('[webpack]', stats.toString({}))
        callback()
    })
})
/** * 使用gulp-uglify插件對js文件進行醜化,在webpack以後進行 */
gulp.task('minify', ['webpack'], function () {
    if (environment) {
        return
    } else {
        return gulp.src('build/' + directory + '/*.js').pipe(uglify())
    }
})
/** * 使用gulp-rev插件爲打包後的文件增長hash,在minify以後運行 * * gulp-rev會作什麼: * 根據靜態資源內容,生成md5簽名,打包出來的文件名會加上md5簽名,同時生成一個json用來保存文件名路徑對應關係。 * 替換html裏靜態資源的路徑爲帶有md5值的文件路徑,這樣html才能找到資源路徑。 * 有些人可能會作:靜態服務器配置靜態資源的過時時間爲永不過時。 * 達到什麼效果: * 靜態資源只需請求一次,永久緩存,不會發送協商請求304 * 版本更新只會更新修改的靜態資源內容 * 不刪除舊版本的靜態資源,版本回滾的時候只須要更新html,一樣不會增長http請求次數 */
gulp.task('hashJS', ['minify'], function () {
    var dest = gulp.src(['一串入口文件...'])
        .pipe(rev()) // 設置文件的hash key
        .pipe(gulp.dest('build/' + directory)) // 將通過管道處理的文件寫出到目錄
        .pipe(rev.manifest({})) // 生成映射hash key的json
        .pipe(gulp.dest('build/rev')) // 將通過管道處理的文件寫出到目錄
    !environment && gulp.src(['一串入口文件...']).pipe(clean())
    return dest
})
/** * 使用gulp-rev-replace插件爲html中引用的js和css替換新的hash * 使用gulp-livereload插件在全部文件從新打包完成後局部更新頁面 */
gulp.task('revReplace', ['hashJS'], function () {
    return gulp.src(['html/*.html'])
        .pipe(revReplace({ ... })) // 給html中的js引用提供新的hash
        .pipe(gulp.dest('build/html')) // 輸出文件
        .pipe(livereload()) // 局部更新頁面
})
/** * 使用gulp.watch,當應用程序目錄下有任何文件發生改變,則從新執行一遍打包命令 * gulp.watch:監視文件,而且能夠在文件發生改動時候作一些事情。 */
gulp.task('watch', ['revReplace'], function () {
    stopClean = true
    livereload.listen()
    gulp.watch('app/**/*', ['clean', 'webpack', 'minify', 'hashJS', 'revReplace'])
})
/** * 輸出dev和build的工做流 */
gulp.task('default', ['clean', 'webpack', 'minify', 'hashJS', 'revReplace', 'watch']) // dev
gulp.task('build', ['clean', 'webpack', 'minify', 'hashJS', 'revReplace']) // build
/** * webpack配置 */
var devCompiler = webpack({
    entry: {
        ... // 一衆入口文件
        vendor: ['vue', 'vue-router', 'lodash', 'echarts'] // 公共模塊
    },
    output: {
        path: ..., // 全部輸出文件的目標路徑
        publicPath: ..., // 輸出解析文件的目錄
        filename: ..., // 輸出文件
        chunkFilename: ... // 經過異步請求的文件
    },
    // 排除如下內容打包到 bundle,減少文件大小
    external: {
        jquery: 'jQuery',
        dialog: 'dialog'
    },
    plugins: [
        /** * 經過將公共模塊拆出來,最終合成的文件可以在最開始的時候加載一次,便存到緩存中供後續使用。 * 這個帶來頁面速度上的提高,由於瀏覽器會迅速將公共的代碼從緩存中取出來,而不是每次訪問一個新頁面時,再去加載一個更大的文件。 */
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor']
        }),
        /** * DefinePlugin 容許建立一個在編譯時能夠配置的全局常量。這可能會對開發模式和生產模式的構建容許不一樣的行爲很是有用。 */
        new webpack.DefinePlugin({
            __VERSION__: new Date().getTime()
        })
    ],
    resolve: {
        root: __dirname,
        extensions: ['', '.js', '.vue', '.json'], // 解析組件的文件後綴白名單
        alias: { ... } // 配置路徑別名
    },
    module: {
        // 各個文件的loaders
        loaders: [
            { test: /\.vue$/, loader: 'vue-loader' },
            { test: /\.css$/, loader: 'style-loader!css-loader' },
            { test: /\.jsx$/, loader: 'babel-loader', include: [path.join(__dirname, 'app')], exclude: /core/ },
            { test: /\.json$/, loader: 'json' }
        ]
    },
    vue: {
        loaders: {
            js: 'babel-loader'
        }
    }
})
複製代碼

2. 將Gulp的功能移到Webpack1上執行

使用html-webpack-plugin插件構建項目的主.html文件

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            filename: '...', // 輸出的路徑
            template: '...', // 提取源html的路徑
            chunks: ['...'], // 須要導入的模塊
            inject: true // 是否附加到body底部
        })
    ]
}
複製代碼

使用webpack.optimize.UglifyJsPlugin插件進行JS壓縮

module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: { warnings: false }
        })
    ]
}
複製代碼

使用webpack-dev-server模塊,提供node搭建的開發環境

module.exports = {
    devServer: {
        clientLogLevel: 'warning', // 輸出日誌的級別,配置爲警告級別以上才輸出
        inline: true, // 啓動 live reload
        hot: true, // 容許啓用熱重載
        compress: true, // 對全部靜態資源進行gzip壓縮
        open: true, // 默認在啓動本地服務時打開瀏覽器
        quiet: true, // 禁止輸出繁雜的構建日誌
        host: ..., // 服務啓動的域名
        port: ..., // 服務啓動的端口
        proxy: { ... }, // http代理配置
        /** * 這個配置經常使用於解決spa應用h5路由模式下將全部404路由匹配回index.html的問題 * 因爲生產環境爲主頁匹配了一個比較簡單的別名,所以開發環境也照搬後端服務的配置 */
        historyApiFallback: {
            rewrites: [{ from: '/^\/admin/', to: '...' }]
        }
    }
}
複製代碼

踩坑

  1. webpack-dev-server@3.2.1 requires a peer of webpack^@4.0.0 but none is installed.:這兩個模塊版本不兼容,回退到webpack-dev-server@2成功運行。
  2. Cannot resolve module 'fsevents' ...:將全局的webpack調用改成直接從node_modules/webpack下直接調用,解決了問題,node node_modules/webpack/bin/webpack.js --config webpack.config.js
  3. Cannot resolve module 'fs' ...:配置config.node.fs = 'empty',爲Webpack提供node原生模塊,使其能加載到這個對象。
  4. 熱重載只對.js.css.vue中的<style>內樣式生效,對.vue文件中的html模板及js內容都不生效,會打印「模塊代碼已發生改變並從新編譯,但熱重載不生效,可能會啓用全局刷新的策略」之類的信息,暫時沒有解決,初步判斷是低版本的vue-hot-reload-api對這些部分的處理有問題,有大神瞭解原理能夠在評論區科普一哈=.=。

3. 從Webpack1升級到Webpack3

因爲Webpack2Webpack3幾乎徹底兼容,只是涉及到一些增量的功能,所以選擇直接從Webpack1遷移到Webapck3,先在項目中安裝Webpack3,而後根據Webpack2文檔中《從Webpack1遷移》的章節,對配置項進行更改,參考的文檔戳這個:www.html.cn/doc/webpack…html

此次升級沒有遇到什麼問題,根據文檔配置稍做更改就跑通了。梳理一下目前爲止實現的功能:前端

  1. 新的Webpack構建代碼已經實現了原有的全部功能,下面列舉新增的功能。
  2. 使用webpack-dev-server做爲開發服務器,實現了保存時live reload的功能。
  3. 使用http-proxy-middleware插件,將請求直接代理到測試服,讓開發環境脫離了本地部署的後端服務,大大下降了開發環境部署的時間成本。
  4. 新增friendly-errors-webpack-plugin,輸出友好的構建日誌,打印幾個重要模塊的開發環境地址,配置方面徹底參考了vue-cli@2的默認配置。
  5. 新增postcss-loader,對css添加兼容處理,配置方面徹底參考了vue-cli@2的默認配置。
  6. 使用webpack.optimize.UglifyJsPlugin壓縮js代碼。

嘗試進行構建,輸出構建時間記錄:vue

  • npm run build:約135s
  • npm run dev:初次構建約58s,持續構建約30s

項目構建時間過長(第一次打包把本身嚇了一跳...),只能繼續尋求構建速度上的優化java

4. 在Webpack3下進行構建速度的優化

使用webpack-jarvis監測構建性能

webpack-jarvis是一個圖形化的webpack性能監測工具,它配置簡便,對構建過程的時間佔比、構建結果的詳細記錄都有具體的輸出node

// 通過簡單的配置就能夠在本地3001端口輸出構建結果記錄
const Jarvis = require('webpack-jarvis')
module.exports = {
    plugins: [
        new Jarvis({
            watchOnly: false,
            port: 3001
        })
    ]
}
複製代碼

使用happypack

先根據網上搜到的文章,作一些簡單的優化,如使用happypack,這個模塊經過多進程模型,來加速代碼構建,可是使用以後貌似沒有太明顯的結果,構建時間大概減小了幾秒吧...暫時還不太懂這個模塊對優化什麼場景的效果比較明顯,以前有看到一篇講解happypack原理的文章,但還沒細看,有興趣小夥伴能夠研究一下,要是能在評論裏簡潔明瞭的給渣渣樓主解釋一下就更好了TUT:taobaofed.org/blog/2016/1…jquery

const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
    plugins: [
        new HappyPack({
            // happypack的id,在調用時須要聲明,若須要編譯其餘類型的文件須要再聲明一個happypack
            id: 'js',
            // cacheDirectory:設置後,將盡可能在babel編譯時使用緩存加載器的結果,避免從新走一遍babel的高昂代價
            use: [{ loader: 'babel-loader', cacheDirectory: true }],
            // 根據cpu的核心數判斷須要拆分多少個進程池
            threadPool: happyThreadPool,
            // 是否輸出編譯過程的日誌
            verbose: true
        })
    ]
}
複製代碼

作完這一步後,輸出構建時間記錄:webpack

  • npm run build:約130s
  • npm run dev:初次構建約60s,持續構建約30s

devtool配置爲cheap-module-eval-source-map

devtool選項啓用cheap-module-eval-source-map模式:vue-cli@2默認配置爲這種模式,cheap表明在輸出source-map時省略列信息;module表示在編譯過程當中啓用如babel-loader這樣的預編譯器,使得調試時能夠直接看到未經編譯的源代碼;eval表示啓用eval模式編譯,該模式直接使用eval函數執行編譯後模塊的字符串,減小了將字符串轉化爲可執行的代碼文件這個步驟,加快了項目開發中重建的速度;source-map表示輸出源代碼的映射表,使得開發時能夠直接把錯誤定位到源代碼,提升開發效率。git

作完這一步後,效果並不明顯=.=(相比原來的source-map),大概減小了幾秒,輸出構建時間記錄:

  • npm run build:約130s
  • npm run dev:初次構建約58s,持續構建約30s

使用html-webpack-plugin-for-multihtml提高多入口項目重建速度

重建一次居然須要30s!各類搜索找到了html-webpack-plugin的一條issue,發現html-webpack-plugin@2在構建多入口應用時速度確實有明顯變慢的狀況,緣由是沒有成功的對構建內容進行緩存,使每次重建都從新編譯全部代碼。做者給出的解決方案是使用這個模塊的一個分支項目(是由做者本人fork原項目並針對這個問題進行修復的項目)html-webpack-plugin-for-multihtml,用法與html-webpack-plugin徹底相同,使用以後重建僅需1s左右。

作完這一步後,輸出構建時間記錄:

  • npm run build:約130s
  • npm run dev:初次構建約58s,持續構建約1s

使用webpack.DllPlugin提取公共模塊

在輸出結果中找到了很多較大的依賴包,如Vue的核心庫、lodashecharts等等,還有一些不但願被打包的靜態資源,想辦法避免每次都編譯這些內容,提高編譯速度,因此找到了這個插件。

webpack.DllPlugin這個插件是來源於Windows系統的.dll文件(動態連接庫)的用法:首先經過DllPlugin模塊構建出一個包含公共模塊的包和一個映射表,再經過DllReferencePlugin模塊經過映射表給每一個模塊關聯對應的依賴,這樣能夠對這些公共模塊進行預先打包,之後構建的時候就不須要處理這些模塊,減小打包時間。

// webpack.dll.conf.js
const webpack = require('webpack')
module.exports = {
    entry: {
        vendor: [...]
    },
    output: {
        path: resolve('build/development'),
        filename: '[name].dll.js',
        library: '[name]_library'
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin(),
        new webpack.DllPlugin({
            path: resolve('build/development/[name]-manifest.json'), // 生成manifest文件輸出的位置和文件名稱
            name: '[name]-library', // 與output.library是同樣的,對應manifest.json文件中name字段的值,防止全局變量衝突
            context: __dirname
        })
    ]
}
// webpack.base.conf.js
const webpack = require('webpack')
module.exports = {
    plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('../build/development/vendor-manifest.json') // 讓webpack從映射表獲取使用的依賴
        })
    ]
}
複製代碼

打包出來以後還須要在html文件中引入公共庫vendor.dll.js文件

<html>
    <head></head>
    <body>
        <div id="app"></div>
        <script src="/build/development/vendor.dll.js"></script>
        <!-- 其餘JS應該注入到dll的後面,確保可以引用到公共庫的內容 -->
    </body>
</html>
複製代碼

作完這一步後,輸出構建時間記錄,發現構建效率有了明顯的提升:

  • npm run dll:約25s
  • npm run build:約70s
  • npm run dev:初次構建約55s,持續構建約1s

5. 後記

優化到這裏就差很少結束,此次的優化爲舊項目提供了新一代spa項目應有的一些功能,搭建了更現代的本地開發環境。因爲本文篇幅有點太長,完整的配置就丟在另外一篇文章裏。

6. Q&A

Q: 爲何不直接升級到Webpack4

A: Webpack4只支持vue-loader@15以上版本,而這個版本已經沒法解析Vue1的文件。

做者信息

做者其餘文章

相關文章
相關標籤/搜索