深入了解 Babel

深入了解 Babel

本文所研究的是 babel 6 版本。
babel 62015年10月30号 发布,主要做了以下更新:

  • 拆分成几个核心包,babel-core,babel-node,babel-cli…
  • 没有了默认的转换,现在你需要手动的添加 plugin。也就是插件化
  • 添加了 preset,也就是预置条件。
  • 增加了 .babelrc 文件,方便自定义的配置。

babel 中有很多包,必须弄明白这些是干嘛的,才能让我们更好的使用这个工具。

  • babel-core
  • babel-cli
  • babel-external-helpers
  • babel-node
  • babel-register
  • babel-runtime
  • babel-polyfill

babel-core

babel 的核心包,包括核心 api,比如 transform,主要是处理转码的。 它会把我们的 js 代码,抽象成 ast,即 abstract syntax tree 的缩写,是源代码的抽象语法结构的树状表现形式。

主要 API

1
2
var babel = require('babel-core');
var transform = babel.transform;
1
2
// babel.transform(code: string, options?: Object)
transform("code", options) // => { code, map, ast }
1
2
3
4
5
6
7
8
9
10
11
// babel.transformFile(filename: string, options?: Object, callback: Function)
var path = require('path');
var result = babel.transformFileSync(
path.resolve(__dirname, './test.js'),
{
presets: ['env'],
plugins: ['transform-runtime'],
},
function(err, result) {// { code, map, ast }
console.log(result);
});
1
2
3
4
5
6
7
8
// babel.transformFileSync(filename: string, options?: Object)
var result = babel.transformFileSync(
path.resolve(__dirname, './test.js'),
{
presets: ['env'],
plugins: ['transform-runtime'],
}
);
1
2
// babel.transformFromAst(ast: Object, code?: string, options?: Object)
// 把 ast 传入,解析为 code 代码

babel-cli

提供命令行运行 babel

babel-external-helpers

babel-cli 中的一个 command,用来生成 helper 函数。

babel 有很多辅助函数,例如 toArray 函数, jsx 转化函数。这些函数是 babel transform 的时候用的,都放在 babel-helpers 这个包中。如果 babel 编译的时候检测到某个文件需要这些 helpers,在编译成模块的时候,会放到模块的顶部。

但是如果多个文件都需要提供,会重复引用这些 helpers,会导致每一个模块都定义一份,代码冗余。所以 babel 提供了这个命令,用于生成一个包含了所有 helpers 的 js 文件,用于直接引用。然后再通过一个 plugin,去检测全局下是否存在这个模块,存在就不需要重新定义了。

使用:

  1. 执行 babel-external-helpers 生成 helpers.js 文件

    1
    node_modules/.bin/babel-external-helpers > helpers.js
  2. 安装 plugin

    1
    npm install --save-dev babel-plugin-external-helpers
  3. 然后在 babel 的配置文件加入

    1
    2
    3
    {
    "plugins": ["external-helpers"]
    }
  4. 入口文件引入 helpers.js

    1
    require('./helpers.js');

如果使用了 transform-runtime,就不需要生成 helpers.js 文件了,这个在后面的 babel-runtime 再说。

babel-node

也是 babel-cli 下面的一个 command,主要是实现了 node 执行脚本和命令行写代码的能力。

babel-register

通过改写 node 本身的 require,添加钩子,然后在 require 其他模块的时候,就会触发 babel 编译。也就是你引入 require('babel-register') 的文件代码,是不会被编译的。只有通过 require 引入的其他代码才会。

1
npm install babel-register --save-dev
1
2
require('babel-register')({ presets: ['react'] });
require('./test')

它的特点就是实时编译,不需要输出文件,执行的时候再去编译。所以它很适用于开发。总结一下就是,多用在 node 跑程序,做实时编译用的,通常会结合其他插件作编译器使用,比如 mocha 做测试的时候。

babel-runtime

1
npm install babel-runtime --save

