從搭建vue-腳手架到掌握webpack配置(四.自動化封裝)

JensonWild發表於2018-02-14

前言

上一期我們對webpack的構建進行了改進,新增了babelrc和postcss編譯器,還有把專案的構建能力適應了多頁面開發。但是大家發現這個工程還不能算得上是一個腳手架,尤其是新增了多頁面能力之後,每次新增頁面都要手動新增外掛配置,所以我們要進行一些簡單的封裝,達到通過簡單配置進行統一設定配置的效果。

本期重點:對目前的專案進行簡單的封裝,簡化專案配置難度

首先,沒有webpack配置基礎或者配置修改經驗的同學請移步第一期(基礎配置)第二期(外掛與提取)

GitHub : github.com/wwwjason199…

往期連結:
從搭建vue-腳手架到掌握webpack配置(一.基礎配置)
從搭建vue-腳手架到掌握webpack配置(二.外掛與提取)
從搭建vue-腳手架到掌握webpack配置(三.多頁面構建)

構思配置檔案

首先我們要構思一下具體那些配置項是我們會經常用到的,而且會需要經常修改的。
新建一個config目錄,在該目錄下新建index.js檔案
index.js的內容如下:

 const config = {
    page:{
        index:'./src/main.js',
        home: ['./src/home.js','home page']
    },
    defaultTitle:"this is all title",//頁面的預設title
    externals : {//大三方外部引入庫宣告
        'jquery':'window.jQuery'
    },
    cssLoader : 'less',//記得預先安裝對應loader
    // cssLoader : 'less!sass',//可以用!號新增多個css預載入器
    usePostCSS :  true, //需要提前安裝postcss-loader
    toExtractCss : true,

    assetsPublicPath: '/',//資源字首、可以寫cdn地址
    assetsSubDirectory: 'static',//建立的的靜態資源目錄地址

    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,//除錯開啟時是否自動開啟瀏覽器
    
    uglifyJs : true,//是否醜化js
    sourceMap : true,//是否開啟資源對映
    plugins:[]//額外外掛
}
module.exports = config;
複製程式碼

注意:配置項中的路徑都相對於跟目錄

配置項說明

page:就如webpack.config裡面的entry,進行了改良,如果屬性是陣列的話,第二個引數是html的標題(title)

defaultTitle:是所有頁面預設的title

externals:如註釋

cssLoader:要使用的css預載入器,可以用!分割設定多個載入器,使用的同時記住npm install less-loader安裝對應的loader

usePostCss:是否使用postcss,也是要預先安裝post-loader

toExtractCss:是否抽取css檔案

assetsPublicPath:資源的公共地址字首,頁面的所有資源引入會指向該地址,可以是一個cdn的域名。

assetsSubDirectory:要在根目錄下建立一個static目錄存放不被webpack編譯的檔案(靜態檔案),而assetsSubDirectory值是dist目錄下的靜態資源地址,如值是static的話,build之後,~\static目錄下的檔案就會被複制到dist/static目錄下

host \port \autoOpenBrower:如註釋

uglifyJs \sourceMap :如註釋

plugins:可以new一些外掛進去

整理專案配置

在config目錄下放封裝的配置邏輯指令碼檔案,新建一個static目錄放靜態資源,多出的幾個檔案後面會慢慢道來

image

按版本生成程式碼

以免多次build程式碼的時候都會覆蓋上次的生成記錄,我們可以做一個小優化,用package.json裡的version值作為目錄名在dist下生成如dist/1.0.0/的目錄

image

只要改一下output的值就能實現這一需求

output:{
        path:path.resolve(__dirname,'./dist/'+ process.env.npm_package_version),
        filename:"js/[name].js"
    },
複製程式碼

process.env.npm_package_version能得到package.json裡的version值,具體參考這裡

在每次build之前按需要修改package.json裡的version值就可以區分版本生成目錄

修改npm script

"scripts": {
    "clean": "node config/build.js",
    "build": "webpack --progress --hide-modules --config config/webpack.prod.conf.js",
    "dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
    "c-b": "npm run clean && npm run build"
  },
