脑阔疼的webpack按需加载

栏目: 编程语言 · 发布时间: 7年前

内容简介:随着单页应用发展的越来越庞大,拆分js就是第一要务,拆分后的js,就可以根据我们需求来有选择性的加载了。所以第一个问题,就是js怎么拆?来个demo,先看一下未拆分之前是什么样子: a.js:

随着单页应用发展的越来越庞大,拆分js就是第一要务,拆分后的js,就可以根据我们需求来有选择性的加载了。

所以第一个问题,就是js怎么拆?

Q2:js怎么拆?

1,未拆分前是什么样子?

来个demo,先看一下未拆分之前是什么样子: a.js:

import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
  b();
}
复制代码

b.js:

export default ()=>{
  console.log("this is b");
}
复制代码

html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="btn">btn</div>
  <script src="./dist/main.js"></script>
</body>
</html>
复制代码

webpack.config.js

module.exports = {
  entry:'./a.js',
  output:{
    filename:'[name].js'
  }
}
复制代码
  1. a.js引用b.js
  2. webpack打包将b、a都打包到了一起,输出一个默认的main.js
  3. html引用打包好的main.js 结果如下:
脑阔疼的webpack按需加载

2,开搞!

step1:修改webpack.config.js

module.exports = {
  entry:'./a.js',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js'// 设置按需加载后的chunk名字
  }
}
复制代码

这里就添加了一句,chunkFilename而已,chunkFilename的作用就是用来给拆分后的chunk们起名字的配置项。 ok,执行webpack

脑阔疼的webpack按需加载

还是只打包出了一个main.js,毫无变化... 不用担心,这是因为还有设置没搞定。

step2:修改a.js

// import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
    import('./b').then(function(module){
      const b = module.default;
      b();
    })
}
复制代码
  1. 使用es6的import按需语法
  2. 在promise后执行拿到的返回的结果 此时再次执行webpack:
脑阔疼的webpack按需加载

输出文件变成了两个,1个main.js、1个1.js 这个1.js很迷...

脑阔疼的webpack按需加载

查看一下源码,可以看出来,它其实就是我们的b.js

总结一下 :

  • webpack中output的设置并不决定是否拆分代码
  • 拆分代码的决定因素在import语法上
  • webpack在扫描到代码中有import语法的时候,才决定执行拆分代码

step3:怎么使用?

脑阔疼的webpack按需加载

额,成功报错了...脑阔疼 分析报错:

  • 按需加载找的文件是/1.js
  • 但我们打包的结果在dist目录下,自然不可能在根目录下找到

step4:配置Public Path基础路径

该配置能帮助你为项目中的所有资源指定一个基础路径。它被称为 公共路径(publicPath) 修改webpack.config.js

module.exports = {
  entry:'./a.js',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 设置按需加载后的chunk名字
    publicPath:'dist/' // 设置基础路径
  }
}
复制代码

step5:验证结果

脑阔疼的webpack按需加载
  • 点击前
  • 只引用了main.js
脑阔疼的webpack按需加载
  • 点击后
  • 加载了1.js
  • 并执行了1.js中的js代码
  • 控制台输出this is b.js
  • ok,验证成功

step6:填坑

前面1.js这玩意也不可读啊,有问题也很难明确,webpack,提供了定义按需chunkname的方式,修改a.js:

// import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
  import(/* webpackChunkName: "b" */ './b').then(function(module){
    const b = module.default;
    b();
  })
}

复制代码

在动态引入的语法前,添加了注释,注释就是为chunk命明的方式,结果:

脑阔疼的webpack按需加载

输出了b.js,测试回归一次:

脑阔疼的webpack按需加载
  • chunk名字对按需加载没有影响
  • 修改了按需chunk的名字也只是方便文件可读性

Q3:按需加载之后还能热更新吗?

1,先跑个webpack-dev-server集成起来

先安装webpack-dev-server,配置npm scripts

{
  "devDependencies": {
    "webpack-dev-server": "^3.1.9"
  },
  "scripts": {
    "start:dev": "webpack-dev-server"
  },
  "dependencies": {
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2"
  }
}

复制代码

修改webpack.config.js

var path = require('path');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 设置按需加载后的chunk名字
    publicPath:'dist/'
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000
  }
}
复制代码
  • 这一次不再通过webpack命令来执行了
  • 而是通过npm run start:dev命令行来执行
  • webpack-dev-server会读取webpack.config.js中的devServer配置
  • ok,devServer已经集成好了

2,跑起来看看

修改webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 设置按需加载后的chunk名字
    publicPath:'dist/'
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000,
    hot: true, // 开启热更新
  },
  plugins: [ // 开始热更新
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ],
}
复制代码

上面一共起作用的就是3句话:

  • devServer中的hot语句
  • plugins中的两个webpack内置插件 将这两个插件开启后,还不行,还需要修改入口文件
// import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
  import(/* webpackChunkName: "b" */ './b').then(function(module){
    const b = module.default;
    b();
  })
}

if (module.hot) {// 开启热替换
     module.hot.accept()
}
复制代码

ok,就这么简单,热更新+按需加载就齐活了。

Q4:react-router集成按需加载

react-router文档

业务中,除了点击的时候按需加载,还有大部分场景都是在路由切换的时候进行按需加载

step1:添加babel-loader

修改webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 设置按需加载后的chunk名字
    publicPath:'dist/'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000,
    hot: true,
  },
  plugins: [
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ],
}
复制代码

上面新增的就是添加了一个babel-loader

step2:添加.babelrc

{
  "presets": ["@babel/preset-react","@babel/preset-env"]
}
复制代码

step3:书写jsx

修改a.js

import React,{Component} from 'react';
import ReactDom from 'react-dom';
import B from './b.js';
export default class A extends Component{
  render(){
    return <div>
      this is A
      <B />
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}
复制代码

修改b.js

import React,{Component} from 'react';
export default class B extends Component{
  render(){
    return <div>this is B</div>
  }
}
复制代码

测试一下:

  • react跑起来了
  • 热更新依旧有效果

step4:集成 react-loadable

react按需加载进化了好几个方式,目前最新的方式就是使用react-loadable这个组件 官方也推荐使用这个库来实现,目前这个库已经1w+star了

修改a.js

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';
import Loadable from 'react-loadable';

const Loading = () => <div>Loading...</div>;

const B = Loadable({
  loader: () => import('./b.js'),
  loading: Loading,
})
const C = Loadable({
  loader: () => import('./C.js'),
  loading: Loading,
})
export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Route path="/B" component={B}/>
          <Route path="/C" component={C}/>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}

复制代码
  • loadable中使用的import语法是ECMA未来会支持的动态加载特性
  • loadable很简单,只需要按照它所规定的语法,包裹一下需要加载的组件就可以
脑阔疼的webpack按需加载

点击跳转toC

脑阔疼的webpack按需加载

可以看到加载了1.js,也就是说异步加载顺利完成 但是现在存在问题:在/C路径下刷新,会出现无法命中路由的情况

step5:跑个express验证一下

var express = require('express')
var app = express()
app.use(express.static('dist'))


app.get('*', function (req, res) {
  res.send(`<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
  </head>
  <body>
    <div id="btn">btn</div>
    <script src="./main.js"></script>
  </body>
  </html>`)
})
app.listen(5000);
复制代码

创建一个简单的express应用:

  • 验证通过
  • 同样会执行按需加载

step6:嵌套路由按需加载

路由一个很常见的功能就是路由嵌套,所以我们的按需加载必须支持嵌套路由才算合理 修改a.js

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';
import Loadable from 'react-loadable';

const Loading = (props) => {
  return <div>Loading...</div>
};
const B = Loadable({
  loader: () => import('./b.js'),
  loading: Loading,
})
const C = Loadable({
  loader: () => import('./c.js'),
  loading: Loading,
})
export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Route path="/B" component={B}/>
          <Route path="/C" component={C}/>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}

复制代码

修改c.js

import React,{Component} from 'react';
import { Route,Link} from 'react-router-dom';
import Loadable from 'react-loadable';

const Loading = (props) => {
  return <div>Loadingc...</div>
};

const D = Loadable({
  loader: () => import('./d.js'),
  loading: Loading,
})
export default class C extends Component{
  render(){
    return <div>
      this is C
      <Route path="/C/D" component={D}/>
      <Link to="/C/D">to D</Link>
    </div>
  }
}

复制代码
  • 入口文件引入两个动态路由B、C
  • c.js中嵌套了路由/C/D
  • 路由/C/D中使用了按需组件D

step7:验证嵌套路由

入口没问题

脑阔疼的webpack按需加载

点击跳转动态加载C没问题

脑阔疼的webpack按需加载

点击跳转D不行了

脑阔疼的webpack按需加载

可以看到动态引入资源./d.js的时候,出现了异常,莫名其妙的添加了路径/C

step8:该死的 publicPath

这里疑惑了好一会,还查了很多内容,最后痛定思痛察觉到应该还是publicPath设置有问题,重新检查了设置,修改webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    path:path.resolve(__dirname, 'dist'),
    filename:'[name].js',
    chunkFilename:'[name].js',// 设置按需加载后的chunk名字
    publicPath:'/dist/'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000,
    hot: true,
  },
  plugins: [
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ],
}
复制代码

这里唯一的改动,就是publicPath由原来的dist/,变成/dist/,只要把前面的路径补上,就不会去找相对的地址了。

Q5:到真实项目里怎么搞?

前面看似解决了问题,但在真实场景下,我们的要求肯定会更高! 首先就是要封装一个便捷使用的按需加载组件。

step1:封装LazyLoad组件

理想很美好,现实很骨干

const LazyLoad = (path)=>{
  return Loadable({
    loader: () => import(path),
    loading: Loading,
  })
}

const B = LazyLoad('./b.js')
复制代码

然后就收获了报错

脑阔疼的webpack按需加载

这是因为webpack编译的时候import预发==不支持动态路径==

step2:可怕的import,了解一下

import不支持动态路径,是因为webpack需要先扫一遍js文件,找出里面按需加载的部分,进行按需打包,但不会关心内部的js执行上下文,也就是说,在webpack扫描的时候,js中的变量并不会计算出结果,所以import不支持动态路径。

step3:封装非import部分

既然import不能搞,那只能封装非import的部分了

const LazyLoad = loader => Loadable({
  loader,
  loading:Loading,
})
复制代码

把loader这部分当作参数分离出去,下面就是具体的使用

const B = LazyLoad(()=>import('./b.js'));
const C = LazyLoad(()=>import('./c.js'));
复制代码

下面是全部代码

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';
import Loadable from 'react-loadable';

const Loading = (props) => {
  return <div>Loading...</div>
};

const LazyLoad = loader => Loadable({
  loader,
  loading:Loading,
})
const B = LazyLoad(()=>import('./b.js'));
const C = LazyLoad(()=>import('./c.js'));

export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Route path="/B" component={B}/>
          <Route path="/C" component={C}/>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}

复制代码

上面的封装方式并不是十分完美,webpack文档上说支持: ==import( ./dynamic/\${path} )的方式== 只要不全是变量貌似也是支持的,这就要看具体的业务形态了,如果按需的部分都在某个目录下,这种操作或许更舒适一些。

按目前的方式的话,看似比较繁琐,不过可以通过配置webpack的alias别名来进行路径支持。

Q6:按需加载+router config

react router除了组件方式以外,还可以通过config的方式来进行配置,config的方式便于统一维护controller层。

step1:封装LazyLoad

创建LazyLoad.js文件

import React from 'react';
import Loadable from 'react-loadable';
const Loading = (props) => {
  return <div>Loading...</div>
};

export default loader => Loadable({
  loader,
  loading:Loading,
})
复制代码

首先把Lazyload组件单独封装出去

step2:配置routes

创建routes.js

import LazyLoad from './LazyLoad';
export default [
  {
    path: "/B",
    component: LazyLoad(()=>import('./b.js'))
  },
  {
    path: "/C",
    component: LazyLoad(()=>import('./c.js')),
    routes: [
      {
        path: "/C/D",
        component: LazyLoad(()=>import('./d.js'))
      },
      {
        path: "/C/E",
        component: LazyLoad(()=>import('./e.js'))
      }
    ]
  }
];
复制代码

配置routes文件,用来动态引入路由

step3:封装 工具 方法 RouteWithSubRoutes

创建utils.js

import React from 'react';
import {Route} from 'react-router-dom';
export const RouteWithSubRoutes = route => (
  <Route
    path={route.path}
    render={props => (
      // pass the sub-routes down to keep nesting
      <route.component {...props} routes={route.routes} />
    )}
  />
);
复制代码

==这一步特别重要、特别重要、特别重要==

这个工具方法的作用就是将组件渲染出来

step4:修改第一层路由入口

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';

import {RouteWithSubRoutes} from './utils';
import routes from './routes';

export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
          {routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />)}
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}
复制代码
  1. 引入RouteWithSubRoutes工具方法
  2. 引入routes路由配置文件
  3. 在包裹的文件中进行routes遍历渲染

==注意:这里只处理了第一层路由== ==注意:这里只处理了第一层路由== ==注意:这里只处理了第一层路由==

step5:修改二级路由入口

路由配置化之后,嵌套子路由要以函数式来书写

import React,{Component} from 'react';
import {RouteWithSubRoutes} from './utils';
import { Link} from 'react-router-dom';

export default ({ routes }) => (
  <div>
    this is C
    <Link to="/C/D">to D</Link>
    <Link to="/C/E">to E</Link>
    {routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />)}
  </div>
);
复制代码
  1. 引入RouteWithSubRoutes工具方法
  2. 暴露的函数接受一个参数routes
  3. routes即config中内层的配置,也就是二级路由配置
  4. 二级路由配置,继续通过RouteWithSubRoutes进行渲染

==注意:config嵌套路由,需要逐层,一层一层的通过RouteWithSubRoutes来渲染。== ==新人很容易忽视这一点!== ==新人很容易忽视这一点!== ==新人很容易忽视这一点!==

Q7:router随心用?

前面使用config的方式配置了路由,但其实这里也可以混用,就是config方式+组件的方式混合使用。 修改二级路由入口:

import React from 'react';
import { Link,Route} from 'react-router-dom';
//import {RouteWithSubRoutes} from './utils';
import LazyLoad from './LazyLoad';

const D = LazyLoad(() => import('./d.js'))
const E = LazyLoad(() => import('./e.js'))

export default ({ routes }) => (
  <div>
    this is C
    <Route path="/C/D" component={D}/>
    <Route path="/C/E" component={E}/>
    <Link to="/C/D">to D</Link>
    <Link to="/C/E">to E</Link>
    {/* {routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />)} */}
  </div>
);
复制代码

其实,这里的话,就是随便搞了

路由的话,还是统一维护为好,当然也可以根据业务来自主选择需要的方式!。

脑阔疼的webpack按需加载告一段落了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hackers

Hackers

Steven Levy / O'Reilly Media / 2010-5-30 / USD 21.99

This 25th anniversary edition of Steven Levy's classic book traces the exploits of the computer revolution's original hackers -- those brilliant and eccentric nerds from the late 1950s through the ear......一起来看看 《Hackers》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具