
能力说明:
掌握企业中如何利用常见工具,进行前端开发软件的版本控制与项目构建和协同。开发方面,熟练掌握Vue.js、React、AngularJS和响应式框架Bootstrap,具备开发高级交互网页的能力,具备基于移动设备的Web前端开发,以及Node.js服务器端开发技能。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明SQL 先说点废话,很久没发文了,整理了下自己当时入门 SQL 的笔记,无论用于入门,回顾,参考查询,应该都是有一定价值的,可以按照目录各取所需。SQL数据库有很多,MySQL是一种,本文基本都是SQL通用标准,有些标准很不统一的地方就用MySQL的写法了。希望本文帮你快速了解SQL的基本操作和概念。 文章格式上有些问题,可以点击这里获得更加的阅读体验 目录 检索 过滤检索结果 数据汇总处理 分组 给检索结果排序 表操作 插入数据 更新删除数据 子查询-迭代查询 联结-关联多个表 组合查询 视图 其它 检索 检索某表中单个列: SELECT 列名 FROM 表名; 检索某表中多个列: SELECT 列名,列名,列名 FROM 表名; 检索某表中所有列:(尽量不用) SELECT * FROM 表名; 只检索某表中某列里不重复的项: SELECT DISTINCT 列名 (如果有两列或以上,需要这些列组合起来是不重复的) FROM 表名; 检索指定行数: SELECT 列名 FROM 表名 LIMIT 5 OFFSET n; (mySQL中,选第n行后的五行。 OFFSET n 可不填写默认为0,其它 SQL 数据库中有不同写法) 过滤检索结果 寻找指定行:(举例) SELECT prod_name, prod_price FROM Products WHERE prod_price = 3.49;(和字符串比较加单引号,数值不用) 查找列名为prod_name和列名为prod_price的两列,检索其中prod_price = 3.49; 的所有行。 = 可以替换为其它操作符,如下表 | 操作符 | 描述 | | --- | --- | | = | 等于 | | <> | 不等于 | | > | 大于 | | < | 小于 | | >= | 大于等于 | | <= | 小于等于 | | BETWEEN | 在某个范围内 | | LIKE | 搜索某种模式 | 组合WHERE子句: SELECT prod_id, prod_price, prod_name FROM Products WHERE vend_id = 'DLL01' AND prod_price <= 4; AND 连接同时需要满足的两个条件,OR即满足一个条件即可,NOT 找到与后边条件不匹配的行。 且not,and和or可以组合使用,用小括号声明逻辑循序。 `WHERE vend_id IN ( 'DLL01', 'BRS01' ) ` IN 起到作用类似于or,速度更快,逻辑更清晰。 通配符搜索: SELECT prod_id, prod_name FROM Products WHERE prod_name LIKE '%bean bag%'; %表示任意字符出现任意次数。也可以出现在中间位置。 _ 表示一个字符。 [charlist] 表示包含在里面的任意字符,[^charlist]不包含在里面的任意字符。 少使用通配符,搜索速度较慢。 数据汇总处理 算术计算: SELECT prod_id, quantity, item_price, quantity * item_price AS expanded_price FROM OrderItems WHERE order_num = 20008; `expanded_price`成为计算出来的新列。 字符串拼接: 不同数据库有差异,MySQL中: SELECT concat(vend_name , vend_country) AS vend_title FROM Vendors ORDER BY vend_name; concat_ws( ':' , vend_name , vend_country) 形式第一个参数为分隔符。 其他数据库用+或者||拼接字符串。 日期时间处理不同数据库差异较大。 内置方法: 求平均值: SELECT AVG(prod_price) AS avg_price FROM Products; 表Products中prod_price的平均值。返回给 avg_price。 可以配合WHERE语句计算指定行的平均值。 求最大值:MAX(prod_price) 求最小值:MIN(prod_price) 求和:SUM(prod_price) 近似的小数点后几位:ROUND(column_name,decimals) 当前日期: Now() 求行数: SELECT COUNT(*) AS num_cust FROM Customers; 求表Customers有几行。返回给num_cust。 *可以换成指定列如:cust_email。计算所得行数不包括该列值为null的行。 DISTINCT 列名,求不重复的列。 组合: SELECT COUNT(*) AS num_items, MIN(prod_price) AS price_min, MAX(prod_price) AS price_max, AVG(prod_price) AS price_avg FROM Products; 分组 创建分组: SELECT vend_id FROM Products GROUP BY vend_id; 根据 vend_id列中内容对 vend_id分组, 第一行换成 `SELECT vend_id, COUNT(*) AS num_prods` 即对每一个组计算行数。 注意:多行NULL会分为一组,GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。 可以对一个以上的列进行 GROUP BY 过滤分组: HAVING:类似于WHERE。唯一的差别是,WHERE过滤行,而HAVING过滤分组。 SELECT vend_id, COUNT(*) AS num_prods FROM Products WHERE prod_price >= 4 GROUP BY vend_id HAVING num_prods >= 2; 过滤出有(两个价格大与4的产品)的供应商 给检索结果排序 SELECT Company, OrderNumber FROM Orders ORDER BY Company DESC, OrderNumber ASC 可以 ORDER BY 列名1,列名2; 先按列名1内容排序,排序结果相同的按列名2内容排序。 列名后接 DESC 按该列内容倒序排列,ASC 正序(默认)。 ORDER BY 命令放在查询、分组等语句的最后。 表操作 创建表: CREATE TABLE newProducts ( prod_id CHAR(10) NOT NULL, vend_id CHAR(10) NOT NULL, prod_name CHAR(254) NOT NULL, prod_price DECIMAL(8,2) NOT NULL, prod_desc VARCHAR(1000) NULL ); NOT NULL 非空约束,不允许列中有NULL值下面介绍其他约束。 列的设置可以加上默认值,如NOT NULL后边接 DEFAULT CURRENT_DATE() ,默认值为当前日期。(每个数据库获取当前日期语句不同。) 后面接 PRIMARY KEY 即设置改列为主键。 后面接 AUTO_INCREMENT 即设置为自增,只有int型可以设置。 约束: 每个列可以有一种或几种约束。 NOT NULL 非空约束. UNIQUE 唯一约束,可唯一标识数据库表中的每条记录。 PRIMARY KEY 主键约束,唯一标识数据库表中的每条记录,唯一且非空。 FOREIGN KEY 外键约束,一个表中的 FOREIGN KEY 指向另一个表中的 PRIMARY KEY。 CHECK 检查约束,用于限制列中的值的范围。 DEFAULT 默认约束,用于向列中插入默认值 每个表可以有多个 UNIQUE 约束,但是每个表只能有一个 PRIMARY KEY 约束。 每种约束可以创建表时设置好,也可以后期增删. 索引: 在不读取整个表的情况下,索引使数据库应用程序可以更快地查找数据。 CREATE INDEX 索引名 ON Person (列名[,列名]) 复制表或表中部分列: CREATE TABLE CustCopy AS SELECT * FROM Customers; 创建Customers表的复制,CustCopy。 修改表: ALTER TABLE Vendors ADD vend_phone CHAR(20); ALTER TABLE Vendors DROP COLUMN vend_phone; 各数据库有不兼容现象,复杂表操作列可能要新建表删除旧表。 ALTER 还可以用来添加删除约束,删除索引等。 删除表:DROP TABLE CustCopy; 重命名表:RENAME Table oldTable TO newTable; 插入数据 插入整行或部分行: INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email) VALUES('1000000007', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA', NULL, NULL); 插入整行时,可省略 Customers 括号内的内容,即按照列的顺序,分别插入数据(不推荐)。省略 Customers 括号内的内容时,无内容的列必须用NULL占位。 插入部分行时,把要插入的列填入 Customers 括号内,与VALUES内容一一对应,没有提到的列默认NULL或其他默认值。 插入查询到的值: INSERT INTO Customers(cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country) SELECT cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_city, cust_state, cust_zip, cust_country FROM CustNew; 把从CustNew表中查到的内容插入 Customers表中。一次插入多行的方式。 更新和删除数据: 更新数据: UPDATE Customers SET cust_email = 'kim@thetoystore.com' WHERE cust_id = '1000000005'; 步骤为,要更新的表,要更新的列,要更新的行。一个SET可以跟多个列用逗号隔开。 删除某个值,即设置他为NULL。 删除数据: DELETE FROM Customers WHERE cust_id = '1000000008'; 删除表中指定整行,删除部分列用UPDATE 在UPDATE或DELETE语句使用WHERE子句前,应该先用SELECT进行测试,保证它过滤的是正确的记录,以防编写的WHERE子句不正确。如果不写WHERE会更新或删除所有行内容。 子查询-迭代查询 一种形式: SELECT cust_name, cust_contact FROM Customers WHERE cust_id IN (SELECT cust_id FROM Orders WHERE order_num IN (SELECT order_num FROM OrderItems WHERE prod_id = 'RGAN01')); 先从第二个括号选择符合条件的order_num,成为第二个括号内容,再向上找到第一个括号,查到符合条件的cust_id返回给第一个括号,最后根据第一个括号内容执行主查询语句。性能问题不要嵌套太多层。 也就是对Customers表的查询要用到Orders表查询后返回的内容,对Orders表的查询要用到OrderItems表查询后返回的内容。 另一种形式: SELECT cust_name, cust_state, (SELECT COUNT(*) FROM Orders WHERE Orders.cust_id = Customers.cust_id) AS orders FROM Customers 根据Customers 表中的cust_id,去Orders表中取得计算后的数据。 同一个表迭代查询: SELECT cust_id, cust_name, cust_contact FROM Customers WHERE cust_name = (SELECT cust_name FROM Customers WHERE cust_contact = 'Jim Jones'); 联结-关联多个表 两个表: 内联结 SELECT vend_name, prod_name, prod_price FROM Vendors, Products WHERE Vendors.vend_id = Products.vend_id; 根据两个表共同的列vend_id把Vendors, Products关联起来。 与 SELECT vend_name, prod_name, prod_price FROM Vendors INNER JOIN Products ON Vendors.vend_id = Products.vend_id; 结果相同。都是内联结,前一种是后一种的简写。 INNER 可省略。 外联结: SELECT Customers.cust_id, Orders.order_num FROM Customers LEFT OUTER JOIN Orders ON Orders.cust_id = Customers.cust_id; LEFT OUTER JOIN 把Customers表中没有被匹配到的 cust_id 也联结进去(会显示在结果里)。 RIGHT OUTER JOIN 是把Orders表中没有被匹配到的 cust_id 也联结进去(会显示在结果里)。 FULL OUTER JOIN 会把两张表中没有匹配到的列也显示出来(mysql 不支持,可通过 UNION 实现) OUTER 可省略。 多个表: SELECT cust_name, cust_contact FROM Customers, Orders, OrderItems WHERE Customers.cust_id = Orders.cust_id AND OrderItems.order_num = Orders.order_num AND prod_id = 'RGAN01'; 作用同子查询中a。同样不要关联太多,有性能问题。其中表名可以使用别名,如: SELECT cust_name, cust_contact FROM Customers AS C, Orders AS O, OrderItems AS OI WHERE C.cust_id = O.cust_id AND OI.order_num = O.order_num AND prod_id = 'RGAN01'; 组合查询 SELECT cust_name, cust_contact, cust_email FROM Customers WHERE cust_state IN ('IL','IN','MI') UNION ALL SELECT cust_name, cust_contact, cust_email FROM Customers WHERE cust_name = 'Fun4All'; UNION ALL 链接两句查询语句,统一返回结果,包含重复结果。 去掉ALL以后,去掉重复结果。 此处(从同一个表中查询)可以用WHERE , OR代替。 常用作从不同表中查询时,只要列数相同就可以拼接到一起,列名按照第一句中查询的列名。 视图 对已存在的表,进行筛选,数据处理,联结等操作后返回的数据,创建的虚拟表。视图是为了重用和简化常用的查询。对视图的查询同表。 视图总是显示最近的数据。每当用户查询视图时,数据库引擎通过使用 SQL 语句来重建数据。 创建视图: CREATE VIEW ProductCustomers AS SELECT cust_name, cust_contact, prod_id FROM Customers, Orders, OrderItems WHERE Customers.cust_id = Orders.cust_id AND OrderItems.order_num = Orders.order_num; 对OrderItems, Orders和Customers三个表进行联结,联结后结果形成 ProductCustomers 视图,可以把它当一张表来查询。 删除视图:DROP VIEW ProductCustomers; 其它 存储过程:为以后的使用保存一条或多条SQL语句,用于简化操作。每个数据库不同,见数据库具体介绍。 事务处理:事务处理模块中的语句,或者全部执行,或者全部不执行。可以设立保留点,执行失败时回到保留点。 创建数据库: CREATE DATABASE database_name 删除数据库:DROP DATABASE 数据库名称
WebSocket详解 因为 http 协议是单向的,之前如果服务器端有连续的变化需要通知客户端,只能通过客户端进行轮询(或者 long poll),但是轮询非常浪费资源,工程师们就发明了WebSocket。2011年成为国际标准,目前所有浏览器都已经支持了。 WebSocket 协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。这是百度百科的定义,非常容易理解。服务端主动推送消息到客户端解决了一大痛点,也是使用WebSocket的最大原因。 在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。 特点 建立在TCP协议之上,服务器端容易实现。 与 HTTP 兼容良好,握手阶段采用 HTTP 协议。 性能开销小,通信高效。 可发送文本或二进制数据。 没有同源限制。 优势 一句话就是:持久化链接 相比于轮询和 long poll,WebSocket 带来的性能开销优势就是因为持久化链接。 简单说下轮询和 long poll,轮询就是每隔一段时间就请求,每次请求无论数据是否更新都要占据后台服务的资源,非常浪费,需要后台服务有很快的处理速度,long poll 就是发送请求后,直到服务器更新了才返回更新的信息,也很浪费,需要后台服务很好的处理高并发的能力。 对比一下,每条 http 请求需要经过 Nginx 转发到特定的后台服务,后台服务处理请求,而且因为是无状态的,每次都要解析头信息,搞的后台服务也很烦。而 WebSocket 与 Nginx 建立持久化链接后,后台服务与客户端通信变得非常高效。Nginx 能力很强,保持这种持久化链接小菜一碟。 使用 示例: // Create WebSocket connection. const socket = new WebSocket('ws://localhost:8080'); // // Connection opened socket.addEventListener('open', function (event) { socket.send('Hello Server!'); }); // // Listen for messages socket.addEventListener('message', function (event) { console.log('Message from server', event.data); }); 详细 API 见 MDN文档 详细使用见 地址 Socket.IO 了解 WebSocket 的时候很容易接触到 Socket.IO,两者关系:Socket.IO 是 WebSocket 在 node.js 和客户端的一种实现,这么说也不准确,因为他会在无法使用 Websocket 的时候采用其他代替方案实现类似持久链接。 这样描述应该跟准确:Socket.IO 是建立 node.js服务端与客户端之间持久链接的一个框架,会优先采用 WebSocket 协议。 其接口简单容易理解,参考官方文档
本文可以解答以下问题: 链接 redux 后 react-router 不切换页面 react-router 路由改变页面却不改变 redux 与 react-router 结合使用的方案 简单的问题我多说两句。 先解决问题: react-router 文档的最后一个 API withRouter。在 connect 组建外面再包裹一层 withRouter。 推荐你接着往下看。 如果把 redux 链接到 Switch 或者 NavLink 以及他们的父组件,会产生一个问题: 路由不会切换页面了,或者 activeStyle 不工作了。react-router 4.x 的使用更加灵活,在 Route 外面绑定智能组件再正常不过,切换不了页面感觉有点尴尬,关键是:和我想的不一样啊! 第一反应就是 react-router-redux。 redux 管理状态,react-router 本质上也就是单页应用的一个状态而已,只不过和 H5 的 history API 联系到了一起,逻辑上 router 应该就是 redux的store 里的一个状态而已,归 redux 管理十分正常。看了看 react-router-redux 的源码,只是利用 history 的 API 修改路由,还有监听路由的变化修改 store。并没有什么魔法,没有解决我的问题。 去 react-router 文档瞅一眼,最后一个 API 藏在角落当时看文档时候一掠而过根本没注意到。withRouter。 原来是这么回事 redux 的 connect 方法重写了组件的 shouldComponentUpdate 方法,导致不能响应 location 的变化而重新渲染组件。withRouter 写在 connect 外面,withRouter 的 shouldComponentUpdate 不会被重写,组件会成功的重新渲染。 问题到此为止解决了,那 react-router-redux 是不是两者结合使用的最佳或者合理的方案么? 我觉得大多数情况下不需要 react-router-redux。 包裹 withRouter 后路由信息可以通过组件 props 获得,还可以通过 history 操控路由获取路由。可以说 react-router 4.x 引入 H5 的 history 后,把路由信息放到 store 里显得毫无必要。 而引入的好处是,对单页应用而言,逻辑上路由确实应该是应用状态的一部分(个人理解)。对需要频繁和在多处操作路由进行深度时光穿梭的应用,他提供了操作的方便性。除此之外,个人认为无必要性。
常用包 框架: yarn add express 数据库链接: yarn add sequelize yarn add mysql2 处理 favicon: yarn add serve-favicon 纪录日志: yarn add morgan 生成文档: yarn add --dev apidoc 解析请求参数: yarn add body-parser 设置 HTTP 头(提高安全性): yarn add helmet 文件变动监控(自动重启): yarn add --dev nodemon (启动服务器脚本中替换 node 即可) 允许 cors 请求: yarn add cors 压缩数据: yarn add compression 响应时间: yarn add response-time 数据伪造: yarn add faker – 数据验证: yarn add express-validator 进程管理: yarn add --dev pm2 带重启(nodemon用于开发环境),日志,负载均衡 serve-favicon 优点:把请求 favicon 的记录从日志中去除。缓存 icon 提高性能。使用兼容性最好的 Content-Type。 使用方式: var favicon = require('serve-favicon') app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) morgan 使用方式: var morgan = require('morgan') app.use(morgan('combined')) //参数可选 dev tiny 或自定义输出日志格式,详见文档 // 导出日志文件 var express = require('express') var fs = require('fs') var morgan = require('morgan') var path = require('path') var app = express() // create a write stream (in append mode) var accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), {flags: 'a'}) // setup the logger app.use(morgan('combined', {stream: accessLogStream})) body-parser 使用方式: var bodyParser = require('body-parser') // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })) //设置 false 使用 querystring 解析,处理 ajax 提交的复杂数据更在行。(true 使用 qs 解析) // parse application/json app.use(bodyParser.json()) apidoc 使用方式: 生成文档命令: apidoc -i routes/ -o doc/( routes 是程序入口,doc 是文档出口) 注释示例: /** * @api {get} /user/:id Read data of a User * @apiVersion 0.3.0 * @apiName GetUser * @apiGroup User * @apiPermission admin * * @apiDescription Compare Verison 0.3.0 with 0.2.0 and you will see the green markers with new items in version 0.3.0 and red markers with removed items since 0.2.0. * * @apiParam {String} id The Users-ID. * * @apiSuccess {String} id The Users-ID. * @apiSuccess {Date} registered Registration Date. * @apiSuccess {Date} name Fullname of the User. * @apiSuccess {String[]} nicknames List of Users nicknames (Array of Strings). * @apiSuccess {Object} profile Profile data (example for an Object) * @apiSuccess {Number} profile.age Users age. * @apiSuccess {String} profile.image Avatar-Image. * @apiSuccess {Object[]} options List of Users options (Array of Objects). * @apiSuccess {String} options.name Option Name. * @apiSuccess {String} options.value Option Value. * * @apiError NoAccessRight Only authenticated Admins can access the data. * @apiError UserNotFound The <code>id</code> of the User was not found. * * @apiErrorExample Response (example): * HTTP/1.1 401 Not Authenticated * { * "error": "NoAccessRight" * } */ helmet var express = require('express') var helmet = require('helmet') var app = express() app.use(helmet()) cors 使用方式: // 允许所有跨域请求 var express = require('express') var cors = require('cors') var app = express() app.use(cors()) // 允许某路由的跨域请求 app.get('/products/:id', cors(), function (req, res, next) { res.json({msg: 'This is CORS-enabled for a Single Route'}) }) // 允许某些域的请求 var whitelist = ['http://example1.com', 'http://example2.com'] var corsOptions = { origin: function (origin, callback) { if (whitelist.indexOf(origin) !== -1) { callback(null, true) } else { callback(new Error('Not allowed by CORS')) } } } app.get('/products/:id', cors(corsOptions), function (req, res, next) { res.json({msg: 'This is CORS-enabled for a whitelisted domain.'}) }) // 允许 GET/POST 以外的请求 app.options('/products/:id', cors()) // enable pre-flight request for DELETE request app.del('/products/:id', cors(), function (req, res, next) { res.json({msg: 'This is CORS-enabled for all origins!'}) }) // 对所有路由允许 app.options('*', cors()) // include before other routes compression 使用方式: var compression = require('compression') var express = require('express') var app = express() app.use(compression({filter: shouldCompress})) function shouldCompress (req, res) { if (req.headers['x-no-compression']) { // don't compress responses with this request header return false } // fallback to standard filter function return compression.filter(req, res) } response-time 使用方式: 该中间件将响应时间写在响应头 X-Response-Time 中 var express = require('express') var responseTime = require('response-time') var app = express() // 统计响应进入该中间件到写完响应头的毫秒数 app.use(responseTime()) express-validator 验证规则 // 初始化 app.use(expressValidator()) // this line must be immediately after any of the bodyParser middlewares! // 检查参数是否符合标准 req.check('testparam', 'Error Message').notEmpty().isInt() // 将参数转化为 req.sanitize('postparam').toBoolean() // 返回验证结果 req.getValidationResult().then(function(result) { // do something with the validation result }) pm2 pm2 start app.js --name="api" # Start application and name it "api" pm2 stop all # Stop all apps pm2 logs # Display logs of all apps pm2 web 后访问 http://localhost:9615/ # 查看系统状态
webpack算是个磨人的小妖精了。之前一直站在glup阵营,使用browserify打包,发现webpack已经火到爆炸,深怕被社区遗落,赶紧拿起来把玩一下。本来只想玩一下的。尝试打包了以后,就想启个webpack服务器,之后就想添加热替换,什么css文件单独拆分,各种 loader 处理优化打包结果,各种 source-map 有什么不同,一个都不能少。其中添加热替换时候,因为应用的服务器和webpack服务器没有使用同一个,产生了一点波折。然后就到了今天这个主题了。逐步展开今天的主题:为什么要分离第三方库? 这个好处显而易见,第三方库是比较稳定的,不会轻易改变,利用浏览器缓存后,用户再次加载页面会减少服务器请求,提高速度优化体验。提取多个应用(入口)公共模块的作用和他类似,公共部分会被缓存,所有应用都可以利用缓存内容从而提高性能。分离第三方库就能利用浏览器换缓存了么? 同样显而易见是否定的,导致无法利用缓存的因素有很多,比如最明显的有可能你每次分离的库文件重新打包都会得到不同的名称,这个比较容易发现,再比如说后台的同事给js文件设置的缓存过期时间为0,这就尴尬了,但0就不能利用缓存了么?并不是,只要文件是完全不变的,注意是完全不变,包括修改时间,依然会利用缓存,性能飞起。想利用缓存必须先了解缓存,这里简单提一下:浏览器缓存机制是什么样的? HTTP1.1给的策略是使用Cache-control配合Etag, Cache-control设置举例: 'Cache-Control': 'public, max-age=600', max-age即过期时间,如果已过期的话,还会查看Etag, ETag的值: Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。 如果Etag相同,依然不会请求新资源,而会使用以前的文件。CommonsChunkPlugin 到底是用来干什么的? 字面理解,提取公共包,公共包那就是不只一个地方使用喽,单页应用(单入口)的库只有他自己使用,不能算公共包吧?这个插件提取的公共包,每次是会重新打包的(Etag会不同),无论是节约打包时间,(虽然微不足道的时间但毕竟是无用功:库根本没变么),还是对浏览器缓存的利用(万一 max-age 过期了你就放弃缓存了么?)都不是好的方案。最佳方案浮出水面:DllPluginDllPlugin有什么优势? 只对库文件打包一次。也就是说,只要库文件不变,只需要打包一次,以后再打包业务代码和库文件没关系啦,这样一来真正做到了库文件永远是那个库文件,只要库文件不变,缓存永远有效(Etag不变),打起包来把库丢到脑后,神清气爽。介绍一下最简单的使用方式: 首先另写一个 webpack 配置文件,毕竟是单独打包库了,假设 webpack.config.dll.js const path = require('path') const webpack = require('webpack'); module.exports = { entry: { vendor: ['react', 'react-dom', 'react-hot-loader', 'immutable', 'redux', 'react-redux', 'react-router-dom', 'redux-logger', 'redux-persist', 'redux-persist-transform-immutable', 'redux-thunk'], }, output: { filename: 'js/[name].js', path: path.resolve(__dirname, 'public'), library: '[name]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致 }, plugins: [ new webpack.DllPlugin({ path: path.resolve(__dirname, 'public/manifest.json'), // 本Dll文件中各模块的索引,供DllReferencePlugin读取使用 name: '[name]', }), ], } 在原来的配置文件中添加 DllReferencePlugin 插件 new webpack.DllReferencePlugin({ manifest: require('./public/manifest.json'), // 指定manifest.json name: 'vendor', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致 }), 终端先运行:webpack -p --progress --config ./webpack.config.dll.js 把库文件先打个包,只要库不变,以后就用这个包了,再打包业务代码,完活。推荐策略: 各行其是。 如果是单页应用,那只用DllPlugin打包库文件即可,业务代码一个包搞定。 如果是多页应用,DllPlugin打包完库文件,开发时可能会用很多公共的业务代码而且可能随时变动,这就要利用CommonsChunkPlugin来做他本该做的事,再把公共业务提取出来,至于缓存,起码在页面间切换时,公共部分还是会被缓存的。