從零開始基於vue2 webpack3構建多頁應用

WE_清風發表於2017-10-19
序:基於vue2和webpack3進行的多頁面應用構建,github地址:github.com/FedWithMori…

一、 專案目錄結構

任何一個專案開始構建之前最先要做的就是先確定我們專案的目錄結構,包括開發目錄和生產目錄。
1. 開發目錄
.
├── README.md
├── build
│   ├── devtool.js                     // 服務配置
│   ├── entry.js                       // 獲取所有的入口路徑
│   ├── output.js                      // 輸出
│   ├── plugins.js                     // 配置外掛
├── package.json
├── webpack.config.dev.js              // 開發環境配置
├── webpack.config.js                  // 生產環境配置
└── src
    ├── assets                         // 靜態目錄
    │   ├── less                       // 基本樣式和基礎依賴
    │       ├── mixin.less
    │       ├── reset.less
    │       ├── variable.less
    │   ├── images                     // 圖片
    │       ├── home
    │           ├── home.png
    │       ├── index.png
    │       ├── about.png
    │   ├── fonts                      // 字型
    │       ├── a.woff
    ├── components                     // 元件
    │   ├── button.vue
    ├── entry                          // 入口js
    │   ├── home
    │       ├── home.js
    │   ├── index.js
    │   ├── about.js
    ├── page                           // 頁面模組
    │   ├── home
    │       ├── home.vue
    │   ├── index.vue
    │   ├── about.vue複製程式碼
2. 生產目錄
.
├── dist
    ├── css
    ├── html
    ├── js
    ├── images
    ├── fonts
    ├── vendor複製程式碼

二、 開始構建專案

第一步:新建一個專案目錄

在命令皮膚輸入如下命令可建立新的目錄:mkdir vue2-webpack3

第二步:初始化專案

2.1 直接在cli裡輸入命令 cd vue2-webpack3 進入專案
2.2 然後輸入命令 npm init,然後依次輸入相關的資訊後輸入yes儲存相關的專案資訊
2.3 這時候專案裡多了一個package.json檔案,這個檔案裡儲存了我們專案相關的一些資訊,具體情況可以移步 package.json說明文件

第三步:搭建專案結構
3.1 按照開發目錄結構建立完所有的目錄
3.2 接下來就輪到webpack登場了

大家都知道,webpack的配置檔案主要由:entry,output,module,plugins,devtool等幾部分構成,為了方便管理(如果全在一個檔案內,隨著專案的龐大會導致配置頁面的內容過多),我單獨為除了module以外的幾個屬性建立了檔案

3.3 entry配置

因為我們是多頁面應用,所以我們的入口檔案肯定是非常多的,為了方便獲取所有的入口檔案,我們可以利用Node的fs檔案系統來獲取entry目錄下的所有入口檔案的路徑,程式碼如下:


const fs = require('fs');
const path = require('path');

const directory = path.resolve(__dirname, '../src/entry') ;
const entryList = {};

(getEntry = (dir) => {

    const entryArr = fs.readdirSync(dir);
    let pathName,
        filePath;

    entryArr.forEach(function(filename) {

        filePath = dir + '/' + filename; 
        if(fs.statSync(filePath).isDirectory()) {

            getEntry(filePath);

        } else {

            pathName = filePath.split('entry/')[1].replace('.js', '');
            entryList[pathName] = filePath;

        }

    })

})(directory)

module.exports = entryList複製程式碼

簡單的解析下這段程式碼,主要是利用到了fs.readdirSync和fs.statSync兩個方法。fs.readdirSync方法能夠根據你提供路徑,獲取該路徑下的所有檔案路徑,比如上面程式碼中我傳遞的dir(需要注意fs.readdirSync的引數必須是一個絕對路徑,相對路徑無法獲取),fs.readdirSync會返回entry下所有檔案的路徑,然後我們拿到這些路徑以後,再根據fs.statSync來判斷這個路徑對應的是一個檔案還是一個目錄,如果是目錄,那就再呼叫一次,直到拿到檔案的路徑。

3.4 解決了入口檔案的問題,接下來繼續配置output,程式碼如下:
const path = require('path');

module.exports = {

    path: path.resolve(__dirname, '../dist'),
    // publicPath: 'http://img.xxx.com',
    filename: 'js/[name].js?ver=[hash:6]'

}複製程式碼

配置很簡單,path把所有的檔案都輸出到dist目錄裡,filename把所有的js檔案都輸出到dist/js目錄下,同時[name]對應的是入口檔案中的pathName,如果有需要,也可以對publicPath進行配置,比如靜態單獨打包到了一個伺服器上,那麼我們就需要對靜態的路徑做統一的處理了。

3.5 配置好入口和出口,繼續配置從入口到出口所經歷的一些loader,程式碼如下:

rules: [

    {
        test: /\.less$/,
        exclude: /node_modules/,
        use: ExtractTextPlugin.extract(['css-loader', 'less-loader'])
    },
    {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
            loaders: {
                less: ExtractTextPlugin.extract({
                    use: ['css-loader', 'less-loader'],
                    fallback: 'vue-style-loader'
                })
            }
        }
    },
    {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
    },
    {
        test: /\.(jpg|jpeg|png|gif)$/,
        loaders: 'url-loader',
        options: {
            limit: 10000
        }
    },
    {
        test: /\.(woff|woff2|svg|eot|ttf)$/,
        use: 'file-loader'
    }

]複製程式碼

