老项目一些优化

写个webplugin 记录了webpack在一次compilation所用的时间

const chalk = require('chalk') /* console 颜色 */
var slog = require('single-line-log'); /* 单行打印 console */

class ConsolePlugin {
    constructor(options){
       this.options = options
    }
    apply(compiler){
        /**
         * Monitor file change 记录当前改动文件
         */
        compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => {
            const changeFiles = watching.watchFileSystem.watcher.mtimes
            for(let file in changeFiles){
                console.log(chalk.green('当前改动文件:'+ file))
            }
        })
        /**
         *  before a new compilation is created. 开始 compilation 编译 。
         */
        compiler.hooks.compile.tap('ConsolePlugin',()=>{
            this.beginCompile()
        })
        /**
         * Executed when the compilation has completed. 一次 compilation 完成。
         */
        compiler.hooks.done.tap('ConsolePlugin',()=>{
            this.timer && clearInterval( this.timer )
            const endTime =  new Date().getTime()
            const time = (endTime - this.starTime) / 1000
            console.log( chalk.yellow(' 编译完成') )
            console.log( chalk.yellow('编译用时:' + time + '秒' ) )
        })
    }
    beginCompile(){
       const lineSlog = slog.stdout
       let text  = '开始编译:'
       /* 记录开始时间 */
       this.starTime =  new Date().getTime()
       this.timer = setInterval(()=>{
          text +=  '█'
          lineSlog( chalk.green(text))
       },50)
    }
}
构建时间如上

webpack 优化项

  1. include 或 exclude 限制 loader 范围。
{
    test: /\.jsx?$/,
    exclude: /node_modules/,
    include: path.resolve(__dirname, '../src'),
    use:['happypack/loader?id=babel']
    // loader: 'babel-loader'
}

2.  happypack多进程编译 和 缓存babel编译过的文件

// 加 pluglin 
new HappyPack({
    id:'babel',
    loaders:['babel-loader?cacheDirectory=true']
})

3. tree Shaking 删除冗余代码

4. 按需加载,按需引入。

.babelrc 增加对 antd 样式按需引入

["import", {
    "libraryName":
    "antd",
    "libraryDirectory": "es",
    "style": true
}]

二 路由懒加载,路由监听器

传统路由

<Switch>
    <Route path={'/index'} component={Index} ></Route>
    <Route path={'/list'} component={List} ></Route>
    <Route path={'/detail'} component={ Detail } ></Route>
    <Redirect from='/*' to='/index' />
</Switch>

或者 list保存路由信息,方便在进行路由拦截,或者配置路由菜单等

const router = [
    {
        'path': '/index',
        'component': Index
    },
    {
        'path': '/list'',
        'component': List
    },
    {
        'path': '/detail',
        'component': Detail
    },
]

asyncRouter懒加载路由,并实现路由监听

我们今天讲的这种react路由懒加载是基于import 函数路由懒加载, 众所周知 ,import 执行会返回一个Promise作为异步加载的手段。我们可以利用这点来实现react异步加载路由

const routerObserveQueue = [] /* 存放路由卫视钩子 */
/* 懒加载路由卫士钩子 */
export const RouterHooks = {
  /* 路由组件加载之前 */
  beforeRouterComponentLoad: function(callback) {
    routerObserveQueue.push({
      type: 'before',
      callback
    })
  },
  /* 路由组件加载之后 */
  afterRouterComponentDidLoaded(callback) {
    routerObserveQueue.push({
      type: 'after',
      callback
    })
  }
}
/* 路由懒加载HOC */
export default function AsyncRouter(loadRouter) {
  return class Content extends React.Component {
    constructor(props) {
      super(props)
      /* 触发每个路由加载之前钩子函数 */
      this.dispatchRouterQueue('before')
    }
    state = {Component: null}
    dispatchRouterQueue(type) {
      const {history} = this.props
      routerObserveQueue.forEach(item => {
        if (item.type === type) item.callback(history)
      })
    }
    componentDidMount() {
      if (this.state.Component) return
      loadRouter()
        .then(module => module.default)
        .then(Component => this.setState({Component},
          () => {
            /* 触发每个路由加载之后钩子函数 */
            this.dispatchRouterQueue('after')
          }))
    }
    render() {
      const {Component} = this.state
      return Component ? <Component {
      ...this.props
      }
      /> : null
    }
  }
}

