摘要:前言本文講解如何在項(xiàng)目中使用來(lái)搭建并開發(fā)項(xiàng)目,并在此過(guò)程中踩過(guò)的坑。具有類型系統(tǒng),且是的超集,在年勢(shì)頭迅猛,可謂遍地開花。年將會(huì)更加普及,能夠熟練掌握,并使用開發(fā)過(guò)項(xiàng)目,將更加成為前端開發(fā)者的優(yōu)勢(shì)。
前言
本文講解如何在 Vue 項(xiàng)目中使用 TypeScript 來(lái)搭建并開發(fā)項(xiàng)目,并在此過(guò)程中踩過(guò)的坑 。
TypeScript 具有類型系統(tǒng),且是 JavaScript 的超集,TypeScript 在 2018年 勢(shì)頭迅猛,可謂遍地開花。
Vue3.0 將使用 TS 重寫,重寫后的 Vue3.0 將更好的支持 TS。2019 年 TypeScript 將會(huì)更加普及,能夠熟練掌握 TS,并使用 TS 開發(fā)過(guò)項(xiàng)目,將更加成為前端開發(fā)者的優(yōu)勢(shì)。
所以筆者就當(dāng)然也要學(xué)這個(gè)必備技能,就以 邊學(xué)邊實(shí)踐 的方式,做個(gè)博客項(xiàng)目來(lái)玩玩。
此項(xiàng)目是基于 Vue 全家桶 + TypeScript + Element-UI 的技術(shù)棧,且已經(jīng)開源,github 地址 blog-vue-typescript 。
因?yàn)橹皩懥似?Vue 項(xiàng)目搭建的相關(guān)文章 基于vue+mint-ui的mobile-h5的項(xiàng)目說(shuō)明 ,有不少人加我微信,要源碼來(lái)學(xué)習(xí),但是這個(gè)是我司的項(xiàng)目,不能提供原碼。
所以做一個(gè)不是我司的項(xiàng)目,且又是 vue 相關(guān)的項(xiàng)目來(lái)練手并開源吧。
1. 效果效果圖:
pc 端
移動(dòng)端
完整效果請(qǐng)看:https://biaochenxuying.cn
2. 功能 已經(jīng)完成功能[x] 登錄
[x] 注冊(cè)
[x] 文章列表
[x] 文章歸檔
[x] 標(biāo)簽
[x] 關(guān)于
[x] 點(diǎn)贊與評(píng)論
[x] 留言
[x] 歷程
[x] 文章詳情(支持代碼語(yǔ)法高亮)
[x] 文章詳情目錄
[x] 移動(dòng)端適配
[x] github 授權(quán)登錄
待優(yōu)化或者實(shí)現(xiàn)[ ] 使用 vuex-class
[ ] 更多 TypeScript 的優(yōu)化技巧
[ ] 服務(wù)器渲染 SSR
3. 前端主要技術(shù)所有技術(shù)都是當(dāng)前最新的。
vue: ^2.6.6
typescript : ^3.2.1
element-ui: 2.6.3
vue-router : ^3.0.1
webpack: 4.28.4
vuex: ^3.0.1
axios:0.18.0
redux: 4.0.0
highlight.js: 9.15.6
marked:0.6.1
4. 5 分鐘上手 TypeScript如果沒有一點(diǎn)點(diǎn)基礎(chǔ),可能沒學(xué)過(guò) TypeScript 的讀者會(huì)看不懂往下的內(nèi)容,所以先學(xué)點(diǎn)基礎(chǔ)。
TypeScript 的靜態(tài)類型檢查是個(gè)好東西,可以避免很多不必要的錯(cuò)誤, 不用在調(diào)試或者項(xiàng)目上線的時(shí)候才發(fā)現(xiàn)問(wèn)題 。
類型注解
TypeScript 里的類型注解是一種輕量級(jí)的為函數(shù)或變量添加約束的方式。變量定義時(shí)也要定義他的類型,比如常見的 :
// 布爾值 let isDone: boolean = false; // 相當(dāng)于 js 的 let isDone = false; // 變量定義之后不可以隨便變更它的類型 isDone = true // 不報(bào)錯(cuò) isDone = "我要變?yōu)樽址? // 報(bào)錯(cuò)
// 數(shù)字 let decLiteral: number = 6; // 相當(dāng)于 js 的 let decLiteral = 6;
// 字符串 let name: string = "bob"; // 相當(dāng)于 js 的 let name = "bob";
// 數(shù)組 // 第一種,可以在元素類型后面接上 [],表示由此類型元素組成的一個(gè)數(shù)組: let list: number[] = [1, 2, 3]; // 相當(dāng)于 js 的let list = [1, 2, 3]; // 第二種方式是使用數(shù)組泛型,Array<元素類型>: let list: Array= [1, 2, 3]; // 相當(dāng)于 js 的let list = [1, 2, 3];
// 在 TypeScript 中,我們使用接口(Interfaces)來(lái)定義 對(duì)象 的類型。 interface Person { name: string; age: number; } let tom: Person = { name: "Tom", age: 25 }; // 以上 對(duì)象 的代碼相當(dāng)于 let tom = { name: "Tom", age: 25 };
// Any 可以隨便變更類型 (當(dāng)這個(gè)值可能來(lái)自于動(dòng)態(tài)的內(nèi)容,比如來(lái)自用戶輸入或第三方代碼庫(kù)) let notSure: any = 4; notSure = "我可以隨便變更類型" // 不報(bào)錯(cuò) notSure = false; // 不報(bào)錯(cuò)
// Void 當(dāng)一個(gè)函數(shù)沒有返回值時(shí),你通常會(huì)見到其返回值類型是 void function warnUser(): void { console.log("This is my warning message"); }
// 方法的參數(shù)也要定義類型,不知道就定義為 any function fetch(url: string, id : number, params: any): void { console.log("fetch"); }
以上是最簡(jiǎn)單的一些知識(shí)點(diǎn),更多知識(shí)請(qǐng)看 TypeScript 中文官網(wǎng)
5. 5 分鐘上手 Vue +TypeScriptvue-class-component?
vue-class-component 對(duì)?Vue?組件進(jìn)行了一層封裝,讓?Vue?組件語(yǔ)法在結(jié)合了?TypeScript?語(yǔ)法之后更加扁平化:
prop: {{propMessage}}
msg: {{msg}}
helloMsg: {{helloMsg}}
computed msg: {{computedMsg}}
上面的代碼跟下面的代碼作用是一樣的:
prop: {{propMessage}}
msg: {{msg}}
helloMsg: {{helloMsg}}
computed msg: {{computedMsg}}
vue-property-decorator?
vue-property-decorator 是在?vue-class-component?上增強(qiáng)了更多的結(jié)合?Vue?特性的裝飾器,新增了這 7 個(gè)裝飾器:
@Emit
@Inject
@Model
@Prop
@Provide
@Watch
@Component?(從?vue-class-component?繼承)
在這里列舉幾個(gè)常用的@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) { } }
上面的代碼相當(dāng)于:
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 } } }
vuex-class
vuex-class?:在?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"] } }6. 用 vue-cli 搭建 項(xiàng)目
筆者使用最新的 vue-cli 3 搭建項(xiàng)目,詳細(xì)的教程,請(qǐng)看我之前寫的 vue-cli3.x 新特性及踩坑記,里面已經(jīng)有詳細(xì)講解 ,但文章里面的配置和此項(xiàng)目不同的是,我加入了 TypeScript ,其他的配置都是 vue-cli 本來(lái)配好的了。詳情請(qǐng)看 vue-cli 官網(wǎng) 。
6.1 安裝及構(gòu)建項(xiàng)目目錄安裝的依賴:
安裝過(guò)程選擇的一些配置:
搭建好之后,初始項(xiàng)目結(jié)構(gòu)長(zhǎng)這樣:
├── public // 靜態(tài)頁(yè)面 ├── src // 主目錄 ├── assets // 靜態(tài)資源 ├── components // 組件 ├── views // 頁(yè)面 ├── App.vue // 頁(yè)面主入口 ├── main.ts // 腳本主入口 ├── router.ts // 路由 ├── shims-tsx.d.ts // 相關(guān) tsx 模塊注入 ├── shims-vue.d.ts // Vue 模塊注入 └── store.ts // vuex 配置 ├── tests // 測(cè)試用例 ├── .eslintrc.js // eslint 相關(guān)配置 ├── .gitignore // git 忽略文件配置 ├── babel.config.js // babel 配置 ├── postcss.config.js // postcss 配置 ├── package.json // 依賴 └── tsconfig.json // ts 配置
奔著 大型項(xiàng)目的結(jié)構(gòu) 來(lái)改造項(xiàng)目結(jié)構(gòu),改造后 :
├── public // 靜態(tài)頁(yè)面 ├── src // 主目錄 ├── assets // 靜態(tài)資源 ├── filters // 過(guò)濾 ├── store // vuex 配置 ├── less // 樣式 ├── utils // 工具方法(axios封裝,全局方法等) ├── views // 頁(yè)面 ├── App.vue // 頁(yè)面主入口 ├── main.ts // 腳本主入口 ├── router.ts // 路由 ├── shime-global.d.ts // 相關(guān) 全局或者插件 模塊注入 ├── shims-tsx.d.ts // 相關(guān) tsx 模塊注入 ├── shims-vue.d.ts // Vue 模塊注入, 使 TypeScript 支持 *.vue 后綴的文件 ├── tests // 測(cè)試用例 ├── .eslintrc.js // eslint 相關(guān)配置 ├── postcss.config.js // postcss 配置 ├── .gitignore // git 忽略文件配置 ├── babel.config.js // preset 記錄 ├── package.json // 依賴 ├── README.md // 項(xiàng)目 readme ├── tsconfig.json // ts 配置 └── vue.config.js // webpack 配置
tsconfig.json 文件中指定了用來(lái)編譯這個(gè)項(xiàng)目的根文件和編譯選項(xiàng)。
本項(xiàng)目的 tsconfig.json 配置如下 :
{ // 編譯選項(xiàng) "compilerOptions": { // 編譯輸出目標(biāo) ES 版本 "target": "esnext", // 采用的模塊系統(tǒng) "module": "esnext", // 以嚴(yán)格模式解析 "strict": true, "jsx": "preserve", // 從 tslib 導(dǎo)入外部幫助庫(kù): 比如__extends,__rest等 "importHelpers": true, // 如何處理模塊 "moduleResolution": "node", // 啟用裝飾器 "experimentalDecorators": true, "esModuleInterop": true, // 允許從沒有設(shè)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入 "allowSyntheticDefaultImports": true, // 定義一個(gè)變量就必須給它一個(gè)初始值 "strictPropertyInitialization" : false, // 允許編譯javascript文件 "allowJs": true, // 是否包含可以用于 debug 的 sourceMap "sourceMap": true, // 忽略 this 的類型檢查, Raise error on this expressions with an implied any type. "noImplicitThis": false, // 解析非相對(duì)模塊名的基準(zhǔn)目錄 "baseUrl": ".", // 給錯(cuò)誤和消息設(shè)置樣式,使用顏色和上下文。 "pretty": true, // 設(shè)置引入的定義文件 "types": ["webpack-env", "mocha", "chai"], // 指定特殊模塊的路徑 "paths": { "@/*": ["src/*"] }, // 編譯過(guò)程中需要引入的庫(kù)文件的列表 "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, // ts 管理的文件 "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], // ts 排除的文件 "exclude": ["node_modules"] }
更多配置請(qǐng)看官網(wǎng)的 tsconfig.json 的 編譯選項(xiàng)
本項(xiàng)目的 vue.config.js:
const path = require("path"); const sourceMap = process.env.NODE_ENV === "development"; module.exports = { // 基本路徑 publicPath: "./", // 輸出文件目錄 outputDir: "dist", // eslint-loader 是否在保存的時(shí)候檢查 lintOnSave: false, // webpack配置 // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md chainWebpack: () => {}, configureWebpack: config => { if (process.env.NODE_ENV === "production") { // 為生產(chǎn)環(huán)境修改配置... config.mode = "production"; } else { // 為開發(fā)環(huán)境修改配置... config.mode = "development"; } Object.assign(config, { // 開發(fā)生產(chǎn)共同配置 resolve: { extensions: [".js", ".vue", ".json", ".ts", ".tsx"], alias: { vue$: "vue/dist/vue.js", "@": path.resolve(__dirname, "./src") } } }); }, // 生產(chǎn)環(huán)境是否生成 sourceMap 文件 productionSourceMap: sourceMap, // css相關(guān)配置 css: { // 是否使用css分離插件 ExtractTextPlugin extract: true, // 開啟 CSS source maps? sourceMap: false, // css預(yù)設(shè)器配置項(xiàng) 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 插件相關(guān)配置 // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa pwa: {}, // webpack-dev-server 相關(guān)配置 devServer: { open: process.platform === "darwin", host: "localhost", port: 3001, //8080, https: false, hotOnly: false, proxy: { // 設(shè)置代理 // 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: { // ... } };6.2 安裝 element-ui
本來(lái)想搭配 iview-ui 來(lái)用的,但后續(xù)還想把這個(gè)項(xiàng)目搞成 ssr 的,而 vue + typescript + iview + Nuxt.js 的服務(wù)端渲染還有不少坑, 而 vue + typescript + element + Nuxt.js 對(duì) ssr 的支持已經(jīng)不錯(cuò)了,所以選擇了 element-ui 。
安裝:
npm i element-ui -S
按需引入, 借助?babel-plugin-component,我們可以只引入需要的組件,以達(dá)到減小項(xiàng)目體積的目的。
npm install babel-plugin-component -D
然后,將 babel.config.js 修改為:
module.exports = { presets: ["@vue/app"], plugins: [ [ "component", { libraryName: "element-ui", styleLibraryName: "theme-chalk" } ] ] };
接下來(lái),如果你只希望引入部分組件,比如 Button 和 Select,那么需要在 main.js 中寫入以下內(nèi)容:
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); /* 或?qū)憺? * Vue.use(Button) * Vue.use(Select) */ new Vue({ el: "#app", render: h => h(App) });6.3 完善項(xiàng)目目錄與文件 route
使用路由懶加載功能。
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") }, ] });utils
utils/utils.ts 常用函數(shù)的封裝, 比如 事件的節(jié)流(throttle)與防抖(debounce)方法:
// fn是我們需要包裝的事件回調(diào), delay是時(shí)間間隔的閾值 export function throttle(fn: Function, delay: number) { // last為上一次觸發(fā)回調(diào)的時(shí)間, timer是定時(shí)器 let last = 0, timer: any = null; // 將throttle處理結(jié)果當(dāng)作函數(shù)返回 return function() { // 保留調(diào)用時(shí)的this上下文 let context = this; // 保留調(diào)用時(shí)傳入的參數(shù) let args = arguments; // 記錄本次觸發(fā)回調(diào)的時(shí)間 let now = +new Date(); // 判斷上次觸發(fā)的時(shí)間和本次觸發(fā)的時(shí)間差是否小于時(shí)間間隔的閾值 if (now - last < delay) { // 如果時(shí)間間隔小于我們?cè)O(shè)定的時(shí)間間隔閾值,則為本次觸發(fā)操作設(shè)立一個(gè)新的定時(shí)器 clearTimeout(timer); timer = setTimeout(function() { last = now; fn.apply(context, args); }, delay); } else { // 如果時(shí)間間隔超出了我們?cè)O(shè)定的時(shí)間間隔閾值,那就不等了,無(wú)論如何要反饋給用戶一次響應(yīng) last = now; fn.apply(context, args); } }; }
utils/config.ts 配置文件,比如 github 授權(quán)登錄的回調(diào)地址、client_id、client_secret 等。
const config = { "oauth_uri": "https://github.com/login/oauth/authorize", "redirect_uri": "https://biaochenxuying.cn/login", "client_id": "XXXXXXXXXX", "client_secret": "XXXXXXXXXX", }; // 本地開發(fā)環(huán)境下 if (process.env.NODE_ENV === "development") { config.redirect_uri = "http://localhost:3001/login" config.client_id = "502176cec65773057a9e" config.client_secret = "65d444de381a026301a2c7cffb6952b9a86ac235" } export default config;
如果你的生產(chǎn)環(huán)境也要 github 登錄授權(quán)的話,請(qǐng)?jiān)?github 上申請(qǐng)一個(gè) Oauth App ,把你的 redirect_uri,client_id,client_secret 的信息填在 config 里面即可。具體詳情請(qǐng)看我寫的這篇文章 github 授權(quán)登錄教程與如何設(shè)計(jì)第三方授權(quán)登錄的用戶表
utils/urls.ts 請(qǐng)求接口地址,統(tǒng)一管理。
// url的鏈接 export const urls: object = { login: "login", register: "register", getArticleList: "getArticleList", }; export default urls;
utils/https.ts axios 請(qǐng)求的封裝。
import axios from "axios"; // 創(chuàng)建axios實(shí)例 let service: any = {}; service = axios.create({ baseURL: "/api", // api的base_url timeout: 50000 // 請(qǐng)求超時(shí)時(shí)間 }); // 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; // 其他頁(yè)面在使用 axios 的時(shí)候直接 this.$http 就可以了 Vue.prototype.$urls = urls; // 其他頁(yè)面在使用 urls 的時(shí)候直接 this.$urls 就可以了
然后就可以統(tǒng)一管理接口,而且調(diào)用起來(lái)也很方便啦。比如下面 文章列表的請(qǐng)求。
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: "網(wǎng)絡(luò)錯(cuò)誤!", type: "error" }); } }store ( Vuex )
一般大型的項(xiàng)目都有很多模塊的,比如本項(xiàng)目中有公共信息(比如 token )、 用戶模塊、文章模塊。
├── modules // 模塊 ├── user.ts // 用戶模塊 ├── article.ts // 文章模塊 ├── types.ts // 類型 └── index.ts // vuex 主入口
store/index.ts 存放公共的信息,并導(dǎo)入其他模塊
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;
types.ts
// 公共 token export const SAVE_TOKEN = "SAVE_TOKEN"; // 用戶 export const SAVE_USER = "SAVE_USER";
user.ts
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;7. markdown 渲染
markdown 渲染效果圖:
markdown 渲染 采用了開源的 marked, 代碼高亮用了 highlight.js 。
用法:
第一步:npm i marked highlight.js --save
npm i marked highlight.js --save
第二步: 導(dǎo)入封裝成 markdown.js,將文章詳情由字符串轉(zhuǎn)成 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是最外層 //
第三步: 使用
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 轉(zhuǎn)換 const article = markdown.marked(res.data.data.content); article.then((response: any) => { this.articleDetail.content = response.content; this.articleDetail.toc = response.toc; }); } else { // ... } else { // ... } } // 渲染
第四步:引入 monokai_sublime 的 css 樣式
第五步:對(duì) markdown 樣式的補(bǔ)充
如果不補(bǔ)充樣式,是沒有黑色背景的,字體大小等也會(huì)比較小,圖片也不會(huì)居中顯示
/*對(duì) markdown 樣式的補(bǔ)充*/ 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; }8. 注意點(diǎn)
關(guān)于 頁(yè)面
對(duì)于 關(guān)于 的頁(yè)面,其實(shí)是一篇文章來(lái)的,根據(jù)文章類型 type 來(lái)決定的,數(shù)據(jù)庫(kù)里面 type 為 3
的文章,只能有一篇就是 博主介紹 ;達(dá)到了想什么時(shí)候修改內(nèi)容都可以。
所以當(dāng) 當(dāng)前路由 === "/about" 時(shí)就是請(qǐng)求類型為 博主介紹 的文章。
type: 3, // 文章類型: 1:普通文章;2:是博主簡(jiǎn)歷;3 :是博主簡(jiǎn)介;
移動(dòng)端適配
移動(dòng)端使用 rem 單位適配。
// 屏幕適配( window.screen.width / 移動(dòng)端設(shè)計(jì)稿寬 * 100)也即是 (window.screen.width / 750 * 100) ——*100 為了方便計(jì)算。即 font-size 值是手機(jī) deviceWidth 與設(shè)計(jì)稿比值的 100 倍 document.getElementsByTagName("html")[0].style.fontSize = window.screen.width / 7.5 + "px";
如上:通過(guò)查詢屏幕寬度,動(dòng)態(tài)的設(shè)置 html 的 font-size 值,移動(dòng)端的設(shè)計(jì)稿大多以寬為 750 px 來(lái)設(shè)置的。
比如在設(shè)計(jì)圖上一個(gè) 150 * 250 的盒子(單位 px):
原本在 css 中的寫法:
width: 150px; heigth: 250px;
通過(guò)上述換算后,在 css 中對(duì)應(yīng)的 rem 值只需要寫:
width: 1.5rem; // 150 / 100 rem heigth: 2.5rem; // 250 / 100 rem
如果你的移動(dòng)端的設(shè)計(jì)稿是以寬為 1080 px 來(lái)設(shè)置的話,就用 window.screen.width / 10.8 吧。
9. 踩坑記1. 讓 vue 識(shí)別全局方法/變量
我們經(jīng)常在 main.ts 中給 vue.prototype 掛載實(shí)例或者內(nèi)容,以方便在組件里面使用。
import service from "./utils/https"; import urls from "./utils/urls"; Vue.prototype.$https = service; // 其他頁(yè)面在使用 axios 的時(shí)候直接 this.$http 就可以了 Vue.prototype.$urls = urls; // 其他頁(yè)面在使用 urls 的時(shí)候直接 this.$urls 就可以了
然而當(dāng)你在組件中直接 this.$http 或者 this.$urls 時(shí)會(huì)報(bào)錯(cuò)的,那是因?yàn)?$http 和 $urls 屬性,并沒有在 vue 實(shí)例中聲明。
再比如使用 Element-uI 的 meesage。
import { Message } from "element-ui"; Vue.prototype.$message = Message;
之前用法如下圖:
this.$message({ message: "恭喜你,這是一條成功消息", type: "success" })
然而還是會(huì)報(bào)錯(cuò)的。
再比如 監(jiān)聽路由的變化:
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 } }
只是這樣寫的話,監(jiān)聽 $route 還是會(huì)報(bào)錯(cuò)的。
想要以上三種做法都正常執(zhí)行,就還要補(bǔ)充如下內(nèi)容:
在 src 下的 shims-vue.d.ts 中加入要掛載的內(nèi)容。 表示 vue 里面的 this 下有這些東西。
import VueRouter, { Route } from "vue-router"; declare module "vue/types/vue" { interface Vue { $router: VueRouter; // 這表示this下有這個(gè)東西 $route: Route; $https: any; // 不知道類型就定為 any 吧(偷懶) $urls: any; $Message: any; } }
2. 引入的模塊要聲明
比如 在組件里面使用 window.document 或者 document.querySelector 的時(shí)候會(huì)報(bào)錯(cuò)的,npm run build 不給通過(guò)。
再比如:按需引用 element 的組件與動(dòng)畫組件:
import { Button } from "element-ui"; import CollapseTransition from "element-ui/lib/transitions/collapse-transition";
npm run serve 時(shí)可以執(zhí)行,但是在 npm run build 的時(shí)候,會(huì)直接報(bào)錯(cuò)的,因?yàn)闆]有聲明。
正確做法:
我在 src 下新建一個(gè)文件 shime-global.d.ts ,加入內(nèi)容如下:
// 聲明全局的 window ,不然使用 window.XX 時(shí)會(huì)報(bào)錯(cuò) declare var window: Window; declare var document: Document; declare module "element-ui/lib/transitions/collapse-transition"; declare module "element-ui";
當(dāng)然,這個(gè)文件你加在其他地方也可以,起其他名字都 OK。
但是即使配置了以上方法之后,有些地方使用 document.XXX ,比如 document.title 的時(shí)候,npm run build 還是通過(guò)不了,所以只能這樣了:
3. this 的類型檢查
比如之前的 事件的節(jié)流(throttle)與防抖(debounce)方法:
export function throttle(fn: Function, delay: number) { return function() { // 保留調(diào)用時(shí)的 this 上下文 let context = this; }
function 里面的 this 在 npm run serve 時(shí)會(huì)報(bào)錯(cuò)的,因?yàn)?tyescript 檢測(cè)到它不是在類(class)里面。
正確做法:
在根目錄的 tsconfig.json 里面加上 "noImplicitThis": false ,忽略 this 的類型檢查。
// 忽略 this 的類型檢查, Raise error on this expressions with an implied any type. "noImplicitThis": false,
4. import 的 .vue 文件
import .vue 的文件的時(shí)候,要補(bǔ)全 .vue 的后綴,不然 npm run build 會(huì)報(bào)錯(cuò)的。
比如:
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
5. 裝飾器 @Component
報(bào)錯(cuò)。
以下才是正確,因?yàn)檫@里的 Vue 是從 vue-property-decorator import 來(lái)的。
6. 路由的組件導(dǎo)航守衛(wèi)失效
vue-class-component 官網(wǎng)里面的路由的導(dǎo)航鉤子的用法是沒有效果的 Adding Custom Hooks
路由的導(dǎo)航鉤子不屬于 Vue 本身,這會(huì)導(dǎo)致 class 組件轉(zhuǎn)義到配置對(duì)象時(shí)導(dǎo)航鉤子無(wú)效,因此如果要使用導(dǎo)航鉤子需要在 router 的配置里聲明(網(wǎng)上別人說(shuō)的,還沒實(shí)踐,不確定是否可行)。
7. tsconfig.json 的 strictPropertyInitialization 設(shè)為 false,不然你定義一個(gè)變量就必須給它一個(gè)初始值。
position: sticky;
本項(xiàng)目中的文章詳情的目錄就是用了 sticky。
.anchor { position: sticky; top: 213px; margin-top: 213px; }
position:sticky 是 css 定位新增屬性;可以說(shuō)是相對(duì)定位 relative 和固定定位 fixed 的結(jié)合;它主要用在對(duì) scroll 事件的監(jiān)聽上;簡(jiǎn)單來(lái)說(shuō),在滑動(dòng)過(guò)程中,某個(gè)元素距離其父元素的距離達(dá)到 sticky 粘性定位的要求時(shí)(比如 top:100px );position:sticky 這時(shí)的效果相當(dāng)于 fixed 定位,固定到適當(dāng)位置。
用法像上面那樣用即可,但是有使用條件:
1、父元素不能 overflow:hidden 或者 overflow:auto 屬性。
2、必須指定 top、bottom、left、right 4 個(gè)值之一,否則只會(huì)處于相對(duì)定位
3、父元素的高度不能低于 sticky 元素的高度
4、sticky 元素僅在其父元素內(nèi)生效
8. eslint 報(bào)找不到文件和裝飾器的錯(cuò)
App.vue 中只是寫了引用文件而已,而且 webpack 和 tsconfig.josn 里面已經(jīng)配置了別名了的。
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";
但是,還是會(huì)報(bào)如下的錯(cuò):
只是代碼不影響文件的打包,而且本地與生產(chǎn)環(huán)境的代碼也正常,沒報(bào)錯(cuò)而已。
這個(gè) eslint 的檢測(cè)目前還沒找到相關(guān)的配置可以把這些錯(cuò)誤去掉。
9. 路由模式修改為 history
因?yàn)槲恼略斍轫?yè)面有目錄,點(diǎn)擊目錄時(shí)定位定相應(yīng)的內(nèi)容,但是這個(gè)目錄定位內(nèi)容是根據(jù)錨點(diǎn)來(lái)做的,如果路由模式為 hash 模式的話,本來(lái)文章詳情頁(yè)面的路由就是 #articleDetail 了,再點(diǎn)擊目錄的話(比如 #title2 ),會(huì)在 #articleDetail 后面再加上 #title2,一刷新會(huì)找不到這個(gè)頁(yè)面的。
10. Build Setup# 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
Customize configuration
See Configuration Reference.
如果要看有后臺(tái)數(shù)據(jù)完整的效果,是要和后臺(tái)項(xiàng)目 blog-node 一起運(yùn)行才行的,不然接口請(qǐng)求會(huì)失敗。
雖然引入了 mock 了,但是還沒有時(shí)間做模擬數(shù)據(jù),想看具體效果,請(qǐng)穩(wěn)步到我的網(wǎng)站上查看 https://biaochenxuying.cn
11. 項(xiàng)目地址與系列相關(guān)文章基于 Vue + TypeScript + Element 的 blog-vue-typescript 前臺(tái)展示: https://github.com/biaochenxuying/blog-vue-typescript
基于 react + node + express + ant + mongodb 的博客前臺(tái),這個(gè)是筆者之前做的,效果和這個(gè)類似,地址如下:
blog-react 前臺(tái)展示: https://github.com/biaochenxuying/blog-react
推薦閱讀 :
本博客系統(tǒng)的系列文章:
react + node + express + ant + mongodb 的簡(jiǎn)潔兼時(shí)尚的博客網(wǎng)站
react + Ant Design + 支持 markdown 的 blog-react 項(xiàng)目文檔說(shuō)明
基于 node + express + mongodb 的 blog-node 項(xiàng)目文檔說(shuō)明
服務(wù)器小白的我,是如何將node+mongodb項(xiàng)目部署在服務(wù)器上并進(jìn)行性能優(yōu)化的
github 授權(quán)登錄教程與如何設(shè)計(jì)第三方授權(quán)登錄的用戶表
一次網(wǎng)站的性能優(yōu)化之路 -- 天下武功,唯快不破
Vue + TypeScript + Element 搭建簡(jiǎn)潔時(shí)尚的博客網(wǎng)站及踩坑記
12. 最后筆者也是初學(xué) TS ,如果文章有錯(cuò)的地方,請(qǐng)指出,感謝。
一開始用 Vue + TS 來(lái)搭建時(shí),我也是挺抵觸的,因?yàn)椴攘撕枚嗫?,而且很多類型檢查方面也挺煩人。后面解決了,明白原理之后,是越用越爽,哈哈。
權(quán)衡如何更好的利用 JS 的動(dòng)態(tài)性和 TS 的靜態(tài)特質(zhì),我們需要結(jié)合項(xiàng)目的實(shí)際情況來(lái)進(jìn)行綜合判斷。一些建議:
如果是中小型項(xiàng)目,且生命周期不是很長(zhǎng),那就直接用 JS 吧,不要被 TS 束縛住了手腳。
如果是大型應(yīng)用,且生命周期比較長(zhǎng),那建議試試 TS。
如果是框架、庫(kù)之類的公共模塊,那更建議用 TS 了。
至于到底用不用TS,還是要看實(shí)際項(xiàng)目規(guī)模、項(xiàng)目生命周期、團(tuán)隊(duì)規(guī)模、團(tuán)隊(duì)成員情況等實(shí)際情況綜合考慮。
其實(shí)本項(xiàng)目也是小項(xiàng)目來(lái)的,其實(shí)并不太適合加入 TypeScript ,不過(guò)這個(gè)項(xiàng)目是個(gè)人的項(xiàng)目,是為了練手用的,所以就無(wú)傷大大雅。
未來(lái),class-compoent 也將成為主流,現(xiàn)在寫 TypeScript 以后進(jìn)行 3.0 的遷移會(huì)更加方便。
每天下班后,用幾個(gè)晚上的時(shí)間來(lái)寫這篇文章,碼字不易,如果您覺得這篇文章不錯(cuò)或者對(duì)你有所幫助,請(qǐng)給個(gè)贊或者星吧,你的點(diǎn)贊就是我繼續(xù)創(chuàng)作的最大動(dòng)力。
參考文章:
vue + typescript 項(xiàng)目起手式
TypeScript + 大型項(xiàng)目實(shí)戰(zhàn)
對(duì) 全棧修煉 有興趣的朋友可以掃下方二維碼關(guān)注我的公眾號(hào)
我會(huì)不定期更新有價(jià)值的內(nèi)容,長(zhǎng)期運(yùn)營(yíng)。
關(guān)注公眾號(hào)并回復(fù) 福利 可領(lǐng)取免費(fèi)學(xué)習(xí)資料,福利詳情請(qǐng)猛戳: Python、Java、Linux、Go、node、vue、react、javaScript
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/109257.html
摘要:具體問(wèn)題,就是中通過(guò)標(biāo)簽引入一個(gè)第三方的圖片地址,報(bào)。解決方案如原網(wǎng)址顯示此圖片來(lái)自微信公眾平臺(tái),未經(jīng)允許不得應(yīng)用方法在標(biāo)簽里加這樣存在第三方網(wǎng)站上的圖片,在你的網(wǎng)站上就可以訪問(wèn)了。 showImg(https://segmentfault.com/img/bVbtK8u?w=436&h=284); 問(wèn)題 筆者網(wǎng)站的圖片都是上傳到第三方網(wǎng)站上的,比如 簡(jiǎn)書、掘金、七牛云上的,但是最近簡(jiǎn)...
摘要:前言都到了,所以是時(shí)候玩轉(zhuǎn)一下的新特性了。安裝的包名稱由改成了。方法一原因的配置改變了,導(dǎo)致正確的不能用。打開終端,切換到根路徑文件里面修改為方法二是默認(rèn)路徑修改了路徑會(huì)出現(xiàn)錯(cuò)誤。按上面的方法修改完,再全局卸載果然就成功了。 showImg(https://segmentfault.com/img/remote/1460000016423946); 前言 vue-cli 都到 3.0....
摘要:執(zhí)行過(guò)程如下實(shí)現(xiàn)瀏覽器的前進(jìn)后退第二個(gè)方法就是用兩個(gè)棧實(shí)現(xiàn)瀏覽器的前進(jìn)后退功能。我們使用兩個(gè)棧,和,我們把首次瀏覽的頁(yè)面依次壓入棧,當(dāng)點(diǎn)擊后退按鈕時(shí),再依次從棧中出棧,并將出棧的數(shù)據(jù)依次放入棧。 showImg(https://segmentfault.com/img/bVbtK6U?w=1280&h=910); 如果要你實(shí)現(xiàn)一個(gè)前端路由,應(yīng)該如何實(shí)現(xiàn)瀏覽器的前進(jìn)與后退 ? 2. 問(wèn)題...
摘要:五六月份推薦集合查看最新的請(qǐng)點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語(yǔ)。葉上初陽(yáng)乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長(zhǎng)安旅。五月漁郎相憶否。小楫輕舟,夢(mèng)入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請(qǐng)::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...
閱讀 1587·2021-09-26 09:46
閱讀 2675·2021-09-07 09:59
閱讀 2760·2021-09-07 09:59
閱讀 1887·2019-08-30 14:20
閱讀 936·2019-08-26 13:39
閱讀 3183·2019-08-26 12:24
閱讀 781·2019-08-26 11:55
閱讀 1222·2019-08-23 16:49