loader的配置很簡單,但是需要注意幾個問題。
1)我期望的是能夠把vue元件中的樣式和less的樣式單獨抽離到css檔案中,所以需要利用extract-text-webpack-plugin這個外掛,後面會講到這個外掛的使用
2)我期望能夠利用外掛自動補齊css樣式的字首,所以引入了postcss-loader轉換器和autoprefixer規則,關於自動補齊的配置,我直接新建個postcss.config.js,然後做了如下簡單的配置:

module.exports = {
    plugins: {
        'autoprefixer': {}
    }
}複製程式碼

如此配置以後,專案編譯less的時候會預設讀取該配置檔案的內容並根據內容進行後續的處理。
3)我還期望能夠在開發的時候用上es6語法,所以我們需要依賴babel來實現轉譯,webpack中也有babel-loader來做這個事情,我們需要執行下面的命令來安裝相關的外掛:

npm i babel-loader babel-core babel-preset-env babel-plugin-transform-runtime --save-dev
npm i babel-runtime --save複製程式碼

安裝完後,新建一個.babelrc檔案用來配置babel,程式碼如下:

{
    presets: [
        [
            'env',
            {
                'targets': {
                    'browsers': ['last 2 versions', 'ie >= 8']
                }
            }
        ]
    ],
    plugins: ['transform-runtime']
}複製程式碼

簡單說下配置
presets的作用主要是告訴babel用哪個語法版本來進行轉譯(其實presets就是一堆外掛的集合),比如說常見的是babel-preset-es2015,配置如下:

{
    presets: ['es2015']
}複製程式碼

按照這個配置babel會將所有的屬於es2015正式版本的語法轉譯為es5的語法,那麼一些es2016,es2017之類的語法是無法被轉譯的,而且現在一些現代瀏覽器對於es2015+的支援也越來越好,有時候也並不是所有的語法都需要轉譯,所以babel推出了新的配置,也就是.babelrc的配置,‘env’這個配置可以指定我們期望轉譯最低相容的瀏覽器,比如這裡配置的是瀏覽器最新的兩個版本和ie8以上的版本,而且對於es2015+的語法也是同樣支援的。
plugins的作用很簡單就是引入外掛,這裡引入了個transform-runtime的外掛,它的作用在文章結束時會提到。

3.6 接下來繼續配置plugins。

1)因為是多頁面專案,所以我們的html頁面都需要前端自行建立,為了避免進行這些無用的重複操作,我引入了html-webpack-plugin外掛,程式碼如下:

const entry = require('./entry');
let configPlugins = [];
// 根據入口js陣列生成頁面
Object.keys(entry).forEach((item) => {

    config = {
        filename: '../dist/html/' + item + '.html',
        template: path.resolve(__dirname, '../src/index.html'),
        chunks: [item]
    }

    configPlugins.push(new HtmlWebpackPlugin(config));

})複製程式碼

首先要將入口路徑陣列引入進來,因為html-webpack-plugin外掛的用法就是例項化一次就會生成一個頁面,所以我們對entry的key進行了迴圈,在每一次的迴圈中,根據key的資訊生成config,然後例項化HtmlWebpackPlugin外掛,如此就可以根據entry生成我們想要的html目錄和頁面

2)上面講到了extract-text-webpack-plugin外掛提取css並生成css檔案,配置程式碼如下:

new ExtractTextPlugin({
    filename: 'css/[name].css'
})複製程式碼

該配置會根據入口的目錄結構和檔名稱在css中生成對應的結構和css檔案,[name]同樣取決於pathName

3)生成了css檔案後,我還期望能夠對通用的css和js進行提取,所以引入了CommonsChunkPlugin,這個外掛提供了對chunk中公共的部分進行提取並生成檔案的能力,配置如下:

new webpack.optimize.CommonsChunkPlugin({
    name: 'reset',
    filename: 'vendor/common.js',
    minChunks: 3
})複製程式碼

第一個name屬性決定了提取出來的公共css檔案的名稱,這個reset.css檔案會生成到css目錄下
第二個filename屬性決定了公共js的目錄和名稱,該common.js會生成到vendor目錄下
第三個minChunks決定了當有多少個入口檔案都含有該模組會對該模組進行抽離,我設定為3,意味著只有當至少3個入口js中都擁有某個相同的部分,才會對該部分進行提取。
ps:在提取的時候要注意個問題,CommonsChunkPlugin只提供了提取公用部分並生成檔案的能力,而並沒有提供往html頁面中自動生成公共檔案引入的能力,所有我們需要在模板檔案中預設引入公共檔案。

4)熱更新功能對於任何一個開發人員來說肯定是必不可少的,webpack.HotModuleReplacementPlugin外掛提供了這個能力,不過在使用這個外掛之前,我們要先安裝個webpack-dev-server外掛,兩個外掛結合才可以實現這個熱更新的功能。