複製程式碼

你會發現多了一個clean和c-b,而且build指向了一個新的檔案 config/webpack.prod.conf.js,clean也執行了一個新的檔案config/build.js。從名字能看出來clean是用來清理目錄的,c-b是clean和build一起執行的

所以改完了npm script之後我們在config目錄建立這兩個檔案吧。
config/webpack.prod.conf.js檔案用於獨立生成環境是用到的webpack配置項
config/build.js是清理邏輯。

先清理目錄

config/build.js內容如下:

'use strict'
process.env.NODE_ENV = 'production'

const rm = require('rimraf')
const path = require('path')
// const webpack = require('webpack')
// const webpackConfig = require('./webpack.prod.conf')


rm(path.resolve(__dirname,'../dist/'+ process.env.npm_package_version), err => {
  if (err) throw err
  // webpack(webpackConfig, (err, stats) => {
  //   if (err) throw err
  // })
})
複製程式碼

就是簡單地用node的rimraf元件刪除當前版本的目錄
為什麼有一些註釋的部分呢?
其實這些程式碼是從官方的vue-cli裡面貼上過來的,原本vue-cli預設是刪除和webpack執行一起執行的,但是我發現這樣做 一來沒有了webpack --progress載入進度顯示,二來要引入很多node外掛來書寫載入提示,三來clean和build一起執行太過絕對了。所以我把執行webpack的邏輯註釋掉了,然後用npm script裡的build進行代替。

獨立生產環境配置

在上幾期我們簡單的用if (process.env.NODE_ENV === 'production')作為生產環境的判斷,在webpack.config.js檔案裡面一起編寫配置項。為了規範化和獨立性,把if裡的內容抽離到一個新的檔案(config/webpack.prod.conf.js)裡面,如下

process.env.NODE_ENV = 'production'
const path = require('path')
const config = require('../config')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require("../webpack.config.js")

const webpackConfig = merge(baseWebpackConfig, {
    devtool : '#source-map',
    output:{
        path:path.resolve(__dirname,'../dist/'+ process.env.npm_package_version),
        filename:"js/[name].[chunkhash].js",
        chunkFilename:"js/[id].[chunkhash].js",
        publicPath:config.assetsPublicPath || '/'
    },
    plugins :[
        new webpack.DefinePlugin({
            'process.env': {
            NODE_ENV: '"production"'
            }
        }),
        new webpack.LoaderOptionsPlugin({
            minimize: true
        }),
        //提取多入庫的公共模組
        Object.keys(config.page).length >= 2 ? new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            minChunks:2
        }):()=>{},
        //抽取從node_modules引入的模組,如vue
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vender',
            minChunks:function(module,count){
                var sPath = module.resource;
                // console.log(sPath,count);
                return sPath &&
                    /\.js$/.test(sPath) &&
                    sPath.indexOf(
                        path.join(__dirname, '../node_modules')
                    ) === 0
            }
        }),
        //將webpack runtime 和一些複用部分抽取出來
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest',
            minChunks:Infinity
        }),
        //將import()非同步載入複用的公用模組再進行提取
        new webpack.optimize.CommonsChunkPlugin({
            // name: ['app','home'],
            async: 'vendor-async',
            children: true,
            deepChildren:true,
            minChunks:2
        }),
    ]
})
if(config.uglifyJs){
    module.exports.plugins = (module.exports.plugins || []).concat([
        new webpack.optimize.UglifyJsPlugin({
            sourceMap: config.sourceMap,
            compress: {
            warnings: false
            }
        }),
    ])
}
if(config.sourceMap){
    module.exports.devtool = false
}
module.exports = webpackConfig
複製程式碼

process.env.NODE_ENV = 'production'首先宣告當前是生成環境

const config = require('../config')引入了上面構思的配置項的配置檔案

你會發現用到了webpack-merge外掛(記得npm install --save-dev webpack-merge),顧名思義這是合併兩個物件裡面的webpack配置項的。

