Vue + TypeScript + Element 项目实践(简洁时尚博客网站)及踩坑记(中)
笔者使用最新的 vue-cli 3 搭建项目 详细的教程 请看我之前写的 vue-cli3.x 新特性及踩坑记 里面已经有详细讲解 但文章里面的配置和此项目不同的是 我加入了 TypeScript 其他的配置都是 vue-cli 本来配好的了。详情请看 vue-cli 官网 。
安装的依赖
安装过程选择的一些配置
搭建好之后 初始项目结构长这样 ├── public // 静态页面 ├── src // 主目录 ├── assets // 静态资源 ├── components // 组件 ├── views // 页面 ├── App.vue // 页面主入口 ├── main.ts // 脚本主入口 ├── router.ts // 路由 ├── shims-tsx.d.ts // 相关 tsx 模块注入 ├── shims-vue.d.ts // Vue 模块注入 └── store.ts // vuex 配置 ├── tests // 测试用例 ├── .eslintrc.js // eslint 相关配置 ├── .gitignore // git 忽略文件配置 ├── babel.config.js // babel 配置 ├── postcss.config.js // postcss 配置 ├── package.json // 依赖 └── tsconfig.json // ts 配置
奔着 大型项目的结构 来改造项目结构 改造后 :
├── public // 静态页面 ├── src // 主目录 ├── assets // 静态资源 ├── filters // 过滤 ├── store // vuex 配置 ├── less // 样式 ├── utils // 工具方法(axios封装 全局方法等) ├── views // 页面 ├── App.vue // 页面主入口 ├── main.ts // 脚本主入口 ├── router.ts // 路由 ├── shime-global.d.ts // 相关 全局或者插件 模块注入 ├── shims-tsx.d.ts // 相关 tsx 模块注入 ├── shims-vue.d.ts // Vue 模块注入, 使 TypeScript 支持 *.vue 后缀的文件 ├── tests // 测试用例 ├── .eslintrc.js // eslint 相关配置 ├── postcss.config.js // postcss 配置 ├── .gitignore // git 忽略文件配置 ├── babel.config.js // preset 记录 ├── package.json // 依赖 ├── README.md // 项目 readme ├── tsconfig.json // ts 配置 └── vue.config.js // webpack 配置
tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。
本项目的 tsconfig.json 配置如下
{ // 编译选项 compilerOptions : { // 编译输出目标 ES 版本 target : esnext , // 采用的模块系统 module : esnext , // 以严格模式解析 strict : true, jsx : preserve , // 从 tslib 导入外部帮助库: 比如__extends __rest等 importHelpers : true, // 如何处理模块 moduleResolution : node , // 启用装饰器 experimentalDecorators : true, esModuleInterop : true, // 允许从没有设置默认导出的模块中默认导入 allowSyntheticDefaultImports : true, // 定义一个变量就必须给它一个初始值 strictPropertyInitialization : false, // 允许编译javascript文件 allowJs : true, // 是否包含可以用于 debug 的 sourceMap sourceMap : true, // 忽略 this 的类型检查, Raise error on this expressions with an implied any type. noImplicitThis : false, // 解析非相对模块名的基准目录 baseUrl : . , // 给错误和消息设置样式 使用颜色和上下文。 pretty : true, // 设置引入的定义文件 types : [ webpack-env , mocha , chai ], // 指定特殊模块的路径 paths : { /* : [ src/* ] // 编译过程中需要引入的库文件的列表 lib : [ esnext , dom , dom.iterable , scripthost ] // ts 管理的文件 include : [ src/**/*.ts , src/**/*.tsx , src/**/*.vue , tests/**/*.ts , tests/**/*.tsx // ts 排除的文件 exclude : [ node_modules ] }
更多配置请看官网的 tsconfig.json 的 编译选项
本项目的 vue.config.js:
const path require( path const sourceMap process.env.NODE_ENV development module.exports { // 基本路径 publicPath: ./ , // 输出文件目录 outputDir: dist , // eslint-loader 是否在保存的时候检查 lintOnSave: false, // webpack配置 // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md chainWebpack: () {}, configureWebpack: config { if (process.env.NODE_ENV production ) { // 为生产环境修改配置... config.mode production } else { // 为开发环境修改配置... config.mode development Object.assign(config, { // 开发生产共同配置 resolve: { extensions: [ .js , .vue , .json , .ts , .tsx ], alias: { vue$: vue/dist/vue.js , : path.resolve(__dirname, ./src ) // 生产环境是否生成 sourceMap 文件 productionSourceMap: sourceMap, // css相关配置 css: { // 是否使用css分离插件 ExtractTextPlugin extract: true, // 开启 CSS source maps? sourceMap: false, // css预设器配置项 loaderOptions: {}, // 启用 CSS modules for all css / pre-processor files. modules: false // use thread-loader for babel TS in production build // enabled by default if the machine has more than 1 cores parallel: require( os ).cpus().length 1, // PWA 插件相关配置 // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa pwa: {}, // webpack-dev-server 相关配置 devServer: { open: process.platform darwin , host: localhost , port: 3001, //8080, https: false, hotOnly: false, proxy: { // 设置代理 // proxy all requests starting with /api to jsonplaceholder /api : { // target: https://emm.cmccbigdata.com:8443/ , target: http://localhost:3000/ , // target: http://47.106.136.114/ , changeOrigin: true, ws: true, pathRewrite: { ^/api : before: app {} // 第三方插件配置 pluginOptions: { // ... };
本来想搭配 iview-ui 来用的 但后续还想把这个项目搞成 ssr 的 而 vue typescript iview Nuxt.js 的服务端渲染还有不少坑, 而 vue typescript element Nuxt.js 对 ssr 的支持已经不错了 所以选择了 element-ui 。
安装
npm i element-ui -S
按需引入, 借助 babel-plugin-component 我们可以只引入需要的组件 以达到减小项目体积的目的。
npm install babel-plugin-component -D
然后 将 babel.config.js 修改为
module.exports { presets: [ vue/app ], plugins: [ component , libraryName: element-ui , styleLibraryName: theme-chalk };
接下来 如果你只希望引入部分组件 比如 Button 和 Select 那么需要在 main.js 中写入以下内容
import Vue from vue import { Button, Select } from element-ui import App from ./App.vue Vue.component(Button.name, Button); Vue.component(Select.name, Select); /* 或写为 * Vue.use(Button) * Vue.use(Select) new Vue({ el: #app , render: h h(App) });
使用路由懒加载功能。
export default new Router({ mode: history , routes: [ path: / , name: home , component: () import(/* webpackChunkName: home */ ./views/home.vue ) path: /articles , name: articles , // route level code-splitting // this generates a separate chunk (articles.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () import(/* webpackChunkName: articles */ ./views/articles.vue ) });
// fn是我们需要包装的事件回调, delay是时间间隔的阈值 export function throttle(fn: Function, delay: number) { // last为上一次触发回调的时间, timer是定时器 let last 0, timer: any null; // 将throttle处理结果当作函数返回 return function() { // 保留调用时的this上下文 let context this; // 保留调用时传入的参数 let args arguments; // 记录本次触发回调的时间 let now new Date(); // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值 if (now - last delay) { // 如果时间间隔小于我们设定的时间间隔阈值 则为本次触发操作设立一个新的定时器 clearTimeout(timer); timer setTimeout(function() { last now; fn.apply(context, args); }, delay); } else { // 如果时间间隔超出了我们设定的时间间隔阈值 那就不等了 无论如何要反馈给用户一次响应 last now; fn.apply(context, args); }
const config { oauth_uri : https://github.com/login/oauth/authorize , redirect_uri : https://biaochenxuying.cn/login , client_id : XXXXXXXXXX , client_secret : XXXXXXXXXX , // 本地开发环境下 if (process.env.NODE_ENV development ) { config.redirect_uri http://localhost:3001/login config.client_id 502176cec65773057a9e config.client_secret 65d444de381a026301a2c7cffb6952b9a86ac235 export default config;
如果你的生产环境也要 github 登录授权的话 请在 github 上申请一个 Oauth App 把你的 redirect_uri client_id client_secret 的信息填在 config 里面即可。具体详情请看我写的这篇文章 github 授权登录教程与如何设计第三方授权登录的用户表
// url的链接 export const urls: object { login: login , register: register , getArticleList: getArticleList , export default urls;
import axios from axios // 创建axios实例 let service: any {}; service axios.create({ baseURL: /api , // api的base_url timeout: 50000 // 请求超时时间 // request拦截器 axios的一些配置 service.interceptors.request.use( (config: any) { return config; (error: any) { // Do something with request error console.error( error: , error); // for debug Promise.reject(error); // respone拦截器 axios的一些配置 service.interceptors.response.use( (response: any) { return response; (error: any) { console.error( error: error); // for debug return Promise.reject(error); export default service;
把 urls 和 https 挂载到 main.ts 里面的 Vue 的 prototype 上面。
import service from ./utils/https import urls from ./utils/urls Vue.prototype.$https service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了 Vue.prototype.$urls urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了
然后就可以统一管理接口 而且调用起来也很方便啦。比如下面 文章列表的请求。
async handleSearch() { this.isLoading true; const res: any await this.$https.get(this.$urls.getArticleList, { params: this.params this.isLoading false; if (res.status 200) { if (res.data.code 0) { const data: any res.data.data; this.articlesList [...this.articlesList, ...data.list]; this.total data.count; this.params.pageNum if (this.total this.articlesList.length) { this.isLoadEnd true; } else { this.$message({ message: res.data.message, type: error } else { this.$message({ message: 网络错误! , type: error }
一般大型的项目都有很多模块的 比如本项目中有公共信息(比如 token )、 用户模块、文章模块。
├── modules // 模块 ├── user.ts // 用户模块 ├── article.ts // 文章模块 ├── types.ts // 类型 └── index.ts // vuex 主入口
import Vue from vue import Vuex from vuex import * as types from ./types import user from ./modules/user import article from ./modules/article Vue.use(Vuex); const initPageState () { return { token: const store new Vuex.Store({ strict: process.env.NODE_ENV ! production , // 具体模块 modules: { user, article state: initPageState(), mutations: { [types.SAVE_TOKEN](state: any, pageState: any) { for (const prop in pageState) { state[prop] pageState[prop]; actions: {} export default store;
// 公共 token export const SAVE_TOKEN SAVE_TOKEN // 用户 export const SAVE_USER SAVE_USER
import * as types from ../types const initPageState () { return { userInfo: { _id: , name: , avator: const user { state: initPageState(), mutations: { [types.SAVE_USER](state: any, pageState: any) { for (const prop in pageState) { state[prop] pageState[prop]; actions: {} export default user;
markdown 渲染效果图:
markdown 渲染 采用了开源的 marked 代码高亮用了 highlight.js 。
用法
第一步 npm i marked highlight.js --save
npm i marked highlight.js --save
第二步 导入封装成 markdown.js 将文章详情由字符串转成 html 并抽离出文章目录。
marked 的封装 得感谢这位老哥。
const highlight require( highlight.js const marked require( marked const tocObj { add: function(text, level) { var anchor #toc${level}${ this.index} this.toc.push({ anchor: anchor, level: level, text: text }); return anchor; // 使用堆栈的方式处理嵌套的ul,li level即ul的嵌套层次 1是最外层 // ul // li /li // ul // li /li // /ul // li /li // /ul toHTML: function() { let levelStack []; let result const addStartUL () { result ul class anchor-ul id anchor-fix const addEndUL () { result /ul \n const addLI (anchor, text) { result li a class toc-link href # anchor text a /li \n this.toc.forEach(function(item) { let levelIndex levelStack.indexOf(item.level); // 没有找到相应level的ul标签 则将li放入新增的ul中 if (levelIndex -1) { levelStack.unshift(item.level); addStartUL(); addLI(item.anchor, item.text); } // 找到了相应level的ul标签 并且在栈顶的位置则直接将li放在此ul下 else if (levelIndex 0) { addLI(item.anchor, item.text); } // 找到了相应level的ul标签 但是不在栈顶位置 需要将之前的所有level出栈并且打上闭合标签 最后新增li else { while (levelIndex--) { levelStack.shift(); addEndUL(); addLI(item.anchor, item.text); // 如果栈中还有level 全部出栈打上闭合标签 while (levelStack.length) { levelStack.shift(); addEndUL(); // 清理先前数据供下次使用 this.toc []; this.index 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;
相关文章
- 【Vue 快速入门系列】通过天气案例快速掌握监视属性
- Vue CLI3 开启gzip压缩
- vue中设置props参数类型
- 基于SpringBoot+VUE实现博客系统
- 如何通过 Vue-Cli3 - Vuex 完成一个 TodoList
- Vue进阶之表单控件绑定
- 一字一句的搞懂vue-cli之vue webpack template配置
- Vue + TypeScript + Element 项目实践(简洁时尚博客网站)及踩坑记(上)
- vue element-ui表格里时间戳转换成时间显示
- Vue核心⑬(生命周期)
- 【实战】Vue Element+Node.js开发企业通用管理后台系统——服务端开发框架搭建
- properties starting with “$“,“_“ are not proxied in the Vue instance to prevent conflicts
- 结合 Vue 源码谈谈发布-订阅模式
- 实现一个简易的vue的mvvm(defineProperty)