介紹
前端的世界變化之快速,從 2010 開始小弟經(jīng)歷了 jQuery, Backbone, Angular, 到 React。這一路走來(lái)雖然學(xué)習(xí)到了許多高明開發(fā)者融合於框架或函式庫(kù)中的智慧,卻也因?yàn)椴粩嗫焖僮兓械狡v。時(shí)至 2016 小弟認(rèn)為在實(shí)務(wù)與理想之間取得一個(gè)完美平衡的前端框架大概就屬 vue.js了。
當(dāng)然這前端世界裡並沒有萬(wàn)能藥可以完美的處理所有問(wèn)題,不過(guò) vue.js
的精美,不只容易與傳統(tǒng) MVC 框架(Rails, ASP.NET MVC)等結(jié)合,當(dāng)要使用最新的設(shè)計(jì)模式如 Flux, redux 等也都是沒問(wèn)題的,再加上易學(xué)與一些你肯定能感受到作者從實(shí)戰(zhàn)淬煉出來(lái)的特性。因此在 2016 我也決定轉(zhuǎn)戰(zhàn) vue.js。
隨著 Javascript 社群快速的演進(jìn),很可怕一個(gè)問(wèn)題是 - 專案的環(huán)境設(shè)定,關(guān)於那些 tooling
這不只是 React 的問(wèn)題,當(dāng)你想使用 ES2015 的新語(yǔ)法,方便的持續(xù)整合與測(cè)試,匯入?yún)R出模組時(shí),我們就需要設(shè)定這些專案工具。
雖然 vue 本身有提供指令介面 vue-cli 讓我們快速建立專案,但對(duì)這些相關(guān)技術(shù)和設(shè)定有些瞭解肯定能幫助你執(zhí)行更多客製的行為。
從頭自己一點(diǎn)一點(diǎn)設(shè)定有一些好處:
- 每個(gè)專案都有不同的需求,您可以根據(jù)自身的需求來(lái)設(shè)定
- 我們也提到 Javascript(nodejs) 的世界變得很快,如果有局部的套件壞了那我們也比較清楚該怎麼處理
- 直接使用別人的 start-kit 也許會(huì)多裝了一堆你不需要的東西
這篇文章將會(huì)透過(guò)實(shí)作介紹最基本的概念,使用 webpack 設(shè)定一個(gè)基本的 vue 專案
Part 1 基本目錄架構(gòu)
建立專案與 package.json
$ mkdir [project_name]
$ cd [project_name]
$ npm init -y
$ npm install vue -S
我們先把需要的程式與目錄結(jié)構(gòu)準(zhǔn)備好,需求是使用 Vue + ES2015來(lái)開發(fā)。第一步在根目錄建立一個(gè) index.html。下面是一個(gè)簡(jiǎn)單的 vue 範(fàn)例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue.js v2</title>
</head>
<body>
<div id="app">{{ message }}</div>
<script src="dist/bundle.js"></script>
</body>
</html>
注意到兩件事
- 我們使用 dist/build.js 這個(gè)檔案在編譯之前是不存在的
- {{message}} 這個(gè)語(yǔ)法是 vue.js 處理的
建立 src目錄與 src/main.js檔案,這邊您可以隨您自己的偏好組織專案架構(gòu)
import Vue from 'vue'
new Vue({
el: '#app',
data: {
message: "Hello Vue"
}
})
在這一步我們已經(jīng)完成一個(gè)簡(jiǎn)單的 Vue 專案,但是關(guān)於建置編譯的設(shè)定我們還未完成。
Do not mount Vue to <html> or <body> - mount to normal elements instead.
關(guān)於 v2 之後值得注意的地方,便是我們不能直接將元件掛載到 html 或 body 上。
Part 2 webapck 建置設(shè)定
1.安裝 webpack, webpack-dev-server 與相關(guān) loaders
為了專注在基本的說(shuō)明,本次更新已將一些不屬於基本功能的模組移除。
# 2016-10-04 更新
$ npm i webpack webpack-dev-server webpack-merge css-loader style-loader file-loader url-loader babel-loader babel-core babel-plugin-transform-runtime babel-preset-es2015 vue-loader vue-hot-reload-api -D
"dependencies": {
"vue": "^2.0.1"
},
"devDependencies": {
"babel-core": "^6.17.0", // babel 核心程式
"babel-loader": "^6.2.5", // webpack 使用的 babel 編譯器
"babel-plugin-transform-runtime": "^6.15.0", // 預(yù)設(shè) babel 會(huì)在每一隻編譯檔案注入 polyfill 的程式碼,為了避免重複而將這部分抽出去。詳細(xì)說(shuō)明:http://babeljs.io/docs/plugins/transform-runtime/
"babel-preset-es2015": "^6.16.0", // 支援 ES2015 語(yǔ)法
"css-loader": "^0.25.0", // webpack 使用於處理 css
"file-loader": "^0.9.0", // webpack 使用於處理檔案
"style-loader": "^0.13.1", // webpack 將 css 整合進(jìn)元件中
"url-loader": "^0.5.7", // 編譯匯入檔案類型的資源,把檔案轉(zhuǎn)成 base64
"vue-hot-reload-api": "^2.0.6", // 支援 Hot Reload
"vue-loader": "^9.5.1", // 使用 Vue Component Spec
"webpack": "^1.13.2",
"webpack-dev-server": "^1.16.1", // webpack 開發(fā)伺服器
"webpack-merge": "^0.14.1" // 合併 webpack 設(shè)定參數(shù)
}
2. 裝完 loaders 後,撰寫設(shè)定 webpack.config.js
根目錄下建立與撰寫 webpack.config.js
var path = require('path')
var config = {
entry: path.join(__dirname, 'src', 'main'),
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/dist/'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['', '.js', '.vue'],
/**
* Vue v2.x 之後 NPM Package 預(yù)設(shè)只會(huì)匯出 runtime-only 版本,若要使用 standalone 功能則需下列設(shè)定
*/
alias: {
vue: 'vue/dist/vue.js'
}
}
}
module.exports = config
Failed to mount component: template or render function not defined. (found in root instance)
若您看到上面錯(cuò)誤訊息,這是由於 Vue 2 之後分成 standalone 完整版與 runtime-only 版。差異在於完整版包含了編譯器,支援 template 以及使用了瀏覽器的 API。而 NPM 模組預(yù)設(shè)只會(huì)匯出 runtime-only ,若要加入 compiler 和 template 支援則需增加 webpack 的設(shè)定。
3. 設(shè)定 babel 的部分
根目錄建立 .babelrc 簡(jiǎn)化 webpack.config.js ,這是因?yàn)?babel 6之後把功能拆散了,要用就要裝。同時(shí)也可以用 .babelrc 來(lái)設(shè)定,如果不使用這個(gè)檔案我們就需要在 webapck.config.js設(shè)定。
- .babelrc
{
"presets": ["es2015"],
"plugins": ["transform-runtime"]
}
另外 package.json 和環(huán)境變數(shù)也能夠設(shè)定,不過(guò)為了單純起見我們選擇建立 .babelrc 。當(dāng)然您也可以選擇設(shè)定在 package.json 中。
- package.json
{
"name": "YOUR PROJECT NAME",
...,
"babel": {
"presets": [
"es2015"
],
"plugins": [
"transform-runtime"
]
}
}
上面我們已經(jīng)完成基本的設(shè)定,雖然我們一口氣安裝了很多 loaders 但相關(guān)設(shè)定我們只先設(shè)定了 babel 的部份。到了這一步我們的專案架構(gòu)已經(jīng)可以被編譯執(zhí)行了。
# 如果在這一步您想先執(zhí)行編譯看看可以安裝全域的 webpack
$ npm i webpack -g
$ webpack
$ open index.html # 編譯後檢視內(nèi)容
編譯之後點(diǎn)擊 index.html 即可以運(yùn)行。眼尖的讀者可能會(huì)好奇,那我們剛剛有裝 vue-loader 那是在幹嘛的?
Part 3 使用 vue-loader 與 .vue
vue-loader 的用途是提供一種更方便的組織方式讓我們把元件即一個(gè) component 中需要的 js 行為, css 樣式, template 樣板放在一個(gè).vue的檔案中。
- 修改 view
首先讓我們先修改 index.html ,加入 <app>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue.js v2</title>
</head>
<body>
<div id="app">
<app></app>
</div>
<script src="dist/bundle.js"></script>
</body>
</html>
- 匯入元件
接著我們?cè)?main.js 把元件 app.vue 加入 components,在這邊我們是反向的推導(dǎo)回去,從想 怎麼使用接著反著建立程式檔案。
import Vue from 'vue'
import App from './app.vue'
new Vue({
el: '#app',
components: { App }
})
- 新增元件
最後我們新增一個(gè) app.vue 檔案
<template lang="html">
<div>
<div class="message">
{{ message }}
</div>
</div>
</template>
<script>
export default {
data () {
return {
message: 'Helo, Vue.js 2.0'
}
}
}
</script>
<style lang="css">
.message {
color: pink;
font-size: 1.4em;
}
</style>
- 更新 webpack.config.js
這個(gè)時(shí)候如果直接執(zhí)行 webpack 編譯會(huì)產(chǎn)生錯(cuò)誤,因?yàn)槲覀冞€沒設(shè)定 webpack.config.js 處理 .vue 檔案的部分
var path = require('path')
var config = {
entry: [
'webpack/hot/dev-server',
path.join(__dirname, 'src', 'main')
],
output: {
publicPath: '/dist/',
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
},
{
test: /\.vue$/,
loader: 'vue'
}
]
},
resolve: {
/**
* Vue v2.x 之後 NPM Package 預(yù)設(shè)只會(huì)匯出 runtime-only 版本
*/
alias: {
vue: 'vue/dist/vue.js'
},
extensions: ['', '.js', '.vue']
}
}
module.exports = config
再次執(zhí)行 webpack 編譯,我們的 app.vue 可以正常運(yùn)作了。
Part 4 HMR / Hot Reload
如果您有發(fā)現(xiàn)修改之後卻沒有改變的問(wèn)題,請(qǐng)注意關(guān)於路徑部分,取得的是編譯後的實(shí)體檔案還是 webpack-dev-server 使用記憶體中的內(nèi)容
Hot Module Replacement 或稱 Hot Reload 是 Javascript 世界中近期很熱門的新技術(shù),簡(jiǎn)單的說(shuō)就是當(dāng)你在開發(fā)時(shí),你一存檔,改寫的部份就即時(shí)更新元件到執(zhí)行環(huán)境。大致上流程就是
- 處?kù)堕_發(fā) app 階段,撰寫程式碼
- 打開瀏覽器觀察 app 行為
- app 在瀏覽器畫面上運(yùn)作
- 當(dāng)你發(fā)現(xiàn)一些 bug 或行為不如您所預(yù)期您通常會(huì)編輯程式碼,然後重新載入
- 使用 HRM 時(shí),當(dāng)你一存檔 webpack 就會(huì)偵測(cè)那些改變的部分並更新瀏覽器
- 重點(diǎn)是一些關(guān)於狀態(tài)的資料並不會(huì)被洗掉
要完成這功能,我們會(huì)需要 webpack-dev-server 以及套件 vue-hot-reload-api。然後執(zhí)行。
在這之前,我們需要修改一下 webpack.config.js 加入 webpack/hot/dev-server
var webpack = require('webpack')
var config = {
entry: [
'webpack/hot/dev-server',
path.join(__dirname, 'src', 'main')
],
...
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
接著,您可以選擇在全域安裝 webpack-dev-server
$ npm i webpack-dev-server -g
$ webpack-dev-server --inline --hot
又或者使用我們?cè)缦纫寻惭b在專案中的 webpack-dev-server,一般來(lái)說(shuō)會(huì)建議使用專案相依的這個(gè)。
需要在 package.json 加上 scripts
"scripts": {
"dev": "webpack-dev-server"
}
然后執(zhí)行:
$ npm run dev
為了觀察出我們是否有正確的啟用 hot reload 我們修改 app.vue
<template lang="html">
<div>
<div class="message">
{{ message }}
</div>
<div>
{{ count }}
</div>
</div>
</template>
<script>
export default {
data () {
return {
message: 'Helo, Vue.js 2.0',
count: 0
}
},
mounted () {
this.handle = setInterval(() => {
this.count++
}, 1000)
},
destroyed () {
clearInterval(this.handle)
}
}
</script>
<style lang="css">
.message {
color: pink;
font-size: 1.4em;
}
</style>
打開 http://localhost:8080 然後異動(dòng) app.vue css 與 template 的部分觀察看看變化。至此我們已經(jīng)跑完一次基本的用法。
文章剩下的部份則是整理一些 webpack 的指令與設(shè)定。
webpack 編譯指令
$ webpack [source] [destination]
$ webpack src/v1.js dist/v1.bundle.js
$ webpack bar=./src/v2.js "dist/[name].bundle.js"
# >> output dist/bar.bundle.js
# 指定設(shè)定檔
$ webpack --config [webpack.config.js]
注意 require 的 path 分成 函式庫(kù) 相對(duì)路徑 絕對(duì)路徑
- 函式庫(kù):什麼都不加,單純 library name
- 相對(duì)路徑:./ 開頭
- 絕對(duì)路徑:/ 開頭
資源
快速指令流程 & 程式碼片段
$ npm init -y
$ npm i webpack -D
# Add webpack.config.js
# Add scripts to package.json
# Setup webpack-dev-server
$ npm i webpack-dev-server -D
# 由於 prod & dev 會(huì)需要不同的設(shè)定因此我們需要至少兩份設(shè)定檔
# 有許多實(shí)作方式如下:
# 1. 維護(hù)多份設(shè)定檔,透過(guò) `--config` 指定不同的檔案
# 2. 把設(shè)定組織成一份 Library
# 3. 在一份檔案中依據(jù) `環(huán)境` 或 `指令` 套用不同設(shè)定
# 使用 webpack-merge,合併設(shè)定更方便
$ npm i webpack-merge -D
$ npm i css-loader style-loader -D
$ npm i file-loader url-loader -D
# 強(qiáng)大のplugin
# npm i npm-install-webpack-plugin -D
# 安裝處理 ES2015
$ npm i babel-core babel-loader babel-plugin-transform-runtime babel-preset-es2015 -D
# 加入 .babelrc
# 加入 loader 設(shè)定
# (Optional)安裝 vue-loader
$ npm i vue-loader vue-hot-reload-api -D
# 安裝 vue 與所需的模組
{
"presets": ["es2015"],
"plugins": ["transform-runtime"]
}
var path = require('path')
var webpack = require('webpack')
var merge = require('webpack-merge')
var precss = require('precss');
var autoprefixer = require('autoprefixer');
var T = process.env.npm_lifecycle_event
var common = {
entry: {
main: path.join(__dirname, 'src', 'main'),
venders: path.join(__dirname, 'src', 'venders')
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
},
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.css$/,
loaders: ['style', 'css', 'postcss']
/* include: path.join(__dirname, 'src') */
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url',
query: {
limit: 10000,
name: path.posix.join(__dirname, 'public', '[name].[hash:7].[ext]')
}
}
]
},
resolve: {
extensions: ['', '.js', '.vue', '.json', '.css']
},
postcss: function () {
return [precss, autoprefixer];
}
}
if (T === 'dev' || !T) {
var config = merge(common, {
devServer: {
historyApiFallback: true,
hot: true,
inline: true,
progress: true,
stats: 'errors-only',
host: process.env.HOST || '0.0.0.0',
port: process.env.PORT
},
devtool: 'eval-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
config.entry.main = ['webpack/hot/dev-server', config.entry.main]
module.exports = config
}
if (T === 'build') {
module.exports = merge(common, {})
}
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
}