重构UI管理系统基础篇1

前言

重构项目之前一般都先找几个成熟的开源项目作为参考,看看别人的思路。 首先确定一下自己需要重构哪些,带着自己的目标去翻看源码总会事半功倍。


1.项目结构
2.代码风格
3.javascript源码需要转为ES6
4.ajax需要替换为其它方式
5.去除编程过程中快速开发遗留的冗余代码以及优化

准备工作

第一个项目我找的是vue-ghpages-blog 这里不贴地址,自行去github搜一下就行了。

先看这份项目的组织方式:

├── dist
│   ├── app.js
│   └── vendors.js
├── favicon.ico
├── index.html
├── package.json
├── src
│   ├── components
│   │   ├── App.vue
│   │   ├── ListView.vue
│   │   └── PostView.vue
│   ├── filters
│   │   └── index.js
│   ├── main.js
│   ├── setting
│   │   └── index.js
│   ├── store
│   │   └── index.js
│   └── templates
│       └── index.html
└── webpack.config.js

粗略看了下代码,发现比较有学习价值的只有store目录下的Index.js 因为就这部分包含了ES6的一些特性。来看一下这个文件内容,我将直接分解内容。

// vendor
//ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

import { EventEmitter } from 'events';
import { Promise } from 'es6-promise';
// ES6模块
上面代码的实质是从events模块加载EventEmitter方法,其他方法不加载。这种加载称为“编译时加载”,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。当然,这也导致了没法引用ES6模块本身,因为它不是对象。

// setting
import setting from '../setting';

// https://developer.github.com/v3/repos/#get
const LIST_API_URL = `https://api.github.com/repos/${setting.config.repo}/contents/${setting.config.path}?ref=${setting.config.branch}`;


let store = new EventEmitter();
//EventEmitter可以说是观察者模式的体现,所以实现概念可以理解为基于事件的监听和发布


export default store;//让ES6模块有一个默认的导出

/**
 * fetch post content from github
 *
 * @param title
 * @returns {Promise}
 */
 
 
 /*
 基础补充
 ES6允许使用“箭头”(=>)定义函数。

	var f = v => v;
	上面的箭头函数等同于:
	
	var f = function(v) {
	  return v;
	};
 
 
 
 
 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
 
 Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise新建后就会立即执行。
 
 
 */
store.getPost = (title) => {

    const POST_API_URL = `https://api.github.com/repos/${setting.config.repo}/contents/${setting.config.path}/${title}?ref=${setting.config.branch}`;

    return new Promise((resolve, reject) => {

        const xhr = new XMLHttpRequest();

        xhr.open('GET', `${POST_API_URL}`);
        // https://developer.github.com/v3/media/#html
        xhr.setRequestHeader("Accept", "application/vnd.github.v3.html");
        xhr.onload = () => {
            const resText = xhr.responseText;
            resolve(resText);
        };
        xhr.onerror = () => reject;
        xhr.send();

    });
}

/**
 * fetch the list from cache or from github api
 *
 * @todo paginate not yet
 *
 * @param page
 * @returns {Promise}
 */
store.getListByPage = (page = 1) => {
    return new Promise((resolve, reject) => {

        if (sessionStorage && sessionStorage.getItem('posts')) {

            // read data from cache
            resolve(JSON.parse(sessionStorage.posts));

        } else {

            const xhr = new XMLHttpRequest();

            xhr.open('GET', LIST_API_URL);
            xhr.onload = () => {
                const resJson = xhr.responseText;
                // caching
                sessionStorage.setItem('posts', resJson);

                resolve(JSON.parse(resJson));
            };
            xhr.onerror = () => reject;
            xhr.send();

        }

    });

}

从上段代码和以前看过的几个项目来看,filters目录好像很常见,把一些通用的处理数据的函数放进去多个地方调用。这是优化的一部分。

接下来是三个版本不同的cnodejs的写法。其中一个是大概去年拿到的学习版本。

