以新手視角,詳細介紹各個步驟內容,不深刻講步驟涉及的原理,主要介紹如何操做。css
新建工程目錄 vue2practice,在目錄下執行npm init -y
來建立一個 package.json,在 package.json 中先添加如下必備模塊:html
{ "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 的版本號是相同的,讓用戶本身指定版本才能保證這一點。查看做者的回答(英文) 。若是二者版本不一致,運行時會出現下圖所示的錯誤提示。vue
新建目錄結構以下,新增的目錄及文件先空着,後面的步驟會說明添加什麼內容。node
vue2pratice |-- package.json |-- index.html // 啓動頁面 |-- webpack.config.js // webpack配置文件 |-- src |-- views // vue頁面組件目錄 |-- main.js // 入口文件 |-- router.js // vue-router配置 |-- app.vue // 工程首頁組件
Webpack 默認讀取 webpack.config.js,文件名不能隨便改,其中 entry 是必須配置的。webpack
module.exports = { entry: './src/main.js', output: { path: __dirname + '/dist', publicPath: '/static/', filename: 'build.js' } }
Webpack 2+ 要求
output.path
必須爲絕對路徑。關於 Webpack 的各類路徑在《詳解Webpack2的那些路徑》有詳細的介紹git
配置 webpack-dev-server,只需在 package.json 添加如下啓動命令便可。github
"scripts": { "dev": "webpack-dev-server --hot --open" }
webpack-dev-server 2 默認爲 inline 模式,熱模塊替換仍需本身設置。web
在 index.html 中添加測試代碼,引入打包後的 JS 文件。vue-router
<body> Hello, Webpack 3. <br> <script src="/static/build.js"></script> </body>
在 main.js 中添加測試代碼。vue-cli
// main.js document.write('來自main.js的問候!')
執行下面的命令來安裝模塊並啓動服務器。
// 安裝依賴 npm install // 運行 npm run dev
啓動後瀏覽器會自動打開http://localhost:8080
,若是控制檯沒有報錯,頁面正確顯示 main.js 和 index.html 的內容,改動 main.js 後瀏覽器不刷新能看到效果,則表示配置沒問題。
在 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 } ] }
從 vue-loader@13.0.0,不能用 require 來引入 .vue 文件,由於 .vue 文件最終會被編譯成 ES6 module。
首頁引入 ouput 配置的 JS,添加 Vue 實例的掛載目標。
<body> <div id="app"></div> <script src="/static/build.js"></script> </body>
入口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>
配置 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-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>
以 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>
使用淘寶鏡像: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。
在以前的文章提到過,打開未壓縮版的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.)。
使用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-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
將開發和生產配置文件分離,方便增長各個環境下的個性配置。Webpack2文檔中也詳細闡述瞭如何爲多環境配置webpack。基本思路以下:
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的配置,須要說明兩點:
html-webpack-plugin關於template和filename路徑源碼以下:
// https://github.com/jantimon/html-webpack-plugin/blob/master/index.js // 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 }) ] })
生產構建(prod.js)內容以下:
var webpack = require("webpack"); var webpackProdConfig = require('./webpack.prod.config'); webpack(webpackProdConfig, function(err, stats) { process.stdout.write(stats.toString()); });
對應在package.json中添加開發和生產構建的命令以下:
"scripts": { "dev": "node build/dev.js", "build": "node build/prod.js" }
參照vue-cli及webpack文檔的提取公共代碼的方式,利用插件webpack.optimize.CommonsChunkPlugin未來自node_modules下的模塊提取到vendor.js(通常來說都是外部庫,短期不會發生變化)。因而有以下代碼:
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module, count) { return module.resource && /\.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0 } })
若是你查看未壓縮版的vendor.js,會發現不只包含vue的代碼,還包含了webpackBootstrap(webpack模塊加載器)的代碼,按理說放在vendor裏面也沒啥問題,畢竟後面的模塊都須要依賴於此,可是若是你的chunk使用了hash,一旦app代碼發生了改變,相應的hash值會發生變化,webpackBootstrap的代碼也會發生變化(如圖),而咱們提取vendor的初心就是這部分代碼改變頻率不大,顯然這不符合咱們的目標,那麼應當將webpackBootstrap再提取到一個文件中,代碼以下:
new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] })
vue 2.2.0 後不能直接使用 require 來加載 vue,那麼到底改是該使用 ES6 Module 仍是 CommonJS 呢?,先看一個對比圖:
所有使用 ES6 Module,即import、export default,最終打包的app.js爲1.20KB。
所有使用 CommonJS(除了vue),即require、module.exports,最終打包的app.js爲1.12KB。
雖然二者大小相差不大,可是這不由讓人以爲用 CommonJS 彷佛是一條優化措施,那麼代碼層面究竟是怎麼回事呢?
爲了一探究竟,註釋了壓縮插件,以about.vue最終構建的代碼來看,使用 CommonJS 的代碼以下:
(function (module, exports, __webpack_require__) { "use strict"; module.exports = { data: function data() { return { page: 'about' }; } }; }),
這基本上和 about.vue 中的代碼相差無幾,而使用 ES6 Module 構建的代碼以下:
(function (module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = { data: function data() { return { page: 'about' }; } }; }),
對比兩種規範下構建後的代碼,使用 ES6 Module 主要是多了 Object.defineProperty 那一部分,從而致使了最終打包的文件大一點兒。那是否是爲了最終文件體積能小些就所有使用 CommonJS 呢?這個須要你充分理解 CommonJS 和 ES6 Module 的區別,引用《ECMAScript 6入門》這本書的解釋以下:
ES6模塊加載的機制,與CommonJS模塊徹底不一樣。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。
爲了更清楚的理解這段話,咱們稍微作一下測試,在 src 下增長一個test.js,內容以下(分別將兩種寫法列出):
// CommonJS var counter = 1; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; // ES6 Module export let counter = 1; export function incCounter() { counter++; }
在about.vue中測試以下:
// CommonJS var test = require('../test'); export default { created() { console.log(test.counter); // 1 test.incCounter(); console.log(test.counter); // 1 } } // ES6 Module import { counter, incCounter } from '../test'; export default { created() { console.log(counter); // 1 incCounter(); console.log(counter); // 2 } }
最終的輸出值也印證了前面的解釋,更詳細的解讀請查閱相關資料或書籍。只有深刻理解了二者的區別,天然就能明白什麼時候該使用何種規範。固然 ES6 做爲將來的趨勢,咱們應該去作更多的嘗試。
以前用懶加載的方式是:require.ensure,在 webpack2 中引入了 Code Splitting-Async 的新方法 import(),用於動態引入 ES Module。require.ensure 能夠指定 chunkFilename,可是 import 沒有很好的途徑去指定,webpack3 爲了解決這個問題,提出了用魔法註釋的方式來指定模塊名。
結合 vue-router 能夠輕鬆實現懶加載,配置路由指向異步組件便可實現懶加載,好比:
{ path: '/async', component: () => import(/* webpackChunkName: "async" */'./views/async.vue') }
若是你發現使用 import() 運行時報錯,有幾種狀況:
npm i babel-preset-stage-2 -D
),或者 babel 插件(npm i babel-plugin-syntax-dynamic-import -D
);npm i babel-plugin-syntax-dynamic-import -D
)解決。
魔法註釋雖然指定了塊名,可是 webpack 默認的塊名配置爲 [id].js,即用的模塊的 id 做爲塊名,所以須要咱們手動改下配置。
修改 webpack.base.config.js 的 output:
output: { filename: 'js/[name].js', chunkFilename: "js/[name].[chunkhash].js" }
效果以下圖:
若是發現魔法註釋沒有生效,要檢查下 .babelrc 的配置項 comments 是否設爲 true。或者不配置 comments(默認爲true)
extract-text-webpack-plugin 默認不會提取異步模塊中的 CSS,須要加上配置:
new ExtractTextPlugin({ allChunks:true, filename: "css/style.css?[contenthash:8]" })
http://www.qinshenxue.com/article/20161118151423.html