asyncRouter实际就是一个高级组件,将()=>import()作为加载函数传进来,然后当外部Route加载当前组件的时候,在componentDidMount生命周期函数,加载真实的组件,并渲染组件,我们还可以写针对路由懒加载状态定制属于自己的路由监听器beforeRouterComponentLoadafterRouterComponentDidLoaded,类似vue中 watch $route 功能。接下来我们看看如何使用。

import AsyncRouter ,{ RouterHooks }  from ‘./asyncRouter.js’
const { beforeRouterComponentLoad} = RouterHooks
const Index = AsyncRouter(()=>import(‘../src/page/home/index’))
const List = AsyncRouter(()=>import(‘../src/page/list’))
const Detail = AsyncRouter(()=>import(‘../src/page/detail’))
const index = () => {
  useEffect(()=>{
/* 增加监听函数 */
    beforeRouterComponentLoad((history)=>{
      console.log(‘当前激活的路由是’,history.location.pathname)
    })
  },[])
  return <div >
    <div >
      <Router  >
      <Meuns/>
      <Switch>
          <Route path={‘/index’} component={Index} ></Route>
          <Route path={‘/list’} component={List} ></Route>
          <Route path={‘/detail’} component={ Detail } ></Route>
          <Redirect from=’/*’ to=’/index’ />
       </Switch>
      </Router>
    </div>
  </div>
}

受控性组件颗粒化 ,独立请求服务渲染单元

不是所有状态都应该放在组件的 state 中. 例如缓存数据。如果需要组件响应它的变动, 或者需要渲染到视图中的数据才应该放到 state 中。这样可以避免不必要的数据变动导致组件重新渲染.

颗粒化控制可控性组件 / 独立的请求渲染单元

1 可以避免父组件的冗余渲染 ,react的数据驱动,依赖于 state 和 props 的改变,改变state 必然会对组件 render 函数调用,如果父组件中的子组件过于复杂,一个自组件的 state 改变,就会牵一发动全身,必然影响性能,所以如果把很多依赖请求的组件抽离出来,可以直接减少渲染次数。

2 可以优化组件自身性能,无论从class声明的有状态组件还是fun声明的无状态,都有一套自身优化机制,无论是用shouldupdate 还是用 hooks中 useMemo useCallback ,都可以根据自身情况,定制符合场景的渲条 件,使得依赖数据请求组件形成自己一个小的,适合自身的渲染环境。

3 能够和redux ,以及redux衍生出来 redux-action , dva,更加契合的工作,用 connect 包裹的组件,就能通过制定好的契约,根据所需求的数据更新,而更新自身,而把这种模式用在这种小的,需要数据驱动的组件上,就会起到物尽其用的效果。

shouldComponentUpdate ,PureComponent 和 React.memo ,immetable.js 助力性能调优

我们用react-redux来简单举一个例子,如下所示 数据都已经被 immetable.js处理。

import { is  } from 'immutable'
const GoodItems = connect(state =>
    ({ GoodItems: filter(state.getIn(['Items', 'payload', 'list']), state.getIn(['customItems', 'payload', 'list'])) || Immutable.List(), })
    /* 此处省略很多代码~~~~~~ */
)(memo(({ Items, dispatch, setSeivceId }) => {
   /*  */
}, (pre, next) => is(pre.Items, next.Items)))

规范写法,合理处理细节问题

绑定事件尽量不要使用箭头函数

react更新来大部分情况自于props的改变(被动渲染),和state改变(主动渲染)。当我们给未加任何更新限定条件子组件绑定事件的时候,或者是PureComponent 纯组件, 如果我们箭头函数使用的话。

<ChildComponent handerClick={()=>{ console.log(666) }}  />

每次渲染时都会创建一个新的事件处理器,这会导致 ChildComponent 每次都会被渲染。

解决问题

function index(){
    const handerClick1 = useMemo(()=>()=>{
       console.log(777)
    },[])  /* [] 存在当前 handerClick1 的依赖项*/
    const handerClick = useCallback(()=>{ console.log(666) },[])  /* [] 存在当前 handerClick 的依赖项*/
    return <div>
        <ChildComponent handerClick={ handerClick }  />
        <div onClick={ handerClick1 }  >hello,world</div>
    </div>
}

循环正确使用key

错误用法一:用index做key

