一、什么是模板引擎 — 介绍宏观背景、历史沿革

模板引擎是将数据变为视图最优雅的解决方案

历史上曾出现的数据变为视图的方法

  • 纯DOM:非常笨拙,没有实战价值

    <!DOCTYPE html>
    <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
    </head><body><ul id="list"></ul><script>var arr = [{ "name": "小明", "age": 12, "sex": "男" },{ "name": "小红", "age": 11, "sex": "女" },{ "name": "小强", "age": 13, "sex": "男" }];var list = document.getElementById('list');for (var i = 0; i < arr.length; i++) {// 每遍历一项,都要用DOM方法去创建li标签let oLi = document.createElement('li');// 创建hd这个divlet hdDiv = document.createElement('div');hdDiv.className = 'hd';hdDiv.innerText = arr[i].name + '的基本信息';// 创建bd这个divlet bdDiv = document.createElement('div');bdDiv.className = 'bd';// 创建三个plet p1 = document.createElement('p');p1.innerText = '姓名:' + arr[i].name;bdDiv.appendChild(p1);let p2 = document.createElement('p');p2.innerText = '年龄:' + arr[i].age;bdDiv.appendChild(p2);let p3 = document.createElement('p');p3.innerText = '性别:' + arr[i].sex;bdDiv.appendChild(p3);// 创建的节点是孤儿节点,所以必须要上树才能被用户看见oLi.appendChild(hdDiv);// 创建的节点是孤儿节点,所以必须要上树才能被用户看见oLi.appendChild(bdDiv);// 创建的节点是孤儿节点,所以必须要上树才能被用户看见list.appendChild(oLi);}</script>
    </body></html>
    
  • 数据join法:曾几何时非常流行,前端必会

    <!DOCTYPE html>
    <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
    </head><body><ul id="list"></ul><script>var arr = [{ "name": "小明", "age": 12, "sex": "男" },{ "name": "小红", "age": 11, "sex": "女" },{ "name": "小强", "age": 13, "sex": "男" }];var list = document.getElementById('list');// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中for (let i = 0; i < arr.length; i++) {list.innerHTML += ['<li>','    <div class="hd">' + arr[i].name + '的信息</div>','    <div class="bd">','        <p>姓名:' + arr[i].name + '</p>','        <p>年龄:' + arr[i].age  + '</p>','        <p>性别:' + arr[i].sex + '</p>','    </div>','</li>'].join('')}</script>
    </body></html>
    
  • ES6的反引号法:ES6中新增的`${a}`语法糖,非常好用

    <!DOCTYPE html>
    <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
    </head><body><ul id="list"></ul><script>var arr = [{ "name": "小明", "age": 12, "sex": "男" },{ "name": "小红", "age": 11, "sex": "女" },{ "name": "小强", "age": 13, "sex": "男" }];var list = document.getElementById('list');// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中for (let i = 0; i < arr.length; i++) {list.innerHTML += `<li><div class="hd">${arr[i].name}的基本信息</div>    <div class="bd"><p>姓名:${arr[i].name}</p>    <p>性别:${arr[i].sex}</p>    <p>年龄:${arr[i].age}</p>    </div>    </li>`;}</script>
    </body></html>
    
  • 模板引擎:解决数据变为视图的最优雅的方法

二、mustache基本使用 — 怎么用

2.1 mustache库简介

  • mustache官方git

  • mustache是"胡子"的意思,因为它的嵌入标记{{}}非常像胡子

  • {{}}的语法也被Vue沿用

  • mustache是最早的模板引擎库,诞生早于Vue,它的底层实现机理在当时非常有创造性,轰动性,为后续模板引擎的发展提供了崭新的思路

2.2 mustache库基本使用

  • 必须要引入mustache库,可以在bootcdn.com找到它

  • mustache的模板语法非常简单,比如前述案例的模板语法如下:

    #arr表示开始循环arr数组,/arr表示arr数组循环结束,内部使用元素直接使用键进行标识即可。

2.2.1 循环对象数组

templateStr:模板字符串

data:数据

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="container"></div><!-- 模板 --><script type="text/template" id="mytemplate"><ul>{{#arr}}<li><div class="hd">{{name}}的基本信息</div>    <div class="bd"><p>姓名:{{name}}</p>    <p>性别:{{sex}}</p>    <p>年龄:{{age}}</p>    </div></li>{{/arr}}</ul></script><script src="jslib/mustache.js"></script><script>var templateStr = document.getElementById('mytemplate').innerHTML;var data = {arr: [{ "name": "小明", "age": 12, "sex": "男" },{ "name": "小红", "age": 11, "sex": "女" },{ "name": "小强", "age": 13, "sex": "男" }]};var domStr = Mustache.render(templateStr, data);var container = document.getElementById('container');container.innerHTML = domStr;</script>
</body></html>

2.2.2 不循环

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div id="container"></div><script src="jslib/mustache.js"></script><script>var templateStr = `<h1>我买了一个{{thing}},好{{mood}}啊</h1>`;var data = {thing: '华为手机',mood: '开心'};var domStr = Mustache.render(templateStr, data);var container = document.getElementById('container');container.innerHTML = domStr;</script>
</body>
</html>

2.2.3 循环简单数组

循环简单数组使用.标识每一项数据

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div id="container"></div><script src="jslib/mustache.js"></script><script>var templateStr = `<ul>{{#arr}}<li>{{.}}</li>    {{/arr}}</ul>`;var data = {arr: ['A', 'B', 'C']};var domStr = Mustache.render(templateStr, data);var container = document.getElementById('container');container.innerHTML = domStr;</script>
</body>
</html>

2.2.4 数组可以嵌套

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div id="container"></div><script src="jslib/mustache.js"></script><script>var templateStr = `<ul>{{#arr}}<li>{{name}}的爱好是:<ol>{{#hobbies}} <li>{{.}}</li>{{/hobbies}} </ol></li>    {{/arr}}</ul>`;var data = {arr: [{'name': '小明', 'age': 12, 'hobbies': ['游泳', '羽毛球']},{'name': '小红', 'age': 11, 'hobbies': ['编程', '写作文', '看报纸']},{'name': '小强', 'age': 13, 'hobbies': ['打台球']},]};var domStr = Mustache.render(templateStr, data);var container = document.getElementById('container');container.innerHTML = domStr;</script>
</body>
</html>

2.2.5 布尔值

可以通过#temp中temp的bool值来决定内部内容是否显示

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div id="container"></div><script src="jslib/mustache.js"></script><script>var templateStr = `{{#m}}<h1>你好</h1>{{/m}}`;var data = {m: false};var domStr = Mustache.render(templateStr, data);var container = document.getElementById('container');container.innerHTML = domStr;</script>
</body>
</html>

三、mustache的底层核心机理 — 底层机理

3.1 mustache库不能用简单的正则表达式思路是先

  • 在较为简单的示例情况下,可以用正则表达式实现

    模板字符串:

    数据:

    模拟实现:

    字符串的replace方法,可以第一个参数接收一个正则,第二个参数接收一个函数,有四个参数。

    第一个参数代表正则匹配的部分,第二个参数代表()中捕获到的内容,第三个参数代表位置,第四个参数代表源字符串

    <!DOCTYPE html>
    <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
    </head><body><script>var templateStr = '<h1>我买了一个{{thing}},花了{{money}}元,好{{mood}}</h1>';var data = {thing: '白菜',money: 5,mood: '激动'};// 最简单的模板引擎的实现机理,利用的是正则表达式中的replace()方法。// replace()的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,就是$1// 结合data对象,即可进行智能的替换function render(templateStr, data) {return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, $1) {return data[$1];});}var result = render(templateStr, data);console.log(result);</script>
    </body></html>
    
  • 但是在情况复杂时,正则表达式的思路肯定就不行了。比如这样的模板字符串,是不能用正则表达式的思路实现的

    模板字符串:

3.2 mustache库的机理

3.3 什么是tokens?

  • tokens是一个JS的嵌套数组,说白了,就是模板字符串的JS表示

  • 它是"抽象语法树"、"虚拟节点"等等的开山鼻祖

    模板字符串:

    tokens:

    tokens是将字符串转换成了JS的嵌套数组,如果是纯文本(包括HTML标签),会被标识为text

    如果是带有双括号({{}})的参数,会被标识为name

3.4 非常有创造性的、轰动性的模板解析原理

3.5 循环情况下的tokens

  • 当模板字符串中有循环存在时,它将被编译为嵌套更深的tokens

3.6 双重循环情况下的tokens

  • 当循环是双重的,那么tokens会更深一层

3.7 mustache库的机理

mustache库底层重点要做两个事情:

  • 将模板字符串编译为tokens形式
  • 将tokens结合数据,解析为dom字符串

3.8 观察token

把256行这里

改为

四、手写实现mustache库 — 手写

4.1 使用webpack和webpack-dev-server构建

  • 模块化打包工具有webpack(webpack-dev-server)、rollup、Parcel等

  • mustache官方库使用rollup进行模块化打包,而我们使用webpack(webpack-dev-server)进行模块化打包,这是因为webpack(webpack-dev-server)能更方便的在浏览器中(而不是nodeJS环境中)实时调试程序,相比nodeJS控制台,浏览器控制台更好用,比如能够点击展开数组的每一项

  • 生成库是UMD的,可以同时在nodeJS环境中使用,也可以在浏览器环境中使用。实现UMD不难,只需要一个"通用头"即可

4.2 注意webpack和webpack-dev-server的版本

webpack最新版是5,webpack-dev-server最新版是4,但是目前最新版兼容成都不好,建议使用如下版本:

"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"

4.3 webpack.config.js文件

const path = require('path');module.exports = {mode: 'development',entry: './index.js',output: {filename: 'bundle.js'},devServer: {contentBase: path.join(__dirname, 'www'),compress: false,port: 8080,// 虚拟打包的路径,bundle.js文件没有真正的生成publicPath: '/xuni/'}
}

4.4 开发注意事项

  • 学习源码时,源码思想要借鉴,而不要抄袭。

    要能够发现源码中书写精彩的地方。

  • 将独立的功能拆分为独立的js文件中完成,通常是一个独立的类,每个单独的功能必须能独立的"单元测试"

  • 应该围绕中心功能,先把主干完成,然后修剪枝叶

  • 功能并不需要一步到位,功能的拓展要一步步完成,有的非核心功能甚至不需要实现

4.5 将模板字符串变为tokens

4.6 生成tokens数组

4.7 将零散的tokens嵌套起来

4.7.1 栈是什么

4.8 将tokens结合数据,解析为dom字符串

粗浅的完成简单的目标:

不带有#标记

  • #标记的tokens,需要递归处理它的下标为2的小数组

  • 现在遇到一个问题,JS不认识点符号

五、源码

项目目录

index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>
<div id="container"></div>
<script src='./xuni/bundle.js'></script>
<script>// 模板字符串// let templateStr = '我买了一个{{thing}}, 好{{mood}}啊';let templateStr = `<div><ol>{{#students}}<li>学生{{name}}的爱好是<ol>{{#hobbies}}<li>{{.}}</li>{{/hobbies}}</ol></li>{{/students}}</ol></div>`;// 数据let data = {students: [{'name': '小明', 'hobbies': ['游泳', '健身']},{'name': '小红', 'hobbies': ['足球', '篮球', '羽毛球']},{'name': '小强', 'hobbies': ['吃饭', '睡觉']}]};// let templateStr = '我爱{{somebody}},{{somebody}}也爱我';// let data = {//   somebody: 'kai'// }// 构造const DOMSTR = HK_TemplateEngine.render(templateStr, data);console.log(DOMSTR);document.getElementById('container').innerHTML = DOMSTR;
</script>
</body>
</html>

index.js

import parseTemplateToTokens from "./parseTemplateToTokens";
import renderTemplate from "./renderTemplate";// 全局提供HK_TemplateEngine方法
window.HK_TemplateEngine = {// 渲染方法render(templateStr, data) {// 调用parseTemplateToTokens方法,让模板字符串可以变成数组let tokens = parseTemplateToTokens(templateStr);// 调用renderTemplate函数,让tokens数组变为dom字符串return renderTemplate(tokens, data);}
}

lookup.js

/*** 功能是可以在dataObj对象中,寻找用连续点符号的keyName属性* 比如,dataObj是* {*   a:{*     b:{*       c:100*     }*   }* }* 那么lookup(dataObj, 'a.b.c')结果就是100*/
export default function lookup(dataObj, keyName) {// 看看keyName中有没有点符号if (keyName.indexOf('.') !== -1 && keyName !== '.') {// 如果有点符号,那么拆开let keys = keyName.split('.');// 设置一个临时变量,这个临时变量用于周转,一层一层找下去let temp = dataObj;// 每找一层,就把它设置为新的临时变量for (let i = 0; i < keys.length; i++)temp = temp[keys[i]];return temp;}// 如果没有点符号return dataObj[keyName];
};

nextTokens.js

/*** 折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标3* @param tokens*/
export default function nextTokens(tokens) {// 结果数组let nestedTokens = [];// 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的数组中当前操作的这个tokens小数组let sections = [];// 收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组// 收集器的指向会变化,当遇到#的时候,收集器会指向这个item的下标为2的新数组let collector = nestedTokens;tokens.forEach(token => {switch (token[0]) {case '#':// 收集器中放入tokencollector.push(token);// 入栈sections.push(token);// 更换收集器。给item添加下标为2的项,并让收集器指向它collector = token[2] = [];break;case '/':// 出栈 pop()方法返回出栈结果sections.pop();// 改变收集器为栈结构队尾(队尾是栈顶)那项下标为2的数组collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;break;default:collector.push(token);}})return nestedTokens;
};

parseArray.js

import lookup from "./loopup";
import renderTemplate from "./renderTemplate";/*** 处理数组,结合renderTemplate实现递归* 注意:这个函数接收的参数是token,而不是tokens* token是什么,就是一个简单的["#", 'students', []]* 这个函数要递归调用renderTemplate函数* 调用次数由data决定*/
export default function parseArray(token, data) {// 得到整体数据data中这个数组要使用的部分let v = lookup(data, token[1]);// 结果字符串let resultStr = '';// 遍历v数组// 遍历v数组,v一定是数组// 注意,下面这个循环可能是整个包中最难思考的一个循环// 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。for (let i = 0; i < v.length; i++) {// 这里要补一个“.”属性// 拼接resultStr += renderTemplate(token[2], {...v[i],'.': v[i]});}return resultStr;
}

parseTemplateToTokens.js

import Scanner from './Scanner.js';
import nextTokens from "./nextTokens";/*将模板字符串变为tokens数组
*/
export default function parseTemplateToTokens(templateStr) {let tokens = [];// 创建扫描器let scanner = new Scanner(templateStr);let words;// 让扫描器工作while (!scanner.eos()) {// 收集开始标记出现之前的文字words = scanner.scanUtil('{{');if (words !== '') {// 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格// 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格let isInJJH = false;// 空白字符串let _words = '';for (let i = 0; i < words.length; i++) {// 判断是否在标签里if (words[i] === '<') {isInJJH = true;} else if (words[i] === '>') {isInJJH = false;}// 如果这项不是空格,拼接上if (!/\s/.test(words[i])) {_words += words[i];} else {// 如果这项是空格,只有当它在标签内的时候,才拼接上if (isInJJH) {_words += ' ';}}}// 存起来,去掉空格tokens.push(['text', _words]);}// 过双大括号scanner.scan('{{');// 收集开始标记出现之前的文字words = scanner.scanUtil('}}');if (words !== '') {// 这个words就是{{}}中间的东西。判断一下首字符if (words[0] === '#') {// 存起来,从下标为1的项开始存,因为下标为0的项是#tokens.push(['#', words.substring(1)]);} else if (words[0] === '/') {// 存起来,从下标为1的项开始存,因为下标为0的项是/tokens.push(['/', words.substring(1)]);} else {// 存起来tokens.push(['name', words]);}}// 过双大括号scanner.scan('}}');}// 返回折叠收集的tokensreturn nextTokens(tokens);
}

renderTemplate.js

import lookup from "./loopup";
import parseArray from "./parseArray";/*** 函数的功能是让tokens数组变为dom字符串*/
export default function renderTemplate(tokens, data) {// 结果字符串let resultStr = '';// 遍历tokenstokens.forEach(token => {// 看类型if (token[0] === 'text') {resultStr += token[1];} else if (token[0] === 'name') {// 如果是name类型,那么就直接使用它的值,当然要用lookup// 因为防止'a.b.c'有逗号的形式resultStr += lookup(data, token[1]);} else if (token[0] === '#') {resultStr += parseArray(token, data);}})return resultStr;
}

Scanner.js

/*** 扫描器类*/export default class Scanner {constructor(templateStr) {this.templateStr = templateStr;// 指针this.pos = 0;// 尾巴,一开始就是模板字符串原文this.tail = templateStr;}// 功能弱,就是走过指定内容,没有返回值scan(tag) {if (this.tail.indexOf(tag) === 0) {// tag有多长,就让指针后移多少位this.pos += tag.length;// 尾巴也要变,改变尾巴为从当前指针这个字符开始,到最后的全部字符this.tail = this.templateStr.substring(this.pos);}}// 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字scanUtil(stopTag) {// 记录一下执行本方法的时候pos的值const POS = this.pos;// 当尾巴的开头不是stopTag的时候,说明还没有扫描到stopTag// 写&&很有必要,因为防止找不到,那么寻找到最后也要停下来while (!this.eos()&&this.tail.indexOf(stopTag) !== 0)// 改变尾巴为从当前指针这个字符开始,到最后的全部字符this.tail = this.templateStr.substr(++this.pos);return this.templateStr.substring(POS, this.pos);}// 指针是否已经到头,返回布尔值。 end of stringeos() {return this.pos >= this.templateStr.length;}
}

六、总结

  • Mustache底层太美了!tokens的意义也不言自明了。如果没有token,那么数组的循环形式,就很难处理。
  • 在Mustache的源码中,还有Context类和Writer类,在代码演示中,对其进行了简化,但不影响主干功能。

Vue源码:mustache模板引擎学习相关推荐

  1. VUE源码:模板引擎mustache

    文章目录 模板引擎的定义 mustache的基本使用 手写原理代码(简化版) 模板引擎的定义 模板引擎就是将数据变为视图最优雅的解决方案 例如:VUE的v-for.mustache 历史上数据变为视图 ...

  2. [Vue源码分析] 模板的编译

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: Vue有两个版本:Runtime + Compiler . Runtime only ,前者是包含编译代码的版本,后者不包含编译代码,编 ...

  3. Vue源码之mustache模板引擎(二) 手写实现mustache

    Vue源码之mustache模板引擎(二) 手写实现mustache mustache.js 个人练习结果仓库(持续更新):Vue源码解析 webpack配置 可以参考之前的笔记Webpack笔记 安 ...

  4. Vue源码之mustache模板引擎(一)

    Vue源码之mustache模板引擎(一) 个人练习结果仓库(持续更新):Vue源码解析 抽空把之前学的东西写成笔记. 学习视频链接:[尚硅谷]Vue源码解析之mustache模板引擎 模板引擎是什么 ...

  5. 【Vue源码】mustache模板引擎 - 基本使用 - 底层原理 - 手写实现

    文章目录 1. 模板引擎的介绍 1.1 模板引擎是什么? 1.2 模板引擎是怎么来的?(发展历史) 1. 使用原生的DOM操作 2. 使用数组中的join方法 3. 使用ES6反引号的方法 2. mu ...

  6. 学习Vue的mustache语法-mustache模板引擎

    学习地址 : https://www.bilibili.com/video/BV1EV411h79m?vd_source=a81826692f4afea80764f4048dc1ae0a 代码地址 : ...

  7. vue实例没有挂载到html上,vue 源码学习 - 实例挂载

    前言 在学习vue源码之前需要先了解源码目录设计(了解各个模块的功能)丶Flow语法. src ├── compiler # 把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能. ├── ...

  8. vue源码学习--vue源码学习入门

    本文为开始学习vue源码的思路整理.在拿到vue项目源码的之后看到那些项目中的文件夹,会很困惑,不知道每个文件夹内的世界,怎么变换,怎样的魔力,最后产生了vue框架.学习源码也无从学起.我解决了这些困 ...

  9. 学习vue源码(14)深入学习diff

    大白话简述 这一节,先对diff进行简单的描述,不会出现任何的源码,只是为了帮助大家建立一种思路,了解下 Diff 的大概内容. 1.Diff 的作用2.Diff 的做法3.Diff 的比较逻辑4.简 ...

  10. 学习vue源码(14)就慢慢由表入里学习diff

    大白话简述 这一节,先对diff进行简单的描述,不会出现任何的源码,只是为了帮助大家建立一种思路,了解下 Diff 的大概内容. 1.Diff 的作用2.Diff 的做法3.Diff 的比较逻辑4.简 ...

最新文章

  1. __purecall 链接错误
  2. Windows Azure 将正式更名为 Microsoft Azure
  3. 映月城与电子姬服务器维护,映月城与电子姬11月16日更新公告 加强玩家作弊检测增加举报功能...
  4. Java第十二次作业:继承与抽象类解决工人与学生的问题,抽象类实例。抽象类作用——为多态创造了可能。抽象类的作用总结...
  5. videoJs常用方法、事件、VUE中使用的注意事项
  6. 遇到一个奇怪的问题——关于VS2013、VS2015中字符集(多字节字符集和Unicode字符集)的选择
  7. UWB定位系统部署原则
  8. GIF微信表情如何制作
  9. 关于使用ArcGIS裁剪栅格后像元值发生变化的问题
  10. 前端Vue 对称加密(AES),后台php解密
  11. Azure虚拟机部署Skype for Business Server 三、部署Skype前端服务器
  12. 华三服务器如何修改默认ip,H3C路由器默认登录入口 192.168.124.1 设置步骤
  13. 百家姓中国心头像大全
  14. java 堆栈内存例子,内存溢出OOM和堆栈溢出SOF的示例
  15. 海量数据大课学习笔记(2)-不在其位要谋其政,技术Leader能力模型提升-小滴课堂
  16. OA系统是把无所不能的“万能钥匙”?
  17. 解决CSDN上传MD文件不能显示照片
  18. 基于UX-APP的学伴原型化系统
  19. 电脑怎么设置定时关机?用这个方法就可以搞定
  20. arm为什么不支持java_为什么在Java 7中没有为ARM改进StAX类

热门文章

  1. 4.业务架构·应用架构·数据架构实战 --- 业务驱动的应用架构设计
  2. 20.高性能MySQL --- 在MySQL上使用 Sphinx
  3. 7.docker pull
  4. 28.程序管理(ps,top)
  5. 4. ex 命令(1)
  6. 15. 发货选项(Shipping Options)
  7. 14. PHP 数组排序
  8. 循序渐进之Spring AOP(1) - 原理
  9. python报错Nonetype object is not iterable
  10. MFC绘制图片闪烁详解