從零搭建 vue2 vue-router2 webpack3 工程

以新手視角,詳細介紹各個步驟內容,不深入講步驟涉及的原理,主要介紹如何操作。
本文示例工程 GitHub:https://github.com/qinshenxue/vue2-vue-router2-webpack2

2017-09-30 升級了 Vue(2.4.4)和 Webpack(3.6.0),github 項目已更新。
2017-11-02 修復了 babel 配置爲」presets」: [[「env」, { 「modules」: false }]]後,無法使用 import 動態導入功能的問題,在「異步組件(懶加載)」章節已更新解決方法。

初始化工程

新建工程目錄 vue2practice,在目錄下執行npm init -y來創建一個 package.json,在 package.json 中先添加以下必備模塊:

{
   "name" : "vue2-vue-router2-webpack3" ,
   "version" : "1.0.0" ,
   "devDependencies" : {
     "vue" : "^2.4.2" ,
     "vue-loader" : "^13.0.2" ,
     "vue-router" : "^2.7.0" ,
     "vue-template-compiler" : "^2.4.2" ,
     "webpack" : "^3.4.1" ,
     "webpack-dev-server" : "^2.6.1"
   }
}

其中 vue-template-compiler 是 vue-loader 的 peerDependencies,npm3 不會自動安裝 peerDependencies,然而 vue-template-compiler 又是必備的,那爲什麼作者不將其放到 dependencies 中呢?有人在 github 上提過這個問題,我大致翻譯一下作者的回答(僅供參考):這樣做的原因是因爲沒有可靠的方式來固定嵌套依賴的關係,怎麼理解這句話?首先 vue-template-compiler 和 vue 的版本號是一致的(目前是同步更新),將 vue-template-compiler 指定爲 vue-loader 的 dependencies 並不能保證 vue-template-compiler 和 vue 的版本號是相同的,讓用戶自己指定版本才能保證這一點。查看作者的回答(英文) 。如果兩者版本不一致,運行時會出現下圖所示的錯誤提示。

新建目錄結構如下,新增的目錄及文件先空着,後面的步驟會說明添加什麼內容。

vue2pratice
     |-- package.json
     |-- index.html         // 啓動頁面
     |-- webpack.config.js  // webpack配置文件
     |-- src
         |-- views       // vue頁面組件目錄
         |-- main.js     // 入口文件
         |-- router.js   // vue-router配置
         |-- app.vue     // 工程首頁組件

配置Webpack

Webpack 默認讀取 webpack.config.js,文件名不能隨便改,其中 entry 是必須配置的。

module.exports = {
     entry: './src/main.js' ,
     output: {
         path: __dirname + '/dist' ,
         publicPath: '/static/' ,
         filename: 'build.js'
     }
}

Webpack 2+ 要求output.path必須爲絕對路徑。關於 Webpack 的各種路徑在《詳解Webpack2的那些路徑》有詳細的介紹。

配置 webpack-dev-server,只需在 package.json 添加以下啓動命令即可。

"scripts" : {
   "dev" : "webpack-dev-server --hot --open"
}

webpack-dev-server 2 默認爲 inline 模式,熱模塊替換仍需自己設置。

驗證配置

在 index.html 中添加測試代碼,引入打包後的 JS 文件。

Hello, Webpack 3.
<br>
<script src= "/static/build.js" ></script>

在 main.js 中添加測試代碼。

// main.js
document.write( '來自main.js的問候!' )

執行下面的命令來安裝模塊並啓動服務器。

// 安裝依賴
npm install
 
// 運行
npm run dev

啓動後瀏覽器會自動打開http://localhost:8080,如果控制檯沒有報錯,頁面正確顯示 main.js 和 index.html 的內容,改動 main.js 後瀏覽器不刷新能看到效果,則表示配置沒問題。

Vue

新建頁面

在 views 目錄下新建 index.vue。

<template>
     <div>
         這是{{page}}頁面
     </div>
</template>
<script>
export default {
     data: function () {
         return {
             page: 'index'
         }
     }
}
</script>

webpack 1 需要特定的 loader 來轉換 ES 2015 import/export,webpack 2 起可以開箱即用。但是 ES6 的新語法還是需要 loader 來轉換,在沒有配置前,先不要用新語法。用了也沒報錯(比如 let,const等),那是因爲你的瀏覽器已經支持了 ES6 語法(新版本瀏覽器都已經支持)。

配置路由

將 vue-router 實例化傳入的參數new VueRouter(參數)提取到 router.js 形成路由配置文件。

