在线协作编辑器之周报收集

在线协作编辑器之周报收集

一、实验说明

下述介绍为实验楼默认环境,如果您使用的是定制环境,请修改成您自己的环境介绍。

1. 环境登录

无需密码自动登录,系统用户名shiyanlou

2. 环境介绍

本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到桌面上的程序:

  1. LX终端(LXTerminal): Linux命令行终端,打开后会进入Bash环境,可以使用Linux命令
  2. Firefox:浏览器,可以用在需要前端界面的课程里,只需要打开环境里写的HTML/JS页面即可
  3. Eclipse:Eclipse 是著名的跨平台的自由集成开发环境(IDE)。主要用来 Java 语言开发,但是目前亦有人通过插件使其作为 C++ 和 Python 等语言的开发工具

3. 环境使用

使用GVim编辑器输入实验所需的代码及文件,使用LX终端(LXTerminal)运行所需命令进行操作。

实验报告页面可以在“我的主页”中查看,其中含有每次实验的截图及笔记,以及实验的有效学习时间(指的是在实验桌面内操作的时间,如果没有操作,系统会记录为发呆时间),这些都是您学习的真实性证明。

二、课程介绍

1. 实验内容

大家在工作和学习中时常会遇到多人编辑一个文件的情况;那么多人在线协作文档编辑器将是一个很实用的工具,适合小组内的文档编辑,例如可用于小团队内部进行实时编写和收集周报等。

课程通过分析项目要实现的功能,调研目前的技术现状,设计了该课程来实现该功能,课程使用java语言开发,应用非常流行的springmvc框架,引入ckeditor插件,并加入localStorage缓存技术,最终使用Eclipse开发工具完成。

项目不太难,非常适合JAVA或其他爱好者进行共同学习,只要有一些 Java Web开发基础的同学都会看懂的!

2. 实验知识点

  • ckeditor (在线的编辑器) 【重点掌握】
  • localStorage (HTML 5 Web 存储)【重点掌握】
  • springmvc (轻量级Web框架) 【需提前了解java web的springmvc框架】
  • spring jdbcTemplate 【需提前了解java web的spring框架】
  • mysql 数据库 【需了解mysql的创建数据、创建表】

3. 源码下载

本课程提供源码下载地址,建议大家根据实验步骤一步一步完成,将收获更大;

项目源码

4.项目效果截图

代码开发完成后,在eclipse中启动项目后,打开火狐浏览器,输入 localhost:9090/index ,将出现本节实验的效果图,如下:

实验效果图
实验效果图

三、实验原理

1、待实现的功能需求

序号 需求名称 需求详述
1 在线协作编辑 嵌入浏览器网页的在线编辑器,支持多人协作编辑,刷新后读取后台最新编辑保存的内容
2 实时自动保存 通过在线编辑器,输入的内容,能够实时自动保存到客户端;刷新后数据不丢失;点击提交能够保存到数据库中

2、技术调研

1 在线协作编辑

实现多人在线编辑的功能,可自行开发web编辑器插件,但是实现成本较高,经调研目前已经有很多在线编辑器插件可以调用,请参靠:
HTML编辑器-HTML网页表单可视化在线编辑器插件大全

其中,CKEditor(原FCKEditor)是一个现成的使用旨在简化Web内容创建HTML文本编辑器。是国外比较流行的网页文本在线编辑器,早期DEDECMS管理后台发布内容地方使用此编辑器,这是一个所见即所得的编辑器,带来了共同的文字处理器的功能,直接到您的网页。

最后,因为CKEditor免费开源、完全可定制、高标准的质量等优点,该课程选择该插件作为前端的输入。

2 实时自动保存

web缓存技术,涉及内容很多;可参看

最终,为了兼顾数据保存的简单高效和安全,我们选用 sessionStorage作为前端存储,因为sessionStorage的优点就是方便高效;同时为了保证数据的安全不丢失,我们在用户确认编写信息无误后,通过触发按钮的方式将数据提交后台,交由服务器进行存储,因为服务器存储数据安全性高。

3、系统设计

系统框架图
系统框架图

四、开发准备工作

1 新建项目

双击桌面的eclipse图标,打开eclipse软件,点击工具栏FILE-New-Dynamic Web Project,进入新建java web项目页面;新建 Dynamic Web Project,命名为 WeekReport,然后点击 Next,


new-project
new-project

第二次点击 Next 按钮会进入如下所示的步骤,注意勾选生成 web.xml 选项。


勾选web.xml
勾选web.xml

2 导入jar包

先下载依赖的jar包,下载地址url为http://labfile.oss.aliyuncs.com/courses/742/lib.zip

$ wget url
$ unzip lib.zip
$ cp lib/* /home/shiyanlou/workspace/WeekReprot/WebContent/WEB-INF/lib 
解压复制jar包
解压复制jar包

将jar包解压之后,将所有的jar包文件,全部都复制到WeekReport项目工程下的 WebContent/WEB-INF/lib 目录下面即可(jar文件目录截图如下)。

jar文件目录截图
jar文件目录截图

3 搭建springmvc框架

包含三步,

  • 增加必要文件及其目录;【增加service、controller、dao、model目录等】
  • 修改web.xml配置;【配置了listener和servlet】
  • 增加spring、spring-mvc和数据库jdbc配置文件。
    该部分内容不是本文的重点,如有疑问可以参考如下文章,
    http://www.cnblogs.com/xing901022/p/5240044.html ;或者在课程问答里进行提问解答。

a) 在src下新建一个包,包名自定义,该实验此处我命令为:com.zn.wr。
然后在该包下,分别新建4个文件夹,service、controller、dao、model;供后续服务器端保存数据时使用。


新建包
新建包

b) 修改web.xml,配置了spring监听和servlet。为了是增加spring和spring-mvc配置文件,使项目完成spring-mvc的框架。

<!-- 增加spring监听-->  
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:applicationContext.xml</param-value>
  </context-param> 
  <!-- springmvc文件配置在这里 -->  
  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
     <param-value>classpath*:spring-mvc.xml</param-value> 
      <!-- 上面是放在 resources下, 也可以配置在 /WEB-INF/spring-mvc.xml -->
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>   
</web-app>

c) 增加配置文件
在 WebContent\WEB-INF新建3个配置文件applicationContext.xml,spring-mvc.xml,jdbc.properties。
applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
       
    <!-- 配置jdbc文件 -->
    <context:property-placeholder location="/WEB-INF/jdbc.properties" />

    <!-- 扫描类包,将标注Spring注解的类自动转化Bean,同时完成Bean的注入 -->
    <context:component-scan base-package="com.zn.wr.controller" />
    <context:component-scan base-package="com.zn.wr.dao" />

    <!-- 配置Jdbc模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
        destroy-method="close">
        <property name="driverClass">
            <value>${jdbc.driverClassName}</value>
        </property>
        <property name="jdbcUrl">
            <value>${jdbc.url}</value>
        </property>
        <property name="user">
            <value>${jdbc.username}</value>
        </property>
        <property name="password">
            <value>${jdbc.password}</value>
        </property>
        <!--连接池中保留的最小连接数。 -->
        <property name="minPoolSize">
            <value>5</value>
        </property>
        <!--连接池中保留的最大连接数。Default: 15 -->
        <property name="maxPoolSize">
            <value>30</value>
        </property>
        <!--初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
        <property name="initialPoolSize">
            <value>10</value>
        </property>
        <!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
        <property name="maxIdleTime">
            <value>60</value>
        </property>
        <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
        <property name="acquireIncrement">
            <value>5</value>
        </property>
        <!--JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 
            如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0 -->
        <property name="maxStatements">
            <value>0</value>
        </property>
        <!--每60秒检查所有连接池中的空闲连接。Default: 0 -->
        <property name="idleConnectionTestPeriod">
            <value>60</value>
        </property>
        <!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 -->
        <property name="acquireRetryAttempts">
            <value>30</value>
        </property>
        <!--获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常。但是数据源仍有效 保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试 
            获取连接失败后该数据源将申明已断开并永久关闭。Default: false -->
        <property name="breakAfterAcquireFailure">
            <value>true</value>
        </property>
        <!--因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的 时候都将校验其有效性。建议使用idleConnectionTestPeriod或automaticTestTable 
            等方法来提升连接测试的性能。Default: false -->
        <property name="testConnectionOnCheckout">
            <value>false</value>
        </property>
    </bean>
</beans>

spring-mvc.xml 下载地址:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <!-- 定义 SpringMVC的扫描路径 base-package的值与上述 a) 中在src下新建一个包,包名自定义 的值保持一致即可 --> 
    <context:component-scan base-package="com.zn.wr" />
    <!-- 定义view 【jsp】映射关系  -->
     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
       <property name="viewClass"  value="org.springframework.web.servlet.view.JstlView" />
       <property name="prefix" value="/WEB-INF/jsp/" />
       <property name="suffix" value=".jsp" />
    </bean>
</beans>

jdbc.properties下载地址:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/weekreport
jdbc.username=root
jdbc.password=admin

其中,本实验涉及到的如下文件需修改下。
jdbc.properties中的mysql的数据库的用户名和密码,请修改为你安装时设定的用户名和密码,即可。

修改jdbc配置文件
修改jdbc配置文件

3 项目文件结构

最终,项目的文件结构如下截图:


项目文件结构截图
项目文件结构截图

五、实验步骤

1. 前台 在线协作编辑 新建jsp

在\WebContent\WEB-INF下新建jsp文件目录,在jsp目录下新建index.jsp文件作为前台输入页面。index.jsp 页面代码如下:

<html>
<head>
    <title>CKEditor Classic Editing Sample</title> 
 
    <script src="http://cdn.bootcss.com/jquery/1.11.1/jquery.min.js" type="text/javascript"></script> 
    
    <script src="https://cdn.ckeditor.com/4.6.0-441b33b/full-all/ckeditor/ckeditor.js"></script>     
 
    <style type="text/css">
    /* Minimal styling to center the editor in this sample */
    body {
        padding: 30px;
        display: flex;
        align-items: center;
        text-align: center;
    }
    p{
    text-align:right; 
    }
    
    .container {
        margin: 0 auto;
    }
    </style>
</head>
<body>
    <%-- 1 添加编辑页面 --%>
    <div class="container">   
          <form method="post">
            <h2><label for="editor1">周报编辑器</label></h2>  
            <p><a href="javascript:void(0);" onclick="ManualSave();" >保存</a></p>             
<!--         <a href="javascript:void(0);" onclick="Export();" >导出</a></p> -->
            <textarea name="editor1" id="editor1"> </textarea>      
          </form> 
    </div>
</body>
</html>

在该jsp页面添加div,包含一个label、一个保存按钮和一个输入框。
另外通过引入js插件(jquery.min.js和ckeditor.js),来实现“在线协作编辑”功能。

2. 前台 在线协作编辑 编写js

通过初始化一个编辑器函数,并设置了编辑器的主题样式以及模式等,从而可以看到一个web端的编辑器,并且可以输入内容,就像在本地电脑的word中进行编辑。其中初始化函数中的相关参数设置可以去参考官网资料Ckeditor

  1. 增加实时缓存功能,采用sessionStorage的方式,将在线编辑器中输入内容进行本地缓存,保证数据刷新后不丢失;
  2. 添加手动保存功能,点击保存后,将编辑好的数据异步传到后台,保存到mysql数据库中;
  3. 刷新加载最新数据,手动保存后会自动刷新前台一次,将最新的数据加载到前台页面中;
<script>    
/*
* 1. 每次进入页面前    都从后台取最新的数据到前台   【在index方法里】
* 2. 缓存数据判断   缓存为空 则将后台最新数据赋值    2 本地缓存  localStorage 自动保存
* 3. 缓存数据赋值给 texture值
* 4. 遇到 texture值变化,则自动保存
*/

$(document).ready(function () {  
    var editor=initeditor();
    var  initcontent= window.sessionStorage.getItem("comment_top");     
    // 初始化页面赋值
    initdata(editor,initcontent);
    // 触发自动保存  自动保存功能
    editor.on('change', function(evt) {  
        window.sessionStorage.setItem("comment_top", evt.editor.getData()); 
    });
});
 
function initeditor(){
      var editor=  CKEDITOR.replace( 'editor1' , {
        toolbar: [
                    { name: 'clipboard', items: [ 'Undo', 'Redo' ] },
                    { name: 'styles', items: [ 'Format', 'Font', 'FontSize' ] },
                    { name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'RemoveFormat', 'CopyFormatting' ] },
                    { name: 'colors', items: [ 'TextColor', 'BGColor' ] },
                    { name: 'align', items: [ 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
                    { name: 'links', items: [ 'Link', 'Unlink' ] },
                    { name: 'paragraph', items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote' ] },
                    { name: 'insert', items: [ 'Image', 'Table' ] },
                    { name: 'tools', items: [ 'Maximize' ] },
                    { name: 'editing', items: [ 'Scayt' ] }
                ],
                
        height: 800 ,  //外面长方形 高多少
        contentsCss: [ 'https://cdn.ckeditor.com/4.6.0-441b33b/full-all/ckeditor/contents.css', 'mystyles.css' ] ,   //有无里面的方格          
        allowedContent : true ,
        disallowedContent: 'img{width,height,float}'   ,                
        extraAllowedContent: 'img[width,height,align]' ,                
        extraPlugins: 'tableresize,uploadimage,uploadfile' , 
        bodyClass: 'document-editor' ,               
        format_tags: 'p;h1;h2;h3;pre' ,               
        removeDialogTabs: 'image:advanced;link:advanced'  
      });
      return editor;
};
      
function initdata(editor,initcontent){  
        //显示进度条代码结束
    var aj =$.ajax({
            type : "POST",
            url : "initdata",
            async: false,
            dataType : "json",       
            success : function(data) {
                if (data == null) {
                    alert("没查到最新的信息");
                } else if (!initcontent && typeof initcontent != "undefined" && initcontent != 0) {
                    editor.setData(data.scontent); 
                }else{
                    editor.setData(initcontent); 
                }
            }
    });
};
//手动保存页面 
 function ManualSave(){    
            var aj = $.ajax( {    
                url:'save',   // 跳转到 action               
                type:'POST',       
                dataType:'json',    
               contentType: 'application/json;chartset=UTF-8',           
               data:JSON.stringify(window.sessionStorage.getItem("comment_top")), //  传批量的参数 list           
                success:function(data) {    
                    if(data.state){       
                        alert("保存成功!");    
                        window.sessionStorage.removeItem("comment_top");
                        window.location.reload();   
                    }else{    
                        alert("保存失败!");    
                    }    
                 },    
                 error : function() {      
                      alert("网络异常!");    
                 }    
    });  
};    
        
</script> 
     

3. 后台实时自动保存功能解析

在src\com\zn\wr\model下新建一个实体类Content.java用于对应前台的输入框,其中包含的字段scontent用来存储编辑的内容。Model层,用持久化类描述实例对象。

 public class Content{
    String scontent; // 编辑内容
    public String getScontent() {
        return scontent;
    }
    public void setScontent(String scontent) {
        this.scontent = scontent;
    }   

在src\com\zn\wr\controller下新建一个实体类SaveController.java,用来处理在前台输入编辑内容完成后,点击保存按钮动作,触发的后台操作。Controller层,连接前台页面和后台业务,接收数据并调用Dao层操作。

 
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; 
 import com.zn.wr.dao.contentDAO;
import com.zn.wr.model.Content;

@Controller
public class SaveController {
    private Content content;
    
    @Resource 
    private contentDAO contentdao;
 
    @RequestMapping("/index") // 通过spring mvc 的requestingmapping 跳转到前台index.jsp
    public String toLoginPage()throws Exception {
        return "index";
    }
       /*
        * 初始化最新数据
        */
    @ResponseBody
    @RequestMapping(value = "initdata", method = RequestMethod.POST)
    public Content toInitData(){
        this.content = new Content();
        this.content.setScontent(contentdao.search());
        System.out.println("content:"+contentdao.search());
        return content;
    }
    
   /*
    * 手工保存
    */
    @RequestMapping(value="save", method = {RequestMethod.POST} ,produces = "text/html;charset=UTF-8") 
    @ResponseBody  
     public  Map<String, Object> doSave(@RequestBody String initcontent) {   
        boolean state = contentdao.insert(initcontent); 
        Map<String, Object> modelMap = new HashMap<String, Object>();  
        modelMap.put("state", state);  
        return modelMap;  
     } 

}

在src\com\zn\wr\dao下新建一个实体类contentDAO.java,用于响应Controller层的操作, DAO层,数据持久化,把对象的各种操作进行封装;并将前台的数据存储到数据库中。

 import java.util.HashMap; 
import java.util.Map; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.jdbc.core.JdbcTemplate; 
import org.springframework.stereotype.Repository;

@Repository
public class contentDAO {
    @Autowired // 注入 spring jdbctemplate
    private JdbcTemplate jdbcTemplate;
    
    // 手动保存功能
    public boolean insert(String content)
    {
        try {
            String sql = " INSERT INTO w_content(content,createtime,updatetime )VALUES (?,now(),now())";
            Object[] params = new Object[] { content };
            jdbcTemplate.update(sql, params);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    
    //刷新最新数据    
    public String search()
    {    
        String content = null;
        try {
            String sql = "SELECT content from w_content  ORDER BY updatetime DESC LIMIT 1";
            content = (String) jdbcTemplate.queryForObject(sql, java.lang.String.class);             
        } catch (Exception e) {
            e.printStackTrace();         
        }
        return content;
    }   
}

Service层,业务逻辑层,调用DAO层操作,此处业务逻辑简单,省略该层。

六、数据库

1 查看数据库状态

$ sudo service mysql status  // 查看mysql状态
$ sudo service mysql start   //如果没启动就可以启动
$ sudo service mysql stop    //使用完毕之后就可以关闭

2 启动mysql数据库

启动数据库
启动数据库

启动mysql数据库,如不熟悉这部分内容,可先学习实验楼中的【SQL的介绍及MySQL的安装】这个课程。

默认实验楼环境中的mysql的root用户的密码为null,为了安全起见,需要进行修改密码。修改root密码的方法。

通过使用SET PASSWORD命令

$ mysql -u root
mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('newpass');

3 创建项目数据库和表

创建数据库

$ mysql -u root -p
$  mysql> create database weekreport default character set utf8 collate utf8_general_ci;
$  mysql> show databases;
$  mysql> use weekreport;
创建使用数据库
创建使用数据库

创建表的SQL

Navicat MySQL Data Transfer

Source Server         : localhost_3306
Source Server Version : 50715
Source Host           : localhost:3306
Source Database       : weekreport

Target Server Type    : MYSQL
Target Server Version : 50715
File Encoding         : 65001

Date: 2016-12-21 10:47:58
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for w_content
-- ----------------------------
DROP TABLE IF EXISTS `w_content`;
CREATE TABLE `w_content` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `content` text,
  `createtime` datetime DEFAULT NULL,
  `updatetime` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS=1;

七、实验效果图

1 eclipse项目启动

选中server中的Tomcat V7.0 Server,然后右键点击 add and remove,


tomcat添加工程
tomcat添加工程

选择weeklyreport 项目,然后双击server中TomcatV7.0Server,出现如下图示,进行修改访问的port=9090(可随意定义,保证没有其他的服务使用该端口即可)


tomcat配置
tomcat配置

启动成功的console提示页面


tomcat console成功
tomcat console成功

2 火狐浏览器访问测试

打开火狐浏览器,输入 localhost:9090/index ,出现课程的初始效果图:


初始化实验效果图
初始化实验效果图

输入 “2017 first course shiyanlou 2017.2.12”等字样后,数据自动保存到缓存中,直接刷新页面,数据不丢失


输入汉字
输入汉字

3 保存之后显示最新数据

点击保存按钮,弹出保存成功字样。


保存成功
保存成功

点击,确定后自动刷新最新数据:


确定后刷新
确定后刷新

八、课后习题

  • 本次项目课的内容比较多,建议多动手操作几遍,并且仔细回顾和思考,才能真正理解。
  • 目前还不支持导出功能;可以考虑添加另存为word、pdf等格式的文件;还可以添加工作流、添加邮件、短信通知功能。
  • 课后作业:本项目的前台代码中,js调用的是外部链接,请动手修改为本地的链接调用试试?看下效果是不是一样。

感兴趣的可以直接到实验楼去操作完成这个课程。

友情提示

我是和奇谷朴,一个在帝都周末自己选择加班的有志好青年,读完我的文章如果有收获,记得打赏、关注和点赞哦!么么哒!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,834评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,778评论 6 342
  • 马上就到毕业季了,很多同学想进入IT行业,应聘软件工程师,如何准备一份高质量的简历。还有的同学培训刚刚结束,自己也...
    嵌入式学习阅读 1,053评论 0 1
  • 满世界都是成功学理论,满世界都是成功人士的励志言论,然而他们的成功之路我们真的可以复制吗?他们的言论真的可以拯救普...
    南星云阅读 443评论 0 1