错误用法二:用index拼接其他的字段

正确用法:用唯一id作为key

无状态组件hooks-useMemo 避免重复声明

错误用法

function Index(){
    const [ number , setNumber  ] = useState(0)
    const handerClick1 = ()=>{
        /* 一些操作 */
    }
    const handerClick2 = ()=>{
        /* 一些操作 */
    }
    const handerClick3 = ()=>{
        /* 一些操作 */
    }
    return <div>
        <a onClick={ handerClick1 } >点我有惊喜1</a>
        <a onClick={ handerClick2 } >点我有惊喜2</a>
        <a onClick={ handerClick3 } >点我有惊喜3</a>
        <button onClick={ ()=> setNumber(number+1) } > 点击 { number } </button>
    </div>
}

正确写法

function Index(){
    const [ number , setNumber  ] = useState(0)
    const [ handerClick1 , handerClick2  ,handerClick3] = useMemo(()=>{
        const fn1 = ()=>{
            /* 一些操作 */
        }
        const fn2 = ()=>{
            /* 一些操作 */
        }
        const  fn3= ()=>{
            /* 一些操作 */
        }
        return [fn1 , fn2 ,fn3]
    },[]) /* 只有当数据里面的依赖项,发生改变的时候,才会重新声明函数。 */
    return <div>
        <a onClick={ handerClick1 } >点我有惊喜1</a>
        <a onClick={ handerClick2 } >点我有惊喜2</a>
        <a onClick={ handerClick3 } >点我有惊喜3</a>
        <button onClick={ ()=> setNumber(number+1) } > 点击 { number } </button>
    </div>
}

懒加载 Suspense 和 lazy

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function demo () {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
        <LazyComponent1 /> //Suspense 可以包裹多个懒加载组件
      </Suspense>
    </div>
  )
}

多种方式避免重复渲染

批量更新

class index extends React.Component{
    constructor(prop){
        super(prop)
        this.state = {
            a:1,
            b:2,
            c:3,
        }
    }
    handerClick=()=>{
        const { a,b,c } :any = this.state
        this.setState({ a:a+1 })
        this.setState({ b:b+1 })
        this.setState({ c:c+1 })
    }
    render= () => <div onClick={this.handerClick} />
}

点击事件发生之后,会触发三次 setState,但是不会渲染三次,因为有一个批量更新batchUpdate批量更新的概念。三次setState最后被合成类似如下样子




    const  [ a , setA ] = useState(1)
    const  [ b , setB ] = useState({})
    const  [ c , setC ] = useState(1)
    const handerClick = () => {
        setB( { ...b } ) 
        setC( c+1 ) 
        setA( a+1 )
    }

webpack !! 作用

  • adding ! to a request will disable configured preLoaders
  • adding !! to a request will disable all loaders specified in the configuration
  • adding -! to a request will disable configured preLoaders and loaders but not the postLoaders
require('!!bootstrap-webpack!bootstrapConfig');
 // bootstrapConfig是我在webpack配置文件中设好的alias,不设的话这里就填实际的路径就好了
// 后面一个感叹号分割的意思

bootstrap-webpack其实就是一个webpack的loader,所以这里是用loader的语法。需要注意的是,如果你在webpack配置文件中针对js文件设置了loader(比如说babel),那么在加载bootstrap-webpack的时候请在最前面加个!!,表示这个require语句忽略webpack配置文件中所有loader的配置

loader 里感叹号

 {
            test: /\.css$/,
            loader: 'style-loader!css-loader?modules'
   }

  • !感叹号是分割符,表示两个工具都参与处理。
  • ?问号,其实跟url的问号一样,就是后面要跟参数的意思。
  • 而modules这个参数呢,就是将css打包成模块。跟js打包是一样的,你不必再担心不同模块具有相同类名时造成的问题了。

webpack require 和 import 详解 / hash

import的加载是加载的模块的引用。而import()加载的是模块的拷贝,就是类似于require(),怎么来说明?看下面的例子
module.js 文件:

[cc lang=”js”]export let counter = 3;
export function incCounter() {
counter++;
}[/cc]

main.js 文件:

[cc lang=”js”]let filename = ‘module.js’;
import(‘./’ + filename).then(({counter, incCounter})=>{
console.log(counter); //3
incCounter();
console.log(counter); //3
}); [/cc]

[cc lang=”js”]import {counter, incCounter} from ‘./module.js’;
console.log(counter); //3
incCounter();
console.log(counter); //4[/cc]

commonjs同步语法
经典的commonjs同步语法如下:

var a = require(‘./a’);
a.show();

commonjs异步加载
[cc lang=”js”]
require.ensure([ ], function(require){
var list = require(‘./list’);
list.show();
}, ‘list’); // 第三个参数 可以有可无 是给chunk命名

[/cc]
此时list.js会被打包成一个单独的chunk文件,大概长这样 :list.fb874860b35831bc96a8.js

如果你在require.ensure的函数中引用了两个以上的模块,webpack会把它们打包在一起,比如:
[cc lang=”js”]require.ensure([ ], function(require){
var list = require(‘./list’);
list.show();
var edit = require(‘./edit’);
edit.display();
}, ‘list_and_edit’);
[/cc]
list.js和edit.js将会被打包成一个文件,并命名为list_and_edit.js。这就需要根据你的实际情况来衡量了,如果你不希望打包在一起,只能写两个require.ensure分别引用这两个文件。

commonjs预加载懒执行
在上面的用法中,我们给require.ensure的第一个参数传了空数组,实际上这里是可以接收模块名称的,作用就是实现预加载懒执行。用法如下:
先下载 后执行
[cc lang=”js”]
require.ensure([ ‘./list’], function(require){
var list = require(‘./list’);
list.show();
});
执行到这里的时候list.js会被浏览器下载下来,但是并不会执行list.js模块中的代码,真正进行evaluate的时候是到了后面这句var list = require(‘./list’)
[/cc]

webpack自带的require.include
require.include是webpack自己提供的,并没有什么规范做后台,所以是个小角色。它可以实现上面是预加载功能,而不用把模块写在数组中,用法如下:
[cc lang=”js”]
require.ensure([ ], function(require){
require.include(‘./preview’); //加载
let p = require(‘./preview’); //执行
p.getUrl(); //使用
}, ‘pre’);
[/cc]

AMD异步加载
webpack既支持commonjs规范也支持AMD规范,这就意味着AMD的经典语法是可以正常使用的,如:

[cc lang=”js”]
require([ ‘./list’], function(list){
list.show();
});
//多文件
require([ ‘./list’, ‘./edit’], function(list, edit){
list.show();
edit.display();
});
[/cc]

runtimeChunk

runtime 指的是 webpack 的运行环境(具体作用就是模块解析, 加载) 和 模块信息清单, 模块信息清单在每次有模块变更(hash 变更)时都会变更, 所以我们想把这部分代码单独打包出来, 配合后端缓存策略, 这样就不会因为某个模块的变更导致包含模块信息的模块

// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true,
},
};

// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ‘vendor’,

minChunks: module => module.context &&
module.context.includes(‘node_modules’),
}),

// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: ‘runtime’,

// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity,
}),
],
};

runtime 写入 html 减少请求
new HtmlWebpackPlugin({
title: “HappyEasyGo”,
template: “./index.html”,
filename: path.resolve(__dirname, “./build/index.html”),
// chunksSortMode: ‘dependency’,
inlineSource: ‘runtime.+\\.js’,
}),

new InlineSourcePlugin(HtmlWebpackPlugin),

CommonsChunkPlugin 详解,webpack4 改成optimization.splitChunks

CommonsChunkPlugin:
1. name和names:将符合条件的模块提取到指定name的chunk中. 如果name为不存在的chunk则创建新chunk,如果name指定的chunk存在,则继续往chunk中添加被提取的模块内容所有chunk的公共代码
2 filename:指定提取出的公共代码的文件名称可以带路径,不指定 则使用output配置项中文件名的占位符。未定义时使用name作为文件名。
3 chunks:可以指定要提取公共模块的源chunks,指定的chunk必须是公共chunk的子模块,如果没有指定则使用所有entry中定义的入口chunk。
4 minChunks:在一个模块被提取到公共chunk之前,它必须被最少minChunks个chunk所包含。(通俗的说就是一个模块至少要被minChunks个模块所引用,才能被提取到公共模块。)
该数字必须不小于2或者不大于chunks的个数。默认值等于chunks的个数
如果指定了Infinity,则创建一个公共chunk,但是不包含任何模块,内部是一些webpack生成的runtime代码和chunk自身包含的模块(如果chunk存在的话)。
5 children:children和chunks不能同时设置,因为它们都是指定source chunks的
6 async

