内容简介:随着单页应用发展的越来越庞大,拆分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' } } 复制代码
- a.js引用b.js
- webpack打包将b、a都打包到了一起,输出一个默认的main.js
- html引用打包好的main.js 结果如下:
2,开搞!
step1:修改webpack.config.js
module.exports = { entry:'./a.js', output:{ filename:'[name].js', chunkFilename:'[name].js'// 设置按需加载后的chunk名字 } } 复制代码
这里就添加了一句,chunkFilename而已,chunkFilename的作用就是用来给拆分后的chunk们起名字的配置项。 ok,执行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(); }) } 复制代码
- 使用es6的import按需语法
- 在promise后执行拿到的返回的结果 此时再次执行webpack:
输出文件变成了两个,1个main.js、1个1.js 这个1.js很迷...
查看一下源码,可以看出来,它其实就是我们的b.js
总结一下 :
- webpack中output的设置并不决定是否拆分代码
- 拆分代码的决定因素在import语法上
- webpack在扫描到代码中有import语法的时候,才决定执行拆分代码
step3:怎么使用?
额,成功报错了...脑阔疼 分析报错:
- 按需加载找的文件是/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:验证结果
- 点击前
- 只引用了main.js
- 点击后
- 加载了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命明的方式,结果:
输出了b.js,测试回归一次:
- 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集成按需加载
业务中,除了点击的时候按需加载,还有大部分场景都是在路由切换的时候进行按需加载
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很简单,只需要按照它所规定的语法,包裹一下需要加载的组件就可以
点击跳转toC
可以看到加载了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:验证嵌套路由
入口没问题
点击跳转动态加载C没问题
点击跳转D不行了
可以看到动态引入资源./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编译的时候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() } 复制代码
- 引入RouteWithSubRoutes工具方法
- 引入routes路由配置文件
- 在包裹的文件中进行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> ); 复制代码
- 引入RouteWithSubRoutes工具方法
- 暴露的函数接受一个参数routes
- routes即config中内层的配置,也就是二级路由配置
- 二级路由配置,继续通过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按需加载告一段落了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 介绍同步加载、异步加载、延迟加载[原创]
- .net加载失败的程序集重新加载
- 虚拟机类加载机制:类加载时机
- 探秘类加载器和类加载机制
- hibernate中加载策略+批加载+懒加载异常【原创】
- [译] React 16.6 懒加载(与预加载)组件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web开发秘方
Brian P. Hogan、Chris Warren、Mike Weber、Chris Johnson、Aaron Godin / 七印部落 / 华中科技大学出版社 / 2013-7-10 / 66.00元
猜猜硅谷的前端工程师怎么折腾JS的?想知道无限下拉的列表怎么做吗?你知道DropBox可以当Web服务器用吗?你知道怎么做出跨平台的幻灯片效果吗?不借助插件,怎样在移动设备上实现动画效果?怎样快速搭建和测试HTML电子邮箱?怎样制作跨PC和移动设备显示的应用界面?怎样利用最新的JavaScript框架(Backbone和Knockout)提高应用的响应速度?怎样有效利用CoffeeScript和S......一起来看看 《Web开发秘方》 这本书的介绍吧!