- 🏡
- 🌱
- 😺
- 💬
- 🤔
- 👬
liujie2019 / blog Goto Github PK
View Code? Open in Web Editor NEW💪Star是最大鼓励--平时学习中总结和demo,请多多指教。最新博客地址:
Home Page: https://liujie2019.github.io/
💪Star是最大鼓励--平时学习中总结和demo,请多多指教。最新博客地址:
Home Page: https://liujie2019.github.io/
具体的源码分析文件请戳:源码详见
Redux
的核心**是:将应用的状态state
存储在唯一的store
中。通过store.dispatch
一个action
来描述触发了什么动作,用reducer
处理相应的action
并返回新的state
。需要注意的是:创建store
的时候需要传入reducer
,真正可以改变应用状态state
的是store.dispatch
API。
middleware
串联起来生成一个更强大的dispatch
函数,这就是中间件的本质作用;action creators
转成拥有同名keys
的对象,使用dispatch
把每个action creator
包围起来,使用时可以直接调用;reducer
组合起来,每一个reducer
独立管理自己对应的state
;middleware
从右向左依次调用,函数式编程中的常用方法,被applyMiddleware
调用;store
,包括实现了subscribe,unsubscribe,dispatch及state
的储存;推荐源码的阅读顺序为:
index.js -> creatStore.js -> applyMiddleware.js (compose.js) -> combineReducers.js -> bindActionCreators.js
redux
是一个状态管理框架,是在Flux
的基础上产生的,基本**是:保证数据的单向流动,同时便于控制、使用、测试。
webpack 是一个强大的模块打包工具,之所以强大的一个原因在于它拥有灵活、丰富的插件机制。webpack 本质上是一个打包工具,它会根据代码的内容解析模块依赖,帮助我们把多个模块的代码打包。webpack 会把我们项目中使用到的多个代码模块(可以是不同文件类型),打包构建成项目运行仅需要的几个静态文件。
特别注意:webpack 4 不是必须要有配置文件。它将查找./src/index.js作为默认入口点。 而且,它会在./dist/main.js中输出模块包。
webpack启动后会在entry里配置的module开始递归解析entry所依赖的所有module,每找到一个module, 就会根据配置的loader去找相应的转换规则,对module进行转换后,再解析当前module所依赖的module,这些模块会以entry为分组,一个entry和所有相依赖的module也就是一个chunk,最后webpack会把所有chunk转换成文件输出,在整个流程中webpack会在恰当的时机执行plugin的逻辑。
# npm全局安装
npm install webpack webpack-cli -g
# yarn安装
yarn global add webpack webpack-cli
# 全局执行webpack命令
webpack --help
#在项目目录中安装
npm install webpack webpack-cli -D
特别注意: webpack-cli
是使用 webpack
的命令行工具,在 webpack4.x
版本之后不再作为 webpack
的依赖了,我们使用时需要单独安装这个工具。
引入了 mode 配置项,开发者可在 none,development(开发 ) 以及 production(产品)三种模式间选择。该配置项缺省情况下默认使用 production 模式。
webpack4有两种模式:development和production,默认为production。
#生产环境
webpack --mode production
#开发环境
webpack --mode development
在package.json
文件中的scripts
字段中进行如下配置:
"scripts": {
"build": "webpack --mode production --config webpack.production.config.js",
"dev": "webpack-dev-server --mode development --open"
}
webpack 在构建时需要有入口文件。webpack 会读取这个文件,并从它开始解析依赖,然后进行打包。webpack4
默认从项目根目录下的 ./src/index.js
中加载入口模块。默认的入口文件就是 ./src/index.js
。
我们常见的项目中,如果是单页面应用,那么可能入口只有一个;如果是多个页面的项目,那么经常是一个页面会对应一个构建入口。
入口可以使用 entry
字段来进行配置,webpack
支持配置多个入口来进行构建:
module.exports = {
entry: './src/index.js'
}
// 上述配置等同于
module.exports = {
entry: {
main: './src/index.js'
}
}
// 或者配置多个入口
module.exports = {
entry: {
foo: './src/page-foo.js',
bar: './src/page-bar.js',
// ...
}
}
// 使用数组来对多个文件进行打包
// 可以理解为多个文件作为一个入口,webpack 会解析两个文件的依赖后进行打包
module.exports = {
entry: {
main: [
'./src/foo.js',
'./src/bar.js'
]
}
}
webpack 的输出即指 webpack 最终构建出来的静态文件,可以看看上面 webpack 官方图片右侧的那些文件。当然,构建结果的文件名、路径等都是可以配置的,使用 output
字段:
module.exports = {
// ...
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: 'http://cdn.eaxmple.com/assets/'
},
}
// 或者多个入口生成不同文件
module.exports = {
entry: {
foo: './src/foo.js',
bar: './src/bar.js',
},
output: {
filename: '[name].js', //[name]是entry里的key
path: __dirname + '/dist',
},
}
// 路径中使用 hash,每次构建时会有一个不同 hash 值,避免发布新版本时线上使用浏览器缓存
module.exports = {
// ...
output: {
filename: '[name].js',
path: __dirname + '/dist/[hash]',
},
}
**publicPath属性:**指定了在浏览器中用什么地址来引用静态文件,包括图片、js脚本以及css样式加载的地址,一般用于线上发布以及CDN部署的时候使用。具体例子如下:
<link href="http://cdn.eaxmple.com/assets/main.css" rel="stylesheet"></head>
<body>
<div id="root"></div>
<script type="text/javascript" src="http://cdn.eaxmple.com/assets/bundle.7e74c10f3f0fabe41a65.js">
</script>
</body>
之所以会自动使用publicPath
属性中设置的值,主要在于使用了html-webpack-plugin
插件来自动生成项目首页文件,这样一来,link中的href属性和script中的src属性都会被自动替换。
# 全局安装babel
npm install -g babel
# 在项目目录下安装相关包
npm install babel-loader babel-core babel-preset-env babel-preset-react -D
.babelrc
文件进行如下配置:
{
"presets": ["env", "react"]
}
npm install --save-dev react react-dom
webpack
中提供一种处理多种文件格式的机制,便是使用 loader
。我们可以把 loader
理解为是一个转换器,负责把某种文件格式的内容转换成 webpack
可以支持打包的模块。
在没有添加额外插件的情况下,webpack 会默认把所有依赖打包成 js 文件,如果入口文件依赖一个 .hbs
的模板文件以及一个 .css
的样式文件,那么我们需要 handlebars-loader
来处理 .hbs
文件,需要 css-loader
和 style-loader
来处理 .css
文件,最终把不同格式的文件都解析成 js
代码,以便打包后在浏览器中运行。
当我们需要使用不同的 loader 来解析处理不同类型的文件时,我们可以在 module.rules
字段下来配置相关的规则。
postcss-loader
用来自动给css属性加浏览器兼容性前缀。需要注意的是webpack4.x
版本后,postcss-loader
需要结合postcss-cssnext
来使用,而不是autoprefixer
。在此之前,需要先在根目录下创建一个postcss.config.js
文件(类似于.babelrc
文件)。
#postcss.config.js相关配置
module.exports = {
plugins: [
require('postcss-cssnext')
]
}
#处理scss文件
#特别注意除了安装sass-loader之外,还需要安装node-sass
npm install sass-loader node-sass postcss-loader postcss-cssnext -D
#相关配置
rules:[
{
test: /\.css$/,
use: [
'style-loader',
{
loader:'css-loader',
options:{
modules:true, //css模块化
minimize: true //在开发环境下压缩css
}
}
],
include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
exclude: /node_modules/ //排除打包目录
}, {
test: /\.less$/,
use: ['style-loader', 'css-loader', {
loader:'less-loader',
options:{
modifyVars:{
"color":"#ccc" //设置变量
}
}
}]
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', {
loader: 'postcss-loader',
options: {
config: {
path: './postcss.config.js'//得在项目根目录创建此文件
}
}
}, 'sass-loader']
}),
include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
exclude: /node_modules/ //排除打包目录
}
]
其实url-loader
封装了file-loader
。url-loader
不依赖于file-loader
。我们在使用url-loader
的时候,只需要安装url-loader
,因为url-loader
内置了file-loader
。
url-loader
在处理图片资源时分两种情况:
url-loader
将会把文件转为base64编码字符串DataURL
;url-loader
会调用file-loader
进行处理。#file-loader:在输出目录生成对应的图片,解决css等文件中引入图片路径的问题
{
module:{
rules:[
{
test: /\.(png|jpg|gif)$/,
use: ['file-loader']
}
]
}
}
#url-loader
{
module:{
rules:[
{
test:/\.(jpg|gif|jpeg|gif|png)$/,
use:[
{
loader: 'url-loader',
options: {
outputPath: 'images/', // 图片会被打包在 dist/images 目录下
limit: 1024 * 10, //小于10kb进行base64转码引用
name: '[hash:8].[name].[ext]'//打包后图片的名称,在原图片名前加上8位hash值
}
}
]
}
]
}
}
在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。可以这么理解,模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成。
该插件的主要是为了抽离css样式,防止将样式打包在js中引起页面样式加载错乱的现象。
#安装
npm install extract-text-webpack-plugin --save-dev 或 -D
#特别注意:webpack4.x,现在要安装一下版本
npm install extract-text-webpack-plugin@next -D
#引入插件
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', {
loader: 'postcss-loader',
options: {
config: {
path: './postcss.config.js'//需要在项目根目录创建此文件
}
}
}, 'sass-loader']
}),
include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
exclude: /node_modules/ //排除打包目录
}
]
},
plugins: [
new ExtractTextWebpackPlugin({
filename: 'css/[name].css' //放到dist/css/下
})
]
}
该插件有三个参数:
#如果是生产模式下,会自动压缩,不需要使用该插件进行压缩
webpack --mode production
#开发环境下
#安装该插件
npm install uglifyjs-webpack-plugin -D
#引入插件
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
#调用插件
plugins: [
new UglifyjsWebpackPlugin()
]
#安装
npm install clean-webpack-plugin -D
#引入插件
const CleanWebpackPlugin = require('clean-webpack-plugin');
#调用插件
new CleanWebpackPlugin('./dist/bundle.*.js')
webpack.DefinePlugin相当于是给配置环境定义了一组全局变量,业务代码可以直接使用定义在里面的变量。
new webpack.ProvidePlugin({
identifier: 'module1',
// ...
})
# 自动加载lodash和jquery,可以将两个变量($和_)都指向对应的 node 模块:
new webpack.ProvidePlugin({
$: 'jquery',
_: 'lodash'
})
#安装
npm install copy-webpack-plugin
#引入插件
const CopyWebpackPlugin = require('copy-webpack-plugin');
#调用
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, 'pages/static'),
ignore: ['.*']
}
])
#react-hot-loader用来支持react热加载
npm install webpack-dev-server react-hot-loader -D
#在webpack配置文件中添加devServer相应的配置
devServer: {
contentBase: './dist',//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true, //实时刷新,
compress: true,
port: 8088,
hot: true //热加载
}
还需要在.babelrc
文件中进行插件项配置:
{
"presets": ["env", "react"],
"plugins": ["react-hot-loader/babel"] //新增加
}
webpack.config.js
#rules是一个规则数组,每一项是一个对象,配置loader
rules:[
{
test:'匹配文件正则',
include:'在哪个目录匹配',
exclude:'排除哪个目录',
use:[
//配置多个loader,从右往左依次执行
{
loader:"需要的loader",
options:{
//loader的相关配置项
}
}
]
}
]
const path = require('path');
const webpack = require('webpack'); // 用于访问内置插件
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: 'babel-loader'
}, {
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
minimize: true
}
}
}, {
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
}
]
},
devServer: {
contentBase: './dist',//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true, //实时刷新,
compress: true,
port: 8088,
hot: true //热加载
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
//每次打包都会先清除当前目录中dist目录下的文件
new CleanWebpackPlugin('./dist/bundle.*.js'),
new webpack.HotModuleReplacementPlugin(),//热加载插件
],
//由于压缩后的代码不易于定位错误, 配置该项后发生错误时即可采用source-map的形式直接显示你出错代码的位置
devtool: 'eval-source-map',
resolve: {
//配置简写, 配置过后, 书写该文件路径的时候可以省略文件后缀。
extensions: ['.js', '.jsx', '.coffee', '.css', './scss']
}
};
webpack.production.config.js
const path = require('path');
const webpack = require('webpack'); // 用于访问内置插件
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[hash].js'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
]
}, {
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', {
loader: 'postcss-loader',
options: {
config: {
path: './postcss.config.js' // 得在项目根目录创建此文件
}
}
}, 'sass-loader']
}),
include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
exclude: /node_modules/ //排除打包目录
}, {
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
minimize: true
}
}
}, {
test:/\.(jpg|gif|jpeg|gif|png)$/,
use:[
{
loader: 'url-loader',
options: {
outputPath: 'images/', // 图片会被打包在 dist/images 目录下
limit: 10240, //小于10kb进行base64转码引用
name: '[hash:8].[name].[ext]'//打包后图片的名称,在原图片名前加上8位hash值
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
title: 'webpack实战练习',
template: './src/index.html'
}),
//每次打包都会先清除当前目录中dist目录下的文件
new CleanWebpackPlugin('./dist/bundle.*.js'),
new ExtractTextPlugin({
filename: '[name].css'
})
],
//由于压缩后的代码不易于定位错误, 配置该项后发生错误时即可采用source-map的形式直接显示你出错代码的位置
devtool: 'eval-source-map',
resolve: {
//配置简写, 配置过后, 书写该文件路径的时候可以省略文件后缀。
extensions: ['.js', '.jsx', '.coffee', '.css', '.scss']
}
};
webpack 的配置其实是一个 Node.js 的脚本,这个脚本对外暴露一个配置对象,webpack 通过这个对象来读取相关的一些配置。因为是 Node.js 脚本,所以可玩性非常高,你可以使用任何的 Node.js 模块,如上述用到的 path 模块,当然第三方的模块也可以。
创建了 webpack.config.js 后再执行 webpack 命令,webpack 就会使用这个配置文件的配置了。
现今,大多数前端框架都提供了简单的工具来协助快速生成项目基础文件,一般都会包含项目使用的 webpack 的配置,如:
create-react-app 的 webpack 配置在这个项目下:react-scripts。
vue-cli 使用 webpack 模板生成的项目文件中,webpack 相关配置存放在 build 目录下。
通常 angular 的项目开发和生产的构建任务都是使用 angular-cli 来运行的,但 angular-cli 只是命令的使用接口,基础功能是由 angular/devkit来实现的,webpack 的构建相关只是其中一部分,详细的配置可以参考webpack-configs。
# webpack.config.js
const path = require('path'); //引入node的path模块
const webpack = require('webpack'); //引入的webpack,使用lodash
const HtmlWebpackPlugin = require('html-webpack-plugin') //将html打包
const ExtractTextPlugin = require('extract-text-webpack-plugin') //打包的css拆分,将一部分抽离出来
const CopyWebpackPlugin = require('copy-webpack-plugin')
// console.log(path.resolve(__dirname,'dist')); //物理地址拼接
module.exports = {
entry: './src/index.js', //入口文件 在vue-cli main.js
output: { //webpack如何输出
path: path.resolve(__dirname, 'dist'), //定位,输出文件的目标路径
filename: '[name].js'
},
module: { //模块的相关配置
rules: [ //根据文件的后缀提供一个loader,解析规则
{
test: /\.js$/, //es6 => es5
include: [
path.resolve(__dirname, 'src')
],
// exclude:[], 不匹配选项(优先级高于test和include)
use: 'babel-loader'
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader',
'less-loader'
]
})
},
{ //图片loader
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader' //根据文件地址加载文件
}
]
}
]
},
resolve: { //解析模块的可选项
// modules: [ ]//模块的查找目录 配置其他的css等文件
extensions: [".js", ".json", ".jsx",".less", ".css"], //用到文件的扩展名
alias: { //模快别名列表
utils: path.resolve(__dirname,'src/utils')
}
},
plugins: [ //插进的引用, 压缩,分离美化
new ExtractTextPlugin('[name].css'), //[name] 默认 也可以自定义name 声明使用
new HtmlWebpackPlugin({ //将模板的头部和尾部添加css和js模板,dist 目录发布到服务器上,项目包。可以直接上线
file: 'index.html', //打造单页面运用 最后运行的不是这个
template: 'src/index.html' //vue-cli放在跟目录下
}),
new CopyWebpackPlugin([ //src下其他的文件直接复制到dist目录下
{ from:'src/assets/favicon.ico',to: 'favicon.ico' }
]),
new webpack.ProvidePlugin({ //引用框架 jquery lodash工具库是很多组件会复用的,省去了import
'_': 'lodash' //引用webpack
})
],
devServer: { //服务于webpack-dev-server 内部封装了一个express
port: '8080',
before(app) {
app.get('/api/test.json', (req, res) => {
res.json({
code: 200,
message: 'Hello World'
})
})
}
}
}
npm install --save-dev cross-env
"scripts": {
"build": "cross-env NODE_ENV=production webpack --config webpack.production.config.js --mode production",
"dev": "cross-env NODE_ENV=development webpack-dev-server --mode development --open",
"dll": "webpack --config webpack_dll.config.js --mode development"
}
什么是Tree Shaking
?字面意思是摇树,一句话:项目中没有使用的代码会在打包时候丢掉。JS
的Tree Shaking
依赖的是ES2015
的模块系统(比如:import和export
)。
Tree Shaking
可以用来剔除javascript
中用不上的死代码。依赖静态的ES6
模块化语法,例如通过import和export
导入、导出。Tree Shaking
最先在Rollup
中出现,Webpack
在2.0
版本中将其引入。
为了更直观的理解它,来看一个具体的例子。假如有一个文件util.js
里存放了很多工具函数和常量,在main.js
中会导入和使用util.js
,代码如下:
util.js
源码:
export function funcA() {
}
export function funcB() {
}
main.js
源码:
import { funcA } from './util.js';
funcA();
Tree Shaking
后的util.js
:
export function funcA() {
}
由于只用到了util.js
中的funcA
,所以剩下的都被Tree Shaking
当作死代码给剔除了。
需要注意的是:要让
Tree Shaking
正常工作的前提是交给Webpack
的JavaScript
代码必须是采用ES6
模块化语法的,因为ES6
模块化语法是静态的(导入导出语句中的路径必须是静态的字符串,而且不能放入其它代码块中),这让Webpack
可以简单的分析出哪些export
的被import
过了。如果采用ES5
中的模块化,例如module.export = {...}、require(x+y)、if(x){require('./util')}
,Webpack
无法分析出哪些代码可以剔除。
Tree Shaking
上面讲了Tree Shaking
是做什么的,接下来一步步教你如何配置Webpack
让Tree Shaking
生效。
首先,为了把采用ES6
模块化的代码交给Webpack
,需要配置Babel
让其保留ES6
模块化语句,修改.babelrc
文件为如下:
{
"presets": [
[
"env",
{
"modules": false
}
]
]
}
其中"modules": false
的含义是关闭Babel
的模块转换功能,保留原本的 ES6
模块化语法。
配置好Babel
后,重新运行Webpac
k,在启动Webpack
时带上--display-used-exports
参数,以方便追踪Tree Shaking
的工作, 这时你会发现在控制台中输出了如下的日志:
> webpack --display-used-exports
bundle.js 3.5 kB 0 [emitted] main
[0] ./main.js 41 bytes {0} [built]
[1] ./util.js 511 bytes {0} [built]
[only some exports used: funcA]
其中[only some exports used: funcA]
提示了util.js
只导出了用到的funcA
,说明Webpack
确实正确的分析出了如何剔除死代码。
但当你打开Webpack
输出的bundle.js
文件看下时,你会发现用不上的代码还在里面,如下:
/* harmony export (immutable) */
__webpack_exports__["a"] = funcA;
/* unused harmony export funB */
function funcA() {
console.log('funcA');
}
function funB() {
console.log('funcB');
}
Webpack
只是指出了哪些函数用上了哪些没用上,要剔除用不上的代码还得经过 UglifyJS
去处理一遍。要接入UglifyJS
也很简单,不仅可以通过UglifyJSPlugin
插件去实现,也可以简单的通过在启动Webpack
时带上--optimize-minimize
参数,为了快速验证Tree Shaking
,我们采用较简单的后者来实验下。
通过webpack --display-used-exports --optimize-minimize
重启 Webpack
后,打开新输出的bundle.js
,内容如下:
function r() {
console.log("funcA")
}
t.a = r
可以看出Tree Shaking
确实做到了,用不上的代码都被剔除了。
当我们的项目使用了大量第三方库时,你会发现Tree Shaking
似乎不生效了,原因是大部分Npm
中的代码都是采用的CommonJS
语法,这导致Tree Shaking
无法正常工作而降级处理。但幸运的时有些库考虑到了这点,这些库在发布到Npm
上时会同时提供两份代码,一份采用CommonJS
模块化语法,一份采用 ES6
模块化语法。并且在package.json
文件中分别指出这两份代码的入口。
以redux
库为例,其发布到 Npm 上的目录结构为:
node_modules/redux
|-- es
| |-- index.js # 采用 ES6 模块化语法
|-- lib
| |-- index.js # 采用 ES5 模块化语法
|-- package.json
package.json
文件中有两个字段:
{
"main": "lib/index.js", // 指明采用 CommonJS 模块化的代码入口
"jsnext:main": "es/index.js" // 指明采用 ES6 模块化的代码入口
}
mainFields
用于配置采用哪个字段作为模块的入口描述。为了让Tree Shaking
对redux
生效,需要配置Webpack
的文件寻找规则为如下:
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
};
以上配置的含义是优先使用jsnext:main
作为入口,如果不存在jsnext:main
就采用browser
或者main
作为入口。虽然并不是每个Npm
中的第三方模块都会提供ES6
模块化语法的代码,但对于提供了的不能放过,能优化的就优化。
目前越来越多的Npm
中的第三方模块考虑到了Tree Shaking
,并对其提供了支持。采用jsnext:main
作为ES6
模块化代码的入口是社区的一个约定,假如将来你要发布一个库到Npm
时,希望你能支持Tree Shaking
,以让Tree Shaking
发挥更大的优化效果,让更多的人为此受益。
webpack4
中使用Tree Shaking
需要注意的是:在webpack4
中使用Tree Shaking
,不再需要[uglifyjs-webpack-plugin]
Koa 必须使用 7.6 以上的版本。如果你的版本低于这个要求,就要先升级 Node。
新建一个项目文件夹,命令如下:
mkdir koa2 //创建koa2文件夹
cd koa2 //进入koa2文件夹
进入之后,我们初始化生产package.json 文件。
npm init -y
生成package.json后,安装koa包,我们这里使用npm来进行安装。
npm install --save koa
如果你安装错误,一般都是网速问题,可以使用cnpm来进行安装。
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
Koa提供一个Context对象,表示一次对话的上下文(包括HTTP请求和HTTP响应)。通过加工这个对象,就可以控制返回给用户的内容。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.response.body = 'hello world';
});
app.listen(3000);
ctx.response代表 HTTP Response。同样地,ctx.request代表 HTTP Request。ctx则是Koa所提供的 Context对象(上下文), ctx.body则是 ctx.response.body的alias(别名),这是响应体设置的API。
Koa 默认的返回类型是text/plain,如果想返回其他类型的内容,可以先用ctx.request.accepts判断一下,客户端希望接受什么数据(根据 HTTP Request 的Accept字段),然后使用ctx.response.type指定返回类型。
可以根据 ctx.request.url
或者 ctx.request.path
获取用户请求的路径,来实现简单的路由。
要想实现原生路由,需要得到地址栏输入的路径,然后根据路径的不同进行跳转。用ctx.request.url
就可以实现。我们通过一个简单的例子了解一下如何获得访问路径。
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.body = url
})
app.listen(3000);
这时候访问http://127.0.0.1:3000/test/123
页面会输出/test/123
。这样一来,我们就可以根据输出的不同,实现的页面结果。
const Koa = require('koa');
const fs = require('fs');
const app = new Koa();
function render(page) {
return new Promise((resolve, reject) => {
let pageUrl = `./page/${page}`;
fs.readFile(pageUrl, 'utf-8', (err,data) => {
console.log(444);
if(err) {
reject(err)
} else {
resolve(data);
}
})
});
}
const route = async (url) => {
let page = 'index.html';
switch(url) {
case '/':
page ='index.html';
break;
case '/about':
page = 'about.html';
break;
case '/404':
page = '404.html';
break;
default:
break;
}
const html = await render(page);
return html;
}
app.use(async(ctx) => {
const url = ctx.request.url;
const html = await route(url);
ctx.body = html;
})
app.listen(3000);
console.log('starting at 3000');
原生路由的实现需要引入
fs
模块来读取文件。然后再根据路由的路径去读取,最后返回给页面,进行渲染。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
let _html = '404 NotFound';
switch (ctx.url) {
case '/':
_html = '<h1>Index</h1>';
break;
case '/about':
_html = '<h1>About</h1>';
break;
case '/hello':
_html = '<h1>world</h1>';
break;
default:
break;
}
ctx.body = _html;
});
app.listen(3000);
const Koa = require('koa');
const route = require('koa-route');
const app = new Koa();
const home = async (ctx) => {
ctx.response.body = 'Hello Koa';
}
const about = async (ctx) => {
ctx.response.type = 'html';
ctx.response.body = '<a href="/">跳转到首页</a>';
}
// 根路径/的处理函数是home
app.use(route.get('/', home));
// 路径/about的处理函数是about
app.use(route.get('/about', about));
app.listen(3000);
// 安装
npm i -S koa-router
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', (ctx, next) => {
ctx.body = 'Hello Koa';
})
.get('/todo', (ctx,next) => {
ctx.body = "Todo page"
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000, () => {
console.log('starting at port 3000');
});
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', async (ctx) => {
ctx.body = `
<ul>
<li><a href="/hello">hello world</a></li>
<li><a href="/about">hello about</a></li>
</ul>
`;
}).get('/hello', async (ctx) => {
ctx.body = 'hello world';
}).get('/about', async (ctx) => {
ctx.body = 'hello about';
});
app.use(router.routes(), router.allowedMethods());
app.listen(3000);
有时候我们想把所有的路径前面都再加入一个级别,比如原来我们访问的路径是
http://127.0.0.1:3000/todo
,现在我们希望在所有的路径前面都加上一个project
层级,把路径变成http://127.0.0.1:3000/project/todo
。这时候就可以使用层级来完成这个功能。路由在创建的时候是可以指定一个前缀的,这个前缀会被至于路由的最顶层,也就是说,这个路由的所有请求都是相对于这个前缀的。
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
// 设置路由前缀
const router = new Router({
prefix:'/project'
})
router.get('/', (ctx) => {
ctx.body = 'Hello Koa';
})
.get('/todo', (ctx) => {
ctx.body = 'Todo page';
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000,() => {
console.log('starting at port 3000');
});
// 访问http://127.0.0.1:3000/project/todo
设置前缀一般都是全局的,并不能实现路由的层级,如果你想为单个页面设置层级,也是很简单的。只要在use时使用路径就可以了。
// 这里声明了两个路由,第一个是home,第二个是page,然后通过use赋予不同的前层级。
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
const home = new Router();
home.get('/liujie', async(ctx) => {
ctx.body = 'Home liujie';
}).get('/todo',async(ctx) => {
ctx.body = 'Home ToDo';
})
const page = new Router();
page.get('/liujie', async(ctx) => {
ctx.body = 'Page liujie';
}).get('/todo', async(ctx) => {
ctx.body = 'Page ToDo';
});
// 装载所有子路由
const router = new Router();
router.use('/home', home.routes(), home.allowedMethods());
router.use('/page', page.routes(), page.allowedMethods());
// 加载路由中间件
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('[demo] server is starting at port 3000');
});
获取GET请求参数:ctx.query。
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', (ctx) => {
ctx.body = ctx.query;
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000,() => {
console.log('starting at port 3000');
});
// 访问http://127.0.0.1:3000/?name=liujie&age=23
{
"name": "liujie",
"age": "23"
}
koa-static -> 静态资源中间件。
// 安装
npm i -S koa-static
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const static = require('koa-static');
app.use(static(path.join(__dirname)));
app.listen(3000);
运行该脚本文件,访问 http://127.0.0.1:3000,在浏览器里就可以看到该目录下的所有静态资源。
const Koa = require('koa');
const path = require('path');
const static = require('koa-static');
const app = new Koa();
const staticPath = './static';
app.use(static(
path.join( __dirname, staticPath)
));
app.use(async (ctx) => {
ctx.body = 'hello world111';
});
app.listen(3000, () => {
console.log('[demo] static-use-middleware is starting at port 3000');
});
// 访问http://localhost:3000/test.js,即可看到test.js脚本的结果。
在项目的某些场景下,服务器需要重定向(redirect)访问请求。比如,用户登陆以后,将他重定向到登陆前的页面。ctx.response.redirect()
方法可以发出一个302跳转,将用户导向另一个路由。
const Koa = require('koa');
const app = new Koa();
const route = require('koa-route');
const redirect = async (ctx) => {
console.log('重定向了');
ctx.response.redirect('/');
};
const home = async (ctx) => {
ctx.response.body = '<a href="/">我是首页</a>'
};
app.use(route.get('/', home));
app.use(route.get('/redirect', redirect));
app.use(home);
app.listen(3000);
Koa 的最大特色,也是最重要的一个设计,就是中间件(middleware)Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。Koa中使用 app.use()用来加载中间件,基本上Koa 所有的功能都是通过中间件实现的。每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是 next函数。只要调用 next函数,就可以把执行权转交给下一个中间件。
const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-response-time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'hello world';
});
app.listen(3000);
上面的执行顺序就是:请求 ==> x-response-time中间件 ==> logger中间件 ==> 响应中间件 ==> logger中间件 ==> response-time中间件 ==> 响应。 通过这个顺序我们可以发现这是个栈结构以"先进后出"(first-in-last-out)的顺序执行。
const Koa = require('koa');
const app = new Koa();
const main = async (ctx) => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
ctx.response.body = 'Hello World';
};
app.use(main);
app.listen(3000);
// 输出
1534731922607 GET /
const Koa = require('koa');
const app = new Koa();
const logger = async (ctx, next) => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
next();
}
const home = async (ctx) => {
ctx.response.body = 'Hello World';
};
app.use(logger);
app.use(home);
app.listen(3000);
上面代码中的logger函数就叫做"中间件"(middleware),因为它处在 HTTP Request 和 HTTP Response 中间,用来实现某种中间功能。app.use()用来加载中间件。基本上,Koa 所有的功能都是通过中间件实现的,前面例子里面的main也是中间件。每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是next函数。只要调用next函数,就可以把执行权转交给下一个中间件。
多个中间件会形成一个栈结构(middle stack),以"先进后出"(first-in-last-out)的顺序执行。
const Koa = require('koa');
const app = new Koa();
const one = async (ctx, next) => {
console.log('>> one');
next();
console.log('<< one');
}
const two = async (ctx, next) => {
console.log('>> two');
next();
console.log('<< two');
}
const three = async (ctx, next) => {
console.log('>> three');
next();
console.log('<< three');
}
app.use(one);
app.use(two);
app.use(three);
app.listen(3000);
// 运行结果
>> one
>> two
>> three
<< three
<< two
<< one
特别注意:如果中间件内部没有调用next函数,那么执行权就不会传递下去。
如果有异步操作(比如读取数据库),中间件就必须写成 async 函数。
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('./template.html', 'utf8');
};
app.use(main);
app.listen(3000);
# 需要安装fs.promised
npm i -S fs.promised
koa-compose
模块可以将多个中间件合成为一个。
const Koa = require('koa');
const compose = require('koa-compose');
const app = new Koa();
const logger = (ctx, next) => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
next();
}
const main = ctx => {
ctx.response.body = 'Hello World';
};
// 合成中间件
const middlewares = compose([logger, main]);
app.use(middlewares);
app.listen(3000);
如果代码运行过程中发生错误,我们需要把错误信息返回给用户。HTTP 协定约定这时要返回500状态码。Koa 提供了ctx.throw()
方法,用来抛出错误,ctx.throw(500)就是抛出500错误。
const Koa = require('koa');
const app = new Koa();
const main = ctx => {
ctx.throw(500);
};
app.use(main);
app.listen(3000);
如果将ctx.response.status
设置成404,就相当于ctx.throw(404)
,返回404错误。
const Koa = require('koa');
const app = new Koa();
const main = ctx => {
ctx.response.status = 404;
ctx.response.body = 'Page Not Found';
};
app.use(main);
app.listen(3000);
为了方便处理错误,最好使用try...catch将其捕获。但是,为每个中间件都写try...catch太麻烦,我们可以让最外层的中间件,负责所有中间件的错误处理。
const Koa = require('koa');
const app = new Koa();
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(404);
};
app.use(handler);
app.use(main);
app.listen(3000);
访问 http://127.0.0.1:3000 ,你会看到一个500页,里面有报错提示 {"message": "Not Found"}。
运行过程中一旦出错,Koa 会触发一个error事件。监听这个事件,也可以处理错误。
const Koa = require('koa');
const app = new Koa();
const main = ctx => {
ctx.throw(500);
};
app.on('error', (err, ctx) => {
console.error('server error', err);
});
app.use(main);
app.listen(3000);
访问 http://127.0.0.1:3000 ,你会在命令行窗口看到"server error xxx"。
需要注意的是,如果错误被try...catch
捕获,就不会触发error事件。这时,必须调用ctx.app.emit()
,手动释放error事件,才能让监听函数生效。
const Koa = require('koa');
const app = new Koa();
const handler = async (ctx, next) => {
try {
await next(); // 没有错误则将执行权交给下一个中间件
}
catch (err) {
ctx.response.status = err.statusCode || err.status || 500;
ctx.response.type = 'html';
ctx.response.body = '<p>Something wrong, please contact administrator.</p>';
ctx.app.emit('error', err, ctx);
}
};
const main = ctx => {
ctx.throw(500);
};
app.on('error', function(err) {
console.log('logging error ', err.message);
console.log(err);
});
app.use(handler);
app.use(main);
app.listen(3000);
上面代码中,main函数抛出错误,被handler函数捕获。catch代码块里面使用ctx.app.emit()手动释放error事件,才能让监听函数监听到。
ctx.cookies
用来读写 Cookie。
const Koa = require('koa');
const app = new Koa();
const main = (ctx) => {
const n = Number(ctx.cookies.get('view') || 0) + 1;
ctx.cookies.set('view', n);
ctx.response.body = n + ' views';
}
app.use(main);
app.listen(3000);
访问 http://127.0.0.1:3000 ,你会看到1 views。刷新一次页面,就变成了2 views。再刷新,每次都会计数增加1。
Web 应用离不开处理表单。本质上,表单就是 POST 方法发送到服务器的键值对。koa-body
模块可以用来从 POST 请求的数据体里面提取键值对。
const Koa = require('koa');
const koaBody = require('koa-body');
const app = new Koa();
const main = async (ctx) => {
const body = ctx.request.body;
if (!body.name) ctx.throw(400, '.name required');
ctx.body = { name: body.name };
};
app.use(koaBody());
app.use(main);
app.listen(3000);
上面代码使用 POST 方法向服务器发送一个键值对,会被正确解析。如果发送的数据不正确,就会收到错误提示。
curl -X POST --data "name=liujie" 127.0.0.1:3000
{"name":"liujie"}
curl -X POST --data "name" 127.0.0.1:3000
.name required
koa-body
模块还可以用来处理文件上传。
const os = require('os');
const path = require('path');
const Koa = require('koa');
const fs = require('fs');
const koaBody = require('koa-body');
const app = new Koa();
const main = async (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 }));
app.use(main);
app.listen(3000);
打开另一个命令行窗口,运行下面的命令,上传一个文件。注意,/path/to/file要更换为真实的文件路径。
$ curl --form upload=@/path/to/file http://127.0.0.1:3000
["/tmp/file"]
# 安装koa模板相关中间件
$ npm i -S koa-views
# 安装ejs模板引擎
$ npm i -S ejs
const Koa = require('koa');
const views = require('koa-views');
const path = require('path');
const app = new Koa();
// 加载模板引擎,模板放在views目录中
app.use(views(path.join(__dirname, './views'), {
extension: 'ejs'
}));
app.use(async (ctx) => {
const title = 'Koa2';
await ctx.render('index', {
title
});
});
app.listen(3000);
// ./view/index.ejs 模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<p>EJS Welcome to <%= title %></p>
</body>
</html>
在koa2中,获取GET请求数据需要使用request对象中的query方法或querystring方法。两者的区别在于:query返回是格式化好的参数对象,querystring返回的是请求字符串。
ctx.querystring(或ctx.request.querystring)
,返回如 a=1&b=2const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
const url = ctx.url;
const query = ctx.query;
const querystring = ctx.querystring;
ctx.body = {
url,
query,
querystring
};
});
app.listen(3000, () => {
console.log(server is starting at port 3000);
});
访问:http://localhost:3000/demo?name=liujie&age=18
结果如下:
{
"url": "/demo?name=liujie&age=18",
"query": {
"name": "liujie",
"age": "18"
},
"querystring": "name=liujie&age=18"
}
总结:获得GET请求的方式有两种,一种是从ctx.request对象中获得,另一种是直接从从上下文对象ctx中获得。获得的格式也有两种:query和querystring。
获取Post请求的步骤:
ctx.request和ctx.req的区别:
Koa2中提供了ctx.method属性,可以获取到请求的类型,然后根据请求类型编写不同的相应方法,这在工作中非常常用。
const Koa = require('koa');
const app = new Koa();
app.use( async (ctx) => {
//当请求时GET请求时,显示表单让用户填写
if(ctx.url === '/' && ctx.method === 'GET') {
const html =`
<h1>Koa2 request post demo</h1>
<form method="POST" action="/">
<p>userName</p>
<input name="userName" /> <br/>
<p>age</p>
<input name="age" /> <br/>
<p>webSite</p>
<input name='webSite' /><br/>
<button type="submit">submit</button>
</form>
`;
ctx.body = html;
//当请求时POST请求时
}
else if(ctx.url === '/' && ctx.method === 'POST') {
ctx.body = '接收到请求';
}
else {
//其它请求显示404页面
ctx.body = '<h1>404 not found!</h1>';
}
})
app.listen(3000, () => {
console.log('[demo] server is starting at port 3000');
});
const Koa = require('koa');
const app = new Koa();
app.use( async (ctx) => {
//当请求时GET请求时,显示表单让用户填写
if(ctx.url === '/' && ctx.method === 'GET') {
const html =`
<h1>Koa2 request post demo</h1>
<form method="POST" action="/">
<p>userName</p>
<input name="userName" /> <br/>
<p>age</p>
<input name="age" /> <br/>
<p>webSite</p>
<input name='webSite' /><br/>
<button type="submit">submit</button>
</form>
`;
ctx.body = html;
//当请求时POST请求时
}
else if(ctx.url === '/' && ctx.method === 'POST') {
const pastData = await parsePostData(ctx);
ctx.body = pastData;
}
else {
//其它请求显示404页面
ctx.body = '<h1>404 not found!</h1>';
}
});
// 解析Node原生POST参数
function parsePostData(ctx) {
return new Promise((resolve, reject) => {
try {
let postdata = '';
ctx.req.on('data', (data) => {
postdata += data
});
ctx.req.addListener('end', () => {
const parseData = parseQueryStr(postdata);
resolve(parseData);
});
} catch (error) {
reject(error);
};
});
}
// POST字符串解析JSON对象
function parseQueryStr(queryStr) {
let queryData = {};
let queryStrList = queryStr.split('&');
console.log(queryStrList);
for ( let [index,queryStr] of queryStrList.entries() ) {
let itemList = queryStr.split('=');
console.log(itemList);
queryData[itemList[0]] = decodeURIComponent(itemList[1]);
}
return queryData;
}
app.listen(3000, () => {
console.log('[demo] server is starting at port 3000');
});
// 运行结果:
{
"userName": "liujie",
"age": "20",
"webSite": "www.baidu.com"
}
对于POST请求的处理,koa2没有封装获取参数的方法,需要通过自己解析上下文context中的原生node.js请求对象req,将POST表单数据解析成querystring(例如:a=1&b=2&c=3),再将querystring 解析成JSON格式(例如:{"a":"1","b":"2","c":"3"}
),我们来直接使用koa-bodyparser
模块从 POST
请求的数据体里面提取键值对。
// 安装(-S是因为需要在生产环境使用)
npm i -S koa-bodyparser
const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
// 使用koa-bodyparser中间件
app.use(bodyParser());
app.use(async (ctx) => {
if(ctx.url === '/' && ctx.method === 'GET') {
// 当GET请求时返回表单页面
ctx.body = `
<h1>koa-bodyparser</h1>
<form method="post" action="/">
姓名:<input name="name" />
年龄:<input name="age" />
邮箱:<input name="email" />
<button type="submit">提交</button>
</form>
`;
}
else if(ctx.url === '/' && ctx.method === 'POST') {
// 当POST请求的时候,中间件koa-bodyparser解析POST表单里面的数据,并展示出来
ctx.body = ctx.request.body;
}
else {
ctx.body = '<h1>404 Not Found</h1>';
}
});
app.listen(3000);
// 提交结果展示:
{
"name": "liujie",
"age": "18",
"email": "[email protected]"
}
开发中制作登录和保存用户信息在本地,最常用的就是cookie操作。比如我们在作一个登录功能时,希望用户在接下来的一周内都不需要重新登录就可以访问资源,这时候就需要我们操作cookie来完成我们的需求。koa的上下文(ctx)直接提供了读取和写入的方法。
ctx.cookies.get(name,[optins])
:读取上下文请求中的cookie。ctx.cookies.set(name,value,[options])
:在上下文中写入cookie。const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
if(ctx.url === '/index'){
ctx.cookies.set(
'name','liujie'
);
ctx.body = 'cookie is ok';
}
else {
ctx.body = 'hello world';
}
});
app.listen(3000, () => {
console.log('[demo] server is starting at port 3000');
})
写好后预览,打开控制台,可以在Application中找到Cookies选项。就可以找到我们写入的name和value了。
const Koa = require('koa');
const app = new Koa();
app.use(async(ctx) => {
console.log(ctx.url);
if(ctx.url === '/index') {
ctx.cookies.set(
'age','22', {
domain: '127.0.0.1', // 写cookie所在的域名
path: '/index', // 写cookie所在的路径
maxAge: 1000*60*60*24, // cookie有效时长
expires: new Date('2019-12-31'), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取
overwrite: false // 是否允许重写
}
);
ctx.body = 'cookie is ok222';
}
else {
ctx.body = 'hello world'
}
});
app.listen(3000, () => {
console.log('[demo] server is starting at port 3000');
});
特别注意:127.0.0.1和localhost是两个不同的domain。
const Koa = require('koa');
const app = new Koa();
app.use(async(ctx) => {
if (ctx.url === '/index') {
ctx.cookies.set(
'name','liujie', {
domain:'127.0.0.1', // 写cookie所在的域名
path:'/index', // 写cookie所在的路径
maxAge:1000*60*60*24, // cookie有效时长
expires:new Date('2018-12-31'), // cookie失效时间
httpOnly:false, // 是否只用于http请求中获取
overwrite:false // 是否允许重写
}
);
ctx.body = 'cookie is ok';
}
else {
if (ctx.cookies.get('name')) {
ctx.body = ctx.cookies.get('name');
}
else {
ctx.body = 'Cookie is not found';
}
}
});
app.listen(3000, () => {
console.log('[demo] server is starting at port 3000');
})
[TOC]
目前项目中,js
主要是用ES6+
编写的。但是,并不是所有浏览器都支持ES6+
,这就需要对其进行转换,这个转换步骤称为 transpiling
(转译)。
Webpack需要借助于loader
(加载器)进行相应的转换。babel-loader
就是用于将ES6+
转译成ES5
。
一个webpack loader
作用就是:把输入进去的文件转化成指定的文件格式输出。其中babel-loader
负责将传入的es6
文件转化成浏览器可以运行的文件。
es6+
语法转化;ES6+
转换成ES5
(注意:babel-preset-es2015
已经被废弃了);es6
内置方法和函数转化垫片;polyfill
污染全局变量。需要注意的是:babel-loader和babel-polyfill
。前者负责语法转化,比如:箭头函数;后者负责内置方法和函数,比如:new Set()
。
下面来安装和配置Babel:
npm i @babel/core babel-loader @babel/preset-env --save-dev
在项目根目录新建一个.babelrc
文件,内容为:
{
"presets": ["@babel/preset-env"]
}
创建一个新的文件webpack.config.js
,内容为
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};
上述webpack
的配置很简单,所有以.js
结尾的文件都会用babel-loader
把ES6
编译转化成ES5
的文件。同时指定了入口文件的路径为src/index.js
,输出为dist/bundle.js
。
node
中引入:
require("babel-polyfill");
如果在应用入口使用ES6
的import
语法,需要在入口顶部通过 import
将polyfill
引入,以确保它能够最先加载:
import "babel-polyfill";
在webpack.config.js
中,将babel-polyfill
加到entry 数组中:
module.exports = {
entry: ['babel-polyfill', './src/index.js']
};
--module-bind
参数允许我们在命令行中指定加载器。该参数从webpack3
开始就有了。
如果希望在没有配置文件的情况下使用babel-loader
,需要在 package.json
中配置npm scripts
,如下所示:
"scripts": {
"dev": "webpack --mode development --module-bind js=babel-loader",
"build": "webpack --mode production --module-bind js=babel-loader"
}
运行npm run build
构建项目。
常用压缩格式:
.zip
,.gz
,.bz2
,.tar.gz
,.tar.bz2
.
.zip格式压缩
#压缩文件
zip 压缩文件名 源文件
#压缩目录
zip -r 压缩文件名 源目录
#解压缩.zip文件
unzip 压缩文件
.gz格式压缩
#压缩为.gz格式的压缩文件,源文件会消失
gzip 源文件
#压缩为.gz格式,源文件保留
#例如:gzip -c img > img.gz
gzip -c 源文件 > 压缩文件
#压缩目录下所有的子文件,但是不能压缩目录
gzip -r 目录
#解压缩文件
gzip -d 压缩文件
或者
gunzip 压缩文件
#解压缩目录(目录中的子文件会被解压缩,目录不会发生变化)
gunzip -r 目录
.bz2格式压缩
#压缩为.bz2格式,不保留源文件
bzip2 源文件
#压缩之后保留源文件
bzip2 -k 源文件
#注意:bzip2命令不能压缩目录
#解压缩,-k保留压缩文件
bzip2 -d 压缩文件
或者
bunzip2 压缩文件
#打包
tar -cvf 打包文件名 源文件
选项:
-c: 打包
-v: 显示打包过程
-f: 指定打包后的文件名
例如:
tar -cvf img1.tar img1
#打包到指定目录
tar czvf test.tar *.txt -C /home/work
#解打包
tar -xvf 打包文件名
选项:
-x: 解打包
例如:
tar -xvf img1.tar
.tar.gz压缩格式
.tar.gz
格式是先打包为.tar
格式,再压缩为.gz
格式。
tar -zcvf 压缩包名.tar.gz 源文件
选项:
-z: 压缩为.tar.gz格式
#解压缩
tar -zxvf 压缩包名.tar.gz
选项:
-x: 解压缩.tar.gz格式
.tar.bz2压缩格式
tar -jcvf 压缩包名.tar.bz2 源文件
选项:
-z: 压缩为.tar.bz2格式
#解压缩
tar -jxvf 压缩包名.tar.bz2
选项:
-x: 解压缩.tar.bz2格式
tar -jxvf 压缩包名.tar.bz2 -C 解压目录
选项:
-C: 用来指定想要解压到的目录
#把压缩包放到指定位置
tar -zcvf 绝对路径+压缩包名.tar.gz 源文件
例如:
tar -zcvf /test/img.tar.gz img
Linux中很多压缩程序只能针对一个文件进行压缩,这样当你想要压缩一大堆文件时,你得将这一大堆文件先打成一个包(tar命令),然后再用压缩程序进行压缩(gzip bzip2命令)。
Linux下最常用的打包程序就是tar
了,使用tar程序打出来的包我们常称为tar包
,tar包
文件的命令通常都是以.tar
结尾的。生成tar包后,就可以用其它的程序来进行压缩。
tar [必要参数][选择参数] [文件]
用来压缩和解压文件,tar本身不具有压缩功能,通过调用压缩功能实现的。
必要参数有如下:
可选参数如下:
.tar
#解包
tar xvf FileName.tar
#打包
tar cvf FileName.tar DirName
#说明:tar是打包,不是压缩
.gz
#解压1
gunzip FileName.gz
#解压2
gzip -d FileName.gz
#压缩
gzip FileName
.tar.gz 和 .tgz
#解压
tar zxvf FileName.tar.gz
#压缩
tar zcvf FileName.tar.gz DirName
.bz2
#解压1
bzip2 -d FileName.bz2
#解压2
bunzip2 FileName.bz2
#压缩
bzip2 -z FileName
.tar.bz2
#解压
tar jxvf FileName.tar.bz2
#压缩
tar jcvf FileName.tar.bz2 DirName
.bz
#解压1
bzip2 -d FileName.bz
#解压2
bunzip2 FileName.bz
.tar.bz
#解压
tar jxvf FileName.tar.bz
.Z
#解压
uncompress FileName.Z
#压缩
compress FileName
.tar.Z
#解压
tar Zxvf FileName.tar.Z
#压缩
tar Zcvf FileName.tar.Z DirName
.zip
#解压
unzip FileName.zip
#压缩
zip FileName.zip DirName
.rar
#解压
rar x FileName.rar
#压缩
rar a FileName.rar DirName
C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security
;everyting
软件找到chrome.exe
所在的路径,在chrome.exe
所在的路径按下shift
键,点击右键,点击“在此处打开命令行窗口”,然后输入chrome --disable-web-security
。chrome --disable-web-security --user-data-dir=g:\temp3
jsonp
返回的是js
代码,不是json
对象。
content-type
:发送信息至服务器时内容编码类型,即客户端发送请求数据的类型;ajax
的属性添加cache:true
,表示结果可以被缓存,请求的链接中就没有_=某个值;
jsonp
的弊端:
GET
请求;xhr
请求。
jsonp
方式发出的请求为script类型。
客户端->http服务器->应用服务器,然后从,应用服务器->http服务器->客户端。
Apache/nginx为http静态服务器,用来处于静态请求或者负载均衡。
表示允许所有的域名和方法。
Access-Control-Max-Age
指定缓存预检请求的时间。非简单请求每次都要发送两条请求,效率很低,可以通过将预检请求缓存来减少请求数量,设置方法是服务端响应头设置Access-Control-Max-Age
,值是预检请求缓存时间,如下所示:
// 缓存预检请求1个小时
"Access-Control-Max-Age": "3600"
$.ajax({
type: "get",
xhrFields: {
widthCredentials: true // 发送ajax请求的时候会带上cookie
}
})
不允许设置:Access-Control-Allow-Origin: *;
,必须指定为特定的域名。
// enable cookie
res.addHeader("Access-Control-Allow-Credentials", "true")
当产生跨域的时候,请求头中会多一个字段,叫做origin
,这个字段存储着当前域的信息。所以在发送带cookie
的请求,后台又不知道调用方的域的信息时,可以先取到请求头中origin
字段的值,然后再赋值给响应头的access-control-allow-origin
字段。
虚拟主机:多个域名指向同一个服务器,服务器根据不同的域名把请求转到不同的应用服务器。看上去有多个主机,实际上只有一个主机。
nginx配置:
server {
listen 80; // 监听的端口
server_name b.com; // 监听的域名
// 所有的请求都转发到这个地址
location /{
proxy_pass http://localhost:8080/;
}
}
nginx配置跨域:
server {
listen 80; // 监听的端口
server_name b.com; // 监听的域名
// 所有的请求都转发到这个地址
location /{
proxy_pass http://localhost:8080/;
}
}
反向代理:访问同一个域名的不同
url
,最后去到两个不同的服务器。
反向代理-nginx配置:
反向代理-apache配置:
当前请求地址:
创建自定义对象的最简单方式就是创建一个Object
的实例,然后再为这个实例添加相应的属性和方法,例子如下:
var person = new Object();
person.name = "lisi";
person.age = 22;
person.job = "worker";
person.sayName = function() {
console.log(this.name);
}
对象字面量形式创建对象(首选方式):
var person = {
name: "lisi",
age: 22,
job: "worker",
sayName: function() {
console.log(this.name);
}
};
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。
delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,这个特性默认值为true;for-in
循环返回属性;直接在对象上定义的属性,这个特性默认值为true;undefined
。特别说明:直接在对象上定义的属性,它们的
[[Configurable]]、[[Enumerable]]和[[Writable]]
特性都被设置为true
,而[[Value]]
特性被设置为特定的值。
var person = {
name: 'lisi'
};
这里创建了一个名为
name
的属性,为它指定的值为lisi
。[[Value]]
特性被设置为lisi
,而对这个值的任何修改都将反映在这个位置。
要修改属性默认的特性,必须使用
ECMAScript5
的Object.defineProperty()
方法。该方法接收三个参数:属性所属对象、属性名称和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value
。设置其中的一或多个值,可以修改对性的特性值。例如:
var person = {};
Object.defineProperty(person, 'name', {
// 表明name属性是只读的
writable: false,
value: 'lisi'
});
console.log(person.name); // lisi
person.name = 'wangwu';
console.log(person.name); // lisi
在上述例子中:
name
属性是只读的,即该属性的值不可修改,如果尝试为它指定新值,在非严格模式下,赋值操作会被忽略;在严格模式下,赋值操作将会导致错误:TypeError: Cannot assign to read only property 'name' of object '#<Object>'
。
var person = {};
Object.defineProperty(person, 'name', {
// 表明name属性不可配置
configurable: false,
value: 'lisi'
});
console.log(person.name); // lisi
delete person.name;
console.log(person.name); // lisi
严格模式下将报错:
TypeError: Cannot delete property 'name' of #<Object>
。
var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
value: 'lisi'
});
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// 运行结果如下:
{ value: 'lisi',
writable: false,
enumerable: false,
configurable: false }
特别说明:在调用
Object.defineProperty()
方法时,如果不指定,configurable、enumerable和writable
特性的默认值都是false
。在上述代码中enumerable和writable
都没有被指定,默认值均为false。
特别注意:把
configurable
设置为false
,表示不能从对象中删除属性,即一旦把属性定义为不可配置的,就不能再把它变回可配置的了。再调用Object.defineProperty()
方法修改除writable
之外的特性,都会导致错误。
var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
writable: true,
value: 'lisi'
});
Object.defineProperty(person, 'name', {
writable: false,
value: 'lisi'
});
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// 运行结果:
{ value: 'lisi',
writable: false,
enumerable: false,
configurable: false }
var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
value: 'lisi'
});
Object.defineProperty(person, 'name', {
writable: true,
value: 'lisi'
});
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// 运行结果:
TypeError: Cannot redefine property: name
从上述例子中我们可以知道:可以多次调用
Object.defineProperty()
方法修改同一个属性,但是前提是configurable
设置为true
。当configurable
设置为false
时就会有限制。
configurable值 | 描述 |
---|---|
true | 可以多次使用,任意修改其他属性 |
false | 仅可以再次将writable由true修改为false,其他均会报错 |
访问器属性不包含数据值;它们包含一对儿getter和setter
函数(不是必需的)。在读取访问器属性是,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性:
delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。直接在对象上定义的属性,这个特性的默认值为true
。for-in
循环返回属性。直接在对象上定义的属性,这个特性的默认值为true
。undefined
。undefined
。特别说明:访问器属性不能直接定义,必须使用
Object.defineProperty()
来定义。
var book = {
_year: 2004,
edition: 1
};
// year是访问器属性
Object.defineProperty(book, 'year', {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
console.log(Object.getOwnPropertyDescriptor(book, '_year'));
console.log(Object.getOwnPropertyDescriptor(book, 'year'));
// 这里修改访问器属性,会调用访问器属性的set函数
book.year = 2005;
console.log(book.edition); // 2
// 这里读取访问器属性year,会调用访问器属性的get函数,返回this._year
console.log(book.year);
console.log(book._year); // 2005
// 运行结果如下:
{ value: 2004,
writable: true,
enumerable: true,
configurable: true }
{ get: [Function: get],
set: [Function: set],
enumerable: false,
configurable: false }
2
上述例子中定义了一个
book
对象,该对象有两个默认属性_year和edition
。_year
前面的下划线是一种常用的记号,表示只能通过对象方法访问的属性。访问器属性year
包含一个getter函数和一个setter函数,getter函数返回_year
的值,setter函数通过计算来确定正确的版本。
var book = {
_year: 2004,
edition: 1
};
// year是访问器属性
Object.defineProperty(book, 'year', {
get: function() {
return this._year;
}
});
book.year = 2005;
console.log(book.edition);
运行结果如下:
book.year = 2005;
^
TypeError: Cannot set property year of #<Object> which has only a getter
如上例所示:没有指定
setter
函数的属性不能写,再严格模式下尝试写入属性会报错。
ECMAScript5
定义了一个Object.defineProperties()方法
,可以通过描述符一次定义多个属性。接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例如下例所示:
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {// 访问器属性
get: function() {
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue -2004;
}
}
}
});
ECMAScript5
的Object.getOwnPropertyDescriptor()
方法,可以取得给定属性的描述符。方法接受两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set
;如果是数据对象,这个对象的属性有configurable、enumerable、writable和value
。例如:
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"
对于数据属性
_year
,value
等于最初的值,configurable
是false
,而get
等于undefined
;对于访问器属性year
,value
等于undefined
,enumerable
是false
,而get
是一个指向getter
函数的指针。
注意:在JavaScript中,可以针对任何对象——包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()方法。
虽然
Object
构造函数或对象字面量都可以用来创建单个对象,但这些方法有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复代码。下面介绍一些创建对象的模式:
<script type="text/javascript">
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
var person1 = createPerson("liujie", 13, "student");
var person2 = createPerson("lisi", 14, "doctor");
person1.sayName();
person2.sayName();
</script>
可以无数次的调用这个函数,每次它都会返回一个包含3个属性和一个方法的对象。 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题即怎样知道一个对象的类型。
<script type="text/javascript">
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
var person1 = new Person("liujie", 13, "student");
var person2 = new Person("lisi", 15, "doctor");
person2.sayName();
person1.sayName();
// 实例的constructor属性指向构造函数
console.log(person1.constructor == Person);//true
console.log(person2.constructor == Person);//true
//创建的所有对象既是Object的实例,也是Person的实例
//之所以是Object的实例,是因为所有对象均继承自Object
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
</script>
Person()
中的代码除了与createPerson()
中相同的部分外,还存在以下不同之处:
特别注意:构造函数始终都应该以一个大写字母开头,非构造函数应该以小写字母开头。构造函数本身也是函数,只不过用来创建对象而已。
要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上经历以下4个步骤:
在上述例子的中,
person1和person2
分别保存着Person
的一个不同实例。这两个对象都有一个constructor
(构造函数)属性,该属性指向Person
。
创建自定义的构造函数意味着将来可以将它的实例标志为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
<script type="text/javascript">
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
// 当作构造函数使用
var person = new Person("liujie", 14, "student");
person.sayName();// liujie
// 当作普通函数调用(属性和方法都添加到window对象)
Person("lisi", 16, "doctor");
window.sayName();// lisi
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "wangwu", 15, "master");//这里是在对象o的作用域中调用Person构造函数,调用后o就拥有了所有属性和sayName()方法
o.sayName();//wangwu
</script>
由上述例子可知:构造函数与其他函数的唯一区别在于:调用方式不同。但是,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过
new
操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。例如,前面例子的Person()
函数可以通过上述例子中的任何一种方式来调用。
console.log(person1.sayName == person2.sayName);// false
构造函数的主要问题在于:每个方法都要在每个实例上重新创建一遍。每个Person实例都有一个不同的Function实例。在前面例子中
person1
和person2
都有一个sayName()
方法,但是这两个方法不是同一个Function
的实例。原因在于:在ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。
创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,可以把函数定义转移到构造函数外部来解决这个问题。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在上述例子中:我们把
sayName()
函数的定义转移到了构造函数外部。在构造函数内部将sayName
属性设置成等于全局的sayName()
函数。这样依赖,sayName
属性中包含的是一个指向函数的指针,因为person1和person2
对象就共享了在全局作用域中定义的同一个sayName()
函数。
这样做确实解决了两个函数做同一件事的问题,但是带来了新问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可通过原型模式解决。
我们创建的每个函数都有一个prototype(原型)
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思来理解,prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是: 可以让所有对象实例共享它所包含的属性和方法。即不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下:
function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
console.log(person1.sayName == person2.sayName); //true
上述例子中:直接将
sayName()
方法和所有属性添加到了Person
的prototype
属性中,构造函数变成了空函数。这样一来,通过构造函数创建的新对象都会具有相同的属性和方法,而且新对象的这些属性和方法是由所有实例共享的。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个
prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor
(构造函数)属性,这个属性包含一个指向prototype
属性所在函数的指针。
拿前面的例子来说,
Person.prototype.constructor
指向Person
。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
>如上图,展示了`Person`构造函数、`Person`的原型属性以及`Person`现有的两个实例之间的关系。其中,`Person.prototype`指向了原型对象,而`Person.prototype.constructor`又指回了`Person`。原型对象中除了包含`constructor`属性之外,还包括后来添加的其他属性。`Person`的每个实例——`person1和person2`都包含了一个内部属性,该属性仅仅指向了`Person.prototype`;换句话说,它们与构造函数没有直接关系。 ###### 3.3.2 isPrototypeOf():确定对象原型方法 ```js console.log(Person.prototype.isPrototypeOf(person1)); //true console.log(Person.prototype.isPrototypeOf(person2)); //true ``` >上述代码说明`person1和person2`内部都有一个指向`Person.prototype`的指针。 ###### 3.3.3 Object.getPrototypeOf():ECMAScript 5新增方法 ```js console.log(Object.getPrototypeOf(person1) == Person.prototype); //true console.log(Object.getPrototypeOf(person1).name); //"Nicholas" ``` >通过上述代码可知:`Object.getPrototypeOf()`方法可以方便地获取到一个对象的原型。 ###### 3.3.4 对象属性读取的搜索顺序 >每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。**搜索首先从实例本身开始**。如果实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有指定名字的属性。所以,当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。这同时也是多个对象实例共享原型所保存的属性和方法的基本原理。创建了自定义的构造函数之后,其原型对象默认只会取得
constructor
属性;至于其他方法,则都是从Object
继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
<script type="text/javascript">
function Person() {
}
Person.prototype.name = "liujie";
Person.prototype.age = 13;
Person.prototype.job = "student";
Person.prototype.sayName = function() {
console.log(this.name);
};
//这里person1和person2访问的是同一组属性和同一个sayName()方法
var person1 = new Person();
person1.sayName();//liujie
var person2 = new Person();
person2.sayName();//liujie
console.log(person1.sayName == person2.sayName);//true
console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true
//Object.getPrototypeOf()返回的对象就是原型对象
console.log(Object.getPrototypeOf(person1) == Person.prototype);//true
console.log(Object.getPrototypeOf(person2) == Person.prototype);//true
</script>
<script type="text/javascript">
function Person() {
}
Person.prototype.name = "liujie";
Person.prototype.age = 13;
Person.prototype.job = "student";
Person.prototype.sayName = function() {
console.log(this.name);
};
//这里person1和person2访问的是同一组属性和同一个sayName()方法
var person1 = new Person();
person1.name = "liujiejie";
//如果对象实例中的属性与实例原型中的一个属性同名,实例中的属性会屏蔽原型中的那个属性
//person1.name=null;//即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的链接
delete person1.name;//如果使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性
person1.sayName();//liujiejie
var person2 = new Person();
// 说明了可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值
person2.sayName();//liujie
</script>
hasOwnProperty()
方法可以检测一个属性存在于实例中还是存在于原型中。只有给定属性存在于对象实例中时,该方法才返回true
。
<script type="text/javascript">
function Person() {
}
Person.prototype.name = "liujie";
Person.prototype.age = 13;
Person.prototype.job = "student";
Person.prototype.sayName = function(){
console.log(this.name);
};
//这里person1和person2访问的是同一组属性和同一个sayName()方法
//使用hasOwnProperty()方法,当给定属性存在于实例中时,返回true
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name"));//false
person1.name = "liujiejie";
console.log(person1.name);//liujiejie 来自实例
console.log(person1.hasOwnProperty("name"));//true
console.log(person2.name);//liujie 来自原型
console.log(person2.hasOwnProperty("name"));//false
delete person1.name;
console.log(person1.name);//liujie 来自原型
console.log(person1.hasOwnProperty("name")); // false
</script>
特别注意:只要能够访问到给定属性,in操作符就返回true。不管属性存在于原型上还是实例上。
<script type="text/javascript">
function Person() {
}
Person.prototype.name = "liujie";
Person.prototype.age = 13;
Person.prototype.job = "student";
Person.prototype.sayName = function() {
console.log(this.name);
};
//这里person1和person2访问的是同一组属性和同一个sayName()方法
//使用hasOwnProperty()方法,当给定属性存在于实例中时,返回true
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name"));//false
console.log("name" in person1);// true
person1.name = "liujiejie";
console.log(person1.name); // liujiejie 来自实例
console.log(person1.hasOwnProperty("name"));// true
console.log("name" in person1);// true
console.log(person2.name); //liujie 来自原型
console.log(person2.hasOwnProperty("name"));//false
console.log("name" in person2);//true
delete person1.name;
console.log(person1.name);//liujie 来自原型
console.log(person1.hasOwnProperty("name"));// false
console.log("name" in person1);// true
</script>
同时使用
hasOwnProperty()方法和in操作符
,就可以确定该属性到底是存在于对象中,还是存在于原型中。如下代码所示:
// 返回true表示存在于原型,false表示存在于实例中
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
在使用
for-in
循环时,返回的是所有能够通过对象访问的、可枚举(enumerated) 的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会存在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的,但是在IE8及更早版本中例外。
<script type="text/javascript">
var obj = {
toString: function(){
return "myvalue";
}
}
for(var prop in obj) {
if(prop == "toString"){
alert("toString found"); // 在IE中不会显示
}
}
</script>
要取得对象上所有可枚举的实例属性,可以使用ECMAScript5的
Object.keys()
方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
<script type="text/javascript">
function Person(){}
Person.prototype.name = "lisi";
Person.prototype.age = 23;
Person.prototype.sayName = function(){
alert(this.name + "--" + this.age);
}
var keys = Object.keys(Person.prototype);
console.log(keys);//["name", "age", "sayName"]
var person1 = new Person();
person1.name = "wangwu";
person1.age = 23;
console.log(Object.keys(person1));//["name", "age"]
</script>
如果想要得到所有的实例属性,无论它是否可枚举,可以使用
Object.getOwnPropertyNames()
方法。
<script type="text/javascript">
function Person(){}
Person.prototype.name = "lisi";
Person.prototype.age = 23;
Person.prototype.sayName = function(){
alert(this.name + "--" + this.age);
}
var keys2 = Object.getOwnPropertyNames(Person.prototype);
console.log(keys2); // ["constructor", "name", "age", "sayName"]
</script>
注意:这里的结果中包含了不可枚举的constructor属性。
Object.getOwnPropertyNames()
和Object.keys()
方法都可以用来代替for-in循环。
在前面的例子中每添加一个属性和方法都要敲一遍Person.prototype
。为了减少不必要的输入,从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下代码所示:
function Person() {
}
// 这里将一个新的对象字面量赋值给Person.prototype
// 新的对象字面量的constructor指向Object
Person.prototype = {
name : "Nicholoas",
age : 29,
job : "Software Engineer",
sayName : function () {
console.log(this.name);
}
};
var friend = new Person();
console.log(friend instanceof Object); //true
console.log(friend instanceof Person); //true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object); //true
在上述代码中:将
Person.prototype
设置为等于一个对象字面量形式创建的新对象,最终结果相同。但是有一个例外:constructor
属性不再指向Person
了。因为,每创建一个函数,就会同时创建它的prototype
对象,这个对象也会自动获得constructor
属性。而我们在这里本质上完全重写了默认的prototype
对象,因此constructor
属性也就变成了新对象的constructor
属性(指向Object
构造函数),不再指向Person
函数。所以需要显式设置constructor属性到适当的值。
function Person() {
}
Person.prototype = {
// 显示指定constructor属性
constructor : Person,
name : "Nicholoas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
这样重设
constructor
属性会导致它的[[Enumerable]]
特性被设置为true
。默认情况下,原生的constructor
属性是不可枚举的。因此,考虑使用Object.defineProperty()
方法。
function Person() {
}
Person.prototype = {
name : "Nicholoas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
<script type="text/javascript">
function Person() {
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
var friend = new Person(); // 即使先创建了实例,后修改了原型也没有问题
Person.prototype.sayHi = function() {//向原型中添加方法
alert("hi");
};
friend.sayHi(); //"hi" – works! 先在实例中寻找sayHi方法,找不到的话再搜索原型
</script>
function Person() {
}
var friend = new Person();// 调用构造函数创建实例,并向实例中添加一个指向最初原型的[[Prototype]]指针,而把原型重写修改后就等于切断了构造函数与最初原型之间的联系。
Person.prototype = {
//这里重写原型对象切断了现有的原型与任何之前已经存在的对象实例之间的联系,之前的实例引用的仍然是最初的原型
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
console.log(this.name);
}
};
friend.sayName(); // TypeError: friend.sayName is not a function
var person = new Person();
person.sayName(); // Nicholas
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。最重要的,其共享的本性对于函数非常合适,对于包含基本值的属性也说得过去,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。但对于包含引用类型值得属性来说,例如数组,就会有问题了。
function Person() {
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends: ['lisi', 'wangwu'],
sayName : function () {
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('xiaohong');
console.log(person1.friends); // [ 'lisi', 'wangwu', 'xiaohong' ]
console.log(person2.friends); // [ 'lisi', 'wangwu', 'xiaohong' ]
console.log(person1.friends === person2.friends); // true
person1和person2
实例共享了同一个friends
,但是实例一般都是要有属于自己的全部属性。这就是原型模式的问题所在。
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function() {
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(perosn1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true
该例中,实例属性都是在构造函数中定义,共享属性constructor和方法sayName()则是在原型中定义的。这种构造函数和原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
动态原型模式把所有信息都封装在了构造函数中,而通过构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function" {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
通常,在前几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本**是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。用于不能直接修改Array构造函数,可以使用这个模式。
function SpecialArray() {
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function() {
return this.join("|");
};
//返回数组
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
注意:返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此,不能依赖instanceof操作符来确定对象类型。
道格拉斯 · 克罗克福德(Douglas Crockford)发明了JavaScrip中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用this;二是不适用new操作符调用构造函数。
function Person(name, age, job) {
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName function() {
alert(name);
};
//返回对象
return o;
}
除了使用sayName()方法外,没有别的方式可以访问name的值。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
变量friend中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全环境——例如,ADsafe和Caja提供的环境——下使用。
[TOC]
该插件直接为项目生成一个或多个HTML
文件(文件个数由插件实例的个数决定),并将webpack
打包后输出的所有脚本文件自动添加到生成的HTML文件中。通过配置,可以将根目录下用户自定义的HTML文件作为插件生成HTML文件的模板。另外,还可以通过向插件传递参数,控制HTML文件的输出。
第一步:在项目根目录下安装插件:
npm install html-webpack-plugin --save-dev
第二步:在webpack配置文件中引入该插件:
const htmlWebpackPlugin = require('html-webpack-plugin');
第三步:在webpack配置文件暴露的对象中添加一个
plugins
属性,该属性值是一个数组,将新建的html-webpack-plugin
对象实例添加到数组中。若不传入任何参数,那么插件将生成默认的html
文件。
module.exports = {
entry: {
main:'./src/index.js'
},
output: {
path: './dist',
filename: '[name].bundle.js'
},
plugins:[
new htmlWebpackPlugin()
]
}
第四步:配置参数,为新建的对象实例传入一个对象字面量参数,初始化对象实例的属性。
module.exports = {
... ,
plugins:[
new htmlWebpackPlugin({
filename: 'index.html',
template: './src/template.html',
inject: false,
title: 'Common template',
chunks: ['main']
})
]
}
htmlWebpackPlugin
对象有两个属性,一个是files
,一个是options
。这两个属性的值都是对象。通过EJS
语法,可以在HTML
模板文件中遍历这两个属性:
<% for(var key in htmlWebpackPlugin.files) { %>
<%= key %> : <%= JSON.stringify(htmlWebpackPlugin.files[key]) %> //将对象或数组转换为JSON字符串。
<% } %>
<% for(var key in htmlWebpackPlugin.options) { %>
<%= key %> : <%= JSON.stringify(htmlWebpackPlugin.options[key]) %>
<% } %>
遍历结果如下:
index.html
,可以直接配置带有子目录;html、jade、ejs
等。但是要注意的是,如果想使用自定义的模板文件的时候,需要安装对应的loader
。举个例子:
npm install jade-loader --save-dev
// webpack.config.js
loaders: {
...
{
test: /\.jade$/,
loader: 'jade'
}
}
plugins: [
new HtmlWebpackPlugin({
...
jade: 'path/to/yourfile.jade'
})
]
favicon
,值是一个路径:plugins: [
new HtmlWebpackPlugin({
favicon: 'path/to/my_favicon.ico'
})
然后在生成的html
中就有了一个link
标签:
<link rel="shortcut icon" href="example.ico">
html
文件进行压缩,默认是false
。html-webpack-plugin
内部集成了html-minifier
。因此,还可以对minify
进行配置,注意,虽然minify
支持BooleanObject
,但是不能直接这样写:minify: true
,这样会报错ERROR in TypeError: Cannot use 'in' operator to search for 'html5' in true
,使用时候必须给定一个{}
对象。具体配置如下:plugins: [
new HtmlWebpackPlugin({
...
minify: {
removeAttributeQuotes: true // 移除属性的引号
}
})
]
chunks: 允许我们只对页面添加部分chunks
,主要用于多入口文件的情况。当有多个入口文件时,打包后就会生成多个文件,那么chunks
选项用于设置想要使用的js文件,具体配置如下:
entry: {
index: path.resolve(__dirname, './src/index.js'),
vender: path.resolve(__dirname, './src/vender.js'),
main: path.resolve(__dirname, './src/main.js')
}
plugins: [
new htmlWebpackPlugin({
chunks: ['index', 'vender']
})
]
那么编译打包后将只包含如下两个文件:
<script type=text/javascript src="index.js"></script>
<script type=text/javascript src="vender.js"></script>
如果没有设置chunks
选项,那么默认是全部包含。
// 等价于上面的配置
excludeChunks: ['main.js']
chunks
被加入到html
文件前,允许控制chunks
应当如何被排序。允许的值:'none','auto','dependency',{function}
,默认值: 'auto'
。
false
,如果为true
,则以兼容xhtml
的模式引用文件。
- 由于inject参数不能被同时设置为
'head'和'body'
,因此,当有的打包后的脚本文件需要被添加到head标签,而另外的需要被添加到body标签中时,就需要手动向HTML模板注入脚本。
<head>
<script src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>
</head>
<body>
<% for(let k in htmlWebpackPlugin.files.chunks){ %>
<% if(k!=='main'){ %>
<script src="<%= htmlWebpackPlugin.files.chunks[k].entry %>"></script>
<% } %>
<% } %>
</body>
- 为了网页的加载性能,减少HTTP请求数,当有的打包后的脚本文件需要被内嵌到head标签中,而其余的需要以引用外部资源的方式添加到HTML模板中时,也需要手动向HTML模板注入脚本。
<head>
...
<script type="text/javascript" src="<%= compilation.assets[htmlWebpackPlugin.files.chunks.main.entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>"></script>
</head>
<body>
<% for(var k in htmlWebpackPlugin.files.chunks){ %>
<% if(k!=='main'){ %>
<script src="<%= htmlWebpackPlugin.files.chunks[k].entry %>"></script>
<% } %>
<% } %>
</body>
当开发一个多页面应用程序,那么我们就需要为不同的页面生成不同的HTML文件。通过向plugins
数组添加多个插件实例就可以实现:
module.exports = {
entry: 'index.js',
output: {
path: './dist',
filename: '[name].bundle.js'
},
plugins: [
new HtmlWebpackPlugin(), // 生成index.html
new HtmlWebpackPlugin({ // 生成test.html
filename: 'test.html',
template: './src/test.html'
})
]
}
// 不生效
<%= htmlWebpackPlugin.options.title %>
应该是webpack.config.js
的配置文件里面加了html-loader
,加了之后会正常解析html
文件作为模版,就会直接把<%= htmlWebpackPlugin.options.title %>
解析成字符串。如果有html-loader
,去掉就可以了。
[TOC]
entry
是配置模块的入口,可抽象成输入,Webpack
执行构建的第一步将从入口开始,搜寻及递归解析出所有入口依赖的模块。
context
参数用于配置基础目录,是绝对路径,用于从配置中解析入口起点和加载器。
Webpack
在寻找相对路径的文件时,会以context
为根目录,context
默认为执行启动Webpack
时所在的当前工作目录。如果想改变context
的默认配置,可以在配置文件里设置:
module.exports = {
context: path.resolve(__dirname, 'src')
}
注意:
context
必须是一个绝对路径的字符串。 除此之外,还可以通过在启动Webpack
时带上参数webpack --context
来设置context
。
const path = require('path');
module.exports = {
mode: 'none',
// 从当前目录下(运行webpack命令的目录)的src文件夹下寻找入口文件
context: path.resolve(__dirname, 'src'),
// 这里因为设置了context
// 所以是从当前目录下的src目录下查找index.js文件作为入口文件
entry: './index.js',
output: {
publicPath: __dirname + '/dist/', // js引用路径或者CDN地址
path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
filename: 'bundle.js'
}
};
需要注意:如果没有设置
context
配置项,webpack
也是默认从当前目录下查找的,因此,如果使用context
的话,建议加上第二个参数,即从当前目录下哪个目录下查找,或者直接不要这个配置项。
Entry
类型可以是以下三种中的一种或者相互组合:
string类型,具体配置如下:
module.exports = {
entry: './js/index.js',
output: {
// 将所有依赖的模块合并输出到一个叫bundle.js文件内
filename: 'bundle.js',
// 将输出的文件都放在dist目录下
path: path.resolve(__dirname, './dist')
}
};
array类型,具体配置如下:
module.exports = {
entry: ['./js/index.js', './js/index2.js'],
output: {
// 将所有依赖的模块合并输出到一个叫bundle.js文件内
filename: 'bundle.js',
// 将输出的文件都放在dist目录下
path: path.resolve(__dirname, './dist')
}
};
如果入口类型为数组的话,将会创建多个主入口,并且把数组中的js打包在一起到一个文件里面去。
object类型,具体配置如下:
module.exports = {
entry: {
'index': './js/index.js',
'index2': './js/index2.js'
},
output: {
filename: '[name].js', // [name]的值是entry的键值, 会输出多个入口文件
// 将输出的文件都放在dist目录下
path: path.resolve(__dirname, './dist')
}
};
Webpack
会为每个生成的Chunk
取一个名称,Chunk
的名称和Entry
的配置有关:
entry
是一个string或array
,就只会生成一个Chunk
,这时Chunk
的名称是main
;entry
是一个object
,就可能会出现多个Chunk
,这时Chunk
的名称是object
键值对中键的名称。假如项目里有多个页面需要为每个页面的入口配置一个 Entry ,但这些页面的数量可能会不断增长,则这时 Entry 的配置会受到到其他因素的影响导致不能写成静态的值。其解决方法是把 Entry 设置成一个函数去动态返回上面所说的配置,代码如下:
// 同步函数
entry: () => {
return {
a:'./pages/a',
b:'./pages/b',
}
};
// 异步函数
entry: () => {
return new Promise((resolve)=>{
resolve({
a:'./pages/a',
b:'./pages/b',
});
});
};
output
配置如何输出最终想要的代码。output
是一个object
,里面包含一系列配置项:
output.filename
配置输出文件的名称,为string
类型。如果只有一个输出文件,则可以将它写成静态不变的:
filename: 'bundle.js'
但是在有多个Chunk
要输出时,就需要借助模版和变量了。前面说到,Webpack
会为每个Chunk
取一个名称,所以我们可以根据Chunk
的名称来区分输出的文件名:
filename: '[name].js'
单入口配置如下:
{
entry: './js/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist')
}
}
如果有多个chunk
要输出时,就需要借助[name]
变量了,webpack
会为每个chunk
取一个名称,因此我们根据chunk
的名称来区分输出的文件名。如下:
filename: '[name].js'
多入口配置如下:
{
entry: {
index: './js/index.js',
index2: './js/index2.js'
}
output: {
filename: '[name].js', // [name]的值是entry的键值, 会输出多个入口文件
path: path.resolve(__dirname, './dist')
}
}
上面的配置,将会在当前目录下的dist目录中输出
index.js
和index2.js
。
代码里的[name]
代表用内置的name
变量去替换[name]
,这时我们可以将它看作一个字符串模块函数,每个要输出的Chunk
都会通过这个函数去拼接出输出的文件名称。除了name
之外,还有如下内置变量列表:
变量名 | 含义 |
---|---|
id | Chunk 的唯一标识,从0开始 |
name | Chunk 的名称 |
hash | Chunk 的唯一标识的 Hash 值 |
chunkhash | Chunk 内容的 Hash 值 |
其中hash和chunkhash 的长度是可指定的,[hash:8] 代表取8 位Hash 值,默认是20 位。hash 配置如下: |
entry: {
'index': './js/index.js',
'index2': './js/index2.js'
},
output: {
filename: '[name]_[hash:8].js', // [name]的值是entry的键值, 会输出多个入口文件
// 将输出的文件都放在dist目录下
path: path.resolve(__dirname, './dist')
}
上述配置,将会在当前目录下的
dist
目录中输出index_xxxx.js
和index2_xxxx.js
,其中xxxx
是hash
值的随机八位。
注意:
ExtractTextWebpackPlugin
插件是使用contenthash
来代表哈希值而不是chunkhash
,原因在于ExtractTextWebpackPlugin
提取出来的内容是代码内容本身,而不是由一组模块组成的Chunk
。
output.chunkFilename
用于配置无入口的Chunk
在输出时的文件名称。 chunkFilename
和上面的filename
非常类似,但chunkFilename
只用于指定在运行过程中生成的Chunk
在输出时的文件名称。常见的会在运行时生成 Chunk
场景有:使用CommonChunkPlugin
(webpack4已经废弃)、使用 import('path/to/module')
异步按需动态加载模块等时,一般这样的文件没有被列在entry
中。chunkFilename
支持和 filename
一致的内置变量。
在项目src目录下再新建common
文件夹,里面包含一个common.js
文件;代码如下:
function a() {
console.log('a.js');
}
module.exports = a;
然后在index.js
入口文件如下编写代码;使用异步方式引用a.js
,代码如下:
require.ensure(['./plugins/a.js'], function(require) {
var aModule = require('./common/a.js');
}, 'a');
然后在webpack
的output
配置添加chunkFilename
,配置如下:
module.exports = {
context: path.resolve(__dirname, ''),
entry: {
'index': './js/index.js',
'index2': './js/index2.js'
},
output: {
filename: '[name]_[hash:8].js', // [name]的值是entry的键值, 会输出多个入口文件
chunkFilename: '[name].min.js',
// 将输出的文件都放在dist目录下
path: path.resolve(__dirname, './dist')
}
};
其中上面的require.ensure()
API的第三个参数是给这个模块命名的,因此在webpack
配置完成后,继续打包下,会在dist
文件夹下打出a.min.js
文件了,这就是chunkFilename
的用途场景了。
output.path
配置输出文件存放在本地的目录,必须是string
类型的绝对路径,一般通过Node.js
的path
模块获取绝对路径,代码如下:
path: path.resolve(__dirname, './dist')
在复杂的项目里可能会有一些构建出的资源需要异步加载,加载这些异步资源需要对应的URL
地址。
output.publicPath
配置发布到线上资源的URL
前缀,为string
类型。 默认值是空字符串''
,即使用相对路径。
举个例子,将构建出的资源文件上传到
CDN
服务上,以利于加快页面的打开速度。配置代码如下:
filename:'[name]_[chunkhash:8].js'
publicPath: 'https://cdn.example.com/assets/'
这时发布到线上的HTML
在引入JavaScript
文件时就需要以下配置项:
<script src='https://cdn.example.com/assets/a_12345678.js'></script>
使用该配置项时要小心,稍有不慎将导致资源加载404
错误。
output.path和output.publicPath
都支持字符串模版,内置变量只有一个:hash
,代表一次编译操作的Hash
值。
Webpack
输出的部分代码块可能需要异步加载,而异步加载是通过JSONP
方式实现的。JSONP
的原理是:动态地向HTML
中插入一个<script src="url"></script>
标签去加载异步资源。
output.crossOriginLoading
则是用于配置这个异步插入的标签的 crossorigin
值。
script
标签的crossorigin
属性可以取以下值:
with credentials
,在加载此脚本资源时会带上用户的Cookies。通常用设置crossorigin
来获取异步加载的脚本执行时的详细错误信息。
当用Webpack
去构建一个可以被其他模块导入使用的库时,需要用到LibraryTarget 和 Library
。
它们通常搭配在一起使用。output.libraryTarget
是字符串的枚举类型,支持以下配置。
编写的库将通过var
被赋值给通过library
指定名称的变量。
假如配置了output.library='LibraryName'
,则输出和使用的代码如下:
// Webpack 输出的代码
var LibraryName = lib_code;
// 使用库的方法
LibraryName.doSomething();
假如output.library
为空,则将直接输出:lib_code
,其中lib_code
代指导出库的代码内容,是有返回值的一个自执行函数。
编写的库将通过CommonJS2
规范导出,输出和使用的代码如下:
// Webpack 输出的代码
module.exports = lib_code;
// 使用库的方法
require('library-name-in-npm').doSomething();
library-name-in-npm
是指模块被发布到Npm代码仓库时的名称。
CommonJS2和CommonJS
规范很相似,差别在于CommonJS
只能用exports
导出,而CommonJS2
在CommonJS
的基础上增加了module.exports
的导出方式。
在output.libraryTarget
为commonjs2
时,配置output.library
将没有意义。
编写的库将通过this
被赋值给通过library
指定的名称,输出和使用的代码如下:
// Webpack 输出的代码
this['LibraryName'] = lib_code;
// 使用库的方法
this.LibraryName.doSomething();
编写的库将通过window
赋值给通过library
指定的名称,即把库挂载到window
上,输出和使用的代码如下:
// Webpack 输出的代码
window['LibraryName'] = lib_code;
// 使用库的方法
window.LibraryName.doSomething();
编写的库将通过global
被赋值给通过library
指定的名称,即把库挂载到 global
上,输出和使用的代码如下:
// Webpack 输出的代码
global['LibraryName'] = lib_code;
// 使用库的方法
global.LibraryName.doSomething();
output.libraryExport
配置要导出的模块中哪些子模块需要被导出。它只有在 output.libraryTarget
被设置成commonjs 或者 commonjs2
时使用才有意义。
假如要导出的模块源代码是:
export const a=1;
export default b=2;
现在想让构建输出的代码只导出其中的a
,可以把output.libraryExport
设置成 a
,那么构建输出的代码和使用方法将变成如下:
// Webpack 输出的代码
module.exports = lib_code['a'];
// 使用库的方法
require('library-name-in-npm')===1;
rules
配置模块的读取和解析规则,通常用来配置Loader
。其类型是一个数组,数组里的每一项都描述了如何去处理部分文件。配置一项rules
时大致通过以下方式:
test、include、exclude
三个配置项来选中Loader
要应用规则的文件。use
配置项来应用Loader
,可以只应用一个Loader
或者按照从后往前的顺序应用一组Loader
,同时还可以分别给 Loader
传入参数。Loader
的执行顺序默认是从右到左执行,通过 enforce
选项可以让其中一个Loader
的执行顺序放到最前或者最后。举个例子:
module: {
rules: [
{
// 命中 JavaScript 文件
test: /\.js$/,
// 用babel-loader转换JavaScript文件
// ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度
use: ['babel-loader?cacheDirectory'],
// 只命中src目录里的js文件,加快 Webpack 搜索速度
include: path.resolve(__dirname, 'src')
},
{
// 命中 SCSS 文件
test: /\.scss$/,
// 使用一组 Loader 去处理 SCSS 文件。
// 处理顺序为从后到前,即先交给 sass-loader 处理,再把结果交给 css-loader,最后再给 style-loader。
use: ['style-loader', 'css-loader', 'sass-loader'],
// 排除 node_modules 目录下的文件
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 对非文本文件采用file-loader加载
test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
use: ['file-loader']
},
]
}
在Loader
需要传入很多参数时,我们还可以通过一个Object
来描述,例如在上面的babel-loader
配置中有如下代码:
use: [
{
loader:'babel-loader',
options:{
cacheDirectory:true,
},
// enforce: 'post'的含义是把该`Loader`的执行顺序放到最后
// enforce的值还可以是pre,代表把Loader的执行顺序放到最前面
enforce:'post'
},
// 省略其它 Loader
]
上面的例子中test、include、exclude
这三个命中文件的配置项只传入了一个字符串或正则,其实它们还都支持数组类型,使用如下:
{
test: [
/\.jsx?$/,
/\.tsx?$/
],
include:[
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'tests'),
],
exclude:[
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'bower_modules'),
]
}
数组里的每项之间是或的关系,即文件路径符合数组中的任何一个条件就会被命中。
noParse
配置项可以让Webpack
忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能。原因是一些库例如jQuery、ChartJS 庞大又没有采用模块化标准,让Webpack
去解析这些文件耗时又没有意义。
noParse
是可选配置项,类型需要是RegExp、[RegExp]、function
中的一种。
例如想要忽略掉jQuery、ChartJS
,可以使用如下代码:
// 使用正则表达式
noParse: /jquery|chartjs/
// 使用函数,从Webpack 3.0.0开始支持
noParse: (content)=> {
// content 代表一个模块的文件路径
// 返回 true or false
return /jquery|chartjs/.test(content);
}
注意:被忽略掉的文件里不应该包含
import、require、define
等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。
因为Webpack
是以模块化的JavaScript
文件为入口,所以内置了对模块化 JavaScript
的解析功能,支持AMD、CommonJS、SystemJS、ES6
。
parser
属性可以更细粒度的配置哪些模块语法要解析、哪些不解析。和noParse
配置项的区别在于:parser
可以精确到语法层面,而noParse
只能控制哪些文件不被解析。parser
使用如下:
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
parser: {
amd: false, // 禁用 AMD
commonjs: false, // 禁用 CommonJS
system: false, // 禁用 SystemJS
harmony: false, // 禁用 ES6 import/export
requireInclude: false, // 禁用 require.include
requireEnsure: false, // 禁用 require.ensure
requireContext: false, // 禁用 require.context
browserify: false, // 禁用 browserify
requireJs: false, // 禁用 requirejs
}
},
]
}
Webpack
在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve
配置Webpack
如何寻找模块对应的文件。Webpack
内置JavaScript
模块化语法解析功能,默认会采用模块化标准里约定的规则去寻找,但我们也可以根据自己的需要修改默认的规则。
resolve.alias
配置项通过别名来将原导入路径映射成一个新的导入路径。例如使用以下配置:
// Webpack alias 配置
resolve:{
alias:{
components: './src/components/'
}
}
当通过import Button from 'components/button'
导入时,实际上被 alias
等价替换成了import Button from './src/components/button'
。
以上alias
配置的含义是:把导入语句里的components
关键字替换成 ./src/components/
。
这样做可能会命中太多的导入语句,alias
还支持$
符号来缩小范围到只命中以关键字结尾的导入语句:
resolve:{
alias:{
'react$': '/path/to/react.min.js'
}
}
react$
只会命中以react
结尾的导入语句,即只会把import 'react'
关键字替换成import '/path/to/react.min.js'
。
有一些第三方模块会针对不同的环境提供几份代码。例如分别提供采用了ES5和ES6
的两份代码,这两份代码的位置写在了package.json
文件里,代码如下:
{
"jsnext:main": "es/index.js", // 采用ES6语法的代码入口文件
"main": "src/index.js" // 采用ES5语法的代码入口文件
}
Webpack
会根据mainFields
的配置去决定优先采用哪份代码,mainFields
默认如下:
module.exports = {
//...
resolve: {
mainFields: ['browser', 'main']
}
};
Webpack
会按照数组里的顺序在package.json
文件里寻找,只会使用找到的第一个文件。假如我们想优先采用ES6
的代码,则可以如下配置:
mainFields: ['jsnext:main', 'browser', 'main']
在导入语句没带文件后缀时,Webpack
会自动带上后缀去尝试访问文件是否存在。resolve.extensions
用于配置在尝试过程中用到的后缀列表,默认是:
module.exports = {
//...
resolve: {
extensions: ['.js', '.json']
}
};
也就是说,当遇到
require('./data')
这样的导入语句时,Webpack
会先寻找./data.ts
文件,如果该文件不存在,就去寻找./data.js
文件,如果该文件不存在,就去寻找./data.json
文件,如果还是找不到,就报错。
假如我们想让Webpack
优先使用目录下的TypeScript
文件,则可以这样配置:
extensions: ['.ts', '.js', '.json']
resolve.modules
配置Webpack
去哪些目录下寻找第三方模块,默认只会去node_modules
目录下寻找。有时项目里会有一些模块被其他模块大量依赖和导入,由于其他模块的位置不定,针对不同的文件都要计算被导入的模块文件的相对路径,这个路径有时会很长,就像import '../../../components/button'
,这就可以利用resolve.modules
配置项进行优化。假如那些大量被导入的模块都在./src/components
目录下,则进行如下配置即可:
resolve: {
modules: ['./src/components', 'node_modules']
}
之后可以简单的通过
import 'button'
导入。
resolve.descriptionFiles
配置描述第三方模块的文件名称,默认为:package.json
,具体配置如下:
module.exports = {
//...
resolve: {
descriptionFiles: ['package.json']
}
};
resolve.enforceExtension
默认值为false
,如果被配置为true
,则所有导入语句都必须带文件后缀,例如设置之前import './foo'
能正常工作,开启后就必须写成import './foo.js'
。
module.exports = {
//...
resolve: {
enforceExtension: false
}
};
resolve.enforceModuleExtension
和resolve.enforceExtension
的作用类似,但是enforceModuleExtension
只对node_modules
下的模块生效。enforceModuleExtension
通常搭配enforceExtension
使用,在enforceExtension: true
时,因为安装的第三方模块中大多数导入语句都没有带文件的后缀,所以这时候通过设置enforceModuleExtension: false
来兼容第三方模块。
Plugin
用于扩展Webpack
的功能,各种各样的Plugin
几乎让Webpack
可以做任何构建相关的事情。
Plugin
的配置很简单,plugins
配置项接受一个数组,数组里每一项都是一个要使用的Plugin
的实例,Plugin
需要的参数通过构造函数传入。
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
module.exports = {
plugins: [
// 所有页面都会用到的公共代码提取到 common 代码块中
new CommonsChunkPlugin({
name: 'common',
chunks: ['a', 'b']
}),
]
};
Plugin
使用的难点在于:掌握Plugin
本身提供的配置项,而不是如何在Webpack
中接入Plugin
。
要配置DevServer
,除了可以在配置文件里通过devServer
传入参数,还可以通过命令行参数传入。需要注意的是: 只有在通过DevServer
启动Webpack时,配置文件里的devServer
才会生效,因为这些参数所对应的功能都是DevServer
提供的,Webpack
本身并不认识devServer
配置项。
devServer.hot
配置模块热替换功能。DevServer
的默认行为是:在发现源代码被更新后通过自动刷新整个页面来做到实时预览,开启模块热替换功能后,将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览。
DevServer
的实时预览功能依赖一个注入页面里的代理客户端,去接收来自DevServer
的命令并负责刷新网页的工作。devServer.inline
用于配置是否将这个代理客户端自动注入将运行在页面中的Chunk
里,默认自动注入。DevServer
会根据我们是否开启inline
来调整它的自动刷新策略。
DevServer
会在构建变化后的代码时通过代理客户端控制网页刷新;DevServer
将无法直接控制要开发的网页。这时它会通过iframe
的方式去运行要开发的网页。在构建完变化后的代码时,会通过刷新iframe
来实现实时预览,但这时我们需要通过http://localhost:8080/webpack-dev-server/
实时预览自己的网页。如果想使用DevServer
去自动刷新网页实现实时预览,最方便的方法是直接开启 inline
。
devServer.historyApiFallback
用于方便地开发使用了HTML5 History API
的单页应用。这类单页应用要求服务器在针对任何命中的路由时,都返回一个对应的HTML
文件。例如:在访问http://localhost/user
和http://localhost/home
时都返回index.html
文件,浏览器端的javascript
代码会从url
里解析出当前页面的状态,显示对应的界面。
配置
historyApiFallback
最简单的做法是:
historyApiFallback: true
上述配置会导致任何请求都会返回
index.html
文件,这只能用于只有一个HTML
文件的应用。如果应用由多个单页应用组成,则需要DevServer
根据不同的请求返回不同的HTML
文件,配置如下:
historyApiFallback: {
// 使用正则匹配命中路由
rewrites: [
// /user开头的都返回user.html
{from: /^\/user/, to: '/user.html'},
// /game开头的都返回user.html
{from: /^\/game/, to: '/game.html'},
// 其他的都返回index.html
{from: /./, to: '/index.html'},
]
}
devServer.contentBase
配置DevServerHTTP
服务器的文件根目录。在默认情况下为当前的执行目录,通常是项目根目录,所以在一般情况下不需要设置它,除非有额外的文件需要被DevServer
服务。例如:想将项目根目录下的public
目录设置成DevServer
服务器的文件根目录,则可以这样配置:
devServer: {
contentBase: path.join(__dirname, 'public')
}
需要注意的是:
DevServer
服务器通过HTTP
服务暴露文件的方式分为两类:
Webpack
构建出的结果,由于构建出的结果交给了DevServer
,所以我们在使用DevServer
时,会在本地找不到构建出的文件。
contentBase
只能用来配置暴露本地文件的规则,可以通过contentBase: false
来关闭暴露本地文件。
devServer.headers
配置项可以在HTTP
响应中注入一些HTTP
响应头,使用如下:
devServer: {
headers: {
'X-foo': 'bar'
}
}
devServer.host
配置项用于配置DevServer
服务监听的地址。host
的默认值是127.0.0.1
,即只有本地可以访问DevServer
的HTTP
服务。如果想让局域网中的其他设备访问自己的本地服务,可以这样配置:
module.exports = {
//...
devServer: {
host: '0.0.0.0'
}
};
命令行使用方式:
webpack-dev-server --host 0.0.0.0
devServer.port
配置项用于配置DevServer
服务监听的端口,默认使用8080
端口。
devServer.allowedHosts
配置一个白名单列表,只有HTTP
请求的HOST
在列表里才正常返回,配置如下:
module.exports = {
//...
devServer: {
allowedHosts: [
// 匹配单个域名
'host.com',
'subdomain.host.com',
'subdomain2.host.com',
'host2.com',
// host.com和所有的子域名*.host.com都将匹配
'.host.com'
]
}
};
devServer.disableHostCheck
配置项用于配置是否关闭用于DNS
重新绑定的HTTP
请求的HOST
检查。DevServer
默认只接收来自本地的请求,关闭后可以接收来自任意HOST
的请求。通常用于搭配--host 0.0.0.0
使用,因为想让其他设备访问自己的本地服务,但访问时是直接通过IP地址访问而不是通过HOST
访问,所以需要关闭HOST
检查。
DevServer
默认使用HTTP
服务,也可以使用HTTPS
服务。配置如下:
module.exports = {
//...
devServer: {
https: true
}
};
DevServer
会自动为我们生成一份HTTPS
证书,如果想使用自己的证书,可以这样配置:
module.exports = {
//...
devServer: {
https: {
key: fs.readFileSync('/path/to/server.key'),
cert: fs.readFileSync('/path/to/server.crt'),
ca: fs.readFileSync('/path/to/ca.pem'),
}
}
};
devServer.clientLogLevel
配置客户端的日志等级,这会影响我们在浏览器开发者工具控制台里看到的日志内容。clientLogLevel
是枚举类型,可取如下值之一:none、error、warning、info
。默认为info
级别,即输出所有类型的日志,设置为none
时可以不输出任何日志。
devServer.compress
配置是否启用Gzip
压缩,默认为false
。
devServer.open
用于在DevServer
启动且第一次构建完成时,自动用我们的系统的默认浏览器去打开要开发的网页。还可以使用devServer.openPage
配置项来打开指定URL
的网页。
devServer.overlay
配置在浏览器中显示编译的错误或者警告。
Target
配置项可以让Webpack
构建出针对不同运行环境的代码。
在设置
target: 'node'
时,在源代码中导入Node.js
原生模块的语句require('fs')
将会保留,fs
模块的内容不会被打包到Chunk
中。
devtool
配置Webpack
如何生成Source Map
,默认值是false
,即不生成Source Map
,若想为构建出的代码生成Source Map
以方便调试,可以这样配置:
module.export = {
devtool: 'source-map'
}
watch
配置项用来配置Webpack
的监听模式,支持监听文件更新,在文件发生变化时重新编译。在使用Webpack
时,监听模式默认是关闭的,打开需进行如下配置:
module.exports = {
//...
watch: true
};
在使用
DevServer
时,监听模式默认开启。除此之外,还提供了watchOptions
配置项来更灵活地控制监听模式,具体配置如下:
module.exports = {
// 只有在开启监听模式时,watchOptions才有意义
// 默认为false,也就是不开启
watch: true,
// 监听模式运行时的参数
// 在开启监听模式时才有意义
watchOptions: {
// 不监听的文件或文件夹,支持正则匹配
// 默认为空
ignored: /node_modules/,
// 监听到变化发生后等300ms再去执行动作,截流
// 防止文件更新太快而导致重新编译频率太快。默认为300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停地询问系统指定文件有没有变化实现的
// 默认每秒询问1000次
poll: 1000
}
};
Externals
用来告诉Webpack
要构建的代码中使用了哪些不用被打包的模块,也就是说这些模板是外部环境提供的,Webpack
在打包时可以忽略它们。
有些
Javascript
运行环境可能内置了一些全局变量或者模块,例如:在HTML
的HEAD
标签通过以下代码引入jQuery
:
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
这时,全局变量
jQuery
就会被注入网页的Javascript
运行环境里。
如果想在使用模块化的源代码里导入和使用jQuery
,可能需要这样:
import $ from 'jquery';
$('.my-element').animate(/* ... */);
构建后我们会发现输出的Chunk里包含的jQuery库的内容,这导致jQuery库出现了两次,浪费加载流量,最好是Chunk里不会包含jQuery库的内容。
Externals
配置项就是用于解决这个问题的。通过Externals
可以告诉Webpack
在Javascript
运行环境中已经内置了哪些全局变量,不用将这些全局变量打包到代码中而是直接使用它们。具体配置如下:
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
resolveLoader
配置项用来告诉Webpack如何去寻找Loader,因为在使用Loader时是通过其包名称去引用的,Webpack需要根据配置的Loader包名去找到Loader的实际代码,以调用Loader去处理源文件。默认配置如下:
module.exports = {
//...
resolveLoader: {
// 去哪个目录下寻找Loader
modules: [ 'node_modules' ],
// 入口文件的后缀
extensions: [ '.js', '.json' ],
// 指明入口文件位置的字段
mainFields: [ 'loader', 'main' ]
}
};
[TOC]
模块化是指将一个复杂的系统分解为多个模块以方便编码。
CommonJS
是一种被广泛使用的javascript
模块化规范,其核心**是:通过require
方法来同步加载依赖的其他模块,通过module.exports
导出需要暴露的接口。CommonJS
规范的流行得益于Node.js
采用了这种方式,后来这种方式被引入到了网页开发中。
// 导入
const moduleA = require('./moduleA');
// 导出
module.exports = moduleA.someFunc;
CommonJS
的优点:
CommonJS
规范。
CommonJS
的缺点:这样的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5。
AMD
也是一种javascript
模块化规范,与CommonJS
最大的不同在于:它采用了异步的方式去加载依赖的模块。AMD
规范主要用于解决针对浏览器环境的模块化问题,最具代表性的实现是requirejs
。
// 定义一个模块
define('module', ['dep'], function(dep) {
return exports;
});
// 导入和使用
require(['module'], function(module) {
});
AMD
的优点:
AMD
的缺点:在javascript运行环境没有原生支持AMD,需要先导入实现了AMD
的库后才能正常使用。
ES6模块化是国际标准化组织ECMA提出的javascript模块化规范,它在语言层面上实现了模块化。浏览器厂商和Node.js都宣布要原生支持该规范,它将逐渐取代CommonJS和AMD规范,成为浏览器和服务器通用的模块化解决方案。
// 导入
improt React, {Component} from 'react';
// 导出
export function hello() {};
export default {
// ...
}
ES6
模块化虽然是终极模块化方案,但是它的缺点在于:目前无法直接运行在大部分javascript运行环境下,必须通过工具转换成标准的ES5后才能正常运行。
从
Webpack 2
版本开始,Webpack已经内置了对ES6、CommonJS、AMD模块化语句的支持。
npm i -g webpack webpack-cli
// 安装最新的稳定版本
npm i -D webpack webpack-cli
// 安装指定版本
npm i -D webpack@<version> webpack-cli@<version>
// 安装最新的体验版本
npm i -D webpack@beta webpack-cli@beta
特别注意:一般不推荐全局安装
webpack
,原因是可防止不同的项目因依赖不同版本的webpack
而导致冲突。
在编写webpack代码之前,先搭建如下的项目目录:
webpack-demo # 项目名
| |--- dist # 打包后生成的文件目录
| |--- node_modules # 所有的依赖包
| |--- src # 项目源码目录
| | |-- js # js文件目录
| | |-- styles # css文件目录
| |--- webpack.config.js # webpack配置文件
| |--- index.html # html文件
| |--- .gitignore
| |--- README.md
| |--- package.json
// 安装loader
npm i -D style-loader css-loader
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader?minimize']
}
]
}
use
属性的值是一个由Loader
名称组成的数组,Loader
的执行顺序是由后到前的;Loader
都可以通过URL querystring
的方式传入参数,例如上述代码中'css-loader?minimize'
中的minimize
就是告诉css-loader
要开启css
压缩。向
loader
传入属性的方式除了可以通过URL querystring
实现,还可以通过Object
实现,配置如下:
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader',
{
loader: 'css-loader',
// 设置loader相关参数
options: {
minimize: true
}
}
]
}
]
}
style-loader
的工作原理是:将css
的内容用javascript
里的字符串存储起来,在网页执行javascript
时通过DOM
操作,动态地向HTML head
标签里插入HTML style
标签。
Plugin
是用来扩展webpack
功能的,通过在构建流程里注入钩子实现,为webpack
带来了很大的灵活性。
如果想要使用一个插件,我们只需要require()
它,然后把它添加到plugins
数组中。我们可以在一个配置文件中因为不同的目的多次使用用一个插件,因此我们可以使用new操作符来创建它的实列。
mini-css-extract-plugin
是将css提取到单独文件中的插件,具体配置如下:
npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [
new MiniCssExtractPlugin({
// 指定提取出来的css文件的名称
filename: "[name]_[contenthash:8].css"
})
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
}
}
[name]
代表文件的名称,[contenthash:8]
代表根据文件内容算出的8位Hash值。
DevServer
会启动一个HTTP
服务器用于服务网页请求,同时会帮助启动Webpack
,并接收Webpack
发出的文件变更信号,通过WebSocket
协议自动刷新网页做到实时预览。
npm i -D webpack-dev-server
需要注意的是:
DevServer
会将Webpack
构建出的文件保存在内存中,在要访问输出的文件时,必须通过HTTP
服务访问。
Webpack
在启动时可以开启监听模式,默认是关闭的,之后Webpack
会监听本地文件系统的变化,在发生变化时重新构建出新的结果。Webpack
默认关闭监听模式,我们可以在启动Webpack
时通过如下命令来开启监听模式:
webpack --watch
通过DevServer
启动的Webpack会开启监听模式,当发生变化时重新执行构建,然后通知DevServer
。DevServer
会让Webpack
在构建出的javascript
代码里注入一个代理客户端用于控制网页,网页和DevServer
之间通过WebSocket
协议通信,以方便DevServer
主动向客户端发送命令。DevServer
在收到来自Webpack
的文件变化通知时,通过注入的客户端控制网页刷新。
如果尝试修改index.html
文件并保存,则我们会发现这并不会触发以上机制,导致这个问题的原因是:Webpack
在启动时会以配置里的entry
为入口去递归解析出entry
所依赖的文件,只有entry
本身和依赖的文件才会被Webpack
添加到监听列表里。而index.html
文件是脱离了javascript
模块化系统的,所以Webpack
不知道它的存在。但是要完成上面的实时预览,html
页面的bundle.js
要直接引入即可,如下html
页面代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>
为什么http://localhost:8080/bundle.js
这样也能访问的到呢?原因是:DevServer
会将webpack
构建出的文件保存在内存中,DevServer
不会理会webpack.config.js
里配置的output.path
属性的。
除了上面介绍的改动本地入口文件或依赖文件后,会自动打包,然后会自动刷新浏览器即可看到更新效果外,我们还可以使用模块热替换技术,
模块热替换技术能做到在不重新加载整个网页的情况下,通过将已更新的模块替换旧模块,它默认是关闭的,要开启模块热替换,我们只需在启动DevServer
时带上--inline
参数即可。如下命令:
webpack-dev-server --inline 或
webpack-dev-server --inline --hot
webpack-dev-server
有如下两种启动模式:
在浏览器中运行javascript
代码都是编译器输出的代码,但是如果在代码中碰到一个bug
的时候,我们不好调式,因此我们需要Source Map
来映射到源代码上,Webpack
支持生成Source Map
,只需在启动时带上--devtool source-map
参数即可;如下命令:
webpack-dev-server --inline --hot --devtool source-map
注意:每次运行如上命令,感觉非常长,因此我们可以在项目的根目录的
package.json
文件的scripts
中添加如下配置:
"scripts": {
"dev": "webpack-dev-server --devtool source-map --hot --inline"
}
加上如上配置后,我们只需要在命令行中运行npm run dev
即可;
其他配置常见的选项:
因此在项目中scripts经常会做如下配置:
"scripts": {
"dev": "webpack-dev-server --progress --colors --devtool source-map --hot --inline",
"build": "webpack --progress --colors"
}
这样的话,打包的时候会显示打包进度。
也可以在
webpack.config.js
中添加如下配置:
// 开启Source Map
devtool: 'cheap-module-source-map'
随着互联网的发展,一个网页需要承载的功能越来越多。采用单页应用作为前端架构的网站会面临着网页需要加载的代码量很大的问题,因为许多功能都被集中做到了一个HTML
里。这会导致网页加载缓慢、交互卡顿,使用户体验非常糟糕。
导致这个问题的根本原因在于:一次性加载所有功能对应的代码,但其实用户在每个阶段只可能使用其中一部分功能。所以解决以上问题的方法就是用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载。
在为单页应用做按需加载优化时,一般采用以下原则:
Chunk
,按需加载对应的Chunk
。Chunk
中,以降低用户能感知的网页加载时间。Chart.js
去画图表、依赖flv.js
去播放视频的功能点,可再对其进行按需加载。被分割出去的代码的加载需要一定的时机去触发,即当用户操作到了或者即将操作到对应的功能时再去加载对应的代码。 被分割出去的代码的加载时机需要开发者自己去根据网页的需求去衡量和确定。
由于被分割出去进行按需加载的代码在加载的过程中也需要耗时,所以可以预估用户接下来可能会进行的操作,并提前加载好对应的代码,从而让用户感知不到网络加载时间。
Webpack
实现按需加载Webpack
内置了强大的分割代码的功能去实现按需加载,实现起来非常简单。举个例子,现在需要做这样一个进行了按需加载优化的网页:
main.js
文件,网页会展示一个按钮,在main.js
文件中只包含监听按钮事件和加载按需加载的代码。show.js
文件,在加载成功后再执行 show.js
里的函数。其中
main.js
文件内容如下:
window.document.getElementById('btn').addEventListener('click', function () {
// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
import(/* webpackChunkName: "show" */ './show').then((show) => {
show('Webpack');
})
});
show.js
文件内容如下:
module.exports = function (content) {
window.alert('Hello ' + content);
};
代码中最关键的一句是:
import(/* webpackChunkName: "show" */ './show')
Webpack
内置了对import(*)
语句的支持,当Webpack
遇到了类似的语句时会这样处理:
./show.js
为入口新生成一个Chunk
;import
所在的语句时才会去加载由Chunk
对应生成的文件。import
返回一个Promise
,当文件加载成功时可以在Promise
的then
方法中获取到show.js
导出的内容。在使用import()
分割代码后,浏览器要支持Promise API
才能让代码正常运行,因为import()
返回一个Promise
,它依赖Promise
。对于不原生支持 Promise
的浏览器,可以注入Promise polyfill
。
/* webpackChunkName: "show" */
的含义是:为动态生成的Chunk
赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的Chunk
的名称,则其默认的名称将会是[id].js
。 /* webpackChunkName: "show" */
是在Webpack3
中引入的新特性,在Webpack3
之前是无法为动态生成的Chunk
赋予名称的。
为了正确输出在/ webpackChunkName: “show” /
中配置的ChunkName
,还需要配置下Webpack
,具体配置如下:
module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 为从entry中配置生成的Chunk配置输出文件的名称
filename: '[name].js',
// 为动态加载的Chunk配置输出文件的名称
chunkFilename: '[name].js',
}
};
其中,最关键的一行是chunkFilename: '[name].js'
,,它专门指定动态生成的 Chunk
在输出时的文件名称。如果没有这一行,则分割出的代码的文件名称将会是 [id].js
。
ReactRouter
在实战中,不可能会有上面那么简单的场景,接下来举一个实战中的例子:对采用了 ReactRouter
的应用进行按需加载优化。这个例子由一个单页应用构成,这个单页应用由两个子页面构成,通过ReactRouter
在两个子页面之间切换和管理路由。
这个单页应用的入口文件main.js
如下:
import React, { PureComponent, createElement } from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';
/**
* 异步加载组件
* @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
* @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件
*/
function getAsyncComponent(load) {
return class AsyncComponent extends PureComponent {
componentDidMount() {
// 在高阶组件 DidMount 时才去执行网络加载步骤
load().then(({default: component}) => {
// 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
this.setState({
component,
})
});
}
render() {
const {component} = this.state || {};
// component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
return component ? createElement(component) : null;
}
}
}
// 根组件
function App() {
return (
<HashRouter>
<div>
<nav>
<Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
</nav>
<hr/>
<Route exact path='/' component={PageHome}/>
<Route path='/about' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-about' */'./pages/about')
)}
/>
<Route path='/login' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-login' */'./pages/login')
)}
/>
</div>
</HashRouter>
)
}
// 渲染根组件
render(<App/>, window.document.getElementById('app'));
以上代码中最关键的部分是getAsyncComponent
函数,它的作用是配合 ReactRouter
去按需加载组件,具体含义请看代码中的注释。
由于以上源码需要通过Babel
去转换后才能在浏览器中正常运行,需要在Webpack
中配置好对应的babel-loader
,源码先交给babel-loader
处理后再交给 Webpack
去处理其中的import(*)
语句。
但这样做后你很快会发现一个问题:Babel
报出错误说不认识import(*)
语法。 导致这个问题的原因是:import(*)
语法还没有被加入到在使用ES6
语言中提到的 ECMAScript
标准中去,为此我们需要安装一个Babel
插件babel-plugin-syntax-dynamic-import
,并且将其加入到.babelrc
中去:
{
"presets": [
"env",
"react"
],
"plugins": [
"syntax-dynamic-import"
]
}
执行Webpack
构建后,你会发现输出了三个文件:
main.js
:执行入口所在的代码块,同时还包括PageHome
所需的代码,因为用户首次打开网页时就需要看到PageHome
的内容,所以不对其进行按需加载,以降低用户能感知到的加载时间;page-about.js
:当用户访问/about
时才会加载的代码块;page-login.js
:当用户访问/login
时才会加载的代码块。同时我们还会发现,page-about.js和page-login.js
这两个文件在首页是不会加载的,而是会在当你切换到了对应的子页面后文件才会开始加载。
[TOC]
Babel
是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。
如果在CLI(command-line interface
命令行界面)使用babel的话,请安装babel-cli
:
# 全局安装
$ npm install -g babel-cli
如果想结合node.js
来使用的话,需要安装babel-core
:
$ npm install -g babel-core
babel6里并没有默认的转换规则,所以你安装了如上两项,用babel运行你的文件会发现并没有什么变化,因此我们需要安装所需插件,并在.babelrc
文件做一些设置:
$ npm install -D babel-plugin-transform-es2015-arrow-functions
同时在.babelrc
文件添加:
{
"plugins": ["transform-es2015-arrow-functions"]
}
当然还有很多细节我们不可能一点点全部去安装,我们如果想要转换某些特性的话,可以去安装某个版本的预置,
babel
可以去向下兼容:
// 把ES2015(即ES6)编译成ES5(目前已经废弃)
$ npm install -D babel-preset-es2015
// 在.babelrc文件中添加:
{
"presets": ["es2015"]
}
如果想包含所有javascript版本的话:
npm install -D babel-preset-env
// 在.babelrc文件中添加
{
"presets": ["env"]
}
配置项目所支持浏览器所需的
polyfill
和transform
。只编译所需的代码会使你的代码包更小。
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
上面的例子只包含了支持每个浏览器最后两个版本和safari大于等于7版本所需的
polyfill
和代码转换。我们使用browserslist
来解析这些信息,所以你可以使用 browserslist 支持的有效的查询格式。
同样,如果你目标开发
Node.js
而不是浏览器应用的话,你可以配置babel-preset-env
仅包含特定版本所需的polyfill
和transform
:
{
"presets": [
["env", {
"targets": {
"node": "6.10"
}
}]
]
}
方便起见,你可以使用
"node": "current"
来包含用于运行Babel
的Node.js
最新版所必需的polyfills
和transforms
。
{
"presets": [
["env", {
"targets": {
"node": "current"
}
}]
]
}
插件中每个访问者都有排序问题。这意味着如果两次转译都访问相同的程序节点,则转译将按照plugin 或 preset 的规则进行排序然后执行。
Plugin
会运行在Preset
之前;Plugin
会从第一个开始顺序执行;Preset
的顺序则刚好相反(从最后一个逆序执行)。{
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}
# 将先执行 transform-decorators-legacy,再执行 transform-class-properties
# 一定要记得 preset 的顺序是反向的。举个例子:
{
"presets": [
"es2015",
"react",
"stage-0"
]
}
# 按以下顺序运行: stage-0,react,最后es2015
这主要是为了保证向后兼容,因为大多数用户会在stage-0
之前列出es2015
。
babel-plugin-transform-runtime
插件// 源代码如下
let obj = {name: 'lisi'};
let obj2 = {age: 23};
let obj3 = Object.assign({}, obj, obj2);
console.log(obj3);
// 对Object.assign进行编译
// 没有配置babel-plugin-transform-runtime
'use strict';
var obj = { name: 'lisi' };
var obj2 = { age: 23 };
var obj3 = Object.assign({}, obj, obj2);
console.log(obj3);
// 配置babel-plugin-transform-runtime
'use strict';
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 = { name: 'lisi' };
var obj2 = { age: 23 };
var obj3 = (0, _assign2.default)({}, obj, obj2);
console.log(obj3);
# 安装
npm install -D babel-polyfill
使用方法:
特别说明:第三种方法适用于使用webpack构建的情况,加入到webpack配置文件(webpack.config.js)的entry项中。
两种方式的原理:
ECMAScript
,其原理是:当运行环境中并没有实现的一些方法,babel-polyfill
中会给做兼容。es6
编译成es5
去运行,前端可以使用es6
的语法来写,最终浏览器上运行的是es5
。两种方式的优缺点:
prototype
上添加方法来实现,比如运行环境中不支持Array-prototype.find
,引入polyfill
,前端就可以放心的在代码里用es6的语法来写;但是这样会造成全局空间污染。比如像Array-prototype.find
就不存在了,还会引起版本之前的冲突。不过即便是引入babel-polyfill
,也不能全用,代码量比较大。babel-runtime/core-js/promise
来获取promise,或者通过babel-plugin-transform-runtime
自动重写你的promise。但是它不会模拟内置对象原型上的方法,比如Array-prototype.find,就没法支持了,如果运行环境不支持es6,代码里又使用了find方法,就会出错,因为es5并没有这个方法。babel-polyfill 与 babel-runtime 的最大区别在于:babel-polyfill改造目标浏览器,让你的浏览器拥有本来不支持的特性;babel-runtime改造你的代码,让你的代码能在所有目标浏览器上运行,但不改造浏览器。
.babelrc
Babel的配置文件是 .babelrc
,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件。babel 会自动读取 .babelrc
里的配置并应用到编译中。
该文件用来设置转码规则和插件,基本格式如下:
{
"presets": [],
"plugins": []
}
presets
字段设定转码规则,官方提供以下的规则集,你可以根据需要安装:
ES2015转码规则
$ npm install --save-dev babel-preset-es2015
react转码规则
$ npm install --save-dev babel-preset-react
# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个,其中0功能最全
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3
然后,将这些规则加入 .babelrc
:
{
"presets": [
"es2015",
"react",
"stage-2"
],
"plugins": []
}
注意: 所有Babel工具和模块的使用,都必须先写好 .babelrc
。
需要注意的是:
babel-preset-env
等同于babel-preset-latest
,同时又等同于babel-preset-es2015
,babel-preset-es2016
和babel-preset-es2017
三者的集合。
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["last 1 Chrome versions"] //表示只想支持最新版本的Chrome
}
}]
]
}
最新版本的
Chrome
已经支持箭头函数、class、const,所以 babel 在编译过程中,不会编译它们。这也是为什么我们把@babel/preset-env
称为 JavaScript 的Autoprefixer
。
Babel提供 babel-cli
工具,用于命令行转码。
安装命令如下:
// 局部安装
npm install -D babel-cli
// 全局安装
npm isntall babel-cli -g
基本用法:
# 将转码结果输出到标准输出(输出到命令行窗口)
$ babel example.js
# 局部安装使用
# 转码结果写入一个新的文件,--out-file 或 -o 参数指定输出文件
node_module/.bin/babel example.js --out-file compiled.js 或者
npx babel example.js --out-file compiled.js
# 全局安装使用
babel example.js --out-file compiled.js
# 或者
babel example.js -o compiled.js
# 整个目录转码,使用--out-dir 或 -d 参数指定输出目录
babel src --out-dir lib
或者
$ babel src -d lib
# 使用-s参数来生成source map文件
$ babel src -d lib -s
在上面给出的转码命令是在全局环境下利用Babel进行转码。也就是说,如果项目要运行,全局环境必须有Babel,这就导致项目对环境产生了依赖。另一方面,这样做也无法支持不同项目使用不同版本的Babel。
尽管你可以把
Babel CLI
全局安装在你的机器上,但是在项目中安装会更好。
有两个主要的原因:
Babel
并允许你有选择的更新。对这一问题,相应的解决办法是将babel-cli
安装在项目中。然后,在 package.json
文件中的scripts
字段中加入:
"scripts": {
"build": "babel src -d lib"
}
要对项目文件进行转码的时候,就执行下面的命令:
$ npm run build
对于项目中,我们还可以使用npx,npx会自动寻找项目依赖包中的
babel-node
和babel
。
// 进入`REPL`环境
npx babel-node
// 编译并运行test.js
npx babel-node test.js
特别注意: 由于全局运行
Babel
是一个坏习惯,如果你要卸载全局安装的版本可以执行:
npm uninstall -g babel-cli
babel-cli
工具自带一个babel-node
命令,提供一个支持ES6的REPL环境。它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码。babel-node
实现了node执行脚本和命令行写代码的能力。
它不用单独安装,而是随 babel-cli
一起安装。然后,执行 babel-node
就进入PEPL环境。
$ babel-node
> (x => x * 3)(2)
6
babel-node
命令可以直接运行ES6脚本。将上面的代码放入脚本文件demo.js,然后直接运行:
$ babel-node demo.js
6
babel-node
也可以安装在项目中:
$ npm install --save-dev babel-node
然后,改写package.json文件中的 scripts
字段:
"scripts": {
"script-name": "babel-node script.js"
}
上面代码中,使用babel-node
替代 node
,这样script.js本身就不用做任何转码处理。
经过babel
的编译后,我们的源代码与运行在生产下的代码是不一样的。
babel-register
则提供了动态编译。换句话说:我们的源代码能够真正运行在生产环境下,不需要babel
编译这一环节。
我们先在项目下安装babel-register
:
npm install --save-dev @babel/register
然后在入口文件中require:
require('@babel/register')
require('./app');
在入口文件头部引入@babel/register
后,我们的app
文件中即可使用任意 es2015+
的特性。
当然,坏处是动态编译,导致程序在速度、性能上有所损耗。
babel-register
模块改写require
命令,为它加上一个钩子。此后,每当使用require
加载.js
、.jsx
后缀名的文件,就会先用Babel
进行转码。
npm install --save-dev babel-register
使用时,必须首先加载babel-register
。
require("babel-register");
require("./index.js");
然后,就不需要手动对index.js转码了。
需要注意的是,babel-register
只会对require
命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在 开发环境 中使用。
babel-core
可以看做babel
的编译器。babel
的核心api
都在这里面。
如果某些代码需要调用babel的API进行转码,就要使用 babel-core
模块。
安装命令如下:
npm install babel-core -S
然后,在项目中就可以调用 babel-core
:
var babel = require('babel-core');
webpack
工具除了安装babel
自己的包,还需要多装一个babel-loader
配合webpack
使用。
npm install -D babel-loader babel-core
在webpack.config.js
中加入loader
相应的配置:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
}
]
}
目前babel
官方推荐是写到.babelrc
文件下,你还可以在package.json
里面添加babel
字段。不用配置文件的话,可以把配置当做参数传给babel-cli
。
.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-runtime", {
"helpers": true,
"polyfill": true,
"regenerator": true,
"moduleName": "babel-runtime"
}]
]
}
package.json
文件中配置"babel": {
"presets": [
"env"
],
}
babel example.js --plugins=transform-runtime --presets=env
webpack
和babel
配置react
开发环境react
# 安装以下两个包
npm install --save react react-dom
要让babel
转换react
代码,首先要安装好babel
,再装babel
转化react
的包。
npm install --save-dev babel-core babel-preset-react babel-preset-env
# babel-preset-env这个插件很全,能把所有es6语法都转化
创建.babelrc
文件,并进行如下配置:
{
"presets": ["env", "react"]
}
但是,有时候我们不需要那么全的功能,比如说只需要能转化async函数即可。babel-plugin-transform-async-to-generator
这个插件就能办到。
npm install --save-dev babel-plugin-transform-async-to-generator
安装完之后,直接在命令行里转化:
babel index.js --out-file main.js --plugins=transform-async-to-generator
webpack
中配置babel-loader
需要在webpack
中使用一个loader
来转化react
的代码。
# 安装相应的loader
npm install --save-dev babel-loader
# 相应的配置文件webpack.config.js
module.exports = {
entry: './src/app.js',
...
module: {
rules: [
{
test: /\.(js | jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
}
};
使用 eES7的async 会报:ReferenceError: regeneratorRuntime is not defined".
$ npm i -D babel-plugin-transform-runtime
在 .babelrc
文件中添加:
"plugins": [[
"transform-runtime",
{
"helpers": false,
"polyfill": false,
"regenerator": true,
"moduleName": "babel-runtime"
}
]]
[TOC]
DOM操作比起非DOM交互需要更多的内存和CPU时间,连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。
如果在程序中使用了onresize
事件处理程序,当调整浏览器大小的时候,该事件会连续触发。如果在该事件处理程序内部进行了相关DOM操作,其高频率的更改可能会导致浏览器崩溃。为了绕开这个问题,我们可以考虑使用定时器对该函数进行节流。
函数节流背后的基本**是:某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除之前的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是在只有在执行函数的请求停止了一段时间之后才执行。
以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。
实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。
throttle(又称节流)和debounce(又称去抖)其实都是函数调用频率的控制器。
debounce
强制函数在某段时间内只执行一次,throttle
强制函数以固定的速率执行。在处理一些高频率触发的DOM
事件的时候,它们都能极大提高用户体验。
在处理诸如resize、scroll、mousemove
和 keydown/keyup/keypress
等事件的时候,通常我们不希望这些事件太过频繁地触发,尤其是监听程序中涉及到大量的计算或者有非常耗费资源的操作。
有多频繁呢?以mousemove
为例,根据DOM Level 3
的规定,「如果鼠标连续移动,那么浏览器就应该触发多个连续的mousemove
事件」,这意味着浏览器会在其内部计时器允许的情况下,根据用户移动鼠标的速度来触发mousemove
事件。(当然了,如果移动鼠标的速度足够快,比如刷
一下扫过去,浏览器是不会触发这个事件的)。resize、scroll 和 key*
等事件与此类似。
DOM事件里的
debounce
概念其实是从机械开关和继电器的去弹跳(debounce)
衍生而来的,基本思路就是:把多个信号合并为一个信号。
在JavaScript
中,debounce
函数所做的事情就是:强制一个函数在某个连续时间段内只执行一次,哪怕它本来会被调用多次。我们希望在用户停止某个操作一段时间之后才执行相应的监听函数,而不是在用户操作的过程当中,浏览器触发多少次事件,就执行多少次监听函数。
比如:在某个3s
的时间段内连续地移动了鼠标,浏览器可能会触发几十(甚至几百)个 mousemove
事件,不使用debounce
的话,监听函数就要执行这么多次;如果对监听函数使用100ms
的去弹跳
,那么浏览器只会执行一次这个监听函数,而且是在第3.1s
的时候执行的。
现在,我们就来实现一个 debounce 函数。
debounce
函数接收两个参数,第一个是要去弹跳
的回调函数 fn,第二个是延迟的时间delay
。实际上,大部分的完整debounce
实现还有第三个参数immediate
,表明回调函数是在一个时间区间的最开始执行(immediate为true)
还是最后执行(immediate为false)
,比如underscore
的_.debounce
。本文不考虑这个参数,只考虑最后执行的情况,感兴趣的可以自行研究。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#box, #test {
width: 100px;
height: 100px;
background-color: aqua;
}
</style>
</head>
<body>
<div id="box"></div>
<div id="test"></div>
<script>
function debounce(fn, delay) {
var timer = null;
return function () {
// 保存函数调用时的上下文和参数
var that = this;
var args = arguments;
// 每次这个返回的函数被调用,就清除定时器,以保证不执行fn
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(that, args);
}, delay);
}
}
const box = document.querySelector('#box');
const box2 = document.querySelector('#test');
let time = 0;
let time2 = 0;
// 没有采用防抖
document.addEventListener('mousemove', function() {
box2.innerHTML = ++time;
}, false);
// 采用防抖处理
document.addEventListener('mousemove', debounce(function() {
box.innerHTML = ++time2;
}, 250), false);
document.addEventListener('mouseleave', function() {
time = 0;
time2 = 0;
box.innerHTML = 0;
box2.innerHTML = 0;
}, false);
</script>
</body>
</html>
实现思路:
debounce
返回了一个闭包,这个闭包依然会被连续频繁地调用,但是在闭包内部,却限制了原始函数fn
的执行,强制fn
只在连续操作停止后只执行一次。
再来考虑另外一个场景:根据用户的输入实时向服务器发
ajax
请求获取数据。我们知道,浏览器触发key*
事件也是非常快的,即便是正常人的正常打字速度,key*
事件被触发的频率也是很高的。以这种频率发送请求,一是我们并没有拿到用户的完整输入发送给服务器,二是这种频繁的无用请求实在没有必要。
更合理的处理方式是:在用户停止输入一小段时间以后,再发送请求。那么 debounce
就派上用场了:
$('input').on('keyup', debounce(function(e) {
// 发送 ajax 请求
}, 300))
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#box {
width: 100px;
height: 100px;
background-color: blueviolet;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
var box = document.querySelector('#box');
var height = 100;
function debounce(fn, delay) {
var timer = null;
return function () {
var that = this;
clearTimeout(timer);
timer = setTimeout(function() {
fn.call(that);
}, delay);
}
}
/*
没有做防抖处理
window.addEventListener('resize', function() {
height += 1;
box.style.height = height + 'px';
}, false);
*/
window.addEventListener('resize', debounce(function() {
height += 1;
box.style.height = height + 'px';
}, 250), false);
</script>
</body>
</html>
throttle
的概念理解起来更容易,就是固定函数执行的速率,即所谓的节流。正常情况下,假设mousemove
的监听函数每20ms
执行一次,如果设置200ms
的节流,那么它就会每200ms
执行一次。比如在 1s 的时间段内,正常的监听函数可能会执行 50(1000/20) 次,节流
200ms 后则会执行 5(1000/200)次。
与debounce
类似,throttle
也接收两个参数,一个实际要执行的函数fn
,一个执行间隔阈值threshhold
。
同样的,
throttle
的更完整实现可以参看underscore
的_.throttle
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.wrapper {
width: 200px;
height: 200px;
float: left;
border: 1px solid #ddd;
overflow: auto;
position: relative;
}
.wrapper .content {
height: 100%;
width: 100%;
overflow: auto;
}
.content .inner {
height: 6000px;
}
.wrapper .desc {
position: absolute;
}
.wrapper .count {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.normal {
margin-right: 20px;
}
</style>
</head>
<body>
<h3>Try scrolling in the 2 boxes...</h3>
<div>
<div class="wrapper normal">
<div class="desc">Normal scroll</div>
<div class="content">
<div class="inner"></div>
</div>
<span id="normal" class="count">0</span>
</div>
<div class="wrapper throttled">
<div class="desc">Throttled scroll</div>
<div class="content">
<div class="inner"></div>
</div>
<span id="throttled" class="count">0</span>
</div>
</div>
<script>
function throttle(fn, threshhold) {
// 记录上次执行的时间
var last;
// 定时器
var timer = null;
// 默认间隔为250ms
threshhold || (threshhold = 250);
// 返回函数,每隔threshhold毫秒就执行一次fn函数
return function () {
// 保存函数调用时的上下文和参数,传递给fn
var that = this;
var args = arguments;
var now = +new Date();
// 如果距离上次执行fn函数的时间小于threshhold,就不执行fn
// 否则执行fn,并重新计时
if (last && now < last + threshhold) {
clearTimeout(timer);
// 保证在当前时间区间结束后,再执行一次fn
timer = setTimeout(function() {
last = now;
fn.apply(that, args);
}, threshhold);
}
else {
last = now;
fn.apply(that, args);
}
}
}
var normalCount = 0;
var throttledCount = 0;
var normalSpan = document.querySelector('#normal');
var throttledSpan = document.querySelector('#throttled');
var normalContent = document.querySelector('.normal .content');
var throttledContent = document.querySelector('.throttled .content');
normalContent.addEventListener('scroll', function() {
normalSpan.innerText = ++normalCount;
}, false);
throttledContent.addEventListener('scroll', throttle(function() {
throttledSpan.innerText = ++throttledCount;
}, 250), false);
document.addEventListener('mouseleave', function() {
normalCount = 0;
throttledCount = 0;
normalSpan.innerText = 0;
throttledSpan.innerText = 0;
}, false);
</script>
</body>
</html>
原理也不复杂,相比 debounce,无非是多了一个时间间隔的判断,其他的逻辑基本一致。throttle 的使用方式如下:
$(document).on('mouvemove', throttle(function(e) {
// 代码
}, 250))
第一次触发后,进行倒计wait毫秒,如果倒计时过程中有其他触发,则重置倒计时;否则执行fn。用它来丢弃一些重复的密集操作、活动,直到流量减慢。例如:
第一次触发后先执行fn(当然可以通过{leading: false}来取消),然后wait ms后再次执行,在单位wait毫秒内的所有重复触发都被抛弃。即如果有连续不断的触发,每wait ms执行fn一次。与debounce相同的用例,但是你想保证在一定间隔必须执行的回调函数。例如:
一个相当常见的例子:用户在你无限滚动的页面上向下滚动鼠标加载页面,你需要判断现在距离页面底部多少。如果用户快接近底部时,我们应该发送请求来加载更多内容到页面。在此debounce没有用,因为它只会在用户停止滚动时触发,但我们需要用户快到达底部时去请求。通过throttle我们可以不间断的监测距离底部多远。
$(document).ready(function(){
// 这里设置时间间隔为300ms
$(document).on('scroll', throttle(function(){
check_if_needs_more_content();
}, 300));
// 是否需要加载更多资源
function check_if_needs_more_content() {
var pixelsFromWindowBottomToBottom = 0 + $(document).height() - $(window).scrollTop() - $(window).height();
// 滚动条距离页面底部小于200,加载更多内容
if (pixelsFromWindowBottomToBottom < 200){
// 加载更多内容
$('body').append($('.item').clone());
}
}
});
在学习Vue的时候,官网也用到了一个里子,就是用于对用户输入的事件进行了去抖,因为用户输入后需要进行ajax请求,如果不进行去抖会频繁的发送ajax请求,所以通过debounce对ajax请求的频率进行了限制。
methods: {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
getAnswer: _.debounce(function() {
if (!reg.test(this.question)) {
this.answer = 'Questions usually end with a question mark. ;-)';
return;
}
this.answer = 'Thinking ... ';
let self = this;
axios.get('https://yesno.wtf/api')
// then中的函数如果不是箭头函数,则需要对this赋值self
.then((response) = > {
this.answer = _.capitalize(response.data.answer)
}).
catch ((error) = > {
this.answer = 'Error! Could not reach the API. ' + error
})
}, 500) // 这是我们为判定用户停止输入等待的毫秒数
},
在使用不同的编辑器,或者在window 或 mac 等不同的系统上编写的代码,会有一定的风格差异,如在window 上默认换行 使用 crlf , 而在 mac 上的换行风格是 lf ; 有的编辑器默认缩进使用 tab , 而有的编辑器使用 space ; 为了统一这些差异, 我们需要使用 editorconfig 来统一规范。
在团队开发中,统一的代码格式是必要的。但是不同开发人员的代码风格不同,代码编辑工具的默认格式也不相同,这样就造成代码的differ。而editorConfig可以帮助开发人员在不同的编辑器和IDE中定义和维护一致的编码风格。本文将详细介绍统一代码风格工具editorConfig
。
editorConfig
不是什么软件,而是一个名称为.editorconfig
的自定义文件。该文件用来定义项目的编码规范,编辑器的行为会与.editorconfig
文件中定义的一致,并且其优先级比编辑器自身的设置要高,这在多人合作开发项目时十分有用而且必要
有些编辑器默认支持editorConfig,如webstorm;而有些编辑器则需要安装editorConfig插件,如ATOM、Sublime、VS Code等
当打开一个文件时,EditorConfig插件会在打开文件的目录和其每一级父目录查找.editorconfig文件,直到有一个配置文件root=true
EditorConfig的配置文件是从上往下读取的并且最近的EditorConfig配置文件会被最先读取. 匹配EditorConfig配置文件中的配置项会按照读取顺序被应用, 所以最近的配置文件中的配置项拥有优先权。
如果.editorconfig
文件没有进行某些配置,则使用编辑器默认的设置。
当多人团队进行一个项目开发时,每个人可能喜欢的编辑器不同,有人喜欢Webstrom、有人喜欢sublime、还有人喜欢Hbuilder。
这个时候,问题便迎面而来,如何使使用不同编辑器的开发者能够轻松惬意的遵守最基本的代码规范呢?
最后终于找到了editorConfig这个东东,发现在这里配置的代码规范规则优先级高于编辑器默认的代码格式化规则。比如我使用的是Webstrom编辑器,我每一次写完代码之后,都习惯性的按下Ctrl+Alt+L
快捷键去整理代码格式。如果我没有配置editorconfig,执行的就是编辑器默认的代码格式化规则;如果我已经配置了editorConfig,则按照我设置的规则来,从而忽略浏览器的设置。
什么是EditorConfig?官方介绍:帮助开发人员定义和维护一致性开发风格(coding style)。它可以让代码在各种编辑器和IDE中保持风格一致,当然也可以让不同的队员写一致风格的代码
EditorConfig
插件;.editorconfig
的文件。.editorconfig
文件中配置相应的规范。其工作原理是:当你在编码时,EditorConfig 插件会去查找当前编辑文件的所在文件夹或其上级文件夹中是否有
.editorconfig
文件。如果有,则编辑器的行为会与.editorconfig
文件中定义的一致,并且其优先级高于编辑器自身的设置。
editorConfig配置文件需要是UTF-8字符集编码的, 以回车换行或换行作为一行的分隔符。
斜线(/)被用作为一个路径分隔符,井号(#)或分号(;)被用作于注释. 注释需要与注释符号写在同一行。
.editorconfig
文件。EditorConfig
下面的实例中使用4
个空格来缩进,并不是说按一下空格会自动打出4
个空格来,也不是说要连按4
下空格;而是按tab
键时,编辑器会自动输出4
个空格的宽度,而不是之前默认的制表符(\t)
。
使用EditorConfig
很简单,在项目根目录放置一个.editorconfig
文件,官方的一个示例:
# top-most EditorConfig file
root = true
# 使用Unix-style 换行符,并且每个文件以换行结束
[*]
end_of_line = lf
insert_final_newline = true
# 可以使用通配符匹配多个文件
# 设置默认编码为utf-8
[*.{js,py}]
charset = utf-8
# py文件 4 个空格缩进
[*.py]
indent_style = space
indent_size = 4
# 使用Tab缩进
[Makefile]
indent_style = tab
# lib目录下的js使用2个空格缩进
[lib/**.js]
indent_style = space
indent_size = 2
# 配置 package.json 或 .travis.yml文件 设置其为2个空格缩进
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
上面这个例子,是对js和py代码风格作了定义,看注释应该很容易理解。
EditorConfig
文件使用INI格式。支持的属性如下:
按照上面配置即可,在
IDE
中需要开启相应EditorConfig
插件,比如`VScode中启用后,即可完成配置。下面是我的配置:
root = true
# 匹配所有文件类型
[*]
# 设置字符编码
charset = utf-8
# 设置缩进格式
indent_style = space
# 使用4个空格缩进
indent_size = 4
# 设置换行格式
end_of_line = lf
# 在文件最后添加一个空行
insert_final_newline = true
# 删除行末尾多余的空格
trim_trailing_whitespace = true
[*.json]
indent_size = 4
# 对后缀名为md的文件生效
[*.md]
trim_trailing_whitespace = false
ESLint
是代码约束工具,也可以理解成代码质量工具,JS开发人员可以定义规则,ESLint根据这些规则来检查代码是否符合规范。之前有个工具叫JSLint,我没怎么用过。据说ESLint比JSLint更好用,它适配最新的ES6, ES7等语法。
不管写前端还是写后台代码都有所谓的代码规范,从约束层面上来说,以前只是大家约定了这样的规范,可能还配合使用IDE的自动化格式等工具,但实际上写好还是写坏,是没有检查的。通过ESLint可以让整个团队写出高度一致性,极具美观的代码。
npm install eslint babel-eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react -D
{
"parser": "babel-eslint",
"extends": "airbnb",
"installedESLint": true,
"plugins": [
"react",
"jsx-a11y",
"import"
],
"rules": {
// js缩进规则是4个空格
"indent": ["error", 4],
// 一行长度最多是150个字符
"max-len": ["error", 150],
// jsx的缩进规则是4个空格
"react/jsx-indent": ["error", 4],
// jsx文件可以是.js或.jsx后缀
"react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }],
"react/jsx-indent-props": ["error", 4]
}
特别注意:这里的4个空格,其实我实际是用
tab
来完成缩进,而不是按4下空格,所以上面的EditorConfig
就可以帮上大忙了。如果不用EditorConfig
,那么ESLint
会警告你使用了Tab
,破坏了规则。
在经常有这样的事情发生,当你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支上进行一些工作。问题是,你不想提交进行了一半的工作,否则以后你无法回到这个工作点。解决这个问题的办法就是git stash命令。
“‘储藏”“可以获取你工作目录的中间状态——也就是你修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以重新应用。
储藏你的工作
为了演示这一功能,你可以进入你的项目,在一些文件上进行工作,有可能还暂存其中一个变更。如果你运行 git status,你可以看到你的中间状态:
默认情况下,git stash
会缓存下列文件:
但不会缓存一下文件:
git stash
命令提供了参数用于缓存上面两种类型的文件。使用-u
或者--include-untracked
可以stash untracked
文件;使用-a
或者--all
命令可以stash
当前目录下的所有修改。
[TOC]
- 创建项目目录并进入目录:
mkdir webpack4-demo && cd $_
- 初始化package.json文件
npm init -y
- 安装webpack(需要同时安装webpack-cli)
npm i webpack webpack-cli -D
- 打开package.json文件,并添加相应的build(构建)脚本:
"scripts": {
"dev": "webpack --mode develop",
"build": "webpack --mode production"
}
需要注意的是:在webpack4以前的版本中,必须在
webpack.config.js
的配置文件中通过entry
属性定义entry point
(入口点)和output属性定义输出目录和文件。但是,从webpack4
开始,我们不需要必须定义entry point
(入口点)和输出目录了,webpack将入口点默认为./src/index.js
,./dist/main.js
为输出模块包。
所谓entry point
(入口点)是webpack
寻找开始构建Javascript
包的文件。
webpack
同时支持es6,CommonJS,AMD
三种模块化规范。
// ES6
import sum from './vender/sum';
console.log('sum(1, 2) = ', sum(1, 2));
// CommonJS
var minus = require('./vender/minus');
console.log('minus(1, 2) = ', minus(1, 2));
// AMD
require(['./vender/multi'], function(multi) {
console.log('multi(1, 2) = ', multi(1, 2));
});
运行
npm run build
打包:
运行如下命令:
npm run dev
查看./dist/main.js
,是一个未被压缩过的文件。
运行如下命令:
npm run build
查看./dist/main.js
,是一个未被压缩过的文件。
这说明了,production mode
(生产模式)可以开箱即用地进行各种优化。包括压缩,作用域提升,tree-shaking等;development mode
(开发模式)针对速度进行了优化,仅仅提供了一种不压缩的bundle
。
在webpack4
中,我们可以在没有一行配置的情况下完成任务!只需定义–mode
参数。
webpack4支持了零配置,但如何覆盖默认entry point
(入口点) 和默认output
(输出)呢?
在package.json
中进行如下配置:
"scripts": {
"dev": "webpack --mode development ./demo/src/js/index.js --output ./demo/dist/main.js",
"build": "webpack --mode production ./demo/src/js/index.js --output ./demo/dist/main.js"
}
本节课讲解webpack4
打包单页应用过程中的代码分割和代码懒加载。不同于多页面应用的提取公共代码,单页面的代码分割和懒加载不是通过webpack
配置来实现的,而是通过webpack
的写法和内置函数实现的。
目前webpack
针对此项功能提供2
种函数:
代码目录如下:
其中,index.js
是入口文件,subPageA.js
和subPageB.js
共同引用module.js
。下面,我们按照代码引用的逻辑,从底向上展示代码:
module.js:
export default 'module';
subPageA.js:
import './module';
console.log('This is subPageA');
export default 'subPageA';
subPageB.js:
import './module';
console.log('This is subPageB');
export default 'subPageB';
注意:subPageA.js
和subPageB.js
两个文件中都执行了console.log()
语句。之后将会看到import()和require()
不同的表现形式:是否会自动执行js
的代码?
编写webpack配置文件:
const path = require('path');
module.exports = {
mode: 'none',
entry: './src/index2.js',
output: {
publicPath: __dirname + '/dist/',
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js'
}
};
采用
import()
编写入口文件index.js
,个人是非常推荐import()写
法,因为和es6
语法看起来很像。除此之外,import()
可以通过注释的方法来指定打包后的chunk
的名称。
除此之外,相信对vue-router
熟悉的朋友应该知道,其官方文档的路由懒加载的配置也是通过import()
来书写的。
index.js
文件内容如下:
import(/* webpackChunkName: 'subPageA'*/ './subPageA').then(function(subPageA) {
console.log(subPageA);
});
import(/* webpackChunkName: 'subPageB'*/ './subPageB').then(function(subPageB) {
console.log(subPageB);
});
document.querySelector('#btn').onclick = () => {
import(/* webpackChunkName: 'lodash'*/ 'lodash').then(function(_) {
console.log(_.join(['1', '2']));
});
}
export default 'page';
在命令行中运行
webpack
,打包结果如下:
我们创建index.html
文件,通过<script>
标签引入我们打包结果,需要注意的是:因为是单页应用,所以只要引用入口文件即可(即是上图中的main.bundle.js
)。
index.html
文件如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button id="btn">按需加载</button>
<script src="./dist/main.bundle.js"></script>
</body>
</html>
打开浏览器控制台,刷新界面,结果如下图所示:
图中圈出的部分,就是说明import()
会自动运行subPageA.js和subPageB.js
的代码。
在
NetWork
选项中,我们可以看到,懒加载也成功了:
点击按需加载的按钮也会动态加载对应的
chunk
:
require.ensure()
不会自动执行js
代码,请注意注释。
require.include('./module.js'); // 将subPageA和subPageB共用的module.js打包在此page中
require.ensure(
['./subPageA.js', './subPageB.js'], // js文件或者模块名称
function() {
var subPageA = require('./subPageA'); // 引入后需要手动执行,控制台才会打印
var subPageB = require('./subPageB');
},
'subPage' // chunkName
);
require.ensure(
['lodash'],
function() {
var _ = require('lodash');
_.join(['1', '2']);
},
'lodash'
);
export default 'page';
打包结果如下:
首先总结一下在前端开发中实现动画的方式:
animation+keyframes
;transition
过渡效果;canvas
上作图来实现动画;jQuery
动画相关的API方便地实现;window.setTimout()
或者window.setInterval()
通过不断改变元素的状态位置等来实现动画,前提是画面的更新频率要达到每秒60
次(因为大多数显示器的刷新频率是60Hz
)才能让肉眼看到流畅的动画效果;window.requestAnimationFrame()
方法。requestAnimationFrame
解决了浏览器不知道javascript动画什么时候开始、不知道最佳循环间隔时间的问题。它是跟着浏览器的绘制走的,如果浏览器绘制间隔是16.7ms
,它就按这个间隔绘制;如果浏览器绘制间隔是10ms, 它就按10ms绘制。这样就不会存在过度绘制的问题,动画不会丢帧。
内部是这么运作的: 浏览器页面每次要重绘,就会通知
requestAnimationFrame
,这是资源非常高效的一种利用方式。怎么讲呢?有以下两点:
requestAnimationFrame()
要执行,浏览器只要通知一次就可以了。而setTimeout
是多个独立绘制。requestAnimationFrame
也不会触发(因为没有通知)。页面绘制全部停止,资源高效利用。编写动画循环的关键: 是要知道延迟时间多长合适。一方面,循环间隔必须足够短,这样才能保证不同的动画效果显得更平滑流畅;另一方面,循环间隔还要足够长,这样才能保证浏览器有能力渲染产生的变化。大多数显示器的刷新频率是
60Hz
,相当于每秒重绘60
次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过了这个频率,用户体验也不会有提升。
因此,最平滑动画的最佳循环间隔是1000ms/60
,约等于17ms
。以这个循环间隔重绘的动画是平滑的,因为这个速度最接近浏览器的最高限速。为了适应17ms
的循环间隔,多重动画可能需要加以节制,以便不会完成得太快。
虽然与使用多组setTimeout
相比,使用setInterval
的动画循环效率更高。但是无论setTimeout
还是setInterval
都不十分精确。为它们传入的第二个参数,实际上只是指定了把动画代码添加到浏览器UI线程队列以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务执行完成后再执行。如果UI线程繁忙,比如忙于处理用户操作,那么即使把代码加入队列也不会立即执行。
因此,知道什么时候绘制下一帧是保证动画平滑的关键。然而,面对不十分精确的setTimeout和setInterval
,开发人员至今都没有办法确保浏览器按时绘制下一帧。以下是几个浏览器的计时器精度:
更为复杂的是:浏览器开始限制后台标签页或不活动标签页的计数器。因此,即使你优化了循环间隔,可能仍然只能接近你想要的效果。
CSS3动画的优势在于浏览器知道动画什么时候开始,因此会计算出正确的循环间隔,在适当的时候刷新UI。而对于JavaScript动画,浏览器就无从知晓什么时候开始。
在JS中,我们可以通过requestAnimationFrame( )方法告诉浏览器某些代码将要执行动画。这样浏览器可以在运行某些代码后进行适当的优化。
与setTimeout
和setInterval
方法不同,requestAnimationFrame
不需要调用者指定帧速率,浏览器会自行决定最佳的帧效率。
requestAnimationFrame
是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是:按帧对网页进行重绘。
设置这个API的目的是:为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。
requestAnimationFrame的优势在于: 充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,
requestAnimationFrame
的基本**就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。
requestAnimationFrame
方法接收一个参数(一个函数),即在重绘屏幕前调用这个函数。这个函数负责改变下一次重绘时的DOM样式。为了创建动画循环,可以像使用setTimeout
一样,把多个对 requestAnimationFrame
的调用连起来。如:
function updateFrame() {
//其他代码
if(一定条件){
//在满足一定条件的情况下,继续调用
window.requestAnimationFrame(updateFrame);
}
}
window.requestAnimationFrame(updateFrame);
特别注意: requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。
requestID = window.requestAnimationFrame(callback);
requestAnimationFrame
使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。
cancelAnimationFrame方法用于取消重绘。
window.cancelAnimationFrame(requestID);
它的参数是requestAnimationFrame
返回的一个代表任务ID的整数值。
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
};
})();
首先判断各个浏览器是否支持这个API。如果不支持,则自行模拟部署该方法。上面的代码按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame
。
Demo:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<style type="text/css">
#anim{
width: 200px;
height: 200px;
background-color: red;
position: absolute;
}
</style>
</head>
<body>
<div id="anim">点击运行动画</div>
<script type="text/javascript">
//这里是兼容requestAnimationFrame
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
};
})();
var elem = document.getElementById("anim");
var startTime = undefined; //全局的
function render(time){ //time是局部的
if (time === undefined)
time = Date.now(); //获取到当前时间的毫秒数
if (startTime === undefined)
//第一次调用的时候startTime是undefined,之后就不是了
//而time每次调用都等于当前时间的毫秒数
startTime = time;
//当是500的整数倍的时候,left值从0开始
elem.style.left = ((time - startTime)/10%500) + "px";
}
elem.onclick = function(){
(function animloop(){
render();
//这里利用requestAnimFrame方法来控制渲染的帧数
requestAnimFrame(animloop);
})();
};
</script>
</body>
</html>
Demo2:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>模拟进度条</title>
<style type="text/css">
#box{
height: 16px;
background-color: #f00;
color:#fff;
}
</style>
</head>
<body>
<div id="box">0%</div>
<input type="button" name="" value="加载" id="btn">
<script type="text/javascript">
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
var oBox = document.getElementById("box"),
oBtn = document.getElementById("btn"),
progress = 0;
function requestAnim(timestamp){
progress += 1;
oBox.style.width = progress + 'px';
oBox.innerHTML = progress + '%';
if(progress<100){
requestAnimationFrame(requestAnim);
}
}
requestAnimationFrame(requestAnim);
oBtn.addEventListener('click',function(){
oBox.style.width = "1px";
progress = 0;
requestAnimationFrame(requestAnim);
},false);
</script>
</body>
</html>
[TOC]
npm是javascript的包管理工具,是前端模块化下的一个标志性产物。简单地地说,就是通过npm下载模块,复用已有的代码,提高工作效率。
Node.js模块就是发布到npm的代码包。
创建一个新模块的第一步就是创建一个package.json
文件。 我们可以使用npm init -y
来快速创建一个package.json
文件。这个过程中命令行会逐步提示你输入这个模块的信息,其中模块名字和版本号是必填项。
{
"name": "npm_lj_test",
"version": "1.0.2",
"description": "npm package test",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "lj",
"license": "ISC"
}
入口文件默认是index.js
。这里给出一个最简单的例子,在默认的index.js
文件里写一个要导出的函数,这个函数也就是别人的代码里可以import或者require的。
exports.sayHello = function () {
console.log("This is my first npm module");
};
至此,我们的第一个node模块就已经创建完成了,下一步就是发布到npm服务器了。
目前有两种方式:
npm adduser
(按照提示创建)首次需要登录,npm login
存储证书到本地,之后就不需要每次都登录了。登录过程中需要输入用户名,密码,还有邮箱,这些都是刚刚注册时候填写的。
NPM 打包发布的时候,会默认把当前目录下所有文件打包。但是 Git 仓库中,有些东西是不需要 发布到 NPM 的,因此我们需要使用一个文件 .npmignore
来忽略这些文件,常用配置如下:
/.git/
/.vscode/
/docs/
/node_modules/
.gitignore
.npmignore
tslint.json
tsconfig.json
*.log
这些文件都不是发布需要的内容,因此可以忽略。
// 发布包
npm publish
发布过程会把整个目录发布,不想发布的内容模块,可以通过 .gitignore
或 .npmignore
文件忽略不想发布的内容。
发布成功后可以到npm个人中心的packages页面查看。
对包中的相关文件进行修改完毕后,在发布修改的模块之前,必须先创建新版本的version,执行一下命令:
npm version [patch/minor/major]
#运行命令后,package.json中的version将被修改
#然后在之下发布命令
npm publish
语义化的版本:
维护一个包,肯定是要进行包的版本升级的。如何进行呢?手动修改 package.json 的 version 字段是一个办法,但是显得有点 low。可以使用下面的命令:
# 主版本号.子版本号.修订版本号 => 0.1.0
# 版本号变成 0.1.0,即显式设置版本号
npm version v0.1.0
# 版本号从 0.1.0 变成 0.1.1,即修订版本号加一
npm version patch
# 版本号从 0.1.1 变成 0.2.0,即子版本号加一
npm version minor
# 版本号从 0.2.0 变成 1.0.0,即主版本号加一
npm version major
除了上述命令之外,还有四个命令用于创建预发布版本(非稳定版本),具体命令如下:
#显式设置版本为1.2.3
npm version v1.2.3
# 版本号从 1.2.3 变成 1.2.4-0,就是 1.2.4 版本的第一个预发布版本
npm version prepatch
# 版本号从 1.2.4-0 变成 1.3.0-0,就是 1.3.0 版本的第一个预发布版本
npm version preminor
# 版本号从 1.2.3 变成 2.0.0-0,就是 2.0.0 版本的第一个预发布版本
npm version premajor
# 版本号从 2.0.0-0 变成 2.0.0-1,就是使预发布版本号加一
npm version prerelease
特别注意: version 命令默认会给你的 git 仓库自动 commit 一把,并打一个 tag。如果不想它动你的 git 仓库,你应该使用 --no-git-tag-version
参数,例如:
npm --no-git-tag-version version patch
如果想一劳永逸,那么可以使用如下 NPM 设置彻底禁止它:
npm config set git-tag-version false # 不要自动打 tag
npm config set commit-hooks false # 不要自动 commit
以 TypeScript 为例,通过 npm info typescript
可以看到 dist-tags
字段有着五个 值,分别是 latest, beta, rc, next, insiders
,这些都是 dist-tag
,可以 称之为标签——你可以把它理解为 git 里面的分支。
有什么用呢?其实,我们平时用 npm install xxx
的时候,是使用了一个潜在的选项 tag = latest
,可以通过 npm config list -l | grep tag
看到。
因此实际上是执行了 npm install xxx@latest
。也就是安装了 latest
这个标签对应的最新版本。
不同的标签可以有不同的版本,这就方便我们发表非稳定版本到 npm 上,与稳定版本分开。默认是发布到 latest 标签下的。
例如 npm publish --tag dev
就可以发布一个版本到 dev 标签下。
如果你使用过 AngularJS 或者 TypeScript,那么肯定知道有一些包的名字是这样的:
@types/node
@types/jquery
@angular/core
@types/ 和 @angular/
叫做包前缀(scope)。
作者起初以为使用包前缀也是收费的,后来仔细阅读了文档才发现公开的包可以免费使用包前缀。
那我们怎么使用呢?很简单,首先在 package.json
里面把 name 字段加上一个前缀。
前缀必须是你 NPM 账户的用户名,比如你注册了一个用户名为 abc 的账户,则你只能使用 @abc/ 为你的包前缀。
举个例子,将你的包名设置为 @abc/test
。
如果你要初始化一个带包前缀的包,则可以使用下面的命令:
npm init --scope=abc #可以加上个 `-y` 快速创建。
或者你想每次都使用 @abc/
包前缀,加个设置即可:
#这样每次初始化新的 package.json,都将自动应用 @abc/ 包前缀。
npm config set scope abc
现在,可以发布你的包到 npmjs.org 了。哦不,别忘了一点:
官方文档表示:所有带前缀的包,在发布的时候,默认都是发布为私有包。
这意味着你不能就这么发布,因为你(可能)不是付费用户,不能发布私有的包。那怎么办呢?别担心,npm publish
命令还有一个参数 --access
,通过这个参数可以指定发布的是公共包还是私有包。因此,只要用下面的命令就可以发布一个公共的带包前缀的包了:
npm publish --access=public
#发布第一个稳定版本
npm publish //1.0.0
#进行相应修改,发布第二个稳定版本
npm version patch
npm publish //1.0.1
#继续修改文件发布一个prerelease版本
npm version prerelease
npm publish --tag beta //1.0.2-0
#继续修改文件发布一个prerelease版本
npm version prerelease
npm publish --tag beta //1.0.2-1
使用npm info
命令查看相关信息:
#主要关注以下信息
'dist-tags': { latest: '1.0.1', beta: '1.0.2-1' },
versions: [ '1.0.0', '1.0.1', '1.0.2-0', '1.0.2-1' ]
dist-tags
字段中存储了当前模块的最新的稳定版本和最新的beta版本,versions数组中存储的是当前模块的所有版本列表。
npm dist-tag
命令#获取到所有的最新的版本,包括prerelease与稳定版本
npm dist-tag ls
#结果如下:
beta: 1.0.2-1
latest: 1.0.1
#<pkg>代表当前npm包名
#<version>是版本号
#<tag>可以为latest, beta, rc, next, insiders
npm dist-tag add <pkg>@<version> [<tag>]
npm dist-tag rm <pkg> <tag>
npm dist-tag ls [<pkg>]
当模块的prerelease版本已经稳定了,可以将其设置为稳定版本,具体命令如下:
npm dist-tag add [email protected] latest
通过npm info
查看结果如下:
#latest: '1.0.2-1' => 已经改变
name: 'npm_test_lj',
'dist-tags': { latest: '1.0.2-1', beta: '1.0.2-1' },
versions: [ '1.0.0', '1.0.1', '1.0.2-0', '1.0.2-1' ]
在使用 React 的过程中,不可避免的需要在组件间进行消息传递,组件间通信大体有下面几种情况:
通信是单向的,数据必须是由一方传到另一方。在 React 中,父组件可以向子组件通过传 props
的方式,向子组件进行通讯。
#Parent.jsx
import React, { Component } from 'react';
import Child from './Child';
export default class Parent extends Component {
constructor(props) {
super(props);
this.state = {
msg: '我是父组件传来的消息'
};
}
render() {
return (
<div>
我是父组件
<Child
title={this.state.msg}
/>
</div>
)
}
}
#Child.jsx
import React from 'react';
export default class Child extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.props.title}
</div>
)
}
}
利用回调函数,可以实现子组件向父组件通信。父组件将一个函数作为 props
传递给子组件,子组件调用该回调函数,便可以向父组件通信。
#Parent.jsx
import React, { Component } from 'react';
import Child from './Child';
export default class Parent extends Component {
constructor(props) {
super(props);
this.state = {
msg: '我是父组件传来的消息'
};
}
//回调函数
cbFn(msg) {
this.setState({
msg: msg
})
}
render() {
return (
<div>
我是父组件
<Child
title={this.state.msg}
cbFn={(msg) => this.cbFn(msg)}
/>
</div>
)
}
}
#Child.jsx
import React from 'react';
export default class Child extends React.Component {
constructor(props) {
super(props);
}
handleClick() {
//调用父组件传过来的回调函数
this.props.cbFn('我是子组件传来的信息');
}
render() {
return (
<div>
{this.props.title}
<button
onClick={() => this.handleClick()}
>
点击我进行通信吧
</button>
</div>
)
}
}
对于没有直接关联关系的两个节点,就如 Child_1
与 Child_2
之间的关系,他们唯一的关联点,就是拥有相同的父组件。参考之前介绍的两种关系的通讯方式,如果我们向由 Child_1
向 Child_2
进行通讯,我们可以先通过 Child_1 向 Parent
组件进行通讯,再由 Parent 向 Child_2
组件进行通讯,具体示例如下:
所谓跨级组件通信,就是父组件向子组件的子组件通信,向更深层的子组件通信。跨级组件通信可以采用下面两种方式:
#Parent.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Child from './Child';
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
getChildContext() {
return {
color: "red"
};
}
cbFn(msg) {
console.log(msg);
}
render() {
return (
<div>
我是父组件
<Child
title="我是子组件"
cbFn={(msg) => this.cbFn(msg)}
/>
</div>
)
}
}
Parent.childContextTypes = {
color: PropTypes.string
};
#Child.jsx
import React from 'react';
import PropTypes from 'prop-types';
import SubChild from './SubChild';
export default class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
handleClick() {
this.props.cbFn('我是子组件传来的信息');
}
render() {
return (
<div>
{this.props.title}
<button
onClick={() => this.handleClick()}
>
点击我进行通信吧
</button>
<SubChild />
</div>
)
}
}
#SubChild.jsx
import React from 'react';
import PropTypes from 'prop-types';
export default class SubChild extends React.Component {
render() {
return (
<button
style={{background: this.context.color}}
>
删除
</button>
);
}
}
SubChild.contextTypes = {
color: PropTypes.string
};
redux-thunk
是redux
解决异步的中间件。
当我们只使用redux
的时候,我们需要dispatch
的是一个action
对象。但是当我们使用redux-thunk
之后,我们dispatch
的是一个function
。redux-thunk
会自动调用这个function
,并且传递dispatch
方法作为其第一个参数。这样一来,我们就能在这个function
内根据我们的请求状态:开始请求,请求中,请求成功/失败,dispatch
我们期望的任何action
了,这也是为什么它能支持异步dispatch (action)
。
本质上是
redux-thunk
对store.dispatch
方法进行了增强改造,使其具备接受一个函数作为参数的能力,从而达到middleware
的效果,即在redux
的dispatch(action) => reducer => update store
这个流程中,在action
被发起之后,到达reducer
之前,加入相关操作,比如发生请求、打印log等。
npm install -S redux-thunk
redux-thunk
的源码非常简洁,除去空格一共只有11
行,这11
行中如果不算上}
,则只有8
行。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// 如果action是一个function,就返回action(dispatch, getState, extraArgument),否则返回next(action)。
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// next为之前传入的store.dispatch,即改写前的dispatch
return next(action);
};
}
const thunk = createThunkMiddleware();
// 给thunk设置一个变量withExtraArgument,并且将createThunkMiddleware整个函数赋给它
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
// thunk的内容如下
({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
}
// thunk.withExtraArgument的结果如下
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
thunk.withExtraArgument
允许给返回的函数传入额外的参数,它比较难理解的部分和thunk
一样,如下:
({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
}
我们先看看,在reudx
中如何使用中间件:直接将thunk
中间件引入,作为 applyMiddleware
的参数,然后传入createStore
方法中,就完成了 store.dispatch()
的功能增强,这样就可以进行一些异步的操作了。其中 applyMiddleware
是Redux
的一个原生方法,将所有中间件组成一个数组,依次执行,中间件多了可以当做参数依次传进去。
let store = createStore(
reducer,
applyMiddleware(thunk)
);
那么createThunkMiddleware函数中
dispatch,getState,next,action
这些参数是从哪里来的呢?这就需要看看applyMiddleware
的源码实现了,如下:
export default function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
let chain = []
// 要传给middleware的参数
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
将
thunk
作为参数传入之后,即applyMiddleware(thunk)
,返回了一个函数,这个函数其实就是一个enhancer
,然后传入redux
的createStore
函数中:
let store = createStore(
reducer,
applyMiddleware(thunk) // 返回一个`enhancer`
);
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
}
在上述
redux
源码中,createStore
函数中的enhancer
被执行,传入参数createStore
,紧接着执行了其返回的函数,传入reducer和preloadedState
。接下来,我们进入applyMiddleware和thunk
的关键部分,上面applyMiddleware
接受的最初的(…middlewares)
参数其实就是thunk
,thunk
会被执行,并且传入参数getState和dispatch
:
// 传入到thunk的参数
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 依次执行所有的中间件(thunk)
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 改写dispatch
dispatch = compose(...chain)(store.dispatch)
上述代码中的
chain
是什么呢?这就需要结合redux-thunk
源码来分析了:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
redux-thunk
中间件export default
的就是createThunkMiddleware()
函数处理之后的thunk
,再看createThunkMiddleware
这个函数,返回的是一个返回函数的函数。将上述代码编译成ES5
的代码:
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) {
// 这里返回的函数就是chain
return function(next) {
// 这里返回的函数就是改写的dispatch
return function(action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
}
}
// compose源码
// 当funcs长度为1时,返回funcs中的第一项对应的函数
if (funcs.length === 1) {
return funcs[0]
}
从上述代码中我们可以看出,
chain
就是以next
作为形参的匿名函数,compose
函数作用是:将多个函数连接起来,将一个函数的返回值作为另一个函数的传参进行计算,得出最终的返回值。这里chain
是包含一个函数的数组,根据compose
的源码,我们可以知道compose(...chain)
直接返回数组中的唯一函数。所以很简单,这里就是直接执行chain
,并将store.dispatch
作为实参传递给next
。
改造后的
dispatch
最终变为:
function(action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// next为之前传入的store.dispatch,即改写前的dispatch
return next(action);
};
如果传入的action是函数,则执行函数;否则直接
dispatch(action)
。
从上述分析中可以得出如下结论:
middleware
执行时传入的参数对象middlewareAPI
中确实包含getState和dispatch
两项,next
则来自dispatch = compose(...chain)(store.dispatch)
这一句中的store.dispatch
,而action
在dispatch
某个action
时传入。
一般来说,一个有效携带数据的action
是如下这样的:
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
加入redux-thunk
后,action
可以是函数了,依据redux-thunk
的源码,我们可以看出如果传入的action
是函数,则返回这个函数的调用。如果传入的函数是一个异步函数,我们完全可以在函数调用结束后,获取必要的数据再次触发dispatch
,由此实现异步效果。
使用场景如下:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// 注册thunk到applyMiddleware
const createStoreWithMiddleware = applyMiddleware(
thunk
)(createStore);
const store = createStoreWithMiddleware(rootReducer);
// action方法
function increment() {
return {
type: INCREMENT_COUNTER
};
}
// 执行一个异步的dispatch
function incrementAsync() {
return dispatch => {
setTimeout(() => {
dispatch(increment());
}, 1000);
};
}
JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()
和setInterval()
这两个函数来完成。它们向任务队列添加定时任务。
js是运行于单线程的环境中的,定时器仅仅只是计划代码在未来的某个时间执行(但是并不保证在该时间点一定执行)。执行时机是不能保证的,因为在页面的生命周期中,不同时间可能有其他代码在控制js进程。在页面下载完后的代码运行、事件处理程序、Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
如上图所示:我们可以把javascript
想象成在时间线上运行的。当页面载入时,首先执行是任何包含在<script>
元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,有时候也包含一些初始数据的处理。在这之后,javascript
进程将等待更多代码执行,当进程空闲时,下一个代码会被触发并立刻执行。例如:当点击某个按钮时,onclick
事件处理程序会立刻执行,只要javascript
进程处于空闲状态。
除了javascript主执行进程
外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序添加到队列中。例如:当某个按钮被按下,它的事件处理程序代码就会被添加到队列中,并在下一个可能的时间里执行。当接收到某个Ajax响应时,回调函数的代码会被添加到队列。在javascript
中没有任何代码是立刻执行的,但是一旦进程空闲则尽快执行。
定时器对队列的工作方式是: 当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立刻执行,而只能表示它会尽快执行。例如:设定一个150ms后执行的定时器不代表到了150ms代码就立刻执行,它表示代码会在150ms后被加入到队列中。如果在这个时间点,队列中没有其他东西,那么这段代码就会被执行,表面上看上去就好像代码就在精确的时间点上执行了。其他情况,代码可能明显等待更长时间才执行。
在上图中:给按钮设置了一个事件处理程序,该事件处理程序设置了一个250ms
后调用的定时器。点击该按钮后,首先将onclick
事件处理程序加入队列。该事件处理程序执行后才设置定时器,再有250ms
后,指定的代码才被添加到队列中等待执行。
对于定时器而言:我们要记住指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。如果上图中的onclick
事件处理程序执行了300ms
,那么定时器的代码至少要在定时器设置之后的300ms
后才会被执行。队列中所有的代码都要等到js
进程空闲之后才能执行,而不管它们是如何添加到队列中的。
上图中,尽管在255ms
处添加了定时器代码,但是这个时候不能执行,因为onclick
事件处理程序还在运行。定时器代码最早的执行时机在300ms
处,即onclick
事件处理程序结束之后。
setTimeout
函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
const timer = setTimeout(func|code, delay);
上面代码中,setTimeout
函数接受两个参数,第一个参数func|code
是将要推迟执行的函数名或者一段代码,第二个参数delay
是推迟执行的毫秒数。
console.log(111);
setTimeout('console.log(222)', 2000);
console.log(333);
// 运行结果:
111
333
222
上面代码会先输出111和333,然后等待2秒再输出222。特别注意:console.log(2)
必须以字符串的形式,作为setTimeout
的参数。
如果推迟执行的是函数,就直接将函数名作为setTimeout的参数。
const fn = () => {
console.log(222);
}
console.log(111);
setTimeout(fn, 2000);
console.log(333);
// 运行结果:
111
333
222
特别注意:
setTimeout
的第二个参数如果省略,则默认为0。
setTimeout(f);
// 等同于
setTimeout(f, 0);
除了前面提到的两个参数,setTimeout还允许更多的参数。它们将依次传入推迟执行的函数(回调函数)。
console.log(111);
setTimeout((a,b) => {
console.log(a + b);
}, 1000, 1, 2);
console.log(333);
// 运行结果:
111
333
3
上面代码中,setTimeout共有4个参数。最后那两个参数(1和2),将在1秒之后回调函数执行时,作为回调函数的参数。
特别注意:如果回调函数是对象的方法,那么
setTimeout
使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象。
var x = 1;
var obj = {
x: 2,
y: function () {
// 这里this指向window
console.log(this.x);
}
};
setTimeout(obj.y, 1000); // 1
上面代码输出的是1,而不是2。因为当obj.y
在1秒后运行时,this
所指向的已经不是obj
了,而是全局环境window
。
obj.y
放入一个函数)var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(function () {
console.log(this); // window
obj.y(); // 2
}, 1000);
上面代码中,obj.y
放在一个匿名函数之中,这使得obj.y
在obj
的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值。
bind
方法,将obj.y
这个方法绑定在obj
上面)var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y.bind(obj), 1000); // 2
自己在总结的时候,自己用ES6声明全局变量x和obj。这样导致输出
undefined
。原因在于:let命令、const命令、class命令
声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩,这一点要特别注意。
const x = 1;
const obj = {
x: 2,
y: () => {
console.log(this); // window
console.log(this.x);
}
};
setTimeout(obj.y, 1000); // undefined
ES6
声明变量的六种方法ES5
只有两种声明变量的方法:var命令和function命令
。ES6
除了添加let和const
命令,后面章节还会提到,另外两种声明变量的方法:import命令和class
命令。所以,ES6
一共有6
种声明变量的方法。
顶层对象,在浏览器环境指的是window
对象,在Node
中指的是global
对象。需要注意的是在ES5
中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是一回事。
需要注意的是:在
ES6
中改变了这一点,一方面规定,为了保持兼容性,var命令和function命令
声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令
声明的全局变量,不属于顶层对象的属性。也就是说,从ES6
开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。
使用setInterval
创建的定时器确保了定时器代码规则地插入队列中。但是该方法的问题在于: 定时器代码可能在代码再次被添加到队列之前还没有执行完成,结果导致定时器代码连续运行好几次,而之间没有任何停顿。然而,javascript
引擎够聪明,能避免这个问题。当使用setInterval
时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
setInterval
函数的用法与setTimeout
完全一致,区别仅仅在于:setInterval
指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。
var timer = setInterval(function() {
console.log(2);
}, 1000)
上面代码中,每隔1秒就输出一个2,会无限运行下去,直到关闭当前窗口。与setTimeout
一样,除了前两个参数,setInterval
方法还可以接受更多的参数,它们会传入回调函数。
// 通过setInterval方法实现网页动画的例子。
var div = document.querySelector('#box');
var opacity = 1;
var fader = setInterval(() => {
opacity -= 0.1;
if (opacity >= 0) {
div.style.opacity = opacity;
}
else {
clearInterval(fader);
}
}, 100);
上面代码每隔100毫秒,设置一次div元素的透明度,直至其完全透明为止。
setInterval
的一个常见用途是:实现轮询。
// 轮询URL的Hash值是否发生变化
var hash = window.location.hash;
var hashWatcher = setInterval(function() {
if (window.location.hash != hash) {
updatePage();
}
}, 1000);
需要注意的是:
setInterval
指定的是函数开始执行之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如:setInterval
指定某个函数每100ms
执行一次,函数每次执行需要5ms
,那么第一次执行结束后95
毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。
如上图所示:重复定时器有两个问题:1.某些间隔会被跳过;2.多个定时器的代码执行之间的间隔可能会比预期的小。假设,某个onclick
事件处理程序使用setInterval
设置了一个200ms
间隔的重复定时器。如果事件处理程序花费了300ms
多一点的时间完成,同时定时器代码也花费了差不多的时间,就会跳过一个间隔同时运行着一个定时器代码。
在上图的例子中:第一个定时器在205ms
时被添加到队列中,但是直到过了300ms
处才能够执行。当执行这个定时器代码时,在405ms
处又给队列添加了另外一个副本。在下一个间隔,即605ms
处。第一个定时器代码扔在运行,同时在队列中已经存在一个定时器代码的实例。结果导致在这个时间点上的定时器代码不会被添加到队列中。同时,当5ms
处添加的定时器代码结束后,405ms
处添加的定时器代码就立刻执行。
为了避免
setInterval
的这两个缺点,确保两次执行之间有固定的间隔,可以使用链式setTimeout
,即每次执行结束后,使用setTimeout
指定下一次执行的具体时间。
// 主要用于重复定时器
var timer = setTimeout(function () {
// 处理中
timer = setTimeout(arguments.callee, 2000);
}, 2000);
上述代码中,链式调用了setTimeout
。每次函数执行的时候都会创建一个新的定时器。第二个setTimeout
调用使用了arguments.callee
来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做的好处是:在前一个定时器代码执行完成之前,不会向队列中插入新的定时器代码,确保不会有任何缺失的间隔。而且,可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免连续的运行。上面代码可以确保,下一次执行总是在本次执行结束之后的2秒开始。
setTimeout和setInterval
函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout和clearInterval
函数,就可以取消对应的定时器。
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
上面代码中,回调函数f不会再执行了,因为两个定时器都被取消了。
setTimeout和setInterval
返回的整数值是连续的,也就是说,第二个setTimeout
方法返回的整数值,将比第一个的整数值大1。
function f() {}
setTimeout(f, 1000) // 10
setTimeout(f, 1000) // 11
setTimeout(f, 1000) // 12
上面代码中,连续调用三次setTimeout,返回值都比上一次大了1。
利用这一点,可以写一个函数,取消当前所有的setTimeout定时器。
(function() {
var gid = setInterval(clearAllTimeouts, 0);
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
上面代码中,先调用
setTimeout
,得到一个计算器编号,然后把编号比它小的计数器全部取消。
有时,我们不希望回调函数被频繁调用。比如:用户填入网页输入框的内容,希望通过Ajax
方法传回服务器,jQuery
的写法如下:
$('textarea').on('keydown', ajaxAction);
这样写有一个很大的缺点是:如果用户连续击键,就会连续触发
keydown
事件,造成大量的Ajax
通信。这是不必要的,而且很可能产生性能问题。正确的做法应该是,设置一个门槛值,表示两次Ajax
通信的最小间隔时间。如果在间隔时间内,发生新的keydown
事件,则不触发Aja
通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown
事件,再将数据发送出去。这种做法叫做debounce(防抖动)。假定两次Ajax
通信的间隔不得小于2500毫秒,上面的代码可以改写成下面这样。
$('textarea').on('keydown', debounce(ajaxAction, 2500));
function debounce(fn, delay){
var timer = null; // 声明计时器
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
上面代码中,只要在2500毫秒之内,用户再次击键,就会取消上一次的定时器,然后再新建一个定时器。这样就保证了回调函数之间的调用间隔,至少是2500毫秒。
setTimeout
和setInterval
的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
这意味着:
setTimeout和setInterval
指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的。所以,没有办法保证setTimeout和setInterval
指定的任务一定会按照预定时间执行。
setTimeout(someTask, 100);
veryLongTask();
上面代码的setTimeout
,指定100ms
以后运行一个任务。但是,如果后面的veryLongTask
函数(同步任务)运行时间非常长,过了100ms
还无法结束,那么被推迟运行的someTask
就只有等着,等到veryLongTask
运行结束,才轮到它执行。
setInterval(function () {
console.log(2);
}, 1000);
sleep(3000);
function sleep(ms) {
var start = Date.now();
while ((Date.now() - start) < ms) {
}
}
上面代码中,setInterval
要求每隔1秒,就输出一个2。但是,紧接着的sleep
语句需要3秒才能完成,那么setInterval
就必须推迟到3秒之后才开始生效。特别注意: 生效后setInterval
不会产生累积效应,即不会一下子输出三个2,而是只会输出一个2。
setTimeout
的作用是:将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0)
,那么会立刻执行吗?
答案是不会。因为
setTimeout
指定的回调函数f,必须要等到当前脚本的同步任务全部处理完以后才会执行。也就是说,setTimeout(f, 0)
会在下一轮事件循环一开始就执行。
setTimeout(() => {
console.log(1);
}, 0);
console.log(2);
// 2
// 1
上面代码先输出2,再输出1。因为2是同步任务,在本轮事件循环执行,而1是下一轮事件循环执行。总之,
setTimeout(f, 0)
这种写法的目的是,尽可能早地执行f,但是并不能保证立刻就执行f。
setTimeout(f, 0)
有几个非常重要的用途。它的一大应用是:可以调整事件的发生顺序。 比如:网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果想让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)
。
<body>
<input id="btn" type="button" value="click">
<script>
var btn = document.querySelector('#btn');
btn.addEventListener('click', function() {
btn.value += ' input';
}, false);
document.body.addEventListener('click', function() {
btn.value += ' body';
}, false);
</script>
</body>
上面代码按照常规的事件冒泡机制触发。
<body>
<input id="btn" type="button" value="click">
<script>
var btn = document.querySelector('#btn');
btn.addEventListener('click', function() {
setTimeout(function() {
btn.value += ' input';
})
}, false);
document.body.addEventListener('click', function() {
btn.value += ' body';
}, false);
</script>
</body>
上面代码在点击按钮后,
setTimeout
将子元素的回调函数推迟到下一轮事件循环执行,这样就起到了先触发父元素的回调函数的目的了。
另一个应用是:用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,
keypress
事件会在浏览器接收文本之前触发。因此,下面的回调函数是达不到目的的。如下图所示:
<body>
<input type="text" id="txt">
<script>
var txt = document.querySelector('#txt');
function handleKeyPress(e) {
e.target.value = e.target.value.toUpperCase();
}
txt.addEventListener('keypress', handleKeyPress, false);
</script>
</body>
上面代码想在用户每次输入文本后,立即将字符转为大写。但是实际上,它只能将本次输入前的字符转为大写,因为浏览器此时还没接收到新的文本,所以
this.value
取不到最新输入的那个字符。只有用setTimeout
改写,上面的代码才能发挥作用。如下图所示:
<body>
<input type="text" id="txt">
<script>
var txt = document.querySelector('#txt');
function handleKeyPress(e) {
setTimeout(function() {
e.target.value = e.target.value.toUpperCase();
}, 0);
}
txt.addEventListener('keypress', handleKeyPress, false);
</script>
</body>
上面代码将代码放入
setTimeout
之中,就能使得它在浏览器接收到文本之后触发。
由于
setTimeout(f, 0)
实际上意味着:将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f, 0)
里面执行。
<body>
<div id="box" style="width: 100px;height: 100px;"></div>
<script>
var box = document.querySelector('#box');
// 写法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
div.style.backgroundColor = '#' + i.toString(16);
}
// 写法二
var timer = null;
var i = 0x100000;
function fn() {
timer = setTimeout(fn, 0);
box.style.backgroundColor = '#' + i.toString(16);
if(i++ === 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(fn, 0);
</script>
</body>
上面代码有两种写法都是改变一个网页元素的背景色。写法一会造成浏览器堵塞,因为JavaScript执行速度远高于DOM,会造成大量DOM操作堆积,而写法二就不会,这就是
setTimeout(f, 0)
的好处。
另一个使用这种技巧的例子是:代码高亮的处理。如果代码块很大,一次性处理,可能会对性能造成很大的压力,那么将其分成一个个小块,一次处理一块,比如写成
setTimeout(highlightNext, 50)
的样子,性能压力就会减轻。
[TOC]
// 执行npm link
/usr/local/bin/hello -> /usr/local/lib/node_modules/node-cli/bin/hello.js
/usr/local/lib/node_modules/node-cli -> /Users/liujie26/study/node/expressDemo/node-cli
开发NPM模块的时候,有时我们会希望,边开发边试用,比如本地调试的时候,require('myModule')
会自动加载本机开发中的模块。Node规定,使用一个模块时,需要将其安装到全局的或项目的node_modules
目录之中。对于开发中的模块,解决方法就是在全局的node_modules
目录之中,生成一个符号链接,指向模块的本地目录。
npm link
就能起到这个作用,会自动建立这个符号链接。
请设想这样一个场景,你开发了一个模块myModule,目录为src/myModule
,你自己的项目myProject要用到这个模块,项目目录为src/myProject
。首先,在模块目录(src/myModule
)下运行npm link命令。
src/myModule$ npm link
上面的命令会在NPM的全局模块目录内,生成一个符号链接文件,该文件的名字就是package.json文件中指定的模块名。
/path/to/global/node_modules/myModule -> src/myModule
这个时候,已经可以全局调用myModule模块了。但是,如果我们要让这个模块安装在项目内,还要进行下面的步骤。
切换到项目目录,再次运行npm link命令,并指定模块名。
src/myProject$ npm link myModule
上面命令等同于生成了本地模块的符号链接。
src/myProject/node_modules/myModule -> /path/to/global/node_modules/myModule
然后,就可以在你的项目中,加载该模块了。
var myModule = require('myModule');
这样一来,myModule的任何变化,都可以直接反映在myProject项目之中。但是,这样也出现了风险,任何在myProject目录中对myModule的修改,都会反映到模块的源码中。
如果你的项目不再需要该模块,可以在项目目录内使用npm unlink命令,删除符号链接。
src/myProject$ npm unlink myModule
#! /usr/bin/env node
console.log(process.argv);
const argv = process.argv.slice(2);
console.log(argv);
const name = argv[0];
console.log(`hello, ${name}`);
# 执行 hello liujie
# 结果如下:
[ '/usr/local/bin/node', '/usr/local/bin/hello', 'liujie' ]
[ 'liujie' ]
hello, liujie
命令行参数可通过系统变量
process.argv
获取。process.argv
返回一个数组 第一个是node 第二个是脚本文件 第三个是输入的参数,process.argv[2]
开始得到才是真正的参数部分。
对于参数处理,我们一般使用commander,commander是一个轻巧的nodejs模块,提供了用户命令行输入和参数解析强大功能如:自记录代码、自动生成帮助、合并短参数(“ABC”==“-A-B-C”)、默认选项、强制选项、命令解析、提示符。
#!/usr/bin/env node
var program = require('commander');
program
.version('0.0.1')
.option('-p, --peppers', 'Add peppers')
.option('-P, --pineapple', 'Add pineapple')
.option('-b, --bbq-sauce', 'Add bbq sauce')
.option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
.parse(process.argv);
console.log('you ordered a pizza with:');
if (program.peppers) console.log(' - peppers');
if (program.pineapple) console.log(' - pineapple');
if (program.bbqSauce) console.log(' - bbq');
console.log(' - %s cheese', program.cheese);
// 安装相关依赖
npm install commander superagent cli-table2 --save
// 新建bin/translator.js文件,并加入package.json文件中
"bin": {
"translator": "bin/translator.js"
}
然后执行:
npm link
#! /usr/bin/env node
// 引入需要的模块
const program = require('commander');
const Table = require('cli-table2'); // 表格输出
const superagent = require('superagent'); // http请求
// 初始化commander
program
.allowUnknownOption()
.version('0.0.1')
.usage('translator <cmd> [input]');
// 有道api
const API = 'http://fanyi.youdao.com/openapi.do?keyfrom=toaijf&key=868480929&type=data&doctype=json&version=1.1';
// 添加自定义命令
program
.command('query')
.description('翻译输入')
.action((word) => {
// 发起请求
superagent.get(API)
.query({ q: word})
.end((err, res) => {
if(err){
console.log('excuse me, try again');
return false;
}
const data = JSON.parse(res.text);
const result = {};
// 返回的数据处理
if(data.basic) {
result[word] = data['basic']['explains'];
}
else if(data.translation) {
result[word] = data['translation'];
}
else {
console.error('error');
}
// 输出表格
const table = new Table();
table.push(result);
console.log(table.toString());
})
})
// 没有参数时显示帮助信息
if (!process.argv[2]) {
program.help();
console.log();
}
program.parse(process.argv);
// 执行:translator query food
┌──────┬───────────────┐
│ food │ n. 食物;养料 │
cheerio
可理解为服务器端的jQuery
,基本用法与jQuery一样。有了它,我们在写小爬虫时就可抛开那可爱又可恨的正则表达式了。从此拥抱幸福生活。具体用法如下:
const cheerio = require('cheerio')
const $ = cheerio.load('<h2 class="title">Hello world</h2>');
$('h2.title').text('Hello there!');
$('h2').addClass('welcome');
$.html();
//=> <h2 class="title welcome">Hello there!</h2>
superagent
专注于处理服务端/客户端的http请求,用法如下:
request
.get(url)
.end((err, res) => {
});
mkdir joke-cli
cd joke-cli
npm init
npm install cheerio superagent colors --save //colors 输出美化
新建
bin/index.js
文件,并加入package.json
文件中:
"bin": {
"joke-cli": "./bin/index.js"
}
执行
npm link
:
#!/usr/bin/env node
const superAgent = require('superagent');
const cheerio = require('cheerio');
const readline = require('readline');
const colors = require('colors');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '您正在使用joke-cli,按回车键查看笑话>>>'
});
const baseUrl = 'https://www.qiushibaike.com/text/page/';
let page = 1;
// 使用数组来存放笑话
const jokeArr = [];
// 获取笑话并存入数组中
function getJokes() {
// 数组中的笑话不足三条时就请求下一页的数据
if (jokeArr.length < 3) {
superAgent
.get(baseUrl + page)
.end((err, res) => {
if(err) console.error(err);
const $ = cheerio.load(res.text);
const jokeList = $('.article .content span');
jokeList.each((index, element) => {
jokeArr.push($(element).text()); // 存入数组
});
page++;
});
}
}
rl.prompt();
getJokes();
// line事件 每当 input 流接收到接收行结束符(\n、\r 或 \r\n)时触发 'line' 事件。 通常发生在用户按下 <Enter> 键或 <Return> 键。
// 按下回车键显示一条笑话
rl.on('line', (line) => {
if(jokeArr.length > 0) {
console.log('======================');
console.log(jokeArr.shift().bgCyan.black); //用colors模块改变输出颜色
getJokes();
}
else {
console.log('正在加载中~~~'.green);
}
rl.prompt();
}).on('close', () => {
console.log('Bye!');
process.exit(0);
});
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.