注意:vender是我们提取出来的公共chunk,通常不会被修改,所以理应在每次编译后文件名保持一致
所以 抽离第三方库和自定义公共模块

[cc lang=”javascript”]
// 第一种方法minChunks设为Infinity
//Infinity:只有当入口文件(entry chunks) >= 3 才生效,用来在第三方库中分离自定义的公共模块
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: [‘vendor’,’runtime’], // 在vendor上继续抽离 webpack运行文件
filename: ‘[name].js’,
minChunks: Infinity // 抽离自定义公共模块
}),
new webpack.optimize.CommonsChunkPlugin({
name: ‘common’,
filename: ‘[name].js’,
chunks: [‘first’,’second’]
//从first.js和second.js中抽取commons chunk, 也可以用 children和async 异步加载公共模块
}),
][/cc]

[cc lang=”javascript”]
//module:当前chunk及其包含的模块
//count:当前chunk及其包含的模块被引用的次数
//minChunks作为函数会遍历每一个入口文件及其依赖的模块,返回一个布尔值,为true代表当前正在处理的文件(module.resource)合并到commons chunk中,为false则不合并。
const config = {
entry: {
first: ‘./src/first.js’,
second: ‘./src/second.js’,
//vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ‘vendor’,
filename: ‘[name].js’,
minChunks: function (module,count) {
console.log(module.resource,`引用次数${count}`);
//”有正在处理文件” + “这个文件是 .js 后缀” + “这个文件是在 node_modules 中”
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(path.join(__dirname, ‘./node_modules’)) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: ‘runtime’,
filename: ‘[name].js’,
chunks: [‘vendor’]
}),
]
}[/cc]

[cc lang=”javascript”]

new webpack.optimize.CommonsChunkPlugin({
children: true,
async: ‘children-async’
})[/cc]

你设置之后,也得看children的脸色怎么来划分:
children为true,共同引用的模块就会被打包合并到名为children-async的公共模块,当你懒加载first或者second的时候并行加载这和children-async公共模块
children为false,共同引用的模块就会被打包到首屏加载的app.bundle当中,这就会导致首屏加载过长了,而且也不要用到,所以最好还是设为true

webpack 4

splitChunks

[cc lang="js"]

// b.js
import vue from "vue" 

// 入口js
import _ from "lodash"
import "./a.js" //同步加载
import ('./b.js') //异步加载
[/cc]

chunks:抽取公共模块的来源,只有三个值

 async:(动态加载模块) 只分割异步打包的代码,b.js 会打包出b和vue两个chunk, lodash 拆不出来

 initial:(入口模块),只从入口进行拆分,但是异步内部的引入将不再考虑,直接打包一起,会将vue和b的内容打在一起

 all:(全部模块入口和动态的) 分割异步同步代码(需要定义新规则,将同步的代码打包) 

[cc lang=”js”]
//同步代码分割配置
splitChunks: {
chunks: ‘all’,
cacheGroups: {
a : {
name: ‘a’,
chunks: ‘all’,
}
}
}
// 会生成 一个同步代码a.js 和 分离出的b.js 和 vue 两个 chunk
[/cc]

Function(chunk): chunk对象包含chunk本身的许多信息,有用的也就是chunk.name,可以通过name选择特定的模块来源,返回true则选中,否则不选中。

cacheGroups 自定义配置决定生成的文件,缓存策略

  • test : 限制范围,正则匹配文件夹或文件
  • name : 打包的chunks的名字
  • priority : 优先级,多个分组冲突时决定把代码放在哪块
  • enforce: 强制生成

maxAsyncRequests:  异步的按需加载模块最大的并行请求数,通过import()或者require.ensure()方式引入的模块,分离出来的包是异步加载的。default:5
maxInitialRequests: entry文件最大的并行请求数, 默认3
minSize: 生成新的chunk的最小体积,默认30000B
maxSize: 打包出来的新的chunk最大的文件大小,与minSize相对,超过这个值,将新的chunk再进一步拆分成更小的chunk,单位B,默认0。
automaticNameDelimiter: name连接符,分离出来的新chunk的名字,默认基于cacheGroupsKey,chunks来源的name来取,例:缓存组的cacheGroupsKey是vendor,来源chunk是的name是adminA,adminB,那么连接符是~,分离出的chunk名字是vendor~adminA~adminB.js。

name: Boolean | string | function (module, chunks, cacheGroupKey)

分离出来的chunk的取名规则。不能和入口文件同名,如果和入口文件同名,那么入口文件将会被移除,不会被打包

          Boolean :如果是true,基于cacheGroupsKey,chunks来源的name来取。如果是false,按照数字0排序。
         string : string作为name, string.js。
         function (module, chunks, cacheGroupKey)
              cacheGroupKey: 缓存组的键名;
              chunks: Array类型,splitChunks.chunks的数组集合,每一个chunk对象有很多属性,这里有用也就         只有chunks[i].name,即打包来源chunk的键名;
              module: 可能有用的值module.context,module.resource,module.type;module.context公共模块来源chunk的所在文件夹,例如通过 npm 下载的包chunks.context == path.resolve(__dirname , ‘node_modules’);,module.resource公共模块来源chunk的完整路径,module.type文件类型javascript/auto,css/auto。
            返回值: 返回值作为文件名,同样不能和入口文件同名,否则入口文件会被移除

!!@  如果你要做精细化的代码分离,需要配置缓存组cacheGroups,

cacheGroups拥有上面所有的属性,除此之外还有priority、reuseExistingChunk、test、enforce四个属性,并且缓存组能够覆盖上面所有的属性。

priority :优先级,default:0;如果两个缓存组都需要将某一公共模块打包,为了不重复打包,肯定只能打包进入其中之一,那么优先考虑priority 高的。
reuseExistingChunk:是否重用已经存在的模块,default:true;例:如果在当前缓存组需要抽离出 jquery.js,但是 jquery.js已经被其它缓存组抽取出来了,那么将会重用已经抽取出来的 jquery.js。
test:function (module, chunks) | RegExp | string 在chunks的基础上,精确的选择那些公共模块应该被打包。
enforce:忽略minSize、maxSize、maxAsyncRequests、maxInitalRequests等限制条件直接打包。

缓存组也有默认配置 default ,default的配置会覆盖splitChunks中的默认配置,并且其它缓存组的minSize的优先级低于default缓存组的优先级,所以默认配置最好还是在splitChunks配置,不要在缓存组配置:

[cc lang=”js”]
splitChunks: {
chunks: ‘all’, // initial、async和all
minSize: 30000, // 形成一个新代码块最小的体积
maxAsyncRequests: 5, // 按需加载时候最大的并行请求数
maxInitialRequests: 3, // 最大初始化请求数
automaticNameDelimiter: ‘~’, // 打包分割符
name: true,
cacheGroups: {
vendors: { // 项目基本框架等
chunks: ‘all’,
test: /[\\/]node_modules[\\/](vue|vue-resource|vuex|underscore|mint-ui|jsonp|axios|babel-polyfill)[\\/]/, // 需要加node_modules,不然打包不纯
priority: 100,
name: ‘vendors’,
},
‘async-commons’: { // 异步加载公共包、组件等
chunks: ‘async’,
minChunks: 2,
name: ‘async-commons’,
priority: 90,
},
commons: { // 其他同步加载公共包
chunks: ‘all’,
minChunks: 2,
name: ‘commons’,
priority: 80,
},
}
}
// 同样用到react 单priority 等级高, 只会打包到vendors 里
// 如果我们这里将commons的配置去掉,将会生成一个topic~home的包,我们配置了async-common提取出异步加载的公共包后,将会默认将同步加载的公共包打包生成以automaticNameDelimiter连接符‘~’生成的名字’topic~home’包,内容其实和commons包内容完全一样
[/cc]

最后
hash 是 build-specific ,即每次编译都不同——适用于开发阶段
chunkhash 是 chunk-specific,是根据每个 chunk 的内容计算出的 hash——适用于生产

问题
chunk 不能打包到html 里

webpack 2 升级到 webpack 4

1 安装 webpck 4, 根据提示安装新的 版本,
涉及插件

[cc lang=”json”]”vue-loader”: “^15.0.10”,
“vue-style-loader”: “^4.1.0”,
“vue-template-compiler”: “^2.5.16”,
“copy-webpack-plugin”: “^4.0.1”,
“extract-text-webpack-plugin”: “^4.0.0-beta.0”,
“html-webpack-plugin”: “^3.1.0”,
“optimize-css-assets-webpack-plugin”: “^4.0.0”,
“webpack-bundle-analyzer”: “^2.11.1”,
“webpack-dev-middleware”: “^3.1.2”,
“webpack-dev-server”: “^3.1.3”,
“webpack-merge”: “^4.1.0”
[/cc]

2 更新各种loader
mini-css-extract-plugin
这个插件将css按模块化解压到单独的文件中,为每个包含css的js文件创键一个css文件。
style-loader 是将css文件以行内样式style的标签写入打包后的html页面,mini-css-extract-plugin是直接link方式引入进去
extract-text-webpack-plugin
根据创键实例、或者配置多个入口chunk来的,比如配置了一个入口文件,最终所有的css都会提取在一个样式文件中;如果n个入口(即创键了n个实例),则会生成多个css文件
3 webpack.optimize.CommonsChunkPlugin 提取公共代码 改为 optimization.splitChunks

4 vue-loader
*需要把 VueLoaderPlugin = require(‘vue-loader/lib/plugin’) 添加到plugin 中
*v15版本之后(使用了不同的策略 推导.vue文件中各个语言块使用的loader,将各个语言块视为独立的文件,使用webpack中配置了规则的loader处理,由此带来的配置变化就是针对样式处理,webpack.rules中必须显示提供对应loader处理的规则),所以原有vue-loader配置中内联传入的样式相关的loader可以去除

[cc lang=”js”]test: /\.vue$/,
loader: ‘vue-loader’,
options: {
// v15+版本不再需要内联的cssloader配置
// 如果不去掉会报错??(我的项目未去掉也没报错,emmm…?)
}
}[/cc]

*鉴于第2点的推导变化,<script><script/>标签内的js代码将被视为独立的js文件并根据webpack配置使用babel-loader转译;因此,如果项目配置babel-loader时使用exclude:/node_modules/排除了依赖包中代码的转译,并且导入了/node_modules/中的.vue文件,<script>部分将不会被转译,故需要将.vue文件加入排除名单中

[cc lang=”js”]{
test: /\.js$/,
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
),
loader:’babel-loader’
},[/cc]

5 package.json
// window下直接set会不支持,需要npm i cross-env
“dev”: “cross-env NODE_ENV=development …”
// webpack4
webpack运行时还会根据mode设置一个全局变量process.env.NODE_ENV,这里的process.env.NODE_ENV不是node中的环境变量,而是webpack.DefinePlugin中定义的全局变量,允许你根据不同的环境执行不同的代码.

“dev”: “webpack –mode development”
“build”: “webpack –mode production”

6. minimize
默认为true,效果就是压缩js代码(代替以前的webpack.optimize.UglifyJsPlugin)

minimizer
可以自定义UglifyJsPlugin和一些配置,默认的压缩为uglifyjs-webpack-plugin 【当使用了OptimizeCSSAssetsPlugin插件,内置的uglifyjs-webpack-plugin插件会失效,这边需要重新配置下UglifyJsPlugin】

// 新增css的压缩
minimizer:
mode === “development”
? []
: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: mode === “development”
}),
new OptimizeCSSAssetsPlugin()
]

7 CSS文件提取插件
mini-css-extract-plugin 替代 extract-text-webpack-plugin

[cc lang=”js”]
//webpack 文件配置
const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);
const devMode = process.env.NODE_ENV !== ‘production’;

module.exports = {
plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: devMode ? ‘[name].css’ : ‘[name].[hash].css’,
chunkFilename: devMode ? ‘[id].css’ : ‘[id].[hash].css’,
}),
],
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: process.env.NODE_ENV === ‘development’,
// if hmr does not work, this is a forceful method.
reloadAll: true,
},
},
‘css-loader’,
‘postcss-loader’,
‘sass-loader’,
],
},
],
},
};[/cc]

[cc lang=”js”]
//Extracting all CSS in a single file 生成css 到一个文件
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: ‘styles’,
test: /\.css$/,
chunks: ‘all’,
enforce: true,
},
},
},
},[/cc]

根据 entry point 或 chunk 打包不同css 文件 参考 https://www.npmjs.com/package/mini-css-extract-plugin

!!!优化css的处理器

[cc lang=”js”] new OptimizeCSSAssetsPlugin ({
// 默认是全部的CSS都压缩,该字段可以指定某些要处理的文件
assetNameRegExp: /\.(sa|sc|c)ss$/g,
// 指定一个优化css的处理器,默认cssnano
cssProcessor: require(‘cssnano’),

cssProcessorPluginOptions: {
preset: [ ‘default’, {
discardComments: { removeAll: true}, //对注释的处理
normalizeUnicode: false // 建议false,否则在使用unicode-range的时候会产生乱码
}]
},
canPrint: true // 是否打印编译过程中的日志
})[/cc]

!! 消除未使用的CSS
1、安装
npm i purify-webpack purify-css -D

2、引入及配置

[cc lang=”js”]const glob = require(‘glob’)
const PurifyCssPlugin = require(‘purifycss-webpack’)
plugins: [
new PurifyCssPlugin ({
paths: glob.sync(path.join(__dirname, ‘/*.html’))
})
][/cc]

!! !1. 开发模式要用到 new webpack.HotModuleReplacementPlugin()
2. 开发模式 配置里不能用 contentHash, chunkHash, 直接用hash 或者不用
3. 大量输出日志处理

[cc lang=”js”]stats: {
children: false
},[/cc]

4

分离出第三方库、自定义公共模块、webpack运行文件

1、抽离webpack运行文件

[cc lang=”javascript” tab-size=”2″]
entry: {
first: ‘./src/first.js’,
second: ‘./src/second.js’,
vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
},

//先来抽离webpack运行文件,修改webpack配置文件:

plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: [‘vendor’,’runtime’],
filename: ‘[name].js’
}),
]

[/cc]

其实上面这段代码,等价于下面这段:

[cc lang=”javascript” tab-size=”2″]

plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ‘vendor’,
filename: ‘[name].js’
}),
new webpack.optimize.CommonsChunkPlugin({
name: ‘runtime’,
filename: ‘[name].js’,
chunks: [‘vendor’]
}),
]

[/cc]

上面两段抽离webpack运行文件代码的意思是创建一个名为runtime的commons chunk进行webpack运行文件的抽离,其中source chunks是vendor.js。

2、抽离第三方库和自定义公共模块

要在vendor.js中把第三方库单独抽离出来,上面也说到了有两种方法。

第一种方法minChunks设为Infinity,修改webpack配置文件如下:

[cc lang=”javascript” tab-size=”2″]

plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: [‘vendor’,’runtime’],
filename: ‘[name].js’,
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: ‘common’,
filename: ‘[name].js’,
chunks: [‘first’,’second’]//从first.js和second.js中抽取commons chunk
}),
]

[/cc]

这时候的vendor.js就纯白无瑕,只包含第三方库文件,common.js就是自定义的公共模块,runtime.js就是webpack的运行文件。

第二种方法把它们分离开来,就是利用minChunks作为函数的时候,说一下minChunks作为函数两个参数的含义:
module:当前chunk及其包含的模块
count:当前chunk及其包含的模块被引用的次数
minChunks作为函数会遍历每一个入口文件及其依赖的模块,返回一个布尔值,为true代表当前正在处理的文件(module.resource)合并到commons chunk中,为false则不合并。

[cc lang=”javascript” tab-size=”2″]
const config = {
entry: {
first: ‘./src/first.js’,
second: ‘./src/second.js’,
//vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
},
output: {
path: path.resolve(__dirname,’./dist’),
filename: ‘[name].js’
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ‘vendor’,
filename: ‘[name].js’,
minChunks: function (module,count) {
console.log(module.resource,`引用次数${count}`);
//”有正在处理文件” + “这个文件是 .js 后缀” + “这个文件是在 node_modules 中”
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(path.join(__dirname, ‘./node_modules’)) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: ‘runtime’,
filename: ‘[name].js’,
chunks: [‘vendor’]
}),
]
}

[/cc]

参考文章

webpack externals providePlugin expose-loader

externals

用script 引入的 库

// 入口文件
import $ from 'jquery'
console.log($)
 
//webpack配置
module.exports = {
    externals: {
        jquery: 'jquery'
    }
}

providePlugin

provideplugin 自动加载到每个模块, 而不必导出 import 或 require

new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery'
})

expose-loader 将库引入到全局作用域

module: {
  rules: [{
    test: require.resolve('jquery'),
    use: {
      loader: 'expose-loader',
      options: '$'
    }
  }]
}
console.log(window.$)