webpack回顾篇
source link: http://blog.poetries.top/2018/11/21/webpack-review/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
大版本变化
1.2 功能进化
Webpack V1
HMR
Webpack V2
Tree Shaking ES module Import
Webpack V3
-
Scope Hoisting
(作用域提升) -
Magic Comments
(配合动态import
使用)
版本迁移
V1 -> V2
迁移指南 https://doc.webpack-china.org/guides/migrating/
V2 -> V3
更新升级即可
二、webpack核心概念
2.1 Entry
- 代码的入口
- 打包的入口
- 单个或多个
写法建议使用键值对写法
module.exports = { entry: 'index.js' }
module.exports = { entry: '[index.js','vendor.js'] }
module.exports = { entry: { index:'index.js' } }
module.exports = { entry: { index:['index.js','app.js'], vendor: 'vendor.js' } }
2.2 Output
bundle CDN
module.exports = { entry: 'index.js', output: { filename: 'index.min.js' } }
module.exports = { entry: { index: 'index.js', vendor: 'vendor.js' }, output: { filename: '[name].min[hash:5].js' } }
2.3 Loaders
- 处理文件
- 转化为模块
module.exports = { module: { rules: [ { test: /\.css$/, use: 'css-loader' } ] } }
2.3.1 常用Loader
编译相关
babel-loader ts-loader
样式相关
style-loader css-loader less-loader postcss-loader
文件相关
file-loader url-loader
2.4 Plugins
- 参与打包整个过程
- 打包优化和压缩
- 配置编译时的变量
- 极其灵活
module.exports = { plugins: [ new webpack.optimize.UglifyJsPlugin() ] }
2.4.1 常用plugins
优化相关
CommonsChunkPlugin UglifyJsWebpackPlugin
功能相关
ExtractTextWebpackPlugin HtmlWebpackPlugin HotModuleReplacementPlugin CopyWebpackPlugin
2.5 名词
Chunk Bundle Module
三、初探 webpack
3.1 使用babel打包es6
3.1.1 编译 ES 6/7
Babel
npm install [email protected] @babel/core npm install –save-dev babel-loader babel-core
Babel Presets
主要有几种类型选择
es2015 es2016 es2017 env babel-preset-react babel-preset-stage 0 - 3
npm install @babel/preset-env –save-dev npm install babel-preset-env –save-dev
Babel Polyfill
针对一些不能处理的函数方法( Generator
、 Set
、 Map
、 Array.from...
)需要用到 babel-Polyfill
处理
- 全局垫片
- 为应用准备
npm install babel-polyfill –save
import “babel-polyfill”
Babel Runtime Transform
- 局部垫片
- 为开发框架准备
npm install babel-plugin-transform-runtime –save-dev npm install babel-runtime –save
例子
module.exports = { entry: { app: 'app.js' }, output: { filename: '[name].[hash:8].js' }, module: { rules:[ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env',{ //指定target为根据哪些语法编译 targets: { browsers: ['> 1%','last 2 versions'] } } ] } }, exclude: '/node_modules' ] } }
对于 webpack
中 babel
的配置可以单独提取处理 .babelrc
统一管理
{ "presets": [ ["@babel/preset-env",{ targets: { browsers: ['> 1%','last 2 versions'] } }] ], "plugins": [ "transform-runtime" ] ]
3.2 打包 Typescript
npm i typescipt ts-loader --save-dev npm i typescipt awesome-typescript-loader --save-dev
配置
tsconfig.json webpack.config.js
tsconfig
- 配置选项:官网
/docs/handbook/compiler-options.html
- 常用选项
compilerOptions
include
exclude
声明文件
用于编译时检查错误
以 loadsh
为例,需要安装 @types/lodash
带有声明文件的,而不是安装 lodash
npm install @types/lodash npm install @types/vue
Typings
也可以这样安装带有 type
的包
npm install typings typings install lodash
例子
module.exports = { entry: { 'app': 'app.js' }, output: { filename: '[name].bundle.js' }, module: { rules: [ test: /\.tsx?$/, use: { loader: 'ts-loader' } ] } }
在项目根目录创建 tsconfig.json
{ "compilerOptions": { "module": "comonjs", "target": "es5", //编译后的文件在哪个环境运行 "allowJs": true,//允许js语法 }, "include": [ //编译路径 "./src/*" ], "exclude": [ //排除编译文件 "./node_modules" ] }
3.3 提取 js 的公用代码
- 减少代码冗余
- 提高加载速度
主要使用内置插件实现 webpack.optimize.CommonsChunkPlugin
{ plugins: [ new webpack.optimize.CommonsChunkPlugin(option) ] }
例子
module.exports = { entry: { "pageA": "./src/pageA", "pageB": "./src/pageB", "vendor": ['loash']//业务代码和第三方代码区分开,给loash单独打一个包 }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', chunkFilename: '[name].chunk.js' }, plugins: [ // 提取common new webpack.optimize.CommonsChunkPlugin({ name: 'common', minChunks:2,//出现两次就打包成common代码 chunks: ['pageA','pageB']//指定范围提取公共代码 }) // 提取vendor、取业务代码manifest new webpack.optimize.CommonsChunkPlugin({ //把entry的vendor代码和这里的common(webpack打包的)代码合并 names: ['vendor','manifest'] minChunks: Infinity }) // ==== 提取业务代码manifest== 合并到上面names中 // 如果不想把webpack打包的代码和vendor代码合并 需要提取到manifest //new webpack.optimize.CommonsChunkPlugin({ // webpack代码和vendordiam区分开 // name: 'manifest' //manifest即生成 // minChunks: Infinity // }) ] }
3.4 代码分割和懒加载
第一种方式:通过wepack内置方法
require.ensure
动态加载一个模块,接收四个参数
[]:dependencies callback errorCallback chunkName
第二种方式:通过ES2015 Loader Spec
System.import()
后面演变为 import()
来动态加载模块
import()
方式返回一个 promise
在 import
中传入需要依赖的明,动态加载模块,就可以像使用 Promise
一样使用 import().then()
代码分割场景
- 分离业务代码 和 第三方依赖
- 分离业务代码 和 业务公共代码 和 第三方依赖
- 分离首次加载 和 访问后加载的代码
例子
module.exports = { entry: { "pageA": "./src/pageA" }, output: { path: path.resolve(__dirname, './dist'), publicPath: './dist/',//动态加载路径 filename: '[name].bundle.js', chunkFilename: '[name].chunk.js' } }
目标是提取 pageA
、 pageB
中公共的模块 moduleA
//src/pageA.js import './subPageA' import './subPageB' //ensure的时候代码不会执行 需要在下面加载一次 require.ensure(['lodash'],function(){ var _ = require('lodash') _.join([1,2,3]) },'vendor')// 指定chunk名称 export default 'pageA'
运行打包这时 loadash
提取到 vendor
中
这时候还不是我们想要的
//src/pageA.js require.include('./moduleA') if(page === 'subPageA'){ require.ensure(['./subPageA'],function(){ var subPageA = require('./subPageA') },'subPageA')// 指定chunk名称 }else{ require.ensure(['./subPageB'],function(){ var subPageB = require('./subPageB') },'subPageB')// 指定chunk名称 } //ensure的时候代码不会执行 需要在下面加载一次 require.ensure(['lodash'],function(){ var _ = require('lodash') _.join([1,2,3]) },'vendor')// 指定chunk名称 export default 'pageA'
这时候这有 pageA
中才有 moduleA
新建一个html验证是否动态加载
<html> <body> <script src="./dist/pageA.bundle.js"></script> </body> </html>
import()动态加载的写法
//src/pageA.js require.include('./moduleA') var page = 'subPageA' if(page === 'subPageA'){ // 指定chunkName /** webpackChunkName: 'subPageA' **/ import(/** webpackChunkName: 'subPageA' **/,'./subPageA').then(function(subPageA){ console.log(subPageA) }) }else{ import(/** webpackChunkName: 'subPageB' **/'./subPageB').then(function(subPageB){ console.log(subPageB) }) } // 异步加载lodash //ensure的时候代码不会执行 需要在下面加载一次 require.ensure(['lodash'],function(){ var _ = require('lodash') _.join([1,2,3]) },'vendor')// 指定chunk名称 export default 'pageA'
如果 /** webpackChunkName: 'subPageA' **/
相同,则会合并处理
合并了 subPageA
和 subPageB
来看看打包后的文件,既有A、B
在 webpack
代码分割中使用 async
异步加载
module.exports = { entry: { "pageA": "./src/pageA", "pageB": "./src/pageB", "vendor": ['loash']//业务代码和第三方代码区分开,给loash单独打一个包 }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', chunkFilename: '[name].chunk.js' }, plugins: [ // add 异步模块 new webpack.optimize.CommonsChunkPlugin({ aysnc:'async-common',//异步共同的东西 children: true, names: ['vendor','manifest'] minChunks: Infinity }) // 提取vendor、取业务代码manifest new webpack.optimize.CommonsChunkPlugin({ //把entry的vendor代码和这里的common(webpack打包的)代码合并 names: ['vendor','manifest'] minChunks: Infinity }) ] }
//src/subPageA.js import './moduleA' console.log('this. is subPageA') export default 'subPageA'
//src/subPageB.js import './moduleA' console.log('this. is subPageB') export default 'subPageB'
//src/subPageB.js import './moduleA' console.log('this. is moduleA') export default 'moduleA'
//src/pageA.js // 异步加载不能include 否则会和pageA打包到一起 // require.include('./moduleA') import _ from 'lodash' var page = 'subPageA' if(page === 'subPageA'){ // 指定chunkName /** webpackChunkName: 'subPageA' **/ import(/** webpackChunkName: 'subPageA' **/,'./subPageA').then(function(subPageA){ console.log(subPageA) }) }else{ import(/** webpackChunkName: 'subPageB' **/'./subPageB').then(function(subPageB){ console.log(subPageB) }) } // === lodash不在异步加载 //ensure的时候代码不会执行 需要在下面加载一次 //require.ensure(['lodash'],function(){ // var _ = require('lodash') // _.join([1,2,3]) //},'vendor')// 指定chunk名称 export default 'pageA'
//src/pageB.js import _ from 'lodash' var page = 'subPageB' if(page === 'subPageA'){ // 指定chunkName /** webpackChunkName: 'subPageA' **/ import(/** webpackChunkName: 'subPageA' **/,'./subPageA').then(function(subPageA){ console.log(subPageA) }) }else{ import(/** webpackChunkName: 'subPageB' **/'./subPageB').then(function(subPageB){ console.log(subPageB) }) } // === lodash不在异步加载 //ensure的时候代码不会执行 需要在下面加载一次 //require.ensure(['lodash'],function(){ // var _ = require('lodash') // _.join([1,2,3]) //},'vendor')// 指定chunk名称 export default 'pageB'
这样就把 subpageA
和 subPageB
共同依赖的 moduleA
异步提取出来
3.5 处理 CSS 和 CSS 模块化
引入css
需要两个 loader
, style-loader
(创建标签到文档流中)、 css-loader
(可以 import
一个样式文件,使得在 js
中可以使用)
Style-Loader
style-loader
除了本身,还有这几个 loader
style-loader/url style-loader/useable
Style-Loader的options
-
insertAt
(插入位置) -
insertInto
(插入到dom
) -
singleton
(是否只使用一个style
标签) -
transform
(转化,浏览器环境下,插入页面前)
例子
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { // loader解析从后往前处理 rules: [ { test: /\.css$/, use: [ { //loader: 'style-loader', loader: 'style-loader/url', //使用这个可以往页面注入link标签 而不是style,这个并不常用 }, { //loader: 'css-loader', loader: 'file-loader'//使用这个可以往页面注入link标签 而不是style 这个并不常用 } ] } ] } }
CSS-Loader
options
-
alias
(解析的别名) -
importLoader
(@import
) -
Minimize
(是否压缩) -
modules
(启用css-modules
)
CSS-Modules
localIdentName: '[path][name]__[local]--[hash:base64:5]'
例子
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { // loader解析从后往前处理 rules: [ { test: /\.css$/, use: [ { loader: 'style-loader', options: { //合并多个style为一个 singleton:true } }, { loader: 'css-loader', options: { minimize:true, modules: true, // css模块化 localIdentName: '[path][name]_[local]_[hash:base64:5]' } } ] } ] } }
配置Less / Sass
npm install less-loader less --save-dev npm install sass-loader node-sass --save-dev
例子
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { // loader解析从后往前处理 rules: [ { test: /\.(css|less)$/, use: [ { loader: 'style-loader', options: { //合并多个style为一个 singleton:true } }, { loader: 'css-loader', options: { minimize:true, modules: true, // css模块化 localIdentName: '[path][name]_[local]_[hash:base64:5]' } }, { loader: 'less-loader' }, { loader: 'sass-loader' } ] } ] } }
提取 CSS
extract-loader ExtractTextWebpackPlugin
例子
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { // loader解析从后往前处理 rules: [ { test: /\.(css|less)$/, use: ExtractTextWebpackPlugin.extra({ // 提取css并不会自动加入到文档中,需要在HTML手动加入css文件 fallback: { loader: 'style-loader', options: { //合并多个style为一个 singleton:true } }, // 处理css use: [ { loader: 'css-loader', options: { minimize:true, modules: true, // css模块化 localIdentName: '[path][name]_[local]_[hash:base64:5]' } }, { loader: 'less-loader' }, { loader: 'sass-loader' } ] }) } ] }, plugins: [ new ExtractTextWebpackPlugin({ filename: '[name].min.css', allChunks: false //指定提取css范围,true所有import进来的css }) ] }
3.6 PostCSS in Webpack
安装
postcss postcss-loader Autoprefixer cssnano postcss-cssnext
例子
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { // loader解析从后往前处理 rules: [ { test: /\.(css|less)$/, use: ExtractTextWebpackPlugin.extra({ // 提取css并不会自动加入到文档中,需要在HTML手动加入css文件 fallback: { loader: 'style-loader', options: { //合并多个style为一个 singleton:true } }, // 处理css use: [ { loader: 'css-loader', options: { minimize:true, modules: true, // css模块化 localIdentName: '[path][name]_[local]_[hash:base64:5]' } }, { loader: 'less-loader' }, { loader: 'sass-loader' }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: [ require('antoprefixer')() ] } } ] }) } ] }, plugins: [ new ExtractTextWebpackPlugin({ filename: '[name].min.css', allChunks: false //指定提取css范围,true所有import进来的css }) ] }
3.7 Tree shaking
3.7.1 JS Tree shaking
使用场景
- 常规优化
-
引入第三方库的某一个功能
例子
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { rules: [ { test: /\.js$/, use: [ { loader: 'babel-loader', options: { presets: ['env'], plugins: [ // lodash Tree shaking 'lodash' ] } } ] } ] }, plugins: [ new ExtractTextWebpackPlugin({ filename: '[name].min.css', allChunks: false //指定提取css范围,true所有import进来的css }) // Tree shaking new webpack.optimize.UglifyJsPlugin({ }) ] }
有些库不是es模块写的,并不能 tree shaking
。需要借助其他工具
npm i babel-loader babel-core babel-preset-env babel-plugin-lodash --save
lodash Tree不生效
lodash-es babel-lugin-lodash
查看模块是否Tree Shaking方式:去第三方库index.js中看模块书写方式是否是es
3.7.2 CSS Tree shaking
主要使用 purifycss-webpack
例子
const glob = require('glob-all') module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), publicPath: './dist/', //指定从项目中哪里引入资源 filename: '[name].bundle.js' }, module: { rules: [ { test: /\.js$/, use: [ { loader: 'babel-loader', options: { presets: ['env'], plugins: [ // lodash Tree shaking 'lodash' ] } } ] } ] }, plugins: [ new ExtractTextWebpackPlugin({ filename: '[name].min.css', allChunks: false //指定提取css范围,true所有import进来的css }) // 放到ExtractTextWebpackPlugin后面 new PurifyCss({ paths: glob.sync([ path.join(__dirname,'./*.html'), path.join(__dirnname,'./src/*.js') ]) }) // Tree shaking new webpack.optimize.UglifyJsPlugin({ }) ] }
四、由浅入深Webpack
4.1 文件处理
4.1.1 图片处理
css Base64
处理需要用到的 loader
-
file-loader
css
中引入图片 -
url-loader
base64
编码 -
img-loader
压缩图片 -
postcss-sprites
合成雪碧图
4.1.2 处理雪碧图、base64、压缩图片
module.exports = { module: { rules: [ { test: /\.(css|less)$/, use: extractLess.extract({ // 提取css并不会自动加入到文档中,需要在HTML手动加入css文件 fallback: { loader: 'style-loader', options: { //合并多个style为一个 singleton:true } }, // 处理css use: [ { loader: 'css-loader', options: { minimize:true, modules: true, // css模块化 localIdentName: '[path][name]_[local]_[hash:base64:5]' } }, { loader: 'less-loader' }, { loader: 'sass-loader' }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: [ // 合并雪碧图 require('postcss-sprites')({ // 指定雪碧图输出路径 spritePath: 'dist/assets/imgs/sprites', retina: true // 处理苹果高清retina 图片命名需要 [email protected],对应的图片的css大小设置也要减小一半 }) ] } } ] }) }, { test: /\.(png|jpg|gif)$/, use:[ //{ // loader: //'file-loader',//处理图片 // options: { // publicPath:'',// 使得图片地址可以访问 // outputPath: 'dist/' // useRelativePath:true //} //} // url-loader会把图片转成base64 { loader: 'url-loader', options: { name: '[name].min.[ext]' //5kb内会转成base64 ,否则输出图片路径 limit: 1000, publicPath:'', outputPath: 'dist/', useRelativePath:true } }, // 压缩图片 { loader: 'img-loader', options: { pngquant: { //图片质量 quality:80 } } }, ] } ] } }
4.1.2 处理字体文件
file-loader url-loader
module.exports = { module: { rules: [ { test: /\.(css|less)$/, use: extractLess.extract({ // 提取css并不会自动加入到文档中,需要在HTML手动加入css文件 fallback: { loader: 'style-loader', options: { //合并多个style为一个 singleton:true } }, // 处理css use: [ { loader: 'css-loader', options: { minimize:true, modules: true, // css模块化 localIdentName: '[path][name]_[local]_[hash:base64:5]' } }, { loader: 'less-loader' }, { loader: 'sass-loader' }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: [ // 合并雪碧图 require('postcss-sprites')({ // 指定雪碧图输出路径 spritePath: 'dist/assets/imgs/sprites', retina: true // 处理苹果高清retina 图片命名需要 [email protected],对应的图片的css大小设置也要减小一半 }) ] } } ] }) }, { test: /\.(png|jpg|gif)$/, use:[ //{ // loader: //'file-loader',//处理图片 // options: { // publicPath:'',// 使得图片地址可以访问 // outputPath: 'dist/' // useRelativePath:true //} //} // url-loader会把图片转成base64 { loader: 'url-loader', options: { name: '[name].min.[ext]' //5kb内会转成base64 ,否则输出图片路径 limit: 1000, publicPath:'', outputPath: 'dist/', useRelativePath:true } }, // 压缩图片 { loader: 'img-loader', options: { pngquant: { //图片质量 quality:80 } } }, // 处理字体文件 { test: /\.(eot|woff2|woff|ttf|svg)$/, use: [ { loader:'url-loader', options: { name: '[name].min.[ext]' //5kb内会转成base64 ,否则输出图片路径 limit: 5000, publicPath:'', outputPath: 'dist/', useRelativePath:true } } ] } ] } ] } }
4.1.3 处理第三方 JS 库
处理第三方库 用到 providePlugin
、 imports-loader
1.providePlugin
以引入 jQuery
为例
npm i jquery
module.exports = { plugins: [ new webpack.ProvidePlugin({ // 在全局注入jQuery变量 $:'jquery' }) ] }
引入本地 libs
中的 jQuery
module.exports = { resolve: { alias: { // $确切匹配 把jQuery这个关键字解析到某个目录下 jquery$:path.resolve(__dirname,'src/libs/jquery.min.js') } }, plugins: [ new webpack.ProvidePlugin({ // 在全局注入jQuery变量 $:'jquery' }) ] }
2.imports-loader
module.exports = { module:{ rules:[ { test:path.resolve(__dirname,'src/app.js'), use:[ { loader: 'imports-loader', options: { $: 'jquery' } } ] } ] } }
4.2 HTML in webpack(自动生成HTML)
自动生成 HTML
,把这个页面需要的 js
、 css
插入到页面中
4.2.1 生成 HTML
htmlWebpackPlugin
options
-
template
-
filename
-
minify
是否压缩 -
chunks
-
inject
是否让css、js
通过标签形式插入到页面中
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js' }, plugin:[ new htmlWebpackPlugin({ filename:'index.html', // 不传默认index.html template: './index.html',//传入模板 inject:true,//控制js\css是否插入到页面 minify: { collapseWhitespace:true //压缩空格 }, chunks:['app']//指定chunks会把跟这个入口相关的chunks插入到页面中 }) ] }
4.2.2 HTML 中引入图片
需要用到 html-loader
html-loader
options
-
attrs: [img:src]
module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', publicPath:'/' //网站路径为/ 图片等资源引用不会发生错误 }, module:{ rules:[ { loader: 'url-loader', options: { name: '[name].min.[ext]' //5kb内会转成base64 ,否则输出图片路径 limit: 1000, publicPath:'', outputPath: 'assets/imgs/', //useRelativePath:true // 这里不能使用这个 因为图片路径在HTML中、css中都存在,打包的时候图片会放错地方。需要用到绝对路径,在output指定publicPath:'/' } }, // 处理HTML中的图片引用路径问题 { test: /\.html$/, use: [ { loader: 'html-loader', options: { attrs: ['img:src','img:data-src'] } } ] } ] } }
require在HTML中引入图片
<img src="${require('./public/imgs/xx.png)}" />
4.2.3 配合优化
提前载入webpack加载代码
inline-manifest-webpack-plugin html-webpack-inline-chunk-plugin
建议使用 html-webpack-inline-chunk-plugin
npm i html-webpack-inline-chunk-plugin
var HTMLInlieChunk = require('html-webpack-inline-chunk-plugin') module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', publicPath:'/' }, plugin:[ new htmlWebpackPlugin({ filename:'index.html', // 不传默认index.html template: './index.html',//传入模板 inject:true,//控制js\css是否插入到页面 minify: { collapseWhitespace:true //压缩空格 }, chunks:['app']//指定chunks会把跟这个入口相关的chunks插入到页面中 }) new webpack.mdtimize.CommonsChunkPlugin({ name: 'manifest' }) new HTMLInlieChunk({ inlineChunks: ['manifest'] //把webpack生成的manifest提取到HTML文件script中,减少请求 }) ] }
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK