Vue + TypeScript + Element 项目实战及踩坑记

前言

本文讲解如何在 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 端

Vue + TypeScript + Element 项目实战及踩坑记_第1张图片

  • 移动端

Vue + TypeScript + Element 项目实战及踩坑记_第2张图片

完整效果请看:https://biaochenxuying.cn

2. 功能

已经完成功能

登录

注册

文章列表

文章归档

标签

关于

点赞与评论

留言

历程

文章详情(支持代码语法高亮)

文章详情目录

移动端适配

github 授权登录

待优化或者实现

x使用 vuex-class

x更多 TypeScript 的优化技巧

x服务器渲染 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
  • highlight.js: 9.15.6
  • marked:0.6.1

4. 5 分钟上手 TypeScript

如果没有一点点基础,可能没学过 TypeScript 的读者会看不懂往下的内容,所以先学点基础。

TypeScript 的静态类型检查是个好东西,可以避免很多不必要的错误, 不用在调试或者项目上线的时候才发现问题 。

  • 类型注解

TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。变量定义时也要定义他的类型,比如常见的 :


 
   
   
   
   
  1. // 布尔值
  2. let isDone: boolean = false; // 相当于 js 的 let isDone = false;
  3. // 变量定义之后不可以随便变更它的类型
  4. isDone = true // 不报错
  5. isDone = "我要变为字符串" // 报错
  6. // 数字
  7. let decLiteral: number = 6; // 相当于 js 的 let decLiteral = 6;
  8. // 字符串
  9. let name: string = "bob"; // 相当于 js 的 let name = "bob";
  10. // 数组
  11. // 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:
  12. let list: number[] = [ 1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
  13. // 第二种方式是使用数组泛型,Array<元素类型>:
  14. let list: Array = [ 1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
  15. // 在 TypeScript 中,我们使用接口(Interfaces)来定义 对象 的类型。
  16. interface Person {
  17. name: string;
  18. age: number;
  19. }
  20. let tom: Person = {
  21. name: 'Tom',
  22. age: 25
  23. };
  24. // 以上 对象 的代码相当于
  25. let tom = {
  26. name: 'Tom',
  27. age: 25
  28. };
  29. // Any 可以随便变更类型 (当这个值可能来自于动态的内容,比如来自用户输入或第三方代码库)
  30. let notSure: any = 4;
  31. notSure = "我可以随便变更类型" // 不报错
  32. notSure = false; // 不报错
  33. // Void 当一个函数没有返回值时,你通常会见到其返回值类型是 void
  34. function warnUser(): void {
  35. console.log( "This is my warning message");
  36. }
  37. // 方法的参数也要定义类型,不知道就定义为 any
  38. function fetch(url: string, id : number, params: any): void {
  39. console.log( "fetch");
  40. }

以上是最简单的一些知识点,更多知识请看 TypeScript 中文官网

5. 5 分钟上手 Vue +TypeScript

  • vue-class-component
    vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:

 
   
   
   
   
  1. <template>
  2. <div>
  3. <input v-model="msg">
  4. <p>prop: {{propMessage}} p>
  5. <p>msg: {{msg}} p>
  6. <p>helloMsg: {{helloMsg}} p>
  7. <p>computed msg: {{computedMsg}} p>
  8. <button @click="greet">Greet button>
  9. div>
  10. template>
  11. <script>
  12. import Vue from 'vue'
  13. import Component from 'vue-class-component'
  14. @Component({
  15. props: {
  16. propMessage: String
  17. }
  18. })
  19. export default class App extends Vue {
  20. // initial data
  21. msg = 123
  22. // use prop values for initial data
  23. helloMsg = 'Hello, ' + this.propMessage
  24. // lifecycle hook
  25. mounted () {
  26. this.greet()
  27. }
  28. // computed
  29. get computedMsg () {
  30. return 'computed ' + this.msg
  31. }
  32. // method
  33. greet () {
  34. alert( 'greeting: ' + this.msg)
  35. }
  36. }
  37. script>

上面的代码跟下面的代码作用是一样的:


 
   
   
   
   
  1. <template>
  2. <div>
  3. <input v-model="msg">
  4. <p>prop: {{propMessage}} p>
  5. <p>msg: {{msg}} p>
  6. <p>helloMsg: {{helloMsg}} p>
  7. <p>computed msg: {{computedMsg}} p>
  8. <button @click="greet">Greet button>
  9. div>
  10. template>
  11. <script>
  12. export default {
  13. // 属性
  14. props: {
  15. propMessage: {
  16. type: String
  17. }
  18. },
  19. data () {
  20. return {
  21. msg: 123,
  22. helloMsg: 'Hello, ' + this.propMessage
  23. }
  24. },
  25. // 声明周期钩子
  26. mounted () {
  27. this.greet()
  28. },
  29. // 计算属性
  30. computed: {
  31. computedMsg () {
  32. return 'computed ' + this.msg
  33. }
  34. },
  35. // 方法
  36. methods: {
  37. greet () {
  38. alert( 'greeting: ' + this.msg)
  39. }
  40. },
  41. }
  42. 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, 更多信息,详见官方文档


 
   
   
   
   
  1. import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'
  2. @Component
  3. export class MyComponent extends Vue {
  4. @Prop()
  5. propA: number = 1
  6. @Prop({ default: 'default value' })
  7. propB: string
  8. @Prop([String, Boolean])
  9. propC: string | boolean
  10. @Prop({ type: null })
  11. propD: any
  12. @Watch('child')
  13. onChildChanged( val: string, oldVal: string) { }
  14. }

上面的代码相当于:


 
   
   
   
   
  1. export default {
  2. props: {
  3. checked: Boolean,
  4. propA: Number,
  5. propB: {
  6. type: String,
  7. default: 'default value'
  8. },
  9. propC: [ String, Boolean],
  10. propD: { type: null }
  11. }
  12. methods: {
  13. onChildChanged(val, oldVal) { }
  14. },
  15. watch: {
  16. 'child': {
  17. handler: 'onChildChanged',
  18. immediate: false,
  19. deep: false
  20. }
  21. }
  22. }
  • vuex-class
    vuex-class :在 vue-class-component 写法中 绑定 vuex 。

 
   
   
   
   
  1. import Vue from 'vue'
  2. import Component from 'vue-class-component'
  3. import {
  4. State,
  5. Getter,
  6. Action,
  7. Mutation,
  8. namespace
  9. } from 'vuex-class'
  10. const someModule = namespace( 'path/to/module')
  11. @Component
  12. export class MyComp extends Vue {
  13. @State('foo') stateFoo
  14. @State(state => state.bar) stateBar
  15. @Getter('foo') getterFoo
  16. @Action('foo') actionFoo
  17. @Mutation('foo') mutationFoo
  18. @someModule.Getter( 'foo') moduleGetterFoo
  19. // If the argument is omitted, use the property name
  20. // for each state/getter/action/mutation type
  21. @State foo
  22. @Getter bar
  23. @Action baz
  24. @Mutation qux
  25. created () {
  26. this.stateFoo // -> store.state.foo
  27. this.stateBar // -> store.state.bar
  28. this.getterFoo // -> store.getters.foo
  29. this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
  30. this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
  31. this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  32. }
  33. }

6. 用 vue-cli 搭建 项目

笔者使用最新的 vue-cli 3 搭建项目,详细的教程,请看我之前写的 vue-cli3.x 新特性及踩坑记,里面已经有详细讲解 ,但文章里面的配置和此项目不同的是,我加入了 TypeScript ,其他的配置都是 vue-cli 本来配好的了。详情请看 vue-cli 官网 。

6.1 安装及构建项目目录

安装的依赖:

Vue + TypeScript + Element 项目实战及踩坑记_第3张图片

安装过程选择的一些配置:

Vue + TypeScript + Element 项目实战及踩坑记_第4张图片

搭建好之后,初始项目结构长这样:


 
   
   
   
   
  1. ├── public // 静态页面
  2. ├── src // 主目录
  3. ├── assets // 静态资源
  4. ├── components // 组件
  5. ├── views // 页面
  6. ├── App.vue // 页面主入口
  7. ├── main.ts // 脚本主入口
  8. ├── router.ts // 路由
  9. ├── shims-tsx.d.ts // 相关 tsx 模块注入
  10. ├── shims-vue.d.ts // Vue 模块注入
  11. └── store.ts // vuex 配置
  12. ├── tests // 测试用例
  13. ├── .eslintrc.js // eslint 相关配置
  14. ├── .gitignore // git 忽略文件配置
  15. ├── babel.config.js // babel 配置
  16. ├── postcss.config.js // postcss 配置
  17. ├── package.json // 依赖
  18. └── tsconfig.json // ts 配置

奔着 大型项目的结构 来改造项目结构,改造后 :


 
   
   
   
   
  1. ├── public // 静态页面
  2. ├── src // 主目录
  3. ├── assets // 静态资源
  4. ├── filters // 过滤
  5. ├── store // vuex 配置
  6. ├── less // 样式
  7. ├── utils // 工具方法(axios封装,全局方法等)
  8. ├── views // 页面
  9. ├── App.vue // 页面主入口
  10. ├── main.ts // 脚本主入口
  11. ├── router.ts // 路由
  12. ├── shime-global.d.ts // 相关 全局或者插件 模块注入
  13. ├── shims-tsx.d.ts // 相关 tsx 模块注入
  14. ├── shims-vue.d.ts // Vue 模块注入, 使 TypeScript 支持 *.vue 后缀的文件
  15. ├── tests // 测试用例
  16. ├── .eslintrc.js // eslint 相关配置
  17. ├── postcss.config.js // postcss 配置
  18. ├── .gitignore // git 忽略文件配置
  19. ├── babel.config.js // preset 记录
  20. ├── package.json // 依赖
  21. ├── README.md // 项目 readme
  22. ├── tsconfig.json // ts 配置
  23. └── vue.config.js // webpack 配置

tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。
本项目的 tsconfig.json 配置如下 :


 
   
   
   
   
  1. {
  2. // 编译选项
  3. "compilerOptions": {
  4. // 编译输出目标 ES 版本
  5. "target": "esnext",
  6. // 采用的模块系统
  7. "module": "esnext",
  8. // 以严格模式解析
  9. "strict": true,
  10. "jsx": "preserve",
  11. // 从 tslib 导入外部帮助库: 比如__extends,__rest等
  12. "importHelpers": true,
  13. // 如何处理模块
  14. "moduleResolution": "node",
  15. // 启用装饰器
  16. "experimentalDecorators": true,
  17. "esModuleInterop": true,
  18. // 允许从没有设置默认导出的模块中默认导入
  19. "allowSyntheticDefaultImports": true,
  20. // 定义一个变量就必须给它一个初始值
  21. "strictPropertyInitialization" : false,
  22. // 允许编译javascript文件
  23. "allowJs": true,
  24. // 是否包含可以用于 debug 的 sourceMap
  25. "sourceMap": true,
  26. // 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
  27. "noImplicitThis": false,
  28. // 解析非相对模块名的基准目录
  29. "baseUrl": ".",
  30. // 给错误和消息设置样式,使用颜色和上下文。
  31. "pretty": true,
  32. // 设置引入的定义文件
  33. "types": [ "webpack-env", "mocha", "chai"],
  34. // 指定特殊模块的路径
  35. "paths": {
  36. "@/*": [ "src/*"]
  37. },
  38. // 编译过程中需要引入的库文件的列表
  39. "lib": [ "esnext", "dom", "dom.iterable", "scripthost"]
  40. },
  41. // ts 管理的文件
  42. "include": [
  43. "src/**/*.ts",
  44. "src/**/*.tsx",
  45. "src/**/*.vue",
  46. "tests/**/*.ts",
  47. "tests/**/*.tsx"
  48. ],
  49. // ts 排除的文件
  50. "exclude": [ "node_modules"]
  51. }

更多配置请看官网的 tsconfig.json 的 编译选项

本项目的 vue.config.js:


 
   
   
   
   
  1. const path = require( "path");
  2. const sourceMap = process.env.NODE_ENV === "development";
  3. module.exports = {
  4. // 基本路径
  5. publicPath: "./",
  6. // 输出文件目录
  7. outputDir: "dist",
  8. // eslint-loader 是否在保存的时候检查
  9. lintOnSave: false,
  10. // webpack配置
  11. // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
  12. chainWebpack: () => {},
  13. configureWebpack: config => {
  14. if (process.env.NODE_ENV === "production") {
  15. // 为生产环境修改配置...
  16. config.mode = "production";
  17. } else {
  18. // 为开发环境修改配置...
  19. config.mode = "development";
  20. }
  21. Object.assign(config, {
  22. // 开发生产共同配置
  23. resolve: {
  24. extensions: [ ".js", ".vue", ".json", ".ts", ".tsx"],
  25. alias: {
  26. vue$: "vue/dist/vue.js",
  27. "@": path.resolve(__dirname, "./src")
  28. }
  29. }
  30. });
  31. },
  32. // 生产环境是否生成 sourceMap 文件
  33. productionSourceMap: sourceMap,
  34. // css相关配置
  35. css: {
  36. // 是否使用css分离插件 ExtractTextPlugin
  37. extract: true,
  38. // 开启 CSS source maps?
  39. sourceMap: false,
  40. // css预设器配置项
  41. loaderOptions: {},
  42. // 启用 CSS modules for all css / pre-processor files.
  43. modules: false
  44. },
  45. // use thread-loader for babel & TS in production build
  46. // enabled by default if the machine has more than 1 cores
  47. parallel: require( "os").cpus().length > 1,
  48. // PWA 插件相关配置
  49. // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
  50. pwa: {},
  51. // webpack-dev-server 相关配置
  52. devServer: {
  53. open: process.platform === "darwin",
  54. host: "localhost",
  55. port: 3001, //8080,
  56. https: false,
  57. hotOnly: false,
  58. proxy: {
  59. // 设置代理
  60. // proxy all requests starting with /api to jsonplaceholder
  61. "/api": {
  62. // target: "https://emm.cmccbigdata.com:8443/",
  63. target: "http://localhost:3000/",
  64. // target: "http://47.106.136.114/",
  65. changeOrigin: true,
  66. ws: true,
  67. pathRewrite: {
  68. "^/api": ""
  69. }
  70. }
  71. },
  72. before: app => {}
  73. },
  74. // 第三方插件配置
  75. pluginOptions: {
  76. // ...
  77. }
  78. };

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 修改为:


 
   
   
   
   
  1. module. exports = {
  2. presets: [ "@vue/app"],
  3. plugins: [
  4. [
  5. "component",
  6. {
  7. libraryName: "element-ui",
  8. styleLibraryName: "theme-chalk"
  9. }
  10. ]
  11. ]
  12. };

接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:


 
   
   
   
   
  1. import Vue from 'vue';
  2. import { Button, Select } from 'element-ui';
  3. import App from './App.vue';
  4. Vue.component(Button.name, Button);
  5. Vue.component(Select.name, Select);
  6. /* 或写为
  7. * Vue.use(Button)
  8. * Vue.use(Select)
  9. */
  10. new Vue({
  11. el: '#app',
  12. render: h => h(App)
  13. });

6.3 完善项目目录与文件

route

使用路由懒加载功能。


 
   
   
   
   
  1. export default new Router({
  2. mode: "history",
  3. routes: [
  4. {
  5. path: "/",
  6. name: "home",
  7. component: () => import(/* webpackChunkName: "home" */ "./views/home.vue")
  8. },
  9. {
  10. path: "/articles",
  11. name: "articles",
  12. // route level code-splitting
  13. // this generates a separate chunk (articles.[hash].js) for this route
  14. // which is lazy-loaded when the route is visited.
  15. component: () =>
  16. import(/* webpackChunkName: "articles" */ "./views/articles.vue")
  17. },
  18. ]
  19. });

utils

  • utils/utils.ts 常用函数的封装, 比如 事件的节流(throttle)与防抖(debounce)方法:

 
   
   
   
   
  1. // fn是我们需要包装的事件回调, delay是时间间隔的阈值
  2. export function throttle(fn: Function, delay: number) {
  3. // last为上一次触发回调的时间, timer是定时器
  4. let last = 0,
  5. timer: any = null;
  6. // 将throttle处理结果当作函数返回
  7. return function() {
  8. // 保留调用时的this上下文
  9. let context = this;
  10. // 保留调用时传入的参数
  11. let args = arguments;
  12. // 记录本次触发回调的时间
  13. let now = + new Date();
  14. // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
  15. if (now - last < delay) {
  16. // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
  17. clearTimeout(timer);
  18. timer = setTimeout( function() {
  19. last = now;
  20. fn.apply(context, args);
  21. }, delay);
  22. } else {
  23. // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
  24. last = now;
  25. fn.apply(context, args);
  26. }
  27. };
  28. }
  • utils/config.ts 配置文件,比如 github 授权登录的回调地址、client_id、client_secret 等。

 
   
   
   
   
  1. const config = {
  2. 'oauth_uri': 'https://github.com/login/oauth/authorize',
  3. 'redirect_uri': 'https://biaochenxuying.cn/login',
  4. 'client_id': 'XXXXXXXXXX',
  5. 'client_secret': 'XXXXXXXXXX',
  6. };
  7. // 本地开发环境下
  8. if (process.env.NODE_ENV === 'development') {
  9. config.redirect_uri = "http://localhost:3001/login"
  10. config.client_id = "502176cec65773057a9e"
  11. config.client_secret = "65d444de381a026301a2c7cffb6952b9a86ac235"
  12. }
  13. export default config;

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

  • utils/urls.ts 请求接口地址,统一管理。

 
   
   
   
   
  1. // url的链接
  2. export const urls: object = {
  3. login: "login",
  4. register: "register",
  5. getArticleList: "getArticleList",
  6. };
  7. export default urls;
  • utils/https.ts axios 请求的封装。

 
   
   
   
   
  1. import axios from "axios";
  2. // 创建axios实例
  3. let service: any = {};
  4. service = axios.create({
  5. baseURL: "/api", // api的base_url
  6. timeout: 50000 // 请求超时时间
  7. });
  8. // request拦截器 axios的一些配置
  9. service.interceptors.request.use(
  10. (config: any) => {
  11. return config;
  12. },
  13. (error: any) => {
  14. // Do something with request error
  15. console.error( "error:", error); // for debug
  16. Promise.reject(error);
  17. }
  18. );
  19. // respone拦截器 axios的一些配置
  20. service.interceptors.response.use(
  21. (response: any) => {
  22. return response;
  23. },
  24. (error: any) => {
  25. console.error( "error:" + error); // for debug
  26. return Promise.reject(error);
  27. }
  28. );
  29. export default service;

把 urls 和 https 挂载到 main.ts 里面的 Vue 的 prototype 上面。


 
   
   
   
   
  1. import service from "./utils/https";
  2. import urls from "./utils/urls";
  3. Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了
  4. Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了

然后就可以统一管理接口,而且调用起来也很方便啦。比如下面 文章列表的请求。


 
   
   
   
   
  1. async handleSearch() {
  2. this.isLoading = true;
  3. const res: any = await this.$https. get( this.$urls.getArticleList, {
  4. params: this.params
  5. });
  6. this.isLoading = false;
  7. if (res.status === 200) {
  8. if (res. data.code === 0) {
  9. const data: any = res. data. data;
  10. this.articlesList = [... this.articlesList, ... data.list];
  11. this.total = data.count;
  12. this.params.pageNum++;
  13. if ( this.total === this.articlesList.length) {
  14. this.isLoadEnd = true;
  15. }
  16. } else {
  17. this.$message({
  18. message: res. data.message,
  19. type: "error"
  20. });
  21. }
  22. } else {
  23. this.$message({
  24. message: "网络错误!",
  25. type: "error"
  26. });
  27. }
  28. }

store ( Vuex )

一般大型的项目都有很多模块的,比如本项目中有公共信息(比如 token )、 用户模块、文章模块。


 
   
   
   
   
  1. ├── modules // 模块
  2. ├── user.ts // 用户模块
  3. ├── article.ts // 文章模块
  4. ├── types.ts // 类型
  5. └── index.ts // vuex 主入口
  • store/index.ts 存放公共的信息,并导入其他模块

 
   
   
   
   
  1. import Vue from "vue";
  2. import Vuex from "vuex";
  3. import * as types from "./types";
  4. import user from "./modules/user";
  5. import article from "./modules/article";
  6. Vue.use(Vuex);
  7. const initPageState = () => {
  8. return {
  9. token: ""
  10. };
  11. };
  12. const store = new Vuex.Store({
  13. strict: process.env.NODE_ENV !== "production",
  14. // 具体模块
  15. modules: {
  16. user,
  17. article
  18. },
  19. state: initPageState(),
  20. mutations: {
  21. [types.SAVE_TOKEN](state: any, pageState: any) {
  22. for ( const prop in pageState) {
  23. state[prop] = pageState[prop];
  24. }
  25. }
  26. },
  27. actions: {}
  28. });
  29. export default store;
  • types.ts

 
   
   
   
   
  1. // 公共 token
  2. export const SAVE_TOKEN = "SAVE_TOKEN";
  3. // 用户
  4. export const SAVE_USER = "SAVE_USER";
  • user.ts

 
   
   
   
   
  1. import * as types from "../types";
  2. const initPageState = () => {
  3. return {
  4. userInfo: {
  5. _id: "",
  6. name: "",
  7. avator: ""
  8. }
  9. };
  10. };
  11. const user = {
  12. state: initPageState(),
  13. mutations: {
  14. [types.SAVE_USER](state: any, pageState: any) {
  15. for ( const prop in pageState) {
  16. state[prop] = pageState[prop];
  17. }
  18. }
  19. },
  20. actions: {}
  21. };
  22. export default user;

7. markdown 渲染

markdown 渲染效果图:

Vue + TypeScript + Element 项目实战及踩坑记_第5张图片

markdown 渲染 采用了开源的 marked, 代码高亮用了 highlight.js 。

用法:

第一步:npm i marked highlight.js --save

npm i marked highlight.js --save

 
   
   
   
   

第二步: 导入封装成 markdown.js,将文章详情由字符串转成 html, 并抽离出文章目录。

marked 的封装 得感谢这位老哥。


 
   
   
   
   
  1. const highlight = require( "highlight.js");
  2. const marked = require( "marked");
  3. const tocObj = {
  4. add: function(text, level) {
  5. var anchor = `#toc${level}${++this.index}`;
  6. this.toc.push({ anchor: anchor, level: level, text: text });
  7. return anchor;
  8. },
  9. // 使用堆栈的方式处理嵌套的ul,li,level即ul的嵌套层次,1是最外层
  10. //
  11. //
  • //
  • //
  • //
  • //
  • //
  • toHTML: function() {
  • let levelStack = [];
  • let result = "";
  • const addStartUL = () => {
  • result += '
      ';
  • };
  • const addEndUL = () => {
  • result += "\n";
  • };
  • const addLI = (anchor, text) => {
  • result +=
  • '
  • '">' + text + "
  • \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 `${level} id=${anchor}>${text}${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;
  • 第三步: 使用

    
     
       
       
       
       
    1. import markdown from "@/utils/markdown";
    2. // 获取文章详情
    3. async handleSearch() {
    4. const res: any = await this.$https.post(
    5. this.$urls.getArticleDetail,
    6. this.params
    7. );
    8. if (res.status === 200) {
    9. if (res. data.code === 0) {
    10. this.articleDetail = res. data. data;
    11. // 使用 marked 转换
    12. const article = markdown.marked(res. data. data.content);
    13. article.then((response: any) => {
    14. this.articleDetail.content = response.content;
    15. this.articleDetail.toc = response.toc;
    16. });
    17. } else {
    18. // ...
    19. } else {
    20. // ...
    21. }
    22. }
    23. // 渲染
    24. "content"
    25. class="article-detail"
    26. v-html= "articleDetail.content">

    第四步:引入 monokai_sublime 的 css 样式

    <link href="http://cdn.bootcss.com/highlight.js/8.0/styles/monokai_sublime.min.css" rel="stylesheet">
    
     
       
       
       
       

    第五步:对 markdown 样式的补充

    如果不补充样式,是没有黑色背景的,字体大小等也会比较小,图片也不会居中显示

    
     
       
       
       
       
    1. /*对 markdown 样式的补充*/
    2. pre {
    3. display: block;
    4. padding: 10px;
    5. margin: 0 0 10px;
    6. font-size: 14px;
    7. line-height: 1.42857143;
    8. color: #abb2bf;
    9. background: #282c34;
    10. word-break: break-all;
    11. word-wrap: break-word;
    12. overflow: auto;
    13. }
    14. h1, h2, h3, h4, h5, h6{
    15. margin-top: 1em;
    16. /* margin-bottom: 1em; */
    17. }
    18. strong {
    19. font-weight: bold;
    20. }
    21. p > code :not( [class]) {
    22. padding: 2px 4px;
    23. font-size: 90%;
    24. color: #c7254e;
    25. background-color: #f9f2f4;
    26. border-radius: 4px;
    27. }
    28. p img{
    29. /* 图片居中 */
    30. margin: 0 auto;
    31. display: flex;
    32. }
    33. #content {
    34. font-family: "Microsoft YaHei", 'sans-serif';
    35. font-size: 16px;
    36. line-height: 30px;
    37. }
    38. #content .desc ul, #content .desc ol {
    39. color: #333333;
    40. margin: 1.5em 0 0 25px;
    41. }
    42. #content .desc h1, #content .desc h2 {
    43. border-bottom: 1px solid #eee;
    44. padding-bottom: 10px;
    45. }
    46. #content .desc a {
    47. color: #009a61;
    48. }

    8. 注意点

    对于 关于 的页面,其实是一篇文章来的,根据文章类型 type 来决定的,数据库里面 type 为 3 
    的文章,只能有一篇就是 博主介绍 ;达到了想什么时候修改内容都可以。

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

    type: 3,  // 文章类型: 1:普通文章;2:是博主简历;3 :是博主简介;
    
     
       
       
       
       
    
     
       
       
       
       
    1. // 屏幕适配( window.screen.width / 移动端设计稿宽 * 100)也即是 (window.screen.width / 750 * 100) ——*100 为了方便计算。即 font-size 值是手机 deviceWidth 与设计稿比值的 100 倍
    2. document.getElementsByTagName( 'html')[ 0].style.fontSize = window.screen.width / 7.5 + 'px';

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

    比如在设计图上一个 150 * 250 的盒子(单位 px):

    原本在 css 中的写法:

    
     
       
       
       
       
    1. width: 150px;
    2. heigth: 250px;

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

    
     
       
       
       
       
    1. width: 1.5rem; // 150 / 100 rem
    2. heigth: 2.5rem; // 250 / 100 rem

    如果你的移动端的设计稿是以宽为 1080 px 来设置的话,就用 window.screen.width / 10.8 吧。

    9. 踩坑记

    1. 我们经常在 main.ts 中给 vue.prototype 挂载实例或者内容,以方便在组件里面使用。
    
     
       
       
       
       
    1. import service from "./utils/https";
    2. import urls from "./utils/urls";
    3. Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了
    4. Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了

    然而当你在组件中直接 this.http或者this.urls 时会报错的,那是因为 http和urls 属性,并没有在 vue 实例中声明。

    1. 再比如使用 Element-uI 的 meesage。
    
     
       
       
       
       
    1. import { Message } from "element-ui";
    2. Vue.prototype.$message = Message;

    之前用法如下图:

    
     
       
       
       
       
    1. this. $message({
    2. message: '恭喜你,这是一条成功消息',
    3. type: 'success'
    4. })

    然而还是会报错的。

    再比如 监听路由的变化:

    
     
       
       
       
       
    1. import { Vue, Watch } from "vue-property-decorator";
    2. import Component from "vue-class-component";
    3. import { Route } from "vue-router";
    4. @Component
    5. export default class App extends Vue {
    6. @Watch( "$route")
    7. routeChange(val: Route, oldVal: Route) {
    8. // do something
    9. }
    10. }

    只是这样写的话,监听 $route 还是会报错的。

    想要以上三种做法都正常执行,就还要补充如下内容:

    在 src 下的 shims-vue.d.ts 中加入要挂载的内容。 表示 vue 里面的 this 下有这些东西。

    
     
       
       
       
       
    1. import VueRouter, { Route } from "vue-router";
    2. declare module "vue/types/vue" {
    3. interface Vue {
    4. $router: VueRouter; // 这表示this下有这个东西
    5. $route: Route;
    6. $https: any; // 不知道类型就定为 any 吧(偷懒)
    7. $urls: any;
    8. $Message: any;
    9. }
    10. }

    比如 在组件里面使用 window.document 或者 document.querySelector 的时候会报错的,npm run build 不给通过。

    再比如:按需引用 element 的组件与动画组件:

    
     
       
       
       
       
    1. import { Button } from "element-ui";
    2. import CollapseTransition from "element-ui/lib/transitions/collapse-transition";


    npm run serve 时可以执行,但是在 npm run build 的时候,会直接报错的,因为没有声明。

    正确做法:

    我在 src 下新建一个文件 shime-global.d.ts ,加入内容如下:

    
     
       
       
       
       
    1. // 声明全局的 window ,不然使用 window.XX 时会报错
    2. declare var window: Window;
    3. declare var document: Document;
    4. declare module "element-ui/lib/transitions/collapse-transition";
    5. declare module "element-ui";

    当然,这个文件你加在其他地方也可以,起其他名字都 OK。

    但是即使配置了以上方法之后,有些地方使用 http://document.XXX ,比如 document.title 的时候,npm run build 还是通过不了,所以只能这样了:

    
     
       
       
       
       
    1. <script lang="ts">
    2. // 在用到 document.XXX 的文件中声明一下即可
    3. declare var document: any;
    4. // 此处省略 XXXX 多的代码
    5. script>

    比如之前的 事件的节流(throttle)与防抖(debounce)方法:

    
     
       
       
       
       
    1. export function throttle(fn: Function, delay: number) {
    2. return function() {
    3. // 保留调用时的 this 上下文
    4. let context = this;
    5. }

    function 里面的 this 在 npm run serve 时会报错的,因为 tyescript 检测到它不是在类(class)里面。

    正确做法:

    在根目录的 tsconfig.json 里面加上 "noImplicitThis": false ,忽略 this 的类型检查。

    
     
       
       
       
       
    1. // 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
    2. "noImplicitThis": false,

    import .vue 的文件的时候,要补全 .vue 的后缀,不然 npm run build 会报错的。

    比如:

    
     
       
       
       
       
    1. import Nav from "@/components/nav"; // @ is an alias to /src
    2. import Footer from "@/components/footer"; // @ is an alias to /src

    要修改为:

    
     
       
       
       
       
    1. import Nav from "@/components/nav.vue"; // @ is an alias to /src
    2. import Footer from "@/components/footer.vue"; // @ is an alias to /src

    报错。

    
     
       
       
       
       
    1. <script lang="ts">
    2. import { Vue, Component } from "vue-property-decorator";
    3. export default class LoadingCustom extends Vue {}
    4. script>

    以下才是正确,因为这里的 Vue 是从 vue-property-decorator import 来的。

    
     
       
       
       
       
    1. <script lang="ts">
    2. import { Vue, Component } from "vue-property-decorator";
    3. @Component
    4. export default class LoadingCustom extends Vue {}
    5. script>

    vue-class-component 官网里面的路由的导航钩子的用法是没有效果的 Adding Custom Hooks

    路由的导航钩子不属于 Vue 本身,这会导致 class 组件转义到配置对象时导航钩子无效,因此如果要使用导航钩子需要在 router 的配置里声明(网上别人说的,还没实践,不确定是否可行)。

    本项目中的文章详情的目录就是用了 sticky。

    
     
       
       
       
       
    1. .anchor {
    2. position: sticky;
    3. top: 213px;
    4. margin-top: 213px;
    5. }

    position:sticky 是 css 定位新增属性;可以说是相对定位 relative 和固定定位 fixed 的结合;它主要用在对 scroll 事件的监听上;简单来说,在滑动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时(比如 top:100px );position:sticky 这时的效果相当于 fixed 定位,固定到适当位置。

    用法像上面那样用即可,但是有使用条件:

    1、父元素不能 overflow:hidden 或者 overflow:auto 属性。
    2、必须指定 top、bottom、left、right 4 个值之一,否则只会处于相对定位
    3、父元素的高度不能低于 sticky 元素的高度
    4、sticky 元素仅在其父元素内生效

    App.vue 中只是写了引用文件而已,而且 webpack 和 tsconfig.josn 里面已经配置了别名了的。

    
     
       
       
       
       
    1. import Nav from "@/components/nav.vue"; // @ is an alias to /src
    2. import Slider from "@/components/slider.vue"; // @ is an alias to /src
    3. import Footer from "@/components/footer.vue"; // @ is an alias to /src
    4. import ArrowUp from "@/components/arrowUp.vue"; // @ is an alias to /src
    5. import { isMobileOrPc } from "@/utils/utils";

    但是,还是会报如下的错:

    Vue + TypeScript + Element 项目实战及踩坑记_第6张图片

    只是代码不影响文件的打包,而且本地与生产环境的代码也正常,没报错而已。

    这个 eslint 的检测目前还没找到相关的配置可以把这些错误去掉。

    因为文章详情页面有目录,点击目录时定位定相应的内容,但是这个目录定位内容是根据锚点来做的,如果路由模式为 hash 模式的话,本来文章详情页面的路由就是 #articleDetail 了,再点击目录的话(比如 #title2 ),会在 #articleDetail 后面再加上 #title2,一刷新会找不到这个页面的。

    10. Build Setup

    
     
       
       
       
       
    1. // git clone
    2. git clone https: //github.com/biaochenxuying/blog-vue-typescript.git
    3. // cd
    4. cd blog-vue-typescript
    5. // install dependencies
    6. npm install
    7. // Compiles and hot-reloads for development
    8. npm run serve
    9. // Compiles and minifies for production
    10. npm run build

    如果要看有后台数据完整的效果,是要和后台项目 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

    推荐阅读 :

    本博客系统的系列文章:

    12. 最后

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

    一开始用 Vue + TS 来搭建时,我也是挺抵触的,因为踩了好多坑,而且很多类型检查方面也挺烦人。后面解决了,明白原理之后,是越用越爽,哈哈。

    Vue + TypeScript + Element 项目实战及踩坑记_第7张图片

    权衡

    如何更好的利用 JS 的动态性和 TS 的静态特质,我们需要结合项目的实际情况来进行综合判断。一些建议:

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

    其实本项目也是小项目来的,其实并不太适合加入 TypeScript ,不过这个项目是个人的项目,是为了练手用的,所以就无伤大大雅。

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

    每天下班后,用几个晚上的时间来写这篇文章,码字不易,如果您觉得这篇文章不错或者对你有所帮助,请给个赞或者星吧,你的点赞就是我继续创作的最大动力。

    你可能感兴趣的:(Vue)