import index from './views/index.vue'
export default {
     routes: [
         {
             path: '/index' ,
             component: index
         }
     ]
}

[email protected],不能用 require 來引入 .vue 文件,因爲 .vue 文件最終會被編譯成 ES6 module。

首頁

首頁引入 ouput 配置的 JS,添加 Vue 實例的掛載目標。

< div id = "app" ></ div >
< script src = "/static/build.js" ></ script >

入口JS完成路由配置、初始化 Vue 實例。

import Vue from 'vue' ;
import VueRouter from 'vue-router' ;
import App from './app.vue' ;
import routerConfig from './router' ;
Vue.use(VueRouter);
var router = new VueRouter(routerConfig)
new Vue({
     el: '#app' ,
     router: router,
     render: h => h(App)
});

從 Vue 2.2.0 後使用 require(‘vue’) 會報錯,應使用 ES6 module(import),具體原因請參考 Vue 更新說明 https://github.com/vuejs/vue/releases,截圖如下:

在首頁組件 app.vue 中添加路由鏈接、路由視圖組件。

< template >
     < div >
         < div >
             < router-link to = "/index" >Home</ router-link >
         </ div >
         < div >
             < router-view ></ router-view >
         </ div >
     </ div >
</ template >

配置loader

配置 vue 文件對應的 loader。

module: {
     rules: [
         {
             test: /\.vue$/,
             use: [ "vue-loader" ]
         }
     ]
}

Webpack2 必須在 module.rules 下配置 loader。’-loader’不能省略,必須將 loader 名寫全。可以使用 Rule.use 或 Rule.loader 來配置 loader(Rule.loader 是 Rule.use: [ { loader } ] 的簡寫),建議用 use。

上面完成了新增頁面及訪問該頁面所需的配置,下面來測試下是否能正常訪問/index。執行npm run dev,瀏覽器顯示如圖界面。

支持CSS

安裝 css-loader 後即可在 vue 文件中使用

npm i css-loader -D

想要支持import / require引入CSS文件,則需要配置對應的 Rule。

{
     test: /\.css$/,
     use: [ "vue-style-loader" , "css-loader" ]
}
<script>
import "../style/style.css"
</script>

支持CSS預處理語言

以 stylus 爲例,安裝 stylus 及 stylus-loader。

npm install stylus stylus-loader -D

增加 .styl 文件對應的 loader 配置。

{
     test: /\.styl$/,
     use: [ "vue-style-loader" , "css-loader" , "stylus-loader" ]
}

使用示例:

<style lang= "stylus" >
     .stylus
         .red
             color red
</style>
<script>
     import "../css/stylus-example.styl"
</script>

node-sass 安裝慢的解決辦法

使用淘寶鏡像:npm set disturl https://npm.taobao.org/dist
也可以單獨設置node-sass鏡像:npm set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass

支持圖片及圖標字體

安裝圖片及圖標字體依賴的loader。

npm install url-loader file-loader -D

增加圖片及圖標字體的loader配置。

{
     test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
     use: [{
         loader: "url-loader" ,
         options: {
             limit: 10000,
             name: 'images/[name].[hash:7].[ext]'    // 將圖片都放入images文件夾下,[hash:7]防緩存
         }
     }]
},
{
     test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
     use: [{
         loader: "url-loader" ,
         options: {
             limit: 10000,
             name: 'fonts/[name].[hash:7].[ext]'    // 將字體放入fonts文件夾下
         }
     }]
}

構建

添加打包命令如下:

"build" : "webpack --progress --colors"

執行npm run build開始構建,完成後,可以看到工程目錄下多了dist目錄以及 dist/build.js。

使用 Webpack 插件

壓縮JS

在之前的文章提到過,打開未壓縮版的build.js,你會發現ES6的語法沒有被轉化爲ES5,因此需要安裝babel 套件來完成語法的轉化,否則壓縮的時候就會報錯。之前廣泛使用的轉碼規則爲 babel-preset-es2015,但 Babel 的官網上在9月宣佈 ES2015 / ES2016/ ES2017 等等 ES20xx 時代的 presets 通通被廢棄(deprecated),取而代之的是 babel-preset-env,並且承諾它將成爲「未來不會過時的(future-proof)」解決方案。

npm i babel-loader babel-core babel-preset-env -D

增加babel的配置文件.babelrc。

{
     "presets" : [
         [ "env" , { "modules" : false }]
     ],
     "comments" : false
}

