zl程序教程

您现在的位置是:首页 >  前端

当前栏目

Vue + TypeScript + Element 项目实践(简洁时尚博客网站)及踩坑记(中)

Vue博客typescript项目网站 实践 Element 简洁
2023-09-27 14:25:58 时间
6. 用 vue-cli 搭建 项目

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


6.1 安装及构建项目目录


安装的依赖


微信图片_20220513222136.png


安装过程选择的一些配置


微信图片_20220513222150.png


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

├── 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: {

 // ...

};


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: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 授权登录教程与如何设计第三方授权登录的用户表


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: 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 

 }


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 渲染效果图:


微信图片_20220513222816.png


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 

 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;