兩年前的筆記,可能有點過時,先將文章上傳,整理筆記中。
核心功能
- Vue - 陳列式渲染
- Vuex - 狀態管理
- Vue-Router - SPA路由管理
- Vue-server-renderer - SSR(本次無介紹)
VueCLI3
node
版本建議 > 8.11.0 快速安裝
npm install -g @vue/cli
新建專案
UI介面
vue ui
終端機指令
vue create <Project-Name>
入口
src/main.js
// import package from 'node_package'
// Vue use(<package name>)
import Vue from 'vue'; // Vue package
import App from './App.vue'; // 網站入口模板
import router from './router'; // vue-router
import store from './store'; // vuex
new Vue({
router,
store,
// other package ...
render: h => h(App),
}).$mount('#app');
Vue.config.js
VueCLI
將Webpack4
封裝成自身的config
預設是沒有建立的
vue.config.js
webpack 主要功能
- API服務 - devServer
- 熱渲染 - hotReload
- 壓縮及分離代碼 - chunk
vue.config.js
基於[email protected]
方式 所以與webpack4
寫法不同
vue inspect // Vue檢查
vue inspect --rules // 檢查規則
vue inspect --plugins // 檢查套件
vue.config.js
'use strict';
const path = require('path');
function resolve(dir) {
return path.join(__dirname, dir);
}
const name = 'title';
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001/',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/api': '',
},
},
},
},
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
configureWebpack: {
name,
resolve: {
alias: {
'@': resolve('src'),
},
},
},
transpileDependencies: [
'vue-echarts',
'resize-detector',
],
chainWebpack(config) {
config.plugins.delete('preload'); // TODO: need test
config.plugins.delete('prefetch'); // TODO: need test
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end();
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]',
})
.end();
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap((options) => {
options.compilerOptions.preserveWhitespace = true;
return options;
})
.end();
config
.when(process.env.NODE_ENV === 'development',
conf => conf.devtool('cheap-source-map'));
config
.when(process.env.NODE_ENV !== 'development',
(conf) => {
conf
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
inline: /runtime\..*\.js$/,
}])
.end();
conf
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial',
},
elementUI: {
name: 'chunk-elementUI',
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/,
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3,
priority: 5,
reuseExistingChunk: true,
},
},
});
conf.optimization.runtimeChunk('single');
conf.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin);
});
},
};
vscode Plugin
{
"eslint.validate": [
"javascript",
"javascriptreact",
{ "language": "vue", "autoFix": true }
],
"eslint.autoFixOnSave": true,
}
vscode 的 setting
.eslintrc.js
/* **************
* "off" or 0 - 關閉規則
* "warn" or 1 - 將規則作為警告
* "error" or 2 - 將規則作為錯誤打開
* **************/
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: [
'plugin:vue/recommended',
'@vue/airbnb',
],
'settings': {
"import/resolver": {
"webpack": {
"config": "node_modules/@vue/cli-service/webpack.config.js"
}
}
},
'rules': {
'indent': [2, 4, { 'SwitchCase': 1 }],
'quotes': [2, 'single'],
'no-console': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'class-methods-use-this': 0,
'no-underscore-dangle': 0,
'no-plusplus': 0,
'strict': 0,
'max-len': [1, { "code": 140, "tabWidth": 4 }],
'camelcase': 1,
'no-unused-vars': 1,
'no-unused-expressions': [2, {
'allowShortCircuit': false,
'allowTernary': true
}],
'brace-style': [2, 'stroustrup'],
'import/no-unresolved': 0,
'import/no-cycle': 0,
'no-param-reassign': 0,
'func-names': 0,
'prefer-destructuring': 0,
'import/prefer-default-export': 2,
'no-continue': 1,
'no-use-before-define': [2, { "functions": false, "classes": false }],
'import/extensions': ['error', 'always', {
'js': 'never',
'vue': 'never'
}],
'no-bitwise': 0
}
}
{
"jest.showCoverageOnLoad": true,
"jest.debugMode": true,
}
vscode 的 setting
jest.config.js
process.env.VUE_CLI_BABEL_TARGET_NODE = true;
process.env.VUE_CLI_BABEL_TRANSPILE_MODULES = true;
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue',
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest',
},
transformIgnorePatterns: ['/node_modules/(?!@babel)'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
// 快照測試
snapshotSerializers: [
'jest-serializer-vue',
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)',
],
// 覆蓋範圍
collectCoverageFrom: ['src/utils/**/*.{js,vue}',
// '!src/utils/auth.js',
// 'src/lang/index.js',
'src/components/**/*.{js,vue}',
// 'src/views/**/*.{js,vue}',
],
// 覆蓋輸出
coverageDirectory: '<rootDir>/tests/unit/coverage',
// 收集覆蓋率
collectCoverage: true,
verbose: true,
testURL: 'http://localhost/',
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
};
chrome debug
Vue
陳列式渲染
生命週期
![](https://cn.vuejs.org/images/lifecycle.png =20%x)
Template
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
name: 'MyTemplate',
data() {
return {
msg:''
};
},
};
</script>
<style>
div{
color: #ffffff;
}
</style>
v-on / v-bind / v-model
<!-- 完整語法 -->
<a v-on:click="doSomething">...</a>
<!-- 縮寫 -->
<a @click="doSomething">...</a>
<!-- 完整語法 -->
<a v-bind:href="url">...</a>
<!-- 縮寫 -->
<a :href="url">...</a>
<input v-model="model" />
v-if/v-show
v-if
是條件渲染,v-show
是CSS切換
<!-- v-if -->
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else>
Not A/B
</div>
<!-- v-show -->
<h1 v-show="ok">Hello!</h1>
v-for
<template>
<div v-for="(item, index) in items" v-bind:key="index">
{{ message }}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
};
},
};
</script>
watch vs computed
這兩項的計算模式極相似難以區分
觸發方式 | 呼叫方式 |
---|---|
data | 「變數」被賦值,渲染 view |
watch | 「同名的變數」被賦值就執行 |
computed | 「取用的變數」被賦值就執行 |
method
觸發方式 | 呼叫方式 |
---|---|
methods | 被呼叫就執行 |
export default {
data() {
return {
// 去做綁定資料的格式
dataName:'',
};
},
watch: {
// watch
dataName(){
// 「同名的變數」被賦值就執行 ...
}
},
computed: {
// computed
computedFunction(){
// 「取用的變數」被賦值就執行...
}
},
methods: {
// v-on $events or call this function
methodsFunction(){
// Do something ...
}
},
};
組件相關
props & $emit
props
<!-- parent -->
<template>
<child :childObj="obj" />
</template>
<script>
import child from '@/components/child.vue';
export default {
components: {
child,
},
data(){
return{
obj:{},
}
}
};
</script>
<!-- child -->
<template>
<div>
{{ childObj }}
</div>
</template>
<script>
export default {
props: {
childObj: {
type: Object,
},
},
};
</script>
$emit
<!-- parent -->
<template>
<div id="app">
<child @childMethod="parentMethod"></child>
</div>
</template>
<script>
import Child from './components/Child';
export default {
name: 'App',
components: {
Child,
},
methods: {
parentMethod() {
console.log('Hello World');
},
},
};
</script>
<!-- child -->
<template>
<button @click="handleClick">Emit</button>
</template>
<script>
export default {
methods: {
handleClick() {
this.$emit('childMethod');
},
},
};
</script>
slot
<!-- parent -->
<template>
<SlotComponent>
<template slot="header">
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template slot="footer">
<p>Here's some contact info</p>
</template>
</SlotComponent>
</template>
<script>
import SlotComponent from '@/components/SlotComponent.vue';
export default {
components: {
SlotComponent,
},
};
</script>
<!-- child -->
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
[email protected] 有使用到slot-scope 未來 Vue3將此功能廢除
Vuex
狀態管理
- 多個component依賴於同一狀態,例如:權限
- 不同component的行為需要變更同一狀態。
- 組件中使用 this.$store
Vuex vs Vue
Vuex | Vue |
---|---|
State | data |
Getter | computed |
Mutation | -處理同步- |
Action | -處理非同步- |
Module | -分割模塊- |
State
用法和Vue裡頭的data區塊亦同,屬於-資料狀態
// store/index.js
export default new Vuex.Store({
state: {
data:''
}
})
// *.vue
<script>
export default {
computed: {
storeStateData () {
return this.$store.state.data
}
}
}
</script>
則 mapState
統一講解
Getter
用法和Vue裡頭的computed區塊亦同,屬於-計算狀態
// store/index.js
export default new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
// *.vue
<script>
export default {
computed: {
doneTodos () {
return this.$store.getters.doneTodos
}
}
}
</script>
則 mapGetter
統一講解
Mutation
同步操作的功能,具有回調功能(Handler)
// store/index.js
export default new Vuex.Store({
state: {
count: 1
},
mutations: {
INCREMENT (state, payload) {
state.count += payload.amount // 參數-amount
}
}
})
// *.vue
<template>
// 點擊事件
<button @click="countHandler"> Click ++ </button>
</template>
<script>
export default {
methods: {
countHandler(){
this.$store.commit({
type: 'INCREMENT',
amount: 10 // 參數-amount
})
}
}
}
則 mapMutations
統一講解
$store.commit
是 Mutation 使用
Action
Action 提交是 Mutation,不是直接變更狀態,Action 可以包含非同步操作
// store/index.js
export default new Vuex.Store({
state: {
count: 1
},
mutations: {
INCREMENT (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('INCREMENT')
}
})
// *.vue
<template>
//點擊事件
<button @click="countHandler"> Click ++ </button>
</template>
<script>
export default {
methods: {
countHandler(){
this.$store.dispatch('increment')
}
}
}
</script>
// 非同步action組合 async / await
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
則 mapActions
統一講解
$store.dispatch
是 Action 使用
Module
主要是分割功能,例如:會員區塊,權限區塊 …
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as modules from './modules'
Vue.use(Vuex);
const store = new Vuex.Store({
modules,
});
export default store;
// store/modules/*.js
import permission from './permission'; // 權限Module
import user from './user'; // 會員Module
export {
permission,
user,
};
// user.js or permission.js
// 這裡我把所有的Vuex大區塊功能寫在同一支檔案,也可分裝成一支支的js
const state = {
...
}
const getters = {
...
}
const actions = {
...
}
const mutations = {
...
}
export default {
namespaced: true, // true 成為自帶命名的區塊
state,
getters,
actions,
mutations
}
// *.vue
export default {
computed:{
stateName(){
// 這樣就可以進入到你的module取得 state
return this.$store.state.namespaced.stateName
}
},
methods:{
mutationFuncHandler:{
this.$store.commit('namespaced/INCREMENT')
}
}
}
Helper輔助函數
可以擴增Module,更為縮減於*.vue
使用
import { mapState, mapGetters ,mapActions, mapMutations } from 'vuex';
export default {
computed:{
...mapState('namespaced',['']),
...mapGetters('namespaced',[''])
},
methods:{
...mapActions('namespaced',[''])
...mapMutations('namespaced',[''])
}
}
...
是Array擴增使用Func的,就可以直接在Vue使用 this.FuncName
Vue-Router
SPA路由管理
- 定義組件 > 定義路由 > 創建路由 > 掛載路由
- 組件內使用router
- 組件中使用 this.$router
- 定義組件 -
*.vue
- 定義路由 -
const routes = []
- 創建路由 -
const router = new VueRouter
- 掛載路由 -
main.js new Vue({ router })
定義路由
// router/index.js
const routes =[
{
path: '/foo',
component: Foo, // 組件
children: [
{
path: 'bar',
component: Bar,
meta: { requiresAuth: true } // 配置 meta 字段
}
]
}
]
創建路由
// router/index.js
const createRouter = () => new Router({
mode: 'history',
routes: routes,
});
const router = createRouter();
export default router;
掛載路由
// main.js
import Vue from 'vue';
import router from './router';
import App from './App'; // App.vue Vue的入口
new Vue({
router,
render: h => h(App),
}).$mount('#app');
template 中使用
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 當導航 -->
<!-- <router-link> 默認會被渲染成 `<a>` -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配的組件會渲染成這 -->
<router-view></router-view>
</div>
router 使用
陳述式 | 程式化 |
---|---|
router.push(…) | |
router.replace(…) |
router.go(n) === window.history.go(n)
命名路由
router
也可以使用 params
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
// 陳述式
<router-link
:to="{
name: 'user',
params: { userId: 123 }}">
User
</router-link>
// 程式化
router.push(
{ name: 'user',
params: {
userId: 123
}
}
)
SPA配置
Nginx,後端網址的部分記得更換
server {
listen 8080;
server_name localhost;
client_max_body_size 20m;
charset utf-8;
root /usr/share/nginx/html;
index index.html;
location ~ ^/health {
default_type text/html;
return 200 'Hello World';
}
location ~ ^/api {
rewrite ^/api/(.*) /$1 break;
proxy_pass 後端網址;
proxy_ssl_server_name on;
proxy_redirect off;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location / {
try_files $uri $uri/ @rewrites;
}
location @rewrites {
rewrite ^(.+)$ /index.html last;
}
}
Nginx 不多介紹
Hunky
git commit/push & npm run test 再提交Git前,檢查過濾程式碼風格/測試
前提,eslint(.eslintrc) 及 jest(jest.config.js) 規範及設定已定義
npm i husky
// package.json
{
...
"scripts": {
"start": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
...
},
"husky": {
"hooks": {
"pre-commit": "npm run test:ci",
"pre-push": "npm run test:ci"
}
},
"dependencies": {
...
},
"devDependencies": {
...
"husky": "1.3.1",
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}