Babel 转译后的代码要实现源代码同样的功能需要借助一些帮助函数。可能会重复出现在一些模块里,导致编译后的代码体积变大。Babel 为了解决这个问题,提供了单独的包 babel-runtime 供编译模块复用工具函数。(core-jsregenerator

启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数,转译代码如下:

1
2
3
4
5
6
'use strict';
// 之前的 _defineProperty 函数已经作为公共模块 `babel-runtime/helpers/defineProperty` 使用
var _defineProperty2 = require('babel-runtime/helpers/defineProperty');
var _defineProperty3 = _interopRequireDefault(_defineProperty2);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = (0, _defineProperty3.default)({}, 'name', 'JavaScript');

除此之外,babel 还为源代码的非实例方法(Object.assign,实例方法是类似这样的 “foobar”.includes(“foo”))和 babel-runtime/helps 下的工具函数自动引用了 polyfill。这样可以避免污染全局命名空间,非常适合于 JavaScript 库和工具包的实现。例如 const obj = {}, Object.assign(obj, { age: 30 }); 转译后的代码如下所示:

1
2
3
4
5
6
7
8
9
'use strict';
// 使用了 core-js 提供的 assign
var _assign = require('babel-runtime/core-js/object/assign');
var _assign2 = _interopRequireDefault(_assign);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = {};
(0, _assign2.default)(obj, {
age: 30
});

babel-runtime 适合 JavaScript 库和工具包实现

  • 避免 babel 编译的工具函数在每个模块里重复出现,减小库和工具包的体积;
  • 在没有使用 babel-runtime 之前,库和工具包一般不会直接引入 polyfill。否则像 Promise 这样的全局对象会污染全局命名空间,这就要求库的使用者自己提供 polyfill。这些 polyfill 一般在库和工具的使用说明中会提到,比如很多库都会有要求提供 es5 的 polyfill。在使用 babel-runtime 后,库和工具只要在 package.json 中增加依赖 babel-runtime,交给 babel-runtime 去引入 polyfill 就行了;

core-js

core-js 是用于 JavaScript 的组合式标准化库,它包含 ES5 (e.g: object.freeze), ES6PromiseSymbols, Collections, Iterators, Typed arrayses7+提案等等的 polyfills 实现。

regenerator

它是来自于 facebook 的一个库,链接。主要就是实现了 generator/yeild, async/await。

所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。

babel-polyfill

Babel 默认只转换新的 JavaScript 语法,而不转换新的 API。例如,IteratorGeneratorSetMapsProxyReflectSymbolPromise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。

不同于 babel-runtime 的是,babel-polyfill 是一次性引入你的项目中的,就像是 React 包一样,同项目代码一起编译到生产环境。

注意: babel

transform-runtime 和 babel-polyfile 对比

  • babel-polyfill 是当前环境注入这些 es6+ 标准的垫片,好处是引用一次,不再担心兼容,而且它就是全局下的包,代码的任何地方都可以使用。缺点也很明显,它可能会污染原生的一些方法而把原生的方法重写。如果当前项目已经有一个 polyfill 的包了,那你只能保留其一。而且一次性引入这么一个包,会大大增加体积。如果你只是用几个特性,就没必要了,如果你是开发较大的应用,而且会频繁使用新特性并考虑兼容,那就直接引入吧。

  • transform-runtime 是利用 plugin 自动识别并替换代码中的新特性,你不需要再引入,只需要装好 babel-runtime 和 配好 plugin 就可以了。好处是按需替换,检测到你需要哪个,就引入哪个 polyfill,如果只用了一部分,打包完的文件体积对比 babel-polyfill 会小很多。而且 transform-runtime 不会污染原生的对象,方法,也不会对其他 polyfill 产生影响。所以 transform-runtime 的方式更适合开发工具包,库,一方面是体积够小,另一方面是用户(开发者)不会因为引用了我们的工具,包而污染了全局的原生方法,产生副作用,还是应该留给用户自己去选择。缺点是随着应用的增大,相同的 polyfill 每个模块都要做重复的工作(检测,替换),虽然 polyfill 只是引用,编译效率不够高效。

plugin

babel-plugin-transform-runtime

transform-runtime 是为了方便使用 babel-runtime 的,它会分析我们的 ast 中,是否有引用 babel-rumtime 中的垫片(通过映射关系),如果有,就会在当前模块顶部插入我们需要的垫片。

另外,它还有几个配置

1
2
3
4
5
6
7
8
9
10
11
// 默认值
{
"plugins": [
["transform-runtime", {
"helpers": true,
"polyfill": true,
"regenerator": true,
"moduleName": "babel-runtime"
}]
]
}

如果你只需要用 regenerator,不需要 core-js 里面的 polyfill 那你就可以在 options 中把 polyfill 设为 falsehelpers 设为 false,就相当于没有启用 babel-plugin-external-helpers 的效果,比如翻译 async 的时候,用到了 asyncToGenerator 函数,每个文件还会重新定义一下。moduleName 的话,就是用到的库,你可以把 babel-runtime 换成其他类似的。

presets

presets 就是 plugins 的组合,你也可以理解为是套餐

babel-preset-lastet(包括 es2105,es2016,es2017)跟默认情况下的 env 是一样的,也就是说包括 lastest 在内,这四个 presets 都要被 babel-preset-env 代替。即:

babel-preset-env

它能根据当前的运行环境,自动确定你需要的 pluginspolyfills。通过各个 es 标准 feature 在不同浏览器以及 node 版本的支持情况,再去维护一个 featureplugins 之间的映射关系,最终确定需要的 plugins

注意: babel-preset-env 并不是包括所有的 babel-preset-esbabel-preset-stag,而是所有的 babel-preset-esbabel-preset-stag-4
详情请看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//.babelrc

{
"presets": [
[
"env",
{
"targets": { // 配支持的环境
"browsers": [ // 浏览器
"last 2 versions",
"safari >= 7"
],
"node": "current"
},
"modules": true, //设置ES6 模块转译的模块格式 默认是 commonjs
"debug": true, // debug,编译的时候 console
"useBuiltIns": false, // 是否开启自动支持 polyfill
"include": [], // 总是启用哪些 plugins
"exclude": [] // 强制不启用哪些 plugins,用来防止某些插件被启用
}
]
],
plugins: [
"transform-react-jsx" //如果是需要支持 jsx 这个东西要单独装一下。
]
}

useBuiltIns

env 会自动根据我们的运行环境,去判断需要什么样的 polyfill,而且,打包后的代码体积也会大大减小,但是这一切都在使用 useBuiltIns,而且需要你安装 babel-polyfill,并 import。它会启用一个插件,替换你的import 'babel-polyfill',不是整个引入了,而是根据你配置的环境和个人需要单独的引入 polyfill

总结

  • 具体项目还是需要使用 babel-polyfill 配合 useBuiltIns,只使用 babel-runtime 的话,实例方法不能正常工作(例如 "foobar".includes("foo"));
  • JavaScript 库和工具可以使用 babel-runtime 配合 babel-plugin-transform-runtime,在实际项目中使用这些库和工具,需要该项目本身提供 polyfill

参考文献

评论和共享

Koa2 基本使用

Koa2 基本使用

hello world

1
2
3
4
const Koa = require('koa');
const app = new Koa();

app.listen(3000);

Context 对象

1
2
3
4
5
6
7
const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
ctx.response.body = 'Hello World';
});
app.listen(3000);

