兄弟,大写这个问题解决了吗
没有足够的数据
(゚∀゚ )
暂时没有任何数据
w世说心语 赞了问题 · 2020-10-27
在Mac上使用VS Code时,在命令行上发现了一些问题,包括
1,在使用自带的简体拼音输入法时,开启caps lock键后,并不会转为输入小写字母,而是大写字母;
2,使用shift+方向键时,会直接输出;2A/;2B/;2C/;2D(这个问题在无输入法下也会有)
这些问题在其他app或是vscode其他位置(如setting和editor)并不会有,只有在命令行中有。将bash改为zsh并不会解决,自带的Terminal.app也无此问题
VS Code版本: 1.31.1
macOS版本: 10.14.3
在Mac上使用VS Code时,在命令行上发现了一些问题,包括1,在使用自带的简体拼音输入法时,开启caps lock键后,并不会转为输入小写字母,而是大写字母;2,使用shift+方向键时,会直接输出;2A/;2B/;2C/;2D(这个问题在无输入法下也会有)
关注 3 回答 3
w世说心语 提出了问题 · 2020-08-28
请问有没有在微拍堂工作过的小伙伴呢,工作是否天天加班,管理严格,一不小心就被开除的那种呢
请问有没有在微拍堂工作过的小伙伴呢,工作是否天天加班,管理严格,一不小心就被开除的那种呢
关注 1 回答 0
w世说心语 提出了问题 · 2020-07-13
请问script整体代码是宏任务怎么理解呢,是说像console.log()这样的同步代码也应该属于宏任务吗?还请大神指点
请问script整体代码是宏任务怎么理解呢,是说像console.log()这样的同步代码也应该属于宏任务吗?还请大神指点
关注 2 回答 2
w世说心语 关注了问题 · 2020-05-08
1.参数“_”隐式具有“any”类型。
2.绑定元素“call”隐式具有“any”类型。
我给参数后面加上any类型后就不报错了
这样的解决办法肯定不是最好的,请问各位大神这样的报错应该怎么解决呢,多谢多谢!
关注 1 回答 0
w世说心语 提出了问题 · 2020-05-08
1.参数“_”隐式具有“any”类型。
2.绑定元素“call”隐式具有“any”类型。
我给参数后面加上any类型后就不报错了
这样的解决办法肯定不是最好的,请问各位大神这样的报错应该怎么解决呢,多谢多谢!
1.参数“_”隐式具有“any”类型。2.绑定元素“call”隐式具有“any”类型。 我给参数后面加上any类型后就不报错了 这样的解决办法肯定不是最好的,请问各位大神这样的报错应该怎么解决呢,多谢多谢!
关注 1 回答 0
w世说心语 收藏了文章 · 2020-04-29
Webpack是现在主流的功能强大的模块化打包工具,在使用Webpack时,如果不注意性能优化,有非常大的可能会产生性能问题,性能问题主要分为开发时打包构建速度慢、开发调试时的重复性工作、以及输出文件质量不高等,因此性能优化也主要从这些方面来分析。本文主要是根据自己的理解对《深入浅出Webpack》这本书进行总结,涵盖了大部分的优化方法,可以作为Webpack性能优化时的参考和检查清单。基于Webpack3.4版本,阅读本文需要您熟悉Webpack基本使用方法,读完大约需要三十分钟。
by MaryTien from http://supermaryy.com
Webpack在启动后会根据Entry配置的入口出发,递归地解析所依赖的文件。这个过程分为搜索文件和把匹配的文件进行分析、转化的两个过程,因此可以从这两个角度来进行优化配置。
搜索过程优化方式包括:
resolve
字段告诉webpack怎么去搜索文件,所以首先要重视resolve字段的配置:
resolve.modules:[path.resolve(__dirname, 'node_modules')]
避免层层查找。resolve.modules
告诉webpack去哪些目录下寻找第三方模块,默认值为['node_modules']
,会依次查找./node_modules、../node_modules、../../node_modules。
resolve.mainFields:['main']
,设置尽量少的值可以减少入口文件的搜索步骤第三方模块为了适应不同的使用环境,会定义多个入口文件,mainFields定义使用第三方模块的哪个入口文件,由于大多数第三方模块都使用main字段描述入口文件的位置,所以可以设置单独一个main值,减少搜索
对庞大的第三方模块设置resolve.alias
, 使webpack直接使用库的min文件,避免库内解析
如对于react:
resolve.alias:{
'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
这样会影响Tree-Shaking,适合对整体性比较强的库使用,如果是像lodash这类工具类的比较分散的库,比较适合Tree-Shaking,避免使用这种方式。
合理配置resolve.extensions
,减少文件查找
默认值:extensions:['.js', '.json']
,当导入语句没带文件后缀时,Webpack会根据extensions定义的后缀列表进行文件查找,所以:
require(./data)
要写成require(./data.json)
module.noParse
字段告诉Webpack不必解析哪些文件,可以用来排除对非模块化库文件的解析如jQuery、ChartJS,另外如果使用resolve.alias配置了react.min.js,则也应该排除解析,因为react.min.js经过构建,已经是可以直接运行在浏览器的、非模块化的文件了。noParse值可以是RegExp、[RegExp]、function
module:{ noParse:[/jquery|chartjs/, /react\.min\.js$/] }
DllPlugin动态链接库插件,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取。为什么会提升构建速度呢?原因在于dll中大多包含的是常用的第三方模块,如react、react-dom,所以只要这些模块版本不升级,就只需被编译一次。我认为这样做和配置resolve.alias和module.noParse的效果有异曲同工的效果。
使用方法:
使用DllPlugin配置一个webpack_dll.config.js来构建dll文件:
// webpack_dll.config.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
entry:{
react:['react','react-dom'],
polyfill:['core-js/fn/promise','whatwg-fetch']
},
output:{
filename:'[name].dll.js',
path:path.resolve(__dirname, 'dist'),
library:'_dll_[name]', //dll的全局变量名
},
plugins:[
new DllPlugin({
name:'_dll_[name]', //dll的全局变量名
path:path.join(__dirname,'dist','[name].manifest.json'),//描述生成的manifest文件
})
]
}
需要注意DllPlugin的参数中name值必须和output.library值保持一致,并且生成的manifest文件中会引用output.library值。
最终构建出的文件:
|-- polyfill.dll.js
|-- polyfill.manifest.json
|-- react.dll.js
└── react.manifest.json
其中xx.dll.js包含打包的n多模块,这些模块存在一个数组里,并以数组索引作为ID,通过一个变量假设为_xx_dll暴露在全局中,可以通过window._xx_dll访问这些模块。xx.manifest.json文件描述dll文件包含哪些模块、每个模块的路径和ID。然后再在项目的主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件。
在主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件:
//webpack.config.json
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry:{ main:'./main.js' },
//... 省略output、loader等的配置
plugins:[
new DllReferencePlugin({
manifest:require('./dist/react.manifest.json')
}),
new DllReferenctPlugin({
manifest:require('./dist/polyfill.manifest.json')
})
]
}
最终构建生成main.js
在整个构建流程中,最耗时的就是Loader对文件的转换操作了,而运行在Node.js之上的Webpack是单线程模型的,也就是只能一个一个文件进行处理,不能并行处理。HappyPack可以将任务分解给多个子进程,最后将结果发给主进程。JS是单线程模型,只能通过这种多进程的方式提高性能。
HappyPack使用如下:
npm i -D happypack
// webpack.config.json
const path = require('path');
const HappyPack = require('happypack');
module.exports = {
//...
module:{
rules:[{
test:/\.js$/,
use:['happypack/loader?id=babel']
exclude:path.resolve(__dirname, 'node_modules')
},{
test:/\.css/,
use:['happypack/loader?id=css']
}],
plugins:[
new HappyPack({
id:'babel',
loaders:['babel-loader?cacheDirectory']
}),
new HappyPack({
id:'css',
loaders:['css-loader']
})
]
}
}
除了id和loaders,HappyPack还支持这三个参数:threads、verbose、threadpool
,threadpool代表共享进程池,即多个HappyPack实例都用同个进程池中的子进程处理任务,以防资源占用过多。
使用UglifyJS插件压缩JS代码时,需要先将代码解析成Object表示的AST(抽象语法树),再去应用各种规则去分析和处理AST,所以这个过程计算量大耗时较多。ParallelUglifyPlugin可以开启多个子进程,每个子进程使用UglifyJS压缩代码,可以并行执行,能显著缩短压缩时间。
使用也很简单,把原来的UglifyJS插件换成本插件即可,使用如下:
npm i -D webpack-parallel-uglify-plugin
// webpack.config.json
const ParallelUglifyPlugin = require('wbepack-parallel-uglify-plugin');
//...
plugins: [
new ParallelUglifyPlugin({
uglifyJS:{
//...这里放uglifyJS的参数
},
//...其他ParallelUglifyPlugin的参数,设置cacheDir可以开启缓存,加快构建速度
})
]
开发过程中修改源码后,需要自动构建和刷新浏览器,以查看效果。这个过程可以使用Webpack实现自动化,Webpack负责监听文件的变化,DevServer负责刷新浏览器。
Webpack可以使用两种方式开启监听:1. 启动webpack时加上--watch参数;2. 在配置文件中设置watch:true。此外还有如下配置参数。合理设置watchOptions可以优化监听体验。
module.exports = {
watch: true,
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300, //文件变动后多久发起构建,越大越好
poll: 1000, //每秒询问次数,越小越好
}
}
ignored:设置不监听的目录,排除node_modules后可以显著减少Webpack消耗的内存
aggregateTimeout:文件变动后多久发起构建,避免文件更新太快而造成的频繁编译以至卡死,越大越好
poll:通过向系统轮询文件是否变化来判断文件是否改变,poll为每秒询问次数,越小越好
DevServer刷新浏览器有两种方式:
默认情况下,以及 devserver: {inline:true}
都是采用第一种方式刷新页面。第一种方式DevServer因为不知道网页依赖哪些Chunk,所以会向每个chunk中都注入客户端代码,当要输出很多chunk时,会导致构建变慢。而一个页面只需要一个客户端,所以关闭inline模式可以减少构建时间,chunk越多提升月明显。关闭方式:
devserver:{inline:false}
关闭inline后入口网址变为http://localhost:8080/webpack-dev-server/
另外devServer.compress
参数可配置是否采用Gzip压缩,默认为false
模块热替换不刷新整个网页而只重新编译发生变化的模块,并用新模块替换老模块,所以预览反应更快,等待时间更少,同时不刷新页面能保留当前网页的运行状态。原理也是向每一个chunk中注入代理客户端来连接DevServer和网页。开启方式:
开启后如果修改子模块就可以实现局部刷新,但如果修改的是根JS文件,会整页刷新,原因在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,然后执行回调函数。如果一层层向外抛直到最外层都没有文件接收,就会刷新整页。
使用 NamedModulesPlugin
可以使控制台打印出被替换的模块的名称而非数字ID,另外同webpack监听,忽略node_modules目录的文件可以提升性能。
代码运行环境分为开发环境和生产环境,代码需要根据不同环境做不同的操作,许多第三方库中也有大量的根据开发环境判断的if else代码,构建也需要根据不同环境输出不同的代码,所以需要一套机制可以在源码中区分环境,区分环境之后可以使输出的生产环境的代码体积减小。Webpack中使用DefinePlugin插件来定义配置文件适用的环境。
const DefinePlugin = require('webpack/lib/DefinePlugin');
//...
plugins:[
new DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})
]
注意,JSON.stringify('production')
的原因是,环境变量值需要一个双引号包裹的字符串,而stringify后的值是'"production"'
然后就可以在源码中使用定义的环境:
if(process.env.NODE_ENV === 'production'){
console.log('你在生产环境')
doSth();
}else{
console.log('你在开发环境')
doSthElse();
}
当代码中使用了process时,Webpack会自动打包进process模块的代码以支持非Node.js的运行环境,这个模块的作用是模拟Node.js中的process,以支持process.env.NODE_ENV === 'production'
语句。
压缩JS:Webpack内置UglifyJS插件、ParallelUglifyPlugin
会分析JS代码语法树,理解代码的含义,从而做到去掉无效代码、去掉日志输入代码、缩短变量名等优化。常用配置参数如下:
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
//...
plugins: [
new UglifyJSPlugin({
compress: {
warnings: false, //删除无用代码时不输出警告
drop_console: true, //删除所有console语句,可以兼容IE
collapse_vars: true, //内嵌已定义但只使用一次的变量
reduce_vars: true, //提取使用多次但没定义的静态值到变量
},
output: {
beautify: false, //最紧凑的输出,不保留空格和制表符
comments: false, //删除所有注释
}
})
]
使用webpack --optimize-minimize
启动webpack,可以注入默认配置的UglifyJSPlugin
压缩ES6:第三方UglifyJS插件
随着越来越多的浏览器支持直接执行ES6代码,应尽可能的运行原生ES6,这样比起转换后的ES5代码,代码量更少,且ES6代码性能更好。直接运行ES6代码时,也需要代码压缩,第三方的uglify-webpack-plugin提供了压缩ES6代码的功能:
npm i -D uglify-webpack-plugin@beta //要使用最新版本的插件
//webpack.config.json
const UglifyESPlugin = require('uglify-webpack-plugin');
//...
plugins:[
new UglifyESPlugin({
uglifyOptions: { //比UglifyJS多嵌套一层
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
},
output: {
beautify: false,
comments: false
}
}
})
]
另外要防止babel-loader转换ES6代码,要在.babelrc中去掉babel-preset-env,因为正是babel-preset-env负责把ES6转换为ES5。
cssnano基于PostCSS,不仅是删掉空格,还能理解代码含义,例如把color:#ff0000
转换成 color:red
,css-loader内置了cssnano,只需要使用 css-loader?minimize
就可以开启cssnano压缩。
另外一种压缩CSS的方式是使用PurifyCSSPlugin,需要配合 extract-text-webpack-plugin
使用,它主要的作用是可以去除没有用到的CSS代码,类似JS的Tree Shaking。
Tree Shaking可以剔除用不上的死代码,它依赖ES6的import、export的模块化语法,最先在Rollup中出现,Webpack 2.0将其引入。适合用于Lodash、utils.js等工具类较分散的文件。它正常工作的前提是代码必须采用ES6的模块化语法,因为ES6模块化语法是静态的(在导入、导出语句中的路径必须是静态字符串,且不能放入其他代码块中)。如果采用了ES5中的模块化,例如module.export = {...}、require( x+y )、if (x) { require( './util' ) },则Webpack无法分析出可以剔除哪些代码。
启用Tree Shaking:
修改.babelrc以保留ES6模块化语句:
{
"presets": [
[
"env",
{ "module": false }, //关闭Babel的模块转换功能,保留ES6模块化语法
]
]
}
resolve.mainFields: ['jsnext:main', 'main']
以指明解析第三方库代码时,采用ES6模块化的代码入口CND加速的原理
CDN通过将资源部署到世界各地,使得用户可以就近访问资源,加快访问速度。要接入CDN,需要把网页的静态资源上传到CDN服务上,在访问这些资源时,使用CDN服务提供的URL。
由于CDN会为资源开启长时间的缓存,例如用户从CDN上获取了index.html,即使之后替换了CDN上的index.html,用户那边仍会在使用之前的版本直到缓存时间过期。业界做法:
另外,HTTP1.x版本的协议下,浏览器会对于向同一域名并行发起的请求数限制在4~8个。那么把所有静态资源放在同一域名下的CDN服务上就会遇到这种限制,所以可以把他们分散放在不同的CDN服务上,例如JS文件放在js.cdn.com下,将CSS文件放在css.cdn.com下等。这样又会带来一个新的问题:增加了域名解析时间,这个可以通过dns-prefetch来解决 <link rel='dns-prefetch' href='//js.cdn.com'>
来缩减域名解析的时间。形如//xx.com
这样的URL省略了协议,这样做的好处是,浏览器在访问资源时会自动根据当前URL采用的模式来决定使用HTTP还是HTTPS协议。
总之,构建需要满足以下几点:
最终配置:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, 'dist'),
publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
rules:[{
test: /\.css/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'],
publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
}),
},{
test: /\.png/,
use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值
}]
},
plugins:[
new WebPlugin({
template: './template.html',
filename: 'index.html',
stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
}),
new ExtractTextPlugin({
filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
})
]
大型网站通常由多个页面组成,每个页面都是一个独立的单页应用,多个页面间肯定会依赖同样的样式文件、技术栈等。如果不把这些公共文件提取出来,那么每个单页打包出来的chunk中都会包含公共代码,相当于要传输n份重复代码。如果把公共文件提取出一个文件,那么当用户访问了一个网页,加载了这个公共文件,再访问其他依赖公共文件的网页时,就直接使用文件在浏览器的缓存,这样公共文件就只用被传输一次。
应用方法
把多个页面依赖的公共代码提取到common.js中,此时common.js包含基础库的代码
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
//...
plugins:[
new CommonsChunkPlugin({
chunks:['a','b'], //从哪些chunk中提取
name:'common', // 提取出的公共部分形成一个新的chunk
})
]
找出依赖的基础库,写一个base.js文件,再与common.js提取公共代码到base中,common.js就剔除了基础库代码,而base.js保持不变
//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
entry:{
base: './base.js'
},
plugins:[
new CommonsChunkPlugin({
chunks:['base','common'],
name:'base',
//minChunks:2, 表示文件要被提取出来需要在指定的chunks中出现的最小次数,防止common.js中没有代码的情况
})
]
页面引用顺序如下:base.js--> common.js--> xx.js
原理
单页应用的一个问题在于使用一个页面承载复杂的功能,要加载的文件体积很大,不进行优化的话会导致首屏加载时间过长,影响用户体验。做按需加载可以解决这个问题。具体方法如下:
做法
一个最简单的例子:网页首次只加载main.js,网页展示一个按钮,点击按钮时加载分割出去的show.js,加载成功后执行show.js里的函数
//main.js
document.getElementById('btn').addEventListener('click',function(){
import(/* webpackChunkName:"show" */ './show').then((show)=>{
show('Webpack');
})
})
//show.js
module.exports = function (content) {
window.alert('Hello ' + content);
}
import(/* webpackChunkName:show */ './show').then()
是实现按需加载的关键,Webpack内置对import( *)语句的支持,Webpack会以./show.js
为入口重新生成一个Chunk。代码在浏览器上运行时只有点击了按钮才会开始加载show.js,且import语句会返回一个Promise,加载成功后可以在then方法中获取加载的内容。这要求浏览器支持Promise API,对于不支持的浏览器,需要注入Promise polyfill。/* webpackChunkName:show */
是定义动态生成的Chunk的名称,默认名称是[id].js,定义名称方便调试代码。为了正确输出这个配置的ChunkName,还需要配置Webpack:
//...
output:{
filename:'[name].js',
chunkFilename:'[name].js', //指定动态生成的Chunk在输出时的文件名称
}
书中另外提供了更复杂的React-Router中异步加载组件的实战场景。P212
Prepack是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。通过在便一阶段预先执行源码来得到执行结果,再直接将运行结果输出以提升性能。但是现在Prepack还不够成熟,用于线上环境还为时过早。
使用方法
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
module.exports = {
plugins:[
new PrepackWebpackPlugin()
]
}
译作“作用域提升”,是在Webpack3中推出的功能,它分析模块间的依赖关系,尽可能将被打散的模块合并到一个函数中,但不能造成代码冗余,所以只有被引用一次的模块才能被合并。由于需要分析模块间的依赖关系,所以源码必须是采用了ES6模块化的,否则Webpack会降级处理不采用Scope Hoisting。
使用方法
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
//...
plugins:[
new ModuleConcatenationPlugin();
],
resolve:{
mainFields:['jsnext:main','browser','main']
}
webpack --display-optimization-bailout
输出日志中会提示哪个文件导致了降级处理
启动Webpack时带上这两个参数可以生成一个json文件,输出分析工具大多依赖该文件进行分析:
webpack --profile --json > stats.json
其中 --profile
记录构建过程中的耗时信息,--json
以JSON的格式输出构建结果,>stats.json
是UNIX / Linux系统中的管道命令,含义是将内容通过管道输出到stats.json文件中。
打开该工具的官网http://webpack.github.io/anal...,就可以得到分析结果
webpack-bundle-analyzer
可视化分析工具,比Webapck Analyse更直观。使用也很简单:
webpack-bundle-analyzer
,浏览器会自动打开结果分析页面。use: [‘babel-loader?cacheDirectory’]
cacheDirectory用于缓存babel的编译结果,加快重新编译的速度。另外注意排除node_modules文件夹,因为文件都使用了ES5的语法,没必要再使用Babel转换。Webpack是现在主流的功能强大的模块化打包工具,在使用Webpack时,如果不注意性能优化,有非常大的可能会产生性能问题,性能问题主要分为开发时打包构建速度慢、开发调试时的重复性工作、以及输出文件质量不高等,因此性能优化也主要从这些方面来分析。本文主要是根...
w世说心语 提出了问题 · 2020-04-26
IE:IE9
谷歌:?
火狐:?
Opera:?
360:?
请各位大神告知,小弟在此谢过
IE:IE9谷歌:?火狐:?Opera:?360:?请各位大神告知,小弟在此谢过
关注 1 回答 0
w世说心语 提出了问题 · 2020-04-26
现在公司有新的项目要开始了,想要使用React16.4版本来开发,但需要考虑尽可能兼容低版本的浏览器,想问下React16.4版本能兼容ie9吗
现在公司有新的项目要开始了,想要使用React16.4版本来开发,但需要考虑尽可能兼容低版本的浏览器,想问下React16.4版本能兼容ie9吗
关注 1 回答 0
w世说心语 赞了文章 · 2020-04-02
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。
广义的跨域:
1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等
其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。
什么是同源策略?
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送
URL 说明 是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js 同一域名,不同文件或路径 允许
http://www.domain.com/lab/c.js
http://www.domain.com:8000/a.js
http://www.domain.com/b.js 同一域名,不同端口 不允许
http://www.domain.com/a.js
https://www.domain.com/b.js 同一域名,不同协议 不允许
http://www.domain.com/a.js
http://192.168.4.12/b.js 域名和域名对应相同ip 不允许
http://www.domain.com/a.js
http://x.domain.com/b.js 主域相同,子域不同 不允许
http://domain.com/c.js
http://www.domain1.com/a.js
http://www.domain2.com/b.js 不同域名 不允许
1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
1.)原生实现:
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数):
handleCallback({"status": true, "user": "admin"})
2.)jquery ajax:
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {}
});
3.)vue.js:
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
后端node.js代码示例:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
jsonp缺点:只能实现get一种请求。
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1.)父窗口:(http://www.domain.com/a.html)
<iframe id="iframe" data-original="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
2.)子窗口:(http://child.domain.com/b.html)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
1.)a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" data-original="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2.)b.html:(http://www.domain2.com/b.html)
<iframe id="iframe" data-original="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3.)c.html:(http://www.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
1.)a.html:(http://www.domain1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
2.)proxy.html:(http://www.domain1.com/proxy....
中间代理页,与a.html同域,内容为空即可。
3.)b.html:(http://www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
1.)a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" data-original="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
2.)b.html:(http://www.domain2.com/b.html)
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。
1.)原生ajax
// 前端设置是否带cookie
xhr.withCredentials = true;
示例代码:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
2.)jQuery ajax
$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});
3.)vue框架
a.) axios设置:
axios.defaults.withCredentials = true
b.) vue-resource设置:
Vue.http.options.credentials = true
若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
1.)Java后台:
/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
2.)Nodejs后台示例:
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var postData = '';
// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly的作用是让js无法读取cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen('8080');
console.log('Server is running at port 8080...');
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
nginx具体配置:
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
1.) 前端代码示例:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
2.) Nodejs后台示例:
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
利用node + express + http-proxy-middleware搭建一个proxy服务器。
1.)前端代码示例:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
2.)中间件服务器:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
3.)Nodejs后台同(六:nginx)
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1.)前端代码:
<div>user input:<input type="text"></div>
<script data-original="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
2.)Nodejs socket后台:
var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
查看原文什么是同源策略?同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一...
赞 1424 收藏 2483 评论 59
查看全部 个人动态 →
(゚∀゚ )
暂时没有
(゚∀゚ )
暂时没有
注册于 2019-01-24
个人主页被 361 人浏览
推荐关注