基於webpack打包多頁應用,對前端工程化的思考(下)

語言: CN / TW / HK

前言

上一篇文章 基於webpack打包多頁應用,對前端工程化的思考 107👍

這也是我第一個 點贊破百 的文章,感謝掘友的支持,開心😃

總結起來,上一篇文章並沒有寫什麼實際性的東西,可能大家對工程化比較感興趣,其中也有不少掘友熱情的探討

十分感謝大家不同的意見,如有問題,都可以在下面留言哦!

  1. 完整源碼已放github,並配有完整註釋,歡迎直接去github上看源碼
  2. 如有幫助,歡迎star,萬分感謝

下面我們接着上一篇文章,談論些更多有意思的東西。

按照約定,路由自動生成

單頁面應用中,有很多框架都做了該功能,比如基於vueNuxt.js,基於reactUmiJS。比起單頁面,多頁面中並沒有路由嵌套,路由傳參等複雜的路由結構,所以在多頁面中實現路由自動生成要比單頁面簡單些。

為了使用更加"野性",我在新建 文件夾 的時候會自動生成路由和配套的html,css,js,效果如下:

實現思路

代碼實現

自動生成路由最主要的是文件的自動監聽,我使用的是chokidar(速度還可以,比watch模塊強太多了)。為了便於維護,我把此功能抽離成獨立的 webpack插件 大家可在源碼文件plugins/router-auto-plugin.js中查看插件全部源碼。核心代碼如下:

  compiler.hooks.entryOption.tap("invoke", () => {
    // 初始化chokidar
    const watcher = chokidar.watch(this.watchPath, { persistent: true });
    watcher.on("addDir", async (pathname, store) => {
      // 判斷文件格式,只要創建文件夾,自動生成裏面的文件
      const p = pathname.split(path.sep).pop();
      if (p === "pages") return false;
      // 生成對應的html css 和 js
      this.writeInit(p);
      // 跟新路由
      this.changeRouterTemplate();
    });

    watcher.on("unlinkDir", async () => {
      console.log(chalk.blue(`刪除成功!`));
      this.changeRouterTemplate();
    });
  });
複製代碼

因為不存在路由嵌套問題,每次新增文件相當於循環一次特定的配置模板

//生成動態配置核心方法
const changeRouterCompile = (meta, filePath, templatePath, text) => {
  if (fs.existsSync(templatePath)) {
    const content = fs.readFileSync(templatePath).toString();
    const reslut = handlebars.compile(content)(meta);
    fs.writeFileSync(filePath, beautify(reslut));
  }
  console.log(chalk.blue(`${text}`));
};
// 根據模板生成路由信息
changeRouterTemplate() {
  const list = fs.readdirSync(this.watchPath).map((v) => ({
    name: v,
  }));
  changeRouterCompile(
    { list },
    `${root}/config/routerTemplate.js`,
    `${root}/config/template/routerTemplate.js.hbs`,
    `路由生成成功!`
  );
  changeRouterCompile(
    { list },
    `${root}/config/entryTemplate.js`,
    `${root}/config/template/entryTemplate.js.hbs`,
    `entry生成成功!`
  );
}
複製代碼

.hbs文件就是要循環的模板

// entryTemplate.js.hbs
module.exports = {
entry: {
{{#each list}}
{{name}}: path.join(__dirname, "../src/pages/{{name}}/{{name}}.js"),
{{/each}}
},
};
複製代碼

當然此方法只能用於簡單場景,複雜的路由生成還需要使用ast(抽象語法樹)來實現。

每生成一次頁面就意味着更改一次webpack配置,所有就需要重啟webpack,硬傷😐

js,css tree-shaking

tree-shaking 就是把沒用到的jscss不打包到 生成環境 中,這樣可以大大減少我們代碼體積

js tree-shaking

webpack5已經自帶jstree-shaking,在webpack4中當我們把mode設置為production時已經開啟了tree-shaking,但是隻對ES6模塊的代碼有效,所以我們還需設置babel在處理js時不讓他轉化為CommonJS

.babelrc

{
  "presets": [
    ["@babel/preset-env",
      {
        "modules": false
      }
    ]
  ]
}
複製代碼

關於為什麼不在項目中使用webpack5,我勸你在等等,説多了都是淚😭

css tree-shaking

css tree-shaking可能是最容易被大家忽略的優化點,但在實際開發中有很多無用的css充斥在代碼裏,我們可以進行如下配置來進行tree-shaking,主要用到以下插件

 "purify-css": "^1.2.5",
 "purifycss-webpack": "^0.7.0",
 "glob-all": "^3.2.1"  // 用於匹配路徑,簡化操作
複製代碼

配置如下

plugin:[
...
 new PurifyCss({
      paths: glob.sync([
        path.resolve(__dirname, "./src*.html"),
        path.resolve(__dirname, "./src/*.js"),
      ]),
    }),
...
]

複製代碼

其他優化

自動生成css前綴

增加css前綴,再也不被兼容性所煩惱,具體配置如下:

npm i postcss-loader autoprefixer -D
複製代碼
// webpack.pro.config.js
{
  test: /\.css$/,
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        publicPath: "../../",
        plugins: [require("autoprefixer")],
      },
    },
    "css-loader",
  ],
},

複製代碼

使用HappyPack提高打包速度

webpack在打包時最耗時的就是眾多的loader轉化處理,這時我們可以使用HappyPack開啟多個線程從而提高打包速度

