1. 程式人生 > >如何用vue-cli3腳手架搭建一個基於ts的基礎腳手架

如何用vue-cli3腳手架搭建一個基於ts的基礎腳手架

目錄

  • 準備工作
  • 搭建專案
  • vue 中 ts 語法
  • 專案代理及 webpack 效能優化
  • 其他

忙裡偷閒,整理了一下關於如何藉助 vue-cli3 搭建 ts + 裝飾器 的腳手架,並如何自定義 webpack 配置,優化。

準備工作

安裝 node

  • 安裝 node
  • 全域性安裝 nrm,npm 的映象源管理工具。
npm i nrm -g // 安裝
nrm ls // 檢視可用源,及當前源,帶*的是當前使用的源
nrm use taobao // 切換源,使用源
nrm add <registry> <url> // 其中reigstry為源名,url為源的路徑
nrm del <registry> // 刪除對應的源
nrm test npm // 測試源的響應速度

安裝 vue-cli3

  • 參考官方文件:https://cli.vuejs.org/zh/guide/
npm i @vue/cli -g // 全域性安裝

vue --version // 檢查是否安裝

補充

npm list -g --depth 0 // 檢視全域性安裝的包
npm outdated -g --depth=0 // 檢視需要更新的全域性包
npm update 包名 -g // 更新全域性安裝的包

搭建專案

可參考:使用Vue-cli 3.0搭建Vue專案

新建一個基於 ts 的 vue 專案

vue create vue-cli3-ts

備註:如果是 window 系統,用 git bash 互動提示符(切換)不會工作,用以下命令,即可解決:

winpty vue.cmd create vue-cli3-ts
  • 自定義選項 - Manually select features
  • 新增 ts 支援 - TypeScript
  • 基於類的元件 - y
  • tslint
  • 根據需要新增 router、vuex、css(less 或 scss) 前處理器、單元測試(jest)

互動說明:

  • 上下箭頭鍵切換
  • 空格鍵選中
  • 回車確定

在已存在的專案中新增 ts

vue add @vue/typescript

會把所有 .js 更改為 .ts

script 命令

// - 啟動服務
npm run serve
// - 打包編譯
npm run build
// - 執行lint
npm run lint
// - 執行單元測試
npm run test:unit

npm run serve 啟動服務:http://localhost:8080/#/

vue 中 ts 語法

demo: src/components/HelloWorld.vue

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
}
</script>

和普通的 vue 專案不一樣的就是.vue 檔案中 script 的 寫法。

主要用到的一個庫:vue-property-decorator

用法可參考:

  • npm
  • vue-property-decorator用法
  • ts 官方文件

1. 型別註解,型別推論

  • 變數後面通過 冒號+型別 來做型別註解。
  • 編譯時型別檢查,寫程式碼時程式碼提醒。
  • 型別推論,根據賦值型別推論出被賦值變數的型別,進行型別限制。
let title: string; // 型別註解
title = 'ts'; // 正確
title = 4; // 錯誤

let text = 'txt'; // 型別推論
text = 2; // 錯誤

錯誤時,vscode 編輯器會有紅色波浪號提示。

陣列

let names: string[]; // Array<string>
names = ['Tom'];

任意型別,沒有型別限制

let foo: any;
foo = 'foo';
foo = 3;

let list: any[];
list = [1, true, 'free'];
list[1] = 100;

函式中使用型別

function greeting (person: string): string {
    return 'Hello, ' + person;
}

// void 型別,常用於沒有返回值的函式
function warnUser (): void {
    alert('This is msg');
}

案例:vue demo

<template>
  <div class="hello">
    <input type="text" placeholder="請輸入新特性" @keyup.enter="addFeature" />
    <ul>
      <li v-for="feature in features" :key="feature">{{feature}}</li>
    </ul>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class Demo extends Vue {

  // 相當於 data 中的資料項
  features: string[];
  constructor () {
    super();
    this.features = ['型別註解', '型別推論', '編譯型語言'];
  }

  // 相當於 methods 中的方法
  addFeature (event: any) {
    console.log(event);
    this.features.push(event.target.value);
    event.target.value = '';
  }
}
</script>

2.類

ts 中的類和 es6 中的大體相同,關注特性 訪問修飾符

  • private 私有屬性,不能在類的外部訪問
  • protected 保護屬性,可以在類的內部和派生類的內部訪問,不能在類的外部訪問
  • public 公有屬性,可以在任意地方訪問,預設值
  • readonly 只讀屬性,必須在宣告時或建構函式裡初始化,不可改變值

