Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记

  • 时间:
  • 浏览:37
  • 来源:极速快3_快3链接_极速快3链接

前言

本文讲解怎么都里能在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 。

TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript 在 2018年 势头迅猛,可谓遍地开花。

Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。2019 年 TypeScript 可能更加普及,要能熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优势。

可是有笔者就当然也要学你你什儿 必备技能,就以 边学边实践 的方法,做个博客项目来玩玩。

此项目是基于 Vue 全家桶 + TypeScript + Element-UI 的技术栈,且可能开源,github 地址 blog-vue-typescript 。

可能后后写了篇纯 Vue 项目搭建的相关文章 基于vue+mint-ui的mobile-h5的项目说明 ,有不少人加我微信,要源码来学习,或者 你你什儿 是我司的项目,这样 提供原码。

可是有做一有有一有十有几个 有的是我司的项目,且又是 vue 相关的项目来练手并开源吧。

1. 效果

效果图:

  • pc 端

  • 移动端

删剪效果请看:https://biaochenxuying.cn

2. 功能

可能完成功能

  • [x] 登录

  • [x] 注册

  • [x] 文章列表
  • [x] 文章归档
  • [x] 标签

  • [x] 关于

  • [x] 点赞与评论
  • [x] 留言
  • [x] 历程
  • [x] 文章详情(支持代码语法高亮)
  • [x] 文章详情目录
  • [x] 移动端适配
  • [x] github 授权登录

待优化可能实现

  • [ ] 使用 vuex-class
  • [ ] 更多 TypeScript 的优化技巧
  • [ ] 服务器渲染 SSR

3. 前端主要技术

所有技术有的是当前最新的。

  • 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

可能这样 你什儿 点基础,可能没学过 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 中文官网

5. 5 分钟上手 Vue +TypeScript

  • vue-class-component 

    vue-class-component 对 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-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
    }
  }
}
  • 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 搭建 项目

笔者使用最新的 vue-cli 3 搭建项目,删剪的教程,请看我后后写的 vue-cli3.x 新价值形式及踩坑记,上边可能有删剪讲解 ,但文章上边的配置和此项目不同的是,我加入了 TypeScript ,你什儿 的配置有的是 vue-cli 另一有一有十有几个 配好的了。详情请看 vue-cli 官网 。

6.1 安装及构建项目目录

安装的依赖:

安装过程选泽的你什儿 配置:

搭建好后后,初始项目价值形式长另一有一有十有几个 :

├── 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: 30001, //30003000,
    https: false,
    hotOnly: false,
    proxy: {
      // 设置代理
      // proxy all requests starting with /api to jsonplaceholder
      "/api": {
        // target: "https://emm.cmccbigdata.com:8443/",
        target: "http://localhost:30000/",
        // target: "http://47.106.136.114/",
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          "^/api": ""
        }
      }
    },
    before: app => {}
  },
  // 第三方插件配置
  pluginOptions: {
    // ...
  }
};

6.2 安装 element-ui

另一有一有十有几个 想搭配 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)
});

6.3 完善项目目录与文件

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 常用函数的封装, 比如 事件的节流(throttle)与防抖(debounce)方法:
// 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);
    }
  };
}
  • utils/config.ts 配置文件,比如 github 授权登录的回调地址、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',
};

// 本地开发环境下
if (process.env.NODE_ENV === 'development') {
  config.redirect_uri = "http://localhost:30001/login"
  config.client_id = "30002176cec657730057a9e"
  config.client_secret = "65d444de381a0263001a2c7cffb6952b9a86ac235"
}
export default config;

可能你的生产环境也要 github 登录授权得话,请在 github 上申请一有有一有十有几个 Oauth App ,把你的 redirect_uri,client_id,client_secret 的信息填在 config 上边即可。具体详情请看我写的这篇文章 github 授权登录教程与怎么都里能设计第三方授权登录的用户表

  • utils/urls.ts 请求接口地址,统一管理。
// url的链接
export const urls: object = {
  login: "login",
  register: "register",
  getArticleList: "getArticleList",
};
export default urls;
  • utils/https.ts axios 请求的封装。
import axios from "axios";

// 创建axios实例
let service: any = {};
service = axios.create({
    baseURL: "/api", // api的base_url
    timeout: 3000000 // 请求超时时间
  });

// 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 === 3000) {
      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"
      });
    }
  }

store ( Vuex )

一般大型的项目有的是可是有模块的,比如本项目饱含公共信息(比如 token )、 用户模块、文章模块。

├── modules                         // 模块

    ├── user.ts                     // 用户模块 
    
    ├── article.ts                 // 文章模块 

├── types.ts                        // 类型

└── index.ts                        // vuex 主入口
  • store/index.ts 存放公共的信息,并导入你什儿 模块
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

第二步: 导入封装成 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 === 3000) {
      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: 300px;
}

#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. 注意点

  • 关于 页面

对于 关于 的页面,随便说说是一篇文章来的,根据文章类型 type 来决定的,数据库上边 type 为 3

的文章,这样 有一篇可是 博主介绍 ;达到了想那此后后修改内容都可无需能 。

可是有当 当前路由 === '/about' 时可是请求类型为 博主介绍 的文章。

type: 3,  // 文章类型: 1:普通文章;2:是博主简历;3 :是博主简介;
  • 移动端适配

    移动端使用 rem 单位适配。
// 屏幕适配( window.screen.width / 移动端设计稿宽 * 3000)也即是 (window.screen.width / 73000 * 3000)  ——*3000 为了方便计算。即 font-size 值是手机 deviceWidth 与设计稿比值的 3000 倍
document.getElementsByTagName('html')[0].style.fontSize = window.screen.width / 7.5 + 'px';

如上:通过查询屏幕淬硬层 ,动态的设置 html 的 font-size 值,移动端的设计稿大多以宽为 73000 px 来设置的。

比如在设计图上一有有一有十有几个 3000 * 23000 的盒子(单位 px):

另一有一有十有几个 在 css 中的写法:

width: 3000px;
heigth: 23000px;

通过上述换算后,在 css 中对应的 rem 值只要能 写:

width: 1.5rem; // 3000 / 3000 rem
heigth: 2.5rem; // 23000 / 3000 rem

可能你的移动端的设计稿是以宽为 103000 px 来设置得话,就用 window.screen.width / 10.8 吧。

9. 踩坑记

  • 1. 让 vue 识别全局方法/变量
  1. 你什儿 人儿时不时在 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 就可无需能

了

然而当你在组件中直接 this.$http 可能 this.$urls 都会报错的,那是可能 $http 和 $urls 属性,并这样 在 vue 实例中声明。

  1. 再比如使用 Element-uI 的 meesage。
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;
  }
}
  • 2. 引入的模块要声明

比如 在组件上边使用 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>
  • 3. this 的类型检查

比如后后的 事件的节流(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,
  • 4. import 的 .vue 文件

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
  • 5. 装饰器 @Component

报错。

<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>
  • 6. 路由的组件导航守卫失效

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:3000px );position:sticky 这时的效果为宜 fixed 定位,固定到适当位置。

用法像上边那样用即可,或者 有使用条件:

1、父元素这样 overflow:hidden 可能 overflow:auto 属性。

2、要能 指定 top、bottom、left、right 4 个值之一,或者 只会处在相对定位

3、父元素的淬硬层 这样 低于 sticky 元素的淬硬层

4、sticky 元素仅在其父元素内生效

  • 8. eslint 报找这样 文件和装饰器的错

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 的检测目前还没找到相关的配置可无需能 把那此错误加上。

  • 9. 路由模式修改为 history

可能文章详情页面有目录,点击目录时定位定相应的内容,或者 你你什儿 目录定位内容是根据锚点来做的,可能路由模式为 hash 模式得话,另一有一有十有几个 文章详情页面的路由可是 #articleDetail 了,再点击目录得话(比如 #title2 ),会在 #articleDetail 上边加上上 #title2,一刷新会找这样 你你什儿 页面的。

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.

可能要看有后台数据删剪的效果,是要和后台项目 blog-node 同时运行才行的,不然接口请求会失败。

随便说说引入了 mock 了,或者 还这样 时间做模拟数据,看得人具体效果,请稳步到我的网站上查看 https://biaochenxuying.cn

11. 项目地址与系列相关文章

基于 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

推荐阅读 :

本博客系统的系列文章:

    1. react + node + express + ant + mongodb 的简洁兼时尚的博客网站
    1. react + Ant Design + 支持 markdown 的 blog-react 项目文档说明
    1. 基于 node + express + mongodb 的 blog-node 项目文档说明
    1. 服务器小白的我,是怎么都里能将node+mongodb项目部署在服务器上并进行性能优化的
    1. github 授权登录教程与怎么都里能设计第三方授权登录的用户表
    1. 一次网站的性能优化之路 -- 天下武功,唯快不破
    1. Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记

12. 最后

笔者也是初学 TS ,可能文章有错的地方,请指出,感谢。

一刚开始用 Vue + TS 来搭建时,我也是挺抵触的,可能踩了好多坑,或者 可是有类型检查方面也挺烦人。上边防止了,明白原理后后,是越用越爽,哈哈。

权衡

怎么都里能更好的利用 JS 的动态性和 TS 的静态特质,你什儿 人儿要能 结合项目的实际情况汇报来进行综合判断。你什儿 建议:

  • 可能是中小型项目,且生命周期有的是很长,那就直接用 JS 吧,无需被 TS 束缚住了手脚。
  • 可能是大型应用,且生命周期比较长,那建议试试 TS。
  • 可能是框架、库相似于的公共模块,那更建议用 TS 了。

至于到底用无需TS,还是要看实际项目规模、项目生命周期、团队规模、团队成员情况汇报等实际情况汇报综合考虑。

随便说说本项目也是小项目来的,随便说说无需太适合加入 TypeScript ,不过你你什儿 项目是自己的项目,是为了练手用的,可是有就无伤大大雅。

未来,class-compoent 也将成为主流,现在写 TypeScript 后后进行 3.0 的迁移会更加方便。

每天下班后,用十有几个 晚上的时间来写这篇文章,码字不易,可能您随便说说这篇文章不错可能对你有所帮助,请给个赞可能星吧,你的点赞可是继续创作的最大动力。

参考文章:

  1. vue + typescript 项目起手式

  2. TypeScript + 大型项目实战

  3. Vue全家桶+TypeScript使用总结