工程构建
1. 学习要点
- 构建功能解决的问题
- 规范和API设计原则
- 使用ES开发的优势以及对应的构建方案设计
- CSS 预编译器和PostCSS各自区别和作用,以及构建方案设计
- JS模块化规范与webpack支持性
- 针对浏览器缓存策略的构建功能的设计
- 资源定位功能的设计与实现
2. 构建功能解决的问题
将无法在浏览器环境下运行的代码,转化为宿主可执行的代码。 (面向语言)
考虑Web应用的性能因素。(面向优化)
- 依赖打包:减少HTTP请求
- 资源嵌入:如小图转base64,减少HTTP请求
- 文件压缩
- hash指纹:应对浏览器缓存
当引用资源的URL改变时(比如引用文件名改变,引用域名改变等),需要对HTML文件同步更新,这成为资源定位。 (面向部署)
3. 配置API设计原则和编程范式约束
目的: 统一的对yeoman webpack express进行上层封装可以间谍一线业务开发人员的学习曲线,简化配置复杂度。
- 降低构建工具配置的复杂度,不需要开放所有配置,可以提供插件机制。
- 编程凡是约束尽量做到可移植性。
4. ECMAScript 与 Babel
ES6的意义不只是因为语言本身加入了类、静态模块体系,块级作用域等等,更多的是因为以ES6为代表的前端接规范意识的加强。
结合webpack与Babel实现JavaScript构建
- babel-loader是webpack的插件,使用方法与常规的webpack loader插件相似
// 一个典型的配置例子
module:{
rules:[{
test:/\.js$/,
exclude:/(node_modules|bower_components)/,
use:{
loader:'bebel-loader',
option:{
presets: [[
'env':{
'modules':false,
'targets':{
'chrome':59
}
}
],
'stage-2' //配置转换实验性规范
],
// 其他插件
plugins: [require('babel-plugin-transform-object-rest-spread')]
}
}
}]
}
需要注意的有两点
babel-preset-env的使用
它的作用是:节省了你搭配babel的preset插件插件的时间,你可以直接告诉兼容的浏览器的目标版本
// 可以将presets改成,告诉babel兼用chrome59版本 "presets": [ [ "env", { "targets":{ "chrome":59 } } ] ]
另外注意在使用babel-loader编译时一定要将 modules配置项设置为false,因为这个配置项是将es代码转换为AMD或者CommonJS的,这个会和Webpack有冲突
如果想使用Babel编译test规则以外文件中< script >标签内的JS代码,必须有.babelrc文件进行配置,比如.vue文件中的ES代码,如果不配置.babelrc文件是不会编译的。
5. CSS预编译与PostCSS
CSS预编译器
- 增强了编程能力
- 增加了源代码的可复用性
- 增强源码可维护性
- 更便于解决浏览器兼容性
它的目标是实现:
- 嵌套
- 变量
- mixin/继承
- 运算
- 模块化
PostCSS
预编译CSS的一个问题是不是规范的CSS语法,PostCSS提倡规范的CSS原生语法编写,然后配置编译器取药兼容的浏览器版本,最后经过编译将源代码转化为目标浏览器可用的CSS代码。
目前普遍的使用方法是将CSS预编译和PostCSS综合在一起使用。
webpack 结合预编译与PostCSS实现CSS构建
- webpack配置项中的use指定的loader是按照索引反向执行的。比如
{
test:/\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
执行顺序是 less-loader ---> css-loader ---> style-loader
// webpack 的CSS预编译 与 PostCss 的编译方案
{
test:/\.less$/,
use:[
{
loader:'style-loader',
options:{}
},{
loader:'css-loader',
options:{
importLoaders:2 // 作用是@import的资源之前需要经过的其他loader的个数。
}
},{
loader:'postcss-loader',
options:{}
},{
loader:'less-loader',
options:{}
},
]
}
// style-loader的作用是将CSS增加到HTML里 css-loader 是获取引用的资源 如url() 等如果独立导出CSS 则使用extract-text-webpack-plugin
{
test:/\.less$/,
use:ExtractTextPlugin.extract({
filename:'./dest/[name].[contenthash:8].css',
use:[
{
loader:'css-loader',
options:{
importLoaders:2
}
},{
loader:'postcss-loader',
options:{}
},{
loader:'less-loader',
options:{}
},
],
publicPath:'/'
})
}
案例: 自动生成CSS Sprites功能实现
功能需求: 将CSS中引用的散列图标合并成一张Sprites图,减少HTTP请求。
注意要点:
- 区分CSS中引用的图片是否可合并
- 多页项目生成多张sprites如何处理
- 多分辨率屏幕生成不同分辨率的sprites
设计规范:
- API需要尽量语义化,简单明了。
- 减少对源代码的绑架
- 方案的封装必然会带来工具模块原本的限制,需要提供可以对工具模块直接配置的高级API
实现方案:
自动生成CSS Sprites功能实现借助于PostCSS插件 postcss-sprites 配置如下
loader:'postcss-loader',
options:{
plugins: [
require('postcss-sprites')(postcssSpritesOptis)
]
}
// postcssSpritesOptis 这个是postcss-sprites 的配置参数
6. 模块化开发
前端工程体系的构建系统重要功能之一就是讲散列的模块构建为利于部署的整合文件。
模块化开发的价值
- 避免命名冲突
- 便于依赖管理
- 利于性能优化
- 提高可维护性
- 利于代码复用
webpack 模块化构建
webpack支持 CommonJS,AMD,ES6的模块规范,这三种规范存在一定的差异
- CommonJS主要用于开发Node.js,规范本身不具备异步加载的功能,需要借助webpack提供的API实现。
- AMD 在webpack构建时不能定义输出的文件名,导致异步文件的名称欠缺语义
- ES6 Module 已经被webpack支持。
webpack配置如下:
output:{
path:'./output',
filename:'[name].[chunkhash:8].js',
chunkFilename:'[name].[chunkhash:8].js'
}
ES6 的import()异步引用的方式代码
import a from './module.a.js';
a();
window.onload = import(
/* webpackChunkName:"aysnc" */
'./module.b.js'
).then(b => {
b();
});
编译后的chunkFilename 的name通过注释添加。
7. 增量更新与缓存
前端工程化起到的作用
- 构建产出文件hash指纹,这是实现增量更新的必要条件
- 构建更新html文件对其他静态资源的引用URL
如果不能使用hash指纹,则采用协商缓存策略。
按需加载与多模块架构场景下的增量更新
- 同步模块只影响主主模块的hash指纹,对异步模块没有影响。
- 异步模块修改不仅仅影响异步模块指纹 也影响主模块指纹,因为主模块代码中有对异步js的引用,这个引用的文件名变了也就是主模块的代码也要发生改变。
webpack实现增量更新构建方案
- webpack产生hash指纹的配置有两个
// 这种形式任何一个文件改动会影响所有的资源 同步文件与异步文件拥有相同的指纹
output:{
filename: '[name].[hash:8].js'
}
// 这种是分块产生指纹,符合多模块更新
output:{
filename:'[name].[chunkhash:8].js'
}
- contenthash: webpack 默认将构建后的CSS代码合并到引用他的js文件中,此js文件运行时在HTML文档中动态添加< style >标签。
实际工作中将CSS单独打包更有利于缓存和优化,
{
test:/\.css$/,
use:ExtractTextPlugin.extract({
filename:'./dest/[name].[contenthash:8].css',
use:[
{
loader:'css-loader',
options:{
importLoaders:2
}
}
],
publicPath:'/'
})
}
contenthash 就是解耦JS 与CSS 指纹的关键。如果使用chunkhash 会导致单独的CSS文件和主JS文件的hash值一样,改变CSS 主文件hash不变。从而导致CSS改变失效。
- webpack能力之外 - 静态资源定位
8. 资源定位
资源定位指的是存在引用关系的资源之间被引用方地址的改动会同步到引用方,对于构建工具而言就是通过引用方为入口寻找被引用方并
资源定位的阶段和变迁
- 原始形态,仅仅使用相对路径就搞定了
- CDN内容分发网络
- 前后端分离 前端完成资源定位
常规资源定位思维 通过HTML文档引用了哪些文件 以及这些文件具体的应用位置作为资源定位和地址替换的唯一依据。但是这不适合webpack。
webpack的逆向注入模式
webpack将JS视为一切资源的入口(包括HTML),
html可以使用 html-webpack-plugin编译HTML并且将其导出为独立文件。
所以 webpack试讲项目构建输出的js和CSS文件逆向的注入到HTML文档。
- html-loader 和 html-webpack-plugin
html-webpack-plugin中对HTML的解析和编译工作荣然是由html-loader负责的,并且会沿袭webpack中对html-loader的配置项,除了支持导出html文件意外,html-webpack-plugin还增加了诸多利于解决资源自定问配置项
new HtmlWebpackPlugin({
// 构建输出文件
filename:'dest/index.html',
// 源文件
template:'src/index.html',
// 自动注入chunks
inject: true,
// 注入指定的chunks
chunks:[],
// chunks 排序规则
chunksSortMode:'auto'
})
inject 、chunks、chunksSortMode是资源定位的关键配置。
单页面应用的webpack的配置内容
// 文件目录结构如下图
|--assets
| |--logo.png
|--index.html
|--js
| |--main.app.js
|--style
| |--main.app.css
// webpack 的配置内容
{
entry: {
'main.app':'./js/main.app.js'
},
output: {
path: './dest',
filename:'[name].[chunkhash:8].js',
publicPath:'//static.app.com/app/'
},
module:{
rules:[{
test:/\.js$/,
loader:'babel-loader'
},{
test:/\.css$/,
use: ExtractTextPlugin.extract({
use:'css-loader'
})
},{
test:/\.(png|jpe?g|gif)$/,
loader:'url-loader',
options:{
name:'assets/[name].[hash:8].[ext]'
}
}]
},
plugins:[
new ExtractTextPlugin({
filename:'style/[name].[chunkhash:8].css',
}),
new HtmlWebpackPlugin({
filename:'index.html',
template:'index.html',
inject:true
})
]
}
对页面项目的资源定位 使用到了chunks 和 chunksSortMode 实现指定资源的引用 和 排序
比如 index.html页面 要同时引用 a.js 和 b.js 则需要些上 chunks:[a,b] (其中 a和b是在entry参数中指定的js文件的key)。排序 的参数 chunksSortMode的使用
chunksSortMode: (chunkA,chunkb) =>{
const Order = ['a','b'];
const OrderA = Order.indexOf(chunkA.names[0]);
const OrderB = Order.indexOf(chunkB.names[0]);
if(OrderA > OrderB){
return 1
}else if(OrderA < OrderB){
return -1
}else {
return 0
}
}
如果只想注入CSS 不想注入JS 或者想CSS和JS单独排序 则需要借住扩展(也就是自己编写webpack插件)
html-webpack-plugin-befor-html-processing 是一个在编译HTML过程中抛出的事件类型。这种扩展开发比较复杂,可以自行学习webpack插件开发(后面我会搭建一个综合实例演示类似的功能)