版本1的目录结构:
├── LICENSE
├── README.md
├── dist
│   ├── app.css
│   ├── app.css.map
│   ├── app.js
│   ├── app.js.map
│   ├── dcddd5036c95dcdc001313b59b500fdf.gif
│   └── main.js
├── favicon.ico
├── index.html
├── npm-debug.log
├── package.json
├── server.js
├── src
│   ├── api.js
│   ├── app.js
│   ├── app.vue
│   ├── components
│   │   ├── loading.vue
│   │   ├── message-default.vue
│   │   ├── modal.vue
│   │   ├── nickname.vue
│   │   ├── no-data.vue
│   │   ├── profile.vue
│   │   ├── read.vue
│   │   ├── slide.vue
│   │   ├── tips.vue
│   │   └── unread.vue
│   ├── css
│   │   ├── about.css
│   │   ├── app.css
│   │   ├── common.css
│   │   ├── home.css
│   │   ├── layout-box.css
│   │   ├── layout.css
│   │   ├── message.css
│   │   ├── normalize.css
│   │   ├── post.css
│   │   ├── profile.css
│   │   └── topic.css
│   ├── directives.js
│   ├── filters.js
│   ├── fonts
│   │   ├── iconfont.svg
│   │   └── iconfont.ttf
│   ├── images
│   │   ├── bg.jpg
│   │   ├── bg.png
│   │   ├── github.ico
│   │   ├── loading.gif
│   │   └── loading.png
│   ├── router.js
│   └── views
│       ├── about.vue
│       ├── home.vue
│       ├── login.vue
│       ├── message.vue
│       ├── post.vue
│       ├── tail.vue
│       └── topic.vue
├── static
│   └── vue.js
├── webpack.config.babel.js
├── webpack.config.prod.babel.js

这个版本有个api.js里面,里面内容如下:

import promise from "es6-promise"
import "whatwg-fetch"