建構函式:初始化成員變數,引數加上修飾符,能夠定義並初始化一個屬性

constructor (private name = 'Tom') {
    super();
}

等同於

name: string;
constructor () {
    super();
    this.name = 'Tom';
}

存取器,暴露存取資料時可新增額外邏輯;在 vue 中可用作計算屬性

get fullName () { return this.name; }
set fullName (val) { this.name = val; }

案例:vue demo

<template>
    <p>特性數量:{{count}}</p>
</template>
<script lang="ts">
    export default class Demo extends Vue {
        // 定義 getter 作為計算屬性
        get count () {
            return this.features.length;
        }
    }
</script>

介面

介面僅約束結構,不要求實現

interface Person {
    firstName: string;
    lastName: string;
}
function greeting (person: Person) {
    return `Hello, ${person.firstName} ${person.lastName}`;
}
const user = {firstName: 'Jane', lastName: 'user'};
console.log(greeting(user));

案例:vue demo,宣告介面型別約束資料結構

<template>
    <li v-for="feature in features" :key="feature.id">{{feature.name}}</li>
</template>
<script lang="ts">
    // 定義一個介面約束feature的資料結構
    interface Feature {
        id: number;
        name: string;
    }
    
    export default class Demo extends Vue {
        private features: Feature[];
        
        constructor () {
            super();
            
            this.features = [
                {id: 1, name: '型別註解'},
                {id: 2, name: '型別推論'},
                {id: 3, name: '編譯型語言'}
            ]
        }
    }
</script>

泛型

泛型 是指在定義函式、介面或類的時候,不預先指定具體的類,而是在使用時才指定型別的一種特性。

interface Result<T> {
    data: T;
}

// 不使用泛型
interface Result {
    data: Feature[];
}

案例:使用泛型約束介面返回型別

function getData<T>(): Result<T> {
  const data: any = [
      {id: 1, name: '型別註解'},
      {id: 2, name: '型別推論'},
      {id: 3, name: '編譯型語言'} 
  ];
  return {data};
}

// 呼叫
this.features = getData<Feature[]>().data;

案例:使用泛型約束介面返回型別 Promise

function getData<T>(): Promise<Result<T>> {
  const data: any = [
      {id: 1, name: '型別註解'},
      {id: 2, name: '型別推論'},
      {id: 3, name: '編譯型語言'} 
  ];
  return Promise.resolve<Result<T>>({data});
}

// 呼叫 async 方式
async mounted () {
    this.features = (await getData<Feature[]>()).data;
}

// 呼叫 then 方式
mouted () {
    getData<Feature[]>().then((res: Result<Feature[]>) => {
      this.features = res.data;
    })
}

裝飾器

裝飾器用於擴充套件類或者它的屬性和方法。

屬性宣告:@Prop

除了在 @Component 中宣告,還可以採用@Prop的方式宣告元件屬性

export default class Demo extends Vue {
    // Props() 引數是為 vue 提供屬性選項
    // !稱為明確賦值斷言,它是提供給ts的
    @Prop({type: String, require: true})
    private msg!: string;
}

事件處理:@Emit

// 通知父類新增事件,若未指定事件名則函式名作為事件名(駝峰變中劃線分隔)
@Emit()
private addFeature(event: any) {//     若沒有返回值形參將作為事件引數
    const feature = { name: event.target.value, id: this.features.length + 1 };
    this.features.push(feature);
    event.target.value = "";
    return feature;// 若有返回值則返回值作為事件引數
}

template 模板元件上正常寫,@add-feature

變更監測:@Watch

@Watch('msg')
onRouteChange(val:string, oldVal:any){
    console.log(val, oldVal);
}

裝飾器原理

裝飾器本質是工廠函式,修改傳入的類、方法、屬性等

類裝飾器

// 類裝飾器表示式會在執行時當作函式被呼叫,類的建構函式作為其唯一的引數。
function log(target: Function) {
    // target是建構函式
    console.log(target === Foo); // true
    target.prototype.log = function() {
    console.log(this.bar);
}
// 如果類裝飾器返回一個值,它會使用提供的建構函式來替換類的宣告。
}
@log
class Foo {
    bar = 'bar'
}
const foo = new Foo();
// @ts-ignore
foo.log();

實戰一下 Component,新建 Decor.vue

<template>
    <div>{{msg}}</div>
</template>
<script lang='ts'>
    import { Vue } from "vue-property-decorator";
    function Component(options: any) {
        return function(target: any) {
            return Vue.extend(options);
        };
    }
    
    @Component({
        props: {
            msg: {
                type: String,
                default: ""
            }
        }
    })
    export default class Decor extends Vue {}
</script>

原始碼簡單瞭解

類裝飾器主要依賴庫:vue-class-component,深入原始碼,瞭解其背後究竟做了什麼。

vue-property-decorator.js

import Vue from 'vue';
import Component, { createDecorator, mixins } from 'vue-class-component';
export { Component, Vue, mixins as Mixins };

createDecorator、applyMetadata 是核心,後續實現都依賴它,比如 Prop、Watch、Ref。

Prop 原始碼實現:

export function Prop(options) {
    if (options === void 0) { options = {}; }
    return function (target, key) {
        applyMetadata(options, target, key);
        createDecorator(function (componentOptions, k) {
            ;
            (componentOptions.props || (componentOptions.props = {}))[k] = options;
        })(target, key);
    };
}

applyMetadata,見名知義,就是將裝飾器中的資訊拿出來放到 options.type 中。

/** @see {@link https://github.com/vuejs/vue-class-component/blob/master/src/reflect.ts} */
var reflectMetadataIsSupported = typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined';
function applyMetadata(options, target, key) {
    if (reflectMetadataIsSupported) {
        if (!Array.isArray(options) &&
            typeof options !== 'function' &&
            typeof options.type === 'undefined') {
            options.type = Reflect.getMetadata('design:type', target, key);
        }
    }
}

Reflect.getMetadata 獲取設定在類裝飾器上的元資料。可參考文章理解:

  • Decorators
  • TypeScript:理解 Reflect Metadata
  • JavaScript Reflect Metadata 詳解

createDecorator,見名知義,就是建立裝飾器。本質是在類上定義一個私有屬性

export function createDecorator(factory) {
    return function (target, key, index) {
        var Ctor = typeof target === 'function'
            ? target
            : target.constructor;
        if (!Ctor.__decorators__) {
            Ctor.__decorators__ = [];
        }
        if (typeof index !== 'number') {
            index = undefined;
        }
        Ctor.__decorators__.push(function (options) { return factory(options, key, index); });
    };
}

專案代理及 webpack 效能優化

在專案根目錄下新建 vue.config.js

本地開發 api 代理

module.exports = {
    devServer: {
        proxy: {
          '/api': {
            target: '<url>',
            changeOrigin: true,
            pathRewrite: {
              '^/api': ''
            }
          }
        }
    }
}

本地開發 api 模擬

devServer: {
    before (app) {
        before (app) {
            app.get('/api/getList', (req, res) => {
                res.json({data: [{id: 1, name: 'vue'}]})
            })
        }
    }
}

效能優化

檢視打包依賴

在 package.json 檔案 script 中加入命令:

"build:report": "vue-cli-service build --report"

會在 dist 目錄下生成 report.html,可直接開啟,檢視打包依賴,進行分析,進行打包優化

打包優化 - cdn 引入公共庫

在 vue.config.js 中加入配置:

configureWebpack: {
    externals: { // cdn 外鏈,避免包太大,首屏優化
      'vue': 'Vue',
      'vue-router': 'VueRouter',
      'vuex': 'Vuex'
    }
}

在 public/index.html 中加入 cdn 庫地址

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.1.3/vue-router.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.2/vuex.min.js"></script>

<!-- built files will be auto injected -->

再次優化,html head 資訊中加,dns 域名預解析,js 庫 reload 預載入。

<link rel="dns-prefetch" href="cdnjs.cloudflare.com" >
<link href="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js" rel="preload" as="script">
<link href="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.1.3/vue-router.min.js" rel="preload" as="script">
<link href="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.2/vuex.min.js" rel="preload" as="script">

其他

修改本地開發埠號,在 vue.config.js 中加入配置:

devServer: {
    port: 8888
}

體驗優化-打包完成提示:

const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
const path = require('path');

module.exports = {
    // 鏈式操作
    chainWebpack: config => {
        // 移除 prefetch 外掛,移動端對頻寬敏感
        // 路由懶載入,只對使用者頻繁操作的路由,通過 註釋 提前獲取
        // component: () => import(/* webpackChunkName: "about" */ /* webpackPrefetch: true */'../views/About.vue')
        config.plugins.delete('prefetch');
    
        // 生產打包才提示,開發不提示
        if (process.env.NODE_ENV === 'production') {
          config.plugin('build-notify').use(WebpackBuildNotifierPlugin, [{
            title: "My Project Webpack Build",
            logo: path.resolve("./img/favicon.png"),
            suppressSuccess: true
          }])
        }
    }
}