那些花兒,從零構建Vue工程(webpack4)
從零搭建Vue開發環境:webpack4 + vue-loader + koa2 + babel-loader v8 + Babel v7 + eslint + git hooks + editorconfig
從2009到2019社會在不斷進步 技術也在不斷進步 我們當然也不能落後

“不積跬步無以至千里,不積小流無以成江海”
先開始webpack基本構建
建立一個工程目錄 vue-structure
mkdir vue-structure && cd vue-structure 複製程式碼
安裝webpack
npm i webpack webpack-cli -D 複製程式碼
建立build目錄
mkdir build 複製程式碼
在build目錄裡, 建立webpack.config.js
cd build && touch webpack.config.js 複製程式碼
建立入口檔案 src/main.js
mkdir src cd src && touch main.js 複製程式碼
main.js
alert('hello world!') 複製程式碼
配置npm scripts
// package.json "scripts": { "build": "webpack --config build/webpack.config.js --progress --mode production" } 複製程式碼
配置devServer
npm i webpack-dev-server -D 複製程式碼
配置npm scripts
"scripts": { ... "dev": "webpack-dev-server --config build/webpack.config.js --progress --mode development" } 複製程式碼
html 外掛
npm i html-webpack-plugin -D 複製程式碼
webpack配置
// build/webpack.config.js const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const resolve = dir => path.join(__dirname, '..', dir) module.exports = { entry: resolve('src/main.js'), output: { filename: '[name].[hash:5].js', path: resolve('dist') }, devServer: { host: '0.0.0.0', port: 7000, open: true }, plugins: [ new HtmlWebpackPlugin({ template: resolve('index.html') }) ] } 複製程式碼
執行webpack dev server
npm run dev 複製程式碼
瀏覽器自動開啟http://0.0.0.0:7000/

到這裡webpack開發服務基本跑通了
配置babel v7
webpack 4.x | babel-loader 8.x | babel 7.x
npm i -D babel-loader @babel/core @babel/preset-env 複製程式碼
babel plugin 支援動態import()
npm i @babel/plugin-syntax-dynamic-import -D 複製程式碼
配置webpack.config.js
module: { rules: [ { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ } ] } 複製程式碼
建立.babelrc檔案
{ "plugins": [ "@babel/plugin-syntax-dynamic-import" ], "presets": [ [ "@babel/preset-env", { "modules": false } ] ] } 複製程式碼
測試下ES6程式碼
test.js
// src/test.js export default () => alert('hello vue!') 複製程式碼
index.html
// src/index.html <body> <div id="app">請說say</div> </body> 複製程式碼
main.js
// src/main.js document.querySelector('#app').addEventListener('click', () => { import('./test.js').then(res => { res.default() }) }) 複製程式碼
執行下dev
npm run dev 複製程式碼
點選頁面div

ok 沒問題
配置Vue Loader
Vue Loader 是什麼?
Vue Loader 是一個 webpack 的 loader,它允許你以一種以單檔案元件(*.vue檔案) 的格式撰寫 Vue 元件:
建立App.vue根元件
<template> <div class="example">{{ msg }}</div> </template> <script> export default { data () { return { msg: 'Hello Vue!' } } } </script> <style> .example { color: red; } </style> 複製程式碼
安裝Vue
npm i vue 複製程式碼
src/main.js
import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App) }).$mount('#app') 複製程式碼
修改index.html
<body> <div id="app"></div> </body> 複製程式碼
執行dev
npm run dev 複製程式碼
結果報錯了 webpack預設只能識別JavaScript檔案,不能解析.vue檔案(vue單檔案元件 是Vue獨有的),於是作者提供了vue-loader。

Vue單檔案元件
配置vue-loader
npm i vue-loader vue-template-compiler 複製程式碼
vue-template-compiler (peer dependency) 是vue-loader的同版本依賴
webpack.config.js
// webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { module: { rules: [ // ... 其它規則 { test: /\.vue$/, loader: 'vue-loader' } ] }, plugins: [ // 請確保引入這個外掛! new VueLoaderPlugin() ] } 複製程式碼
vue單檔案元件中css 也需要css-loader解析
npm i css-loader -D 複製程式碼
配置webpack
// webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { mode: 'development', module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, // 它會應用到普通的 `.js` 檔案 // 以及 `.vue` 檔案中的 `<script>` 塊 { test: /\.js$/, loader: 'babel-loader' }, // 它會應用到普通的 `.css` 檔案 // 以及 `.vue` 檔案中的 `<style>` 塊 { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] } ] }, plugins: [ // 請確保引入這個外掛來施展魔法 new VueLoaderPlugin() ] } 複製程式碼
此時執行npm run dev OK了,App.vue被成功掛載到頁面

css前處理器配置
npm i stylus stylus-loader 複製程式碼
webpack.config.js
module: { rules: [ { test: /\.styl(us)?$/, use: [ 'vue-style-loader', 'css-loader', 'stylus-loader' ] } ] } 複製程式碼
vue元件中使用
<style lang='stylus' scoped> .example .title color: red </style> 複製程式碼
postcss配置
postcss 提供了一個解析器,它能夠將 CSS 解析成抽象語法樹(AST)。
npm i -D postcss-loader 複製程式碼
autoprefixer(外掛) 它可以解析CSS檔案並且新增瀏覽器字首到CSS內容裡
npm i -D autoprefixer 複製程式碼
建立postcss.config.js
module.exports = { plugins: [ require('autoprefixer') ] } 複製程式碼
配置webpack
// webpack.config.js module: { rules: [ ... { test: /\.css$/, use: [ devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true } }, { loader: 'postcss-loader', options: { sourceMap: true } } ] }, { test: /\.styl(us)?$/, use: [ devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true } }, { loader: 'postcss-loader', options: { sourceMap: true } }, { loader: 'stylus-loader', options: { sourceMap: true } } ] } ] } 複製程式碼
給App.vue新增css3樣式

此時 npm run dev 可以看到 自動添加了瀏覽器字首

圖片資源載入配置
url-loader
將圖片資源轉換成base64 URI
// webpack.config.js module: { rules: [ { test: /\.(png|svg|jpe?g)$/, loader: 'url-loader', options: { limit: 8192 } } ] } 複製程式碼

轉換後圖片地址變為了base64 URI