export let getList = async (page, tag) => {
	let response = await fetch(`https://cnodejs.org/api/v1/topics?page=${page}&limit=20&tab=${tag}`, {
		mode: "cors"
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let getTopic = async (topicId) => {
	let response = await fetch(`https://cnodejs.org/api/v1/topic/${topicId}`, {
		mode: "cors"
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let login = async (token) => {
	let response = await fetch(`https://cnodejs.org/api/v1/accesstoken `, {
		method: "POST",
		mode: "cors",
		headers: {
			"Content-Type": "application/x-www-form-urlencoded"
		},
		body: `accesstoken=${token}`
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let like = async (id, token) => {
	let response = await fetch(`https://cnodejs.org/api/v1/reply/${id}/ups`, {
		method: "POST",
		mode: "cors",
		headers: {
			"Content-Type": "application/x-www-form-urlencoded"
		},
		body: `accesstoken=${token}`
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let reply = async (token, topicId, content, replyId) => {
	let body = replyId ? `accesstoken=${token}&content=${content}&reply_id=${replyId}` : `accesstoken=${token}&content=${content}`

	let response = await fetch(`https://cnodejs.org/api/v1/topic/${topicId}/replies`, {
		method: "POST",
		mode: "cors",
		headers: {
			"Content-Type": "application/x-www-form-urlencoded"
		},
		body: body
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let getProfile = async (nickname) => {
	let response = await fetch(`https://cnodejs.org/api/v1/user/${nickname}`, {
		mode: "cors"
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let getMessages = async (token) => {
	let response = await fetch(`https://cnodejs.org/api/v1/messages?accesstoken=${token}`, {
		mode: "cors"
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let getMessageCount = async (token) => {
	let response = await fetch(`https://cnodejs.org/api/v1/message/count?accesstoken=${token}`, {
		mode: "cors"
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

export let post = async ({token, title, tab, content}) => {
	let response = await fetch("https://cnodejs.org/api/v1/topics", {
		method: "POST",
		headers: {
			"Content-Type": "application/x-www-form-urlencoded"
		},
		mode: "cors",
		body: `accesstoken=${token}&title=${title}&tab=${tab}&content=${content}`
	}).catch((error) => {
		console.log(error)
	})

	return await response.json().catch((error) => {
		console.log(error)
	})
}

awaitasync是es7的异步解决方案。然后用了fetch来获取数据。 其它部分优化部分不明显也就不看了~

另一个版本cnodejs-vue它的主文件Main.js含有这么一段:

router.beforeEach((transition) => {
  document.body.scrollTop = 0;
  const token = getToken(store.state);
  if (token) {
    fetchMsgCount(store, token)
      .catch((e) => console.log(e));
  }
  if (transition.to.auth) {
    if (token) {
      transition.next();
    } else {
      const redirect = encodeURIComponent(transition.to.path);
      transition.redirect({ name: 'login', query: { redirect } });
    }
  } else {
    transition.next();
  }
});

它把用户验证这块放在了路由这边处理。router.beforeEach(hook).这个函数会在路由切换开始时调用。调用发生在整个切换流水线之前。这个适合单页面的,但是我项目用了好几个路由,包括前端和后台的,因此即使是分拆好像也不是很适合。

而且这个项目引用了vuex,关于vuex我具体还不是很了解。因此先跳过。不过值得一提的是它数据获取也是通过fetch来获取的。

第三个版本的vue-cnodejs它的main.js中也写了路由中间验证

//登录中间验证,页面需要登录而没有登录的情况直接跳转登录
router.beforeEach((transition) => {
    //处理左侧滚动不影响右边
    $("html, body, #page").removeClass("scroll-hide");
    FastClick.attach(document.body);

    if (transition.to.auth) {
        if (localStorage.userId) {
            transition.next();
        } else {
            var redirect = encodeURIComponent(transition.to.path);
            transition.redirect('/login?redirect=' + redirect);
        }
    } else {
        transition.next();
    }
});

可以对比一下,一个把id放到vuex,一个放到了localStorage。我写的是存储在redis和localStorage里。

第三个版本的数据获取是用ajax。

而且这三个版本有个共同点就是都使用了scss来写css.

在某个vue版知乎日报里我发现这么一段代码:

    data: function() {
        return {
            msg: "article",
            api: {},
            title: "今日要闻",
            tin:Tin
        }
    },
    methods: {
        err: function(event) {
            var _this = event.target;
            _this.style.visibility = "hidden";
        }
    },
    route: {
        data: function() {
            var query = this.$route.query;
            //$route.query对象,包含路由中查询参数的键值对。例如,对于 /foo?user=1 ,会得到 $route.query.user == 1 。
            
            this.$http.get(Tin+'/tiny-theme', {
                params: {
                    id: query.id
                }
            }).then(function(response) {
                this.$data.api = response.json();
                this.$data.title = query.name;
                this.$root.$data.headerTitle = query.name;
            });
        }
    },

这是在artcile.vue里面的,它获取数据是通过路由加载的时候用vue resource来获取.这是让组件去主动获取数据。可以借鉴一下。

最后一个项目是vue版豆瓣。 它的路由直接写在router.js里面

	app.route('/movie/:id')
			 .get(Movie.detail)
			 .delete(MovieComment.del);

	// User.signinRequired 用户登录控制   User.adminRequired 用户权限控制

	// 用户评论路由
	app.post('/admin/movie/movieComment',User.signinRequired,MovieComment.save);

从这么一小段我们可以看到它代码写的也蛮严谨。

然后翻出app.js我们来看它的用户信息存储在哪

    session = require('express-session'),  				      // session依赖cookie模块
    mongoStore = require('connect-mongo')(session),		  // 对session进行持久化
    
    ...
    
app.use(session({
	secret:'douban',                          // 设置的secret字符串,来计算hash值并放在cookie中
	resave: false,                                    // session变化才进行存储
	saveUninitialized: true,
	// 使用mongo对session进行持久化,将session存储进数据库中
	store: new mongoStore({
		url: dbUrl,                                     // 本地数据库地址
		collection: 'sessions'                          // 存储到mongodb中的字段名
	})
}));

用Mongodb的扩展来做session持久化。

这块有个地方值得学习的是它的schemas部分:

这边获取数据又有个小变化,直接看代码

export default {
		name: 'ChooseMovies',
		components: {
			ChooseMoviesTitle,
			ChooseMovieItem
		},
		data() {
			return {
				title: ['热门','最新','经典','豆瓣高分','冷门佳片','华语','欧美','韩国'],	// 标题列表
				selected: '热门',													// 当前选择标题
				dataList: [],															// 存储Ajax返回的数据
				currentData: {}														// 当前标题显示数据
			}
		},
		created() {																		// 实例创建之后调用getData方法请求该标题对应的数据
			this.getData(this.selected);
		},
		watch: {																			// 监听selected属性,属性发生变化执行getData方法
			selected: 'getData'													// 该属性是双向绑定,当子组件中改变该属性会触发getData方法
		},
		methods: {
			getData(value) {
				// 在dataList中查找该value值是否存在,若存在则说明该数据已请求过,无需再次发送get请求
				for(let item of this.dataList) {
          if(item.name.includes(value)) {
            this.currentData = item;
            return;
          }				
				}
				// dataList中没有该标题数据 发送新请求到服务器
				let url = '/?fliterName=' + encodeURIComponent(value + '电影');
				$.get(url, (results) => {
					this.currentData = results.data;				 // 该属性值变化会让榜单该属性的子组件重新渲染
					this.dataList.push(results.data);				 // 添加到dataList中以便再次请求时进行判断
				});
			}
		}
	};
</script>

它是对面板所选择的内容进行颗粒化请求数据。

结尾

看了这些项目,大概对自己的项目重构有了一定想法。接下来就是一点点去重构了~

Table of Contents