其中 ctx 就是 Context 对象

request

最基本的一个 request 对象如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"method": "GET",
"url": "/index.json",
"header": {
"host": "localhost:3000",
"connection": "keep-alive",
"cache-control": "max-age=0",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"accept-encoding": "gzip, deflate, br",
"accept-language": "zh-CN,zh;q=0.8"
}
}

request.accepts(‘xml’)

这个函数用来判断客户端想要接受的数据类型,例如

1
2
3
4
5
6
7
8
9
const main = ctx => {
if (ctx.request.accepts('json')) {
ctx.response.type = 'json';
ctx.response.body = { data: 'Hello World' };
} else (ctx.request.accepts('html')) {
ctx.response.type = 'html';
ctx.response.body = '<p>Hello World</p>';
}
};

response

ctx.response.body

直接给 response.body 赋值即可返回给数据给客户端

1
2
3
4
const main = ctx => {
ctx.response.type = 'json';
ctx.response.body = JSON.stringify(ctx.request)
};

网页模版

先读取 html 页面,然后返回给客户端

1
2
3
4
5
6
const fs = require('fs');

const main = ctx => {
ctx.response.type = 'html';
ctx.response.body = fs.createReadStream('./demos/template.html');
};

路由

原生路由

可以通过 ctx.request.path 获取到用户的请求

koa-route

1
2
3
4
5
6
7
8
9
10
11
12
13
const route = require('koa-route');

const about = ctx => {
ctx.response.type = 'html';
ctx.response.body = '<a href="/">Index Page</a>';
};

const main = ctx => {
ctx.response.body = 'Hello World';
};

app.use(route.get('/', main));
app.use(route.get('/about', about));

静态资源

如果网站提供静态资源(图片、字体、样式表、脚本……),为它们一个个写路由就很麻烦,也没必要。koa-static 模块封装了这部分的请求

1
2
3
4
5
const path = require('path');
const serve = require('koa-static');

const main = serve(path.join(__dirname));
app.use(main);

重定向

ctx.response.redirect('/');

中间件

1
2
3
4
5
const logger = (ctx, next) => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
next();
}
app.use(logger);

像上面代码中的 logger 函数就叫做”中间件” (middleware),因为它处在 HTTP RequestHTTP Response 中间,用来实现某种中间功能。app.use() 用来加载中间件。

基本上,Koa 所有的功能都是通过中间件实现的,前面例子里面的 main 也是中间件。每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是 next 函数。只要调用 next 函数,就可以把执行权转交给下一个中间件。

中间件栈

多个中间件会形成一个栈结构(middle stack),以”先进后出”(first-in-last-out)的顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const one = (ctx, next) => {
console.log('>> one');
next();
console.log('<< one');
}

const two = (ctx, next) => {
console.log('>> two');
next();
console.log('<< two');
}