其他的就是生產程式碼時用到的公共塊抽取外掛、醜化js等外掛,不清楚這些外掛的可以翻回去看(二)和(三)期
還有重新配置了一下output的配置,主要是多了publicPath項,這是設定支援公用地址字首的配置項。

重點來了!

用配置項進行自動配置專案的魅力就在於,可以通過通俗易懂的配置規則得到配置複雜的webpack構建邏輯的效果。這也正正是考驗一個程式設計師程式設計能力的地方,半自動或全自動的配置背後可是執行著你封裝的邏輯指令碼。

自動配置多入口

我們之前構思的配置項第一個page就是一個宣告多入口,他類似wepack裡的entry,每一項對應一個入口,也對應一個頁面,如下就對應兩個頁面

page:{
        index:'./src/main.js',
        home: ['./src/home.js','home page']
    },
複製程式碼

這可以說是半自動的配置方法,有的人會傾向於全自動的方法,就是通過查詢給定的檔案目錄下包含的入口js檔案自動生成入口配置,而不用像我這樣手動宣告用到的入口。感興趣的可以參考這裡:link

Jason做過不少的小程式開發,比較習慣明確的列出所包含的頁面,所以更青睞這種半自動的配置方式。列明頁面入口不僅方面深入新增自己想要的規則,而且可以通過該配置項知道本專案包含哪些頁面。

開始封裝

config/index.js底下開始封裝我們要的邏輯,當然你可以獨立出一個新的檔案。這裡寫到一起是因為,一方面考慮到入門教程的複雜性,另一方面我們可以在同一個檔案下一遍對照配置項一遍封裝邏輯。

 const config = {
    page:{
        index:'./src/main.js',
        home: ['./src/home.js','home page']
    },
    //...
}
module.exports = config;

/**
 * some auto-create-function
 */
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

const isProduction = process.env.NODE_ENV === 'production'

//自動生成HTML模板
const createHTMLTamplate = function(obj){
    let htmlList = [];
    let pageList = obj;
    for(let key in pageList){
        htmlList.push(
            new HtmlWebpackPlugin({
                filename: key + '.html',
                title:Array.isArray(pageList[key])&&pageList[key][1]
                    ?pageList[key][1].toString()
                    :config.defaultTitle,
                template:path.resolve(__dirname,'../index.html'),
                chunks:[key,'vender','manifest','common'],
                chunksSortMode: 'dependency'
            })
        )
    }
    return htmlList
}
//設定多入口
const setEntry = function(obj){
    let entry = {};
    let pageList = obj;
    for(let key in pageList){
        if(Array.isArray(pageList[key]) && pageList[key][0]){
            entry[key] = path.resolve(__dirname,'../'+pageList[key][0].toString());
        }else{
            entry[key] = path.resolve(__dirname,'../'+pageList[key].toString());
        }
    }
    return entry
}

module.exports.plugins = (module.exports.plugins || []).concat(
    createHTMLTamplate(config.page)
);
module.exports.entry = setEntry(config.page);
複製程式碼