5)clean-webpack-plugin外掛也是必不可少的,它能夠在每次編譯完成之前先幫助我們刪除指定的目錄以及目錄下所有的檔案,這種做法能夠幫助我們確保每次生成的程式碼都是最新的。配置程式碼如下:

new CleanWebpackPlugin(['dist'], {
    root: path.resolve(__dirname, '../')
})複製程式碼
3.7 關於resolve配置,程式碼如下:
resolve: {
    extensions: ['.js', '.vue', '.less'],
    alias: {
        less$: path.resolve(__dirname, 'src/assets/less'),
        components$: path.resolve(__dirname, 'src/components')
    }
}複製程式碼

extensions的作用主要是方便我們在引入別的檔案時可以省略字尾
alias的作用是設定一些路徑的別名,那麼在引入別的檔案的時候,就可以利用該別名來替代冗長的路徑
ps:這裡提一下path.resolve,為什麼要使用它,是為了保證所有模組引入時地址的統一,畢竟專案的結構是有各種層級的,如果不進行統一,那麼不同結構的模組引入時的路徑也會不一樣。

3.8 配置完外掛後,接下來配置DevServer,該配置必須要安裝webpack-dev-server外掛,配置起來也很簡單,程式碼如下:
var path = require('path');

module.exports = {
  contentBase: path.resolve(__dirname, '../dist'),
  host: 'we.cli',       // 別忘了配置host哦
  port: 8001,           // 埠8001
  inline: true,         // 可以監控js變化
  hot: true,            // 熱啟動
  compress: true,
  watchContentBase: false
};複製程式碼
3.9 以上都是針對開發環境的配置,我們還需要針對生產環境進行配置,比如對程式碼進行壓縮,新建立個webpack.config.js,然後將webpack.config.dev.js的內容複製一份過來。

1)壓縮css
這個比較簡單,只需要在每個css-loader後面增加一個minimize引數就行了,如下:

{
    test: /\.less$/,
    exclude: /node_modules/,
    use: ExtractTextPlugin.extract(['css-loader?minimize', 'less-loader'])
}複製程式碼

2) 壓縮js
對js的壓縮需要依賴webpack.optimize.UglifyJsPlugin外掛,我們將plugins.js外掛複製一個副本,重新命名為plugins.prod.js,然後引入webpack.optimize.UglifyJsPlugin外掛,配置如下:

new webpack.optimize.UglifyJsPlugin({
    compress: {
        warnings: false
    }
})複製程式碼

3) 壓縮html頁面
我們還可以對html頁面進行壓縮,以達到進一步減少頁面體積的效果,而HtmlWebpackPlugin外掛本身已經具備這個能力,新增個minify配置即可,配置如下:

Object.keys(entry).forEach((item) => {

    config = {
        filename: '../dist/html/' + item + '.html',
        template: path.resolve(__dirname, '../src/index.html'),
        chunks: [item],
        minify: { 
            // 移除HTML中的註釋
            removeComments: true, 
            // 刪除空白符與換行符
            collapseWhitespace: true 
        }
    }

    configPlugins.push(new HtmlWebpackPlugin(config));

})複製程式碼

好了,經歷了這些步驟以後,專案就構建完了。有興趣的可以到github上clone程式碼下來執行看看,地址: github.com/FedWithMori…

三、總結

1. 為什麼說presets就是一堆外掛的集合呢?

從babel的官網你可以看到babel-preset-es2015其實就是包含了以下這些外掛的集合:
transform-es2015-arrow-functions
transform-es2015-block-scoped-functions
transform-es2015-block-scoping
transform-es2015-classes
transform-es2015-computed-properties
transform-es2015-constants
transform-es2015-destructuring
transform-es2015-for-of
transform-es2015-function-name
transform-es2015-literals
transform-es2015-modules-commonjs
transform-es2015-object-super
transform-es2015-parameters
transform-es2015-shorthand-properties
transform-es2015-spread
transform-es2015-sticky-regex
transform-es2015-template-literals
transform-es2015-typeof-symbol
transform-es2015-unicode-regex
transform-regenerator
通過這個我們可以知道,不一定要引入babel-preset-es2015,你也可以針對某個特別的新特性進行單獨的轉譯配置

2.transform-runtime的作用是什麼?

答案可以參考下 segmentfault.com/q/101000000… 的高分回答
大概的作用就是在編譯的時候預設會使用babel-runtime的工具函式,從而減少編譯後的程式碼量

總的來說,這是一個比較基礎的vue+webpack的配置,後續也會進一步的完善,包括對固定第三方依賴的打包快取,程式碼檢查等
主要參考文獻:

webpack文件:doc.webpack-china.org/concepts/
webpack loader:doc.webpack-china.org/loaders/
webpack外掛:doc.webpack-china.org/plugins/
Babel的presets和plugins配置解析:excaliburhan.com/post/babel-…
vue-loader:vue-loader.vuejs.org/zh-cn/start…

最後悄悄打個小廣告,歡迎對前端興趣的朋友加入QQ群:474471759

相關文章