Vue + WebApi 小项目:构造自己的在线 Markdown 笔记本应用

Vue + WebApi 小项目:构造自己的在线 Markdown 笔记本应用

目录

  • 概要
  • 知识点
  • 完整示例图
  • 代码与资源文件
  • 流程步骤

 

概要

  基于 MVP 最小可行性产品设计理念,我们先完成一个可以使用,并具备基本功能的 Markdown 笔记本应用,再进行逐步完善。

 

知识点

  本文会指导初学者如何一步步运用 Vue 的计算属性、双向绑定、指令、生命周期钩子,还有 localStorage 和异步请求等知识点。

 

完整示例图

 
 

代码与资源文件

   https://github.com/liqingwen2015/MarkdownDemo

  为了避免网络原因造成的问题,文中所使用的第三方库(可自己去官方下载最新版,文章使用的是当前发布时间最新版本的 js 文件)以及 css 文件都下载好并且已经放入里面。

body {
    font-family: sans-serif;
    font-size: 16px;
    height: 100%;
    margin: 0;
    box-sizing: border-box;
  }
  
  .material-icons {
    font-size: 24px;
    line-height: 1;
    vertical-align: middle;
    margin: -3px;
    padding-bottom: 1px;
  }
  
  #app > * {
    float: left;
    display: flex;
    flex-direction: column;
    height: 100%;
  
    > * {
      flex: auto 0 0;
    }
  }
  
  .side-bar {
    background: #f8f8f8;
    width: 20%;
    box-sizing: border-box;
  }
  
  .note {
    padding: 16px;
    cursor: pointer;
  }
  
  .note:hover {
    background: #ade2ca;
  }
  
  .note .icon {
    float: right;
  }
  
  button,
  input,
  textarea {
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
    box-sizing: border-box;
    outline: none !important;
  }
  
  button,
  .note.selected {
    background: orange;
    color: white;
  }
  
  button {
    border-radius: 3px;
    border: none;
    display: inline-block;
    padding: 8px 12px;
    cursor: pointer;
  }
  
  button:hover {
    background: #63c89b;
  }
  
  input {
    border: solid 2px #ade2ca;
    border-radius: 3px;
    padding: 6px 10px;
    background: #f0f9f5;
    color: #666;
  }
  
  input:focus {
    border-color: orange;
    background: white;
    color: black;
  }
  
  button,
  input {
    height: 34px;
  }
  
  .main, .preview {
    width: 40%;
  }
  
  .toolbar {
    padding: 4px;
    box-sizing: border-box;
  }
  
  .status-bar {
    color: #999;
    font-style: italic;
  }
  
  textarea {
    resize: none;
    border: none;
    box-sizing: border-box;
    margin: 0 4px;
    font-family: monospace;
  }
  
  textarea, .notes, .preview {
    flex: auto 1 1;
    overflow: auto;
  }
  
  .preview {
    padding: 12px;
    box-sizing: border-box;
    border-left: solid 4px #f8f8f8;
  }
  
  .preview p:first-child {
    margin-top: 0;
  }
  
  a {
    color: orange;
  }
  
  h1,
  h2,
  h3 {
    margin: 10px 0 4px;
    color: orange;
  }
  
  h1 {
    font-size: 2em;
  }
  
  h2 {
    font-size: 1.5em;
  }
  
  h3 {
    font-size: 1.2em;
  }
  
  h4 {
    font-size: 1.1em;
    font-weight: normal;
  }
使用的 index.css 文件

 

流程步骤

  1.先构建一个基本的 html 文件,并引入核心 js 库。

  这里需要引入的第三方库为 vue.js、marked.js。

<html>

<head>
    <title></title>
    <!-- 引入样式文件 -->
    <link rel="stylesheet" href="index.css" />
</head>

<body>
    <!-- 引入 js 库 -->
    <script src="/lib/vue.js"></script>
    <script src="/lib/marked.js"></script>

    <!-- js 代码 -->
    <script src="index.js"></script>
</body>

</html>

 

  因为考虑到项目主要划分为两块,左边是书写区域,右边为预览区域,<body> 块代码修改为:

<body>
    <!-- 引入 js 库 -->
    <script src="lib/vue.js"></script>
    <script src="lib/marked.js"></script>

    <div id="app">
        <!-- 主区域:书写 -->
        <section class="main"></section>

        <!-- 预览区域 -->
        <aside class="preview"></aside>
    </div>

    <!-- js 代码 -->
    <script src="index.js"></script>
</body>

 

  修改 js 代码:创建 Vue 实例,并将其挂载到 DOM 元素上。

new Vue({
    el: '#app'
})

 

  【备注】上面的挂载方式是比较常见的一种,我们也可以使用 app.$mount('#app') 进行挂载。

 

  2.接下来我们使用 Vue 的双向绑定机制控制输入的内容和预览的内容。

  修改 html:

<body>
    <!-- 引入 js 库 -->
    <script src="lib/vue.js"></script>
    <script src="lib/marked.js"></script>

    <div id="app">
        <!-- 主区域:书写 -->
        <section class="main">
            <textarea v-model="editor"></textarea>
        </section>

        <!-- 预览区域 -->
        <aside class="preview">
            {{editor}}
        </aside>
    </div>

    <!-- js 代码 -->
    <script src="index.js"></script>
</body>

 

  修改 js,增加数据属性:

new Vue({
    el: '#app',
    data() {
        return {
            editor: '编辑器'
        }
    }
})

 

  现在,打开 index.html 页面,在浏览器页面中的左侧进行输入就可以在预览窗口中同步看到输入后的情况。

 

  3.接下来,我们需要对输入的内容经过 Markdown 形式转换,在这里,我们使用 Vue 的计算属性来进行优化渲染 Markdown 的实时预览

  修改 js:

new Vue({
    // 挂载
    el: '#app',
    
    // 数据
    data() {
        return {
            editor: '编辑器'
        }
    },

    // 计算属性
    computed: {
        editorPreview() {
            return marked(this.editor);
        }
    }
})

 

  修改 <body>,使用 v-html 指令取代 {{ }},以这种方式来渲染 HTML 元素。

<body>
    <!-- 引入 js 库 -->
    <script src="lib/vue.js"></script>
    <script src="lib/marked.js"></script>

    <div id="app">
        <!-- 主区域:书写 -->
        <section class="main">
            <textarea v-model="editor"></textarea>
        </section>

        <!-- 预览区域 -->
        <aside class="preview" v-html="editorPreview"> </aside>
    </div>

    <!-- js 代码 -->
    <script src="index.js"></script>
</body>

 

 
运行效果图

 

  4.保存内容

  目前,如果关闭了浏览器或者对页面进行了刷新,所有内容都会丢失。所以,我们目前使用 localStorage  的方式进行数据的保存操作。

  现在产生了一个疑问:应该什么时候进行保存呢?

  我们现在使用 Vue 的侦听器功能来对数据的改动进行保存操作,因为它可以监听到 editor 的每一改动操作,意思是每次输入操作都会触发侦听器里面的方法。

  修改 js:

new Vue({
    // 挂载
    el: '#app',

    // 数据
    data() {
        return {
            editor: '编辑器'
        }
    },

    // 计算属性
    computed: {
        editorPreview() {
            return marked(this.editor);
        }
    },

    // 侦听器
    watch: {
        editor(val) {
            localStorage.setItem('editor', this.editor);
        }
    }
})

 

  那么现在又产生了新的疑问:应该怎样才能够在每次进入这个页面时显示之前保存的信息呢?

  现在,我们通过利用 Vue 的生命周期钩子(目前使用 created 钩子)来进行数据的读取及恢复。

  修改 js:

new Vue({
    // 挂载
    el: '#app',

    // 数据
    data() {
        return {
            editor: '编辑器',
            key: {
                editor: 'editor'
            }
        }
    },

    // 计算属性
    computed: {
        editorPreview() {
            return marked(this.editor);
        }
    },

    // 侦听器
    watch: {
        editor(val) {
            localStorage.setItem(this.key.editor, this.editor);
        }
    },

    // 生命周期钩子
    created() {
        this.editor = localStorage.getItem(this.key.editor) || '第一次使用 Markdown 笔记本';
    }
})

 

  【备注】在进行修改 js 后,editor 属性第一次加载的时候可能为 null,这会导致整个应用出错,所以这里采用了默认值。

 

  5.localStorage 毕竟不是永久保存的方式,这里我使用一种较为简单的方式,保存方法替换为异步请求到 WebApi 接口保存到数据库的方式

  修改 html,引入 axios 库:

<script src="lib/axios.min.js"></script>

 

  同时,修改 js,增加两个 Http 请求的方法,获取和保存:

new Vue({
    // 挂载
    el: '#app',

    // 数据
    data() {
        return {
            editor: '',
            key: {
                editor: 'editor'
            },
            url: 'http://localhost:34473/api/markdown'  // 需要替换成自己的 API 路径
        }
    },

    // 计算属性
    computed: {
        editorPreview() {
            return marked(this.editor);
        }
    },

    // 侦听器
    watch: {
        editor(val) {
            //localStorage.setItem(this.key.editor, this.editor);
            this.save();
        }
    },

    // 生命周期钩子
    created() {
        this.load();
        // this.editor = localStorage.getItem(this.key.editor) || '第一次使用 Markdown 笔记本';
    },

    // 方法
    methods: {
        load() {
            var that = this;
            axios.get(that.url).then(function (result) {
                console.log(result.data);
                that.editor = result.data;
            });
        },
        save() {
            var that = this;
            axios.post(that.url, { content: that.editor }).then(function (result) { });
        }
    }
})

 

  新增的 API 控制器 MarkdownController.cs 的内容如下:

    [Route("api/[controller]")]
    [ApiController]
    public class MarkdownController : ControllerBase
    {
        public static MarkdownViewModel MarkdownViewModel = new MarkdownViewModel()
        {
            Content = "我的第一个 Markdown 应用"
        };

        [HttpGet]
        public ActionResult<string> Get()
        {
            return MarkdownViewModel.Content;
        }

        [HttpPost]
        public void Save([FromBody] MarkdownViewModel vm)
        {
            MarkdownViewModel = vm;
        }
    }

 

  视图模型 MarkdownViewModel.cs 的内容如下:

    public class MarkdownViewModel
    {
        public string Content { get; set; }
    }

 

  【备注】需要自行进行 WebApi 的跨域配置,演示时进行了忽略配置  

  【备注】示例代码可从 https://github.com/liqingwen2015/MarkdownDemo 下载

 

【切换阅读方式】https://www.jianshu.com/p/a17033ca91d9
【参考】Vue.js 2 Web Development Projects

posted @ 2019-01-14 08:11  反骨仔  阅读(4530)  评论(4编辑  收藏  举报