將 modules 設置爲 false,即交由 Webpack 來處理模塊化,通過其 TreeShaking 特性將有效減少打包出來的 JS 文件大小,可以自行對比下前後打包出來的文件的大小,效果還是不錯的。
comments 即是否保留註釋。
接着配置 JS 文件的 loader。

{
     test: /\.js$/,
     use: "babel-loader" ,
     include: [path.resolve(__dirname, 'src' )]
}

注意:Webpack2建議儘量避免exclude,更傾向於使用include。
壓縮 JS 採用webpack.optimize.UglifyJsPlugin,配置如下:

new webpack.optimize.UglifyJsPlugin()

官網稱warnings默認爲false,你可能會遇到即使沒有配置warnings: true,控制檯仍顯示警告,看下面這段源碼就知道了。查看源碼

只有當options.compress !== false時 warnings 纔會被設置默認值 false,所以一旦配置了 compress 其它選項,那就需同時配置warnings: false。
warnings作用是當插件在壓縮過程中移除的無效代碼或定義是顯示警告信息(display warnings when dropping unreachable code or unused declarations etc.)。

提取CSS

使用extract-text-webpack-plugin插件提取CSS。更改 css 及 less 的 loader 配置如下。

// 安裝插件
npm i extract-text-webpack-plugin -D
// var ExtractTextPlugin = require("extract-text-webpack-plugin")
{
     test: /\.css$/,
     use: ExtractTextPlugin.extract({
         use: "css-loader"
     })
},
{
     test: /\.styl$/,
     use: ExtractTextPlugin.extract({
         use: [ "css-loader" , "stylus-loader" ]
     })
}

上述配置並不能提取 vue 文件中的 style,需要設置 vue-loader 參數纔可以。

{
     test: /\.vue$/,
     use: {
         loader: "vue-loader" ,
         options: {
             loaders: {
                 css: ExtractTextPlugin.extract({
                     use: 'css-loader'
                 }),
                 stylus: ExtractTextPlugin.extract({
                     use: [ "css-loader" , "stylus-loader" ]
                 })
             }
         }
     }
}

初始化插件,filename 可以指定 CSS 文件的目錄。

new ExtractTextPlugin({
     filename: "css/style.css"
})

PostCSS

安裝 postcss-loader 及 postcss 插件。

npm i postcss-loader cssnano -D

配置 loader 如下:

// css-loader配置改爲
use: [ 'css-loader' , "postcss-loader" ]
// stylus-loader配置改爲
use: [ "css-loader" , "postcss-loader" , "stylus-loader" ]

postcss-loader 要放在 css-loader 和 style-loader 之後,CSS 預處理語言 loader 之前(stylus-loader)。
新增 postcss.config.js 來配置postcss插件,這樣就不用給每個 postcss-loader 去配置。更多 postcss-loader 的配置方式請參考 postcss-load-config

module.exports = {
     plugins: [
         require( 'cssnano' )
     ]
}

cssnano 使用了一系列 postcss 插件,包含了常用的 autoprefixer 等,如何傳入 autoprefixer 的配置?

require( 'cssnano' )({
     autoprefixer: {
         add: true ,
         browsers: [ '> 5%' ]
     }
})

其中有一個插件 postcss-zindex 使用中發現有些問題。如果想禁用這個插件的話,配置如下:

require( 'cssnano' )({
     zindex: {
         disable: true
     }
})

附:postcss插件分類搜索網站:http://postcss.parts/

生成首頁

安裝 html-webpack-plugin 插件。

npm i html-webpack-plugin -D

初始化插件。

// var HtmlWebpackPlugin = require('html-webpack-plugin');
new HtmlWebpackPlugin({
     filename: 'index.html' ,
     template: 'index.tpl.html'
})

其它插件

Webpack3 新增的作用域提升。

new webpack.optimize.ModuleConcatenationPlugin()

指定生產環境,以便在壓縮時可以讓 UglifyJS 自動刪除代碼塊內的警告語句。

new webpack.DefinePlugin({
     'process.env.NODE_ENV' : JSON.stringify( 'production' )
})

因爲這個插件直接做的文本替換,給定的值必須包含字符串本身內的實際引號。通常,有兩種方式來達到這個效果,使用 ‘」production」‘, 或者使用 JSON.stringify(‘production’)。
你完全可以在自己的代碼中使用process.env.NODE_ENV來區分開發和生產,從而針對不同的環境做一些事情。不用擔心這部分代碼會被保留,最終會被 UglifyJS 刪除。例如:

if (process.env.NODE_ENV != "production" ) {
     // 開發環境
}
// webpack.DefinePlugin插件替換後,上述代碼會變成
if ( "production" != "production" ) {
     // 開發環境
}
// 輸出
if ( false ) {
     // 開發環境
}
// UglifyJS 會刪除這段無效代碼

使用上述插件後再次構建,會發現生成的JS相比原來的體積小了不少。
friendly-errors-webpack-plugin 是一個更友好顯示 webpack 錯誤信息的插件。插件 github 地址:https://github.com/geowarin/friendly-errors-webpack-plugin
一般在開發環境下使用。

var FriendlyErrorsWebpackPlugin = require( 'friendly-errors-webpack-plugin' );
 
var webpackConfig = {
   // ...
   plugins: [
     new FriendlyErrorsWebpackPlugin(),
   ],
   // ...
}

效果如下圖:

顯示構建進度插件:webpack.ProgressPlugin

{
   // ...
   plugins: [
     new webpack.ProgressPlugin(),
   ],
   // ...
}

效果如下圖:

美化 webpack 編譯控制檯打印的信息的插件webpack-dashboard

分離Webpack配置

將開發和生產配置文件分離,方便增加各個環境下的個性配置。Webpack2文檔中也詳細闡述瞭如何爲多環境配置webpack。基本思路如下:

  • 編寫一個基本配置文件(webpack.base.config.js)
  • 使用webpack-merge合併這個基礎配置和針對環境的特定的配置(webpack.dev.config.js,webpack.prod.config.js)

webpack.base.config.js 內容如下:

var webpack = require( 'webpack' );
var path = require( 'path' );
var utils = require( './utils' );
function resolve(relPath) {
     return path.resolve(__dirname, relPath);
}
module.exports = {
     entry: { app: resolve( '../src/main.js' ) },
     output: {
         filename: 'js/[name].js'
     },
     module: {
         rules: [{
                 test: /\.js$/,
                 use: "babel-loader" ,
                 include: [resolve( '../src' )]
             },
             {
                 test: /\.vue$/,
                 use: {
                     loader: "vue-loader" ,
                     options: utils.vueLoaderOptions()
                 }
             },
             {
                 test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                 use: {
                     loader: "url-loader" ,
                     options: {
                         limit: 10000,
                         name: 'images/[name].[hash:7].[ext]'
                     }
                 }
             },
             {
                 test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                 use: [{
                     loader: "url-loader" ,
                     options: {
                         limit: 10000,
                         name: 'fonts/[name].[hash:7].[ext]'
                     }
                 }]
             }
         ]
     }
}

爲什麼要將vue-loader的options提取出來?其主要是用來配置CSS及CSS預處理語言的loader,開發環境可以不用配置,但是生產環境需要提取CSS、增加postcss-loader等,因此需要提取出來針對不同環境返回相應的options。後面會列出utils.vueLoaderOptions的內容。
爲什麼沒有配置CSS的loader?理由和上面的vue-loader一樣。
爲什麼沒有配置path和publicPath?一方面是個性化參數,另外開發和生產可能不相同,因此在webpack.dev.config和webpack.prod.config中分別配置更爲合適。
webpack.dev.config.js 內容如下:

var webpack = require( 'webpack' );
var merge = require( 'webpack-merge' );
var HtmlWebpackPlugin = require( 'html-webpack-plugin' );
var baseWebpackConfig = require( './webpack.base.config' );
var utils = require( './utils' );
var config = require( './config' );
Object.keys(baseWebpackConfig.entry).forEach( function (name) {
     baseWebpackConfig.entry[name] = [
         `webpack-dev-server/client?http: //localhost:${config.dev.port}/`,
         "webpack/hot/dev-server"
     ].concat(baseWebpackConfig.entry[name])
});
module.exports = merge(baseWebpackConfig, {
     output: {
         path: config.dev.outputPath,
         publicPath: config.dev.outputPublicPath
     },
     module: {
         rules: utils.styleLoaders()
     },
     plugins: [
         new webpack.HotModuleReplacementPlugin(),
         new HtmlWebpackPlugin({
             filename: 'index.html' ,
             template: 'index.html' ,
             inject: true
         })
     ]
})