const three = (ctx, next) => {
console.log('>> three');
next();
console.log('<< three');
}

app.use(one);
app.use(two);
app.use(three);

运行结果:

1
2
3
4
5
6
>> one
>> two
>> three
<< three
<< two
<< one

如果中间件内部没有调用 next 函数,那么执行权就不会传递下去
去掉上面 two 函数中的 next() ,结果如下:

1
2
3
4
>> one
>> two
<< two
<< one

异步中间件

如果有异步操作(比如读取数据库),中间件就必须写成 async 函数

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();

const main = async (ctx, next)=> {
ctx.response.type = 'html';
ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
};

app.use(main);
app.listen(3000);

上面代码中,fs.readFile 是一个异步操作,必须写成 await fs.readFile() ,然后中间件必须写成 async 函数。

中间件的合成

koa-compose 模块可以将多个中间件合成为一个。请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const compose = require('koa-compose');

const one = (ctx, next) => {
console.log('>> one');
next();
console.log('<< one');
}

const two = (ctx, next) => {
console.log('>> two');
next();
console.log('<< two');
}

const three = (ctx, next) => {
console.log('>> three');
next();
console.log('<< three');
}

const middlewares = compose([one, two, three]);
app.use(middlewares);

错误处理

500 错误

Koa 提供了 ctx.throw() 方法,用来抛出错误,ctx.throw(500)就是抛出500错误

1
2
3
const main = ctx => {
ctx.throw(500);
};

处理错误中间件

为了方便处理错误,最好使用 try...catch 将其捕获。但是,为每个中间件都写 try...catch 太麻烦,我们可以让最外层的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const handler = async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.response.status = err.statusCode || err.status || 500;
ctx.response.body = {
message: err.message
};
}
};

const main = ctx => {
ctx.throw(500);
};

app.use(handler);
app.use(main);

error 事件的监听

运行过程中一旦出错,Koa 会触发一个 error 事件。监听这个事件,也可以处理错误

1
2
3
4
5
6
7
const main = ctx => {
ctx.throw(500);
};

app.on('error', (err, ctx) =>
console.error('server error', err);
);

释放 error 事件

如果错误被 try...catch 捕获,就不会触发 error 事件。这时,必须调用 ctx.app.emit(),手动释放error事件,才能让监听函数生效。

Cookies

ctx.cookies 用来读写 Cookie

1
2
3
4
5
const main = function(ctx) {
const n = Number(ctx.cookies.get('view') || 0) + 1;
ctx.cookies.set('view', n);
ctx.response.body = n + ' views';
}

表单

koa-body 模块可以用来从 POST 请求的数据体里面提取键值对

1
2
3
4
5
6
7
8
const koaBody = require('koa-body');

const main = async function(ctx) {
const { name='' } = ctx.request.body;
ctx.body = { name };
};

app.use(koaBody());

get 请求

1
2
3
4
const main = async function(ctx) {
const { name='' } = ctx.query;
ctx.body = { name };
};

get 请求不需要 koa-body 依赖,post 则需要

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const os = require('os');
const path = require('path');
const koaBody = require('koa-body');

const main = async function(ctx) {
const tmpdir = os.tmpdir();
const filePaths = [];
const files = ctx.request.body.files || {};

for (let key in files) {
const file = files[key];
const filePath = path.join(tmpdir, file.name);
const reader = fs.createReadStream(file.path);
const writer = fs.createWriteStream(filePath);
reader.pipe(writer);
filePaths.push(filePath);
}

ctx.body = filePaths;
};

app.use(koaBody({ multipart: true }));

评论和共享

this.props.children

this.props.children

this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const NotesList = React.createClass({
render: function() {
return (
<ol>
{
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
}
</ol>
);
}
});

ReactDOM.render(
<NotesList>
<span>hello</span>
<span>world</span>
</NotesList>,
document.body
);

这里需要注意, this.props.children 的值有三种可能:如果当前组件没有子节点,它就是 undefined ;如果有一个子节点,数据类型是 object ;如果有多个子节点,数据类型就是 array 。所以,处理 this.props.children 的时候要小心。

React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object。更多的 React.Children 的方法,请参考官方文档。

评论和共享

webpack 踩坑

不同平台设置node环境变量的差异

在Mac上,可以在package.json中配置:

1
2
3
"scripts": {
"build":"NODE_ENV=production && ..."
}

但在Windows中是不行的。要解决这个问题,需要用到cross-env模块。

先安装它:

1
npm i cross-env --save-dev

然后把上面的配置可以改成:

1
2
3
"scripts": {
"build":"node ./node_modules/.bin/cross-env NODE_ENV=production && ..."
}

处理未模块化的库,如 Zepto

对于未模块化的库,如果直接import,在webpack打包的时候会报错的。详见:如何在 webpack 中引入未模块化的库,如 Zepto

解决的办法就是在module的rules下增加如下配置项:

1
2
3
4
{
test: require.resolve('zepto'),
loader: 'exports-loader?window.Zepto!script-loader'
}

其中,require.resolve()nodejs 用来查找模块位置的方法,返回模块的入口文件,如 zepto 为 ./node_modules/zepto/dist/zepto.js

此外,这里还用到了两个loader,我们需要先安装他们:

1
$ npm i --save-dev script-loader exports-loader

评论和共享

过程

  1. webpack.config.js 改成 webpack.config.babel.js
  2. 在根目录添加 .babelrc
  3. 安装 babel-core babel-loader babel-preset-es2015

webpack.config.babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import path from 'path'

export default {
entry: [
path.resolve(__dirname, './src/index.js')
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader'
}
],
exclude: [
resolve(__dirname, './node_modules/')
]
}
]
}
}

注意: 这里须要将 exclude: [resolve(__dirname, './node_modules/')] 加入,否则打包后的代码会包括 node_modules 下的文件,而且不能写成 '/node_modules/'

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name": "webpacklearning",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack --config webpack.config.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"babel-runtime": "^6.26.0"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"webpack": "^3.5.6"
}
}

.babelrc

这里注意一点是 不能使用 modules: false modules: false配置项是告诉es2015 preset避免把import statements编译成CommonJS,这样Webpack才好做tree shaking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"presets": [
[
"env",
{
"targets": {
"browsers": [
"last 2 versions",
"ie >= 9"
],
"node": "current"
}
}
],
"react",
"stage-3"
],
"plugins": [
"react-hot-loader/babel",
"transform-runtime"
]
}

评论和共享

组成

  • 先写一个 JavaScript 构造函数
  • 在构造函数的原型上添加 apply 方法,传入 compiler
  • 在传入的 compiler 上挂载钩子事件。
  • 钩子函数中传入 compilation 和一个回调函数。
  • 功能完成后调用 webpack 提供的回调函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 命名函数
function MyExampleWebpackPlugin() {

};

// 在它的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定挂载的webpack事件钩子。
compiler.plugin('webpacksEventHook', function(compilation /* 处理webpack内部实例的特定数据。*/, callback) {
console.log("This is an example plugin!!!");

// 功能完成后调用webpack提供的回调。
callback();
});
};

compiler

compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可以使用它来访问 webpack 的主环境。

compilation

Compilation 实例继承于 compiler, compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
"errors":[], //错误字符串的数组
"warnings":[], //警告字符串的数组
"version":"1.3.7", // 用来编译的 webpack 的版本
"hash":"b4806b1be53e47fbab31", // 编译使用的 hash
"time":3080, // 编译耗时 (ms)
"assetsByChunkName":{
// 用作映射的 chunk 的名称
"main":"b4806b1be53e47fbab31.js"
},
"assets":[
// asset 对象 (asset objects) 的数组
{
"name":"59e68da5e8cbc0ba28bd706801d425ba.jpg",
"size":672,
"chunks":[],
"chunkNames":[],
"emitted":true
},
{
"name":"b4806b1be53e47fbab31.js",
"size":571644,
"chunks":[0],
"chunkNames":["main"],
"emitted":true
}
],
"chunks":[
// chunk 对象 (chunk objects) 的数组
{
"id":0,
"rendered":true,
"initial":true,
"entry":true,
"size":539740,
"names":["main"],
"files":["b4806b1be53e47fbab31.js"],
"parents":[],
"origins":[
{
"moduleId":0,
"module":"...",
"moduleIdentifier":"...",
"moduleName":"./app/entry.js",
"loc":"",
"name":"main",
"reasons":[

]
}
]
}
],
"modules":[
// 模块对象 (module objects) 的数组
],
"children":[]
}

Asset对象

每一个 assets 对象都表示一个编译出的 output 文件。 assets 都会有一个共同的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"entry": true, // 表示这个 chunk 是否包含 webpack 的运行时
"files": [
// 一个包含这个 chunk 的文件名的数组
],
"filteredModules": 0, // 当 `exclude`传入`toJson` 函数时,统计被无视的模块的数量
"id": 0, // 这个 chunk 的id
"initial": true, // 表示这个 chunk 是开始就要加载还是 懒加载(lazy-loading)
"modules": [
// 模块对象 (module objects)的数组
"web.js?h=11593e3b3ac85436984a"
],
"names": [
// 包含在这个 chunk 内的 chunk 的名字的数组
],
"origins": [
// 下文详述
],
"parents": [], // 父 chunk 的 ids
"rendered": true, // 表示这个 chunk 是否会参与进编译
"size": 188057 // chunk 的大小(byte)
}

