ReactJS前后端复用代码

越来越多的前端应用开始基于 Facebook 的 React,React 在组件化方面有着更为灵活的实现和简便的配置,在组件的使用成本上也有很大程度的降低。

除了这些优点,了解 React 服务端渲染的同学应该还知道 React 还有一个非常大的特性——前后端同构特性,本文就将基于这一特性去研究如何设计一个可以前后端公用的组件。

React 前后端同构的原理
const React = require('react');

class myComponent extends React.Component {
 constructor (props) {
 super(props);
 }

 render () {
 return <h1>This is title.</h1>;
 }
}

React 由 render() 方法定义该组件最后输出的 html 片段;前端利用 React 的内置渲染引擎将其渲染成 DOM 树,而后端也同样提供了两个方法去渲染一个 React 组件:

React.renderToString 是把 React 元素转成一个 HTML 字符串,因为服务端渲染已经标识了 reactid,所以在浏览器端再次渲染,React 只是做事件绑定,而不会将所有的 DOM 树重新渲染,这样能带来高性能的页面首次加载!同构原理主要从这个 API 而来。
React.renderToStaticMarkup,这个 API 相当于一个简化版的 renderToString,如果你的应用基本上是静态文本,建议用这个方法,少了一大批的 reactid,DOM 树自然精简了,在 IO 流传输上节省一部分流量。
由这两个方法在服务端生成的 html 字符串输出到浏览器,浏览器生成对应的 DOM 树,然后再引用编译好的 react js 文件,再次在浏览器端进行 render。

由于输出的 DOM 树结构并无改变,所以肉眼看起来页面并没有再次渲染(一些现代高级浏览器也会对页面的 DOM 树进行对比,如果 DOM 树 结合 CSS 的渲染树并没有改变,也不会真正进行渲染),这就是 React 实现前后端同构的主要原理。

前后端公用一套组件进行页面渲染的优势
首屏速度
首屏服务端渲染的好处是可以让用户更早的见到页面主体,而且服务端渲染另外一个好处是可以减少很多 ajax 接口请求,利用 nodejs 的事件机制,也可以很方便的在 Node 端实现接口的并行异步获取。

搜索引擎友好
搜索引擎只能抓取“页面源代码”中的字符串。

成本 & 可维护性
前后端公用一套组件,无疑至少减少了一半的文件,维护成本至少降低一半。

更友好的渐进增强
对于不支持/禁用 javascript 的浏览器,避免了直接前端渲染无法显示内容的劣势。

组件设计
前后端公用的 React 组件需要满足以下几个条件:

初始化 constructor 方法中不能引用 BOM 对象,因为 BOM 对象只存在于浏览器宿主对象中,Node 端会报错;解决办法;可以做下容错处理,比如:
window && window.location.href = 'xxx';

ES6 语法的处理:前端:编译成 ES5
Node 端:利用 webpack-dev-middleware/koa-webpack-dev-middleware 中间件将 ES6 语法转化成 ES5(经过 babel 处理) 并打包好存储在内存中
const app = require('koa')();
const webpackMiddleware = require("koa-webpack-dev-middleware");
app.use(webpackMiddleware({
 entry: "...",
 output: {
 path: "/"
 // no real path is required, just pass "/" 
 // but it will work with other paths too. 
 }
}));

组件文件结构规范
 

Node 玩转 React
下面以 koa 为基础框架,搭建一个支持渲染 React 的开发环境

给 koa 添加一个可以渲染 react 的中间件——reactView
// lib/reactView.js
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');

class ReactView {
 constructor (app) {
 this.app = app;
 }

 render (tpl, props) {
 return new Promise((resolve, reject) => {
 let viewPath = this.app.config.reactView.viewPath;
 let tplPath = path.join(viewPath, tpl);

 try {
 let module = require(tplPath);
 let Factory = React.createFactory(module);
 let Component = Factory(props);

 let renderString = ReactDOMServer.renderToString(Component);

 resolve(renderString);
 } catch (e) {
 reject(e);
 }
 });
 }
}

modue.exports = function (app) {
 app.context.renderReact = new ReactView(app);
};

在 koa 中使用该插件:

// app.js
const koa = require('koa');
const koaRouter = require('koa-router');
const reactView = require('./lib/reactView');

const App = () => {
 let app = koa();
 let router = koaRouter();

 router.get('/', function*() {
 this.body = this.render('index', {
 title: 'test title'
 });
 });

 // 给应用注册 reactView 插件
 reactView(app, {
 viewPath: 'app/views'
 });

 app.use(router.routes());

 return app;
};

const createApp = () => {
 const app = App();

 app.listen(3000, ()=> {
 console.log('3000 is listening!');
 });

 return app;
};

createApp();

新建一个 react 组件来进行渲染:

// app/views/index.js
const React = require('react');

class myComponent extends React.Component {
 constructor (props) {
 super(props); 
 }

 componentDidMount () {
 // 组件渲染之后,做点啥
 }

 render () {
 let title = this.props.title;

 return <h1>{title}</h1>;
 }
}

利用 webpack 将组件打包成 ES5 语法,发布到 cdn
定义 entry.js 将模块打包好:

// entry.js
'use strict';

const ReactDom = require('react-dom');
const App = require('./app/view/index.js');

// appData 从全局 window.appData 中获取
ReactDom.render(<App appData={require('appData')['index']} />, document.querySelector('#id');

// webpack.dev.js
const webpackConfig = {
 entry: 'entry.js',

 devtool: '#eval',

 resolve: {
 extensions: ['', '.js', '.jsx'],
 },

 resolveLoader: {
 root: path.join(__dirname, '../node_modules'),
 },

 module: {/**/},

 externals: {
 appData: 'window.appData',
 React: 'window.React',
 ReactDOM: 'window.ReactDOM',
 }
};

后端数据直接通过 props 的方式传递给组件
前端可以从后端传递给 window.appData 的对象中获取数据
这样就实现了 React 前后端同构渲染。

Nodejs调试方法

介绍三种调试Node的方法:命令行调试、Node-Inspector、使用WebStrom进行调试。

待调试文件index.js,以下调试方法均是基于该文件:

var http=require('http');

http.createServer(function(req,res){
    res.writeHead(200,{'Content-Type':'text/html'});
    res.write('<h1>Node.js</h1>');
    res.write('<p>Hello World</p>');
}).listen(3000);
console.log("listening at port 3000");

方式一:命令行调试Debugger

Debugger是Node内置的调试工具。下面主要介绍使用Debugger的调试步骤以及各调试指令的含义。

1. 在代码中加入断点

调试前,通过语句debugger;设置断点位置,修改index.js文件如下:

var http=require('http');

http.createServer(function(req,res){
    res.writeHead(200,{'Content-Type':'text/html'});
    debugger;   //第一个断点
    res.write('<h1>Node.js</h1>');
    debugger;   //第二个断点
    res.write('<p>Hello World</p>');
}).listen(3000);
console.log("listening at port 3000");

2.通过命令行进入调试模式

在浏览器中访问要调试的页面,在命令行输入以下命令:

$node debug index.js 

开启Node的调试功能,输出结果如下:

11
代码执行到debugger;语句会暂停,提示输入指令。
输入指令进行调试,如下图:

12

相关指令含义

以下部分来自Debugger

步进指令

  • cont或c —— 继续执行
  • next或n —— 执行到下一个断点
  • step或s —— 进入函数内部
  • out或o ——跳出函数
  • pause ——暂停执行

断点操作

  • setBreakpoint()或sb() —— 在当前行设置断点
  • setBreakpoint(line)或sb(line) —— 在特定行line设置断点
  • setBreakpoint(‘fn()’), sb(…) —— 在函数fn内部第一个声明出设置断点
  • setBreakpoint(‘script.js’, 1), sb(…) —— 在文件script.js第一行设置断点
  • clearBreakpoint, cb(…) —— 删除断点

信息查看

  • backtrace或bt —— 输出当前执行下的堆栈信息
  • list(5) —— 输出当前上下各5行源代码
  • watch(expr) —— 添加表达式expr至观察列表
  • unwatch(expr) —— 从观察列表中移除表达式expr
  • watchers —— 列出观察列表中所有的表达式和他们的值
  • repl —— 调试脚本上下文中代码

执行控制

  • run —— 启动脚本
  • restart —— 重启脚本
  • kill —— 终止脚本

其他

  • scripts – 列出所有已加载的脚本
  • version – 显示 V8 的版本

方式二:Node-inspector

Node Inspector是基于方式一中的API实现的界面化的Node调试工具,不过node-inspector需要在Webkit内核下浏览器进行调试。以下是调试步骤:

1. 全局安装node-inspector

$ npm install node-inspector -g

2. 启动node-inspector监听Nodejs的debug调试端口

$ node-inspector

命令行输出如下图:
1

3. 打开浏览器,输入上图中显示地址

http://127.0.0.1:8080/debug?port=5858,将看到如下调试窗口:

2

4. 将需要调试的代码文件载入到上述调试窗口。

再打开一个命令行,输入:

$ node --debug index.js  

此时刷新http://127.0.0.1:8080/debug?port=5858会发现index.js已载入调试窗口,如下图所示:

3

5. 新打开一个浏览器页签,刷新你要调试的页面.

当执行到设置断点的位置时,页面会暂停加载,此时回到调试窗口node-inspector页签,能看到详细的调试信息:
4

PS:

在最新的API中,可以将步骤2,4统一为以下命令:

$ node-debug index.js

方式三:使用WebStorm调试Node

1. 配置需调试的Nodejs开发文件夹

根据下图依次操作,对需要调试的文件添加配置信息:
5
6
7

2.启动调试并设置断点

启动调试:

8

将出现调试窗口以及控制台,如下图:

9

3.在浏览器中访问要调试的页面

执行到断点位置时,页面会停止加载,可以通过WebStrom的调试窗口看到详细信息:

10

Gulpjs 的日常

全局安装Gulpjs

npm install -g gulp  #全局安装

局部安装Gulpjs

npm install gulp –save-dev # 局部安装

全局安装
1. 将安装包放在 /usr/local 下
2. 可以直接在命令行里使用

本地安装
1. 将安装包放在 ./node_modules 下(运行npm时所在的目录)
2. 可以通过 require() 来引入本地安装的包

 

==================

前端项目需要的功能:
1、图片(压缩图片支持jpg、png、gif)
2、样式 (支持sass 同时支持合并、压缩、重命名)
3、javascript (检查、合并、压缩、重命名)
4、html (压缩)
5、客户端同步刷新显示修改
6、构建项目前清除发布环境下的文件(保持发布环境的清洁)

通过gulp plugins寻找gulp组件
gulp-imagemin: 压缩图片
gulp-ruby-sass: 支持sass
gulp-minify-css: 压缩css
gulp-jshint: 检查js
gulp-uglify: 压缩js
gulp-concat: 合并文件
gulp-rename: 重命名文件
gulp-htmlmin: 压缩html
gulp-clean: 清空文件夹
gulp-livereload: 服务器控制客户端同步刷新

================

下面是一个简单的任务:

编写gulp任务

/**
* 组件安装
* npm install gulp-util gulp-imagemin gulp-ruby-sass gulp-minify-css gulp-jshint gulp-uglify gulp-rename gulp-concat gulp-clean gulp-livereload tiny-lr –save-dev
*/

// 引入 gulp及组件
var gulp    = require(‘gulp’),                 //基础库
imagemin = require(‘gulp-imagemin’),       //图片压缩
sass = require(‘gulp-ruby-sass’),          //sass
minifycss = require(‘gulp-minify-css’),    //css压缩
jshint = require(‘gulp-jshint’),           //js检查
uglify  = require(‘gulp-uglify’),          //js压缩
rename = require(‘gulp-rename’),           //重命名
concat  = require(‘gulp-concat’),          //合并文件
clean = require(‘gulp-clean’),             //清空文件夹
tinylr = require(‘tiny-lr’),               //livereload
server = tinylr(),
port = 35729,
livereload = require(‘gulp-livereload’);   //livereload

// HTML处理
gulp.task(‘html’, function() {
var htmlSrc = ‘./src/*.html’,
htmlDst = ‘./dist/’;

gulp.src(htmlSrc)
.pipe(livereload(server))
.pipe(gulp.dest(htmlDst))
});

// 样式处理
gulp.task(‘css’, function () {
var cssSrc = ‘./src/scss/*.scss’,
cssDst = ‘./dist/css’;

gulp.src(cssSrc)
.pipe(sass({ style: ‘expanded’}))
.pipe(gulp.dest(cssDst))
.pipe(rename({ suffix: ‘.min’ }))
.pipe(minifycss())
.pipe(livereload(server))
.pipe(gulp.dest(cssDst));
});

// 图片处理
gulp.task(‘images’, function(){
var imgSrc = ‘./src/images/**/*’,
imgDst = ‘./dist/images’;
gulp.src(imgSrc)
.pipe(imagemin())
.pipe(livereload(server))
.pipe(gulp.dest(imgDst));
})

// js处理
gulp.task(‘js’, function () {
var jsSrc = ‘./src/js/*.js’,
jsDst =‘./dist/js’;

gulp.src(jsSrc)
.pipe(jshint(‘.jshintrc’))
.pipe(jshint.reporter(‘default’))
.pipe(concat(‘main.js’))
.pipe(gulp.dest(jsDst))
.pipe(rename({ suffix: ‘.min’ }))
.pipe(uglify())
.pipe(livereload(server))
.pipe(gulp.dest(jsDst));
});

// 清空图片、样式、js
gulp.task(‘clean’, function() {
gulp.src([‘./dist/css’, ‘./dist/js’, ‘./dist/images’], {read: false})
.pipe(clean());
});

// 默认任务 清空图片、样式、js并重建 运行语句 gulp
gulp.task(‘default’, [‘clean’], function(){
gulp.start(‘html’,‘css’,‘images’,‘js’);
});

// 监听任务 运行语句 gulp watch
gulp.task(‘watch’,function(){

server.listen(port, function(err){
if (err) {
return console.log(err);
}

// 监听html
gulp.watch(‘./src/*.html’, function(event){
gulp.run(‘html’);
})

// 监听css
gulp.watch(‘./src/scss/*.scss’, function(){
gulp.run(‘css’);
});

// 监听images
gulp.watch(‘./src/images/**/*’, function(){
gulp.run(‘images’);
});

// 监听js
gulp.watch(‘./src/js/*.js’, function(){
gulp.run(‘js’);
});

});
});

移动端前端开发调试概览

回顾前端开发的发展历程,我们经历过alert时代、Firefox插件、Opera DragonFly、Safari开发工具及Chrome DevTools等等,这些工具的丰富演化为我们提升了大把的效率,重要性不言而喻。

以前我们更多精力“释放”在桌面端(PC端)的前端开发调试,因为有 Firebug、Chrome DevTools 等工具,直接右击打开就可以看到元素的 CSS,并且可以查看各种资源、修改运行调错 JS 等。而移动端浏览器显然没法带有这些功能,那么我们就要花费一些精力来研究如何更方便地进行移动端的开发调试工作,所以接下来梳理一下目前业界或公司内部常用的一些调试方法,便于大家在工作中可以尝试使用。

基于 Android 的移动端前端开发调试

Android 系统是份额最大的移动端设备操作系统;一方面,Android 是 Google 开发的,浏览器等也是基于 Blink 内核(早期版本基于 Webkit ),同根同宗所以技术上应该是没有问题的。另一方面,Google 无偿开源 Android 系统,结果导致很多不同厂商自己乱改 Android 系统,各种版本系统遍地生花,导致各种 BUG 层出不穷。

那么,下面会通过 “虚拟机” 和 “真机” 两方面来介绍调试工具:

Android 虚拟机测试

目前在业界普遍采用的虚拟机多以 “ Genymotion ” 和 “ Parallels ” 两款软件为主,拥有一定量的使用群里,相应的使用资料也比较丰富,对于使用者可以提供最大的支持。

Genymotion (安卓模拟器)

Genymotion是一个很棒的 Android 虚拟机。但是首次安装配置稍显麻烦一些。同时由于基于VirtualBox内核,所以要先安装 VirtualBox,然后需要先注册账号才能进行下载,下载页面提供两种类型,一种是针对个人的免费版本(即有很多功能限制);另外一种就是需要付费的高端使用版。

个人感觉安装虚拟机的方式稍显麻烦,针对系统或硬件也有一定要求,所以选择性使用吧。

Parallels

Parallels 是基于 Mac 平台的虚拟机,使用它创建虚拟机的时候,可以直接下载 Android 系统并安装,超级傻瓜的操作,但是超级好用。(官网上经常会搞活动价,买一套也不贵¥300不到)

安装完虚拟机,就可以用里面的浏览器打开网页进行测试了,虚拟机与本机处于一个局域网,可以用局域网 IP 来调试本地页面。

Android 真机调试

使用真机测试需要一些必备条件:

  • Chrome 版本必须高于 32
  • 测试机 Android 系统要高于 4.0
  • 测试机安装 Chrome for Android 才可以使用 Chrome 远程调试这项功能

满足以上几个条件的话,就可以采用一下步骤进行真机调试了:

  1. 先用USB数据线将 Android 测试机连接到电脑上。
  2. 需要打开测试机上面“开发者选项”中的 “USB 调试”功能。
  3. 然后在桌面版 Chrome 中输入 chrome://inspect 即可查找你的设备,在移动设备中的 Chrome 浏览器中打开要测试的网页即可,然后就可以在桌面版 Chrome DevTools 调试操作移动设备上的页面了。

在 Android 4.2+ 系统上“开发者选项”默认是隐藏的,所以你需要先开启“开发者选项”(开启方法:设置 -》 关于本机 -》 猛击版本号(Build number)多次 即可开启开发者选项)。之后如果没有开启,或者没有反应,可能是你的版本问题或者点击错了,你可以尝试把 关于本机 上所有的选项都猛击几次,就会开启。

更多细节不再赘述,可以查看 Remote Debugging 官方文档

基于 Android WebView 的开发调试

现在越来越多的移动端 APP 是 WebView,因为开发方便,更新快捷。那么就会有调试 WebView 的需求,因为他们本身就是网页。

使用 Weinre 调试

说起Weinre,是个比较早期的调试方式了,初期使用起来很“蹩脚”,但在处理调试html/CSS元素、请求跟踪和日志输出方面还是可以起到很大帮助的;随着Web技术的日新月异,Weinre的使用也是越来越方便了,直接通过 NPM 可以直接快速安装了。

npm install -g weinre

Weinre 的实现原理很简单,就是它会在你本地创建一个监听服务器,并提供一个 JavaScript,你只需要在需要测试的页面中加载这段 JS,就可以被 Weinre 监听到,在 Inspect 面板中调试你这个页面即可。

图片中红色剪头所指就说明了已经的开启本地监听服务器。

打开本地服务器地址,会看到一些Weinre的相关说明;箭头所指的这段脚本,我们需要把这个 JS 放到我们要调试的页面中,这样访问页面的时候,加载这个 JS,就会被 Weinre 监听到进行控制。

注意这段脚本后面的 #anonymous 起到的是标识作用,为了区分不同的监听,我们可以将其修改成 任何便于识别的名称放到页面中。

在调试移动设备时你可能需要在本地搭建一个局域网 IP 的服务器,可以尝试Charles等代理工具将设备与本机网络连接成一个局域网,用移动设备访问这个网页即可。或者还可以尝试结合一些实时同步的工具更加事半功倍,例如BrowserSync + Weinre 跨平台移动端调试方案

 

基于 iOS 的移动端前端开发调试

iPhone 等一系列苹果设备对前端还是相当友好的,性能够好,Safari 浏览器也是不错,型号固定统一,问题也比较好解决,此外苹果公司也提供了一系列开发者工具。

Safari 默认是隐藏开发选项的,在第一次使用的时候,需要在 Safari 中选择 “偏好设置”-》“高级”-》“在菜单栏中显示开发选项”:

使用 iOS Simulator 调试开发

iOS Simulator 是 Xcode 开发工具内置的 iOS 模拟器,因此该功能仅能在 Mac 系统下使用。按照如下方式即可打开:

打开之后,你可以用模拟器里面的 Safari 打开需要调试的网页。它可以直接打开本地 localhost 的页面,无须任何设置。如果需要调试,打开桌面版的 Safari,在“开发”中选择要调试的页面,即可打开 Safari 调试面板,操作使用类似于 Chrome的DevTools,便于快速上手使用。

iOS 设备真机调试

模拟器已经足够强大方便了,但有些手势操作测试以及最真实的用户体验测试还是需要真机。

使用起来非常简单,整体操作步骤如下:

  1. 首先需要在 iPhone 等设备上设置一下 Safari 浏览器,开启调试功能。具体步骤:“设置”-》“Safari”-》“高级”-》“Web 检查器”。
  2. 使用数据线连接电脑,
  3. 在设备上用 Safari 浏览器打开需要调试的页面,之后在桌面版的 Safari 开发选项中即可看到进行调试,跟用 iOS Simulator 一样,只不过现在换成了真机。

本文译自A Practical Guide to AngularJS Directives

原文地址http://www.sitepoint.com/series/a-practical-guide-to-angularjs-directives/

综述

一个指令就是一个引入新语法的东西。指令是在DOM元素上做的标记,并同时附加了一些特定的行为。例如,静态的HTML并不知道如何来创建并显示一个日期选择插件。为了将这个新语法教给HTML我们需要一条指令。这个指令将会创建一个充当日期选择器的元素。我们将在随后看到如何实现这个指令。

如果你之前已经编写过Angular应用,那么你已经使用过指令了,不管你有没有意识到这点。你可能已经使用过像是ng-modelng-repeatng-show等等这样的指令。所有这些指令都将特定的功能绑定到了DOM元素之上。例如,ng-repeat会重复特定的元素,而ng-show会有条件的展示元素。如果你想要创建一个可拖动元素的话你可能需要创建一个指令。指令背后的基本思想很简单。它通过在元素上绑定事件监听器并且将DOM变形来使HTML变得具有交互性。

从jQuery的角度来看指令

想想你如何使用jQuery来创建一个日期选择器。我们首先在HTML中添加一个普通的input字段然后在jQuery中我们调用$(element).dataPicker()来将其转换为一个日期选择器。但是,考虑一下。当一个设计师想要来检查这个标记时,他/她能够立刻猜出这个字段究竟是干什么用的吗?它仅仅是一个普通的input字段还是一个日期选择器?你必须要查看jQuery来确认这点。Angular的方法是使用指令来扩展HTML。因此,一个日期选择器的指令看上去可能如下所示:

<date-picker></date-picker>或者如下所示:

<input type='text' data-picker/>
这种创建UI成分的方法既直观又清楚。你可以看到元素就知道它的用途。

创建自定义指令

一个Angular指令可能以四种形式出现:

1.一个新的HTML元素(<date-picker></date-picker>
2.一个元素上的属性(<input type='text' date-picker/>
3.作为一个类(<input type='text' class='date-picker'/>) 4.作为注释(<!--directive:date-picker-->

当然,我们完全可以决定我们的指令以什么形式出现在HTML中。现在,我们来看看一个典型的Angular指令是如何写成的。它和controller的注册方式类似,但是它会返回一个简单的对象(指令定义),其中那个包含有一些配置指令的属性。下面的代码展示了一个简单和Hello World指令:

var app = angular.module('myapp',[]);   

app.directive('helloWorld',function(){
    return {
        restrict: 'AE',
        replace: true,
        template: '<h3>Hello World!</h3>'
    }
});   

在上面的代码中,app.diretive()函数在我们的模块中注册了一个新的指令。这个函数的第一个参数是指令的名称。第二个参数是一个返回指令定义对象的函数。如果你的指令对额外的对象/服务(services)例如 $rootScope, $http 或者 $compile 有依赖,它们也可以在其中被注入。这个指令可以作为一个HTML元素来使用,如下所示:

<hello-world/>   

或者:

<hello:world/>  

或者作为一个属性来使用:

<div hello-world></div>   

或者:

<div hello:world/>   

如果你想要兼容HTML5,你可以在属性前面加上x-或者data-前缀。因此,下面的标记将会匹配helloWorld指令:

<div data‐hello‐world></div>    

或者

<di vx‐hello‐world></div>      

注意

当匹配指令时,Angular会从元素/属性名之前去除前缀x-或者data-。然后将分隔符 – 或者 : 转换为驼峰表示法已匹配注册的指令。这就是为什么我们的helloWorld指令用在HTML中的时候实际上写成了hello-world。

尽管上面的这个简单的指令仅仅只是展示了一些静态的文本,其中还是有一些值得我们去探究的有趣的点。我们已经在这个指令定义对象中使用了三个属性。我们来看看这三个属性分别都有什么用:

  • restrict – 这个属性指明了一个指令应该如何在HTML中使用(记住指令可以以四种方式出现)。在这个例子中我们将它设置为’AE’。因此,这条指令可以作为一个HTML元素或者一个属性来使用。为了允许指令作为一个类来使用我们可以将restrict设置为’AEC’。
  • template – 这个实行指明了当指令被Angular编译和链接时生成的HTML标记。它不一定是一个简单的字符串。template可以很复杂,其中经常会涉及其它的指令,表达式({{}}),等等。在大多数情况下你可能会想要使用templateUrl而不是template。因此,理想情况下你应该首先将模板放置在一个单独的HTML文件中然后让templateUrl指向它。
  • replace – 这个属性指明了是否生成的模板会代替绑定指令的元素。在前面的例子中我们在HTML中使用指令为<hello-world></hello-world>,并将replace属性设置为true。因此,在指令编译后,生成的模板代替了<hello-world></hello-world>。最后的输出结果是<h3>Hello World!</h3>。如果你将replace设置为false,默认情况下,输出模板将会被插入到指令被调用的元素中。

link函数和作用域

有一个指令生成的模板是没有用的除非它在正确的作用域中北编译。默认情况下一个指令并不会得到一个新的子作用域。然而,它可以得到父作用域。这意味着如果一个指令位于在一个控制器中那么它将使用控制器的作用域。

为了利用作用域,我们可以使用一个叫做link的函数。它可以通过指令定义对象中的link属性来配置。我们现在对helloworld指令做一些修改一遍当用户在一个input字段中输入一个颜色名称时,Hello Wolld文字的背景颜色会自动发生改变。同样,当一个用户点击Hello World文字时,背景颜色会重置为白色。相应的HTML标记如下所示:

<body ng-controller='MainCtrl'>   
    <input type='text' ng-model='color' placeholder='Enter a color' / >
    <hello-wolrd/>   
</body>

修改后的helloWorld指令代码如下所示:

app.directive('helloWorld',function(){
    return {
        restrict: 'AE',   
        replace: true,
        template: '<p style="background-color:{{color}}"></p>',   
        link: function(scope,elem,attr){
            elem.bind('click',function(){
                elem.css('background-color','white');
            scope.$apply(function(){
                scope.color = "white";
            });
            });
            elem.bind('mouseover',function(){
                elem.css('cursor','pointer');
            });
        }
    }
});   

注意到link函数被用在了指令中。它接收三个参数:

  • scope – 它代表指令被使用的作用域。在上面的例子中它等同于符控制器的作用域。
  • elem – 它代表绑定指令的元素的jQlite(jQuery的一个自己)包裹元素。如果你在AngularJS被包含之前就包括了jQuery,那么它将变成jQuery包裹元素。由于该元素已经被jQuery/jQlite包裹,我们没有必要将它包含在$()中来进行DOM操作。
  • attars – 它代表绑定指令的元素上的属性。例如,如果你在HTML元素上有一些指令形式为:<hello-world some-attribute></hello-world>,你可以在link函数内用attrs.someAttribute来引用这些属性。

link函数主要是用来对DOM元素绑定事件监听器,监视模型属性变化,并更新DOM。在前面的指令代码中,我们绑定了两个监听器,click和mouseover。click处理函数重置了

的背景颜色,而mouseover处理函数则将游标改变为pointer。模板中拥有表达式{{color}},它将随着父作用域中的模型color的变化而变化,从而改变了Hello World的背景色。

Compile函数

Compile函数主要用来在link函数运行之前进行一些DOM转化。它接收下面几个参数:

  • tElement – 指令绑定的元素
  • attrs – 元素上声明的属性

这里要注意compile不能够访问scope,而且必须返回一个link函数。但是,如果没有compile函数以依然可以配置link函数。compile函数可以被写成下面的样子:

app.directive('test',function(){
    return {
        compile: function(tElem,attrs){
            //在这里原则性的做一些DOM转换   
            return function(scope,elem,attrs){
             //这里编写link函数
            }
        }
    }
});   

大多数时候,你仅仅只需要编写link函数。这是因为大部分指令都只关心与注册事件监听器,监视器,更新DOM等等,它们在link函数中即可完成。像是ng-repeat这样的指令,需要多次克隆并重复DOM元素,就需要在link函数运行之前使用compile函数。你可能会问威慑呢么要将两个函数分别使用。为什么我们不能只编写一个函数?为了回答这个问题我们需要理解Angular是如何编译指令的!

指令是如何被编译的

当应用在启动时,Angular开始使用$compile服务解析DOM。这项服务会在标记中寻找指令然后将它们各自匹配到注册的适龄。一旦所有的指令都已经被识别完成,Angular就开始执行它们的compile函数。正如前面所提到的,compile函数返回一个link函数,该函数会被添加到稍后执行的link函数队列中。这叫做编译阶段(compile phase)。注意到即使同一个指令有几个实例存在,compile函数也只会运行一次。

在编译阶段之后就到了链接阶段(link phase),这时link函数就一个接一个的执行。在这个阶段中模板被生成,指令被运用到正确的作用域,DOM元素上开始有了事件监听器。不像是compile函数,lin函数会对每个指令的实例都执行一次。

改变指令的作用域

默认情况下指令应该访问父作用域。但是我们并不像对所有情况一概而论。如果我们对指令暴露了父控制器的scope,那么指令就可以自由的修改scope属性。在一些情况下你的指令可能想要添加一些只有内部可以使用的属性和函数。如果我们都在父作用域中完成,可能会污染了父作用域。因此,我们有两种选择:

  • 一个子作用域 – 这个作用域会原型继承父作用域。
  • 一个隔离的作用域 – 一个全新的、不继承、独立存在的作用域。

作用域可以由指令定义对象中的scope属性定义。下面的例子展示了这一点:

app.directive('helloWorld',function(){
    return {
        scope: true, //使用一个继承父作用域的自作用域   
        restrict: 'AE',
        replace: true,
        template: '<h3>Hello World!</h3>'
    }
});   

上面的代码要求Angular为指令提供一个能够原型继承父作用域的子组用于。另一种情形,一个隔离作用域,代码如下所示:

app.directive('helloWorld',function(){
    return {
        scope: {}, //使用一个全新的隔离作用域   
        restrict: 'AE',
        replace: true,
        template: '<h3>Hello World!</h3>'
    }
});

上面的指令使用一个不继承父作用域的全新隔离作用域。当你想要创建一个可重用的组件时隔离作用域是一个很好的选择。通过隔离作用域我们确保指令是自包含的兵可以轻松地插入到任何HTML app中。这种做法防止了父作用域被污染,由于它不可访问父作用域。在我们修改后的helloWorld指令中如果你将scope设置为{},那么代码就不会再正常运行。它将创建一个隔离的作用域然后表达式{{color}}将无法引用隔离作用域中的属性因此值变为undefined。

隔离作用域并不意味着你一点都不能获取到父作用域中的属性。有一些技巧可以使你访问父作用域中的属性同时监听这些属性的变化。我们将在下一篇文章中提到这种高级技巧。

创建一个简单的帮助框(纯文字,HTML内嵌,带图片的~)

// html 效果
QQ图片20150111224932

<div class="help-tip">
	<p>这里是文字.</p>
</div>


// html

<div class="help-tip">
 <p>文字<br><br>
 <img src="balloon.jpg" width="300" />
 <br><a href="www.upour.com">链接</a>
</p>
</div>

// css
.help-tip{
	position: absolute;
	top: 18px;
	right: 18px;
	text-align: center;
	background-color: #BCDBEA;
	border-radius: 50%;
	width: 24px;
	height: 24px;
	font-size: 14px;
	line-height: 26px;
	cursor: default;
}

.help-tip:before{
	content:'?';
	font-weight: bold;
	color:#fff;
}

.help-tip:hover p{
	display:block;
	transform-origin: 100% 0%;

	-webkit-animation: fadeIn 0.3s ease-in-out;
	animation: fadeIn 0.3s ease-in-out;

}

.help-tip p{	/* The tooltip */
	display: none;
	text-align: left;
	background-color: #1E2021;
	padding: 20px;
	width: 300px;
	position: absolute;
	border-radius: 3px;
	box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
	right: -4px;
	color: #FFF;
	font-size: 13px;
	line-height: 1.4;
}

.help-tip p:before{ 
	position: absolute;
	content: '';
	width:0;
	height: 0;
	border:6px solid transparent;
	border-bottom-color:#1E2021;
	right:10px;
	top:-12px;
}

.help-tip p:after{ 
	width:100%;
	height:40px;
	content:'';
	position: absolute;
	top:-40px;
	left:0;
}

/* CSS animation */

@-webkit-keyframes fadeIn {
	0% { 
		opacity:0; 
		transform: scale(0.6);
	}

	100% {
		opacity:100%;
		transform: scale(1);
	}
}

@keyframes fadeIn {
	0% { opacity:0; }
	100% { opacity:100%; }
}

Handlebars.js初体验

Handlebars.js 是一个 Javascript 客户端的模板引擎,它通过对 view 和 data 的分离来快速构建 Web 模板。
它是在加载时被预编译,而不是到了客户端执行到代码时再去编译,可以保证模板加载和运行的速度;
它是一个 Javascript 库,就像你在页面中包含其他.js文件一样。有了它,你可以在HTML页面内添加模板。

其他:
一个弱逻辑模板引擎;Mustache.js 的超集;在服务端预编译模板;支持 Helper 方法;在模板中支持 this 的概念。

目录

  1. 安装和使用
  2. 表达式
    Block 表达式:可以改变js的上下文
    Handlebars 的内置块表达式(Block helper):对数据的逻辑操作
    (1)each block helper
    (2)if else block helper
    (3)unless block helper
    (4)with block helper:可将上下文转移到数据的一个 section 里面(编译时候进行)
    自定义helper:在 js 中用 Handlebars.registerHelper 注册 helpers
  3. 其他
    (1)Handlebar 的注释(comments)
    (2)Handlebars 的访问(Path)
    (3)调试技巧
    (4)handlebars 的 jquery 插件
    (5)partials
    当你想要复用模板的一部分,或者将长模板分割成为多个模板方便维护时,partials就派上用场了。

安装和使用

Github 上下载最新版本。它是一个纯JS库,你可以像使用其他JS脚本一样用script标签来包含handlebars.js。

如何使用?见【3】
1.在 HTML 页面上,通过使用 Handlebars 表达式设定模板,将模板添加到 script 标签中
单独建立一个模板,注意type和id(目前Handlebars仅支持id操作),要用它们来获取模板内容。
2.在 js 中,初始化数据对象
3.在 js 中,使用 Handlebars 编译函数来编译模板

Handlebar的表达式

Handlebars expressions 是 handlebars 模板中最基本的单元,使用方法是用双大括号。
表达式也支持点操作符 content.title,该形式可以调用嵌套的值或者方法。
handlebars 会根据上下文来自动对表达式进行匹配,对象甚至是函数:
1.如果匹配项是个变量,则会输出变量的值
2.如果匹配项是个函数,则函数会被调用
3.如果没找到匹配项,则没有输出

块表达式(Block Expressions)

如果当前的表达式是一个数组,则 Handlebars 会“自动展开数组”,并将 Blocks 的上下文设为数组中的元素。

内置块表达式(Block helper)

用 Helpers,用户可以操作 handlebars 模板中的数据,添加相应的逻辑等等。

each block helper

内置的 each helper遍 历列表块内容,用 this 来引用遍历的元素,this 指的是数组里的每一项元素。

这和上面的Block很像,但是原理不一样。name是数组,而内置的each就是为了遍历数组用的,更复杂的数据也同样使用。 【?】

if else block helper

if 就像你使用 JavaScript 一样,指定条件渲染 DOM。
如果它的参数返回false,undefined, null, “” 或者 [] (a “false” value), Handlebar 将不会渲染 DOM。

unless block helper

反向的if

with block helper

一般情况下,Handlebars 模板会在编译的阶段的时候进行 context 传递和赋值。
使用 with 的方法,我们可以将 context 转移到数据的一个 section 里面(如果你的数据包含 section)。
这个方法在操作复杂的 template 时候非常有用。

自定义helper

其他

Handlebars的访问(Path)

Handlebar 支持路径和 mustache,Handlebar 还支持嵌套的路径,使得能够查找嵌套低于当前上下文的属性
可以通过.来访问属性,也可以使用../,来访问父级属性。

调试技巧

加载一段”debug helper”,就可以在模板文件里通过 debug 或是 debug someValue 方便数据了。