添加了HtmlWebpackPlugin後,index.html中就不需要在自己去引用打包的JS了,會自動根據打包的JS添加引用,這樣更加方便,關於HtmlWebpackPlugin的配置,需要說明兩點:
1.template的路徑是相對於webpack編譯時的上下文目錄,說白了就是項目根目錄,因此上面可以直接配置index.html,其指向的就是根目錄下的index.html;
2.filename則是相對於webpack配置項output.path(打包資源存儲路徑)。
html-webpack-plugin關於template和filename路徑源碼如下:

// template
this .options.template = this .getFullTemplatePath( this .options.template, compiler.context);
// filename
this .options.filename = path.relative(compiler.options.output.path, filename);

config.js內容如下:

module.exports = {
     dev: {
         outputPath: path.resolve(__dirname, '../static' ),
         outputPublicPath: '/' ,
         port: 8000
     },
     prod: {
         outputPath: path.resolve(__dirname, '../static' ),
         outputPublicPath: '/static/'
     }
}

utils.js內容如下:

var ExtractTextPlugin = require( 'extract-text-webpack-plugin' );
var isProd = process.env.NODE_ENV === "production" ;
// 根據項目需求添加CSS預處理語言並安裝相應的loader,以stylus-loader爲例
var cssLang = [{
     name: 'css' ,
     reg: /\.css$/,
     loader: 'css-loader'
}, {
     name: 'stylus' ,
     reg: /\.styl$/,
     loader: "stylus-loader"
}];
function genLoaders(lang) {
     var loaders = [ 'css-loader' , 'postcss-loader' ];
     if (lang.name !== 'css' ) {
         loaders.push(lang.loader);
     }
     if (isProd) {
         // 生產環境需要提取CSS
         loaders = ExtractTextPlugin.extract({
             use: loaders
         });
     } else {
         // 開發環境需要vue-style-loader將CSS提取到頁面頭部
         loaders.unshift( 'vue-style-loader' );
     }
     return loaders;
}
// 各種CSS的loader
exports.styleLoaders = function () {
     var output = [];
     cssLang.forEach(lang => {
         output.push({
             test: lang.reg,
             use: genLoaders(lang)
         })
     })
     return output;
};
// vue-loader的options
exports.vueLoaderOptions = function () {
     var options = {
         loaders: {}
     };
     cssLang.forEach(lang => {
         options.loaders[lang.name] = genLoaders(lang);
     });
     return options;
}

接下來就是如何啓動webpack-dev-server,vue-cli的webpack模板工程用的express及webpack中間件做開發服務器,其實用webpack-dev-server就能滿足需求,當然用express能夠做更多的事情,畢竟webpack-dev-server是一個輕量級的express。dev.js內容如下:

var webpack = require( 'webpack' );
var webpackDevServer = require( 'webpack-dev-server' );
var devConfig = require( "./webpack.dev.config" );
var config = require( "./config" );
var compiler = webpack(devConfig);
var server = new webpackDevServer(compiler, {
     hot: true ,
     noInfo: true ,
     publicPath: config.dev.outputPublicPath,
     stats: { colors: true }
});
server.listen(config.dev.port, "0.0.0.0" );
var url = `http: //localhost:${config.dev.port}/`;
// 需先安裝 opn 模塊 npm i opn -D
var opn = require( 'opn' );
// 打包完畢後啓動瀏覽器
server.middleware.waitUntilValid( function () {
     console.log(`> Listening at ${url}`);
     opn(`${url}`);
})

生產配置文件(webpack.prod.config.js)內容如下:

// 設定爲生產環境
process.env.NODE_ENV = 'production' ;
var webpack = require( 'webpack' );
var merge = require( 'webpack-merge' );
var HtmlWebpackPlugin = require( 'html-webpack-plugin' );
var ExtractTextPlugin = require( 'extract-text-webpack-plugin' );
var baseWebpackConfig = require( './webpack.base.config' );
var utils = require( './utils' );
var config = require( './config' );
 
module.exports = merge(baseWebpackConfig, {
     output: {
         path: config.prod.outputPath,
         publicPath: config.prod.outputPublicPath
     },
     module: {
         rules: utils.styleLoaders()
     },
     plugins: [
         new webpack.DefinePlugin({
             'process.env.NODE_ENV' : '"production"'
         }),
         new webpack.optimize.UglifyJsPlugin(),
         new ExtractTextPlugin({
             filename: "css/style.css?[contenthash:8]"
         }),
         new HtmlWebpackPlugin({
             filename: 'index.html' ,
             template: 'index.html' ,
             inject: true
         })
     ]
相關文章
相關標籤/搜索