Chunk对象

每一个 chunks 表示一组称为 chunk 的模块。每一个对象都满足以下的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"entry": true, // 表示这个 chunk 是否包含 webpack 的运行时
"files": [
// 一个包含这个 chunk 的文件名的数组
],
"filteredModules": 0, // 见上文的 结构
"id": 0, // 这个 chunk 的id
"initial": true, // 表示这个 chunk 是开始就要加载还是 懒加载(lazy-loading)
"modules": [
// 模块对象 (module objects)的数组
"web.js?h=11593e3b3ac85436984a"
],
"names": [
// 包含在这个 chunk 内的 chunk 的名字的数组
],
"origins": [
// 下文详述
],
"parents": [], // 父 chunk 的 ids
"rendered": true, // 表示这个 chunk 是否会参与进编译
"size": 188057 // chunk 的大小(byte)
}

模块对象

缺少了对实际参与进编译的模块的描述,这些数据又有什么意义呢。每一个在依赖图表中的模块都可以表示成以下的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"assets": [
// asset对象 (asset objects)的数组
],
"built": true, // 表示这个模块会参与 Loaders , 解析, 并被编译
"cacheable": true, // 表示这个模块是否会被缓存
"chunks": [
// 包含这个模块的 chunks 的 id
],
"errors": 0, // 处理这个模块发现的错误的数量
"failed": false, // 编译是否失败
"id": 0, // 这个模块的ID (类似于 `module.id`)
"identifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", // webpack内部使用的唯一的标识
"name": "./lib/index.web.js", // 实际文件的地址
"optional": false, // 每一个对这个模块的请求都会包裹在 `try... catch` 内 (与ESM无关)
"prefetched": false, // 表示这个模块是否会被 prefetched
"profile": {
// 有关 `--profile` flag 的这个模块特有的编译数据 (ms)
"building": 73, // 载入和解析
"dependencies": 242, // 编译依赖
"factory": 11 // 解决依赖
},
"reasons": [
// 见下文描述
],
"size": 3593, // 预估模块的大小 (byte)
"source": "// Should not break it...\r\nif(typeof...", // 字符串化的输入
"warnings": 0 // 处理模块时警告的数量
}

每一个模块都包含一个 理由 (reasons) 对象,这个对象描述了这个模块被加入依赖图表的理由。每一个 理由 (reasons) 都类似于上文 chunk objects中的 来源 (origins):

1
2
3
4
5
6
7
8
9
{
"loc": "33:24-93", // 导致这个被加入依赖图标的代码行数
"module": "./lib/index.web.js", // 所基于模块的相对地址 context
"moduleId": 0, // 模块的 ID
"moduleIdentifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", // 模块的地址
"moduleName": "./lib/index.web.js", // 可读性更好的模块名称 (用于 "更好的打印 (pretty-printing)")
"type": "require.context", // 使用的请求的种类 (type of request)
"userRequest": "../../cases" // 用来 `import` 或者 `require` 的源字符串
}

错误与警告

错误 (errors) 和 警告 (warnings) 会包含一个字符串数组。每个字符串包含了信息和栈的追溯:

评论和共享

Normalize.css

不同浏览器的默认样式存在差异,可以使用 Normalize.css 抹平这些差异。当然,你也可以定制属于自己业务的 reset.css

  • 直接放在HTML中

    1
    <link href="https://cdn.bootcss.com/normalize/7.0.0/normalize.min.css" rel="stylesheet">
  • 使用 webpack

1
$ npm install --save normalize.css
1
2
3
//webpack.config.polyfills.js

import 'normalize.css'

html5shiv.js

解决 ie9 以下浏览器对 html5 新增标签不识别的问题。

1
2
3
<!--[if lt IE 9]>
<script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
1
$ bower install html5shiv --save-dev
1
2
3
<!--[if lt IE 9]>
<script src="bower_components/html5shiv/dist/html5shiv.js"></script>
<![endif]-->

matchMedia.js

matchMedia() 函数 polyfill

1
$ npm install --save matchmedia-polyfill

1
import 'matchmedia-polyfill'

respond.js

解决 ie9 以下浏览器不支持 CSS3 Media Query 的问题。

1
<script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>

1
$ npm install respond.js
1
import 'respond.js'

picturefill.js

解决 IE 9 10 11 等浏览器不支持 标签的问题

1
<script src="https://cdn.bootcss.com/picturefill/3.0.3/picturefill.min.js"></script>

IE 条件注释

操作符 含义
lt 小于
gt 大于
lte 小于等于
get 不小于
! 不等于

完美解决 Placeholder

1
<input type="text" value="Name *" onFocus="this.value = '';" onBlur="if (this.value == '') {this.value = 'Name *';}">