file-loader
載入圖示字型
module: { rules: [ { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] } 複製程式碼
cross-env
設定命令執行時的環境變數 以便在webpack配置檔案中區分環境
npm i cross-env -D 複製程式碼
package.json
"scripts": { "build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js --progress --mode production", "dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js --progress --mode development" }, 複製程式碼
css提取
webpack4
npm i mini-css-extract-plugin -D 複製程式碼
webpack.config.js
// 區分當前環境是 development 還是 production const devMode = process.env.NODE_ENV === 'development' module: { rules: [ { test: /\.css$/, use: [ devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader, 'css-loader' ] }, { test: /\.styl(us)?$/, use: [ devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader' ] } ] }, plugins: [ ... new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css' }) ] 複製程式碼
開發環境下一般不開啟css提取,要不然每次程式碼改動重新編譯速度會慢。通常在生成環境下打包時 才開啟css提取。
此時npm run build, css被單獨打包成一個css檔案

清理dist目錄
生產環境下 為什麼要清理dist目錄 因為我們的輸出檔案為了快取再檔名拼接上hash值,只要檔案有改動就會產生新的hash值,dist目錄下每次都會新增一份輸出檔案 但我們只要編譯後的最終的那個就可以了
npm run build 三次 dist目錄如下
dist ├── app.bundle.0e380cea371d050137cd.js ├── app.bundle.259c34c1603489ef3572.js ├── app.bundle.e56abf8d6e5742c78c4b.js ├── index.html └── style.css 複製程式碼
module.exports = { output: { filename: '[name].[hash:6].js', path: resolve('dist') }, } 複製程式碼
利用webpack外掛清理
clean-webpack-plugin
npm i clean-webpack-plugin -D 複製程式碼
webpack配置
// build/webpack.config.js const CleanWebpackPlugin = require('clean-webpack-plugin') module.exports = { plugins: [ new CleanWebpackPlugin(['dist'], { root: path.join(__dirname, '../') }) ] } 複製程式碼
然後執行npm run build 每次打包前就會把之前的dist目錄清理下
第二種方式 用rimraf命令 清理dist目錄
rimraf
The UNIX command rm -rf for node.
npm i rimraf -D 複製程式碼
修改package.json
"scripts": { "clean": "rimraf dist", "build": "npm run clean && cross-env NODE_ENV=production webpack --config build/webpack.config.js --progress --mode production", } 複製程式碼
npm run build 也是ok的
.editorconfig
在不同的編輯器和IDEs中為多個從事同一專案的開發人員保持一致的編碼風格。
root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true 複製程式碼
程式碼校驗 (Linting)
安裝eslint
npm i eslint eslint-plugin-vue -D 複製程式碼
eslint各種安裝
npm i -D babel-eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node 複製程式碼
建立.eslintrc檔案
{ root: true, env: { node: true }, parserOptions: { parser: "babel-eslint", sourceMap: "module" }, extends: [ "plugin:vue/essential", "standard" ], rules: {} } 複製程式碼
webpack中配置 eslint
通過webpack實時編譯,進行程式碼效驗
npm i eslint-loader -D 複製程式碼
webpack.config.js
module: { rules: [ { { enforce: 'pre', test: /\.(js|vue)$/, loader: 'eslint-loader', exclude: /node_modules/ }, ...... } ] } 複製程式碼
在src/main.js中定義一個未使用變數
let title = 'eslint' 複製程式碼
執行 npm run dev

eslint基本配置完了 但是我想在控制檯報錯資訊更友好些
eslint-friendly-formatter
一個簡單的eslint格式設定工具/報告器,它使用高階文字和iterm2“點選開啟檔案”功能友好
安裝
npm i -D eslint-friendly-formatter 複製程式碼
修改webpack配置
// build/webpack.config.js module: { rules: [ { { enforce: 'pre', test: /\.(js|vue)$/, loader: 'eslint-loader', exclude: /node_modules/, options: { formatter: require('eslint-friendly-formatter') } }, ...... } ] } 複製程式碼
再次 npm run dev

此時命令列中報錯資訊更加友好 顯示rules詳細規則
devServer.overlay
把編譯錯誤,直接顯示到瀏覽器頁面上。
webpack.config.js
module.exports = { devServer: { overlay: { errors: true, warnings: true } } } 複製程式碼
再次npm run dev 這樣就可以直接在頁面中看到錯誤資訊了

package.json中配置eslint
"scripts": { "lint": "eslint --ext .js,.vue src" }, 複製程式碼
通過npm 單獨進行效驗程式碼也可以
npm run lint 複製程式碼
eslint自動修復
Eslint檢測出的問題如何自動修復
"scripts": { "lint": "eslint --ext .js,.vue src", "lint:fix": "eslint --fix --ext .js,.vue src" }, 複製程式碼
npm run lint:fix 複製程式碼
會把你程式碼中一些常規錯誤 進行自動修復
Git Hooks
Git 能在特定的重要動作發生時觸發自定義指令碼。 類似於框架中的生命週期
husky
npm install husky --save-dev 複製程式碼
建立.huskyrc
// .huskyrc { "hooks": { "pre-commit": "npm run lint" } } 複製程式碼
package.json
"scripts": { "lint": "eslint --ext .js,.vue src" }, 複製程式碼
當每次git commit時 自動執行npm run lint效驗

webpack程式碼分離
程式碼分離 屬於效能優化 (業務程式碼 第三方程式碼 webpack執行時生成程式碼...)
webpack4中直接配置就可以
// build/webpack.config.js module.exports = { optimization: { splitChunks: { // 預設將node_modules中依賴打包到venders.js chunks: 'all' }, // 將webpack執行時生成程式碼打包到runtime.js runtimeChunk: true }, } 複製程式碼
此時 npm run build 會看到vendors.js 和 runtime.js

webpack4中 optimization選項更多配置請看官方文件
目前完整配置檔案
package.json
"scripts": { "clean": "rimraf dist", "build": "npm run clean && cross-env NODE_ENV=production webpack --config build/webpack.config.js --progress --mode production", "dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js --progress --mode development", "lint": "eslint --ext .js,.vue src", "lint:fix": "eslint --fix --ext .js,.vue src" }, 複製程式碼
build/webpack.config.js
const path = require('path') const VueLoaderPlugin = require('vue-loader/lib/plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CleanWebpackPlugin = require('clean-webpack-plugin') const resolve = dir => path.join(__dirname, '..', dir) const devMode = process.env.NODE_ENV === 'development' module.exports = { entry: resolve('src/main.js'), output: { filename: '[name].[hash:6].js', path: resolve('dist') }, module: { rules: [ { enforce: 'pre', test: /\.(js|vue)$/, loader: 'eslint-loader', exclude: /node_modules/, options: { formatter: require('eslint-friendly-formatter') } }, { test: /\.vue$/, use: 'vue-loader', exclude: /node_modules/ }, { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ }, { test: /\.css$/, use: [ devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true } }, { loader: 'postcss-loader', options: { sourceMap: true } } ] }, { test: /\.styl(us)?$/, use: [ devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true } }, { loader: 'postcss-loader', options: { sourceMap: true } }, { loader: 'stylus-loader', options: { sourceMap: true } } ] }, { test: /\.(png|svg|jpe?g)$/, loader: 'url-loader', options: { limit: 8192 } }, { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] }, optimization: { splitChunks: { // 預設將node_modules中依賴打包到venders.js chunks: 'all' }, // 將webpack執行時程式碼打包到runtime.js runtimeChunk: true }, devServer: { host: '0.0.0.0', port: 7000, open: true, overlay: { warnings: true, errors: true } }, plugins: [ new VueLoaderPlugin(), new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css' }), new HtmlWebpackPlugin({ template: resolve('index.html') }) // new CleanWebpackPlugin(['dist'], { //root: path.join(__dirname, '../') // }) ] } 複製程式碼