本文講解如何在 Vue 項目中使用 TypeScript 來搭建並開發項目,並在此過程當中踩過的坑 。javascript
TypeScript 具備類型系統,且是 JavaScript 的超集,TypeScript 在 2018年 勢頭迅猛,可謂遍地開花。css
Vue3.0 將使用 TS 重寫,重寫後的 Vue3.0 將更好的支持 TS。2019 年 TypeScript 將會更加普及,可以熟練掌握 TS,並使用 TS 開發過項目,將更加成爲前端開發者的優點。html
因此筆者就固然也要學這個必備技能,就以 邊學邊實踐 的方式,作個博客項目來玩玩。前端
此項目是基於 Vue 全家桶 + TypeScript + Element-UI 的技術棧,且已經開源,github 地址 blog-vue-typescript 。vue
由於以前寫了篇純 Vue 項目搭建的相關文章 基於vue+mint-ui的mobile-h5的項目說明 ,有很多人加我微信,要源碼來學習,可是這個是我司的項目,不能提供原碼。java
因此作一個不是我司的項目,且又是 vue 相關的項目來練手並開源吧。node
效果圖:react
完整效果請看:https://biaochenxuying.cnwebpack
全部技術都是當前最新的。ios
若是沒有一點點基礎,可能沒學過 TypeScript 的讀者會看不懂往下的內容,因此先學點基礎。
TypeScript 的靜態類型檢查是個好東西,能夠避免不少沒必要要的錯誤, 不用在調試或者項目上線的時候才發現問題 。
TypeScript 裏的類型註解是一種輕量級的爲函數或變量添加約束的方式。變量定義時也要定義他的類型,好比常見的 :
// 布爾值 let isDone: boolean = false; // 至關於 js 的 let isDone = false; // 變量定義以後不能夠隨便變動它的類型 isDone = true // 不報錯 isDone = "我要變爲字符串" // 報錯
// 數字 let decLiteral: number = 6; // 至關於 js 的 let decLiteral = 6;
// 字符串 let name: string = "bob"; // 至關於 js 的 let name = "bob";
// 數組 // 第一種,能夠在元素類型後面接上 [],表示由此類型元素組成的一個數組: let list: number[] = [1, 2, 3]; // 至關於 js 的let list = [1, 2, 3]; // 第二種方式是使用數組泛型,Array<元素類型>: let list: Array<number> = [1, 2, 3]; // 至關於 js 的let list = [1, 2, 3];
// 在 TypeScript 中,咱們使用接口(Interfaces)來定義 對象 的類型。 interface Person { name: string; age: number; } let tom: Person = { name: 'Tom', age: 25 }; // 以上 對象 的代碼至關於 let tom = { name: 'Tom', age: 25 };
// Any 能夠隨便變動類型 (當這個值可能來自於動態的內容,好比來自用戶輸入或第三方代碼庫) let notSure: any = 4; notSure = "我能夠隨便變動類型" // 不報錯 notSure = false; // 不報錯
// Void 當一個函數沒有返回值時,你一般會見到其返回值類型是 void function warnUser(): void { console.log("This is my warning message"); }
// 方法的參數也要定義類型,不知道就定義爲 any function fetch(url: string, id : number, params: any): void { console.log("fetch"); }
以上是最簡單的一些知識點,更多知識請看 TypeScript 中文官網
Vue
組件進行了一層封裝,讓 Vue
組件語法在結合了 TypeScript
語法以後更加扁平化:<template> <div> <input v-model="msg"> <p>prop: {{propMessage}}</p> <p>msg: {{msg}}</p> <p>helloMsg: {{helloMsg}}</p> <p>computed msg: {{computedMsg}}</p> <button @click="greet">Greet</button> </div> </template> <script> import Vue from 'vue' import Component from 'vue-class-component' @Component({ props: { propMessage: String } }) export default class App extends Vue { // initial data msg = 123 // use prop values for initial data helloMsg = 'Hello, ' + this.propMessage // lifecycle hook mounted () { this.greet() } // computed get computedMsg () { return 'computed ' + this.msg } // method greet () { alert('greeting: ' + this.msg) } } </script>
上面的代碼跟下面的代碼做用是同樣的:
<template> <div> <input v-model="msg"> <p>prop: {{propMessage}}</p> <p>msg: {{msg}}</p> <p>helloMsg: {{helloMsg}}</p> <p>computed msg: {{computedMsg}}</p> <button @click="greet">Greet</button> </div> </template> <script> export default { // 屬性 props: { propMessage: { type: String } }, data () { return { msg: 123, helloMsg: 'Hello, ' + this.propMessage } }, // 聲明週期鉤子 mounted () { this.greet() }, // 計算屬性 computed: { computedMsg () { return 'computed ' + this.msg } }, // 方法 methods: { greet () { alert('greeting: ' + this.msg) } }, } </script>
vue-property-decorator 是在 vue-class-component
上加強了更多的結合 Vue
特性的裝飾器,新增了這 7 個裝飾器:
@Emit
@Inject
@Model
@Prop
@Provide
@Watch
@Component
(從 vue-class-component
繼承)在這裏列舉幾個經常使用的@Prop/@Watch/@Component
, 更多信息,詳見官方文檔
import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator' @Component export class MyComponent extends Vue { @Prop() propA: number = 1 @Prop({ default: 'default value' }) propB: string @Prop([String, Boolean]) propC: string | boolean @Prop({ type: null }) propD: any @Watch('child') onChildChanged(val: string, oldVal: string) { } }
上面的代碼至關於:
export default { props: { checked: Boolean, propA: Number, propB: { type: String, default: 'default value' }, propC: [String, Boolean], propD: { type: null } } methods: { onChildChanged(val, oldVal) { } }, watch: { 'child': { handler: 'onChildChanged', immediate: false, deep: false } } }
vue-class-component
寫法中 綁定 vuex
。import Vue from 'vue' import Component from 'vue-class-component' import { State, Getter, Action, Mutation, namespace } from 'vuex-class' const someModule = namespace('path/to/module') @Component export class MyComp extends Vue { @State('foo') stateFoo @State(state => state.bar) stateBar @Getter('foo') getterFoo @Action('foo') actionFoo @Mutation('foo') mutationFoo @someModule.Getter('foo') moduleGetterFoo // If the argument is omitted, use the property name // for each state/getter/action/mutation type @State foo @Getter bar @Action baz @Mutation qux created () { this.stateFoo // -> store.state.foo this.stateBar // -> store.state.bar this.getterFoo // -> store.getters.foo this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true }) this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true }) this.moduleGetterFoo // -> store.getters['path/to/module/foo'] } }
筆者使用最新的 vue-cli 3 搭建項目,詳細的教程,請看我以前寫的 vue-cli3.x 新特性及踩坑記,裏面已經有詳細講解 ,但文章裏面的配置和此項目不一樣的是,我加入了 TypeScript ,其餘的配置都是 vue-cli 原本配好的了。詳情請看 vue-cli 官網 。
安裝的依賴:
安裝過程選擇的一些配置:
搭建好以後,初始項目結構長這樣:
├── public // 靜態頁面 ├── src // 主目錄 ├── assets // 靜態資源 ├── components // 組件 ├── views // 頁面 ├── App.vue // 頁面主入口 ├── main.ts // 腳本主入口 ├── router.ts // 路由 ├── shims-tsx.d.ts // 相關 tsx 模塊注入 ├── shims-vue.d.ts // Vue 模塊注入 └── store.ts // vuex 配置 ├── tests // 測試用例 ├── .eslintrc.js // eslint 相關配置 ├── .gitignore // git 忽略文件配置 ├── babel.config.js // babel 配置 ├── postcss.config.js // postcss 配置 ├── package.json // 依賴 └── tsconfig.json // ts 配置
奔着 大型項目的結構 來改造項目結構,改造後 :
├── public // 靜態頁面 ├── src // 主目錄 ├── assets // 靜態資源 ├── filters // 過濾 ├── store // vuex 配置 ├── less // 樣式 ├── utils // 工具方法(axios封裝,全局方法等) ├── views // 頁面 ├── App.vue // 頁面主入口 ├── main.ts // 腳本主入口 ├── router.ts // 路由 ├── shime-global.d.ts // 相關 全局或者插件 模塊注入 ├── shims-tsx.d.ts // 相關 tsx 模塊注入 ├── shims-vue.d.ts // Vue 模塊注入, 使 TypeScript 支持 *.vue 後綴的文件 ├── tests // 測試用例 ├── .eslintrc.js // eslint 相關配置 ├── postcss.config.js // postcss 配置 ├── .gitignore // git 忽略文件配置 ├── babel.config.js // preset 記錄 ├── package.json // 依賴 ├── README.md // 項目 readme ├── tsconfig.json // ts 配置 └── vue.config.js // webpack 配置
tsconfig.json 文件中指定了用來編譯這個項目的根文件和編譯選項。
本項目的 tsconfig.json 配置以下 :
{ // 編譯選項 "compilerOptions": { // 編譯輸出目標 ES 版本 "target": "esnext", // 採用的模塊系統 "module": "esnext", // 以嚴格模式解析 "strict": true, "jsx": "preserve", // 從 tslib 導入外部幫助庫: 好比__extends,__rest等 "importHelpers": true, // 如何處理模塊 "moduleResolution": "node", // 啓用裝飾器 "experimentalDecorators": true, "esModuleInterop": true, // 容許從沒有設置默認導出的模塊中默認導入 "allowSyntheticDefaultImports": true, // 定義一個變量就必須給它一個初始值 "strictPropertyInitialization" : false, // 容許編譯javascript文件 "allowJs": true, // 是否包含能夠用於 debug 的 sourceMap "sourceMap": true, // 忽略 this 的類型檢查, Raise error on this expressions with an implied any type. "noImplicitThis": false, // 解析非相對模塊名的基準目錄 "baseUrl": ".", // 給錯誤和消息設置樣式,使用顏色和上下文。 "pretty": true, // 設置引入的定義文件 "types": ["webpack-env", "mocha", "chai"], // 指定特殊模塊的路徑 "paths": { "@/*": ["src/*"] }, // 編譯過程當中須要引入的庫文件的列表 "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, // ts 管理的文件 "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], // ts 排除的文件 "exclude": ["node_modules"] }
更多配置請看官網的 tsconfig.json 的 編譯選項
本項目的 vue.config.js:
const path = require("path"); const sourceMap = process.env.NODE_ENV === "development"; module.exports = { // 基本路徑 publicPath: "./", // 輸出文件目錄 outputDir: "dist", // eslint-loader 是否在保存的時候檢查 lintOnSave: false, // webpack配置 // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md chainWebpack: () => {}, configureWebpack: config => { if (process.env.NODE_ENV === "production") { // 爲生產環境修改配置... config.mode = "production"; } else { // 爲開發環境修改配置... config.mode = "development"; } Object.assign(config, { // 開發生產共同配置 resolve: { extensions: [".js", ".vue", ".json", ".ts", ".tsx"], alias: { vue$: "vue/dist/vue.js", "@": path.resolve(__dirname, "./src") } } }); }, // 生產環境是否生成 sourceMap 文件 productionSourceMap: sourceMap, // css相關配置 css: { // 是否使用css分離插件 ExtractTextPlugin extract: true, // 開啓 CSS source maps? sourceMap: false, // css預設器配置項 loaderOptions: {}, // 啓用 CSS modules for all css / pre-processor files. modules: false }, // use thread-loader for babel & TS in production build // enabled by default if the machine has more than 1 cores parallel: require("os").cpus().length > 1, // PWA 插件相關配置 // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa pwa: {}, // webpack-dev-server 相關配置 devServer: { open: process.platform === "darwin", host: "localhost", port: 3001, //8080, https: false, hotOnly: false, proxy: { // 設置代理 // proxy all requests starting with /api to jsonplaceholder "/api": { // target: "https://emm.cmccbigdata.com:8443/", target: "http://localhost:3000/", // target: "http://47.106.136.114/", changeOrigin: true, ws: true, pathRewrite: { "^/api": "" } } }, before: app => {} }, // 第三方插件配置 pluginOptions: { // ... } };
原本想搭配 iview-ui 來用的,但後續還想把這個項目搞成 ssr 的,而 vue + typescript + iview + Nuxt.js 的服務端渲染還有很多坑, 而 vue + typescript + element + Nuxt.js 對 ssr 的支持已經不錯了,因此選擇了 element-ui 。
安裝:
npm i element-ui -S
按需引入, 藉助 babel-plugin-component,咱們能夠只引入須要的組件,以達到減少項目體積的目的。
npm install babel-plugin-component -D
而後,將 babel.config.js 修改成:
module.exports = { presets: ["@vue/app"], plugins: [ [ "component", { libraryName: "element-ui", styleLibraryName: "theme-chalk" } ] ] };
接下來,若是你只但願引入部分組件,好比 Button 和 Select,那麼須要在 main.js 中寫入如下內容:
import Vue from 'vue'; import { Button, Select } from 'element-ui'; import App from './App.vue'; Vue.component(Button.name, Button); Vue.component(Select.name, Select); /* 或寫爲 * Vue.use(Button) * Vue.use(Select) */ new Vue({ el: '#app', render: h => h(App) });
使用路由懶加載功能。
export default new Router({ mode: "history", routes: [ { path: "/", name: "home", component: () => import(/* webpackChunkName: "home" */ "./views/home.vue") }, { path: "/articles", name: "articles", // route level code-splitting // this generates a separate chunk (articles.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "articles" */ "./views/articles.vue") }, ] });
// fn是咱們須要包裝的事件回調, delay是時間間隔的閾值 export function throttle(fn: Function, delay: number) { // last爲上一次觸發回調的時間, timer是定時器 let last = 0, timer: any = null; // 將throttle處理結果看成函數返回 return function() { // 保留調用時的this上下文 let context = this; // 保留調用時傳入的參數 let args = arguments; // 記錄本次觸發回調的時間 let now = +new Date(); // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值 if (now - last < delay) { // 若是時間間隔小於咱們設定的時間間隔閾值,則爲本次觸發操做設立一個新的定時器 clearTimeout(timer); timer = setTimeout(function() { last = now; fn.apply(context, args); }, delay); } else { // 若是時間間隔超出了咱們設定的時間間隔閾值,那就不等了,不管如何要反饋給用戶一次響應 last = now; fn.apply(context, args); } }; }
const config = { 'oauth_uri': 'https://github.com/login/oauth/authorize', 'redirect_uri': 'https://biaochenxuying.cn/login', 'client_id': 'XXXXXXXXXX', 'client_secret': 'XXXXXXXXXX', }; // 本地開發環境下 if (process.env.NODE_ENV === 'development') { config.redirect_uri = "http://localhost:3001/login" config.client_id = "502176cec65773057a9e" config.client_secret = "65d444de381a026301a2c7cffb6952b9a86ac235" } export default config;
若是你的生產環境也要 github 登陸受權的話,請在 github 上申請一個 Oauth App ,把你的 redirect_uri,client_id,client_secret 的信息填在 config 裏面便可。具體詳情請看我寫的這篇文章 github 受權登陸教程與如何設計第三方受權登陸的用戶表
// url的連接 export const urls: object = { login: "login", register: "register", getArticleList: "getArticleList", }; export default urls;
import axios from "axios"; // 建立axios實例 let service: any = {}; service = axios.create({ baseURL: "/api", // api的base_url timeout: 50000 // 請求超時時間 }); // request攔截器 axios的一些配置 service.interceptors.request.use( (config: any) => { return config; }, (error: any) => { // Do something with request error console.error("error:", error); // for debug Promise.reject(error); } ); // respone攔截器 axios的一些配置 service.interceptors.response.use( (response: any) => { return response; }, (error: any) => { console.error("error:" + error); // for debug return Promise.reject(error); } ); export default service;
把 urls 和 https 掛載到 main.ts 裏面的 Vue 的 prototype 上面。
import service from "./utils/https"; import urls from "./utils/urls"; Vue.prototype.$https = service; // 其餘頁面在使用 axios 的時候直接 this.$http 就能夠了 Vue.prototype.$urls = urls; // 其餘頁面在使用 urls 的時候直接 this.$urls 就能夠了
而後就能夠統一管理接口,並且調用起來也很方便啦。好比下面 文章列表的請求。
async handleSearch() { this.isLoading = true; const res: any = await this.$https.get(this.$urls.getArticleList, { params: this.params }); this.isLoading = false; if (res.status === 200) { if (res.data.code === 0) { const data: any = res.data.data; this.articlesList = [...this.articlesList, ...data.list]; this.total = data.count; this.params.pageNum++; if (this.total === this.articlesList.length) { this.isLoadEnd = true; } } else { this.$message({ message: res.data.message, type: "error" }); } } else { this.$message({ message: "網絡錯誤!", type: "error" }); } }
通常大型的項目都有不少模塊的,好比本項目中有公共信息(好比 token )、 用戶模塊、文章模塊。
├── modules // 模塊 ├── user.ts // 用戶模塊 ├── article.ts // 文章模塊 ├── types.ts // 類型 └── index.ts // vuex 主入口
import Vue from "vue"; import Vuex from "vuex"; import * as types from "./types"; import user from "./modules/user"; import article from "./modules/article"; Vue.use(Vuex); const initPageState = () => { return { token: "" }; }; const store = new Vuex.Store({ strict: process.env.NODE_ENV !== "production", // 具體模塊 modules: { user, article }, state: initPageState(), mutations: { [types.SAVE_TOKEN](state: any, pageState: any) { for (const prop in pageState) { state[prop] = pageState[prop]; } } }, actions: {} }); export default store;
// 公共 token export const SAVE_TOKEN = "SAVE_TOKEN"; // 用戶 export const SAVE_USER = "SAVE_USER";
import * as types from "../types"; const initPageState = () => { return { userInfo: { _id: "", name: "", avator: "" } }; }; const user = { state: initPageState(), mutations: { [types.SAVE_USER](state: any, pageState: any) { for (const prop in pageState) { state[prop] = pageState[prop]; } } }, actions: {} }; export default user;
markdown 渲染效果圖:
markdown 渲染 採用了開源的 marked, 代碼高亮用了 highlight.js 。
用法:
第一步:npm i marked highlight.js --save
npm i marked highlight.js --save
第二步: 導入封裝成 markdown.js,將文章詳情由字符串轉成 html, 並抽離出文章目錄。
marked 的封裝 得感謝這位老哥。
const highlight = require("highlight.js"); const marked = require("marked"); const tocObj = { add: function(text, level) { var anchor = `#toc${level}${++this.index}`; this.toc.push({ anchor: anchor, level: level, text: text }); return anchor; }, // 使用堆棧的方式處理嵌套的ul,li,level即ul的嵌套層次,1是最外層 // <ul> // <li></li> // <ul> // <li></li> // </ul> // <li></li> // </ul> toHTML: function() { let levelStack = []; let result = ""; const addStartUL = () => { result += '<ul class="anchor-ul" id="anchor-fix">'; }; const addEndUL = () => { result += "</ul>\n"; }; const addLI = (anchor, text) => { result += '<li><a class="toc-link" href="#' + anchor + '">' + text + "<a></li>\n"; }; this.toc.forEach(function(item) { let levelIndex = levelStack.indexOf(item.level); // 沒有找到相應level的ul標籤,則將li放入新增的ul中 if (levelIndex === -1) { levelStack.unshift(item.level); addStartUL(); addLI(item.anchor, item.text); } // 找到了相應level的ul標籤,而且在棧頂的位置則直接將li放在此ul下 else if (levelIndex === 0) { addLI(item.anchor, item.text); } // 找到了相應level的ul標籤,可是不在棧頂位置,須要將以前的全部level出棧而且打上閉合標籤,最後新增li else { while (levelIndex--) { levelStack.shift(); addEndUL(); } addLI(item.anchor, item.text); } }); // 若是棧中還有level,所有出棧打上閉合標籤 while (levelStack.length) { levelStack.shift(); addEndUL(); } // 清理先前數據供下次使用 this.toc = []; this.index = 0; return result; }, toc: [], index: 0 }; class MarkUtils { constructor() { this.rendererMD = new marked.Renderer(); this.rendererMD.heading = function(text, level, raw) { var anchor = tocObj.add(text, level); return `<h${level} id=${anchor}>${text}</h${level}>\n`; }; highlight.configure({ useBR: true }); marked.setOptions({ renderer: this.rendererMD, headerIds: false, gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false, highlight: function(code) { return highlight.highlightAuto(code).value; } }); } async marked(data) { if (data) { let content = await marked(data); // 文章內容 let toc = tocObj.toHTML(); // 文章目錄 return { content: content, toc: toc }; } else { return null; } } } const markdown = new MarkUtils(); export default markdown;
第三步: 使用
import markdown from "@/utils/markdown"; // 獲取文章詳情 async handleSearch() { const res: any = await this.$https.post( this.$urls.getArticleDetail, this.params ); if (res.status === 200) { if (res.data.code === 0) { this.articleDetail = res.data.data; // 使用 marked 轉換 const article = markdown.marked(res.data.data.content); article.then((response: any) => { this.articleDetail.content = response.content; this.articleDetail.toc = response.toc; }); } else { // ... } else { // ... } } // 渲染 <div id="content" class="article-detail" v-html="articleDetail.content"> </div>
第四步:引入 monokai_sublime 的 css 樣式
<link href="http://cdn.bootcss.com/highlight.js/8.0/styles/monokai_sublime.min.css" rel="stylesheet">
第五步:對 markdown 樣式的補充
若是不補充樣式,是沒有黑色背景的,字體大小等也會比較小,圖片也不會居中顯示
/*對 markdown 樣式的補充*/ pre { display: block; padding: 10px; margin: 0 0 10px; font-size: 14px; line-height: 1.42857143; color: #abb2bf; background: #282c34; word-break: break-all; word-wrap: break-word; overflow: auto; } h1,h2,h3,h4,h5,h6{ margin-top: 1em; /* margin-bottom: 1em; */ } strong { font-weight: bold; } p > code:not([class]) { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px; } p img{ /* 圖片居中 */ margin: 0 auto; display: flex; } #content { font-family: "Microsoft YaHei", 'sans-serif'; font-size: 16px; line-height: 30px; } #content .desc ul,#content .desc ol { color: #333333; margin: 1.5em 0 0 25px; } #content .desc h1, #content .desc h2 { border-bottom: 1px solid #eee; padding-bottom: 10px; } #content .desc a { color: #009a61; }
對於 關於 的頁面,實際上是一篇文章來的,根據文章類型 type 來決定的,數據庫裏面 type 爲 3
的文章,只能有一篇就是 博主介紹 ;達到了想何時修改內容均可以。
因此當 當前路由 === '/about' 時就是請求類型爲 博主介紹 的文章。
type: 3, // 文章類型: 1:普通文章;2:是博主簡歷;3 :是博主簡介;
// 屏幕適配( window.screen.width / 移動端設計稿寬 * 100)也便是 (window.screen.width / 750 * 100) ——*100 爲了方便計算。即 font-size 值是手機 deviceWidth 與設計稿比值的 100 倍 document.getElementsByTagName('html')[0].style.fontSize = window.screen.width / 7.5 + 'px';
如上:經過查詢屏幕寬度,動態的設置 html 的 font-size 值,移動端的設計稿大多以寬爲 750 px 來設置的。
好比在設計圖上一個 150 * 250 的盒子(單位 px):
本來在 css 中的寫法:
width: 150px; heigth: 250px;
經過上述換算後,在 css 中對應的 rem 值只須要寫:
width: 1.5rem; // 150 / 100 rem heigth: 2.5rem; // 250 / 100 rem
若是你的移動端的設計稿是以寬爲 1080 px 來設置的話,就用 window.screen.width / 10.8 吧。
import service from "./utils/https"; import urls from "./utils/urls"; Vue.prototype.$https = service; // 其餘頁面在使用 axios 的時候直接 this.$http 就能夠了 Vue.prototype.$urls = urls; // 其餘頁面在使用 urls 的時候直接 this.$urls 就能夠了
然而當你在組件中直接 this.$http 或者 this.$urls 時會報錯的,那是由於 $http 和 $urls 屬性,並無在 vue 實例中聲明。
import { Message } from "element-ui"; Vue.prototype.$message = Message;
以前用法以下圖:
this.$message({ message: '恭喜你,這是一條成功消息', type: 'success' })
然而仍是會報錯的。
再好比 監聽路由的變化:
import { Vue, Watch } from "vue-property-decorator"; import Component from "vue-class-component"; import { Route } from "vue-router"; @Component export default class App extends Vue { @Watch("$route") routeChange(val: Route, oldVal: Route) { // do something } }
只是這樣寫的話,監聽 $route 仍是會報錯的。
想要以上三種作法都正常執行,就還要補充以下內容:
在 src 下的 shims-vue.d.ts 中加入要掛載的內容。 表示 vue 裏面的 this 下有這些東西。
import VueRouter, { Route } from "vue-router"; declare module "vue/types/vue" { interface Vue { $router: VueRouter; // 這表示this下有這個東西 $route: Route; $https: any; // 不知道類型就定爲 any 吧(偷懶) $urls: any; $Message: any; } }
好比 在組件裏面使用 window.document 或者 document.querySelector 的時候會報錯的,npm run build 不給經過。
再好比:按需引用 element 的組件與動畫組件:
import { Button } from "element-ui"; import CollapseTransition from "element-ui/lib/transitions/collapse-transition";
npm run serve 時能夠執行,可是在 npm run build 的時候,會直接報錯的,由於沒有聲明。
正確作法:
我在 src 下新建一個文件 shime-global.d.ts ,加入內容以下:
// 聲明全局的 window ,否則使用 window.XX 時會報錯 declare var window: Window; declare var document: Document; declare module "element-ui/lib/transitions/collapse-transition"; declare module "element-ui";
固然,這個文件你加在其餘地方也能夠,起其餘名字都 OK。
可是即便配置了以上方法以後,有些地方使用 document.XXX ,好比 document.title 的時候,npm run build 仍是經過不了,因此只能這樣了:
<script lang="ts"> // 在用到 document.XXX 的文件中聲明一下便可 declare var document: any; // 此處省略 XXXX 多的代碼 </script>
好比以前的 事件的節流(throttle)與防抖(debounce)方法:
export function throttle(fn: Function, delay: number) { return function() { // 保留調用時的 this 上下文 let context = this; }
function 裏面的 this 在 npm run serve 時會報錯的,由於 tyescript 檢測到它不是在類(class)裏面。
正確作法:
在根目錄的 tsconfig.json 裏面加上 "noImplicitThis": false ,忽略 this 的類型檢查。
// 忽略 this 的類型檢查, Raise error on this expressions with an implied any type. "noImplicitThis": false,
import .vue 的文件的時候,要補全 .vue 的後綴,否則 npm run build 會報錯的。
好比:
import Nav from "@/components/nav"; // @ is an alias to /src import Footer from "@/components/footer"; // @ is an alias to /src
要修改成:
import Nav from "@/components/nav.vue"; // @ is an alias to /src import Footer from "@/components/footer.vue"; // @ is an alias to /src
報錯。
<script lang="ts"> import { Vue, Component } from "vue-property-decorator"; export default class LoadingCustom extends Vue {} </script>
如下才是正確,由於這裏的 Vue 是從 vue-property-decorator import 來的。
<script lang="ts"> import { Vue, Component } from "vue-property-decorator"; @Component export default class LoadingCustom extends Vue {} </script>
vue-class-component 官網裏面的路由的導航鉤子的用法是沒有效果的 Adding Custom Hooks
路由的導航鉤子不屬於 Vue 自己,這會致使 class 組件轉義到配置對象時導航鉤子無效,所以若是要使用導航鉤子須要在 router 的配置裏聲明(網上別人說的,還沒實踐,不肯定是否可行)。
7. tsconfig.json 的 strictPropertyInitialization 設爲 false,否則你定義一個變量就必須給它一個初始值。
position: sticky;
本項目中的文章詳情的目錄就是用了 sticky。
.anchor { position: sticky; top: 213px; margin-top: 213px; }
position:sticky 是 css 定位新增屬性;能夠說是相對定位 relative 和固定定位 fixed 的結合;它主要用在對 scroll 事件的監聽上;簡單來講,在滑動過程當中,某個元素距離其父元素的距離達到 sticky 粘性定位的要求時(好比 top:100px );position:sticky 這時的效果至關於 fixed 定位,固定到適當位置。
用法像上面那樣用便可,可是有使用條件:
一、父元素不能 overflow:hidden 或者 overflow:auto 屬性。
二、必須指定 top、bottom、left、right 4 個值之一,不然只會處於相對定位
三、父元素的高度不能低於 sticky 元素的高度
四、sticky 元素僅在其父元素內生效
App.vue 中只是寫了引用文件而已,並且 webpack 和 tsconfig.josn 裏面已經配置了別名了的。
import Nav from "@/components/nav.vue"; // @ is an alias to /src import Slider from "@/components/slider.vue"; // @ is an alias to /src import Footer from "@/components/footer.vue"; // @ is an alias to /src import ArrowUp from "@/components/arrowUp.vue"; // @ is an alias to /src import { isMobileOrPc } from "@/utils/utils";
可是,仍是會報以下的錯:
只是代碼不影響文件的打包,並且本地與生產環境的代碼也正常,沒報錯而已。
這個 eslint 的檢測目前還沒找到相關的配置能夠把這些錯誤去掉。
由於文章詳情頁面有目錄,點擊目錄時定位定相應的內容,可是這個目錄定位內容是根據錨點來作的,若是路由模式爲 hash 模式的話,原本文章詳情頁面的路由就是 #articleDetail 了,再點擊目錄的話(好比 #title2 ),會在 #articleDetail 後面再加上 #title2,一刷新會找不到這個頁面的。
# clone git clone https://github.com/biaochenxuying/blog-vue-typescript.git
# cd cd blog-vue-typescript
# install dependencies npm install
# Compiles and hot-reloads for development npm run serve
# Compiles and minifies for production npm run build
### Run your tests npm run test
### Lints and fixes files npm run lint
### Run your unit tests npm run test:unit
若是要看有後臺數據完整的效果,是要和後臺項目 blog-node 一塊兒運行才行的,否則接口請求會失敗。
雖然引入了 mock 了,可是尚未時間作模擬數據,想看具體效果,請穩步到個人網站上查看 https://biaochenxuying.cn
基於 Vue + TypeScript + Element 的 blog-vue-typescript 前臺展現: https://github.com/biaochenxuying/blog-vue-typescript
基於 react + node + express + ant + mongodb 的博客前臺,這個是筆者以前作的,效果和這個相似,地址以下:
blog-react 前臺展現: https://github.com/biaochenxuying/blog-react
推薦閱讀 :
本博客系統的系列文章:
筆者也是初學 TS ,若是文章有錯的地方,請指出,感謝。
一開始用 Vue + TS 來搭建時,我也是挺抵觸的,由於踩了好多坑,並且不少類型檢查方面也挺煩人。後面解決了,明白原理以後,是越用越爽,哈哈。
如何更好的利用 JS 的動態性和 TS 的靜態特質,咱們須要結合項目的實際狀況來進行綜合判斷。一些建議:
至於到底用不用TS,仍是要看實際項目規模、項目生命週期、團隊規模、團隊成員狀況等實際狀況綜合考慮。
其實本項目也是小項目來的,其實並不太適合加入 TypeScript ,不過這個項目是我的的項目,是爲了練手用的,因此就無傷大大雅。
將來,class-compoent 也將成爲主流,如今寫 TypeScript 之後進行 3.0 的遷移會更加方便。
天天下班後,用幾個晚上的時間來寫這篇文章,碼字不易,若是您以爲這篇文章不錯或者對你有所幫助,請給個贊或者星吧,你的點贊就是我繼續創做的最大動力。
參考文章: