Repository: chaxus/ran Branch: main Commit: 652113ce338a Files: 919 Total size: 53.0 MB Directory structure: gitextract_yjd90j70/ ├── .editorconfig ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── pages-build-site.yml │ └── semantic-pull-request.yml ├── .gitignore ├── .jscpd.json ├── .prettierignore ├── .prettierrc.json ├── .vscode/ │ ├── c_cpp_properties.json │ ├── launch.json │ ├── markdown.code-snippets │ ├── ranui-components.code-snippets │ ├── settings.json │ └── ts.json.code-snippets ├── LICENSE ├── SECURITY.md ├── bin/ │ ├── build.sh │ ├── clean.sh │ ├── install.sh │ ├── lint.sh │ ├── nvm.sh │ ├── preinstall.sh │ └── test.sh ├── eslint.config.js ├── index.html ├── package.json ├── packages/ │ ├── cpro/ │ │ ├── package.json │ │ ├── readme.md │ │ └── sum.cpp │ ├── debug/ │ │ ├── koaStatic/ │ │ │ ├── index.ts │ │ │ └── readme.md │ │ ├── nginxconf/ │ │ │ ├── agency.conf │ │ │ └── readme.md │ │ ├── package.json │ │ └── tsconfig.json │ ├── docs/ │ │ ├── .vitepress/ │ │ │ ├── common/ │ │ │ │ ├── index.ts │ │ │ │ └── sw.ts │ │ │ ├── components/ │ │ │ │ ├── Layout.vue │ │ │ │ └── TOTP.vue │ │ │ ├── composition/ │ │ │ │ └── useBasic.ts │ │ │ ├── config.ts │ │ │ ├── lang/ │ │ │ │ ├── en.json │ │ │ │ ├── index.ts │ │ │ │ └── zh-CN.json │ │ │ ├── langs/ │ │ │ │ ├── cn/ │ │ │ │ │ └── index.ts │ │ │ │ └── en/ │ │ │ │ └── index.ts │ │ │ ├── lib/ │ │ │ │ └── constant.ts │ │ │ ├── plugins/ │ │ │ │ └── env.ts │ │ │ └── theme/ │ │ │ ├── index.ts │ │ │ ├── styles/ │ │ │ │ ├── index.less │ │ │ │ └── vars.less │ │ │ └── theme.less │ │ ├── bin/ │ │ │ └── build.sh │ │ ├── cn/ │ │ │ ├── index.md │ │ │ └── src/ │ │ │ ├── article/ │ │ │ │ ├── ai/ │ │ │ │ │ └── index.md │ │ │ │ ├── ast_parse/ │ │ │ │ │ └── tokenizer.md │ │ │ │ ├── babel.md │ │ │ │ ├── bundle.md │ │ │ │ ├── design_mode.md │ │ │ │ ├── doc_preview.md │ │ │ │ ├── functional_programming.md │ │ │ │ ├── imagemin.md │ │ │ │ ├── javascript/ │ │ │ │ │ └── dom_load.md │ │ │ │ ├── math/ │ │ │ │ │ └── linear_algebra.md │ │ │ │ ├── sort/ │ │ │ │ │ ├── bubble/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── bucket/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── count/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── heap/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── index.md │ │ │ │ │ ├── insert/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── merge/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── quick/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── radix/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── select/ │ │ │ │ │ │ └── index.md │ │ │ │ │ └── shell/ │ │ │ │ │ └── index.md │ │ │ │ ├── system_design.md │ │ │ │ ├── typescript/ │ │ │ │ │ ├── calculate.md │ │ │ │ │ ├── index.md │ │ │ │ │ ├── pattern.md │ │ │ │ │ ├── reconstruction.md │ │ │ │ │ ├── recursion.md │ │ │ │ │ └── union_type.md │ │ │ │ ├── video.md │ │ │ │ └── visual.md │ │ │ ├── blockchain/ │ │ │ │ ├── index.md │ │ │ │ └── web3.md │ │ │ ├── note/ │ │ │ │ ├── centos.md │ │ │ │ ├── docker.md │ │ │ │ ├── libreoffice2wasm.md │ │ │ │ └── ubuntu.md │ │ │ ├── ranui/ │ │ │ │ ├── button/ │ │ │ │ │ └── index.md │ │ │ │ ├── checkbox/ │ │ │ │ │ └── index.md │ │ │ │ ├── icon/ │ │ │ │ │ └── index.md │ │ │ │ ├── image/ │ │ │ │ │ └── index.md │ │ │ │ ├── index.md │ │ │ │ ├── input/ │ │ │ │ │ └── index.md │ │ │ │ ├── loading/ │ │ │ │ │ └── index.md │ │ │ │ ├── math/ │ │ │ │ │ └── index.md │ │ │ │ ├── message/ │ │ │ │ │ └── index.md │ │ │ │ ├── modal/ │ │ │ │ │ └── index.md │ │ │ │ ├── player/ │ │ │ │ │ └── index.md │ │ │ │ ├── popover/ │ │ │ │ │ └── index.md │ │ │ │ ├── preview/ │ │ │ │ │ └── index.md │ │ │ │ ├── progress/ │ │ │ │ │ └── index.md │ │ │ │ ├── radar/ │ │ │ │ │ └── index.md │ │ │ │ ├── select/ │ │ │ │ │ └── index.md │ │ │ │ ├── skeleton/ │ │ │ │ │ └── index.md │ │ │ │ ├── tab/ │ │ │ │ │ └── index.md │ │ │ │ └── tabs/ │ │ │ │ └── index.md │ │ │ ├── ranuts/ │ │ │ │ ├── binary_tree/ │ │ │ │ │ └── index.md │ │ │ │ ├── bundler/ │ │ │ │ │ └── index.md │ │ │ │ ├── file/ │ │ │ │ │ ├── append_file.md │ │ │ │ │ ├── file_info.md │ │ │ │ │ ├── read_dir.md │ │ │ │ │ ├── read_file.md │ │ │ │ │ ├── watch_file.md │ │ │ │ │ └── write_file.md │ │ │ │ ├── index.md │ │ │ │ ├── mime_type/ │ │ │ │ │ └── mime_type.md │ │ │ │ ├── mode/ │ │ │ │ │ └── subscribe.md │ │ │ │ └── utils/ │ │ │ │ ├── add_class_to_element.md │ │ │ │ ├── add_num_sym.md │ │ │ │ ├── append_url.md │ │ │ │ ├── chain.md │ │ │ │ ├── change_hump_to_lower_case.md │ │ │ │ ├── check_encoding.md │ │ │ │ ├── clear_br.md │ │ │ │ ├── clear_str.md │ │ │ │ ├── clone_deep.md │ │ │ │ ├── compose.md │ │ │ │ ├── connection.md │ │ │ │ ├── convert_image_to_base64.md │ │ │ │ ├── create.md │ │ │ │ ├── create_document_fragment.md │ │ │ │ ├── create_object_url.md │ │ │ │ ├── create_signal.md │ │ │ │ ├── current_device.md │ │ │ │ ├── debounce.md │ │ │ │ ├── duration_handler.md │ │ │ │ ├── encode_url.md │ │ │ │ ├── escape_html.md │ │ │ │ ├── filter_obj.md │ │ │ │ ├── format_json.md │ │ │ │ ├── get_all_query_string.md │ │ │ │ ├── get_cookie.md │ │ │ │ ├── get_cookie_by_name.md │ │ │ │ ├── get_extensions.md │ │ │ │ ├── get_frame.md │ │ │ │ ├── get_host.md │ │ │ │ ├── get_matching_sentences.md │ │ │ │ ├── get_performance.md │ │ │ │ ├── get_pixel_ratio.md │ │ │ │ ├── get_query.md │ │ │ │ ├── get_window.md │ │ │ │ ├── handle_console.md │ │ │ │ ├── handle_error.md │ │ │ │ ├── handle_fetch_hook.md │ │ │ │ ├── hex_to_rgb.md │ │ │ │ ├── image_request.md │ │ │ │ ├── index.md │ │ │ │ ├── is_bang_device.md │ │ │ │ ├── is_client.md │ │ │ │ ├── is_equal.md │ │ │ │ ├── is_image_size.md │ │ │ │ ├── is_mobile.md │ │ │ │ ├── is_safari.md │ │ │ │ ├── is_string.md │ │ │ │ ├── is_weixin.md │ │ │ │ ├── local_storage.md │ │ │ │ ├── mathjs.md │ │ │ │ ├── md5.md │ │ │ │ ├── memoize.md │ │ │ │ ├── merge.md │ │ │ │ ├── network_speed.md │ │ │ │ ├── noop.md │ │ │ │ ├── ocr.md │ │ │ │ ├── per_to_num.md │ │ │ │ ├── performance_time.md │ │ │ │ ├── querystring.md │ │ │ │ ├── random_color.md │ │ │ │ ├── random_string.md │ │ │ │ ├── range.md │ │ │ │ ├── remove_class_to_element.md │ │ │ │ ├── remove_ghosting.md │ │ │ │ ├── retain.md │ │ │ │ ├── rgb_to_hex.md │ │ │ │ ├── script_on_load.md │ │ │ │ ├── set_attribute_by_global.md │ │ │ │ ├── set_mime.md │ │ │ │ ├── str2xml.md │ │ │ │ ├── str_parse.md │ │ │ │ ├── sync_hook.md │ │ │ │ ├── task.md │ │ │ │ ├── throttle.md │ │ │ │ ├── time_format.md │ │ │ │ ├── timestamp_to_time.md │ │ │ │ ├── to_string.md │ │ │ │ ├── totp.md │ │ │ │ ├── transform_number.md │ │ │ │ └── transform_text.md │ │ │ └── types/ │ │ │ ├── TS类型.md │ │ │ ├── 模式匹配.md │ │ │ ├── 类型运算.md │ │ │ └── 高级类型.md │ │ ├── index.md │ │ ├── package.json │ │ ├── public/ │ │ │ ├── hls/ │ │ │ │ ├── 5_1701577744714/ │ │ │ │ │ ├── 5.key │ │ │ │ │ ├── 5.m3u8 │ │ │ │ │ ├── 5_00000.ts │ │ │ │ │ ├── 5_00001.ts │ │ │ │ │ ├── 5_00002.ts │ │ │ │ │ ├── 5_00003.ts │ │ │ │ │ ├── 5_00004.ts │ │ │ │ │ ├── 5_00005.ts │ │ │ │ │ └── 5_00006.ts │ │ │ │ ├── 5_1701577771368/ │ │ │ │ │ ├── 5.key │ │ │ │ │ ├── 5.m3u8 │ │ │ │ │ ├── 5_00000.ts │ │ │ │ │ ├── 5_00001.ts │ │ │ │ │ ├── 5_00002.ts │ │ │ │ │ ├── 5_00003.ts │ │ │ │ │ ├── 5_00004.ts │ │ │ │ │ ├── 5_00005.ts │ │ │ │ │ └── 5_00006.ts │ │ │ │ └── example.m3u8 │ │ │ ├── manifest.json │ │ │ └── sw.js │ │ ├── src/ │ │ │ ├── article/ │ │ │ │ ├── ast_parse/ │ │ │ │ │ └── tokenizer.md │ │ │ │ ├── babel.md │ │ │ │ ├── bundle.md │ │ │ │ ├── design_mode.md │ │ │ │ ├── functional_programming.md │ │ │ │ ├── imagemin.md │ │ │ │ ├── javascript/ │ │ │ │ │ └── dom_load.md │ │ │ │ ├── sort/ │ │ │ │ │ ├── bubble/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── bucket/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── count/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── heap/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── index.md │ │ │ │ │ ├── insert/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── merge/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── quick/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── radix/ │ │ │ │ │ │ └── index.md │ │ │ │ │ ├── select/ │ │ │ │ │ │ └── index.md │ │ │ │ │ └── shell/ │ │ │ │ │ └── index.md │ │ │ │ └── typescript/ │ │ │ │ ├── calculate.md │ │ │ │ ├── index.md │ │ │ │ ├── pattern.md │ │ │ │ ├── reconstruction.md │ │ │ │ ├── recursion.md │ │ │ │ └── union_type.md │ │ │ ├── ranui/ │ │ │ │ ├── button/ │ │ │ │ │ └── index.md │ │ │ │ ├── checkbox/ │ │ │ │ │ └── index.md │ │ │ │ ├── icon/ │ │ │ │ │ └── index.md │ │ │ │ ├── image/ │ │ │ │ │ └── index.md │ │ │ │ ├── index.md │ │ │ │ ├── input/ │ │ │ │ │ └── index.md │ │ │ │ ├── loading/ │ │ │ │ │ └── index.md │ │ │ │ ├── math/ │ │ │ │ │ └── index.md │ │ │ │ ├── message/ │ │ │ │ │ └── index.md │ │ │ │ ├── modal/ │ │ │ │ │ └── index.md │ │ │ │ ├── player/ │ │ │ │ │ └── index.md │ │ │ │ ├── popover/ │ │ │ │ │ └── index.md │ │ │ │ ├── preview/ │ │ │ │ │ └── index.md │ │ │ │ ├── progress/ │ │ │ │ │ └── index.md │ │ │ │ ├── radar/ │ │ │ │ │ └── index.md │ │ │ │ ├── select/ │ │ │ │ │ └── index.md │ │ │ │ ├── skeleton/ │ │ │ │ │ └── index.md │ │ │ │ ├── tab/ │ │ │ │ │ └── index.md │ │ │ │ └── tabs/ │ │ │ │ └── index.md │ │ │ ├── ranuts/ │ │ │ │ ├── binary_tree/ │ │ │ │ │ └── index.md │ │ │ │ ├── bundler/ │ │ │ │ │ └── index.md │ │ │ │ ├── file/ │ │ │ │ │ ├── append_file.md │ │ │ │ │ ├── file_info.md │ │ │ │ │ ├── read_dir.md │ │ │ │ │ ├── read_file.md │ │ │ │ │ ├── watch_file.md │ │ │ │ │ └── write_file.md │ │ │ │ ├── index.md │ │ │ │ ├── mime_type/ │ │ │ │ │ └── mime_type.md │ │ │ │ ├── mode/ │ │ │ │ │ └── subscribe.md │ │ │ │ └── utils/ │ │ │ │ ├── add_class_to_element.md │ │ │ │ ├── add_num_sym.md │ │ │ │ ├── append_url.md │ │ │ │ ├── chain.md │ │ │ │ ├── change_hump_to_lower_case.md │ │ │ │ ├── check_encoding.md │ │ │ │ ├── clear_br.md │ │ │ │ ├── clear_str.md │ │ │ │ ├── clone_deep.md │ │ │ │ ├── compose.md │ │ │ │ ├── connection.md │ │ │ │ ├── convert_image_to_base64.md │ │ │ │ ├── create.md │ │ │ │ ├── create_document_fragment.md │ │ │ │ ├── create_object_url.md │ │ │ │ ├── create_signal.md │ │ │ │ ├── current_device.md │ │ │ │ ├── debounce.md │ │ │ │ ├── duration_handler.md │ │ │ │ ├── encode_url.md │ │ │ │ ├── escape_html.md │ │ │ │ ├── filter_obj.md │ │ │ │ ├── format_json.md │ │ │ │ ├── get_all_query_string.md │ │ │ │ ├── get_cookie.md │ │ │ │ ├── get_cookie_by_name.md │ │ │ │ ├── get_extensions.md │ │ │ │ ├── get_frame.md │ │ │ │ ├── get_host.md │ │ │ │ ├── get_matching_sentences.md │ │ │ │ ├── get_performance.md │ │ │ │ ├── get_pixel_ratio.md │ │ │ │ ├── get_query.md │ │ │ │ ├── get_window.md │ │ │ │ ├── handle_console.md │ │ │ │ ├── handle_error.md │ │ │ │ ├── handle_fetch_hook.md │ │ │ │ ├── hex_to_rgb.md │ │ │ │ ├── image_request.md │ │ │ │ ├── index.md │ │ │ │ ├── is_bang_device.md │ │ │ │ ├── is_client.md │ │ │ │ ├── is_equal.md │ │ │ │ ├── is_image_size.md │ │ │ │ ├── is_mobile.md │ │ │ │ ├── is_safari.md │ │ │ │ ├── is_string.md │ │ │ │ ├── is_weixin.md │ │ │ │ ├── local_storage.md │ │ │ │ ├── mathjs.md │ │ │ │ ├── md5.md │ │ │ │ ├── memoize.md │ │ │ │ ├── merge.md │ │ │ │ ├── network_speed.md │ │ │ │ ├── noop.md │ │ │ │ ├── ocr.md │ │ │ │ ├── per_to_num.md │ │ │ │ ├── performance_time.md │ │ │ │ ├── querystring.md │ │ │ │ ├── random_color.md │ │ │ │ ├── random_string.md │ │ │ │ ├── range.md │ │ │ │ ├── remove_class_to_element.md │ │ │ │ ├── remove_ghosting.md │ │ │ │ ├── retain.md │ │ │ │ ├── rgb_to_hex.md │ │ │ │ ├── script_on_load.md │ │ │ │ ├── set_attribute_by_global.md │ │ │ │ ├── set_mime.md │ │ │ │ ├── str2xml.md │ │ │ │ ├── str_parse.md │ │ │ │ ├── sync_hook.md │ │ │ │ ├── task.md │ │ │ │ ├── throttle.md │ │ │ │ ├── time_format.md │ │ │ │ ├── timestamp_to_time.md │ │ │ │ ├── to_string.md │ │ │ │ ├── totp.md │ │ │ │ ├── transform_number.md │ │ │ │ └── transform_text.md │ │ │ └── types/ │ │ │ ├── TS类型.md │ │ │ ├── 模式匹配.md │ │ │ ├── 类型运算.md │ │ │ └── 高级类型.md │ │ ├── tsconfig.json │ │ ├── typings.d.ts │ │ ├── variable/ │ │ │ └── SERVICE_WORK_VERSION.ts │ │ └── vue/ │ │ └── loading.vue │ ├── im/ │ │ ├── app/ │ │ │ ├── controllers/ │ │ │ │ ├── home.ts │ │ │ │ ├── im.ts │ │ │ │ └── user.ts │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ ├── constant.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ ├── request.ts │ │ │ │ ├── response.ts │ │ │ │ └── vite.ts │ │ │ ├── router.config.ts │ │ │ ├── routes.ts │ │ │ └── types/ │ │ │ └── index.ts │ │ ├── bin/ │ │ │ └── dev.sh │ │ ├── client/ │ │ │ ├── app.tsx │ │ │ ├── assets/ │ │ │ │ └── base.css │ │ │ ├── client.tsx │ │ │ ├── components/ │ │ │ │ ├── backdrop/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ └── loading/ │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── lib/ │ │ │ │ ├── eventSource.ts │ │ │ │ ├── hook.ts │ │ │ │ └── index.ts │ │ │ ├── pages/ │ │ │ │ └── home/ │ │ │ │ └── index.tsx │ │ │ ├── router/ │ │ │ │ └── index.tsx │ │ │ └── server.tsx │ │ ├── package.json │ │ ├── readme.md │ │ ├── rest/ │ │ │ └── user.rest │ │ ├── tsconfig.json │ │ ├── typings.d.ts │ │ ├── views/ │ │ │ └── index.html │ │ └── vite.config.ts │ ├── image-process/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── CMakeUserPresets.json │ │ ├── conanfile.txt │ │ ├── index.ts │ │ ├── main.c │ │ ├── makefile │ │ ├── package.json │ │ ├── readme.md │ │ └── src/ │ │ ├── image_processing.c │ │ └── image_processing.h │ ├── ml/ │ │ ├── assets/ │ │ │ ├── dataset/ │ │ │ │ ├── ChnSentiCorp_htl_all.csv │ │ │ │ ├── kc_house_data.csv │ │ │ │ └── min_label_review.csv │ │ │ └── ebook/ │ │ │ └── txt/ │ │ │ ├── JaneEyre.txt │ │ │ ├── TheHunchbackofNotre-Dame.txt │ │ │ ├── camelXiangzi.txt │ │ │ ├── princekin.txt │ │ │ ├── shakespeare.txt │ │ │ ├── snowWhite.txt │ │ │ ├── theThreeKingdoms.txt │ │ │ ├── theWealthOfNations.txt │ │ │ ├── training_data_1748151735437.json │ │ │ └── walden.txt │ │ ├── bin/ │ │ │ └── build.sh │ │ ├── client/ │ │ │ ├── components/ │ │ │ │ ├── BinaryModel.tsx │ │ │ │ ├── Emotion.tsx │ │ │ │ ├── LineModel.tsx │ │ │ │ └── MultiModel.tsx │ │ │ ├── index.tsx │ │ │ └── lib.ts │ │ ├── config/ │ │ │ ├── build.es.ts │ │ │ └── build.umd.ts │ │ ├── index.html │ │ ├── index.ts │ │ ├── package.json │ │ ├── readme.md │ │ ├── server/ │ │ │ └── index.ts │ │ ├── src/ │ │ │ └── ebook/ │ │ │ ├── index.ts │ │ │ └── trainData.ts │ │ ├── tsconfig.json │ │ ├── typings.d.ts │ │ └── vite.config.ts │ ├── precss/ │ │ └── sass/ │ │ ├── _base.scss │ │ ├── _function.scss │ │ └── styles/ │ │ ├── animation.scss │ │ ├── common.scss │ │ ├── global.css │ │ ├── mixin/ │ │ │ ├── bangs.scss │ │ │ ├── ellipsis.scss │ │ │ ├── index.scss │ │ │ ├── make-animation.scss │ │ │ ├── px2rem.scss │ │ │ ├── screen.scss │ │ │ ├── supports-safe-area-insets.scss │ │ │ └── thinBorder.scss │ │ ├── themes.scss │ │ └── variables.scss │ ├── ranite/ │ │ ├── bin/ │ │ │ └── ranite │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── client/ │ │ │ │ └── client.ts │ │ │ └── node/ │ │ │ ├── ModuleGraph.ts │ │ │ ├── cli.ts │ │ │ ├── constants.ts │ │ │ ├── hmr.ts │ │ │ ├── optimizer/ │ │ │ │ ├── index.ts │ │ │ │ ├── preBundlePlugin.ts │ │ │ │ └── scanPlugin.ts │ │ │ ├── plugin.ts │ │ │ ├── pluginContainer.ts │ │ │ ├── plugins/ │ │ │ │ ├── assets.ts │ │ │ │ ├── clientInject.ts │ │ │ │ ├── css.ts │ │ │ │ ├── esbuild.ts │ │ │ │ ├── importAnalysis.ts │ │ │ │ ├── index.ts │ │ │ │ ├── react-hmr.ts │ │ │ │ └── resolve.ts │ │ │ ├── server/ │ │ │ │ ├── index.ts │ │ │ │ └── middlewares/ │ │ │ │ ├── indexHtml.ts │ │ │ │ ├── static.ts │ │ │ │ └── transform.ts │ │ │ ├── server.ts │ │ │ ├── utils.ts │ │ │ └── ws.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── ranui/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.zh-CN.md │ │ ├── assets/ │ │ │ ├── image/ │ │ │ │ └── failImage.ts │ │ │ └── js/ │ │ │ ├── hls.js │ │ │ └── katex/ │ │ │ ├── katex-es-core.js │ │ │ └── katex-es.js │ │ ├── babel.config.cjs │ │ ├── base.less │ │ ├── bin/ │ │ │ ├── build.sh │ │ │ └── test.sh │ │ ├── build/ │ │ │ ├── config.es.ts │ │ │ ├── config.sha.ts │ │ │ ├── config.ts │ │ │ └── config.umd.ts │ │ ├── components/ │ │ │ ├── button/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── checkbox/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── colorpicker/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── dropdown/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── form/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── icon/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── image/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── input/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── loading/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── math/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── message/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── modal/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── player/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── popover/ │ │ │ │ ├── content/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.ts │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── progress/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── radar/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── scratch/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── select/ │ │ │ │ ├── dropdown-item/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.ts │ │ │ │ ├── index.less │ │ │ │ ├── index.ts │ │ │ │ └── option/ │ │ │ │ └── index.ts │ │ │ ├── skeleton/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── tab/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ └── tabpane/ │ │ │ ├── index.less │ │ │ └── index.ts │ │ ├── demo/ │ │ │ └── react.tsx │ │ ├── index.html │ │ ├── index.ts │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── plugins/ │ │ │ ├── auto-import-file.ts │ │ │ ├── load-style.ts │ │ │ └── load-svg.ts │ │ ├── readme.md │ │ ├── rollup.d.ts │ │ ├── shadowless/ │ │ │ ├── icon/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ ├── input/ │ │ │ │ ├── index.less │ │ │ │ └── index.ts │ │ │ └── select/ │ │ │ ├── index.less │ │ │ ├── index.ts │ │ │ └── option/ │ │ │ └── index.ts │ │ ├── shadowless.ts │ │ ├── tests/ │ │ │ └── button.spec.ts │ │ ├── theme/ │ │ │ ├── color.less │ │ │ ├── font.less │ │ │ └── index.less │ │ ├── tsconfig.json │ │ ├── typings.d.ts │ │ ├── utils/ │ │ │ ├── color.ts │ │ │ ├── image.ts │ │ │ ├── index.ts │ │ │ ├── math.ts │ │ │ └── tween.ts │ │ └── vite.config.ts │ ├── ranuts/ │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.zh-CN.md │ │ ├── assets/ │ │ │ └── wasm/ │ │ │ └── word/ │ │ │ ├── LICENSE_APACHE │ │ │ ├── LICENSE_MIT │ │ │ ├── wasmjieba-web.d.ts │ │ │ ├── wasmjieba-web.js │ │ │ ├── wasmjieba-web_bg.wasm │ │ │ └── wasmjieba-web_bg.wasm.d.ts │ │ ├── bin/ │ │ │ ├── build.sh │ │ │ └── test.sh │ │ ├── build/ │ │ │ ├── build.es.ts │ │ │ ├── build.umd.ml.ts │ │ │ ├── build.umd.node.ts │ │ │ ├── build.umd.ts │ │ │ ├── build.umd.utils.ts │ │ │ └── build.umd.wasm.ts │ │ ├── docs/ │ │ │ ├── binaryTree/ │ │ │ │ └── index.md │ │ │ ├── file/ │ │ │ │ └── index.md │ │ │ ├── index.md │ │ │ ├── sort/ │ │ │ │ ├── bubble/ │ │ │ │ │ └── index.md │ │ │ │ ├── bucket/ │ │ │ │ │ └── index.md │ │ │ │ ├── count/ │ │ │ │ │ └── index.md │ │ │ │ ├── heap/ │ │ │ │ │ └── index.md │ │ │ │ ├── index.md │ │ │ │ ├── insert/ │ │ │ │ │ └── index.md │ │ │ │ ├── merge/ │ │ │ │ │ └── index.md │ │ │ │ ├── quick/ │ │ │ │ │ └── index.md │ │ │ │ ├── radix/ │ │ │ │ │ └── index.md │ │ │ │ └── select/ │ │ │ │ └── index.md │ │ │ └── utils/ │ │ │ ├── compose.md │ │ │ ├── index.md │ │ │ └── task.md │ │ ├── examples/ │ │ │ ├── clone-deep-example.ts │ │ │ └── is-equal-example.ts │ │ ├── index.html │ │ ├── index.ts │ │ ├── package.json │ │ ├── plugins/ │ │ │ └── vite-plugins-banner.ts │ │ ├── readme.md │ │ ├── rollup.d.ts │ │ ├── src/ │ │ │ ├── arithmetic/ │ │ │ │ └── index.ts │ │ │ ├── ml/ │ │ │ │ ├── index.ts │ │ │ │ └── ocr.ts │ │ │ ├── node/ │ │ │ │ ├── appendFile.ts │ │ │ │ ├── body.ts │ │ │ │ ├── color.ts │ │ │ │ ├── command.ts │ │ │ │ ├── ctx2req.ts │ │ │ │ ├── fileInfo.ts │ │ │ │ ├── fs.ts │ │ │ │ ├── get.ts │ │ │ │ ├── getIPAdress.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isColorSupported.ts │ │ │ │ ├── paresUrl.ts │ │ │ │ ├── readDir.ts │ │ │ │ ├── readFile.ts │ │ │ │ ├── router.ts │ │ │ │ ├── send.ts │ │ │ │ ├── server.ts │ │ │ │ ├── startTask.ts │ │ │ │ ├── stream.ts │ │ │ │ ├── taskEnd.ts │ │ │ │ ├── traverse.ts │ │ │ │ ├── watchFile.ts │ │ │ │ ├── websocket.ts │ │ │ │ ├── writeFile.ts │ │ │ │ └── ws.ts │ │ │ ├── optimize/ │ │ │ │ ├── index.ts │ │ │ │ └── promise.ts │ │ │ ├── ran/ │ │ │ │ ├── build-react.md │ │ │ │ ├── commit.ts │ │ │ │ ├── dom.ts │ │ │ │ ├── hooks.ts │ │ │ │ ├── index.ts │ │ │ │ ├── min.ts │ │ │ │ ├── reconcile.ts │ │ │ │ └── schedule.ts │ │ │ ├── sort/ │ │ │ │ ├── bubble.ts │ │ │ │ ├── bucket.ts │ │ │ │ ├── count.ts │ │ │ │ ├── heap.ts │ │ │ │ ├── index.ts │ │ │ │ ├── insert.ts │ │ │ │ ├── merge.ts │ │ │ │ ├── quick.ts │ │ │ │ ├── radix.ts │ │ │ │ ├── randomArray.ts │ │ │ │ ├── select.ts │ │ │ │ └── shell.ts │ │ │ ├── utils/ │ │ │ │ ├── audioRecorder.ts │ │ │ │ ├── behavior.ts │ │ │ │ ├── bom.ts │ │ │ │ ├── color.ts │ │ │ │ ├── compose.ts │ │ │ │ ├── console.ts │ │ │ │ ├── debounce.ts │ │ │ │ ├── device.ts │ │ │ │ ├── dom.ts │ │ │ │ ├── error.ts │ │ │ │ ├── func.ts │ │ │ │ ├── img.ts │ │ │ │ ├── index.ts │ │ │ │ ├── memoize.ts │ │ │ │ ├── mimeType.ts │ │ │ │ ├── monitor.ts │ │ │ │ ├── network.ts │ │ │ │ ├── noop.ts │ │ │ │ ├── number.ts │ │ │ │ ├── obj.ts │ │ │ │ ├── performance.ts │ │ │ │ ├── queue.ts │ │ │ │ ├── report.ts │ │ │ │ ├── request.ts │ │ │ │ ├── script.ts │ │ │ │ ├── signal.ts │ │ │ │ ├── storage.ts │ │ │ │ ├── str.ts │ │ │ │ ├── subscribe.ts │ │ │ │ ├── throttle.ts │ │ │ │ ├── time.ts │ │ │ │ ├── totp/ │ │ │ │ │ ├── sha/ │ │ │ │ │ │ ├── LICENSE │ │ │ │ │ │ ├── common.ts │ │ │ │ │ │ ├── converters.ts │ │ │ │ │ │ ├── custom_types.ts │ │ │ │ │ │ ├── license_header.es3.txt │ │ │ │ │ │ ├── license_header.es6.txt │ │ │ │ │ │ ├── primitives_32.ts │ │ │ │ │ │ ├── primitives_64.ts │ │ │ │ │ │ ├── sha.ts │ │ │ │ │ │ ├── sha1.ts │ │ │ │ │ │ ├── sha256.ts │ │ │ │ │ │ ├── sha3.ts │ │ │ │ │ │ └── sha512.ts │ │ │ │ │ └── totp.ts │ │ │ │ └── visual/ │ │ │ │ ├── application.ts │ │ │ │ ├── demo/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── nodeHierarchy.html │ │ │ │ │ └── sankey.ts │ │ │ │ ├── enums.ts │ │ │ │ ├── event/ │ │ │ │ │ ├── boundary.ts │ │ │ │ │ ├── event.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── graphics/ │ │ │ │ │ ├── graphics.ts │ │ │ │ │ ├── graphicsData.ts │ │ │ │ │ ├── graphicsGeometry.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── math/ │ │ │ │ │ ├── bezier.ts │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── matrix.ts │ │ │ │ │ └── transform.ts │ │ │ │ ├── render/ │ │ │ │ │ ├── batchRenderer.ts │ │ │ │ │ ├── canvasRenderer.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── render.ts │ │ │ │ │ ├── utils/ │ │ │ │ │ │ ├── batch/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── float.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── verticy.ts │ │ │ │ │ │ ├── webgl/ │ │ │ │ │ │ │ ├── batchPool.ts │ │ │ │ │ │ │ ├── initShader.ts │ │ │ │ │ │ │ └── shaders.ts │ │ │ │ │ │ └── webgpu/ │ │ │ │ │ │ └── shaders.ts │ │ │ │ │ ├── webGPURenderer.ts │ │ │ │ │ └── webGlRenderer.ts │ │ │ │ ├── shape/ │ │ │ │ │ ├── circle.ts │ │ │ │ │ ├── ellipse.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── polygon.ts │ │ │ │ │ ├── rectangle.ts │ │ │ │ │ ├── roundedRectangle.ts │ │ │ │ │ └── shape.ts │ │ │ │ ├── style/ │ │ │ │ │ ├── fill.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── line.ts │ │ │ │ ├── types.ts │ │ │ │ └── vertex/ │ │ │ │ ├── container.ts │ │ │ │ ├── index.ts │ │ │ │ ├── point.ts │ │ │ │ └── vertex.ts │ │ │ ├── vnode/ │ │ │ │ ├── chainDom.ts │ │ │ │ ├── h.ts │ │ │ │ ├── hooks.ts │ │ │ │ ├── htmlDomApi.ts │ │ │ │ ├── init.ts │ │ │ │ ├── is.ts │ │ │ │ ├── modules/ │ │ │ │ │ ├── attributes.ts │ │ │ │ │ ├── class.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── listeners.ts │ │ │ │ │ ├── props.ts │ │ │ │ │ └── style.ts │ │ │ │ └── vnode.ts │ │ │ ├── wasm/ │ │ │ │ ├── index.ts │ │ │ │ └── word.ts │ │ │ └── wicket/ │ │ │ ├── bridge.ts │ │ │ └── index.ts │ │ ├── test/ │ │ │ ├── arithmetic.test.ts │ │ │ ├── cloneDeep.test.ts │ │ │ ├── encodeUrl.test.ts │ │ │ ├── escapeHtml.test.ts │ │ │ ├── is-equal.test.ts │ │ │ ├── mimeType.test.ts │ │ │ ├── server.test.ts │ │ │ ├── sort.test.ts │ │ │ ├── status.test.ts │ │ │ ├── totp.test.ts │ │ │ ├── utils/ │ │ │ │ ├── compose.test.ts │ │ │ │ ├── mergeObj.test.ts │ │ │ │ └── ocr.test.ts │ │ │ ├── visual/ │ │ │ │ └── math.test.ts │ │ │ ├── vnode/ │ │ │ │ ├── index.html │ │ │ │ ├── index.ts │ │ │ │ ├── server.ts │ │ │ │ └── vnode.ts │ │ │ ├── websocket.test.ts │ │ │ └── writeFile.test.ts │ │ ├── tsconfig.json │ │ ├── typings.d.ts │ │ └── vite.config.ts │ ├── rust/ │ │ ├── index.md │ │ └── start.md │ ├── solidity/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── bin/ │ │ │ ├── compile.sh │ │ │ ├── eth.sh │ │ │ └── test.sh │ │ ├── blockchain-data-structure.md │ │ ├── consensus.md │ │ ├── contracts/ │ │ │ └── Lock.sol │ │ ├── encryption.md │ │ ├── eth/ │ │ │ ├── doc/ │ │ │ │ ├── Foundry.md │ │ │ │ ├── ethers.md │ │ │ │ ├── introduce.md │ │ │ │ └── pragma.md │ │ │ └── project/ │ │ │ ├── crowdFund/ │ │ │ │ ├── contracts/ │ │ │ │ │ ├── CrowdFund.sol │ │ │ │ │ └── artifacts/ │ │ │ │ │ ├── CrowdFund.json │ │ │ │ │ ├── CrowdFund_metadata.json │ │ │ │ │ ├── IERC20.json │ │ │ │ │ ├── IERC20_metadata.json │ │ │ │ │ └── build-info/ │ │ │ │ │ └── 51a87644e167c2c0acbed68604ab9364.json │ │ │ │ └── readme.md │ │ │ ├── cryptoKitty/ │ │ │ │ ├── contracts/ │ │ │ │ │ └── cryptoKitty.sol │ │ │ │ └── readme.md │ │ │ ├── deploy.md │ │ │ └── fungibleToken/ │ │ │ ├── .states/ │ │ │ │ └── vm-shanghai/ │ │ │ │ └── state.json │ │ │ ├── contracts/ │ │ │ │ ├── FungibleToken.sol │ │ │ │ └── artifacts/ │ │ │ │ ├── FungibleToken.json │ │ │ │ ├── FungibleToken_metadata.json │ │ │ │ └── build-info/ │ │ │ │ └── 161b7ad96fda298680715448c4a2ef7f.json │ │ │ └── readme.md │ │ ├── hardhat.config.ts │ │ ├── ignition/ │ │ │ └── modules/ │ │ │ └── Lock.ts │ │ ├── index.md │ │ ├── package.json │ │ ├── pos.md │ │ ├── pow.md │ │ ├── program.md │ │ ├── remix.md │ │ ├── signature.md │ │ ├── start.md │ │ ├── test/ │ │ │ └── Lock.ts │ │ ├── tsconfig.json │ │ ├── wallet.md │ │ └── web3-wallet.md │ └── visual/ │ ├── bin/ │ │ └── build.sh │ ├── build/ │ │ ├── build.es.ts │ │ └── build.umd.ts │ ├── components/ │ │ ├── bar/ │ │ │ └── index.ts │ │ ├── line/ │ │ │ └── index.ts │ │ ├── pie/ │ │ │ └── index.ts │ │ └── sankey/ │ │ ├── data.ts │ │ └── index.ts │ ├── docs/ │ │ ├── demo.glsl │ │ ├── fourColor.glsl │ │ ├── glsl.md │ │ ├── index.md │ │ ├── random.md │ │ ├── red.glsl │ │ ├── start.md │ │ ├── texture.md │ │ ├── ux.md │ │ └── yellow.glsl │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── src/ │ │ ├── application.ts │ │ ├── enums.ts │ │ ├── event/ │ │ │ ├── boundry.ts │ │ │ ├── enums.ts │ │ │ ├── event.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── graphics/ │ │ │ ├── geometry.ts │ │ │ ├── graphics.ts │ │ │ ├── graphicsData.ts │ │ │ └── index.ts │ │ ├── render/ │ │ │ ├── canvasRender.ts │ │ │ ├── index.ts │ │ │ ├── render.ts │ │ │ ├── webGLRender.ts │ │ │ └── webGPURender.ts │ │ ├── shape/ │ │ │ ├── bezier.ts │ │ │ ├── circle.ts │ │ │ ├── ellipse.ts │ │ │ ├── enums.ts │ │ │ ├── fill.ts │ │ │ ├── index.ts │ │ │ ├── line.ts │ │ │ ├── polygon.ts │ │ │ ├── rectangle.ts │ │ │ ├── roundedRectangle.ts │ │ │ ├── shape.ts │ │ │ └── types.ts │ │ ├── transform/ │ │ │ ├── enums.ts │ │ │ ├── index.ts │ │ │ ├── matrix.ts │ │ │ ├── transform.ts │ │ │ └── types.ts │ │ ├── types.ts │ │ └── vertex/ │ │ ├── container.ts │ │ ├── index.ts │ │ ├── point.ts │ │ └── vertex.ts │ ├── tsconfig.json │ ├── typings.d.ts │ └── vite.config.ts ├── pnpm-workspace.yaml ├── readme-zh_CN.md └── readme.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly day: sunday time: "01:00" labels: - scope:dependency target-branch: "main" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: branches: [main] paths: - "packages/**" push: branches: [main] paths: - "packages/**" permissions: contents: read pull-requests: read jobs: lint: if: github.repository == 'chaxus/ran' timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] node_version: [20, 22] include: # Active LTS + other OS - os: macos-latest node_version: 22 - os: windows-latest node_version: 22 fail-fast: false name: "Build&Test: node-${{ matrix.node_version }}, ${{ matrix.os }}" steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 with: python-version: "3.10" - uses: pnpm/action-setup@v6 name: Install pnpm id: pnpm-install with: version: 7 run_install: false - name: Get pnpm store directory id: pnpm-cache shell: bash run: | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - name: prepare run: pnpm pre - name: Install deps run: pnpm install - name: build run: pnpm run build - name: format run: pnpm run format - name: Lint run: pnpm run lint - name: Test run: pnpm run test ================================================ FILE: .github/workflows/pages-build-site.yml ================================================ name: pages-build-site on: push: branches: - main permissions: contents: write pages: write id-token: write jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Install Node.js uses: actions/setup-node@v6 with: node-version: 20 - uses: pnpm/action-setup@v6 name: Install pnpm id: pnpm-install with: version: 7 run_install: false - name: Get pnpm store directory id: pnpm-cache shell: bash run: | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v5 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Build ranuts run: pnpm -F ranuts build - name: Build ranui run: pnpm -F ranui build - name: Build docs run: pnpm -F docs build - name: Deploy uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.RAN_ACTIONS_TOKEN }} exclude_assets: "" publish_dir: packages/docs/.vitepress/dist ================================================ FILE: .github/workflows/semantic-pull-request.yml ================================================ name: Semantic Pull Request on: pull_request_target: types: - opened - edited - synchronize permissions: pull-requests: write contents: read jobs: main: if: github.repository == 'chaxus/ran' runs-on: ubuntu-latest name: Semantic Pull Request steps: - name: Validate PR title uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.RAN_ACTIONS_TOKEN }} ================================================ FILE: .gitignore ================================================ # npm node_modules package-lock.json npminstall-debug.log # common .temp cache # mac .DS_Store # eslint .eslintcache # vite vite.config.ts.timestamp* config.ts.timestamp* dist # ranui /test-results/ /playwright-report/ /playwright/.cache/ # jscpd report # tesseract *.traineddata ================================================ FILE: .jscpd.json ================================================ { "threshold": 10, "reporters": ["html", "console"], "ignore": [ "**/__snapshots__/**", "**.config.ts", "**.md", "**.html", "tsconfig.json", "node_modules", "CMakeFiles", "**.txt" ], "absolute": true, "gitignore": true } ================================================ FILE: .prettierignore ================================================ dist/ LICENSE.md pnpm-lock.yaml pnpm-workspace.yaml report public assets ================================================ FILE: .prettierrc.json ================================================ { "semi": true, "tabWidth": 2, "singleQuote": true, "printWidth": 120, "trailingComma": "all", "overrides": [ { "files": ["*.json5"], "options": { "singleQuote": false, "quoteProps": "preserve" } }, { "files": ["*.yml"], "options": { "singleQuote": false } } ] } ================================================ FILE: .vscode/c_cpp_properties.json ================================================ { "configurations": [ { "name": "Mac", "includePath": [ "${workspaceFolder}/**", // 需要设置环境变量 .conan2 的路径,一般是在当前用户目录下,比如 /Users/chaxus/.conan2 // conan install 会将依赖库安装到这个目录下 // 其中 echo $chaxus 可以查看环境变量 chaxus 的值 // export chaxus=/path/to/chaxus 可以设置环境变量 chaxus 的值 "${env:chaxus}/.conan2/**", "/usr/local/include/**", // 将 Emscripten 的头文件路径添加到 includePath 中 "/opt/homebrew/Cellar/emscripten/3.1.74/libexec/system/include/**", "/opt/homebrew/include/**" ], "defines": [], "macFrameworkPath": ["/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks"], "compilerPath": "/usr/bin/clang", "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "macos-clang-x64", "configurationProvider": "ms-vscode.makefile-tools" } ], "version": 4 } ================================================ FILE: .vscode/launch.json ================================================ { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问:https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": ["/**"], "program": "${workspaceFolder}/packages/ranuts/test/write-file.spec.ts", "outFiles": ["${workspaceFolder}/**/*.js"] } ] } ================================================ FILE: .vscode/markdown.code-snippets ================================================ { // Place your ran 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. // Placeholders with the same ids are connected. // Example: // "Print to console": { // "scope": "javascript,typescript", // "prefix": "log", // "body": [ // "console.log('$1');", // "$2" // ], // "description": "Log output to console" // } "生成参考文献的角标": { "scope": "markdown", "prefix": "sup", "body": ["[[$2]]($1)。"], "description": "生成参考文献的角标", }, "生成链接": { "scope": "markdown", "prefix": "[]", "body": ["[$2]($1)。"], "description": "生成链接", }, "生成图片引用": { "scope": "markdown", "prefix": "[]", "body": ["![$2]($1)。"], "description": "生成图片引用", }, } ================================================ FILE: .vscode/ranui-components.code-snippets ================================================ { // Place your ran 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. // Placeholders with the same ids are connected. // Example: // "Print to console": { // "scope": "javascript,typescript", // "prefix": "log", // "body": [ // "console.log('$1');", // "$2" // ], // "description": "Log output to console" // } "print component": { "scope": "javascript,typescript", "prefix": "tfc", "body": [ "function Component() {", " const template = document.createElement('template');", " const slot = document.createElement('slot');", " const div = document.createElement('div');", " div.setAttribute('class', 'class');", " slot.setAttribute('name', 'name');", " div.appendChild(slot);", " template.appendChild(div);", " class CustomElement extends HTMLElement {", " static get observedAttributes() { return ['disabled'] }", " _div: HTMLElement;", " constructor() {", " super();", " this._div = div.cloneNode(true) as HTMLElement", " const shadowRoot = this.attachShadow({ mode: 'closed' });", " shadowRoot.appendChild(this._div);", " }", " connectedCallback() {", "", " }", " disconnectCallback() {", "", " }", " attributeChangedCallback (name:string, oldValue:string, newValue:string) {", "", " }", " }", "window.customElements.define('r-$1', CustomElement)", "}", "export default Component()", // $通过tab切换 ], "description": "ranui component template", }, "Print sort function": { "scope": "javascript,typescript", "prefix": "sf", "body": ["const $1 = ($2) => {", "", "}", "", "export default $1"], "description": "Log output to console", }, "Print to return": { "scope": "javascript,typescript", "prefix": "rt", "body": ["return"], "description": "Log output to console", }, "Print to for": { "scope": "javascript,typescript", "prefix": "for", "body": ["for(let i = 0; i < $1; i++) {", "", "}"], "description": "Log output to console", }, } ================================================ FILE: .vscode/settings.json ================================================ { "cSpell.words": [ "abilitys", "Alloc", "allowclear", "ANALYSE", "analysed", "binarystring", "bottombar", "brotli", "ccall", "chaxus", "classname", "colorpicker", "componentization", "cond", "cout", "Crossentropy", "cstring", "cwrap", "dataavailable", "denormalise", "Deoptimization", "DOMAPI", "dropdownclass", "embin", "Embind", "emcc", "Emscripten", "endl", "esbuild", "exceljs", "GLSL", "HEAPF", "HEAPU", "IIFE", "iostream", "katex", "KEEPALIVE", "keyof", "lembind", "lerp", "localstorage", "malloc", "maxlength", "maxrows", "memset", "Metiral", "minrows", "normalise", "normalised", "nullptr", "nums", "originjs", "Ovride", "pacman", "pagefind", "picocolors", "playsinline", "preinstall", "progressbar", "pthread", "PTHREADS", "RAII", "ranite", "rankey", "ranloadedmetadata", "ranlog", "ranml", "ranpack", "ranui", "ranuireact", "ranuts", "Reactified", "Sankey", "scatterplot", "showcount", "simplefy", "skipsvgo", "slotchange", "sqft", "struct", "tabpane", "tailwindcss", "tesseract", "tfjs", "tfvis", "TOTP", "treeshake", "Treeshaking", "TTFB", "typeof", "vitepress", "vnode", "vnodes", "wasmjieba", "xlabels" ], "liveServer.settings.port": 5556, "files.associations": { "*.cjson": "jsonc", "*.wxss": "css", "*.wxs": "javascript", "vips.h": "c", "stdio.h": "c", "gmodule.h": "c", "glib.h": "c", "image_processing.h": "c" }, "cmake.ignoreCMakeListsMissing": true } ================================================ FILE: .vscode/ts.json.code-snippets ================================================ { // Place your ran 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. // Placeholders with the same ids are connected. // Example: // "Print to console": { // "scope": "javascript,typescript", // "prefix": "log", // "body": [ // "console.log('$1');", // "$2" // ], // "description": "Log output to console" // } "Print to console": { "scope": "javascript,typescript", "prefix": "clg", "body": ["console.log('$1');"], "description": "Log output to console", }, "web components class": { "scope": "typescript", "prefix": "wc", "body": [ "import { isDisabled } from '@/utils/index';", "", "class Option extends HTMLElement {", " _option: HTMLDivElement;", " _optionContent: HTMLDivElement;", " _shadowDom: ShadowRoot;", " _slot: HTMLSlotElement;", " static get observedAttributes() {", " return ['disabled', 'sheet', 'value'];", " }", " constructor() {", " super();", " this._slot = document.createElement('slot');", " this._option = document.createElement('div');", " this._optionContent = document.createElement('div');", " this._optionContent.appendChild(this._slot);", " this._option.appendChild(this._optionContent);", " const shadowRoot = this.attachShadow({ mode: 'closed' });", " this._shadowDom = shadowRoot;", " shadowRoot.appendChild(this._option);", " }", " get sheet() {", " return this.getAttribute('sheet');", " }", " set sheet(value) {", " this.setAttribute('sheet', value || '');", " }", " get disabled() {", " return isDisabled(this);", " }", " set disabled(value: boolean | string | undefined | null) {", " if (!value || value === 'false') {", " this.removeAttribute('disabled');", " } else {", " this.setAttribute('disabled', '');", " }", " }", " handlerExternalCss() {", " if (this.sheet) {", " try {", " const sheet = new CSSStyleSheet();", " sheet.insertRule(this.sheet);", " this._shadowDom.adoptedStyleSheets = [sheet];", " } catch (error) {", " console.error(", " `Failed to parse the rule in CSSStyleSheet: ${this.sheet}`", " );", " }", " }", " }", " connectedCallback() {}", " disconnectCallback() {}", " attributeChangedCallback(name: string, oldValue: string, newValue: string) {", " if (name === 'disabled' && this._option) {", " if (!newValue || newValue === 'false') {", " this._option.setAttribute('disabled', '');", " } else {", " this._option.removeAttribute('disabled');", " }", " }", " if (name === 'sheet' && this._shadowDom && oldValue !== newValue)", " this.handlerExternalCss();", " }", "}", ], "description": "generate web components ts code", }, } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 chaxus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Use this section to tell people about which versions of your project are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 5.1.x | :white_check_mark: | | 5.0.x | :x: | | 4.0.x | :white_check_mark: | | < 4.0 | :x: | ## Reporting a Vulnerability Use this section to tell people how to report a vulnerability. Tell them where to go, how often they can expect to get an update on a reported vulnerability, what to expect if the vulnerability is accepted or declined, etc. ================================================ FILE: bin/build.sh ================================================ npx pnpm -F ranuts build && pnpm -F ranui build && pnpm -F docs build ================================================ FILE: bin/clean.sh ================================================ rm -rf $(pnpm store path) rm -rf node_modules rm -rf packages/ranui/node_modules/ rm -rf packages/ranuts/node_modules/ rm -rf packages/docs/node_modules/ rm -rf packages/ranite/node_modules/ rm -rf packages/ml/node_modules/ npm cache clean --force rm -rf pnpm-lock.yaml ================================================ FILE: bin/install.sh ================================================ # 子package安装 pnpm i [package] -r --filter smarty-ui-vite # 或者 直接在 ranui 目录下 pnpm i [package] # 安装 workspace 中 pnpm i [package] -w # 检查只能用pnpm安装,用于workspace `"preinstall": "npx only-allow pnpm"` # Monorepo ================================================ FILE: bin/lint.sh ================================================ bin=./node_modules/.bin $bin/prettier --check --cache . $bin/eslint --cache . && $bin/prettier --check --cache . # before lint, make sure all packages are built # pnpm build pnpm -F docs tsc pnpm -F image-process tsc pnpm -F ml tsc pnpm -F ranui tsc pnpm -F ranuts tsc pnpm -F solidity tsc # pnpm -F read tsc # $bin/tsc --noEmit # npm run lint:es && npm run lint:prettier ================================================ FILE: bin/nvm.sh ================================================ nvm alias default node nvm use node ================================================ FILE: bin/preinstall.sh ================================================ # @tensorflow/tfjs-node in github actions python3 --version node --version ================================================ FILE: bin/test.sh ================================================ pnpm -F docs tsc pnpm -F image-process tsc pnpm -F ml tsc pnpm -F ranui tsc pnpm -F ranuts tsc pnpm -F solidity tsc pnpm -F ranuts test pnpm -F ranuts build # pnpm -F ranui test ================================================ FILE: eslint.config.js ================================================ // @ts-check import { builtinModules } from 'node:module'; import eslint from '@eslint/js'; import pluginN from 'eslint-plugin-n'; import pluginImportX from 'eslint-plugin-import-x'; import pluginRegExp from 'eslint-plugin-regexp'; import tseslint from 'typescript-eslint'; import globals from 'globals'; // Some rules work better with typechecking enabled, but as enabling it is slow, // we only do so when linting in IDEs for now. If you want to lint with typechecking // explicitly, set this to `true` manually. const shouldTypeCheck = typeof process.env.VSCODE_PID === 'string'; export default tseslint.config( { ignores: [ 'packages/solidity/**', '**/dist/**', '**/report/**', '**/assets/**', '**/temp/**', '**/public/**', '**/cache/**', '**/*.snap', ], }, eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.stylistic, pluginRegExp.configs['flat/recommended'], { name: 'main', languageOptions: { parser: tseslint.parser, parserOptions: { sourceType: 'module', ecmaVersion: 2022, project: shouldTypeCheck ? ['./packages/*/tsconfig.json', './packages/vite/src/*/tsconfig.json'] : undefined, }, globals: { ...globals.es2021, ...globals.node, }, }, plugins: { n: pluginN, 'import-x': pluginImportX, }, rules: { 'n/no-exports-assign': 'error', 'n/no-unpublished-bin': 'error', 'n/no-unsupported-features/es-builtins': 'error', 'n/no-unsupported-features/node-builtins': 'error', 'n/process-exit-as-throw': 'error', '@typescript-eslint/no-unused-expressions': 'off', '@typescript-eslint/no-namespace': 'off', 'n/hashbang': 'error', eqeqeq: 'off', 'no-debugger': ['error'], 'no-empty': ['warn', { allowEmptyCatch: true }], 'no-process-exit': 'off', 'prefer-const': [ 'warn', { destructuring: 'all', }, ], 'n/no-missing-require': [ 'error', { // for try-catching yarn pnp allowModules: ['pnpapi', 'vite'], tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], }, ], 'n/no-extraneous-import': [ 'error', { allowModules: ['vite', 'less', 'sass', 'sass-embedded', 'lightningcss', 'vitest', 'unbuild'], }, ], 'n/no-extraneous-require': [ 'error', { allowModules: ['vite'], }, ], '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/no-unsafe-function-type': 'off', '@typescript-eslint/explicit-module-boundary-types': ['error', { allowArgumentsExplicitlyTypedAsAny: true }], '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }], '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-explicit-any': 'off', 'no-extra-semi': 'off', '@typescript-eslint/no-extra-semi': 'off', // conflicts with prettier '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { args: 'all', argsIgnorePattern: '^_', caughtErrors: 'all', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true, }, ], '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/consistent-type-imports': [ 'error', { prefer: 'type-imports', disallowTypeAnnotations: false }, ], // disable rules set in @typescript-eslint/stylistic which conflict with current code // we should discuss if we want to enable these as they encourage consistent code '@typescript-eslint/array-type': 'off', '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/prefer-for-of': 'off', '@typescript-eslint/prefer-function-type': 'off', 'import-x/no-nodejs-modules': ['error', { allow: builtinModules.map((mod) => `node:${mod}`) }], 'import-x/no-duplicates': 'error', 'import-x/order': 'error', 'sort-imports': [ 'error', { ignoreCase: false, ignoreDeclarationSort: true, ignoreMemberSort: false, memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], allowSeparatedGroups: false, }, ], 'regexp/prefer-regexp-exec': 'error', 'regexp/prefer-regexp-test': 'error', // in some cases using explicit letter-casing is more performant than the `i` flag 'regexp/use-ignore-case': 'off', }, }, { name: 'disables/playground', files: ['playground/**/*.?([cm])[jt]s?(x)', 'docs/**/*.?([cm])[jt]s?(x)'], rules: { 'n/no-extraneous-import': 'off', 'n/no-extraneous-require': 'off', 'n/no-missing-import': 'off', 'n/no-missing-require': 'off', 'n/no-unsupported-features/es-builtins': 'off', 'n/no-unsupported-features/node-builtins': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-expressions': 'off', '@typescript-eslint/no-unused-vars': 'off', 'no-undef': 'off', 'no-empty': 'off', 'no-constant-condition': 'off', '@typescript-eslint/no-empty-function': 'off', }, }, { name: 'disables/playground/tsconfig-json', files: [ 'playground/tsconfig-json/**/*.?([cm])[jt]s?(x)', 'playground/tsconfig-json-load-error/**/*.?([cm])[jt]s?(x)', ], ignores: ['**/__tests__/**'], rules: { '@typescript-eslint/ban-ts-comment': 'off', }, }, { name: 'disables/js', files: ['**/*.js', '**/*.mjs', '**/*.cjs'], rules: { '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, { name: 'disables/dts', files: ['**/*.d.ts'], rules: { '@typescript-eslint/consistent-indexed-object-style': 'off', '@typescript-eslint/triple-slash-reference': 'off', }, }, { name: 'disables/test', files: ['**/__tests__/**/*.?([cm])[jt]s?(x)'], rules: { 'no-console': 'off', '@typescript-eslint/ban-ts-comment': 'off', }, }, { name: 'disables/typechecking', files: [ '**/*.js', '**/*.mjs', '**/*.cjs', '**/*.d.ts', '**/*.d.cts', '**/__tests__/**', 'docs/**', 'playground/**', 'scripts/**', 'vitest.config.ts', ], languageOptions: { parserOptions: { project: false, }, }, }, ); ================================================ FILE: index.html ================================================ Sankey Diagram with Canvas ================================================ FILE: package.json ================================================ { "name": "@ran/monorepo", "version": "1.0.0", "description": "Monorepo", "type": "module", "engines": { "node": ">=23.10.0" }, "scripts": { "pre": "sh ./bin/preinstall.sh", "preinstall": "npx only-allow pnpm", "clean": "sh ./bin/clean.sh", "build": "sh ./bin/build.sh", "test": "sh ./bin/test.sh", "lint": "sh ./bin/lint.sh", "lint:es": "eslint --cache .", "lint:fix": "eslint --cache --fix", "lint:prettier": "prettier --check --cache .", "format": "prettier --write --cache .", "cpd": "jscpd ./packages" }, "homepage": "https://chaxus.github.io/ran/", "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", "@typescript-eslint/parser": "^8.46.4", "eslint": "^9.39.1", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-n": "^17.23.1", "eslint-plugin-regexp": "^2.10.0", "jscpd": "^4.0.5", "prettier": "^3.6.2", "terser": "^5.44.1", "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", "vite": "^6.4.1", "vite-plugin-dts": "^4.5.4", "vitest": "^4.0.9", "vue": "^3.5.24", "vue-tsc": "^3.1.3" }, "dependencies": { "globals": "^16.5.0", "ranui": "0.1.10-alpha-27" }, "pnpm": { "ignoredBuiltDependencies": [ "fsevents", "keccak", "secp256k1" ] } } ================================================ FILE: packages/cpro/package.json ================================================ { "name": "cpro", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } ================================================ FILE: packages/cpro/readme.md ================================================ # C++ 如果要为 macOS 进行 C++ 开发,建议安装 Clang 编译器。只需在“终端”窗口中运行以下命令即可安装命令行开发人员工具: ```sh xcode-select --install ``` 然后,要验证已安装 clang,请在“终端”窗口中运行以下命令。你应该会看到一条消息,其中包含有关所使用的 Clang 版本的信息。 ```sh clang --version ``` ```c++ #include using namespace std; int main(){ cout << "Hello~" << endl; return 0; } ``` 命令行执行: ```sh clang++ main.cpp # if meet some error like this: error: non-aggregate type 'vector' cannot be initialized with an initializer list # g++ -std=c++11 ./a.out ``` # WebAssembly 官网: Mac 安装 `Emscripten` ```sh brew install emscripten ``` 安装完成后测试是否存在 ```sh emcc -v ``` 编译 `C++` 代码 ```sh emcc main.cpp ``` 命令成功执行后,你会发现 `main.cpp` 同级目录下会生成两个文件:`a.out.wasm` 和 `a.out.js`。其中,`a.out.wasm` 就是我们想要的 `Wasm` 模块。 ```sh node a.out.js ``` `Node.js` 是支持 `Wasm` 模块的(自 `Node.js 8` 开始)。 如果你希望到浏览器环境下测试文件,那么你可以在命令行下使用如下指令编译我们的 `C++` 代码: ```sh emcc main.cpp -o main.html ``` 命令成功执行后,`main.cpp` 同级目录下会再多出三个文件:`main.wasm`、`main.js` 和 `main.html`。你可以用`Live Server`打开 `main.html` 文件,就能看到结果了: ![](./assets//images/emsc-web-preview.jpg) ## 调试 WebAssembly 我们可以在编译 `WebAssembly` 的时候加入 `-v` 参数来让 `Emscripten` 为我们输出更多编译信息,以辅助我们发现 `C++` 代码中的问题。 如果你想在运行期调试 `WebAssembly` 的代码,那么你需要使用如下指令重新编译 `WebAssembly` 模块以让你的 `.wasm` 文件包含调试信息: ```sh emcc -g main.cpp -o main.html ``` 参数解释: - `-g`: 编译 + 链接,同时保留调试信息。当编译为目标文件时,这与 Clang 和 gcc 中的相同,它将 DWARF 调试信息添加到目标文件中。 - `-o`: 链接可执行文件时,目标文件扩展名定义要生成的输出类型,默认是 `JavaScript`。如果指定是`html`,会生成一个`html`加`JavaScript`的可运行文件。 接着你需要为你的谷歌浏览器安装一个插件 [`C/C++ DevTools Support (DWARF)`](https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb) ,这是 `Chrome DevTools` 开发团队为开发者提供的专门用于调试 `WebAssembly` 的浏览器插件,注意安装过程中要保持良好的网络环境。 ![](./assets/images/emsc-chrome-tools.jpg) 安装完成后,打开 `Chrome` 浏览器的调试工具 `DevTools`,点击设置按钮(⚙),打开调试工具的设置面板,勾选实验选项卡下的 `WebAssembly Debugging: Enable DWARF support` 选项。 ![](./assets/images/emsc-chrome-setting.jpg) 接着退回到 `DevTools` 主面板,把你的源码路径添加到 `DevTools` 的文件系统中,然后在 `main.cpp` 中下一个断点,刷新 `main.html` 页面,你会发现刚刚下的断点已经命中了,而且调用堆栈也会显示在 `DevTools` 右侧的面板中,如下图所示: ![](./assets/images/emsc-chrome-debug.jpg) ## C++ 与 JS 的交互 ### ccall 如果需要调用一个在 C 语言自定义的函数,你可以使用 Emscripten 中的 ccall() 函数,以及 EMSCRIPTEN_KEEPALIVE 声明 > Emscripten 会消除未从编译代码中调用的函数的死代码。虽然这确实最大限度地减少了代码大小,但它可以删除您计划自己调用的函数(在已编译的代码之外)。比如通过 EMSCRIPTEN_KEEPALIVE 和 EXPORTED_FUNCTIONS 去指定需要导出的函数。EMSCRIPTEN_KEEPALIVE 的作用是告诉编译器和链接器保留符号并将其导出,就像将其添加到 EXPORTED_FUNCTIONS 中一样。 默认情况下,Emscripten 生成的代码只会调用 main() 函数,其他的函数将被视为无用代码。在一个函数名之前添加 EMSCRIPTEN_KEEPALIVE 能够防止这样的事情发生。你需要导入 emscripten.h 库来使用 EMSCRIPTEN_KEEPALIVE。 EMSCRIPTEN_KEEPALIVE: ```c++ // EMSCRIPTEN_KEEPALIVE void EMSCRIPTEN_KEEPALIVE my_function() { printf("I am being kept alive\n"); } ``` EXPORTED_FUNCTIONS: ```sh # EXPORTED_FUNCTIONS emcc -sEXPORTED_FUNCTIONS=_main,_my_func ... ``` > 如果您有 main() 函数,则\_main 应该位于导出列表中,如该示例所示。否则,它将作为死代码被删除;默认情况下没有特殊的逻辑来保持 main() 的活动。 ```html ``` [](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html) ```sh emcc -v main.cpp -o main.html -sEXPORTED_FUNCTIONS=_quick_sort,_main -sEXPORTED_RUNTIME_METHODS=ccall,cwrap ``` EXPORTED_RUNTIME_METHODS 是 Emscripten 中一个用于指定哪些运行时方法可以在生成的 JavaScript 文件中被访问的选项。 通过设置 EXPORTED_RUNTIME_METHODS,你可以控制哪些 Emscripten 运行时的 API 函数被导出,使其可以从生成的 JavaScript 文件中访问和调用。这对于需要在 JavaScript 中调用特定 Emscripten 提供的功能非常有用。 假设你有一个简单的 C 文件 hello.c: ```c++ #include void say_hello() { printf("Hello, World!\n"); } ``` 你可以使用以下命令编译这个文件,同时导出 cwrap 和 ccall 方法: ```c++ emcc hello.c -o hello.js -s EXPORTED_RUNTIME_METHODS=['cwrap','ccall'] ``` 在这个例子中,我们导出了 cwrap 和 ccall 方法,这两个方法在 Emscripten 中常用于从 JavaScript 调用 C 函数。生成的 hello.js 文件可以在浏览器中使用。 接下来,你可以在 JavaScript 中这样使用这些导出的方法: ```html Hello Emscripten ``` 在这个例子中,我们通过 Module.cwrap 方法创建了一个 JavaScript 包装函数 sayHello,并调用了它。由于 say_hello 是一个无返回值且无参数的 C 函数,因此我们在 cwrap 中传入了 null 和一个空数组 []。 使用 EXPORTED_RUNTIME_METHODS 可以确保 cwrap 和 ccall 方法在生成的 JavaScript 文件中被导出,并且可以在 JavaScript 中访问和使用这些方法。 ### Emscripten 运行时的 API 函数 Emscripten 运行时的 API 函数是指在生成的 JavaScript 文件中提供的用于与编译后的 WebAssembly 或 asm.js 代码交互的函数。Emscripten 提供了多种运行时 API 函数,方便开发者在 JavaScript 中调用 C/C++ 代码,处理内存,进行文件系统操作等。 下面是一些常用的 Emscripten 运行时 API 函数及其用途: #### 1. cwrap: - 用于创建一个 JavaScript 函数,该函数可以调用编译后的 WebAssembly 函数。 - 语法: ```js Module.cwrap('function_name', 'return_type', ['arg_type1', 'arg_type2', ...]) ``` #### 2. ccall: - 用于直接调用编译后的 WebAssembly 函数。 语法: ```js Module.ccall('function_name', 'return_type', ['arg_type1', 'arg_type2', ...], [arg1, arg2, ...]) ``` #### 3. UTF8ToString: - 将指向 UTF-8 编码字符串的指针转换为 JavaScript 字符串。 语法: ```js UTF8ToString(ptr); ``` #### 4. allocate: - 在 Emscripten 堆中分配内存。 语法: ```js Module._malloc(size); ``` #### 5. HEAP8 / HEAP16 / HEAP32 / HEAPU8 / HEAPU16 / HEAPU32 / HEAPF32 / HEAPF64: - 直接访问 Emscripten 堆内存的视图,允许读取和写入不同类型的数据。 例如:Module.HEAP32[ptr >> 2] 读取 ptr 位置的 32 位整数。 Emscripten 提供了一系列 API 函数和视图,用于直接访问和操作 WebAssembly 内存。这些 API 和视图在处理复杂的数据结构、字符串、数组等方面非常有用。下面是 allocate 和各种 HEAP 视图的用法。 allocate 函数用于在 Emscripten 堆中分配内存。该函数在较新的 Emscripten 版本中已经不推荐使用,通常推荐使用 \_malloc 代替。 ```js // 分配 1024 字节的内存 var ptr = Module._malloc(1024); // 使用完毕后释放内存 Module._free(ptr); ``` HEAP 视图 Emscripten 提供了多个 HEAP 视图,用于访问 WebAssembly 内存中的数据。这些视图本质上是 TypedArray,它们允许你读取和写入不同类型的数据。 - HEAP8: 8-bit signed integer (Int8Array) - HEAPU8: 8-bit unsigned integer (Uint8Array) - HEAP16: 16-bit signed integer (Int16Array) - HEAPU16: 16-bit unsigned integer (Uint16Array) - HEAP32: 32-bit signed integer (Int32Array) - HEAPU32: 32-bit unsigned integer (Uint32Array) - HEAPF32: 32-bit floating point (Float32Array) - HEAPF64: 64-bit floating point (Float64Array) 以下是一个完整的示例,展示如何使用 \_malloc 分配内存,并使用 HEAPU8 视图操作数据: ```c++ #include #include EMSCRIPTEN_KEEPALIVE void set_memory(char* ptr, int size) { memset(ptr, 42, size); // 将内存区域设置为 42 (即 '*') } ``` 编译: ```sh emcc memory.c -o memory.js -s EXPORTED_RUNTIME_METHODS=['_malloc', '_free', 'ccall'] ``` JavaScript 代码 ```html Emscripten Memory Example ``` 更复杂的示例 假设你需要传递一个数组到 C 函数,并且函数会修改这个数组。 C 代码 (假设文件名为 array.c) ```c++ #include EMSCRIPTEN_KEEPALIVE void double_values(int* array, int size) { for (int i = 0; i < size; i++) { array[i] *= 2; } } ``` 编译 ```sh emcc array.c -o array.js -s EXPORTED_RUNTIME_METHODS=['_malloc', '_free', 'ccall'] ``` JavaScript 代码 ```html Emscripten Array Example ``` 在这个示例中,我们分配了一块足够大的内存以存储一个整数数组,并使用 HEAP32 视图将 JavaScript 数组复制到 Emscripten 堆中。然后,我们调用 C 函数修改数组,并使用 HEAP32 视图读取修改后的数组。 #### 6. addFunction: 将 JavaScript 函数指针添加到 WebAssembly 表中,使其可以从 WebAssembly 代码中调用。 语法: ```js Module.addFunction(func, 'sig'); ``` #### 7. removeFunction: - 从 WebAssembly 表中移除函数指针。 语法: ```js Module.removeFunction(func_ptr); ``` 下面是一个完整的示例,演示如何使用 addFunction 和 removeFunction: 假设你有一个简单的 C 文件 call_js.c: ```c++ #include // 声明一个外部函数指针 typedef int (*func_ptr)(int, int); EM_JS(int, call_js_function, (func_ptr f, int a, int b), { return f(a, b); }); int main() { return 0; } ``` 编译这个文件: ```sh emcc call_js.c -o call_js.js -s EXPORTED_RUNTIME_METHODS=['addFunction','removeFunction'] ``` 在 JavaScript 中调用这个函数: ```html Emscripten addFunction Example ``` 在这个示例中,我们首先定义了一个 JavaScript 函数 add,并使用 Module.addFunction 将其添加到 WebAssembly 表中。然后,我们调用 WebAssembly 函数 \_call_js_function,传入函数指针和参数。最后,我们使用 Module.removeFunction 从 WebAssembly 表中移除了函数指针。 #### 8. getValue 和 setValue: - 用于读取和写入 Emscripten 堆内存中的值。 语法: ```js Module.getValue(ptr, type) 和 Module.setValue(ptr, value, type) ``` getValue 和 setValue 是 Emscripten 提供的两个 API,用于读取和写入 WebAssembly 内存中的值。这些函数可以处理不同类型的数据,如整数、浮点数和指针。 getValue 用于从指定内存地址读取值。其语法如下: ```js var value = Module.getValue(ptr, type); ``` ptr 是内存地址(指针)。 type 是值的类型,可以是 'i8', 'i16', 'i32', 'i64', 'float', 'double', 'i8*', 'i16*', 'i32\*' 等。 setValue 用于向指定内存地址写入值。其语法如下: Module.setValue(ptr, value, type); ptr 是内存地址(指针)。 value 是要写入的值。 type 是值的类型,可以是 'i8', 'i16', 'i32', 'i64', 'float', 'double', 'i8*', 'i16*', 'i32\*' 等。 下面是一个示例,展示如何使用 getValue 和 setValue 读取和写入内存中的值。 ```c++ #include EMSCRIPTEN_KEEPALIVE int get_value_from_memory(int* ptr) { return *ptr; } EMSCRIPTEN_KEEPALIVE void set_value_in_memory(int* ptr, int value) { *ptr = value; } ``` 编译 ```sh emcc values.c -o values.js -s EXPORTED_RUNTIME_METHODS=['getValue', 'setValue', '_malloc', '_free'] ``` JavaScript 代码 ```js Emscripten getValue and setValue Example ``` 在这个示例中,我们分配了一块 4 字节的内存(用于存储一个 32 位整数),并使用 setValue 写入值 42,然后使用 getValue 读取这个值。接着,我们调用 C 函数 get_value_from_memory 来读取内存中的值,并调用 C 函数 set_value_in_memory 来修改内存中的值。最后,我们使用 getValue 再次读取修改后的值。 假设你需要在 WebAssembly 中处理更复杂的数据结构,如结构体。你可以使用 getValue 和 setValue 来操作结构体的成员。 ```c++ #include typedef struct { int x; float y; } Point; EMSCRIPTEN_KEEPALIVE void set_point(Point* p, int x, float y) { p->x = x; p->y = y; } EMSCRIPTEN_KEEPALIVE void get_point(Point* p, int* x, float* y) { *x = p->x; *y = p->y; } ``` 编译 ```sh emcc struct.c -o struct.js -s EXPORTED_RUNTIME_METHODS=['getValue', 'setValue', '_malloc', '_free', 'ccall'] ``` JavaScript 代码 ```html Emscripten Struct Example ``` 在这个示例中,我们使用 set_point 函数设置结构体 Point 的值,并使用 get_point 函数读取结构体的值。我们还展示了如何使用 getValue 和 setValue 操作结构体的成员。 #### 9. FS (File System API): - 用于在 Emscripten 虚拟文件系统中操作文件。 常用方法包括:FS.createDataFile, FS.readFile, FS.writeFile, FS.unlink 等。 #### 10. stackSave, stackRestore, stackAlloc: 用于管理 Emscripten 堆栈指针,帮助分配和释放堆栈空间。 语法:stackSave(), stackRestore(ptr), stackAlloc(size) 以下是一个使用 ccall 和 UTF8ToString 的示例: ```c++ #include EMSCRIPTEN_KEEPALIVE const char* get_hello() { return "Hello from C!"; } ``` 编译这个文件: ```sh emcc hello.c -o hello.js -s EXPORTED_RUNTIME_METHODS=['ccall','UTF8ToString'] ``` 在 JavaScript 中调用这个函数: ```html Hello Emscripten ``` 这个例子展示了如何使用 Emscripten 运行时 API 函数 ccall 调用 C 函数,并使用 UTF8ToString 将返回的指针转换为 JavaScript 字符串。 ### cwrap `cwrap`的方式会更加简单 ```html ``` ### Embind https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html?highlight=emscripten_bindings 以下代码使用 EMSCRIPTEN_BINDINGS() 块将简单 C++ lerp() function() 公开给 JavaScript。 ```cpp // quick_example.cpp #include using namespace emscripten; float lerp(float a, float b, float t) { return (1 - t) * a + t * b; } EMSCRIPTEN_BINDINGS(my_module) { function("lerp", &lerp); } ``` 为了使用 `embin` 编译上面的示例,我们使用 `bind` 选项调用 `emcc:` ```sh emcc -lembind -o main.js main.cpp ``` 生成的`main.js`文件可以作为节点模块或通过 ` ``` 绑定代码作为静态构造函数运行,并且仅当链接中包含对象文件时,静态构造函数才会运行,因此在为库文件生成绑定时,必须显式指示编译器包含对象文件。 例如,要为一个假设的库生成绑定.a 编译 Emscripten 运行带有编译器标志的 --whole-archive emcc: ```sh emcc -lembind -o library.js -Wl,--whole-archive library.a -Wl,--no-whole-archive ``` 向 JavaScript 公开类需要更复杂的绑定语句。例如: ```cpp class MyClass { public: MyClass(int x, std::string y) : x(x) , y(y) {} void incrementX() { ++x; } int getX() const { return x; } void setX(int x_) { x = x_; } static std::string getStringFromInstance(const MyClass& instance) { return instance.y; } private: int x; std::string y; }; // Binding code EMSCRIPTEN_BINDINGS(my_class_example) { class_("MyClass") .constructor() .function("incrementX", &MyClass::incrementX) .property("x", &MyClass::getX, &MyClass::setX) .class_function("getStringFromInstance", &MyClass::getStringFromInstance) ; } ``` 绑定块定义了临时 class\_ 对象上的成员函数调用链(在 Boost.Python 中使用了相同的样式)。这些函数注册类、其 constructor() 、成员 function() 、class_function() (静态)和 property() . 然后可以在 JavaScript 中创建和使用 的 MyClass 实例,如下所示: ```js var instance = new Module.MyClass(10, 'hello'); instance.incrementX(); instance.x; // 11 instance.x = 20; // 20 Module.MyClass.getStringFromInstance(instance); // "hello" instance.delete(); ``` 为了防止闭包编译器重命名上述示例代码中的符号,需要按如下方式重写: ```js var instance = new Module['MyClass'](10, 'hello'); instance['incrementX'](); instance['x']; // 11 instance['x'] = 20; // 20 Module['MyClass']['getStringFromInstance'](instance); // "hello" instance.delete(); ``` 请注意,只有优化程序看到的代码才需要这样做,例如,如 in --pre-js 或 上所述,或在 EM_ASM 或 EM_JS --post-js .对于未通过闭包编译器优化的其他代码,您无需进行此类更改。如果您在构建时没有 --closure 1 启用闭包编译器,则也不需要它。 ### clone 在某些情况下,JavaScript 代码库的多个长期部分需要将同一 C++ 对象保留不同的时间。 为了适应该用例,Emscripten 提供了一种引用计数机制,在该机制中,可以为同一底层 C++ 对象生成多个句柄。仅当删除所有句柄时,才会销毁对象。 clone() JavaScript 方法返回一个新的句柄。它最终还必须与 delete() : ```js async function myLongRunningProcess(x, milliseconds) { // sleep for the specified number of milliseconds await new Promise((resolve) => setTimeout(resolve, milliseconds)); x.method(); x.delete(); } const y = new Module.MyClass(); // refCount = 1 myLongRunningProcess(y.clone(), 5000); // refCount = 2 myLongRunningProcess(y.clone(), 3000); // refCount = 3 y.delete(); // refCount = 2 // (after 3000ms) refCount = 1 // (after 5000ms) refCount = 0 -> object is deleted ``` 基本类型的手动内存管理很繁重,因此 embind 提供了对值类型的支持。Value arrays 与 JavaScript 数组相互转换,并 value objects 相互转换和从 JavaScript 对象转换。 ### 总结 ```sh emcc -lembind -o main.js main.cpp -sEXPORTED_FUNCTIONS=_quick_sort,_main -sEXPORTED_RUNTIME_METHODS=ccall,cwrap ``` ### 基于内存的方式 1. 声明和分配内存 在 WebAssembly 中,内存是通过 WebAssembly.Memory 对象管理的。Emscripten 通常会自动处理内存分配,但你也可以手动管理。 2. 在 C/C++ 中访问内存 在 C/C++ 代码中,你可以通过指针直接访问和操作内存。 ```c++ #include #include // 简单的函数来设置内存中的数据 extern "C" { EMSCRIPTEN_KEEPALIVE void set_memory(int offset, int value) { int *ptr = reinterpret_cast(offset); *ptr = value; } EMSCRIPTEN_KEEPALIVE int get_memory(int offset) { int *ptr = reinterpret_cast(offset); return *ptr; } } ``` 3. 编译 C++ 代码 使用 Emscripten 将上述代码编译为 WebAssembly 模块。 ```sh emcc memory_example.cpp -o memory_example.js -s EXPORTED_FUNCTIONS='["_set_memory", "_get_memory"]' -s ALLOW_MEMORY_GROWTH=1 ``` 4. 在 JavaScript 中访问和操作内存 Emscripten 会为你提供一个 HEAP 对象,可以用来直接访问 WebAssembly 模块的内存。常用的内存视图包括: - HEAP8: 8-bit signed integer array - HEAPU8: 8-bit unsigned integer array - HEAP16: 16-bit signed integer array - HEAPU16: 16-bit unsigned integer array - HEAP32: 32-bit signed integer array - HEAPU32: 32-bit unsigned integer array - HEAPF32: 32-bit floating-point array - HEAPF64: 64-bit floating-point array ```html Memory Access Example ``` 5. 更复杂的数据类型 你可以使用 HEAP 对象来访问和操作更复杂的数据类型,例如结构体或数组。 ```c++ #include #include struct MyStruct { int a; float b; }; extern "C" { EMSCRIPTEN_KEEPALIVE void set_struct(int offset, int a, float b) { MyStruct *ptr = reinterpret_cast(offset); ptr->a = a; ptr->b = b; } EMSCRIPTEN_KEEPALIVE void get_struct(int offset, int *a, float *b) { MyStruct *ptr = reinterpret_cast(offset); *a = ptr->a; *b = ptr->b; } } ``` 编译命令 ```sh emcc struct_example.cpp -o struct_example.js -s EXPORTED_FUNCTIONS='["_set_struct", "_get_struct"]' -s ALLOW_MEMORY_GROWTH=1 ``` JS 代码: ```html Struct Memory Access Example ``` 通过这些步骤,你可以在 JavaScript 和 C/C++ 之间通过共享内存进行高效的通信。这种方法特别适合需要频繁交互或传递大量数据的场景。 但也有一些限制和注意事项: 1. 内存安全 - 缓冲区溢出:在直接操作内存时,需要小心避免缓冲区溢出。错误的内存访问可能导致崩溃或不可预测的行为。 - 类型安全:JavaScript 和 C/C++ 之间的数据类型可能不匹配。确保正确地处理和转换数据类型以避免错误。 2. 内存对齐 - 对齐要求:在操作内存时,确保数据对齐。例如,32 位整数需要 4 字节对齐,64 位浮点数需要 8 字节对齐。未对齐的内存访问可能导致性能下降或错误。 3. 性能问题 - 频繁的内存访问:尽管共享内存访问比函数调用更快,但频繁的内存读写仍可能带来性能开销。应尽量减少不必要的内存操作。 - 内存分配和释放:频繁的内存分配和释放可能导致内存碎片,从而影响性能。 4. 内存管理 - 手动内存管理:需要手动管理内存的分配和释放。忘记释放分配的内存可能导致内存泄漏。 - 固定内存大小:默认情况下,WebAssembly 内存大小是固定的,尽管可以配置增长,但需要额外的设置。 5. 内存视图限制 - 视图限制:WebAssembly 内存视图 (HEAP8, HEAP16, HEAP32, 等) 是基于 ArrayBuffer 的,需要注意视图的边界和大小。 6. 数据转换 - 字符串处理:C/C++ 中的字符串和 JavaScript 中的字符串处理方式不同。需要进行适当的转换。 从 C/C++ 到 JavaScript:使用 UTF8ToString。 从 JavaScript 到 C/C++:使用 stringToUTF8。 7. 并发和线程 - 线程安全:WebAssembly 目前支持共享内存和线程(使用 WebAssembly Threads),但需要小心处理并发和同步问题,以避免竞争条件和死锁。 8. 调试和开发 - 调试困难:直接操作内存可能使调试变得更加困难。需要仔细检查内存地址和数据,以确保正确性。 - 可维护性:直接内存访问的代码可能比使用更高级别接口的代码更难维护和理解。 示例代码中的改进建议 - 内存对齐:确保在使用 malloc 分配内存时,对齐地址。例如,对于 32 位整数使用 HEAP32 视图时,确保地址是 4 的倍数。 - 字符串转换:如果需要传递字符串,请使用 Emscripten 提供的工具函数。 改进后的示例 以下是一些改进建议: C++ 代码(改进内存对齐) ```c++ #include #include // 简单的函数来设置内存中的数据 extern "C" { EMSCRIPTEN_KEEPALIVE void set_memory(int offset, int value) { if (offset % 4 != 0) return; // 检查对齐 int *ptr = reinterpret_cast(offset); *ptr = value; } EMSCRIPTEN_KEEPALIVE int get_memory(int offset) { if (offset % 4 != 0) return 0; // 检查对齐 int *ptr = reinterpret_cast(offset); return *ptr; } } ``` JavaScript 代码(改进内存对齐和字符串处理) ```html Memory Access Example ``` 通过注意上述限制和改进建议,你可以更高效和安全地在 JavaScript 和 WebAssembly(C/C++)之间通过共享内存进行通信。 ## 多线程 WebAssembly 目前支持多线程,通过 SharedArrayBuffer 和 Web Workers 实现。Emscripten 提供了一些工具和支持来使多线程编程更加容易。以下是使用 Emscripten 和 WebAssembly 实现多线程的方法。 前提条件 确保在你的浏览器环境中启用了 SharedArrayBuffer,因为它是实现多线程的关键。 设置 Emscripten 项目以支持多线程: - 编译时启用线程支持 - 使用 Web Workers - 使用 pthread 库 示例项目 1. 创建一个 C++ 文件 假设你的 C++ 文件名为 threads_example.cpp。 ```c++ #include #include #include void* thread_main(void* arg) { int* num = static_cast(arg); std::cout << "Hello from thread! Argument: " << *num << std::endl; return 0; } extern "C" { EMSCRIPTEN_KEEPALIVE void create_thread(int num) { pthread_t thread; int* arg = new int(num); pthread_create(&thread, nullptr, thread_main, arg); pthread_detach(thread); } } ``` 2. 编译 C++ 文件 使用 Emscripten 编译文件,并启用线程支持。 ```sh emcc threads_example.cpp -o threads_example.js -s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=4 -s EXPORTED_FUNCTIONS='["_create_thread"]' -s ALLOW_MEMORY_GROWTH=1 ``` 3. 创建 HTML 文件 创建一个 HTML 文件以加载和运行编译后的 WebAssembly 模块。 ```html WebAssembly Multithreading Example ``` 说明: C++ 代码 - pthread_create:用于创建新线程。 - pthread_detach:分离线程,使其在完成后自动释放资源。 - EMSCRIPTEN_KEEPALIVE:确保函数不会被编译器优化掉,使其在 JavaScript 中可用。 JavaScript 代码 - Module.onRuntimeInitialized:等待 WebAssembly 模块初始化完成。 - Module.\_create_thread:调用导出的 create_thread 函数创建新线程。 多线程注意事项 1. 线程安全:确保在多线程环境中正确处理数据竞争和同步问题。 2. 共享内存:使用 SharedArrayBuffer 在线程之间共享内存。 3. 性能开销:线程的创建和管理会带来一些性能开销,尽量避免频繁创建和销毁线程。 4. 浏览器兼容性:确保你的浏览器支持 SharedArrayBuffer 和多线程 WebAssembly。 示例代码扩展 你可以扩展上述示例,使多个线程协作完成更复杂的任务。例如,实现一个简单的并行计算。 扩展 C++ 代码 ```c++ #include #include #include #include const int num_threads = 4; int results[num_threads]; void* thread_compute(void* arg) { int index = *static_cast(arg); results[index] = index * index; // 简单的计算任务 return 0; } extern "C" { EMSCRIPTEN_KEEPALIVE void parallel_compute() { pthread_t threads[num_threads]; std::vector args(num_threads); for (int i = 0; i < num_threads; ++i) { args[i] = i; pthread_create(&threads[i], nullptr, thread_compute, &args[i]); } for (int i = 0; i < num_threads; ++i) { pthread_join(threads[i], nullptr); } for (int i = 0; i < num_threads; ++i) { std::cout << "Result from thread " << i << ": " << results[i] << std::endl; } } } ``` 扩展 JavaScript 代码 ```js WebAssembly Multithreading Example ``` 通过 Emscripten 和 WebAssembly,你可以利用多线程来提高性能和响应能力。确保处理好线程安全和同步问题,以避免数据竞争和其他多线程相关的问题。 以下是一些关键点: 1. 数据竞争和同步 - 数据竞争:当多个线程同时访问和修改共享数据时,可能会出现数据竞争问题。这会导致不可预测的行为和错误。 - 避免数据竞争:使用线程同步机制,例如互斥锁(mutex)、信号量(semaphore)和条件变量(condition variable)来保护共享数据。 示例:使用互斥锁 ```c++ #include #include #include pthread_mutex_t mutex; void* thread_safe_increment(void* arg) { pthread_mutex_lock(&mutex); int* counter = static_cast(arg); (*counter)++; pthread_mutex_unlock(&mutex); return 0; } extern "C" { EMSCRIPTEN_KEEPALIVE void create_threads(int num_threads, int* counter) { pthread_t threads[num_threads]; pthread_mutex_init(&mutex, nullptr); for (int i = 0; i < num_threads; ++i) { pthread_create(&threads[i], nullptr, thread_safe_increment, counter); } for (int i = 0; i < num_threads; ++i) { pthread_join(threads[i], nullptr); } pthread_mutex_destroy(&mutex); } } ``` 2. 死锁 - 死锁:当两个或多个线程相互等待对方释放资源时,会发生死锁,导致所有相关线程都被无限期阻塞。 - 避免死锁:小心设计锁的获取顺序,避免嵌套锁定。尽量使用尝试锁(try-lock)而不是阻塞锁。 3. 线程池 - 线程池:创建和销毁线程开销较大,使用线程池可以重复利用线程,减少开销。 - 实现线程池:通过预先创建一组线程,并将任务分配给这些线程执行。 示例:简单线程池 ```c++ #include #include #include #include const int num_threads = 4; pthread_t threads[num_threads]; std::queue task_queue; pthread_mutex_t queue_mutex; pthread_cond_t queue_cond; void* thread_main(void* arg) { while (true) { pthread_mutex_lock(&queue_mutex); while (task_queue.empty()) { pthread_cond_wait(&queue_cond, &queue_mutex); } auto task = task_queue.front(); task_queue.pop(); pthread_mutex_unlock(&queue_mutex); task(); } return 0; } extern "C" { EMSCRIPTEN_KEEPALIVE void initialize_thread_pool() { pthread_mutex_init(&queue_mutex, nullptr); pthread_cond_init(&queue_cond, nullptr); for (int i = 0; i < num_threads; ++i) { pthread_create(&threads[i], nullptr, thread_main, nullptr); } } EMSCRIPTEN_KEEPALIVE void add_task(void (*task)()) { pthread_mutex_lock(&queue_mutex); task_queue.push(task); pthread_cond_signal(&queue_cond); pthread_mutex_unlock(&queue_mutex); } } ``` 4. 资源管理 - 资源泄漏:确保所有动态分配的资源在使用完毕后正确释放,包括内存、文件描述符和锁等。 - 自动管理资源:使用智能指针(如 C++ 的 std::unique_ptr 和 std::shared_ptr)或 RAII(资源获取即初始化)模式。 5. 性能开销 - 线程切换:频繁的线程切换会导致性能下降。尽量减少不必要的线程创建和销毁。 - 批量处理:将多个任务批量处理,以减少线程切换开销。 6. 浏览器兼容性 - 浏览器支持:确保目标浏览器支持 WebAssembly 多线程和 SharedArrayBuffer。一些旧版浏览器可能不支持这些特性。 - 特性检测:在代码中检测浏览器是否支持 SharedArrayBuffer,并在不支持的情况下提供替代方案。 7. 调试和测试 - 调试工具:使用调试工具和日志记录来跟踪线程行为和问题。 - 并发测试:进行并发测试以确保代码在多线程环境下的正确性和稳定性。 8. 内存对齐 - 内存对齐:确保内存对齐以提高性能和避免潜在的错误,特别是在访问共享内存时。 示例总结 通过以下 JavaScript 代码来初始化线程池并添加任务: ```html WebAssembly Multithreading Example ``` 通过注意上述多线程编程中的注意事项,可以有效地利用 WebAssembly 和 Emscripten 提供的多线程功能,实现高性能和高效的并发处理。确保正确处理数据竞争、同步和资源管理,以避免常见的多线程问题。 [emcc 指令参考](https://emscripten.org/docs/tools_reference/settings_reference.html) ================================================ FILE: packages/cpro/sum.cpp ================================================ #include using namespace emscripten; //接收两个浮点型参数,返回一个字符串 std::string sum(double a,double b) { return std::to_string(a*b); } //公开一个方法给 JavaScript 访问 EMSCRIPTEN_BINDINGS(myModule) { function("sum", &sum); } ================================================ FILE: packages/debug/koaStatic/index.ts ================================================ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import Koa from 'koa'; import serve from 'koa-static'; import mount from 'koa-mount'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = new Koa(); const PORT = 5555; const STATIC_DIR = 'dist'; // 设置正确的 MIME 类型 app.use(async (ctx, next) => { if (ctx.path.endsWith('.js')) { ctx.type = 'application/javascript'; } else if (ctx.path.endsWith('.mjs')) { ctx.type = 'application/javascript'; } else if (ctx.path.endsWith('.json')) { ctx.type = 'application/json'; } else if (ctx.path.endsWith('.css')) { ctx.type = 'text/css'; } await next(); }); app.use(async (ctx, next) => { ctx.set('Cross-Origin-Opener-Policy', 'same-origin'); ctx.set('Cross-Origin-Embedder-Policy', 'require-corp'); await next(); }); // Serve static files from the 'dist' directory with /weread prefix app.use(mount('/weread', serve(path.join(__dirname, STATIC_DIR)))); // 启动服务器 app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); export default app; ================================================ FILE: packages/debug/koaStatic/readme.md ================================================ # 静态服务 用于排查一些本地启动没有问题,但是部署到线上环境就无法运行的问题 有时候需要搭配 nginx 做反向代理域名映射,用于完整并彻底的模拟线上环境 ================================================ FILE: packages/debug/nginxconf/agency.conf ================================================ events { worker_connections 1024; } http { server { listen 80; server_name trade-app.longbridge.xyz; location / { proxy_pass http://localhost:5555; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } } ================================================ FILE: packages/debug/nginxconf/readme.md ================================================ 1. 使用命令行参数指定配置文件路径 ```sh nginx -c /path/to/your/nginx.conf ``` 请注意,当您使用 -c 参数时,Nginx 将仅加载您指定的配置文件,而不会加载任何其他通过 include 指令包含的文件(除非这些 include 指令位于您指定的配置文件中)。因此,请确保您的自定义配置文件中包含了所有必要的设置和 include 指令。 在启动之前,可以先通过`-t`测试一下: ```sh nginx -t -c /path/to/your/nginx.conf ``` 重启 ```sh nginx -s reload ``` 另外,如果您已经运行了 Nginx 实例,并且想要更改其配置而无需完全停止并重新启动服务,您可以使用 nginx -s reload 命令,但前提是您的主配置文件(或您通过 -c 参数指定的配置文件)已经更改,并且 Nginx 能够重新加载这些更改。不过,请注意,nginx -s reload 命令不会直接接受 -c 参数来指定一个新的配置文件;它只会重新加载当前正在使用的配置文件(或您通过 Nginx 的启动脚本/服务管理器指定的配置文件)的更改。 2. 配置本地 DNS 解析 由于 nginx 配置的 server_name 往往是一个真实的域名,你的本地机器默认会尝试通过互联网解析它。为了让你在本地开发环境中测试反向代理,你需要确保当在本地机器上访问这个域名时,它会被解析到 127.0.0.1(即 localhost)。 有几种方法可以实现这一点,包括修改 /etc/hosts 文件或在你的 DNS 服务器中设置条目(如果你正在使用本地 DNS 服务器)。 ```sh cat /etc/hosts ``` 在文件末尾添加一行,将域名映射到 localhost: ```sh 127.0.0.1 server_name ``` 保存并关闭文件。现在,当你尝试在浏览器中访问 server_name 域名时,请求应该会被转发到你的本地 localhost:5555。 注意:确保 localhost:5555 上有一个正在运行的服务器,以便 Nginx 可以成功转发请求。 3. 启动 进入 Nginx 安装目录的 sbin 文件夹:通常 Nginx 的可执行文件位于/usr/local/nginx/sbin(具体路径可能因安装方式和操作系统而异)。 执行重启命令:在 sbin 目录下,执行./nginx -s reload 命令。这条命令会优雅地重启 Nginx 服务,即不会立即关闭已经建立的连接,而是等待现有请求处理完成后,重新加载配置文件并启动新的工作进程。 使用 brew 服务命令: ```sh brew services restart nginx ``` 直接使用 Nginx 命令: 你也可以直接使用 Nginx 的-s 参数来停止服务。有两种停止方式: 快速停止(不等待处理完当前请求): ```sh sudo nginx -s stop ``` 优雅停止(等待处理完当前请求后再停止): ```sh sudo nginx -s quit ``` 4. 关闭 进入 Nginx 安装目录的 sbin 文件夹(如果尚未进入)。 执行关闭命令:可以使用./nginx -s stop 命令来快速停止 Nginx 服务,这将立即关闭所有连接。另外,./nginx -s quit 命令会等待所有请求处理完成后才停止服务,这是一种更优雅的关闭方式。 在某些情况下,你可能需要手动停止 Nginx 服务。这通常涉及到找到 Nginx 的主进程号(PID),并使用 kill 命令发送相应的信号来停止进程。 查找 Nginx 主进程的 PID:可以使用 ps 命令结合 grep 来查找,如: ```sh ps -ef | grep nginx ``` 发送停止信号:根据找到的 PID,使用 kill 命令发送 TERM(快速停止)或 INT(优雅停止)信号。例如,kill -TERM 或 kill -INT 。此外,kill -HUP 也可以用于平滑重启 Nginx,但这实际上是发送了一个挂起信号,而不是停止信号。 使用 brew 服务命令: 关闭 Nginx 服务的最简单方式同样是使用 brew 提供的服务管理命令。在终端中输入以下命令: ```sh brew services stop nginx ``` Nginx 的配置文件通常遵循以下结构: ```sh # 全局块 events { # events 块 } http { # http 块 server { # server 块 location / { # location 块 } # 更多的 location 块 } # 更多的 server 块 } # 注意:server 指令必须嵌套在 http 块中 ``` 查看状态: ```sh ps aux | grep nginx ``` ================================================ FILE: packages/debug/package.json ================================================ { "name": "debug", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "koa": "^3.0.3", "koa-mount": "^4.2.0", "koa-send": "^5.0.1", "koa-static": "^5.0.0" }, "devDependencies": { "@types/koa": "^2.15.0", "@types/koa-mount": "^4.0.5", "@types/koa-send": "^4.1.6", "@types/koa-static": "^4.0.4" } } ================================================ FILE: packages/debug/tsconfig.json ================================================ { "exclude": ["node_modules/**", "dist/**", "public"], "include": ["**/*.ts", "**/*.d.ts", "**/*.tsx", "tailwind.config.ts"], "compilerOptions": { "module": "ESNext", "moduleResolution": "bundler", "esModuleInterop": true, "target": "ESNext", "declaration": true, "outDir": "dist", "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "isolatedModules": false, "strict": true, "skipLibCheck": true, "noImplicitAny": true, "useUnknownInCatchVariables": false, "inlineSourceMap": true, "baseUrl": ".", "experimentalDecorators": true } } ================================================ FILE: packages/docs/.vitepress/common/index.ts ================================================ import { SERVICE_WORK_VERSION } from '../../variable/SERVICE_WORK_VERSION'; export const GTAG = 'https://www.googletagmanager.com/gtag/js?id=G-0MPS5WH1C0'; export const GOOGLE_ANALYSE = `;window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);};gtag('js', new Date());gtag('config', 'G-0MPS5WH1C0');`; export const BD_ANALYSE = ` ;var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?3bc20bd8070ce614078a36c686209456"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); `; export const PREVIEW_CODE = ` window.uploadFile = (name) => { const preview = document.getElementById(name) const uploadFile = document.createElement('input') uploadFile.setAttribute('type', 'file') uploadFile.click() uploadFile.onchange = (e) => { const { files = [] } = uploadFile if (preview) { if (files && files.length > 0) { preview.setAttribute('src', '') const file = files[0] const url = URL.createObjectURL(file) preview.setAttribute('src', url) } } } } `; export const DESCRIPTION = 'Based on web component library, common function library utils, personal article record and so on'; export const HOME = 'https://chaxus.github.io/ran/'; export const BASE_PATH = '/ran/'; export const HOME_ICON = `${HOME}home.svg`; export const UTILS_PATH = `${HOME}src/ranuts/utils/`; export const RANUI_PATH = `${HOME}src/ranui/`; export const ARTICLE_PATH = `${HOME}src/article/designMode.html`; export const KEY_WORDS = 'ran,component,components,ui,design,ranui,web-components,javascript,typescript,js'; export const GITHUB = 'https://github.com/chaxus/ran'; export const SERVICE_WORK = ` // 注册 Service worker const registerServiceWorker = async () => { if ('serviceWorker' in window.navigator) { try { const registration = await navigator.serviceWorker.register('/ran/sw${SERVICE_WORK_VERSION}.js', { scope: '/ran/', }); if (registration.installing) { console.log('installing Service worker'); } else if (registration.waiting) { console.log('Service worker installed'); } else if (registration.active) { console.log('Service worker active'); } } catch (error) { console.error('service worker register error:', error); } } }; registerServiceWorker(); `; export const SET_FONT_SIZE = ` function initFontSize() { let base = 375; const { documentElement } = document; const mediaQuery = window.matchMedia('(orientation: portrait)'); let timer; let standardRatio = 667 / 375; const ua = navigator.userAgent.toLowerCase(); const ipad = /ipad|ipod/.test(ua) if (ipad) { standardRatio = 1024 / 768; base = 768; } function setFontSize() { const ua = navigator.userAgent.toLowerCase(); const ipad = /ipad|ipod/.test(ua) const android = /android/.test(ua) const iphone = /iphone/.test(ua) if(!ipad && !android && !iphone) { documentElement.style.fontSize = ''; return } const isLandscape = !mediaQuery.matches; let screenWidth = window.screen.width; let screenHeight = window.screen.height; if (screenWidth < screenHeight) { [ screenWidth, screenHeight ] = [ screenHeight, screenWidth ]; } let width = documentElement.clientWidth; let height = screenHeight; const realRatio = width / height; if (realRatio >= standardRatio) { width = height * standardRatio; documentElement.classList.remove('adjustHeight'); documentElement.classList.add('adjustWidth'); } else { height = width / standardRatio; documentElement.classList.remove('adjustWidth'); documentElement.classList.add('adjustHeight'); } window.adjustWidth = width; window.adjustHeight = height; let target = width / base * 16; if (isLandscape) { target /= standardRatio; } documentElement.style.fontSize = target + 'px'; const currentSize = window.getComputedStyle(documentElement).fontSize.replace('px', ''); if (target !== currentSize) { documentElement.style.fontSize = target / currentSize * target + 'px'; } } window.addEventListener('resize', function() { clearTimeout(timer); timer = setTimeout(setFontSize, 300); }, !1); window.addEventListener('pageshow', function(e) { e.persisted && (clearTimeout(timer), timer = setTimeout(setFontSize, 300)); }, !1); window.addEventListener('orientationchange', function() { console.log('改变了手机方向'); setFontSize(); }, false); setFontSize(); }; initFontSize(); `; ================================================ FILE: packages/docs/.vitepress/common/sw.ts ================================================ // 注册 Service worker const registerServiceWorker = async () => { if ('serviceWorker' in window.navigator) { try { // eslint-disable-next-line n/no-unsupported-features/node-builtins const registration = await navigator.serviceWorker.register('/ran/sw.js', { scope: '/ran/', }); if (registration.installing) { console.log('installing Service worker'); } else if (registration.waiting) { console.log('Service worker installed'); } else if (registration.active) { console.log('Service worker active'); } } catch (error) { console.error('service worker register error:', error); } } }; registerServiceWorker(); ================================================ FILE: packages/docs/.vitepress/components/Layout.vue ================================================ ================================================ FILE: packages/docs/.vitepress/components/TOTP.vue ================================================ ================================================ FILE: packages/docs/.vitepress/composition/useBasic.ts ================================================ import { inject } from 'vue'; import { useI18n } from 'vue-i18n'; import type { Env } from '../plugins/env'; export default (): any => { const $env: Env = inject('$env')!; const { t, locale } = useI18n(); return { locale, $env, t }; }; ================================================ FILE: packages/docs/.vitepress/config.ts ================================================ import { defineConfig } from 'vitepress'; import { themeEnConfig } from './langs/en'; import { themeCnConfig } from './langs/cn'; import { ARTICLE_PATH, BASE_PATH, BD_ANALYSE, DESCRIPTION, GOOGLE_ANALYSE, GTAG, HOME, HOME_ICON, // KEY_WORDS, PREVIEW_CODE, RANUI_PATH, SERVICE_WORK, SET_FONT_SIZE, UTILS_PATH, } from './common/index'; import { LANGS_DICT } from './lib/constant'; export default defineConfig({ title: 'ran', description: DESCRIPTION, base: BASE_PATH, lastUpdated: true, locales: { // root: { label: '简体中文', lang: 'zh-CN' }, // en: { // label: 'English', // lang: 'en', // themeConfig: themeEnConfig, // }, root: { label: 'English', lang: LANGS_DICT.EN }, cn: { label: '简体中文', lang: LANGS_DICT.ZH_CN, themeConfig: themeCnConfig, }, }, vue: { template: { compilerOptions: { isCustomElement: (tag: string) => { return tag.startsWith('r-'); }, }, }, }, head: [ // base ['link', { rel: 'icon', href: `${BASE_PATH}favicon.ico` }], ['link', { rel: 'manifest', href: `${BASE_PATH}manifest.json` }], ['meta', { name: 'theme-color', content: '#646cff' }], // author ['meta', { name: 'author', content: '81380@163.com' }], // 表示爬虫对此页面的处理行为 或 应当遵守的规则,是用来做搜索引擎抓取的 // all:搜索引擎将索引此网页,并继续通过此 网页的链接索引文件 将被检索 // none:搜索引擎将 忽略 此网页 // index:搜索引擎 索引 此网页 // follow:搜索引擎继续通过此网页的链接索引搜索 其它的网页 ['meta', { name: 'robots', content: 'all' }], // 用来指定支持双核浏览器要采用哪种的渲染方式 ['meta', { name: 'renderer', content: 'webkit' }], // 已经有国际化,禁止谷歌自动翻译 ['meta', { name: 'google', content: 'notranslate' }], // og ['meta', { property: 'og:title', content: 'ran' }], [ 'meta', { property: 'og:description', content: DESCRIPTION, }, ], ['meta', { property: 'og:url', content: HOME }], ['meta', { property: 'og:image', content: HOME_ICON }], ['meta', { property: 'og:type', content: 'article' }], [ 'meta', { property: 'article:home', content: HOME, }, ], ['meta', { property: 'article:ranui', content: RANUI_PATH }], ['meta', { property: 'article:ranuts', content: UTILS_PATH }], ['meta', { property: 'article:section', content: ARTICLE_PATH }], // keywords // ['meta', { name: 'keywords', content: KEY_WORDS }], // chrome ['meta', { httpEquiv: 'Permissions-Policy', content: 'interest-cohort=()' }], // set font size ['script', {}, SET_FONT_SIZE], // report ['script', { defer: 'true', src: GTAG }], ['script', {}, GOOGLE_ANALYSE], ['script', {}, BD_ANALYSE], // preview component script ['script', {}, PREVIEW_CODE], // service worker and pwa ['script', {}, SERVICE_WORK], ], themeConfig: themeEnConfig, }); ================================================ FILE: packages/docs/.vitepress/lang/en.json ================================================ { "lang": "English", "components_totp_1": "Generate", "components_totp_2": "Please enter the key of 2FA", "components_totp_3": "You haven't entered the key for 2FA", "components_totp_4": "expires", "components_totp_5": "The entered key value is invalid", "components_totp_6": "2FA Verification" } ================================================ FILE: packages/docs/.vitepress/lang/index.ts ================================================ import { createI18n } from 'vue-i18n'; import { $env } from '../plugins/env'; import { I18N_MODE, LANGS_DICT, LANG_MESSAGES, LOADED_LOCALES } from '../lib/constant'; const locale = $env.locale; const i18n = createI18n({ legacy: false, locale, fallbackLocale: LANGS_DICT.EN, messages: LANG_MESSAGES, devtools: false, }); export const setI18nLanguage = (lang: string): string => { if (i18n.mode === I18N_MODE.LEGACY) { i18n.global.locale.value = lang as 'zh-CN' | 'en'; } else { i18n.global.locale.value = lang as 'zh-CN' | 'en'; } return lang; }; // 合并公共语言词条 export const mergeCommonMessage = (message: string, lang = locale): void => { i18n.global.mergeLocaleMessage(lang, message); }; // 异步加载语言词条 export const loadLanguageAsync = (lang: string): Promise => { if (!lang) return Promise.reject('lang is undefined'); // 如果语言相同 if (i18n.global.locale.value === lang) { return Promise.resolve(setI18nLanguage(lang)); } // 如果语言已经加载 if (LOADED_LOCALES.includes(lang)) { return Promise.resolve(setI18nLanguage(lang)); } return import(`../lang/${lang}.json`).then((messages) => { mergeCommonMessage(messages.default, lang); LOADED_LOCALES.push(lang); return setI18nLanguage(lang); }); }; export default i18n; ================================================ FILE: packages/docs/.vitepress/lang/zh-CN.json ================================================ { "lang": "简体中文", "components_totp_1": "生成结果", "components_totp_2": "请输入 2FA 的 key", "components_totp_3": "你还没有输入 2FA 的 key", "components_totp_4": "有效期", "components_totp_5": "输入的 key 值无效", "components_totp_6": "2FA 验证" } ================================================ FILE: packages/docs/.vitepress/langs/cn/index.ts ================================================ import type { DefaultTheme } from 'vitepress'; import { GITHUB } from '../../common/index'; const themeCnConfig: DefaultTheme.Config = { logo: '/home.svg', search: { provider: 'local', }, nav: [ { text: '首页', link: '/cn/' }, { text: '函数', link: '/cn/src/ranuts/' }, { text: '组件', link: '/cn/src/ranui/' }, { text: '璀璨', link: '/cn/src/article/design_mode.md' }, ], socialLinks: [{ icon: 'github', link: GITHUB }], footer: { message: 'Released under the MIT License.', copyright: 'Copyright © 2022-11-11', }, // algolia: { // appId: 'RDX0Y4AQW1', // apiKey: 'c7b6e28f95335eddc66c5a1b54ad9834', // indexName: 'chaxus_ran', // placeholder: 'search', // }, sidebar: { '/cn/src/ranuts/': [ { text: '总览', link: '/cn/src/ranuts/', }, { text: '工具函数', link: '/cn/src/ranuts/utils/', collapsed: false, items: [ { text: '函数式编程', collapsed: true, items: [ { text: 'debounce - 防抖函数', link: '/cn/src/ranuts/utils/debounce.md' }, { text: 'throttle - 节流函数', link: '/cn/src/ranuts/utils/throttle.md' }, { text: 'memoize - 记忆化函数', link: '/cn/src/ranuts/utils/memoize.md' }, { text: 'noop - 空函数', link: '/cn/src/ranuts/utils/noop.md' }, { text: 'compose - 组合中间件函数', link: '/cn/src/ranuts/utils/compose.md' }, ], }, { text: '字符串处理', collapsed: true, items: [ { text: 'md5 - MD5 哈希函数', link: '/cn/src/ranuts/utils/md5.md' }, { text: 'randomString - 生成随机字符串', link: '/cn/src/ranuts/utils/random_string.md' }, { text: 'clearBr - 清除空格和换行', link: '/cn/src/ranuts/utils/clear_br.md' }, { text: 'clearStr - 去除首尾空格和引号', link: '/cn/src/ranuts/utils/clear_str.md' }, { text: 'strParse - 字符串解析为对象', link: '/cn/src/ranuts/utils/str_parse.md' }, { text: 'toString - 转换为字符串', link: '/cn/src/ranuts/utils/to_string.md' }, { text: 'transformText - ArrayBuffer 转文本', link: '/cn/src/ranuts/utils/transform_text.md' }, { text: 'checkEncoding - 检测字符编码', link: '/cn/src/ranuts/utils/check_encoding.md' }, { text: 'changeHumpToLowerCase - 驼峰转下划线', link: '/cn/src/ranuts/utils/change_hump_to_lower_case.md', }, { text: 'getMatchingSentences - 提取匹配句子', link: '/cn/src/ranuts/utils/get_matching_sentences.md' }, { text: 'isString - 判断是否为字符串', link: '/cn/src/ranuts/utils/is_string.md' }, { text: 'str2Xml - 字符串转 XML', link: '/cn/src/ranuts/utils/str2xml.md' }, ], }, { text: '对象处理', collapsed: true, items: [ { text: 'merge - 合并对象', link: '/cn/src/ranuts/utils/merge.md' }, { text: 'isEqual - 深度比较', link: '/cn/src/ranuts/utils/is_equal.md' }, { text: 'cloneDeep - 深度克隆', link: '/cn/src/ranuts/utils/clone_deep.md' }, { text: 'querystring - 对象转查询字符串', link: '/cn/src/ranuts/utils/querystring.md' }, { text: 'filterObj - 过滤对象', link: '/cn/src/ranuts/utils/filter_obj.md' }, { text: 'formatJson - 格式化 JSON', link: '/cn/src/ranuts/utils/format_json.md' }, ], }, { text: '数字处理', collapsed: true, items: [ { text: 'range - 限制数字范围', link: '/cn/src/ranuts/utils/range.md' }, { text: 'mathjs - 精确数字运算', link: '/cn/src/ranuts/utils/mathjs.md' }, { text: 'perToNum - 百分比转数字', link: '/cn/src/ranuts/utils/per_to_num.md' }, { text: 'transformNumber - 数字格式化', link: '/cn/src/ranuts/utils/transform_number.md' }, { text: 'addNumSym - 添加正负号', link: '/cn/src/ranuts/utils/add_num_sym.md' }, ], }, { text: '颜色处理', collapsed: true, items: [ { text: 'hexToRgb - 十六进制转 RGB', link: '/cn/src/ranuts/utils/hex_to_rgb.md' }, { text: 'rgbToHex - RGB 转十六进制', link: '/cn/src/ranuts/utils/rgb_to_hex.md' }, { text: 'randomColor - 生成随机颜色', link: '/cn/src/ranuts/utils/random_color.md' }, ], }, { text: '时间处理', collapsed: true, items: [ { text: 'timeFormat - 时间格式化', link: '/cn/src/ranuts/utils/time_format.md' }, { text: 'timestampToTime - 时间戳转 Date', link: '/cn/src/ranuts/utils/timestamp_to_time.md' }, { text: 'performanceTime - 高精度时间戳', link: '/cn/src/ranuts/utils/performance_time.md' }, ], }, { text: '设备检测', collapsed: true, items: [ { text: 'isMobile - 判断移动端', link: '/cn/src/ranuts/utils/is_mobile.md' }, { text: 'isWeiXin - 判断微信浏览器', link: '/cn/src/ranuts/utils/is_weixin.md' }, { text: 'isClient - 判断客户端环境', link: '/cn/src/ranuts/utils/is_client.md' }, { text: 'isSafari - 判断 Safari', link: '/cn/src/ranuts/utils/is_safari.md' }, { text: 'isBangDevice - 判断刘海屏', link: '/cn/src/ranuts/utils/is_bang_device.md' }, { text: 'currentDevice - 获取设备类型', link: '/cn/src/ranuts/utils/current_device.md' }, ], }, { text: 'DOM 操作', collapsed: true, items: [ { text: 'addClassToElement - 添加类名', link: '/cn/src/ranuts/utils/add_class_to_element.md' }, { text: 'removeClassToElement - 移除类名', link: '/cn/src/ranuts/utils/remove_class_to_element.md' }, { text: 'createDocumentFragment - 创建文档片段', link: '/cn/src/ranuts/utils/create_document_fragment.md', }, { text: 'escapeHtml - 转义 HTML', link: '/cn/src/ranuts/utils/escape_html.md' }, { text: 'Chain - 链式 DOM 操作', link: '/cn/src/ranuts/utils/chain.md' }, { text: 'create - 创建 DOM 元素', link: '/cn/src/ranuts/utils/create.md' }, ], }, { text: '存储', collapsed: true, items: [ { text: 'localStorageGetItem - 获取存储值', link: '/cn/src/ranuts/utils/local_storage.md' }, { text: 'localStorageSetItem - 设置存储值', link: '/cn/src/ranuts/utils/local_storage.md' }, ], }, { text: 'URL/Query', collapsed: true, items: [ { text: 'getAllQueryString - 提取查询参数', link: '/cn/src/ranuts/utils/get_all_query_string.md' }, { text: 'getQuery - 提取查询参数', link: '/cn/src/ranuts/utils/get_query.md' }, { text: 'encodeUrl - 安全编码 URL', link: '/cn/src/ranuts/utils/encode_url.md' }, { text: 'appendUrl - 拼接查询参数', link: '/cn/src/ranuts/utils/append_url.md' }, ], }, { text: 'Cookie', collapsed: true, items: [ { text: 'getCookie - 获取 Cookie', link: '/cn/src/ranuts/utils/get_cookie.md' }, { text: 'getCookieByName - 正则获取 Cookie', link: '/cn/src/ranuts/utils/get_cookie_by_name.md' }, ], }, { text: '图片处理', collapsed: true, items: [ { text: 'convertImageToBase64 - 图片转 Base64', link: '/cn/src/ranuts/utils/convert_image_to_base64.md' }, { text: 'isImageSize - 校验图片尺寸', link: '/cn/src/ranuts/utils/is_image_size.md' }, ], }, { text: '性能', collapsed: true, items: [ { text: 'getPerformance - 获取性能指标', link: '/cn/src/ranuts/utils/get_performance.md' }, { text: 'getFrame - 计算帧率', link: '/cn/src/ranuts/utils/get_frame.md' }, { text: 'getPixelRatio - 获取分辨率比例', link: '/cn/src/ranuts/utils/get_pixel_ratio.md' }, ], }, { text: '网络', collapsed: true, items: [ { text: 'imageRequest - 测试网络延迟', link: '/cn/src/ranuts/utils/image_request.md' }, { text: 'networkSpeed - 测试网络速度', link: '/cn/src/ranuts/utils/network_speed.md' }, { text: 'connection - 获取网络连接信息', link: '/cn/src/ranuts/utils/connection.md' }, ], }, { text: '浏览器', collapsed: true, items: [ { text: 'getWindow - 获取窗口大小', link: '/cn/src/ranuts/utils/get_window.md' }, { text: 'getHost - 获取主机地址', link: '/cn/src/ranuts/utils/get_host.md' }, { text: 'createObjectURL - 创建对象 URL', link: '/cn/src/ranuts/utils/create_object_url.md' }, { text: 'removeGhosting - 移除拖拽阴影', link: '/cn/src/ranuts/utils/remove_ghosting.md' }, { text: 'retain - 覆盖后退事件', link: '/cn/src/ranuts/utils/retain.md' }, ], }, { text: '脚本加载', collapsed: true, items: [{ text: 'scriptOnLoad - 动态加载脚本', link: '/cn/src/ranuts/utils/script_on_load.md' }], }, { text: '错误处理', collapsed: true, items: [ { text: 'handleConsole - 拦截 console', link: '/cn/src/ranuts/utils/handle_console.md' }, { text: 'handleError - 全局错误处理', link: '/cn/src/ranuts/utils/handle_error.md' }, { text: 'handleFetchHook - 拦截 fetch', link: '/cn/src/ranuts/utils/handle_fetch_hook.md' }, ], }, { text: '其他', collapsed: true, items: [ { text: 'TOTP - 一次性密码生成器', link: '/cn/src/ranuts/utils/totp.md' }, { text: 'OCR - 文字识别', link: '/cn/src/ranuts/utils/ocr.md' }, { text: 'createSignal - 创建响应式信号', link: '/cn/src/ranuts/utils/create_signal.md' }, { text: 'setMime - 设置 MIME 类型', link: '/cn/src/ranuts/utils/set_mime.md' }, { text: 'getExtensions - 获取扩展名', link: '/cn/src/ranuts/utils/get_extensions.md' }, { text: 'setAttributeByGlobal - 全局属性', link: '/cn/src/ranuts/utils/set_attribute_by_global.md' }, { text: 'SyncHook - 同步事件钩子', link: '/cn/src/ranuts/utils/sync_hook.md' }, { text: 'durationHandler - 延迟执行', link: '/cn/src/ranuts/utils/duration_handler.md' }, { text: 'task - 统计执行时间', link: '/cn/src/ranuts/utils/task.md' }, ], }, ], }, { text: '文件操作', collapsed: true, items: [ { text: 'writeFile - 写入文件', link: '/cn/src/ranuts/file/write_file.md' }, { text: 'readFile - 读取文件', link: '/cn/src/ranuts/file/read_file.md' }, { text: 'readDir - 读取目录', link: '/cn/src/ranuts/file/read_dir.md' }, { text: 'watchFile - 监听文件变化', link: '/cn/src/ranuts/file/watch_file.md' }, { text: 'queryFileInfo - 查询文件信息', link: '/cn/src/ranuts/file/file_info.md' }, { text: 'appendFile - 追加文件内容', link: '/cn/src/ranuts/file/append_file.md' }, ], }, { text: '事件系统', collapsed: true, items: [{ text: 'EventEmitter - 发布订阅模式', link: '/cn/src/ranuts/mode/subscribe.md' }], }, { text: 'MIME 类型', collapsed: true, items: [{ text: 'getMime - 获取 MIME 类型', link: '/cn/src/ranuts/mime_type/mime_type.md' }], }, { text: '其他', collapsed: true, items: [ { text: '二叉树', link: '/cn/src/ranuts/binary_tree/index.md' }, { text: '打包器', link: '/cn/src/ranuts/bundler/index.md' }, ], }, ], '/cn/src/ranui/': [ { text: 'Overview 总览', link: '/cn/src/ranui/', }, { text: '通用', items: [ { text: 'Button 按钮', link: '/cn/src/ranui/button/' }, { text: 'Icon 图标', link: '/cn/src/ranui/icon/' }, { text: 'Loading 加载中', link: '/cn/src/ranui/loading/' }, ], }, { text: '数据展示', items: [ { text: 'Image 图片', link: '/cn/src/ranui/image/' }, { text: 'Math 数学公式', link: '/cn/src/ranui/math/' }, { text: 'CheckBox 多选框', link: '/cn/src/ranui/checkbox/' }, { text: 'Tabs 标签页', link: '/cn/src/ranui/tabs/' }, { text: 'Preview 预览', link: '/cn/src/ranui/preview/' }, { text: 'Radar 雷达图', link: '/cn/src/ranui/radar/' }, { text: 'Select 选择框', link: '/cn/src/ranui/select/' }, { text: 'Player 视频播放器', link: '/cn/src/ranui/player/' }, { text: 'Progress 进度条', link: '/cn/src/ranui/progress/' }, { text: 'Popover 气泡卡片', link: '/cn/src/ranui/popover/' }, ], }, { text: '数据录入', items: [{ text: 'Input 输入框', link: '/cn/src/ranui/input/' }], }, { text: '反馈', items: [ { text: 'Message 全局提示', link: '/cn/src/ranui/message/' }, { text: 'Skeleton 骨架屏', link: '/cn/src/ranui/skeleton/' }, // { text: 'Modal 对话框', link: '/src/ranui/modal/' }, ], }, ], '/cn/src/article/': [ { items: [ { text: '23 种经典设计模式', link: '/cn/src/article/design_mode.md' }, { text: '函数式编程', link: '/cn/src/article/functional_programming.md', }, { text: 'web 文档预览方案', link: '/cn/src/article/doc_preview.md', }, { text: 'Web 视频加密动态方案', link: '/cn/src/article/video.md', }, { text: '可视化渲染引擎', link: '/cn/src/article/visual.md', }, { text: '排序算法', link: '/cn/src/article/sort/index.md', collapsed: true, items: [ { text: '冒泡排序', link: '/cn/src/article/sort/bubble/' }, { text: '选择排序', link: '/cn/src/article/sort/select/' }, { text: '插入排序', link: '/cn/src/article/sort/insert/' }, { text: '希尔排序', link: '/cn/src/article/sort/shell/' }, { text: '归并排序', link: '/cn/src/article/sort/merge/' }, { text: '快速排序', link: '/cn/src/article/sort/quick/' }, { text: '堆排序', link: '/cn/src/article/sort/heap/' }, { text: '计数排序', link: '/cn/src/article/sort/count/' }, { text: '桶排序', link: '/cn/src/article/sort/bucket/' }, { text: '基数排序', link: '/cn/src/article/sort/radix/' }, ], }, { text: '数学', collapsed: true, items: [{ text: '线性代数', link: '/cn/src/article/math/linear_algebra.md' }], }, ], }, ], }, }; export { themeCnConfig }; ================================================ FILE: packages/docs/.vitepress/langs/en/index.ts ================================================ import type { DefaultTheme } from 'vitepress'; import { GITHUB } from '../../common/index'; const themeEnConfig: DefaultTheme.Config = { logo: '/home.svg', search: { provider: 'local', }, nav: [ { text: 'Home', link: '/' }, { text: 'Function', link: '/src/ranuts/' }, { text: 'Component', link: '/src/ranui/' }, { text: 'Article', link: '/src/article/design_mode.md' }, ], socialLinks: [{ icon: 'github', link: GITHUB }], footer: { message: 'Released under the MIT License.', copyright: 'Copyright © 2022-11-11', }, // algolia: { // appId: 'RDX0Y4AQW1', // apiKey: 'c7b6e28f95335eddc66c5a1b54ad9834', // indexName: 'chaxus_ran', // placeholder: 'search', // }, sidebar: { '/src/ranuts/': [ { text: 'Overview', link: '/src/ranuts/', }, { text: 'Utility Functions', link: '/src/ranuts/utils/', collapsed: false, items: [ { text: 'Functional Programming', collapsed: true, items: [ { text: 'debounce - Debounce function', link: '/src/ranuts/utils/debounce.md' }, { text: 'throttle - Throttle function', link: '/src/ranuts/utils/throttle.md' }, { text: 'memoize - Memoization function', link: '/src/ranuts/utils/memoize.md' }, { text: 'noop - No-op function', link: '/src/ranuts/utils/noop.md' }, { text: 'compose - Compose middleware', link: '/src/ranuts/utils/compose.md' }, ], }, { text: 'String Processing', collapsed: true, items: [ { text: 'md5 - MD5 hash function', link: '/src/ranuts/utils/md5.md' }, { text: 'randomString - Generate random string', link: '/src/ranuts/utils/random_string.md' }, { text: 'clearBr - Remove spaces and line breaks', link: '/src/ranuts/utils/clear_br.md' }, { text: 'clearStr - Remove leading/trailing spaces', link: '/src/ranuts/utils/clear_str.md' }, { text: 'strParse - Parse string to object', link: '/src/ranuts/utils/str_parse.md' }, { text: 'toString - Convert to string', link: '/src/ranuts/utils/to_string.md' }, { text: 'transformText - ArrayBuffer to text', link: '/src/ranuts/utils/transform_text.md' }, { text: 'checkEncoding - Detect encoding', link: '/src/ranuts/utils/check_encoding.md' }, { text: 'changeHumpToLowerCase - Camel to snake', link: '/src/ranuts/utils/change_hump_to_lower_case.md', }, { text: 'getMatchingSentences - Extract sentences', link: '/src/ranuts/utils/get_matching_sentences.md' }, { text: 'isString - Check if string', link: '/src/ranuts/utils/is_string.md' }, { text: 'str2Xml - String to XML', link: '/src/ranuts/utils/str2xml.md' }, ], }, { text: 'Object Processing', collapsed: true, items: [ { text: 'merge - Merge objects', link: '/src/ranuts/utils/merge.md' }, { text: 'isEqual - Deep comparison', link: '/src/ranuts/utils/is_equal.md' }, { text: 'cloneDeep - Deep clone', link: '/src/ranuts/utils/clone_deep.md' }, { text: 'querystring - Object to query string', link: '/src/ranuts/utils/querystring.md' }, { text: 'filterObj - Filter object', link: '/src/ranuts/utils/filter_obj.md' }, { text: 'formatJson - Format JSON', link: '/src/ranuts/utils/format_json.md' }, ], }, { text: 'Number Processing', collapsed: true, items: [ { text: 'range - Clamp number range', link: '/src/ranuts/utils/range.md' }, { text: 'mathjs - Precise calculation', link: '/src/ranuts/utils/mathjs.md' }, { text: 'perToNum - Percentage to number', link: '/src/ranuts/utils/per_to_num.md' }, { text: 'transformNumber - Format number', link: '/src/ranuts/utils/transform_number.md' }, { text: 'addNumSym - Add sign to number', link: '/src/ranuts/utils/add_num_sym.md' }, ], }, { text: 'Color Processing', collapsed: true, items: [ { text: 'hexToRgb - Hex to RGB', link: '/src/ranuts/utils/hex_to_rgb.md' }, { text: 'rgbToHex - RGB to hex', link: '/src/ranuts/utils/rgb_to_hex.md' }, { text: 'randomColor - Generate random color', link: '/src/ranuts/utils/random_color.md' }, ], }, { text: 'Time Processing', collapsed: true, items: [ { text: 'timeFormat - Format time', link: '/src/ranuts/utils/time_format.md' }, { text: 'timestampToTime - Timestamp to Date', link: '/src/ranuts/utils/timestamp_to_time.md' }, { text: 'performanceTime - High precision timestamp', link: '/src/ranuts/utils/performance_time.md' }, ], }, { text: 'Device Detection', collapsed: true, items: [ { text: 'isMobile - Check mobile device', link: '/src/ranuts/utils/is_mobile.md' }, { text: 'isWeiXin - Check WeChat browser', link: '/src/ranuts/utils/is_weixin.md' }, { text: 'isClient - Check client environment', link: '/src/ranuts/utils/is_client.md' }, { text: 'isSafari - Check Safari browser', link: '/src/ranuts/utils/is_safari.md' }, { text: 'isBangDevice - Check notch device', link: '/src/ranuts/utils/is_bang_device.md' }, { text: 'currentDevice - Get device type', link: '/src/ranuts/utils/current_device.md' }, ], }, { text: 'DOM Manipulation', collapsed: true, items: [ { text: 'addClassToElement - Add class', link: '/src/ranuts/utils/add_class_to_element.md' }, { text: 'removeClassToElement - Remove class', link: '/src/ranuts/utils/remove_class_to_element.md' }, { text: 'createDocumentFragment - Create fragment', link: '/src/ranuts/utils/create_document_fragment.md', }, { text: 'escapeHtml - Escape HTML', link: '/src/ranuts/utils/escape_html.md' }, { text: 'Chain - Chainable DOM', link: '/src/ranuts/utils/chain.md' }, { text: 'create - Create DOM element', link: '/src/ranuts/utils/create.md' }, ], }, { text: 'Storage', collapsed: true, items: [ { text: 'localStorageGetItem - Get storage', link: '/src/ranuts/utils/local_storage.md' }, { text: 'localStorageSetItem - Set storage', link: '/src/ranuts/utils/local_storage.md' }, ], }, { text: 'URL/Query', collapsed: true, items: [ { text: 'getAllQueryString - Extract query', link: '/src/ranuts/utils/get_all_query_string.md' }, { text: 'getQuery - Extract query', link: '/src/ranuts/utils/get_query.md' }, { text: 'encodeUrl - Encode URL safely', link: '/src/ranuts/utils/encode_url.md' }, { text: 'appendUrl - Append query params', link: '/src/ranuts/utils/append_url.md' }, ], }, { text: 'Cookie', collapsed: true, items: [ { text: 'getCookie - Get cookie', link: '/src/ranuts/utils/get_cookie.md' }, { text: 'getCookieByName - Get cookie by regex', link: '/src/ranuts/utils/get_cookie_by_name.md' }, ], }, { text: 'Image Processing', collapsed: true, items: [ { text: 'convertImageToBase64 - Image to Base64', link: '/src/ranuts/utils/convert_image_to_base64.md' }, { text: 'isImageSize - Validate image size', link: '/src/ranuts/utils/is_image_size.md' }, ], }, { text: 'Performance', collapsed: true, items: [ { text: 'getPerformance - Get performance metrics', link: '/src/ranuts/utils/get_performance.md' }, { text: 'getFrame - Calculate frame rate', link: '/src/ranuts/utils/get_frame.md' }, { text: 'getPixelRatio - Get resolution ratio', link: '/src/ranuts/utils/get_pixel_ratio.md' }, ], }, { text: 'Network', collapsed: true, items: [ { text: 'imageRequest - Test network latency', link: '/src/ranuts/utils/image_request.md' }, { text: 'networkSpeed - Test network speed', link: '/src/ranuts/utils/network_speed.md' }, { text: 'connection - Get network info', link: '/src/ranuts/utils/connection.md' }, ], }, { text: 'Browser', collapsed: true, items: [ { text: 'getWindow - Get window size', link: '/src/ranuts/utils/get_window.md' }, { text: 'getHost - Get host address', link: '/src/ranuts/utils/get_host.md' }, { text: 'createObjectURL - Create object URL', link: '/src/ranuts/utils/create_object_url.md' }, { text: 'removeGhosting - Remove drag shadow', link: '/src/ranuts/utils/remove_ghosting.md' }, { text: 'retain - Override back event', link: '/src/ranuts/utils/retain.md' }, ], }, { text: 'Script Loading', collapsed: true, items: [{ text: 'scriptOnLoad - Dynamic script loading', link: '/src/ranuts/utils/script_on_load.md' }], }, { text: 'Error Handling', collapsed: true, items: [ { text: 'handleConsole - Intercept console', link: '/src/ranuts/utils/handle_console.md' }, { text: 'handleError - Global error handling', link: '/src/ranuts/utils/handle_error.md' }, { text: 'handleFetchHook - Intercept fetch', link: '/src/ranuts/utils/handle_fetch_hook.md' }, ], }, { text: 'Others', collapsed: true, items: [ { text: 'TOTP - One-time password generator', link: '/src/ranuts/utils/totp.md' }, { text: 'OCR - Text recognition', link: '/src/ranuts/utils/ocr.md' }, { text: 'createSignal - Create reactive signal', link: '/src/ranuts/utils/create_signal.md' }, { text: 'setMime - Set MIME type', link: '/src/ranuts/utils/set_mime.md' }, { text: 'getExtensions - Get extensions', link: '/src/ranuts/utils/get_extensions.md' }, { text: 'setAttributeByGlobal - Global attribute', link: '/src/ranuts/utils/set_attribute_by_global.md' }, { text: 'SyncHook - Sync event hook', link: '/src/ranuts/utils/sync_hook.md' }, { text: 'durationHandler - Delayed execution', link: '/src/ranuts/utils/duration_handler.md' }, { text: 'task - Statistical execution time', link: '/src/ranuts/utils/task.md' }, ], }, ], }, { text: 'File Operations', collapsed: true, items: [ { text: 'writeFile - Write to file', link: '/src/ranuts/file/write_file.md' }, { text: 'readFile - Read file', link: '/src/ranuts/file/read_file.md' }, { text: 'readDir - Read directory', link: '/src/ranuts/file/read_dir.md' }, { text: 'watchFile - Watch file changes', link: '/src/ranuts/file/watch_file.md' }, { text: 'queryFileInfo - Query file info', link: '/src/ranuts/file/file_info.md' }, { text: 'appendFile - Append to file', link: '/src/ranuts/file/append_file.md' }, ], }, { text: 'Event System', collapsed: true, items: [{ text: 'EventEmitter - Publish-subscribe', link: '/src/ranuts/mode/subscribe.md' }], }, { text: 'MIME Type', collapsed: true, items: [{ text: 'getMime - Get MIME type', link: '/src/ranuts/mime_type/mime_type.md' }], }, { text: 'Others', collapsed: true, items: [ { text: 'Binary Tree', link: '/src/ranuts/binary_tree/index.md' }, { text: 'Bundler', link: '/src/ranuts/bundler/index.md' }, ], }, ], '/src/ranui/': [ { text: 'Overview ', link: '/src/ranui/', }, { text: 'Common', items: [ { text: 'Button', link: '/src/ranui/button/' }, { text: 'Icon', link: '/src/ranui/icon/' }, { text: 'Loading', link: '/src/ranui/loading/' }, ], }, { text: 'Data Presentation', items: [ { text: 'Image', link: '/src/ranui/image/' }, { text: 'Math', link: '/src/ranui/math' }, { text: 'CheckBox', link: '/src/ranui/checkbox/' }, { text: 'Tabs', link: '/src/ranui/tabs/' }, { text: 'Preview', link: '/src/ranui/preview/' }, { text: 'Radar', link: '/src/ranui/radar/' }, { text: 'Select', link: '/src/ranui/select/' }, { text: 'Player', link: '/src/ranui/player/' }, { text: 'Progress', link: '/src/ranui/progress/' }, { text: 'Popover', link: '/src/ranui/popover/' }, ], }, { text: 'Data Entry', items: [{ text: 'Input', link: '/src/ranui/input/' }], }, { text: 'Feedback', items: [ { text: 'Message', link: '/src/ranui/message/' }, { text: 'Skeleton', link: '/src/ranui/skeleton/' }, // { text: 'Modal 对话框', link: '/src/ranui/modal/' }, ], }, ], '/src/article/': [ { items: [ { text: '23 classic design patterns', link: '/src/article/design_mode.md', }, { text: 'Functional programming', link: '/src/article/functional_programming.md', }, { text: 'Sorting algorithm', link: '/src/article/sort/index.md', collapsed: true, items: [ { text: 'bubble sort', link: '/src/article/sort/bubble/' }, { text: 'selection sort', link: '/src/article/sort/select/' }, { text: 'insertion sort', link: '/src/article/sort/insert/' }, { text: 'shell sort', link: '/src/article/sort/shell/' }, { text: 'Merge sort', link: '/src/article/sort/merge/' }, { text: 'Quick sort', link: '/src/article/sort/quick/' }, { text: 'Heap Sort', link: '/src/article/sort/heap/' }, { text: 'Counting sort', link: '/src/article/sort/count/' }, { text: 'Bucket sort', link: '/src/article/sort/bucket/' }, { text: 'Radix sort', link: '/src/article/sort/radix/' }, ], }, ], }, ], }, }; export { themeEnConfig }; ================================================ FILE: packages/docs/.vitepress/lib/constant.ts ================================================ export enum INPUT_STATUS { NORMAL = 'normal', ERROR = 'error', WARNING = 'warning', } // 支持语言包字典 export enum LANGS_DICT { EN = 'en', // 英文 ZH_CN = 'zh-CN', // 简体中文 } // localStorage 中的多语言标识 export const RAN_CHAXUS_LANG = 'ran_chaxus_lang'; export const LOADED_LOCALES: string[] = []; export const LANG_MESSAGES = { 'zh-CN': { lang: '简体中文', }, en: { lang: 'English', }, }; export enum I18N_MODE { LEGACY = 'legacy', } export const PWA_INSTALL_ID = 'PWA_INSTALL_ID'; export const PWA_ELEMENT_NAME = 'pwa-install'; export const MANIFEST_PATH_ROOT = '/ran/manifest.json'; ================================================ FILE: packages/docs/.vitepress/plugins/env.ts ================================================ import { currentDevice, isBangDevice, isMobile, isWeiXin } from 'ranuts/utils'; import type { App } from 'vue'; import type { CurrentDevice } from 'ranuts/utils'; import { LANGS_DICT } from '../lib/constant'; // env 信息 const isDev = process.env.NODE_ENV !== 'production'; // debug 开关信息 // const { debug = '' } = getAllQueryString() || {}; export interface Env { isDev: boolean; locale: LANGS_DICT | string; currentDevice: CurrentDevice; isWeiXin: boolean; isMobile: boolean; isBang: boolean; } export const $env: Env = { isDev, locale: LANGS_DICT.EN, currentDevice: currentDevice(), isWeiXin: isWeiXin(), isMobile: isMobile(), isBang: isBangDevice(), // 是否是刘海机型 // theme: localStorage.getItem("ran-chaxus-theme") }; export default { install: (app: App): void => { app.config.globalProperties.$env = $env; app.provide('$env', $env); }, }; ================================================ FILE: packages/docs/.vitepress/theme/index.ts ================================================ import DefaultTheme from 'vitepress/theme'; import type { EnhanceAppContext } from 'vitepress'; import { localStorageGetItem, setAttributeByGlobal } from 'ranuts/utils'; import env from '../plugins/env'; import TOTP from '../components/TOTP.vue'; import Layout from '../components/Layout.vue'; import i18n, { loadLanguageAsync } from '../lang'; import { LANGS_DICT, MANIFEST_PATH_ROOT, PWA_ELEMENT_NAME, PWA_INSTALL_ID, RAN_CHAXUS_LANG } from '../lib/constant'; import './styles/index.less'; import './styles/vars.less'; import './tailwind.min.css'; import 'ranui/style'; declare global { interface ImportMeta { env: { SSR: boolean; }; } } /** * @description: pwa 引导安装 */ const pwaInstall = () => { import('@khmyznikov/pwa-install').then(() => { let pwaInstall = document.getElementById(PWA_INSTALL_ID); if (!pwaInstall) { pwaInstall = document.createElement(PWA_ELEMENT_NAME); pwaInstall.setAttribute('manifest-url', MANIFEST_PATH_ROOT); pwaInstall.setAttribute('id', PWA_INSTALL_ID); document.body.appendChild(pwaInstall); } }); }; export default { extends: DefaultTheme, // Layout, enhanceApp({ app }: EnhanceAppContext): void { if (!import.meta.env.SSR) { import('ranui'); import('@ranui/preview'); pwaInstall(); } app.use(env); const locale = localStorageGetItem(RAN_CHAXUS_LANG) || LANGS_DICT.EN; loadLanguageAsync(locale) .then(() => { setAttributeByGlobal('__VUE_PROD_DEVTOOLS__', false); app.use(i18n); app.component('Layout', Layout); app.component('TOTP', TOTP); }) .catch((error) => { console.log('error', error); }); }, }; ================================================ FILE: packages/docs/.vitepress/theme/styles/index.less ================================================ #app .VPNavBar { border-bottom: 1px solid var(--vp-c-divider-light); } #app .VPHomeHero { margin-top: 20px; margin-bottom: 35px; .main { margin-top: -40px; } } #app .VPNavBarTitle { .title { color: transparent; background: var(--vp-home-hero-name-background); -webkit-background-clip: text; } } .font-icon { padding: 2px 8px; border-radius: 4px; display: inline-block; background-color: var(--vp-c-gray-light-4); } .dark .font-icon { background-color: var(--vp-c-bg); } p img { margin-top: 2px; } @media (max-width: 960px) { #app .image { margin-top: -75px; } #app .VPHomeHero { margin-bottom: 5px; .main { margin-top: 50px; } } } @media (min-width: 960px) { } ================================================ FILE: packages/docs/.vitepress/theme/styles/vars.less ================================================ /** * Colors * -------------------------------------------------------------------------- */ :root { --vp-c-brand: #646cff; --vp-c-brand-light: #747bff; --vp-c-brand-lighter: #9499ff; --vp-c-brand-lightest: #bcc0ff; --vp-c-brand-dark: #535bf2; --vp-c-brand-darker: #454ce1; --vp-c-brand-dimm: rgba(100, 108, 255, 0.08); } /** * Component: Button * -------------------------------------------------------------------------- */ :root { --vp-button-brand-border: var(--vp-c-brand-light); --vp-button-brand-bg: var(--vp-c-brand); --vp-button-brand-text: #fff; --vp-button-brand-hover-border: var(--vp-c-brand-light); --vp-button-alt-hover-text: var(--vp-c-brand); --vp-button-brand-hover-bg: var(--vp-c-brand-lighter); --vp-button-brand-hover-border: var(--vp-c-brand-lighter); --vp-button-brand-active-border: var(--vp-c-brand-light); --vp-button-brand-active-text: var(--vp-c-text-dark-1); --vp-button-brand-active-bg: var(--vp-button-brand-bg); } /** * Component: Home * -------------------------------------------------------------------------- */ :root { --vp-home-hero-name-color: transparent; --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe 30%, #41d1ff); --vp-home-hero-image-background-image: linear-gradient(-45deg, #bd34fe 50%, #47caff 50%); --vp-home-hero-image-filter: blur(40px); } @media (min-width: 640px) { :root { --vp-home-hero-image-filter: blur(56px); } } @media (min-width: 960px) { :root { --vp-home-hero-image-filter: blur(72px); } } /** * Component: Custom Block * -------------------------------------------------------------------------- */ :root { --vp-custom-block-tip-border: var(--vp-c-brand); --vp-custom-block-tip-text: var(--vp-c-brand-darker); --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); } .dark { --vp-custom-block-tip-border: var(--vp-c-brand); --vp-custom-block-tip-text: var(--vp-c-brand-lightest); --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); } /** * Component: Algolia * -------------------------------------------------------------------------- */ .DocSearch { --docsearch-primary-color: var(--vp-c-brand) !important; } /** * VitePress: Custom fix * -------------------------------------------------------------------------- */ /* Use lighter colors for links in dark mode for a11y. Also specify some classes twice to have higher specificity over scoped class data attribute. */ .dark .vp-doc a, .dark .vp-doc a > code, .dark .VPNavBarMenuLink.VPNavBarMenuLink:hover, .dark .VPNavBarMenuLink.VPNavBarMenuLink.active, .dark .link.link:hover, .dark .link.link.active, .dark .edit-link-button.edit-link-button, .dark .pager-link .title { color: var(--vp-c-brand-lighter); } .dark .vp-doc a:hover, .dark .vp-doc a > code:hover { color: var(--vp-c-brand-lightest); opacity: 1; } /* Transition by color instead of opacity */ .dark .vp-doc .custom-block a { transition: color 0.25s; } ================================================ FILE: packages/docs/.vitepress/theme/theme.less ================================================ :root { --bg-color: #f5f6f6; --front-bg-color-2: #fafafa; --front-bg-color-1: #ffffff; --front-bg-color1: #ffffff; --front-bg-color: #f5f6f6; --tag-bg-color: #f5f6f6; --line-color: #eaebec; --tag-border-color: #c7cacc; --text-color-3: #afb3b6; --text-color-2: #82888d; --text-color-2-raw: 130, 136, 141; --text-color-1-supplement: #464f56; --text-color-1: #000000; --link-text-color: #37a0ff; --environment-text-color-3: #ffffff; --environment-text-color-2: #f5f6f6; --environment-text-color-1: #1f2a32; --wtt-brand-color: #5622cf; --brand-color: #37a0ff; --brand-color-raw: 55, 160, 255; --brand-color-2: #ff4040; --brand-color-3: #ffc700; --up-color: #ff5000; --down-color: #00b99a; --list-bg-color: #ffffff; --green-color: #00b99a; --red-color: #ff5000; } :root[theme='dark'] { --bg-color: #07121c; --front-bg-color-2: #141f28; --front-bg-color-1: #1a252e; --front-bg-color1: #1a252e; --front-bg-color: #2a343c; --tag-bg-color: #2a343c; --line-color: #384149; --tag-border-color: #424b53; --text-color-3: #61686e; --text-color-2: #82888d; --text-color-2-raw: 130, 136, 141; --text-color-1-supplement: #cfd1d3; --text-color-1: #ffffff; --link-text-color: #37a0ff; --environment-text-color-3: #ffffff; --environment-text-color-2: #f5f6f6; --environment-text-color-1: #1f2a32; --wtt-brand-color: #5622cf; --brand-color: #37a0ff; --brand-color-2: #ff4040; --brand-color-3: #ffc700; --up-color: #ff5000; --down-color: #00b99a; --list-bg-color: #2a343c; --green-color: #00b99a; --red-color: #ff5000; } ================================================ FILE: packages/docs/bin/build.sh ================================================ #!/bin/bash # 更新 service work的版本号 version=$(date +%s) # 将版本号写入 variable 目录下 SERVICE_WORK_VERSION.ts SERVICE_WORK_VERSION="./variable/SERVICE_WORK_VERSION.ts" echo "export const SERVICE_WORK_VERSION = \"$version\"" > $SERVICE_WORK_VERSION # 执行 ssg 构建命令 bin=./node_modules/.bin $bin/vitepress build # 开启调试模式 # set -x # 指定输出的目录 dir="./.vitepress/dist" # 生成的目标文件 target="./.vitepress/dist/sw.js" # 改名 mv "$target" "./.vitepress/dist/sw$version.js" target="./.vitepress/dist/sw$version.js" # 创建一个临时文件 tmpfile=$(mktemp) # 将目录 dir 下的文件名追加到临时文件中 find "$dir" -type f > "$tmpfile" # service worker中生成 # SERVICE_WORK_CACHE_FILE_PATHS(根据打包后生成的文件来生成) # VERSION (时间戳) # 的临时文件 SERVICE_WORK_VARABLE="./.vitepress/dist/sw-file.js" # 拼接字符串 echo "const SERVICE_WORK_CACHE_FILE_PATHS = [" > "$SERVICE_WORK_VARABLE" # 根路径 ran="/ran" while read -r file; do # if [[ $file != *".DS_Store"* ]]; then str="${file##./.vitepress/dist}" echo "\"$ran$str\"," >> "$SERVICE_WORK_VARABLE" # fi done < "$tmpfile" # 拼接字符串 echo "];" >> "$SERVICE_WORK_VARABLE" # 更新 sw 的版本号 echo "const VERSION = \"$version\";" >> "$SERVICE_WORK_VARABLE" # 删除临时文件 rm "$tmpfile" tmpfile=$(mktemp) cat "$SERVICE_WORK_VARABLE" >> "$tmpfile" cat "$target" >> "$tmpfile" mv "$tmpfile" "$target" rm "$SERVICE_WORK_VARABLE" # # 打印完成消息 echo "service work file paths have been generate for $target" # 关闭调试模式 # set +x ================================================ FILE: packages/docs/cn/index.md ================================================ --- layout: home title: Home hero: name: ran text: 风起于青萍之末 tagline: A ship in harbor is safe, but that is not what ships are built for. image: src: /home.svg alt: logo actions: - theme: alt text: 更多详情 link: https://github.com/chaxus/ran - theme: alt text: 访问我的 GitHub link: https://github.com/chaxus/ran features: - icon: ⚡️ title: 记录 details: 每段旅程都有终点 - icon: 🛠️ title: 解决 details: 无法衡量,无法改进 - icon: 🖖 title: 分享 details: 一个人可以走得很快,一群人可以走的很远 --- ================================================ FILE: packages/docs/cn/src/article/ai/index.md ================================================ # Prompt ## System Prompt 用来描述 AI 的角色,性格,背景知识,语气等,总之只要不是用户直接说出来的内容,都可以放到 System Prompt 里面。 ## User Prompt 用户直接发送给 AI 的内容。 每次用户发送 User Prompt 的时候,系统会自动把 System Prompt 也一起发起 AI 模型,这样就会显得非常自然。 但到了这个程度,AI 也只是一个聊天机器人,只能问问题,然后 AI 模型去回复问题。 那么如何让 AI 模型去自动的执行一些任务呢?这时候,Agent 就出现了。 # Agent 比如期望用 AI 来管理一些文件,那么得先写好一些文件管理函数: - list_file 列出目录下的所有文件 - read_file 读取文件的内容 然后把这些函数及其他们的功能描述,使用方法,注册到 Agent 中。 Agent 会根据这些信息,生成一个 System Prompt,告诉 AI 模型,用户给了哪些工具,能够做什么。 以及 AI 使用他们应该返回什么样的格式。 当用户在发送 User Prompt 的时候,连同 System Prompt,一起发送给 AI 模型。 如果 AI 模型足够聪明,就会返回一个格式:需要调用某个函数,比如 list_file 给 Agent。 Agent 解析之后,就会调用对应的函数,然后把结果再给 AI,AI 再根据 Agent 返回的结果,再决定下一步去做什么操作。 这个过程就这样反复,直到任务完成为止。 最后,把这种在 AI 模型,提供的工具 (list_file,read_file),和最终用户之间传话的工具,就叫 AI Agent。 这些提供给 AI Agent 调用的函数,或者服务,就叫 Agent Tool。 但这样可能存在一个问题,虽然我们在 System Prompt 里面描述和规定了 AI 应该用什么格式进行返回,但 AI 模型说到底是一个概率模型。还是有可能返回的格式不对。 一般的 AI Agent 会判断,如果 AI 模型返回的格式不对,会自动进行重试。面对这种场景,FunctionCalling 就出现了。 # FunctionCalling 核心作用就是统一格式,规范描述,比如上面的 System Prompt,使用自然语言描述的,AI 看的懂就行。 FunctionCalling 则对这些描述进行了标准化,每一个 Tool 都用一个 json 来定义,比如: ```json { "name": "list_file", "desc": "列出目录下的所有文件", "params": { "path": "str" } } ``` 然后这些字段也从 System Prompt 中剥离了出来,这样,所有的工具定义,描述,和返回都放在了相同的地方。这样 AI 使用工具时,也会遵循相同 json 格式进行回复。于是人们就可以更加有针对性的训练 AI 模型。甚至在这种情况下,如果 AI 依然返回了错误的回复,因为这种返回的格式是固定的,AI 服务端自己就能检测到,然后进行重试。这样用户根本感觉不到,降低了用户端的开发难度,同时也节约了重试的 token 成本。 但 FunctionCalling 也有自己的问题,那就是没有统一的标准。目前每家大厂的 API 格式都不太一样。甚至还有一些模型,不支持 FunctionCalling,所以,要写一个通用的 AI Agent,还是挺麻烦的。 因此 FunctionCalling 和 System Prompt 这两种方式,在市面上都是并存的。 以上都是 AI Agent 如何和 AI 模型之间的通信,那么 Agent Tool 和 Agent 是怎么通信的呢。 # MCP 一般都是把 Agent 和 Agent Tool 写到一个程序里,这样就能直接调用,搞定。 但后来人们发现,有些 Agent Tool 还是挺通用的,比如浏览网页的功能,每个 Agent 都需要,那么总不能在每个 Agent 里面都拷贝一份吧。于是,就想到来一个办法: 把 Tool 做成一个服务,进行统一的托管。让所有的 Agent 都来调用,这个过程,就是 MCP。 MCP 是一个通信协议,专门用来规范 Agent 和 Tool 服务之间是怎么交互的。 运行 Tool 的服务叫 MCP Server,调用它的 Agent 叫 MCP Client。 MCP 规定了 MCP Server 如何和 MCP Client 进行通信,以及 MCP Server 要提供哪些接口,比如查询 MCP Server 有哪些接口,接口的功能,描述,如何使用。除了普通的 Tool 这种函数的调用形式。 MCP 也可以直接提供数据,提供文件读取的服务 Resources,或者为 Agent 提供提示词的模版叫 Prompt。 MCP Server 既可以和 Agent 跑在同一台机器上,通过标准输入输出进行通信,也可以部署在网络上,通过 http 进行通信。 虽然 MCP 是为了 AI 而定制出来的标准,但实际上,MCP 本身和 AI 模型没有关系,它并不关心 Agent 用的是哪个模型,MCP 只负责帮 Agent 管理工具,资源和提示词。 最后,总结一下全部的流程: 用户 --> 发送消息给 Agent,Agent(MCP Client)调用 MCP Server 的函数,并把结果一起给 AI 模型。 AI 模型通过 FunctionCalling 或者普通回复的方式,产生调用 Tool 的请求,Agent 收到这个请求后,通过 MCP 协议去调用 MCP Server 的工具。将结果返回给 Agent,Agent 再把结果给 AI 模型,AI 模型再把最终结果,返回个 Agent,Agent 再发送给用户。 # 写一个 Agent Agent 是一个在用户,AI 模型,工具函数之间进行传话的程序。 ```mermaid sequenceDiagram Tools->>Agent: 将 Tools 里的函数注册到 Agent,让 Agent 知道有哪些工具函数可以使用 User->>Agent: 将问题发送给 Agent Agent->>AI: 用户的问题是 User Prompt,还有将工具函数信息通过 System Prompt 或者 FunctionCalling 的方式告诉 AI 模型 AI->>Agent: 思考用户的问题,并返回响应的指令,告诉 Agent 应该执行 Tools 中的哪些方法。 Agent->>Tools: 使用 Tools 中的方法,并获得结果 Agent->>AI: 将结果告诉给 AI 模型,AI 模型继续分析问题,看是否需要继续调用 Agent,直到结束。 AI->>Agent: 告诉 Agent 思考过程结束,并将结果发送给 Agent。 Agent->>User: 将最终结果给用户输出。 ``` 简单来说,就是下面这种方式 ```mermaid graph TD customer[用户] --> Agent Agent --> AI Tools[工具函数] --> Agent Agent --> customer[用户] Agent --> Tools[工具函数] Agent --> AI ``` ================================================ FILE: packages/docs/cn/src/article/ast_parse/tokenizer.md ================================================ # Abstract Syntax Tree ## 一.(`abstract syntax tree`)抽象语法树的作用 源码是一串按照语法格式来组织的字符串,人能够认识,但是计算机并不认识,想让计算机认识就要转成一种数据结构,通过不同的对象来保存不同的数据,并且按照依赖关系组织起来,这种数据结构就是抽象语法树(`abstract syntax tree`)。 之所以叫“抽象”语法树是因为数据结构中省略掉了一些无具体意义的分隔符比如 `; { }` 等。 有了 `AST`,计算机就能理解源码字符串的意思,而理解是能够转换的前提,所以编译的第一步需要把源码 `parse` 成 `AST`。 转成 `AST` 之后就可以通过修改 `AST` ,分析 `AST` 的方式来修改和分析代码,比如 `babel` 就通过这种方式进行代码的转换,比如 `rollup` 的 `Tree Shaking` ,就是通过分析 `AST`的 导入导出语法,从而分析出没有使用的代码,进行去除。 ## 二。常见的 AST 节点 常见的 AST 节点 AST 是对源码的抽象,字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。 我们分别来了解一下: ### Literal `Literal` 是字面量的意思,比如 `let name = 'value'`中,`'value'`就是一个字符串字面量 `StringLiteral`,相应的还有数字字面量 `NumericLiteral`,布尔字面量 `BooleanLiteral`,字符串字面量 `StringLiteral`,正则表达式字面量 `RegExpLiteral` 等。 下面这些字面量都有对应的 `Literal` 节点: ![](../../../../assets//ranuts//astParse//Literal.jpeg) 代码中的字面量很多,`babel` 就是通过 `xxLiteral` 来抽象这部分内容的。 ### Identifier `Identifer` 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是`Identifer`。 我们知道, `JS` 中的标识符只能包含字母或数字或下划线 `(“_”)` 或美元符号 `(“$”)` ,且不能以数字开头。这是 `Identifier` 的词法特点。 尝试分析一下,下面这一段代码里面有多少 `Identifier` 呢? ```js const name = 'value'; function say(name) { console.log(name); } const obj = { name: 'guang', }; ``` 答案是这些 ![](../../../../assets//ranuts//astParse//Identifier.jpeg) ### Statement `statement` 是语句,它是可以独立执行的单位,比如 `break、continue、debugger、return` 或者 `if` 语句、`while` 语句、`for` 语句,还有声明语句,表达式语句等。我们写的每一条可以独立执行的代码都是语句。 语句末尾一般会加一个分号分隔,或者用换行分隔。 下面这些我们经常写的代码,每一行都是一个 `Statement`: ```js break; continue; return; debugger; throw Error(); {} try {} catch(e) {} finally{} for (let key in obj) {} for (let i = 0;i < 10;i ++) {} while (true) {} do {} while (true) switch (v){case 1: break;default:;} label: console.log(); with (a){} ``` 它们对应的 AST 节点如下图所示: ![](../../../../assets//ranuts//astParse//Statement.jpeg) 语句是代码执行的最小单位,可以说,代码是由语句 `(Statement)` 构成的。 ### Declaration 声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、 `class、import、export` 等。 比如下面这些语句都是声明语句: ```js const a = 1; function b() {} class C {} import d from 'e'; export default e = 1; export { e }; export * from 'e'; ``` 它们对应的 AST 节点如下图: ![](../../../../assets//ranuts//astParse//Declaration.jpeg) 声明语句用于定义变量,这也是代码中一个基础组成部分。 ### Expression `expression` 是表达式,特点是执行完以后有返回值,这是和语句 (`statement`) 的区别。 下面是一些常见的表达式 ```js [1,2,3] a = 1 1 + 2; -1; function(){}; () => {}; class{}; a; this; super; a::b; ``` 它们对应的 AST 如图: ![](../../../../assets//ranuts//astParse//Expression.jpeg) 细心的同学可能会问 `identifier` 和 `super` 怎么也是表达式呢? 因为 `identifier、super` 有返回值,符合表达式的特点,所以也是 `expression` 。 我们判断 `AST` 节点是不是某种类型要看它是不是符合该种类型的特点,比如语句的特点是能够单独执行,表达式的特点是有返回值。 有的表达式可以单独执行,符合语句的特点,所以也是语句,比如赋值表达式、数组表达式等。 ```js a = 1; [1, 2, 3]; ``` 但有的表达式不能单独执行,需要和其他类型的节点组合在一起构成语句。 比如匿名函数表达式和匿名 `class` 表达式单独执行会报错: ```js function(){}; class{} ``` 需要和其他部分一起构成一条语句,比如组成赋值语句: ```js a = function () {}; b = class {}; ``` 这条赋值语句对应的 `AST` 是这样的: ![](../../../../assets//ranuts//astParse//ExpressionStatement.jpeg) 你会发现赋值语句的 AST 节点 `AssignmentExpression` 包裹了一层 `ExpressionStatement` 的节点,代表这个表达式是被当成语句执行的。 ### Class `class` 的语法也有专门的 AST 节点来表示。 整个 `class` 的内容是 `ClassBody` ,属性是 `ClassProperty` ,方法是 `ClassMethod` (通过 `kind` 属性来区分是 `constructor` 还是 `method` )。 比如下面的代码 ```js class Guang extends Person { name = 'guang'; constructor() {} eat() {} } ``` 对应的 AST 是这样的 ![](../../../../assets//ranuts//astParse//Class.jpeg) `class` 是 `es next` 的语法, `babel` 中有专门的 `AST` 来表示它的内容。 ### Modules `es module` 是语法级别的模块规范,所以也有专门的 `AST` 节点。 **import** `import` 有 3 种语法: `named import`: ```js import { c, d } from 'c'; ``` `default import`: ```js import a from 'a'; ``` `namespaced import`: ```js import * as b from 'b'; ``` 这 3 种语法都对应 `ImportDeclaration` 节点,但是 `specifiers` 属性不同,分别对应 `ImportSpicifier` `、ImportDefaultSpecifier` `、ImportNamespaceSpcifier` 。 ![](../../../../assets//ranuts//astParse//import.jpeg) 图中黄框标出的就是 `specifier` 部分。可以直观的看出整体结构相同,只是 `specifier` 部分不同,所以 `import` 语法的 `AST` 的结构是 `ImportDeclaration` 包含着各种 `import specifier` 。 **export** `export` 也有 3 种语法: `named export`: ```js export { b, d }; ``` `default export`: ```js export default a; ``` `all export`: ```js export * from 'c'; ``` 分别对应 `ExportNamedDeclaration` `、ExportDefaultDeclaration` `、ExportAllDeclaration` 的 `AST` 。 比如这三种 `export` ```js export { b, d }; export default a; export * from 'c'; ``` 对应的 AST 节点为 ![](../../../../assets//ranuts//astParse//export.jpeg) ### Program & Directive `program` 是代表整个程序的节点,它有 `body` 属性代表程序体,存放 `statement` 数组,就是具体执行的语句的集合。还有 `directives` 属性,存放 `Directive` 节点,比如 `"use strict"` 这种指令会使用 `Directive` 节点表示。 ![](../../../../assets//ranuts//astParse//Program.jpeg) `Program` 是包裹具体执行语句的节点,而 `Directive` 则是代码中的指令部分。 ### File & Comment `babel` 的 `AST` 最外层节点是 `File` ,它有 `program` 、 `comments` 、 `tokens` 等属性,分别存放 `Program` 程序体、注释、 `token` 等,是最外层节点。 注释分为块注释和行内注释,对应 `CommentBlock` 和 `CommentLine` 节点。 ![](../../../../assets//ranuts//astParse//File.jpeg) 上面 6 种就是常见的一些 `AST` 节点类型, `babel` 就是通过这些节点来抽象源码中不同的部分。 ### `AST` 可视化查看工具 这么多 `AST` 我们都要记住么? 不需要。可以通过 `axtexplorer.net` 这个网站来可视化的查看。 ![](../../../../assets//ranuts//astParse//axtexplorer.jpeg) 这个网站可以查看代码 `parse` 以后的 `AST` ,可以切换 `parse` 的语言和用的 `parser` ,也可以修改 `parse options` 。 点击这里的 `save` 就可以保存下来,然后把 `url` 分享出去: ![](../../../../assets//ranuts//astParse//axtexplorerSave.jpeg) 比如这个链接:`https://astexplorer.net/` 如果想查看全部的 `AST` 可以在 `babel parser` 仓库里的 `AST` 文档里查,或者直接去看 `@babel/types` 的 `typescript` 类型定义。 ### AST 的公共属性 每种 `AST` 都有自己的属性,但是它们也有一些公共的属性: `type`: `AST` 节点的类型 `start` 、 `end` 、`loc:start` 和 `end` 代表该节点在源码中的开始和结束下标。而 `loc` 属性是一个对象,有 `line` 和 `column` 属性分别记录开始和结束的行列号。 `leadingComments` 、 `innerComments` 、 `trailingComments` :表示开始的注释、中间的注释、结尾的注释,每个 `AST` 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个 AST 的注释就通过这三个属性。 比如这段有注释的代码的 `AST` : ![](../../../../assets//ranuts//astParse//Comment.jpeg) `extra:`记录一些额外的信息,用于处理一些特殊情况。比如 `StringLiteral` 的 `value` 只是值的修改,而修改 `extra.raw` 则可以连同单双引号一起修改。 比如这段代码的 `AST`: ![](../../../../assets//ranuts//astParse//extra.jpeg) 修改 `value` 只能修改值,修改 `extra.raw` 可以连引号一起修改。 ### 总结 了解了这些节点,就能知道平时写的代码是怎么用 `AST` 表示的。 当然也不需要记,可以用 `(astexpoler.net)` 可视化的查看。 `AST` 节点可能同时有多种类型,确定一种 AST 节点是什么类型主要看它的特点,比如 `Statement` 的特点是可以单独执行, `Expression` 的特点是有返回值,所以一些可以单独执行的 `Expression` 会包一层 `ExpressionStatement`。 不同 `AST` 节点有不同的属性来存放对应的源码信息,但是都有一些公共属性如 `type` `、xxComments` 、 `loc` 等。 学会了 `AST` ,就可以把对代码的操作转为对 `AST` 的操作了,这是编译、静态分析的第一步。 ## 三。编写词法分析器(Tokenizer) 词法分析器,也叫分词器 (`Tokenizer`),它的作用是将代码划分为一个个词法单元,便于进行后续的语法分析。比如下面的这段代码: ```js let foo = function () {}; ``` 在经过分词之后,代码会被切分为如下的 `token` 数组: ```js ['let', 'foo', '=', 'function', '(', ')', '{', '}']; ``` 从中你可以看到,原本一行普通的代码字符串被拆分成了拥有语法属性的 `token` 列表,不同的 `token` 之间也存在千丝万缕的联系,而后面所要介绍的语法分析器,就是来梳理各个 `token` 之间的联系,整理出 `AST` 数据结构。 当下我们所要实现的词法分析器,本质上是对代码字符串进行逐个字符的扫描,然后根据一定的语法规则进行分组。其中,涉及到几个关键的步骤: 1. 确定语法规则,包括语言内置的关键词、单字符、分隔符等 2. 逐个代码字符扫描,根据语法规则进行 `token` 分组 ### 1. 确定 Token 的类型和规则 增加 `Token` 的类型 ```ts export enum TokenType { // let Let = 'Let', // = Assign = 'Assign', // function Function = 'Function', // 变量名 Identifier = 'Identifier', // ( LeftParen = 'LeftParen', // ) RightParen = 'RightParen', // { LeftCurly = 'LeftCurly', // } RightCurly = 'RightCurly', } export type Token = { type: TokenType; value?: string; start: number; end: number; raw?: string; }; ``` 定义 Token 类型到规则的映射 ```ts const TOKENS_GENERATOR: Record Token> = { let(start: number) { return { type: TokenType.Let, value: 'let', start, end: start + 3 }; }, assign(start: number) { return { type: TokenType.Assign, value: '=', start, end: start + 1 }; }, function(start: number) { return { type: TokenType.Function, value: 'function', start, end: start + 8, }; }, leftParen(start: number) { return { type: TokenType.LeftParen, value: '(', start, end: start + 1 }; }, rightParen(start: number) { return { type: TokenType.RightParen, value: ')', start, end: start + 1 }; }, leftCurly(start: number) { return { type: TokenType.LeftCurly, value: '{', start, end: start + 1 }; }, rightCurly(start: number) { return { type: TokenType.RightCurly, value: '}', start, end: start + 1 }; }, identifier(start: number, value: string) { return { type: TokenType.Identifier, value, start, end: start + value.length, }; }, }; type SingleCharTokens = '(' | ')' | '{' | '}' | '='; // 单字符到 Token 生成器的映射 const KNOWN_SINGLE_CHAR_TOKENS = new Map([ ['(', TOKENS_GENERATOR.leftParen], [')', TOKENS_GENERATOR.rightParen], ['{', TOKENS_GENERATOR.leftCurly], ['}', TOKENS_GENERATOR.rightCurly], ['=', TOKENS_GENERATOR.assign], ]); ``` 有了 Token 类型和对应生成的规则,我们便可以去遍历分析代码,输出分析后的结果。 ### 2.代码字符扫描 在扫描字符的过程,我们需要对不同的字符各自进行不同的处理,具体的策略如下: - 当前字符为分隔符,如空格,直接跳过,不处理; - 当前字符为字母,需要继续扫描,获取完整的单词: - 如果单词为语法关键字,则新建相应关键字的 `Token` - 否则视为普通的变量名 - 当前字符为单字符,如`{、}、(、)`,则新建单字符对应的 `Token` ```ts export class Tokenizer { private _tokens: Token[] = []; private _currentIndex: number = 0; private _source: string; constructor(input: string) { this._source = input; } tokenize(): Token[] { while (this._currentIndex < this._source.length) { let currentChar = this._source[this._currentIndex]; const startIndex = this._currentIndex; // 根据语法规则进行 token 分组 // while 循环内部 let currentChar = this._source[this._currentIndex]; const startIndex = this._currentIndex; const isAlpha = (char: string): boolean => { return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z'); }; // 1. 处理空格 if (currentChar === ' ') { this._currentIndex++; continue; } // 2. 处理字母 else if (isAlpha(currentChar)) { let identifier = ''; while (isAlpha(currentChar)) { identifier += currentChar; this._currentIndex++; currentChar = this._source[this._currentIndex]; } let token: Token; if (identifier in TOKENS_GENERATOR) { // 如果是关键字 token = TOKENS_GENERATOR[identifier as keyof typeof TOKENS_GENERATOR](startIndex); } else { // 如果是普通标识符 token = TOKENS_GENERATOR['identifier'](startIndex, identifier); } this._tokens.push(token); continue; } // 3. 处理单字符 else if (KNOWN_SINGLE_CHAR_TOKENS.has(currentChar as SingleCharTokens)) { const token = KNOWN_SINGLE_CHAR_TOKENS.get(currentChar as SingleCharTokens)!(startIndex); this._tokens.push(token); this._currentIndex++; continue; } } return this._tokens; } } ``` 使用方式 ```ts const tokenizer = new Tokenizer('let a = function() {}'); ``` 结果 ```ts const tokenizer = [ { type: 'Let', value: 'let', start: 0, end: 3 }, { type: 'Identifier', value: 'a', start: 4, end: 5 }, { type: 'Assign', value: '=', start: 6, end: 7 }, { type: 'Function', value: 'function', start: 8, end: 16 }, { type: 'LeftParen', value: '(', start: 16, end: 17 }, { type: 'RightParen', value: ')', start: 17, end: 18 }, { type: 'LeftCurly', value: '{', start: 19, end: 20 }, { type: 'RightCurly', value: '}', start: 20, end: 21 }, ]; ``` 一个简易版本的分词器已经被我们开发出来了,不过目前的分词器还比较简陋,仅仅支持有限的语法,不过在明确了核心的开发步骤之后,后面继续完善的过程就比较简单了。 ## 四。编写语法分析器(Parser) 在解析出词法 `token` 之后,我们就可以进入语法分析阶段了。在这个阶段,我们会依次遍历 `token` ,对代码进行语法结构层面的分析,最后的目标是生成 `AST` 数据结构。至于代码的 `AST` 结构到底是什么样子,你可以去 `AST Explorer` 网站进行在线预览: ![](../../../../assets//ranuts//astParse//Comment.jpeg) 接下来,我们要做的就是将 `token` 数组转换为上图所示的 `AST` 数据。 开发步骤主要分为: - 初始化类型声明 - ================================================ FILE: packages/docs/cn/src/article/babel.md ================================================ # Babel babel 核心库主要是: - @babel/parser 对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法,功能是把源码转成 AST。 - @babel/traverse 通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据 - @babel/types 用于创建、判断 AST 节点,提供了 xxx、isXxx、assertXxx 的 api - @babel/template 当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑。 - @babel/code-frame 可以创建友好的报错信息 - @babel/generator 打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。 - @babel/core 基于上面的包来完成 babel 的编译流程,并应用 plugin 和 preset。 ================================================ FILE: packages/docs/cn/src/article/bundle.md ================================================ # Bundle Bundle 的本质就是输入,转换,输出。在机器上直接运行的代码,往往都难以维护和理解,我们需要将开发者方便理解和维护的代码,通过打包等工具转换成方便机器或者程序使用的代码。对于 web 前端来说,打包工具,至少需要以下功能: - 编译能力 - 插件机制 - HMR - cli 和命令行能力 ================================================ FILE: packages/docs/cn/src/article/design_mode.md ================================================ # 23 种经典设计模式 > 设计模式 `Design Pattern` 是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。。 在《设计模式:可复用面向对象软件的基础》一书中所介绍的 23 种经典设计模式,不过设计模式并不仅仅只有这 23 种,随着软件开发行业的发展,越来越多的新模式不断诞生并得以应用。有经验的开发者在学习设计模式可以和过往的经验互相印证,更容易理解这些设计模式。 设计模式一般包含模式名称、问题、目的、解决方案、效果等组成要素。问题描述了应该在何时使用模式,它包含了设计中存在的问题以及问题存在的原因。解决方案描述了一个设计模式的组成成分,以及这些组成成分之间的相互关系,各自的职责和协作方式,通常解决方案通过 `UML` 类图和核心代码来进行描述。效果描述了模式的优缺点以及在使用模式时应权衡的问题。 **为什么要学习设计模式**: - 设计模式来源众多专家的经验和智慧,它们是从许多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作 - 设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通和交流,使得设计方案更加通俗易懂 - 大部分设计模式都兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码 - 合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统 - 学习设计模式将有助于初学者更加深入地理解面向对象思想 **储备知识**: - 抽象类:一般抽象类都是作为基类,比如说「电脑」就可以作为一个抽象类,根据抽象类派生出「台式电脑」和「笔记本电脑」2 种具体类。一般不对抽象类进行实例化。 - 组合优于继承:不能滥用继承来拓展功能,配合组合会更灵活。同样拿「电脑」抽象类来举例,如果使用继承,区分不同类型的「电脑」我们可以派生出「台式电脑」和「笔记本电脑」,如果再增加一个维度,根据品牌又能继续细分出「联想台式电脑」、「联想笔记本电脑」、「苹果台式电脑」和「苹果笔记本电脑」等等,如果再增加一个维度继续细分下去,显然继承是无法胜任的。这个时候可以使用继承加组合方式,组合的对象也可以进行抽象化设计: ```ts // 品牌 interface Brand { // ... } interface Lenovo extends Brand { // ... } interface Apple extends Brand { // ... } // CPU interface CPU { // ... } interface Inter extends CPU { // ... } interface AMD extends CPU { // ... } // 电脑 interface Computer { // ... } interface DesktopComputer extends Computer {} interface NotebookComputer extends Computer {} ``` ## 一、UML 类图 每个模式都有相应的对象结构图,同时为了展示对象间的交互细节,有些时候会用到 `UML` 图来介绍其如何运行。这里不会将 `UML` 的各种元素都提到,只想讲讲类图中各个类之间的关系,能看懂类图中各个类之间的线条、箭头代表什么意思后,也就足够应对日常的工作和交流。同时,我们应该能将类图所表达的含义和最终的代码对应起来。有了这些知识,看后面章节的设计模式结构图就没有什么问题了。 ### 1.1 继承 继承用一条带空心箭头的直接表示。 ![](../../../assets/article/designPattern/继承.png) ### 1.2 实现 实现关系用一条带空心箭头的虚线表示。 ![](../../../assets/article/designPattern/实现.png) ### 1.3 组合 与聚合关系一样,组合关系同样表示整体由部分构成的语义。比如公司由多个部门组成,但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了。例如,公司不存在了,部门也将不存在了。 ![](../../../assets/article/designPattern/组合.png) ### 1.4 聚合 聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义,例如一个部门由多个员工组成。与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在。例如,部门撤销了,人员不会消失,他们依然存在。 ![](../../../assets/article/designPattern/聚合.png) ### 1.5 关联 关联关系是用一条直线表示的,它描述不同类的对象之间的结构关系,它是一种静态关系,通常与运行状态无关,一般由常识等因素决定的。它一般用来定义对象之间静态的、天然的结构,所以,关联关系是一种“强关联”的关系。 比如,乘车人和车票之间就是一种关联关系,学生和学校就是一种关联关系,关联关系默认不强调方向,表示对象间相互知道。如果特别强调方向,如下图,表示 A 知道 B,但 B 不知道 A。 ![](../../../assets/article/designPattern/关联.png) ### 1.6 依赖 依赖关系是用一套带箭头的虚线表示的,如 A 依赖于 B,他描述一个对象在运行期间会用到另一个对象的关系。 与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化,依赖关系也可能发生变化。显然,依赖也有方向,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生。 ![](../../../assets/article/designPattern/依赖.png) ## 二、六大原则 ### 2.1 开闭原则 > 一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。 任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。 为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在`Java`、`C#`等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。 **优点:实践开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。** ### 2.2 里氏替换原则 > 所有引用基类对象的地方能够透明地使用其子类的对象 里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类。但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。 例如有两个类,一个类为`BaseClass`,另一个是`SubClass`类,并且`SubClass`类是`BaseClass`类的子类,那么一个方法如果可以接受一个`BaseClass`类型的基类对象`base`的话,如:`method1(base)`,那么它必然可以接受一个`BaseClass`类型的子类对象`sub`,`method1(sub)`能够正常运行。反过来的代换不成立,如一个方法`method2`接受`BaseClass`类型的子类对象`sub`为参数:`method2(sub)`,那么一般而言不可以有`method2(base)`,除非是重载方法。 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。 **优点:可以检验继承使用的正确性,约束继承在使用上的泛滥。** ### 2.3 依赖倒置原则 > 抽象不应该依赖于具体类,具体类应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。 在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。 **优点:通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。** ### 2.4 单一职责原则 > 一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。 单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。 **优点:如果类与方法的职责划分得很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让 bug 无处藏身,也有利于 bug 的追踪,也就是降低了程序的维护成本。** ### 2.5 迪米特法则(最少知道原则) > 一个软件实体应当尽可能少地与其他实体发生相互作用 如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。 迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。 在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及。在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。在类的设计上,只要有可能,一个类型应当设计成不变类。在对其他类的引用上,一个对象对其他对象的引用应当降到最低。 **优点:实践迪米特法则可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接。** ### 2.6 接口分离原则 > 使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。 根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。 在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护。接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。 **优点:避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。** ### 2.7 合成复用原则(六大之外的) > 尽量使用对象组合,而不是继承来达到复用的目的 合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用功能的目的。简而言之,复用时要尽量使用组合/聚合关系(关联关系),少用继承。 在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度。一个类的变化对其他类造成的影响相对较少,其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。 **优点:避免复用时滥用继承,合理使用组合关系,增加灵活性。** ### 2.8 六大原则 - 学习心得 六大原则中,**开闭原则**、**里氏替换原则**、**依赖倒置原则** 联系比较紧密,后两者是实现开闭原则重要前提,使用中通过抽象化设计具有很好的可拓展性和可维护性。 **知道最少原则** 可以降低耦合,减少不必要的交互,主张设计接口和类要简单易使用,将复杂的逻辑封装并提供简单易用的接口。 **单一职责原则** 使项目中的类和方法根据职责细分,避免单个类负担过重。职责越多,被复用的可能性就越小或使用起来越麻烦。 **接口分离原则** 将功能复杂的接口细分成多个特定功能的接口,只做该做的事情,降低耦合,但是细化粒度不能太细,容易导致接口过多。单一职责原则强调单个类内部根据职责细分的设计,接口分离原则强调类之间的耦合,尽量建立最小的依赖关系。 ## 三、模式分类 《设计模式:可复用面向对象软件的基础》一书中设计模式有 23 个,它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型 (`Creational`),结构型 (`Structural`) 和行为型 (`Behavioral`) 三种,其中创建型模式主要用于描述如何创建对象,结构型模式主要用于描述如何实现类或对象的组合,行为型模式主要用于描述类或对象怎样交互以及怎样分配职责。 此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为类模式和对象模式。我们经常将两种分类方式结合使用,如单例模式是对象创建型模式,模板方法模式是类行为型模式。 ### 3.1 创建型 创建型模式 (`Creational Pattern`) 对类的实例化过程进行了抽象,能够将模块中对象的创建和对象的使用分离。为了使结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 1. 简单工厂模式(`Simple Factory Pattern`) 2. 工厂方法模式(`Factory Method Pattern`) 3. 抽象工厂模式(`Abstract Factory Pattern`) 4. 单例模式(`Singleton Pattern`) 5. 生成器模式(`Builder Pattern`) 6. 原型模式(`Prototype Pattern`) ### 3.2 结构型 结构型模式 (`Structural Pattern`) 描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。结构型模式可以分为类结构型模式和对象结构型模式: - 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。 - 对象结构型模式关心类与对象的组合,通过关联关系使得在一 个类中定义另一个类的实例对象,然后通过该对象调用其方法。根据“合成复用原则”,在系统中尽量使用关联关系来替代继 承关系,因此大部分结构型模式都是对象结构型模式。 1. 外观模式 2. 适配器模式 3. 桥接模式 4. 代理模式 5. 装饰者模式 6. 享元模式 ### 3.3 行为型 行为型模式 (`Behavioral Pattern`) 是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。 1. 职责链模式 2. 命令模式 3. 解释器模式 4. 迭代器模式 5. 中介者模式 6. 备忘录模式 7. 观察者模式 8. 状态模式 9. 策略模式 10. 模板方法模式 11. 访问者模式 ## 四、创建型 ### 4.1 简单工厂模式 > 简单工厂模式 (`Simple Factory Pattern`):专门定义一个类(工厂类)来负责创建其他类的实例。可以根据创建方法的参数来返回不同类的实例,被创建的实例通常都具有共同的父类。 ![](../../../assets/article/designPattern/简单工厂.png) **举例:** 简单工厂模式像一个代工厂,一个工厂可以生产多种产品。举个例子,一个饮料加工厂同时帮百事可乐和可口可乐生产,加工厂根据输入参数`Type`来生产不同的产品。 ```ts // 可乐抽象类 interface Cola {} // 可口可乐产品类 interface CocaCola extends Cola {} // 百事可乐产品类 interface PepsiCola extends Cola {} ``` ```ts // 简单工厂实现 // SimpleFactory const createColaWithType = (type: number) => { switch (type) { case 0: return new CocaCola(); case 1: return new PepsiCola(); default: return null; break; } }; ``` ```ts // 0 生产可口可乐 const cocaCola: CocaCola = createColaWithType(0); // 1 生产百事可乐 const pepsiCola: PepsiCola = createColaWithType(1); ``` **优点:** - 使用者只需要给工厂类传入一个正确的约定好的参数,就可以获取你所需要的对象,而不需要知道其创建细节,一定程度上减少系统的耦合。 - 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,减少开发者的记忆成本。 **缺点:** - 如果业务上添加新产品的话,就需要修改工厂类原有的判断逻辑,这其实是违背了开闭原则的。 - 在产品类型较多时,有可能造成工厂逻辑过于复杂。所以简单工厂模式比较适合产品种类比较少而且增多的概率很低的情况。 ### 4.2 工厂方法模式 > 工厂方法模式 (`Factory Method Pattern`) 又称为工厂模式,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,即通过不同的工厂子类来创建不同的产品对象。 ![](../../../assets/article/designPattern/工厂方法.png) **举例:** 工厂方法和简单工厂有一些区别,简单工厂是由一个代工厂生产不同的产品,而工厂方法是对工厂进行抽象化,不同产品都由专门的具体工厂来生产。可口可乐工厂专门生产可口可乐,百事可乐工厂专门生产百事可乐。 ```ts // 工厂抽象类 class Cola {} // 可口可乐工厂 class CocaCola extends Cola {} // 百事可乐工厂 class PepsiCola extends Cola {} ``` ```ts // 根据不同的工厂类生产不同的产品 const cocaCola = new CocaCola(); const pepsiCola = new PepsiCola(); ``` **优点:** - 用户只需要关心其所需产品对应的具体工厂是哪一个即可,不需要关心产品的创建细节,也不需要知道具体产品类的类名。 - 当系统中加入新产品时,不需要修改抽象工厂和抽象产品提供的接口,也无须修改客户端和其他的具体工厂和具体产品,而只要添加一个具体工厂和与其对应的具体产品就可以了,符合了开闭原则。 **缺点:** - 当系统中加入新产品时,除了需要提供新的产品类之外,还要提供与其对应的具体工厂类。因此系统中类的个数将成对增加,增加了系统的复杂度。 ### 4.3 抽象工厂模式 抽象工厂模式并不直接生成实例,而是用于对产品类簇的创建。 > 抽象工厂模式 (Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。 ![](../../../assets/article/designPattern/抽象工厂.png) **举例:** 抽象工厂和工厂方法不同的地方在于,生产产品的工厂是抽象的。举例,可口可乐公司生产可乐的同时,也需要生产装可乐的瓶子和箱子,瓶子和箱子也是可口可乐专属定制的,同样百事可乐公司也会有这个需求。这个时候我们的工厂不仅仅是生产可乐饮料的工厂,还必须同时生产同一主题的瓶子和箱子,所以它是一个抽象的主题工厂,专门生产同一主题的不同商品。 ```ts // 可乐抽象类和派生类 class Cola {} class CocaCola extends Cola {} class PepsiCola extends Cola {} // 瓶子抽象类和派生类 class Bottle {} class CocaColaBottle extends Bottle {} class PepsiColaBottle extends Bottle {} // 箱子抽象类和派生类 class Box {} class CocaColaBox extends Box {} class PepsiColaBox extends Box {} // 工厂抽象类 const Factory = { createCola: () => new Cola(), createBottle: () => new Bottle(), createBox: () => new Box(), }; // 可口可乐主题工厂 const CocaColaFactory = { createCola: () => new CocaCola(), createBottle: () => new CocaColaBottle(), createBox: () => new CocaColaBox(), }; // 百事可乐主题工厂 const PepsiColaFactory = { createCola: () => new PepsiCola(), createBottle: () => new PepsiColaBottle(), createBox: () => new PepsiColaBox(), }; ``` ```ts // 可口可乐主题 const cocaCola = CocaColaFactory.createCola(); const cocaColaBottle = CocaColaFactory.createBottle(); const cocaColaBox = CocaColaFactory.createBox(); // 百事可乐主题 const pepsiCola = PepsiColaFactory.createCola(); const pepsiColaBottle = PepsiColaFactory.createBottle(); const pepsiColaBox = PepsiColaFactory.createBox(); ``` **优点:** - 具体产品在应用层代码隔离,不需要关心产品细节。只需要知道自己需要的产品是属于哪个工厂的即可 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。 **缺点:** - 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口。 ### 4.4 单例模式 > 单例模式 (`Singleton Pattern`):单例模式确保某一个类只有一个实例,并提供一个访问它的全剧访问点。 ![](../../../assets/article/designPattern/UML/创建型/单例.jpg) **举例:** 单例模式下,对应类只能生成一个实例。就像一个王国只能有一个国王,一旦王国里的事务多起来,这唯一的国王也容易职责过重。 ```ts class Singleton {} function createSingleton() { let instance; return function () { if (!instance) return new Singleton(); return instance; }; } ``` **优点:** - 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。 - 因为该类在系统内存中只存在一个对象,所以可以节约系统资源。 **缺点:** - 由于单例模式中没有抽象层,因此单例类很难进行扩展。 - 对于有垃圾回收系统的语言 Java,C# 来说,如果对象长时间不被利用,则可能会被回收。那么如果这个单例持有一些数据的话,在回收后重新实例化时就不复存在了。 ### 4.5 生成器模式 > 生成器模式 (Builder Pattern):也叫创建者模式,它将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 工厂模式主要是为了创建对象实例或者类簇(抽象工厂),关心的是最终产出 (创建) 的是什么,而不关心创建的过程。而建造者模式关心的是创建这个对象的整个过程,甚至于创建对象的每一个细节。 ![](../../../assets/article/designPattern/建造者.jpeg) **举例:** 生成器模式的主要角色如下: 1. 生成器:接口生命再所有类型生成器中通用的产品构造步骤 2. 具体生成器:提供构造过程的不同实现。具体生成器也可以构造不遵循通用接口的产品 3. 产品:是最终生成的对象。由不同生成器构造的产品无需属于同一类层次构造或接口 4. 指挥者:定义调用构造步骤的顺序,这样你就可以创建和服用特定的产品配置 5. 客户端:必须将某个生成器对象与主管类关联,一般情况下,你只需要通过指挥者类构造函数的参数进行一次性关联即可 ```ts // 抽象建造者 abstract class Builder { public abstract buildPartA(): void; public abstract buildPartB(): void; public abstract buildPartC(): void; public abstract buildProduct(): Product; } // 具体建造者 class ConcreteBuilder extends Builder { private product: Product; constructor(product: Product) { super(); this.product = product; } public buildPartA(): void {} public buildPartB(): void {} public buildPartC(): void {} // 最终组建一个产品 public buildProduct(): Product { return this.product; } } // 产品角色 class Product { public doSomething(): void { // 独立业务 } } // 指挥者 class Director { private _builder: Builder; constructor(builder: Builder) { this._builder = builder; } set builder(builder: Builder) { this._builder = builder; } // 将处理建造的流程交给指挥者 public constructorProduct() { this._builder.buildPartA(); this._builder.buildPartB(); this._builder.buildPartC(); return this._builder.buildProduct(); } } // 使用 const builder: Builder = new ConcreteBuilder(new Product()); const director: Director = new Director(builder); const product: Product = director.constructorProduct(); ``` **优点:** - 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。 - 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。 - 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。 - 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。 **缺点:** - 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。 - 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加系统的理解难度和运行成本。 ### 4.6 原型模式 > 原型模式(`Prototype Pattern`): 用原型实例指向创建对象的类,使用于创建新的对象的类的共享原型的属性与方法。 ![](../../../assets/article/designPattern/UML/创建型/原型.jpg) **举例:** 原型模式就像复印技术,根据原对象复印出一个新对象,并根据需求对新对象进行微调。 ```ts // 因为不是构造函数,所以不用大写 const car = { drive: function () {}, name: '马自达 3', }; // 使用 Object.create 创建一个新车 x const anotherCar = Object.create(someCar); anotherCar.name = '丰田佳美'; ``` ```ts const vehiclePrototype = { init: function (carModel) { this.model = carModel; }, getModel: function () { console.log('车辆模具是:' + this.model); }, }; function vehicle(model) { function F() {} F.prototype = vehiclePrototype; const f = new F(); f.init(model); return f; } const car = vehicle('福特 Escort'); car.getModel(); ``` **优点:** - 可以利用原型模式简化对象的创建过程,尤其是对一些创建过程繁琐,包含对象层级比较多的对象来说,使用原型模式可以节约系统资源,提高对象生成的效率。 - 可以很方便得通过改变值来生成新的对象:有些对象之间的差别可能只在于某些值的不同;用原型模式可以快速复制出新的对象并手动修改值即可。 **缺点:** - 对象包含的所有对象都需要配备一个克隆的方法,这就使得在对象层级比较多的情况下,代码量会很大,也更加复杂。 ## 五、结构型 ### 5.1 装饰模式 > 装饰模式 (`Decorator Pattern`) :向一个现有的对象添加新的功能,同时又不改变其结构的设计模式被称为装饰器模式,它是作为现有的类的一个包装。 可以将装饰器理解为游戏人物购买的装备,例如 LOL 中的英雄刚开始游戏时只有基础的攻击力和法强。但是在购买的装备后,在触发攻击和技能时,能够享受到装备带来的输出加成。我们可以理解为购买的装备给英雄的攻击和技能的相关方法进行了装饰。 ![](../../../assets/article/designPattern/装饰.jpg) **举例:** 装饰模式贴合开闭原则,在不改变原有类的情况下,对父类进行改造或新增功能。 装饰类 ```ts @annotation class MyClass {} function annotation(target) { target.annotated = true; } ``` 装饰方法或属性 ```js class MyClass { @readonly method() {} } function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor; } ``` **优点:** - 比继承更加灵活:不同于在编译期起作用的继承;装饰者模式可以在运行时扩展一个对象的功能。另外也可以通过配置文件在运行时选择不同的装饰器,从而实现不同的行为。也可以通过不同的组合,可以实现不同效果。 - 符合“开闭原则”:装饰者和被装饰者可以独立变化。用户可以根据需要增加新的装饰类,在使用时再对其进行组合,原有代码无须改变。 **缺点:** - 装饰者模式需要创建一些具体装饰类,会增加系统的复杂度。 ### 5.2 外观模式 > 外观模式 (Facade Pattern):外观模式定义了一个高层接口,为子系统中的一组接口提供一个统一的接口。使得子系统更容易使用,不仅简化类中的接口,而且实现调用者和接口的解耦。外观模式又称为门面模式,它是一种结构型设计模式模式。 ![](../../../assets/article/designPattern/外观.png) **举例:** 外观模式提供了简单明确的接口,但是在内部众多子系统功能进行整合。就像图片缓存,内部包含了涉及到其他子系统的如缓存、下载等处理,外观模式将这些复杂的逻辑都隐藏了。在兼容浏览器事件绑定,你只需要调一个`addMyEvent`接口就可以了,达到解耦合的目的。 ```js const addMyEvent = function (el, ev, fn) { if (el.addEventListener) { el.addEventListener(ev, fn, false); } else if (el.attachEvent) { el.attachEvent('on' + ev, fn); } else { el['on' + ev] = fn; } }; ``` **优点:** - 实现了客户端与子系统间的解耦:客户端无需知道子系统的接口,简化了客户端调用子系统的调用过程,使得子系统使用起来更加容易。同时便于子系统的扩展和维护。 - 符合迪米特法则(最少知道原则):子系统只需要将需要外部调用的接口暴露给外观类即可,而且他的接口则可以隐藏起来。 **缺点:** - 违背了开闭原则:在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的代码。 ### 5.3 代理模式 > 代理模式 (Proxy Pattern) :为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。 ![](../../../assets/article/designPattern/代理.png) **举例:** 代理模式像一个房屋中介,买家只能通过中介来买房,代理具备被代理类的所有功能,就像房东有卖房功能,中介也具有卖房功能。此外代理实例还可以帮助被代理实例进行一些额外处理,比如中介可以帮助房东筛选优质买家的功能,帮助房东 pass 掉一些不符合条件的买家。还有消息队列也是该模式。 参考`koa`中的代理模式,把`response`上的一些属性和方法代理出来,方便使用 ```js /** * Response delegation. */ const delegate = require('delegates'); const prototype = (module.exports = {}); delegate(prototype, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('has') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable'); ``` 对`context`,`request`,`response`做一个代理,保护真正的`context`,`request`,`response` ```js this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); ``` **优点:** - 降低系统的耦合度:代理模式能够协调调用者和被调用者,在一定程度上降低了系 统的耦合度。 - 不同类型的代理可以对客户端对目标对象的访问进行不同的控制: - 远程代理,使得客户端可以访问在远程机器上的对象,远程机器 可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。 - 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。 - 保护代理可以控制客户端对真实对象的使用权限。 **缺点:** - 由于在客户端和被代理对象之间增加了代理对象,因此可能会让客户端请求的速度变慢。 ### 5.4 享元模式 > 享元模式 (`Flyweight Pattern`):享元模式是一种优化程序性能的模式,本质为减少对象创建的个数。运用共享技术复用大量细粒度的对象,降低程序内存的占用,提高程序的性能。以下情况可以使用享元模式:有大量相似的对象,占用了大量内存。对象中大部分状态可以抽离为外部状态。 ![](../../../assets/article/designPattern/享元.png) **举例:** 举例,音乐服务根据收费划分出免费用户和会员用户,免费用户只能听部分免费音乐,会员用户可以听全部的音乐,并且可以下载。虽然权限上二者间有一些区别,但是他们所享受的音乐来是自于同一个音乐库,这样所有的音乐都只需要保存一份就可以了。另外如果出现音乐库里没有的音乐时,则需要新增该音乐,然后其他服务也可以享受新增的音乐,相当于享元池或缓存池的功能。 享元模式区保证共享内部状态如音乐库,而外部状态根据不同需求定制如各种访问权限,使用中不能去改变内部状态,以达到共享的目的。 ```ts // 音乐服务 const MusicService = {} // 共享的音乐库 const musicLibrary = {}; // 听音乐 const listenToMusic = (music) => { ... } // 下载音乐 const downloadMusic = (music) => { ... } // 免费音乐服务 const FreeMusicService = { listenFreeMusic: (music)=>{ if(isMusicFree(music)){ // 如果是免费则播放 listenToMusic() }else{ // 如果是收费音乐,则提示用户升级 Vip console.log("please upgrade to Vip") } } } // Vip 音乐服务 const VipMusicService = { // 可以听全部的音乐 listenMusic // 可以下载音乐 downloadMusic } ``` **优点:** - 使用享元模可以减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份,降低系统的使用内存,也可以提性能。 - 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。 **缺点:** - 使用享元模式需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。 - 对象在缓冲池中的复用需要考虑线程问题。 ### 5.5 桥接模式 > 桥接模式 (`Simple Factory Pattern`):将抽象部分与它的实现部分分离,使它们都可以独立地变化。 ![](../../../assets/article/designPattern/桥接.png) **举例:** 球和人都可以进行运动,但球有运动和颜色,人可以运动和说话。对共同部分进行抽象。 ```js class Speed { // 运动模块 constructor(x, y) { this.x = x; this.y = y; } run() { console.log(`运动起来 ${this.x} + ${this.y}`); } } class Color { // 着色模块 constructor(cl) { this.color = cl; } draw() { console.log(`绘制颜色 ${this.color}`); } } class Speak { constructor(wd) { this.word = wd; } say() { console.log(`说话 ${this.word}`); } } class Ball { // 创建球类,可以着色和运动 constructor(x, y, cl) { this.speed = new Speed(x, y); this.color = new Color(cl); } init() { this.speed.run(); this.color.draw(); } } class Man { // 人类,可以运动和说话 constructor(x, y, wd) { this.speed = new Speed(x, y); this.speak = new Speak(wd); } init() { this.speed.run(); this.speak.say(); } } const man = new Man(1, 2, 'hello ?'); man.init(); // 运动起来 1 + 2 说话 hello? ``` **优点:** - 扩展性好,符合开闭原则:将抽象与实现分离,让二者可以独立变化 **缺点:** - 在设计之前,需要识别出两个独立变化的维度。 ### 5.6 适配器模式 > 适配器模式 (`Adapter Pattern`) :适配器模式是用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式,实现两个接口正常协作。当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求,则可以用适配器模式。 ![](../../../assets/article/designPattern/适配器.png) **举例:** 事件绑定兼容各浏览器 ```js function addEvent(ele, event, callback) { if (ele.addEventListener) { ele.addEventListener(event, callback) } else if(ele.attachEvent) { ele.attachEvent('on' + event, callback) } else { ele['on' + event] = callback } } ​ ``` **优点:** - 符合开闭原则:使用适配器而不需要改变现有类,提高类的复用性。 - 目标类和适配器类解耦,提高程序扩展性。 **缺点:** - 增加了系统的复杂性 ## 六、行为型 ### 6.1 职责链模式 > 职责链模式 (Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。类似多米诺骨牌,通过请求第一个条件,会持续执行后续的条件,直到返回结果为止。 ![](../../../assets/article/designPattern/责任链.png) **举例:** 场景:某电商针对已付过定金的用户有优惠政策,在正式购买后,已经支付过 500 元定金的用户会收到 100 元的优惠券,200 元定金的用户可以收到 50 元优惠券,没有支付过定金的用户只能正常购买。 ```js const order500 = function (orderType, pay, stock) { if (orderType === 1 && pay == true) { console.log('500 元定金预购,得到 100 元优惠劵'); } else { return 'nextSuccess'; } }; const order200 = function (orderType, pay, stock) { if (orderType === 2 && pay === true) { console.log('200 元定金预购,得到 50 元優惠卷'); } else { return 'nextSuccess'; } }; const orderCommon = function (orderType, pay, stock) { if (orderType == 3 && stock > 0) { console.log('普通購買,无優惠卷'); } else { console.log('库存不够'); } }; //链路代码 const chain = function (fn) { this.fn = fn; this.successor = null; }; chain.prototype.setNext = function (successor) { this.successor = successor; }; chain.prototype.init = function () { const result = this.fn.apply(this, arguments); if (result == 'nextSuccess') { this.successor.init.apply(this.successor, arguments); } }; const order500New = new chain(order500); const order200New = new chain(order200); const orderCommonNew = new chain(orderCommon); order500New.setNext(order200New); order200New.setNext(orderCommonNew); order500New.init(3, true, 500); // 普通购买,无优惠券 ``` **优点:** - 职责链模式使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。 - 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。 - 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责。 - 在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是符合“开闭原则”的。 **缺点:** - 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理。 - 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便。 - 如果建链不当,可能会造成循环调用,将导致系统陷入死循环。 ### 6.2 命令模式 > 命令模式 (`Command Pattern`):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;命令模式是一种对象行为型模式,其别名为动作 (`Action`) 模式或事务 (`Transaction`) 模式。 命令模式由三种角色构成: 1. 发布者 `invoker`(发出命令,调用命令对象,不知道如何执行与谁执行); 2. 接收者 `receiver` (提供对应接口处理请求,不知道谁发起请求); 3. 命令对象 `command`(接收命令,调用接收者对应接口处理发布者的请求)。 发布者 invoker 和接收者 receiver 各自独立,将请求封装成命令对象 command,请求的具体执行由命令对象 command 调用接收者 receiver 对应接口执行。 ![](../../../assets/article/designPattern/命令.png) **举例:** 和之前代理模式中的举例有些相似,不过命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。例如遥控器是一个调用者,不同按钮代表不同的命令,而电视是接收者。 ```js class Receiver { // 接收者类 execute() { console.log('接收者执行请求'); } } class Command { // 命令对象类 constructor(receiver) { this.receiver = receiver; } execute() { // 调用接收者对应接口执行 console.log('命令对象->接收者->对应接口执行'); this.receiver.execute(); } } class Invoker { // 发布者类 constructor(command) { this.command = command; } invoke() { // 发布请求,调用命令对象 console.log('发布者发布请求'); this.command.execute(); } } const warehouse = new Receiver(); // 仓库 const order = new Command(warehouse); // 订单 const client = new Invoker(order); // 客户 client.invoke(); ``` **优点:** - 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。 - 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。 - 可以比较容易地设计一个命令队列或宏命令(组合命令)。 - 为请求的撤销 (Undo) 和恢复 (Redo) 操作提供了一种设计和实现方案。 **缺点:** - 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。 ### 6.3 解释器模式 > 解释器模式 (`Interpreter Pattern`):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。 ![](../../../assets/article/designPattern/解释器.jpg) **举例:** 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 ```js class Context { constructor() { this._list = []; // 存放 终结符表达式 this._sum = 0; // 存放 非终结符表达式 (运算结果) } get sum() { return this._sum; } set sum(newValue) { this._sum = newValue; } add(expression) { this._list.push(expression); } get list() { return [...this._list]; } } class PlusExpression { interpret(context) { if (!(context instanceof Context)) { throw new Error('TypeError'); } context.sum = ++context.sum; } } class MinusExpression { interpret(context) { if (!(context instanceof Context)) { throw new Error('TypeError'); } context.sum = --context.sum; } } /** 以下是测试代码 **/ const context = new Context(); // 依次添加:加法 | 加法 | 减法 表达式 context.add(new PlusExpression()); context.add(new PlusExpression()); context.add(new MinusExpression()); // 依次执行:加法 | 加法 | 减法 表达式 context.list.forEach((expression) => expression.interpret(context)); console.log(context.sum); ``` **优点:** - 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。 - 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。 - 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。 - 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。 **缺点:** - 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。 - 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。 ### 6.4 迭代器模式 > 迭代器模式 (`Iterator Pattern`):一个相对简单的模式,目前绝大多数语言都内置了迭代器,以至于大家都不觉得这是一种设计模式。迭代器并不只迭代数组,迭代器可以中止。提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标 (`Cursor`)。迭代器模式是一种对象行为型模式。 ![](../../../assets/article/designPattern/迭代器.png) **举例:** 迭代器帮助请求方获取数据,避免直接操作数据聚合类,使数据聚合类专注存储数据。具体应用有分页等功能,分页功能的迭代器将专门负责操作分页数据,将操作逻辑和数据源分离。 ```js var each = function (arr, callback) { for (var i = 0, len = arr.length; i < len; i++) { callback.call(arr[i], i, arr[i]); } }; each([1, 2, 3, 4, 5], function (i, el) { console.log('index: ', i); console.log('item: ', el); }); ``` **优点:** - 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。 - 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。 - 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。 **缺点:** - 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。 - 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如 JDK 内置迭代器 Iterator 就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类 ListIterator 等来实现,而 ListIterator 迭代器无法用于操作 Set 类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。 ### 6.5 中介者模式 > 中介者模式`(Mediator Pattern`):对象和对象之间借助第三方中介者进行通信。用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。 ![](../../../assets/article/designPattern/中介者.png) **举例:** 中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。所有成员通过中介者交互,方便拓展新的成员,例如下面的例子,一场测试结束后,公布结果:告知解答出题目的人挑战成功,否则挑战失败。在这段代码中 A、B、C 之间没有直接发生关系,而是通过另外的 playerMiddle 对象建立链接,姑且将之当成是中介者模式了。 ```js const player = function (name) { this.name = name; playerMiddle.add(name); }; player.prototype.win = function () { playerMiddle.win(this.name); }; player.prototype.lose = function () { playerMiddle.lose(this.name); }; const playerMiddle = (function () { //将就用下这个 demo, 这个函数充当中介者 const players = []; const winArr = []; const loseArr = []; return { add: function (name) { players.push(name); }, win: function (name) { winArr.push(name); if (winArr.length + loseArr.length === players.length) { this.show(); } }, lose: function (name) { loseArr.push(name); if (winArr.length + loseArr.length === players.length) { this.show(); } }, show: function () { for (let winner of winArr) { console.log(winner + '挑戰成功;'); } for (let loser of loseArr) { console.log(loser + '挑战失败;'); } }, }; })(); const a = new player('A 选手'); const b = new player('B 选手'); const c = new player('C 选手'); a.win(); b.lose(); c.win(); // A 选手挑战成功; // B 选手挑战成功; // C 选手挑战失败; ``` **优点:** - 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。 - 中介者模式可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合“开闭原则”。 - 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展。 **缺点:** - 在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。 ### 6.6 备忘录模式 > 备忘录模式 (`Memento Pattern`):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便日后对象使用或者对象恢复到以前的某个状态。它是一种对象行为型模式,其别名为 Token。 ![](../../../assets/article/designPattern/UML/行为型/备忘录.jpg) **举例:** 备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,当前很多软件都提供了撤销操作,其中就使用了备忘录模式。 当我们开发一个分页组件的时候,点击下一页获取新的数据,但是当点击上一页时,又重新获取数据,造成无谓的流量浪费,这时可以对数据进行缓存。 ```js // 备忘录模式伪代码 var Page = function () { // 通过 cache 对象缓存数据 var cache = {}; return function (page, fn) { if (cache[page]) { showPage(page, cache[page]); } else { $.post('/url', function (data) { showPage(page, data); cache[page] = data; }); } fn && fn(); }; }; ``` **优点:** - 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。 - 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。 **缺点:** - 资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。 ### 6.7 观察者模式 > 观察者模式 (`Observer Pattern)`:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布 - 订阅(`Publish/Subscribe`)模式、模型 - 视图(`Model/View`)模式、源 - 监听器(`Source/Listener`)模式或从属者(`Dependents`)模式。观察者模式是一种对象行为型模式。 ![](../../../assets/article/designPattern/观察者.png) **举例:** 观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。 在`JavaScript`中观察者模式的实现主要用事件模型,`DOM`事件。 ```js // 发布者 var pub = function () { console.log('欢迎订阅!'); }; // 订阅者 var sub = document.body; // 订阅者实现订阅 sub.addEventListener('click', pub, false); ``` **优点:** - 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。 - 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。 - 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。 - 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。 **缺点:** - 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。 - 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 - 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。 ### 6.8 状态模式 > 状态模式 (`State Pattern`):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象 (`Objects for States`),其实就是用一个对象或者数组记录一组状态,每个状态对应一个实现,实现的时候根据状态挨个去运行实现。状态模式是一种对象行为型模式。 ![](../../../assets/article/designPattern/状态.png) **举例:** 状态模式用于解决复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,所以对象在不同状态下具有不同行为时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。 比如超级玛丽,就可能同时有好几个状态比如 跳跃,移动,射击,蹲下 等,如果对这些动作一个个进行处理判断,需要多个 if-else 或者 switch 不仅丑陋不说,而且在遇到有组合动作的时候,实现就会变的更为复杂,这里可以使用状态模式来实现。 状态模式的思路是:首先创建一个状态对象或者数组,内部保存状态变量,然后内部封装好每种动作对应的状态,然后状态对象返回一个接口对象,它可以对内部的状态修改或者调用。 ```js class SuperMarry { constructor() { this._currentState = []; this.states = { jump() { console.log('跳跃!'); }, move() { console.log('移动!'); }, shoot() { console.log('射击!'); }, squat() { console.log('蹲下!'); }, }; } change(arr) { // 更改当前动作 this._currentState = arr; return this; } go() { console.log('触发动作'); this._currentState.forEach((T) => this.states[T] && this.states[T]()); return this; } } new SuperMarry() .change(['jump', 'shoot']) .go() // 触发动作 跳跃!射击! .go() // 触发动作 跳跃!射击! .change(['squat']) .go(); // 触发动作 蹲下! ``` **优点:** - 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。 - 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。 - 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。 - 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。 **缺点:** - 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。 - 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。 - 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。 ### 6.9 策略模式 > 策略模式 (`Strategy Pattern`):定义一些列算法,把他们封装起来,并且可以相互替换。就是把看似毫无联系的代码提取封装、复用,使之更容易被理解和拓展。常见的用于一次 if 判断、switch 枚举、数据字典等流程判断语句中。也称为政策模式 (`Policy`)。策略模式是一种对象行为型模式。 ![](../../../assets/article/designPattern/策略.png) **举例:** 使用策略模式时,我们可以定义一些策略类,每一个策略类中封装一种具体的算法。在这里,每一个封装算法的类我们都可以称之为一种策略,根据传入不同的策略类,使环境类执行不同策略类中的算法。 在游戏中,我们每玩完一局游戏都有对用户进行等级评价,比如 S 级 4 倍经验,A 级 3 倍经验,B 级 2 倍经验,其他 1 倍经验,用函数来表达如下: ```js // 改为策略模式 分成两个函数来写 const strategy = { S: function (experience) { return 4 * experience; }, A: function (experience) { return 3 * experience; }, B: function (experience) { return 2 * experience; }, }; // getExperience 可以复用 function getExperience(strategy, level, experience) { return level in strategy ? strategy[level](experience) : experience; } var s = getExperience(strategy, 'S', 100); var a = getExperience(strategy, 'A', 100); console.log(s, a); // 400 300 ``` ```js // 指令处理集合 var compileUtil = { // v-text 更新视图原理 text: function(node, vm, exp) { this.bind(node, vm, exp, 'text'); }, // v-html 更新视图原理 html: function(node, vm, exp) { this.bind(node, vm, exp, 'html'); }, // v-class 绑定原理 class: function(node, vm, exp) { this.bind(node, vm, exp, 'class'); }, bind: function(node, vm, exp, dir) { // 不同指令触发视图更新 var updaterFn = updater[dir + 'Updater']; updaterFn && updaterFn(node, this._getVMVal(vm, exp)); new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); }); } ...... } ``` **优点:** - 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。 - 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。 - 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。 - 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码 (Hard Coding) 在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。 - 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。 **缺点:** - 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。 - 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。 - 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。 ### 6.10 模板方法模式 > 模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 ![](../../../assets/article/designPattern/模板.png) **举例:** 模板方法模式的使用场景 - 模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空 - 钩子方法:各种框架中的钩子函数往往在初始化时就规定各个钩子函数的名称以及执行时机,对于使用者只需要在钩子函数中注入自定义逻辑代码即可 - 回调函数:回调函数在特定的时机执行,但是具体的操作交给具体的函数实现。把变化的部分封装成一个函数剩下的就成了模板 模板方法模式具体应用又分为三类: - 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。 - 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。 - 钩子方法:一个钩子方法由一个抽象类或具体类声明并实现,而其子类可能会加以扩展。通常在父类中给出的实现是一个空实现,并以该空实现作为方法的默认实现,当然钩子方法也可以提供一个非空的默认实现。通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制。 泡一杯咖啡 先我们先来泡一杯咖啡,一般来说,泡咖啡的步骤通常如下: 1.先把水煮沸; 2.用沸水冲泡咖啡; 3.把咖啡倒进杯子; 4.加糖和牛奶。 我们用 es5 来得到一杯香浓的咖啡吧: ```js var Coffee = function () {}; Coffee.prototype.boilWater = function () { console.log('水煮开了'); }; Coffee.prototype.brewCoffeeGriends = function () { console.log('用沸水冲泡咖啡'); }; Coffee.prototype.pourInCup = function () { console.log('把咖啡倒进杯子'); }; Coffee.prototype.addSugarAndMilk = function () { console.log('加糖和牛奶'); }; // 封装 将实现的细节交给类的内部 Coffee.prototype.init = function () { this.boilWater(); this.brewCoffeeGriends(); this.pourInCup(); this.addSugarAndMilk(); }; var coffee = new Coffee(); coffee.init(); ``` 泡一壶茶 其实呢,泡茶的步骤跟泡咖啡的步骤相差不大,大致是这样的: 1.把水煮沸; 2.用沸水浸泡茶叶; 3.把茶水倒进杯子; 4.加柠檬。 来,咱用 es6 来泡茶: ```js class Tea { constructor() {} boilWater() { console.log('把水烧开'); } steepTeaBag() { console.log('浸泡茶叶'); } pourInCup() { console.log('倒进杯子'); } addLemon() { console.log('加柠檬'); } init() { this.boilWater(); this.steepTeaBag(); this.pourInCup(); this.addLemon(); } } var tea = new Tea(); tea.init(); ``` 现在到了思考的时间,我们刚刚泡了一杯咖啡和一壶茶,有没有觉得这两个过程是大同小异的。我们能很容易的就找出他们的共同点,不同点就是原料不同嘛,茶和咖啡,我们可以把他们抽象为"饮料"哇;泡的方式不同嘛,一个是冲泡,一个是浸泡,我们可以把这个行为抽象为"泡";加入的调料也不同咯,加糖和牛奶,加柠檬,它们也可以抽象为"调料"吖。 这么一分析,是不是很清楚了吖,我们整理一下就是: 1.把水煮沸; 2.用沸水冲泡饮料; 3.把饮料倒进杯子; 4.加调料。 大家请注意!大家请注意!主角来了!之前我们已经扔出了概念,所以我们现在可以创建一个抽象父类来表示泡一杯饮料的过程。那么,抽象父类? 抽象类? 抽象类是不能被实例化的,一定是用来继承的。继承了抽象类的所有子类都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为它的子类定义这些公共接口。 通过上面分析,这里具体来说就是要把泡茶和泡咖啡的共同步骤共同点找出来,封装到父类,也就是抽象类中,然后不同的步骤写在子类中,也就是茶和咖啡中。抽象类既然不能被实例化,不怕啊,子类就是他的实例化。 泡饮料啦! ```js var Beverage = function () {}; Beverage.prototype.boilWater = function () { console.log('把水煮沸'); }; Beverage.prototype.brew = function () {}; Beverage.prototype.pourInCup = function () {}; Beverage.prototype.addCondiments = function () {}; // 抽象方法 Beverage.prototype.init = function () { this.boilWater(); this.brew(); this.pourInCup(); this.addCondiments(); }; var Coffee = function () { // 将父类的构造方法拿来执行一下 Beverage.apply(this, arguments); // 就像 es6 的 super 执行 执行后 this 才会有对象的属性 }; Coffee.prototype = new Beverage(); var coffee = new Coffee(); coffee.init(); var Tea = function () {}; Tea.prototype = new Beverage(); Tea.prototype.brew = function () { console.log('用沸水浸泡茶叶'); }; Tea.prototype.pourInCup = function () { console.log('把茶叶倒进杯子'); }; Tea.prototype.addCondiments = function () { console.log('加柠檬'); }; var tea = new Tea(); tea.init(); ``` 这里既泡了咖啡又泡了茶,是不是没有之前那么繁琐呢,这里的代码可是很高级的呢。 这里用一个父类 Beverage 来表示 Coffee 和 Tea,然后子类就是后面的 Coffee 和 Tea 啦,因为这里的 Beverage 是一个抽象的存在,需要子类来继承它。泡饮品的流程,可以理解为一个模板模式,抽象类 Beverage,抽象方法 init() 在子类中实现。js 的继承是基于原型链的继承,这里 prototype 就是类的原型链。这里由于 coffee 对象和 tea 对象的原型 prototype 上都没有对应的 init(),所以请求会顺着原型链,找到父类 Beverage 的 init()。子类寻找对应的属性和方法的时候会顺着原型链去查找,先找自己,没有找到会顺着去父类里面查找。 Beverage.prototype.init 被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。 **优点:** - 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。 - 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。 - 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。 - 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。 **缺点:** - 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。 ### 6.11 访问者模式 > 访问者模式 (`Visitor Pattern`):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。 ![](../../../assets/article/designPattern/访问者.png) **举例:** 访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。 在使用访问者模式时,被访问元素通常不是单独存在的,它们存储在一个集合中,这个集合被称为「对象结构」,访问者通过遍历对象结构实现对其中存储的元素的逐个操作。 ```js // 访问者模式:DOM 事件绑定 var bindEvent = function(dom, type, fn, data) { if (dom.addEventListener) { dom.addEventListener(type, fn, false); } else if (dom.attachEvent) { // dom.attachEvent('on'+type, fn); var data = data || {}; dom.attachEvent('on' + type, function(e) { // 在 IE 中 this 指向 window,使用 call 改变 this 的指向 fn.call(dom, e, data); }); } else { dom['on' + type] = fn; } } function $(id) { return document.getElementById(id); } ​ bindEvent($(demo), 'click', function() { // this 指向 dom 对象 this.style.background = 'red'; }); ​ bindEvent($('btn'), 'click', function(e, data) { $('text').innerHTML = e.type + data.text + this.tagName; }, { text: 'demo' }); ``` 访问者模式的思想就是在不改变操作对象的同时,为它添加新的操作方法,以实现对操作对象的访问。我们知道,call 和 apply 的作用就是更改函数执行时的作用域,这正是访问者模式的精髓。通过 call、apply 这两种方式我们就可以让某个对象在其它作用域中运行。 ```js // 数组方法封装 var Visitor = (function() { return { splice: function() { var args = Array.prototype.splice.call(arguments, 1); return Array.prototype.splice.apply(arguments[0], args); }, push: function() { var len = arguments[0].length || 0; var args = this.splice(arguments, 1); arguments[0].length = len + arguments.length - 1; return Array.prototype.push.apply(arguments[0], args); }, pop: function() { return Array.prototype.pop.apply(arguments[0]); } } })(); ​ var a = new Object(); Visitor.push(a,1,2,3,4); Visitor.push(a,4,5,6); Visitor.pop(a); Visitor.splice(a,2); ``` 访问者模式解决了数据与数据的操作方法之间的耦合,让数据的操作方法独立于数据,使其可以自由演变。因此,访问者模式更适合于那些数据稳定、但数据的操作方法易变的环境下。 **优点:** - 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。 - 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。 - 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。 **缺点:** - 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。 - 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。 ## 总结 系统地学习设计模式后,你可以在过往的开发经历中发现,设计模式是无处不在的。在学习设计模式之前的很多时候我们是凭借过往经验和智慧来完善系统的设计,而这些经验很多和某个设计模式的思想不谋而合。 还有一些地方没有完全理解,文中有误之处还望不吝指出。 ## 参考资料 - [Study-Plan](https://github.com/xietao3/Study-Plan) - [javaScript 设计模式统计](https://zhuanlan.zhihu.com/p/472719016) - [ES6 系列之我们来聊聊装饰器](https://juejin.cn/post/6844903713866252296) - [设计模式之生成器模式](https://segmentfault.com/a/1190000038250020) - [https://juejin.im/user/57f8ffda2e958a005581e3c0/posts](https://juejin.im/user/57f8ffda2e958a005581e3c0/posts) - [https://design-patterns.readthedocs.io/zh_CN/latest/index.html](https://design-patterns.readthedocs.io/zh_CN/latest/index.html) - [https://blog.csdn.net/lovelion/article/details/17517213](https://blog.csdn.net/lovelion/article/details/17517213) - [https://github.com/skyming/Trip-to-iOS-Design-Patterns](https://github.com/skyming/Trip-to-iOS-Design-Patterns) ================================================ FILE: packages/docs/cn/src/article/doc_preview.md ================================================

最全的 docx,pptx,xlsx(excel),pdf 文件预览方案总结

最近遇到了文件预览的需求,但一搜索发现,这还不是一个简单的功能。于是又去查询了很多资料,调研了一些方案,也踩了好多坑。最后总结方案如下 1. 花钱解决 (使用市面上现有的文件预览服务) 1. 微软 2. google 3. 阿里云 IMM 4. XDOC 5. Office Web 365 6. wps 开放平台 2. 前端方案 1. pptx 的预览方案 2. pdf 的预览方案 3. docx 的预览方案 4. xlsx(excel) 的预览方案 5. 前端预览方案总结 3. 服务端方案 1. openOffice 2. kkFileView 3. onlyOffice 如果有其他人也遇到了同样的问题,有了这篇文章,希望能更方便的解决。 基本涵盖了所有解决方案。因此,标题写上 **最全** 的文件预览方案调研总结,应该不为过吧。 ## 一:市面上现有的文件预览服务 ### 1.微软 `docx`,`pptx`,`xlsx`可以说是`office`三件套,那自然得看一下 **微软官方** 提供的文件预览服务。使用方法特别简单,只需要将文件链接,拼接到参数后面即可。 记得`encodeURL` ```js https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)} ``` #### (1).PPTX 预览效果: ![image.png](../../../assets/article/docPreview/ms_ppt.webp) - 优点:还原度很高,功能很丰富,可以选择翻页,甚至支持点击播放动画。 - 缺点:不知道是不是墙的原因,加载稍慢。 #### (2).Excel 预览效果: ![image.png](../../../assets/article/docPreview/ms_excel.webp) #### (3).Doxc 预览效果 ![image.png](../../../assets/article/docPreview/ms_word.webp) #### (4).PDF 预览效果 这个我测试没有成功,返回了一个错误,其他人可以试试。 ![image.png](../../../assets/article/docPreview/ms_file_not.webp) #### (5).总的来说 对于`docx`,`pptx`,`xlsx`都有较好的支持,`pdf`不行。 还有一个坑点是:这个服务是否稳定,有什么限制,是否收费,都查不到一个定论。在`office`官方网站上甚至找不到介绍这个东西的地方。 目前只能找到一个`Q&A`:https://answers.microsoft.com/en-us/msoffice/forum/all/what-is-the-status-of-viewofficeappslivecom/830fd75c-9b47-43f9-89c9-4303703fd7f6 微软官方人员回答表示: ![image.png](../../../assets/article/docPreview/ms_answer.webp) 翻译翻译,就是:几乎永久使用,没有收费计划,不会存储预览的文件数据,限制文件`10MB`,建议用于 **查看互联网上公开的文件**。 但经过某些用户测试发现: ![image.png](../../../assets/article/docPreview/ms_answer_2.webp) 使用了微软的文件预览服务,然后删除了文件地址,仍然可访问,但过一段时间会失效。 ### 2.Google Drive 查看器 接入简单,同 `Office Web Viewer`,只需要把 `src` 改为`https://drive.google.com/viewer?url=${encodeURIComponent(url)}`即可。 限制`25MB`,支持以下格式: ![image.png](../../../assets/article/docPreview/google_doc_view.webp) 测试效果,支持`docx,pptx,xlsx,pdf`预览,但`pptx`预览的效果不如微软,没有动画效果,样式有小部分会错乱。 **由于某些众所周知的原因,不可用** ### 3.阿里云 IMM 官方文档如下:https://help.aliyun.com/document_detail/63273.html ![image.png](../../../assets/article/docPreview/ali_doc.webp) 付费使用 ### 4.XDOC 文档预览 说了一些大厂的,在介绍一些其他的,**需要自行分辨** 官网地址:https://view.xdocin.com/view-xdocin-com_6x5f4x.htm ![image.png](../../../assets/article/docPreview/xdoc.webp) ### 5.Office Web 365 需要注意的是,虽然名字很像`office`,但我们看网页的`Copyright`可以发现,其实是一个西安的公司,**不是微软**。 但毕竟也提供了文件预览的服务 官网地址:https://www.officeweb365.com/ ![image.png](../../../assets/article/docPreview/ow365.webp) ### 6.WPS 开放平台 官方地址:https://solution.wps.cn/ ![image.png](../../../assets/article/docPreview/wps_office.webp) 付费使用,价格如下: ![image.png](../../../assets/article/docPreview/wps_office_price.webp) ## 二:前端处理方案 ### 1.pptx 的预览方案 先查一下有没有现成的轮子,目前`pptx`的开源预览方案能找到的只有这个:https://github.com/g21589/PPTX2HTML。但已经六七年没有更新,也没有维护,笔者使用的时候发现有很多兼容性问题。 简单来说就是,没有。对于这种情况,我们可以自行解析,主要步骤如下: 1. 查询`pptx`的国际标准 2. 解析`pptx`文件 3. 渲染成`html`或者`canvas`进行展示 我们先去找一下`pptx`的国际标准,官方地址:[officeopenxml](http://officeopenxml.com/) 先解释下什么是`officeopenxml`: > Office OpenXML,也称为 OpenXML 或 OOXML,是一种基于 XML 的办公文档格式,包括文字处理文档、电子表格、演示文稿以及图表、图表、形状和其他图形材料。该规范由微软开发,并于 2006 年被 ECMA 国际采用为 ECMA-376。第二个版本于 2008 年 12 月发布,第三个版本于 2011 年 6 月发布。该规范已被 ISO 和 IEC 采用为 ISO/IEC 29500。 > 虽然 Microsoft 继续支持较旧的二进制格式 (.doc、.xls 和.ppt),但 OOXML 现在是所有 Microsoft Office 文档 (.docx、.xlsx 和.pptx) 的默认格式。 由此可见,`Office OpenXML`由微软开发,目前已经是国际标准。接下来我们看一下`pptx`里面有哪些内容,具体可以看`pptx`的官方标准:[officeopenxml-pptx](http://officeopenxml.com/anatomyofOOXML-pptx.php) > PresentationML 或.pptx 文件是一个**zip 文件**,其中包含许多“部分”(通常是 UTF-8 或 UTF-16 编码)或 XML 文件。该包还可能包含其他媒体文件,例如图像。该结构根据 OOXML 标准 ECMA-376 第 2 部分中概述的开放打包约定进行组织。 ![image.png](../../../assets/article/docPreview/xml.webp) 根据国际标准,我们知道,`pptx`文件本质就是一个`zip`文件,其中包含许多部分: > 部件的数量和类型将根据演示文稿中的内容而有所不同,但始终会有一个 [Content_Types].xml、一个或多个关系(.rels)部件和一个演示文稿部件(演示文稿.xml),它位于 ppt 文件夹中,用于 Microsoft Powerpoint 文件。通常,还将至少有一个幻灯片部件,以及一张母版幻灯片和一张版式幻灯片,从中形成幻灯片。 那么`js`如何读取`zip`呢? 找到一个工具:https://www.npmjs.com/package/jszip 于是我们可以开始尝试解析`pptx`了。 ```ts import JSZip from 'jszip'; // 加载 pptx 数据 const zip = await JSZip.loadAsync(pptxData); ``` - 解析`[Content_Types].xml` 每个`pptx`必然会有一个 `[Content_Types].xml`。此文件包含包中部件的所有内容类型的列表。每个部件及其类型都必须列在 `[Content_Types].xml` 中。通过它里面的内容,可以解析其他的文件数据 ```ts const filesInfo = await getContentTypes(zip); async function getContentTypes(zip: JSZip) { const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml'); const subObj = ContentTypesJson['Types']['Override']; const slidesLocArray = []; const slideLayoutsLocArray = []; for (let i = 0; i < subObj.length; i++) { switch (subObj[i]['attrs']['ContentType']) { case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml': slidesLocArray.push(subObj[i]['attrs']['PartName'].substr(1)); break; case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml': slideLayoutsLocArray.push(subObj[i]['attrs']['PartName'].substr(1)); break; default: } } return { slides: slidesLocArray, slideLayouts: slideLayoutsLocArray, }; } ``` - 解析演示文稿 先获取`ppt`目录下的`presentation.xml`演示文稿的大小 由于演示文稿是`xml`格式,要真正的读取内容需要执行 `readXmlFile` ```ts const slideSize = await getSlideSize(zip); async function getSlideSize(zip: JSZip) { const content = await readXmlFile(zip, 'ppt/presentation.xml'); const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs']; return { width: (parseInt(sldSzAttrs['cx']) * 96) / 914400, height: (parseInt(sldSzAttrs['cy']) * 96) / 914400, }; } ``` - 加载主题 根据 `officeopenxml`的标准解释 > 每个包都包含一个关系部件,用于定义其他部件之间的关系以及与包外部资源的关系。这样可以将关系与内容分开,并且可以轻松地更改关系,而无需更改引用目标的源。 > 除了包的关系部分之外,作为一个或多个关系源的每个部件都有自己的关系部分。每个这样的关系部件都可以在部件的\_rels 子文件夹中找到,并通过在部件名称后附加“.rels”来命名。 其中主题的相关信息就在`ppt/_rels/presentation.xml.rels`中 ```ts async function loadTheme(zip: JSZip) { const preResContent = await readXmlFile(zip, 'ppt/_rels/presentation.xml.rels'); const relationshipArray = preResContent['Relationships']['Relationship']; let themeURI; if (relationshipArray.constructor === Array) { for (let i = 0; i < relationshipArray.length; i++) { if ( relationshipArray[i]['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme' ) { themeURI = relationshipArray[i]['attrs']['Target']; break; } } } else if ( relationshipArray['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme' ) { themeURI = relationshipArray['attrs']['Target']; } if (themeURI === undefined) { throw Error("Can't open theme file."); } return readXmlFile(zip, 'ppt/' + themeURI); } ``` 后续`ppt`里面的其他内容,都可以这么去解析。根据`officeopenxml`标准,可能包含: Part | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Comments Authors | Contains information about each author who has added a comment to the presentation. | | Comments | Contains comments for a single slide. | | Handout Master | Contains the look, position, and size of the slides, notes, header and footer text, date, or page number on the presentation's handout. There can be only one such part. | | Notes Master | Contains information about the content and formatting of all notes pages. There can be only one such part. | | Notes Slide | Contains the notes for a single slide. | | Presentation | Contains the definition of a slide presentation. There must be one and only one such part. See [Presentation](http://officeopenxml.com/PrPresentation.php). | | Presentation Properties | Contains all of the presentation's properties. There must be one and only one such part. | | Slide | Contains the content of a single slide. | | Slide Layout | Contains the definition for a slide template. It defines the default appearance and positioning of drawing objects on the slide. There must be one or more such parts. | | Slide Master | Contains the master definition of formatting, text, and objects that appear on each slide in the presentation that is derived from the slide master. There must be one or more such parts. | | Slide Synchronization Data | Contains properties specifying the current state of a slide that is being synchronized with a version of the slide stored on a central server. | | User-Defined Tags | Contains a set of user-defined properties for an object in a presentation. There can be zero or more such parts. | | View Properties | Contains display properties for the presentation. 等等内容,我们根据标准一点点解析并渲染就好了。 完整源码:[ranui](https://github.com/chaxus/ran/tree/main/packages/ranui) 使用文档:[preview 组件](https://chaxus.github.io/ran/src/ranui/preview/) ### 2.pdf 的预览方案 #### (1).iframe 和 embed `pdf`比较特别,一般的浏览器默认支持预览`pdf`。因此,我们可以使用浏览器的能力: ```html