有點程式設計能力的同學不難看懂這些邏輯。只是遍歷page值裡面的每一項,返回entry和html模板外掛陣列。注意一點就是這裡用了Array.isArray(pageList[key]判斷當前是否陣列,作簡單的值相容。

還是那句,不懂HtmlWebpackPlugin看前兩期 或者 看這裡

createHTMLTamplate 返回對應配置項的HtmlWebpackPlugin外掛列表
setEntry 返回入口chunk物件entry

回到webpack.config.js

然後我們返回到webpack.config.js檔案把這些方法的然後值引用到對應的配置項上。留意註釋~~~~我在這裡~~~~

const path = require('path')
const config = require('./config')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const merge = require('webpack-merge')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

baseWebpackConfig = {
    entry:config.entry, //~~~~~~~~~我在這裡~~~~~~~~
    output:{
        path:path.resolve(__dirname,'./dist/'+ process.env.npm_package_version),
        filename:"js/[name].js"
    },
    module:{
        rules:[
        //...
        ]
    },
    plugins:[
        new CopyWebpackPlugin([
            {
              from: path.resolve(__dirname, './static'),
              to: config.assetsSubDirectory,
            }
        ])
    ].concat(config.plugins),//~~~~~~~我在這裡~~~~~~~
    resolve:{
        extensions: ['.js', '.vue', '.json'],
        alias:{
            'vue$':'vue/dist/vue.esm.js',// 'vue/dist/vue.common.js' for webpack 1
            '@': path.resolve(__dirname,'./src'),
        }
    },
    externals:config.externals,
}

module.exports = baseWebpackConfig;
複製程式碼

當然還用到了很多其他的配置項,檢查一下config關鍵字自己對號入座。

自動新增css前處理器

同樣在config/index.js下面新增自動配置css前處理器的邏輯,下面貼出程式碼有點長,但是請一定細心看一下,有註釋幫助理解,認真看下其實重點邏輯也就中間一部分

 const config = {
    //...
    
    cssLoader : 'less',//記得預先安裝對應loader
    // cssLoader : 'less!sass',//可以用!號新增多個css預載入器
    usePostCSS :  true, //需要提前安裝postcss-loader
    toExtractCss : true,
    
    //...
}
module.exports = config;

/**
 * some auto-create-function
 */
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const ExtractTextPlugin = require("extract-text-webpack-plugin")
const ExtractRootCss = new ExtractTextPlugin({filename:'styles/[name].root.[hash].css',allChunks:false});
const ExtractVueCss = new ExtractTextPlugin({filename:'styles/[name].[chunkhash].css',allChunks:true});

const isProduction = process.env.NODE_ENV === 'production'

//自動生成HTML模板
const createHTMLTamplate = function(obj){
    //...
}
//設定多入口
const setEntry = function(obj){
    //...
}
//設定樣式前處理器
const cssRules = {
    less: {name:'less'},
    sass: {name:'sass', options:{indentedSyntax: true}},
    scss: {name:'sass'},
    stylus: {name:'stylus'},
    styl: {name:'stylus'}
}
//vue內嵌樣式用到的配置規則
const cssLoaders = function(options){
    options = options || {}
    
    let loaders = {};
    const loaderList = options.loaders
    //判斷樣式是來自檔案還是.vue檔案內嵌,然後用對應的外掛例項
    const ExtractCss = options.isRootCss ? ExtractRootCss : ExtractVueCss;
    const cssLoader = {
        loader: 'css-loader',
        options: {
            sourceMap: options.sourceMap
        }
    }//css-loader
    const postcssLoader = {
        loader: 'postcss-loader',
        options: {
            sourceMap: options.sourceMap
        }
    }
    //判斷是否使用postcss
    const frontLoader = options.usePostCSS ? [cssLoader,postcssLoader]:[cssLoader]
    
    //出了less等預載入的loader之外,還一定要有一般css的編譯
    if(loaderList.indexOf('css') === -1)loaderList.unshift("css")
    
    //遍歷陣列生成loader佇列
    loaderList.forEach(element => {
        const loaderOptions = cssRules[element]&&cssRules[element].options;
        const loaderName = cssRules[element]&&cssRules[element].name;
        let arr = element==="css" ? [] : [{
            loader: loaderName+"-loader",
            options: Object.assign({}, loaderOptions, {
                sourceMap: options.sourceMap
            })
        }]
        //是否提取到css檔案
        if(options.Extract){
            loaders[element] = ExtractCss.extract({
                use: frontLoader.concat(arr),
                fallback: 'vue-style-loader' 
            })
        }else{
            loaders[element] = ['vue-style-loader'].concat(frontLoader,arr)
        }
    });
    //是否提取到css檔案
    if(options.Extract){
        module.exports.plugins = (module.exports.plugins || []).concat([ExtractRootCss,ExtractVueCss]);
    }

    return loaders
}
//樣式檔案用到的配置規則
const styleLoaders = function(options){
    options.isRootCss = true;
    let output = [];
    const loaders = cssLoaders(options);

    for (const extension in loaders) {
        let loader = loaders[extension]
        output.push({
          test: new RegExp('\\.' + extension + '$'),
          use: loader
        })
    }
    return output
}

module.exports.plugins = (module.exports.plugins || []).concat(
    createHTMLTamplate(config.page)
);
module.exports.entry = setEntry(config.page);
module.exports.styleLoaders = styleLoaders({
    loaders: config.cssLoader.split('!'),
    sourceMap : config.sourceMap,
    usePostCSS : config.usePostCSS,
    Extract : isProduction&&config.toExtractCss,//生成環境才判斷是否進行提取
});
module.exports.cssLoaders = cssLoaders({
    loaders: config.cssLoader.split('!'),
    sourceMap : config.sourceMap,
    //vue-loader內部自動開啟postcss所以開發環境下會有警告,所以也是生成環境才進行進一步判斷
    usePostCSS : isProduction&&config.usePostCSS,
    Extract : isProduction&&config.toExtractCss,
});
複製程式碼

有看過vue-cli內部封裝的程式碼(沒看過也沒關係)的同學可能會發現以上的程式碼有點像vue-cli裡面的邏輯。Jason確實借鑑了一點vue-cli的封裝思想。

cssRules :該物件是預先設定好不同預載入的名稱很options配置項。我們會發現同一種css處理器也會有不一樣的規則和字尾名(就如sass和scss),構思的配置項裡面很難一一列出,那麼我們就要藉助這裡物件進行區分。後期有什麼需要新增的options配置也可以在cssRules的第二個引數中新增。

cssLoaders:生成cssloader佇列的方法,同時可以直接賦值到vue-loader內的規則裡面。返回值如下

{
    css:[vue-style-loader,css-loader],
    less:[vue-style-loader,css-loader,less-loader]
}
複製程式碼

styleLoaders:該方法則是匹配對應css處理器字尾名檔案的配置規則。

細心的同學會留意到cssLoaders和styleLoaders,在對是否使用postcss時候多出 isProduction判斷。因為 vue-loader內部自動開啟postcss所以開發環境下會有警告,所以也是生成環境才進行進一步判斷是否開啟postcss。

再回到webpack.config.js
baseWebpackConfig = {
    //...
    module:{
        rules:[
            {
                test:/\.js$/,
                loader:"babel-loader",
                exclude:/node_modules/
            },
            {
                test:/\.(png|jpe?j|gif|svg)(\?.*)?$/,
                loader:'url-loader',
                options:{
                    limit:10000,
                    name:'img/[name].[ext]?[hash]'
                }
            },
            {
                test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                loader:"url-loader",
                options:{
                    limit:10000,
                    name:'fonts/[name].[ext]?[hash]'
                }
            },
            {
                test:/\.vue$/,
                loader:'vue-loader',
                options:{
                    loaders: config.cssLoaders //~~~~~~~我在這裡~~~~~~~
                }
            },
        ].concat(config.styleLoaders) //~~~~~~~我在這裡~~~~~~~
    },
    
    //...
}
module.exports = baseWebpackConfig;
複製程式碼

好了到現在半自動化的封裝邏輯都寫好了,下面選取一些需要注意的配置進行介紹。

其他配置項

host \port \autoOpenBrower

host \port \autoOpenBrower 這些和開發伺服器相關的配置項,以前引入到webpack.config.js裡面。

webpack.config.js下面新增程式碼(你也可以想vue-cli一樣再獨立一個檔案webpack.dev.conf.js),如下

const path = require('path')
const config = require('./config')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const merge = require('webpack-merge')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

baseWebpackConfig = {
    entry:config.entry,
    //...
}
if (process.env.NODE_ENV === 'development') {
    console.log(process.env.NODE_ENV);
    baseWebpackConfig = merge(baseWebpackConfig,{
        devtool : '#eval-source-map',
        devServer : {
            clientLogLevel: 'warning',
            historyApiFallback: true,
            hot: true,
            compress: true,
            host: HOST || config.host,
            port: PORT || config.port,
            open: config.autoOpenBrowser,
            publicPath:config.assetsPublicPath || '/'
        },
        plugins : [
            new webpack.DefinePlugin({
                'process.env': {
                NODE_ENV: '"development"'
                }
            }),
            new webpack.HotModuleReplacementPlugin()
        ]
    })
}
module.exports = baseWebpackConfig;
複製程式碼

顯然,host和port都可以被process.env.PORT\HOST 覆蓋。其他devServer配置項可以參考 官方文件

有一點要注意,HotModuleReplacementPlugin外掛一定要在開啟webpack-dev-server的時候才呼叫,所以要獨立在該開發環境判斷中。

assetsSubDirectory

一開始構思配置項的時候對該屬性有介紹
assetsSubDirectory值是dist目錄下的靜態資源地址,如值是static的話,build之後,~\static目錄下的檔案就會被複制到dist/static目錄下

而實現檔案複製的外掛是 CopyWebpackPlugin,使用前記得install

    new CopyWebpackPlugin([
            {
              from: path.resolve(__dirname, './static'),
              to: config.assetsSubDirectory,
            //   ignore: ['.*']
            }
        ])
複製程式碼
assetsPublicPath

該項是地址字首,如果用到了第三方配置資源的地址,那麼這裡就可以填寫對應的域名。

開發環境中它在

devServer : {
    clientLogLevel: 'warning',
    historyApiFallback: true,
    hot: true,
    compress: true,
    host: HOST || config.host,
    port: PORT || config.port,
    open: config.autoOpenBrowser,
    publicPath:config.assetsPublicPath || '/'
},
複製程式碼

生成環境中它在

output:{
    path:path.resolve(__dirname,'../dist/'+ process.env.npm_package_version),
    filename:"js/[name].[chunkhash].js",
    chunkFilename:"js/[id].[chunkhash].js",
    publicPath:config.assetsPublicPath || '/'
}
複製程式碼

其他配置項都淺而易懂,不多解釋,當然你可以發揮自己的創造力新增更多自己需要的配置項。

執行一下

執行清理並構建

npm run c-b
複製程式碼

得到的如下結果

image

完整的專案、webapck.config.js、config/index.js等檔案可以下載或者克隆本專案的github

GitHub : github.com/wwwjason199…

總結

整個系列學習編寫vue腳手架的過程到這裡算是得到了一個比較完整的入門,從一開始入門webpack的配置項、到引入常用外掛實現檔案抽離、再到適配多頁面多入口、最終對專案進行自動化的封裝。
不知不覺差不多實現vue官方給出的vue-cli裡面的大部分能力,是不是發現自己不再是webpack的小白,還挺有成就感呢。

我們在整個學習的過程中有很多借鑑vue-cli的思想和規範。可能有人會說自己寫這麼麻煩幹嘛,直接用vue-cli不就行了嗎?此言差矣,這不僅是一個學習webpack的過程,更是學會因地制宜按專案的實際情況構建工程的過程。而且能讓我們深入體會工程化和自動化的思想。

Jason的一些話

前段時間工作有點忙,而且廣州寒氣逼人下班都懶得動了,停更了差不多有一個月,雖然等更新的人不多,但是真的要跟有關注的同學說一聲對不起。開始放假而且天氣暖和了才把這一期碼完,請大家原諒,也希望大家不要學我這個重度拖延症患者一樣懶。

後面該碼什麼文章呢?

Jason寫的這些文章文筆不怎麼好,但都是以和大家一起學習一門技術為初衷在寫,Jason相信有意去入門webpack的同學看完了這一系列的文章肯定對webpack有了更多的瞭解。
Jason後面會複習一下es6+ 和 想去深入學習一下node,還會寫一些vue的專案。後面要寫什麼文章可能就看哪方面積累和了解的更深入,還有哪些內容跟適合總結成文章了。

大家有什麼想和Jason一起學習的前端框架、技術,可以留言哦,歡迎給意見和交流。

後面會不定期更新,喜歡的同學可以點下關注的。

參考

相關文章