清除浮动 最佳实践

1
2
3
4
.fl { float: left; }
.fr { float: right; }
.clearfix:after { display: block; clear: both; content: ""; visibility: hidden; height: 0; }
.clearfix { zoom: 1; }

IE6 双倍边距的问题

设置 ie6 中设置浮动,同时又设置 margin,会出现双倍边距的问题

1
2
3
.box {
display: inline;
}

解决 IE9 以下浏览器不能使用 opacity

1
2
3
4
5
.box {
opacity: 0.5;
filter: alpha(opacity = 50);
filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50);
}

解决 IE6 不支持 fixed 绝对定位以及IE6下被绝对定位的元素在滚动的时候会闪动的问题

1
2
3
4
5
6
7
8
9
/* IE6 hack */
*html, *html body {
background-image: url(about:blank);
background-attachment: fixed;
}
*html #menu {
position: absolute;
top: expression(((e=document.documentElement.scrollTop) ? e : document.body.scrollTop) + 100 + 'px');
}

IE6 背景闪烁的问题

问题:链接、按钮用 CSSsprites 作为背景,在 ie6 下会有背景图闪烁的现象。原因是 IE6 没有将背景图缓存,每次触发 hover 的时候都会重新加载

解决:可以用 JavaScript 设置 ie6 缓存这些图片:

1
document.execCommand("BackgroundImageCache", false, true);

解决在 IE6 下,列表与日期错位的问题

日期 标签放在标题 标签之前即可

解决 IE6 不支持 min-height 属性的问题

1
2
3
4
.box {
min-height: 350px;
_height: 350px;
}

让 IE7 IE8 支持 CSS3 background-size属性

由于 background-size 是 CSS3 新增的属性,所以 IE 低版本自然就不支持了,但是老外写了一个 htc 文件,名叫 background-size polyfill,使用该文件能够让 IE7、IE8 支持 background-size 属性。其原理是创建一个 img 元素插入到容器中,并重新计算宽度、高度、left、top 等值,模拟 background-size 的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
background-image: url('img/37.png');
background-repeat: no-repeat;
background-size: cover;
-ms-behavior: url('css/backgroundsize.min.htc');
behavior: url('css/backgroundsize.min.htc');
}

IE6-7 line-height 失效的问题

问题:在ie 中 img 与文字放一起时,line-height 不起作用

解决:都设置成 float

width:100%

width:100% 这个东西在 ie 里用很方便,会向上逐层搜索 width 值,忽视浮动层的影响.

Firefox 下搜索至浮动层结束,如此,只能给中间的所有浮动层加 width:100%才行,累啊。

opera 这点倒学乖了,跟了 ie

cursor:hand

显示手型 cursor: hand,ie6/7/8、opera 都支持,但是safari 、 ff 不支持

cursor: pointer;

td 自动换行的问题

问题:table 宽度固定,td 自动换行

解决:设置 Table 为 table-layout: fixed,td 为 word-wrap: break-word

让层显示在 FLASH 之上

想让层的内容显示在 flash 上,把 FLASH 设置透明即可

1
2
<param name=" wmode " value="transparent" />
<param name="wmode" value="opaque"/>

键盘事件 keyCode 兼容性写法

1
2
3
4
5
6
7
8
9
10
11
var inp = document.getElementById('inp')
var result = document.getElementById('result')

function getKeyCode(e) {
e = e ? e : (window.event ? window.event : "")
return e.keyCode ? e.keyCode : e.which
}

inp.onkeypress = function(e) {
result.innerHTML = getKeyCode(e)
}

求窗口大小的兼容写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)
// 1600 * 525
var client_w = document.documentElement.clientWidth || document.body.clientWidth;
var client_h = document.documentElement.clientHeight || document.body.clientHeight;

// 网页内容实际宽高(包括工具栏和滚动条等边线)
// 1600 * 8
var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;

// 网页内容实际宽高 (不包括工具栏和滚动条等边线)
// 1600 * 8
var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;

// 滚动的高度
var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;

DOM 事件处理程序的兼容写法(能力检测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var eventshiv = {
// event兼容
getEvent: function(event) {
return event ? event : window.event;
},

// type兼容
getType: function(event) {
return event.type;
},

// target兼容
getTarget: function(event) {
return event.target ? event.target : event.srcelem;
},

// 添加事件句柄
addHandler: function(elem, type, listener) {
if (elem.addEventListener) {
elem.addEventListener(type, listener, false);
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, listener);
} else {
// 在这里由于.与'on'字符串不能链接,只能用 []
elem['on' + type] = listener;
}
},

// 移除事件句柄
removeHandler: function(elem, type, listener) {
if (elem.removeEventListener) {
elem.removeEventListener(type, listener, false);
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, listener);
} else {
elem['on' + type] = null;
}
},