npm i happypack -D
複製代碼
// webpack.pro.js
const HappyPack = require('happypack')
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); // 自動獲取線程數

rules:[
	...
   {
      test: /\.js$/,
      exclude: "/node_modules/",
      loader: "HappyPack/loader?id=js", // 注意不要在rules中使用options配置,需要在plugins中配置
    },
 	...
]

plugins:[
  new HappyPack({
      id: "js", // 對應上面loader?id=js
      loaders: [
        {
          loader: "babel-loader",
          options: {
            plugins: ["dynamic-import-webpack"],
            cacheDirectory: true,
          },
        },
      ],
      threadPool: happyThreadPool, 
    }),
]
複製代碼

使用cache-loader設置緩存

我們每次執行構建都會把所有的文件都重複編譯一遍,那對於那些不變的文件,可以不可以緩存起來呢?使用cache-loader就可以做到

請注意,保存和讀取這些緩存文件會有一些時間開銷,所以請只對性能開銷較大的 loader 使用此 loader。

比如我們緩存babel-loader

{
  test: /\.ext$/,
  use: ["cache-loader", "babel-loader"],
  include: path.resolve("src"),
},
複製代碼

使用expose-loader將模塊暴露為全局變量

開發多頁面少不了會使用到jquery,那我們如何把jquery暴露到全局,不用在每個頁面都引用呢

npm i jquery -S
npm i expose-loader -D
複製代碼
// webpack.base.config
rules:[
  ...
    {
      test: require.resolve("jquery"),
      use: "expose-loader?$",
    }
  ...
 ]
plugins:[
  new webpack.ProvidePlugin({
    $: "jquery",
    jQuery: "jquery",
  }),
]

複製代碼

製作為命令行工具

類比創建vue項目,我們使用vue-cli提供的命令行vue create <app-name>就可以創建一個完整的vue項目。那麼我們可不可以做一個自己的命令行工具呢?下面我們就一步一步來實現。效果如下:

新建項目

新建文件夾lyh-cli使用npm init -y初始化項目,這時會生成一個package.json文件,新增bin配置項,配置腳手架名稱

{
  "name": "lyh-cli",
  "version": "1.0.0",
  "description": "lyh-pages 專屬腳手架",
  "main": "index.js",
  "bin": {
    "lyh": "./bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",                                                          
  "license": "ISC",
  "dependencies": {
    "commander": "^6.2.1"
  }
}
複製代碼

新建bin文件夾和index.js文件,此時項目文件如下所示:

下面兩個步驟至關重要:

  1. 打開bin/index.js文件,在文件頭部加上一下標識,及測試代碼:
#!/usr/bin/env node

console.log("init");
複製代碼
  1. 命令行執行npm linknpm模塊鏈接到對應項目中去

  2. 在命令行輸入lyh,可以成功顯示出我們在bin/index.js寫的測試代碼

現在我們的腳手架架子已經搭建完畢✌。

實現lyh create <project name>命令

安裝依賴

npm i commander -S
複製代碼

commander完整的 node.js 命令行解決方案,是開發腳手架必不可少的利器

下面我們先來思考,在執行npm create命令時,都需要幹哪些事

具體代碼實現

// 項目所用依賴

## commander node 命令行插件 必須

## figlet 給文字加特效

## clear 清除命令行

## chalk 畫筆工具

## download-git-repo 從git上clone代碼

## ora 顯示 loading

複製代碼
#!/usr/bin/env node
// bin/index.js
const program = require("commander");
program.version(require("../package.json").version);
const init = require("../lib/init");
program
  .command("create <name>")
  .description("初始化項目")
  .action((name) => {
    init(name);
  });

program.parse(process.argv);
複製代碼
// lib/init.js
const { promisify } = require("util");
const figlet = promisify(require("figlet"));
const chalk = require("chalk"); // 畫筆
const clear = require("clear");
const { clone } = require("./clone");
const init = async (projectName) => {
  clear(); // 清理命令行
  console.log(chalk.green(await figlet("lyh-cli")));
  await clone("github:lyh0371/lyh-pages", projectName);
  console.log(
    chalk.green(`
  下載成功!
  進入項目: cd ${projectName}
  安裝依賴: cnpm/npm  install
  運行項目: npm run dev
  打包項目: npm run build
  `)
  );
};
module.exports = init;
複製代碼
//lib/clone.js
const { promisify } = require("util");
module.exports.clone = async (repo, desc) => {
  const download = promisify(require("download-git-repo"));
  const ora = require("ora");
  const process = ora(`下載.....${repo}`);
  process.start();
  await download(repo, desc);
  process.succeed();
};

複製代碼

發佈到npm

發佈到npm很簡單,只要你有npm賬號(沒有的先在官網註冊,在這裏就不贅述了),只要以下兩步即可發佈一個npm包

  1. 使用npm login輸入個人信息登錄到npm
  2. 使用npm publish發佈
  • 如果在發佈的時候報錯,可能是你起的包名已經被別人佔用,在package.json換一個name即可
  • 每次npm publish前都需要改一個版本(package.json的version)字段

新鮮出爐的lyhs-cli

最後

  1. 完整源碼已放github,並配有完整註釋,歡迎直接去github上看源碼
  2. 如有幫助,歡迎star,萬分感謝

如有幫助,歡迎點贊關注喲😁