返回
Featured image of post Vue 2.x

Vue 2.x

Vue 2.x

兩年前的筆記,可能有點過時,先將文章上傳,整理筆記中。

核心功能


  • 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

VueCLIWebpack4封裝成自身的config預設是沒有建立的 vue.config.js


webpack 主要功能

  • API服務 - devServer
  • 熱渲染 - hotReload
  • 壓縮及分離代碼 - chunk

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


Plugin
Plugin


Plugin
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


Chrome Debug
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

VuexVue
Statedata
Gettercomputed
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"
  ]
}
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus