diff --git a/example/.vuepress/config.js b/example/.vuepress/config.js index 663bf10..33340ad 100644 --- a/example/.vuepress/config.js +++ b/example/.vuepress/config.js @@ -7,42 +7,20 @@ module.exports = { ['meta', { name: 'viewport', content: 'width=device-width,initial-scale=1,user-scalable=no' }] ], // theme: 'reco', - locales: { - '/': { - lang: 'ja-JP', - title: "vuepress-theme-reco", - description: 'A simple and beautiful vuepress blog theme.', - }, - }, theme: require.resolve('../../packages/vuepress-theme-reco'), themeConfig: { - locales: { - '/': { - recoLocales: { - pagation: { - prev: '上壹頁', - next: '下壹頁', - go: '前往', - jump: '跳轉至' - } - } - } - }, nav: [ { text: 'Home', link: '/', icon: 'reco-home' }, { text: 'TimeLine', link: '/timeline/', icon: 'reco-date' }, - { text: 'Contact', - icon: 'reco-message', - items: [ - { text: 'NPM', link: 'https://www.npmjs.com/~reco_luan', icon: 'reco-npm' }, - { text: 'GitHub', link: 'https://github.com/recoluan', icon: 'reco-github' }, - { text: '简书', link: 'https://www.jianshu.com/u/cd674a19515e', icon: 'reco-jianshu' }, - { text: 'CSDN', link: 'https://blog.csdn.net/recoluan', icon: 'reco-csdn' }, - { text: '博客圆', link: 'https://www.cnblogs.com/luanhewei/', icon: 'reco-bokeyuan' }, - { text: 'WeChat', link: 'https://mp.weixin.qq.com/s/mXFqeUTegdvPliXknAAG_A', icon: 'reco-wechat' }, - ] - } + { text: 'sidebar', link: '/views/sidebar/' } ], + sidebar: { + '/views/sidebar/': [ + '', + 'bar1', + 'bar2' + ] + }, type: 'blog', // 博客设置 blogConfig: { @@ -55,14 +33,14 @@ module.exports = { text: 'Tag' // 默认 “标签” } }, - type: 'blog', logo: '/head.png', authorAvatar: '/head.png', // 搜索设置 search: true, searchMaxSuggestions: 10, // 自动形成侧边导航 - sidebar: 'auto', + // sidebar: 'auto', + sidebarDepth: 4, // 最后更新时间 lastUpdated: 'Last Updated', // 作者 diff --git a/example/views/category1/2019/092101.md b/example/views/category1/2019/092101.md index 6567cb4..82792c6 100644 --- a/example/views/category1/2019/092101.md +++ b/example/views/category1/2019/092101.md @@ -1,6 +1,7 @@ --- -title: siderbar test +title: sidebar test date: 2019-09-21 +sidebarDepth: 5 tags: - tag2 categories: diff --git a/example/views/other/webpack.md b/example/views/other/webpack.md new file mode 100644 index 0000000..68966a5 --- /dev/null +++ b/example/views/other/webpack.md @@ -0,0 +1,2244 @@ +--- +title: webpack 基础知识整理 +date: 2019-07-24 +tags: + - webpack +categories: + - frontEnd +--- + +## webpack简介 + +webpack是一个 **模块打包工具**,支持所有的打包语法,比如 `ES Module`、`CommonJS`、`CMD`、`AMD`。初期的webpack是用来模块打包js的,发展到现在,已经可以打包很多种文件类型,比如 `css`、`img` 。 + +优化打包速度最有效的方法就是保持 `nodejs` 和 `webpack` 为最新版本。 + + + +## 安装 + +安装 `webpack` 建议根据项目安装而不是全局安装,可以使用以下命令: + +```bash +npm install webpack webpack-cli --save-dev + +# 或 + +yarn add webpack webpack-cli --dev +``` + +这个时候执行 `webpack -v` 是查不到版本号的,因为 `nodejs` 默认是去全局找 `webpack`,这个时候是找不到的,nodejs还提供了 `npx webpack -v` 这个方法。 + +## 运行 + +如果不生成配置文件,webpack会按照默认配置去打包,如果我们想自定义配置文件可以在项目根目录添加 `webpack.config.js` 来自定义配置信息,配置文件的名字也可以自定义: + +```bash +# 默认配置或者默认配置文件 +npx webpack + +# 自定义配置并且修改默认配置名字 +npx webpack --config my-webpack-config.js + +# npm scripts 中配置 "build": "webpack" +npm run build +``` + +一个简单的配置: + +```js +module.exports = { + mode: 'production', // production:默认,生产环境,代码被压缩;development:开发环境,代码不压缩 + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist') + } +} +``` + +其中 `entry` 可以写成这样: + +```js +entry: { + main: './src/index.js' +} +``` + +其实,开始安装的 `webpack-cli` 就是为了在命令行工具中可以正确地执行命令行工具。 + +## loader + +`webpack` 可以使用 `loader` 来预处理文件。这允许你打包除 `JavaScript` 之外的任何静态资源,js的打包是webpack内置的。你可以使用 `Node.js` 来很简单地编写自己的 `loader`。 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.jpg$/, + use: { + loader: 'file-loader' + } + } + ] + } +} +``` + +### file-loader + +处理文件模块的 webpack loader。 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.jpg$/, + use: { + loader: 'file-loader', + options: { + // 设置输出文件名 + name: '[name]_[hash].[ext]', + // 设置输出文件夹 + outputPath: 'images/', + // 指定目标文件的自定义公共路径 + publicPath: 'assets/' + } + } + } + ] + } +} +``` + +### url-loader + +`file-loader` 的增强版,除了上述功能,还可以将文件转换为 `base64 URI`。 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.jpg$/, + use: { + loader: 'url-loader', + options: { + // 设置输出文件名 + name: '[name]_[hash].[ext]', + // 设置需要转换base64的文件大小(太大的文件转换后需要更大的请求压力) + limit: 2048 + } + } + } + ] + } +} +``` + +### css相关 + +#### style-loader 和 css-loader + +- css-loader:加入 a.css 中引入了 b.css 和 c.css,css-loader 会将其合并成一个css文件 +- style-loader:将合并后的 css 文件挂载到 head 标签内 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'] + } + ] + } +} +``` + +#### sass-loader + +如果使用 scss、less、stylus 等 css 预处理器。例如,我们要使用 sass 预处理器,首先要安装 sass-loader 和 node-sass。 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.css$/i, + use: ['style-loader', 'css-loader', 'sass-loader'] + } + ] + } +} +``` + +::: warning +loader的加载顺序是从右到左、从下到上,所以处理 scss 文件时,将 sass-loader放在最后。 +::: + +#### postcss-loader + +通过 postcss-loader 来给新属性添加厂商前缀。 + +```js +// webpack.config.js + +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.scss$/i, + use: [ + 'style-loader', + 'css-loader', + 'sass-loader', + 'postcss-loader' + ] + } + ] + } +} +``` + +```js +// postcss.config.js +// 首先安装 autoprefixer + +module.exports = { + plugins: [ + require('autoprefixer') + ] +} +``` + +如果 a.css 中引入了 b.css 和 c.css,当读到 `@import('./b.css')` 时就会略过 postcss-loader 和 sass-loader,直接从 css-loader 直接运行,可以通过 `importLoaders` 配置来改善。 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.scss$/i, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2 + } + }, + 'sass-loader', + 'postcss-loader' + ] + } + ] + } +} +``` + +#### css模块化 + +在 `index.js` 通过import `'./index.css'` 引入样式会全局有效,如果想在某个模块有效,如何去做呢? + +```js +// 模块A + +import style from 'index.css' + +const img = new Image() +img.src = headImg +img.classList.add(style.avator) +``` + +```js +// webpack.config.js + +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.scss$/i, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + modules: true + } + }, + 'sass-loader', + 'postcss-loader' + ] + } + ] + } +} +``` + +#### 字体 + +字体文件只需要通过 file-loader 将字体文件转移到打包文件夹内即可。 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [ + { + test: /\.(eot|ttf|svg|woff)$/i, + use: { + loader: 'file-loader', + options: { + outputPath: 'fonts/' + } + } + } + ] + } +} +``` + +## plugin + +可以在webpack运行到某个时刻的时候,做一些事情。 + +### html-webpack-plugin + +会在打包之后,自动生成一个 html 文件,并把打包生成的 js 自动引入到这个 html 文件中。 + +```js +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + plugins: [new HtmlWebpackPlugin()] +} +``` + +但是我们可能需要在 index.html 中写一些默认代码,比如 meta,这时就可以按照某个模板来生成这个 index.html。 + +```js +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + plugins: [new HtmlWebpackPlugin({ + template: 'src/index.html' + })] +} +``` + +### clean-webpack-plugin + +会在打包前先清空打包目标文件夹的文件。 + +```js +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const CleanWebpackPlugin = require('clean-webpack-plugin') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist') + }, + plugins: [ + new HtmlWebpackPlugin({ + template: 'src/index.html' + }), + new CleanWebpackPlugin() + ] +} +``` + +## 多个输出文件 + +```js +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const CleanWebpackPlugin = require('clean-webpack-plugin') + +module.exports = { + entry: { + main: './src/index.js', + sub: './src/index.js' + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'dist') + } +} +``` + +如果我们的打包后的文件中,index.html 需要给后台做配置文件,assets 文件夹需要放在 cdn 上,这样的话我们的就需要在 output 中设置 publicPath: + +```js +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const CleanWebpackPlugin = require('clean-webpack-plugin') + +module.exports = { + entry: { + main: './src/index.js', + sub: './src/index.js' + }, + output: { + publicPath: 'http://cdn.com.cn', + filename: '[name].js', + path: path.resolve(__dirname, 'dist') + } +} +``` + +## sourceMap + +如果运行打包后的文件,某个地方有错误,控制台会显示打包后的文件的某个位置有错误,如果我们想知道错误来自于源文件的所在位置,那么就需要借助 sourceMap 了。所以 sourceMap 其实就是一种映射,它知道 dist 目录 main.js 文件的某个错误,实际对应的是 src 目录下 index.js 文件的第一行。 + +sourceMap 通过配置中的 devtool 去配置,参数的含义大概有以下几种情况: + +| devtool | 作用 | +| ----------------------- | --------------------------------------------------------------------------------- | +| source-map | 生成 map 文件,错误精确到行和列 | +| inline-source-map | inline,不生成 map 文件,以 base64 形式嵌入js中,错误精确到行和列 | +| cheap-source-map | cheap,错误只精确到行,且只针对业务代码,不包括第三方模块 | +| cheap-module-source-map | cheap-module,错误只精确到行,且只针对业务代码,包括第三方模块 | +| eval-source-map | eval,不生成 map 文件,在 js 中以 eval 方法的形式出现,但是复杂项目的提示是不全的 | + +**最佳实践** + +1. develop:cheap-module-eval-source-map,提示比较全,打包速度快 +2. production:cheap-module-source-map,提示更全面,打包稍微慢 + +## 监听变动 + +### webpack --watch + +监听文件的变动,自动进行打包。 + +```json +{ + "scripts": { + "build": "webpack", + "watch": "webpack --watch" + } +} +``` + +### webpack-dev-server + +上面的html的打开的方式还是需要通过 `file` 协议打开一个本地文件,在浏览器地址是这样的:`file:///Users/reco/workSpace/git/personal/work/test.html`。这样的话发送 `AJAX` 请求就有问题了,因为发送请求需要 `http` 或者 `https` 协议,这时需要的是在本地启动一个服务,我们可以借助 `webpack-dev-server` (打包时将打包的文件放在内存中,提高打包速度)。 + +```bash +yarn add webpack-dev-server --dev +``` + + +```json +{ + "scripts": { + "build": "webpack", + "watch": "webpack --watch", + "dev": "webpack-dev-server" + } +} +``` + +```js +const path = require('path') + +module.exports = { + entry: { + main: './src/index.js', + sub: './src/index.js' + }, + output: { + publicPath: 'http://cdn.com.cn', + filename: '[name].js', + path: path.resolve(__dirname, 'dist') + }, + // 默认端口 8080 + devServer: { + // 本地服务的根目录 + contentBase: './dist', + // 服务启动后自动打开浏览器 + open: true, + // 端口 + port: 3000, + // 跨域代理 + proxy: { + '/api': 'http://localhost:3000' + } + } +} +``` + +**自己写一个简单的 webpack-dev-server** + + +```json +{ + "scripts": { + "server": "node server.js" + } +} +``` + +```js +// server.js + +const espress = require('express') +const webapck = require('webpack') +const webpackDevMiddleware = require('webpack-dev-middleware') +const config = require('./webpack.config.js') +const complier = webpack(config) + +const app = express() + +// publicPath 不设置的话默认 '/' +app.use(webpackDevMiddleware(complier, { + publicPath: config.output.publicPath +})) + +app.listen(3000, () => { + console.log('server is running!') +}) +``` + +> **缺点:**需要自己手动刷新 + +上面这种方式就是在node中使用webpack,这是除了在命令行中的使用 `webpack` 的另一种方式。 + +**开启 Hot Module Replacement** + +解决下面的问题: + 1. 修改页面某个颜色,页面会刷新,导致动态添加的 dom 会消失; + 2. 一个页面同时引入两个模块的js,修改某个模块的js,页面会刷新,导致灵感一个模块的js也会初始化。 + +存在的问题: + 1. 在多页面应用里,html更改时并不会刷新,需手动,所以这种情况下,可以去掉更更新功能。 + +```js +const path = require('path') +const webpack = require('webpack') + +module.exports = { + devServer: { + // 1. 开启 HMR + hot: true, + // 只有在开启 HMR 的时候才会监听变动并刷新 + hotOnly: true + }, + // 2. 插件 + plugins: [ + new webpack.HotModuleReplacementPlugin() + ] +} +``` + +案例: + +```js +import counter from './counter' +import number from './number' + +counter() +number() + +// 如果 HMR 生效的话 +if (module.hot) { + // 监听文件的变动 + module.hot.accept('./number', () => { + // 做一些清空操作 + // ... + + number() + }) +} +``` + +当引用 css 的时候只需要引用,并不需要上面这一坨代码,就是因为 `css-loader` 已经内置了上面的方法,就像在写 vue、react 代码不需要写上面这坨代码一样,他们的 loader 中也内置了这些方法。只有在使用一些特殊的文件类型才需要。(react 是借助 babel-preset 实现的) + +> 业务开发时,一般不是设置 hotOnly 这样才能试试显示最新代码和更改效果 + +## Babel + +### 安装 + +```bash +# babel-loader将 webpack 与 babel 建立关联 +# @babel/core 语法转换 + +npm install --save-dev babel-loader @babel/core +``` + +```js +module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader" + } + ] +} +``` + +### 语法转换 + +这个时候还是不可以转换,还需要这样 + +```bash +npm install @babel/preset-env --save-dev +``` + +然后配置 options + +```js +module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader", + options: { + "presets": ["@babel/preset-env"] + } + } + ] +} +``` + +### 变量、对象转换 + +这个时候也只能对一些语法进行转换,比如 “箭头函数”,如果想要对 `Promise` 这些新的对象进行转换(准确来说,浏览器可能不支持新规范的的一些对象,所以需要单独封装这些方法,然后在全局注入),还需要这样: + +```bash +npm install --save @babel/polyfill +``` + +```js +// 在入口文件 +require("@babel/polyfill"); + +// or +import "@babel/polyfill"; +``` + +这个时候会默认全部转换,这样会增加很多兼容性代码,如果我们想按需引入: + +```js +module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader", + options: { + "presets": [["@babel/preset-env", { + useBuiltIns: 'usage' + }]] + } + } + ] +} +``` + +有的同学问:`babel-polyfill` 这样引用可不可以?答案是可以,但是在使用 `useBuiltIns: 'usage'` 时就不可以了。 + +```js +// 在入口配置 +module.exports = { + entry: ["@babel-polyfill", "./app/js"] +} +``` + +`@babel/preset-env` 还可以拥有其他配置参数,比如: + +```json +{ + "presets": [["@babel/preset-env", { + targets: { + chrome: "67" + }, + useBuiltIns: 'usage' + }]] +} +``` + +上面配置的意思是,只需要兼容 chrome 浏览器 67 版本以上就可以了,这样转译时会根据浏览器的兼容性来合理处理转译结果。 + +### 组件库的封装 + +如果只是开发业务代码,使用上面 `presets` + `babel-polyfill` 的方式就可以了,但是 `babel-polyfill` 有一个确定就是会将变量全局注入,这里可以使用 `transform-runtime` 来以闭包(或其他)的形式来进行引入,避免全局环境的污染。 + +```bash +npm install --save-dev @babel/plugin-transform-runtime + +npm install --save @babel/runtime +``` + +```js +module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader", + options: { + "plugins": [ + [ + "@babel/plugin-transform-runtime", + { + "absoluteRuntime": false, + "corejs": 2, + "helpers": true, + "regenerator": true, + "useESModules": false + } + ] + ] + } + } + ] +} +``` + +将 `corejs` 的值设置为 2,才会将 `map` 、`promise`等方法打包到 `main.js`,当然还需要引入另一个依赖: + +```bash +npm install --save @babel/runtime-corejs2 +``` + +### `.babelrc` + +如果 babel 的配置过于复杂,内容较多,可以将其单独放在 `.babelrc` 文件内: + +```json +{ + "presets": [["@babel/preset-env", { + useBuiltIns: 'usage' + }]] +} +``` + +## React打包 + +```bash +npm install --save-dev @babel/preset-react +``` + +```json +{ + "presets": [ + ["@babel/preset-env", { + useBuiltIns: 'usage' + }], + [ + "@babel/preset-react", + { + "pragma": "dom", // default pragma is React.createElement + "pragmaFrag": "DomFrag", // default is React.Fragment + "throwIfNamespace": false // defaults to true + } + ] + ] +} +``` + +## Tree Shaking + +> 只支持 ES Module,因为 ES Module 是静态引入 + +作用:模块按需引入,不会将全部代码引用过来 + +### development + +**webpack.config.js** + +```js +module.exports = { + mode: 'development', + optimization: : { + usedExports: ture + } +} +``` + +`usedExports` 意思就是检查一下哪些模块被使用了再做打包。 + +**package.json** + +```json +{ + "sideEffects": false +} +``` + +如果引入的一些 `css` 或依赖不需要 `Tree Shaking`,那将 `sideEffects` 设置为 `["./a.css", "@babel/polyfill"]`,如果没有需要配置的,直接设置为 `false` 即可。 + +### production + +线上环境是不需要配置 `usedExports` 的,但是还是需要配置 `package.json`。 + +## 配置文件整理 + +比如在 `Vue` 官方的脚手架中 `webpack` 的配置文件都放在 `build` 文件夹中,如果我们希望对配置文件进行整理的话,需要做一下处理: + +1. 将开发环境和线上环境的公共配置提取到 `/build/webpack.base.js` 中 +2. 分别在开发环境和线上环境的配置中合并公共配置,配置合并需要使用 `webpack-merge` + ```js + // /build/webpack.dev.js + const merge = require('webpack-merge') + const baseConfig = require('./webpack.base.js') + + const devConfig = { + mode: 'development' + } + + module.exports = merge(baseConfig, devConfig) + ``` +3. 修改 `package.json` + ```json + { + "script": { + "dev": "webpack-dev-server --config ./build/webpack.dev.js", + "build": "webpack --config ./build/webpack.prod.js" + } + } + ``` +4. 这个时候较之前打包输出和清空的目录就应该修改一下了 + ```js + module.exports = { + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, '../dist') + }, + plugins: [ + new HtmlWebpackPlugin({ + template: 'src/index.html' + }), + new CleanWebpackPlugin(["dist"], { + root: path.resolve(__dirname, "../") + }) + ] + } + ``` + + **更新**:上面 `CleanWebpackPlugin` 的语法是 `1.0` 版本的。`2.0` 它所清空的文件夹默认就是打包输出目录,无需再单独指定。 + +## Code Splitting + +### 原理 + +代码拆分——通过对公用代码的拆分来提升性能。 + +本来代码拆分和 `webpack` 是没关系的,只不过是一种优化手段,比如将公共代码单独打包到一个文件内,业务代码打包到另一个文件内,从而提升加载体验。这里可以运用多入口文件的方式分开打包。 + +### webpack实现 + +`webpack4.0` 实现代码分割,分两种情况: + +1. 同步代码需要配置 `splitChunks` + +```js +module.exports = { + optimization: : { + splitChunks: { + chunks: "all" + } + } +} +``` + +2. 异步代码不需要做任何操作,异步代码比如下面这种情况: + +```js +function createElement () { + import('lodash').then(({ default: _ }) => { + const element = document.createElement('div') + div.innerHTML = _.join(['a', 'b'], '-') + return element + }) +} + +createElement().then(element => { + documnet.body.append(element) +}) +``` + +> 这个写法会报错,因为动态来获取依赖的这种方式是试验性语法,目前还不支持,需要借助插件:`babel-plugin-dynamic-import-webpack` + +--- + +### SplitChunksPlugin + +**魔法注释** + +```js +function createElement () { + import(/* webpackChunkName: 'loadsh' */, 'lodash').then(({ default: _ }) => { + const element = document.createElement('div') + div.innerHTML = _.join(['a', 'b'], '-') + return element + }) +} + +createElement().then(element => { + documnet.body.append(element) +}) +``` + +`babel-plugin-dynamic-import-webpack` 不支持魔法注释,所以要换成 `@babel/plugin-syntax-dynamic-import`,这个时候会打包生成 `vendors~lodash.js`,如果需要修改打包后的名字,可以设置 `optimization`: + +```js +module.exports = { + optimization: { + splitChunks: { + chunks: "async", // async 异步,initial 同步, all 全部,但是同步还需要配置 cacheGroups,这是重点 + minSize: 30000, // 可以处理依赖的最小值 + maxSize: 0, // 可以处理依赖的最大值 + minChunks: 2, // 被引用2次及以上,才会被拆分 + maxAsyncRequests: 5, // 最大请求次数,也就是拆分时最多拆分5个包 + maxInitialRequests: 3, // 入口最大请求次数,也就是拆分时最多拆分3个包 + automaticNameDelimiter: '~', // 文件生成时名字的连接符 + name: true, // 拆分块的名称,提供true将根据块和缓存组密钥自动生成名称。 + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + priority: -10, // 优先级,数值越大优先级越高,符合多个规则时优先权重高的规则 + filename: "venders.js" // 打包时的名字 + }, + // 不在 verdors 内的打包 + default: { + minChunks: 2, + priority: -20, + reuseExistingChunk: true // 如果一个模块被打包过了,再次遇到,就不会再打包了,而是会去找之前打包过的那个模块 + } + } + } + } + } +} +``` + +## Lazy Loading & Chunk + +### Lazy Loading + +`webpack` 可以识别 `ECMAScript` 的import返回的promise,并进行分割,实现懒加载,但是必须依赖 `babel-polyfill` 或者 `promise-polyfill`。 + +```js +function createElement () { + import(/* webpackChunkName: 'loadsh' */, 'lodash').then(({ default: _ }) => { + const element = document.createElement('div') + div.innerHTML = _.join(['a', 'b'], '-') + return element + }) +} + +document.addEventListener('click', () => { + createElement().then(element => { + documnet.body.append(element) + }) +}) +``` + +用 `ES7` 的 `async` 和 `await` 函数重构一下: + +```js +async function createElement () { + const { default: _ } = await import(/* webpackChunkName: 'loadsh' */, 'lodash') + const element = document.createElement('div') + div.innerHTML = _.join(['a', 'b'], '-') + return element +} + +document.addEventListener('click', () => { + createElement().then(element => { + documnet.body.append(element) + }) +}) +``` + +### Chunk + +像上面的 `Lazy Loading` 所拆分打包的每一个文件都是一个 `Chunk`,而前面的配置参数` minChunks: 2` 的意思就是:当有2个以上的 · 使用到某个依赖时,才会对其进行拆分成一个 `Chunk`。 + +```js +const path = require('path') + +module.exports = { + output: { + publicPath: 'http://cdn.com.cn', + filename: '[name].js', + chunkFilename: '[name].chunk.js', // 打包之后的入口文件之外的js的会在这里过滤一下名字 + path: path.resolve(__dirname, 'dist') + } +} +``` + +## 打包分析 + +`http://webpack.js.org/guides/code-splitting/#bundle-analysis`,这是官网对打包分析的几个总结,其中最好用的是 `webpack-bundle-analyzer`。 + +**webpack 希望我们怎么样写代码呢?** + +```js +// 原来我们是这么写代码的 +document.addEventListener('click', () => { + const element = document.createElement('div') + element.innerHTML = 123 + documnet.body.append(element) +}) +``` + +上面的代码的加载利用率是较低的,因为创建元素实在点击事件触发后才触发的,所以可以分离出去: + +```js +// 现在我们可以这么写 + +// 将生成代码的代码放到另一个文件中去,比如叫 click.js +function createElement () { + const element = document.createElement('div') + element.innerHTML = 123 + documnet.body.append(element) +} + +export default createElement + + +document.addEventListener('click', () => { + import('./click.js').then(({default: func}) => { + func() + }) +}) +``` + +所以我们现在写代码应该考虑的不是缓存的问题,而是代码的利用率。所以在 `chunks` 默认设置为 `async` 而不是 `all`,是因为,同步的代码只能增加缓存,而对性能提升非常有限。 + +> 谷歌浏览器查看网页的利用率:控制台 --> ctrl+shift+p --> coverage + +--- + +比如点击登录的时候会出现一个模态框,首页的加载并不需要加载模态框的,但是点击登录按钮再加载,模态框的加载是会变慢的,这就需要下面的两个方法了:`Preloading` 和 `Prefetching`。 + +### Prefetching + +**非主要业务模块会在主要业务模块加载完之后,空闲时间再去加载。** + +```js +// 现在我们可以这么写 + +// 将生成代码的代码放到另一个文件中去,比如叫 click.js +function createElement () { + const element = document.createElement('div') + element.innerHTML = 123 + documnet.body.append(element) +} + +export default createElement + + +document.addEventListener('click', () => { + // 通过魔法注释来开启 webpackPrefetch + import(/* webpackPrefetch: true */'./click.js').then(({default: func}) => { + func() + }) +}) +``` + +### Preloading + +**而这个模式下,非主要业务模块会和主要业务模块一起加载** + +```js +// 现在我们可以这么写 + +// 将生成代码的代码放到另一个文件中去,比如叫 click.js +function createElement () { + const element = document.createElement('div') + element.innerHTML = 123 + documnet.body.append(element) +} + +export default createElement + + +document.addEventListener('click', () => { + // 通过魔法注释来开启 webpackPreload + import(/* webpackPreload: true */'./click.js').then(({default: func}) => { + func() + }) +}) +``` + +### 总结 + +目前考虑前端的性能优化,不能总是考虑缓存,而是主要考虑代码的使用率。 + +## CSS代码分割 + +### CSS分割 + +不做处理的情况下,`webpack` 会将 `css` 打包到 `js` 中去,如果需要生成单独的 `css` 文件,可以使用 `MiniCssExtractPlugin`。 + +```bash +# 安装 +npm install --save-dev mini-css-extract-plugin +``` + +配置步骤: +1. 配置 `plugins`; +2. 将 `style-loader` 改为 `MiniCssExtractPlugin.loader`; +3. 如果设置了 `Tree Shaking`,需要将 `"sideEffects": false` 改为 `"sideEffects": ["*.css"]`。 + +```js +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + plugins: [ + new MiniCssExtractPlugin({ + // 设置输出文件的命名规则 + filename: '[name].css', + chunkFilename: '[id].css', + }), + ], + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + // you can specify a publicPath here + // by default it uses publicPath in webpackOptions.output + publicPath: '../', + hmr: process.env.NODE_ENV === 'development', + }, + }, + 'css-loader', + ], + }, + ], + }, + optimization: { + usedExports: ture + } +} +``` + +```json +{ + "sideEffects": ["*.css"] +} +``` + +### CSS 压缩 + +`optimize-css-assets-webpack-plugin` + +```js +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); + +module.exports = { + optimization: { + minimizer: [new OptimizeCSSAssetsPlugin({})], + } +}; +``` + +### 合并 CSS + +将多个入口文件的 `css` 单独放到每个文件中,需要设置 `optimization.splitChunks.cacheGroups` 为对应的多个分组。 + +```js +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +function recursiveIssuer(m) { + if (m.issuer) { + return recursiveIssuer(m.issuer); + } else if (m.name) { + return m.name; + } else { + return false; + } +} + +module.exports = { + entry: { + foo: path.resolve(__dirname, 'src/foo'), + bar: path.resolve(__dirname, 'src/bar'), + }, + optimization: { + splitChunks: { + cacheGroups: { + fooStyles: { + name: 'foo', + test: (m, c, entry = 'foo') => + m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry, + chunks: 'all', + enforce: true, + }, + barStyles: { + name: 'bar', + test: (m, c, entry = 'bar') => + m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry, + chunks: 'all', + enforce: true, + }, + }, + }, + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + ], + module: { + rules: [ + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, +}; +``` + +## 浏览器缓存 + +`contenthash` 如果内容没有变化,hash值不会变;如果内容变化,hash就会变。这样项目重新打包上线后,项目就不会全部重新加载了。 + +```js +const path = require('path') + +module.exports = { + output: { + publicPath: 'http://cdn.com.cn', + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].chunk.js', + path: path.resolve(__dirname, 'dist') + } +} +``` + +如果 `webpack` 是老版本,还需要配置一下 `runtimeChunk`: + +```js +module.exports = { + optimization: { + runtimeChunk: { + name: 'runtime' + } + } +} +``` + +这是因为:代码有没有变化的逻辑关系被打包到一个叫 `manifest` 的东西来里,旧版本的 `webpack` 是会将这个东西直接打包到每个 `chunk` 文件中,而所以导致每次打包都不一致,这样的话配置好 `runtimeChunk`之后,所有的`manifest` 都会提取到名为 `runtime` 的文件内,所以就不会影响打包了。 + +## Shimming (垫片) + +`babel-polyfill` 就是一个全局垫片,`babel-plugin-transform-runtime` 是一个局部垫片。下面介绍几种垫片: + +1. 如果我们在 `a.js` 引用了一个 `utils.js` 的里面的方法,而这个方法需要借助 `jquery`, `a.js` 引用了`jquery`,而 `utils.js` 没有,这时候是会报错的,所以可以借助 `webpack.ProvidePlugin` 全局 在使用 `$` 的地方引用 `jquery`。 + +2. `webpack.ProvidePlugin` 还有另外一个功能:如果我们想要将 `$.each` 功能直接这样使用 `$each`,我们在下面这么来配置。 + + ```js + import webpack from 'webpack' + + module.exports = { + plugins: [ + new webpack.ProvidePlugin({ + $: 'jquery', + $each: ['jquery', 'each'] + }) + ] + } + ``` + +3. 每个模块的 `this` 都是指向当前模块的,如果想让每个模块都指向 `window`,我们需要借助 `imports-loader`: + + ```js + module.exports = { + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader' + }, + { + loader: 'imports-loader?this=?window' + } + ] + } + ] + } + } + ``` + +## 环境变量 + +```js +// webpack.common.js + +const merge = require('webpack-merge') +const devConfig = require('./webpack.dev.js') +const prodConfig = require('./webpack.prod.js') + +const commonConfig = { + // ... +} + +module.exports = (env) => { + if (env && env.production) { + return merge(commonConfig, prodConfig) + } else { + return merge(commonConfig, devConfig) + } +} +``` + +```json +{ + "scripts": { + "dev-build": "webpack --config ./build/webpack.common.js", + "dev": "webpack-dev-server --config ./build/webpack.common.js", + "build": "webpack --env.production --config ./build/webpack.common.js", + } +} +``` + +## 区分模式打包 + +区别: +1. develop 模式下的sourceMap 是非常全的; +2. develop 模式下的代码不需要压缩; + +## 函数库打包 + +### 指定代码运行范围 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'library.js', + path: path.resolve(__dirname, 'dist'), + library: 'library', // 通过 script 标签引入,全局注入 library 这个变量 + libraryTarget: 'umd' // 模块引入方式 ES Module 和 CommonJS + } +} +``` + +`library` 和 `libraryTarget` 两个是配合使用的,`library` 的意思就是指定暴露的全局变量的名字,但是这个全局变量挂在到哪里呢?这就由 `libraryTarget` 来指定了。`umd` 的意思是允许它与CommonJS,AMD和全局变量一起使用,除了它还有 `this/window/global/amd` 等值可以设置。 + +### 略过不需要的依赖 + +```js +const path = require('path') + +module.exports = { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'library.js', + path: path.resolve(__dirname, 'dist'), + externals: ["lodash"] // 打包时当遇到 lodash 这个依赖就自动忽略 + } +} +``` + +比如我的这个函数库依赖 `jquery`,但是用户也可能引用了 `jquery`,这样就会多打包一份,所以为了减少代码量,这时就可以通过 `externals` 来忽略 `jquery`(`externals` 支持 `Arrary/Object`)。 + +## Progressive Web Application + +第一次访问成功,第二次访问时如果服务挂掉了,这个时候让项目走缓存,而不是显示服务错误页面。 + +`PWA` 的技术原理是 `server work`,这里可以借助 `workbox-webpack-plugin`: + +```js +// webpack.config.js +const WorkboxPlugin = require('workbox-webpack-plugin') + +module.exports = { + plugins: [ + new WorkboxPlugin.GenerateSW({ + clientsClaim: true, + skipWaiting: true + }) + ] +} +``` + +打包之后就产生两个文件:`service-work.js` 和 `precache-manifest.js`,下面还需要在入口文件写一下相关配置: + +```js +// index.js + +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-work.js').then(registeration => { + console.log(`service-work registered`) + }).catch(err => { + console.log(`service-work register error`) + }) + }) +} +``` + +## TypeScript 的打包配置 + +```bash +npm install ts-loader -D +``` + +```js +// webpack.config.js +module.exports = { + mode: 'production', + entry: './src/index.tsx', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + output: { + filename: 'index.js', + path: path.resolve(__dirname, 'dist') + } +} +``` + +还需要创建 `tsconfig.json`: + +```json +{ + "compilerOptions": { + "outDir": "./dist", // 打包到那个文件夹内 + "module": "es6", // 使用es6的模块化方式 + "target": "es5", // 打包成 es5 语法 + "allowJs": true // 允许在ts文件里在引入一些js模块 + } +} +``` + +如果我们引入了 `jquery` 这个模块,要想在使用 `jquery` 语法时让typescript有效,还需要引入 `@types/jquery` 这个依赖(这是 2.0 的做法,1.0 稍有区别),不然会报错:`TS2688: Cannot find type definition file for 'unist'.`。 + +## WebpackDevServer 请求转发 + +### proxy + +`WebpackDevServer` 的 `proxy` 是可以直接配置代理的: + +```js +// webpack.config.js +module.exports = { + devServer: { + proxy: { + '/react/api': { + target: 'https://www.xxx.com', + secure: false, // 可以对 https 生效 + pathRewrite: { // 改变接口路由 + 'header.json': 'demo.json' + }, + changeOrigin: true, // 有些接口为了防止爬虫是不允许改变 origin 的,这里设置为 true 就可以了 + headers: { // 改变请求头 + host: 'www.xxx.com' + } + } + } + } +} +``` + +### historyApiFallback + +```js +// webpack.config.js +module.exports = { + devServer: { + historyApiFallback: true + } +} +``` + +如果一个项目里在写单页面应用时,某个路由我们没有配置某个路由 A,访问时会显示 `can't get A`,这是我们可以配置 `historyApiFallback: true` 来将没有配置的页面直接转向 `index.html`,详细用法见 webpack官网。 + +## ESLint + +```bash +# 安装 +# eslint 是命令工具 eslint-loader 是在编译er或启动项目时实时报错 +npm install eslint eslint-loader --save-dev + +# 初始化 eslint 规范,生成 .eslintrc.js 文件 +# Aribnb 是一种很变态的规范 +npx eslint init +``` + +```js +// webpack.config.js +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + exclude: /node-modules/, + use: ['babel-loader', 'eslint-loader'] // 先进行代码检验,再编译 + } + ] + }, + devServer: { + overlay: true // 如果过程出现错误,会通过蒙层来提示错误 + } +} +``` + +`eslint-loader` 还有许多配置参数可以设置: + +```js +// webpack.config.js +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + exclude: /node-modules/, + use: [ + { + loader: 'eslint-loader', + options: { + fix: true, // 如果有小的错误,可以直接修复 + cache: true // 优化打包速度 + }, + enforce: 'pre' // 虽然在 babel-loader 前面,但是可以提前执行(实际测试这个参数放在这里有问题,这里的loader就按照它本该有的循序去排列它,不要使用这个参数去控制了) + }, + 'babel-loader' + ] // 先进行代码检验,再编译 + } + ] + }, + devServer: { + overlay: true // 如果过程出现错误,会通过蒙层来提示错误 + } +} +``` + +```js +// .eslintrc.js +module.exports = { + "extends": "airbnb", + "parser": "babel-eslint", + "rules": { + "react/prefer-stateless-function": 0, + "react/jsx-filename-extension": 0 + }, + globals: { + document: false // 解决全局变量出错 + } +} +``` + +> VSCode 安装 ESLint 插件,实时显示错误。 + +如果报错 `Eslint parsing error: Unexpected token <`,可以借助 [eslint-plugin-html](https://github.com/BenoitZugmeyer/eslint-plugin-html)来解决。 + +:::tip +实际项目中为了不影响打包速度,可以不配置 `eslint-loader`,而是直接通过 git 钩子,在提交命令代码时进行检测,当然这个时候就放弃了实时报错的特性。 +```bash +git 钩子 eslint src +``` +::: + +## 提升打包速度 + +1. 跟上技术的迭代,保持最新(Node/Npm/Yarn) +2. 在尽快少的模块上使用 `loader`,比如通过 `include` 和 `exclude` 来指定打包监听范围 + ```js + module.exports = { + module: { + rules: [ + { + test: /\.js$/, + include: path.resolve(__dirname, '../src'), + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader' + } + ] + } + ] + } + } + ``` +3. 尽量精简 `plugin`,并且确保其可靠性 +4. 合理配置 extensions + ```js + module.exports = { + resolve: { + // 配置太多多引发多次查找,注意精简 + extensions: ['.js', '.jsx', '.css'], + // 引用如果引用文件时只写到上级目录,会默认引用 index 文件,这样如果没有 index 回去找 child 文件,也不要配置特别多,尽量不使用 + mainFiles: ['index', 'child'] + // 配置别名,缩短引用名称 + alias: { + "@component": path.resolve(__dirname, '../src/component') + } + } + } + ``` + +## Loader 原理 + +### 小案例 + +创建一个可以将 字符串 `reco` 替换为 `luan` 的简单 `loader` + +```js +// 新建一个loader /loaders/replaceLoader.js + +// 这里不可以使用箭头函数 +module.exports = function (source) { + return source.replace('reco', 'luan') +} + +// 使用 +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + use: [ + { + loader: path.resolve(__dirname, '/loaders/replaceLoader.js') + } + ] + } + ] + } +} +``` + +### 获取参数 + +```js +const loaderUtils = require('loader-utils') + +module.exports = function (source) { + // 参数会被放在 this.query 里面 + const { name } = this.query + + // 有的时候 options 可能不是对象而是字符串,我们可以借助 loader-utils + const loaderUtils = require('loader-utils') + const { name } = loaderUtils.getOptions(this) + + return source.replace('reco', name) +} + +// 使用 +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + use: [ + { + loader: path.resolve(__dirname, '/loaders/replaceLoader.js'), + options: { + name: 'luan' + } + } + ] + } + ] + } +} +``` + +### 丰富反馈内容 + +借助 `this.callback`: + +```js +this.callback( + err: Error | null, + content: string | Buffer, + sourceMap?: sourceMap, + meta?: any +) +``` + +```js +const loaderUtils = require('loader-utils') + +module.exports = function (source) { + const { name } = loaderUtils.getOptions(this) + const result = source.replace('reco', name) + + this.callback(null, result, sourceMap, meta) +} +``` + +### resolveLoader + +作用是寻找 loader 时可以直接去我们自定义的文件夹内去寻找。 + +```js +// 使用 +module.exports = { + resolveLoader: { + modules: ['node_modules', './loaders'] + }, + module: { + rules: [ + { + test: /\.js$/, + use: [ + { + loader: 'replaceLoader2' + }, + { + loader: path.resolve(__dirname, '/loaders/replaceLoader.js'), + options: { + name: 'luan' + } + } + ] + } + ] + } +} +``` + +### 异步处理 + +```js +const loaderUtils = require('loader-utils') + +module.exports = function (source) { + const { name } = loaderUtils.getOptions(this) + const callback = this.asunc() + + setTimeout(() => { + const result = source.replace('reco', name) + callback(null, result) + }, 1000) +} +``` + +```js +const loaderUtils = require('loader-utils') + +module.exports = function (source) { + const { name } = loaderUtils.getOptions(this) + const result = source.replace('reco', name) + + this.callback(null, result, sourceMap, meta) +} +``` + +> loader 还可以做哪些工作呢?比如 给代码添加 try catch,本地化,替换中英文 + +## Plugin 原理 + +### 简单 Plugin + +```js +// /plugins/copyright-webpack-plugin.js + +class CopyrightWebpackPlugin { + constructor (options) { + console.log(options) // { name: 'reco' } + } + + // compiler 是 webapck 的一个实例,存放着配置等所有的东西 + apply (compiler) { + /** + * hooks 是钩子 + * emit 将打包好的文件放到输出目录之前(异步钩子) + * compilation 和本次打包相关的东西 + */ + compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { + // 增加一个 txt 文件 + compilation.assets['copyright.txt'] = { + /** + * source 文本内容 + * size 文本字节大小 + */ + source: function () { + return 'copyright by reco_luan' + }, + size: function () { + return 22 + } + } + + // 必须回调 + cb() + }) + + // compile 同步钩子,不需要callback + compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => { + console.log('同步钩子') + }) + } +} + +module.exports = CopyrightWebpackPlugin +``` + +```js +// /webpack.config.js + +const CopyrightWebpackPlugin = require('/plugins/copyright-webpack-plugin.js') + +module.exports = { + plugins: [ + new CopyrightWebpackPlugin({ + name: 'reco' + }) + ] +} +``` + +### Node 调试 + +```json +// package.json +{ + "script": { + "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js", + "build": "webpack" + } +} +``` + +- `--inspect` 开启 Node 调试 +- `--inspect-brk` 在代码第一行添加一个 debug 命令 + +```js +class CopyrightWebpackPlugin { + apply (compiler) { + compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => { + // 打断点 + debugger; + console.log('同步钩子') + }) + } +} + +module.exports = CopyrightWebpackPlugin +``` + +打开控制台的 Node 图标,就进入了 Node 调试 + +## Bundler 源码编写 + +### 入口文件分析 + +```js +const fs = require('fs') +const path = require('path') +const babel = require('@babel/core') +const parser = require('@babel/parser') // 分析抽象语法树 +const traverse = require('@babel/traverse').default + + +// ************ 入口文件分析 ************** +const moduleAnalyser = (filename) => { + // 读取文件 + const content = fs.readFileSync(filename, 'utf-8') + + //分析抽象语法树 + const ast = parser.parse(content, { + sourceType: 'module' + }) + + // 分析依赖 + let dependencies = {} + traverse(ast, { // 第一个语法是抽象语法树 + ImportDeclaration ({ node }) { + // 获取依赖的相对路径 + const value = node.source.value + const dirname = path.dirname(filename) + const newFile = `./${path.join(dirname, value)}` + + // key: 将相对路径 value: 绝对路径 + dependencies[value] = newFile + } + }) + + // 将 ES6 语法转译为 浏览器可以执行的语法 + const { code } = babel.transformFromAst(ast, null, { + // 需要安装 @babel/preset-env + presets: ["@babel/preset-env"] + }) + + /** + * filename // 入口文件 + * dependencies // 依赖关系 + * code // 打包后的代码 + */ + return { + filename, + dependencies, + code + } +} + +const moduleInfo = moduleAnalyser('./src/index.js') + +console.log(moduleInfo) +``` + +### 依赖图谱 + +```js +const fs = require('fs') +const path = require('path') +const babel = require('@babel/core') +const parser = require('@babel/parser') // 分析抽象语法树 +const traverse = require('@babel/traverse').default + +// ************ 入口文件分析 ************** +const moduleAnalyser = (filename) => { + // 读取文件 + const content = fs.readFileSync(filename, 'utf-8') + + //分析抽象语法树 + const ast = parser.parse(content, { + sourceType: 'module' + }) + + // 分析依赖 + let dependencies = {} + traverse(ast, { // 第一个语法是抽象语法树 + ImportDeclaration ({ node }) { + // 获取依赖的相对路径 + const value = node.source.value + const dirname = path.dirname(filename) + const newFile = `./${path.join(dirname, value)}` + + // key: 将相对路径 value: 绝对路径 + dependencies[value] = newFile + } + }) + + // 将 ES6 语法转译为 浏览器可以执行的语法 + const { code } = babel.transformFromAst(ast, null, { + // 需要安装 @babel/preset-env + presets: ["@babel/preset-env"] + }) + + /** + * filename // 入口文件 + * dependencies // 依赖关系 + * code // 打包后的代码 + */ + return { + filename, + dependencies, + code + } +} + +// const moduleInfo = moduleAnalyser('./src/index.js') +// console.log(moduleInfo) + +// ************ 依赖图谱 ************** +const makeDependenciesGraph = (entry) => { + // 首先在依赖图谱中插入入口文件的分析 + const entryModule = moduleAnalyser(entry) + const graphArray = [ entryModule ] + + /** + * 循环入口文件的依赖并将其添加到 graphArray 中,因为 graphArray 是动态的, + * graphArray.length 也是动态的,所以可以进入下一轮循环 + */ + for (let i = 0; i < graphArray.length; i++) { + const item = graphArray[i] + const { dependencies } = item + if (dependencies) { + for (let j in dependencies) { + graphArray.push(moduleAnalyser(dependencies[j])) + } + } + } + + // 依键值对的形式重新组合数据 + const graph = {} + graphArray.forEach(item => { + graph[item.filename] = { + dependencies: item.dependencies, + code: item.code + } + }) + return graph +} + +const graphInfo = makeDependenciesGraph('./src/index.js') +console.log(graphInfo) +``` + +### 生成可用代码 + +```js +const fs = require('fs') +const path = require('path') +const babel = require('@babel/core') +const parser = require('@babel/parser') // 分析抽象语法树 +const traverse = require('@babel/traverse').default + +// ************ 入口文件分析 ************** +const moduleAnalyser = (filename) => { + // 读取文件 + const content = fs.readFileSync(filename, 'utf-8') + + //分析抽象语法树 + const ast = parser.parse(content, { + sourceType: 'module' + }) + + // 分析依赖 + let dependencies = {} + traverse(ast, { // 第一个语法是抽象语法树 + ImportDeclaration ({ node }) { + // 获取依赖的相对路径 + const value = node.source.value + const dirname = path.dirname(filename) + const newFile = `./${path.join(dirname, value)}` + + // key: 将相对路径 value: 绝对路径 + dependencies[value] = newFile + } + }) + + // 将 ES6 语法转译为 浏览器可以执行的语法 + const { code } = babel.transformFromAst(ast, null, { + // 需要安装 @babel/preset-env + presets: ["@babel/preset-env"] + }) + + /** + * filename // 入口文件 + * dependencies // 依赖关系 + * code // 打包后的代码 + */ + return { + filename, + dependencies, + code + } +} + +// const moduleInfo = moduleAnalyser('./src/index.js') +// console.log(moduleInfo) + +// ************ 依赖图谱 ***************** +const makeDependenciesGraph = (entry) => { + // 首先在依赖图谱中插入入口文件的分析 + const entryModule = moduleAnalyser(entry) + const graphArray = [ entryModule ] + + /** + * 循环入口文件的依赖并将其添加到 graphArray 中,因为 graphArray 是动态的, + * graphArray.length 也是动态的,所以可以进入下一轮循环 + */ + for (let i = 0; i < graphArray.length; i++) { + const item = graphArray[i] + const { dependencies } = item + if (dependencies) { + for (let j in dependencies) { + graphArray.push(moduleAnalyser(dependencies[j])) + } + } + } + + // 依键值对的形式重新组合数据 + const graph = {} + graphArray.forEach(item => { + graph[item.filename] = { + dependencies: item.dependencies, + code: item.code + } + }) + return graph +} + +// const graphInfo = makeDependenciesGraph('./src/index.js') +// console.log(graphInfo) + +// ************ 生成代码 ***************** +const generateCode = (entry) => { + // 依赖树是一个对象,需要解析成字符串 + const graph = JSON.stringify(makeDependenciesGraph(entry)) + + /** + * 依赖树每个模块对应的代码都需要 require/modules 对象,所以需要自己来构建 + */ + return ` + (function (graph) { + function require (module) { + // require 需要引用相对路径,所以创建 localRequire + function localRequire (relativePath) { + return require(graph[module].dependencies[relativePath]) + } + + // 没有 exports 对象,需要手动创建 + // 切记,这里的 分号 是必须有的 + var exports = {}; + (function (require, exports, code) { + // 执行代码 + eval(code) + })(localRequire, exports, graph[module].code) + + // 导出后别的依赖,才能进行引用 + return exports + } + require('${entry}') + })(${graph}) + ` +} + +const code = generateCode('./src/index.js') +console.log(code) + + +``` + +## 深入学习 + +### CreateReactApp + +### vue-cli + +### Vue + +## 其他 + +### 清除性能报错 + +```js +module.exports = { + performance: false +} +``` + +### 启动一个服务 + +```bash +npm install http-server -D +``` + +将 dist 目录 作为根目录启动服务: + +```json +{ + "scripts": { + "start": "http-server dist" + } +} +``` \ No newline at end of file diff --git a/example/views/sidebar/README.md b/example/views/sidebar/README.md new file mode 100644 index 0000000..4d07e87 --- /dev/null +++ b/example/views/sidebar/README.md @@ -0,0 +1,5 @@ +--- +title: 介绍 +--- + +介绍 \ No newline at end of file diff --git a/example/views/sidebar/bar1.md b/example/views/sidebar/bar1.md new file mode 100644 index 0000000..b2f7223 --- /dev/null +++ b/example/views/sidebar/bar1.md @@ -0,0 +1,5 @@ +--- +title: bar1 +--- + +bar1 \ No newline at end of file diff --git a/example/views/sidebar/bar2.md b/example/views/sidebar/bar2.md new file mode 100644 index 0000000..cd19f0c --- /dev/null +++ b/example/views/sidebar/bar2.md @@ -0,0 +1,65 @@ +--- +title: bar2 +--- + +bar2 + +## 二级标题1 + +### 三级标题1-1 + +#### 四级标题1-1-1 +#### 四级标题1-1-2 +#### 四级标题1-1-3 + +### 三体标题1-2 + +#### 四级标题1-2-1 +#### 四级标题1-2-2 +#### 四级标题1-2-3 + +### 三体标题1-3 + +#### 四级标题1-3-1 +#### 四级标题1-3-2 +#### 四级标题1-3-3 + +## 二级标题2 + +### 三级标题2-1 + +#### 四级标题2-1-1 +#### 四级标题2-1-2 +#### 四级标题2-1-3 + +### 三体标题2-2 + +#### 四级标题2-2-1 +#### 四级标题2-2-2 +#### 四级标题2-2-3 + +### 三体标题2-3 + +#### 四级标题2-3-1 +#### 四级标题2-3-2 +#### 四级标题2-3-3 + +## 二级标题3 + +### 三级标题3-1 + +#### 四级标题3-1-1 +#### 四级标题3-1-2 +#### 四级标题3-1-3 + +### 三体标题3-2 + +#### 四级标题3-2-1 +#### 四级标题3-2-2 +#### 四级标题3-2-3 + +### 三体标题3-3 + +#### 四级标题3-3-1 +#### 四级标题3-3-2 +#### 四级标题3-3-3 \ No newline at end of file diff --git a/packages/vuepress-theme-reco/components/Common.vue b/packages/vuepress-theme-reco/components/Common.vue index e3e79a3..7a0f3ee 100644 --- a/packages/vuepress-theme-reco/components/Common.vue +++ b/packages/vuepress-theme-reco/components/Common.vue @@ -128,13 +128,14 @@ export default { }, shouldShowSidebar () { - const { frontmatter } = this.$page - return ( - this.sidebar !== false && - !frontmatter.home && - frontmatter.sidebar !== false && - this.sidebarItems.length - ) + // const { frontmatter } = this.$page + // return ( + // this.sidebar !== false && + // !frontmatter.home && + // frontmatter.sidebar !== false && + // this.sidebarItems.length + // ) + return this.sidebarItems.length > 0 }, pageClasses () { diff --git a/packages/vuepress-theme-reco/components/Page.vue b/packages/vuepress-theme-reco/components/Page.vue index ea01c1f..9626d06 100644 --- a/packages/vuepress-theme-reco/components/Page.vue +++ b/packages/vuepress-theme-reco/components/Page.vue @@ -73,7 +73,7 @@ - + @@ -82,11 +82,11 @@ import PageInfo from '@theme/components/PageInfo' import { resolvePage, outboundRE, endingSlashRE } from '@theme/helpers/utils' import ModuleTransition from '@theme/components/ModuleTransition' import moduleTransitonMixin from '@theme/mixins/moduleTransiton' -import SubSiderbar from '@theme/components/SubSiderbar' +import SubSidebar from '@theme/components/SubSidebar' export default { mixins: [moduleTransitonMixin], - components: { PageInfo, ModuleTransition, SubSiderbar }, + components: { PageInfo, ModuleTransition, SubSidebar }, props: ['sidebarItems'], @@ -242,10 +242,15 @@ function flatten (items, res) { padding-bottom 2rem padding-right 10rem display block - .sider-bar + .side-bar position fixed top 10rem + bottom 10rem right 2rem + overflow-y scroll + &::-webkit-scrollbar + width: 0 + height: 0 .page-title max-width: $contentWidth; margin: 0 auto; @@ -289,7 +294,7 @@ function flatten (items, res) { @media (max-width: $MQMobile) .page padding-right 0 - .sider-bar + .side-bar display none .page-title padding: 0 1rem; diff --git a/packages/vuepress-theme-reco/components/SidebarLinks.vue b/packages/vuepress-theme-reco/components/SidebarLinks.vue index 9d45bfe..fb573cc 100644 --- a/packages/vuepress-theme-reco/components/SidebarLinks.vue +++ b/packages/vuepress-theme-reco/components/SidebarLinks.vue @@ -88,24 +88,24 @@ export default { }, isInViewPortOfOne () { - const siderbarScroll = document.getElementsByClassName('sidebar')[0] + const sidebarScroll = document.getElementsByClassName('sidebar')[0] let el = document.getElementsByClassName('active sidebar-link')[1] if (el == null || el == undefined || el.offsetTop == undefined) { el = document.getElementsByClassName('active sidebar-link')[0] } if (el == null || el == undefined || el.offsetTop == undefined) return - const viewPortHeight = siderbarScroll.clientHeight || window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight + const viewPortHeight = sidebarScroll.clientHeight || window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight const offsetTop = el.offsetTop const offsetBottom = el.offsetTop + el.offsetHeight - const scrollTop = siderbarScroll.scrollTop + const scrollTop = sidebarScroll.scrollTop const bottomVisible = (offsetBottom <= viewPortHeight + scrollTop) if (!bottomVisible) { - siderbarScroll.scrollTop = (offsetBottom + 5 - viewPortHeight) + sidebarScroll.scrollTop = (offsetBottom + 5 - viewPortHeight) } const topVisible = (offsetTop >= scrollTop) if (!topVisible) { - siderbarScroll.scrollTop = (offsetTop - 5) + sidebarScroll.scrollTop = (offsetTop - 5) } }, diff --git a/packages/vuepress-theme-reco/components/SubSideBar.vue b/packages/vuepress-theme-reco/components/SubSideBar.vue new file mode 100644 index 0000000..588f23a --- /dev/null +++ b/packages/vuepress-theme-reco/components/SubSideBar.vue @@ -0,0 +1,82 @@ + + + + diff --git a/packages/vuepress-theme-reco/components/SubSiderBar.vue b/packages/vuepress-theme-reco/components/SubSiderBar.vue deleted file mode 100644 index bb707c9..0000000 --- a/packages/vuepress-theme-reco/components/SubSiderBar.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/packages/vuepress-theme-reco/helpers/utils.js b/packages/vuepress-theme-reco/helpers/utils.js index 80c3efc..d89103b 100644 --- a/packages/vuepress-theme-reco/helpers/utils.js +++ b/packages/vuepress-theme-reco/helpers/utils.js @@ -122,42 +122,49 @@ export function resolveSidebarItems (page, regularPath, site, localePath) { ? themeConfig.locales[localePath] || themeConfig : themeConfig - const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar - if (pageSidebarConfig === 'auto') { - return resolveHeaders(page) - } + // 计算页面的菜单层级 + // const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar + // if (pageSidebarConfig === 'auto') { + // return resolveHeaders(page) + // } + // const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar + // if (!sidebarConfig) { + // return [] + // } else { + // const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig) + // return config + // ? config.map(item => resolveItem(item, pages, base)) + // : [] + // } const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar - if (!sidebarConfig) { - return [] - } else { - const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig) - return config - ? config.map(item => resolveItem(item, pages, base)) - : [] - } + + const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig) + return config + ? config.map(item => resolveItem(item, pages, base)) + : [] } /** * @param { Page } page * @returns { SidebarGroup } */ -function resolveHeaders (page) { - const headers = groupHeaders(page.headers || []) - return [{ - type: 'group', - collapsable: false, - title: page.title, - path: null, - children: headers.map(h => ({ - type: 'auto', - title: h.title, - basePath: page.path, - path: page.path + '#' + h.slug, - children: h.children || [] - })) - }] -} +// function resolveHeaders (page) { +// const headers = groupHeaders(page.headers || []) +// return [{ +// type: 'group', +// collapsable: false, +// title: page.title, +// path: null, +// children: headers.map(h => ({ +// type: 'auto', +// title: h.title, +// basePath: page.path, +// path: page.path + '#' + h.slug, +// children: h.children || [] +// })) +// }] +// } export function groupHeaders (headers) { // group h3s under h2