// 添加事件代理
addAgent: function (elem, type, agent, listener) {
elem.addEventListener(type, function (e) {
if (e.target.matches(agent)) {
listener.call(e.target, e); // this 指向 e.target
}
});
},

// 取消默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},

// 阻止事件冒泡
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};

评论和共享

WebRTC 原理

WebRTC 原理分析

假设A和B要建立视频通话,A为房间创建端,B为加入房间端:

  1. A通过 http 登录、获取其他服务器地址(做一些保存用户信息的操作,获取信令、stunturn服务器地址等,非必要)
  2. A和信令服务器建立 websocket 长连接
  3. A通过 websocket 向信令服务器注册(创建房间,记录房间号,等待B加入房间)
  4. A创建本地视频,获取A的 sdp 信息
  5. B创建本地视频,获取B的 sdp 信息
  6. B发送本地 sdp 信息到信令服务器 sendOffer
    1. B同时也在向stun(穿越)、turn(延时转发)服务器获取ice信息
    2. B发送ice信息到信令服务器(后续会和A交换3种信息,不再赘述)
  7. 信令服务器转发 sdpice 信息到A(通过房间号辨别)
  8. A将B的sdp信息设置到底层setRemoteDescription
    1. A添加B的ice信息
    2. A同时也在向stun(穿越)、turn(延时转发)服务器获取ice信息
    3. A发送ice信息到信令服务器(后续会和B交换3种信息,不再赘述)
  9. A发送本地sdp信息到信令服务器sendAnswer
  10. 信令服务器转发sdp信息到B
  11. B将A的sdp信息设置到底层setRemoteDescription

在交换sdp信息的同时,ice信息也在进行交换,通过交换ice信息,最终会选择一种合适的方式来建立连接(p2p或者基于turn服务器的延时转发通路)

信令服务器作用

  1. 浏览器之间交换建立通信的元数据(信令)必须通过服务器
  2. 为了穿越NAT和防火墙

名词

STUN

STUN (Simple Traversal of UDP over NATs,NAT 的UDP简单穿越)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一 个本地端口所绑定的Internet端端口。

TURN

TURN 协议允许 NAT 或者防火墙后面的对象可以通过 TCP 或者 UDP 接收到数据。

SDP

SDP 完全是一种会话描述格式 ― 它不属于传输协议 ― 它只使用不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP)。SDP协议是也是基于文本的协议,这样就能保证协议的可扩展性比较强,这样就使其具有广泛的应用范围。SDP 不支持会话内容或媒体编码的协商,所以在流媒体中只用来描述媒体信息。媒体协商这一块要用RTSP来实现.

ICE

ICE (互动式连接建立) 由IETF的MMUSIC工作组开发出来的,它所提供的是一种框架,使各种NAT穿透技术可以实现统一。该技术可以让基于SIP的VoIP客户端成功地穿透远程用户与网络之间可能存在的各类防火墙。

评论和共享

Karma 基本用法

安装

1
2
$ npm install karma --save-dev
$ npm install karma-jasmine karma-chrome-launcher jasmine-core --save-dev

生成配置文件

1
$ node ./node_modules/karma/bin/karma init karma.conf.js

运行

1
$ node ./node_modules/karma/bin/karma start karma.conf.js

评论和共享

1
2
3
4
5
6
7
8
<div class="box">

<div id="oDiv">
<a href="javascript:;">点我</a>
<span id="hh">awd</span>
</div>

</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ============ 简单的事件委托
function delegateEvent(interfaceEle, selector, type, fn) {
if(interfaceEle.addEventListener){
interfaceEle.addEventListener(type, eventfn);
}else{
interfaceEle.attachEvent("on"+type, eventfn);
}

function eventfn(e){
var e = e || window.event;
var target = e.target || e.srcElement;
if (matchSelector(target, selector)) {
if(fn) {
fn.call(target, e);
}
}
}
}
/**
* only support #id, tagName, .className
* and it's simple single, no combination
*/
function matchSelector(ele, selector) {
// if use id
if (selector.charAt(0) === '#') {
return ele.id === selector.slice(1);
}
// if use class
if (selector.charAt(0) === '.') {
return (' ' + ele.className + ' ').indexOf(' ' + selector.slice(1) + ' ') != -1;
}
// if use tagName
return ele.tagName.toLowerCase() === selector.toLowerCase();
}

//调用
var odiv = document.getElementById('oDiv');
delegateEvent(odiv,'a','click',function(){
alert('1');
})
delegateEvent(odiv,'#hh','click',function(){
alert('2');
})

评论和共享

作者的图片

Archie Shi

Nothing to say


Front-End Development Engineer